diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..2048f1d1ec --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*.md] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = false +max_line_length = 80 +insert_final_newline = true +charset = utf-8 +end_of_line = lf diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index efc9e57f57..4baa49503d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -157,15 +157,4 @@ backend-deploy-and-test-zcash_testnet: - configs/coins/zcash_testnet.json tags: - blockbook - script: ./contrib/scripts/backend-deploy-and-test.sh zcash_testnet zcash-testnet zcash=test testnet3/debug.log - -backend-deploy-and-test-goerli-archive: - stage: backend-deploy-and-test - only: - refs: - - master - changes: - - configs/coins/ethereum_testnet_goerli_archive.json - tags: - - blockbook - script: ./contrib/scripts/backend-deploy-and-test.sh ethereum_testnet_goerli_archive ethereum-testnet-goerli-archive ethereum=test ethereum_testnet_goerli_archive.log + script: ./contrib/scripts/backend-deploy-and-test.sh zcash_testnet zcash-testnet zcash=test testnet3/debug.log \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..ccb2431d24 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 100, + "arrowParens": "avoid", + "bracketSpacing": true, + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "tabWidth": 4, + "useTabs": false, + "bracketSameLine": false +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8dd2c2f878..43fe3f9f5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,7 +75,7 @@ also in [build guide](/docs/build.md#on-naming-conventions-and-versioning). You *mainnet* option. In the section *blockbook* update information how to build and configure Blockbook service. Usually they are only -*package_name*, *system_user* and *explorer_url* options. Naming conventions are are described +*package_name*, *system_user* and *explorer_url* options. Naming conventions are described [here](/docs/build.md#on-naming-conventions-and-versioning). Update *package_maintainer* and *package_maintainer_email* options in the section *meta*. diff --git a/Makefile b/Makefile index dfe5b5f395..d384f37990 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,10 @@ BIN_IMAGE = blockbook-build DEB_IMAGE = blockbook-build-deb PACKAGER = $(shell id -u):$(shell id -g) +DOCKER_VERSION = $(shell docker version --format '{{.Client.Version}}') 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 -TCMALLOC = +TCMALLOC = PORTABLE = 0 ARGS ?= @@ -27,13 +28,13 @@ test-all: .bin-image 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) -v "$(CURDIR):/src" -v "$(CURDIR)/build:/out" $(DEB_IMAGE) /build/build-deb.sh backend $* $(ARGS) + docker run -t --rm -e PACKAGER=$(PACKAGER) -v /var/run/docker.sock:/var/run/docker.sock -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) -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) -v "$(CURDIR):/src" -v "$(CURDIR)/build:/out" $(DEB_IMAGE) /build/build-deb.sh all $* $(ARGS) + docker run -t --rm -e PACKAGER=$(PACKAGER) -v /var/run/docker.sock:/var/run/docker.sock -v "$(CURDIR):/src" -v "$(CURDIR)/build:/out" $(DEB_IMAGE) /build/build-deb.sh all $* $(ARGS) deb-blockbook-all: clean-deb $(addprefix deb-blockbook-, $(TARGETS)) @@ -55,7 +56,7 @@ build-images: clean-images .deb-image: .bin-image @if [ $$(build/tools/image_status.sh $(DEB_IMAGE):latest build/docker) != "ok" ]; then \ echo "Building image $(DEB_IMAGE)..."; \ - docker build --no-cache=$(NO_CACHE) -t $(DEB_IMAGE) build/docker/deb; \ + docker build --no-cache=$(NO_CACHE) --build-arg DOCKER_VERSION=$(DOCKER_VERSION) -t $(DEB_IMAGE) build/docker/deb; \ else \ echo "Image $(DEB_IMAGE) is up to date"; \ fi @@ -79,3 +80,6 @@ clean-bin-image: clean-deb-image: - docker rmi $(DEB_IMAGE) + +style: + find . -name "*.go" -exec gofmt -w {} \; diff --git a/README.md b/README.md index d49fc1d449..d5933f115a 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ # Blockbook -**Blockbook** is back-end service for Trezor wallet. Main features of **Blockbook** are: +**Blockbook** is a back-end service for Trezor Suite. The main features of **Blockbook** are: -- index of addresses and address balances of the connected block chain -- 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 to other coins -- scripts for easy creation of debian packages for backend and blockbook +- index of addresses and address balances of the connected block chain +- 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 to other coins +- scripts for easy creation of debian packages for backend and blockbook ## Build and installation instructions @@ -19,7 +19,7 @@ Memory and disk requirements for initial synchronization of **Bitcoin mainnet** Other coins should have lower requirements, depending on the size of their block chain. Note that fast SSD disks are highly recommended. -User installation guide is [here](https://wiki.trezor.io/User_manual:Running_a_local_instance_of_Trezor_Wallet_backend_(Blockbook)). +User installation guide is [here](). Developer build guide is [here](/docs/build.md). @@ -27,14 +27,15 @@ Contribution guide is [here](CONTRIBUTING.md). ## Implemented coins -Blockbook currently supports over 30 coins. The Trezor team implemented +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 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 Testnets (Sepolia, Hoodi) List of all implemented coins is in [the registry of ports](/docs/ports.md). @@ -42,19 +43,19 @@ List of all implemented coins is in [the registry of ports](/docs/ports.md). #### Out of memory when doing initial synchronization -How to reduce memory footprint of the initial sync: +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. +- 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). #### 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 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. +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. @@ -73,3 +74,7 @@ Blockbook stores data the key-value store RocksDB. Database format is described ## API Blockbook API is described [here](/docs/api.md). + +## Environment variables + +List of environment variables that affect Blockbook's behavior is [here](/docs/env.md). diff --git a/api/ethereumtype.go b/api/ethereumtype.go new file mode 100644 index 0000000000..1f98f2eea0 --- /dev/null +++ b/api/ethereumtype.go @@ -0,0 +1,92 @@ +package api + +import ( + "sync" + + "github.com/golang/glog" + "github.com/linxGnu/grocksdb" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/db" +) + +// refetch internal data +var refetchingInternalData = false +var refetchInternalDataMux sync.Mutex + +func (w *Worker) IsRefetchingInternalData() bool { + refetchInternalDataMux.Lock() + defer refetchInternalDataMux.Unlock() + return refetchingInternalData +} + +func (w *Worker) RefetchInternalData() error { + refetchInternalDataMux.Lock() + defer refetchInternalDataMux.Unlock() + if !refetchingInternalData { + refetchingInternalData = true + go w.RefetchInternalDataRoutine() + } + return nil +} + +const maxNumberOfRetires = 25 + +func (w *Worker) incrementRefetchInternalDataRetryCount(ie *db.BlockInternalDataError) { + wb := grocksdb.NewWriteBatch() + defer wb.Destroy() + err := w.db.StoreBlockInternalDataErrorEthereumType(wb, &bchain.Block{ + BlockHeader: bchain.BlockHeader{ + Hash: ie.Hash, + Height: ie.Height, + }, + }, ie.ErrorMessage, ie.Retries+1) + if err != nil { + glog.Errorf("StoreBlockInternalDataErrorEthereumType %d %s, error %v", ie.Height, ie.Hash, err) + } else { + w.db.WriteBatch(wb) + } +} + +func (w *Worker) RefetchInternalDataRoutine() { + internalErrors, err := w.db.GetBlockInternalDataErrorsEthereumType() + if err == nil { + for i := range internalErrors { + ie := &internalErrors[i] + if ie.Retries >= maxNumberOfRetires { + glog.Infof("Refetching internal data for %d %s, retries exceeded", ie.Height, ie.Hash) + continue + } + glog.Infof("Refetching internal data for %d %s, retries %d", ie.Height, ie.Hash, ie.Retries) + block, err := w.chain.GetBlock(ie.Hash, ie.Height) + var blockSpecificData *bchain.EthereumBlockSpecificData + if block != nil { + blockSpecificData, _ = block.CoinSpecificData.(*bchain.EthereumBlockSpecificData) + } + if err != nil || block == nil || (blockSpecificData != nil && blockSpecificData.InternalDataError != "") { + glog.Errorf("Refetching internal data for %d %s, error %v, retrying", ie.Height, ie.Hash, err) + // try for second time to fetch the data - the 2nd attempt after the first unsuccessful has many times higher probability of success + // probably something to do with data preloaded to cache on the backend + block, err = w.chain.GetBlock(ie.Hash, ie.Height) + if err != nil || block == nil { + glog.Errorf("Refetching internal data for %d %s, error %v", ie.Height, ie.Hash, err) + continue + } + } + blockSpecificData, _ = block.CoinSpecificData.(*bchain.EthereumBlockSpecificData) + if blockSpecificData != nil && blockSpecificData.InternalDataError != "" { + glog.Errorf("Refetching internal data for %d %s, internal data error %v", ie.Height, ie.Hash, blockSpecificData.InternalDataError) + w.incrementRefetchInternalDataRetryCount(ie) + } else { + err = w.db.ReconnectInternalDataToBlockEthereumType(block) + if err != nil { + glog.Errorf("ReconnectInternalDataToBlockEthereumType %d %s, error %v", ie.Height, ie.Hash, err) + } else { + glog.Infof("Refetching internal data for %d %s, success", ie.Height, ie.Hash) + } + } + } + } + refetchInternalDataMux.Lock() + refetchingInternalData = false + refetchInternalDataMux.Unlock() +} diff --git a/api/types.go b/api/types.go index 23d4dd1fc4..3a0fe913d2 100644 --- a/api/types.go +++ b/api/types.go @@ -3,8 +3,10 @@ package api import ( "encoding/json" "errors" + "fmt" "math/big" "sort" + "strings" "time" "github.com/trezor/blockbook/bchain" @@ -40,8 +42,8 @@ var ErrUnsupportedXpub = errors.New("XPUB not supported") // APIError extends error by information if the error details should be returned to the end user type APIError struct { - Text string - Public bool + Text string `ts_doc:"Human-readable error message describing the issue."` + Public bool `ts_doc:"Whether the error message can safely be shown to the end user."` } func (e *APIError) Error() string { @@ -56,16 +58,16 @@ func NewAPIError(s string, public bool) error { } } -// Amount is datatype holding amounts +// Amount is a datatype holding amounts type Amount big.Int -// IsZeroBigInt if big int has zero value +// IsZeroBigInt checks if big int has zero value func IsZeroBigInt(b *big.Int) bool { return len(b.Bits()) == 0 } // Compare returns an integer comparing two Amounts. The result will be 0 if a == b, -1 if a < b, and +1 if a > b. -// Nil Amount is always less then non nil amount, two nil Amounts are equal +// Nil Amount is always less then non-nil amount, two nil Amounts are equal func (a *Amount) Compare(b *Amount) int { if b == nil { if a == nil { @@ -87,6 +89,21 @@ func (a *Amount) MarshalJSON() (out []byte, err error) { return []byte(`"` + (*big.Int)(a).String() + `"`), nil } +func (a *Amount) UnmarshalJSON(data []byte) error { + s := strings.Trim(string(data), "\"") + if len(s) > 0 { + bigValue, parsed := new(big.Int).SetString(s, 10) + if !parsed { + return fmt.Errorf("couldn't parse number: %s", s) + } + *a = Amount(*bigValue) + } else { + // assuming empty string means zero + *a = Amount{} + } + return nil +} + func (a *Amount) String() string { if a == nil { return "" @@ -119,60 +136,62 @@ func (a *Amount) AsInt64() int64 { // Vin contains information about single transaction input type Vin struct { - Txid string `json:"txid,omitempty"` - Vout uint32 `json:"vout,omitempty"` - Sequence int64 `json:"sequence,omitempty"` - N int `json:"n"` - AddrDesc bchain.AddressDescriptor `json:"-"` - Addresses []string `json:"addresses,omitempty"` - IsAddress bool `json:"isAddress"` - IsOwn bool `json:"isOwn,omitempty"` - ValueSat *Amount `json:"value,omitempty"` - Hex string `json:"hex,omitempty"` - Asm string `json:"asm,omitempty"` - Coinbase string `json:"coinbase,omitempty"` + Txid string `json:"txid,omitempty" ts_doc:"ID/hash of the originating transaction (where the UTXO comes from)."` + Vout uint32 `json:"vout,omitempty" ts_doc:"Index of the output in the referenced transaction."` + Sequence int64 `json:"sequence,omitempty" ts_doc:"Sequence number for this input (e.g. 4294967293)."` + N int `json:"n" ts_doc:"Relative index of this input within the transaction."` + AddrDesc bchain.AddressDescriptor `json:"-" ts_doc:"Internal address descriptor for backend usage (not exposed via JSON)."` + Addresses []string `json:"addresses,omitempty" ts_doc:"List of addresses associated with this input."` + IsAddress bool `json:"isAddress" ts_doc:"Indicates if this input is from a known address."` + IsOwn bool `json:"isOwn,omitempty" ts_doc:"Indicates if this input belongs to the wallet in context."` + ValueSat *Amount `json:"value,omitempty" ts_doc:"Amount (in satoshi or base units) of the input."` + Hex string `json:"hex,omitempty" ts_doc:"Raw script hex data for this input."` + Asm string `json:"asm,omitempty" ts_doc:"Disassembled script for this input."` + Coinbase string `json:"coinbase,omitempty" ts_doc:"Data for coinbase inputs (when mining)."` } // Vout contains information about single transaction output type Vout struct { - ValueSat *Amount `json:"value,omitempty"` - N int `json:"n"` - Spent bool `json:"spent,omitempty"` - SpentTxID string `json:"spentTxId,omitempty"` - SpentIndex int `json:"spentIndex,omitempty"` - SpentHeight int `json:"spentHeight,omitempty"` - Hex string `json:"hex,omitempty"` - Asm string `json:"asm,omitempty"` - AddrDesc bchain.AddressDescriptor `json:"-"` - Addresses []string `json:"addresses"` - IsAddress bool `json:"isAddress"` - IsOwn bool `json:"isOwn,omitempty"` - Type string `json:"type,omitempty"` -} - -// MultiTokenValue contains values for contract with id and value (like ERC1155) + ValueSat *Amount `json:"value,omitempty" ts_doc:"Amount (in satoshi or base units) of the output."` + N int `json:"n" ts_doc:"Relative index of this output within the transaction."` + Spent bool `json:"spent,omitempty" ts_doc:"Indicates whether this output has been spent."` + SpentTxID string `json:"spentTxId,omitempty" ts_doc:"Transaction ID in which this output was spent."` + SpentIndex int `json:"spentIndex,omitempty" ts_doc:"Index of the input that spent this output."` + SpentHeight int `json:"spentHeight,omitempty" ts_doc:"Block height at which this output was spent."` + Hex string `json:"hex,omitempty" ts_doc:"Raw script hex data for this output - aka ScriptPubKey."` + Asm string `json:"asm,omitempty" ts_doc:"Disassembled script for this output."` + AddrDesc bchain.AddressDescriptor `json:"-" ts_doc:"Internal address descriptor for backend usage (not exposed via JSON)."` + Addresses []string `json:"addresses" ts_doc:"List of addresses associated with this output."` + IsAddress bool `json:"isAddress" ts_doc:"Indicates whether this output is owned by valid address."` + IsOwn bool `json:"isOwn,omitempty" ts_doc:"Indicates if this output belongs to the wallet in context."` + Type string `json:"type,omitempty" ts_doc:"Output script type (e.g., 'P2PKH', 'P2SH')."` +} + +// MultiTokenValue contains values for contracts with multiple token IDs type MultiTokenValue struct { - Id *Amount `json:"id,omitempty"` - Value *Amount `json:"value,omitempty"` + Id *Amount `json:"id,omitempty" ts_doc:"Token ID (for ERC1155)."` + Value *Amount `json:"value,omitempty" ts_doc:"Amount of that specific token ID."` } // Token contains info about tokens held by an address type Token struct { - Type bchain.TokenTypeName `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"` - BaseValue float64 `json:"baseValue,omitempty"` // value in the base currency (ETH for Ethereum) - SecondaryValue float64 `json:"secondaryValue,omitempty"` // value in secondary (fiat) currency, if specified - Ids []Amount `json:"ids,omitempty"` // multiple ERC721 tokens - MultiTokenValues []MultiTokenValue `json:"multiTokenValues,omitempty"` // multiple ERC1155 tokens - TotalReceivedSat *Amount `json:"totalReceived,omitempty"` - TotalSentSat *Amount `json:"totalSent,omitempty"` - ContractIndex string `json:"-"` + // Deprecated: Use Standard instead. + Type bchain.TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'" ts_doc:"@deprecated: Use standard instead."` + Standard bchain.TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'"` + Name string `json:"name" ts_doc:"Readable name of the token."` + Path string `json:"path,omitempty" ts_doc:"Derivation path if this token is derived from an XPUB-based address."` + Contract string `json:"contract,omitempty" ts_doc:"Contract address on-chain."` + Transfers int `json:"transfers" ts_doc:"Total number of token transfers for this address."` + Symbol string `json:"symbol,omitempty" ts_doc:"Symbol for the token (e.g., 'ETH', 'USDT')."` + Decimals int `json:"decimals,omitempty" ts_doc:"Number of decimals for this token."` + BalanceSat *Amount `json:"balance,omitempty" ts_doc:"Current token balance (in minimal base units)."` + BaseValue float64 `json:"baseValue,omitempty" ts_doc:"Value in the base currency (e.g. ETH for ERC20 tokens)."` + SecondaryValue float64 `json:"secondaryValue,omitempty" ts_doc:"Value in a secondary currency (e.g. fiat), if available."` + Ids []Amount `json:"ids,omitempty" ts_doc:"List of token IDs (for ERC721, each ID is a unique collectible)."` + MultiTokenValues []MultiTokenValue `json:"multiTokenValues,omitempty" ts_doc:"Multiple ERC1155 token balances (id + value)."` + TotalReceivedSat *Amount `json:"totalReceived,omitempty" ts_doc:"Total amount of tokens received."` + TotalSentSat *Amount `json:"totalSent,omitempty" ts_doc:"Total amount of tokens sent."` + ContractIndex string `json:"-"` } // Tokens is array of Token @@ -204,84 +223,97 @@ func (a Tokens) Less(i, j int) bool { // TokenTransfer contains info about a token transfer done in a transaction type TokenTransfer struct { - Type bchain.TokenTypeName `json:"type"` - From string `json:"from"` - To string `json:"to"` - Contract string `json:"contract"` - Name string `json:"name"` - Symbol string `json:"symbol"` - Decimals int `json:"decimals"` - Value *Amount `json:"value,omitempty"` - MultiTokenValues []MultiTokenValue `json:"multiTokenValues,omitempty"` -} - + // Deprecated: Use Standard instead. + Type bchain.TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'" ts_doc:"@deprecated: Use standard instead."` + Standard bchain.TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'"` + From string `json:"from" ts_doc:"Source address of the token transfer."` + To string `json:"to" ts_doc:"Destination address of the token transfer."` + Contract string `json:"contract" ts_doc:"Contract address of the token."` + Name string `json:"name,omitempty" ts_doc:"Token name."` + Symbol string `json:"symbol,omitempty" ts_doc:"Token symbol."` + Decimals int `json:"decimals,omitempty" ts_doc:"Number of decimals for this token (if applicable)."` + Value *Amount `json:"value,omitempty" ts_doc:"Amount (in base units) of tokens transferred."` + MultiTokenValues []MultiTokenValue `json:"multiTokenValues,omitempty" ts_doc:"List of multiple ID-value pairs for ERC1155 transfers."` +} + +// EthereumInternalTransfer represents internal transaction data in Ethereum-like blockchains type EthereumInternalTransfer struct { - Type bchain.EthereumInternalTransactionType `json:"type"` - From string `json:"from"` - To string `json:"to"` - Value *Amount `json:"value"` + Type bchain.EthereumInternalTransactionType `json:"type" ts_doc:"Type of internal transfer (CALL, CREATE, etc.)."` + From string `json:"from" ts_doc:"Address from which the transfer originated."` + To string `json:"to" ts_doc:"Address to which the transfer was sent."` + Value *Amount `json:"value" ts_doc:"Value transferred internally (in Wei or base units)."` } -// EthereumSpecific contains ethereum specific transaction data +// EthereumSpecific contains ethereum-specific transaction data type EthereumSpecific struct { - Type bchain.EthereumInternalTransactionType `json:"type,omitempty"` - CreatedContract string `json:"createdContract,omitempty"` - Status eth.TxStatus `json:"status"` // 1 OK, 0 Fail, -1 pending - Error string `json:"error,omitempty"` - Nonce uint64 `json:"nonce"` - GasLimit *big.Int `json:"gasLimit"` - GasUsed *big.Int `json:"gasUsed"` - GasPrice *Amount `json:"gasPrice"` - Data string `json:"data,omitempty"` - ParsedData *bchain.EthereumParsedInputData `json:"parsedData,omitempty"` - InternalTransfers []EthereumInternalTransfer `json:"internalTransfers,omitempty"` -} - + Type bchain.EthereumInternalTransactionType `json:"type,omitempty" ts_doc:"High-level type of the Ethereum tx (e.g., 'call', 'create')."` + CreatedContract string `json:"createdContract,omitempty" ts_doc:"Address of contract created by this transaction, if any."` + Status eth.TxStatus `json:"status" ts_doc:"Execution status of the transaction (1: success, 0: fail, -1: pending)."` + Error string `json:"error,omitempty" ts_doc:"Error encountered during execution, if any."` + Nonce uint64 `json:"nonce" ts_doc:"Transaction nonce (sequential number from the sender)."` + GasLimit *big.Int `json:"gasLimit" ts_doc:"Maximum gas allowed by the sender for this transaction."` + GasUsed *big.Int `json:"gasUsed,omitempty" ts_doc:"Actual gas consumed by the transaction execution."` + GasPrice *Amount `json:"gasPrice,omitempty" ts_doc:"Price (in Wei or base units) per gas unit."` + MaxPriorityFeePerGas *Amount `json:"maxPriorityFeePerGas,omitempty"` + MaxFeePerGas *Amount `json:"maxFeePerGas,omitempty"` + BaseFeePerGas *Amount `json:"baseFeePerGas,omitempty"` + L1Fee *big.Int `json:"l1Fee,omitempty" ts_doc:"Fee used for L1 part in rollups (e.g. Optimism)."` + L1FeeScalar string `json:"l1FeeScalar,omitempty" ts_doc:"Scaling factor for L1 fees in certain Layer 2 solutions."` + L1GasPrice *Amount `json:"l1GasPrice,omitempty" ts_doc:"Gas price for L1 component, if applicable."` + L1GasUsed *big.Int `json:"l1GasUsed,omitempty" ts_doc:"Amount of gas used in L1 for this tx, if applicable."` + Data string `json:"data,omitempty" ts_doc:"Hex-encoded input data for the transaction."` + ParsedData *bchain.EthereumParsedInputData `json:"parsedData,omitempty" ts_doc:"Decoded transaction data (function name, params, etc.)."` + InternalTransfers []EthereumInternalTransfer `json:"internalTransfers,omitempty" ts_doc:"List of internal (sub-call) transfers."` +} + +// AddressAlias holds a specialized alias for an address type AddressAlias struct { - Type string - Alias string + Type string `ts_doc:"Type of alias, e.g., user-defined name or contract name."` + Alias string `ts_doc:"Alias string for the address."` } + +// AddressAliasesMap is a map of address strings to their alias definitions type AddressAliasesMap map[string]AddressAlias // 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"` - ConfirmationETABlocks uint32 `json:"confirmationETABlocks,omitempty"` - ConfirmationETASeconds int64 `json:"confirmationETASeconds,omitempty"` - Blocktime int64 `json:"blockTime"` - Size int `json:"size,omitempty"` - VSize int `json:"vsize,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 json.RawMessage `json:"coinSpecificData,omitempty"` - TokenTransfers []TokenTransfer `json:"tokenTransfers,omitempty"` - EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty"` - AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"` + Txid string `json:"txid" ts_doc:"Transaction ID (hash)."` + Version int32 `json:"version,omitempty" ts_doc:"Version of the transaction (if applicable)."` + Locktime uint32 `json:"lockTime,omitempty" ts_doc:"Locktime indicating earliest time/height transaction can be mined."` + Vin []Vin `json:"vin" ts_doc:"Array of inputs for this transaction."` + Vout []Vout `json:"vout" ts_doc:"Array of outputs for this transaction."` + Blockhash string `json:"blockHash,omitempty" ts_doc:"Hash of the block containing this transaction."` + Blockheight int `json:"blockHeight" ts_doc:"Block height in which this transaction was included."` + Confirmations uint32 `json:"confirmations" ts_doc:"Number of confirmations (blocks mined after this tx's block)."` + ConfirmationETABlocks uint32 `json:"confirmationETABlocks,omitempty" ts_doc:"Estimated blocks remaining until confirmation (if unconfirmed)."` + ConfirmationETASeconds int64 `json:"confirmationETASeconds,omitempty" ts_doc:"Estimated seconds remaining until confirmation (if unconfirmed)."` + Blocktime int64 `json:"blockTime" ts_doc:"Unix timestamp of the block in which this transaction was included. 0 if unconfirmed."` + Size int `json:"size,omitempty" ts_doc:"Transaction size in bytes."` + VSize int `json:"vsize,omitempty" ts_doc:"Virtual size in bytes, for SegWit-enabled chains."` + ValueOutSat *Amount `json:"value" ts_doc:"Total value of all outputs (in satoshi or base units)."` + ValueInSat *Amount `json:"valueIn,omitempty" ts_doc:"Total value of all inputs (in satoshi or base units)."` + FeesSat *Amount `json:"fees,omitempty" ts_doc:"Transaction fee (inputs - outputs)."` + Hex string `json:"hex,omitempty" ts_doc:"Raw hex-encoded transaction data."` + Rbf bool `json:"rbf,omitempty" ts_doc:"Indicates if this transaction is replace-by-fee (RBF) enabled."` + CoinSpecificData json.RawMessage `json:"coinSpecificData,omitempty" ts_type:"any" ts_doc:"Blockchain-specific extended data."` + TokenTransfers []TokenTransfer `json:"tokenTransfers,omitempty" ts_doc:"List of token transfers that occurred in this transaction."` + EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty" ts_doc:"Ethereum-like blockchain specific data (if applicable)."` + AddressAliases AddressAliasesMap `json:"addressAliases,omitempty" ts_doc:"Aliases for addresses involved in this transaction."` } // 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" ts_doc:"Number of transactions in the given block."` + TotalFeesSat *Amount `json:"totalFeesSat" ts_doc:"Sum of all fees in satoshi or base units."` + AverageFeePerKb int64 `json:"averageFeePerKb" ts_doc:"Average fee per kilobyte in satoshi or base units."` + DecilesFeePerKb [11]int64 `json:"decilesFeePerKb" ts_doc:"Fee distribution deciles (0%..100%) in satoshi or base units per kB."` } // Paging contains information about paging for address, blocks and block type Paging struct { - Page int `json:"page,omitempty"` - TotalPages int `json:"totalPages,omitempty"` - ItemsOnPage int `json:"itemsOnPage,omitempty"` + Page int `json:"page,omitempty" ts_doc:"Current page index."` + TotalPages int `json:"totalPages,omitempty" ts_doc:"Total number of pages available."` + ItemsOnPage int `json:"itemsOnPage,omitempty" ts_doc:"Number of items returned on this page."` } // TokensToReturn specifies what tokens are returned by GetAddress and GetXpubAddress @@ -307,56 +339,74 @@ const ( // AddressFilter is used to filter data returned from GetAddress api method type AddressFilter struct { - Vout int - Contract string - FromHeight uint32 - ToHeight uint32 - TokensToReturn TokensToReturn + Vout int `ts_doc:"Specifies which output index we are interested in filtering (or use the special constants)."` + Contract string `ts_doc:"Contract address to filter by, if applicable."` + FromHeight uint32 `ts_doc:"Starting block height for filtering transactions."` + ToHeight uint32 `ts_doc:"Ending block height for filtering transactions."` + TokensToReturn TokensToReturn `ts_doc:"Which tokens to include in the result set."` // OnlyConfirmed set to true will ignore mempool transactions; mempool is also ignored if FromHeight/ToHeight filter is specified - OnlyConfirmed bool + OnlyConfirmed bool `ts_doc:"If true, ignores mempool (unconfirmed) transactions."` +} + +// StakingPool holds data about address participation in a staking pool contract +type StakingPool struct { + Contract string `json:"contract" ts_doc:"Staking pool contract address on-chain."` + Name string `json:"name" ts_doc:"Name of the staking pool contract."` + PendingBalance *Amount `json:"pendingBalance" ts_doc:"Balance pending deposit or withdrawal, if any."` + PendingDepositedBalance *Amount `json:"pendingDepositedBalance" ts_doc:"Any pending deposit that is not yet finalized."` + DepositedBalance *Amount `json:"depositedBalance" ts_doc:"Currently deposited/staked balance."` + WithdrawTotalAmount *Amount `json:"withdrawTotalAmount" ts_doc:"Total amount withdrawn from this pool by the address."` + ClaimableAmount *Amount `json:"claimableAmount" ts_doc:"Rewards or principal currently claimable by the address."` + RestakedReward *Amount `json:"restakedReward" ts_doc:"Total rewards that have been restaked automatically."` + AutocompoundBalance *Amount `json:"autocompoundBalance" ts_doc:"Any balance automatically reinvested into the pool."` } -// Address holds information about address and its transactions +// Address holds information about an 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"` - UnconfirmedTxs int `json:"unconfirmedTxs"` - Txs int `json:"txs"` - NonTokenTxs int `json:"nonTokenTxs,omitempty"` - InternalTxs int `json:"internalTxs,omitempty"` - Transactions []*Tx `json:"transactions,omitempty"` - Txids []string `json:"txids,omitempty"` - Nonce string `json:"nonce,omitempty"` - UsedTokens int `json:"usedTokens,omitempty"` - Tokens Tokens `json:"tokens,omitempty"` - SecondaryValue float64 `json:"secondaryValue,omitempty"` // address value in secondary currency - TokensBaseValue float64 `json:"tokensBaseValue,omitempty"` - TokensSecondaryValue float64 `json:"tokensSecondaryValue,omitempty"` - TotalBaseValue float64 `json:"totalBaseValue,omitempty"` // value including tokens in base currency - TotalSecondaryValue float64 `json:"totalSecondaryValue,omitempty"` // value including tokens in secondary currency - ContractInfo *bchain.ContractInfo `json:"contractInfo,omitempty"` - Erc20Contract *bchain.ContractInfo `json:"erc20Contract,omitempty"` // deprecated - AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"` + AddrStr string `json:"address" ts_doc:"The address string in standard format."` + BalanceSat *Amount `json:"balance" ts_doc:"Current confirmed balance (in satoshi or base units)."` + TotalReceivedSat *Amount `json:"totalReceived,omitempty" ts_doc:"Total amount ever received by this address."` + TotalSentSat *Amount `json:"totalSent,omitempty" ts_doc:"Total amount ever sent by this address."` + UnconfirmedBalanceSat *Amount `json:"unconfirmedBalance" ts_doc:"Unconfirmed balance for this address."` + UnconfirmedTxs int `json:"unconfirmedTxs" ts_doc:"Number of unconfirmed transactions for this address."` + UnconfirmedSending *Amount `json:"unconfirmedSending,omitempty" ts_doc:"Unconfirmed outgoing balance for this address."` + UnconfirmedReceiving *Amount `json:"unconfirmedReceiving,omitempty" ts_doc:"Unconfirmed incoming balance for this address."` + Txs int `json:"txs" ts_doc:"Number of transactions for this address (including confirmed)."` + AddrTxCount int `json:"addrTxCount,omitempty" ts_doc:"Historical total count of transactions, if known."` + NonTokenTxs int `json:"nonTokenTxs,omitempty" ts_doc:"Number of transactions not involving tokens (pure coin transfers)."` + InternalTxs int `json:"internalTxs,omitempty" ts_doc:"Number of internal transactions (e.g., Ethereum calls)."` + Transactions []*Tx `json:"transactions,omitempty" ts_doc:"List of transaction details (if requested)."` + Txids []string `json:"txids,omitempty" ts_doc:"List of transaction IDs (if detailed data is not requested)."` + Nonce string `json:"nonce,omitempty" ts_doc:"Current transaction nonce for Ethereum-like addresses."` + UsedTokens int `json:"usedTokens,omitempty" ts_doc:"Number of tokens with any historical usage at this address."` + Tokens Tokens `json:"tokens,omitempty" ts_doc:"List of tokens associated with this address."` + SecondaryValue float64 `json:"secondaryValue,omitempty" ts_doc:"Total value of the address in secondary currency (e.g. fiat)."` + TokensBaseValue float64 `json:"tokensBaseValue,omitempty" ts_doc:"Sum of token values in base currency."` + TokensSecondaryValue float64 `json:"tokensSecondaryValue,omitempty" ts_doc:"Sum of token values in secondary currency (fiat)."` + TotalBaseValue float64 `json:"totalBaseValue,omitempty" ts_doc:"Address's entire value in base currency, including tokens."` + TotalSecondaryValue float64 `json:"totalSecondaryValue,omitempty" ts_doc:"Address's entire value in secondary currency, including tokens."` + ContractInfo *bchain.ContractInfo `json:"contractInfo,omitempty" ts_doc:"Extra info if the address is a contract (ABI, type)."` + // Deprecated: replaced by ContractInfo + Erc20Contract *bchain.ContractInfo `json:"erc20Contract,omitempty" ts_doc:"@deprecated: replaced by contractInfo"` + AddressAliases AddressAliasesMap `json:"addressAliases,omitempty" ts_doc:"Aliases assigned to this address."` + StakingPools []StakingPool `json:"stakingPools,omitempty" ts_doc:"List of staking pool data if address interacts with staking."` // helpers for explorer - Filter string `json:"-"` - XPubAddresses map[string]struct{} `json:"-"` + Filter string `json:"-" ts_doc:"Filter used internally for data retrieval."` + XPubAddresses map[string]struct{} `json:"-" ts_doc:"Set of derived XPUB addresses (internal usage)."` } // 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"` + Txid string `json:"txid" ts_doc:"Transaction ID in which this UTXO was created."` + Vout int32 `json:"vout" ts_doc:"Index of the output in that transaction."` + AmountSat *Amount `json:"value" ts_doc:"Value of this UTXO (in satoshi or base units)."` + Height int `json:"height,omitempty" ts_doc:"Block height in which the UTXO was confirmed."` + Confirmations int `json:"confirmations" ts_doc:"Number of confirmations for this UTXO."` + Address string `json:"address,omitempty" ts_doc:"Address to which this UTXO belongs."` + Path string `json:"path,omitempty" ts_doc:"Derivation path for XPUB-based wallets, if applicable."` + Locktime uint32 `json:"lockTime,omitempty" ts_doc:"If non-zero, locktime required before spending this UTXO."` + Coinbase bool `json:"coinbase,omitempty" ts_doc:"Indicates if this UTXO originated from a coinbase transaction."` } // Utxos is array of Utxo @@ -379,13 +429,13 @@ func (a Utxos) Less(i, j int) bool { // 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"` - SentToSelfSat *Amount `json:"sentToSelf"` - FiatRates map[string]float32 `json:"rates,omitempty"` - Txid string `json:"txid,omitempty"` + Time uint32 `json:"time" ts_doc:"Unix timestamp for this point in the balance history."` + Txs uint32 `json:"txs" ts_doc:"Number of transactions in this interval."` + ReceivedSat *Amount `json:"received" ts_doc:"Amount received in this interval (in satoshi or base units)."` + SentSat *Amount `json:"sent" ts_doc:"Amount sent in this interval (in satoshi or base units)."` + SentToSelfSat *Amount `json:"sentToSelf" ts_doc:"Amount sent to the same address (self-transfer)."` + FiatRates map[string]float32 `json:"rates,omitempty" ts_doc:"Exchange rates at this point in time, if available."` + Txid string `json:"txid,omitempty" ts_doc:"Transaction ID if the time corresponds to a specific tx."` } // BalanceHistories is array of BalanceHistory @@ -447,101 +497,131 @@ 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 []db.BlockInfo `json:"blocks" ts_doc:"List of 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 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"` + Hash string `json:"hash" ts_doc:"Block hash."` + Prev string `json:"previousBlockHash,omitempty" ts_doc:"Hash of the previous block in the chain."` + Next string `json:"nextBlockHash,omitempty" ts_doc:"Hash of the next block, if known."` + Height uint32 `json:"height" ts_doc:"Block height (0-based index in the chain)."` + Confirmations int `json:"confirmations" ts_doc:"Number of confirmations of this block (distance from best chain tip)."` + Size int `json:"size" ts_doc:"Size of the block in bytes."` + Time int64 `json:"time,omitempty" ts_doc:"Timestamp of when this block was mined."` + Version common.JSONNumber `json:"version" ts_doc:"Block version (chain-specific meaning)."` + MerkleRoot string `json:"merkleRoot" ts_doc:"Merkle root of the block's transactions."` + Nonce string `json:"nonce" ts_doc:"Nonce used in the mining process."` + Bits string `json:"bits" ts_doc:"Compact representation of the target threshold."` + Difficulty string `json:"difficulty" ts_doc:"Difficulty target for mining this block."` + Txids []string `json:"tx,omitempty" ts_doc:"List of transaction IDs included in this block."` } // Block contains information about block type Block struct { Paging BlockInfo - TxCount int `json:"txCount"` - Transactions []*Tx `json:"txs,omitempty"` - AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"` + TxCount int `json:"txCount" ts_doc:"Total count of transactions in this block."` + Transactions []*Tx `json:"txs,omitempty" ts_doc:"List of full transaction details (if requested)."` + AddressAliases AddressAliasesMap `json:"addressAliases,omitempty" ts_doc:"Optional aliases for addresses found in this block."` } // BlockRaw contains raw block in hex type BlockRaw struct { - Hex string `json:"hex"` + Hex string `json:"hex" ts_doc:"Hex-encoded block data."` } // BlockbookInfo contains information about the running blockbook instance type BlockbookInfo struct { - Coin string `json:"coin"` - Host string `json:"host"` - Version string `json:"version"` - GitCommit string `json:"gitCommit"` - BuildTime string `json:"buildTime"` - SyncMode bool `json:"syncMode"` - InitialSync bool `json:"initialSync"` - InSync bool `json:"inSync"` - BestHeight uint32 `json:"bestHeight"` - LastBlockTime time.Time `json:"lastBlockTime"` - InSyncMempool bool `json:"inSyncMempool"` - LastMempoolTime time.Time `json:"lastMempoolTime"` - MempoolSize int `json:"mempoolSize"` - Decimals int `json:"decimals"` - DbSize int64 `json:"dbSize"` - HasFiatRates bool `json:"hasFiatRates,omitempty"` - HasTokenFiatRates bool `json:"hasTokenFiatRates,omitempty"` - CurrentFiatRatesTime *time.Time `json:"currentFiatRatesTime,omitempty"` - HistoricalFiatRatesTime *time.Time `json:"historicalFiatRatesTime,omitempty"` - HistoricalTokenFiatRatesTime *time.Time `json:"historicalTokenFiatRatesTime,omitempty"` - DbSizeFromColumns int64 `json:"dbSizeFromColumns,omitempty"` - DbColumns []common.InternalStateColumn `json:"dbColumns,omitempty"` - About string `json:"about"` + Coin string `json:"coin" ts_doc:"Coin name, e.g. 'Bitcoin'."` + Network string `json:"network" ts_doc:"Network shortcut, e.g. 'BTC'."` + Host string `json:"host" ts_doc:"Hostname of the blockbook instance, e.g. 'backend5'."` + Version string `json:"version" ts_doc:"Running blockbook version, e.g. '0.4.0'."` + GitCommit string `json:"gitCommit" ts_doc:"Git commit hash of the running blockbook, e.g. 'a0960c8e'."` + BuildTime string `json:"buildTime" ts_doc:"Build time of running blockbook, e.g. '2024-08-08T12:32:50+00:00'."` + SyncMode bool `json:"syncMode" ts_doc:"If true, blockbook is syncing from scratch or in a special sync mode."` + InitialSync bool `json:"initialSync" ts_doc:"Indicates if blockbook is in its initial sync phase."` + InSync bool `json:"inSync" ts_doc:"Indicates if the backend is fully synced with the blockchain."` + BestHeight uint32 `json:"bestHeight" ts_doc:"Best (latest) block height according to this instance."` + LastBlockTime time.Time `json:"lastBlockTime" ts_doc:"Timestamp of the latest block in the chain."` + InSyncMempool bool `json:"inSyncMempool" ts_doc:"Indicates if mempool info is synced as well."` + LastMempoolTime time.Time `json:"lastMempoolTime" ts_doc:"Timestamp of the last mempool update."` + MempoolSize int `json:"mempoolSize" ts_doc:"Number of unconfirmed transactions in the mempool."` + Decimals int `json:"decimals" ts_doc:"Number of decimals for this coin's base unit."` + DbSize int64 `json:"dbSize" ts_doc:"Size of the underlying database in bytes."` + HasFiatRates bool `json:"hasFiatRates,omitempty" ts_doc:"Whether this instance provides fiat exchange rates."` + HasTokenFiatRates bool `json:"hasTokenFiatRates,omitempty" ts_doc:"Whether this instance provides fiat exchange rates for tokens."` + CurrentFiatRatesTime *time.Time `json:"currentFiatRatesTime,omitempty" ts_doc:"Timestamp of the latest fiat rates update."` + HistoricalFiatRatesTime *time.Time `json:"historicalFiatRatesTime,omitempty" ts_doc:"Timestamp of the latest historical fiat rates update."` + HistoricalTokenFiatRatesTime *time.Time `json:"historicalTokenFiatRatesTime,omitempty" ts_doc:"Timestamp of the latest historical token fiat rates update."` + SupportedStakingPools []string `json:"supportedStakingPools,omitempty" ts_doc:"List of contract addresses supported for staking."` + DbSizeFromColumns int64 `json:"dbSizeFromColumns,omitempty" ts_doc:"Optional calculated DB size from columns."` + DbColumns []common.InternalStateColumn `json:"dbColumns,omitempty" ts_doc:"List of columns/tables in the DB for internal state."` + About string `json:"about" ts_doc:"Additional human-readable info about this blockbook instance."` } // SystemInfo contains information about the running blockbook and backend instance type SystemInfo struct { - Blockbook *BlockbookInfo `json:"blockbook"` - Backend *common.BackendInfo `json:"backend"` + Blockbook *BlockbookInfo `json:"blockbook" ts_doc:"Blockbook instance information."` + Backend *common.BackendInfo `json:"backend" ts_doc:"Information about the connected backend node."` } // MempoolTxid contains information about a transaction in mempool type MempoolTxid struct { - Time int64 `json:"time"` - Txid string `json:"txid"` + Time int64 `json:"time" ts_doc:"Timestamp when the transaction was received in the mempool."` + Txid string `json:"txid" ts_doc:"Transaction hash for this mempool entry."` } // MempoolTxids contains a list of mempool txids with paging information type MempoolTxids struct { Paging - Mempool []MempoolTxid `json:"mempool"` - MempoolSize int `json:"mempoolSize"` + Mempool []MempoolTxid `json:"mempool" ts_doc:"List of transactions currently in the mempool."` + MempoolSize int `json:"mempoolSize" ts_doc:"Number of unconfirmed transactions in the mempool."` } // FiatTicker contains formatted CurrencyRatesTicker data type FiatTicker struct { - Timestamp int64 `json:"ts,omitempty"` - Rates map[string]float32 `json:"rates"` - Error string `json:"error,omitempty"` + Timestamp int64 `json:"ts,omitempty" ts_doc:"Unix timestamp for these fiat rates."` + Rates map[string]float32 `json:"rates" ts_doc:"Map of currency codes to their exchange rate."` + Error string `json:"error,omitempty" ts_doc:"Any error message encountered while fetching rates."` } // FiatTickers contains a formatted CurrencyRatesTicker list type FiatTickers struct { - Tickers []FiatTicker `json:"tickers"` + Tickers []FiatTicker `json:"tickers" ts_doc:"List of fiat tickers with timestamps and rates."` } // AvailableVsCurrencies contains formatted data about available versus currencies for exchange rates type AvailableVsCurrencies struct { - Timestamp int64 `json:"ts,omitempty"` - Tickers []string `json:"available_currencies"` - Error string `json:"error,omitempty"` + Timestamp int64 `json:"ts,omitempty" ts_doc:"Timestamp for the available currency list."` + Tickers []string `json:"available_currencies" ts_doc:"List of currency codes (e.g., USD, EUR) supported by the rates."` + Error string `json:"error,omitempty" ts_doc:"Error message, if any, when fetching the available currencies."` +} + +// Eip1559Fee +type Eip1559Fee struct { + MaxFeePerGas *Amount `json:"maxFeePerGas"` + MaxPriorityFeePerGas *Amount `json:"maxPriorityFeePerGas"` + MinWaitTimeEstimate int `json:"minWaitTimeEstimate,omitempty"` + MaxWaitTimeEstimate int `json:"maxWaitTimeEstimate,omitempty"` +} + +// Eip1559Fees +type Eip1559Fees struct { + BaseFeePerGas *Amount `json:"baseFeePerGas,omitempty"` + Low *Eip1559Fee `json:"low,omitempty"` + Medium *Eip1559Fee `json:"medium,omitempty"` + High *Eip1559Fee `json:"high,omitempty"` + Instant *Eip1559Fee `json:"instant,omitempty"` + NetworkCongestion float64 `json:"networkCongestion,omitempty"` + LatestPriorityFeeRange []*Amount `json:"latestPriorityFeeRange,omitempty"` + HistoricalPriorityFeeRange []*Amount `json:"historicalPriorityFeeRange,omitempty"` + HistoricalBaseFeeRange []*Amount `json:"historicalBaseFeeRange,omitempty"` + PriorityFeeTrend string `json:"priorityFeeTrend,omitempty" ts_type:"'up' | 'down'"` + BaseFeeTrend string `json:"baseFeeTrend,omitempty" ts_type:"'up' | 'down'"` +} + +type LongTermFeeRate struct { + FeePerUnit string `json:"feePerUnit" ts_doc:"Long term fee rate (in sat/kByte)."` + Blocks uint64 `json:"blocks" ts_doc:"Amount of blocks used for the long term fee rate estimation."` } diff --git a/api/types_test.go b/api/types_test.go index d2d53380a3..12bb8fec9f 100644 --- a/api/types_test.go +++ b/api/types_test.go @@ -47,6 +47,15 @@ func TestAmount_MarshalJSON(t *testing.T) { if !reflect.DeepEqual(string(b), tt.want) { t.Errorf("json.Marshal() = %v, want %v", string(b), tt.want) } + var parsed amounts + err = json.Unmarshal(b, &parsed) + if err != nil { + t.Errorf("json.Unmarshal() error = %v", err) + return + } + if !reflect.DeepEqual(parsed, tt.a) { + t.Errorf("json.Unmarshal() = %v, want %v", parsed, tt.a) + } }) } } diff --git a/api/worker.go b/api/worker.go index 5f0873a3b8..135417a609 100644 --- a/api/worker.go +++ b/api/worker.go @@ -19,6 +19,7 @@ import ( "github.com/trezor/blockbook/bchain/coins/eth" "github.com/trezor/blockbook/common" "github.com/trezor/blockbook/db" + "github.com/trezor/blockbook/fiat" ) // Worker is handle to api worker @@ -31,11 +32,15 @@ type Worker struct { useAddressAliases bool mempool bchain.Mempool is *common.InternalState + fiatRates *fiat.FiatRates metrics *common.Metrics } +// contractInfoCache is a temporary cache of contract information for ethereum token transfers +type contractInfoCache = map[string]*bchain.ContractInfo + // NewWorker creates new api worker -func NewWorker(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, 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, fiatRates *fiat.FiatRates) (*Worker, error) { w := &Worker{ db: db, txCache: txCache, @@ -45,6 +50,7 @@ func NewWorker(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, useAddressAliases: chain.GetChainParser().UseAddressAliases(), mempool: mempool, is: is, + fiatRates: fiatRates, metrics: metrics, } if w.chainType == bchain.ChainBitcoinType { @@ -101,6 +107,19 @@ func (w *Worker) setSpendingTxToVout(vout *Vout, txid string, height uint32) err // GetSpendingTxid returns transaction id of transaction that spent given output func (w *Worker) GetSpendingTxid(txid string, n int) (string, error) { + if w.db.HasExtendedIndex() { + tsp, err := w.db.GetTxAddresses(txid) + if err != nil { + return "", err + } else if tsp == nil { + glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses") + return "", NewAPIError(fmt.Sprintf("Txid %v not found", txid), false) + } + if n >= len(tsp.Outputs) || n < 0 { + return "", NewAPIError(fmt.Sprintf("Passed incorrect vout index %v for tx %v, len vout %v", n, txid, len(tsp.Outputs)), false) + } + return tsp.Outputs[n].SpentTxid, nil + } start := time.Now() tx, err := w.getTransaction(txid, false, false, nil) if err != nil { @@ -153,9 +172,18 @@ func (w *Worker) getAddressAliases(addresses map[string]struct{}) AddressAliases } for a := range addresses { if w.chainType == bchain.ChainEthereumType { - ci, err := w.db.GetContractInfoForAddress(a) - if err == nil && ci != nil && ci.Name != "" { - aliases[a] = AddressAlias{Type: "Contract", Alias: ci.Name} + addrDesc, err := w.chainParser.GetAddrDescFromAddress(a) + if err != nil || addrDesc == nil { + continue + } + ci, err := w.db.GetContractInfo(addrDesc, bchain.UnknownTokenStandard) + if err == nil && ci != nil { + if ci.Standard == bchain.UnhandledTokenStandard { + ci, _, err = w.getContractDescriptorInfo(addrDesc, bchain.UnknownTokenStandard) + } + if err == nil && ci != nil && ci.Name != "" { + aliases[a] = AddressAlias{Type: "Contract", Alias: ci.Name} + } } } n := w.db.GetAddressAlias(a) @@ -179,6 +207,11 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool, specificJSON bool return tx, nil } +// GetRawTransaction gets raw transaction data in hex format from txid +func (w *Worker) GetRawTransaction(txid string) (string, error) { + return w.chain.EthereumTypeGetRawTransaction(txid) +} + // getTransaction reads transaction data from txid func (w *Worker) getTransaction(txid string, spendingTxs bool, specificJSON bool, addresses map[string]struct{}) (*Tx, error) { bchainTx, height, err := w.txCache.GetTransaction(txid) @@ -368,10 +401,16 @@ func (w *Worker) getTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe aggregateAddresses(addresses, vout.Addresses, vout.IsAddress) if ta != nil { vout.Spent = ta.Outputs[i].Spent - if spendingTxs && vout.Spent { - err = w.setSpendingTxToVout(vout, bchainTx.Txid, uint32(height)) - if err != nil { - glog.Errorf("setSpendingTxToVout error %v, %v, output %v", err, vout.AddrDesc, vout.N) + if vout.Spent { + if w.db.HasExtendedIndex() { + vout.SpentTxID = ta.Outputs[i].SpentTxid + vout.SpentIndex = int(ta.Outputs[i].SpentIndex) + vout.SpentHeight = int(ta.Outputs[i].SpentHeight) + } else if spendingTxs { + err = w.setSpendingTxToVout(vout, bchainTx.Txid, uint32(height)) + if err != nil { + glog.Errorf("setSpendingTxToVout error %v, %v, output %v", err, vout.AddrDesc, vout.N) + } } } } @@ -404,18 +443,28 @@ func (w *Worker) getTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe // mempool txs do not have fees yet if ethTxData.GasUsed != nil { feesSat.Mul(ethTxData.GasPrice, ethTxData.GasUsed) + if ethTxData.L1Fee != nil { + feesSat.Add(&feesSat, ethTxData.L1Fee) + } } if len(bchainTx.Vout) > 0 { valOutSat = bchainTx.Vout[0].ValueSat } ethSpecific = &EthereumSpecific{ - GasLimit: ethTxData.GasLimit, - GasPrice: (*Amount)(ethTxData.GasPrice), - GasUsed: ethTxData.GasUsed, - Nonce: ethTxData.Nonce, - Status: ethTxData.Status, - Data: ethTxData.Data, - ParsedData: parsedInputData, + GasLimit: ethTxData.GasLimit, + GasPrice: (*Amount)(ethTxData.GasPrice), + MaxPriorityFeePerGas: (*Amount)(ethTxData.MaxPriorityFeePerGas), + MaxFeePerGas: (*Amount)(ethTxData.MaxFeePerGas), + BaseFeePerGas: (*Amount)(ethTxData.BaseFeePerGas), + GasUsed: ethTxData.GasUsed, + L1Fee: ethTxData.L1Fee, + L1FeeScalar: ethTxData.L1FeeScalar, + L1GasPrice: (*Amount)(ethTxData.L1GasPrice), + L1GasUsed: ethTxData.L1GasUsed, + Nonce: ethTxData.Nonce, + Status: ethTxData.Status, + Data: ethTxData.Data, + ParsedData: parsedInputData, } if internalData != nil { ethSpecific.Type = internalData.Type @@ -546,12 +595,15 @@ func (w *Worker) GetTransactionFromMempoolTx(mempoolTx *bchain.MempoolTx) (*Tx, tokens = w.getEthereumTokensTransfers(mempoolTx.TokenTransfers, addresses) ethTxData := eth.GetEthereumTxDataFromSpecificData(mempoolTx.CoinSpecificData) ethSpecific = &EthereumSpecific{ - GasLimit: ethTxData.GasLimit, - GasPrice: (*Amount)(ethTxData.GasPrice), - GasUsed: ethTxData.GasUsed, - Nonce: ethTxData.Nonce, - Status: ethTxData.Status, - Data: ethTxData.Data, + GasLimit: ethTxData.GasLimit, + GasPrice: (*Amount)(ethTxData.GasPrice), + MaxPriorityFeePerGas: (*Amount)(ethTxData.MaxPriorityFeePerGas), + MaxFeePerGas: (*Amount)(ethTxData.MaxFeePerGas), + BaseFeePerGas: (*Amount)(ethTxData.BaseFeePerGas), + GasUsed: ethTxData.GasUsed, + Nonce: ethTxData.Nonce, + Status: ethTxData.Status, + Data: ethTxData.Data, } } r := &Tx{ @@ -576,32 +628,32 @@ func (w *Worker) GetTransactionFromMempoolTx(mempoolTx *bchain.MempoolTx) (*Tx, return r, nil } -func (w *Worker) getContractInfo(contract string, typeFromContext bchain.TokenTypeName) (*bchain.ContractInfo, bool, error) { +func (w *Worker) GetContractInfo(contract string, standardFromContext bchain.TokenStandardName) (*bchain.ContractInfo, bool, error) { cd, err := w.chainParser.GetAddrDescFromAddress(contract) if err != nil { return nil, false, err } - return w.getContractDescriptorInfo(cd, typeFromContext) + return w.getContractDescriptorInfo(cd, standardFromContext) } -func (w *Worker) getContractDescriptorInfo(cd bchain.AddressDescriptor, typeFromContext bchain.TokenTypeName) (*bchain.ContractInfo, bool, error) { +func (w *Worker) getContractDescriptorInfo(cd bchain.AddressDescriptor, standardFromContext bchain.TokenStandardName) (*bchain.ContractInfo, bool, error) { var err error validContract := true - contractInfo, err := w.db.GetContractInfo(cd, typeFromContext) + contractInfo, err := w.db.GetContractInfo(cd, standardFromContext) if err != nil { return nil, false, err } if contractInfo == nil { // log warning only if the contract should have been known from processing of the internal data if eth.ProcessInternalTransactions { - glog.Warningf("Contract %v %v not found in DB", cd, typeFromContext) + glog.Warningf("Contract %v %v not found in DB", cd, standardFromContext) } contractInfo, err = w.chain.GetContractInfo(cd) if err != nil { glog.Errorf("GetContractInfo from chain error %v, contract %v", err, cd) } if contractInfo == nil { - contractInfo = &bchain.ContractInfo{Type: bchain.UnknownTokenType, Decimals: w.chainParser.AmountDecimals()} + contractInfo = &bchain.ContractInfo{Standard: bchain.UnknownTokenStandard, Decimals: w.chainParser.AmountDecimals()} addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(cd) if len(addresses) > 0 { contractInfo.Contract = addresses[0] @@ -609,14 +661,15 @@ func (w *Worker) getContractDescriptorInfo(cd bchain.AddressDescriptor, typeFrom validContract = false } else { - if typeFromContext != bchain.UnknownTokenType && contractInfo.Type == bchain.UnknownTokenType { - contractInfo.Type = typeFromContext + if standardFromContext != bchain.UnknownTokenStandard && contractInfo.Standard == bchain.UnknownTokenStandard { + contractInfo.Standard = standardFromContext + contractInfo.Type = standardFromContext } if err = w.db.StoreContractInfo(contractInfo); err != nil { glog.Errorf("StoreContractInfo error %v, contract %v", err, cd) } } - } else if (len(contractInfo.Name) > 0 && contractInfo.Name[0] == 0) || (len(contractInfo.Symbol) > 0 && contractInfo.Symbol[0] == 0) { + } else if (contractInfo.Standard == bchain.UnhandledTokenStandard || len(contractInfo.Name) > 0 && contractInfo.Name[0] == 0) || (len(contractInfo.Symbol) > 0 && contractInfo.Symbol[0] == 0) { // fix contract name/symbol that was parsed as a string consisting of zeroes blockchainContractInfo, err := w.chain.GetContractInfo(cd) if err != nil { @@ -635,6 +688,11 @@ func (w *Worker) getContractDescriptorInfo(cd bchain.AddressDescriptor, typeFrom if blockchainContractInfo != nil { contractInfo.Decimals = blockchainContractInfo.Decimals } + if contractInfo.Standard == bchain.UnhandledTokenStandard { + glog.Infof("Contract %v %v [%s] handled", cd, standardFromContext, contractInfo.Name) + contractInfo.Standard = standardFromContext + contractInfo.Type = standardFromContext + } if err = w.db.StoreContractInfo(contractInfo); err != nil { glog.Errorf("StoreContractInfo error %v, contract %v", err, cd) } @@ -644,39 +702,50 @@ func (w *Worker) getContractDescriptorInfo(cd bchain.AddressDescriptor, typeFrom } func (w *Worker) getEthereumTokensTransfers(transfers bchain.TokenTransfers, addresses map[string]struct{}) []TokenTransfer { - sort.Sort(transfers) tokens := make([]TokenTransfer, len(transfers)) - for i := range transfers { - t := transfers[i] - typeName := bchain.EthereumTokenTypeMap[t.Type] - contractInfo, _, err := w.getContractInfo(t.Contract, typeName) - if err != nil { - glog.Errorf("getContractInfo error %v, contract %v", err, t.Contract) - continue - } - var value *Amount - var values []MultiTokenValue - if t.Type == bchain.MultiToken { - values = make([]MultiTokenValue, len(t.MultiTokenValues)) - for j := range values { - values[j].Id = (*Amount)(&t.MultiTokenValues[j].Id) - values[j].Value = (*Amount)(&t.MultiTokenValues[j].Value) + if len(transfers) > 0 { + sort.Sort(transfers) + contractCache := make(contractInfoCache) + for i := range transfers { + t := transfers[i] + standard := bchain.EthereumTokenStandardMap[t.Standard] + var contractInfo *bchain.ContractInfo + if info, ok := contractCache[t.Contract]; ok { + contractInfo = info + } else { + info, _, err := w.GetContractInfo(t.Contract, standard) + if err != nil { + glog.Errorf("getContractInfo error %v, contract %v", err, t.Contract) + continue + } + contractInfo = info + contractCache[t.Contract] = info + } + var value *Amount + var values []MultiTokenValue + if t.Standard == bchain.MultiToken { + values = make([]MultiTokenValue, len(t.MultiTokenValues)) + for j := range values { + values[j].Id = (*Amount)(&t.MultiTokenValues[j].Id) + values[j].Value = (*Amount)(&t.MultiTokenValues[j].Value) + } + } else { + value = (*Amount)(&t.Value) + } + aggregateAddress(addresses, t.From) + aggregateAddress(addresses, t.To) + tokens[i] = TokenTransfer{ + Type: standard, + Standard: standard, + Contract: t.Contract, + From: t.From, + To: t.To, + Value: value, + MultiTokenValues: values, + Decimals: contractInfo.Decimals, + Name: contractInfo.Name, + Symbol: contractInfo.Symbol, } - } else { - value = (*Amount)(&t.Value) - } - aggregateAddress(addresses, t.From) - aggregateAddress(addresses, t.To) - tokens[i] = TokenTransfer{ - Type: typeName, - Contract: t.Contract, - From: t.From, - To: t.To, - Value: value, - MultiTokenValues: values, - Decimals: contractInfo.Decimals, - Name: contractInfo.Name, - Symbol: contractInfo.Symbol, } } return tokens @@ -695,7 +764,7 @@ func (w *Worker) GetEthereumTokenURI(contract string, id string) (string, *bchai if err != nil { return "", nil, err } - ci, _, err := w.getContractDescriptorInfo(cd, bchain.UnknownTokenType) + ci, _, err := w.getContractDescriptorInfo(cd, bchain.UnknownTokenStandard) if err != nil { return "", nil, err } @@ -825,6 +894,10 @@ func (w *Worker) txFromTxAddress(txid string, ta *db.TxAddresses, bi *db.BlockIn if err != nil { glog.Errorf("tai.Addresses error %v, tx %v, input %v, tai %+v", err, txid, i, tai) } + if w.db.HasExtendedIndex() { + vin.Txid = tai.Txid + vin.Vout = tai.Vout + } aggregateAddresses(addresses, vin.Addresses, vin.IsAddress) } vouts := make([]Vout, len(ta.Outputs)) @@ -839,6 +912,11 @@ func (w *Worker) txFromTxAddress(txid string, ta *db.TxAddresses, bi *db.BlockIn glog.Errorf("tai.Addresses error %v, tx %v, output %v, tao %+v", err, txid, i, tao) } vout.Spent = tao.Spent + if vout.Spent && w.db.HasExtendedIndex() { + vout.SpentTxID = tao.SpentTxid + vout.SpentIndex = int(tao.SpentIndex) + vout.SpentHeight = int(tao.SpentHeight) + } aggregateAddresses(addresses, vout.Addresses, vout.IsAddress) } // for coinbase transactions valIn is 0 @@ -858,6 +936,11 @@ func (w *Worker) txFromTxAddress(txid string, ta *db.TxAddresses, bi *db.BlockIn Vin: vins, Vout: vouts, } + if w.chainParser.SupportsVSize() { + r.VSize = int(ta.VSize) + } else { + r.Size = int(ta.VSize) + } return r } @@ -883,8 +966,8 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) { } func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, index int, c *db.AddrContract, details AccountDetails, ticker *common.CurrencyRatesTicker, secondaryCoin string) (*Token, error) { - typeName := bchain.EthereumTokenTypeMap[c.Type] - ci, validContract, err := w.getContractDescriptorInfo(c.Contract, typeName) + standard := bchain.EthereumTokenStandardMap[c.Standard] + ci, validContract, err := w.getContractDescriptorInfo(c.Contract, standard) if err != nil { return nil, errors.Annotatef(err, "getEthereumContractBalance %v", c.Contract) } @@ -892,14 +975,15 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i Contract: ci.Contract, Name: ci.Name, Symbol: ci.Symbol, - Type: typeName, + Type: standard, + Standard: standard, Transfers: int(c.Txs), Decimals: ci.Decimals, ContractIndex: strconv.Itoa(index), } // return contract balances/values only at or above AccountDetailsTokenBalances if details >= AccountDetailsTokenBalances && validContract { - if c.Type == bchain.FungibleToken { + if c.Standard == bchain.FungibleToken { // get Erc20 Contract Balance from blockchain, balance obtained from adding and subtracting transfers is not correct b, err := w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract) if err != nil { @@ -948,7 +1032,7 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i // a fallback method in case internal transactions are not processed and there is no indexed info about contract balance for an address func (w *Worker) getEthereumContractBalanceFromBlockchain(addrDesc, contract bchain.AddressDescriptor, details AccountDetails) (*Token, error) { var b *big.Int - ci, validContract, err := w.getContractDescriptorInfo(contract, bchain.UnknownTokenType) + ci, validContract, err := w.getContractDescriptorInfo(contract, bchain.UnknownTokenStandard) if err != nil { return nil, errors.Annotatef(err, "GetContractInfo %v", contract) } @@ -963,7 +1047,8 @@ func (w *Worker) getEthereumContractBalanceFromBlockchain(addrDesc, contract bch b = nil } return &Token{ - Type: ci.Type, + Type: ci.Standard, + Standard: ci.Standard, BalanceSat: (*Amount)(b), Contract: ci.Contract, Name: ci.Name, @@ -974,24 +1059,27 @@ func (w *Worker) getEthereumContractBalanceFromBlockchain(addrDesc, contract bch }, nil } -// GetContractBaseRate returns contract rate in base coin from the ticker or DB at the blocktime. Zero blocktime means now. -func (w *Worker) GetContractBaseRate(ticker *common.CurrencyRatesTicker, contract string, blocktime int64) (float64, bool) { +// GetContractBaseRate returns contract rate in base coin from the ticker or DB at the timestamp. Zero timestamp means now. +func (w *Worker) GetContractBaseRate(ticker *common.CurrencyRatesTicker, token string, timestamp int64) (float64, bool) { if ticker == nil { return 0, false } - rate, found := ticker.GetTokenRate(contract) + rate, found := ticker.GetTokenRate(token) if !found { - var date time.Time - if blocktime == 0 { - date = time.Now().UTC() + if timestamp == 0 { + ticker = w.fiatRates.GetCurrentTicker("", token) } else { - date = time.Unix(blocktime, 0).UTC() + tickers, err := w.fiatRates.GetTickersForTimestamps([]int64{timestamp}, "", token) + if err != nil || tickers == nil || len(*tickers) == 0 { + ticker = nil + } else { + ticker = (*tickers)[0] + } } - ticker, _ = w.db.FiatRatesFindTicker(&date, "", contract) if ticker == nil { return 0, false } - rate, found = ticker.GetTokenRate(contract) + rate, found = ticker.GetTokenRate(token) } return float64(rate), found @@ -1006,6 +1094,7 @@ type ethereumTypeAddressData struct { totalResults int tokensBaseValue float64 tokensSecondaryValue float64 + stakingPools []StakingPool } func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter, secondaryCoin string) (*db.AddrBalance, *ethereumTypeAddressData, error) { @@ -1039,7 +1128,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto if err != nil { return nil, nil, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc) } - ticker := w.is.GetCurrentTicker("", "") + ticker := w.fiatRates.GetCurrentTicker("", "") if details > AccountDetailsBasic { d.tokens = make([]Token, len(ca.Contracts)) var j int @@ -1064,10 +1153,16 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto d.tokens = d.tokens[:j] sort.Sort(d.tokens) } - d.contractInfo, err = w.db.GetContractInfo(addrDesc, "") + d.contractInfo, err = w.db.GetContractInfo(addrDesc, bchain.UnknownTokenStandard) if err != nil { return nil, nil, err } + if d.contractInfo != nil && d.contractInfo.Standard == bchain.UnhandledTokenStandard { + d.contractInfo, _, err = w.getContractDescriptorInfo(addrDesc, bchain.UnknownTokenStandard) + if err != nil { + return nil, nil, err + } + } if filter.FromHeight == 0 && filter.ToHeight == 0 { // compute total results for paging if filter.Vout == AddressFilterVoutOff { @@ -1105,9 +1200,43 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto filter.Vout = AddressFilterVoutQueryNotNecessary d.totalResults = -1 } + // if staking pool enabled, fetch the staking pool details + if details >= AccountDetailsBasic { + if len(w.chain.EthereumTypeGetSupportedStakingPools()) > 0 { + d.stakingPools, err = w.getStakingPoolsData(addrDesc) + if err != nil { + return nil, nil, err + } + } + } return ba, &d, nil } +func (w *Worker) getStakingPoolsData(addrDesc bchain.AddressDescriptor) ([]StakingPool, error) { + var pools []StakingPool + if len(w.chain.EthereumTypeGetSupportedStakingPools()) > 0 { + sp, err := w.chain.EthereumTypeGetStakingPoolsData(addrDesc) + if err != nil { + return nil, err + } + for i := range sp { + p := &sp[i] + pools = append(pools, StakingPool{ + Contract: p.Contract, + Name: p.Name, + PendingBalance: (*Amount)(&p.PendingBalance), + PendingDepositedBalance: (*Amount)(&p.PendingDepositedBalance), + DepositedBalance: (*Amount)(&p.DepositedBalance), + WithdrawTotalAmount: (*Amount)(&p.WithdrawTotalAmount), + ClaimableAmount: (*Amount)(&p.ClaimableAmount), + RestakedReward: (*Amount)(&p.RestakedReward), + AutocompoundBalance: (*Amount)(&p.AutocompoundBalance), + }) + } + } + return pools, nil +} + func (w *Worker) txFromTxid(txid string, bestHeight uint32, option AccountDetails, blockInfo *db.BlockInfo, addresses map[string]struct{}) (*Tx, error) { var tx *Tx var err error @@ -1204,6 +1333,8 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco txids []string pg Paging uBalSat big.Int + uBalSending big.Int + uBalReceiving big.Int totalReceived, totalSent *big.Int unconfirmedTxs int totalResults int @@ -1255,12 +1386,12 @@ 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)) + uBalReceiving.Add(&uBalReceiving, tx.getAddrVoutValue(addrDesc)) // 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)) + uBalSending.Add(&uBalSending, tx.getAddrEthereumTypeMempoolInputValue(addrDesc)) } else { - uBalSat.Sub(&uBalSat, tx.getAddrVinValue(addrDesc)) + uBalSending.Add(&uBalSending, tx.getAddrVinValue(addrDesc)) } if page == 0 { if option == AccountDetailsTxidHistory { @@ -1313,7 +1444,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco } var secondaryRate, totalSecondaryValue, totalBaseValue, secondaryValue float64 if secondaryCoin != "" { - ticker := w.is.GetCurrentTicker("", "") + ticker := w.fiatRates.GetCurrentTicker("", "") balance, err := strconv.ParseFloat((*Amount)(&ba.BalanceSat).DecimalString(w.chainParser.AmountDecimals()), 64) if ticker != nil && err == nil { r, found := ticker.Rates[secondaryCoin] @@ -1327,6 +1458,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco totalSecondaryValue = secondaryRate * totalBaseValue } } + uBalSat.Sub(&uBalReceiving, &uBalSending) r := &Address{ Paging: pg, AddrStr: address, @@ -1338,6 +1470,8 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco InternalTxs: ed.internalTxs, UnconfirmedBalanceSat: (*Amount)(&uBalSat), UnconfirmedTxs: unconfirmedTxs, + UnconfirmedSending: amountOrNil(&uBalSending), + UnconfirmedReceiving: amountOrNil(&uBalReceiving), Transactions: txs, Txids: txids, Tokens: ed.tokens, @@ -1349,15 +1483,24 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco ContractInfo: ed.contractInfo, Nonce: ed.nonce, AddressAliases: w.getAddressAliases(addresses), + StakingPools: ed.stakingPools, } // keep address backward compatible, set deprecated Erc20Contract value if ERC20 token - if ed.contractInfo != nil && ed.contractInfo.Type == bchain.ERC20TokenType { + if ed.contractInfo != nil && ed.contractInfo.Standard == bchain.ERC20TokenStandard { r.Erc20Contract = ed.contractInfo } - glog.Info("GetAddress ", address, ", ", time.Since(start)) + glog.Info("GetAddress-", option, " ", address, ", ", time.Since(start)) return r, nil } +// Returns either the Amount or nil if the number is zero +func amountOrNil(num *big.Int) *Amount { + if num.Cmp(big.NewInt(0)) == 0 { + return nil + } + return (*Amount)(num) +} + func (w *Worker) balanceHistoryHeightsFromTo(fromTimestamp, toTimestamp int64) (uint32, uint32, uint32, uint32) { fromUnix := uint32(0) toUnix := maxUint32 @@ -1528,12 +1671,13 @@ func (w *Worker) balanceHistoryForTxid(addrDesc bchain.AddressDescriptor, txid s func (w *Worker) setFiatRateToBalanceHistories(histories BalanceHistories, currencies []string) error { for i := range histories { bh := &histories[i] - t := time.Unix(int64(bh.Time), 0) - ticker, err := w.db.FiatRatesFindTicker(&t, "", "") - if err != nil { - glog.Errorf("Error finding ticker by date %v. Error: %v", t, err) + tickers, err := w.fiatRates.GetTickersForTimestamps([]int64{int64(bh.Time)}, "", "") + if err != nil || tickers == nil || len(*tickers) == 0 { + glog.Errorf("Error finding ticker by date %v. Error: %v", bh.Time, err) continue - } else if ticker == nil { + } + ticker := (*tickers)[0] + if ticker == nil { continue } if len(currencies) == 0 { @@ -1563,6 +1707,17 @@ func (w *Worker) GetBalanceHistory(address string, fromTimestamp, toTimestamp in if err != nil { return nil, err } + // do not get balance history for contracts + if w.chainType == bchain.ChainEthereumType { + ci, err := w.db.GetContractInfo(addrDesc, bchain.UnknownTokenStandard) + if err != nil { + return nil, err + } + if ci != nil { + glog.Info("GetBalanceHistory ", address, " is a contract, skipping") + return nil, NewAPIError("GetBalanceHistory for a contract not allowed", true) + } + } fromUnix, fromHeight, toUnix, toHeight := w.balanceHistoryHeightsFromTo(fromTimestamp, toTimestamp) if fromHeight >= toHeight { return bhs, nil @@ -1592,12 +1747,12 @@ func (w *Worker) GetBalanceHistory(address string, fromTimestamp, toTimestamp in func (w *Worker) waitForBackendSync() { // wait a short time if blockbook is synchronizing with backend - inSync, _, _ := w.is.GetSyncState() + inSync, _, _, _ := w.is.GetSyncState() count := 30 for !inSync && count > 0 { time.Sleep(time.Millisecond * 100) count-- - inSync, _, _ = w.is.GetSyncState() + inSync, _, _, _ = w.is.GetSyncState() } } @@ -1782,16 +1937,29 @@ func removeEmpty(stringSlice []string) []string { // getFiatRatesResult checks if CurrencyRatesTicker contains all necessary data and returns formatted result func (w *Worker) getFiatRatesResult(currencies []string, ticker *common.CurrencyRatesTicker, token string) (*FiatTicker, error) { if token != "" { - if len(currencies) != 1 { - return nil, NewAPIError("Rates for token only for a single currency", true) - } - rate := ticker.TokenRateInCurrency(token, currencies[0]) - if rate <= 0 { - rate = -1 + rates := make(map[string]float32) + if len(currencies) == 0 { + for currency := range ticker.Rates { + currency = strings.ToLower(currency) + rate := ticker.TokenRateInCurrency(token, currency) + if rate <= 0 { + rate = -1 + } + rates[currency] = rate + } + } else { + for _, currency := range currencies { + currency = strings.ToLower(currency) + rate := ticker.TokenRateInCurrency(token, currency) + if rate <= 0 { + rate = -1 + } + rates[currency] = rate + } } return &FiatTicker{ Timestamp: ticker.Timestamp.UTC().Unix(), - Rates: map[string]float32{currencies[0]: rate}, + Rates: rates, }, nil } if len(currencies) == 0 { @@ -1817,36 +1985,6 @@ func (w *Worker) getFiatRatesResult(currencies []string, ticker *common.Currency }, nil } -// GetFiatRatesForBlockID returns fiat rates for block height or block hash -func (w *Worker) GetFiatRatesForBlockID(blockID string, currencies []string, token string) (*FiatTicker, error) { - var ticker *common.CurrencyRatesTicker - bi, err := w.getBlockInfoFromBlockID(blockID) - if err != nil { - if err == bchain.ErrBlockNotFound { - return nil, NewAPIError(fmt.Sprintf("Block %v not found", blockID), true) - } - return nil, NewAPIError(fmt.Sprintf("Block %v not found, error: %v", blockID, err), false) - } - dbi := &db.BlockInfo{Time: bi.Time} // get Unix timestamp from block - tm := time.Unix(dbi.Time, 0) // convert it to Time object - vsCurrency := "" - currencies = removeEmpty(currencies) - if len(currencies) == 1 { - vsCurrency = currencies[0] - } - ticker, err = w.db.FiatRatesFindTicker(&tm, vsCurrency, token) - if err != nil { - return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false) - } else if ticker == nil { - return nil, NewAPIError(fmt.Sprintf("No tickers available for %s", tm), true) - } - result, err := w.getFiatRatesResult(currencies, ticker, token) - if err != nil { - return nil, err - } - return result, nil -} - // GetCurrentFiatRates returns last available fiat rates func (w *Worker) GetCurrentFiatRates(currencies []string, token string) (*FiatTicker, error) { vsCurrency := "" @@ -1854,10 +1992,14 @@ func (w *Worker) GetCurrentFiatRates(currencies []string, token string) (*FiatTi if len(currencies) == 1 { vsCurrency = currencies[0] } - ticker := w.is.GetCurrentTicker(vsCurrency, token) + ticker := w.fiatRates.GetCurrentTicker(vsCurrency, token) var err error if ticker == nil { - ticker, err = w.db.FiatRatesFindLastTicker(vsCurrency, token) + if token == "" { + // fallback - get last fiat rate from db if not in current ticker + // not for tokens, many tokens do not have fiat rates at all and it is very costly to do DB search for token without an exchange rate + ticker, err = w.db.FiatRatesFindLastTicker(vsCurrency, token) + } if err != nil { return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false) } else if ticker == nil { @@ -1891,47 +2033,64 @@ func (w *Worker) GetFiatRatesForTimestamps(timestamps []int64, currencies []stri if len(currencies) == 1 { vsCurrency = currencies[0] } - - ret := &FiatTickers{} - for _, timestamp := range timestamps { - date := time.Unix(timestamp, 0) - date = date.UTC() - ticker, err := w.db.FiatRatesFindTicker(&date, vsCurrency, token) - if err != nil { - glog.Errorf("Error finding ticker for date %v. Error: %v", date, err) - ret.Tickers = append(ret.Tickers, FiatTicker{Timestamp: date.Unix(), Rates: makeErrorRates(currencies)}) - continue - } else if ticker == nil { - ret.Tickers = append(ret.Tickers, FiatTicker{Timestamp: date.Unix(), Rates: makeErrorRates(currencies)}) + tickers, err := w.fiatRates.GetTickersForTimestamps(timestamps, vsCurrency, token) + if err != nil { + return nil, err + } + if tickers == nil { + return nil, NewAPIError("No tickers found", true) + } + if len(*tickers) != len(timestamps) { + glog.Error("GetFiatRatesForTimestamps: number of tickers does not match timestamps ", len(*tickers), ", ", len(timestamps)) + return nil, NewAPIError("No tickers found", false) + } + fiatTickers := make([]FiatTicker, len(*tickers)) + for i, t := range *tickers { + if t == nil { + fiatTickers[i] = FiatTicker{Timestamp: timestamps[i], Rates: makeErrorRates(currencies)} continue } - result, err := w.getFiatRatesResult(currencies, ticker, token) + result, err := w.getFiatRatesResult(currencies, t, token) if err != nil { if apiErr, ok := err.(*APIError); ok { if apiErr.Public { return nil, err } } - ret.Tickers = append(ret.Tickers, FiatTicker{Timestamp: date.Unix(), Rates: makeErrorRates(currencies)}) + fiatTickers[i] = FiatTicker{Timestamp: timestamps[i], Rates: makeErrorRates(currencies)} continue } - ret.Tickers = append(ret.Tickers, *result) + fiatTickers[i] = *result } - return ret, nil + return &FiatTickers{Tickers: fiatTickers}, nil +} + +// GetFiatRatesForBlockID returns fiat rates for block height or block hash +func (w *Worker) GetFiatRatesForBlockID(blockID string, currencies []string, token string) (*FiatTicker, error) { + bi, err := w.getBlockInfoFromBlockID(blockID) + if err != nil { + if err == bchain.ErrBlockNotFound { + return nil, NewAPIError(fmt.Sprintf("Block %v not found", blockID), true) + } + return nil, NewAPIError(fmt.Sprintf("Block %v not found, error: %v", blockID, err), false) + } + tickers, err := w.GetFiatRatesForTimestamps([]int64{bi.Time}, currencies, token) + if err != nil || tickers == nil || len(tickers.Tickers) == 0 { + return nil, err + } + return &tickers.Tickers[0], nil } // GetAvailableVsCurrencies returns the list of available versus currencies for exchange rates func (w *Worker) GetAvailableVsCurrencies(timestamp int64, token string) (*AvailableVsCurrencies, error) { - date := time.Unix(timestamp, 0) - date = date.UTC() - - ticker, err := w.db.FiatRatesFindTicker(&date, "", strings.ToLower(token)) + tickers, err := w.fiatRates.GetTickersForTimestamps([]int64{timestamp}, "", token) if err != nil { return nil, NewAPIError(fmt.Sprintf("Error finding ticker: %v", err), false) - } else if ticker == nil { - return nil, NewAPIError(fmt.Sprintf("No tickers found for date %v.", date), true) } - + if tickers == nil || len(*tickers) == 0 { + return nil, NewAPIError("No tickers found", true) + } + ticker := (*tickers)[0] keys := make([]string, 0, len(ticker.Rates)) for k := range ticker.Rates { keys = append(keys, k) @@ -2144,7 +2303,7 @@ func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) { }, nil } -// GetBlock returns paged data about block +// GetBlockRaw returns paged data about block func (w *Worker) GetBlockRaw(bid string) (*BlockRaw, error) { hash := w.getBlockHashBlockID(bid) if hash == "" { @@ -2160,6 +2319,48 @@ func (w *Worker) GetBlockRaw(bid string) (*BlockRaw, error) { return &BlockRaw{Hex: hex}, err } +// GetBlockFiltersBatch returns array of block filter data in the format ["height:hash:filter",...] if blocks greater than bestKnownBlockHash +func (w *Worker) GetBlockFiltersBatch(bestKnownBlockHash string, pageSize int) ([]string, error) { + if w.is.BlockGolombFilterP == 0 { + return nil, NewAPIError("Not supported", true) + } + if pageSize > 10000 { + return nil, NewAPIError("pageSize max 10000", true) + } + if pageSize <= 0 { + pageSize = 1000 + } + bi, err := w.chain.GetBlockInfo(bestKnownBlockHash) + if err != nil { + return nil, err + } + bestHeight, _, err := w.db.GetBestBlock() + if err != nil { + return nil, err + } + from := bi.Height + 1 + to := bestHeight + 1 + if from >= to { + return []string{}, nil + } + if to-from > uint32(pageSize) { + to = from + uint32(pageSize) + } + r := make([]string, 0, to-from) + for i := from; i < to; i++ { + blockHash, err := w.db.GetBlockHash(uint32(i)) + if err != nil { + return nil, err + } + blockFilter, err := w.db.GetBlockFilter(blockHash) + if err != nil { + return nil, err + } + r = append(r, fmt.Sprintf("%d:%s:%s", i, blockHash, blockFilter)) + } + return r, 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() @@ -2229,9 +2430,16 @@ func nonZeroTime(t time.Time) *time.Time { // GetSystemInfo returns information about system func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) { - start := time.Now() + start := time.Now().UTC() vi := common.GetVersionInfo() - inSync, bestHeight, lastBlockTime := w.is.GetSyncState() + inSync, bestHeight, lastBlockTime, startSync := w.is.GetSyncState() + blockPeriod := w.is.GetAvgBlockPeriod() + if !inSync && !w.is.InitialSync { + // if less than 5 seconds into syncing, return inSync=true to avoid short time not in sync reports that confuse monitoring + if startSync.Add(5 * time.Second).After(start) { + inSync = true + } + } inSyncMempool, lastMempoolTime, mempoolSize := w.is.GetMempoolSyncState() ci, err := w.chain.GetChainInfo() var backendError string @@ -2243,6 +2451,13 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) { inSync = false inSyncMempool = false } + // for networks with stable block period, set not in sync if last sync more than 12 block periods ago + if inSync && blockPeriod > 0 && w.chainType == bchain.ChainEthereumType { + threshold := 12 * time.Duration(blockPeriod) * time.Second + if lastBlockTime.Add(threshold).Before(time.Now().UTC()) { + inSync = false + } + } var columnStats []common.InternalStateColumn var internalDBSize int64 if internal { @@ -2250,11 +2465,13 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) { internalDBSize = w.is.DBSizeTotal() } var currentFiatRatesTime time.Time - if w.is.CurrentTicker != nil { - currentFiatRatesTime = w.is.CurrentTicker.Timestamp + ct := w.fiatRates.GetCurrentTicker("", "") + if ct != nil { + currentFiatRatesTime = ct.Timestamp } blockbookInfo := &BlockbookInfo{ Coin: w.is.Coin, + Network: w.is.GetNetwork(), Host: w.is.Host, Version: vi.Version, GitCommit: vi.GitCommit, @@ -2273,6 +2490,7 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) { CurrentFiatRatesTime: nonZeroTime(currentFiatRatesTime), HistoricalFiatRatesTime: nonZeroTime(w.is.HistoricalFiatRatesTime), HistoricalTokenFiatRatesTime: nonZeroTime(w.is.HistoricalTokenFiatRatesTime), + SupportedStakingPools: w.chain.EthereumTypeGetSupportedStakingPools(), DbSize: w.db.DatabaseSizeOnDisk(), DbSizeFromColumns: internalDBSize, DbColumns: columnStats, diff --git a/api/xpub.go b/api/xpub.go index da24525883..ca1c4c009c 100644 --- a/api/xpub.go +++ b/api/xpub.go @@ -267,7 +267,9 @@ func (w *Worker) tokenFromXpubAddress(data *xpubData, ad *xpubAddress, changeInd } } return Token{ - Type: bchain.XPUBAddressTokenType, + // Deprecated: Use Standard instead. + Type: bchain.XPUBAddressStandard, + Standard: bchain.XPUBAddressStandard, Name: address, Decimals: w.chainParser.AmountDecimals(), BalanceSat: (*Amount)(balance), @@ -541,6 +543,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc } else { txCount = int(data.txCountEstimate) } + addrTxCount := int(data.txCountEstimate) usedTokens := 0 var tokens []Token var xpubAddresses map[string]struct{} @@ -555,7 +558,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc usedTokens++ } if option > AccountDetailsBasic { - token := w.tokenFromXpubAddress(data, ad, ci, i, option) + token := w.tokenFromXpubAddress(data, ad, int(xd.ChangeIndexes[ci]), i, option) if filter.TokensToReturn == TokensToReturnDerived || filter.TokensToReturn == TokensToReturnUsed && ad.balance != nil || filter.TokensToReturn == TokensToReturnNonzeroBalance && ad.balance != nil && !IsZeroBigInt(&ad.balance.BalanceSat) { @@ -571,7 +574,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc var secondaryValue float64 if secondaryCoin != "" { - ticker := w.is.GetCurrentTicker("", "") + ticker := w.fiatRates.GetCurrentTicker("", "") balance, err := strconv.ParseFloat((*Amount)(&data.balanceSat).DecimalString(w.chainParser.AmountDecimals()), 64) if ticker != nil && err == nil { r, found := ticker.Rates[secondaryCoin] @@ -589,6 +592,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc TotalReceivedSat: (*Amount)(&totalReceived), TotalSentSat: (*Amount)(&data.sentSat), Txs: txCount, + AddrTxCount: addrTxCount, UnconfirmedBalanceSat: (*Amount)(&uBalSat), UnconfirmedTxs: unconfirmedTxs, Transactions: txs, diff --git a/bchain/basechain.go b/bchain/basechain.go index bcd03d195d..7e34c988ca 100644 --- a/bchain/basechain.go +++ b/bchain/basechain.go @@ -39,32 +39,59 @@ func (b *BaseChain) GetMempoolEntry(txid string) (*MempoolEntry, error) { return nil, errors.New("GetMempoolEntry: not supported") } +// LongTermFeeRate returns smallest fee rate from historic blocks. +func (b *BaseChain) LongTermFeeRate() (*LongTermFeeRate, error) { + return nil, errors.New("not supported") +} + // EthereumTypeGetBalance is not supported func (b *BaseChain) EthereumTypeGetBalance(addrDesc AddressDescriptor) (*big.Int, error) { - return nil, errors.New("Not supported") + return nil, errors.New("not supported") } // EthereumTypeGetNonce is not supported func (b *BaseChain) EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error) { - return 0, errors.New("Not supported") + return 0, errors.New("not supported") } // EthereumTypeEstimateGas is not supported func (b *BaseChain) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) { - return 0, errors.New("Not supported") + return 0, errors.New("not supported") +} + +// EthereumTypeGetEip1559Fees is not supported +func (b *BaseChain) EthereumTypeGetEip1559Fees() (*Eip1559Fees, error) { + return nil, errors.New("not supported") } // GetContractInfo is not supported func (b *BaseChain) GetContractInfo(contractDesc AddressDescriptor) (*ContractInfo, error) { - return nil, errors.New("Not supported") + return nil, errors.New("not supported") } // EthereumTypeGetErc20ContractBalance is not supported func (b *BaseChain) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) { - return nil, errors.New("Not supported") + return nil, errors.New("not supported") } -// GetContractInfo returns URI of non fungible or multi token defined by token id +// GetTokenURI returns URI of non fungible or multi token defined by token id func (p *BaseChain) GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error) { - return "", errors.New("Not supported") + return "", errors.New("not supported") +} + +func (b *BaseChain) EthereumTypeGetSupportedStakingPools() []string { + return nil +} + +func (b *BaseChain) EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error) { + return nil, errors.New("not supported") +} + +// EthereumTypeRpcCall calls eth_call with given data and to address +func (b *BaseChain) EthereumTypeRpcCall(data, to, from string) (string, error) { + return "", errors.New("not supported") +} + +func (b *BaseChain) EthereumTypeGetRawTransaction(txid string) (string, error) { + return "", errors.New("not supported") } diff --git a/bchain/basemempool.go b/bchain/basemempool.go index d22c94956c..4561ca8898 100644 --- a/bchain/basemempool.go +++ b/bchain/basemempool.go @@ -14,11 +14,13 @@ type addrIndex struct { type txEntry struct { addrIndexes []addrIndex time uint32 + filter string } type txidio struct { - txid string - io []addrIndex + txid string + io []addrIndex + filter string } // BaseMempool is mempool base handle @@ -70,19 +72,27 @@ func (a MempoolTxidEntries) Less(i, j int) bool { // removeEntryFromMempool removes entry from mempool structs. The caller is responsible for locking! func (m *BaseMempool) removeEntryFromMempool(txid string, entry txEntry) { delete(m.txEntries, txid) + // store already processed addrDesc - it can appear multiple times as a different outpoint + processedAddrDesc := make(map[string]struct{}) for _, si := range entry.addrIndexes { outpoints, found := m.addrDescToTx[si.addrDesc] if found { - newOutpoints := make([]Outpoint, 0, len(outpoints)-1) - for _, o := range outpoints { - if o.Txid != txid { - newOutpoints = append(newOutpoints, o) + _, processed := processedAddrDesc[si.addrDesc] + if !processed { + processedAddrDesc[si.addrDesc] = struct{}{} + j := 0 + for i := 0; i < len(outpoints); i++ { + if outpoints[i].Txid != txid { + outpoints[j] = outpoints[i] + j++ + } + } + outpoints = outpoints[:j] + if len(outpoints) > 0 { + m.addrDescToTx[si.addrDesc] = outpoints + } else { + delete(m.addrDescToTx, si.addrDesc) } - } - if len(newOutpoints) > 0 { - m.addrDescToTx[si.addrDesc] = newOutpoints - } else { - delete(m.addrDescToTx, si.addrDesc) } } } diff --git a/bchain/basemempool_test.go b/bchain/basemempool_test.go new file mode 100644 index 0000000000..5842456d1f --- /dev/null +++ b/bchain/basemempool_test.go @@ -0,0 +1,176 @@ +package bchain + +import ( + reflect "reflect" + "strconv" + "testing" +) + +func generateAddIndexes(count int) []addrIndex { + rv := make([]addrIndex, count) + for i := range count { + rv[i] = addrIndex{ + addrDesc: "ad" + strconv.Itoa(i), + } + } + return rv +} + +func generateTxEntries(count int, skipTx int) map[string]txEntry { + rv := make(map[string]txEntry) + for i := range count { + if i != skipTx { + tx := "tx" + strconv.Itoa(i) + rv[tx] = txEntry{ + addrIndexes: generateAddIndexes(count), + } + } + } + return rv +} + +func generateAddrDescToTx(count int, skipTx int) map[string][]Outpoint { + rv := make(map[string][]Outpoint) + for i := range count { + ad := "ad" + strconv.Itoa(i) + op := []Outpoint{} + for j := range count { + if j != skipTx { + tx := "tx" + strconv.Itoa(j) + op = append(op, Outpoint{ + Txid: tx, + }) + } + } + if len(op) > 0 { + rv[ad] = op + } + } + return rv +} + +func TestBaseMempool_removeEntryFromMempool(t *testing.T) { + tests := []struct { + name string + m *BaseMempool + want *BaseMempool + txid string + entry txEntry + }{ + { + name: "test1", + m: &BaseMempool{ + txEntries: map[string]txEntry{ + "tx1": { + addrIndexes: []addrIndex{{addrDesc: "ad1", n: 0}, {addrDesc: "ad1", n: 1}}, + }, + "tx2": { + addrIndexes: []addrIndex{{addrDesc: "ad1"}}, + }, + }, + addrDescToTx: map[string][]Outpoint{ + "ad1": { + {Txid: "tx1", Vout: 0}, + {Txid: "tx1", Vout: 1}, + {Txid: "tx2"}, + }, + }, + }, + want: &BaseMempool{ + txEntries: map[string]txEntry{ + "tx2": { + addrIndexes: []addrIndex{{addrDesc: "ad1"}}, + }, + }, + addrDescToTx: map[string][]Outpoint{ + "ad1": {{Txid: "tx2"}}}, + }, + txid: "tx1", + entry: txEntry{ + addrIndexes: []addrIndex{ + {addrDesc: "ad1"}, + {addrDesc: "ad2"}, + }, + }, + }, + { + name: "test2", + m: &BaseMempool{ + txEntries: map[string]txEntry{ + "tx1": { + addrIndexes: []addrIndex{{addrDesc: "ad1"}, {addrDesc: "ad1", n: 1}}, + }, + }, + addrDescToTx: map[string][]Outpoint{ + "ad1": { + {Txid: "tx1", Vout: 0}, + {Txid: "tx1", Vout: 1}, + }, + }, + }, + want: &BaseMempool{ + txEntries: map[string]txEntry{}, + addrDescToTx: map[string][]Outpoint{}, + }, + txid: "tx1", + entry: txEntry{ + addrIndexes: []addrIndex{ + {addrDesc: "ad1"}, + }, + }, + }, + { + name: "generated1", + m: &BaseMempool{ + txEntries: generateTxEntries(1, -1), + addrDescToTx: generateAddrDescToTx(1, -1), + }, + want: &BaseMempool{ + txEntries: generateTxEntries(1, 0), + addrDescToTx: generateAddrDescToTx(1, 0), + }, + txid: "tx0", + entry: txEntry{ + addrIndexes: generateAddIndexes(1), + }, + }, + { + name: "generated2", + m: &BaseMempool{ + txEntries: generateTxEntries(2, -1), + addrDescToTx: generateAddrDescToTx(2, -1), + }, + want: &BaseMempool{ + txEntries: generateTxEntries(2, 1), + addrDescToTx: generateAddrDescToTx(2, 1), + }, + txid: "tx1", + entry: txEntry{ + addrIndexes: generateAddIndexes(2), + }, + }, + { + name: "generated5000", + m: &BaseMempool{ + txEntries: generateTxEntries(5000, -1), + addrDescToTx: generateAddrDescToTx(5000, -1), + }, + want: &BaseMempool{ + txEntries: generateTxEntries(5000, 2), + addrDescToTx: generateAddrDescToTx(5000, 2), + }, + txid: "tx2", + entry: txEntry{ + addrIndexes: generateAddIndexes(5000), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.m.removeEntryFromMempool(tt.txid, tt.entry) + if !reflect.DeepEqual(tt.m, tt.want) { + t.Errorf("removeEntryFromMempool() got = %+v, want %+v", tt.m, tt.want) + } + }) + } +} diff --git a/bchain/baseparser.go b/bchain/baseparser.go index f1278cc34d..9a7dcbea91 100644 --- a/bchain/baseparser.go +++ b/bchain/baseparser.go @@ -47,10 +47,7 @@ func (p *BaseParser) AmountToBigInt(n common.JSONNumber) (big.Int, error) { var r big.Int s := string(n) i := strings.IndexByte(s, '.') - d := p.AmountDecimalPoint - if d > len(zeros) { - d = len(zeros) - } + d := min(p.AmountDecimalPoint, len(zeros)) if i == -1 { s = s + zeros[:d] } else { diff --git a/bchain/coins/arbitrum/arbitrumrpc.go b/bchain/coins/arbitrum/arbitrumrpc.go new file mode 100644 index 0000000000..d862e4b8af --- /dev/null +++ b/bchain/coins/arbitrum/arbitrumrpc.go @@ -0,0 +1,79 @@ +package arbitrum + +import ( + "context" + "encoding/json" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/bchain/coins/eth" +) + +const ( + ArbitrumOneMainNet eth.Network = 42161 + ArbitrumNovaMainNet eth.Network = 42170 +) + +// ArbitrumRPC is an interface to JSON-RPC arbitrum service. +type ArbitrumRPC struct { + *eth.EthereumRPC +} + +// NewArbitrumRPC returns new ArbitrumRPC instance. +func NewArbitrumRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { + c, err := eth.NewEthereumRPC(config, pushHandler) + if err != nil { + return nil, err + } + + s := &ArbitrumRPC{ + EthereumRPC: c.(*eth.EthereumRPC), + } + + return s, nil +} + +// Initialize arbitrum rpc interface +func (b *ArbitrumRPC) Initialize() error { + b.OpenRPC = eth.OpenRPC + + rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL) + if err != nil { + return err + } + + // set chain specific + b.Client = ec + b.RPC = rc + b.NewBlock = eth.NewEthereumNewBlock() + b.NewTx = eth.NewEthereumNewTx() + + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) + defer cancel() + + id, err := b.Client.NetworkID(ctx) + if err != nil { + return err + } + + // parameters for getInfo request + switch eth.Network(id.Uint64()) { + case ArbitrumOneMainNet: + b.MainNetChainID = ArbitrumOneMainNet + b.Testnet = false + b.Network = "livenet" + case ArbitrumNovaMainNet: + b.MainNetChainID = ArbitrumNovaMainNet + b.Testnet = false + b.Network = "livenet" + default: + return errors.Errorf("Unknown network id %v", id) + } + + b.InitAlternativeProviders() + + glog.Info("rpc: block chain ", b.Network) + + return nil +} diff --git a/bchain/coins/avalanche/avalancherpc.go b/bchain/coins/avalanche/avalancherpc.go index c7f3d7ec1d..916c4c2f45 100644 --- a/bchain/coins/avalanche/avalancherpc.go +++ b/bchain/coins/avalanche/avalancherpc.go @@ -6,13 +6,10 @@ import ( "fmt" "net/url" - "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/coreth/core/types" - "github.com/ava-labs/coreth/ethclient" - "github.com/ava-labs/coreth/interfaces" - "github.com/ava-labs/coreth/rpc" + jsontypes "github.com/ava-labs/avalanchego/utils/json" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" "github.com/golang/glog" "github.com/juju/errors" "github.com/trezor/blockbook/bchain" @@ -27,7 +24,7 @@ const ( // AvalancheRPC is an interface to JSON-RPC avalanche service. type AvalancheRPC struct { *eth.EthereumRPC - info info.Client + info *rpc.Client } // NewAvalancheRPC returns new AvalancheRPC instance. @@ -52,10 +49,15 @@ func (b *AvalancheRPC) Initialize() error { return nil, nil, err } rc := &AvalancheRPCClient{Client: r} - c := &AvalancheClient{Client: ethclient.NewClient(r)} + c := &AvalancheClient{Client: ethclient.NewClient(r), AvalancheRPCClient: rc} return rc, c, nil } + rpcClient, client, err := b.OpenRPC(b.ChainConfig.RPCURL) + if err != nil { + return err + } + rpcUrl, err := url.Parse(b.ChainConfig.RPCURL) if err != nil { return err @@ -66,7 +68,7 @@ func (b *AvalancheRPC) Initialize() error { scheme = "https" } - rpcClient, client, err := b.OpenRPC(b.ChainConfig.RPCURL) + infoClient, err := rpc.DialHTTP(fmt.Sprintf("%s://%s/ext/info", scheme, rpcUrl.Host)) if err != nil { return err } @@ -74,9 +76,9 @@ func (b *AvalancheRPC) Initialize() error { // set chain specific b.Client = client b.RPC = rpcClient - b.info = info.NewClient(fmt.Sprintf("%s://%s", scheme, rpcUrl.Host)) + b.info = infoClient b.MainNetChainID = MainNet - b.NewBlock = &AvalancheNewBlock{channel: make(chan *types.Header)} + b.NewBlock = &AvalancheNewBlock{channel: make(chan *Header)} b.NewTx = &AvalancheNewTx{channel: make(chan common.Hash)} ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) @@ -96,6 +98,8 @@ func (b *AvalancheRPC) Initialize() error { return errors.Errorf("Unknown network id %v", id) } + b.InitAlternativeProviders() + glog.Info("rpc: block chain ", b.Network) return nil @@ -105,49 +109,25 @@ func (b *AvalancheRPC) Initialize() error { func (b *AvalancheRPC) GetChainInfo() (*bchain.ChainInfo, error) { ci, err := b.EthereumRPC.GetChainInfo() if err != nil { - fmt.Println(err) return nil, err } ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() - v, err := b.info.GetNodeVersion(ctx) - if err != nil { - fmt.Println("here", err) - return nil, err + var v struct { + Version string `json:"version"` + DatabaseVersion string `json:"databaseVersion"` + RPCProtocolVersion jsontypes.Uint32 `json:"rpcProtocolVersion"` + GitCommit string `json:"gitCommit"` + VMVersions map[string]string `json:"vmVersions"` } - if avm, ok := v.VMVersions["avm"]; ok { - ci.Version = avm + if err := b.info.CallContext(ctx, &v, "info.getNodeVersion"); err == nil { + if avm, ok := v.VMVersions["avm"]; ok { + ci.Version = avm + } } return ci, nil } - -// EthereumTypeEstimateGas returns estimation of gas consumption for given transaction parameters -func (b *AvalancheRPC) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) - defer cancel() - msg := interfaces.CallMsg{} - if s, ok := eth.GetStringFromMap("from", params); ok && len(s) > 0 { - msg.From = common.HexToAddress(s) - } - if s, ok := eth.GetStringFromMap("to", params); ok && len(s) > 0 { - a := common.HexToAddress(s) - msg.To = &a - } - if s, ok := eth.GetStringFromMap("data", params); ok && len(s) > 0 { - msg.Data = common.FromHex(s) - } - if s, ok := eth.GetStringFromMap("value", params); ok && len(s) > 0 { - msg.Value, _ = hexutil.DecodeBig(s) - } - if s, ok := eth.GetStringFromMap("gas", params); ok && len(s) > 0 { - msg.Gas, _ = hexutil.DecodeUint64(s) - } - if s, ok := eth.GetStringFromMap("gasPrice", params); ok && len(s) > 0 { - msg.GasPrice, _ = hexutil.DecodeBig(s) - } - return b.Client.EstimateGas(ctx, msg) -} diff --git a/bchain/coins/avalanche/evm.go b/bchain/coins/avalanche/evm.go index 435c0fd5b9..ffff75559c 100644 --- a/bchain/coins/avalanche/evm.go +++ b/bchain/coins/avalanche/evm.go @@ -5,32 +5,32 @@ import ( "math/big" "strings" - "github.com/ava-labs/coreth/core/types" - "github.com/ava-labs/coreth/ethclient" - "github.com/ava-labs/coreth/interfaces" - "github.com/ava-labs/coreth/rpc" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" "github.com/trezor/blockbook/bchain" ) // AvalancheClient wraps a client to implement the EVMClient interface type AvalancheClient struct { - ethclient.Client + *ethclient.Client + *AvalancheRPCClient } // HeaderByNumber returns a block header that implements the EVMHeader interface func (c *AvalancheClient) HeaderByNumber(ctx context.Context, number *big.Int) (bchain.EVMHeader, error) { - h, err := c.Client.HeaderByNumber(ctx, number) - if err != nil { - return nil, err + var head *Header + err := c.AvalancheRPCClient.CallContext(ctx, &head, "eth_getBlockByNumber", bchain.ToBlockNumArg(number), false) + if err == nil && head == nil { + err = ethereum.NotFound } - - return &AvalancheHeader{Header: h}, nil + return &AvalancheHeader{Header: head}, err } // EstimateGas returns the current estimated gas cost for executing a transaction func (c *AvalancheClient) EstimateGas(ctx context.Context, msg interface{}) (uint64, error) { - return c.Client.EstimateGas(ctx, msg.(interfaces.CallMsg)) + return c.Client.EstimateGas(ctx, msg.(ethereum.CallMsg)) } // BalanceAt returns the balance for the given account at a specific block, or latest known block if no block number is provided @@ -72,7 +72,7 @@ func (c *AvalancheRPCClient) CallContext(ctx context.Context, result interface{} // AvalancheHeader wraps a block header to implement the EVMHeader interface type AvalancheHeader struct { - *types.Header + *Header } // Hash returns the block hash as a hex string @@ -102,7 +102,7 @@ type AvalancheClientSubscription struct { // AvalancheNewBlock wraps a block header channel to implement the EVMNewBlockSubscriber interface type AvalancheNewBlock struct { - channel chan *types.Header + channel chan *Header } // Channel returns the underlying channel as an empty interface diff --git a/bchain/coins/avalanche/types.go b/bchain/coins/avalanche/types.go new file mode 100644 index 0000000000..c07ebab244 --- /dev/null +++ b/bchain/coins/avalanche/types.go @@ -0,0 +1,232 @@ +package avalanche + +import ( + "encoding/json" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" +) + +// Header represents a block header in the Avalanche blockchain. +type Header struct { + RpcHash common.Hash `json:"hash" gencodec:"required"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase common.Address `json:"miner" gencodec:"required"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom types.Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *big.Int `json:"difficulty" gencodec:"required"` + Number *big.Int `json:"number" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Time uint64 `json:"timestamp" gencodec:"required"` + Extra []byte `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce types.BlockNonce `json:"nonce"` + ExtDataHash common.Hash `json:"extDataHash" gencodec:"required"` + + // BaseFee was added by EIP-1559 and is ignored in legacy headers. + BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` + + // ExtDataGasUsed was added by Apricot Phase 4 and is ignored in legacy + // headers. + // + // It is not a uint64 like GasLimit or GasUsed because it is not possible to + // correctly encode this field optionally with uint64. + ExtDataGasUsed *big.Int `json:"extDataGasUsed" rlp:"optional"` + + // BlockGasCost was added by Apricot Phase 4 and is ignored in legacy + // headers. + BlockGasCost *big.Int `json:"blockGasCost" rlp:"optional"` + + // BlobGasUsed was added by EIP-4844 and is ignored in legacy headers. + BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"` + + // ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers. + ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"` + + // ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers. + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` +} + +// MarshalJSON marshals as JSON. +func (h Header) MarshalJSON() ([]byte, error) { + type Header struct { + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase common.Address `json:"miner" gencodec:"required"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom types.Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce types.BlockNonce `json:"nonce"` + ExtDataHash common.Hash `json:"extDataHash" gencodec:"required"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + ExtDataGasUsed *hexutil.Big `json:"extDataGasUsed" rlp:"optional"` + BlockGasCost *hexutil.Big `json:"blockGasCost" rlp:"optional"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + Hash common.Hash `json:"hash"` + } + var enc Header + enc.ParentHash = h.ParentHash + enc.UncleHash = h.UncleHash + enc.Coinbase = h.Coinbase + enc.Root = h.Root + enc.TxHash = h.TxHash + enc.ReceiptHash = h.ReceiptHash + enc.Bloom = h.Bloom + enc.Difficulty = (*hexutil.Big)(h.Difficulty) + enc.Number = (*hexutil.Big)(h.Number) + enc.GasLimit = hexutil.Uint64(h.GasLimit) + enc.GasUsed = hexutil.Uint64(h.GasUsed) + enc.Time = hexutil.Uint64(h.Time) + enc.Extra = h.Extra + enc.MixDigest = h.MixDigest + enc.Nonce = h.Nonce + enc.ExtDataHash = h.ExtDataHash + enc.BaseFee = (*hexutil.Big)(h.BaseFee) + enc.ExtDataGasUsed = (*hexutil.Big)(h.ExtDataGasUsed) + enc.BlockGasCost = (*hexutil.Big)(h.BlockGasCost) + enc.BlobGasUsed = (*hexutil.Uint64)(h.BlobGasUsed) + enc.ExcessBlobGas = (*hexutil.Uint64)(h.ExcessBlobGas) + enc.ParentBeaconRoot = h.ParentBeaconRoot + enc.Hash = h.Hash() + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (h *Header) UnmarshalJSON(input []byte) error { + type Header struct { + RpcHash *common.Hash `json:"hash"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase *common.Address `json:"miner" gencodec:"required"` + Root *common.Hash `json:"stateRoot" gencodec:"required"` + TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom *types.Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest *common.Hash `json:"mixHash"` + Nonce *types.BlockNonce `json:"nonce"` + ExtDataHash *common.Hash `json:"extDataHash" gencodec:"required"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + ExtDataGasUsed *hexutil.Big `json:"extDataGasUsed" rlp:"optional"` + BlockGasCost *hexutil.Big `json:"blockGasCost" rlp:"optional"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` + ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + } + var dec Header + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.RpcHash == nil { + return errors.New("missing required field 'hash' for Header") + } + h.RpcHash = *dec.RpcHash + if dec.ParentHash == nil { + return errors.New("missing required field 'parentHash' for Header") + } + h.ParentHash = *dec.ParentHash + if dec.UncleHash == nil { + return errors.New("missing required field 'sha3Uncles' for Header") + } + h.UncleHash = *dec.UncleHash + if dec.Coinbase == nil { + return errors.New("missing required field 'miner' for Header") + } + h.Coinbase = *dec.Coinbase + if dec.Root == nil { + return errors.New("missing required field 'stateRoot' for Header") + } + h.Root = *dec.Root + if dec.TxHash == nil { + return errors.New("missing required field 'transactionsRoot' for Header") + } + h.TxHash = *dec.TxHash + if dec.ReceiptHash == nil { + return errors.New("missing required field 'receiptsRoot' for Header") + } + h.ReceiptHash = *dec.ReceiptHash + if dec.Bloom == nil { + return errors.New("missing required field 'logsBloom' for Header") + } + h.Bloom = *dec.Bloom + if dec.Difficulty == nil { + return errors.New("missing required field 'difficulty' for Header") + } + h.Difficulty = (*big.Int)(dec.Difficulty) + if dec.Number == nil { + return errors.New("missing required field 'number' for Header") + } + h.Number = (*big.Int)(dec.Number) + if dec.GasLimit == nil { + return errors.New("missing required field 'gasLimit' for Header") + } + h.GasLimit = uint64(*dec.GasLimit) + if dec.GasUsed == nil { + return errors.New("missing required field 'gasUsed' for Header") + } + h.GasUsed = uint64(*dec.GasUsed) + if dec.Time == nil { + return errors.New("missing required field 'timestamp' for Header") + } + h.Time = uint64(*dec.Time) + if dec.Extra == nil { + return errors.New("missing required field 'extraData' for Header") + } + h.Extra = *dec.Extra + if dec.MixDigest != nil { + h.MixDigest = *dec.MixDigest + } + if dec.Nonce != nil { + h.Nonce = *dec.Nonce + } + if dec.ExtDataHash == nil { + return errors.New("missing required field 'extDataHash' for Header") + } + h.ExtDataHash = *dec.ExtDataHash + if dec.BaseFee != nil { + h.BaseFee = (*big.Int)(dec.BaseFee) + } + if dec.ExtDataGasUsed != nil { + h.ExtDataGasUsed = (*big.Int)(dec.ExtDataGasUsed) + } + if dec.BlockGasCost != nil { + h.BlockGasCost = (*big.Int)(dec.BlockGasCost) + } + if dec.BlobGasUsed != nil { + h.BlobGasUsed = (*uint64)(dec.BlobGasUsed) + } + if dec.ExcessBlobGas != nil { + h.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) + } + if dec.ParentBeaconRoot != nil { + h.ParentBeaconRoot = dec.ParentBeaconRoot + } + return nil +} + +// Hash returns the block hash of the header +func (h *Header) Hash() common.Hash { + return h.RpcHash +} diff --git a/bchain/coins/base/baserpc.go b/bchain/coins/base/baserpc.go new file mode 100644 index 0000000000..f0d0192118 --- /dev/null +++ b/bchain/coins/base/baserpc.go @@ -0,0 +1,75 @@ +package base + +import ( + "context" + "encoding/json" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/bchain/coins/eth" +) + +const ( + // MainNet is production network + MainNet eth.Network = 8453 +) + +// BaseRPC is an interface to JSON-RPC base service. +type BaseRPC struct { + *eth.EthereumRPC +} + +// NewBaseRPC returns new BaseRPC instance. +func NewBaseRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { + c, err := eth.NewEthereumRPC(config, pushHandler) + if err != nil { + return nil, err + } + + s := &BaseRPC{ + EthereumRPC: c.(*eth.EthereumRPC), + } + + return s, nil +} + +// Initialize base rpc interface +func (b *BaseRPC) Initialize() error { + b.OpenRPC = eth.OpenRPC + + rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL) + if err != nil { + return err + } + + // set chain specific + b.Client = ec + b.RPC = rc + b.MainNetChainID = MainNet + b.NewBlock = eth.NewEthereumNewBlock() + b.NewTx = eth.NewEthereumNewTx() + + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) + defer cancel() + + id, err := b.Client.NetworkID(ctx) + if err != nil { + return err + } + + // parameters for getInfo request + switch eth.Network(id.Uint64()) { + case MainNet: + b.Testnet = false + b.Network = "livenet" + default: + return errors.Errorf("Unknown network id %v", id) + } + + b.InitAlternativeProviders() + + glog.Info("rpc: block chain ", b.Network) + + return nil +} diff --git a/bchain/coins/bch/bcashparser_test.go b/bchain/coins/bch/bcashparser_test.go index a862f558f9..3aec739606 100644 --- a/bchain/coins/bch/bcashparser_test.go +++ b/bchain/coins/bch/bcashparser_test.go @@ -337,10 +337,15 @@ func Test_UnpackTx(t *testing.T) { 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 } + // ignore witness unpacking + for i := range got.Vin { + got.Vin[i].Witness = nil + } if !reflect.DeepEqual(got, tt.want) { t.Errorf("unpackTx() got = %v, want %v", got, tt.want) } diff --git a/bchain/coins/bellcoin/bellcoinparser_test.go b/bchain/coins/bellcoin/bellcoinparser_test.go index 8ed7e2959b..e747fe0312 100644 --- a/bchain/coins/bellcoin/bellcoinparser_test.go +++ b/bchain/coins/bellcoin/bellcoinparser_test.go @@ -316,6 +316,10 @@ func Test_UnpackTx(t *testing.T) { t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) return } + // ignore witness unpacking + for i := range got.Vin { + got.Vin[i].Witness = nil + } if !reflect.DeepEqual(got, tt.want) { t.Errorf("unpackTx() got = %v, want %v", got, tt.want) } diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index b70fa2355b..65b65d41a4 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -4,18 +4,21 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" "math/big" + "os" "reflect" "time" "github.com/juju/errors" "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/bchain/coins/arbitrum" "github.com/trezor/blockbook/bchain/coins/avalanche" + "github.com/trezor/blockbook/bchain/coins/base" "github.com/trezor/blockbook/bchain/coins/bch" "github.com/trezor/blockbook/bchain/coins/bellcoin" "github.com/trezor/blockbook/bchain/coins/bitcore" "github.com/trezor/blockbook/bchain/coins/bitzeny" + "github.com/trezor/blockbook/bchain/coins/bsc" "github.com/trezor/blockbook/bchain/coins/btc" "github.com/trezor/blockbook/bchain/coins/btg" "github.com/trezor/blockbook/bchain/coins/cpuchain" @@ -41,8 +44,10 @@ import ( "github.com/trezor/blockbook/bchain/coins/namecoin" "github.com/trezor/blockbook/bchain/coins/nuls" "github.com/trezor/blockbook/bchain/coins/omotenashicoin" + "github.com/trezor/blockbook/bchain/coins/optimism" "github.com/trezor/blockbook/bchain/coins/pivx" "github.com/trezor/blockbook/bchain/coins/polis" + "github.com/trezor/blockbook/bchain/coins/polygon" "github.com/trezor/blockbook/bchain/coins/qtum" "github.com/trezor/blockbook/bchain/coins/ravencoin" "github.com/trezor/blockbook/bchain/coins/ritocoin" @@ -64,6 +69,7 @@ var BlockChainFactories = make(map[string]blockChainFactory) func init() { BlockChainFactories["Bitcoin"] = btc.NewBitcoinRPC BlockChainFactories["Testnet"] = btc.NewBitcoinRPC + BlockChainFactories["Testnet4"] = btc.NewBitcoinRPC BlockChainFactories["Signet"] = btc.NewBitcoinRPC BlockChainFactories["Regtest"] = btc.NewBitcoinRPC BlockChainFactories["Zcash"] = zec.NewZCashRPC @@ -71,12 +77,12 @@ func init() { BlockChainFactories["Ethereum"] = eth.NewEthereumRPC BlockChainFactories["Ethereum Archive"] = eth.NewEthereumRPC BlockChainFactories["Ethereum Classic"] = eth.NewEthereumRPC - BlockChainFactories["Ethereum Testnet Ropsten"] = eth.NewEthereumRPC - BlockChainFactories["Ethereum Testnet Ropsten Archive"] = eth.NewEthereumRPC - BlockChainFactories["Ethereum Testnet Goerli"] = eth.NewEthereumRPC - BlockChainFactories["Ethereum Testnet Goerli Archive"] = eth.NewEthereumRPC BlockChainFactories["Ethereum Testnet Sepolia"] = eth.NewEthereumRPC BlockChainFactories["Ethereum Testnet Sepolia Archive"] = eth.NewEthereumRPC + BlockChainFactories["Ethereum Testnet Holesky"] = eth.NewEthereumRPC + BlockChainFactories["Ethereum Testnet Holesky Archive"] = eth.NewEthereumRPC + BlockChainFactories["Ethereum Testnet Hoodi"] = eth.NewEthereumRPC + BlockChainFactories["Ethereum Testnet Hoodi Archive"] = eth.NewEthereumRPC BlockChainFactories["Bcash"] = bch.NewBCashRPC BlockChainFactories["Bcash Testnet"] = bch.NewBCashRPC BlockChainFactories["Bgold"] = btg.NewBGoldRPC @@ -134,29 +140,23 @@ func init() { BlockChainFactories["ECash"] = ecash.NewECashRPC BlockChainFactories["Avalanche"] = avalanche.NewAvalancheRPC BlockChainFactories["Avalanche Archive"] = avalanche.NewAvalancheRPC -} - -// GetCoinNameFromConfig gets coin name and coin shortcut from config file -func GetCoinNameFromConfig(configfile string) (string, string, string, error) { - data, err := ioutil.ReadFile(configfile) - if err != nil { - return "", "", "", errors.Annotatef(err, "Error reading file %v", configfile) - } - var cn struct { - CoinName string `json:"coin_name"` - CoinShortcut string `json:"coin_shortcut"` - CoinLabel string `json:"coin_label"` - } - err = json.Unmarshal(data, &cn) - if err != nil { - return "", "", "", errors.Annotatef(err, "Error parsing file %v", configfile) - } - return cn.CoinName, cn.CoinShortcut, cn.CoinLabel, nil + BlockChainFactories["BNB Smart Chain"] = bsc.NewBNBSmartChainRPC + BlockChainFactories["BNB Smart Chain Archive"] = bsc.NewBNBSmartChainRPC + BlockChainFactories["Polygon"] = polygon.NewPolygonRPC + BlockChainFactories["Polygon Archive"] = polygon.NewPolygonRPC + BlockChainFactories["Optimism"] = optimism.NewOptimismRPC + BlockChainFactories["Optimism Archive"] = optimism.NewOptimismRPC + BlockChainFactories["Arbitrum"] = arbitrum.NewArbitrumRPC + BlockChainFactories["Arbitrum Archive"] = arbitrum.NewArbitrumRPC + BlockChainFactories["Arbitrum Nova"] = arbitrum.NewArbitrumRPC + BlockChainFactories["Arbitrum Nova Archive"] = arbitrum.NewArbitrumRPC + BlockChainFactories["Base"] = base.NewBaseRPC + BlockChainFactories["Base Archive"] = base.NewBaseRPC } // NewBlockChain creates bchain.BlockChain and bchain.Mempool for the coin passed by the parameter coin func NewBlockChain(coin string, configfile string, pushHandler func(bchain.NotificationType), metrics *common.Metrics) (bchain.BlockChain, bchain.Mempool, error) { - data, err := ioutil.ReadFile(configfile) + data, err := os.ReadFile(configfile) if err != nil { return nil, nil, errors.Annotatef(err, "Error reading file %v", configfile) } @@ -299,9 +299,14 @@ func (c *blockChainWithMetrics) EstimateFee(blocks int) (v big.Int, err error) { return c.b.EstimateFee(blocks) } -func (c *blockChainWithMetrics) SendRawTransaction(tx string) (v string, err error) { +func (c *blockChainWithMetrics) LongTermFeeRate() (v *bchain.LongTermFeeRate, err error) { + defer func(s time.Time) { c.observeRPCLatency("LongTermFeeRate", s, err) }(time.Now()) + return c.b.LongTermFeeRate() +} + +func (c *blockChainWithMetrics) SendRawTransaction(tx string, disableAlternativeRPC bool) (v string, err error) { defer func(s time.Time) { c.observeRPCLatency("SendRawTransaction", s, err) }(time.Now()) - return c.b.SendRawTransaction(tx) + return c.b.SendRawTransaction(tx, disableAlternativeRPC) } func (c *blockChainWithMetrics) GetMempoolEntry(txid string) (v *bchain.MempoolEntry, err error) { @@ -328,6 +333,11 @@ func (c *blockChainWithMetrics) EthereumTypeEstimateGas(params map[string]interf return c.b.EthereumTypeEstimateGas(params) } +func (c *blockChainWithMetrics) EthereumTypeGetEip1559Fees() (v *bchain.Eip1559Fees, err error) { + defer func(s time.Time) { c.observeRPCLatency("EthereumTypeGetEip1559Fees", s, err) }(time.Now()) + return c.b.EthereumTypeGetEip1559Fees() +} + func (c *blockChainWithMetrics) GetContractInfo(contractDesc bchain.AddressDescriptor) (v *bchain.ContractInfo, err error) { defer func(s time.Time) { c.observeRPCLatency("GetContractInfo", s, err) }(time.Now()) return c.b.GetContractInfo(contractDesc) @@ -338,12 +348,32 @@ func (c *blockChainWithMetrics) EthereumTypeGetErc20ContractBalance(addrDesc, co return c.b.EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc) } -// GetContractInfo returns URI of non fungible or multi token defined by token id +// GetTokenURI returns URI of non fungible or multi token defined by token id func (c *blockChainWithMetrics) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID *big.Int) (v string, err error) { defer func(s time.Time) { c.observeRPCLatency("GetTokenURI", s, err) }(time.Now()) return c.b.GetTokenURI(contractDesc, tokenID) } +func (c *blockChainWithMetrics) EthereumTypeGetSupportedStakingPools() []string { + return c.b.EthereumTypeGetSupportedStakingPools() +} + +func (c *blockChainWithMetrics) EthereumTypeGetStakingPoolsData(addrDesc bchain.AddressDescriptor) (v []bchain.StakingPoolData, err error) { + defer func(s time.Time) { c.observeRPCLatency("EthereumTypeStakingPoolsData", s, err) }(time.Now()) + return c.b.EthereumTypeGetStakingPoolsData(addrDesc) +} + +// EthereumTypeRpcCall calls eth_call with given data and to address +func (c *blockChainWithMetrics) EthereumTypeRpcCall(data, to, from string) (v string, err error) { + defer func(s time.Time) { c.observeRPCLatency("EthereumTypeRpcCall", s, err) }(time.Now()) + return c.b.EthereumTypeRpcCall(data, to, from) +} + +func (c *blockChainWithMetrics) EthereumTypeGetRawTransaction(txid string) (v string, err error) { + defer func(s time.Time) { c.observeRPCLatency("EthereumTypeGetRawTransaction", s, err) }(time.Now()) + return c.b.EthereumTypeGetRawTransaction(txid) +} + type mempoolWithMetrics struct { mempool bchain.Mempool m *common.Metrics @@ -384,3 +414,7 @@ func (c *mempoolWithMetrics) GetAllEntries() (v bchain.MempoolTxidEntries) { func (c *mempoolWithMetrics) GetTransactionTime(txid string) uint32 { return c.mempool.GetTransactionTime(txid) } + +func (c *mempoolWithMetrics) GetTxidFilterEntries(filterScripts string, fromTimestamp uint32) (bchain.MempoolTxidFilterEntries, error) { + return c.mempool.GetTxidFilterEntries(filterScripts, fromTimestamp) +} diff --git a/bchain/coins/bsc/bscrpc.go b/bchain/coins/bsc/bscrpc.go new file mode 100644 index 0000000000..2439e57cf5 --- /dev/null +++ b/bchain/coins/bsc/bscrpc.go @@ -0,0 +1,84 @@ +package bsc + +import ( + "context" + "encoding/json" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/bchain/coins/eth" +) + +const ( + // MainNet is production network + MainNet eth.Network = 56 + + // bsc token standard names + BEP20TokenStandard bchain.TokenStandardName = "BEP20" + BEP721TokenStandard bchain.TokenStandardName = "BEP721" + BEP1155TokenStandard bchain.TokenStandardName = "BEP1155" +) + +// BNBSmartChainRPC is an interface to JSON-RPC bsc service. +type BNBSmartChainRPC struct { + *eth.EthereumRPC +} + +// NewBNBSmartChainRPC returns new BNBSmartChainRPC instance. +func NewBNBSmartChainRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { + c, err := eth.NewEthereumRPC(config, pushHandler) + if err != nil { + return nil, err + } + + // overwrite EthereumTokenStandardMap with bsc specific token standard names + bchain.EthereumTokenStandardMap = []bchain.TokenStandardName{BEP20TokenStandard, BEP721TokenStandard, BEP1155TokenStandard} + + s := &BNBSmartChainRPC{ + EthereumRPC: c.(*eth.EthereumRPC), + } + s.Parser.EnsSuffix = ".bnb" + + return s, nil +} + +// Initialize bnb smart chain rpc interface +func (b *BNBSmartChainRPC) Initialize() error { + b.OpenRPC = eth.OpenRPC + + rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL) + if err != nil { + return err + } + + // set chain specific + b.Client = ec + b.RPC = rc + b.MainNetChainID = MainNet + b.NewBlock = eth.NewEthereumNewBlock() + b.NewTx = eth.NewEthereumNewTx() + + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) + defer cancel() + + id, err := b.Client.NetworkID(ctx) + if err != nil { + return err + } + + // parameters for getInfo request + switch eth.Network(id.Uint64()) { + case MainNet: + b.Testnet = false + b.Network = "livenet" + default: + return errors.Errorf("Unknown network id %v", id) + } + + b.InitAlternativeProviders() + + glog.Info("rpc: block chain ", b.Network) + + return nil +} diff --git a/bchain/coins/btc/alternativefeeprovider.go b/bchain/coins/btc/alternativefeeprovider.go new file mode 100644 index 0000000000..7d72acdbdd --- /dev/null +++ b/bchain/coins/btc/alternativefeeprovider.go @@ -0,0 +1,75 @@ +package btc + +import ( + "fmt" + "math/big" + "sync" + "time" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" +) + +type alternativeFeeProviderFee struct { + blocks int + feePerKB int +} + +type alternativeFeeProvider struct { + fees []alternativeFeeProviderFee + lastSync time.Time + chain bchain.BlockChain + mux sync.Mutex + fallbackFeePerKBIfNotAvailable int +} + +type alternativeFeeProviderInterface interface { + compareToDefault() + estimateFee(blocks int) (big.Int, error) +} + +func (p *alternativeFeeProvider) compareToDefault() { + output := "" + for _, fee := range p.fees { + conservative, err := p.chain.(*BitcoinRPC).blockchainEstimateSmartFee(fee.blocks, true) + if err != nil { + glog.Error(err) + return + } + economical, err := p.chain.(*BitcoinRPC).blockchainEstimateSmartFee(fee.blocks, false) + if err != nil { + glog.Error(err) + return + } + output += fmt.Sprintf("Blocks %d: alternative %d, conservative %s, economical %s\n", fee.blocks, fee.feePerKB, conservative.String(), economical.String()) + } + glog.Info("alternativeFeeProviderCompareToDefault\n", output) +} + +func (p *alternativeFeeProvider) estimateFee(blocks int) (big.Int, error) { + var r big.Int + p.mux.Lock() + defer p.mux.Unlock() + if len(p.fees) == 0 { + return r, errors.New("alternativeFeeProvider: no fees") + } + if p.lastSync.Before(time.Now().Add(time.Duration(-10) * time.Minute)) { + return r, errors.Errorf("alternativeFeeProvider: Missing recent value, last sync at %v", p.lastSync) + } + for i := range p.fees { + if p.fees[i].blocks >= blocks { + r = *big.NewInt(int64(p.fees[i].feePerKB)) + return r, nil + } + } + + if p.fallbackFeePerKBIfNotAvailable > 0 { + r = *big.NewInt(int64(p.fallbackFeePerKBIfNotAvailable)) + return r, nil + } + + // use the last value as fallback + r = *big.NewInt(int64(p.fees[len(p.fees)-1].feePerKB)) + return r, nil +} diff --git a/bchain/coins/btc/bitcoinlikeparser.go b/bchain/coins/btc/bitcoinlikeparser.go index ba99d6fecc..67a31d0c57 100644 --- a/bchain/coins/btc/bitcoinlikeparser.go +++ b/bchain/coins/btc/bitcoinlikeparser.go @@ -231,6 +231,7 @@ func (p *BitcoinLikeParser) TxFromMsgTx(t *wire.MsgTx, parseAddresses bool) bcha Vout: in.PreviousOutPoint.Index, Sequence: in.Sequence, ScriptSig: s, + Witness: in.Witness, } } vout := make([]bchain.Vout, len(t.TxOut)) @@ -439,7 +440,7 @@ var ( ) 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+))/\*)?\)+`) + xpubDesriptorRegex, _ = regexp.Compile(`^(?P(sh\(wpkh|wpkh|pk|pkh|wpkh|wsh|tr))\((\[\w+/(?P\d+)['h]/\d+['h]?/\d+['h]?\])?(?P\w+)(/(({(?P\d+(,\d+)*)})|(<(?P\d+(;\d+)*)>)|(?P\d+))/\*)?\)+`) typeSubexpIndex = xpubDesriptorRegex.SubexpIndex("type") bipSubexpIndex = xpubDesriptorRegex.SubexpIndex("bip") xpubSubexpIndex = xpubDesriptorRegex.SubexpIndex("xpub") diff --git a/bchain/coins/btc/bitcoinparser.go b/bchain/coins/btc/bitcoinparser.go index 77cbcf267e..a022c18e0d 100644 --- a/bchain/coins/btc/bitcoinparser.go +++ b/bchain/coins/btc/bitcoinparser.go @@ -4,11 +4,28 @@ import ( "encoding/json" "math/big" + "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil/chaincfg" "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/common" ) +// temp params for signet(wait btcd commit) +// magic numbers +const ( + Testnet4Magic wire.BitcoinNet = 0x283f161c +) + +// chain parameters +var ( + TestNet4Params chaincfg.Params +) + +func init() { + TestNet4Params = chaincfg.TestNet3Params + TestNet4Params.Net = Testnet4Magic +} + // BitcoinParser handle type BitcoinParser struct { *BitcoinLikeParser @@ -33,6 +50,8 @@ func GetChainParams(chain string) *chaincfg.Params { switch chain { case "test": return &chaincfg.TestNet3Params + case "testnet4": + return &TestNet4Params case "regtest": return &chaincfg.RegressionNetParams case "signet": diff --git a/bchain/coins/btc/bitcoinparser_test.go b/bchain/coins/btc/bitcoinparser_test.go index 201d7ca9e5..78d5ac4fbb 100644 --- a/bchain/coins/btc/bitcoinparser_test.go +++ b/bchain/coins/btc/bitcoinparser_test.go @@ -467,11 +467,12 @@ func TestGetAddressesFromAddrDescTestnet(t *testing.T) { } var ( - testTx1, testTx2, testTx3 bchain.Tx + testTx1, testTx2, testTx3, testTx4 bchain.Tx testTxPacked1 = "0001e2408ba8d7af5401000000017f9a22c9cbf54bd902400df746f138f37bcf5b4d93eb755820e974ba43ed5f42040000006a4730440220037f4ed5427cde81d55b9b6a2fd08c8a25090c2c2fff3a75c1a57625ca8a7118022076c702fe55969fa08137f71afd4851c48e31082dd3c40c919c92cdbc826758d30121029f6da5623c9f9b68a9baf9c1bc7511df88fa34c6c2f71f7c62f2f03ff48dca80feffffff019c9700000000000017a9146144d57c8aff48492c9dfb914e120b20bad72d6f8773d00700" testTxPacked2 = "0007c91a899ab7da6a010000000001019d64f0c72a0d206001decbffaa722eb1044534c74eee7a5df8318e42a4323ec10000000017160014550da1f5d25a9dae2eafd6902b4194c4c6500af6ffffffff02809698000000000017a914cd668d781ece600efa4b2404dc91fd26b8b8aed8870553d7360000000017a914246655bdbd54c7e477d0ea2375e86e0db2b8f80a8702473044022076aba4ad559616905fa51d4ddd357fc1fdb428d40cb388e042cdd1da4a1b7357022011916f90c712ead9a66d5f058252efd280439ad8956a967e95d437d246710bc9012102a80a5964c5612bb769ef73147b2cf3c149bc0fd4ecb02f8097629c94ab013ffd00000000" testTxPacked3 = "00003d818bfda9aa3e02000000000102deb1999a857ab0a13d6b12fbd95ea75b409edde5f2ff747507ce42d9986a8b9d0000000000fdffffff9fd2d3361e203b2375eba6438efbef5b3075531e7e583c7cc76b7294fe7f22980000000000fdffffff02a0860100000000001600148091746745464e7555c31e9a5afceac14a02978ae7fc1c0000000000160014565ea9ff4589d3e05ba149ae6e257752bfdc2a1e0247304402207d67d320a8e813f986b35e9791935fcb736754812b7038686f5de6cfdcda99cd02201c3bb2c178e0056016437ecfe365a7eef84aa9d293ebdc566177af82e22fcdd3012103abb30c1bbe878b07b58dc169b1d061d48c60be8107f632a59778b38bf7ceea5a02473044022044f54a478cfe086e870cb026c9dcd4e14e63778bef569a4d55a6332725cd9a9802202f0e94c04e6f328fc64ad9efe552888c299750d1b8d033324825a3ff29920e030121036fcd433428aa7dc65c4f5408fa31f208c54fe4b4c6c1ae9c39a825ed4f1ac039813d0000" + testTxPacked4 = "0000a2b98ced82b6400300000000010148f8f93ebb12407809920d2ab9cc1bf01289b314eb23028c83fdab21e5fefa690100000000fdffffff0150c3000000000000160014cb888de3c89670a3061fb6ef6590f187649cca060247304402206a9db8d7157e4b0a06a1f090b9de88cdc616028b431b80617a055117877e479a02202937d6d1658d4a8afde86b245325c3bb0e769a87cb09d802bcefaa21550065e201210374aa8f312de4ebccbef55609700a39764387aa4ff5d76f1ccb4d2382e454f05b00000000" ) func init() { @@ -595,6 +596,37 @@ func init() { }, }, } + + testTx4 = bchain.Tx{ + Hex: "0300000000010148f8f93ebb12407809920d2ab9cc1bf01289b314eb23028c83fdab21e5fefa690100000000fdffffff0150c3000000000000160014cb888de3c89670a3061fb6ef6590f187649cca060247304402206a9db8d7157e4b0a06a1f090b9de88cdc616028b431b80617a055117877e479a02202937d6d1658d4a8afde86b245325c3bb0e769a87cb09d802bcefaa21550065e201210374aa8f312de4ebccbef55609700a39764387aa4ff5d76f1ccb4d2382e454f05b00000000", + Blocktime: 1724927392, + Txid: "8e3f38bf6854dd3c358be8d4f9a40a6dccc50de49616125d27af9fdbe65287eb", + LockTime: 0, + VSize: 110, + Version: 3, + Vin: []bchain.Vin{ + { + ScriptSig: bchain.ScriptSig{ + Hex: "", + }, + Txid: "69fafee521abfd838c0223eb14b38912f01bccb92a0d9209784012bb3ef9f848", + Vout: 1, + Sequence: 4294967293, + }, + }, + Vout: []bchain.Vout{ + { + ValueSat: *big.NewInt(50000), + N: 0, + ScriptPubKey: bchain.ScriptPubKey{ + Hex: "0014cb888de3c89670a3061fb6ef6590f187649cca06", + Addresses: []string{ + "tb1qewygmc7gjec2xpslkmhkty83sajfejsxqmy5dq", + }, + }, + }, + }, + } } func TestPackTx(t *testing.T) { @@ -643,6 +675,17 @@ func TestPackTx(t *testing.T) { want: testTxPacked3, wantErr: false, }, + { + name: "testnet4-1", + args: args{ + tx: testTx4, + height: 41657, + blockTime: 1724927392, + parser: NewBitcoinParser(GetChainParams("testnet4"), &Configuration{}), + }, + want: testTxPacked4, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -701,6 +744,16 @@ func TestUnpackTx(t *testing.T) { want1: 15745, wantErr: false, }, + { + name: "testnet4-1", + args: args{ + packedTx: testTxPacked4, + parser: NewBitcoinParser(GetChainParams("testnet4"), &Configuration{}), + }, + want: &testTx4, + want1: 41657, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -710,6 +763,10 @@ func TestUnpackTx(t *testing.T) { t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) return } + // ignore witness unpacking + for i := range got.Vin { + got.Vin[i].Witness = nil + } if !reflect.DeepEqual(got, tt.want) { t.Errorf("unpackTx() got = %v, want %v", got, tt.want) } @@ -766,6 +823,18 @@ func TestParseXpubDescriptors(t *testing.T) { ChangeIndexes: []uint32{0, 1, 2}, }, }, + { + name: "tr([5c9e228d/86h/1h/0h]tpubD/{0,1,2}/*)#4rqwxvej", + xpub: "tr([5c9e228d/86h/1h/0h]tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1,2}/*)#4rqwxvej", + parser: btcTestnetParser, + want: &bchain.XpubDescriptor{ + XpubDescriptor: "tr([5c9e228d/86h/1h/0h]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", diff --git a/bchain/coins/btc/bitcoinrpc.go b/bchain/coins/btc/bitcoinrpc.go index b2618a0364..f6daead229 100644 --- a/bchain/coins/btc/bitcoinrpc.go +++ b/bchain/coins/btc/bitcoinrpc.go @@ -5,12 +5,9 @@ import ( "context" "encoding/hex" "encoding/json" - "io" - "io/ioutil" "math/big" "net" "net/http" - "runtime/debug" "time" "github.com/golang/glog" @@ -23,16 +20,20 @@ import ( // BitcoinRPC is an interface to JSON-RPC bitcoind service. type BitcoinRPC struct { *bchain.BaseChain - client http.Client - rpcURL string - user string - password string - Mempool *bchain.MempoolBitcoinType - ParseBlocks bool - pushHandler func(bchain.NotificationType) - mq *bchain.MQ - ChainConfig *Configuration - RPCMarshaler RPCMarshaler + client http.Client + rpcURL string + user string + password string + Mempool *bchain.MempoolBitcoinType + ParseBlocks bool + pushHandler func(bchain.NotificationType) + mq *bchain.MQ + ChainConfig *Configuration + RPCMarshaler RPCMarshaler + mempoolGolombFilterP uint8 + mempoolFilterScripts string + mempoolUseZeroedKey bool + alternativeFeeProvider alternativeFeeProviderInterface } // Configuration represents json config file @@ -60,6 +61,9 @@ type Configuration struct { AlternativeEstimateFee string `json:"alternative_estimate_fee,omitempty"` AlternativeEstimateFeeParams string `json:"alternative_estimate_fee_params,omitempty"` MinimumCoinbaseConfirmations int `json:"minimumCoinbaseConfirmations,omitempty"` + MempoolGolombFilterP uint8 `json:"mempool_golomb_filter_p,omitempty"` + MempoolFilterScripts string `json:"mempool_filter_scripts,omitempty"` + MempoolFilterUseZeroedKey bool `json:"mempool_filter_use_zeroed_key,omitempty"` } // NewBitcoinRPC returns new BitcoinRPC instance. @@ -96,15 +100,18 @@ func NewBitcoinRPC(config json.RawMessage, pushHandler func(bchain.NotificationT } s := &BitcoinRPC{ - BaseChain: &bchain.BaseChain{}, - client: http.Client{Timeout: time.Duration(c.RPCTimeout) * time.Second, Transport: transport}, - rpcURL: c.RPCURL, - user: c.RPCUser, - password: c.RPCPass, - ParseBlocks: c.Parse, - ChainConfig: &c, - pushHandler: pushHandler, - RPCMarshaler: JSONMarshalerV2{}, + BaseChain: &bchain.BaseChain{}, + client: http.Client{Timeout: time.Duration(c.RPCTimeout) * time.Second, Transport: transport}, + rpcURL: c.RPCURL, + user: c.RPCUser, + password: c.RPCPass, + ParseBlocks: c.Parse, + ChainConfig: &c, + pushHandler: pushHandler, + RPCMarshaler: JSONMarshalerV2{}, + mempoolGolombFilterP: c.MempoolGolombFilterP, + mempoolFilterScripts: c.MempoolFilterScripts, + mempoolUseZeroedKey: c.MempoolFilterUseZeroedKey, } return s, nil @@ -137,11 +144,30 @@ func (b *BitcoinRPC) Initialize() error { glog.Info("rpc: block chain ", params.Name) if b.ChainConfig.AlternativeEstimateFee == "whatthefee" { - if err = InitWhatTheFee(b, b.ChainConfig.AlternativeEstimateFeeParams); err != nil { - glog.Error("InitWhatTheFee error ", err, " Reverting to default estimateFee functionality") + glog.Info("Using WhatTheFee") + if b.alternativeFeeProvider, err = NewWhatTheFee(b, b.ChainConfig.AlternativeEstimateFeeParams); err != nil { + glog.Error("NewWhatTheFee error ", err, " Reverting to default estimateFee functionality") // disable AlternativeEstimateFee logic - b.ChainConfig.AlternativeEstimateFee = "" + b.alternativeFeeProvider = nil } + } else if b.ChainConfig.AlternativeEstimateFee == "mempoolspace" { + glog.Info("Using MempoolSpaceFee") + if b.alternativeFeeProvider, err = NewMempoolSpaceFee(b, b.ChainConfig.AlternativeEstimateFeeParams); err != nil { + glog.Error("MempoolSpaceFee error ", err, " Reverting to default estimateFee functionality") + // disable AlternativeEstimateFee logic + b.alternativeFeeProvider = nil + } + } else if b.ChainConfig.AlternativeEstimateFee == "mempoolspaceblock" { + glog.Info("Using MempoolSpaceBlockFee") + if b.alternativeFeeProvider, err = NewMempoolSpaceBlockFee(b, b.ChainConfig.AlternativeEstimateFeeParams); err != nil { + glog.Error("MempoolSpaceBlockFee error ", err, " Reverting to default estimateFee functionality") + // disable AlternativeEstimateFee logic + b.alternativeFeeProvider = nil + } + } else if len(b.ChainConfig.AlternativeEstimateFee) > 0 { + glog.Error("AlternativeEstimateFee ", b.ChainConfig.AlternativeEstimateFee, " not supported") + } else { + glog.Info("Using default estimateFee") } return nil @@ -150,7 +176,7 @@ func (b *BitcoinRPC) Initialize() error { // CreateMempool creates mempool if not already created, however does not initialize it func (b *BitcoinRPC) CreateMempool(chain bchain.BlockChain) (bchain.Mempool, error) { if b.Mempool == nil { - b.Mempool = bchain.NewMempoolBitcoinType(chain, b.ChainConfig.MempoolWorkers, b.ChainConfig.MempoolSubWorkers) + b.Mempool = bchain.NewMempoolBitcoinType(chain, b.ChainConfig.MempoolWorkers, b.ChainConfig.MempoolSubWorkers, b.mempoolGolombFilterP, b.mempoolFilterScripts, b.mempoolUseZeroedKey) } return b.Mempool, nil } @@ -766,8 +792,7 @@ func (b *BitcoinRPC) getRawTransaction(txid string) (json.RawMessage, error) { return res.Result, nil } -// EstimateSmartFee returns fee estimation -func (b *BitcoinRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, error) { +func (b *BitcoinRPC) blockchainEstimateSmartFee(blocks int, conservative bool) (big.Int, error) { // use EstimateFee if EstimateSmartFee is not supported if !b.ChainConfig.SupportsEstimateSmartFee && b.ChainConfig.SupportsEstimateFee { return b.EstimateFee(blocks) @@ -784,7 +809,6 @@ func (b *BitcoinRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, e req.Params.EstimateMode = "ECONOMICAL" } err := b.Call(&req, &res) - var r big.Int if err != nil { return r, err @@ -799,8 +823,31 @@ func (b *BitcoinRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, e return r, nil } +// EstimateSmartFee returns fee estimation +func (b *BitcoinRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, error) { + // use alternative estimator if enabled + if b.alternativeFeeProvider != nil { + r, err := b.alternativeFeeProvider.estimateFee(blocks) + // in case of error, fallback to default estimator + if err == nil { + return r, nil + } + } + return b.blockchainEstimateSmartFee(blocks, conservative) +} + // EstimateFee returns fee estimation. func (b *BitcoinRPC) EstimateFee(blocks int) (big.Int, error) { + var r big.Int + var err error + // use alternative estimator if enabled + if b.alternativeFeeProvider != nil { + r, err = b.alternativeFeeProvider.estimateFee(blocks) + // in case of error, fallback to default estimator + if err == nil { + return r, nil + } + } // use EstimateSmartFee if EstimateFee is not supported if !b.ChainConfig.SupportsEstimateFee && b.ChainConfig.SupportsEstimateSmartFee { return b.EstimateSmartFee(blocks, true) @@ -811,9 +858,8 @@ func (b *BitcoinRPC) EstimateFee(blocks int) (big.Int, error) { res := ResEstimateFee{} req := CmdEstimateFee{Method: "estimatefee"} req.Params.Blocks = blocks - err := b.Call(&req, &res) + err = b.Call(&req, &res) - var r big.Int if err != nil { return r, err } @@ -827,8 +873,23 @@ func (b *BitcoinRPC) EstimateFee(blocks int) (big.Int, error) { return r, nil } +// LongTermFeeRate returns smallest fee rate from historic blocks. +func (b *BitcoinRPC) LongTermFeeRate() (*bchain.LongTermFeeRate, error) { + blocks := 1008 // ~7 days of blocks, highest number estimatesmartfee supports + glog.V(1).Info("rpc: estimatesmartfee (long term fee rate) - ", blocks) + // Going for the ECONOMICAL mode, to get the lowest fee rate + feePerUnit, err := b.blockchainEstimateSmartFee(blocks, false) + if err != nil { + return nil, err + } + return &bchain.LongTermFeeRate{ + Blocks: uint64(blocks), + FeePerUnit: feePerUnit, + }, nil +} + // SendRawTransaction sends raw transaction -func (b *BitcoinRPC) SendRawTransaction(tx string) (string, error) { +func (b *BitcoinRPC) SendRawTransaction(tx string, disableAlternativeRPC bool) (string, error) { glog.V(1).Info("rpc: sendrawtransaction") res := ResSendRawTransaction{} @@ -872,26 +933,6 @@ func (b *BitcoinRPC) GetMempoolEntry(txid string) (*bchain.MempoolEntry, error) return res.Result, nil } -func safeDecodeResponse(body io.ReadCloser, res interface{}) (err error) { - var data []byte - defer func() { - if r := recover(); r != nil { - glog.Error("unmarshal json recovered from panic: ", r, "; data: ", string(data)) - debug.PrintStack() - if len(data) > 0 && len(data) < 2048 { - err = errors.Errorf("Error: %v", string(data)) - } else { - err = errors.New("Internal error") - } - } - }() - data, err = ioutil.ReadAll(body) - if err != nil { - return err - } - return json.Unmarshal(data, &res) -} - // Call calls Backend RPC interface, using RPCMarshaler interface to marshall the request func (b *BitcoinRPC) Call(req interface{}, res interface{}) error { httpData, err := b.RPCMarshaler.Marshal(req) @@ -915,11 +956,11 @@ func (b *BitcoinRPC) Call(req interface{}, res interface{}) error { // if server returns HTTP error code it might not return json with response // handle both cases if httpRes.StatusCode != 200 { - err = safeDecodeResponse(httpRes.Body, &res) + err = common.SafeDecodeResponseFromReader(httpRes.Body, &res) if err != nil { return errors.Errorf("%v %v", httpRes.Status, err) } return nil } - return safeDecodeResponse(httpRes.Body, &res) + return common.SafeDecodeResponseFromReader(httpRes.Body, &res) } diff --git a/bchain/coins/btc/mempoolspace.go b/bchain/coins/btc/mempoolspace.go new file mode 100644 index 0000000000..6de71db2d1 --- /dev/null +++ b/bchain/coins/btc/mempoolspace.go @@ -0,0 +1,136 @@ +package btc + +import ( + "bytes" + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/common" +) + +// https://mempool.space/api/v1/fees/recommended returns +// {"fastestFee":41,"halfHourFee":39,"hourFee":36,"economyFee":36,"minimumFee":20} + +type mempoolSpaceFeeResult struct { + FastestFee int `json:"fastestFee"` + HalfHourFee int `json:"halfHourFee"` + HourFee int `json:"hourFee"` + EconomyFee int `json:"economyFee"` + MinimumFee int `json:"minimumFee"` +} + +type mempoolSpaceFeeParams struct { + URL string `json:"url"` + PeriodSeconds int `json:"periodSeconds"` +} + +type mempoolSpaceFeeProvider struct { + *alternativeFeeProvider + params mempoolSpaceFeeParams +} + +// NewMempoolSpaceFee initializes https://mempool.space provider +func NewMempoolSpaceFee(chain bchain.BlockChain, params string) (alternativeFeeProviderInterface, error) { + p := &mempoolSpaceFeeProvider{alternativeFeeProvider: &alternativeFeeProvider{}} + err := json.Unmarshal([]byte(params), &p.params) + if err != nil { + return nil, err + } + if p.params.URL == "" || p.params.PeriodSeconds == 0 { + return nil, errors.New("NewMempoolSpaceFee: Missing parameters") + } + p.chain = chain + go p.mempoolSpaceFeeDownloader() + return p, nil +} + +func (p *mempoolSpaceFeeProvider) mempoolSpaceFeeDownloader() { + period := time.Duration(p.params.PeriodSeconds) * time.Second + timer := time.NewTimer(period) + counter := 0 + for { + var data mempoolSpaceFeeResult + err := p.mempoolSpaceFeeGetData(&data) + if err != nil { + glog.Error("mempoolSpaceFeeGetData ", err) + } else { + if p.mempoolSpaceFeeProcessData(&data) { + if counter%60 == 0 { + p.compareToDefault() + } + counter++ + } + } + <-timer.C + timer.Reset(period) + } +} + +func (p *mempoolSpaceFeeProvider) mempoolSpaceFeeProcessData(data *mempoolSpaceFeeResult) bool { + if data.MinimumFee == 0 || data.EconomyFee == 0 || data.HourFee == 0 || data.HalfHourFee == 0 || data.FastestFee == 0 { + glog.Errorf("mempoolSpaceFeeProcessData: invalid data %+v", data) + return false + } + p.mux.Lock() + defer p.mux.Unlock() + p.fees = make([]alternativeFeeProviderFee, 5) + // map mempoool.space fees to blocks + + // FastestFee is for 1 block + p.fees[0] = alternativeFeeProviderFee{ + blocks: 1, + feePerKB: data.FastestFee * 1000, + } + + // HalfHourFee is for 2-6 blocks + p.fees[1] = alternativeFeeProviderFee{ + blocks: 6, + feePerKB: data.HalfHourFee * 1000, + } + + // HourFee is for 7-36 blocks + p.fees[2] = alternativeFeeProviderFee{ + blocks: 36, + feePerKB: data.HourFee * 1000, + } + + // EconomyFee is for 37-200 blocks + p.fees[3] = alternativeFeeProviderFee{ + blocks: 500, + feePerKB: data.EconomyFee * 1000, + } + + // MinimumFee is for over 500 blocks + p.fees[4] = alternativeFeeProviderFee{ + blocks: 1000, + feePerKB: data.MinimumFee * 1000, + } + + p.lastSync = time.Now() + // glog.Infof("mempoolSpaceFees: %+v", p.fees) + return true +} + +func (p *mempoolSpaceFeeProvider) mempoolSpaceFeeGetData(res interface{}) error { + var httpData []byte + httpReq, err := http.NewRequest("GET", p.params.URL, bytes.NewBuffer(httpData)) + if err != nil { + return err + } + httpRes, err := http.DefaultClient.Do(httpReq) + if httpRes != nil { + defer httpRes.Body.Close() + } + if err != nil { + return err + } + if httpRes.StatusCode != http.StatusOK { + return errors.New(p.params.URL + " returned status " + strconv.Itoa(httpRes.StatusCode)) + } + return common.SafeDecodeResponseFromReader(httpRes.Body, &res) +} diff --git a/bchain/coins/btc/mempoolspace_test.go b/bchain/coins/btc/mempoolspace_test.go new file mode 100644 index 0000000000..d27d1250b0 --- /dev/null +++ b/bchain/coins/btc/mempoolspace_test.go @@ -0,0 +1,53 @@ +package btc + +import ( + "math/big" + "strconv" + "testing" +) + +func Test_mempoolSpaceFeeProvider(t *testing.T) { + m := &mempoolSpaceFeeProvider{alternativeFeeProvider: &alternativeFeeProvider{}} + m.mempoolSpaceFeeProcessData(&mempoolSpaceFeeResult{ + MinimumFee: 10, + EconomyFee: 20, + HourFee: 30, + HalfHourFee: 40, + FastestFee: 50, + }) + + tests := []struct { + blocks int + want big.Int + }{ + {0, *big.NewInt(50000)}, + {1, *big.NewInt(50000)}, + {2, *big.NewInt(40000)}, + {5, *big.NewInt(40000)}, + {6, *big.NewInt(40000)}, + {7, *big.NewInt(30000)}, + {10, *big.NewInt(30000)}, + {18, *big.NewInt(30000)}, + {19, *big.NewInt(30000)}, + {36, *big.NewInt(30000)}, + {37, *big.NewInt(20000)}, + {100, *big.NewInt(20000)}, + {101, *big.NewInt(20000)}, + {200, *big.NewInt(20000)}, + {201, *big.NewInt(20000)}, + {500, *big.NewInt(20000)}, + {501, *big.NewInt(10000)}, + {5000000, *big.NewInt(10000)}, + } + for _, tt := range tests { + t.Run(strconv.Itoa(tt.blocks), func(t *testing.T) { + got, err := m.estimateFee(tt.blocks) + if err != nil { + t.Error("estimateFee returned error ", err) + } + if got.Cmp(&tt.want) != 0 { + t.Errorf("estimateFee(%d) = %v, want %v", tt.blocks, got, tt.want) + } + }) + } +} diff --git a/bchain/coins/btc/mempoolspaceblock.go b/bchain/coins/btc/mempoolspaceblock.go new file mode 100644 index 0000000000..1f7d4226d5 --- /dev/null +++ b/bchain/coins/btc/mempoolspaceblock.go @@ -0,0 +1,201 @@ +package btc + +import ( + "encoding/json" + "math" + "net/http" + "strconv" + "time" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/common" +) + +// https://mempool.space/api/v1/fees/mempool-blocks returns a list of upcoming blocks and their medianFee. +// Example response: +// [ +// { +// "blockSize": 1604493, +// "blockVSize": 997944.75, +// "nTx": 3350, +// "totalFees": 8333539, +// "medianFee": 3.0073509137538332, +// "feeRange": [ +// 2.0444444444444443, +// 2.2135922330097086, +// 2.608695652173913, +// 3.016042780748663, +// 4.0048289738430585, +// 9.27631139325092, +// 201.06951871657753 +// ] +// }, +// ... +// ] + +type mempoolSpaceBlockFeeResult struct { + BlockSize float64 `json:"blockSize"` + BlockVSize float64 `json:"blockVSize"` + NTx int `json:"nTx"` + TotalFees int `json:"totalFees"` + MedianFee float64 `json:"medianFee"` + // 2nd, 10th, 25th, 50th, 75th, 90th, 98th percentiles + FeeRange []float64 `json:"feeRange"` +} + +type mempoolSpaceBlockFeeParams struct { + URL string `json:"url"` + PeriodSeconds int `json:"periodSeconds"` + // Either number, then take the specified index. If null or missing, take the medianFee + FeeRangeIndex *int `json:"feeRangeIndex,omitempty"` + FallbackFeePerKB int `json:"fallbackFeePerKB,omitempty"` +} + +type mempoolSpaceBlockFeeProvider struct { + *alternativeFeeProvider + params mempoolSpaceBlockFeeParams +} + +// NewMempoolSpaceBlockFee initializes the provider completely. +func NewMempoolSpaceBlockFee(chain bchain.BlockChain, params string) (alternativeFeeProviderInterface, error) { + var paramsParsed mempoolSpaceBlockFeeParams + err := json.Unmarshal([]byte(params), ¶msParsed) + if err != nil { + return nil, err + } + + p, err := NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain(paramsParsed) + if err != nil { + return nil, err + } + + p.chain = chain + go p.downloader() + return p, nil +} + +// NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain initializes the provider from already parsed parameters and without chain. +// Refactored like this for better testability. +func NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain(params mempoolSpaceBlockFeeParams) (*mempoolSpaceBlockFeeProvider, error) { + // Check mandatory parameters + if params.URL == "" { + return nil, errors.New("NewMempoolSpaceBlockFee: Missing url") + } + if params.PeriodSeconds == 0 { + return nil, errors.New("NewMempoolSpaceBlockFee: Missing periodSeconds") + } + + // Report on what is used + if params.FeeRangeIndex == nil { + glog.Info("NewMempoolSpaceBlockFee: Using median fee") + } else { + index := *params.FeeRangeIndex + if index < 0 || index > 6 { + return nil, errors.New("NewMempoolSpaceBlockFee: feeRangeIndex must be between 0 and 6") + } + glog.Infof("NewMempoolSpaceBlockFee: Using feeRangeIndex %d", index) + } + + p := &mempoolSpaceBlockFeeProvider{ + alternativeFeeProvider: &alternativeFeeProvider{}, + params: params, + } + + if params.FallbackFeePerKB > 0 { + p.fallbackFeePerKBIfNotAvailable = params.FallbackFeePerKB + } + + return p, nil +} + +func (p *mempoolSpaceBlockFeeProvider) downloader() { + period := time.Duration(p.params.PeriodSeconds) * time.Second + timer := time.NewTimer(period) + counter := 0 + for { + var data []mempoolSpaceBlockFeeResult + err := p.getData(&data) + if err != nil { + glog.Error("getData ", err) + } else { + if p.processData(&data) { + if counter%60 == 0 { + p.compareToDefault() + } + counter++ + } + } + <-timer.C + timer.Reset(period) + } +} + +func (p *mempoolSpaceBlockFeeProvider) processData(data *[]mempoolSpaceBlockFeeResult) bool { + if len(*data) == 0 { + glog.Error("processData: empty data") + return false + } + + p.mux.Lock() + defer p.mux.Unlock() + + p.fees = make([]alternativeFeeProviderFee, 0, len(*data)) + + for i, block := range *data { + var fee float64 + + if p.params.FeeRangeIndex == nil { + fee = block.MedianFee + } else { + feeRange := block.FeeRange + index := *p.params.FeeRangeIndex + if len(feeRange) > index { + fee = feeRange[index] + } else { + glog.Warningf("Block %d has too short feeRange (len=%d, required=%d). Replacing by medianFee", i, len(feeRange), index) + fee = block.MedianFee + } + } + + if fee <= 0 { + glog.Warningf("Skipping block at index %d due to invalid fee: %f", i, fee) + continue + } + + // TODO: it might make sense to not include _every_ block, but only e.g. first 20 and then some hardcoded ones like 50, 100, 200, etc. + // But even storing thousands of elements in []alternativeFeeProviderFee should not make a big performance overhead + // Depends on Suite requirements + + // We want to convert the fee to 3 significant digits + feeRounded := common.RoundToSignificantDigits(fee, 3) + feePerKB := int(math.Round(feeRounded * 1000)) + + p.fees = append(p.fees, alternativeFeeProviderFee{ + blocks: i + 1, + feePerKB: feePerKB, + }) + } + + p.lastSync = time.Now() + return true +} + +func (p *mempoolSpaceBlockFeeProvider) getData(res interface{}) error { + httpReq, err := http.NewRequest("GET", p.params.URL, nil) + if err != nil { + return err + } + httpRes, err := http.DefaultClient.Do(httpReq) + if httpRes != nil { + defer httpRes.Body.Close() + } + if err != nil { + return err + } + if httpRes.StatusCode != http.StatusOK { + return errors.New(p.params.URL + " returned status " + strconv.Itoa(httpRes.StatusCode)) + } + return common.SafeDecodeResponseFromReader(httpRes.Body, res) +} diff --git a/bchain/coins/btc/mempoolspaceblock_test.go b/bchain/coins/btc/mempoolspaceblock_test.go new file mode 100644 index 0000000000..09e2bcfede --- /dev/null +++ b/bchain/coins/btc/mempoolspaceblock_test.go @@ -0,0 +1,198 @@ +//go:build unittest + +package btc + +import ( + "math/big" + "strconv" + "strings" + "testing" +) + +var testBlocks = []mempoolSpaceBlockFeeResult{ + { + BlockSize: 1800000, + BlockVSize: 997931, + NTx: 2500, + TotalFees: 6000000, + MedianFee: 25.1, + FeeRange: []float64{1, 5, 10, 20, 30, 50, 300}, + }, + { + BlockSize: 1750000, + BlockVSize: 997930, + NTx: 2200, + TotalFees: 4500000, + MedianFee: 7.31, + FeeRange: []float64{1, 2, 5, 10, 15, 20, 150}, + }, + { + BlockSize: 1700000, + BlockVSize: 997929, + NTx: 2000, + TotalFees: 3000000, + MedianFee: 3.14, + FeeRange: []float64{1, 1.5, 2, 5, 7, 10, 100}, + }, + { + BlockSize: 1650000, + BlockVSize: 997928, + NTx: 1800, + TotalFees: 2000000, + MedianFee: 1.34, + FeeRange: []float64{1, 1.2, 1.5, 3, 4, 5, 50}, + }, + { + BlockSize: 1600000, + BlockVSize: 997927, + NTx: 1500, + TotalFees: 1500000, + MedianFee: 1.11, + FeeRange: []float64{1, 1.05, 1.1, 1.5, 1.8, 2, 20}, + }, +} + +var estimateFeeTestCasesMedian = []struct { + blocks int + want big.Int +}{ + {0, *big.NewInt(25100)}, + {1, *big.NewInt(25100)}, + {2, *big.NewInt(7310)}, + {3, *big.NewInt(3140)}, + {4, *big.NewInt(1340)}, + {5, *big.NewInt(1110)}, + {6, *big.NewInt(1110)}, + {7, *big.NewInt(1110)}, + {10, *big.NewInt(1110)}, + {36, *big.NewInt(1110)}, + {100, *big.NewInt(1110)}, + {201, *big.NewInt(1110)}, + {501, *big.NewInt(1110)}, + {5000000, *big.NewInt(1110)}, +} + +var estimateFeeTestCasesFeeRangeIndex5FallbackSet = []struct { + blocks int + want big.Int +}{ + {0, *big.NewInt(50000)}, + {1, *big.NewInt(50000)}, + {2, *big.NewInt(20000)}, + {3, *big.NewInt(10000)}, + {4, *big.NewInt(5000)}, + {5, *big.NewInt(2000)}, + {6, *big.NewInt(1000)}, + {7, *big.NewInt(1000)}, + {10, *big.NewInt(1000)}, + {36, *big.NewInt(1000)}, + {100, *big.NewInt(1000)}, + {201, *big.NewInt(1000)}, + {501, *big.NewInt(1000)}, + {5000000, *big.NewInt(1000)}, +} + +func runEstimateFeeTest(t *testing.T, testName string, m *mempoolSpaceBlockFeeProvider, expected []struct { + blocks int + want big.Int +}) { + success := m.processData(&testBlocks) + if !success { + t.Fatalf("[%s] Expected data to be processed successfully", testName) + } + + for _, tt := range expected { + t.Run(testName+"_"+strconv.Itoa(tt.blocks), func(t *testing.T) { + got, err := m.estimateFee(tt.blocks) + if err != nil { + t.Errorf("[%s] estimateFee returned error: %v", testName, err) + } + if got.Cmp(&tt.want) != 0 { + t.Errorf("[%s] estimateFee(%d) = %v, want %v", testName, tt.blocks, got, tt.want) + } + }) + } +} + +func Test_mempoolSpaceBlockFeeProviderMedian(t *testing.T) { + // Taking the median explicitly + m, err := + NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain(mempoolSpaceBlockFeeParams{ + URL: "https://mempool.space/api/v1/fees/mempool-blocks", + PeriodSeconds: 20, + FeeRangeIndex: nil, + }) + if err != nil { + t.Fatalf("NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain returned error: %v", err) + } + runEstimateFeeTest(t, "median", m, estimateFeeTestCasesMedian) +} + +func Test_mempoolSpaceBlockFeeProviderSecondLargestIndex(t *testing.T) { + // Taking the valid index + index := 5 + m, err := + NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain(mempoolSpaceBlockFeeParams{ + URL: "https://mempool.space/api/v1/fees/mempool-blocks", + PeriodSeconds: 20, + FeeRangeIndex: &index, + FallbackFeePerKB: 1000, + }) + if err != nil { + t.Fatalf("NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain returned error: %v", err) + } + runEstimateFeeTest(t, "feeRangeIndex_5", m, estimateFeeTestCasesFeeRangeIndex5FallbackSet) +} + +func Test_mempoolSpaceBlockFeeProviderInvalidIndexTooHigh(t *testing.T) { + index := 555 + _, err := + NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain(mempoolSpaceBlockFeeParams{ + URL: "https://mempool.space/api/v1/fees/mempool-blocks", + PeriodSeconds: 20, + FeeRangeIndex: &index, + }) + + if err == nil { + t.Fatalf("expected error, got nil") + } + + expectedSubstring := "feeRangeIndex must be between 0 and 6" + if !strings.Contains(err.Error(), expectedSubstring) { + t.Errorf("expected error message to contain %q, got: %v", expectedSubstring, err) + } +} + +func Test_mempoolSpaceBlockFeeProviderMissingUrl(t *testing.T) { + _, err := + NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain(mempoolSpaceBlockFeeParams{ + PeriodSeconds: 20, + FeeRangeIndex: nil, + }) + + if err == nil { + t.Fatalf("expected error, got nil") + } + + expectedSubstring := "Missing url" + if !strings.Contains(err.Error(), expectedSubstring) { + t.Errorf("expected error message to contain %q, got: %v", expectedSubstring, err) + } +} + +func Test_mempoolSpaceBlockFeeProviderMissingPeriodSeconds(t *testing.T) { + _, err := + NewMempoolSpaceBlockFeeProviderFromParamsWithoutChain(mempoolSpaceBlockFeeParams{ + URL: "https://mempool.space/api/v1/fees/mempool-blocks", + FeeRangeIndex: nil, + }) + + if err == nil { + t.Fatalf("expected error, got nil") + } + + expectedSubstring := "Missing periodSeconds" + if !strings.Contains(err.Error(), expectedSubstring) { + t.Errorf("expected error message to contain %q, got: %v", expectedSubstring, err) + } +} diff --git a/bchain/coins/btc/whatthefee.go b/bchain/coins/btc/whatthefee.go index c0977f80d8..dba3193434 100644 --- a/bchain/coins/btc/whatthefee.go +++ b/bchain/coins/btc/whatthefee.go @@ -3,16 +3,15 @@ package btc import ( "bytes" "encoding/json" - "fmt" "math" "net/http" "strconv" - "sync" "time" "github.com/golang/glog" "github.com/juju/errors" "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/common" ) // https://whatthefee.io returns @@ -34,49 +33,40 @@ type whatTheFeeParams struct { PeriodSeconds int `periodSeconds:"url"` } -type whatTheFeeFee struct { - blocks int - feesPerKB []int -} - -type whatTheFeeData struct { +type whatTheFeeProvider struct { + *alternativeFeeProvider params whatTheFeeParams probabilities []string - fees []whatTheFeeFee - lastSync time.Time - chain bchain.BlockChain - mux sync.Mutex } -var whatTheFee whatTheFeeData - -// InitWhatTheFee initializes https://whatthefee.io handler -func InitWhatTheFee(chain bchain.BlockChain, params string) error { - err := json.Unmarshal([]byte(params), &whatTheFee.params) +// NewWhatTheFee initializes https://whatthefee.io provider +func NewWhatTheFee(chain bchain.BlockChain, params string) (alternativeFeeProviderInterface, error) { + var p whatTheFeeProvider + err := json.Unmarshal([]byte(params), &p.params) if err != nil { - return err + return nil, err } - if whatTheFee.params.URL == "" || whatTheFee.params.PeriodSeconds == 0 { - return errors.New("Missing parameters") + if p.params.URL == "" || p.params.PeriodSeconds == 0 { + return nil, errors.New("NewWhatTheFee: Missing parameters") } - whatTheFee.chain = chain - go whatTheFeeDownloader() - return nil + p.chain = chain + go p.whatTheFeeDownloader() + return &p, nil } -func whatTheFeeDownloader() { - period := time.Duration(whatTheFee.params.PeriodSeconds) * time.Second +func (p *whatTheFeeProvider) whatTheFeeDownloader() { + period := time.Duration(p.params.PeriodSeconds) * time.Second timer := time.NewTimer(period) counter := 0 for { var data whatTheFeeServiceResult - err := whatTheFeeGetData(&data) + err := p.whatTheFeeGetData(&data) if err != nil { glog.Error("whatTheFeeGetData ", err) } else { - if whatTheFeeProcessData(&data) { + if p.whatTheFeeProcessData(&data) { if counter%60 == 0 { - whatTheFeeCompareToDefault() + p.compareToDefault() } counter++ } @@ -86,15 +76,15 @@ func whatTheFeeDownloader() { } } -func whatTheFeeProcessData(data *whatTheFeeServiceResult) bool { +func (p *whatTheFeeProvider) whatTheFeeProcessData(data *whatTheFeeServiceResult) bool { if len(data.Index) == 0 || len(data.Index) != len(data.Data) || len(data.Columns) == 0 { glog.Errorf("invalid data %+v", data) return false } - whatTheFee.mux.Lock() - defer whatTheFee.mux.Unlock() - whatTheFee.probabilities = data.Columns - whatTheFee.fees = make([]whatTheFeeFee, len(data.Index)) + p.mux.Lock() + defer p.mux.Unlock() + p.probabilities = data.Columns + p.fees = make([]alternativeFeeProviderFee, len(data.Index)) for i, blocks := range data.Index { if len(data.Columns) != len(data.Data[i]) { glog.Errorf("invalid data %+v", data) @@ -104,19 +94,19 @@ func whatTheFeeProcessData(data *whatTheFeeServiceResult) bool { for j, l := range data.Data[i] { fees[j] = int(1000 * math.Exp(float64(l)/100)) } - whatTheFee.fees[i] = whatTheFeeFee{ - blocks: blocks, - feesPerKB: fees, + p.fees[i] = alternativeFeeProviderFee{ + blocks: blocks, + feePerKB: fees[len(fees)/2], } } - whatTheFee.lastSync = time.Now() - glog.Infof("%+v", whatTheFee.fees) + p.lastSync = time.Now() + glog.Infof("whatTheFees: %+v", p.fees) return true } -func whatTheFeeGetData(res interface{}) error { +func (p *whatTheFeeProvider) whatTheFeeGetData(res interface{}) error { var httpData []byte - httpReq, err := http.NewRequest("GET", whatTheFee.params.URL, bytes.NewBuffer(httpData)) + httpReq, err := http.NewRequest("GET", p.params.URL, bytes.NewBuffer(httpData)) if err != nil { return err } @@ -130,27 +120,5 @@ func whatTheFeeGetData(res interface{}) error { if httpRes.StatusCode != 200 { return errors.New("whatthefee.io returned status " + strconv.Itoa(httpRes.StatusCode)) } - return safeDecodeResponse(httpRes.Body, &res) -} - -func whatTheFeeCompareToDefault() { - output := "" - for _, fee := range whatTheFee.fees { - output += fmt.Sprint(fee.blocks, ",") - for _, wtf := range fee.feesPerKB { - output += fmt.Sprint(wtf, ",") - } - conservative, err := whatTheFee.chain.EstimateSmartFee(fee.blocks, true) - if err != nil { - glog.Error(err) - return - } - economical, err := whatTheFee.chain.EstimateSmartFee(fee.blocks, false) - if err != nil { - glog.Error(err) - return - } - output += fmt.Sprint(conservative.String(), ",", economical.String(), "\n") - } - glog.Info("whatTheFeeCompareToDefault\n", output) + return common.SafeDecodeResponseFromReader(httpRes.Body, &res) } diff --git a/bchain/coins/dcr/decredrpc.go b/bchain/coins/dcr/decredrpc.go index 07cc459849..31c5810f81 100644 --- a/bchain/coins/dcr/decredrpc.go +++ b/bchain/coins/dcr/decredrpc.go @@ -791,7 +791,7 @@ func (d *DecredRPC) EstimateFee(blocks int) (big.Int, error) { return r, nil } -func (d *DecredRPC) SendRawTransaction(tx string) (string, error) { +func (d *DecredRPC) SendRawTransaction(tx string, disableAlternativeRPC bool) (string, error) { sendRawTxRequest := &GenericCmd{ ID: 1, Method: "sendrawtransaction", diff --git a/bchain/coins/ecash/ecashparser.go b/bchain/coins/ecash/ecashparser.go index e4547ae8ce..2b4ef88184 100644 --- a/bchain/coins/ecash/ecashparser.go +++ b/bchain/coins/ecash/ecashparser.go @@ -3,11 +3,11 @@ package ecash import ( "fmt" - "github.com/pirk/ecashutil" "github.com/martinboehm/btcutil" "github.com/martinboehm/btcutil/chaincfg" "github.com/martinboehm/btcutil/txscript" "github.com/pirk/ecashaddr-converter/address" + "github.com/pirk/ecashutil" "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain/coins/btc" ) diff --git a/bchain/coins/ecash/ecashparser_test.go b/bchain/coins/ecash/ecashparser_test.go index 218299bd10..719ded6c2e 100644 --- a/bchain/coins/ecash/ecashparser_test.go +++ b/bchain/coins/ecash/ecashparser_test.go @@ -342,6 +342,10 @@ func Test_UnpackTx(t *testing.T) { t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) return } + // ignore witness unpacking + for i := range got.Vin { + got.Vin[i].Witness = nil + } if !reflect.DeepEqual(got, tt.want) { t.Errorf("unpackTx() got = %v, want %v", got, tt.want) } diff --git a/bchain/coins/eth/alternativefeeprovider.go b/bchain/coins/eth/alternativefeeprovider.go new file mode 100644 index 0000000000..d361df8fbf --- /dev/null +++ b/bchain/coins/eth/alternativefeeprovider.go @@ -0,0 +1,29 @@ +package eth + +import ( + "sync" + "time" + + "github.com/trezor/blockbook/bchain" +) + +type alternativeFeeProvider struct { + eip1559Fees *bchain.Eip1559Fees + lastSync time.Time + staleSyncDuration time.Duration + chain bchain.BlockChain + mux sync.Mutex +} + +type alternativeFeeProviderInterface interface { + GetEip1559Fees() (*bchain.Eip1559Fees, error) +} + +func (p *alternativeFeeProvider) GetEip1559Fees() (*bchain.Eip1559Fees, error) { + p.mux.Lock() + defer p.mux.Unlock() + if p.lastSync.Add(p.staleSyncDuration).Before(time.Now()) { + return nil, nil + } + return p.eip1559Fees, nil +} diff --git a/bchain/coins/eth/alternativesendtx.go b/bchain/coins/eth/alternativesendtx.go new file mode 100644 index 0000000000..a1e0b85d33 --- /dev/null +++ b/bchain/coins/eth/alternativesendtx.go @@ -0,0 +1,206 @@ +package eth + +import ( + "context" + "encoding/json" + "os" + "strings" + "sync" + "time" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" +) + +type storedTx struct { + tx *bchain.RpcTransaction + time uint32 +} + +// AlternativeSendTxProvider handles sending transactions to alternative providers +type AlternativeSendTxProvider struct { + urls []string + onlyAlternative bool + fetchMempoolTx bool + mempoolTxs map[string]storedTx + mempoolTxsMux sync.Mutex + mempoolTxsTimeout time.Duration + rpcTimeout time.Duration + mempool *bchain.MempoolEthereumType + removeTransactionFromMempool func(string) +} + +// NewAlternativeSendTxProvider creates a new alternative send tx provider if enabled +func NewAlternativeSendTxProvider(network string, rpcTimeout int, mempoolTxsTimeout int) *AlternativeSendTxProvider { + urls := strings.Split(os.Getenv(strings.ToUpper(network)+"_ALTERNATIVE_SENDTX_URLS"), ",") + onlyAlternative := strings.ToUpper(os.Getenv(strings.ToUpper(network)+"_ALTERNATIVE_SENDTX_ONLY")) == "TRUE" + fetchMempoolTx := strings.ToUpper(os.Getenv(strings.ToUpper(network)+"_ALTERNATIVE_FETCH_MEMPOOL_TX")) == "TRUE" + if len(urls) == 0 || urls[0] == "" { + return nil + } + + provider := &AlternativeSendTxProvider{ + urls: urls, + onlyAlternative: onlyAlternative, + fetchMempoolTx: fetchMempoolTx, + rpcTimeout: time.Duration(rpcTimeout) * time.Second, + mempoolTxsTimeout: time.Duration(mempoolTxsTimeout) * time.Hour, + mempoolTxs: make(map[string]storedTx), + } + + glog.Infof("Using alternative send transaction providers %v. Only alternative providers %v", urls, onlyAlternative) + if fetchMempoolTx { + glog.Infof("Alternative fetch mempool tx %v", fetchMempoolTx) + } + + return provider +} + +// SetupMempool sets up connection to the mempool +func (p *AlternativeSendTxProvider) SetupMempool(mempool *bchain.MempoolEthereumType, removeTransactionFromMempool func(string)) { + p.mempool = mempool + p.removeTransactionFromMempool = removeTransactionFromMempool +} + +// SendRawTransaction sends raw transaction to alternative providers +func (p *AlternativeSendTxProvider) SendRawTransaction(hex string) (string, error) { + var txid string + var retErr error + + for i := range p.urls { + r, err := p.callHttpStringResult(p.urls[i], "eth_sendRawTransaction", hex) + glog.Infof("eth_sendRawTransaction to %s, txid %s", p.urls[i], r) + // set success return value; or error only if there was no previous success + if err == nil || len(txid) == 0 { + txid = r + retErr = err + } + } + + if p.onlyAlternative && p.fetchMempoolTx { + p.handleMempoolTransaction(txid) + } + + return txid, retErr +} + +// handleMempoolTransaction handles the transaction when using only alternative providers +func (p *AlternativeSendTxProvider) handleMempoolTransaction(txid string) (string, error) { + hash := ethcommon.HexToHash(txid) + raw, err := p.callHttpRawResult(p.urls[0], "eth_getTransactionByHash", hash) + if err != nil || raw == nil { + glog.Errorf("eth_getTransactionByHash from %s returned error %v", p.urls[0], err) + return txid, err + } + + var tx bchain.RpcTransaction + if err := json.Unmarshal(raw, &tx); err != nil { + glog.Errorf("eth_getTransactionByHash from %s unmarshal returned error %v", p.urls[0], err) + return txid, err + } + + p.mempoolTxsMux.Lock() + // remove potential RBF transactions - with equal from and nonce + var rbfTxid string + for rbf, storedTx := range p.mempoolTxs { + if storedTx.tx.From == tx.From && storedTx.tx.AccountNonce == tx.AccountNonce { + rbfTxid = rbf + break + } + } + p.mempoolTxs[txid] = storedTx{tx: &tx, time: uint32(time.Now().Unix())} + p.mempoolTxsMux.Unlock() + + if rbfTxid != "" { + glog.Infof("eth_sendRawTransaction replacing txid %s by %s", rbfTxid, txid) + if p.removeTransactionFromMempool != nil { + p.removeTransactionFromMempool(rbfTxid) + } + } + + if p.mempool != nil { + p.mempool.AddTransactionToMempool(txid) + } + + return txid, nil +} + +// GetTransaction gets a transaction from alternative mempool cache +func (p *AlternativeSendTxProvider) GetTransaction(txid string) (*bchain.RpcTransaction, bool) { + if !p.fetchMempoolTx { + return nil, false + } + + var storedTx storedTx + var found bool + + p.mempoolTxsMux.Lock() + storedTx, found = p.mempoolTxs[txid] + p.mempoolTxsMux.Unlock() + + if found { + if time.Unix(int64(storedTx.time), 0).Before(time.Now().Add(-p.mempoolTxsTimeout)) { + p.mempoolTxsMux.Lock() + delete(p.mempoolTxs, txid) + p.mempoolTxsMux.Unlock() + return nil, false + } + return storedTx.tx, true + } + + return nil, false +} + +// RemoveTransaction removes a transaction from alternative mempool cache +func (p *AlternativeSendTxProvider) RemoveTransaction(txid string) { + if !p.fetchMempoolTx { + return + } + + p.mempoolTxsMux.Lock() + delete(p.mempoolTxs, txid) + p.mempoolTxsMux.Unlock() +} + +// UseOnlyAlternativeProvider returns true if only alternative providers should be used +func (p *AlternativeSendTxProvider) UseOnlyAlternativeProvider() bool { + return p.onlyAlternative +} + +// Helper function for calling ETH RPC over http with parameters. Creates and closes a new client for every call. +func (p *AlternativeSendTxProvider) callHttpRawResult(url string, rpcMethod string, args ...interface{}) (json.RawMessage, error) { + ctx, cancel := context.WithTimeout(context.Background(), p.rpcTimeout) + defer cancel() + client, err := rpc.DialContext(ctx, url) + if err != nil { + return nil, err + } + defer client.Close() + var raw json.RawMessage + err = client.CallContext(ctx, &raw, rpcMethod, args...) + if err != nil { + return nil, err + } else if len(raw) == 0 { + return nil, errors.New(url + " " + rpcMethod + " : failed") + } + return raw, nil +} + +// Helper function for calling ETH RPC over http with parameters and getting string result. Creates and closes a new client for every call. +func (p *AlternativeSendTxProvider) callHttpStringResult(url string, rpcMethod string, args ...interface{}) (string, error) { + raw, err := p.callHttpRawResult(url, rpcMethod, args...) + if err != nil { + return "", err + } + var result string + if err := json.Unmarshal(raw, &result); err != nil { + return "", errors.Annotatef(err, "%s %s raw result %v", url, rpcMethod, raw) + } + if result == "" { + return "", errors.New(url + " " + rpcMethod + " : failed, empty result") + } + return result, nil +} diff --git a/bchain/coins/eth/contract.go b/bchain/coins/eth/contract.go index 6405c9c813..682f4c2cd7 100644 --- a/bchain/coins/eth/contract.go +++ b/bchain/coins/eth/contract.go @@ -51,16 +51,16 @@ func processTransferEvent(l *bchain.RpcLog) (transfer *bchain.TokenTransfer, err } }() tl := len(l.Topics) - var ttt bchain.TokenType + var standard bchain.TokenStandard var value big.Int if tl == 3 { - ttt = bchain.FungibleToken + standard = bchain.FungibleToken _, ok := value.SetString(l.Data, 0) if !ok { return nil, errors.New("ERC20 log Data is not a number") } } else if tl == 4 { - ttt = bchain.NonFungibleToken + standard = bchain.NonFungibleToken _, ok := value.SetString(l.Topics[3], 0) if !ok { return nil, errors.New("ERC721 log Topics[3] is not a number") @@ -78,7 +78,7 @@ func processTransferEvent(l *bchain.RpcLog) (transfer *bchain.TokenTransfer, err return nil, err } return &bchain.TokenTransfer{ - Type: ttt, + Standard: standard, Contract: EIP55AddressFromAddress(l.Address), From: EIP55AddressFromAddress(from), To: EIP55AddressFromAddress(to), @@ -119,7 +119,7 @@ func processERC1155TransferSingleEvent(l *bchain.RpcLog) (transfer *bchain.Token return nil, errors.New("ERC1155 log Data value is not a number") } return &bchain.TokenTransfer{ - Type: bchain.MultiToken, + Standard: bchain.MultiToken, Contract: EIP55AddressFromAddress(l.Address), From: EIP55AddressFromAddress(from), To: EIP55AddressFromAddress(to), @@ -190,7 +190,7 @@ func processERC1155TransferBatchEvent(l *bchain.RpcLog) (transfer *bchain.TokenT idValues[i] = bchain.MultiTokenValue{Id: id, Value: value} } return &bchain.TokenTransfer{ - Type: bchain.MultiToken, + Standard: bchain.MultiToken, Contract: EIP55AddressFromAddress(l.Address), From: EIP55AddressFromAddress(from), To: EIP55AddressFromAddress(to), @@ -239,7 +239,7 @@ func contractGetTransfersFromTx(tx *bchain.RpcTransaction) (bchain.TokenTransfer return nil, errors.New("Data is not a number") } r = append(r, &bchain.TokenTransfer{ - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: EIP55AddressFromAddress(tx.To), From: EIP55AddressFromAddress(tx.From), To: EIP55AddressFromAddress(to), @@ -263,7 +263,7 @@ func contractGetTransfersFromTx(tx *bchain.RpcTransaction) (bchain.TokenTransfer return nil, errors.New("Data is not a number") } r = append(r, &bchain.TokenTransfer{ - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: EIP55AddressFromAddress(tx.To), From: EIP55AddressFromAddress(from), To: EIP55AddressFromAddress(to), @@ -273,14 +273,20 @@ func contractGetTransfersFromTx(tx *bchain.RpcTransaction) (bchain.TokenTransfer return r, nil } -func (b *EthereumRPC) ethCall(data, to string) (string, error) { +// EthereumTypeRpcCall calls eth_call with given data and to address +func (b *EthereumRPC) EthereumTypeRpcCall(data, to, from string) (string, error) { + args := map[string]interface{}{ + "data": data, + "to": to, + } + if from != "" { + args["from"] = from + } + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var r string - err := b.RPC.CallContext(ctx, &r, "eth_call", map[string]interface{}{ - "data": data, - "to": to, - }, "latest") + err := b.RPC.CallContext(ctx, &r, "eth_call", args, "latest") if err != nil { return "", err } @@ -289,7 +295,7 @@ func (b *EthereumRPC) ethCall(data, to string) (string, error) { func (b *EthereumRPC) fetchContractInfo(address string) (*bchain.ContractInfo, error) { var contract bchain.ContractInfo - data, err := b.ethCall(contractNameSignature, address) + data, err := b.EthereumTypeRpcCall(contractNameSignature, address, "") if err != nil { // 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 @@ -300,14 +306,14 @@ func (b *EthereumRPC) fetchContractInfo(address string) (*bchain.ContractInfo, e } name := strings.TrimSpace(parseSimpleStringProperty(data)) if name != "" { - data, err = b.ethCall(contractSymbolSignature, address) + data, err = b.EthereumTypeRpcCall(contractSymbolSignature, address, "") if err != nil { // glog.Warning(errors.Annotatef(err, "Contract SymbolSignature %v", address)) return nil, nil // return nil, errors.Annotatef(err, "erc20SymbolSignature %v", address) } symbol := strings.TrimSpace(parseSimpleStringProperty(data)) - data, _ = b.ethCall(contractDecimalsSignature, address) + data, _ = b.EthereumTypeRpcCall(contractDecimalsSignature, address, "") // if err != nil { // glog.Warning(errors.Annotatef(err, "Contract DecimalsSignature %v", address)) // // return nil, errors.Annotatef(err, "erc20DecimalsSignature %v", address) @@ -337,10 +343,10 @@ func (b *EthereumRPC) GetContractInfo(contractDesc bchain.AddressDescriptor) (*b // EthereumTypeGetErc20ContractBalance returns balance of ERC20 contract for given address func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) { - addr := hexutil.Encode(addrDesc) + addr := hexutil.Encode(addrDesc)[2:] contract := hexutil.Encode(contractDesc) - req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr)-2:] + addr[2:] - data, err := b.ethCall(req, contract) + req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr + data, err := b.EthereumTypeRpcCall(req, contract, "") if err != nil { return nil, err } @@ -351,7 +357,7 @@ func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc return r, nil } -// GetContractInfo returns URI of non fungible or multi token defined by token id +// GetTokenURI returns URI of non fungible or multi token defined by token id func (b *EthereumRPC) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID *big.Int) (string, error) { address := hexutil.Encode(contractDesc) // CryptoKitties do not fully support ERC721 standard, do not have tokenURI method @@ -364,7 +370,7 @@ func (b *EthereumRPC) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID } // try ERC721 tokenURI method and ERC1155 uri method for _, method := range []string{erc721TokenURIMethodSignature, erc1155URIMethodSignature} { - data, err := b.ethCall(method+id, address) + data, err := b.EthereumTypeRpcCall(method+id, address, "") if err == nil && data != "" { uri := parseSimpleStringProperty(data) // try to sanitize the URI returned from the contract diff --git a/bchain/coins/eth/contract_test.go b/bchain/coins/eth/contract_test.go index 587d98a774..ca70c85878 100644 --- a/bchain/coins/eth/contract_test.go +++ b/bchain/coins/eth/contract_test.go @@ -133,7 +133,7 @@ func Test_contractGetTransfersFromLog(t *testing.T) { }, want: bchain.TokenTransfers{ { - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: "0x5689b918D34C038901870105A6C7fc24744D31eB", From: "0x0a206d4d5ff79cb5069def7fe3598421cff09391", To: "0x6a016d7eec560549ffa0fbdb7f15c2b27302087f", @@ -171,7 +171,7 @@ func Test_contractGetTransfersFromLog(t *testing.T) { }, want: bchain.TokenTransfers{ { - Type: bchain.MultiToken, + Standard: bchain.MultiToken, Contract: "0x6Fd712E3A5B556654044608F9129040A4839E36c", From: "0xa3950b823cb063dd9afc0d27f35008b805b3ed53", To: "0x4392faf3bb96b5694ecc6ef64726f61cdd4bb0ec", @@ -195,7 +195,7 @@ func Test_contractGetTransfersFromLog(t *testing.T) { }, want: bchain.TokenTransfers{ { - Type: bchain.MultiToken, + Standard: bchain.MultiToken, Contract: "0x6c42c26a081c2f509f8bb68fb7ac3062311ccfb7", From: "0x0000000000000000000000000000000000000000", To: "0x5dc6288b35e0807a3d6feb89b3a2ff4ab773168e", @@ -247,7 +247,7 @@ func Test_contractGetTransfersFromTx(t *testing.T) { args: (b1.Txs[1].CoinSpecificData.(bchain.EthereumSpecificData)).Tx, want: bchain.TokenTransfers{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: "0x4af4114f73d1c1c903ac9e0361b379d1291808a2", From: "0x20cd153de35d469ba46127a0c8f18626b59a256a", To: "0x555ee11fbddc0e49a9bab358a8941ad95ffdb48f", @@ -260,7 +260,7 @@ func Test_contractGetTransfersFromTx(t *testing.T) { args: (b2.Txs[2].CoinSpecificData.(bchain.EthereumSpecificData)).Tx, want: bchain.TokenTransfers{ { - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: "0xcda9fc258358ecaa88845f19af595e908bb7efe9", From: "0x837e3f699d85a4b0b99894567e9233dfb1dcb081", To: "0x7b62eb7fe80350dc7ec945c0b73242cb9877fb1b", diff --git a/bchain/coins/eth/dataparser.go b/bchain/coins/eth/dataparser.go index 8182692658..1d06fdda80 100644 --- a/bchain/coins/eth/dataparser.go +++ b/bchain/coins/eth/dataparser.go @@ -51,10 +51,7 @@ func parseSimpleStringProperty(data string) string { // 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 - } + i := min(bytes.Index(b, []byte{0}), 32) if i > 0 { b = b[:i] } diff --git a/bchain/coins/eth/ethparser.go b/bchain/coins/eth/ethparser.go index 975b837494..e3d310cc73 100644 --- a/bchain/coins/eth/ethparser.go +++ b/bchain/coins/eth/ethparser.go @@ -7,10 +7,10 @@ import ( "strings" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/golang/protobuf/proto" "github.com/juju/errors" "github.com/trezor/blockbook/bchain" "golang.org/x/crypto/sha3" + "google.golang.org/protobuf/proto" ) // EthereumTypeAddressDescriptorLen - the AddressDescriptor of EthereumType has fixed length @@ -25,15 +25,19 @@ const EtherAmountDecimalPoint = 18 // EthereumParser handle type EthereumParser struct { *bchain.BaseParser + EnsSuffix string } // NewEthereumParser returns new EthereumParser instance func NewEthereumParser(b int, addressAliases bool) *EthereumParser { - return &EthereumParser{&bchain.BaseParser{ - BlockAddressesToKeep: b, - AmountDecimalPoint: EtherAmountDecimalPoint, - AddressAliases: addressAliases, - }} + return &EthereumParser{ + BaseParser: &bchain.BaseParser{ + BlockAddressesToKeep: b, + AmountDecimalPoint: EtherAmountDecimalPoint, + AddressAliases: addressAliases, + }, + EnsSuffix: ".eth", + } } type rpcHeader struct { @@ -273,6 +277,21 @@ func (p *EthereumParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ( if pt.Tx.GasPrice, err = hexDecodeBig(r.Tx.GasPrice); err != nil { return nil, errors.Annotatef(err, "Price %v", r.Tx.GasPrice) } + if len(r.Tx.MaxPriorityFeePerGas) > 0 { + if pt.Tx.MaxPriorityFeePerGas, err = hexDecodeBig(r.Tx.MaxPriorityFeePerGas); err != nil { + return nil, errors.Annotatef(err, "MaxPriorityFeePerGas %v", r.Tx.MaxPriorityFeePerGas) + } + } + if len(r.Tx.MaxFeePerGas) > 0 { + if pt.Tx.MaxFeePerGas, err = hexDecodeBig(r.Tx.MaxFeePerGas); err != nil { + return nil, errors.Annotatef(err, "MaxFeePerGas %v", r.Tx.MaxFeePerGas) + } + } + if len(r.Tx.BaseFeePerGas) > 0 { + if pt.Tx.BaseFeePerGas, err = hexDecodeBig(r.Tx.BaseFeePerGas); err != nil { + return nil, errors.Annotatef(err, "BaseFeePerGas %v", r.Tx.BaseFeePerGas) + } + } // if pt.R, err = hexDecodeBig(r.R); err != nil { // return nil, errors.Annotatef(err, "R %v", r.R) // } @@ -331,6 +350,24 @@ func (p *EthereumParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ( } pt.Receipt.Log = ptLogs + if r.Receipt.L1Fee != "" { + if pt.Receipt.L1Fee, err = hexDecodeBig(r.Receipt.L1Fee); err != nil { + return nil, errors.Annotatef(err, "L1Fee %v", r.Receipt.L1Fee) + } + } + if r.Receipt.L1FeeScalar != "" { + pt.Receipt.L1FeeScalar = []byte(r.Receipt.L1FeeScalar) + } + if r.Receipt.L1GasPrice != "" { + if pt.Receipt.L1GasPrice, err = hexDecodeBig(r.Receipt.L1GasPrice); err != nil { + return nil, errors.Annotatef(err, "L1GasPrice %v", r.Receipt.L1GasPrice) + } + } + if r.Receipt.L1GasUsed != "" { + if pt.Receipt.L1GasUsed, err = hexDecodeBig(r.Receipt.L1GasUsed); err != nil { + return nil, errors.Annotatef(err, "L1GasUsed %v", r.Receipt.L1GasUsed) + } + } } return proto.Marshal(pt) } @@ -357,29 +394,48 @@ func (p *EthereumParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { TransactionIndex: hexutil.EncodeUint64(uint64(pt.Tx.TransactionIndex)), Value: hexEncodeBig(pt.Tx.Value), } + if len(pt.Tx.MaxPriorityFeePerGas) > 0 { + rt.MaxPriorityFeePerGas = hexEncodeBig(pt.Tx.MaxPriorityFeePerGas) + } + if len(pt.Tx.MaxFeePerGas) > 0 { + rt.MaxFeePerGas = hexEncodeBig(pt.Tx.MaxFeePerGas) + } + if len(pt.Tx.BaseFeePerGas) > 0 { + rt.BaseFeePerGas = hexEncodeBig(pt.Tx.BaseFeePerGas) + } var rr *bchain.RpcReceipt if pt.Receipt != nil { - logs := make([]*bchain.RpcLog, len(pt.Receipt.Log)) + rr = &bchain.RpcReceipt{ + GasUsed: hexEncodeBig(pt.Receipt.GasUsed), + Status: "", + Logs: make([]*bchain.RpcLog, len(pt.Receipt.Log)), + } for i, l := range pt.Receipt.Log { topics := make([]string, len(l.Topics)) for j, t := range l.Topics { topics[j] = hexutil.Encode(t) } - logs[i] = &bchain.RpcLog{ + rr.Logs[i] = &bchain.RpcLog{ Address: EIP55Address(l.Address), Data: hexutil.Encode(l.Data), 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.Status = hexEncodeBig(pt.Receipt.Status) } - rr = &bchain.RpcReceipt{ - GasUsed: hexEncodeBig(pt.Receipt.GasUsed), - Status: status, - Logs: logs, + if len(pt.Receipt.L1Fee) > 0 { + rr.L1Fee = hexEncodeBig(pt.Receipt.L1Fee) + } + if len(pt.Receipt.L1FeeScalar) > 0 { + rr.L1FeeScalar = string(pt.Receipt.L1FeeScalar) + } + if len(pt.Receipt.L1GasPrice) > 0 { + rr.L1GasPrice = hexEncodeBig(pt.Receipt.L1GasPrice) + } + if len(pt.Receipt.L1GasUsed) > 0 { + rr.L1GasUsed = hexEncodeBig(pt.Receipt.L1GasUsed) } } // TODO handle internal transactions @@ -461,7 +517,7 @@ func (p *EthereumParser) EthereumTypeGetTokenTransfersFromTx(tx *bchain.Tx) (bch // FormatAddressAlias adds .eth to a name alias func (p *EthereumParser) FormatAddressAlias(address string, name string) string { - return name + ".eth" + return name + p.EnsSuffix } // TxStatus is status of transaction @@ -477,12 +533,19 @@ const ( // EthereumTxData contains ethereum specific transaction data type EthereumTxData struct { - 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"` + 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"` + MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas,omitempty"` + MaxFeePerGas *big.Int `json:"maxFeePerGas,omitempty"` + BaseFeePerGas *big.Int `json:"baseFeePerGas,omitempty"` + L1Fee *big.Int `json:"l1Fee,omitempty"` + L1FeeScalar string `json:"l1FeeScalar,omitempty"` + L1GasPrice *big.Int `json:"l1GasPrice,omitempty"` + L1GasUsed *big.Int `json:"L1GasUsed,omitempty"` + Data string `json:"data"` } // GetEthereumTxData returns EthereumTxData from bchain.Tx @@ -499,6 +562,9 @@ func GetEthereumTxDataFromSpecificData(coinSpecificData interface{}) *EthereumTx etd.Nonce, _ = hexutil.DecodeUint64(csd.Tx.AccountNonce) etd.GasLimit, _ = hexutil.DecodeBig(csd.Tx.GasLimit) etd.GasPrice, _ = hexutil.DecodeBig(csd.Tx.GasPrice) + etd.MaxPriorityFeePerGas, _ = hexutil.DecodeBig(csd.Tx.MaxPriorityFeePerGas) + etd.MaxFeePerGas, _ = hexutil.DecodeBig(csd.Tx.MaxFeePerGas) + etd.BaseFeePerGas, _ = hexutil.DecodeBig(csd.Tx.BaseFeePerGas) etd.Data = csd.Tx.Payload } if csd.Receipt != nil { @@ -511,6 +577,10 @@ func GetEthereumTxDataFromSpecificData(coinSpecificData interface{}) *EthereumTx etd.Status = TxStatusFailure } etd.GasUsed, _ = hexutil.DecodeBig(csd.Receipt.GasUsed) + etd.L1Fee, _ = hexutil.DecodeBig(csd.Receipt.L1Fee) + etd.L1GasPrice, _ = hexutil.DecodeBig(csd.Receipt.L1GasPrice) + etd.L1GasUsed, _ = hexutil.DecodeBig(csd.Receipt.L1GasUsed) + etd.L1FeeScalar = csd.Receipt.L1FeeScalar } } return &etd diff --git a/bchain/coins/eth/ethparser_test.go b/bchain/coins/eth/ethparser_test.go index aaee177ae6..65b46c7d1a 100644 --- a/bchain/coins/eth/ethparser_test.go +++ b/bchain/coins/eth/ethparser_test.go @@ -91,16 +91,19 @@ func init() { }, CoinSpecificData: bchain.EthereumSpecificData{ Tx: &bchain.RpcTransaction{ - AccountNonce: "0xb26c", - GasPrice: "0x430e23400", - GasLimit: "0x5208", - To: "0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f", - Value: "0x1bc0159d530e6000", - Payload: "0x", - Hash: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", - BlockNumber: "0x41eee8", - From: "0x3E3a3D69dc66bA10737F531ed088954a9EC89d97", - TransactionIndex: "0xa", + AccountNonce: "0xb26c", + GasPrice: "0x430e23400", + MaxPriorityFeePerGas: "0x430e23401", + MaxFeePerGas: "0x430e23402", + BaseFeePerGas: "0x430e23403", + GasLimit: "0x5208", + To: "0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f", + Value: "0x1bc0159d530e6000", + Payload: "0x", + Hash: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b", + BlockNumber: "0x41eee8", + From: "0x3E3a3D69dc66bA10737F531ed088954a9EC89d97", + TransactionIndex: "0xa", }, Receipt: &bchain.RpcReceipt{ GasUsed: "0x5208", @@ -129,16 +132,19 @@ func init() { }, CoinSpecificData: bchain.EthereumSpecificData{ Tx: &bchain.RpcTransaction{ - AccountNonce: "0xd0", - GasPrice: "0x9502f9000", - GasLimit: "0x130d5", - To: "0x4af4114F73d1c1C903aC9E0361b379D1291808A2", - Value: "0x0", - Payload: "0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000", - Hash: "0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101", - BlockNumber: "0x41eee8", - From: "0x20cD153de35D469BA46127A0C8F18626b59a256A", - TransactionIndex: "0x0"}, + AccountNonce: "0xd0", + GasPrice: "0x9502f9000", + MaxPriorityFeePerGas: "0x9502f9001", + MaxFeePerGas: "0x9502f9002", + BaseFeePerGas: "0x9502f9003", + GasLimit: "0x130d5", + To: "0x4af4114F73d1c1C903aC9E0361b379D1291808A2", + Value: "0x0", + Payload: "0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000", + Hash: "0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101", + BlockNumber: "0x41eee8", + From: "0x20cD153de35D469BA46127A0C8F18626b59a256A", + TransactionIndex: "0x0"}, Receipt: &bchain.RpcReceipt{ GasUsed: "0xcb39", Status: "0x1", diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index 61173b05c5..c054f41526 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "math/big" "net/http" "strconv" @@ -30,18 +30,19 @@ type Network uint32 const ( // MainNet is production network MainNet Network = 1 - // TestNet is Ropsten test network - TestNet Network = 3 - // TestNetGoerli is Goerli test network - TestNetGoerli Network = 5 // TestNetSepolia is Sepolia test network TestNetSepolia Network = 11155111 + // TestNetHolesky is Holesky test network + TestNetHolesky Network = 17000 + // TestNetHoodi is Hoodi test network + TestNetHoodi Network = 560048 ) // Configuration represents json config file type Configuration struct { CoinName string `json:"coin_name"` CoinShortcut string `json:"coin_shortcut"` + Network string `json:"network"` RPCURL string `json:"rpc_url"` RPCTimeout int `json:"rpc_timeout"` BlockAddressesToKeep int `json:"block_addresses_to_keep"` @@ -51,28 +52,37 @@ type Configuration struct { ProcessInternalTransactions bool `json:"processInternalTransactions"` ProcessZeroInternalTransactions bool `json:"processZeroInternalTransactions"` ConsensusNodeVersionURL string `json:"consensusNodeVersion"` + DisableMempoolSync bool `json:"disableMempoolSync,omitempty"` + Eip1559Fees bool `json:"eip1559Fees,omitempty"` + AlternativeEstimateFee string `json:"alternative_estimate_fee,omitempty"` + AlternativeEstimateFeeParams string `json:"alternative_estimate_fee_params,omitempty"` } // EthereumRPC is an interface to JSON-RPC eth service. type EthereumRPC struct { *bchain.BaseChain - Client bchain.EVMClient - RPC bchain.EVMRPCClient - MainNetChainID Network - Timeout time.Duration - Parser *EthereumParser - PushHandler func(bchain.NotificationType) - OpenRPC func(string) (bchain.EVMRPCClient, bchain.EVMClient, error) - Mempool *bchain.MempoolEthereumType - mempoolInitialized bool - bestHeaderLock sync.Mutex - bestHeader bchain.EVMHeader - bestHeaderTime time.Time - NewBlock bchain.EVMNewBlockSubscriber - newBlockSubscription bchain.EVMClientSubscription - NewTx bchain.EVMNewTxSubscriber - newTxSubscription bchain.EVMClientSubscription - ChainConfig *Configuration + Client bchain.EVMClient + RPC bchain.EVMRPCClient + MainNetChainID Network + Timeout time.Duration + Parser *EthereumParser + PushHandler func(bchain.NotificationType) + OpenRPC func(string) (bchain.EVMRPCClient, bchain.EVMClient, error) + Mempool *bchain.MempoolEthereumType + mempoolInitialized bool + bestHeaderLock sync.Mutex + bestHeader bchain.EVMHeader + bestHeaderTime time.Time + NewBlock bchain.EVMNewBlockSubscriber + newBlockSubscription bchain.EVMClientSubscription + NewTx bchain.EVMNewTxSubscriber + newTxSubscription bchain.EVMClientSubscription + ChainConfig *Configuration + supportedStakingPools []string + stakingPoolNames []string + stakingPoolContracts []string + alternativeFeeProvider alternativeFeeProviderInterface + alternativeSendTxProvider *AlternativeSendTxProvider } // ProcessInternalTransactions specifies if internal transactions are processed @@ -106,17 +116,22 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification return s, nil } +// OpenRPC opens RPC connection to ETH backend +var OpenRPC = func(url string) (bchain.EVMRPCClient, bchain.EVMClient, error) { + opts := []rpc.ClientOption{} + opts = append(opts, rpc.WithWebsocketMessageSizeLimit(0)) + r, err := rpc.DialOptions(context.Background(), url, opts...) + if err != nil { + return nil, nil, err + } + rc := &EthereumRPCClient{Client: r} + ec := &EthereumClient{Client: ethclient.NewClient(r)} + return rc, ec, nil +} + // Initialize initializes ethereum rpc interface func (b *EthereumRPC) Initialize() error { - b.OpenRPC = func(url string) (bchain.EVMRPCClient, bchain.EVMClient, error) { - r, err := rpc.Dial(url) - if err != nil { - return nil, nil, err - } - rc := &EthereumRPCClient{Client: r} - ec := &EthereumClient{Client: ethclient.NewClient(r)} - return rc, ec, nil - } + b.OpenRPC = OpenRPC rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL) if err != nil { @@ -143,28 +158,51 @@ func (b *EthereumRPC) Initialize() error { case MainNet: b.Testnet = false b.Network = "livenet" - case TestNet: - b.Testnet = true - b.Network = "testnet" - case TestNetGoerli: - b.Testnet = true - b.Network = "goerli" case TestNetSepolia: b.Testnet = true b.Network = "sepolia" + case TestNetHolesky: + b.Testnet = true + b.Network = "holesky" + case TestNetHoodi: + b.Testnet = true + b.Network = "hoodi" default: return errors.Errorf("Unknown network id %v", id) } + + err = b.initStakingPools() + if err != nil { + return err + } + + b.InitAlternativeProviders() + glog.Info("rpc: block chain ", b.Network) return nil } +// InitAlternativeProviders initializes alternative providers +func (b *EthereumRPC) InitAlternativeProviders() { + b.initAlternativeFeeProvider() + + network := b.ChainConfig.Network + if network == "" { + network = b.ChainConfig.CoinShortcut + } + b.alternativeSendTxProvider = NewAlternativeSendTxProvider(network, b.ChainConfig.RPCTimeout, b.ChainConfig.MempoolTxTimeoutHours) +} + // CreateMempool creates mempool if not already created, however does not initialize it func (b *EthereumRPC) CreateMempool(chain bchain.BlockChain) (bchain.Mempool, error) { if b.Mempool == nil { b.Mempool = bchain.NewMempoolEthereumType(chain, b.ChainConfig.MempoolTxTimeoutHours, b.ChainConfig.QueryBackendOnMempoolResync) - glog.Info("mempool created, MempoolTxTimeoutHours=", b.ChainConfig.MempoolTxTimeoutHours, ", QueryBackendOnMempoolResync=", b.ChainConfig.QueryBackendOnMempoolResync) + glog.Info("mempool created, MempoolTxTimeoutHours=", b.ChainConfig.MempoolTxTimeoutHours, ", QueryBackendOnMempoolResync=", b.ChainConfig.QueryBackendOnMempoolResync, ", DisableMempoolSync=", b.ChainConfig.DisableMempoolSync) + if b.alternativeSendTxProvider != nil { + b.alternativeSendTxProvider.SetupMempool(b.Mempool, b.removeTransactionFromMempool) + } + } return b.Mempool, nil } @@ -175,11 +213,19 @@ func (b *EthereumRPC) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOu return errors.New("Mempool not created") } + var err error + var txs []string // get initial mempool transactions - txs, err := b.GetMempoolTransactions() - if err != nil { - return err + // workaround for an occasional `decoding block` error from getBlockRaw - try 3 times with a delay and then proceed + for i := 0; i < 3; i++ { + txs, err = b.GetMempoolTransactions() + if err == nil { + break + } + glog.Error("GetMempoolTransaction ", err) + time.Sleep(time.Second * 5) } + for _, txid := range txs { b.Mempool.AddTransactionToMempool(txid) } @@ -238,26 +284,30 @@ func (b *EthereumRPC) subscribeEvents() error { if glog.V(2) { glog.Info("rpc: new tx ", hex) } - b.Mempool.AddTransactionToMempool(hex) - b.PushHandler(bchain.NotificationNewTx) + added := b.Mempool.AddTransactionToMempool(hex) + if added { + b.PushHandler(bchain.NotificationNewTx) + } } }() - // new mempool transaction subscription - if err := b.subscribe(func() (bchain.EVMClientSubscription, error) { - // invalidate the previous subscription - it is either the first one or there was an error - b.newTxSubscription = nil - ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) - defer cancel() - sub, err := b.RPC.EthSubscribe(ctx, b.NewTx.Channel(), "newPendingTransactions") - if err != nil { - return nil, errors.Annotatef(err, "EthSubscribe newPendingTransactions") + if !b.ChainConfig.DisableMempoolSync { + // new mempool transaction subscription + if err := b.subscribe(func() (bchain.EVMClientSubscription, error) { + // invalidate the previous subscription - it is either the first one or there was an error + b.newTxSubscription = nil + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) + defer cancel() + sub, err := b.RPC.EthSubscribe(ctx, b.NewTx.Channel(), "newPendingTransactions") + if err != nil { + return nil, errors.Annotatef(err, "EthSubscribe newPendingTransactions") + } + b.newTxSubscription = sub + glog.Info("Subscribed to newPendingTransactions") + return sub, nil + }); err != nil { + return err } - b.newTxSubscription = sub - glog.Info("Subscribed to newPendingTransactions") - return sub, nil - }); err != nil { - return err } return nil @@ -303,6 +353,27 @@ func (b *EthereumRPC) subscribe(f func() (bchain.EVMClientSubscription, error)) return nil } +func (b *EthereumRPC) initAlternativeFeeProvider() { + var err error + if b.ChainConfig.AlternativeEstimateFee == "1inch" { + if b.alternativeFeeProvider, err = NewOneInchFeesProvider(b, b.ChainConfig.AlternativeEstimateFeeParams); err != nil { + glog.Error("New1InchFeesProvider error ", err, " Reverting to default estimateFee functionality") + // disable AlternativeEstimateFee logic + b.alternativeFeeProvider = nil + } + } else if b.ChainConfig.AlternativeEstimateFee == "infura" { + if b.alternativeFeeProvider, err = NewInfuraFeesProvider(b, b.ChainConfig.AlternativeEstimateFeeParams); err != nil { + glog.Error("NewInfuraFeesProvider error ", err, " Reverting to default estimateFee functionality") + // disable AlternativeEstimateFee logic + b.alternativeFeeProvider = nil + } + } + if b.alternativeFeeProvider != nil { + glog.Info("Using alternative fee provider ", b.ChainConfig.AlternativeEstimateFee) + } + +} + func (b *EthereumRPC) closeRPC() { if b.newBlockSubscription != nil { b.newBlockSubscription.Unsubscribe() @@ -358,7 +429,7 @@ func (b *EthereumRPC) getConsensusVersion() string { glog.Error("getConsensusVersion ", err) return "" } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { glog.Error("getConsensusVersion ", err) return "" @@ -438,7 +509,7 @@ func (b *EthereumRPC) getBestHeader() (bchain.EVMHeader, error) { // UpdateBestHeader keeps track of the latest block header confirmed on chain func (b *EthereumRPC) UpdateBestHeader(h bchain.EVMHeader) { - glog.V(2).Info("rpc: new block header ", h.Number()) + glog.V(2).Info("rpc: new block header ", h.Number().Uint64()) b.bestHeaderLock.Lock() b.bestHeader = h b.bestHeaderTime = time.Now() @@ -545,7 +616,7 @@ func (b *EthereumRPC) getBlockRaw(hash string, height uint32, fullTxs bool) (jso } if err != nil { return nil, errors.Annotatef(err, "hash %v, height %v", hash, height) - } else if len(raw) == 0 { + } else if len(raw) == 0 || (len(raw) == 4 && string(raw) == "null") { return nil, bchain.ErrBlockNotFound } return raw, nil @@ -591,19 +662,25 @@ type rpcTraceResult struct { } func (b *EthereumRPC) getCreationContractInfo(contract string, height uint32) *bchain.ContractInfo { - ci, err := b.fetchContractInfo(contract) - if ci == nil || err != nil { - ci = &bchain.ContractInfo{ - Contract: contract, - } - } - ci.Type = bchain.UnknownTokenType + // do not fetch fetchContractInfo in sync, it slows it down + // the contract will be fetched only when asked by a client + // ci, err := b.fetchContractInfo(contract) + // if ci == nil || err != nil { + ci := &bchain.ContractInfo{ + Contract: contract, + } + // } + ci.Standard = bchain.UnhandledTokenStandard + ci.Type = bchain.UnhandledTokenStandard ci.CreatedInBlock = height return ci } func (b *EthereumRPC) processCallTrace(call *rpcCallTrace, d *bchain.EthereumInternalData, contracts []bchain.ContractInfo, blockHeight uint32) []bchain.ContractInfo { value, err := hexutil.DecodeBig(call.Value) + if err != nil { + value = new(big.Int) + } if call.Type == "CREATE" || call.Type == "CREATE2" { d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{ Type: bchain.CREATE, @@ -653,8 +730,28 @@ func (b *EthereumRPC) getInternalDataForBlock(blockHash string, blockHeight uint return data, contracts, err } if len(trace) != len(data) { - glog.Error("debug_traceBlockByHash block ", blockHash, ", error: trace length does not match block length ", len(trace), "!=", len(data)) - return data, contracts, err + if len(trace) < len(data) { + for i := range transactions { + tx := &transactions[i] + // bridging transactions in Polygon do not create trace and cause mismatch between the trace size and block size, it is necessary to adjust the trace size + // bridging transaction that from and to zero address + if tx.To == "0x0000000000000000000000000000000000000000" && tx.From == "0x0000000000000000000000000000000000000000" { + if i >= len(trace) { + trace = append(trace, rpcTraceResult{}) + } else { + trace = append(trace[:i+1], trace[i:]...) + trace[i] = rpcTraceResult{} + } + } + } + } + if len(trace) != len(data) { + e := fmt.Sprint("trace length does not match block length ", len(trace), "!=", len(data)) + glog.Error("debug_traceBlockByHash block ", blockHash, ", error: ", e) + return data, contracts, errors.New(e) + } else { + glog.Warning("debug_traceBlockByHash block ", blockHash, ", trace adjusted to match the number of transactions in block") + } } for i, result := range trace { r := &result.Result @@ -747,9 +844,7 @@ func (b *EthereumRPC) GetBlock(hash string, height uint32) (*bchain.Block, error return nil, errors.Annotatef(err, "hash %v, height %v, txid %v", hash, height, tx.Hash) } btxs[i] = *btx - if b.mempoolInitialized { - b.Mempool.RemoveTransactionFromMempool(tx.Hash) - } + b.removeTransactionFromMempool(tx.Hash) } bbk := bchain.Block{ BlockHeader: *bbh, @@ -791,19 +886,37 @@ func (b *EthereumRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) return b.GetTransaction(txid) } +func (b *EthereumRPC) removeTransactionFromMempool(txid string) { + // remove tx from mempool + if b.mempoolInitialized { + b.Mempool.RemoveTransactionFromMempool(txid) + } + // remove tx from mempool txs fetched by alternative method + if b.alternativeSendTxProvider != nil { + b.alternativeSendTxProvider.RemoveTransaction(txid) + } +} + // GetTransaction returns a transaction by the transaction ID. func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) { ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var tx *bchain.RpcTransaction + var txFound bool + var err error hash := ethcommon.HexToHash(txid) - err := b.RPC.CallContext(ctx, &tx, "eth_getTransactionByHash", hash) - if err != nil { - return nil, err - } else if tx == nil { - if b.mempoolInitialized { - b.Mempool.RemoveTransactionFromMempool(txid) + if b.alternativeSendTxProvider != nil { + tx, txFound = b.alternativeSendTxProvider.GetTransaction(txid) + } + if !txFound { + tx = &bchain.RpcTransaction{} + err = b.RPC.CallContext(ctx, tx, "eth_getTransactionByHash", hash) + if err != nil { + return nil, err } + } + if *tx == (bchain.RpcTransaction{}) { + b.removeTransactionFromMempool(txid) return nil, bchain.ErrTxNotFound } var btx *bchain.Tx @@ -820,7 +933,8 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) { return nil, err } var ht struct { - Time string `json:"timestamp"` + Time string `json:"timestamp"` + BaseFeePerGas string `json:"baseFeePerGas"` } if err := json.Unmarshal(raw, &ht); err != nil { return nil, errors.Annotatef(err, "hash %v", hash) @@ -829,6 +943,7 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) { if time, err = ethNumber(ht.Time); err != nil { return nil, errors.Annotatef(err, "txid %v", txid) } + tx.BaseFeePerGas = ht.BaseFeePerGas var receipt bchain.RpcReceipt err = b.RPC.CallContext(ctx, &receipt, "eth_getTransactionReceipt", hash) if err != nil { @@ -846,10 +961,7 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) { if err != nil { return nil, errors.Annotatef(err, "txid %v", txid) } - // remove tx from mempool if it is there - if b.mempoolInitialized { - b.Mempool.RemoveTransactionFromMempool(txid) - } + b.removeTransactionFromMempool(txid) } return btx, nil } @@ -938,26 +1050,142 @@ func (b *EthereumRPC) EthereumTypeEstimateGas(params map[string]interface{}) (ui if s, ok := GetStringFromMap("gasPrice", params); ok && len(s) > 0 { msg.GasPrice, _ = hexutil.DecodeBig(s) } + + if b.alternativeSendTxProvider != nil { + result, err := b.alternativeSendTxProvider.callHttpStringResult( + b.alternativeSendTxProvider.urls[0], + "eth_estimateGas", + params, + ) + if err == nil { + return hexutil.DecodeUint64(result) + } + } return b.Client.EstimateGas(ctx, msg) } +// EthereumTypeGetEip1559Fees retrieves Eip1559Fees, if supported +func (b *EthereumRPC) EthereumTypeGetEip1559Fees() (*bchain.Eip1559Fees, error) { + if !b.ChainConfig.Eip1559Fees { + return nil, nil + } + // if there is an alternative provider, use it + if b.alternativeFeeProvider != nil { + return b.alternativeFeeProvider.GetEip1559Fees() + } + + // otherwise use algorithm from here https://docs.alchemy.com/docs/how-to-build-a-gas-fee-estimator-using-eip-1559 + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) + defer cancel() + + var maxPriorityFeePerGas hexutil.Big + err := b.RPC.CallContext(ctx, &maxPriorityFeePerGas, "eth_maxPriorityFeePerGas") + if err != nil { + return nil, err + } + + var fees bchain.Eip1559Fees + + type history struct { + OldestBlock string `json:"oldestBlock"` + Reward [][]string `json:"reward"` + BaseFeePerGas []string `json:"baseFeePerGas"` + GasUsedRatio []float64 `json:"gasUsedRatio"` + } + var h history + percentiles := []int{ + 20, // low + 70, // medium + 90, // high + 99, // instant + } + blocks := 4 + + err = b.RPC.CallContext(ctx, &h, "eth_feeHistory", blocks, "pending", percentiles) + if err != nil { + return nil, err + } + if len(h.BaseFeePerGas) < blocks { + return nil, nil + } + + hs, _ := json.Marshal(h) + baseFee, _ := hexutil.DecodeUint64(h.BaseFeePerGas[blocks-1]) + fees.BaseFeePerGas = big.NewInt(int64(baseFee)) + maxBasePriorityFee := maxPriorityFeePerGas.ToInt().Int64() + glog.Info("eth_maxPriorityFeePerGas ", maxPriorityFeePerGas) + glog.Info("eth_feeHistory ", string(hs)) + + for i := 0; i < 4; i++ { + var f bchain.Eip1559Fee + priorityFee := int64(0) + for j := 0; j < len(h.Reward); j++ { + p, _ := hexutil.DecodeUint64(h.Reward[j][i]) + priorityFee += int64(p) + } + priorityFee = priorityFee / int64(len(h.Reward)) + f.MaxFeePerGas = big.NewInt(priorityFee) + f.MaxPriorityFeePerGas = big.NewInt(maxBasePriorityFee) + maxBasePriorityFee *= 2 + switch i { + case 0: + fees.Low = &f + case 1: + fees.Medium = &f + case 2: + fees.High = &f + default: + fees.Instant = &f + } + } + return &fees, err +} + // SendRawTransaction sends raw transaction -func (b *EthereumRPC) SendRawTransaction(hex string) (string, error) { +func (b *EthereumRPC) SendRawTransaction(hex string, disableAlternativeRPC bool) (string, error) { + var txid string + var retErr error + + if !disableAlternativeRPC && b.alternativeSendTxProvider != nil { + txid, retErr = b.alternativeSendTxProvider.SendRawTransaction(hex) + if retErr == nil { + return txid, nil + } + if b.alternativeSendTxProvider.UseOnlyAlternativeProvider() { + return txid, retErr + } + } + + txid, retErr = b.callRpcStringResult("eth_sendRawTransaction", hex) + if b.ChainConfig.DisableMempoolSync { + // add transactions submitted by us to mempool if sync is disabled + b.Mempool.AddTransactionToMempool(txid) + } + return txid, retErr +} + +// EthereumTypeGetRawTransaction gets raw transaction in hex format +func (b *EthereumRPC) EthereumTypeGetRawTransaction(txid string) (string, error) { + return b.callRpcStringResult("eth_getRawTransactionByHash", txid) +} + +// Helper function for calling ETH RPC with parameters and getting string result +func (b *EthereumRPC) callRpcStringResult(rpcMethod string, args ...interface{}) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var raw json.RawMessage - err := b.RPC.CallContext(ctx, &raw, "eth_sendRawTransaction", hex) + err := b.RPC.CallContext(ctx, &raw, rpcMethod, args...) if err != nil { return "", err } else if len(raw) == 0 { - return "", errors.New("SendRawTransaction: failed") + return "", errors.New(rpcMethod + " : failed") } var result string if err := json.Unmarshal(raw, &result); err != nil { return "", errors.Annotatef(err, "raw result %v", raw) } if result == "" { - return "", errors.New("SendRawTransaction: failed, empty result") + return "", errors.New(rpcMethod + " : failed, empty result") } return result, nil } @@ -971,9 +1199,41 @@ func (b *EthereumRPC) EthereumTypeGetBalance(addrDesc bchain.AddressDescriptor) // EthereumTypeGetNonce returns current balance of an address func (b *EthereumRPC) EthereumTypeGetNonce(addrDesc bchain.AddressDescriptor) (uint64, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) - defer cancel() - return b.Client.NonceAt(ctx, addrDesc, nil) + var result string + var err error + var usedAlternative bool + + ethAddress := ethcommon.BytesToAddress(addrDesc) + + if b.alternativeSendTxProvider != nil { + result, err = b.alternativeSendTxProvider.callHttpStringResult( + b.alternativeSendTxProvider.urls[0], + "eth_getTransactionCount", + ethAddress, + "pending", + ) + if err == nil && result != "" { + usedAlternative = true + } else { + glog.Errorf("Alternative provider failed for eth_getTransactionCount: %v, falling back to primary RPC", err) + } + } + + if !usedAlternative { + result, err = b.callRpcStringResult("eth_getTransactionCount", ethAddress, "pending") + if err != nil { + glog.Errorf("Primary RPC failed for eth_getTransactionCount: %v", err) + return 0, err + } + } + + nonce, err := hexutil.DecodeUint64(result) + if err != nil { + glog.Errorf("Failed to parse nonce result '%s': %v", result, err) + return 0, err + } + + return nonce, nil } // GetChainParser returns ethereum BlockChainParser diff --git a/bchain/coins/eth/ethtx.pb.go b/bchain/coins/eth/ethtx.pb.go index 6023a259b5..500d5a16ba 100644 --- a/bchain/coins/eth/ethtx.pb.go +++ b/bchain/coins/eth/ethtx.pb.go @@ -1,261 +1,506 @@ // Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.21.5 // source: bchain/coins/eth/ethtx.proto -/* -Package eth is a generated protocol buffer package. +package eth -It is generated from these files: - bchain/coins/eth/ethtx.proto +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) -It has these top-level messages: - ProtoCompleteTransaction -*/ -package eth +type ProtoCompleteTransaction struct { + state protoimpl.MessageState `protogen:"open.v1"` + BlockNumber uint32 `protobuf:"varint,1,opt,name=BlockNumber,proto3" json:"BlockNumber,omitempty"` + BlockTime uint64 `protobuf:"varint,2,opt,name=BlockTime,proto3" json:"BlockTime,omitempty"` + Tx *ProtoCompleteTransaction_TxType `protobuf:"bytes,3,opt,name=Tx,proto3" json:"Tx,omitempty"` + Receipt *ProtoCompleteTransaction_ReceiptType `protobuf:"bytes,4,opt,name=Receipt,proto3" json:"Receipt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" +func (x *ProtoCompleteTransaction) Reset() { + *x = ProtoCompleteTransaction{} + mi := &file_bchain_coins_eth_ethtx_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf +func (x *ProtoCompleteTransaction) String() string { + return protoimpl.X.MessageStringOf(x) +} -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package +func (*ProtoCompleteTransaction) ProtoMessage() {} -type ProtoCompleteTransaction struct { - BlockNumber uint32 `protobuf:"varint,1,opt,name=BlockNumber" json:"BlockNumber,omitempty"` - BlockTime uint64 `protobuf:"varint,2,opt,name=BlockTime" json:"BlockTime,omitempty"` - Tx *ProtoCompleteTransaction_TxType `protobuf:"bytes,3,opt,name=Tx" json:"Tx,omitempty"` - Receipt *ProtoCompleteTransaction_ReceiptType `protobuf:"bytes,4,opt,name=Receipt" json:"Receipt,omitempty"` +func (x *ProtoCompleteTransaction) ProtoReflect() protoreflect.Message { + mi := &file_bchain_coins_eth_ethtx_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -func (m *ProtoCompleteTransaction) Reset() { *m = ProtoCompleteTransaction{} } -func (m *ProtoCompleteTransaction) String() string { return proto.CompactTextString(m) } -func (*ProtoCompleteTransaction) ProtoMessage() {} -func (*ProtoCompleteTransaction) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } +// Deprecated: Use ProtoCompleteTransaction.ProtoReflect.Descriptor instead. +func (*ProtoCompleteTransaction) Descriptor() ([]byte, []int) { + return file_bchain_coins_eth_ethtx_proto_rawDescGZIP(), []int{0} +} -func (m *ProtoCompleteTransaction) GetBlockNumber() uint32 { - if m != nil { - return m.BlockNumber +func (x *ProtoCompleteTransaction) GetBlockNumber() uint32 { + if x != nil { + return x.BlockNumber } return 0 } -func (m *ProtoCompleteTransaction) GetBlockTime() uint64 { - if m != nil { - return m.BlockTime +func (x *ProtoCompleteTransaction) GetBlockTime() uint64 { + if x != nil { + return x.BlockTime } return 0 } -func (m *ProtoCompleteTransaction) GetTx() *ProtoCompleteTransaction_TxType { - if m != nil { - return m.Tx +func (x *ProtoCompleteTransaction) GetTx() *ProtoCompleteTransaction_TxType { + if x != nil { + return x.Tx } return nil } -func (m *ProtoCompleteTransaction) GetReceipt() *ProtoCompleteTransaction_ReceiptType { - if m != nil { - return m.Receipt +func (x *ProtoCompleteTransaction) GetReceipt() *ProtoCompleteTransaction_ReceiptType { + if x != nil { + return x.Receipt } return nil } type ProtoCompleteTransaction_TxType struct { - AccountNonce uint64 `protobuf:"varint,1,opt,name=AccountNonce" json:"AccountNonce,omitempty"` - GasPrice []byte `protobuf:"bytes,2,opt,name=GasPrice,proto3" json:"GasPrice,omitempty"` - GasLimit uint64 `protobuf:"varint,3,opt,name=GasLimit" json:"GasLimit,omitempty"` - Value []byte `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"` - Payload []byte `protobuf:"bytes,5,opt,name=Payload,proto3" json:"Payload,omitempty"` - Hash []byte `protobuf:"bytes,6,opt,name=Hash,proto3" json:"Hash,omitempty"` - To []byte `protobuf:"bytes,7,opt,name=To,proto3" json:"To,omitempty"` - From []byte `protobuf:"bytes,8,opt,name=From,proto3" json:"From,omitempty"` - TransactionIndex uint32 `protobuf:"varint,9,opt,name=TransactionIndex" json:"TransactionIndex,omitempty"` -} - -func (m *ProtoCompleteTransaction_TxType) Reset() { *m = ProtoCompleteTransaction_TxType{} } -func (m *ProtoCompleteTransaction_TxType) String() string { return proto.CompactTextString(m) } -func (*ProtoCompleteTransaction_TxType) ProtoMessage() {} + state protoimpl.MessageState `protogen:"open.v1"` + AccountNonce uint64 `protobuf:"varint,1,opt,name=AccountNonce,proto3" json:"AccountNonce,omitempty"` + GasPrice []byte `protobuf:"bytes,2,opt,name=GasPrice,proto3" json:"GasPrice,omitempty"` + GasLimit uint64 `protobuf:"varint,3,opt,name=GasLimit,proto3" json:"GasLimit,omitempty"` + Value []byte `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"` + Payload []byte `protobuf:"bytes,5,opt,name=Payload,proto3" json:"Payload,omitempty"` + Hash []byte `protobuf:"bytes,6,opt,name=Hash,proto3" json:"Hash,omitempty"` + To []byte `protobuf:"bytes,7,opt,name=To,proto3" json:"To,omitempty"` + From []byte `protobuf:"bytes,8,opt,name=From,proto3" json:"From,omitempty"` + TransactionIndex uint32 `protobuf:"varint,9,opt,name=TransactionIndex,proto3" json:"TransactionIndex,omitempty"` + MaxPriorityFeePerGas []byte `protobuf:"bytes,10,opt,name=MaxPriorityFeePerGas,proto3,oneof" json:"MaxPriorityFeePerGas,omitempty"` + MaxFeePerGas []byte `protobuf:"bytes,11,opt,name=MaxFeePerGas,proto3,oneof" json:"MaxFeePerGas,omitempty"` + BaseFeePerGas []byte `protobuf:"bytes,12,opt,name=BaseFeePerGas,proto3,oneof" json:"BaseFeePerGas,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProtoCompleteTransaction_TxType) Reset() { + *x = ProtoCompleteTransaction_TxType{} + mi := &file_bchain_coins_eth_ethtx_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProtoCompleteTransaction_TxType) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProtoCompleteTransaction_TxType) ProtoMessage() {} + +func (x *ProtoCompleteTransaction_TxType) ProtoReflect() protoreflect.Message { + mi := &file_bchain_coins_eth_ethtx_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProtoCompleteTransaction_TxType.ProtoReflect.Descriptor instead. func (*ProtoCompleteTransaction_TxType) Descriptor() ([]byte, []int) { - return fileDescriptor0, []int{0, 0} + return file_bchain_coins_eth_ethtx_proto_rawDescGZIP(), []int{0, 0} } -func (m *ProtoCompleteTransaction_TxType) GetAccountNonce() uint64 { - if m != nil { - return m.AccountNonce +func (x *ProtoCompleteTransaction_TxType) GetAccountNonce() uint64 { + if x != nil { + return x.AccountNonce } return 0 } -func (m *ProtoCompleteTransaction_TxType) GetGasPrice() []byte { - if m != nil { - return m.GasPrice +func (x *ProtoCompleteTransaction_TxType) GetGasPrice() []byte { + if x != nil { + return x.GasPrice } return nil } -func (m *ProtoCompleteTransaction_TxType) GetGasLimit() uint64 { - if m != nil { - return m.GasLimit +func (x *ProtoCompleteTransaction_TxType) GetGasLimit() uint64 { + if x != nil { + return x.GasLimit } return 0 } -func (m *ProtoCompleteTransaction_TxType) GetValue() []byte { - if m != nil { - return m.Value +func (x *ProtoCompleteTransaction_TxType) GetValue() []byte { + if x != nil { + return x.Value } return nil } -func (m *ProtoCompleteTransaction_TxType) GetPayload() []byte { - if m != nil { - return m.Payload +func (x *ProtoCompleteTransaction_TxType) GetPayload() []byte { + if x != nil { + return x.Payload } return nil } -func (m *ProtoCompleteTransaction_TxType) GetHash() []byte { - if m != nil { - return m.Hash +func (x *ProtoCompleteTransaction_TxType) GetHash() []byte { + if x != nil { + return x.Hash } return nil } -func (m *ProtoCompleteTransaction_TxType) GetTo() []byte { - if m != nil { - return m.To +func (x *ProtoCompleteTransaction_TxType) GetTo() []byte { + if x != nil { + return x.To } return nil } -func (m *ProtoCompleteTransaction_TxType) GetFrom() []byte { - if m != nil { - return m.From +func (x *ProtoCompleteTransaction_TxType) GetFrom() []byte { + if x != nil { + return x.From } return nil } -func (m *ProtoCompleteTransaction_TxType) GetTransactionIndex() uint32 { - if m != nil { - return m.TransactionIndex +func (x *ProtoCompleteTransaction_TxType) GetTransactionIndex() uint32 { + if x != nil { + return x.TransactionIndex } return 0 } +func (x *ProtoCompleteTransaction_TxType) GetMaxPriorityFeePerGas() []byte { + if x != nil { + return x.MaxPriorityFeePerGas + } + return nil +} + +func (x *ProtoCompleteTransaction_TxType) GetMaxFeePerGas() []byte { + if x != nil { + return x.MaxFeePerGas + } + return nil +} + +func (x *ProtoCompleteTransaction_TxType) GetBaseFeePerGas() []byte { + if x != nil { + return x.BaseFeePerGas + } + return nil +} + type ProtoCompleteTransaction_ReceiptType struct { - GasUsed []byte `protobuf:"bytes,1,opt,name=GasUsed,proto3" json:"GasUsed,omitempty"` - Status []byte `protobuf:"bytes,2,opt,name=Status,proto3" json:"Status,omitempty"` - Log []*ProtoCompleteTransaction_ReceiptType_LogType `protobuf:"bytes,3,rep,name=Log" json:"Log,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + GasUsed []byte `protobuf:"bytes,1,opt,name=GasUsed,proto3" json:"GasUsed,omitempty"` + Status []byte `protobuf:"bytes,2,opt,name=Status,proto3" json:"Status,omitempty"` + Log []*ProtoCompleteTransaction_ReceiptType_LogType `protobuf:"bytes,3,rep,name=Log,proto3" json:"Log,omitempty"` + L1Fee []byte `protobuf:"bytes,4,opt,name=L1Fee,proto3,oneof" json:"L1Fee,omitempty"` + L1FeeScalar []byte `protobuf:"bytes,5,opt,name=L1FeeScalar,proto3,oneof" json:"L1FeeScalar,omitempty"` + L1GasPrice []byte `protobuf:"bytes,6,opt,name=L1GasPrice,proto3,oneof" json:"L1GasPrice,omitempty"` + L1GasUsed []byte `protobuf:"bytes,7,opt,name=L1GasUsed,proto3,oneof" json:"L1GasUsed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProtoCompleteTransaction_ReceiptType) Reset() { + *x = ProtoCompleteTransaction_ReceiptType{} + mi := &file_bchain_coins_eth_ethtx_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *ProtoCompleteTransaction_ReceiptType) Reset() { *m = ProtoCompleteTransaction_ReceiptType{} } -func (m *ProtoCompleteTransaction_ReceiptType) String() string { return proto.CompactTextString(m) } -func (*ProtoCompleteTransaction_ReceiptType) ProtoMessage() {} +func (x *ProtoCompleteTransaction_ReceiptType) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProtoCompleteTransaction_ReceiptType) ProtoMessage() {} + +func (x *ProtoCompleteTransaction_ReceiptType) ProtoReflect() protoreflect.Message { + mi := &file_bchain_coins_eth_ethtx_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProtoCompleteTransaction_ReceiptType.ProtoReflect.Descriptor instead. func (*ProtoCompleteTransaction_ReceiptType) Descriptor() ([]byte, []int) { - return fileDescriptor0, []int{0, 1} + return file_bchain_coins_eth_ethtx_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *ProtoCompleteTransaction_ReceiptType) GetGasUsed() []byte { + if x != nil { + return x.GasUsed + } + return nil +} + +func (x *ProtoCompleteTransaction_ReceiptType) GetStatus() []byte { + if x != nil { + return x.Status + } + return nil +} + +func (x *ProtoCompleteTransaction_ReceiptType) GetLog() []*ProtoCompleteTransaction_ReceiptType_LogType { + if x != nil { + return x.Log + } + return nil } -func (m *ProtoCompleteTransaction_ReceiptType) GetGasUsed() []byte { - if m != nil { - return m.GasUsed +func (x *ProtoCompleteTransaction_ReceiptType) GetL1Fee() []byte { + if x != nil { + return x.L1Fee } return nil } -func (m *ProtoCompleteTransaction_ReceiptType) GetStatus() []byte { - if m != nil { - return m.Status +func (x *ProtoCompleteTransaction_ReceiptType) GetL1FeeScalar() []byte { + if x != nil { + return x.L1FeeScalar } return nil } -func (m *ProtoCompleteTransaction_ReceiptType) GetLog() []*ProtoCompleteTransaction_ReceiptType_LogType { - if m != nil { - return m.Log +func (x *ProtoCompleteTransaction_ReceiptType) GetL1GasPrice() []byte { + if x != nil { + return x.L1GasPrice + } + return nil +} + +func (x *ProtoCompleteTransaction_ReceiptType) GetL1GasUsed() []byte { + if x != nil { + return x.L1GasUsed } return nil } type ProtoCompleteTransaction_ReceiptType_LogType struct { - Address []byte `protobuf:"bytes,1,opt,name=Address,proto3" json:"Address,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"` - Topics [][]byte `protobuf:"bytes,3,rep,name=Topics,proto3" json:"Topics,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Address []byte `protobuf:"bytes,1,opt,name=Address,proto3" json:"Address,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"` + Topics [][]byte `protobuf:"bytes,3,rep,name=Topics,proto3" json:"Topics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (m *ProtoCompleteTransaction_ReceiptType_LogType) Reset() { - *m = ProtoCompleteTransaction_ReceiptType_LogType{} +func (x *ProtoCompleteTransaction_ReceiptType_LogType) Reset() { + *x = ProtoCompleteTransaction_ReceiptType_LogType{} + mi := &file_bchain_coins_eth_ethtx_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (m *ProtoCompleteTransaction_ReceiptType_LogType) String() string { - return proto.CompactTextString(m) + +func (x *ProtoCompleteTransaction_ReceiptType_LogType) String() string { + return protoimpl.X.MessageStringOf(x) } + func (*ProtoCompleteTransaction_ReceiptType_LogType) ProtoMessage() {} + +func (x *ProtoCompleteTransaction_ReceiptType_LogType) ProtoReflect() protoreflect.Message { + mi := &file_bchain_coins_eth_ethtx_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProtoCompleteTransaction_ReceiptType_LogType.ProtoReflect.Descriptor instead. func (*ProtoCompleteTransaction_ReceiptType_LogType) Descriptor() ([]byte, []int) { - return fileDescriptor0, []int{0, 1, 0} + return file_bchain_coins_eth_ethtx_proto_rawDescGZIP(), []int{0, 1, 0} } -func (m *ProtoCompleteTransaction_ReceiptType_LogType) GetAddress() []byte { - if m != nil { - return m.Address +func (x *ProtoCompleteTransaction_ReceiptType_LogType) GetAddress() []byte { + if x != nil { + return x.Address } return nil } -func (m *ProtoCompleteTransaction_ReceiptType_LogType) GetData() []byte { - if m != nil { - return m.Data +func (x *ProtoCompleteTransaction_ReceiptType_LogType) GetData() []byte { + if x != nil { + return x.Data } return nil } -func (m *ProtoCompleteTransaction_ReceiptType_LogType) GetTopics() [][]byte { - if m != nil { - return m.Topics +func (x *ProtoCompleteTransaction_ReceiptType_LogType) GetTopics() [][]byte { + if x != nil { + return x.Topics } return nil } -func init() { - proto.RegisterType((*ProtoCompleteTransaction)(nil), "eth.ProtoCompleteTransaction") - proto.RegisterType((*ProtoCompleteTransaction_TxType)(nil), "eth.ProtoCompleteTransaction.TxType") - proto.RegisterType((*ProtoCompleteTransaction_ReceiptType)(nil), "eth.ProtoCompleteTransaction.ReceiptType") - proto.RegisterType((*ProtoCompleteTransaction_ReceiptType_LogType)(nil), "eth.ProtoCompleteTransaction.ReceiptType.LogType") -} - -func init() { proto.RegisterFile("bchain/coins/eth/ethtx.proto", fileDescriptor0) } - -var fileDescriptor0 = []byte{ - // 409 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0xdf, 0x8a, 0xd4, 0x30, - 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, +var File_bchain_coins_eth_ethtx_proto protoreflect.FileDescriptor + +var file_bchain_coins_eth_ethtx_proto_rawDesc = string([]byte{ + 0x0a, 0x1c, 0x62, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x2f, 0x65, + 0x74, 0x68, 0x2f, 0x65, 0x74, 0x68, 0x74, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa6, + 0x08, 0x0a, 0x18, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x42, + 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x0b, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1c, 0x0a, + 0x09, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x09, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x02, 0x54, + 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, 0x52, 0x02, 0x54, 0x78, 0x12, 0x3f, 0x0a, + 0x07, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, + 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, + 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x1a, 0xc1, + 0x03, 0x0a, 0x06, 0x54, 0x78, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x47, 0x61, 0x73, 0x50, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x08, 0x47, 0x61, 0x73, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x47, 0x61, 0x73, + 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x47, 0x61, 0x73, + 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x50, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x50, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x48, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x48, 0x61, 0x73, 0x68, 0x12, 0x0e, 0x0a, 0x02, 0x54, 0x6f, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x46, 0x72, 0x6f, + 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x2a, 0x0a, + 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, + 0x78, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x37, 0x0a, 0x14, 0x4d, 0x61, 0x78, + 0x50, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, 0x61, + 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x14, 0x4d, 0x61, 0x78, 0x50, 0x72, + 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, 0x61, 0x73, 0x88, + 0x01, 0x01, 0x12, 0x27, 0x0a, 0x0c, 0x4d, 0x61, 0x78, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, + 0x61, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x0c, 0x4d, 0x61, 0x78, 0x46, + 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, 0x61, 0x73, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0d, 0x42, + 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, 0x61, 0x73, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x0c, 0x48, 0x02, 0x52, 0x0d, 0x42, 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, + 0x47, 0x61, 0x73, 0x88, 0x01, 0x01, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x4d, 0x61, 0x78, 0x50, 0x72, + 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, 0x61, 0x73, 0x42, + 0x0f, 0x0a, 0x0d, 0x5f, 0x4d, 0x61, 0x78, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, 0x61, 0x73, + 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x42, 0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x50, 0x65, 0x72, 0x47, + 0x61, 0x73, 0x1a, 0x92, 0x03, 0x0a, 0x0b, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x47, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x07, 0x47, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x3f, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x2d, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x63, + 0x65, 0x69, 0x70, 0x74, 0x54, 0x79, 0x70, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x54, 0x79, 0x70, 0x65, + 0x52, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x19, 0x0a, 0x05, 0x4c, 0x31, 0x46, 0x65, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x05, 0x4c, 0x31, 0x46, 0x65, 0x65, 0x88, 0x01, 0x01, + 0x12, 0x25, 0x0a, 0x0b, 0x4c, 0x31, 0x46, 0x65, 0x65, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x0b, 0x4c, 0x31, 0x46, 0x65, 0x65, 0x53, 0x63, + 0x61, 0x6c, 0x61, 0x72, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0a, 0x4c, 0x31, 0x47, 0x61, 0x73, + 0x50, 0x72, 0x69, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x02, 0x52, 0x0a, 0x4c, + 0x31, 0x47, 0x61, 0x73, 0x50, 0x72, 0x69, 0x63, 0x65, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, + 0x4c, 0x31, 0x47, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x48, + 0x03, 0x52, 0x09, 0x4c, 0x31, 0x47, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x88, 0x01, 0x01, 0x1a, + 0x4f, 0x0a, 0x07, 0x4c, 0x6f, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x04, 0x44, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x54, 0x6f, 0x70, 0x69, + 0x63, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x06, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x73, + 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x4c, 0x31, 0x46, 0x65, 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x4c, + 0x31, 0x46, 0x65, 0x65, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x4c, + 0x31, 0x47, 0x61, 0x73, 0x50, 0x72, 0x69, 0x63, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x4c, 0x31, + 0x47, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x42, 0x12, 0x5a, 0x10, 0x62, 0x63, 0x68, 0x61, 0x69, + 0x6e, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x2f, 0x65, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +}) + +var ( + file_bchain_coins_eth_ethtx_proto_rawDescOnce sync.Once + file_bchain_coins_eth_ethtx_proto_rawDescData []byte +) + +func file_bchain_coins_eth_ethtx_proto_rawDescGZIP() []byte { + file_bchain_coins_eth_ethtx_proto_rawDescOnce.Do(func() { + file_bchain_coins_eth_ethtx_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_bchain_coins_eth_ethtx_proto_rawDesc), len(file_bchain_coins_eth_ethtx_proto_rawDesc))) + }) + return file_bchain_coins_eth_ethtx_proto_rawDescData +} + +var file_bchain_coins_eth_ethtx_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_bchain_coins_eth_ethtx_proto_goTypes = []any{ + (*ProtoCompleteTransaction)(nil), // 0: ProtoCompleteTransaction + (*ProtoCompleteTransaction_TxType)(nil), // 1: ProtoCompleteTransaction.TxType + (*ProtoCompleteTransaction_ReceiptType)(nil), // 2: ProtoCompleteTransaction.ReceiptType + (*ProtoCompleteTransaction_ReceiptType_LogType)(nil), // 3: ProtoCompleteTransaction.ReceiptType.LogType +} +var file_bchain_coins_eth_ethtx_proto_depIdxs = []int32{ + 1, // 0: ProtoCompleteTransaction.Tx:type_name -> ProtoCompleteTransaction.TxType + 2, // 1: ProtoCompleteTransaction.Receipt:type_name -> ProtoCompleteTransaction.ReceiptType + 3, // 2: ProtoCompleteTransaction.ReceiptType.Log:type_name -> ProtoCompleteTransaction.ReceiptType.LogType + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_bchain_coins_eth_ethtx_proto_init() } +func file_bchain_coins_eth_ethtx_proto_init() { + if File_bchain_coins_eth_ethtx_proto != nil { + return + } + file_bchain_coins_eth_ethtx_proto_msgTypes[1].OneofWrappers = []any{} + file_bchain_coins_eth_ethtx_proto_msgTypes[2].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_bchain_coins_eth_ethtx_proto_rawDesc), len(file_bchain_coins_eth_ethtx_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_bchain_coins_eth_ethtx_proto_goTypes, + DependencyIndexes: file_bchain_coins_eth_ethtx_proto_depIdxs, + MessageInfos: file_bchain_coins_eth_ethtx_proto_msgTypes, + }.Build() + File_bchain_coins_eth_ethtx_proto = out.File + file_bchain_coins_eth_ethtx_proto_goTypes = nil + file_bchain_coins_eth_ethtx_proto_depIdxs = nil } diff --git a/bchain/coins/eth/ethtx.proto b/bchain/coins/eth/ethtx.proto index ef7c4ce09d..3a3cbfe2ce 100644 --- a/bchain/coins/eth/ethtx.proto +++ b/bchain/coins/eth/ethtx.proto @@ -1,30 +1,37 @@ syntax = "proto3"; - package eth; - - message ProtoCompleteTransaction { - message TxType { - uint64 AccountNonce = 1; - bytes GasPrice = 2; - uint64 GasLimit = 3; - bytes Value = 4; - bytes Payload = 5; - bytes Hash = 6; - bytes To = 7; - bytes From = 8; - uint32 TransactionIndex = 9; - } - message ReceiptType { - message LogType { - bytes Address = 1; - bytes Data = 2; - repeated bytes Topics = 3; - } - bytes GasUsed = 1; - bytes Status = 2; - repeated LogType Log = 3; - } - uint32 BlockNumber = 1; - uint64 BlockTime = 2; - TxType Tx = 3; - ReceiptType Receipt = 4; - } \ No newline at end of file +option go_package = "bchain/coins/eth"; + +message ProtoCompleteTransaction { + message TxType { + uint64 AccountNonce = 1; + bytes GasPrice = 2; + uint64 GasLimit = 3; + bytes Value = 4; + bytes Payload = 5; + bytes Hash = 6; + bytes To = 7; + bytes From = 8; + uint32 TransactionIndex = 9; + optional bytes MaxPriorityFeePerGas = 10; + optional bytes MaxFeePerGas = 11; + optional bytes BaseFeePerGas = 12; + } + message ReceiptType { + message LogType { + bytes Address = 1; + bytes Data = 2; + repeated bytes Topics = 3; + } + bytes GasUsed = 1; + bytes Status = 2; + repeated LogType Log = 3; + optional bytes L1Fee = 4; + optional bytes L1FeeScalar = 5; + optional bytes L1GasPrice = 6; + optional bytes L1GasUsed = 7; + } + uint32 BlockNumber = 1; + uint64 BlockTime = 2; + TxType Tx = 3; + ReceiptType Receipt = 4; +} \ No newline at end of file diff --git a/bchain/coins/eth/evm.go b/bchain/coins/eth/evm.go index 1304b24325..6276a9c237 100644 --- a/bchain/coins/eth/evm.go +++ b/bchain/coins/eth/evm.go @@ -92,6 +92,11 @@ type EthereumNewBlock struct { channel chan *types.Header } +// NewEthereumNewBlock returns an initialized EthereumNewBlock struct +func NewEthereumNewBlock() *EthereumNewBlock { + return &EthereumNewBlock{channel: make(chan *types.Header)} +} + // Channel returns the underlying channel as an empty interface func (s *EthereumNewBlock) Channel() interface{} { return s.channel @@ -113,6 +118,11 @@ type EthereumNewTx struct { channel chan common.Hash } +// NewEthereumNewTx returns an initialized EthereumNewTx struct +func NewEthereumNewTx() *EthereumNewTx { + return &EthereumNewTx{channel: make(chan common.Hash)} +} + // Channel returns the underlying channel as an empty interface func (s *EthereumNewTx) Channel() interface{} { return s.channel diff --git a/bchain/coins/eth/infurafees.go b/bchain/coins/eth/infurafees.go new file mode 100644 index 0000000000..53443e6160 --- /dev/null +++ b/bchain/coins/eth/infurafees.go @@ -0,0 +1,192 @@ +package eth + +import ( + "bytes" + "encoding/json" + "math/big" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/common" +) + +// https://gas.api.infura.io/v3/${api_key}/networks/1/suggestedGasFees returns +// { +// "low": { +// "suggestedMaxPriorityFeePerGas": "0.01128", +// "suggestedMaxFeePerGas": "9.919888552", +// "minWaitTimeEstimate": 15000, +// "maxWaitTimeEstimate": 60000 +// }, +// "medium": { +// "suggestedMaxPriorityFeePerGas": "1.148315423", +// "suggestedMaxFeePerGas": "15.317625653", +// "minWaitTimeEstimate": 15000, +// "maxWaitTimeEstimate": 45000 +// }, +// "high": { +// "suggestedMaxPriorityFeePerGas": "2", +// "suggestedMaxFeePerGas": "24.78979967", +// "minWaitTimeEstimate": 15000, +// "maxWaitTimeEstimate": 30000 +// }, +// "estimatedBaseFee": "9.908608552", +// "networkCongestion": 0.004, +// "latestPriorityFeeRange": [ +// "0.05", +// "4" +// ], +// "historicalPriorityFeeRange": [ +// "0.006381976", +// "155.777346207" +// ], +// "historicalBaseFeeRange": [ +// "9.243163495", +// "16.734915363" +// ], +// "priorityFeeTrend": "up", +// "baseFeeTrend": "up", +// "version": "0.0.1" +// } + +type infuraFeeResult struct { + MaxPriorityFeePerGas string `json:"suggestedMaxPriorityFeePerGas"` + MaxFeePerGas string `json:"suggestedMaxFeePerGas"` + MinWaitTimeEstimate int `json:"minWaitTimeEstimate"` + MaxWaitTimeEstimate int `json:"maxWaitTimeEstimate"` +} + +type infuraFeesResult struct { + BaseFee string `json:"estimatedBaseFee"` + Low infuraFeeResult `json:"low"` + Medium infuraFeeResult `json:"medium"` + High infuraFeeResult `json:"high"` + NetworkCongestion float64 `json:"networkCongestion"` + LatestPriorityFeeRange []string `json:"latestPriorityFeeRange"` + HistoricalPriorityFeeRange []string `json:"historicalPriorityFeeRange"` + HistoricalBaseFeeRange []string `json:"historicalBaseFeeRange"` + PriorityFeeTrend string `json:"priorityFeeTrend"` + BaseFeeTrend string `json:"baseFeeTrend"` +} + +type infuraFeeParams struct { + URL string `json:"url"` + PeriodSeconds int `json:"periodSeconds"` +} + +type infuraFeeProvider struct { + *alternativeFeeProvider + params infuraFeeParams + apiKey string +} + +// NewInfuraFeesProvider initializes https://gas.api.infura.io provider +func NewInfuraFeesProvider(chain bchain.BlockChain, params string) (alternativeFeeProviderInterface, error) { + p := &infuraFeeProvider{alternativeFeeProvider: &alternativeFeeProvider{}} + err := json.Unmarshal([]byte(params), &p.params) + if err != nil { + return nil, err + } + if p.params.URL == "" || p.params.PeriodSeconds == 0 { + return nil, errors.New("NewInfuraFeesProvider: missing config parameters 'url' or 'periodSeconds'.") + } + p.apiKey = os.Getenv("INFURA_API_KEY") + if p.apiKey == "" { + return nil, errors.New("NewInfuraFeesProvider: missing INFURA_API_KEY env variable.") + } + p.params.URL = strings.Replace(p.params.URL, "${api_key}", p.apiKey, -1) + p.chain = chain + // if the data are not successfully downloaded 10 times, stop providing data + p.staleSyncDuration = time.Duration(p.params.PeriodSeconds*10) * time.Second + go p.FeeDownloader() + return p, nil +} + +func (p *infuraFeeProvider) FeeDownloader() { + period := time.Duration(p.params.PeriodSeconds) * time.Second + timer := time.NewTimer(period) + for { + var data infuraFeesResult + err := p.getData(&data) + if err != nil { + glog.Error("infuraFeeProvider.FeeDownloader ", err) + } else { + p.processData(&data) + } + <-timer.C + timer.Reset(period) + } +} + +func bigIntFromFloatString(s string) *big.Int { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil + } + return big.NewInt(int64(f * 1e9)) +} + +func infuraFeesFromResult(result *infuraFeeResult) *bchain.Eip1559Fee { + fee := bchain.Eip1559Fee{} + fee.MaxFeePerGas = bigIntFromFloatString(result.MaxFeePerGas) + fee.MaxPriorityFeePerGas = bigIntFromFloatString(result.MaxPriorityFeePerGas) + fee.MinWaitTimeEstimate = result.MinWaitTimeEstimate + fee.MaxWaitTimeEstimate = result.MaxWaitTimeEstimate + return &fee +} + +func rangeFromString(feeRange []string) []*big.Int { + if feeRange == nil { + return nil + } + result := make([]*big.Int, len(feeRange)) + for i := range feeRange { + result[i] = bigIntFromFloatString(feeRange[i]) + } + return result +} + +func (p *infuraFeeProvider) processData(data *infuraFeesResult) bool { + fees := bchain.Eip1559Fees{} + fees.BaseFeePerGas = bigIntFromFloatString(data.BaseFee) + fees.High = infuraFeesFromResult(&data.High) + fees.Medium = infuraFeesFromResult(&data.Medium) + fees.Low = infuraFeesFromResult(&data.Low) + fees.NetworkCongestion = data.NetworkCongestion + fees.LatestPriorityFeeRange = rangeFromString(data.LatestPriorityFeeRange) + fees.HistoricalPriorityFeeRange = rangeFromString(data.HistoricalPriorityFeeRange) + fees.HistoricalBaseFeeRange = rangeFromString(data.HistoricalBaseFeeRange) + fees.PriorityFeeTrend = data.PriorityFeeTrend + fees.BaseFeeTrend = data.BaseFeeTrend + p.mux.Lock() + defer p.mux.Unlock() + p.lastSync = time.Now() + p.eip1559Fees = &fees + return true +} + +func (p *infuraFeeProvider) getData(res interface{}) error { + var httpData []byte + httpReq, err := http.NewRequest("GET", p.params.URL, bytes.NewBuffer(httpData)) + if err != nil { + return err + } + httpReq.Header.Set("Content-Type", "application/json") + httpRes, err := http.DefaultClient.Do(httpReq) + if httpRes != nil { + defer httpRes.Body.Close() + } + if err != nil { + return err + } + if httpRes.StatusCode != http.StatusOK { + return errors.New(p.params.URL + " returned status " + strconv.Itoa(httpRes.StatusCode)) + } + return common.SafeDecodeResponseFromReader(httpRes.Body, &res) +} diff --git a/bchain/coins/eth/oneinchfees.go b/bchain/coins/eth/oneinchfees.go new file mode 100644 index 0000000000..e7bcecabcb --- /dev/null +++ b/bchain/coins/eth/oneinchfees.go @@ -0,0 +1,144 @@ +package eth + +import ( + "bytes" + "encoding/json" + "math/big" + "net/http" + "os" + "strconv" + "time" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/common" +) + +// https://api.1inch.dev/gas-price/v1.5/1 returns +// { +// "baseFee": "12456587953", +// "low": { +// "maxPriorityFeePerGas": "1000000", +// "maxFeePerGas": "14948905543" +// }, +// "medium": { +// "maxPriorityFeePerGas": "2000000", +// "maxFeePerGas": "14949905543" +// }, +// "high": { +// "maxPriorityFeePerGas": "5000000", +// "maxFeePerGas": "14952905543" +// }, +// "instant": { +// "maxPriorityFeePerGas": "10000000", +// "maxFeePerGas": "29905811086" +// } +// } + +type oneInchFeeFeeResult struct { + MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas"` + MaxFeePerGas string `json:"maxFeePerGas"` +} + +type oneInchFeeFeesResult struct { + BaseFee string `json:"baseFee"` + Low oneInchFeeFeeResult `json:"low"` + Medium oneInchFeeFeeResult `json:"medium"` + High oneInchFeeFeeResult `json:"high"` + Instant oneInchFeeFeeResult `json:"instant"` +} + +type oneInchFeeParams struct { + URL string `json:"url"` + PeriodSeconds int `json:"periodSeconds"` +} + +type oneInchFeeProvider struct { + *alternativeFeeProvider + params oneInchFeeParams + apiKey string +} + +// NewOneInchFeesProvider initializes https://api.1inch.dev provider +func NewOneInchFeesProvider(chain bchain.BlockChain, params string) (alternativeFeeProviderInterface, error) { + p := &oneInchFeeProvider{alternativeFeeProvider: &alternativeFeeProvider{}} + err := json.Unmarshal([]byte(params), &p.params) + if err != nil { + return nil, err + } + if p.params.URL == "" || p.params.PeriodSeconds == 0 { + return nil, errors.New("NewOneInchFeesProvider: missing config parameters 'url' or 'periodSeconds'.") + } + p.apiKey = os.Getenv("ONE_INCH_API_KEY") + if p.apiKey == "" { + return nil, errors.New("NewOneInchFeesProvider: missing ONE_INCH_API_KEY env variable.") + } + p.chain = chain + go p.FeeDownloader() + return p, nil +} + +func (p *oneInchFeeProvider) FeeDownloader() { + period := time.Duration(p.params.PeriodSeconds) * time.Second + timer := time.NewTimer(period) + for { + var data oneInchFeeFeesResult + err := p.getData(&data) + if err != nil { + glog.Error("oneInchFeeProvider.FeeDownloader", err) + } else { + p.processData(&data) + } + <-timer.C + timer.Reset(period) + } +} + +func bigIntFromString(s string) *big.Int { + b := big.NewInt(0) + b, _ = b.SetString(s, 10) + return b +} + +func oneInchFeesFromResult(result *oneInchFeeFeeResult) *bchain.Eip1559Fee { + fee := bchain.Eip1559Fee{} + fee.MaxFeePerGas = bigIntFromString(result.MaxFeePerGas) + fee.MaxPriorityFeePerGas = bigIntFromString(result.MaxPriorityFeePerGas) + return &fee +} + +func (p *oneInchFeeProvider) processData(data *oneInchFeeFeesResult) bool { + fees := bchain.Eip1559Fees{} + fees.BaseFeePerGas = bigIntFromString(data.BaseFee) + fees.Instant = oneInchFeesFromResult(&data.Instant) + fees.High = oneInchFeesFromResult(&data.High) + fees.Medium = oneInchFeesFromResult(&data.Medium) + fees.Low = oneInchFeesFromResult(&data.Low) + p.mux.Lock() + defer p.mux.Unlock() + p.lastSync = time.Now() + p.eip1559Fees = &fees + return true +} + +func (p *oneInchFeeProvider) getData(res interface{}) error { + var httpData []byte + httpReq, err := http.NewRequest("GET", p.params.URL, bytes.NewBuffer(httpData)) + if err != nil { + return err + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", " Bearer "+p.apiKey) + httpRes, err := http.DefaultClient.Do(httpReq) + if httpRes != nil { + defer httpRes.Body.Close() + } + if err != nil { + return err + } + if httpRes.StatusCode != http.StatusOK { + return errors.New(p.params.URL + " returned status " + strconv.Itoa(httpRes.StatusCode)) + } + return common.SafeDecodeResponseFromReader(httpRes.Body, &res) +} diff --git a/bchain/coins/eth/stakingpool.go b/bchain/coins/eth/stakingpool.go new file mode 100644 index 0000000000..659307c877 --- /dev/null +++ b/bchain/coins/eth/stakingpool.go @@ -0,0 +1,146 @@ +package eth + +import ( + "math/big" + "os" + "strings" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" +) + +func (b *EthereumRPC) initStakingPools() error { + network := b.ChainConfig.Network + if network == "" { + network = b.ChainConfig.CoinShortcut + } + // for now only single staking pool + envVar := strings.ToUpper(network) + "_STAKING_POOL_CONTRACT" + envValue := os.Getenv(envVar) + if envValue != "" { + parts := strings.Split(envValue, "/") + if len(parts) != 2 { + glog.Errorf("Wrong format of environment variable %s=%s, expecting value '/', staking pools not enabled", envVar, envValue) + return nil + } + b.supportedStakingPools = []string{envValue} + b.stakingPoolNames = []string{parts[0]} + b.stakingPoolContracts = []string{parts[1]} + glog.Info("Support of staking pools enabled with these pools: ", b.supportedStakingPools) + } + return nil +} + +func (b *EthereumRPC) EthereumTypeGetSupportedStakingPools() []string { + return b.supportedStakingPools +} + +func (b *EthereumRPC) EthereumTypeGetStakingPoolsData(addrDesc bchain.AddressDescriptor) ([]bchain.StakingPoolData, error) { + // for now only single staking pool - Everstake + addr := hexutil.Encode(addrDesc)[2:] + if len(b.supportedStakingPools) == 1 { + data, err := b.everstakePoolData(addr, b.stakingPoolContracts[0], b.stakingPoolNames[0]) + if err != nil { + return nil, err + } + if data != nil { + return []bchain.StakingPoolData{*data}, nil + } + } + return nil, nil +} + +const everstakePendingBalanceOfMethodSignature = "0x59b8c763" // pendingBalanceOf(address) +const everstakePendingDepositedBalanceOfMethodSignature = "0x80f14ecc" // pendingDepositedBalanceOf(address) +const everstakeDepositedBalanceOfMethodSignature = "0x68b48254" // depositedBalanceOf(address) +const everstakeWithdrawRequestMethodSignature = "0x14cbc46a" // withdrawRequest(address) +const everstakeRestakedRewardOfMethodSignature = "0x0c98929a" // restakedRewardOf(address) +const everstakeAutocompoundBalanceOfMethodSignature = "0x2fec7966" // autocompoundBalanceOf(address) + +func isZeroBigInt(b *big.Int) bool { + return len(b.Bits()) == 0 +} + +func (b *EthereumRPC) everstakeBalanceTypeContractCall(signature, addr, contract string) (string, error) { + req := signature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr + return b.EthereumTypeRpcCall(req, contract, "") +} + +func (b *EthereumRPC) everstakeContractCallSimpleNumeric(signature, addr, contract string) (*big.Int, error) { + data, err := b.everstakeBalanceTypeContractCall(signature, addr, contract) + if err != nil { + return nil, err + } + r := parseSimpleNumericProperty(data) + if r == nil { + return nil, errors.New("Invalid balance") + } + return r, nil +} + +func (b *EthereumRPC) everstakePoolData(addr, contract, name string) (*bchain.StakingPoolData, error) { + poolData := bchain.StakingPoolData{ + Contract: contract, + Name: name, + } + allZeros := true + + value, err := b.everstakeContractCallSimpleNumeric(everstakePendingBalanceOfMethodSignature, addr, contract) + if err != nil { + return nil, err + } + poolData.PendingBalance = *value + allZeros = allZeros && isZeroBigInt(value) + + value, err = b.everstakeContractCallSimpleNumeric(everstakePendingDepositedBalanceOfMethodSignature, addr, contract) + if err != nil { + return nil, err + } + poolData.PendingDepositedBalance = *value + allZeros = allZeros && isZeroBigInt(value) + + value, err = b.everstakeContractCallSimpleNumeric(everstakeDepositedBalanceOfMethodSignature, addr, contract) + if err != nil { + return nil, err + } + poolData.DepositedBalance = *value + allZeros = allZeros && isZeroBigInt(value) + + data, err := b.everstakeBalanceTypeContractCall(everstakeWithdrawRequestMethodSignature, addr, contract) + if err != nil { + return nil, err + } + value = parseSimpleNumericProperty(data) + if value == nil { + return nil, errors.New("Invalid balance") + } + poolData.WithdrawTotalAmount = *value + allZeros = allZeros && isZeroBigInt(value) + value = parseSimpleNumericProperty(data[64+2:]) + if value == nil { + return nil, errors.New("Invalid balance") + } + poolData.ClaimableAmount = *value + allZeros = allZeros && isZeroBigInt(value) + + value, err = b.everstakeContractCallSimpleNumeric(everstakeRestakedRewardOfMethodSignature, addr, contract) + if err != nil { + return nil, err + } + poolData.RestakedReward = *value + allZeros = allZeros && isZeroBigInt(value) + + value, err = b.everstakeContractCallSimpleNumeric(everstakeAutocompoundBalanceOfMethodSignature, addr, contract) + if err != nil { + return nil, err + } + poolData.AutocompoundBalance = *value + allZeros = allZeros && isZeroBigInt(value) + + if allZeros { + return nil, nil + } + return &poolData, nil +} diff --git a/bchain/coins/firo/firoparser.go b/bchain/coins/firo/firoparser.go index 4bb800e2df..cfdf9c4a7f 100644 --- a/bchain/coins/firo/firoparser.go +++ b/bchain/coins/firo/firoparser.go @@ -14,13 +14,14 @@ import ( ) const ( - OpZeroCoinMint = 0xc1 - OpZeroCoinSpend = 0xc2 - OpSigmaMint = 0xc3 - OpSigmaSpend = 0xc4 - OpLelantusMint = 0xc5 - OpLelantusJMint = 0xc6 - OpLelantusJoinSplit = 0xc7 + OpZeroCoinMint = 0xc1 + OpZeroCoinSpend = 0xc2 + OpSigmaMint = 0xc3 + OpSigmaSpend = 0xc4 + OpLelantusMint = 0xc5 + OpLelantusJMint = 0xc6 + OpLelantusJoinSplit = 0xc7 + OpLelantusJoinSplitPayload = 0xc9 MainnetMagic wire.BitcoinNet = 0xe3d9fef1 TestnetMagic wire.BitcoinNet = 0xcffcbeea @@ -122,6 +123,8 @@ func (p *FiroParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) return []string{"LelantusJMint"}, false, nil case OpLelantusJoinSplit: return []string{"LelantusJoinSplit"}, false, nil + case OpLelantusJoinSplitPayload: + return []string{"LelantusJoinSplit"}, false, nil } } @@ -170,7 +173,7 @@ func (p *FiroParser) ParseBlock(b []byte) (*bchain.Block, error) { } else { if isMTP(header) { mtpHeader := MTPBlockHeader{} - mtpHashData := MTPHashData{} + mtpHashDataRoot := MTPHashDataRoot{} // header err = binary.Read(reader, binary.LittleEndian, &mtpHeader) @@ -178,28 +181,45 @@ func (p *FiroParser) ParseBlock(b []byte) (*bchain.Block, error) { return nil, err } - // hash data - err = binary.Read(reader, binary.LittleEndian, &mtpHashData) + // hash data root + err = binary.Read(reader, binary.LittleEndian, &mtpHashDataRoot) if err != nil { return nil, err } - // proof - for i := 0; i < MTPL*3; i++ { - var numberProofBlocks uint8 + isAllZero := true + for i := 0; i < 16; i++ { + if mtpHashDataRoot.HashRootMTP[i] != 0 { + isAllZero = false + break + } + } - err = binary.Read(reader, binary.LittleEndian, &numberProofBlocks) + if !isAllZero { + // hash data + mtpHashData := MTPHashData{} + err = binary.Read(reader, binary.LittleEndian, &mtpHashData) if err != nil { return nil, err } - for j := uint8(0); j < numberProofBlocks; j++ { - var mtpData [16]uint8 + // proof + for i := 0; i < MTPL*3; i++ { + var numberProofBlocks uint8 - err = binary.Read(reader, binary.LittleEndian, mtpData[:]) + 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 + } + } } } } @@ -318,9 +338,12 @@ func isProgPow(h *wire.BlockHeader, isTestNet bool) bool { return isTestNet && epoch >= SwitchToProgPowBlockHeaderTestnet || !isTestNet && epoch >= SwitchToProgPowBlockHeaderMainnet } -type MTPHashData struct { +type MTPHashDataRoot struct { HashRootMTP [16]uint8 - BlockMTP [128][128]uint64 +} + +type MTPHashData struct { + BlockMTP [128][128]uint64 } type MTPBlockHeader struct { diff --git a/bchain/coins/nuls/nulsrpc.go b/bchain/coins/nuls/nulsrpc.go index 001cb6dfd9..3c73bed8bc 100644 --- a/bchain/coins/nuls/nulsrpc.go +++ b/bchain/coins/nuls/nulsrpc.go @@ -471,7 +471,7 @@ func (n *NulsRPC) EstimateFee(blocks int) (big.Int, error) { return *big.NewInt(100000), nil } -func (n *NulsRPC) SendRawTransaction(tx string) (string, error) { +func (n *NulsRPC) SendRawTransaction(tx string, alternativeRPC bool) (string, error) { broadcast := CmdTxBroadcast{} req := struct { TxHex string `json:"txHex"` diff --git a/bchain/coins/optimism/evm.go b/bchain/coins/optimism/evm.go new file mode 100644 index 0000000000..e1f7e51e21 --- /dev/null +++ b/bchain/coins/optimism/evm.go @@ -0,0 +1,45 @@ +package optimism + +import ( + "context" + + "github.com/ethereum/go-ethereum/rpc" + "github.com/trezor/blockbook/bchain" +) + +// OptimismRPCClient wraps an rpc client to implement the EVMRPCClient interface +type OptimismRPCClient struct { + *rpc.Client +} + +// EthSubscribe subscribes to events and returns a client subscription that implements the EVMClientSubscription interface +func (c *OptimismRPCClient) EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (bchain.EVMClientSubscription, error) { + sub, err := c.Client.EthSubscribe(ctx, channel, args...) + if err != nil { + return nil, err + } + + return &OptimismClientSubscription{ClientSubscription: sub}, nil +} + +// CallContext performs a JSON-RPC call with the given arguments +func (c *OptimismRPCClient) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { + if err := c.Client.CallContext(ctx, result, method, args...); err != nil { + return err + } + + // special case to handle empty gas price for a valid rpc transaction + // (https://goerli-optimism.etherscan.io/tx/0x9b62094073147508471e3371920b68070979beea32100acdc49c721350b69cb9) + if r, ok := result.(*bchain.RpcTransaction); ok { + if *r != (bchain.RpcTransaction{}) && r.GasPrice == "" { + r.GasPrice = "0x0" + } + } + + return nil +} + +// OptimismClientSubscription wraps a client subcription to implement the EVMClientSubscription interface +type OptimismClientSubscription struct { + *rpc.ClientSubscription +} diff --git a/bchain/coins/optimism/optimismrpc.go b/bchain/coins/optimism/optimismrpc.go new file mode 100644 index 0000000000..3149bf6aae --- /dev/null +++ b/bchain/coins/optimism/optimismrpc.go @@ -0,0 +1,75 @@ +package optimism + +import ( + "context" + "encoding/json" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/bchain/coins/eth" +) + +const ( + // MainNet is production network + MainNet eth.Network = 10 +) + +// OptimismRPC is an interface to JSON-RPC optimism service. +type OptimismRPC struct { + *eth.EthereumRPC +} + +// NewOptimismRPC returns new OptimismRPC instance. +func NewOptimismRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { + c, err := eth.NewEthereumRPC(config, pushHandler) + if err != nil { + return nil, err + } + + s := &OptimismRPC{ + EthereumRPC: c.(*eth.EthereumRPC), + } + + return s, nil +} + +// Initialize bnb smart chain rpc interface +func (b *OptimismRPC) Initialize() error { + b.OpenRPC = eth.OpenRPC + + rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL) + if err != nil { + return err + } + + // set chain specific + b.Client = ec + b.RPC = rc + b.MainNetChainID = MainNet + b.NewBlock = eth.NewEthereumNewBlock() + b.NewTx = eth.NewEthereumNewTx() + + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) + defer cancel() + + id, err := b.Client.NetworkID(ctx) + if err != nil { + return err + } + + // parameters for getInfo request + switch eth.Network(id.Uint64()) { + case MainNet: + b.Testnet = false + b.Network = "livenet" + default: + return errors.Errorf("Unknown network id %v", id) + } + + b.InitAlternativeProviders() + + glog.Info("rpc: block chain ", b.Network) + + return nil +} diff --git a/bchain/coins/pivx/pivxparser.go b/bchain/coins/pivx/pivxparser.go index 4dc92943f7..bd0c280086 100644 --- a/bchain/coins/pivx/pivxparser.go +++ b/bchain/coins/pivx/pivxparser.go @@ -2,8 +2,10 @@ package pivx import ( "bytes" + "encoding/binary" "encoding/hex" "encoding/json" + "fmt" "io" "math/big" @@ -13,7 +15,6 @@ import ( "github.com/martinboehm/btcutil/chaincfg" "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain/coins/btc" - "github.com/trezor/blockbook/bchain/coins/utils" ) // magic numbers @@ -100,7 +101,12 @@ func (p *PivXParser) ParseBlock(b []byte) (*bchain.Block, error) { r.Seek(32, io.SeekCurrent) } - err = utils.DecodeTransactions(r, 0, wire.WitnessEncoding, &w) + if h.Version > 7 { + // Skip new hashFinalSaplingRoot (block version 8 or newer) + r.Seek(32, io.SeekCurrent) + } + + err = p.PivxDecodeTransactions(r, 0, &w) if err != nil { return nil, errors.Annotatef(err, "DecodeTransactions") } @@ -255,6 +261,90 @@ func (p *PivXParser) GetAddrDescForUnknownInput(tx *bchain.Tx, input int) bchain return s } +func (p *PivXParser) PivxDecodeTransactions(r *bytes.Reader, pver uint32, blk *wire.MsgBlock) error { + maxTxPerBlock := uint64((wire.MaxBlockPayload / 10) + 1) + + txCount, err := wire.ReadVarInt(r, pver) + if err != nil { + return err + } + + // Prevent more transactions than could possibly fit into a block. + // It would be possible to cause memory exhaustion and panics without + // a sane upper bound on this count. + if txCount > maxTxPerBlock { + str := fmt.Sprintf("too many transactions to fit into a block "+ + "[count %d, max %d]", txCount, maxTxPerBlock) + return &wire.MessageError{Func: "utils.decodeTransactions", Description: str} + } + + blk.Transactions = make([]*wire.MsgTx, 0, txCount) + for i := uint64(0); i < txCount; i++ { + tx := wire.MsgTx{} + + // read version & seek back to original state + var version uint32 = 0 + if err = binary.Read(r, binary.LittleEndian, &version); err != nil { + return err + } + if _, err = r.Seek(-4, io.SeekCurrent); err != nil { + return err + } + + txVersion := version & 0xffff + enc := wire.WitnessEncoding + + // shielded transactions + if txVersion >= 3 { + enc = wire.BaseEncoding + } + + err := p.PivxDecode(&tx, r, pver, enc) + if err != nil { + return err + } + blk.Transactions = append(blk.Transactions, &tx) + } + + return nil +} + +func (p *PivXParser) PivxDecode(MsgTx *wire.MsgTx, r *bytes.Reader, pver uint32, enc wire.MessageEncoding) error { + if err := MsgTx.BtcDecode(r, pver, enc); err != nil { + return err + } + + // extra + version := uint32(MsgTx.Version) + txVersion := version & 0xffff + + if txVersion >= 3 { + // valueBalance + r.Seek(9, io.SeekCurrent) + + vShieldedSpend, err := wire.ReadVarInt(r, 0) + if err != nil { + return err + } + if vShieldedSpend > 0 { + r.Seek(int64(vShieldedSpend*384), io.SeekCurrent) + } + + vShieldOutput, err := wire.ReadVarInt(r, 0) + if err != nil { + return err + } + if vShieldOutput > 0 { + r.Seek(int64(vShieldOutput*948), io.SeekCurrent) + } + + // bindingSig + r.Seek(64, io.SeekCurrent) + } + + return nil +} + // Checks if script is OP_ZEROCOINMINT func isZeroCoinMintScript(signatureScript []byte) bool { return len(signatureScript) > 1 && signatureScript[0] == OP_ZEROCOINMINT diff --git a/bchain/coins/polygon/polygonrpc.go b/bchain/coins/polygon/polygonrpc.go new file mode 100644 index 0000000000..8ef914143b --- /dev/null +++ b/bchain/coins/polygon/polygonrpc.go @@ -0,0 +1,75 @@ +package polygon + +import ( + "context" + "encoding/json" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/bchain/coins/eth" +) + +const ( + // MainNet is production network + MainNet eth.Network = 137 +) + +// PolygonRPC is an interface to JSON-RPC polygon service. +type PolygonRPC struct { + *eth.EthereumRPC +} + +// NewPolygonRPC returns new PolygonRPC instance. +func NewPolygonRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) { + c, err := eth.NewEthereumRPC(config, pushHandler) + if err != nil { + return nil, err + } + + s := &PolygonRPC{ + EthereumRPC: c.(*eth.EthereumRPC), + } + + return s, nil +} + +// Initialize polygon rpc interface +func (b *PolygonRPC) Initialize() error { + b.OpenRPC = eth.OpenRPC + + rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL) + if err != nil { + return err + } + + // set chain specific + b.Client = ec + b.RPC = rc + b.MainNetChainID = MainNet + b.NewBlock = eth.NewEthereumNewBlock() + b.NewTx = eth.NewEthereumNewTx() + + ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) + defer cancel() + + id, err := b.Client.NetworkID(ctx) + if err != nil { + return err + } + + // parameters for getInfo request + switch eth.Network(id.Uint64()) { + case MainNet: + b.Testnet = false + b.Network = "livenet" + default: + return errors.Errorf("Unknown network id %v", id) + } + + b.InitAlternativeProviders() + + glog.Info("rpc: block chain ", b.Network) + + return nil +} diff --git a/bchain/coins/vipstarcoin/vipstarcoinparser_test.go b/bchain/coins/vipstarcoin/vipstarcoinparser_test.go index 5ab6403182..83f215724f 100644 --- a/bchain/coins/vipstarcoin/vipstarcoinparser_test.go +++ b/bchain/coins/vipstarcoin/vipstarcoinparser_test.go @@ -294,6 +294,10 @@ func Test_UnpackTx(t *testing.T) { t.Errorf("unpackTx() error = %v, wantErr %v", err, tt.wantErr) return } + // ignore witness unpacking + for i := range got.Vin { + got.Vin[i].Witness = nil + } if !reflect.DeepEqual(got, tt.want) { t.Errorf("unpackTx() got = %v, want %v", got, tt.want) } diff --git a/bchain/coins/zec/zcashrpc.go b/bchain/coins/zec/zcashrpc.go index c7f40566de..68ba1481d6 100644 --- a/bchain/coins/zec/zcashrpc.go +++ b/bchain/coins/zec/zcashrpc.go @@ -1,7 +1,11 @@ package zec import ( + "bytes" "encoding/json" + "os/exec" + "reflect" + "strings" "github.com/golang/glog" "github.com/juju/errors" @@ -42,7 +46,7 @@ func NewZCashRPC(config json.RawMessage, pushHandler func(bchain.NotificationTyp z := &ZCashRPC{ BitcoinRPC: b.(*btc.BitcoinRPC), } - z.RPCMarshaler = btc.JSONMarshalerV1{} + z.RPCMarshaler = JSONMarshalerV1Zebra{} z.ChainConfig.SupportsEstimateSmartFee = false return z, nil } @@ -84,13 +88,16 @@ func (z *ZCashRPC) GetChainInfo() (*bchain.ChainInfo, error) { return nil, chainInfo.Error } + // networkinfo not supported by zebra 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 + + zebrad := "zebra" + cmd := exec.Command("/opt/coins/nodes/zcash/bin/zebrad", "--version") + var out bytes.Buffer + cmd.Stdout = &out + err = cmd.Run() + if err == nil { + zebrad = out.String() } return &bchain.ChainInfo{ @@ -100,7 +107,7 @@ func (z *ZCashRPC) GetChainInfo() (*bchain.ChainInfo, error) { Difficulty: string(chainInfo.Result.Difficulty), Headers: chainInfo.Result.Headers, SizeOnDisk: chainInfo.Result.SizeOnDisk, - Version: string(networkInfo.Result.Version), + Version: zebrad, Subversion: string(networkInfo.Result.Subversion), ProtocolVersion: string(networkInfo.Result.ProtocolVersion), Timeoffset: networkInfo.Result.Timeoffset, @@ -111,6 +118,19 @@ func (z *ZCashRPC) GetChainInfo() (*bchain.ChainInfo, error) { // GetBlock returns block with given hash. func (z *ZCashRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { + type rpcBlock struct { + bchain.BlockHeader + Txs []bchain.Tx `json:"tx"` + } + type resGetBlockV1 struct { + Error *bchain.RPCError `json:"error"` + Result bchain.BlockInfo `json:"result"` + } + type resGetBlockV2 struct { + Error *bchain.RPCError `json:"error"` + Result rpcBlock `json:"result"` + } + var err error if hash == "" && height > 0 { hash, err = z.GetBlockHash(height) @@ -119,40 +139,138 @@ func (z *ZCashRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { } } - glog.V(1).Info("rpc: getblock (verbosity=1) ", hash) - - res := btc.ResGetBlockThin{} + var rawResponse json.RawMessage + resV2 := resGetBlockV2{} req := btc.CmdGetBlock{Method: "getblock"} req.Params.BlockHash = hash + req.Params.Verbosity = 2 + err = z.Call(&req, &rawResponse) + if err != nil { + // Check if it's a memory error and fall back + errStr := strings.ToLower(err.Error()) + if strings.Contains(errStr, "memory capacity exceeded") || strings.Contains(errStr, "response is too big") { + glog.Warningf("getblock verbosity=2 failed for block %v, falling back to individual tx fetches", hash) + return z.getBlockWithFallback(hash) + } + return nil, errors.Annotatef(err, "hash %v", hash) + } + // hack for ZCash, where the field "valueZat" is used instead of "valueSat" + rawResponse = bytes.ReplaceAll(rawResponse, []byte(`"valueZat"`), []byte(`"valueSat"`)) + err = json.Unmarshal(rawResponse, &resV2) + if err != nil { + return nil, errors.Annotatef(err, "hash %v", hash) + } + + // Check if verbosity=2 returned an RPC error + if resV2.Error != nil { + // Check if error is memory-related (case-insensitive) + errorMsg := strings.ToLower(resV2.Error.Message) + if strings.Contains(errorMsg, "memory capacity exceeded") || strings.Contains(errorMsg, "response is too big") { + glog.Warningf("getblock verbosity=2 returned memory error for block %v, falling back to verbosity=1 + individual tx fetches", hash) + return z.getBlockWithFallback(hash) + } + return nil, errors.Annotatef(resV2.Error, "hash %v", hash) + } + + block := &bchain.Block{ + BlockHeader: resV2.Result.BlockHeader, + Txs: resV2.Result.Txs, + } + + // transactions fetched in block with verbosity 2 do not contain txids, so we need to get it separately + resV1 := resGetBlockV1{} req.Params.Verbosity = 1 - err = z.Call(&req, &res) + err = z.Call(&req, &resV1) + if err != nil { + return nil, errors.Annotatef(err, "hash %v", hash) + } + if resV1.Error != nil { + return nil, errors.Annotatef(resV1.Error, "hash %v", hash) + } + for i := range resV1.Result.Txids { + block.Txs[i].Txid = resV1.Result.Txids[i] + } + return block, nil +} +// getBlockWithFallback fetches block using verbosity=1 and then fetches each transaction individually +func (z *ZCashRPC) getBlockWithFallback(hash string) (*bchain.Block, error) { + type resGetBlockV1 struct { + Error *bchain.RPCError `json:"error"` + Result bchain.BlockInfo `json:"result"` + } + + // Get block header and txids using verbosity=1 + resV1 := resGetBlockV1{} + req := btc.CmdGetBlock{Method: "getblock"} + req.Params.BlockHash = hash + req.Params.Verbosity = 1 + err := z.Call(&req, &resV1) if err != nil { return nil, errors.Annotatef(err, "hash %v", hash) } - if res.Error != nil { - return nil, errors.Annotatef(res.Error, "hash %v", hash) + if resV1.Error != nil { + return nil, errors.Annotatef(resV1.Error, "hash %v", hash) + } + + // Create block with header from verbosity=1 response + block := &bchain.Block{ + BlockHeader: resV1.Result.BlockHeader, + Txs: make([]bchain.Tx, 0, len(resV1.Result.Txids)), } - txs := make([]bchain.Tx, 0, len(res.Result.Txids)) - for _, txid := range res.Result.Txids { + // Fetch each transaction individually + for _, txid := range resV1.Result.Txids { tx, err := z.GetTransaction(txid) if err != nil { - if err == bchain.ErrTxNotFound { - glog.Errorf("rpc: getblock: skipping transanction in block %s due error: %s", hash, err) - continue - } - return nil, err + return nil, errors.Annotatef(err, "failed to fetch tx %v for block %v", txid, hash) } - txs = append(txs, *tx) - } - block := &bchain.Block{ - BlockHeader: res.Result.BlockHeader, - Txs: txs, + block.Txs = append(block.Txs, *tx) } + return block, nil } +// GetTransaction returns a transaction by the transaction ID +func (z *ZCashRPC) GetTransaction(txid string) (*bchain.Tx, error) { + r, err := z.getRawTransaction(txid) + if err != nil { + return nil, err + } + // hack for ZCash, where the field "valueZat" is used instead of "valueSat" + r = bytes.ReplaceAll(r, []byte(`"valueZat"`), []byte(`"valueSat"`)) + tx, err := z.Parser.ParseTxFromJson(r) + if err != nil { + return nil, errors.Annotatef(err, "txid %v", txid) + } + tx.Blocktime = tx.Time + tx.Txid = txid + tx.CoinSpecificData = r + return tx, nil +} + +// getRawTransaction returns json as returned by backend, with all coin specific data +func (z *ZCashRPC) getRawTransaction(txid string) (json.RawMessage, error) { + glog.V(1).Info("rpc: getrawtransaction ", txid) + + res := btc.ResGetRawTransaction{} + req := btc.CmdGetRawTransaction{Method: "getrawtransaction"} + req.Params.Txid = txid + req.Params.Verbose = true + err := z.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 +} + // GetTransactionForMempool returns a transaction by the transaction ID. // It could be optimized for mempool, i.e. without block time and confirmations func (z *ZCashRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) { @@ -168,3 +286,72 @@ func (z *ZCashRPC) GetMempoolEntry(txid string) (*bchain.MempoolEntry, error) { func (z *ZCashRPC) GetBlockRaw(hash string) (string, error) { return "", errors.New("GetBlockRaw: not supported") } + +// JSONMarshalerV1 is used for marshalling requests to legacy Bitcoin Type RPC interfaces +type JSONMarshalerV1Zebra struct{} + +// Marshal converts struct passed by parameter to JSON +func (JSONMarshalerV1Zebra) Marshal(v interface{}) ([]byte, error) { + u := cmdUntypedParams{} + + switch v := v.(type) { + case *btc.CmdGetBlock: + u.Method = v.Method + u.Params = append(u.Params, v.Params.BlockHash) + u.Params = append(u.Params, v.Params.Verbosity) + case *btc.CmdGetRawTransaction: + var n int + if v.Params.Verbose { + n = 1 + } + u.Method = v.Method + u.Params = append(u.Params, v.Params.Txid) + u.Params = append(u.Params, n) + default: + { + v := reflect.ValueOf(v).Elem() + + f := v.FieldByName("Method") + if !f.IsValid() || f.Kind() != reflect.String { + return nil, btc.ErrInvalidValue + } + u.Method = f.String() + + f = v.FieldByName("Params") + if f.IsValid() { + var arr []interface{} + switch f.Kind() { + case reflect.Slice: + arr = make([]interface{}, f.Len()) + for i := 0; i < f.Len(); i++ { + arr[i] = f.Index(i).Interface() + } + case reflect.Struct: + arr = make([]interface{}, f.NumField()) + for i := 0; i < f.NumField(); i++ { + arr[i] = f.Field(i).Interface() + } + default: + return nil, btc.ErrInvalidValue + } + u.Params = arr + } + } + } + u.Id = "-" + if u.Params == nil { + u.Params = make([]interface{}, 0) + } + d, err := json.Marshal(u) + if err != nil { + return nil, err + } + + return d, nil +} + +type cmdUntypedParams struct { + Method string `json:"method"` + Id string `json:"id"` + Params []interface{} `json:"params"` +} diff --git a/bchain/evm_interface.go b/bchain/evm_interface.go index 1338b01290..8eb94f54a1 100644 --- a/bchain/evm_interface.go +++ b/bchain/evm_interface.go @@ -2,7 +2,11 @@ package bchain import ( "context" + "fmt" "math/big" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" ) // EVMClient provides the necessary client functionality for evm chain sync @@ -57,3 +61,21 @@ type EVMNewTxSubscriber interface { EVMSubscriber Read() (EVMHash, bool) } + +// ToBlockNumArg converts a big.Int to an appropriate string representation of the number if possible +// - valid return values: (hex string, "latest", "pending", "earliest", "finalized", or "safe") +// - invalid return value: "invalid" +func ToBlockNumArg(number *big.Int) string { + if number == nil { + return "latest" + } + if number.Sign() >= 0 { + return hexutil.EncodeBig(number) + } + // It's negative. + if number.IsInt64() { + return rpc.BlockNumber(number.Int64()).String() + } + // It's negative and large, which is invalid. + return fmt.Sprintf("", number) +} diff --git a/bchain/golomb.go b/bchain/golomb.go new file mode 100644 index 0000000000..c0d38e303c --- /dev/null +++ b/bchain/golomb.go @@ -0,0 +1,217 @@ +package bchain + +import ( + "bytes" + "encoding/hex" + + "github.com/golang/glog" + "github.com/juju/errors" + "github.com/martinboehm/btcutil/gcs" +) + +type FilterScriptsType int + +const ( + FilterScriptsInvalid = FilterScriptsType(iota) + FilterScriptsAll + FilterScriptsTaproot + FilterScriptsTaprootNoOrdinals +) + +// GolombFilter is computing golomb filter of address descriptors +type GolombFilter struct { + Enabled bool + UseZeroedKey bool + p uint8 + key string + filterScripts string + filterScriptsType FilterScriptsType + filterData [][]byte + uniqueData map[string]struct{} + // All the unique txids that contain ordinal data + ordinalTxIds map[string]struct{} + // Mapping of txid to address descriptors - only used in case of taproot-noordinals + allAddressDescriptors map[string][]AddressDescriptor +} + +// NewGolombFilter initializes the GolombFilter handler +func NewGolombFilter(p uint8, filterScripts string, key string, useZeroedKey bool) (*GolombFilter, error) { + if p == 0 { + return &GolombFilter{Enabled: false}, nil + } + gf := GolombFilter{ + Enabled: true, + UseZeroedKey: useZeroedKey, + p: p, + key: key, + filterScripts: filterScripts, + filterScriptsType: filterScriptsToScriptsType(filterScripts), + filterData: make([][]byte, 0), + uniqueData: make(map[string]struct{}), + } + // reject invalid filterScripts + if gf.filterScriptsType == FilterScriptsInvalid { + return nil, errors.Errorf("Invalid/unsupported filterScripts parameter %s", filterScripts) + } + // set ordinal-related fields if needed + if gf.ignoreOrdinals() { + gf.ordinalTxIds = make(map[string]struct{}) + gf.allAddressDescriptors = make(map[string][]AddressDescriptor) + } + return &gf, nil +} + +// Gets the M parameter that we are using for the filter +// Currently it relies on P parameter, but that can change +func GetGolombParamM(p uint8) uint64 { + return uint64(1 << uint64(p)) +} + +// Checks whether this input contains ordinal data +func isInputOrdinal(vin Vin) bool { + byte_pattern := []byte{ + 0x00, // OP_0, OP_FALSE + 0x63, // OP_IF + 0x03, // OP_PUSHBYTES_3 + 0x6f, // "o" + 0x72, // "r" + 0x64, // "d" + 0x01, // OP_PUSHBYTES_1 + } + // Witness needs to have at least 3 items and the second one needs to contain certain pattern + return len(vin.Witness) > 2 && bytes.Contains(vin.Witness[1], byte_pattern) +} + +// Whether a transaction contains any ordinal data +func txContainsOrdinal(tx *Tx) bool { + for _, vin := range tx.Vin { + if isInputOrdinal(vin) { + return true + } + } + return false +} + +// Saving all the ordinal-related txIds so we can later ignore their address descriptors +func (f *GolombFilter) markTxAndParentsAsOrdinals(tx *Tx) { + f.ordinalTxIds[tx.Txid] = struct{}{} + for _, vin := range tx.Vin { + f.ordinalTxIds[vin.Txid] = struct{}{} + } +} + +// Adding a new address descriptor mapped to a txid +func (f *GolombFilter) addTxIdMapping(ad AddressDescriptor, tx *Tx) { + f.allAddressDescriptors[tx.Txid] = append(f.allAddressDescriptors[tx.Txid], ad) +} + +// AddAddrDesc adds taproot address descriptor to the data for the filter +func (f *GolombFilter) AddAddrDesc(ad AddressDescriptor, tx *Tx) { + if f.ignoreNonTaproot() && !ad.IsTaproot() { + return + } + if f.ignoreOrdinals() && tx != nil && txContainsOrdinal(tx) { + f.markTxAndParentsAsOrdinals(tx) + return + } + if len(ad) == 0 { + return + } + // When ignoring ordinals, we need to save all the address descriptors before + // filtering out the "invalid" ones. + if f.ignoreOrdinals() && tx != nil { + f.addTxIdMapping(ad, tx) + return + } + f.includeAddrDesc(ad) +} + +// Private function to be called with descriptors that were already validated +func (f *GolombFilter) includeAddrDesc(ad AddressDescriptor) { + s := string(ad) + if _, found := f.uniqueData[s]; !found { + f.filterData = append(f.filterData, ad) + f.uniqueData[s] = struct{}{} + } +} + +// Including all the address descriptors from non-ordinal transactions +func (f *GolombFilter) includeAllAddressDescriptorsOrdinals() { + for txid, ads := range f.allAddressDescriptors { + // Ignoring the txids that contain ordinal data + if _, found := f.ordinalTxIds[txid]; found { + continue + } + for _, ad := range ads { + f.includeAddrDesc(ad) + } + } +} + +// Compute computes golomb filter from the data +func (f *GolombFilter) Compute() []byte { + m := GetGolombParamM(f.p) + + // In case of ignoring the ordinals, we still need to assemble the filter data + if f.ignoreOrdinals() { + f.includeAllAddressDescriptorsOrdinals() + } + + if len(f.filterData) == 0 { + return nil + } + + // Used key is possibly just zeroes, otherwise get it from the supplied key + var key [gcs.KeySize]byte + if f.UseZeroedKey { + key = [gcs.KeySize]byte{} + } else { + b, _ := hex.DecodeString(f.key) + if len(b) < gcs.KeySize { + return nil + } + copy(key[:], b[:gcs.KeySize]) + } + + filter, err := gcs.BuildGCSFilter(f.p, m, key, f.filterData) + if err != nil { + glog.Error("Cannot create golomb filter for ", f.key, ", ", err) + return nil + } + + fb, err := filter.NBytes() + if err != nil { + glog.Error("Error getting NBytes from golomb filter for ", f.key, ", ", err) + return nil + } + + return fb +} + +func (f *GolombFilter) ignoreNonTaproot() bool { + switch f.filterScriptsType { + case FilterScriptsTaproot, FilterScriptsTaprootNoOrdinals: + return true + } + return false +} + +func (f *GolombFilter) ignoreOrdinals() bool { + switch f.filterScriptsType { + case FilterScriptsTaprootNoOrdinals: + return true + } + return false +} + +func filterScriptsToScriptsType(filterScripts string) FilterScriptsType { + switch filterScripts { + case "": + return FilterScriptsAll + case "taproot": + return FilterScriptsTaproot + case "taproot-noordinals": + return FilterScriptsTaprootNoOrdinals + } + return FilterScriptsInvalid +} diff --git a/bchain/golomb_test.go b/bchain/golomb_test.go new file mode 100644 index 0000000000..cd9ddd4689 --- /dev/null +++ b/bchain/golomb_test.go @@ -0,0 +1,282 @@ +// //go:build unittest + +package bchain + +import ( + "encoding/hex" + "testing" +) + +func getCommonAddressDescriptors() []AddressDescriptor { + return []AddressDescriptor{ + // bc1pgeqrcq5capal83ypxczmypjdhk4d9wwcea4k66c7ghe07p2qt97sqh8sy5 + hexToBytes("512046403c0298e87bf3c4813605b2064dbdaad2b9d8cf6b6d6b1e45f2ff0540597d"), + // bc1p7en40zu9hmf9d3luh8evmfyg655pu5k2gtna6j7zr623f9tz7z0stfnwav + hexToBytes("5120f667578b85bed256c7fcb9f2cda488d5281e52ca42e7dd4bc21e95149562f09f"), + // 39ECUF8YaFRX7XfttfAiLa5ir43bsrQUZJ + hexToBytes("a91452ae9441d9920d9eb4a3c0a877ca8d8de547ce6587"), + } +} + +func TestGolombFilter(t *testing.T) { + tests := []struct { + name string + p uint8 + useZeroedKey bool + filterScripts string + key string + addressDescriptors []AddressDescriptor + wantError bool + wantEnabled bool + want string + }{ + { + name: "taproot", + p: 20, + useZeroedKey: false, + filterScripts: "taproot", + key: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + addressDescriptors: getCommonAddressDescriptors(), + wantEnabled: true, + wantError: false, + want: "0235dddcce5d60", + }, + { + name: "taproot-zeroed-key", + p: 20, + useZeroedKey: true, + filterScripts: "taproot", + key: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + addressDescriptors: getCommonAddressDescriptors(), + wantEnabled: true, + wantError: false, + want: "0218c23a013600", + }, + { + name: "taproot p=21", + p: 21, + useZeroedKey: false, + filterScripts: "taproot", + key: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + addressDescriptors: getCommonAddressDescriptors(), + wantEnabled: true, + wantError: false, + want: "0235ddda672eb0", + }, + { + name: "all", + p: 20, + useZeroedKey: false, + filterScripts: "", + key: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + addressDescriptors: getCommonAddressDescriptors(), + wantEnabled: true, + wantError: false, + want: "0350ccc61ac611976c80", + }, + { + name: "taproot-noordinals", + p: 20, + useZeroedKey: false, + filterScripts: "taproot-noordinals", + key: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + addressDescriptors: getCommonAddressDescriptors(), + wantEnabled: true, + wantError: false, + want: "0235dddcce5d60", + }, + { + name: "not supported filter", + p: 20, + useZeroedKey: false, + filterScripts: "notsupported", + wantEnabled: false, + wantError: true, + want: "", + }, + { + name: "not enabled", + p: 0, + useZeroedKey: false, + filterScripts: "", + wantEnabled: false, + wantError: false, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gf, err := NewGolombFilter(tt.p, tt.filterScripts, tt.key, tt.useZeroedKey) + if err != nil && !tt.wantError { + t.Errorf("TestGolombFilter.NewGolombFilter() got unexpected error '%v'", err) + return + } + if err == nil && tt.wantError { + t.Errorf("TestGolombFilter.NewGolombFilter() wanted error, got none") + return + } + if gf == nil && tt.wantError { + return + } + if gf.Enabled != tt.wantEnabled { + t.Errorf("TestGolombFilter.NewGolombFilter() got gf.Enabled %v, want %v", gf.Enabled, tt.wantEnabled) + return + } + for _, ad := range tt.addressDescriptors { + gf.AddAddrDesc(ad, nil) + } + f := gf.Compute() + got := hex.EncodeToString(f) + if got != tt.want { + t.Errorf("TestGolombFilter Compute() got %v, want %v", got, tt.want) + } + }) + } +} + +// Preparation transaction, locking BTC redeemable by ordinal witness - parent of the reveal transaction +func getOrdinalCommitTx() (Tx, []AddressDescriptor) { + tx := Tx{ + // https://mempool.space/tx/11111c17cbe86aebab146ee039d4e354cb55a9fb226ebdd2e30948630e7710ad + Txid: "11111c17cbe86aebab146ee039d4e354cb55a9fb226ebdd2e30948630e7710ad", + Vin: []Vin{ + { + // https://mempool.space/tx/c4cae52a6e681b66c85c12feafb42f3617f34977032df1ee139eae07370863ef + Txid: "c163fe1fdc21269cb05621adec38045e46a65289a356f9354df6010bce064916", + Vout: 0, + Witness: [][]byte{ + hexToBytes("0371633164dd16345c02e80c9963042f9a502aa2c8109c0f61da333ac1503c3ce2a1b79895359bbdee5979ab2cb44f3395892e1c419c3a8f67d31d33d7e764c9"), + }, + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "51206a711358bac6ca8f7ddfdf8f733546e658208122939f0bf7a3727f8143dfbbff", + Addresses: []string{ + "bc1pdfc3xk96cm9g7lwlm78hxd2xuevzpqfzjw0shaarwflczs7lh0lstksdn0", + }, + }, + }, + { + ScriptPubKey: ScriptPubKey{ + Hex: "a9144390d0b3d2b6d48b8c205ffbe40b2d84c40de07f87", + Addresses: []string{ + "37rGgLSLX6C6LS9am4KWd6GT1QCEP4H4py", + }, + }, + }, + { + ScriptPubKey: ScriptPubKey{ + Hex: "76a914ba6b046dd832aa8bc41c158232bcc18211387c4388ac", + Addresses: []string{ + "1HzgtNdRCXszf95rFYemsDSHJQBbs9rbZf", + }, + }, + }, + }, + } + addressDescriptors := []AddressDescriptor{ + // bc1pdfc3xk96cm9g7lwlm78hxd2xuevzpqfzjw0shaarwflczs7lh0lstksdn0 + hexToBytes("51206a711358bac6ca8f7ddfdf8f733546e658208122939f0bf7a3727f8143dfbbff"), + // 37rGgLSLX6C6LS9am4KWd6GT1QCEP4H4py + hexToBytes("a9144390d0b3d2b6d48b8c205ffbe40b2d84c40de07f87"), + // 1HzgtNdRCXszf95rFYemsDSHJQBbs9rbZf + hexToBytes("76a914ba6b046dd832aa8bc41c158232bcc18211387c4388ac"), + } + return tx, addressDescriptors +} + +// Transaction containing the actual ordinal data in witness - child of the commit transaction +func getOrdinalRevealTx() (Tx, []AddressDescriptor) { + tx := Tx{ + // https://mempool.space/tx/c4cae52a6e681b66c85c12feafb42f3617f34977032df1ee139eae07370863ef + Txid: "c4cae52a6e681b66c85c12feafb42f3617f34977032df1ee139eae07370863ef", + Vin: []Vin{ + { + Txid: "11111c17cbe86aebab146ee039d4e354cb55a9fb226ebdd2e30948630e7710ad", + Vout: 0, + Witness: [][]byte{ + hexToBytes("737ad2835962e3d147cd74a578f1109e9314eac9d00c9fad304ce2050b78fac21a2d124fd886d1d646cf1de5d5c9754b0415b960b1319526fa25e36ca1f650ce"), + hexToBytes("2029f34532e043fade4471779b4955005db8fa9b64c9e8d0a2dae4a38bbca23328ac0063036f726401010a696d6167652f77656270004d08025249464650020000574542505650384c440200002f57c2950067a026009086939b7785a104699656f4f53388355445b6415d22f8924000fd83bd31d346ca69f8fcfed6d8d18231846083f90f00ffbf203883666c36463c6ba8662257d789935e002192245bd15ac00216b080052cac85b380052c60e1593859f33a7a7abff7ed88feb361db3692341bc83553aef7aec75669ffb1ffd87fec3ff61ffb8ffdc736f20a96a0fba34071d4fdf111c435381df667728f95c4e82b6872d82471bfdc1665107bb80fd46df1686425bcd2e27eb59adc9d17b54b997ee96776a7c37ca2b57b9551bcffeb71d88768765af7384c2e3ba031ca3f19c9ddb0c6ec55223fbfe3731a1e8d7bb010de8532d53293bbbb6145597ee53559a612e6de4f8fc66936ef463eea7498555643ac0dafad6627575f2733b9fb352e411e7d9df8fc80fde75f5f66f5c5381a46b9a697d9c97555c4bf41a4909b9dd071557c3dfe0bfcd6459e06514266c65756ce9f25705230df63d30fef6076b797e1f49d00b41e87b5ccecb1c237f419e4b3ca6876053c14fc979a629459a62f78d735fb078bfa0e7a1fc69ad379447d817e06b3d7f1de820f28534f85fa20469cd6f93ddc6c5f2a94878fc64a98ac336294c99d27d11742268ae1a34cd61f31e2e4aee94b0ff496f55068fa727ace6ad2ec1e6e3f59e6a8bd154f287f652fbfaa05cac067951de1bfacc0e330c3bf6dd2efde4c509646566836eb71986154731daf722a6ff585001e87f9479559a61265d6e330f3682bf87ab2598fc3fca36da778e59cee71584594ef175e6d7d5f70d6deb02c4b371e5063c35669ffb1ffd87ffe0e730068"), + hexToBytes("c129f34532e043fade4471779b4955005db8fa9b64c9e8d0a2dae4a38bbca23328"), + }, + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "51206850b179630df0f7012ae2b111bafa52ebb9b54e1435fc4f98fbe0af6f95076a", + Addresses: []string{ + "bc1pdpgtz7trphc0wqf2u2c3rwh62t4mnd2wzs6lcnucl0s27mu4qa4q4md9ta", + }, + }, + }, + }, + } + addressDescriptors := []AddressDescriptor{ + // bc1pdpgtz7trphc0wqf2u2c3rwh62t4mnd2wzs6lcnucl0s27mu4qa4q4md9ta + hexToBytes("51206850b179630df0f7012ae2b111bafa52ebb9b54e1435fc4f98fbe0af6f95076a"), + } + return tx, addressDescriptors +} + +func TestGolombIsOrdinal(t *testing.T) { + revealTx, _ := getOrdinalRevealTx() + if txContainsOrdinal(&revealTx) != true { + t.Error("Ordinal not found in reveal Tx") + } + commitTx, _ := getOrdinalCommitTx() + if txContainsOrdinal(&commitTx) != false { + t.Error("Ordinal found in commit Tx, but should not be there") + } +} + +func TestGolombOrdinalTransactions(t *testing.T) { + tests := []struct { + name string + filterScripts string + want string + }{ + { + name: "all", + filterScripts: "", + want: "04256e660160e42ff40ee320", // take all four descriptors + }, + { + name: "taproot", + filterScripts: "taproot", + want: "0212b734c2ebe0", // filter out two non-taproot ones + }, + { + name: "taproot-noordinals", + filterScripts: "taproot-noordinals", + want: "", // ignore everything + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gf, err := NewGolombFilter(20, tt.filterScripts, "", true) + if err != nil { + t.Errorf("TestGolombOrdinalTransactions.NewGolombFilter() got unexpected error '%v'", err) + return + } + + commitTx, addressDescriptorsCommit := getOrdinalCommitTx() + revealTx, addressDescriptorsReveal := getOrdinalRevealTx() + + for _, ad := range addressDescriptorsCommit { + gf.AddAddrDesc(ad, &commitTx) + } + for _, ad := range addressDescriptorsReveal { + gf.AddAddrDesc(ad, &revealTx) + } + + f := gf.Compute() + got := hex.EncodeToString(f) + if got != tt.want { + t.Errorf("TestGolombOrdinalTransactions Compute() got %v, want %v", got, tt.want) + } + }) + } +} diff --git a/bchain/mempool_bitcoin_type.go b/bchain/mempool_bitcoin_type.go index 1063059dc6..b668236e19 100644 --- a/bchain/mempool_bitcoin_type.go +++ b/bchain/mempool_bitcoin_type.go @@ -1,10 +1,12 @@ package bchain import ( + "encoding/hex" "math/big" "time" "github.com/golang/glog" + "github.com/juju/errors" ) type chanInputPayload struct { @@ -18,11 +20,14 @@ type MempoolBitcoinType struct { chanTxid chan string chanAddrIndex chan txidio AddrDescForOutpoint AddrDescForOutpointFunc + golombFilterP uint8 + filterScripts string + useZeroedKey bool } // NewMempoolBitcoinType creates new mempool handler. // For now there is no cleanup of sync routines, the expectation is that the mempool is created only once per process -func NewMempoolBitcoinType(chain BlockChain, workers int, subworkers int) *MempoolBitcoinType { +func NewMempoolBitcoinType(chain BlockChain, workers int, subworkers int, golombFilterP uint8, filterScripts string, useZeroedKey bool) *MempoolBitcoinType { m := &MempoolBitcoinType{ BaseMempool: BaseMempool{ chain: chain, @@ -31,6 +36,9 @@ func NewMempoolBitcoinType(chain BlockChain, workers int, subworkers int) *Mempo }, chanTxid: make(chan string, 1), chanAddrIndex: make(chan txidio, 1), + golombFilterP: golombFilterP, + filterScripts: filterScripts, + useZeroedKey: useZeroedKey, } for i := 0; i < workers; i++ { go func(i int) { @@ -45,11 +53,11 @@ func NewMempoolBitcoinType(chain BlockChain, workers int, subworkers int) *Mempo }(j) } for txid := range m.chanTxid { - io, ok := m.getTxAddrs(txid, chanInput, chanResult) + io, golombFilter, ok := m.getTxAddrs(txid, chanInput, chanResult) if !ok { io = []addrIndex{} } - m.chanAddrIndex <- txidio{txid, io} + m.chanAddrIndex <- txidio{txid, io, golombFilter} } }(i) } @@ -91,11 +99,29 @@ func (m *MempoolBitcoinType) getInputAddress(payload *chanInputPayload) *addrInd } -func (m *MempoolBitcoinType) getTxAddrs(txid string, chanInput chan chanInputPayload, chanResult chan *addrIndex) ([]addrIndex, bool) { +func (m *MempoolBitcoinType) computeGolombFilter(mtx *MempoolTx, tx *Tx) string { + gf, _ := NewGolombFilter(m.golombFilterP, m.filterScripts, mtx.Txid, m.useZeroedKey) + if gf == nil || !gf.Enabled { + return "" + } + for _, vin := range mtx.Vin { + gf.AddAddrDesc(vin.AddrDesc, tx) + } + for _, vout := range mtx.Vout { + b, err := hex.DecodeString(vout.ScriptPubKey.Hex) + if err == nil { + gf.AddAddrDesc(b, tx) + } + } + fb := gf.Compute() + return hex.EncodeToString(fb) +} + +func (m *MempoolBitcoinType) getTxAddrs(txid string, chanInput chan chanInputPayload, chanResult chan *addrIndex) ([]addrIndex, string, bool) { tx, err := m.chain.GetTransactionForMempool(txid) if err != nil { glog.Error("cannot get transaction ", txid, ": ", err) - return nil, false + return nil, "", false } glog.V(2).Info("mempool: gettxaddrs ", txid, ", ", len(tx.Vin), " inputs") mtx := m.txToMempoolTx(tx) @@ -142,10 +168,14 @@ func (m *MempoolBitcoinType) getTxAddrs(txid string, chanInput chan chanInputPay io = append(io, *ai) } } + var golombFilter string + if m.golombFilterP > 0 { + golombFilter = m.computeGolombFilter(mtx, tx) + } if m.OnNewTx != nil { m.OnNewTx(mtx) } - return io, true + return io, golombFilter, true } // Resync gets mempool transactions and maps outputs to transactions. @@ -182,7 +212,7 @@ func (m *MempoolBitcoinType) Resync() (int, error) { select { // store as many processed transactions as possible case tio := <-m.chanAddrIndex: - onNewEntry(tio.txid, txEntry{tio.io, txTime}) + onNewEntry(tio.txid, txEntry{tio.io, txTime, tio.filter}) dispatched-- // send transaction to be processed case m.chanTxid <- txid: @@ -194,7 +224,7 @@ func (m *MempoolBitcoinType) Resync() (int, error) { } for i := 0; i < dispatched; i++ { tio := <-m.chanAddrIndex - onNewEntry(tio.txid, txEntry{tio.io, txTime}) + onNewEntry(tio.txid, txEntry{tio.io, txTime, tio.filter}) } for txid, entry := range m.txEntries { @@ -207,3 +237,19 @@ func (m *MempoolBitcoinType) Resync() (int, error) { glog.Info("mempool: resync finished in ", time.Since(start), ", ", len(m.txEntries), " transactions in mempool") return len(m.txEntries), nil } + +// GetTxidFilterEntries returns all mempool entries with golomb filter from +func (m *MempoolBitcoinType) GetTxidFilterEntries(filterScripts string, fromTimestamp uint32) (MempoolTxidFilterEntries, error) { + if m.filterScripts != filterScripts { + return MempoolTxidFilterEntries{}, errors.Errorf("Unsupported script filter %s", filterScripts) + } + m.mux.Lock() + entries := make(map[string]string) + for txid, entry := range m.txEntries { + if entry.filter != "" && entry.time >= fromTimestamp { + entries[txid] = entry.filter + } + } + m.mux.Unlock() + return MempoolTxidFilterEntries{entries, m.useZeroedKey}, nil +} diff --git a/bchain/mempool_bitcoin_type_test.go b/bchain/mempool_bitcoin_type_test.go new file mode 100644 index 0000000000..ddbe428f3c --- /dev/null +++ b/bchain/mempool_bitcoin_type_test.go @@ -0,0 +1,352 @@ +package bchain + +import ( + "encoding/hex" + "testing" + + "github.com/martinboehm/btcutil/gcs" +) + +func hexToBytes(h string) []byte { + b, _ := hex.DecodeString(h) + return b +} + +func TestMempoolBitcoinType_computeGolombFilter_taproot(t *testing.T) { + randomScript := hexToBytes("a914ff074800343a81ada8fe86c2d5d5a0e55b93dd7a87") + m := &MempoolBitcoinType{ + golombFilterP: 20, + filterScripts: "taproot", + } + golombFilterM := GetGolombParamM(m.golombFilterP) + tests := []struct { + name string + mtx MempoolTx + want string + }{ + { + name: "taproot", + mtx: MempoolTx{ + Txid: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + Vin: []MempoolVin{ + { + // bc1pgeqrcq5capal83ypxczmypjdhk4d9wwcea4k66c7ghe07p2qt97sqh8sy5 + AddrDesc: hexToBytes("512046403c0298e87bf3c4813605b2064dbdaad2b9d8cf6b6d6b1e45f2ff0540597d"), + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "5120f667578b85bed256c7fcb9f2cda488d5281e52ca42e7dd4bc21e95149562f09f", + Addresses: []string{ + "bc1p7en40zu9hmf9d3luh8evmfyg655pu5k2gtna6j7zr623f9tz7z0stfnwav", + }, + }, + }, + }, + }, + want: "0235dddcce5d60", + }, + { + name: "taproot multiple", + mtx: MempoolTx{ + Txid: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + Vin: []MempoolVin{ + { + // bc1pp3752xgfy39w30kggy8vvn0u68x8afwqmq6p96jzr8ffrcvjxgrqrny93y + AddrDesc: hexToBytes("51200c7d451909244ae8bec8410ec64dfcd1cc7ea5c0d83412ea4219d291e1923206"), + }, + { + // bc1p5ldsz3zxnjxrwf4xluf4qu7u839c204ptacwe2k0vzfk8s63mwts3njuwr + AddrDesc: hexToBytes("5120a7db0144469c8c3726a6ff135073dc3c4b853ea15f70ecaacf609363c351db97"), + }, + { + // bc1pgeqrcq5capal83ypxczmypjdhk4d9wwcea4k66c7ghe07p2qt97sqh8sy5 + AddrDesc: hexToBytes("512046403c0298e87bf3c4813605b2064dbdaad2b9d8cf6b6d6b1e45f2ff0540597d"), + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "51209ab20580f77e7cd676f896fc1794f7e8061efc1ce7494f2bb16205262aa12bdb", + Addresses: []string{ + "bc1pn2eqtq8h0e7dvahcjm7p098haqrpalquuay572a3vgzjv24p90dszxzg40", + }, + }, + }, + { + ScriptPubKey: ScriptPubKey{ + Hex: "5120f667578b85bed256c7fcb9f2cda488d5281e52ca42e7dd4bc21e95149562f09f", + Addresses: []string{ + "bc1p7en40zu9hmf9d3luh8evmfyg655pu5k2gtna6j7zr623f9tz7z0stfnwav", + }, + }, + }, + { + ScriptPubKey: ScriptPubKey{ + Hex: "51201341e5a58314d89bcf5add2b2a68f109add5efb1ae774fa33c612da311f25904", + Addresses: []string{ + "bc1pzdq7tfvrznvfhn66m54j5683pxkatma34em5lgeuvyk6xy0jtyzqjt48z3", + }, + }, + }, + { + ScriptPubKey: ScriptPubKey{ + Hex: "512042b2d5c032b68220bfd6d4e26bc015129e168e87e22af743ffdc736708b7d342", + Addresses: []string{ + "bc1pg2edtspjk6pzp07k6n3xhsq4z20pdr58ug40wsllm3ekwz9h6dpq77lhu9", + }, + }, + }, + }, + }, + want: "071143e4ad12730965a5247ac15db8c81c89b0bc", + }, + { + name: "taproot duplicities", + mtx: MempoolTx{ + Txid: "33a03f983b47725bbdd6045f2d5ee0d95dce08eaaf7104759758aabd8af27d34", + Vin: []MempoolVin{ + { + // bc1px2k5tu5mfq23ekkwncz5apx6ccw2nr0rne25r8t8zk7nu035ryxqn9ge8p + AddrDesc: hexToBytes("512032ad45f29b48151cdace9e054e84dac61ca98de39e55419d6715bd3e3e34190c"), + }, + { + // bc1px2k5tu5mfq23ekkwncz5apx6ccw2nr0rne25r8t8zk7nu035ryxqn9ge8p + AddrDesc: hexToBytes("512032ad45f29b48151cdace9e054e84dac61ca98de39e55419d6715bd3e3e34190c"), + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "512032ad45f29b48151cdace9e054e84dac61ca98de39e55419d6715bd3e3e34190c", + Addresses: []string{ + "bc1px2k5tu5mfq23ekkwncz5apx6ccw2nr0rne25r8t8zk7nu035ryxqn9ge8p", + }, + }, + }, + { + ScriptPubKey: ScriptPubKey{ + Hex: "512032ad45f29b48151cdace9e054e84dac61ca98de39e55419d6715bd3e3e34190c", + Addresses: []string{ + "bc1px2k5tu5mfq23ekkwncz5apx6ccw2nr0rne25r8t8zk7nu035ryxqn9ge8p", + }, + }, + }, + { + ScriptPubKey: ScriptPubKey{ + Hex: "512032ad45f29b48151cdace9e054e84dac61ca98de39e55419d6715bd3e3e34190c", + Addresses: []string{ + "bc1px2k5tu5mfq23ekkwncz5apx6ccw2nr0rne25r8t8zk7nu035ryxqn9ge8p", + }, + }, + }, + }, + }, + want: "01778db0", + }, + { + name: "partial taproot", + mtx: MempoolTx{ + Txid: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + Vin: []MempoolVin{ + { + // bc1pgeqrcq5capal83ypxczmypjdhk4d9wwcea4k66c7ghe07p2qt97sqh8sy5 + AddrDesc: hexToBytes("512046403c0298e87bf3c4813605b2064dbdaad2b9d8cf6b6d6b1e45f2ff0540597d"), + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "00145f997834e1135e893b7707ba1b12bcb8d74b821d", + Addresses: []string{ + "bc1qt7vhsd8pzd0gjwmhq7apky4uhrt5hqsa2y58nl", + }, + }, + }, + }, + }, + want: "011aeee8", + }, + { + name: "no taproot", + mtx: MempoolTx{ + Txid: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + Vin: []MempoolVin{ + { + // 39ECUF8YaFRX7XfttfAiLa5ir43bsrQUZJ + AddrDesc: hexToBytes("a91452ae9441d9920d9eb4a3c0a877ca8d8de547ce6587"), + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "00145f997834e1135e893b7707ba1b12bcb8d74b821d", + Addresses: []string{ + "bc1qt7vhsd8pzd0gjwmhq7apky4uhrt5hqsa2y58nl", + }, + }, + }, + }, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := m.computeGolombFilter(&tt.mtx, nil) + if got != tt.want { + t.Errorf("MempoolBitcoinType.computeGolombFilter() = %v, want %v", got, tt.want) + } + if got != "" { + // build the filter from computed value + filter, err := gcs.FromNBytes(m.golombFilterP, golombFilterM, hexToBytes(got)) + if err != nil { + t.Errorf("gcs.BuildGCSFilter() unexpected error %v", err) + } + // check that the vin scripts match the filter + b, _ := hex.DecodeString(tt.mtx.Txid) + for i := range tt.mtx.Vin { + match, err := filter.Match(*(*[gcs.KeySize]byte)(b[:gcs.KeySize]), tt.mtx.Vin[i].AddrDesc) + if err != nil { + t.Errorf("filter.Match vin[%d] unexpected error %v", i, err) + } + if match != tt.mtx.Vin[i].AddrDesc.IsTaproot() { + t.Errorf("filter.Match vin[%d] got %v, want %v", i, match, tt.mtx.Vin[i].AddrDesc.IsTaproot()) + } + } + // check that the vout scripts match the filter + for i := range tt.mtx.Vout { + s := hexToBytes(tt.mtx.Vout[i].ScriptPubKey.Hex) + match, err := filter.Match(*(*[gcs.KeySize]byte)(b[:gcs.KeySize]), s) + if err != nil { + t.Errorf("filter.Match vout[%d] unexpected error %v", i, err) + } + if match != AddressDescriptor(s).IsTaproot() { + t.Errorf("filter.Match vout[%d] got %v, want %v", i, match, AddressDescriptor(s).IsTaproot()) + } + } + // check that a random script does not match the filter + match, err := filter.Match(*(*[gcs.KeySize]byte)(b[:gcs.KeySize]), randomScript) + if err != nil { + t.Errorf("filter.Match randomScript unexpected error %v", err) + } + if match != false { + t.Errorf("filter.Match randomScript got true, want false") + } + } + }) + } +} + +func TestMempoolBitcoinType_computeGolombFilter_taproot_noordinals(t *testing.T) { + m := &MempoolBitcoinType{ + golombFilterP: 20, + filterScripts: "taproot-noordinals", + } + tests := []struct { + name string + mtx MempoolTx + tx Tx + want string + }{ + { + name: "taproot-no-ordinals normal taproot tx", + mtx: MempoolTx{ + Txid: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + Vin: []MempoolVin{ + { + // bc1pdfc3xk96cm9g7lwlm78hxd2xuevzpqfzjw0shaarwflczs7lh0lstksdn0 + AddrDesc: hexToBytes("51206a711358bac6ca8f7ddfdf8f733546e658208122939f0bf7a3727f8143dfbbff"), + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "51206850b179630df0f7012ae2b111bafa52ebb9b54e1435fc4f98fbe0af6f95076a", + Addresses: []string{ + "bc1pdpgtz7trphc0wqf2u2c3rwh62t4mnd2wzs6lcnucl0s27mu4qa4q4md9ta", + }, + }, + }, + }, + }, + tx: Tx{ + Vin: []Vin{ + { + Witness: [][]byte{ + hexToBytes("737ad2835962e3d147cd74a578f1109e9314eac9d00c9fad304ce2050b78fac21a2d124fd886d1d646cf1de5d5c9754b0415b960b1319526fa25e36ca1f650ce"), + }, + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "51206850b179630df0f7012ae2b111bafa52ebb9b54e1435fc4f98fbe0af6f95076a", + Addresses: []string{ + "bc1pdpgtz7trphc0wqf2u2c3rwh62t4mnd2wzs6lcnucl0s27mu4qa4q4md9ta", + }, + }, + }, + }, + }, + want: "02899e8c952b40", + }, + { + name: "taproot-no-ordinals ordinal tx", + mtx: MempoolTx{ + Txid: "86336c62a63f509a278624e3f400cdd50838d035a44e0af8a7d6d133c04cc2d2", + Vin: []MempoolVin{ + { + // bc1pdfc3xk96cm9g7lwlm78hxd2xuevzpqfzjw0shaarwflczs7lh0lstksdn0 + AddrDesc: hexToBytes("51206a711358bac6ca8f7ddfdf8f733546e658208122939f0bf7a3727f8143dfbbff"), + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "51206850b179630df0f7012ae2b111bafa52ebb9b54e1435fc4f98fbe0af6f95076a", + Addresses: []string{ + "bc1pdpgtz7trphc0wqf2u2c3rwh62t4mnd2wzs6lcnucl0s27mu4qa4q4md9ta", + }, + }, + }, + }, + }, + tx: Tx{ + // https://mempool.space/tx/c4cae52a6e681b66c85c12feafb42f3617f34977032df1ee139eae07370863ef + Txid: "c4cae52a6e681b66c85c12feafb42f3617f34977032df1ee139eae07370863ef", + Vin: []Vin{ + { + Txid: "11111c17cbe86aebab146ee039d4e354cb55a9fb226ebdd2e30948630e7710ad", + Vout: 0, + Witness: [][]byte{ + hexToBytes("737ad2835962e3d147cd74a578f1109e9314eac9d00c9fad304ce2050b78fac21a2d124fd886d1d646cf1de5d5c9754b0415b960b1319526fa25e36ca1f650ce"), + hexToBytes("2029f34532e043fade4471779b4955005db8fa9b64c9e8d0a2dae4a38bbca23328ac0063036f726401010a696d6167652f77656270004d08025249464650020000574542505650384c440200002f57c2950067a026009086939b7785a104699656f4f53388355445b6415d22f8924000fd83bd31d346ca69f8fcfed6d8d18231846083f90f00ffbf203883666c36463c6ba8662257d789935e002192245bd15ac00216b080052cac85b380052c60e1593859f33a7a7abff7ed88feb361db3692341bc83553aef7aec75669ffb1ffd87fec3ff61ffb8ffdc736f20a96a0fba34071d4fdf111c435381df667728f95c4e82b6872d82471bfdc1665107bb80fd46df1686425bcd2e27eb59adc9d17b54b997ee96776a7c37ca2b57b9551bcffeb71d88768765af7384c2e3ba031ca3f19c9ddb0c6ec55223fbfe3731a1e8d7bb010de8532d53293bbbb6145597ee53559a612e6de4f8fc66936ef463eea7498555643ac0dafad6627575f2733b9fb352e411e7d9df8fc80fde75f5f66f5c5381a46b9a697d9c97555c4bf41a4909b9dd071557c3dfe0bfcd6459e06514266c65756ce9f25705230df63d30fef6076b797e1f49d00b41e87b5ccecb1c237f419e4b3ca6876053c14fc979a629459a62f78d735fb078bfa0e7a1fc69ad379447d817e06b3d7f1de820f28534f85fa20469cd6f93ddc6c5f2a94878fc64a98ac336294c99d27d11742268ae1a34cd61f31e2e4aee94b0ff496f55068fa727ace6ad2ec1e6e3f59e6a8bd154f287f652fbfaa05cac067951de1bfacc0e330c3bf6dd2efde4c509646566836eb71986154731daf722a6ff585001e87f9479559a61265d6e330f3682bf87ab2598fc3fca36da778e59cee71584594ef175e6d7d5f70d6deb02c4b371e5063c35669ffb1ffd87ffe0e730068"), + hexToBytes("c129f34532e043fade4471779b4955005db8fa9b64c9e8d0a2dae4a38bbca23328"), + }, + }, + }, + Vout: []Vout{ + { + ScriptPubKey: ScriptPubKey{ + Hex: "51206850b179630df0f7012ae2b111bafa52ebb9b54e1435fc4f98fbe0af6f95076a", + Addresses: []string{ + "bc1pdpgtz7trphc0wqf2u2c3rwh62t4mnd2wzs6lcnucl0s27mu4qa4q4md9ta", + }, + }, + }, + }, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := m.computeGolombFilter(&tt.mtx, &tt.tx) + if got != tt.want { + t.Errorf("MempoolBitcoinType.computeGolombFilter() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/bchain/mempool_ethereum_type.go b/bchain/mempool_ethereum_type.go index 23d333fd32..aa1fbb5386 100644 --- a/bchain/mempool_ethereum_type.go +++ b/bchain/mempool_ethereum_type.go @@ -1,6 +1,7 @@ package bchain import ( + "errors" "time" "github.com/golang/glog" @@ -131,8 +132,8 @@ func (m *MempoolEthereumType) Resync() (int, error) { return entries, nil } -// AddTransactionToMempool adds transactions to mempool -func (m *MempoolEthereumType) AddTransactionToMempool(txid string) { +// AddTransactionToMempool adds transactions to mempool, returns true if tx added to mempool, false if not added (for example duplicate call) +func (m *MempoolEthereumType) AddTransactionToMempool(txid string) bool { m.mux.Lock() _, exists := m.txEntries[txid] m.mux.Unlock() @@ -142,7 +143,7 @@ func (m *MempoolEthereumType) AddTransactionToMempool(txid string) { if !exists { entry, ok := m.createTxEntry(txid, uint32(time.Now().Unix())) if !ok { - return + return false } m.mux.Lock() m.txEntries[txid] = entry @@ -151,6 +152,7 @@ func (m *MempoolEthereumType) AddTransactionToMempool(txid string) { } m.mux.Unlock() } + return !exists } // RemoveTransactionFromMempool removes transaction from mempool @@ -165,3 +167,8 @@ func (m *MempoolEthereumType) RemoveTransactionFromMempool(txid string) { } m.mux.Unlock() } + +// GetTxidFilterEntries returns all mempool entries with golomb filter from +func (m *MempoolEthereumType) GetTxidFilterEntries(filterScripts string, fromTimestamp uint32) (MempoolTxidFilterEntries, error) { + return MempoolTxidFilterEntries{}, errors.New("Not supported") +} diff --git a/bchain/types.go b/bchain/types.go index cdabf93531..8e214ae1b1 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -39,101 +39,103 @@ var ( // Outpoint is txid together with output (or input) index type Outpoint struct { - Txid string - Vout int32 + Txid string `ts_doc:"Transaction ID of the referenced outpoint."` + Vout int32 `ts_doc:"Index of the specific output in the transaction."` } // ScriptSig contains data about input script type ScriptSig struct { // Asm string `json:"asm"` - Hex string `json:"hex"` + Hex string `json:"hex" ts_doc:"Hex-encoded representation of the scriptSig."` } // Vin contains data about tx input type Vin struct { - Coinbase string `json:"coinbase"` - Txid string `json:"txid"` - Vout uint32 `json:"vout"` - ScriptSig ScriptSig `json:"scriptSig"` - Sequence uint32 `json:"sequence"` - Addresses []string `json:"addresses"` + Coinbase string `json:"coinbase" ts_doc:"Coinbase data if this is a coinbase input."` + Txid string `json:"txid" ts_doc:"Transaction ID of the input being spent."` + Vout uint32 `json:"vout" ts_doc:"Output index in the referenced transaction."` + ScriptSig ScriptSig `json:"scriptSig" ts_doc:"scriptSig object containing the spending script data."` + Sequence uint32 `json:"sequence" ts_doc:"Sequence number for the input."` + Addresses []string `json:"addresses" ts_doc:"Addresses derived from this input's script (if known)."` + Witness [][]byte `json:"-" ts_doc:"Witness data for SegWit inputs (not exposed via JSON)."` } // ScriptPubKey contains data about output script type ScriptPubKey struct { // Asm string `json:"asm"` - Hex string `json:"hex,omitempty"` + Hex string `json:"hex,omitempty" ts_doc:"Hex-encoded representation of the scriptPubKey."` // Type string `json:"type"` - Addresses []string `json:"addresses"` + Addresses []string `json:"addresses" ts_doc:"Addresses derived from this output's script (if known)."` } // 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"` + ValueSat big.Int `ts_doc:"Amount (in satoshi or base unit) for this output."` + JsonValue common.JSONNumber `json:"value" ts_doc:"String-based amount for JSON usage."` + N uint32 `json:"n" ts_doc:"Index of this output in the transaction."` + ScriptPubKey ScriptPubKey `json:"scriptPubKey" ts_doc:"scriptPubKey object containing the output script data."` } // 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"` - VSize int64 `json:"vsize,omitempty"` - Vin []Vin `json:"vin"` - Vout []Vout `json:"vout"` - BlockHeight uint32 `json:"blockHeight,omitempty"` + Hex string `json:"hex" ts_doc:"Hex-encoded transaction data."` + Txid string `json:"txid" ts_doc:"Transaction ID (hash)."` + Version int32 `json:"version" ts_doc:"Transaction version number."` + LockTime uint32 `json:"locktime" ts_doc:"Locktime specifying earliest time/block a tx can be mined."` + VSize int64 `json:"vsize,omitempty" ts_doc:"Virtual size of the transaction (for SegWit-based networks)."` + Vin []Vin `json:"vin" ts_doc:"List of inputs."` + Vout []Vout `json:"vout" ts_doc:"List of outputs."` + BlockHeight uint32 `json:"blockHeight,omitempty" ts_doc:"Block height in which this transaction was included."` // BlockHash string `json:"blockhash,omitempty"` - Confirmations uint32 `json:"confirmations,omitempty"` - Time int64 `json:"time,omitempty"` - Blocktime int64 `json:"blocktime,omitempty"` - CoinSpecificData interface{} `json:"-"` + Confirmations uint32 `json:"confirmations,omitempty" ts_doc:"Number of confirmations the transaction has."` + Time int64 `json:"time,omitempty" ts_doc:"Timestamp when the transaction was broadcast or included in a block."` + Blocktime int64 `json:"blocktime,omitempty" ts_doc:"Timestamp of the block in which the transaction was mined."` + CoinSpecificData interface{} `json:"-" ts_doc:"Additional chain-specific data (not exposed via JSON)."` } -// MempoolVin contains data about tx input +// MempoolVin contains data about tx input specifically in mempool type MempoolVin struct { Vin - AddrDesc AddressDescriptor `json:"-"` - ValueSat big.Int + AddrDesc AddressDescriptor `json:"-" ts_doc:"Internal descriptor for the input address (not exposed)."` + ValueSat big.Int `ts_doc:"Amount (in satoshi or base unit) of the input."` } // 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"` - VSize int64 `json:"vsize,omitempty"` - Vin []MempoolVin `json:"vin"` - Vout []Vout `json:"vout"` - Blocktime int64 `json:"blocktime,omitempty"` - TokenTransfers TokenTransfers `json:"-"` - CoinSpecificData interface{} `json:"-"` + Hex string `json:"hex" ts_doc:"Hex-encoded transaction data."` + Txid string `json:"txid" ts_doc:"Transaction ID (hash)."` + Version int32 `json:"version" ts_doc:"Transaction version number."` + LockTime uint32 `json:"locktime" ts_doc:"Locktime specifying earliest time/block a tx can be mined."` + VSize int64 `json:"vsize,omitempty" ts_doc:"Virtual size of the transaction (if applicable)."` + Vin []MempoolVin `json:"vin" ts_doc:"List of inputs in this mempool transaction."` + Vout []Vout `json:"vout" ts_doc:"List of outputs in this mempool transaction."` + Blocktime int64 `json:"blocktime,omitempty" ts_doc:"Timestamp for the block in which tx might eventually be mined, if known."` + TokenTransfers TokenTransfers `json:"-" ts_doc:"Token transfers discovered in this mempool transaction (not exposed by default)."` + CoinSpecificData interface{} `json:"-" ts_doc:"Additional chain-specific data (not exposed via JSON)."` } -// TokenType - type of token -type TokenType int +// TokenStandard - standard of token +type TokenStandard int -// TokenType enumeration +// TokenStandard enumeration const ( - FungibleToken = TokenType(iota) // ERC20 - NonFungibleToken // ERC721 - MultiToken // ERC1155 + FungibleToken = TokenStandard(iota) // ERC20/BEP20 + NonFungibleToken // ERC721/BEP721 + MultiToken // ERC1155/BEP1155 ) -// TokenTypeName specifies type of token -type TokenTypeName string +// TokenStandardName specifies standard of token +type TokenStandardName string -// Token types +// Token standards const ( - UnknownTokenType TokenTypeName = "" + UnknownTokenStandard TokenStandardName = "" + UnhandledTokenStandard TokenStandardName = "-" - // XPUBAddressTokenType is address derived from xpub - XPUBAddressTokenType TokenTypeName = "XPUBAddress" + // XPUBAddressStandard is address derived from xpub + XPUBAddressStandard TokenStandardName = "XPUBAddress" ) // TokenTransfers is array of TokenTransfer @@ -142,77 +144,83 @@ type TokenTransfers []*TokenTransfer func (a TokenTransfers) Len() int { return len(a) } func (a TokenTransfers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a TokenTransfers) Less(i, j int) bool { - return a[i].Type < a[j].Type + return a[i].Standard < a[j].Standard } // Block is block header and list of transactions type Block struct { BlockHeader - Txs []Tx `json:"tx"` - CoinSpecificData interface{} `json:"-"` + Txs []Tx `json:"tx" ts_doc:"List of full transactions included in this block."` + CoinSpecificData interface{} `json:"-" ts_doc:"Additional chain-specific data (not exposed via JSON)."` } // BlockHeader contains limited data (as needed for indexing) from backend block header type BlockHeader struct { - Hash string `json:"hash"` - Prev string `json:"previousblockhash"` - Next string `json:"nextblockhash"` - Height uint32 `json:"height"` - Confirmations int `json:"confirmations"` - Size int `json:"size"` - Time int64 `json:"time,omitempty"` + Hash string `json:"hash" ts_doc:"Block hash."` + Prev string `json:"previousblockhash" ts_doc:"Hash of the previous block in the chain."` + Next string `json:"nextblockhash" ts_doc:"Hash of the next block, if known."` + Height uint32 `json:"height" ts_doc:"Block height (0-based index in the chain)."` + Confirmations int `json:"confirmations" ts_doc:"Number of confirmations (distance from best chain tip)."` + Size int `json:"size" ts_doc:"Block size in bytes."` + Time int64 `json:"time,omitempty" ts_doc:"Timestamp of when this block was mined."` } // BlockInfo contains extended block header data and a list of block txids type BlockInfo struct { BlockHeader - 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"` + Version common.JSONNumber `json:"version" ts_doc:"Block version (chain-specific meaning)."` + MerkleRoot string `json:"merkleroot" ts_doc:"Merkle root of the block's transactions."` + Nonce common.JSONNumber `json:"nonce" ts_doc:"Nonce used in the mining process."` + Bits string `json:"bits" ts_doc:"Compact representation of the target threshold."` + Difficulty common.JSONNumber `json:"difficulty" ts_doc:"Difficulty target for mining this block."` + Txids []string `json:"tx,omitempty" ts_doc:"List of transaction IDs included in this block."` } // MempoolEntry is used to get data about mempool entry type MempoolEntry struct { - Size uint32 `json:"size"` - FeeSat big.Int - Fee common.JSONNumber `json:"fee"` - ModifiedFeeSat big.Int - 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"` + Size uint32 `json:"size" ts_doc:"Size of the transaction in bytes, as stored in mempool."` + FeeSat big.Int `ts_doc:"Transaction fee in satoshi/base units."` + Fee common.JSONNumber `json:"fee" ts_doc:"String-based fee for JSON usage."` + ModifiedFeeSat big.Int `ts_doc:"Modified fee in satoshi/base units after priority adjustments."` + ModifiedFee common.JSONNumber `json:"modifiedfee" ts_doc:"String-based modified fee for JSON usage."` + Time uint64 `json:"time" ts_doc:"Unix timestamp when the tx entered the mempool."` + Height uint32 `json:"height" ts_doc:"Block height when the tx entered the mempool."` + DescendantCount uint32 `json:"descendantcount" ts_doc:"Number of descendant transactions in mempool."` + DescendantSize uint32 `json:"descendantsize" ts_doc:"Total size of all descendant transactions in bytes."` + DescendantFees uint32 `json:"descendantfees" ts_doc:"Combined fees of all descendant transactions."` + AncestorCount uint32 `json:"ancestorcount" ts_doc:"Number of ancestor transactions in mempool."` + AncestorSize uint32 `json:"ancestorsize" ts_doc:"Total size of all ancestor transactions in bytes."` + AncestorFees uint32 `json:"ancestorfees" ts_doc:"Combined fees of all ancestor transactions."` + Depends []string `json:"depends" ts_doc:"List of txids this transaction depends on."` } // ChainInfo is used to get information about blockchain type ChainInfo struct { - Chain string `json:"chain"` - Blocks int `json:"blocks"` - Headers int `json:"headers"` - Bestblockhash string `json:"bestblockhash"` - Difficulty string `json:"difficulty"` - SizeOnDisk int64 `json:"size_on_disk"` - Version string `json:"version"` - Subversion string `json:"subversion"` - ProtocolVersion string `json:"protocolversion"` - Timeoffset float64 `json:"timeoffset"` - Warnings string `json:"warnings"` - ConsensusVersion string `json:"consensus_version,omitempty"` - Consensus interface{} `json:"consensus,omitempty"` + Chain string `json:"chain" ts_doc:"Name of the chain (e.g. 'main')."` + Blocks int `json:"blocks" ts_doc:"Number of fully verified blocks in the chain."` + Headers int `json:"headers" ts_doc:"Number of block headers in the chain (can be ahead of full blocks)."` + Bestblockhash string `json:"bestblockhash" ts_doc:"Hash of the best (latest) block."` + Difficulty string `json:"difficulty" ts_doc:"Current difficulty of the network."` + SizeOnDisk int64 `json:"size_on_disk" ts_doc:"Size of the blockchain data on disk in bytes."` + Version string `json:"version" ts_doc:"Version of the blockchain backend."` + Subversion string `json:"subversion" ts_doc:"Subversion string of the blockchain backend."` + ProtocolVersion string `json:"protocolversion" ts_doc:"Protocol version for this chain node."` + Timeoffset float64 `json:"timeoffset" ts_doc:"Time offset (in seconds) reported by the node."` + Warnings string `json:"warnings" ts_doc:"Any warnings generated by the node regarding the chain state."` + ConsensusVersion string `json:"consensus_version,omitempty" ts_doc:"Version of the chain's consensus protocol, if available."` + Consensus interface{} `json:"consensus,omitempty" ts_doc:"Additional consensus details, structure depends on chain."` +} + +// LongTermFeeRate gets information about the fee rate over longer period of time. +type LongTermFeeRate struct { + FeePerUnit big.Int `json:"feePerUnit" ts_doc:"Long term fee rate (in sat/kByte)."` + Blocks uint64 `json:"blocks" ts_doc:"Amount of blocks used for the long term fee rate estimation."` } // RPCError defines rpc error returned by backend type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` + Code int `json:"code" ts_doc:"Error code returned by the backend RPC."` + Message string `json:"message" ts_doc:"Human-readable error message."` } func (e *RPCError) Error() string { @@ -226,6 +234,13 @@ func (ad AddressDescriptor) String() string { return "ad:" + hex.EncodeToString(ad) } +func (ad AddressDescriptor) IsTaproot() bool { + if len(ad) == 34 && ad[0] == 0x51 && ad[1] == 0x20 { + return true + } + return false +} + // AddressDescriptorFromString converts string created by AddressDescriptor.String to AddressDescriptor func AddressDescriptorFromString(s string) (AddressDescriptor, error) { if len(s) > 3 && s[0:3] == "ad:" { @@ -236,8 +251,8 @@ func AddressDescriptorFromString(s string) (AddressDescriptor, error) { // MempoolTxidEntry contains mempool txid with first seen time type MempoolTxidEntry struct { - Txid string - Time uint32 + Txid string `ts_doc:"Transaction ID (hash) of the mempool entry."` + Time uint32 `ts_doc:"Unix timestamp when the transaction was first seen in the mempool."` } // ScriptType - type of output script parsed from xpub (descriptor) @@ -254,17 +269,24 @@ const ( // 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 + XpubDescriptor string `ts_doc:"Full descriptor string including xpub and script type."` + Xpub string `ts_doc:"The xpub part itself extracted from the descriptor."` + Type ScriptType `ts_doc:"Parsed script type (P2PKH, P2WPKH, etc.)."` + Bip string `ts_doc:"BIP standard (e.g. BIP44) inferred from the descriptor."` + ChangeIndexes []uint32 `ts_doc:"Indexes designated as change addresses."` + ExtKey interface{} `ts_doc:"Extended key object parsed from xpub (implementation-specific)."` } // MempoolTxidEntries is array of MempoolTxidEntry type MempoolTxidEntries []MempoolTxidEntry +// MempoolTxidFilterEntries is a map of txids to mempool golomb filters +// Also contains a flag whether constant zeroed key was used when calculating the filters +type MempoolTxidFilterEntries struct { + Entries map[string]string `json:"entries,omitempty" ts_doc:"Map of txid to filter data (hex-encoded)."` + UsedZeroedKey bool `json:"usedZeroedKey,omitempty" ts_doc:"Indicates if a zeroed key was used in filter calculation."` +} + // OnNewBlockFunc is used to send notification about a new block type OnNewBlockFunc func(hash string, height uint32) @@ -308,7 +330,8 @@ type BlockChain interface { GetTransactionSpecific(tx *Tx) (json.RawMessage, error) EstimateSmartFee(blocks int, conservative bool) (big.Int, error) EstimateFee(blocks int) (big.Int, error) - SendRawTransaction(tx string) (string, error) + LongTermFeeRate() (*LongTermFeeRate, error) + SendRawTransaction(tx string, disableAlternativeRPC bool) (string, error) GetMempoolEntry(txid string) (*MempoolEntry, error) GetContractInfo(contractDesc AddressDescriptor) (*ContractInfo, error) // parser @@ -317,7 +340,12 @@ type BlockChain interface { EthereumTypeGetBalance(addrDesc AddressDescriptor) (*big.Int, error) EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) + EthereumTypeGetEip1559Fees() (*Eip1559Fees, error) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) + EthereumTypeGetSupportedStakingPools() []string + EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error) + EthereumTypeRpcCall(data, to, from string) (string, error) + EthereumTypeGetRawTransaction(txid string) (string, error) GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error) } @@ -378,4 +406,5 @@ type Mempool interface { GetAddrDescTransactions(addrDesc AddressDescriptor) ([]Outpoint, error) GetAllEntries() MempoolTxidEntries GetTransactionTime(txid string) uint32 + GetTxidFilterEntries(filterScripts string, fromTimestamp uint32) (MempoolTxidFilterEntries, error) } diff --git a/bchain/types_ethereum_type.go b/bchain/types_ethereum_type.go index adceec3b2e..b93632ec45 100644 --- a/bchain/types_ethereum_type.go +++ b/bchain/types_ethereum_type.go @@ -6,35 +6,37 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" ) -// EthereumType specific - // EthereumInternalTransfer contains data about internal transfer type EthereumInternalTransfer struct { - Type EthereumInternalTransactionType `json:"type"` - From string `json:"from"` - To string `json:"to"` - Value big.Int `json:"value"` + Type EthereumInternalTransactionType `json:"type" ts_doc:"The type of internal transaction (CALL, CREATE, SELFDESTRUCT)."` + From string `json:"from" ts_doc:"Sender address of this internal transfer."` + To string `json:"to" ts_doc:"Recipient address of this internal transfer."` + Value big.Int `json:"value" ts_doc:"Amount (in Wei) transferred internally."` } +// FourByteSignature contains data about a contract function signature type FourByteSignature struct { // stored in DB - Name string - Parameters []string + Name string `ts_doc:"Original function name as stored in the database."` + Parameters []string `ts_doc:"Raw parameter type definitions (e.g. ['uint256','address'])."` // processed from DB data and stored only in cache - DecamelName string - Function string - ParsedParameters []abi.Type + DecamelName string `ts_doc:"A decamelized version of the function name for readability."` + Function string `ts_doc:"Reconstructed function definition string (e.g. 'transfer(address,uint256)')."` + ParsedParameters []abi.Type `ts_doc:"ABI-parsed parameter types (cached for efficiency)."` } +// EthereumParsedInputParam contains data about a contract function parameter type EthereumParsedInputParam struct { - Type string `json:"type"` - Values []string `json:"values,omitempty"` + Type string `json:"type" ts_doc:"Parameter type (e.g. 'uint256')."` + Values []string `json:"values,omitempty" ts_doc:"List of stringified parameter values."` } + +// EthereumParsedInputData contains the parsed data for an input data hex payload type EthereumParsedInputData struct { - MethodId string `json:"methodId"` - Name string `json:"name"` - Function string `json:"function,omitempty"` - Params []EthereumParsedInputParam `json:"params,omitempty"` + MethodId string `json:"methodId" ts_doc:"First 4 bytes of the input data (method signature ID)."` + Name string `json:"name" ts_doc:"Parsed function name if recognized."` + Function string `json:"function,omitempty" ts_doc:"Full function signature (including parameter types)."` + Params []EthereumParsedInputParam `json:"params,omitempty" ts_doc:"List of parsed parameters for this function call."` } // EthereumInternalTransactionType - type of ethereum transaction from internal data @@ -47,64 +49,70 @@ const ( SELFDESTRUCT ) -// EthereumInternalTransaction contains internal transfers +// EthereumInternalData contains internal transfers type EthereumInternalData struct { - Type EthereumInternalTransactionType `json:"type"` - Contract string `json:"contract,omitempty"` - Transfers []EthereumInternalTransfer `json:"transfers,omitempty"` - Error string + Type EthereumInternalTransactionType `json:"type" ts_doc:"High-level type of the internal transaction (CALL, CREATE, etc.)."` + Contract string `json:"contract,omitempty" ts_doc:"Address of the contract involved, if any."` + Transfers []EthereumInternalTransfer `json:"transfers,omitempty" ts_doc:"List of internal transfers associated with this data."` + Error string `ts_doc:"Error message if something went wrong while processing."` } // ContractInfo contains info about a contract type ContractInfo struct { - Type TokenTypeName `json:"type"` - Contract string `json:"contract"` - Name string `json:"name"` - Symbol string `json:"symbol"` - Decimals int `json:"decimals"` - CreatedInBlock uint32 `json:"createdInBlock,omitempty"` - DestructedInBlock uint32 `json:"destructedInBlock,omitempty"` + // Deprecated: Use Standard instead. + Type TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'" ts_doc:"@deprecated: Use standard instead."` + Standard TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'"` + Contract string `json:"contract" ts_doc:"Smart contract address."` + Name string `json:"name" ts_doc:"Readable name of the contract."` + Symbol string `json:"symbol" ts_doc:"Symbol for tokens under this contract, if applicable."` + Decimals int `json:"decimals" ts_doc:"Number of decimal places, if applicable."` + CreatedInBlock uint32 `json:"createdInBlock,omitempty" ts_doc:"Block height where contract was first created."` + DestructedInBlock uint32 `json:"destructedInBlock,omitempty" ts_doc:"Block height where contract was destroyed (if any)."` } -// Ethereum token type names +// Ethereum token standard names const ( - ERC20TokenType TokenTypeName = "ERC20" - ERC771TokenType TokenTypeName = "ERC721" - ERC1155TokenType TokenTypeName = "ERC1155" + ERC20TokenStandard TokenStandardName = "ERC20" + ERC771TokenStandard TokenStandardName = "ERC721" + ERC1155TokenStandard TokenStandardName = "ERC1155" ) -// EthereumTokenTypeMap maps bchain.TokenType to TokenTypeName -// the map must match all bchain.TokenType to avoid index out of range panic -var EthereumTokenTypeMap []TokenTypeName = []TokenTypeName{ERC20TokenType, ERC771TokenType, ERC1155TokenType} +// EthereumTokenStandardMap maps bchain.TokenStandard to TokenStandardName +// the map must match all bchain.TokenStandard to avoid index out of range panic +var EthereumTokenStandardMap = []TokenStandardName{ERC20TokenStandard, ERC771TokenStandard, ERC1155TokenStandard} +// MultiTokenValue holds one ID-value pair for multi-token standards like ERC1155 type MultiTokenValue struct { - Id big.Int - Value big.Int + Id big.Int `ts_doc:"Token ID for this multi-token entry."` + Value big.Int `ts_doc:"Amount of the token ID transferred or owned."` } // TokenTransfer contains a single token transfer type TokenTransfer struct { - Type TokenType - Contract string - From string - To string - Value big.Int - MultiTokenValues []MultiTokenValue + Standard TokenStandard `ts_doc:"Integer value od the token standard."` + Contract string `ts_doc:"Smart contract address for the token."` + From string `ts_doc:"Sender address of the token transfer."` + To string `ts_doc:"Recipient address of the token transfer."` + Value big.Int `ts_doc:"Amount of tokens transferred (for fungible tokens)."` + MultiTokenValues []MultiTokenValue `ts_doc:"List of ID-value pairs for multi-token transfers (e.g., ERC1155)."` } // RpcTransaction is returned by eth_getTransactionByHash type RpcTransaction struct { - AccountNonce string `json:"nonce"` - GasPrice string `json:"gasPrice"` - GasLimit string `json:"gas"` - To string `json:"to"` // nil means contract creation - Value string `json:"value"` - Payload string `json:"input"` - Hash string `json:"hash"` - BlockNumber string `json:"blockNumber"` - BlockHash string `json:"blockHash,omitempty"` - From string `json:"from"` - TransactionIndex string `json:"transactionIndex"` + AccountNonce string `json:"nonce" ts_doc:"Transaction nonce from the sender's account."` + GasPrice string `json:"gasPrice" ts_doc:"Gas price bid by the sender in Wei."` + MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas,omitempty"` + MaxFeePerGas string `json:"maxFeePerGas,omitempty"` + BaseFeePerGas string `json:"baseFeePerGas,omitempty"` + GasLimit string `json:"gas" ts_doc:"Maximum gas allowed for this transaction."` + To string `json:"to" ts_doc:"Recipient address if not a contract creation. Empty if it's contract creation."` + Value string `json:"value" ts_doc:"Amount of Ether (in Wei) sent in this transaction."` + Payload string `json:"input" ts_doc:"Hex-encoded input data for contract calls."` + Hash string `json:"hash" ts_doc:"Transaction hash."` + BlockNumber string `json:"blockNumber" ts_doc:"Block number where this transaction was included, if mined."` + BlockHash string `json:"blockHash,omitempty" ts_doc:"Hash of the block in which this transaction was included, if mined."` + From string `json:"from" ts_doc:"Sender's address derived by the backend."` + TransactionIndex string `json:"transactionIndex" ts_doc:"Index of the transaction within the block, if mined."` // Signature values - ignored // V string `json:"v"` // R string `json:"r"` @@ -113,34 +121,74 @@ type RpcTransaction struct { // RpcLog is returned by eth_getLogs type RpcLog struct { - Address string `json:"address"` - Topics []string `json:"topics"` - Data string `json:"data"` + Address string `json:"address" ts_doc:"Contract or address from which this log originated."` + Topics []string `json:"topics" ts_doc:"Indexed event signatures and parameters."` + Data string `json:"data" ts_doc:"Unindexed event data in hex form."` } -// RpcLog is returned by eth_getTransactionReceipt +// RpcReceipt is returned by eth_getTransactionReceipt type RpcReceipt struct { - GasUsed string `json:"gasUsed"` - Status string `json:"status"` - Logs []*RpcLog `json:"logs"` + GasUsed string `json:"gasUsed" ts_doc:"Amount of gas actually used by the transaction."` + Status string `json:"status" ts_doc:"Transaction execution status (0x0 = fail, 0x1 = success)."` + Logs []*RpcLog `json:"logs" ts_doc:"Array of log entries generated by this transaction."` + L1Fee string `json:"l1Fee,omitempty" ts_doc:"Additional Layer 1 fee, if on a rollup network."` + L1FeeScalar string `json:"l1FeeScalar,omitempty" ts_doc:"Fee scaling factor for L1 fees on some L2s."` + L1GasPrice string `json:"l1GasPrice,omitempty" ts_doc:"Gas price used on L1 for the rollup network."` + L1GasUsed string `json:"l1GasUsed,omitempty" ts_doc:"Amount of L1 gas used by the transaction, if any."` } // EthereumSpecificData contains data specific to Ethereum transactions type EthereumSpecificData struct { - Tx *RpcTransaction `json:"tx"` - InternalData *EthereumInternalData `json:"internalData,omitempty"` - Receipt *RpcReceipt `json:"receipt,omitempty"` + Tx *RpcTransaction `json:"tx" ts_doc:"Raw transaction details from the blockchain node."` + InternalData *EthereumInternalData `json:"internalData,omitempty" ts_doc:"Summary of internal calls/transfers, if any."` + Receipt *RpcReceipt `json:"receipt,omitempty" ts_doc:"Transaction receipt info, including logs and gas usage."` } // AddressAliasRecord maps address to ENS name type AddressAliasRecord struct { - Address string - Name string + Address string `ts_doc:"Address whose alias is being stored."` + Name string `ts_doc:"The resolved name/alias (e.g. ENS domain)."` } // EthereumBlockSpecificData contain data specific for Ethereum block type EthereumBlockSpecificData struct { - InternalDataError string - AddressAliasRecords []AddressAliasRecord - Contracts []ContractInfo + InternalDataError string `ts_doc:"Error message for processing block internal data, if any."` + AddressAliasRecords []AddressAliasRecord `ts_doc:"List of address-to-alias mappings discovered in this block."` + Contracts []ContractInfo `ts_doc:"List of contracts created or updated in this block."` +} + +// StakingPoolData holds data about address participation in a staking pool contract +type StakingPoolData struct { + Contract string `json:"contract" ts_doc:"Address of the staking pool contract."` + Name string `json:"name" ts_doc:"Human-readable name of the staking pool."` + PendingBalance big.Int `json:"pendingBalance" ts_doc:"Amount not yet finalized in the pool (pendingBalanceOf)."` + PendingDepositedBalance big.Int `json:"pendingDepositedBalance" ts_doc:"Amount pending deposit (pendingDepositedBalanceOf)."` + DepositedBalance big.Int `json:"depositedBalance" ts_doc:"Total amount currently deposited (depositedBalanceOf)."` + WithdrawTotalAmount big.Int `json:"withdrawTotalAmount" ts_doc:"Total amount requested for withdrawal (withdrawRequest[0])."` + ClaimableAmount big.Int `json:"claimableAmount" ts_doc:"Amount that can be claimed (withdrawRequest[1])."` + RestakedReward big.Int `json:"restakedReward" ts_doc:"Total reward that has been restaked (restakedRewardOf)."` + AutocompoundBalance big.Int `json:"autocompoundBalance" ts_doc:"Auto-compounded balance (autocompoundBalanceOf)."` +} + +// Eip1559Fee +type Eip1559Fee struct { + MaxFeePerGas *big.Int `json:"maxFeePerGas"` + MaxPriorityFeePerGas *big.Int `json:"maxPriorityFeePerGas"` + MinWaitTimeEstimate int `json:"minWaitTimeEstimate,omitempty"` + MaxWaitTimeEstimate int `json:"maxWaitTimeEstimate,omitempty"` +} + +// Eip1559Fees +type Eip1559Fees struct { + BaseFeePerGas *big.Int `json:"baseFeePerGas,omitempty"` + Low *Eip1559Fee `json:"low,omitempty"` + Medium *Eip1559Fee `json:"medium,omitempty"` + High *Eip1559Fee `json:"high,omitempty"` + Instant *Eip1559Fee `json:"instant,omitempty"` + NetworkCongestion float64 `json:"networkCongestion,omitempty"` + LatestPriorityFeeRange []*big.Int `json:"latestPriorityFeeRange,omitempty"` + HistoricalPriorityFeeRange []*big.Int `json:"historicalPriorityFeeRange,omitempty"` + HistoricalBaseFeeRange []*big.Int `json:"historicalBaseFeeRange,omitempty"` + PriorityFeeTrend string `json:"priorityFeeTrend,omitempty"` + BaseFeeTrend string `json:"baseFeeTrend,omitempty"` } diff --git a/blockbook-api.ts b/blockbook-api.ts new file mode 100644 index 0000000000..56ec8ae73c --- /dev/null +++ b/blockbook-api.ts @@ -0,0 +1,815 @@ +/* Do not change, this code is generated from Golang structs */ + +export interface APIError { + /** Human-readable error message describing the issue. */ + Text: string; + /** Whether the error message can safely be shown to the end user. */ + Public: boolean; +} +export interface AddressAlias { + /** Type of alias, e.g., user-defined name or contract name. */ + Type: string; + /** Alias string for the address. */ + Alias: string; +} +export interface EthereumInternalTransfer { + /** Type of internal transfer (CALL, CREATE, etc.). */ + type: number; + /** Address from which the transfer originated. */ + from: string; + /** Address to which the transfer was sent. */ + to: string; + /** Value transferred internally (in Wei or base units). */ + value: string; +} +export interface EthereumParsedInputParam { + /** Parameter type (e.g. 'uint256'). */ + type: string; + /** List of stringified parameter values. */ + values?: string[]; +} +export interface EthereumParsedInputData { + /** First 4 bytes of the input data (method signature ID). */ + methodId: string; + /** Parsed function name if recognized. */ + name: string; + /** Full function signature (including parameter types). */ + function?: string; + /** List of parsed parameters for this function call. */ + params?: EthereumParsedInputParam[]; +} +export interface EthereumSpecific { + /** High-level type of the Ethereum tx (e.g., 'call', 'create'). */ + type?: number; + /** Address of contract created by this transaction, if any. */ + createdContract?: string; + /** Execution status of the transaction (1: success, 0: fail, -1: pending). */ + status: number; + /** Error encountered during execution, if any. */ + error?: string; + /** Transaction nonce (sequential number from the sender). */ + nonce: number; + /** Maximum gas allowed by the sender for this transaction. */ + gasLimit: number; + /** Actual gas consumed by the transaction execution. */ + gasUsed?: number; + /** Price (in Wei or base units) per gas unit. */ + gasPrice?: string; + maxPriorityFeePerGas?: string; + maxFeePerGas?: string; + baseFeePerGas?: string; + /** Fee used for L1 part in rollups (e.g. Optimism). */ + l1Fee?: number; + /** Scaling factor for L1 fees in certain Layer 2 solutions. */ + l1FeeScalar?: string; + /** Gas price for L1 component, if applicable. */ + l1GasPrice?: string; + /** Amount of gas used in L1 for this tx, if applicable. */ + l1GasUsed?: number; + /** Hex-encoded input data for the transaction. */ + data?: string; + /** Decoded transaction data (function name, params, etc.). */ + parsedData?: EthereumParsedInputData; + /** List of internal (sub-call) transfers. */ + internalTransfers?: EthereumInternalTransfer[]; +} +export interface MultiTokenValue { + /** Token ID (for ERC1155). */ + id?: string; + /** Amount of that specific token ID. */ + value?: string; +} +export interface TokenTransfer { + /** @deprecated: Use standard instead. */ + type: '' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'; + standard: '' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'; + /** Source address of the token transfer. */ + from: string; + /** Destination address of the token transfer. */ + to: string; + /** Contract address of the token. */ + contract: string; + /** Token name. */ + name?: string; + /** Token symbol. */ + symbol?: string; + /** Number of decimals for this token (if applicable). */ + decimals: number; + /** Amount (in base units) of tokens transferred. */ + value?: string; + /** List of multiple ID-value pairs for ERC1155 transfers. */ + multiTokenValues?: MultiTokenValue[]; +} +export interface Vout { + /** Amount (in satoshi or base units) of the output. */ + value?: string; + /** Relative index of this output within the transaction. */ + n: number; + /** Indicates whether this output has been spent. */ + spent?: boolean; + /** Transaction ID in which this output was spent. */ + spentTxId?: string; + /** Index of the input that spent this output. */ + spentIndex?: number; + /** Block height at which this output was spent. */ + spentHeight?: number; + /** Raw script hex data for this output - aka ScriptPubKey. */ + hex?: string; + /** Disassembled script for this output. */ + asm?: string; + /** List of addresses associated with this output. */ + addresses: string[]; + /** Indicates whether this output is owned by valid address. */ + isAddress: boolean; + /** Indicates if this output belongs to the wallet in context. */ + isOwn?: boolean; + /** Output script type (e.g., 'P2PKH', 'P2SH'). */ + type?: string; +} +export interface Vin { + /** ID/hash of the originating transaction (where the UTXO comes from). */ + txid?: string; + /** Index of the output in the referenced transaction. */ + vout?: number; + /** Sequence number for this input (e.g. 4294967293). */ + sequence?: number; + /** Relative index of this input within the transaction. */ + n: number; + /** List of addresses associated with this input. */ + addresses?: string[]; + /** Indicates if this input is from a known address. */ + isAddress: boolean; + /** Indicates if this input belongs to the wallet in context. */ + isOwn?: boolean; + /** Amount (in satoshi or base units) of the input. */ + value?: string; + /** Raw script hex data for this input. */ + hex?: string; + /** Disassembled script for this input. */ + asm?: string; + /** Data for coinbase inputs (when mining). */ + coinbase?: string; +} +export interface Tx { + /** Transaction ID (hash). */ + txid: string; + /** Version of the transaction (if applicable). */ + version?: number; + /** Locktime indicating earliest time/height transaction can be mined. */ + lockTime?: number; + /** Array of inputs for this transaction. */ + vin: Vin[]; + /** Array of outputs for this transaction. */ + vout: Vout[]; + /** Hash of the block containing this transaction. */ + blockHash?: string; + /** Block height in which this transaction was included. */ + blockHeight: number; + /** Number of confirmations (blocks mined after this tx's block). */ + confirmations: number; + /** Estimated blocks remaining until confirmation (if unconfirmed). */ + confirmationETABlocks?: number; + /** Estimated seconds remaining until confirmation (if unconfirmed). */ + confirmationETASeconds?: number; + /** Unix timestamp of the block in which this transaction was included. 0 if unconfirmed. */ + blockTime: number; + /** Transaction size in bytes. */ + size?: number; + /** Virtual size in bytes, for SegWit-enabled chains. */ + vsize?: number; + /** Total value of all outputs (in satoshi or base units). */ + value: string; + /** Total value of all inputs (in satoshi or base units). */ + valueIn?: string; + /** Transaction fee (inputs - outputs). */ + fees?: string; + /** Raw hex-encoded transaction data. */ + hex?: string; + /** Indicates if this transaction is replace-by-fee (RBF) enabled. */ + rbf?: boolean; + /** Blockchain-specific extended data. */ + coinSpecificData?: any; + /** List of token transfers that occurred in this transaction. */ + tokenTransfers?: TokenTransfer[]; + /** Ethereum-like blockchain specific data (if applicable). */ + ethereumSpecific?: EthereumSpecific; + /** Aliases for addresses involved in this transaction. */ + addressAliases?: { [key: string]: AddressAlias }; +} +export interface FeeStats { + /** Number of transactions in the given block. */ + txCount: number; + /** Sum of all fees in satoshi or base units. */ + totalFeesSat: string; + /** Average fee per kilobyte in satoshi or base units. */ + averageFeePerKb: number; + /** Fee distribution deciles (0%..100%) in satoshi or base units per kB. */ + decilesFeePerKb: number[]; +} +export interface StakingPool { + /** Staking pool contract address on-chain. */ + contract: string; + /** Name of the staking pool contract. */ + name: string; + /** Balance pending deposit or withdrawal, if any. */ + pendingBalance: string; + /** Any pending deposit that is not yet finalized. */ + pendingDepositedBalance: string; + /** Currently deposited/staked balance. */ + depositedBalance: string; + /** Total amount withdrawn from this pool by the address. */ + withdrawTotalAmount: string; + /** Rewards or principal currently claimable by the address. */ + claimableAmount: string; + /** Total rewards that have been restaked automatically. */ + restakedReward: string; + /** Any balance automatically reinvested into the pool. */ + autocompoundBalance: string; +} +export interface ContractInfo { + /** @deprecated: Use standard instead. */ + type: '' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'; + standard: '' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'; + /** Smart contract address. */ + contract: string; + /** Readable name of the contract. */ + name: string; + /** Symbol for tokens under this contract, if applicable. */ + symbol: string; + /** Number of decimal places, if applicable. */ + decimals: number; + /** Block height where contract was first created. */ + createdInBlock?: number; + /** Block height where contract was destroyed (if any). */ + destructedInBlock?: number; +} +export interface Token { + /** @deprecated: Use standard instead. */ + type: '' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'; + standard: '' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'; + /** Readable name of the token. */ + name: string; + /** Derivation path if this token is derived from an XPUB-based address. */ + path?: string; + /** Contract address on-chain. */ + contract?: string; + /** Total number of token transfers for this address. */ + transfers: number; + /** Symbol for the token (e.g., 'ETH', 'USDT'). */ + symbol?: string; + /** Number of decimals for this token. */ + decimals: number; + /** Current token balance (in minimal base units). */ + balance?: string; + /** Value in the base currency (e.g. ETH for ERC20 tokens). */ + baseValue?: number; + /** Value in a secondary currency (e.g. fiat), if available. */ + secondaryValue?: number; + /** List of token IDs (for ERC721, each ID is a unique collectible). */ + ids?: string[]; + /** Multiple ERC1155 token balances (id + value). */ + multiTokenValues?: MultiTokenValue[]; + /** Total amount of tokens received. */ + totalReceived?: string; + /** Total amount of tokens sent. */ + totalSent?: string; +} +export interface Address { + /** Current page index. */ + page?: number; + /** Total number of pages available. */ + totalPages?: number; + /** Number of items returned on this page. */ + itemsOnPage?: number; + /** The address string in standard format. */ + address: string; + /** Current confirmed balance (in satoshi or base units). */ + balance: string; + /** Total amount ever received by this address. */ + totalReceived?: string; + /** Total amount ever sent by this address. */ + totalSent?: string; + /** Unconfirmed balance for this address. */ + unconfirmedBalance: string; + /** Number of unconfirmed transactions for this address. */ + unconfirmedTxs: number; + /** Unconfirmed outgoing balance for this address. */ + unconfirmedSending?: string; + /** Unconfirmed incoming balance for this address. */ + unconfirmedReceiving?: string; + /** Number of transactions for this address (including confirmed). */ + txs: number; + /** Historical total count of transactions, if known. */ + addrTxCount?: number; + /** Number of transactions not involving tokens (pure coin transfers). */ + nonTokenTxs?: number; + /** Number of internal transactions (e.g., Ethereum calls). */ + internalTxs?: number; + /** List of transaction details (if requested). */ + transactions?: Tx[]; + /** List of transaction IDs (if detailed data is not requested). */ + txids?: string[]; + /** Current transaction nonce for Ethereum-like addresses. */ + nonce?: string; + /** Number of tokens with any historical usage at this address. */ + usedTokens?: number; + /** List of tokens associated with this address. */ + tokens?: Token[]; + /** Total value of the address in secondary currency (e.g. fiat). */ + secondaryValue?: number; + /** Sum of token values in base currency. */ + tokensBaseValue?: number; + /** Sum of token values in secondary currency (fiat). */ + tokensSecondaryValue?: number; + /** Address's entire value in base currency, including tokens. */ + totalBaseValue?: number; + /** Address's entire value in secondary currency, including tokens. */ + totalSecondaryValue?: number; + /** Extra info if the address is a contract (ABI, type). */ + contractInfo?: ContractInfo; + /** @deprecated: replaced by contractInfo */ + erc20Contract?: ContractInfo; + /** Aliases assigned to this address. */ + addressAliases?: { [key: string]: AddressAlias }; + /** List of staking pool data if address interacts with staking. */ + stakingPools?: StakingPool[]; +} +export interface Utxo { + /** Transaction ID in which this UTXO was created. */ + txid: string; + /** Index of the output in that transaction. */ + vout: number; + /** Value of this UTXO (in satoshi or base units). */ + value: string; + /** Block height in which the UTXO was confirmed. */ + height?: number; + /** Number of confirmations for this UTXO. */ + confirmations: number; + /** Address to which this UTXO belongs. */ + address?: string; + /** Derivation path for XPUB-based wallets, if applicable. */ + path?: string; + /** If non-zero, locktime required before spending this UTXO. */ + lockTime?: number; + /** Indicates if this UTXO originated from a coinbase transaction. */ + coinbase?: boolean; +} +export interface BalanceHistory { + /** Unix timestamp for this point in the balance history. */ + time: number; + /** Number of transactions in this interval. */ + txs: number; + /** Amount received in this interval (in satoshi or base units). */ + received: string; + /** Amount sent in this interval (in satoshi or base units). */ + sent: string; + /** Amount sent to the same address (self-transfer). */ + sentToSelf: string; + /** Exchange rates at this point in time, if available. */ + rates?: { [key: string]: number }; + /** Transaction ID if the time corresponds to a specific tx. */ + txid?: string; +} +export interface BlockInfo { + Hash: string; + Time: number; + Txs: number; + Size: number; + Height: number; +} +export interface Blocks { + /** Current page index. */ + page?: number; + /** Total number of pages available. */ + totalPages?: number; + /** Number of items returned on this page. */ + itemsOnPage?: number; + /** List of blocks. */ + blocks: BlockInfo[]; +} +export interface Block { + /** Current page index. */ + page?: number; + /** Total number of pages available. */ + totalPages?: number; + /** Number of items returned on this page. */ + itemsOnPage?: number; + /** Block hash. */ + hash: string; + /** Hash of the previous block in the chain. */ + previousBlockHash?: string; + /** Hash of the next block, if known. */ + nextBlockHash?: string; + /** Block height (0-based index in the chain). */ + height: number; + /** Number of confirmations of this block (distance from best chain tip). */ + confirmations: number; + /** Size of the block in bytes. */ + size: number; + /** Timestamp of when this block was mined. */ + time?: number; + /** Block version (chain-specific meaning). */ + version: string; + /** Merkle root of the block's transactions. */ + merkleRoot: string; + /** Nonce used in the mining process. */ + nonce: string; + /** Compact representation of the target threshold. */ + bits: string; + /** Difficulty target for mining this block. */ + difficulty: string; + /** List of transaction IDs included in this block. */ + tx?: string[]; + /** Total count of transactions in this block. */ + txCount: number; + /** List of full transaction details (if requested). */ + txs?: Tx[]; + /** Optional aliases for addresses found in this block. */ + addressAliases?: { [key: string]: AddressAlias }; +} +export interface BlockRaw { + /** Hex-encoded block data. */ + hex: string; +} +export interface BackendInfo { + /** Error message if something went wrong in the backend. */ + error?: string; + /** Name of the chain - e.g. 'main'. */ + chain?: string; + /** Number of fully verified blocks in the chain. */ + blocks?: number; + /** Number of block headers in the chain. */ + headers?: number; + /** Hash of the best block in hex. */ + bestBlockHash?: string; + /** Current difficulty of the network. */ + difficulty?: string; + /** Size of the blockchain data on disk in bytes. */ + sizeOnDisk?: number; + /** Version of the blockchain backend - e.g. '280000'. */ + version?: string; + /** Subversion of the blockchain backend - e.g. '/Satoshi:28.0.0/'. */ + subversion?: string; + /** Protocol version of the blockchain backend - e.g. '70016'. */ + protocolVersion?: string; + /** Time offset (in seconds) reported by the backend. */ + timeOffset?: number; + /** Any warnings given by the backend regarding the chain state. */ + warnings?: string; + /** Version or details of the consensus protocol in use. */ + consensus_version?: string; + /** Additional chain-specific consensus data. */ + consensus?: any; +} +export interface InternalStateColumn { + /** Name of the database column. */ + name: string; + /** Version or schema version of the column. */ + version: number; + /** Number of rows stored in this column. */ + rows: number; + /** Total size (in bytes) of keys stored in this column. */ + keyBytes: number; + /** Total size (in bytes) of values stored in this column. */ + valueBytes: number; + /** Timestamp of the last update to this column. */ + updated: string; +} +export interface BlockbookInfo { + /** Coin name, e.g. 'Bitcoin'. */ + coin: string; + /** Network shortcut, e.g. 'BTC'. */ + network: string; + /** Hostname of the blockbook instance, e.g. 'backend5'. */ + host: string; + /** Running blockbook version, e.g. '0.4.0'. */ + version: string; + /** Git commit hash of the running blockbook, e.g. 'a0960c8e'. */ + gitCommit: string; + /** Build time of running blockbook, e.g. '2024-08-08T12:32:50+00:00'. */ + buildTime: string; + /** If true, blockbook is syncing from scratch or in a special sync mode. */ + syncMode: boolean; + /** Indicates if blockbook is in its initial sync phase. */ + initialSync: boolean; + /** Indicates if the backend is fully synced with the blockchain. */ + inSync: boolean; + /** Best (latest) block height according to this instance. */ + bestHeight: number; + /** Timestamp of the latest block in the chain. */ + lastBlockTime: string; + /** Indicates if mempool info is synced as well. */ + inSyncMempool: boolean; + /** Timestamp of the last mempool update. */ + lastMempoolTime: string; + /** Number of unconfirmed transactions in the mempool. */ + mempoolSize: number; + /** Number of decimals for this coin's base unit. */ + decimals: number; + /** Size of the underlying database in bytes. */ + dbSize: number; + /** Whether this instance provides fiat exchange rates. */ + hasFiatRates?: boolean; + /** Whether this instance provides fiat exchange rates for tokens. */ + hasTokenFiatRates?: boolean; + /** Timestamp of the latest fiat rates update. */ + currentFiatRatesTime?: string; + /** Timestamp of the latest historical fiat rates update. */ + historicalFiatRatesTime?: string; + /** Timestamp of the latest historical token fiat rates update. */ + historicalTokenFiatRatesTime?: string; + /** List of contract addresses supported for staking. */ + supportedStakingPools?: string[]; + /** Optional calculated DB size from columns. */ + dbSizeFromColumns?: number; + /** List of columns/tables in the DB for internal state. */ + dbColumns?: InternalStateColumn[]; + /** Additional human-readable info about this blockbook instance. */ + about: string; +} +export interface SystemInfo { + /** Blockbook instance information. */ + blockbook: BlockbookInfo; + /** Information about the connected backend node. */ + backend: BackendInfo; +} +export interface FiatTicker { + /** Unix timestamp for these fiat rates. */ + ts?: number; + /** Map of currency codes to their exchange rate. */ + rates: { [key: string]: number }; + /** Any error message encountered while fetching rates. */ + error?: string; +} +export interface FiatTickers { + /** List of fiat tickers with timestamps and rates. */ + tickers: FiatTicker[]; +} +export interface AvailableVsCurrencies { + /** Timestamp for the available currency list. */ + ts?: number; + /** List of currency codes (e.g., USD, EUR) supported by the rates. */ + available_currencies: string[]; + /** Error message, if any, when fetching the available currencies. */ + error?: string; +} +export interface WsReq { + /** Unique request identifier. */ + id: string; + /** Requested method name. */ + method: + | 'getAccountInfo' + | 'getInfo' + | 'getBlockHash' + | 'getBlock' + | 'getAccountUtxo' + | 'getBalanceHistory' + | 'getTransaction' + | 'getTransactionSpecific' + | 'estimateFee' + | 'sendTransaction' + | 'subscribeNewBlock' + | 'unsubscribeNewBlock' + | 'subscribeNewTransaction' + | 'unsubscribeNewTransaction' + | 'subscribeAddresses' + | 'unsubscribeAddresses' + | 'subscribeFiatRates' + | 'unsubscribeFiatRates' + | 'ping' + | 'getCurrentFiatRates' + | 'getFiatRatesForTimestamps' + | 'getFiatRatesTickersList' + | 'getMempoolFilters'; + /** Parameters for the requested method in raw JSON format. */ + params: any; +} +export interface WsRes { + /** Corresponding request identifier. */ + id: string; + /** Payload of the response, structure depends on the request. */ + data: any; +} +export interface WsAccountInfoReq { + /** Address or XPUB descriptor to query. */ + descriptor: string; + /** Level of detail to retrieve about the account. */ + details?: 'basic' | 'tokens' | 'tokenBalances' | 'txids' | 'txslight' | 'txs'; + /** Which tokens to include in the account info. */ + tokens?: 'derived' | 'used' | 'nonzero'; + /** Number of items per page, if paging is used. */ + pageSize?: number; + /** Requested page index, if paging is used. */ + page?: number; + /** Starting block height for transaction filtering. */ + from?: number; + /** Ending block height for transaction filtering. */ + to?: number; + /** Filter by specific contract address (for token data). */ + contractFilter?: string; + /** Currency code to convert values into (e.g. 'USD'). */ + secondaryCurrency?: string; + /** Gap limit for XPUB scanning, if relevant. */ + gap?: number; +} +export interface WsBackendInfo { + /** Backend version string. */ + version?: string; + /** Backend sub-version string. */ + subversion?: string; + /** Consensus protocol version in use. */ + consensus_version?: string; + /** Additional consensus details, structure depends on blockchain. */ + consensus?: any; +} +export interface WsInfoRes { + /** Human-readable blockchain name. */ + name: string; + /** Short code for the blockchain (e.g. BTC, ETH). */ + shortcut: string; + /** Network identifier (e.g. mainnet, testnet). */ + network: string; + /** Number of decimals in the base unit of the coin. */ + decimals: number; + /** Version of the blockbook or backend service. */ + version: string; + /** Current best chain height according to the backend. */ + bestHeight: number; + /** Block hash of the best (latest) block. */ + bestHash: string; + /** Genesis block hash or identifier. */ + block0Hash: string; + /** Indicates if this is a test network. */ + testnet: boolean; + /** Additional backend-related information. */ + backend: WsBackendInfo; +} +export interface WsBlockHashReq { + /** Block height for which the hash is requested. */ + height: number; +} +export interface WsBlockHashRes { + /** Block hash at the requested height. */ + hash: string; +} +export interface WsBlockReq { + /** Block identifier (hash). */ + id: string; + /** Number of transactions per page in the block. */ + pageSize?: number; + /** Page index to retrieve if multiple pages of transactions are available. */ + page?: number; +} +export interface WsBlockFilterReq { + /** Type of script filter (e.g., P2PKH, P2SH). */ + scriptType: string; + /** Block hash for which we want the filter. */ + blockHash: string; + /** Optional parameter for certain filter logic. */ + M?: number; +} +export interface WsBlockFiltersBatchReq { + /** Type of script filter (e.g., P2PKH, P2SH). */ + scriptType: string; + /** Hash of the latest known block. Filters will be retrieved backward from here. */ + bestKnownBlockHash: string; + /** Number of block filters per request. */ + pageSize?: number; + /** Optional parameter for certain filter logic. */ + M?: number; +} +export interface WsAccountUtxoReq { + /** Address or XPUB descriptor to retrieve UTXOs for. */ + descriptor: string; +} +export interface WsBalanceHistoryReq { + /** Address or XPUB descriptor to query history for. */ + descriptor: string; + /** Unix timestamp from which to start the history. */ + from?: number; + /** Unix timestamp at which to end the history. */ + to?: number; + /** List of currency codes for which to fetch exchange rates at each interval. */ + currencies?: string[]; + /** Gap limit for XPUB scanning, if relevant. */ + gap?: number; + /** Size of each aggregated time window in seconds. */ + groupBy?: number; +} +export interface WsTransactionReq { + /** Transaction ID to retrieve details for. */ + txid: string; +} +export interface WsTransactionSpecificReq { + /** Transaction ID for the detailed blockchain-specific data. */ + txid: string; +} +export interface WsEstimateFeeReq { + /** Block confirmations targets for which fees should be estimated. */ + blocks?: number[]; + /** Additional chain-specific parameters (e.g. for Ethereum). */ + specific?: { + conservative?: boolean; + txsize?: number; + from?: string; + to?: string; + data?: string; + value?: string; + }; +} +export interface Eip1559Fee { + maxFeePerGas: string; + maxPriorityFeePerGas: string; + minWaitTimeEstimate?: number; + maxWaitTimeEstimate?: number; +} +export interface Eip1559Fees { + baseFeePerGas?: string; + low?: Eip1559Fee; + medium?: Eip1559Fee; + high?: Eip1559Fee; + instant?: Eip1559Fee; + networkCongestion?: number; + latestPriorityFeeRange?: string[]; + historicalPriorityFeeRange?: string[]; + historicalBaseFeeRange?: string[]; + priorityFeeTrend?: 'up' | 'down'; + baseFeeTrend?: 'up' | 'down'; +} +export interface WsEstimateFeeRes { + /** Estimated total fee per transaction, if relevant. */ + feePerTx?: string; + /** Estimated fee per unit (sat/byte, Wei/gas, etc.). */ + feePerUnit?: string; + /** Max fee limit for blockchains like Ethereum. */ + feeLimit?: string; + eip1559?: Eip1559Fees; +} +export interface WsLongTermFeeRateRes { + /** Long term fee rate (in sat/kByte). */ + feePerUnit: string; + /** Amount of blocks used for the long term fee rate estimation. */ + blocks: number; +} +export interface WsSendTransactionReq { + /** Hex-encoded transaction data to broadcast. */ + hex: string; + /** Use alternative RPC method to broadcast transaction. */ + disableAlternativeRPC?: boolean; +} +export interface WsSubscribeAddressesReq { + /** List of addresses to subscribe for updates (e.g., new transactions). */ + addresses: string[]; +} +export interface WsSubscribeFiatRatesReq { + /** Fiat currency code (e.g. 'USD'). */ + currency?: string; + /** List of token symbols or IDs to get fiat rates for. */ + tokens?: string[]; +} +export interface WsCurrentFiatRatesReq { + /** List of fiat currencies, e.g. ['USD','EUR']. */ + currencies?: string[]; + /** Token symbol or ID if asking for token fiat rates (e.g. 'ETH'). */ + token?: string; +} +export interface WsFiatRatesForTimestampsReq { + /** List of Unix timestamps for which to retrieve fiat rates. */ + timestamps: number[]; + /** List of fiat currencies, e.g. ['USD','EUR']. */ + currencies?: string[]; + /** Token symbol or ID if asking for token fiat rates. */ + token?: string; +} +export interface WsFiatRatesTickersListReq { + /** Timestamp for which the list of available tickers is needed. */ + timestamp?: number; + /** Token symbol or ID if asking for token-specific fiat rates. */ + token?: string; +} +export interface WsMempoolFiltersReq { + /** Type of script we are filtering for (e.g., P2PKH, P2SH). */ + scriptType: string; + /** Only retrieve filters for mempool txs after this timestamp. */ + fromTimestamp: number; + /** Optional parameter for certain filter logic (e.g., n-bloom). */ + M?: number; +} +export interface WsRpcCallReq { + /** Address from which the RPC call is originated (if relevant). */ + from?: string; + /** Contract or address to which the RPC call is made. */ + to: string; + /** Hex-encoded call data (function signature + parameters). */ + data: string; +} +export interface WsRpcCallRes { + /** Hex-encoded return data from the call. */ + data: string; +} +export interface MempoolTxidFilterEntries { + /** Map of txid to filter data (hex-encoded). */ + entries?: { [key: string]: string }; + /** Indicates if a zeroed key was used in filter calculation. */ + usedZeroedKey?: boolean; +} diff --git a/blockbook.go b/blockbook.go index aac9696498..fa0cfdd7e8 100644 --- a/blockbook.go +++ b/blockbook.go @@ -2,9 +2,7 @@ package main import ( "context" - "encoding/json" "flag" - "io/ioutil" "log" "math/rand" "net/http" @@ -12,6 +10,7 @@ import ( "os" "os/signal" "runtime/debug" + "strconv" "strings" "syscall" "time" @@ -84,6 +83,8 @@ var ( // resync mempool at least each resyncMempoolPeriodMs (could be more often if invoked by message from ZeroMQ) resyncMempoolPeriodMs = flag.Int("resyncmempoolperiod", 60017, "resync mempool period in milliseconds") + + extendedIndex = flag.Bool("extendedindex", false, "if true, create index of input txids and spending transactions") ) var ( @@ -100,6 +101,7 @@ var ( metrics *common.Metrics syncWorker *db.SyncWorker internalState *common.InternalState + fiatRates *fiat.FiatRates callbacksOnNewBlock []bchain.OnNewBlockFunc callbacksOnNewTxAddr []bchain.OnNewTxAddrFunc callbacksOnNewTx []bchain.OnNewTxFunc @@ -150,36 +152,31 @@ func mainWithExitCode() int { return exitCodeOK } - if *configFile == "" { - glog.Error("Missing blockchaincfg configuration parameter") - return exitCodeFatal - } - - coin, coinShortcut, coinLabel, err := coins.GetCoinNameFromConfig(*configFile) + config, err := common.GetConfig(*configFile) if err != nil { glog.Error("config: ", err) return exitCodeFatal } - metrics, err = common.GetMetrics(coin) + metrics, err = common.GetMetrics(config.CoinName) if err != nil { glog.Error("metrics: ", err) return exitCodeFatal } - if chain, mempool, err = getBlockChainWithRetry(coin, *configFile, pushSynchronizationHandler, metrics, 120); err != nil { + if chain, mempool, err = getBlockChainWithRetry(config.CoinName, *configFile, pushSynchronizationHandler, metrics, 120); err != nil { glog.Error("rpc: ", err) return exitCodeFatal } - index, err = db.NewRocksDB(*dbPath, *dbCache, *dbMaxOpenFiles, chain.GetChainParser(), metrics) + index, err = db.NewRocksDB(*dbPath, *dbCache, *dbMaxOpenFiles, chain.GetChainParser(), metrics, *extendedIndex) if err != nil { glog.Error("rocksDB: ", err) return exitCodeFatal } defer index.Close() - internalState, err = newInternalState(coin, coinShortcut, coinLabel, index) + internalState, err = newInternalState(config, index, *enableSubNewTx) if err != nil { glog.Error("internalState: ", err) return exitCodeFatal @@ -194,6 +191,17 @@ func mainWithExitCode() int { } internalState.UtxoChecked = true } + + // sort addressContracts if necessary + if !internalState.SortedAddressContracts { + err = index.SortAddressContracts(chanOsSignal) + if err != nil { + glog.Error("sortAddressContracts: ", err) + return exitCodeFatal + } + internalState.SortedAddressContracts = true + } + index.SetInternalState(internalState) if *fixUtxo { err = index.StoreInternalState(internalState) @@ -260,6 +268,11 @@ func mainWithExitCode() int { return exitCodeFatal } + if fiatRates, err = fiat.NewFiatRates(index, config, metrics, onNewFiatRatesTicker); err != nil { + glog.Error("fiatRates ", err) + return exitCodeFatal + } + // report BlockbookAppInfo metric, only log possible error if err = blockbookAppInfoMetric(index, chain, txCache, internalState, metrics); err != nil { glog.Error("blockbookAppInfoMetric ", err) @@ -332,7 +345,7 @@ func mainWithExitCode() int { until := uint32(*blockUntil) if !*synchronize { - if err = syncWorker.ConnectBlocksParallel(height, until); err != nil { + if err = syncWorker.BulkConnectBlocks(height, until); err != nil { if err != db.ErrOperationInterrupted { glog.Error("connectBlocksParallel ", err) return exitCodeFatal @@ -344,7 +357,7 @@ func mainWithExitCode() int { if internalServer != nil || publicServer != nil || chain != nil { // start fiat rates downloader only if not shutting down immediately - initDownloaders(index, chain, *configFile) + initDownloaders(index, chain, config) waitForSignalAndShutdown(internalServer, publicServer, chain, 10*time.Second) } @@ -384,7 +397,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, metrics, internalState) + internalServer, err := server.NewInternalServer(*internalBinding, *certFiles, index, chain, mempool, txCache, metrics, internalState, fiatRates) if err != nil { return nil, err } @@ -404,7 +417,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, *enableSubNewTx) + publicServer, err := server.NewPublicServer(*publicBinding, *certFiles, index, chain, mempool, txCache, *explorerURL, metrics, internalState, fiatRates, *debugMode) if err != nil { return nil, err } @@ -450,7 +463,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, metrics, is) + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates) if err != nil { return err } @@ -477,16 +490,13 @@ func blockbookAppInfoMetric(db *db.RocksDB, chain bchain.BlockChain, txCache *db return nil } -func newInternalState(coin, coinShortcut, coinLabel string, d *db.RocksDB) (*common.InternalState, error) { - is, err := d.LoadInternalState(coin) +func newInternalState(config *common.Config, d *db.RocksDB, enableSubNewTx bool) (*common.InternalState, error) { + is, err := d.LoadInternalState(config) if err != nil { return nil, err } - is.CoinShortcut = coinShortcut - if coinLabel == "" { - coinLabel = coin - } - is.CoinLabel = coinLabel + + is.EnableSubNewTx = enableSubNewTx name, err := os.Hostname() if err != nil { glog.Error("get hostname ", err) @@ -496,6 +506,12 @@ func newInternalState(coin, coinShortcut, coinLabel string, d *db.RocksDB) (*com } is.Host = name } + + is.WsGetAccountInfoLimit, _ = strconv.Atoi(os.Getenv(strings.ToUpper(is.GetNetwork()) + "_WS_GETACCOUNTINFO_LIMIT")) + if is.WsGetAccountInfoLimit > 0 { + glog.Info("WsGetAccountInfoLimit enabled with limit ", is.WsGetAccountInfoLimit) + is.WsLimitExceedingIPs = make(map[string]int) + } return is, nil } @@ -668,7 +684,7 @@ func waitForSignalAndShutdown(internal *server.InternalServer, public *server.Pu 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, metrics, is) + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates) if err != nil { return err } @@ -677,36 +693,9 @@ func computeFeeStats(stopCompute chan os.Signal, blockFrom, blockTo int, db *db. return err } -func initDownloaders(db *db.RocksDB, chain bchain.BlockChain, configFile string) { - data, err := ioutil.ReadFile(configFile) - if err != nil { - glog.Errorf("Error reading file %v, %v", configFile, err) - return - } - - var config struct { - FiatRates string `json:"fiat_rates"` - FiatRatesParams string `json:"fiat_rates_params"` - FiatRatesVsCurrencies string `json:"fiat_rates_vs_currencies"` - FourByteSignatures string `json:"fourByteSignatures"` - } - - err = json.Unmarshal(data, &config) - if err != nil { - glog.Errorf("Error parsing config file %v, %v", configFile, err) - return - } - - if config.FiatRates == "" || config.FiatRatesParams == "" { - glog.Infof("FiatRates config (%v) is empty, not downloading fiat rates", configFile) - } else { - fiatRates, err := fiat.NewFiatRatesDownloader(db, config.FiatRates, config.FiatRatesParams, config.FiatRatesVsCurrencies, onNewFiatRatesTicker) - if err != nil { - glog.Errorf("NewFiatRatesDownloader Init error: %v", err) - } else { - glog.Infof("Starting %v FiatRates downloader...", config.FiatRates) - go fiatRates.Run() - } +func initDownloaders(db *db.RocksDB, chain bchain.BlockChain, config *common.Config) { + if fiatRates.Enabled { + go fiatRates.RunDownloader() } if config.FourByteSignatures != "" && chain.GetChainParser().GetChainType() == bchain.ChainEthereumType { diff --git a/build/docker/bin/Dockerfile b/build/docker/bin/Dockerfile index a8b0a6380e..07e4254dae 100644 --- a/build/docker/bin/Dockerfile +++ b/build/docker/bin/Dockerfile @@ -11,8 +11,8 @@ RUN apt-get update && \ libzstd-dev liblz4-dev graphviz && \ apt-get clean ARG GOLANG_VERSION -ENV GOLANG_VERSION=go1.19.2 -ENV ROCKSDB_VERSION=v7.7.2 +ENV GOLANG_VERSION=go1.25.4 +ENV ROCKSDB_VERSION=v9.10.0 ENV GOPATH=/go ENV PATH=$PATH:$GOPATH/bin ENV CGO_CFLAGS="-I/opt/rocksdb/include" @@ -39,7 +39,7 @@ RUN echo -n "GOPATH: " && echo $GOPATH # install rocksdb RUN cd /opt && git clone -b $ROCKSDB_VERSION --depth 1 https://github.com/facebook/rocksdb.git -RUN cd /opt/rocksdb && CFLAGS=-fPIC CXXFLAGS=-fPIC PORTABLE=$PORTABLE_ROCKSDB make -j 4 release +RUN cd /opt/rocksdb && CFLAGS=-fPIC CXXFLAGS=-fPIC PORTABLE=$PORTABLE_ROCKSDB DISABLE_WARNING_AS_ERROR=1 make -j 4 release RUN strip /opt/rocksdb/ldb /opt/rocksdb/sst_dump && \ cp /opt/rocksdb/ldb /opt/rocksdb/sst_dump /build diff --git a/build/docker/bin/Makefile b/build/docker/bin/Makefile index 9111e24e0b..96402edd9f 100644 --- a/build/docker/bin/Makefile +++ b/build/docker/bin/Makefile @@ -38,4 +38,3 @@ prepare-sources: mkdir -p $(BLOCKBOOK_BASE) cp -r /src $(BLOCKBOOK_SRC) cd $(BLOCKBOOK_SRC) && go mod download - sed -i 's/wsMessageSizeLimit\ =\ 15\ \*\ 1024\ \*\ 1024/wsMessageSizeLimit = 50 * 1024 * 1024/g' $(GOPATH)/pkg/mod/github.com/ethereum/go-ethereum*/rpc/websocket.go diff --git a/build/docker/deb/Dockerfile b/build/docker/deb/Dockerfile index 55989099ab..fd8fa114ef 100644 --- a/build/docker/deb/Dockerfile +++ b/build/docker/deb/Dockerfile @@ -6,9 +6,19 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get upgrade -y && \ - apt-get install -y devscripts debhelper make dh-exec && \ + apt-get install -y devscripts debhelper make dh-exec zstd && \ apt-get clean +# install docker cli +ARG DOCKER_VERSION + +RUN if [ -z "$DOCKER_VERSION" ]; then echo "DOCKER_VERSION is a required build arg" && exit 1; fi + +RUN wget -O docker.tgz "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" && \ + tar -xzf docker.tgz --strip 1 -C /usr/local/bin/ && \ + rm docker.tgz && \ + docker --version + ADD gpg-keys /tmp/gpg-keys RUN gpg --batch --import /tmp/gpg-keys/* diff --git a/build/docker/deb/gpg-keys/dash-releases.asc b/build/docker/deb/gpg-keys/dash-releases.asc index 66f98fee0a..061617a4a6 100644 --- a/build/docker/deb/gpg-keys/dash-releases.asc +++ b/build/docker/deb/gpg-keys/dash-releases.asc @@ -1,5 +1,103 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- +mQENBGWp8IkBCADEaVzTSOymYATI+x7Wp72QZnMZy5dbiOKvRd1E+zMAxamk3RgP +xu1g9zwecxRR5EU6HQoDawFckDp2kM014N055bXkIoQS04RTspfTWKa5TkcII2vR +sPRI7Hz3UXFvs3FngzLe3Kqp7HZ5dHzBiynm2hT1a0Bmzc19B/9A1zN51Hsvfdgo +tIfb9sHBUiq6+Sx8b/oKiouW/HQA6uFrYZFPwIVntagFcJjkNGwhziFHgo3yrMWm +qR4Nsuag/P0aa1byIvE6vkTOD05W7IfxasWy3bMxvTEWFsQCHJ5he5RBIzh9tq57 +YEhGqYfdTeAZ1GlJC/ByoCzrEQnXylQiRbylABEBAAG0I1Bhc3RhIFl1YmlrZXkg +PHBhc3RhQGRhc2hib29zdC5vcmc+iQFoBBMBCABSAhsDAhkBBQsJCAcCBhUKCQgL +AgUWAgMBAAIeBBYhBGCs9wv3EmRQSe5vFe/q8WaGIl9kBQJlqfxxGBhoa3BzOi8v +a2V5cy5vcGVucGdwLm9yZwAKCRDv6vFmhiJfZFErCAC6Fn5eiLMF0Ge0FFUWFQvw +NDpIEIqECRgp1Y44H6Rn4KPJArmVRB9UYmm9ntPo2v/fX6wFCRm+1sud8pZq4leF +I8efyKcCRqFDQm3GlXqpfqXD/Utbn2MVhUYhFu0FyLBbx9P4ZN5y1+dKJcBISDqD +XZ4GXSVBUPuBaygE5lbcTk+wFQWfiqjg8mk9dq/qlFEuL2rSQIYWW8z8pNYllg8M +T/qQ3ydY/O5BQuliUjFnyLCorghifUtO4cgMSXKdtop+Sle5GEUaQqM13wPOBo3V +SMWCxcPjwMj8x3q4b83fq9q2O1UVHhzmL7wFFUOKWBOZvokJPJqsUYRVGgT9J6WX +iQIzBBABCAAdFiEEKVkDYuyHioH9PCArUlJ77avoeYQFAmWqA6MACgkQUlJ77avo +eYTdGBAAlGZQ0GTf9fp6cwGW057fLZP0ysA+ThJlEqxOLXeGfuHlo+xxlDy6k8SN +DlmcFEgXsAWoD0X/HWZ+7G1kVVPJSixpVuuP513z38a7vNDlgF42livLcKticDpu +6gPuAS7YEEa5uugGJwmylHUeIVE69gp1QgJVPy0Egynv4IpsCiuuWLc/HL0uOS59 +KljH150cxsWX1sUIbgFapEqU5T2f5JFNO/ikBCqh9kFBw9ccMoQWBLw/AwpUqNH/ +8U7czzgnTvJqnXA97s1zUlbvOBpt7om2FRAcSGKcZNEGDp/jIOZUBAT3X+T4mvta +w+3g9U/7yg8mlka+DVxOE43eypQyyNoWP5ZetTb2R1Qq+WBaZHRJh9JoS03EYenL +XxDELYzkt2S6keh7sExc0j4nV9XmoRr5LD848HSQKB9fymcxkxPgn3avK28NMGpm +Xudqh/pz4PrOn+WOJJQg4494UvFtZ2zkAUnc6O0EUbr3ti6AUZCuyIZWc1GJmDrA +F3NtT4FgX40LjV6jcWAurN9HBX5mrV79X/5tqQBpho4DpNPs5rm8tDEYTWF+irFD +O96VJSVr5A9otM5kzHC7aUFCeXPgcCH5lpgZXj/7nE46Xf9MX4lmJ63oQ1hzELOe +Xtl1kSVmmtHDbj55LG496sxn0C5wc7WSZYge9llkLFnlgJQG8h60HlBhc3RhIFl1 +YmlrZXkgPHBhc3RhQGRhc2gub3JnPokBZQQTAQgATwIbAwULCQgHAgYVCgkICwIF +FgIDAQACHgQWIQRgrPcL9xJkUEnubxXv6vFmhiJfZAUCZan8cRgYaGtwczovL2tl +eXMub3BlbnBncC5vcmcACgkQ7+rxZoYiX2SjXAf/fXPwm0j84B9gVxjB4la1YahZ +/jomHhMzZm/HYqEs/3KrBPVUSM0+tkqI6pgVQVI9hTlijkcNhhZKAIF5Ye87Ule1 +x7wlnTJ+msWXMtybhaTv55BQVsnGRN/h88yoZH5UOylbMnFmeYh9IP9WKvrTTfZS +cSDN1Ib2LjeiPvxTyL9HiOTtCz1w6iijdS3rDWIEJhugBnFZ52nG+mQU5sy5+5S2 +W/PKr8hKqDVifCeZAju3sYTRsBBbCnGeTlqOtj/IJ65A2bw5tzM4gK6hrQwolzrC +c7teu9bZdP2dYuspkaGNX6afxR62VZYnpH/VCPp54c0/0Hl+TWEbERfGicLbC4kC +MgQQAQgAHRYhBClZA2Lsh4qB/TwgK1JSe+2r6HmEBQJlqfWlAAoJEFJSe+2r6HmE +C1QP9Ryh2XiUhQmvtiiDFPxzK0sa9YNAk84nUAOSrRLIQ1Xs3g33cg15kxMvtKf9 +OIJD14Mu1ypnfa1jsDr6zdy3CQCKAKEBTH41jw3XLa9R9XWaT6+0YV+meIHZ6uVJ +3+5M1xZGsnErsTM+iGGmneRIt2L0cZTt7HRJaL0EJrd7PXQb8B9BxgPnRa4UVpqd +FlhMhNHad7rz5hFAz8YkYEGX/bctF2y/gmHnu/xKkQsOlV+fQfROOlo/wQ/2vXRY +YBqWrVw0gAFDaI4P43CoKlYFzZOxrX+RLSc6eOSgmRkwMx5NzpOvfbypuiXLCmed +8pTF9SeXH3LzdO1gJQsKkia04OBohCosmnIjOCjeN3bxf606HZpBgXhj72kXZOX0 +NeA+yxEh1QIhvjxvD0WyIUChaXYsGy61F16vIUytE319diU/e/KQKnTC+oepiju6 +N23Iy8c2gRux48ghkmcN58bLOCUUvO+UYb7U9YYsi6HEiL8yd8KVPHVJ293NcMt0 +FsmxFd4Fddr2HYK0NLtf5MDo4yYMw2PmbQ/1/cy/Sr6BvlHmZ6R9+I9beO5LjPBQ +EN62PWWBfl6b2EpYyA9RTFUKFiRhEoqLpmORlzMcUcmIsIYX5ZWanitBnSnIznGe +TapoOXPE93OrpDJU9vIcYx7Y4E8drNAdW1zZcFBo9ilNexq0i1Bhc3RhIFl1Ymlr +ZXkgKFRoaXMgaXMgYW4gb2ZmbGluZSBvbmx5IGtleSB1c2VkIGZvciB0aGUgaGln +aGVzdCBsZXZlbCBvZiB2YWxpZGF0aW9uLiBNeSBtYWluIGtleSBpcyA1MjUyN0JF +REFCRTg3OTg0LiBTZWUga2V5YmFzZS5pby9wYXN0YSmJAWUEEwEIAE8CGwMFCwkI +BwIGFQoJCAsCBRYCAwEAAh4EFiEEYKz3C/cSZFBJ7m8V7+rxZoYiX2QFAmWp/HEY +GGhrcHM6Ly9rZXlzLm9wZW5wZ3Aub3JnAAoJEO/q8WaGIl9kVUYH/2HrXiEHYIZU +NojBSKzBqWUSoXjvN1lITo7WSzdg/saQLtIBuEWwVtZKGH9HcRpi93glAZk+0xeO +Twke4fEAeEiYS3U3t+GqqH5bo4aJD1+EedvpjM5PVhtDyM4VVw8wu/29Tl7lIZQ9 +57Un1dwuYrsO6BEmKWmnV31XpN7JMd4qIAIeQoN9NMOFBT2PS7LXiIUZ36TH3ZAP +hgbec/MhgCQW//KmMd6lqVCNhjJ4ggYeifsAhFo/xMMYxbpFZXkYkpMxziZoG7MT +gQLR2YQEVQm9rQOjdn4IOWN6qoEtxx/82mMq/JynGeMXMyt4rgdSpcjTgnBlKMBv +DU2FF+hvMWiJAjMEEAEIAB0WIQQpWQNi7IeKgf08ICtSUnvtq+h5hAUCZaoDowAK +CRBSUnvtq+h5hKMFD/9zrGMZh6da8RBO1+cU4LZi0KDcFPd0dMHIpnvJ0w1oI3aY +WBmtKbLm5lQZ9OqgRp3MTFZPXbnMrfjqNwmRkEW5V1RjA24MMXjCb5wdD7ZMQ3VN +sXMi4WEJ61o1uVobrBSowmtBJMXyx3tGcHOXOpIXzG+HVx2gnlqFytK621PmSjlA +If498EpqQriIqoEuVkeoyQ0fhSl1d5/gnfP629i1ERnyRN8htJ+J6CJUuHNRPfST +pqvfyrLQTvPSDC7tTNuTY47EKEy3QP1s+R6hLFVbBTxBK1lJVrxBpBqLFCdRQswX +7Xv2p6syn9ia3DmBpw2Bfh8ySPmgVwgonZODXTRAo0uYV3hdeJgblVt9XhSa9C9z +DYgrjXR3EGT+N3GYkjdXqdoOnZzsaUD7CQLnobW4ZIjM+EtwP7QBXv89liqW0ppK +RuZOJ8Zycbiqa+ThK0r2gFm8j7HZWBNE/osVuschQ89d1FmwUKmcMCba/IbNDDHG +JdTr6fJvbXdyF183GZhvSlXdOMPNhcX4dRUcxkooMcUjbnERHKb6q1AKvoIYceb+ +/WaO/RUzCWCRbIEdYKxqYFuKRvuMHcR/F0fGeUUNsujLBuL5xSdZmNDpOrefTH0R +ZDLdTtKATr4GbkVZGBtXvWmd6c5NdJLCMO/n1V6j2ZdpbRBsvB/tl0emdXUvr7kB +DQRlqfCJAQgAqVzAtdH5r5+WezUAbKxwxYopkMJauEhjSE08CLFr8MHiImcIKY2S +rtUTKA+bJYdaaTE1HqIhPTg18wo166/HKdvRR2vi7ACvb8sunAg0/H1Vq6d+y262 +4mLYqoRMQqBBJds0TIC4IDawJFjrkNT/S36jLtaEifENgskTQgashamRFYnwSgKv +BKyobdiRMh26GGoxZLRiZVehCR0FQqchd8GpFOJsSANyX2Hlyi9i8ZhU+Ld2PcPK +nmfkFsS35Dqjm7IkDLpMx7kwjr5YlTcIpQhENbJ68dAzzG9A3mV7Wojfv3Dzpz3j +9wXvoj2EYDYPvNAyftQlfrWKe3r8wcjBKQARAQABiQE2BBgBCAAgFiEEYKz3C/cS +ZFBJ7m8V7+rxZoYiX2QFAmWp8IkCGyAACgkQ7+rxZoYiX2ThTAf/cNb4kEhk+Wjj +FzRHNUinzwA/7+YT5gbEnVh/1x+IpeYpnnuVEdOhNFxz76SL3dtDF8ciIhWxsE4b +v6hpdqcps1Hnq2dkbZ+z9T1r8+IZ03eyYXOo7kZtCwX4UODFwFHi2WaZpCCgOvLX +pA8tKJ04VfIBjp3shlUo+vCROgMouOpJgaLs80LQpoHEB8enHIuNByqWhHl+D4DV +z2l4TPL3HQaCMcW2KCexVz1+9pnPT2hf8DQXrxmchC1CnJVgV64yDzmjhND9C2Hw +OPS0JcBhAzB1FqtVZGYfQSkE5FAA7FLN/IYcCDhxYKVzdKay6m/JL8cbcSpQqLWO +/MR86YndjbkBDQRlqfCJAQgArkCO/giMQ8ReApeP/B4GoNiWlax5bFqMQVPevVix +QfAJ7IQ+8W/JxFmV2F0U2CQU38u9c0kAhYtFk/H/0cC/aEnqKPT6SGpZ4+W7Ehmp +ngSx+1r0sVV1cuZcUncetQeK2IZsBYCCf9XjZIqgFMDygnfM5TvPUyj5qiATxIxV +9bRjI/oNYVPngfnot7VZafVq/yW5+JlYx8u0rKsn5ikpzSDV8IrHmehydrHUUhYj +6/y6ChDzs2ZAq+qoCgFov5z7VzczzEybfPTbAwXpDahCHxF2V6k81c5ZeKEr9l3K +l8Kcc2ybwRe2MbePYCSDHle4GRaYExTXjYnkgyOKtr5YgwARAQABiQE2BBgBCAAg +FiEEYKz3C/cSZFBJ7m8V7+rxZoYiX2QFAmWp8IkCGwwACgkQ7+rxZoYiX2Rx4gf+ +MmibxLDOnVrMv2joky9DJajtZow8ayipXjU1AgIjuvcoMV/GBn8OMx3IAXHVGpyV +16jJ00X8Q+MAwVxd8+7OUoOSFECBqECv5iD4q0OqcZqFx7EyC7iDVUfY9IG0EKjV +4AOzP/azJgT916t3OqcXXDJ2wIUbDIvUQUwTMjX0Fw7OQNGYlHS709UF3y0DwBdq +pCxj1y74D9XzjvWHYxlKI5X8Lt2QW+xsGKkaRp5aIXn6MUnpmdIFZEcTj8s553+m +iqlYokmTvkTa4cQsgwC6RqkVsYopJrYsKnDs/l4/m+4TrPdforaD6mKNKzlsLJSj +gZfWLfoIul+B10SwJHXuoQ== +=/A3N +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- + mQINBF1ULyUBEADFFliU0Hr+PRCQNT9/9ZEhZtLmMMu7tai3VCxhmrHrOpNJJHqX f1/CeUyBhmCvXpKIpAAbH66l/Uc9GH5UgMZ19gMyGa3q3QJn9A6RR9ud4ALRg60P fmYTAci+6Luko7bqTzkS+fYOUSy/LY57s5ANTpveE+iTsBd5grXczCxaYYnthKKA @@ -11,55 +109,317 @@ dH9rZNbO0vuv6rCP7e0nt2ACVT/fExdvrwuHHYZ/7IlwOBlFhab3QYpl/WWep2+X 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 +tHxQYXN0YSAoU2VlIGtleWJhc2UuaW8vcGFzdGEgZm9yIHByb29mcyBvbiBteSBp +ZGVudGlmeS4gNjBBQ0Y3MEJGNzEyNjQ1MDQ5RUU2RjE1RUZFQUYxNjY4NjIyNUY2 +NCBpcyBteSBvZmZsaW5lIG9ubHkgR1BHIGtleS4piQJUBBMBCAA+AhsDBQkNLaMv +AheAFiEEKVkDYuyHioH9PCArUlJ77avoeYQFAmWp/WUFCwkIBwIGFQoJCAsCBBYC +AwECHgUACgkQUlJ77avoeYSFAw/+OIgYP39nPBoZ4G2sIPjpY1PsbGz2D8uj46we +orOJ438fwRbrW5LSSaQ/uQol0keekvt7xDbzQ4L5jFXlgwbhvIea05K8BsM0JMbw +SDcLtBbv0QIhlomV2nkG/rKtvCqwnJ4M19HrVmrqXIbYC2+C3p8qN4enGcNR+vRr +0Op+Q3wMsAPPLWyvBaXCKVIDOEYFGxLs5XqCxuJmtD/iyH9k21//iWjdf+/KEpK1 +OOH1QQQnKTCQPJX4iHeG2tQCMeQqXrTAdQqhvEEmGxqvJ74Oas34Uisd+/LCm4a/ +5enoRfEaVvOVNS1NoMUX1vvUC4YMU6OmtsNo0kCt5wOPxbDFb2vDKtEfnZMEAC0s +k2STti3uuu5WhwODAmjSH1Y/w4jN6tkOfSxQ2k04a12dtZGQBWBIKCgVWB5FZfhS +lPXbS8NMS7CSGnuvwyE2oT3osakEFFSGTW1KsqX57AqA/V/+nH6E77R6v1/61MU/ +m8f1FDe/5WmPPBUrZ7aZ7P+dHCR2PQ5W5tQPStRxeIi3usY1JKMYO88qtEWwClgg +Yh94OD3L9zQvQ8IGqJnpcSLjo0QNgka62D8KFsz3AjcPVYsLego/hn7bP3oXKI6S ++PuxgzbeMBWKLthPXx2klLDoHuNXgUGkTuauUVSoGWxIlyTqBvSpeSZ81O2BE/T/ +wN77yn2JATMEEAEIAB0WIQRgrPcL9xJkUEnubxXv6vFmhiJfZAUCZan2hwAKCRDv +6vFmhiJfZIsRB/4xeq0PhYYyIaAqD15pUIYwmfw35jSerHCkJWrpEAkZ2NhxPgEJ +81PCN1gqoEQ9F8rkk/5VnpFnqcF9nFRN/OiZZYUvoz4DoDX7hjz75Im+dKf4KqW8 +g6MUBTHfuV/srBdENYor2mZCfX6JnQjCjBe9HOUMh/CVzmmFOrthQ1kuCbK0/WPT +KGZ0UfNpNRyrnBpkjAgoO1pU5FTI4KlRhzSx6/NnePW4vHxpZBdd9VhNBU2/WGah +qtNmu7TDSrkpO4ljIJfiq4GMi60ign43zQ4ndJR0CQIcWjhgRAAq5sL8bsEdLhDV +u1+qOQYXaQNf17hqYhCesXfByKYRKqLnGmfrtBtQYXN0YSA8cGFzdGFAZGFzaGJv +b3N0Lm9yZz6JAlQEEwEIAD4WIQQpWQNi7IeKgf08ICtSUnvtq+h5hAUCXVQvJQIb +AwUJA8PHawULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBSUnvtq+h5hMqeEACQ +teY571XK50dW1oQzjgPq5tVuchoRQI727pr75145o2rOe0e0xrWzVNnhd9ZDzC4j +8dh6wWVQWErHr+3Hhn8sCUW2PNU+o3GvhGR6aqPl0Oh5gt4wHZalrcUnZ5u/RtFb +DmGilobdASL/mpZge8ymLBj2lKiRR2X/JQe/KAzr/7QW1zLh2oEUOOGVas6Ev+zi +osAE0b3upGTHJFPQPMFv4za22MbeTKYeqyJ6W6LdQDDssC/RBQKZXj3pRweA6RQF +GOqw44CbtIHuQu/PV8ZDTpE+v9cWAzoNCMcQ2fm5tCM8zYytt3perbA3VPwZNXcs +ITcRpIS5FgoeOntgIwzzKVmY+4GD8uWM/DHtJPxyry7LpSa8CNyx+oN+Z2qCChn0 +3ycJzO3UFsaCMG/CMAEkLxbg0AcxNyQ8kvIGlcEDLINaz1xuHAtAxqTQKMYCP1xt +d5rhGOe1FkGfVYEJX97+JgMGa8+2nD5+A6wG0+JaJllqzfXY1VhNoVmfS/hFPQ+t +/84jNSGR5Kn956C5MvTK65VumH+NRE59kpt1nsIQNKu/v6fZUnbRtCFC05BSwIjo +TzFvKXycJkCVjdSYARWkagki4bbFC1WZQuA9BOF5TOUAYt6zaEBfAJgjeRT71Mr0 +3eNExXaLm9k/hmvapGpmtJQhLY6NKPm/ctyfIaEz/YkCVwQTAQgAQQIbAwIXgAUJ +DS2jLwULCQgHAgYVCgkICwIEFgIDAQIeBRYhBClZA2Lsh4qB/TwgK1JSe+2r6HmE +BQJlrVMsAhkBAAoJEFJSe+2r6HmE0KcP/2EGb4CWvsmn3q6NoBmZ+u+rCitaX33+ +kXc4US6vRvAfhe0YiOWr5tNd4lg2JID+6jsN2NkAZYgzm4TXXJLkjXkrB+s0sFkC +jyG1/wBfZlPUSfxoDFusJry87N/7E9yMX7A+YV2Hh/yOXbR+/jSINfmjC+3ttjWD +UsUWT9m1yN8SBNg6h66TLffFyXgGFkRKYE27eprP0cuVkI6Fks68ocSQ5FQ7gmdM +CC4JFtOI4e1ax6mfvTFz2e2f5DlohPjW9w4eKTn+k98Nuev+s3WGiDXjxSABoehA +dwz2mbEjPsuz0jLeYKn6ialHh+hruYZozx8dxpUIWEVlMwLDBteWCuwTp+XPmOva +KkgYLxkfjjeIqUy17f6py17GrDZFHLeiopcJqyQJ0XLQI/qAKXkySBpvGD86nrM1 +i+5X7nLxZ0YfjKQ7cI+fp5A6SsQPUk9SI95PXRssx481zNse5wxFMP8J9oIB6nge +r39lpRRmvaSUJDNWjfsRZ/XK4mfib2OlLXooWuU5lCwqtQ+Jw9Zr/Gby2kTNIjrf +IpdNyThTnth+uTwcA8KCJRJY2BrPBtWNWqPLxLv9RLR3/N1siyJcichExIBKEzOh +zzi/i/PTU8dK2OBXrSaJ8DXhPwyNTB2l7jnXBO0hxeO4gmzAFQpM7QXXVDguL0b5 +94y05UNOM/ljiQIcBBMBAgAGBQJeut/oAAoJECqAP87D6bin7ZMP/3be6BDv/zf0 +gCTmgjD6StvPHu+F17op4VPj2cHYCgFP1ZHFH2RjqRVhSN6Wk+hbmR5PDHoVA2nc +xITv/DddKRjYc7fPRlrje7H19+urJgqqkWzmuUbNlxKiXiVW/OPmCjjI89Okt3dZ +GCTicEAPzJ6LTpoVgo4n/Eu81nMm6caf++Pzz1vEI3bJdPHPYyI+gN64mEhfP4OJ +u8v2XTbj+0ua3JxYWilxF7haytApmaPqeT7uOEBrX7EV1M+DlQCSM61u2EC5eIwA +oDba/ENXNyg5Z1JbFe3DxqE6ZVcAcZWXGdtPotayuEy6WL3LB2UUsM4UB4FPSUwc +FvnkV8YzBSV8Rqx+mkOFM6BhxzwK0zPvY+vv+rXSwz7uE/yrToqO9KvGhFxMwMwz +TRAJXI870fJQ9c5z2LzxoNg5gOUQH4vPG6YQT1ev04fj7IGYch9EhrSjuLCm94BA +pOEA+h/TTN6+xVLemUSB/l+Obm5701PP/naVprCJcCqIU3tH5HU3BXpZH++AzWo0 +pmgbtd7ECsR/y0NR4Mxoef677q9YGJEG/psYC0GZlzWsY5zjala+bEVn5gvbw6Lh +4Q2gwpvVXdygb6PSPwRSkpgHtUxdvIQsDEaBBGg/ae0x3O55z2/z95acnhIMRqQp +UpnPmDZUBKlsDJ8tivw/2r8o16YtAlJ0iQEzBBABCAAdFiEEYKz3C/cSZFBJ7m8V +7+rxZoYiX2QFAmWp9dIACgkQ7+rxZoYiX2StMwf8CdL0fhz2TM1R79n+FW7QCSaI +NBzIE1lN2TbdVEZeyiwQLn9cbqOvVPFavj4vxWFIXfAYzitLDHkikmg5Qzj7OXB2 +plFnqJxZ1tZSC1EdMHuNX1j55FDAggV/U/yv2PDY2XuwJbj/hLj80oNzIL5qLnNc +o0CLggB8QLLleFw4BTKycGDrzQCk4AGQ8tDRNoyI6Q/oFQtWQgQdm9Cs02Myr51Q +ZBe09XXA4wpyqv9BM+E0o8SLp/x/wZXM99vDNa7Df0nsRIQukFy5HqJJTufP1b6Q +FVMY1ouweyLxABXO4cvtYpOAUwQroY4U/q9ZnRzxj8Sq+reAt8O/wwJ8ujy9ILQW +UGFzdGEgPHBhc3RhQGRhc2gub3JnPokCVAQTAQgAPgIbAwUJA8PHawIXgBYhBClZ +A2Lsh4qB/TwgK1JSe+2r6HmEBQJlqf1lBQsJCAcCBhUKCQgLAgQWAgMBAh4FAAoJ +EFJSe+2r6HmECFwQAIDwX6fe0y6bc42zNU3Sqtd+Q3OgZfW0Rg23viI1ujyJE1uk +mmGR0i0b2luM+lSw1xOpr+pEsRX0dfaqAbbyUVIgyIZ5viXDZyWyJXr7NuBQZalX +k4njNfAELnQN2MPy/dqpelb6/J+kn6q4TC4DN95bJtSzPLK16rI94sSO+XUAJaiU +pr++cUelALoa5yHBL0mGuhlkNgCNdTE0eVwBLRQDrAywcUOEb6f2eNHyK6UY7WLy +0/LZZv2SzG/ZNQEQNY15/vrDwsQvD1ZueY5haCRK0Ga5o3GWZACU/+/c4VL2Ew7K +odxAjhVHBz50wIe35DUKVkYOQDIx9y+e50CPJicKOsnwjpC+NzQCk462ixCO9DFI ++9AFTJ6TD2BxVRHxLyUY7J21Mes4EILKFAV2dAOSZnd6LgqiYzqovJl6FmaLJyRM +JEfqvTi6Vy38Ns/6PCVGJTWKVsKz2lDas6U3/71jS0FSEwEJ9Rv9Yo75uErypNlJ +MiEahwy7kxqs8BKLtuPrF6QKRB7RgWgVxxU7z92VKCBzKDD0Oe3CDu4Lfva0487d ++TwNIGJdDeJ+ywhhFXIoGmeRm1YZferx1u5PCphiDLVkDDlLEolbp3bxKnN+l4wC +OUvhabciX46H3sM6KGMSoDRjh5n0UPr2+67qBq/rNJRCkALEFrG46i/+mNrYiQEz +BBABCAAdFiEEYKz3C/cSZFBJ7m8V7+rxZoYiX2QFAmWp9dIACgkQ7+rxZoYiX2Se +cQf+IKiMpD8+D93HtmmwG0twBbPMOVta0NU90Gvjxkw/v/JIDEWlZECClUW6Se8Z +Icq+WRZeDP6UZharGAg2GfRpfrKIwVt/aP16LsCqq+SiP4xaohmpcXQxacS5u813 +G9FFuxmHud3x7/sXtxKSVQRkhgQlq+RRG/s5CodNvjliM5OQiiXGr+q1tWy5QhRs +xCXj4CTc2CiV0ycWB36Cx9tkx+/s0pf7X4778wCrhzT6Ds5fT0W9uZifcglfI/p5 +jYYQkGpOrnOiHkBU3F80iFowIGsiv8pfaSqBP8yBAOtNBSVo5ksqSaH+TpVeIb0/ +pfGrM1BOzpTVfTmEj77qSE2tvrkCDQRdVC8lARAAu64IaLWAvMStxVsC/+NnwBBx +YPef4Iq5gB5P1NgkmkD+tyohWVnzdN/hwVDX3BAXevF8M+y6MouUA9IxJRt2W9PK +06ArTdwhFpiam2NAO5OOUhuJ1F8eAhRQ5VvI8MbVttZKSk3LiCmXGSj5UUXEFKS1 +B7WztZVwqG6YswoAPwbNerZuwYbH2gfa9LK+av1cdZ8tnDaVmZWL8z1xSCyfRa/U +AtZht/CEoTvAwXJ6CxVUBngIlqVnK0KvOrNzol2m5x4NgPcdtdDlrTQE+SpqTKjy +roRe27D+atiO6pFG/TOTkx4TWXR07YTeZQJT/fntV409daIxEgShD0md7nJ7rVYy +8u+9Z4JLlt2mtnsUKHezo1Axrlri05cewPVYQLuJND/5e2X9UzSTpY3NubQAtkD1 +PpM5JeCbslT9PcMnRuUydZbhn7ieW0b57uWpOpE11s2eIJ5ixSci4mSJE9kW+IcC +ic/PPoD1Rh2CvFTBPl/bsw6Bzw64LMflPjgWkR7NVQb1DETfXo5C2A/QU6Z/o7O4 +JaAeAoGki/sCmeAi5W+F1kcjPk/L/TXM6ZccMytVQOECYBOYVUxZ2VbhknKOcSFQ +cpk8bj2xsD1xX2EYhkXcCQkvutIgHGz/dt6dtvcaaL85krWD/y8h68TTFjQXK0+g +8gcpexfqTMcLnF7pqEEAEQEAAYkCPAQYAQgAJhYhBClZA2Lsh4qB/TwgK1JSe+2r +6HmEBQJdVC8lAhsMBQkDw8drAAoJEFJSe+2r6HmEDzEP/A8H3JkeSa/03kWvudFl +oVbGbfvP+XkKvGnAZPGHz3ne/SV2tcXljNgU15xHvLktI4GluEfJxRPUqvUal1zO +R9hqpas0vX8gsf0r0d3om2DHCyMY8GscfDF05Y8fqf0nU5/oLDlwwp11IyW8BDLS +wwANsTLZ1ysukfYc4hoopU71/wdAl85fae7I2QRduImWlMADfUtc9Orfb1tAhPta +CJVZj5vgfUNSZOTUJ73RGbdL3Z2dc42lO3mRMyDkPdykkq0EgOo6zZLuHZQFhxTz +WIWeUT8vWNjpkdTeRHLvv3cwPRx1k1atrM+pE9YkhCg0EOMTcmN+FMekgnU+ee0c +ibn5wWOvE05zwRKYROx34va2U6TUU6KkV3fFuq3qqkXaiMFauhI1lSFGgccg7BCN +MhbBpOBkfGI3croFGSm2pTydJ87/+P9C9ecOZSqCE7Zt5IfDs/xV7DjxBK99Z5+R +GxtsIpNlxpsUvlMSsxUNhOWyiCKr6NIOfOzdLYDkhHcKMqWGmc1zC3HHHuZvX5u6 +orTyYXWqc8X5p3Kh7Qjf/ChtN2P6SCOUQquEvpiY5J1TdmQSuoqHzg3ZrN+7EOKd +nUH7y1KB7iTvgQ07lcHnAMbkFDcpQA+tAMd99LVNSXh8urXhJ/AtxaJbNbCSvpkO +GB4WHLy/V+JdomFC9Pb3oPeiiQI8BBgBCAAmAhsMFiEEKVkDYuyHioH9PCArUlJ7 +7avoeYQFAmEb0RAFCQ0to2sACgkQUlJ77avoeYRHuxAAigKlhF2q7RYOxcCIsA+z +Af4jJCCkpdOWwWhjqgjtbFrS/39/FoRSC9TClO2CU4j5FIAkPKdv7EFiAXaMIDur +tpN4Ps+l6wUX/tS+xaGDVseRoAdhVjp7ilG9WIvmV3UMqxge6hbam3H5JhiVlmS+ +DAxG07dbHiFrdqeHrVZU/3649K8JOO9/xSs7Qzf6XJqepfzCjQ4ZRnGy4A/0hhYT +yzGeJOcTNigSjsPHl5PNipG0xbnAn7mxFm2i5XdVmTMCqsThkH6Ac3OBbLgRBvBh +VRWUR1Fbod7ypLTjOrXFW3Yvm7mtbZU8oqLKgcaACyXaIvwAoBY9dIXgrws6Z1dg +wvFH+1N7V2A+mVkbjPzS7Iko9lC1e5WBAJ7VkW20/5Ki08JXpLmd7UyglCcioQTM +d7YyE/Aho3zQbo/9A10REC4kOsl/Ou6IeEURa+mfb9MYPgoVGTcKZnaX0d40auRJ +ptosuoYLenXciRdUmfsADAb2pVdm5b2H3+NLXf+TnbyY/zm24ZFGPXBRSj7tQgaV +6kn9NPSg32Z1WcR+pAn3Jwqts3f1PNuYCrZvWv66NohJRrdCZc1wV4dkYvl2M1s+ +zf8iTVti4IifNjn57slXtEsH36miQy2vN6Cp9I3A7m5WeL07i27P8bvhxOg9q6r3 +NAgNcAK3mOfpQ/ej25jgI5w= +=LIEu +-----END PGP PUBLIC KEY BLOCK----- + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGKiMDcBEAC5eXHp6VV0fEBsHvqy2AGTuNAf9Zv7ux5GDT65XM1UuoXqhS0q +EGeijp1a70ndQ8TugzGSzoYT9W5xHPvgzDFpKAsiL1fELljnxd8KSl+3KKEX+QLK +1GHVDyLZTpL+vx+Nmb8920kqUMDLIeb/+TrbxGyWyOt8DFcCuigwNqsIVb5EMG/m +cbpO6pPiMahXZxmW1Hb8Pa047BC5kX2Qy07c/HhDAMyPp8C1xjyusgB+w7mSILzc +/n94CETPUztZbLEL+H9cUPFpSXEm53ZJ9MJIt2/eFYBVZ1XEU2hi341/mv2VPAiN +lUqoESJuim+OECPTUPdS8WLV5bmIAkyLj8uhArA1JpX6QwnhPuxCgptg00oHvmy0 ++DAR4DoIU1jndOIU/go78CHGIg3MrtOrXOvarKIairsX0sczRrdedZx9o1JoOiCt +K6k/lK5cYoH/WMiq3DvIUizOboH9jTj5DRXPoX/0eilGRNgRkpX3E1CbUJimiggx +6sgaC13RIaP/8tb9XQVUDsqaXHVAASZHwq4lAu1VjIh//IwQe++Qgr/k3gtkX3Hd +TvC9/Npx4pyODBGxk8KhJDHBeTP7vwl/VtYGD2XUtV/UfRGIvx93VYicsS04QlOu +3oSEzX6ayFOhwZxlbi/KY65xBMfuWw+zTs2qXbPWPkLuMZUOu0KN3oEQKwARAQAB +tCRLb25zdGFudGluIEFraW1vdiA8a25zdHFxQGdtYWlsLmNvbT6JAjgEEwEIACwF +AmKiMDcJECF2xKXQHqUkAhsDBQkeEzgAAhkBBAsHCQMFFQgKAgMEFgABAgAAYAIP +/i/mjLqeJI4l5WUckyocqALaQhe9pAX6JEk0gOlEuIgH9N/cl8fuEEv8j51TNIh2 +EQQZoNM//9Kj1dMxoy9Wtkh1yFe5OT9tKXkaXNwVeox45OqXYs/ARJ/rDUt1BNXu +Nbhdh5+OAYbFltF33JdfLXMRK22LoSOXPn1opEH1Zu6HS40lXl06CVqa7m3gvLY3 +BC/9pi8bSow/INnpJPjavtSA2uLLtRQRaqXs0iwF2FkyAKmAT7zANCA1pkBVMa7E +W+ulP0cr5/nqIPKIBfZxYmqE4YvN3px3JBNtzj7cdC3hAn1km1thOWSaBzb9lXLT +eXSHSRgG6AY2GdfC3F5UC6g6rEIncEJ8drfnTPpMLvXF3+KZ0ssdbLG9ctfev6X+ +lKS+TFEZs7TCANa1lEPr/ISCQYBbL63+xAbIz9SXG07jH6aFF07j6I3h+bWvZTJn +GIj2pq3QxBwh/pYf6hICxYU+fDP67mhlYor7yNIT83W+Ik4IhbLj9AtiW05NIavx +HPrEeYbjovsGWUhvN1LCAO7GFgmcTyQIqDDtLYLxLjnvjptc8HlKh4WW7KqCVawt +GayAcYYQXePDxerkiR0y6jCUSzr3MR8c9yfYarieQVKQLJTDP0UDYnXd20dlvzR9 +Q7wCbwu6jb0EcRDcnbZg8K8gOu1N2gfyFnesz3rq+PCAtC5Lb25zdGFudGluIEFr +aW1vdiA8a29uc3RhbnRpbi5ha2ltb3ZAZGFzaC5vcmc+iQJUBBMBCgA+FiEEFRkd +BbXPlW/jfJWWIXbEpdAepSQFAmKiSfACGwMFCR4TOAAFCwkIBwIGFQoJCAsCBBYC +AwECHgECF4AACgkQIXbEpdAepSTsahAAlq+6OBs1BL7k0drcK3hzN22y3E1LzBEK +mpxeIJ+eHDMerhVoSuDM75fwWk6SXoKxaRRErQ2EP3a5jDfu8MGD2xDypEcMLvE+ +EcFT3M2X79w/+MduR8cp9lUd0NCwpI7zAANq7Mj6gLDFdKEnA8pe730sHZB9I4G9 +vZl961FqzFUMwMttl8KxTzMKnbH/u5Tsvybh/dsv0lcV10irDuCoGGIM/MP42Hul +9CO3bAs69KXA30r/711ooAL3cpw5J5CeMvV2N5GnE656Cl9wRl6rCOSNoaRNJG4t +KtfNZeDd6na6+fABFnOYzzG/kd1+OcmfCFK79ljtL92b7cJzSkoOXfLYvM+V7UN4 +AohH8Lmon4MzGjieBFitHOOUMQy80hBEhuliajtFTv6JB4wS1K5U0NzNKjvLbUhQ +e+iabtChSAtYr3/liDALdROXyrEzAHYxK8Q5ZWdE9wUIz2HcQpHiFt0L33Y4lA8V +Dm26fi02svgHg5SBGGwQ65hSlzIQgmASaogoW3cYPOqVveibcGlM0bxM+0MN3QR6 +0T98PmqcdUV6S+xUkR1LI+5bj7ObzOusc0UGM8m4GQ+DdY46UqInc4yFrgLPzoj4 +QZPwn7aMRFbBF8YSTh7Cr4XvAx8CP2Abau8Sm6YHxXaausKRKaT4eKQlxryGkKdD +sQO3K/PaBWu5Ag0EYqIwNwEQAMaVJMN/2qrJUQnZgoOTcAmjKKUxphnGR27jqVKh +wTT3JW0qEap4ZUF0o6dJTHA0Ni2FltsGMddfyE++ipDgpW/+q9pFE6rs/eUufBX2 +yeYpf/4CSh1rZ6zqXqBQeifEflhEC1PXI+LGFOUyjuR5DV7cHw/i74UWXpUy8zT6 +RGyExSecmqNu9/6zCMnlNsfCAIfurwtrS6RdsYbvxSGWkNOnqkJ6zxOKgmtlOkeL +eNTxk4Oq+o7vPVh0zK/o5owMGpJzef1myMbB5H1aWeM5ReHf4y0VYCpR/IKhVCMm +qrgg70iGDLeeGaB3KFrCyhkFz/hBEcaL4juglQUq9CsfT+bbHWEoQS8jVBekRJi6 +iObupFIibC+W26p4/d5IYmYfU5gKMxPkfFoSokFGeICb8i35Rshv17vvt/Z/MXvk +RcGLAM2ydDtl5VG/h2dH1Dk9CLE3xa6AgtpIyUot+Y5VU1PC5p6gyD7WEo/dSVw7 +AJlgFKIx/UM3wx9MlVm5rB7sHvwnjaUcCTuBRtVitsmWsY/5N0K+qxGj/S1hKWQX +rmT+0K4/sRHfwv3lnFdeocq4hKfcmfhJJXoGXDL/jn/2ml2Oi+0hl0Mtds85MeJR +FwHETjhi/F5xAu9IgKJv/ewomKo8hwk0yHiNm46CCjHb7XmoIzz3e08z73pODzin +2WX7ABEBAAGJAjUEGAEIACkFAmKiMDcJECF2xKXQHqUkAhsMBQkeEzgABAsHCQMF +FQgKAgMEFgABAgAAT1cP/RStJ3oBrHGWB0fjPCfyossmgSeUKo4it+dHqNPTumIj +Zyy5p4FAhFsYeSQwoqlrNgZgt0MZxWQjvV6vNKqx0DXVR5S+xilPI8vpRSfnJhkI +vVdVY8qMj4I0/cyYqrasiR7YVIKepmEZe4aQTzhs/ifMooeY1+ZIwwLYollN91se +Nf3JqWmhY5Q7lPhUZXiyFNyE87geM1P4aOgwZm4EikEadzBFcoHzAXczSCpBwRxM +u53EQbz7Oq2xnFLORPAAwz9yJjCO/0N9HzH2o2Du6GeRccMeHZ65U8tQLvDO79Od +iWDZsU1h56BkDMhqTOsymHnv/QX4vO/X0tShhZXzLwe97++U+HUDobjcHmAySVNy +OugeGdFyYExMNM6Jd/GoS7Xo+RecBSP1yeDnweZgupCmzHVbrfRf7Vdesf6rY7hl +81amRIjdMlhWjOX8OxE4/u+npiQH+wT0VLOwTbxDNvGAAqzYzuETdNROiqqHGNXR +nc3pdm9EUvG/ur4AABDKllnsa0OP0oTOh+FqMQSlTEHwxPhlE11lyIIh2kkuNMmq +Vr7qNeOq3i6dA6EvGn2bikTsvHDw/kF0h08xZRTuy1I0Fcb6GYStM6Qskt4Hhrsa +xwuUTBELdLnf2nLk7sAoUl269juuWXTELTGC40olQh0m8bEXDinknhu6Jug3d0uW +=d05p +-----END PGP PUBLIC KEY BLOCK----- +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFUkMgoBEAD5lFzlr4fIR3CKlsgx1KXLNR+1+IIe3AT8YloMq3rlvylOTgGl +j1PTeQL0eHH+fD3ukSHHiZC7FcY2aC3vTPCd16+OO+ii/Nfx6vAyve2RiTA4brKi +BOGuI/Neh/ow9Sg1AOZY0xsjXVqkabExg+zlUy/6DoabuVEnv/kpl1Bjr5pTfXNG +yeXDKF7MkItib6E9qDE5AsU31XQEAVKBv6u9r+W297+Db3AH6rK3WXiSLfT4KfmV +oufRIubPQvPnYt9l22mPS0gtO4NLB1Qruu/IEYbSUYcWa1GOe9EYoxbPOhWUOj1G +Dt6E4fb4JtmJ/7vkEeHFDRcrW/3EHQLkdLWE4sWrtxWBS4mfjwW9IiT3uDIHiG4F +OjftU5eCefxa7eLJBwjL6YSvD3IdxCLE2fIhNWFgvvCX4gYOayNk8kseV4qdAh7V +PmNhelB3vOnB6S4ufv3ByCwjkviUMZv+L9miAM3Nr1wnX89//ie99s+0FgHtO12c +LPbNCtfHfocnXYdMKoH8cbziOnoKOSUJYtGrtXXRJlKL9KmYCJnbx+sJXdRucCm1 ++xEPRD8m9KHuuOk3powaAWztmL0fpkfrZ4MgHL64VOHlRVq8BpcUhMhrVUiBPL2U +Qh9Bik5QTF0+Cb0WnYV1ktD5QSuI/7LVngd2VVhynMxJ/0TgFwhGwMkA4wARAQAB +tBpVZGppbk02IDxVZGppbk02QGRhc2gub3JnPokCVwQTAQoAQQIbAwULCQgHAwUV +CgkICwUWAgMBAAIeAQIXgAIZARYhBD9dSMnwApPNNlo6mINZK9FADVjZBQJfX098 +BQkdmE+iAAoJEINZK9FADVjZQKcP/3m+uvemzL2Nfo6Ewm0qUjG8dFvD6scVrX0Y +Wc2C+l8mX8niLJz7p4ulg+f8qqZ9ai7zwPHzXlq+qnFMljqqD0zBkemnfzWboUqP +fQ1OF9p6CYwDWG60+YQqz+2wH8/ScLeBiJEpjGIQR2/TgvX0NH+aU7zkfdT26aVT +S7XgF9BVISlUgnPjmq/5uq3944zkv8afFuHWbo4KHokKIBW9ZQ8auoK/xwCotszX +/q//sqHsYLHu8iQN6qWNMD2uXlp/v10qZsiCgrbCOuxmBZ5si49rgnc0jnJRq4/1 +eBbRVqGlLM79mzUQ6X4lerCpZBXLdC6qGF2N7+7RbRYQ8QZomQhGJPMSJ+pQlgT7 +tb+GhpMy01fGmatL+GEEXzhZPjYSqR/HIzx4ZZUV2R691wzGXk/oLhLyAy4NUabc +G6ykylcEZG27G1PldbZlRCGrr5eCnOFULNYDIKWyoyuabzsgDLIBzNDNo97SmTaB +46iUVYVxxHpVsi/p1TL2jCTo0P15oQoyfVX/a1keRRkymQazTjgMSiSrFG0GxGHV +LZ4x4dcdTVj9PBeJRAS8JJCwR3ZmO1+nEdPAPTiQTjQYZKPTCi3kB1LD69jKY6wp +7pX8gN+U8wWl1sV+CBqU9Ts/lKbH/eKFUcKC2nxYOYdsDjOOjvUGrRYJ3hmhGfoJ +kqlmgoyaiQJXBBMBCgBBAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAhkBFiEE +P11IyfACk802WjqYg1kr0UANWNkFAlyKGfUFCQsoeRsACgkQg1kr0UANWNlpcw/+ +OX/tl7kbtY4ndb2ugscIM2W5mgAlJH/dzXO3W7c1fYb/u4RQlGZlekHjzT15mApd +jy2AKfxGFemFRHT9aQaETHDJwNrkn6PYjXrHDqWmgdygJSUCCBrq3Vz4BbIa0Hse +6eUjOT/bzrmrLbOc3kyITVt+MfvuNiCs0po9FcDt0yU1sIy51Xt3xricA5sXZnwK +iIxWVGtWw0TqIRtWW9piSGDJvGri1MIbLvxjIKEkKZsfcxMB5Lun7lQ7J0qrrOFW +XBbhAyuyOXzcuZBVvDyUrk6f5HDRvO78KYwUudWwW0T0rMDT4hh+Iq4TO3GkU6y2 +FUWfggw3sf5JKC8hrcSLVBZ8Qu+ZcwbWDX1ZBGtl+x6eNhphOapUuwCuwnPQ6vmH +SePhssXLRPMCcketgDtacNuN14OKAJws+40TuEuAW9hsMqXzlJgrMfeGG7m/NMeP +cp4LnYnaOZCzRZjUHlP5ljKvYF1MAYrG1vVYJOi4z3HoRJAg1qA1RsW3CRc/YkRR +cHCXG28srtgALP5jY/264Pd7xKWtpvTiuB0cjQQbwY/xnQK7DDPEhfs9xo0yjhZf +iaycN/BWn6YvZdkXjgDp0BtxqkFaDwqtDLCLnPdAab9czpajQRoneAWPQkh263qf +6Nj8xprw5sdnTrPsNY0QBNh5PgPxzjY2+HMHVPNgDPeJAlcEEwEKACoCGwMFCQeG +H4AFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAlcReQUCGQEAIQkQg1kr0UANWNkW +IQQ/XUjJ8AKTzTZaOpiDWSvRQA1Y2ak/EACu/O/MdMW7g4QJluc4u/TxknVvMyiU +wZpTRztvSc4ktnQIpMa/neRA3dLyA0QhRkPocOPAvcCf1zrgOf+L6TzYcBoDNTST +Rxuy9zCegbjfTMeIhfG8dg3sdB6FAs3+TeeyOTOz5enPVKxHAyyG+UCc3B16T0dY +k+twopQ6Wfuqtr6cK9OSYUDg/7mqHTfHJpt3go9ppuNFiiYHyR3uEztFYNYQj70n +mCgqIajIPoLsaFmtxVKm2jXJkbXlPQG/58XfRQYEskXtJNKItQQxEG/wMryXOknZ +yuituJwTW9eOe7CaUWcsVIbxLjt5nuuatKnbuagjDKtmb44kymPBsgdkgfRM1fCl +lkylxghtTSXdHG3Y+hcixgFuzQsxibtmANsSNd3chuETz5isz2ZWbcW4ItV3Izy7 +Gf9dcCHtIQEVD2ja9Vz2PBN4Y9RmSwPgnAFpS0gx0FKzq7oQbccatrcI6y+PV5D0 +CbA/Tjnt1Ik8W8+qIGzEpv6Pe09sWHKXbLEhoujBa+xHpWU+5tPiRElKDxze4sTh +x7rhN2wIyyqPjKjMAs2b/NFQjYdvA1/D4wOtqpFCwRxcyRO47zlpsD+Zjd8EhIAE +VbUzyFIousHbXl8fM3rtYehcJFufd49F8oUD0fm/HOQvnHQB5VMQ7wNPQQ7VgbjN +PfbzrNzagNvKnYkCVAQTAQoAJwUCVxF46AIbAwUJB4YfgAULCQgHAwUVCgkICwUW +AgMBAAIeAQIXgAAhCRCDWSvRQA1Y2RYhBD9dSMnwApPNNlo6mINZK9FADVjZcAYP +/j5fgs6jYafTrlHpH96yji5t2bJzNLWqQx6KtVVB7hyL2wPdm0lFXn/0m3HjjuY0 +KurIz2BQ7wW/k9mnYxhhCCh3YYf8fax9ECDJrSAMej+ugYBmYBaAmlROSKEzRKNt +rycBYbYwRuh4yAymgi97vFe8B+HPBe/YiqpzZ7h1TPG6+OLCZRQ9tDvPc1cjnzbu +Z+LU52B9jIkxpM8zJsaCaSg3F/S2e2Y3OUaWhNPsNIaAqYVMUlRTy+yzo5F75f7w +e1ze6AK9Z76I/F13tLNJG03BVJ8OnNkwSMuaJZCbzuQ1MSfFlgTOOdrQjnMjB348 +Ry5c2Sdwmn/ygCjzwBxxRrn1GUAzRoO1goe7SYKUXfPj4yN8gWbeeJGnUyHx57BQ +fdnotXbg9k8TIWCTcKKVxdlABgyhUy8AD4maETMASUZLVT04xNptMj4WQ81fk/Np +g6RAOzK35NfBOAjQ9rRIrIyDD1jVqH3bZPjkO0HS2mgldkIDMi+KNL9MdA83P6Cb +DakBWxPeD+xVtMfDa0vGodcOE228Ex6JcjGljqQT8xW+D31cz4Uw4pnzrB8WxybV +sBMsWLyjhRfhv8qnUW0h3icW26gFFSutPnyA51NS8p5HScHdN27ilyz/r0lye2/D +6Z6oyo3gEyvxEEjJaOK6GO1I8C5TCGfdMvPKaRq2uJqMtBtVZGppbk02IDxVZGpp +bk02QGdtYWlsLmNvbT6JAlQEEwEKAD4CGwMFCwkIBwMFFQoJCAsFFgIDAQACHgEC +F4AWIQQ/XUjJ8AKTzTZaOpiDWSvRQA1Y2QUCX19PfAUJHZhPogAKCRCDWSvRQA1Y +2XreEADJKYpzMt6wUm0bqR3oAdSD5WvCl7PNV+uqsREIfA2enkI7HbNXWqr9f/53 +BQwBFhJsLz7xWfY7gMj28YoJ2FVWGHj1ZPLh7XtEmPZwFXSq7v3SoqygrgYZ3yaS +JW3TdDCfMlhKG+oJKWbOIyDR78tM1WtIkmB3UZCKL2ymiEHxRftJcEdlmxUBS2h+ +unHpx7HKWTPJvza/PoVd7YYkXsmZSoCDJ0fCxpDMIzXuP4AA3Mr5uZj+DTfKhaKi +yyBOi+xkZAwpVsnSqAj2s8BWlqjETDCtNOzSmLVXsUv74p5JtQunb8v1waODo68m +aB/VuV1gMJvfOWj88VnkgWglUO859eRWQ5LwEjzZ8KGEV0MFqDFHEI14a5SsZrtn +hVTXT7yUD9IyZod/fWNGZJT3uUkzykpQ2IKszkbuG3zriDv8rk7Ppx8gQ+kBrXwJ +IXCxG8sXj96ugfp23oh6b6iNBJqXFfJ8567LzIr5pFQChRAG+L8qruBNd0LXES/9 +EZBZPB2DOCQnYf/igtdb3XVKHhpHzrwsYhFExNia7eYz1lf7GklL50mzcP2xcQ5D +uZ5acS0JO3y6cUPJQxsEC26naw32sctxaKFz3DeAYlMmIR1Z8PgeO2cdDZucxZHA +FZL4poIRnBcHGPkytlr/zk/F946gtp3HU29w3cwhB6vDikVjfokCVAQTAQoAPgIb +AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgBYhBD9dSMnwApPNNlo6mINZK9FADVjZ +BQJcihn6BQkLKHkbAAoJEINZK9FADVjZuoQQAIuc55ExIDZYkzHy3Q0amIRH7Eif +XJuTGu6NkyzYBmqgfXGLLfqZAXjCSyKa0N/ktW9y6cQtU+bUItzPIaVtn+56WjQw +U7ojQfJeyNu8wraRKiaNlSkLfC447ZB5Eq5w7TML67zCvYGB1DxAsNLiOas/evAY +Fwm7QfpwvmnXnOU7u/EuWRoCCfkP+6pZc26u034zv4CD7Jwp37Tk+L38LlZ8zKn1 +ksMd+nqV6lvdwY2iPCV75rqJ1gDh3I91+een1dHHMllsbWRShaC7Z622SXUsDibA +CfE9aqFyvf7H0AL5cc/7CJbUbmoREnj0N+dBzhsH8Qi6ofgfWLP0lxHyUlLpFAua +wLBBzg21d7goA4yShaE2lVIpRp3pjbbHqE0NMB/FvcL3HDe0SUERkxdA5WSEmEYz +5NSBZkPLSQSs6pMKYRrUXdiwysjEOP6hmydUkwmfSZAGogFgDC/cUxVMv391WQMP +m+VpECQKVTX5IBERiUk4suKMCxBdxUw7wXsnE3OlOwdK6KEclLzy3fhEKA7HsNSs +eJr2NiF4Ue494oJP/TzZO7fmi5Q+H9CASRQySOOhYJFH9bRvJMa/HSvoYbwE05RQ +3zhB3i3dFmWfeRCmhCiRkCWlZJyRuRemyAW0mhDLkatWX/2Wew15/eKn4CeoPubb +nDNXb1NCgs8IK8e6iQJUBBMBCgAnBQJYxm9cAhsDBQkHhh+ABQsJCAcDBRUKCQgL +BRYCAwEAAh4BAheAACEJEINZK9FADVjZFiEEP11IyfACk802WjqYg1kr0UANWNlW +PRAAqZPmW/7lsLFaL0hQ+Votj+32FnamiABJKpS+t6Fkm1ckIK+e+nuFXz3pr/WQ +J0eCmLoUwsngz+eOChPJDRAUdMb4eCKcW0yRd06UWZfwg7ugW/j7nXvDu4kJMnwW +thpysyVDpFpnRWC2bwplJzU+LexIF2ijjQTNFzQg0CGCxP0wZu+Be8NSVq0jgjYk +Hs6ekWBEWGlgCspJD/OeVvicRglump4/G5vqXt3jZyrAxt11N/Kl+uCnt1nnFQrn +6KQQbV42+P4ONGGK0DTlfGDYYICDP7XzNLHf0h7GElSjYEWeXLRh4jerkLIm3/1p +aa2XJuk4YSTAs1AuovAQGsbAMBgoecMFPE4qN+MNG6oXgl3PGrz2wvIZjpLjT9DS +u8FM4UqZX8ne+Hj0nn1wVKebQKfbSRiXaCxd0DM1EjmAZAsX85iikIhgd7/bP2Bw +ybrhQTp6dq+oS6/+z3qWeI1UWeYj49bKd+zTSjRVJEpRCkzXcIclTCcQw4ktRHv6 +ZdnFlx0TPzmvF8l5zOG0XvUQSOjCdGp1YulHAe681XXtYf7xG0lBxx2BsbTTKotm +/p75OytX9Y3/TMVoqkbog6fEt7yMWnWWzA7PLigoJwBfRW0FNvAmlSu3gbyUMw3P +wxLbBzaJsXbgdu5dyOOqyANVmugt2hLAkPds7H4tXsugODS5Ag0EVSQyCgEQAKKA +lbyFjfBNciP4c5JoYiDs/GNwmAh19TvZK9PDcmIQ8in76Yvpyiw9O+V7fCdyE/9N ++Pp8nVMv+HYREE14KsZVZMhi2oLkrta1N7nqwKHNcgh0OE/PN7yGUndq93hrCgDN +hTpfBAMb1tAsVljXTuKlxKgg+2ebznCSR9WfU72028kNBoMas1Z+orkXpknO2BOc +WUP8NShroxBdXg2I2k+w9zGNmLrWOsK+pqCFWY3xEObyy3e47McYiAYYXY3Ifb2Q +Saa4RzDQO97yKQcPWUYbpmbECAIqxsZzo/zCCZTx5c0zsPjuKpCxZY/oYx8K5opm +0cdcN51VsOl2YKGmpHd+lywc6huaWL+uSFspdshaufhvIJZ/neCsf7P5dZaoiUd8 +1RvEMaos4ZIMb5FBZSKqAFwTbAPu3w0UhW2JPCmNOphFenSNbCLjz3xqtZ/lpMy6 +7i+xJY7kv1RNbSXWdZIr2mwLMDJ8dqtacwA/A079ly/ze6iO7yNASQe78gd7/RCd +1tO97PK3xyaLs2lR1fHh8PKzPBxHKeoLjyCM3NH1JFGOtanFpubwBzyV2NShG8Wz +wkImT/noLqhOM/CEY8W6CdMabhoTUjDPRF18EVnSlKkVj7k+J2h7t7/P/CylcMhr +F1r5tUs5Ue48202dYFoNfNsN4b8djSk11HjMry3RABEBAAGJAjwEGAEKACYCGwwW +IQQ/XUjJ8AKTzTZaOpiDWSvRQA1Y2QUCY8Wf3AUJEmPU0gAKCRCDWSvRQA1Y2cKx +EADN/UUwxKSkhp/DWtw8Vp0PCYkuj3edFS+BXw/S8X6QCh6kBcFzh/YFRSVnuxrg +U5KxQ3BXEAEgTtapfPWckE2UAdLgOREjGj+ZPs9YnDbihKeizzBW4aC8e6zNRS7y +f92G00N1cr+LNjOpF9WUkuoU8FdfKo1tXmUi1KW/zhUVOMsZCvWlrDXA/ldSJ8FI +BtrNpc+OvWtOTkfKwPKvE0YUk93ukyxNPmoY8TYrxxzMe7C77tEb5mlW3nRCb8vb +ETOGz2HZCYpSQs7n4UNbUMLojHYbJMtW/UAoNrCYOiTfyTmbsvPvkgP4USlBNr7K +txcJTU+ZhqbQsWz/iHCvTKnP+Vw1CLpjQ4L7hvJwN4v3YI5Arc60YGwycvj23jE/ +5ZH7TuqymJ/1G0pRNk6oTWDDv10zFSIT15w1wYkmpbr9gHgeYOg6uwTPuevbpyLa +U2jKX6faTvhxg/8h2eUNUM6agjWAHxaemEiDX5NWiwA1Tkh/7086/jdu/ZQcGSJ8 +d46lqMDc1BhhR+5WePouf2UElAGdxqWhHKzM2Bt7D+jCrSbvtOlgrotg5Xx35vA5 +LAMYhJG4/etvORZiXuWWHs0gtZ85Itxjet8n58oehUI4mhpXQt2Ya+2oTpc7D5RD +2x++a0fd30gBgGGz81kMJpWewGAKlWEIrGmV/CfzR7eqxQ== +=lTCd +-----END PGP PUBLIC KEY BLOCK----- diff --git a/build/templates/backend/Makefile b/build/templates/backend/Makefile index 5b9e0bd4fa..570444583f 100644 --- a/build/templates/backend/Makefile +++ b/build/templates/backend/Makefile @@ -1,7 +1,21 @@ {{define "main" -}} +{{- if ne .Backend.BinaryURL "" }} ARCHIVE := $(shell basename {{.Backend.BinaryURL}}) +{{- else }} +ARCHIVE := +{{- end }} all: + mkdir backend +{{- if ne .Backend.DockerImage "" }} + docker container inspect extract > /dev/null 2>&1 && docker rm extract || true + docker create --name extract {{.Backend.DockerImage}} +{{- if eq .Backend.VerificationType "docker"}} + [ "$$(docker inspect --format='{{`{{index .RepoDigests 0}}`}}' {{.Backend.DockerImage}} | sed 's/.*@sha256://')" = "{{.Backend.VerificationSource}}" ] +{{- end}} + {{.Backend.ExtractCommand}} + docker rm extract +{{- else }} wget {{.Backend.BinaryURL}} {{- if eq .Backend.VerificationType "gpg"}} wget {{.Backend.VerificationSource}} -O checksum @@ -13,8 +27,8 @@ all: {{- else if eq .Backend.VerificationType "sha256"}} [ "$$(sha256sum ${ARCHIVE} | cut -d ' ' -f 1)" = "{{.Backend.VerificationSource}}" ] {{- end}} - mkdir backend {{.Backend.ExtractCommand}} ${ARCHIVE} +{{- end}} {{- if .Backend.ExcludeFiles}} # generated from exclude_files {{- range $index, $name := .Backend.ExcludeFiles}} @@ -24,6 +38,8 @@ all: clean: rm -rf backend +{{- if ne .Backend.BinaryURL "" }} rm -f ${ARCHIVE} +{{- end }} rm -f checksum {{end}} diff --git a/build/templates/backend/config/bitcoin.conf b/build/templates/backend/config/bitcoin.conf index d10eed8880..619f678536 100644 --- a/build/templates/backend/config/bitcoin.conf +++ b/build/templates/backend/config/bitcoin.conf @@ -10,9 +10,14 @@ zmqpubhashtx={{template "IPC.MessageQueueBindingTemplate" .}} zmqpubhashblock={{template "IPC.MessageQueueBindingTemplate" .}} rpcworkqueue=1100 -maxmempool=2000 +maxmempool=4096 +mempoolexpiry=8760 +mempoolfullrbf=1 + dbcache=1000 +deprecatedrpc=warnings + {{- if .Backend.AdditionalParams}} # generated from additional_params {{- range $name, $value := .Backend.AdditionalParams}} diff --git a/build/templates/backend/config/bitcoin_regtest.conf b/build/templates/backend/config/bitcoin_regtest.conf index 0fb7aef215..3bdfc3dcc2 100644 --- a/build/templates/backend/config/bitcoin_regtest.conf +++ b/build/templates/backend/config/bitcoin_regtest.conf @@ -12,6 +12,8 @@ rpcworkqueue=1100 maxmempool=2000 dbcache=1000 +deprecatedrpc=warnings + {{- if .Backend.AdditionalParams}} # generated from additional_params {{- range $name, $value := .Backend.AdditionalParams}} diff --git a/build/templates/backend/config/bitcoin-signet.conf b/build/templates/backend/config/bitcoin_signet.conf similarity index 96% rename from build/templates/backend/config/bitcoin-signet.conf rename to build/templates/backend/config/bitcoin_signet.conf index c26fa574e1..e88a0fd50e 100644 --- a/build/templates/backend/config/bitcoin-signet.conf +++ b/build/templates/backend/config/bitcoin_signet.conf @@ -13,6 +13,8 @@ rpcworkqueue=1100 maxmempool=2000 dbcache=1000 +deprecatedrpc=warnings + {{- if .Backend.AdditionalParams}} # generated from additional_params {{- range $name, $value := .Backend.AdditionalParams}} diff --git a/build/templates/backend/config/bitcoin_testnet4.conf b/build/templates/backend/config/bitcoin_testnet4.conf new file mode 100644 index 0000000000..46a5370b8c --- /dev/null +++ b/build/templates/backend/config/bitcoin_testnet4.conf @@ -0,0 +1,38 @@ +{{define "main" -}} +daemon=1 +server=1 +{{if .Backend.Mainnet}}mainnet=1{{else}}testnet4=1{{end}} +nolisten=1 +txindex=1 +disablewallet=1 + +zmqpubhashtx={{template "IPC.MessageQueueBindingTemplate" .}} +zmqpubhashblock={{template "IPC.MessageQueueBindingTemplate" .}} + +rpcworkqueue=1100 +maxmempool=4096 +mempoolexpiry=8760 +mempoolfullrbf=1 + +dbcache=1000 + +deprecatedrpc=warnings + +{{- 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}} + +{{if .Backend.Mainnet}}[main]{{else}}[testnet4]{{end}} +{{generateRPCAuth .IPC.RPCUser .IPC.RPCPass -}} +rpcport={{.Ports.BackendRPC}} + +{{end}} diff --git a/build/templates/backend/config/zcash.conf b/build/templates/backend/config/zcash.conf new file mode 100644 index 0000000000..edd7e6c1da --- /dev/null +++ b/build/templates/backend/config/zcash.conf @@ -0,0 +1,56 @@ +{{define "main" -}}[consensus] +checkpoint_sync = true + +[mempool] +eviction_memory_time = "1h" +tx_cost_limit = 80000000 + +[metrics] + +[mining] +internal_miner = false + +[network] +cache_dir = true +crawl_new_peer_interval = "1m 1s" +initial_mainnet_peers = [ + "dnsseed.z.cash:8233", + "dnsseed.str4d.xyz:8233", + "mainnet.seeder.zfnd.org:8233", + "mainnet.is.yolo.money:8233", +] +initial_testnet_peers = [ + "dnsseed.testnet.z.cash:18233", + "testnet.seeder.zfnd.org:18233", + "testnet.is.yolo.money:18233", +] +listen_addr = "0.0.0.0:8233" +max_connections_per_ip = 1 +network = "Mainnet" +peerset_initial_target_size = 25 + +[rpc] +cookie_dir = "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend" +debug_force_finished_sync = false +enable_cookie_auth = false +parallel_cpu_threads = 0 +listen_addr = '127.0.0.1:{{.Ports.BackendRPC}}' + +[state] +cache_dir = "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/zebra" +delete_old_database = true +ephemeral = false + +[sync] +checkpoint_verify_concurrency_limit = 1000 +download_concurrency_limit = 50 +full_verify_concurrency_limit = 20 +parallel_cpu_threads = 0 + +[tracing] +buffer_limit = 128000 +force_use_color = false +use_color = true +use_journald = false +log_file = "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/zebra.log" +{{end}} \ No newline at end of file diff --git a/build/templates/backend/debian/install b/build/templates/backend/debian/install index 27e686617c..950633bb4e 100755 --- a/build/templates/backend/debian/install +++ b/build/templates/backend/debian/install @@ -3,4 +3,5 @@ backend/* {{.Env.BackendInstallPath}}/{{.Coin.Alias}} server.conf => {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf client.conf => {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}_client.conf +{{if .Backend.ExecScript }}exec.sh => {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}_exec.sh{{end}} {{end}} diff --git a/build/templates/backend/debian/service b/build/templates/backend/debian/service index 54473b3b63..2f5193ffbd 100644 --- a/build/templates/backend/debian/service +++ b/build/templates/backend/debian/service @@ -19,7 +19,7 @@ Type=simple {{template "Backend.ServiceAdditionalParamsTemplate" .}} # Resource limits -LimitNOFILE=500000 +LimitNOFILE=2000000 # Hardening measures #################### diff --git a/build/templates/backend/scripts/arbitrum.sh b/build/templates/backend/scripts/arbitrum.sh new file mode 100755 index 0000000000..0872739c21 --- /dev/null +++ b/build/templates/backend/scripts/arbitrum.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +NITRO_BIN=$INSTALL_DIR/nitro + +$NITRO_BIN \ + --chain.name arb1 \ + --init.latest pruned \ + --init.download-path $DATA_DIR/tmp \ + --auth.jwtsecret $DATA_DIR/jwtsecret \ + --persistent.chain $DATA_DIR \ + --parent-chain.connection.url http://127.0.0.1:8136 \ + --parent-chain.blob-client.beacon-url http://127.0.0.1:7536 \ + --http.addr 127.0.0.1 \ + --http.port {{.Ports.BackendHttp}} \ + --http.api eth,net,web3,debug,txpool,arb \ + --http.vhosts '*' \ + --http.corsdomain '*' \ + --ws.addr 127.0.0.1 \ + --ws.api eth,net,web3,debug,txpool,arb \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.origins '*' \ + --file-logging.enable='false' \ + --node.staker.enable='false' \ + --execution.tx-lookup-limit 0 \ + --validation.wasm.allowed-wasm-module-roots "$INSTALL_DIR/nitro-legacy/machines,$INSTALL_DIR/target/machines" + +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/arbitrum_archive.sh b/build/templates/backend/scripts/arbitrum_archive.sh new file mode 100755 index 0000000000..27c7d6dabd --- /dev/null +++ b/build/templates/backend/scripts/arbitrum_archive.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +NITRO_BIN=$INSTALL_DIR/nitro + +$NITRO_BIN \ + --chain.name arb1 \ + --init.latest archive \ + --init.download-path $DATA_DIR/tmp \ + --auth.jwtsecret $DATA_DIR/jwtsecret \ + --persistent.chain $DATA_DIR \ + --parent-chain.connection.url http://127.0.0.1:8116 \ + --parent-chain.blob-client.beacon-url http://127.0.0.1:7516 \ + --http.addr 127.0.0.1 \ + --http.port {{.Ports.BackendHttp}} \ + --http.api eth,net,web3,debug,txpool,arb \ + --http.vhosts '*' \ + --http.corsdomain '*' \ + --ws.addr 127.0.0.1 \ + --ws.api eth,net,web3,debug,txpool,arb \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.origins '*' \ + --file-logging.enable='false' \ + --node.staker.enable='false' \ + --execution.caching.archive \ + --execution.tx-lookup-limit 0 \ + --validation.wasm.allowed-wasm-module-roots "$INSTALL_DIR/nitro-legacy/machines,$INSTALL_DIR/target/machines" + +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/arbitrum_nova.sh b/build/templates/backend/scripts/arbitrum_nova.sh new file mode 100755 index 0000000000..3f15e4ef15 --- /dev/null +++ b/build/templates/backend/scripts/arbitrum_nova.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +NITRO_BIN=$INSTALL_DIR/nitro + +$NITRO_BIN \ + --chain.name nova \ + --init.latest pruned \ + --init.download-path $DATA_DIR/tmp \ + --auth.jwtsecret $DATA_DIR/jwtsecret \ + --persistent.chain $DATA_DIR \ + --parent-chain.connection.url http://127.0.0.1:8136 \ + --parent-chain.blob-client.beacon-url http://127.0.0.1:7536 \ + --http.addr 127.0.0.1 \ + --http.port {{.Ports.BackendHttp}} \ + --http.api eth,net,web3,debug,txpool,arb \ + --http.vhosts '*' \ + --http.corsdomain '*' \ + --ws.addr 127.0.0.1 \ + --ws.api eth,net,web3,debug,txpool,arb \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.origins '*' \ + --file-logging.enable='false' \ + --node.staker.enable='false' \ + --execution.tx-lookup-limit 0 \ + --validation.wasm.allowed-wasm-module-roots "$INSTALL_DIR/nitro-legacy/machines,$INSTALL_DIR/target/machines" + +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/arbitrum_nova_archive.sh b/build/templates/backend/scripts/arbitrum_nova_archive.sh new file mode 100755 index 0000000000..eb150f79b4 --- /dev/null +++ b/build/templates/backend/scripts/arbitrum_nova_archive.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +NITRO_BIN=$INSTALL_DIR/nitro + +$NITRO_BIN \ + --chain.name nova \ + --init.latest archive \ + --init.download-path $DATA_DIR/tmp \ + --auth.jwtsecret $DATA_DIR/jwtsecret \ + --persistent.chain $DATA_DIR \ + --parent-chain.connection.url http://127.0.0.1:8116 \ + --parent-chain.blob-client.beacon-url http://127.0.0.1:7516 \ + --http.addr 127.0.0.1 \ + --http.port {{.Ports.BackendHttp}} \ + --http.api eth,net,web3,debug,txpool,arb \ + --http.vhosts '*' \ + --http.corsdomain '*' \ + --ws.addr 127.0.0.1 \ + --ws.api eth,net,web3,debug,txpool,arb \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.origins '*' \ + --file-logging.enable='false' \ + --node.staker.enable='false' \ + --execution.caching.archive \ + --execution.tx-lookup-limit 0 \ + --validation.wasm.allowed-wasm-module-roots "$INSTALL_DIR/nitro-legacy/machines,$INSTALL_DIR/target/machines" + +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/base.sh b/build/templates/backend/scripts/base.sh new file mode 100644 index 0000000000..1b9305644b --- /dev/null +++ b/build/templates/backend/scripts/base.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +GETH_BIN={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +CHAINDATA_DIR=$DATA_DIR/geth/chaindata +SNAPSHOT=https://mainnet-full-snapshots.base.org/$(curl https://mainnet-full-snapshots.base.org/latest) + +if [ ! -d "$CHAINDATA_DIR" ]; then + wget -c $SNAPSHOT -O - | zstd -cd | tar xf - --strip-components=1 -C $DATA_DIR +fi + +$GETH_BIN \ + --op-network base-mainnet \ + --datadir $DATA_DIR \ + --authrpc.jwtsecret $DATA_DIR/jwtsecret \ + --authrpc.addr 127.0.0.1 \ + --authrpc.port {{.Ports.BackendAuthRpc}} \ + --authrpc.vhosts "*" \ + --port {{.Ports.BackendP2P}} \ + --http \ + --http.port {{.Ports.BackendHttp}} \ + --http.addr 127.0.0.1 \ + --http.api eth,net,web3,debug,txpool,engine \ + --http.vhosts "*" \ + --http.corsdomain "*" \ + --ws \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.addr 127.0.0.1 \ + --ws.api eth,net,web3,debug,txpool,engine \ + --ws.origins "*" \ + --rollup.disabletxpoolgossip=true \ + --rollup.sequencerhttp https://mainnet-sequencer.base.io \ + --state.scheme hash \ + --history.transactions 0 \ + --cache 4096 \ + --syncmode full \ + --maxpeers 0 \ + --nodiscover + +{{end}} diff --git a/build/templates/backend/scripts/base_archive.sh b/build/templates/backend/scripts/base_archive.sh new file mode 100644 index 0000000000..6f344e467e --- /dev/null +++ b/build/templates/backend/scripts/base_archive.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +GETH_BIN={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +CHAINDATA_DIR=$DATA_DIR/geth/chaindata +SNAPSHOT=https://mainnet-full-snapshots.base.org/$(curl https://mainnet-full-snapshots.base.org/latest) + +if [ ! -d "$CHAINDATA_DIR" ]; then + wget -c $SNAPSHOT -O - | zstd -cd | tar xf - --strip-components=1 -C $DATA_DIR +fi + +$GETH_BIN \ + --op-network base-mainnet \ + --datadir $DATA_DIR \ + --authrpc.jwtsecret $DATA_DIR/jwtsecret \ + --authrpc.addr 127.0.0.1 \ + --authrpc.port {{.Ports.BackendAuthRpc}} \ + --authrpc.vhosts "*" \ + --port {{.Ports.BackendP2P}} \ + --http \ + --http.port {{.Ports.BackendHttp}} \ + --http.addr 127.0.0.1 \ + --http.api eth,net,web3,debug,txpool,engine \ + --http.vhosts "*" \ + --http.corsdomain "*" \ + --ws \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.addr 127.0.0.1 \ + --ws.api eth,net,web3,debug,txpool,engine \ + --ws.origins "*" \ + --rollup.disabletxpoolgossip=true \ + --rollup.sequencerhttp https://mainnet.sequencer.optimism.io \ + --cache 4096 \ + --cache.gc 0 \ + --cache.trie 30 \ + --cache.snapshot 20 \ + --syncmode full \ + --gcmode archive \ + --maxpeers 0 \ + --nodiscover + +{{end}} diff --git a/build/templates/backend/scripts/base_archive_op_node.sh b/build/templates/backend/scripts/base_archive_op_node.sh new file mode 100644 index 0000000000..75e122da8e --- /dev/null +++ b/build/templates/backend/scripts/base_archive_op_node.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +BIN={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/op-node + +$BIN \ + --network base-mainnet \ + --l1 http://127.0.0.1:8116 \ + --l1.beacon http://127.0.0.1:7516 \ + --l1.trustrpc \ + --l1.rpckind=debug_geth \ + --l2 http://127.0.0.1:8411 \ + --rpc.addr 127.0.0.1 \ + --rpc.port {{.Ports.BackendRPC}} \ + --l2.jwt-secret {{.Env.BackendDataPath}}/base_archive/backend/jwtsecret \ + --p2p.bootnodes enr:-J24QNz9lbrKbN4iSmmjtnr7SjUMk4zB7f1krHZcTZx-JRKZd0kA2gjufUROD6T3sOWDVDnFJRvqBBo62zuF-hYCohOGAYiOoEyEgmlkgnY0gmlwhAPniryHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQKNVFlCxh_B-716tTs-h1vMzZkSs1FTu_OYTNjgufplG4N0Y3CCJAaDdWRwgiQG,enr:-J24QH-f1wt99sfpHy4c0QJM-NfmsIfmlLAMMcgZCUEgKG_BBYFc6FwYgaMJMQN5dsRBJApIok0jFn-9CS842lGpLmqGAYiOoDRAgmlkgnY0gmlwhLhIgb2Hb3BzdGFja4OFQgCJc2VjcDI1NmsxoQJ9FTIv8B9myn1MWaC_2lJ-sMoeCDkusCsk4BYHjjCq04N0Y3CCJAaDdWRwgiQG,enr:-J24QDXyyxvQYsd0yfsN0cRr1lZ1N11zGTplMNlW4xNEc7LkPXh0NAJ9iSOVdRO95GPYAIc6xmyoCCG6_0JxdL3a0zaGAYiOoAjFgmlkgnY0gmlwhAPckbGHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQJwoS7tzwxqXSyFL7g0JM-KWVbgvjfB8JA__T7yY_cYboN0Y3CCJAaDdWRwgiQG,enr:-J24QHmGyBwUZXIcsGYMaUqGGSl4CFdx9Tozu-vQCn5bHIQbR7On7dZbU61vYvfrJr30t0iahSqhc64J46MnUO2JvQaGAYiOoCKKgmlkgnY0gmlwhAPnCzSHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQINc4fSijfbNIiGhcgvwjsjxVFJHUstK9L1T8OTKUjgloN0Y3CCJAaDdWRwgiQG,enr:-J24QG3ypT4xSu0gjb5PABCmVxZqBjVw9ca7pvsI8jl4KATYAnxBmfkaIuEqy9sKvDHKuNCsy57WwK9wTt2aQgcaDDyGAYiOoGAXgmlkgnY0gmlwhDbGmZaHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQIeAK_--tcLEiu7HvoUlbV52MspE0uCocsx1f_rYvRenIN0Y3CCJAaDdWRwgiQG \ + --p2p.useragent base \ + --rollup.load-protocol-versions=true \ + --verifier.l1-confs 4 + +{{end}} diff --git a/build/templates/backend/scripts/base_op_node.sh b/build/templates/backend/scripts/base_op_node.sh new file mode 100644 index 0000000000..4254b8972e --- /dev/null +++ b/build/templates/backend/scripts/base_op_node.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +BIN={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/op-node + +$BIN \ + --network base-mainnet \ + --l1 http://127.0.0.1:8136 \ + --l1.beacon http://127.0.0.1:7536 \ + --l1.trustrpc \ + --l1.rpckind debug_geth \ + --l2 http://127.0.0.1:8409 \ + --rpc.addr 127.0.0.1 \ + --rpc.port {{.Ports.BackendRPC}} \ + --l2.jwt-secret {{.Env.BackendDataPath}}/base/backend/jwtsecret \ + --p2p.bootnodes enr:-J24QNz9lbrKbN4iSmmjtnr7SjUMk4zB7f1krHZcTZx-JRKZd0kA2gjufUROD6T3sOWDVDnFJRvqBBo62zuF-hYCohOGAYiOoEyEgmlkgnY0gmlwhAPniryHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQKNVFlCxh_B-716tTs-h1vMzZkSs1FTu_OYTNjgufplG4N0Y3CCJAaDdWRwgiQG,enr:-J24QH-f1wt99sfpHy4c0QJM-NfmsIfmlLAMMcgZCUEgKG_BBYFc6FwYgaMJMQN5dsRBJApIok0jFn-9CS842lGpLmqGAYiOoDRAgmlkgnY0gmlwhLhIgb2Hb3BzdGFja4OFQgCJc2VjcDI1NmsxoQJ9FTIv8B9myn1MWaC_2lJ-sMoeCDkusCsk4BYHjjCq04N0Y3CCJAaDdWRwgiQG,enr:-J24QDXyyxvQYsd0yfsN0cRr1lZ1N11zGTplMNlW4xNEc7LkPXh0NAJ9iSOVdRO95GPYAIc6xmyoCCG6_0JxdL3a0zaGAYiOoAjFgmlkgnY0gmlwhAPckbGHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQJwoS7tzwxqXSyFL7g0JM-KWVbgvjfB8JA__T7yY_cYboN0Y3CCJAaDdWRwgiQG,enr:-J24QHmGyBwUZXIcsGYMaUqGGSl4CFdx9Tozu-vQCn5bHIQbR7On7dZbU61vYvfrJr30t0iahSqhc64J46MnUO2JvQaGAYiOoCKKgmlkgnY0gmlwhAPnCzSHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQINc4fSijfbNIiGhcgvwjsjxVFJHUstK9L1T8OTKUjgloN0Y3CCJAaDdWRwgiQG,enr:-J24QG3ypT4xSu0gjb5PABCmVxZqBjVw9ca7pvsI8jl4KATYAnxBmfkaIuEqy9sKvDHKuNCsy57WwK9wTt2aQgcaDDyGAYiOoGAXgmlkgnY0gmlwhDbGmZaHb3BzdGFja4OFQgCJc2VjcDI1NmsxoQIeAK_--tcLEiu7HvoUlbV52MspE0uCocsx1f_rYvRenIN0Y3CCJAaDdWRwgiQG \ + --p2p.useragent base \ + --rollup.load-protocol-versions=true \ + --verifier.l1-confs 4 + +{{end}} diff --git a/build/templates/backend/scripts/bsc.sh b/build/templates/backend/scripts/bsc.sh new file mode 100644 index 0000000000..fdcfbf8035 --- /dev/null +++ b/build/templates/backend/scripts/bsc.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +GETH_BIN=$INSTALL_DIR/geth_linux +CHAINDATA_DIR=$DATA_DIR/geth/chaindata + +if [ ! -d "$CHAINDATA_DIR" ]; then + $GETH_BIN init --datadir $DATA_DIR $INSTALL_DIR/genesis.json +fi + +$GETH_BIN \ + --config $INSTALL_DIR/config.toml \ + --datadir $DATA_DIR \ + --port {{.Ports.BackendP2P}} \ + --http \ + --http.addr 127.0.0.1 \ + --http.port {{.Ports.BackendHttp}} \ + --http.api eth,net,web3,debug,txpool \ + --http.vhosts '*' \ + --http.corsdomain '*' \ + --ws \ + --ws.addr 127.0.0.1 \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.api eth,net,web3,debug,txpool \ + --ws.origins '*' \ + --syncmode full \ + --maxpeers 200 \ + --rpc.allow-unprotected-txs \ + --txlookuplimit 0 \ + --cache 8000 \ + --ipcdisable \ + --nat none + +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/bsc_archive.sh b/build/templates/backend/scripts/bsc_archive.sh new file mode 100644 index 0000000000..17990d9e19 --- /dev/null +++ b/build/templates/backend/scripts/bsc_archive.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +GETH_BIN=$INSTALL_DIR/geth_linux +CHAINDATA_DIR=$DATA_DIR/geth/chaindata + +if [ ! -d "$CHAINDATA_DIR" ]; then + $GETH_BIN init --datadir $DATA_DIR $INSTALL_DIR/genesis.json +fi + +$GETH_BIN \ + --config $INSTALL_DIR/config.toml \ + --datadir $DATA_DIR \ + --port {{.Ports.BackendP2P}} \ + --http \ + --http.addr 127.0.0.1 \ + --http.port {{.Ports.BackendHttp}} \ + --http.api eth,net,web3,debug,txpool \ + --http.vhosts '*' \ + --http.corsdomain '*' \ + --ws \ + --ws.addr 127.0.0.1 \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.api eth,net,web3,debug,txpool \ + --ws.origins '*' \ + --gcmode archive \ + --cache.gc 0 \ + --cache.trie 30 \ + --syncmode full \ + --maxpeers 200 \ + --rpc.allow-unprotected-txs \ + --txlookuplimit 0 \ + --cache 8000 \ + --ipcdisable \ + --nat none + +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/optimism.sh b/build/templates/backend/scripts/optimism.sh new file mode 100644 index 0000000000..faccfe80e5 --- /dev/null +++ b/build/templates/backend/scripts/optimism.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +GETH_BIN={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +CHAINDATA_DIR=$DATA_DIR/geth/chaindata +SNAPSHOT=https://r2-snapshots.fastnode.io/op/$(curl -s https://r2-snapshots.fastnode.io/op/latest-mainnet) + +if [ ! -d "$CHAINDATA_DIR" ]; then + wget -c $SNAPSHOT -O - | lz4 -cd | tar xf - -C $DATA_DIR +fi + +$GETH_BIN \ + --op-network op-mainnet \ + --datadir $DATA_DIR \ + --authrpc.jwtsecret $DATA_DIR/jwtsecret \ + --authrpc.addr 127.0.0.1 \ + --authrpc.port {{.Ports.BackendAuthRpc}} \ + --authrpc.vhosts "*" \ + --port {{.Ports.BackendP2P}} \ + --http \ + --http.port {{.Ports.BackendHttp}} \ + --http.addr 127.0.0.1 \ + --http.api eth,net,web3,debug,txpool,engine \ + --http.vhosts "*" \ + --http.corsdomain "*" \ + --ws \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.addr 127.0.0.1 \ + --ws.api eth,net,web3,debug,txpool,engine \ + --ws.origins "*" \ + --rollup.disabletxpoolgossip=true \ + --rollup.sequencerhttp https://mainnet-sequencer.optimism.io \ + --txlookuplimit 0 \ + --cache 4096 \ + --syncmode full \ + --maxpeers 0 \ + --nodiscover + +{{end}} diff --git a/build/templates/backend/scripts/optimism_archive.sh b/build/templates/backend/scripts/optimism_archive.sh new file mode 100644 index 0000000000..780258841a --- /dev/null +++ b/build/templates/backend/scripts/optimism_archive.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +GETH_BIN={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +CHAINDATA_DIR=$DATA_DIR/geth/chaindata +SNAPSHOT=https://datadirs.optimism.io/latest + +if [ ! -d "$CHAINDATA_DIR" ]; then + wget -c $(curl -sL $SNAPSHOT | grep -oP '(?<=url=)[^"]*') -O - | zstd -cd | tar xf - -C $DATA_DIR +fi + +$GETH_BIN \ + --op-network op-mainnet \ + --datadir $DATA_DIR \ + --authrpc.jwtsecret $DATA_DIR/jwtsecret \ + --authrpc.addr 127.0.0.1 \ + --authrpc.port {{.Ports.BackendAuthRpc}} \ + --authrpc.vhosts "*" \ + --port {{.Ports.BackendP2P}} \ + --http \ + --http.port {{.Ports.BackendHttp}} \ + --http.addr 127.0.0.1 \ + --http.api eth,net,web3,debug,txpool,engine \ + --http.vhosts "*" \ + --http.corsdomain "*" \ + --ws \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.addr 127.0.0.1 \ + --ws.api eth,net,web3,debug,txpool,engine \ + --ws.origins "*" \ + --rollup.disabletxpoolgossip=true \ + --rollup.historicalrpc http://127.0.0.1:8304 \ + --rollup.sequencerhttp https://mainnet.sequencer.optimism.io \ + --cache 4096 \ + --cache.gc 0 \ + --cache.trie 30 \ + --cache.snapshot 20 \ + --syncmode full \ + --gcmode archive \ + --maxpeers 0 \ + --nodiscover + +{{end}} diff --git a/build/templates/backend/scripts/optimism_archive_legacy_geth.sh b/build/templates/backend/scripts/optimism_archive_legacy_geth.sh new file mode 100644 index 0000000000..641da1fe19 --- /dev/null +++ b/build/templates/backend/scripts/optimism_archive_legacy_geth.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +export USING_OVM=true +export ETH1_SYNC_SERVICE_ENABLE=false + +GETH_BIN={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +CHAINDATA_DIR=$DATA_DIR/geth/chaindata +SNAPSHOT=https://datadirs.optimism.io/mainnet-legacy-archival.tar.zst + +if [ ! -d "$CHAINDATA_DIR" ]; then + wget -c $SNAPSHOT -O - | zstd -cd | tar xf - -C $DATA_DIR +fi + +$GETH_BIN \ + --networkid 10 \ + --datadir $DATA_DIR \ + --port {{.Ports.BackendP2P}} \ + --rpc \ + --rpcport {{.Ports.BackendHttp}} \ + --rpcaddr 127.0.0.1 \ + --rpcapi eth,rollup,net,web3,debug \ + --rpcvhosts "*" \ + --rpccorsdomain "*" \ + --ws \ + --wsport {{.Ports.BackendRPC}} \ + --wsaddr 0.0.0.0 \ + --wsapi eth,rollup,net,web3,debug \ + --wsorigins "*" \ + --nousb \ + --ipcdisable \ + --nat=none \ + --nodiscover + +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/optimism_archive_op_node.sh b/build/templates/backend/scripts/optimism_archive_op_node.sh new file mode 100644 index 0000000000..463757032e --- /dev/null +++ b/build/templates/backend/scripts/optimism_archive_op_node.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +BIN={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/op-node +PATH={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +$BIN \ + --network op-mainnet \ + --l1 http://127.0.0.1:8116 \ + --l1.beacon http://127.0.0.1:7516 \ + --l1.trustrpc \ + --l1.rpckind=debug_geth \ + --l2 http://127.0.0.1:8402 \ + --rpc.addr 127.0.0.1 \ + --rpc.port {{.Ports.BackendRPC}} \ + --l2.jwt-secret {{.Env.BackendDataPath}}/optimism_archive/backend/jwtsecret \ + --p2p.priv.path $PATH/opnode_p2p_priv.txt \ + --p2p.peerstore.path $PATH/opnode_peerstore_db \ + --p2p.discovery.path $PATH/opnode_discovery_db + +{{end}} diff --git a/build/templates/backend/scripts/optimism_op_node.sh b/build/templates/backend/scripts/optimism_op_node.sh new file mode 100644 index 0000000000..200c04b687 --- /dev/null +++ b/build/templates/backend/scripts/optimism_op_node.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +BIN={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/op-node +PATH={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +$BIN \ + --network op-mainnet \ + --l1 http://127.0.0.1:8136 \ + --l1.beacon http://127.0.0.1:7536 \ + --l1.trustrpc \ + --l1.rpckind=debug_geth \ + --l2 http://127.0.0.1:8400 \ + --rpc.addr 127.0.0.1 \ + --rpc.port {{.Ports.BackendRPC}} \ + --l2.jwt-secret {{.Env.BackendDataPath}}/optimism/backend/jwtsecret \ + --p2p.priv.path $PATH/opnode_p2p_priv.txt \ + --p2p.peerstore.path $PATH/opnode_peerstore_db \ + --p2p.discovery.path $PATH/opnode_discovery_db + +{{end}} diff --git a/build/templates/backend/scripts/polygon_archive_bor.sh b/build/templates/backend/scripts/polygon_archive_bor.sh new file mode 100644 index 0000000000..340e981cf4 --- /dev/null +++ b/build/templates/backend/scripts/polygon_archive_bor.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +BOR_BIN=$INSTALL_DIR/bor + +if [ -z "${BOR_PEBBLE_DB}" ]; then + ARCHIVE_FLAGS="--gcmode archive --db.engine leveldb --state.scheme hash" +else + ARCHIVE_FLAGS="--db.engine pebble" +fi + +# --bor.heimdall = backend-polygon-heimdall-archive ports.backend_http +$BOR_BIN server \ + --chain $INSTALL_DIR/genesis.json \ + --syncmode full \ + --datadir $DATA_DIR \ + $ARCHIVE_FLAGS \ + --bor.heimdall http://127.0.0.1:8173 \ + --maxpeers 200 \ + --bootnodes enode://76316d1cb93c8ed407d3332d595233401250d48f8fbb1d9c65bd18c0495eca1b43ec38ee0ea1c257c0abb7d1f25d649d359cdfe5a805842159cfe36c5f66b7e8@52.78.36.216:30303,enode://b8f1cc9c5d4403703fbf377116469667d2b1823c0daf16b7250aa576bacf399e42c3930ccfcb02c5df6879565a2b8931335565f0e8d3f8e72385ecf4a4bf160a@3.36.224.80:30303,enode://8729e0c825f3d9cad382555f3e46dcff21af323e89025a0e6312df541f4a9e73abfa562d64906f5e59c51fe6f0501b3e61b07979606c56329c020ed739910759@54.194.245.5:30303,enode://681ebac58d8dd2d8a6eef15329dfbad0ab960561524cf2dfde40ad646736fe5c244020f20b87e7c1520820bc625cfb487dd71d63a3a3bf0baea2dbb8ec7c79f1@34.240.245.39:30303,enode://93faa5d49ba61fa03f43f7e3c76907a9c72953e8628650eef09f5bddc646d9012916824cdd60da989fd954a852205df9a1fd9661379504c92e103a1ada4c2ceb@148.251.142.52:30314,enode://91f6d9873ee2ceee27b4054ec70844e21fa7c525e8d820d6a09989473f4f883951da75a09ef098d544c0c8a71e9ddd2e649e5b455b137260ba8657b2f96cad2c@178.63.148.12:30308,enode://2776f6f0d1c1e4dfddeb9a4b1c3b1a8777fbb3054b92fc55b405d35603667e974e9cad4408f1036cfc17af03dd1a6270c5cb40f854b94760474516b2d8c0f185@88.198.101.172:30308,enode://157321664e79855ee0f914fd05b21cc29ae3a7e805114d1c26efa1d4d2781f5d5bc4e76ed9d00f26d6138f80cc84ea183894c390fcb0e07100a845aed02f6f40@136.243.210.177:30303,enode://6a5e65c6ef3356bc79a780cf0c7534c299fb8cd7b37db80155830478c1e29d35336fe52a888efdf53c0e9bb9b94e20b5349d68798860f1cf36ae96da2b3826cc@178.63.247.234:30304,enode://d6da5ad18e51d492481b29443bd0f588b59d3f72f0da43a722b07fe2a9223a717c976a1cfe00ad86c557756b2bf297ea56c64a1f3d09bebcb9b81290689d8e33@178.63.197.250:30320,enode://51cbc8b750e28d5a4f250d141c032cf282ea873eb1c533c5156cfc51e6a5117d465b7b39b4e0088ee597ee87b89e06cc6c1ed5e6e050b1c3f638765ee584c4f4@178.63.163.68:30310,enode://6484d4394215c222257c97ac74fdcd6f77ecf00e896c38ef35cc41a44add96da64649139b37cc094e88bb985eb84b04d4c6c78f86bf205c9e112c31254cdc443@54.38.217.112:30303?discport=30346,enode://eb3b67d68daef47badfa683c8b04a1cba6a7c431613b8d7619a013aad38bd8d405eb1d0e41279b4f6fe15b264bd388e88282a77a908247b2d1e0198bd4def57b@148.251.224.230:30315,enode://aa228d96217dd91564e13536f3c2808d2040115c7c50509f26f836275e8e65d1bf9400bce3294760be18c9e00a4bf47026e661ba8d8ce1cf2ced30f0a70e5da8@89.187.163.132:30303?discport=30356,enode://c10ab147ba266a80f34dbc423cd12689434cb2cc1f18ced8f4e5828e23d6943a666c2db0f8464983ccc95666b36099b513d1e45d5df94139e42fbecde25832fa@87.249.137.89:30303?discport=30436,enode://e68049c37b182a36c8913fc0780aea5196c1841c917cbd76f83f1a3a8ae99fcfbd2dfa44e36081668120354439008fe4325ffc0d0176771ec2c1863033d4769e@65.108.199.236:30303,enode://a4c74da28447bacd2b3e8443d0917cca7798bca39dbb48b0e210f0fb6685538ba9d1608a2493424086363f04be5e6a99e6eabb70946ed503448d6b282056f87a@198.244.213.85:30303?discport=30315,enode://e28fce95f52cf3368b7b624c6f83379dec858fcebf6a7ff07e97aa9b9445736a165bf1c51cad7bdf6e3167e2b00b11c7911fc330dabb484998d899a1b01d75cf@148.251.194.252:30303?discport=30892,enode://412fdb01125f6868a188f472cf15f07c8f93d606395b909dd5010f2a4a2702739102cea18abb6437fbacd12e695982a77f28edd9bbdd36635b04e9b3c2948f8d@34.203.27.246:30303?discport=30388,enode://9703d9591cb1013b4fa6ea889e8effe7579aa59c324a6e019d690a13e108ef9b4419698347e4305f05291e644a713518a91b0fc32a3442c1394619e2a9b8251e@79.127.216.33:30303?discport=30349 \ + --port {{.Ports.BackendP2P}} \ + --http \ + --http.addr 0.0.0.0 \ + --http.port {{.Ports.BackendHttp}} \ + --http.api eth,net,web3,debug,txpool,bor \ + --http.vhosts '*' \ + --http.corsdomain '*' \ + --ws \ + --ws.addr 0.0.0.0 \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.api eth,net,web3,debug,txpool,bor \ + --ws.origins '*' \ + --txlookuplimit 0 \ + --cache 4096 +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/polygon_archive_heimdall.sh b/build/templates/backend/scripts/polygon_archive_heimdall.sh new file mode 100644 index 0000000000..988956ab6e --- /dev/null +++ b/build/templates/backend/scripts/polygon_archive_heimdall.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +HEIMDALL_BIN=$INSTALL_DIR/heimdalld +HOME_DIR=$DATA_DIR +CONFIG_DIR=$HOME_DIR/config + +if [ ! -d "$CONFIG_DIR" ]; then + # init chain + $HEIMDALL_BIN init $(hostname -s) --home $HOME_DIR --chain-id heimdallv2-137 +fi + +# --bor_rpc_url: backend-polygon-bor-archive ports.backend_http +# --eth_rpc_url: backend-ethereum-archive ports.backend_http +$HEIMDALL_BIN start \ + --home $HOME_DIR \ + --rpc.laddr tcp://127.0.0.1:{{.Ports.BackendRPC}} \ + --p2p.laddr tcp://0.0.0.0:{{.Ports.BackendP2P}} \ + --grpc_server tcp://127.0.0.1:{{.Ports.BackendHttp}} \ + --p2p.seeds "e019e16d4e376723f3adc58eb1761809fea9bee0@35.234.150.253:26656,7f3049e88ac7f820fd86d9120506aaec0dc54b27@34.89.75.187:26656,1f5aff3b4f3193404423c3dd1797ce60cd9fea43@34.142.43.240:26656,2d5484feef4257e56ece025633a6ea132d8cadca@35.246.99.203:26656,17e9efcbd173e81a31579310c502e8cdd8b8ff2e@35.197.233.249:26656,72a83490309f9f63fdca3a0bef16c290e5cbb09c@35.246.95.65:26656,00677b1b2c6282fb060b7bb6e9cc7d2d05cdd599@34.105.180.11:26656,721dd4cebfc4b78760c7ee5d7b1b44d29a0aa854@34.147.169.102:26656,4760b3fc04648522a0bcb2d96a10aadee141ee89@34.89.55.74:26656" \ + --bor_rpc_url http://127.0.0.1:8172 \ + --eth_rpc_url http://127.0.0.1:8116 +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/polygon_bor.sh b/build/templates/backend/scripts/polygon_bor.sh new file mode 100644 index 0000000000..16c110b745 --- /dev/null +++ b/build/templates/backend/scripts/polygon_bor.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +BOR_BIN=$INSTALL_DIR/bor + +# --bor.heimdall = backend-polygon-heimdall ports.backend_http +$BOR_BIN server \ + --chain $INSTALL_DIR/genesis.json \ + --syncmode full \ + --datadir $DATA_DIR \ + --bor.heimdall http://127.0.0.1:8171 \ + --maxpeers 200 \ + --bootnodes enode://76316d1cb93c8ed407d3332d595233401250d48f8fbb1d9c65bd18c0495eca1b43ec38ee0ea1c257c0abb7d1f25d649d359cdfe5a805842159cfe36c5f66b7e8@52.78.36.216:30303,enode://b8f1cc9c5d4403703fbf377116469667d2b1823c0daf16b7250aa576bacf399e42c3930ccfcb02c5df6879565a2b8931335565f0e8d3f8e72385ecf4a4bf160a@3.36.224.80:30303,enode://8729e0c825f3d9cad382555f3e46dcff21af323e89025a0e6312df541f4a9e73abfa562d64906f5e59c51fe6f0501b3e61b07979606c56329c020ed739910759@54.194.245.5:30303,enode://681ebac58d8dd2d8a6eef15329dfbad0ab960561524cf2dfde40ad646736fe5c244020f20b87e7c1520820bc625cfb487dd71d63a3a3bf0baea2dbb8ec7c79f1@34.240.245.39:30303,enode://93faa5d49ba61fa03f43f7e3c76907a9c72953e8628650eef09f5bddc646d9012916824cdd60da989fd954a852205df9a1fd9661379504c92e103a1ada4c2ceb@148.251.142.52:30314,enode://91f6d9873ee2ceee27b4054ec70844e21fa7c525e8d820d6a09989473f4f883951da75a09ef098d544c0c8a71e9ddd2e649e5b455b137260ba8657b2f96cad2c@178.63.148.12:30308,enode://2776f6f0d1c1e4dfddeb9a4b1c3b1a8777fbb3054b92fc55b405d35603667e974e9cad4408f1036cfc17af03dd1a6270c5cb40f854b94760474516b2d8c0f185@88.198.101.172:30308,enode://157321664e79855ee0f914fd05b21cc29ae3a7e805114d1c26efa1d4d2781f5d5bc4e76ed9d00f26d6138f80cc84ea183894c390fcb0e07100a845aed02f6f40@136.243.210.177:30303,enode://6a5e65c6ef3356bc79a780cf0c7534c299fb8cd7b37db80155830478c1e29d35336fe52a888efdf53c0e9bb9b94e20b5349d68798860f1cf36ae96da2b3826cc@178.63.247.234:30304,enode://d6da5ad18e51d492481b29443bd0f588b59d3f72f0da43a722b07fe2a9223a717c976a1cfe00ad86c557756b2bf297ea56c64a1f3d09bebcb9b81290689d8e33@178.63.197.250:30320,enode://51cbc8b750e28d5a4f250d141c032cf282ea873eb1c533c5156cfc51e6a5117d465b7b39b4e0088ee597ee87b89e06cc6c1ed5e6e050b1c3f638765ee584c4f4@178.63.163.68:30310,enode://6484d4394215c222257c97ac74fdcd6f77ecf00e896c38ef35cc41a44add96da64649139b37cc094e88bb985eb84b04d4c6c78f86bf205c9e112c31254cdc443@54.38.217.112:30303?discport=30346,enode://eb3b67d68daef47badfa683c8b04a1cba6a7c431613b8d7619a013aad38bd8d405eb1d0e41279b4f6fe15b264bd388e88282a77a908247b2d1e0198bd4def57b@148.251.224.230:30315,enode://aa228d96217dd91564e13536f3c2808d2040115c7c50509f26f836275e8e65d1bf9400bce3294760be18c9e00a4bf47026e661ba8d8ce1cf2ced30f0a70e5da8@89.187.163.132:30303?discport=30356,enode://c10ab147ba266a80f34dbc423cd12689434cb2cc1f18ced8f4e5828e23d6943a666c2db0f8464983ccc95666b36099b513d1e45d5df94139e42fbecde25832fa@87.249.137.89:30303?discport=30436,enode://e68049c37b182a36c8913fc0780aea5196c1841c917cbd76f83f1a3a8ae99fcfbd2dfa44e36081668120354439008fe4325ffc0d0176771ec2c1863033d4769e@65.108.199.236:30303,enode://a4c74da28447bacd2b3e8443d0917cca7798bca39dbb48b0e210f0fb6685538ba9d1608a2493424086363f04be5e6a99e6eabb70946ed503448d6b282056f87a@198.244.213.85:30303?discport=30315,enode://e28fce95f52cf3368b7b624c6f83379dec858fcebf6a7ff07e97aa9b9445736a165bf1c51cad7bdf6e3167e2b00b11c7911fc330dabb484998d899a1b01d75cf@148.251.194.252:30303?discport=30892,enode://412fdb01125f6868a188f472cf15f07c8f93d606395b909dd5010f2a4a2702739102cea18abb6437fbacd12e695982a77f28edd9bbdd36635b04e9b3c2948f8d@34.203.27.246:30303?discport=30388,enode://9703d9591cb1013b4fa6ea889e8effe7579aa59c324a6e019d690a13e108ef9b4419698347e4305f05291e644a713518a91b0fc32a3442c1394619e2a9b8251e@79.127.216.33:30303?discport=30349 \ + --port {{.Ports.BackendP2P}} \ + --http \ + --http.addr 127.0.0.1 \ + --http.port {{.Ports.BackendHttp}} \ + --http.api eth,net,web3,debug,txpool,bor \ + --http.vhosts '*' \ + --http.corsdomain '*' \ + --ws \ + --ws.addr 127.0.0.1 \ + --ws.port {{.Ports.BackendRPC}} \ + --ws.api eth,net,web3,debug,txpool,bor \ + --ws.origins '*' \ + --txlookuplimit 0 \ + --cache 4096 + +{{end}} \ No newline at end of file diff --git a/build/templates/backend/scripts/polygon_heimdall.sh b/build/templates/backend/scripts/polygon_heimdall.sh new file mode 100644 index 0000000000..c267c1bbed --- /dev/null +++ b/build/templates/backend/scripts/polygon_heimdall.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +{{define "main" -}} + +set -e + +INSTALL_DIR={{.Env.BackendInstallPath}}/{{.Coin.Alias}} +DATA_DIR={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend + +HEIMDALL_BIN=$INSTALL_DIR/heimdalld +HOME_DIR=$DATA_DIR +CONFIG_DIR=$HOME_DIR/config + +if [ ! -d "$CONFIG_DIR" ]; then + # init chain + $HEIMDALL_BIN init $(hostname -s) --home $HOME_DIR --chain-id heimdallv2-137 +fi + +# --bor_rpc_url: backend-polygon-bor ports.backend_http +# --eth_rpc_url: backend-ethereum ports.backend_http +$HEIMDALL_BIN start \ + --home $HOME_DIR \ + --rpc.laddr tcp://127.0.0.1:{{.Ports.BackendRPC}} \ + --p2p.laddr tcp://0.0.0.0:{{.Ports.BackendP2P}} \ + --grpc_server tcp://127.0.0.1:{{.Ports.BackendHttp}} \ + --p2p.seeds "e019e16d4e376723f3adc58eb1761809fea9bee0@35.234.150.253:26656,7f3049e88ac7f820fd86d9120506aaec0dc54b27@34.89.75.187:26656,1f5aff3b4f3193404423c3dd1797ce60cd9fea43@34.142.43.240:26656,2d5484feef4257e56ece025633a6ea132d8cadca@35.246.99.203:26656,17e9efcbd173e81a31579310c502e8cdd8b8ff2e@35.197.233.249:26656,72a83490309f9f63fdca3a0bef16c290e5cbb09c@35.246.95.65:26656,00677b1b2c6282fb060b7bb6e9cc7d2d05cdd599@34.105.180.11:26656,721dd4cebfc4b78760c7ee5d7b1b44d29a0aa854@34.147.169.102:26656,4760b3fc04648522a0bcb2d96a10aadee141ee89@34.89.55.74:26656" \ + --bor_rpc_url http://127.0.0.1:8170 \ + --eth_rpc_url http://127.0.0.1:8136 +{{end}} \ No newline at end of file diff --git a/build/templates/blockbook/blockchaincfg.json b/build/templates/blockbook/blockchaincfg.json index 525937c5be..7d8fe75cee 100644 --- a/build/templates/blockbook/blockchaincfg.json +++ b/build/templates/blockbook/blockchaincfg.json @@ -7,6 +7,8 @@ {{end}} "coin_name": "{{.Coin.Name}}", "coin_shortcut": "{{.Coin.Shortcut}}", +{{- if .Coin.Network}} + "network": "{{.Coin.Network}}",{{end}} "coin_label": "{{.Coin.Label}}", "rpc_url": "{{template "IPC.RPCURLTemplate" .}}", "rpc_user": "{{.IPC.RPCUser}}", diff --git a/build/templates/blockbook/debian/control b/build/templates/blockbook/debian/control index e596de0142..9185723a52 100644 --- a/build/templates/blockbook/debian/control +++ b/build/templates/blockbook/debian/control @@ -8,6 +8,6 @@ Standards-Version: 3.9.5 Package: {{.Blockbook.PackageName}} Architecture: {{.Env.Architecture}} -Depends: ${shlibs:Depends}, ${misc:Depends}, coreutils, passwd, findutils, psmisc, {{.Backend.PackageName}} +Depends: ${shlibs:Depends}, ${misc:Depends}, coreutils, passwd, findutils, psmisc Description: Satoshilabs blockbook server ({{.Coin.Name}}) {{end}} diff --git a/build/tools/templates.go b/build/tools/templates.go index 42b16c03ca..03113d2a1b 100644 --- a/build/tools/templates.go +++ b/build/tools/templates.go @@ -21,11 +21,13 @@ type Backend struct { SystemUser string `json:"system_user"` Version string `json:"version"` BinaryURL string `json:"binary_url"` + DockerImage string `json:"docker_image"` 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"` + ExecScript string `json:"exec_script"` LogrotateFilesTemplate string `json:"logrotate_files_template"` PostinstScriptTemplate string `json:"postinst_script_template"` ServiceType string `json:"service_type"` @@ -43,6 +45,7 @@ type Config struct { Coin struct { Name string `json:"name"` Shortcut string `json:"shortcut"` + Network string `json:"network,omitempty"` Label string `json:"label"` Alias string `json:"alias"` } `json:"coin"` @@ -202,6 +205,7 @@ func LoadConfig(configsDir, coin string) (*Config, error) { case "gpg": case "sha256": case "gpg-sha256": + case "docker": default: return nil, fmt.Errorf("Invalid verification type: %s", config.Backend.VerificationType) } @@ -280,11 +284,15 @@ func GeneratePackageDefinitions(config *Config, templateDir, outputDir string) e } if !isEmpty(config, "backend") { - err = writeBackendServerConfigFile(config, outputDir) - if err == nil { - err = writeBackendClientConfigFile(config, outputDir) + if err := writeBackendServerConfigFile(config, outputDir); err != nil { + return err } - if err != nil { + + if err := writeBackendClientConfigFile(config, outputDir); err != nil { + return err + } + + if err := writeBackendExecScript(config, outputDir); err != nil { return err } } @@ -354,3 +362,24 @@ func writeBackendClientConfigFile(config *Config, outputDir string) error { _, err = io.Copy(out, in) return err } + +func writeBackendExecScript(config *Config, outputDir string) error { + if config.Backend.ExecScript == "" { + return nil + } + + out, err := os.OpenFile(filepath.Join(outputDir, "backend/exec.sh"), os.O_CREATE|os.O_WRONLY, 0777) + if err != nil { + return err + } + defer out.Close() + + in, err := os.Open(filepath.Join(outputDir, "backend/scripts", config.Backend.ExecScript)) + if err != nil { + return err + } + defer in.Close() + + _, 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 f4e90ba14e..acb5518e39 100644 --- a/build/tools/trezor-common/sync-coins.go +++ b/build/tools/trezor-common/sync-coins.go @@ -1,4 +1,4 @@ -//usr/bin/go run $0 $@ ; exit +// usr/bin/go run $0 $@ ; exit package main import ( diff --git a/build/tools/typescriptify/typescriptify.go b/build/tools/typescriptify/typescriptify.go new file mode 100644 index 0000000000..731c8ff230 --- /dev/null +++ b/build/tools/typescriptify/typescriptify.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "math/big" + "time" + + "github.com/tkrajina/typescriptify-golang-structs/typescriptify" + "github.com/trezor/blockbook/api" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/server" +) + +func main() { + t := typescriptify.New() + t.CreateInterface = true + t.Indent = " " + t.BackupDir = "" + + t.ManageType(api.Amount{}, typescriptify.TypeOptions{TSType: "string"}) + t.ManageType([]api.Amount{}, typescriptify.TypeOptions{TSType: "string[]"}) + t.ManageType([]*api.Amount{}, typescriptify.TypeOptions{TSType: "string[]"}) + t.ManageType(big.Int{}, typescriptify.TypeOptions{TSType: "number"}) + t.ManageType(time.Time{}, typescriptify.TypeOptions{TSType: "string", TSDoc: "Time in ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZd"}) + + // API - REST and Websocket + t.Add(api.APIError{}) + t.Add(api.Tx{}) + t.Add(api.FeeStats{}) + t.Add(api.Address{}) + t.Add(api.Utxo{}) + t.Add(api.BalanceHistory{}) + t.Add(api.Blocks{}) + t.Add(api.Block{}) + t.Add(api.BlockRaw{}) + t.Add(api.SystemInfo{}) + t.Add(api.FiatTicker{}) + t.Add(api.FiatTickers{}) + t.Add(api.AvailableVsCurrencies{}) + + // Websocket specific + t.Add(server.WsReq{}) + t.Add(server.WsRes{}) + t.Add(server.WsAccountInfoReq{}) + t.Add(server.WsInfoRes{}) + t.Add(server.WsBlockHashReq{}) + t.Add(server.WsBlockHashRes{}) + t.Add(server.WsBlockReq{}) + t.Add(server.WsBlockFilterReq{}) + t.Add(server.WsBlockFiltersBatchReq{}) + t.Add(server.WsAccountUtxoReq{}) + t.Add(server.WsBalanceHistoryReq{}) + t.Add(server.WsTransactionReq{}) + t.Add(server.WsTransactionSpecificReq{}) + t.Add(server.WsEstimateFeeReq{}) + t.Add(server.WsEstimateFeeRes{}) + t.Add(server.WsLongTermFeeRateRes{}) + t.Add(server.WsSendTransactionReq{}) + t.Add(server.WsSubscribeAddressesReq{}) + t.Add(server.WsSubscribeFiatRatesReq{}) + t.Add(server.WsCurrentFiatRatesReq{}) + t.Add(server.WsFiatRatesForTimestampsReq{}) + t.Add(server.WsFiatRatesTickersListReq{}) + t.Add(server.WsMempoolFiltersReq{}) + t.Add(server.WsRpcCallReq{}) + t.Add(server.WsRpcCallRes{}) + t.Add(bchain.MempoolTxidFilterEntries{}) + + err := t.ConvertToFile("blockbook-api.ts") + if err != nil { + panic(err.Error()) + } + fmt.Println("OK") +} diff --git a/common/config.go b/common/config.go new file mode 100644 index 0000000000..2252b602d3 --- /dev/null +++ b/common/config.go @@ -0,0 +1,42 @@ +package common + +import ( + "encoding/json" + "os" + + "github.com/juju/errors" +) + +// Config struct +type Config struct { + CoinName string `json:"coin_name"` + CoinShortcut string `json:"coin_shortcut"` + CoinLabel string `json:"coin_label"` + Network string `json:"network"` + FourByteSignatures string `json:"fourByteSignatures"` + FiatRates string `json:"fiat_rates"` + FiatRatesParams string `json:"fiat_rates_params"` + FiatRatesVsCurrencies string `json:"fiat_rates_vs_currencies"` + BlockGolombFilterP uint8 `json:"block_golomb_filter_p"` + BlockFilterScripts string `json:"block_filter_scripts"` + BlockFilterUseZeroedKey bool `json:"block_filter_use_zeroed_key"` +} + +// GetConfig loads and parses the config file and returns Config struct +func GetConfig(configFile string) (*Config, error) { + if configFile == "" { + return nil, errors.New("Missing blockchaincfg configuration parameter") + } + + configFileContent, err := os.ReadFile(configFile) + if err != nil { + return nil, errors.Errorf("Error reading file %v, %v", configFile, err) + } + + var cn Config + err = json.Unmarshal(configFileContent, &cn) + if err != nil { + return nil, errors.Annotatef(err, "Error parsing config file ") + } + return &cn, nil +} diff --git a/common/currencyrateticker.go b/common/currencyrateticker.go index f69fc25e76..2c1afe534c 100644 --- a/common/currencyrateticker.go +++ b/common/currencyrateticker.go @@ -7,9 +7,9 @@ import ( // CurrencyRatesTicker contains coin ticker data fetched from API type CurrencyRatesTicker struct { - Timestamp time.Time `json:"timestamp"` // return as unix timestamp in API - Rates map[string]float32 `json:"rates"` // rates of the base currency against a list of vs currencies - TokenRates map[string]float32 `json:"tokenRates"` // rates of the tokens (identified by the address of the contract) against the base currency + Timestamp time.Time `json:"timestamp"` // return as unix timestamp in API + Rates map[string]float32 `json:"rates"` // rates of the base currency against a list of vs currencies + TokenRates map[string]float32 `json:"tokenRates,omitempty"` // rates of the tokens (identified by the address of the contract) against the base currency } var ( @@ -57,7 +57,7 @@ func (t *CurrencyRatesTicker) ConvertTokenToBase(value float64, token string) fl return 0 } -// ConvertTokenToBase converts token value to toCurrency currency +// ConvertToken converts token value to toCurrency currency func (t *CurrencyRatesTicker) ConvertToken(value float64, token string, toCurrency string) float64 { baseValue := t.ConvertTokenToBase(value, token) if baseValue > 0 { diff --git a/common/internalstate.go b/common/internalstate.go index 3e65d9a38e..5fb5273809 100644 --- a/common/internalstate.go +++ b/common/internalstate.go @@ -2,6 +2,7 @@ package common import ( "encoding/json" + "slices" "sort" "sync" "sync/atomic" @@ -23,76 +24,92 @@ var inShutdown int32 // InternalStateColumn contains the data of a db column type InternalStateColumn struct { - Name string `json:"name"` - Version uint32 `json:"version"` - Rows int64 `json:"rows"` - KeyBytes int64 `json:"keyBytes"` - ValueBytes int64 `json:"valueBytes"` - Updated time.Time `json:"updated"` + Name string `json:"name" ts_doc:"Name of the database column."` + Version uint32 `json:"version" ts_doc:"Version or schema version of the column."` + Rows int64 `json:"rows" ts_doc:"Number of rows stored in this column."` + KeyBytes int64 `json:"keyBytes" ts_doc:"Total size (in bytes) of keys stored in this column."` + ValueBytes int64 `json:"valueBytes" ts_doc:"Total size (in bytes) of values stored in this column."` + Updated time.Time `json:"updated" ts_doc:"Timestamp of the last update to this column."` } // 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"` - ConsensusVersion string `json:"consensus_version,omitempty"` - Consensus interface{} `json:"consensus,omitempty"` + BackendError string `json:"error,omitempty" ts_doc:"Error message if something went wrong in the backend."` + Chain string `json:"chain,omitempty" ts_doc:"Name of the chain - e.g. 'main'."` + Blocks int `json:"blocks,omitempty" ts_doc:"Number of fully verified blocks in the chain."` + Headers int `json:"headers,omitempty" ts_doc:"Number of block headers in the chain."` + BestBlockHash string `json:"bestBlockHash,omitempty" ts_doc:"Hash of the best block in hex."` + Difficulty string `json:"difficulty,omitempty" ts_doc:"Current difficulty of the network."` + SizeOnDisk int64 `json:"sizeOnDisk,omitempty" ts_doc:"Size of the blockchain data on disk in bytes."` + Version string `json:"version,omitempty" ts_doc:"Version of the blockchain backend - e.g. '280000'."` + Subversion string `json:"subversion,omitempty" ts_doc:"Subversion of the blockchain backend - e.g. '/Satoshi:28.0.0/'."` + ProtocolVersion string `json:"protocolVersion,omitempty" ts_doc:"Protocol version of the blockchain backend - e.g. '70016'."` + Timeoffset float64 `json:"timeOffset,omitempty" ts_doc:"Time offset (in seconds) reported by the backend."` + Warnings string `json:"warnings,omitempty" ts_doc:"Any warnings given by the backend regarding the chain state."` + ConsensusVersion string `json:"consensus_version,omitempty" ts_doc:"Version or details of the consensus protocol in use."` + Consensus interface{} `json:"consensus,omitempty" ts_doc:"Additional chain-specific consensus data."` } // InternalState contains the data of the internal state type InternalState struct { - mux sync.Mutex + mux sync.Mutex `ts_doc:"Mutex for synchronized access to the internal state."` - Coin string `json:"coin"` - CoinShortcut string `json:"coinShortcut"` - CoinLabel string `json:"coinLabel"` - Host string `json:"host"` + Coin string `json:"coin" ts_doc:"Coin name (e.g. 'Bitcoin')."` + CoinShortcut string `json:"coinShortcut" ts_doc:"Short code for the coin (e.g. 'BTC')."` + CoinLabel string `json:"coinLabel" ts_doc:"Human-readable label for the coin (e.g. 'Bitcoin main')."` + Host string `json:"host" ts_doc:"Hostname of the node or backend."` + Network string `json:"network,omitempty" ts_doc:"Network name if different from CoinShortcut (e.g. 'testnet')."` - DbState uint32 `json:"dbState"` + DbState uint32 `json:"dbState" ts_doc:"State of the database (closed=0, open=1, inconsistent=2)."` + ExtendedIndex bool `json:"extendedIndex" ts_doc:"Indicates if an extended indexing strategy is used."` - LastStore time.Time `json:"lastStore"` + LastStore time.Time `json:"lastStore" ts_doc:"Time when the internal state was last stored/persisted."` // true if application is with flag --sync - SyncMode bool `json:"syncMode"` + SyncMode bool `json:"syncMode" ts_doc:"Flag indicating if the node is in sync mode."` - InitialSync bool `json:"initialSync"` - IsSynchronized bool `json:"isSynchronized"` - BestHeight uint32 `json:"bestHeight"` - LastSync time.Time `json:"lastSync"` - BlockTimes []uint32 `json:"-"` - AvgBlockPeriod uint32 `json:"-"` + InitialSync bool `json:"initialSync" ts_doc:"If true, the system is in the initial sync phase."` + IsSynchronized bool `json:"isSynchronized" ts_doc:"If true, the main index is fully synced to BestHeight."` + BestHeight uint32 `json:"bestHeight" ts_doc:"Current best block height known to the indexer."` + StartSync time.Time `json:"-" ts_doc:"Timestamp when sync started (not exposed via JSON)."` + LastSync time.Time `json:"lastSync" ts_doc:"Timestamp of the last successful sync."` + BlockTimes []uint32 `json:"-" ts_doc:"List of block timestamps (per height) for calculating historical stats (not exposed via JSON)."` + AvgBlockPeriod uint32 `json:"-" ts_doc:"Average time (in seconds) per block for the last 100 blocks (not exposed via JSON)."` - IsMempoolSynchronized bool `json:"isMempoolSynchronized"` - MempoolSize int `json:"mempoolSize"` - LastMempoolSync time.Time `json:"lastMempoolSync"` + IsMempoolSynchronized bool `json:"isMempoolSynchronized" ts_doc:"If true, mempool data is in sync."` + MempoolSize int `json:"mempoolSize" ts_doc:"Number of transactions in the current mempool."` + LastMempoolSync time.Time `json:"lastMempoolSync" ts_doc:"Timestamp of the last mempool sync."` - DbColumns []InternalStateColumn `json:"dbColumns"` + DbColumns []InternalStateColumn `json:"dbColumns" ts_doc:"List of database column statistics."` - UtxoChecked bool `json:"utxoChecked"` + HasFiatRates bool `json:"-" ts_doc:"True if fiat rates are supported (not exposed via JSON)."` + HasTokenFiatRates bool `json:"-" ts_doc:"True if token fiat rates are supported (not exposed via JSON)."` + HistoricalFiatRatesTime time.Time `json:"historicalFiatRatesTime" ts_doc:"Timestamp of the last historical fiat rates update."` + HistoricalTokenFiatRatesTime time.Time `json:"historicalTokenFiatRatesTime" ts_doc:"Timestamp of the last historical token fiat rates update."` - HasFiatRates bool `json:"-"` - HasTokenFiatRates bool `json:"-"` - HistoricalFiatRatesTime time.Time `json:"historicalFiatRatesTime"` - HistoricalTokenFiatRatesTime time.Time `json:"historicalTokenFiatRatesTime"` - CurrentTicker *CurrencyRatesTicker `json:"currentTicker"` + EnableSubNewTx bool `json:"-" ts_doc:"Internal flag controlling subscription to new transactions (not exposed)."` - BackendInfo BackendInfo `json:"-"` + BackendInfo BackendInfo `json:"-" ts_doc:"Information about the connected blockchain backend (not exposed in JSON)."` + + // database migrations + UtxoChecked bool `json:"utxoChecked" ts_doc:"Indicates if UTXO consistency checks have been performed."` + SortedAddressContracts bool `json:"sortedAddressContracts" ts_doc:"Indicates if address/contract sorting has been completed."` + + // golomb filter settings + BlockGolombFilterP uint8 `json:"block_golomb_filter_p" ts_doc:"Parameter P for building Golomb-Rice filters for blocks."` + BlockFilterScripts string `json:"block_filter_scripts" ts_doc:"Scripts included in block filters (e.g., 'p2pkh,p2sh')."` + BlockFilterUseZeroedKey bool `json:"block_filter_use_zeroed_key" ts_doc:"If true, uses a zeroed key for building block filters."` + + // allowed number of fetched accounts over websocket + WsGetAccountInfoLimit int `json:"-" ts_doc:"Limit of how many getAccountInfo calls can be made via WS (not exposed)."` + WsLimitExceedingIPs map[string]int `json:"-" ts_doc:"Tracks IP addresses exceeding the WS limit (not exposed)."` } // StartedSync signals start of synchronization func (is *InternalState) StartedSync() { is.mux.Lock() defer is.mux.Unlock() + is.StartSync = time.Now().UTC() is.IsSynchronized = false } @@ -102,7 +119,7 @@ func (is *InternalState) FinishedSync(bestHeight uint32) { defer is.mux.Unlock() is.IsSynchronized = true is.BestHeight = bestHeight - is.LastSync = time.Now() + is.LastSync = time.Now().UTC() } // UpdateBestHeight sets new best height, without changing IsSynchronized flag @@ -110,7 +127,7 @@ func (is *InternalState) UpdateBestHeight(bestHeight uint32) { is.mux.Lock() defer is.mux.Unlock() is.BestHeight = bestHeight - is.LastSync = time.Now() + is.LastSync = time.Now().UTC() } // FinishedSyncNoChange marks end of synchronization in case no index update was necessary, it does not update lastSync time @@ -121,10 +138,10 @@ func (is *InternalState) FinishedSyncNoChange() { } // GetSyncState gets the state of synchronization -func (is *InternalState) GetSyncState() (bool, uint32, time.Time) { +func (is *InternalState) GetSyncState() (bool, uint32, time.Time, time.Time) { is.mux.Lock() defer is.mux.Unlock() - return is.IsSynchronized, is.BestHeight, is.LastSync + return is.IsSynchronized, is.BestHeight, is.LastSync, is.StartSync } // StartedMempoolSync signals start of mempool synchronization @@ -186,7 +203,7 @@ func (is *InternalState) GetDBColumnStatValues(c int) (int64, int64, int64) { func (is *InternalState) GetAllDBColumnStats() []InternalStateColumn { is.mux.Lock() defer is.mux.Unlock() - return append(is.DbColumns[:0:0], is.DbColumns...) + return slices.Clone(is.DbColumns) } // DBSizeTotal sums the computed sizes of all columns @@ -224,17 +241,29 @@ func (is *InternalState) GetLastBlockTime() uint32 { func (is *InternalState) SetBlockTimes(blockTimes []uint32) uint32 { is.mux.Lock() defer is.mux.Unlock() - is.BlockTimes = blockTimes + if len(is.BlockTimes) < len(blockTimes) { + // no new block was set + is.BlockTimes = blockTimes + } else { + copy(is.BlockTimes, blockTimes) + } is.computeAvgBlockPeriod() glog.Info("set ", len(is.BlockTimes), " block times, average block period ", is.AvgBlockPeriod, "s") return is.AvgBlockPeriod } -// AppendBlockTime appends block time to BlockTimes, returns AvgBlockPeriod -func (is *InternalState) AppendBlockTime(time uint32) uint32 { +// SetBlockTime sets block time to BlockTimes, allocating the slice as necessary, returns AvgBlockPeriod +func (is *InternalState) SetBlockTime(height uint32, time uint32) uint32 { is.mux.Lock() defer is.mux.Unlock() - is.BlockTimes = append(is.BlockTimes, time) + if int(height) >= len(is.BlockTimes) { + extend := int(height) - len(is.BlockTimes) + 1 + for i := 0; i < extend; i++ { + is.BlockTimes = append(is.BlockTimes, time) + } + } else { + is.BlockTimes[height] = time + } is.computeAvgBlockPeriod() return is.AvgBlockPeriod } @@ -290,6 +319,15 @@ func (is *InternalState) computeAvgBlockPeriod() { is.AvgBlockPeriod = (is.BlockTimes[last] - is.BlockTimes[first]) / avgBlockPeriodSample } +// GetNetwork returns network. If not set returns the same value as CoinShortcut +func (is *InternalState) GetNetwork() string { + network := is.Network + if network == "" { + return is.CoinShortcut + } + return network +} + // SetBackendInfo sets new BackendInfo func (is *InternalState) SetBackendInfo(bi *BackendInfo) { is.mux.Lock() @@ -312,24 +350,6 @@ func (is *InternalState) Pack() ([]byte, error) { return json.Marshal(is) } -// GetCurrentTicker returns current ticker -func (is *InternalState) GetCurrentTicker(vsCurrency string, token string) *CurrencyRatesTicker { - is.mux.Lock() - currentTicker := is.CurrentTicker - is.mux.Unlock() - if currentTicker != nil && IsSuitableTicker(currentTicker, vsCurrency, token) { - return currentTicker - } - return nil -} - -// SetCurrentTicker sets current ticker -func (is *InternalState) SetCurrentTicker(t *CurrencyRatesTicker) { - is.mux.Lock() - defer is.mux.Unlock() - is.CurrentTicker = t -} - // UnpackInternalState unmarshals internal state from json func UnpackInternalState(buf []byte) (*InternalState, error) { var is InternalState @@ -348,3 +368,15 @@ func SetInShutdown() { func IsInShutdown() bool { return atomic.LoadInt32(&inShutdown) != 0 } + +func (is *InternalState) AddWsLimitExceedingIP(ip string) { + is.mux.Lock() + defer is.mux.Unlock() + is.WsLimitExceedingIPs[ip] = is.WsLimitExceedingIPs[ip] + 1 +} + +func (is *InternalState) ResetWsLimitExceedingIPs() { + is.mux.Lock() + defer is.mux.Unlock() + is.WsLimitExceedingIPs = make(map[string]int) +} diff --git a/common/jsonnumber.go b/common/jsonnumber.go index d209fbe29b..d6eab76c08 100644 --- a/common/jsonnumber.go +++ b/common/jsonnumber.go @@ -6,7 +6,9 @@ import ( ) // 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 +// +// 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 diff --git a/common/metrics.go b/common/metrics.go index 1769cbf1d5..0cb1ec4561 100644 --- a/common/metrics.go +++ b/common/metrics.go @@ -35,6 +35,7 @@ type Metrics struct { WebsocketPendingRequests *prometheus.GaugeVec SocketIOPendingRequests *prometheus.GaugeVec XPubCacheSize prometheus.Gauge + CoingeckoRequests *prometheus.CounterVec } // Labels represents a collection of label name -> value mappings. @@ -71,7 +72,7 @@ func GetMetrics(coin string) (*Metrics, error) { prometheus.HistogramOpts{ Name: "blockbook_socketio_req_duration", Help: "Socketio request duration by method (in microseconds)", - Buckets: []float64{1, 5, 10, 25, 50, 75, 100, 250}, + Buckets: []float64{10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_0000_000}, ConstLabels: Labels{"coin": coin}, }, []string{"method"}, @@ -103,7 +104,7 @@ func GetMetrics(coin string) (*Metrics, error) { prometheus.HistogramOpts{ Name: "blockbook_websocket_req_duration", Help: "Websocket request duration by method (in microseconds)", - Buckets: []float64{1, 5, 10, 25, 50, 75, 100, 250}, + Buckets: []float64{10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_0000_000}, ConstLabels: Labels{"coin": coin}, }, []string{"method"}, @@ -112,7 +113,7 @@ func GetMetrics(coin string) (*Metrics, error) { prometheus.HistogramOpts{ Name: "blockbook_index_resync_duration", Help: "Duration of index resync operation (in milliseconds)", - Buckets: []float64{50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 600, 700, 1000, 2000, 5000}, + Buckets: []float64{10, 100, 500, 1000, 2000, 5000, 10000}, ConstLabels: Labels{"coin": coin}, }, ) @@ -120,7 +121,7 @@ func GetMetrics(coin string) (*Metrics, error) { prometheus.HistogramOpts{ Name: "blockbook_mempool_resync_duration", Help: "Duration of mempool resync operation (in milliseconds)", - Buckets: []float64{10, 25, 50, 75, 100, 150, 250, 500, 750, 1000, 2000, 5000}, + Buckets: []float64{10, 100, 500, 1000, 2000, 5000, 10000}, ConstLabels: Labels{"coin": coin}, }, ) @@ -255,6 +256,14 @@ func GetMetrics(coin string) (*Metrics, error) { ConstLabels: Labels{"coin": coin}, }, ) + metrics.CoingeckoRequests = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "blockbook_coingecko_requests", + Help: "Total number of requests to coingecko", + ConstLabels: Labels{"coin": coin}, + }, + []string{"endpoint", "status"}, + ) v := reflect.ValueOf(metrics) for i := 0; i < v.NumField(); i++ { diff --git a/common/utils.go b/common/utils.go index bfe8980bf0..4dee4686e0 100644 --- a/common/utils.go +++ b/common/utils.go @@ -1,7 +1,14 @@ package common import ( + "encoding/json" + "io" + "math" + "runtime/debug" "time" + + "github.com/golang/glog" + "github.com/juju/errors" ) // TickAndDebounce calls function f on trigger channel or with tickTime period (whatever is sooner) with debounce @@ -39,3 +46,63 @@ Loop: } } } + +// SafeDecodeResponseFromReader reads from io.ReadCloser safely, with recovery from panic +func SafeDecodeResponseFromReader(body io.ReadCloser, res interface{}) (err error) { + var data []byte + defer func() { + if r := recover(); r != nil { + glog.Error("unmarshal json recovered from panic: ", r, "; data: ", string(data)) + debug.PrintStack() + if len(data) > 0 && len(data) < 2048 { + err = errors.Errorf("Error: %v", string(data)) + } else { + err = errors.New("Internal error") + } + } + }() + data, err = io.ReadAll(body) + if err != nil { + return err + } + return json.Unmarshal(data, &res) +} + +// RoundToSignificantDigits rounds a float64 number `n` to the specified number of significant figures `digits`. +// For example, RoundToSignificantDigits(1234, 3) returns 1230 +// +// This function works by shifting the number's decimal point to make the desired significant figures +// into whole numbers, rounding, and then shifting back. +// +// Example for n = 1234, digits = 3: +// +// log10(1234) ≈ 3.09 → ceil = 4 +// power = 3 - 4 = -1 +// magnitude = 10^-1 = 0.1 +// n * magnitude = 1234 * 0.1 = 123.4 +// round(123.4) = 123 +// 123 / 0.1 = 1230 +// +// Returns the number rounded to the desired number of significant figures. +func RoundToSignificantDigits(n float64, digits int) float64 { + if n == 0 { + return 0 + } + + // Step 1: Compute how many digits are before the decimal point. + // For 1234 → log10(1234) ≈ 3.09 → ceil = 4 + d := math.Ceil(math.Log10(math.Abs(n))) + + // Step 2: Calculate how much we need to shift the number to bring + // the significant digits into the integer part. + // For digits=3 and d=4 → power = -1 + power := digits - int(d) + + // Step 3: Compute 10^power to scale the number + // 10^-1 = 0.1 + magnitude := math.Pow(10, float64(power)) + + // Step 4: Scale, round, and scale back + // 1234 * 0.1 = 123.4 → round = 123 → 123 / 0.1 = 1230 + return math.Round(n*magnitude) / magnitude +} diff --git a/common/utils_test.go b/common/utils_test.go new file mode 100644 index 0000000000..6076742030 --- /dev/null +++ b/common/utils_test.go @@ -0,0 +1,44 @@ +//go:build unittest + +package common + +import ( + "math" + "strconv" + "testing" +) + +func Test_RoundToSignificantDigits(t *testing.T) { + type testCase struct { + input float64 + digits int + want float64 + } + + tests := []testCase{ + {input: 1234.5678, digits: 3, want: 1230}, + {input: 1234.5678, digits: 4, want: 1235}, + {input: 1234.5678, digits: 5, want: 1234.6}, + {input: 0.0123456, digits: 3, want: 0.0123}, + {input: 98765.4321, digits: 3, want: 98800}, + {input: 1.99999, digits: 3, want: 2.00}, + {input: 999.999, digits: 3, want: 1000}, + {input: 0.0006789, digits: 3, want: 0.000679}, + {input: 5.123456, digits: 3, want: 5.12}, + {input: 4.456789, digits: 3, want: 4.46}, + {input: 3.789012, digits: 3, want: 3.79}, + {input: 2.012345, digits: 3, want: 2.01}, + } + + for _, tt := range tests { + t.Run(strconv.FormatFloat(tt.input, 'f', -1, 64), func(t *testing.T) { + got := RoundToSignificantDigits(tt.input, tt.digits) + + // Use relative epsilon for float comparison + epsilon := 1e-9 + if math.Abs(got-tt.want) > epsilon { + t.Errorf("RoundToSignificantDigits(%v, %d) = %v, want %v", tt.input, tt.digits, got, tt.want) + } + }) + } +} diff --git a/configs/coins/arbitrum.json b/configs/coins/arbitrum.json new file mode 100644 index 0000000000..26cf935100 --- /dev/null +++ b/configs/coins/arbitrum.json @@ -0,0 +1,66 @@ +{ + "coin": { + "name": "Arbitrum", + "shortcut": "ETH", + "network": "ARB", + "label": "Arbitrum", + "alias": "arbitrum" + }, + "ports": { + "backend_rpc": 8205, + "backend_p2p": 38405, + "backend_http": 8305, + "blockbook_internal": 9205, + "blockbook_public": 9305 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-arbitrum", + "package_revision": "satoshilabs-1", + "system_user": "arbitrum", + "version": "3.2.1", + "docker_image": "offchainlabs/nitro-node:v3.2.1-d81324d", + "verification_type": "docker", + "verification_source": "724ebdcca39cd0c28ffd025ecea8d1622a376f41344201b729afb60352cbc306", + "extract_command": "docker cp extract:/home/user/target backend/target; docker cp extract:/home/user/nitro-legacy backend/nitro-legacy; docker cp extract:/usr/local/bin/nitro backend/nitro", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/arbitrum_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "arbitrum.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "openssl rand -hex 32 > {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/jwtsecret", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-arbitrum", + "system_user": "blockbook-arbitrum", + "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, + "additional_params": { + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"arbitrum-one\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/arbitrum_archive.json b/configs/coins/arbitrum_archive.json new file mode 100644 index 0000000000..09d0a5af9d --- /dev/null +++ b/configs/coins/arbitrum_archive.json @@ -0,0 +1,71 @@ +{ + "coin": { + "name": "Arbitrum Archive", + "shortcut": "ETH", + "network": "ARB", + "label": "Arbitrum", + "alias": "arbitrum_archive" + }, + "ports": { + "backend_rpc": 8306, + "backend_p2p": 38406, + "blockbook_internal": 9206, + "blockbook_public": 9306 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-arbitrum-archive", + "package_revision": "satoshilabs-1", + "system_user": "arbitrum", + "version": "3.2.1", + "docker_image": "offchainlabs/nitro-node:v3.2.1-d81324d", + "verification_type": "docker", + "verification_source": "724ebdcca39cd0c28ffd025ecea8d1622a376f41344201b729afb60352cbc306", + "extract_command": "docker cp extract:/home/user/target backend/target; docker cp extract:/home/user/nitro-legacy backend/nitro-legacy; docker cp extract:/usr/local/bin/nitro backend/nitro", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/arbitrum_archive_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "arbitrum_archive.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "openssl rand -hex 32 > {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/jwtsecret", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-arbitrum-archive", + "system_user": "blockbook-arbitrum", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 600, + "additional_params": { + "address_aliases": true, + "eip1559Fees": true, + "alternative_estimate_fee": "infura", + "alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/42161/suggestedGasFees\", \"periodSeconds\": 16}", + "mempoolTxTimeoutHours": 48, + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"arbitrum-one\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/arbitrum_nova.json b/configs/coins/arbitrum_nova.json new file mode 100644 index 0000000000..55d20d7d13 --- /dev/null +++ b/configs/coins/arbitrum_nova.json @@ -0,0 +1,65 @@ +{ + "coin": { + "name": "Arbitrum Nova", + "shortcut": "ETH", + "label": "Arbitrum Nova", + "alias": "arbitrum_nova" + }, + "ports": { + "backend_rpc": 8207, + "backend_p2p": 38407, + "backend_http": 8307, + "blockbook_internal": 9207, + "blockbook_public": 9307 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-arbitrum-nova", + "package_revision": "satoshilabs-1", + "system_user": "arbitrum", + "version": "3.2.1", + "docker_image": "offchainlabs/nitro-node:v3.2.1-d81324d", + "verification_type": "docker", + "verification_source": "724ebdcca39cd0c28ffd025ecea8d1622a376f41344201b729afb60352cbc306", + "extract_command": "docker cp extract:/home/user/target backend/target; docker cp extract:/home/user/nitro-legacy backend/nitro-legacy; docker cp extract:/usr/local/bin/nitro backend/nitro", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/arbitrum_nova_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "arbitrum_nova.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "openssl rand -hex 32 > {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/jwtsecret", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-arbitrum-nova", + "system_user": "blockbook-arbitrum", + "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, + "additional_params": { + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/arbitrum_nova_archive.json b/configs/coins/arbitrum_nova_archive.json new file mode 100644 index 0000000000..d0833e4536 --- /dev/null +++ b/configs/coins/arbitrum_nova_archive.json @@ -0,0 +1,67 @@ +{ + "coin": { + "name": "Arbitrum Nova Archive", + "shortcut": "ETH", + "label": "Arbitrum Nova", + "alias": "arbitrum_nova_archive" + }, + "ports": { + "backend_rpc": 8308, + "backend_p2p": 38408, + "blockbook_internal": 9208, + "blockbook_public": 9308 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-arbitrum-nova-archive", + "package_revision": "satoshilabs-1", + "system_user": "arbitrum", + "version": "3.2.1", + "docker_image": "offchainlabs/nitro-node:v3.2.1-d81324d", + "verification_type": "docker", + "verification_source": "724ebdcca39cd0c28ffd025ecea8d1622a376f41344201b729afb60352cbc306", + "extract_command": "docker cp extract:/home/user/target backend/target; docker cp extract:/home/user/nitro-legacy backend/nitro-legacy; docker cp extract:/usr/local/bin/nitro backend/nitro", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/arbitrum_nova_archive_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "arbitrum_nova_archive.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "openssl rand -hex 32 > {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/jwtsecret", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-arbitrum-nova-archive", + "system_user": "blockbook-arbitrum", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 600, + "additional_params": { + "address_aliases": true, + "mempoolTxTimeoutHours": 48, + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/avalanche.json b/configs/coins/avalanche.json index 110c16700d..7a1c8b723d 100644 --- a/configs/coins/avalanche.json +++ b/configs/coins/avalanche.json @@ -1,69 +1,69 @@ { - "coin": { - "name": "Avalanche", - "shortcut": "AVAX", - "label": "Avalanche", - "alias": "avalanche" - }, - "ports": { - "backend_rpc": 8098, - "backend_p2p": 38398, - "blockbook_internal": 9098, - "blockbook_public": 9198 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}/ext/bc/C/ws", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-avalanche", - "package_revision": "satoshilabs-1", - "system_user": "avalanche", - "version": "1.9.7", - "binary_url": "https://github.com/ava-labs/avalanchego/releases/download/v1.9.7/avalanchego-linux-amd64-v1.9.7.tar.gz", - "verification_type": "gpg", - "verification_source": "https://github.com/ava-labs/avalanchego/releases/download/v1.9.7/avalanchego-linux-amd64-v1.9.7.tar.gz.sig", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/avalanchego --data-dir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log-dir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --http-port {{.Ports.BackendRPC}} --staking-port {{.Ports.BackendP2P}} --public-ip 127.0.0.1 --staking-ephemeral-cert-enabled --chain-config-content ewogICJDIjp7CiAgICAiY29uZmlnIjoiZXdvZ0lDSmxkR2d0WVhCcGN5STZXd29nSUNBZ0ltVjBhQ0lzQ2lBZ0lDQWlaWFJvTFdacGJIUmxjaUlzQ2lBZ0lDQWlibVYwSWl3S0lDQWdJQ0prWldKMVp5MTBjbUZqWlhJaUxBb2dJQ0FnSW5kbFlqTWlMQW9nSUNBZ0ltbHVkR1Z5Ym1Gc0xXVjBhQ0lzQ2lBZ0lDQWlhVzUwWlhKdVlXd3RZbXh2WTJ0amFHRnBiaUlzQ2lBZ0lDQWlhVzUwWlhKdVlXd3RkSEpoYm5OaFkzUnBiMjRpTEFvZ0lDQWdJbWx1ZEdWeWJtRnNMWFI0TFhCdmIyd2lMQW9nSUNBZ0ltbHVkR1Z5Ym1Gc0xXUmxZblZuSWdvZ0lGMEtmUT09IgogIH0KfQ==", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", - "postinst_script_template": "", - "service_type": "simple", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": true, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/ava-labs/avalanchego/releases/download/v1.9.7/avalanchego-linux-arm64-v1.9.7.tar.gz", - "verification_source": "https://github.com/ava-labs/avalanchego/releases/download/v1.9.7/avalanchego-linux-arm64-v1.9.7.tar.gz.sig" - } + "coin": { + "name": "Avalanche", + "shortcut": "AVAX", + "label": "Avalanche", + "alias": "avalanche" + }, + "ports": { + "backend_rpc": 8098, + "backend_p2p": 38398, + "blockbook_internal": 9098, + "blockbook_public": 9198 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}/ext/bc/C/ws", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-avalanche", + "package_revision": "satoshilabs-1", + "system_user": "avalanche", + "version": "1.13.2", + "binary_url": "https://github.com/ava-labs/avalanchego/releases/download/v1.13.2/avalanchego-linux-amd64-v1.13.2.tar.gz", + "verification_type": "gpg", + "verification_source": "https://github.com/ava-labs/avalanchego/releases/download/v1.13.2/avalanchego-linux-amd64-v1.13.2.tar.gz.sig", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": [], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/avalanchego --data-dir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log-dir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --http-port {{.Ports.BackendRPC}} --staking-port {{.Ports.BackendP2P}} --public-ip 127.0.0.1 --staking-ephemeral-cert-enabled --chain-config-content ewogICJDIjp7CiAgICAiY29uZmlnIjoiZXdvZ0lDSmxkR2d0WVhCcGN5STZXd29nSUNBZ0ltVjBhQ0lzQ2lBZ0lDQWlaWFJvTFdacGJIUmxjaUlzQ2lBZ0lDQWlibVYwSWl3S0lDQWdJQ0prWldKMVp5MTBjbUZqWlhJaUxBb2dJQ0FnSW5kbFlqTWlMQW9nSUNBZ0ltbHVkR1Z5Ym1Gc0xXVjBhQ0lzQ2lBZ0lDQWlhVzUwWlhKdVlXd3RZbXh2WTJ0amFHRnBiaUlzQ2lBZ0lDQWlhVzUwWlhKdVlXd3RkSEpoYm5OaFkzUnBiMjRpTEFvZ0lDQWdJbWx1ZEdWeWJtRnNMWFI0TFhCdmIyd2lMQW9nSUNBZ0ltbHVkR1Z5Ym1Gc0xXUmxZblZuSWdvZ0lGMHNDaUFnSW5OMFlYUmxMWE41Ym1NdFpXNWhZbXhsWkNJNklHWmhiSE5sQ24wPSIKICB9Cn0=", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/ava-labs/avalanchego/releases/download/v1.13.2/avalanchego-linux-arm64-v1.13.2.tar.gz", + "verification_source": "https://github.com/ava-labs/avalanchego/releases/download/v1.13.2/avalanchego-linux-arm64-v1.13.2.tar.gz.sig" + } + } + }, + "blockbook": { + "package_name": "blockbook-avalanche", + "system_user": "blockbook-avalanche", + "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, + "additional_params": { + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"avalanche-2\",\"platformIdentifier\": \"avalanche\",\"platformVsCurrency\": \"usd\",\"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-avalanche", - "system_user": "blockbook-avalanche", - "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, - "additional_params": { - "mempoolTxTimeoutHours": 48, - "queryBackendOnMempoolResync": false, - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"avalanche-2\",\"platformIdentifier\": \"avalanche\",\"platformVsCurrency\": \"usd\",\"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} \ No newline at end of file +} diff --git a/configs/coins/avalanche_archive.json b/configs/coins/avalanche_archive.json index ca1db11a8c..7f7b7c5b67 100644 --- a/configs/coins/avalanche_archive.json +++ b/configs/coins/avalanche_archive.json @@ -1,72 +1,72 @@ { - "coin": { - "name": "Avalanche Archive", - "shortcut": "AVAX", - "label": "Avalanche", - "alias": "avalanche_archive" - }, - "ports": { - "backend_rpc": 8099, - "backend_p2p": 38399, - "blockbook_internal": 9099, - "blockbook_public": 9199 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}/ext/bc/C/ws", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-avalanche-archive", - "package_revision": "satoshilabs-1", - "system_user": "avalanche", - "version": "1.9.7", - "binary_url": "https://github.com/ava-labs/avalanchego/releases/download/v1.9.7/avalanchego-linux-amd64-v1.9.7.tar.gz", - "verification_type": "gpg", - "verification_source": "https://github.com/ava-labs/avalanchego/releases/download/v1.9.7/avalanchego-linux-amd64-v1.9.7.tar.gz.sig", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/avalanchego --data-dir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log-dir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --http-port {{.Ports.BackendRPC}} --staking-port {{.Ports.BackendP2P}} --public-ip 127.0.0.1 --staking-ephemeral-cert-enabled --chain-config-content ewogICJDIjp7CiAgICAiY29uZmlnIjoiZXdvZ0lDSmxkR2d0WVhCcGN5STZXd29nSUNBZ0ltVjBhQ0lzQ2lBZ0lDQWlaWFJvTFdacGJIUmxjaUlzQ2lBZ0lDQWlibVYwSWl3S0lDQWdJQ0prWldKMVp5MTBjbUZqWlhJaUxBb2dJQ0FnSW5kbFlqTWlMQW9nSUNBZ0ltbHVkR1Z5Ym1Gc0xXVjBhQ0lzQ2lBZ0lDQWlhVzUwWlhKdVlXd3RZbXh2WTJ0amFHRnBiaUlzQ2lBZ0lDQWlhVzUwWlhKdVlXd3RkSEpoYm5OaFkzUnBiMjRpTEFvZ0lDQWdJbWx1ZEdWeWJtRnNMWFI0TFhCdmIyd2lMQW9nSUNBZ0ltbHVkR1Z5Ym1Gc0xXUmxZblZuSWdvZ0lGMHNDaUFnSW5CeWRXNXBibWN0Wlc1aFlteGxaQ0k2Wm1Gc2MyVUtmUT09IgogIH0KfQ==", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", - "postinst_script_template": "", - "service_type": "simple", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": true, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/ava-labs/avalanchego/releases/download/v1.9.7/avalanchego-linux-arm64-v1.9.7.tar.gz", - "verification_source": "https://github.com/ava-labs/avalanchego/releases/download/v1.9.7/avalanchego-linux-arm64-v1.9.7.tar.gz.sig" - } + "coin": { + "name": "Avalanche Archive", + "shortcut": "AVAX", + "label": "Avalanche", + "alias": "avalanche_archive" + }, + "ports": { + "backend_rpc": 8099, + "backend_p2p": 38399, + "blockbook_internal": 9099, + "blockbook_public": 9199 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}/ext/bc/C/ws", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-avalanche-archive", + "package_revision": "satoshilabs-1", + "system_user": "avalanche", + "version": "1.13.2", + "binary_url": "https://github.com/ava-labs/avalanchego/releases/download/v1.13.2/avalanchego-linux-amd64-v1.13.2.tar.gz", + "verification_type": "gpg", + "verification_source": "https://github.com/ava-labs/avalanchego/releases/download/v1.13.2/avalanchego-linux-amd64-v1.13.2.tar.gz.sig", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": [], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/avalanchego --data-dir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log-dir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --http-port {{.Ports.BackendRPC}} --staking-port {{.Ports.BackendP2P}} --public-ip 127.0.0.1 --staking-ephemeral-cert-enabled --chain-config-content ewogICJDIjp7CiAgICAiY29uZmlnIjoiZXdvZ0lDSmxkR2d0WVhCcGN5STZXd29nSUNBZ0ltVjBhQ0lzQ2lBZ0lDQWlaWFJvTFdacGJIUmxjaUlzQ2lBZ0lDQWlibVYwSWl3S0lDQWdJQ0prWldKMVp5MTBjbUZqWlhJaUxBb2dJQ0FnSW5kbFlqTWlMQW9nSUNBZ0ltbHVkR1Z5Ym1Gc0xXVjBhQ0lzQ2lBZ0lDQWlhVzUwWlhKdVlXd3RZbXh2WTJ0amFHRnBiaUlzQ2lBZ0lDQWlhVzUwWlhKdVlXd3RkSEpoYm5OaFkzUnBiMjRpTEFvZ0lDQWdJbWx1ZEdWeWJtRnNMWFI0TFhCdmIyd2lMQW9nSUNBZ0ltbHVkR1Z5Ym1Gc0xXUmxZblZuSWdvZ0lGMHNDaUFnSW5CeWRXNXBibWN0Wlc1aFlteGxaQ0k2Wm1Gc2MyVXNDaUFnSW5OMFlYUmxMWE41Ym1NdFpXNWhZbXhsWkNJNklHWmhiSE5sQ24wPSIKICB9Cn0=", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/ava-labs/avalanchego/releases/download/v1.13.2/avalanchego-linux-arm64-v1.13.2.tar.gz", + "verification_source": "https://github.com/ava-labs/avalanchego/releases/download/v1.13.2/avalanchego-linux-arm64-v1.13.2.tar.gz.sig" + } + } + }, + "blockbook": { + "package_name": "blockbook-avalanche-archive", + "system_user": "blockbook-avalanche", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 600, + "additional_params": { + "address_aliases": true, + "mempoolTxTimeoutHours": 48, + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"avalanche-2\",\"platformIdentifier\": \"avalanche\",\"platformVsCurrency\": \"usd\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-avalanche-archive", - "system_user": "blockbook-avalanche", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "-workers=16", - "block_chain": { - "parse": true, - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 600, - "additional_params": { - "address_aliases": true, - "mempoolTxTimeoutHours": 48, - "processInternalTransactions": true, - "queryBackendOnMempoolResync": false, - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"avalanche-2\",\"platformIdentifier\": \"avalanche\",\"platformVsCurrency\": \"usd\",\"periodSeconds\": 900}", - "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} \ No newline at end of file +} diff --git a/configs/coins/base.json b/configs/coins/base.json new file mode 100644 index 0000000000..83578785be --- /dev/null +++ b/configs/coins/base.json @@ -0,0 +1,67 @@ +{ + "coin": { + "name": "Base", + "shortcut": "ETH", + "network": "BASE", + "label": "Base", + "alias": "base" + }, + "ports": { + "backend_rpc": 8309, + "backend_p2p": 38409, + "backend_http": 8209, + "backend_authrpc": 8409, + "blockbook_internal": 9209, + "blockbook_public": 9309 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-base", + "package_revision": "satoshilabs-1", + "system_user": "base", + "version": "1.101411.3", + "docker_image": "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:v1.101411.3", + "verification_type": "docker", + "verification_source": "aefecdb139d8e3ed3128e7e3c87abb71198dc6a44ef21f012f391af52679e2c5", + "extract_command": "docker cp extract:/usr/local/bin/geth backend/geth", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/base_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "base.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "openssl rand -hex 32 > {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/jwtsecret", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-base", + "system_user": "blockbook-base", + "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, + "additional_params": { + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"base\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/base_archive.json b/configs/coins/base_archive.json new file mode 100644 index 0000000000..57a1805754 --- /dev/null +++ b/configs/coins/base_archive.json @@ -0,0 +1,73 @@ +{ + "coin": { + "name": "Base Archive", + "shortcut": "ETH", + "network": "BASE", + "label": "Base", + "alias": "base_archive" + }, + "ports": { + "backend_rpc": 8211, + "backend_p2p": 38411, + "backend_http": 8311, + "backend_authrpc": 8411, + "blockbook_internal": 9211, + "blockbook_public": 9311 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-base-archive", + "package_revision": "satoshilabs-1", + "system_user": "base", + "version": "1.101411.3", + "docker_image": "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth:v1.101411.3", + "verification_type": "docker", + "verification_source": "aefecdb139d8e3ed3128e7e3c87abb71198dc6a44ef21f012f391af52679e2c5", + "extract_command": "docker cp extract:/usr/local/bin/geth backend/geth", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/base_archive_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "base_archive.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "openssl rand -hex 32 > {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/jwtsecret", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-base-archive", + "system_user": "blockbook-base", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 600, + "additional_params": { + "address_aliases": true, + "eip1559Fees": true, + "alternative_estimate_fee": "infura", + "alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/8453/suggestedGasFees\", \"periodSeconds\": 8}", + "mempoolTxTimeoutHours": 48, + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"base\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/base_archive_op_node.json b/configs/coins/base_archive_op_node.json new file mode 100644 index 0000000000..85a4c5dbe1 --- /dev/null +++ b/configs/coins/base_archive_op_node.json @@ -0,0 +1,38 @@ +{ + "coin": { + "name": "Base Archive Op-Node", + "shortcut": "ETH", + "label": "Base", + "alias": "base_archive_op_node" + }, + "ports": { + "backend_rpc": 8212, + "blockbook_internal": 9212, + "blockbook_public": 9312 + }, + "backend": { + "package_name": "backend-base-archive-op-node", + "package_revision": "satoshilabs-1", + "system_user": "base", + "version": "1.10.1", + "docker_image": "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-node:v1.10.1", + "verification_type": "docker", + "verification_source": "8f40714868fbdc788f67251383a0c0b78a3a937f07b2303bc7d33df5df6297d9", + "extract_command": "docker cp extract:/usr/local/bin/op-node backend/op-node", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/base_archive_op_node_exec.sh 2>&1 >> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "base_archive_op_node.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/base_op_node.json b/configs/coins/base_op_node.json new file mode 100644 index 0000000000..426d718069 --- /dev/null +++ b/configs/coins/base_op_node.json @@ -0,0 +1,38 @@ +{ + "coin": { + "name": "Base Op-Node", + "shortcut": "ETH", + "label": "Base", + "alias": "base_op_node" + }, + "ports": { + "backend_rpc": 8210, + "blockbook_internal": 9210, + "blockbook_public": 9310 + }, + "backend": { + "package_name": "backend-base-op-node", + "package_revision": "satoshilabs-1", + "system_user": "base", + "version": "1.10.1", + "docker_image": "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-node:v1.10.1", + "verification_type": "docker", + "verification_source": "8f40714868fbdc788f67251383a0c0b78a3a937f07b2303bc7d33df5df6297d9", + "extract_command": "docker cp extract:/usr/local/bin/op-node backend/op-node", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/base_op_node_exec.sh 2>&1 >> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "base_op_node.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/bcash.json b/configs/coins/bcash.json index 537c042561..1a6a4e5d4b 100644 --- a/configs/coins/bcash.json +++ b/configs/coins/bcash.json @@ -1,68 +1,68 @@ { - "coin": { - "name": "Bcash", - "shortcut": "BCH", - "label": "Bitcoin Cash", - "alias": "bcash" - }, - "ports": { - "backend_rpc": 8031, - "backend_message_queue": 38331, - "blockbook_internal": 9031, - "blockbook_public": 9131 - }, - "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-bcash", - "package_revision": "satoshilabs-1", - "system_user": "bcash", - "version": "26.0.0", - "binary_url": "https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v26.0.0/bitcoin-cash-node-26.0.0-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "e32e05fd63161f6f1fe717fca789448d2ee48e2017d3d4c6686b4222fe69497e", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/bitcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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": "bcash.conf", - "client_config_file": "bitcoin_like_client.conf" - }, - "blockbook": { - "package_name": "blockbook-bcash", - "system_user": "blockbook-bcash", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "", - "block_chain": { - "parse": true, - "subversion": "/Bitcoin ABC Cash Node:22.1.0/", - "address_format": "cashaddr", - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 300, - "xpub_magic": 76067358, - "slip44": 145, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin-cash\", \"periodSeconds\": 900}" - } + "coin": { + "name": "Bcash", + "shortcut": "BCH", + "label": "Bitcoin Cash", + "alias": "bcash" + }, + "ports": { + "backend_rpc": 8031, + "backend_message_queue": 38331, + "blockbook_internal": 9031, + "blockbook_public": 9131 + }, + "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-bcash", + "package_revision": "satoshilabs-1", + "system_user": "bcash", + "version": "28.0.1", + "binary_url": "https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v28.0.1/bitcoin-cash-node-28.0.1-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "d69ee632147f886ca540cecdff5b1b85512612b4c005e86b09083a63c35b64fa", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/bitcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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": "bcash.conf", + "client_config_file": "bitcoin_like_client.conf" + }, + "blockbook": { + "package_name": "blockbook-bcash", + "system_user": "blockbook-bcash", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "", + "block_chain": { + "parse": true, + "subversion": "/Bitcoin ABC Cash Node:22.1.0/", + "address_format": "cashaddr", + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 145, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"bitcoin-cash\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/bcash_testnet.json b/configs/coins/bcash_testnet.json index e13b5ebc05..fb98530cee 100644 --- a/configs/coins/bcash_testnet.json +++ b/configs/coins/bcash_testnet.json @@ -1,66 +1,64 @@ { - "coin": { - "name": "Bcash Testnet", - "shortcut": "TBCH", - "label": "Bitcoin Cash Testnet", - "alias": "bcash_testnet" - }, - "ports": { - "backend_rpc": 18031, - "backend_message_queue": 48331, - "blockbook_internal": 19031, - "blockbook_public": 19131 - }, - "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-bcash-testnet", - "package_revision": "satoshilabs-1", - "system_user": "bcash", - "version": "26.0.0", - "binary_url": "https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v26.0.0/bitcoin-cash-node-26.0.0-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "e32e05fd63161f6f1fe717fca789448d2ee48e2017d3d4c6686b4222fe69497e", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [ - "bin/bitcoin-qt" - ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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": "bitcoin.conf", - "client_config_file": "bitcoin_client.conf" - }, - "blockbook": { - "package_name": "blockbook-bcash-testnet", - "system_user": "blockbook-bcash", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "", - "block_chain": { - "parse": true, - "subversion": "/Bitcoin ABC Cash Node:22.1.0/", - "address_format": "cashaddr", - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 300, - "xpub_magic": 70617039, - "slip44": 1, - "additional_params": {} + "coin": { + "name": "Bcash Testnet", + "shortcut": "TBCH", + "label": "Bitcoin Cash Testnet", + "alias": "bcash_testnet" + }, + "ports": { + "backend_rpc": 18031, + "backend_message_queue": 48331, + "blockbook_internal": 19031, + "blockbook_public": 19131 + }, + "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-bcash-testnet", + "package_revision": "satoshilabs-1", + "system_user": "bcash", + "version": "28.0.1", + "binary_url": "https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v28.0.1/bitcoin-cash-node-28.0.1-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "d69ee632147f886ca540cecdff5b1b85512612b4c005e86b09083a63c35b64fa", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/bitcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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": "bcash.conf", + "client_config_file": "bitcoin_client.conf" + }, + "blockbook": { + "package_name": "blockbook-bcash-testnet", + "system_user": "blockbook-bcash", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "", + "block_chain": { + "parse": true, + "subversion": "/Bitcoin ABC Cash Node:22.1.0/", + "address_format": "cashaddr", + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "slip44": 1, + "additional_params": {} + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/bgold.json b/configs/coins/bgold.json index e5afbb5fcf..733ff874c2 100644 --- a/configs/coins/bgold.json +++ b/configs/coins/bgold.json @@ -1,264 +1,264 @@ { - "coin": { - "name": "Bgold", - "shortcut": "BTG", - "label": "Bitcoin Gold", - "alias": "bgold" - }, - "ports": { - "backend_rpc": 8035, - "backend_message_queue": 38335, - "blockbook_internal": 9035, - "blockbook_public": 9135 - }, - "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-bgold", - "package_revision": "satoshilabs-1", - "system_user": "bgold", - "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.17.3/SHA256SUMS.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/bitcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bgoldd -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": { - "addnode": [ - "188.126.0.134", - "45.56.84.44", - "109.201.133.93:8338", - "178.63.11.246:8338", - "188.120.223.153:8338", - "79.137.64.158:8338", - "78.193.221.106:8338", - "139.59.151.13:8338", - "76.16.12.81:8338", - "172.104.157.62:8338", - "43.207.67.209:8338", - "178.63.11.246:8338", - "79.137.64.158:8338", - "78.193.221.106:8338", - "139.59.151.13:8338", - "172.104.157.62:8338", - "178.158.247.119:8338", - "109.201.133.93:8338", - "178.63.11.246:8338", - "139.59.151.13:8338", - "172.104.157.62:8338", - "188.120.223.153:8338", - "178.158.247.119:8338", - "78.193.221.106:8338", - "79.137.64.158:8338", - "76.16.12.81:8338", - "176.12.32.153:8338", - "178.158.247.122:8338", - "81.37.147.185:8338", - "176.12.32.153:8338", - "79.137.64.158:8338", - "178.158.247.122:8338", - "66.70.247.151:8338", - "89.18.27.165:8338", - "178.63.11.246:8338", - "91.222.17.86:8338", - "37.59.50.143:8338", - "91.50.219.221:8338", - "154.16.63.17:8338", - "213.136.76.42:8338", - "176.99.4.140:8338", - "176.9.48.36:8338", - "78.193.221.106:8338", - "34.236.228.99:8338", - "213.154.230.107:8338", - "111.231.66.252:8338", - "188.120.223.153:8338", - "219.89.122.82:8338", - "109.192.23.101:8338", - "98.114.91.222:8338", - "217.66.156.41:8338", - "172.104.157.62:8338", - "114.44.222.73:8338", - "91.224.140.216:8338", - "149.154.71.96:8338", - "107.181.183.242:8338", - "36.78.96.92:8338", - "46.22.7.74:8338", - "89.110.53.186:8338", - "73.243.220.85:8338", - "109.86.137.8:8338", - "77.78.12.89:8338", - "87.92.116.26:8338", - "93.78.122.48:8338", - "35.195.83.0:8338", - "46.147.75.220:8338", - "212.47.236.104:8338", - "95.220.100.230:8338", - "178.70.142.247:8338", - "45.76.136.149:8338", - "94.155.74.206:8338", - "178.70.142.247:8338", - "128.199.228.97:8338", - "77.171.144.207:8338", - "159.89.192.119:8338", - "136.63.238.170:8338", - "31.27.193.105:8338", - "176.107.192.240:8338", - "94.140.241.96:8338", - "66.108.15.5:8338", - "81.177.127.204:8338", - "88.18.69.174:8338", - "178.70.130.94:8338", - "78.98.162.140:8338", - "95.133.156.224:8338", - "46.188.16.96:8338", - "94.247.16.21:8338", - "eunode.pool.gold:8338", - "asianode.pool.gold:8338", - "45.56.84.44:8338", - "176.9.48.36:8338", - "93.57.253.121:8338", - "172.104.157.62:8338", - "176.12.32.153:8338", - "pool.serverpower.net:8338", - "213.154.229.126:8338", - "213.154.230.106:8338", - "213.154.230.107:8338", - "213.154.229.50:8338", - "145.239.0.50:8338", - "107.181.183.242:8338", - "109.201.133.93:8338", - "120.41.190.109:8338", - "120.41.191.224:8338", - "138.68.249.79:8338", - "13.95.223.202:8338", - "145.239.0.50:8338", - "149.56.95.26:8338", - "158.69.103.228:8338", - "159.89.192.119:8338", - "164.132.207.143:8338", - "171.100.141.106:8338", - "172.104.157.62:8338", - "173.176.95.92:8338", - "176.12.32.153:8338", - "178.239.54.250:8338", - "178.63.11.246:8338", - "185.139.2.140:8338", - "188.120.223.153:8338", - "190.46.2.92:8338", - "192.99.194.113:8338", - "199.229.248.218:8338", - "213.154.229.126:8338", - "213.154.229.50:8338", - "213.154.230.106:8338", - "213.154.230.107:8338", - "217.182.199.21", - "35.189.127.200:8338", - "35.195.83.0:8338", - "35.197.197.166:8338", - "35.200.168.155:8338", - "35.203.167.11:8338", - "37.59.50.143:8338", - "45.27.161.195:8338", - "45.32.234.160:8338", - "45.56.84.44:8338", - "46.188.16.96:8338", - "46.251.19.171:8338", - "5.157.119.109:8338", - "52.28.162.48:8338", - "54.153.140.202:8338", - "54.68.81.2:83388338", - "62.195.190.190:8338", - "62.216.5.136:8338", - "65.110.125.175:8338", - "67.68.226.130:8338", - "73.243.220.85:8338", - "77.78.12.89:8338", - "78.193.221.106:8338", - "78.98.162.140:8338", - "79.137.64.158:8338", - "84.144.177.238:8338", - "87.92.116.26:8338", - "89.115.139.117:8338", - "89.18.27.165:8338", - "91.50.219.221:8338", - "93.88.74.26", - "93.88.74.26:8338", - "94.155.74.206:8338", - "95.154.201.132:8338", - "98.29.248.131:8338", - "u2.my.to:8338", - "[2001:470:b:ce:dc70:83ff:fe7a:1e74]:8338", - "2001:7b8:61d:1:250:56ff:fe90:c89f:8338", - "2001:7b8:63a:1002:213:154:230:106:8338", - "2001:7b8:63a:1002:213:154:230:107:8338", - "45.56.84.44", - "109.201.133.93:8338", - "120.41.191.224:30607", - "138.68.249.79:50992", - "138.68.249.79:51314", - "172.104.157.62", - "178.63.11.246:8338", - "185.139.2.140:8338", - "199.229.248.218:28830", - "35.189.127.200:41220", - "35.189.127.200:48244", - "35.195.83.0:35172", - "35.195.83.0:35576", - "35.195.83.0:35798", - "35.197.197.166:32794", - "35.197.197.166:33112", - "35.197.197.166:33332", - "35.203.167.11:52158", - "37.59.50.143:35254", - "45.27.161.195:33852", - "45.27.161.195:36738", - "45.27.161.195:58628" - ], - "maxconnections": 250, - "mempoolexpiry": 72, - "timeout": 768 + "coin": { + "name": "Bgold", + "shortcut": "BTG", + "label": "Bitcoin Gold", + "alias": "bgold" + }, + "ports": { + "backend_rpc": 8035, + "backend_message_queue": 38335, + "blockbook_internal": 9035, + "blockbook_public": 9135 + }, + "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-bgold", + "package_revision": "satoshilabs-1", + "system_user": "bgold", + "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.17.3/SHA256SUMS.asc", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/bitcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bgoldd -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": { + "addnode": [ + "188.126.0.134", + "45.56.84.44", + "109.201.133.93:8338", + "178.63.11.246:8338", + "188.120.223.153:8338", + "79.137.64.158:8338", + "78.193.221.106:8338", + "139.59.151.13:8338", + "76.16.12.81:8338", + "172.104.157.62:8338", + "43.207.67.209:8338", + "178.63.11.246:8338", + "79.137.64.158:8338", + "78.193.221.106:8338", + "139.59.151.13:8338", + "172.104.157.62:8338", + "178.158.247.119:8338", + "109.201.133.93:8338", + "178.63.11.246:8338", + "139.59.151.13:8338", + "172.104.157.62:8338", + "188.120.223.153:8338", + "178.158.247.119:8338", + "78.193.221.106:8338", + "79.137.64.158:8338", + "76.16.12.81:8338", + "176.12.32.153:8338", + "178.158.247.122:8338", + "81.37.147.185:8338", + "176.12.32.153:8338", + "79.137.64.158:8338", + "178.158.247.122:8338", + "66.70.247.151:8338", + "89.18.27.165:8338", + "178.63.11.246:8338", + "91.222.17.86:8338", + "37.59.50.143:8338", + "91.50.219.221:8338", + "154.16.63.17:8338", + "213.136.76.42:8338", + "176.99.4.140:8338", + "176.9.48.36:8338", + "78.193.221.106:8338", + "34.236.228.99:8338", + "213.154.230.107:8338", + "111.231.66.252:8338", + "188.120.223.153:8338", + "219.89.122.82:8338", + "109.192.23.101:8338", + "98.114.91.222:8338", + "217.66.156.41:8338", + "172.104.157.62:8338", + "114.44.222.73:8338", + "91.224.140.216:8338", + "149.154.71.96:8338", + "107.181.183.242:8338", + "36.78.96.92:8338", + "46.22.7.74:8338", + "89.110.53.186:8338", + "73.243.220.85:8338", + "109.86.137.8:8338", + "77.78.12.89:8338", + "87.92.116.26:8338", + "93.78.122.48:8338", + "35.195.83.0:8338", + "46.147.75.220:8338", + "212.47.236.104:8338", + "95.220.100.230:8338", + "178.70.142.247:8338", + "45.76.136.149:8338", + "94.155.74.206:8338", + "178.70.142.247:8338", + "128.199.228.97:8338", + "77.171.144.207:8338", + "159.89.192.119:8338", + "136.63.238.170:8338", + "31.27.193.105:8338", + "176.107.192.240:8338", + "94.140.241.96:8338", + "66.108.15.5:8338", + "81.177.127.204:8338", + "88.18.69.174:8338", + "178.70.130.94:8338", + "78.98.162.140:8338", + "95.133.156.224:8338", + "46.188.16.96:8338", + "94.247.16.21:8338", + "eunode.pool.gold:8338", + "asianode.pool.gold:8338", + "45.56.84.44:8338", + "176.9.48.36:8338", + "93.57.253.121:8338", + "172.104.157.62:8338", + "176.12.32.153:8338", + "pool.serverpower.net:8338", + "213.154.229.126:8338", + "213.154.230.106:8338", + "213.154.230.107:8338", + "213.154.229.50:8338", + "145.239.0.50:8338", + "107.181.183.242:8338", + "109.201.133.93:8338", + "120.41.190.109:8338", + "120.41.191.224:8338", + "138.68.249.79:8338", + "13.95.223.202:8338", + "145.239.0.50:8338", + "149.56.95.26:8338", + "158.69.103.228:8338", + "159.89.192.119:8338", + "164.132.207.143:8338", + "171.100.141.106:8338", + "172.104.157.62:8338", + "173.176.95.92:8338", + "176.12.32.153:8338", + "178.239.54.250:8338", + "178.63.11.246:8338", + "185.139.2.140:8338", + "188.120.223.153:8338", + "190.46.2.92:8338", + "192.99.194.113:8338", + "199.229.248.218:8338", + "213.154.229.126:8338", + "213.154.229.50:8338", + "213.154.230.106:8338", + "213.154.230.107:8338", + "217.182.199.21", + "35.189.127.200:8338", + "35.195.83.0:8338", + "35.197.197.166:8338", + "35.200.168.155:8338", + "35.203.167.11:8338", + "37.59.50.143:8338", + "45.27.161.195:8338", + "45.32.234.160:8338", + "45.56.84.44:8338", + "46.188.16.96:8338", + "46.251.19.171:8338", + "5.157.119.109:8338", + "52.28.162.48:8338", + "54.153.140.202:8338", + "54.68.81.2:83388338", + "62.195.190.190:8338", + "62.216.5.136:8338", + "65.110.125.175:8338", + "67.68.226.130:8338", + "73.243.220.85:8338", + "77.78.12.89:8338", + "78.193.221.106:8338", + "78.98.162.140:8338", + "79.137.64.158:8338", + "84.144.177.238:8338", + "87.92.116.26:8338", + "89.115.139.117:8338", + "89.18.27.165:8338", + "91.50.219.221:8338", + "93.88.74.26", + "93.88.74.26:8338", + "94.155.74.206:8338", + "95.154.201.132:8338", + "98.29.248.131:8338", + "u2.my.to:8338", + "[2001:470:b:ce:dc70:83ff:fe7a:1e74]:8338", + "2001:7b8:61d:1:250:56ff:fe90:c89f:8338", + "2001:7b8:63a:1002:213:154:230:106:8338", + "2001:7b8:63a:1002:213:154:230:107:8338", + "45.56.84.44", + "109.201.133.93:8338", + "120.41.191.224:30607", + "138.68.249.79:50992", + "138.68.249.79:51314", + "172.104.157.62", + "178.63.11.246:8338", + "185.139.2.140:8338", + "199.229.248.218:28830", + "35.189.127.200:41220", + "35.189.127.200:48244", + "35.195.83.0:35172", + "35.195.83.0:35576", + "35.195.83.0:35798", + "35.197.197.166:32794", + "35.197.197.166:33112", + "35.197.197.166:33332", + "35.203.167.11:52158", + "37.59.50.143:35254", + "45.27.161.195:33852", + "45.27.161.195:36738", + "45.27.161.195:58628" + ], + "maxconnections": 250, + "mempoolexpiry": 72, + "timeout": 768 + } + }, + "blockbook": { + "package_name": "blockbook-bgold", + "system_user": "blockbook-bgold", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "", + "block_chain": { + "parse": true, + "subversion": "/Bitcoin Gold:0.17.3/", + "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": 156, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"bitcoin-gold\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "Jakub Matys", + "package_maintainer_email": "jakub.matys@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-bgold", - "system_user": "blockbook-bgold", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "", - "block_chain": { - "parse": true, - "subversion": "/Bitcoin Gold:0.17.3/", - "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": 156, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin-gold\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "Jakub Matys", - "package_maintainer_email": "jakub.matys@satoshilabs.com" - } } diff --git a/configs/coins/bitcoin.json b/configs/coins/bitcoin.json index 996b5f1e53..02e10bb198 100644 --- a/configs/coins/bitcoin.json +++ b/configs/coins/bitcoin.json @@ -1,78 +1,85 @@ { - "coin": { - "name": "Bitcoin", - "shortcut": "BTC", - "label": "Bitcoin", - "alias": "bitcoin" - }, - "ports": { - "backend_rpc": 8030, - "backend_message_queue": 38330, - "blockbook_internal": 9030, - "blockbook_public": 9130 - }, - "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-bitcoin", - "package_revision": "satoshilabs-1", - "system_user": "bitcoin", - "version": "24.0.1", - "binary_url": "https://bitcoincore.org/bin/bitcoin-core-24.0.1/bitcoin-24.0.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "49df6e444515d457ea0b885d66f521f2a26ca92ccf73d5296082e633544253bf", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/bitcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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.conf", - "client_config_file": "bitcoin_client.conf", - "additional_params": { - "deprecatedrpc": "estimatefee" + "coin": { + "name": "Bitcoin", + "shortcut": "BTC", + "label": "Bitcoin", + "alias": "bitcoin" }, - "platforms": { - "arm64": { - "binary_url": "https://bitcoincore.org/bin/bitcoin-core-23.0/bitcoin-23.0-aarch64-linux-gnu.tar.gz", - "verification_source": "06f4c78271a77752ba5990d60d81b1751507f77efda1e5981b4e92fd4d9969fb" - } - } - }, - "blockbook": { - "package_name": "blockbook-bitcoin", - "system_user": "blockbook-bitcoin", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "-dbcache=1073741824", - "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, - "additional_params": { - "alternative_estimate_fee": "whatthefee-disabled", - "alternative_estimate_fee_params": "{\"url\": \"https://whatthefee.io/data.json\", \"periodSeconds\": 60}", - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcoin\", \"periodSeconds\": 900}" - } + "ports": { + "backend_rpc": 8030, + "backend_message_queue": 38330, + "blockbook_internal": 9030, + "blockbook_public": 9130 + }, + "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-bitcoin", + "package_revision": "satoshilabs-1", + "system_user": "bitcoin", + "version": "29.2", + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "1fd58d0ae94b8a9e21bbaeab7d53395a44976e82bd5492b0a894826c135f9009", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/bitcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee", + "addnode": ["ove.palatinus.cz"] + }, + "platforms": { + "arm64": { + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-aarch64-linux-gnu.tar.gz", + "verification_source": "f88f72a3c5bf526581aae573be8c1f62133eaecfe3d34646c9ffca7b79dfdc7a" + } + } + }, + "blockbook": { + "package_name": "blockbook-bitcoin", + "system_user": "blockbook-bitcoin", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-dbcache=1073741824 -enablesubnewtx -extendedindex", + "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, + "additional_params": { + "alternative_estimate_fee": "mempoolspaceblock", + "alternative_estimate_fee_params": "{\"url\": \"https://mempool.space/api/v1/fees/mempool-blocks\", \"periodSeconds\": 20, \"feeRangeIndex\": 5, \"fallbackFeePerKB\": 1000}", + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"bitcoin\", \"periodSeconds\": 60}", + "block_golomb_filter_p": 20, + "block_filter_scripts": "taproot-noordinals", + "block_filter_use_zeroed_key": true, + "mempool_golomb_filter_p": 20, + "mempool_filter_scripts": "taproot", + "mempool_filter_use_zeroed_key": false + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/bitcoin_regtest.json b/configs/coins/bitcoin_regtest.json index b613ac2106..a14a85a66d 100644 --- a/configs/coins/bitcoin_regtest.json +++ b/configs/coins/bitcoin_regtest.json @@ -1,75 +1,82 @@ { - "coin": { - "name": "Regtest", - "shortcut": "rBTC", - "label": "Bitcoin Regtest", - "alias": "bitcoin_regtest" - }, - "ports": { - "backend_rpc": 18021, - "backend_message_queue": 48321, - "blockbook_internal": 19021, - "blockbook_public": 19121 - }, - "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-bitcoin-regtest", - "package_revision": "satoshilabs-1", - "system_user": "bitcoin", - "version": "24.0.1", - "binary_url": "https://bitcoincore.org/bin/bitcoin-core-24.0.1/bitcoin-24.0.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "49df6e444515d457ea0b885d66f521f2a26ca92ccf73d5296082e633544253bf", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [ - "bin/bitcoin-qt" - ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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/regtest/*.log", - "postinst_script_template": "", - "service_type": "forking", - "service_additional_params_template": "", - "mainnet": false, - "protect_memory": true, - "server_config_file": "bitcoin_regtest.conf", - "client_config_file": "bitcoin_client.conf", - "additional_params": { - "deprecatedrpc": "estimatefee" + "coin": { + "name": "Regtest", + "shortcut": "rBTC", + "label": "Bitcoin Regtest", + "alias": "bitcoin_regtest" }, - "platforms": { - "arm64": { - "binary_url": "https://bitcoincore.org/bin/bitcoin-core-24.0/bitcoin-24.0-aarch64-linux-gnu.tar.gz", - "verification_source": "904e103f08f776d03935118568411724f9e070e0e888e52c9e5692308fa47d49" - } - } - }, - "blockbook": { - "package_name": "blockbook-bitcoin-regtest", - "system_user": "blockbook-bitcoin", - "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": {} + "ports": { + "backend_rpc": 18021, + "backend_message_queue": 48321, + "blockbook_internal": 19021, + "blockbook_public": 19121 + }, + "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-bitcoin-regtest", + "package_revision": "satoshilabs-1", + "system_user": "bitcoin", + "version": "29.2", + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "1fd58d0ae94b8a9e21bbaeab7d53395a44976e82bd5492b0a894826c135f9009", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/bitcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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/regtest/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "mainnet": false, + "protect_memory": true, + "server_config_file": "bitcoin_regtest.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-aarch64-linux-gnu.tar.gz", + "verification_source": "f88f72a3c5bf526581aae573be8c1f62133eaecfe3d34646c9ffca7b79dfdc7a" + } + } + }, + "blockbook": { + "package_name": "blockbook-bitcoin-regtest", + "system_user": "blockbook-bitcoin", + "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": { + "alternative_estimate_fee": "mempoolspaceblock", + "alternative_estimate_fee_params": "{\"url\": \"https://mempool.space/api/v1/fees/mempool-blocks\", \"periodSeconds\": 20, \"feeRangeIndex\": 5, \"fallbackFeePerKB\": 1000}", + "block_golomb_filter_p": 20, + "block_filter_scripts": "taproot-noordinals", + "block_filter_use_zeroed_key": true, + "mempool_golomb_filter_p": 20, + "mempool_filter_scripts": "taproot", + "mempool_filter_use_zeroed_key": false + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/bitcoin_signet.json b/configs/coins/bitcoin_signet.json index fc82b3e8c0..63f5562a45 100644 --- a/configs/coins/bitcoin_signet.json +++ b/configs/coins/bitcoin_signet.json @@ -1,75 +1,73 @@ { - "coin": { - "name": "Signet", - "shortcut": "sBTC", - "label": "Bitcoin Signet", - "alias": "bitcoin_signet" - }, - "ports": { - "backend_rpc": 18020, - "backend_message_queue": 48320, - "blockbook_internal": 19020, - "blockbook_public": 19120 - }, - "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-bitcoin-signet", - "package_revision": "satoshilabs-1", - "system_user": "bitcoin", - "version": "24.0.1", - "binary_url": "https://bitcoincore.org/bin/bitcoin-core-24.0.1/bitcoin-24.0.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "49df6e444515d457ea0b885d66f521f2a26ca92ccf73d5296082e633544253bf", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [ - "bin/bitcoin-qt" - ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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/signet/*.log", - "postinst_script_template": "", - "service_type": "forking", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "bitcoin-signet.conf", - "client_config_file": "bitcoin_client.conf", - "additional_params": { - "deprecatedrpc": "estimatefee" + "coin": { + "name": "Signet", + "shortcut": "sBTC", + "label": "Bitcoin Signet", + "alias": "bitcoin_signet" }, - "platforms": { - "arm64": { - "binary_url": "https://bitcoincore.org/bin/bitcoin-core-23.0/bitcoin-23.0-aarch64-linux-gnu.tar.gz", - "verification_source": "06f4c78271a77752ba5990d60d81b1751507f77efda1e5981b4e92fd4d9969fb" - } - } - }, - "blockbook": { - "package_name": "blockbook-bitcoin-signet", - "system_user": "blockbook-bitcoin", - "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": {} + "ports": { + "backend_rpc": 18020, + "backend_message_queue": 48320, + "blockbook_internal": 19020, + "blockbook_public": 19120 + }, + "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-bitcoin-signet", + "package_revision": "satoshilabs-1", + "system_user": "bitcoin", + "version": "29.2", + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "1fd58d0ae94b8a9e21bbaeab7d53395a44976e82bd5492b0a894826c135f9009", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/bitcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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/signet/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "bitcoin_signet.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-aarch64-linux-gnu.tar.gz", + "verification_source": "f88f72a3c5bf526581aae573be8c1f62133eaecfe3d34646c9ffca7b79dfdc7a" + } + } + }, + "blockbook": { + "package_name": "blockbook-bitcoin-signet", + "system_user": "blockbook-bitcoin", + "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": "wakiyamap", + "package_maintainer_email": "wakiyamap@gmail.com" } - }, - "meta": { - "package_maintainer": "wakiyamap", - "package_maintainer_email": "wakiyamap@gmail.com" - } } diff --git a/configs/coins/bitcoin_testnet.json b/configs/coins/bitcoin_testnet.json index 13546f6b2f..f3db91e270 100644 --- a/configs/coins/bitcoin_testnet.json +++ b/configs/coins/bitcoin_testnet.json @@ -1,75 +1,80 @@ { - "coin": { - "name": "Testnet", - "shortcut": "TEST", - "label": "Bitcoin Testnet", - "alias": "bitcoin_testnet" - }, - "ports": { - "backend_rpc": 18030, - "backend_message_queue": 48330, - "blockbook_internal": 19030, - "blockbook_public": 19130 - }, - "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-bitcoin-testnet", - "package_revision": "satoshilabs-1", - "system_user": "bitcoin", - "version": "24.0.1", - "binary_url": "https://bitcoincore.org/bin/bitcoin-core-24.0.1/bitcoin-24.0.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "49df6e444515d457ea0b885d66f521f2a26ca92ccf73d5296082e633544253bf", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [ - "bin/bitcoin-qt" - ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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": "bitcoin.conf", - "client_config_file": "bitcoin_client.conf", - "additional_params": { - "deprecatedrpc": "estimatefee" + "coin": { + "name": "Testnet", + "shortcut": "TEST", + "label": "Bitcoin Testnet", + "alias": "bitcoin_testnet" }, - "platforms": { - "arm64": { - "binary_url": "https://bitcoincore.org/bin/bitcoin-core-23.0/bitcoin-23.0-aarch64-linux-gnu.tar.gz", - "verification_source": "06f4c78271a77752ba5990d60d81b1751507f77efda1e5981b4e92fd4d9969fb" - } - } - }, - "blockbook": { - "package_name": "blockbook-bitcoin-testnet", - "system_user": "blockbook-bitcoin", - "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": {} + "ports": { + "backend_rpc": 18030, + "backend_message_queue": 48330, + "blockbook_internal": 19030, + "blockbook_public": 19130 + }, + "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-bitcoin-testnet", + "package_revision": "satoshilabs-1", + "system_user": "bitcoin", + "version": "29.2", + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "1fd58d0ae94b8a9e21bbaeab7d53395a44976e82bd5492b0a894826c135f9009", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/bitcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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": "bitcoin.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-aarch64-linux-gnu.tar.gz", + "verification_source": "f88f72a3c5bf526581aae573be8c1f62133eaecfe3d34646c9ffca7b79dfdc7a" + } + } + }, + "blockbook": { + "package_name": "blockbook-bitcoin-testnet", + "system_user": "blockbook-bitcoin", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-enablesubnewtx -extendedindex", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 10000, + "xpub_magic": 70617039, + "xpub_magic_segwit_p2sh": 71979618, + "xpub_magic_segwit_native": 73342198, + "slip44": 1, + "additional_params": { + "block_golomb_filter_p": 20, + "block_filter_scripts": "taproot-noordinals", + "block_filter_use_zeroed_key": true, + "mempool_golomb_filter_p": 20, + "mempool_filter_scripts": "taproot", + "mempool_filter_use_zeroed_key": false + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/bitcoin_testnet4.json b/configs/coins/bitcoin_testnet4.json new file mode 100644 index 0000000000..4478c3e135 --- /dev/null +++ b/configs/coins/bitcoin_testnet4.json @@ -0,0 +1,82 @@ +{ + "coin": { + "name": "Testnet4", + "shortcut": "TEST", + "label": "Bitcoin Testnet4", + "alias": "bitcoin_testnet4" + }, + "ports": { + "backend_rpc": 18029, + "backend_message_queue": 48329, + "blockbook_internal": 19029, + "blockbook_public": 19129 + }, + "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-bitcoin-testnet4", + "package_revision": "satoshilabs-1", + "system_user": "bitcoin", + "version": "29.2", + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "1fd58d0ae94b8a9e21bbaeab7d53395a44976e82bd5492b0a894826c135f9009", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/bitcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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/testnet4/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "bitcoin_testnet4.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://bitcoincore.org/bin/bitcoin-core-29.2/bitcoin-29.2-aarch64-linux-gnu.tar.gz", + "verification_source": "f88f72a3c5bf526581aae573be8c1f62133eaecfe3d34646c9ffca7b79dfdc7a" + } + } + }, + "blockbook": { + "package_name": "blockbook-bitcoin-testnet4", + "system_user": "blockbook-bitcoin", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-enablesubnewtx -extendedindex", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 10000, + "xpub_magic": 70617039, + "xpub_magic_segwit_p2sh": 71979618, + "xpub_magic_segwit_native": 73342198, + "slip44": 1, + "additional_params": { + "alternative_estimate_fee": "mempoolspace", + "alternative_estimate_fee_params": "{\"url\": \"https://mempool.space/testnet4/api/v1/fees/recommended\", \"periodSeconds\": 60}", + "block_golomb_filter_p": 20, + "block_filter_scripts": "taproot-noordinals", + "block_filter_use_zeroed_key": true, + "mempool_golomb_filter_p": 20, + "mempool_filter_scripts": "taproot", + "mempool_filter_use_zeroed_key": false + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/bitcore.json b/configs/coins/bitcore.json index 193ee7d2c0..4ebb9f2671 100644 --- a/configs/coins/bitcore.json +++ b/configs/coins/bitcore.json @@ -1,72 +1,72 @@ { - "coin": { - "name": "Bitcore", - "shortcut": "BTX", - "label": "Bitcore", - "alias": "bitcore" - }, - "ports": { - "backend_rpc": 8054, - "backend_message_queue": 38354, - "blockbook_internal": 9054, - "blockbook_public": 9154 - }, - "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-bitcore", - "package_revision": "satoshilabs-1", - "system_user": "bitcore", - "version": "0.15.2.1", - "binary_url": "https://github.com/dalijolijo/BitCore/releases/download/0.15.2.1/bitcore-0.15.2.1-x86_64-linux-gnu_no-wallet.tar.gz", - "verification_type": "sha256", - "verification_source": "4e47b33d5fa7d67151c9860f4cd19c99a55d42b27c170bd2391988c67aa24fc8", - "extract_command": "tar -C backend -xf", - "exclude_files": [ - "bin/bitcore-qt", - "bin/test_bitcore-qt", - "bin/bench_bitcore", - "bin/test_bitcore" - ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcored -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" + "coin": { + "name": "Bitcore", + "shortcut": "BTX", + "label": "Bitcore", + "alias": "bitcore" + }, + "ports": { + "backend_rpc": 8054, + "backend_message_queue": 38354, + "blockbook_internal": 9054, + "blockbook_public": 9154 + }, + "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-bitcore", + "package_revision": "satoshilabs-1", + "system_user": "bitcore", + "version": "0.15.2.1", + "binary_url": "https://github.com/dalijolijo/BitCore/releases/download/0.15.2.1/bitcore-0.15.2.1-x86_64-linux-gnu_no-wallet.tar.gz", + "verification_type": "sha256", + "verification_source": "4e47b33d5fa7d67151c9860f4cd19c99a55d42b27c170bd2391988c67aa24fc8", + "extract_command": "tar -C backend -xf", + "exclude_files": [ + "bin/bitcore-qt", + "bin/test_bitcore-qt", + "bin/bench_bitcore", + "bin/test_bitcore" + ], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcored -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-bitcore", + "system_user": "blockbook-bitcore", + "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, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"bitcore\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "LIMXTEC", + "package_maintainer_email": "info@bitcore.cc" } - }, - "blockbook": { - "package_name": "blockbook-bitcore", - "system_user": "blockbook-bitcore", - "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, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"bitcore\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "LIMXTEC", - "package_maintainer_email": "info@bitcore.cc" - } } diff --git a/configs/coins/bsc.json b/configs/coins/bsc.json new file mode 100644 index 0000000000..db599b3ac6 --- /dev/null +++ b/configs/coins/bsc.json @@ -0,0 +1,72 @@ +{ + "coin": { + "name": "BNB Smart Chain", + "shortcut": "BNB", + "network": "BSC", + "label": "BNB Smart Chain", + "alias": "bsc" + }, + "ports": { + "backend_rpc": 8064, + "backend_p2p": 38364, + "backend_http": 8164, + "blockbook_internal": 9064, + "blockbook_public": 9164 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-bsc", + "package_revision": "satoshilabs-1", + "system_user": "bsc", + "version": "1.1.23", + "binary_url": "https://github.com/bnb-chain/bsc/releases/download/v1.1.23/geth_linux", + "verification_type": "sha256", + "verification_source": "6636c40d4e82017257467ab2cfc88b11990cf3bb35faeec9c5194ab90009a81f", + "extract_command": "mv ${ARCHIVE} backend/geth_linux && chmod +x backend/geth_linux && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bsc_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "bsc.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "wget https://github.com/bnb-chain/bsc/releases/download/v1.1.23/mainnet.zip -O {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/mainnet.zip && unzip -o -d {{.Env.BackendInstallPath}}/{{.Coin.Alias}} {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/mainnet.zip && rm -f {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/mainnet.zip && sed -i -e '/\\[Node.LogConfig\\]/,+5d' {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/config.toml", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/bnb-chain/bsc/releases/download/v1.1.23/geth-linux-arm64", + "verification_source": "74105d6b9b8483a92ab8311784315c5f65dac2213004e0b1433cdf9127bced35" + } + } + }, + "blockbook": { + "package_name": "blockbook-bsc", + "system_user": "blockbook-bsc", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-dbcache=1500000000 -workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "additional_params": { + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"binancecoin\",\"platformIdentifier\": \"binance-smart-chain\",\"platformVsCurrency\": \"bnb\",\"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/bsc_archive.json b/configs/coins/bsc_archive.json new file mode 100644 index 0000000000..7fbc2ae610 --- /dev/null +++ b/configs/coins/bsc_archive.json @@ -0,0 +1,79 @@ +{ + "coin": { + "name": "BNB Smart Chain Archive", + "shortcut": "BNB", + "network": "BSC", + "label": "BNB Smart Chain", + "alias": "bsc_archive" + }, + "ports": { + "backend_rpc": 8065, + "backend_p2p": 38365, + "backend_http": 8165, + "blockbook_internal": 9065, + "blockbook_public": 9165 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 240 + }, + "backend": { + "package_name": "backend-bsc-archive", + "package_revision": "satoshilabs-1", + "system_user": "bsc", + "version": "1.1.23", + "binary_url": "https://github.com/bnb-chain/bsc/releases/download/v1.1.23/geth_linux", + "verification_type": "sha256", + "verification_source": "6636c40d4e82017257467ab2cfc88b11990cf3bb35faeec9c5194ab90009a81f", + "extract_command": "mv ${ARCHIVE} backend/geth_linux && chmod +x backend/geth_linux && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bsc_archive_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "bsc_archive.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "wget https://github.com/bnb-chain/bsc/releases/download/v1.1.23/mainnet.zip -O {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/mainnet.zip && unzip -o -d {{.Env.BackendInstallPath}}/{{.Coin.Alias}} {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/mainnet.zip && rm -f {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/mainnet.zip && sed -i -e '/\\[Node.LogConfig\\]/,+5d' {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/config.toml", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/bnb-chain/bsc/releases/download/v1.1.23/geth-linux-arm64", + "verification_source": "74105d6b9b8483a92ab8311784315c5f65dac2213004e0b1433cdf9127bced35" + } + } + }, + "blockbook": { + "package_name": "blockbook-bsc-archive", + "system_user": "blockbook-bsc", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-dbcache=1500000000 -workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 600, + "additional_params": { + "address_aliases": true, + "eip1559Fees": true, + "alternative_estimate_fee": "infura-disabled", + "alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/56/suggestedGasFees\", \"periodSeconds\": 60}", + "mempoolTxTimeoutHours": 48, + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "disableMempoolSync": true, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"binancecoin\",\"platformIdentifier\": \"binance-smart-chain\",\"platformVsCurrency\": \"bnb\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/dash.json b/configs/coins/dash.json index 9b4dc6d2a7..255325ef44 100644 --- a/configs/coins/dash.json +++ b/configs/coins/dash.json @@ -1,70 +1,70 @@ { - "coin": { - "name": "Dash", - "shortcut": "DASH", - "label": "Dash", - "alias": "dash" - }, - "ports": { - "backend_rpc": 8033, - "backend_message_queue": 38333, - "blockbook_internal": 9033, - "blockbook_public": 9133 - }, - "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-dash", - "package_revision": "satoshilabs-1", - "system_user": "dash", - "version": "18.2.1", - "binary_url": "https://github.com/dashpay/dash/releases/download/v18.2.1/dashcore-18.2.1-x86_64-linux-gnu.tar.gz", - "verification_type": "gpg-sha256", - "verification_source": "https://github.com/dashpay/dash/releases/download/v18.2.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 -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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": true, - "server_config_file": "bitcoin_like.conf", - "client_config_file": "bitcoin_like_client.conf", - "additional_params": { - "mempoolexpiry": 72 + "coin": { + "name": "Dash", + "shortcut": "DASH", + "label": "Dash", + "alias": "dash" + }, + "ports": { + "backend_rpc": 8033, + "backend_message_queue": 38333, + "blockbook_internal": 9033, + "blockbook_public": 9133 + }, + "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-dash", + "package_revision": "satoshilabs-1", + "system_user": "dash", + "version": "22.0.0", + "binary_url": "https://github.com/dashpay/dash/releases/download/v22.0.0/dashcore-22.0.0-x86_64-linux-gnu.tar.gz", + "verification_type": "gpg-sha256", + "verification_source": "https://github.com/dashpay/dash/releases/download/v22.0.0/SHA256SUMS.asc", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/dash-qt"], + "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", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "bitcoin_like.conf", + "client_config_file": "bitcoin_like_client.conf", + "additional_params": { + "mempoolexpiry": 72 + } + }, + "blockbook": { + "package_name": "blockbook-dash", + "system_user": "blockbook-dash", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "", + "block_chain": { + "parse": true, + "subversion": "/Dash Core:0.17.0.3/", + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "xpub_magic": 50221772, + "slip44": 5, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"dash\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT Admin", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-dash", - "system_user": "blockbook-dash", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "", - "block_chain": { - "parse": true, - "subversion": "/Dash Core:0.17.0.3/", - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 300, - "xpub_magic": 50221772, - "slip44": 5, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"dash\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "IT Admin", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/dash_testnet.json b/configs/coins/dash_testnet.json index be6f74f0e4..081381a6f6 100644 --- a/configs/coins/dash_testnet.json +++ b/configs/coins/dash_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-dash-testnet", "package_revision": "satoshilabs-1", "system_user": "dash", - "version": "18.2.1", - "binary_url": "https://github.com/dashpay/dash/releases/download/v18.2.1/dashcore-18.2.1-x86_64-linux-gnu.tar.gz", + "version": "22.0.0", + "binary_url": "https://github.com/dashpay/dash/releases/download/v22.0.0/dashcore-22.0.0-x86_64-linux-gnu.tar.gz", "verification_type": "gpg-sha256", - "verification_source": "https://github.com/dashpay/dash/releases/download/v18.2.1/SHA256SUMS.asc", + "verification_source": "https://github.com/dashpay/dash/releases/download/v22.0.0/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/dash-qt" diff --git a/configs/coins/digibyte.json b/configs/coins/digibyte.json index 0c66750f39..f407545e84 100644 --- a/configs/coins/digibyte.json +++ b/configs/coins/digibyte.json @@ -1,71 +1,71 @@ { - "coin": { - "name": "DigiByte", - "shortcut": "DGB", - "label": "DigiByte", - "alias": "digibyte" - }, - "ports": { - "backend_rpc": 8042, - "backend_message_queue": 38342, - "blockbook_internal": 9042, - "blockbook_public": 9142 - }, - "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", - "package_revision": "satoshilabs-1", - "system_user": "digibyte", - "version": "7.17.3", - "binary_url": "https://github.com/digibyte-core/digibyte/releases/download/v7.17.3/digibyte-7.17.3-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "b5cd8f590d359e4846dd5cbe60751221e54d773a6227ea9686d17c4890057f46", - "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", - "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" + "coin": { + "name": "DigiByte", + "shortcut": "DGB", + "label": "DigiByte", + "alias": "digibyte" + }, + "ports": { + "backend_rpc": 8042, + "backend_message_queue": 38342, + "blockbook_internal": 9042, + "blockbook_public": 9142 + }, + "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", + "package_revision": "satoshilabs-1", + "system_user": "digibyte", + "version": "7.17.3", + "binary_url": "https://github.com/digibyte-core/digibyte/releases/download/v7.17.3/digibyte-7.17.3-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "b5cd8f590d359e4846dd5cbe60751221e54d773a6227ea9686d17c4890057f46", + "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", + "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-digibyte", + "system_user": "blockbook-digibyte", + "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": 20, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"digibyte\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "Martin Bohm", + "package_maintainer_email": "martin.bohm@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-digibyte", - "system_user": "blockbook-digibyte", - "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": 20, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"digibyte\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "Martin Bohm", - "package_maintainer_email": "martin.bohm@satoshilabs.com" - } } diff --git a/configs/coins/dogecoin.json b/configs/coins/dogecoin.json index 5b8248422e..f120043288 100644 --- a/configs/coins/dogecoin.json +++ b/configs/coins/dogecoin.json @@ -1,79 +1,79 @@ { - "coin": { - "name": "Dogecoin", - "shortcut": "DOGE", - "label": "Dogecoin", - "alias": "dogecoin" - }, - "ports": { - "backend_rpc": 8038, - "backend_message_queue": 38338, - "blockbook_internal": 9038, - "blockbook_public": 9138 - }, - "ipc": { - "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_user": "rpc", - "rpc_pass": "rpcp", - "rpc_timeout": 25, - "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" - }, - "backend": { - "package_name": "backend-dogecoin", - "package_revision": "satoshilabs-1", - "system_user": "dogecoin", - "version": "1.14.6", - "binary_url": "https://github.com/dogecoin/dogecoin/releases/download/v1.14.6/dogecoin-1.14.6-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "fe9c9cdab946155866a5bd5a5127d2971a9eed3e0b65fb553fe393ad1daaebb0", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/dogecoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/dogecoind -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": false, - "mainnet": true, - "server_config_file": "bitcoin_like.conf", - "client_config_file": "bitcoin_like_client.conf", - "additional_params": { - "discover": 0, - "rpcthreads": 16, - "upnp": 0, - "whitelist": "127.0.0.1" + "coin": { + "name": "Dogecoin", + "shortcut": "DOGE", + "label": "Dogecoin", + "alias": "dogecoin" }, - "platforms": { - "arm64": { - "binary_url": "https://github.com/dogecoin/dogecoin/releases/download/v1.14.6/dogecoin-1.14.6-aarch64-linux-gnu.tar.gz", - "verification_source": "87419c29607b2612746fccebd694037e4be7600fc32198c4989f919be20952db", - "exclude_files": [] - } - } - }, - "blockbook": { - "package_name": "blockbook-dogecoin", - "system_user": "blockbook-dogecoin", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "-resyncindexperiod=30011 -resyncmempoolperiod=2011", - "block_chain": { - "parse": true, - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 300, - "xpub_magic": 49990397, - "slip44": 3, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"dogecoin\", \"periodSeconds\": 900}" - } + "ports": { + "backend_rpc": 8038, + "backend_message_queue": 38338, + "blockbook_internal": 9038, + "blockbook_public": 9138 + }, + "ipc": { + "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_user": "rpc", + "rpc_pass": "rpcp", + "rpc_timeout": 25, + "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" + }, + "backend": { + "package_name": "backend-dogecoin", + "package_revision": "satoshilabs-1", + "system_user": "dogecoin", + "version": "1.14.9", + "binary_url": "https://github.com/dogecoin/dogecoin/releases/download/v1.14.9/dogecoin-1.14.9-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "4f227117b411a7c98622c970986e27bcfc3f547a72bef65e7d9e82989175d4f8", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/dogecoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/dogecoind -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": false, + "mainnet": true, + "server_config_file": "bitcoin_like.conf", + "client_config_file": "bitcoin_like_client.conf", + "additional_params": { + "discover": 0, + "rpcthreads": 16, + "upnp": 0, + "whitelist": "127.0.0.1" + }, + "platforms": { + "arm64": { + "binary_url": "https://github.com/dogecoin/dogecoin/releases/download/v1.14.9/dogecoin-1.14.9-aarch64-linux-gnu.tar.gz", + "verification_source": "6928c895a20d0bcb6d5c7dcec753d35c884a471aaf8ad4242a89a96acb4f2985", + "exclude_files": [] + } + } + }, + "blockbook": { + "package_name": "blockbook-dogecoin", + "system_user": "blockbook-dogecoin", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-resyncindexperiod=30011 -resyncmempoolperiod=2011", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "xpub_magic": 49990397, + "slip44": 3, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"dogecoin\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT Admin", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT Admin", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/dogecoin_testnet.json b/configs/coins/dogecoin_testnet.json index 115ac63c79..8103ba70db 100644 --- a/configs/coins/dogecoin_testnet.json +++ b/configs/coins/dogecoin_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-dogecoin-testnet", "package_revision": "satoshilabs-1", "system_user": "dogecoin", - "version": "1.14.6", - "binary_url": "https://github.com/dogecoin/dogecoin/releases/download/v1.14.6/dogecoin-1.14.6-x86_64-linux-gnu.tar.gz", + "version": "1.14.9", + "binary_url": "https://github.com/dogecoin/dogecoin/releases/download/v1.14.9/dogecoin-1.14.9-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "fe9c9cdab946155866a5bd5a5127d2971a9eed3e0b65fb553fe393ad1daaebb0", + "verification_source": "4f227117b411a7c98622c970986e27bcfc3f547a72bef65e7d9e82989175d4f8", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/dogecoin-qt" @@ -47,8 +47,8 @@ }, "platforms": { "arm64": { - "binary_url": "https://github.com/dogecoin/dogecoin/releases/download/v1.14.6/dogecoin-1.14.6-aarch64-linux-gnu.tar.gz", - "verification_source": "87419c29607b2612746fccebd694037e4be7600fc32198c4989f919be20952db", + "binary_url": "https://github.com/dogecoin/dogecoin/releases/download/v1.14.9/dogecoin-1.14.9-aarch64-linux-gnu.tar.gz", + "verification_source": "6928c895a20d0bcb6d5c7dcec753d35c884a471aaf8ad4242a89a96acb4f2985", "exclude_files": [] } } diff --git a/configs/coins/ecash.json b/configs/coins/ecash.json index 562bb01bdd..6eba1ca3f5 100644 --- a/configs/coins/ecash.json +++ b/configs/coins/ecash.json @@ -1,72 +1,72 @@ { - "coin": { - "name": "ECash", - "shortcut": "XEC", - "label": "eCash", - "alias": "ecash" - }, - "ports": { - "backend_rpc": 8097, - "backend_message_queue": 38397, - "blockbook_internal": 9097, - "blockbook_public": 9197 - }, - "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-ecash", - "package_revision": "satoshilabs-1", - "system_user": "ecash", - "version": "0.26.1", - "binary_url": "https://download.bitcoinabc.org/0.26.1/linux/bitcoin-abc-0.26.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "64c799b339b2aa03f50ac605f7df0586341ff5a2d74321b424f4fe35d37da0be", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/bitcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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": "bcash.conf", - "client_config_file": "bitcoin_like_client.conf", - "additional_params": { - "listen": 1, - "avalanche": 1 + "coin": { + "name": "ECash", + "shortcut": "XEC", + "label": "eCash", + "alias": "ecash" + }, + "ports": { + "backend_rpc": 8097, + "backend_message_queue": 38397, + "blockbook_internal": 9097, + "blockbook_public": 9197 + }, + "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-ecash", + "package_revision": "satoshilabs-1", + "system_user": "ecash", + "version": "0.27.3", + "binary_url": "https://download.bitcoinabc.org/0.27.3/linux/bitcoin-abc-0.27.3-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "390329fa9ad9e88319f5cf5239385268584116710144d6bc156fbcca7514710a", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/bitcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -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": "bcash.conf", + "client_config_file": "bitcoin_like_client.conf", + "additional_params": { + "listen": 1, + "avalanche": 1 + } + }, + "blockbook": { + "package_name": "blockbook-ecash", + "system_user": "blockbook-ecash", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "", + "block_chain": { + "parse": true, + "subversion": "/Bitcoin ABC:0.27.3(EB32.0)/", + "address_format": "cashaddr", + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 899, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ecash\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "eCash", + "package_maintainer_email": "contact@e.cash" } - }, - "blockbook": { - "package_name": "blockbook-ecash", - "system_user": "blockbook-ecash", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "", - "block_chain": { - "parse": true, - "subversion": "/Bitcoin ABC:0.26.1(EB32.0)/", - "address_format": "cashaddr", - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 300, - "xpub_magic": 76067358, - "slip44": 899, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ecash\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "eCash", - "package_maintainer_email": "contact@e.cash" - } } diff --git a/configs/coins/ethereum-classic.json b/configs/coins/ethereum-classic.json index e54b70ef40..dbba500b02 100644 --- a/configs/coins/ethereum-classic.json +++ b/configs/coins/ethereum-classic.json @@ -1,67 +1,67 @@ { - "coin": { - "name": "Ethereum Classic", - "shortcut": "ETC", - "label": "Ethereum Classic", - "alias": "ethereum-classic" - }, - "ports": { - "backend_rpc": 8037, - "backend_message_queue": 0, - "backend_p2p": 38337, - "backend_http": 8137, - "blockbook_internal": 9037, - "blockbook_public": 9137 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-classic", - "package_revision": "satoshilabs-1", - "system_user": "ethereum-classic", - "version": "1.12.10", - "binary_url": "https://github.com/etclabscore/core-geth/releases/download/v1.12.10/core-geth-linux-v1.12.10.zip", - "verification_type": "sha256", - "verification_source": "40f423fb19b36b9412388adb18353d78bfda31c71395be56a2c51772a12cbf81", - "extract_command": "unzip -d backend", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --classic --ipcdisable --txlookuplimit 0 --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --http --http.port {{.Ports.BackendHttp}} --http.addr 127.0.0.1 --http.corsdomain \"*\" 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": true, - "server_config_file": "", - "client_config_file": "" - }, - "blockbook": { - "package_name": "blockbook-ethereum-classic", - "system_user": "blockbook-ethereum-classic", - "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": 10000, - "additional_params": { - "address_aliases": true, - "mempoolTxTimeoutHours": 48, - "queryBackendOnMempoolResync": true, - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum-classic\", \"periodSeconds\": 900}", - "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" - } + "coin": { + "name": "Ethereum Classic", + "shortcut": "ETC", + "label": "Ethereum Classic", + "alias": "ethereum-classic" + }, + "ports": { + "backend_rpc": 8037, + "backend_message_queue": 0, + "backend_p2p": 38337, + "backend_http": 8137, + "blockbook_internal": 9037, + "blockbook_public": 9137 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-classic", + "package_revision": "satoshilabs-1", + "system_user": "ethereum-classic", + "version": "1.12.18", + "binary_url": "https://github.com/etclabscore/core-geth/releases/download/v1.12.18/core-geth-linux-v1.12.18.zip", + "verification_type": "sha256", + "verification_source": "2382a15a53ce364cb41d3985ff3c2941392d8898c6f869666a8d7d7914a5748a", + "extract_command": "unzip -d backend", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --classic --ipcdisable --txlookuplimit 0 --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --http --http.port {{.Ports.BackendHttp}} --http.addr 127.0.0.1 --http.corsdomain \"*\" 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", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-ethereum-classic", + "system_user": "blockbook-ethereum-classic", + "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": 10000, + "additional_params": { + "address_aliases": true, + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": true, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum-classic\", \"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/ethereum.json b/configs/coins/ethereum.json index e32587656b..ef0f93d283 100644 --- a/configs/coins/ethereum.json +++ b/configs/coins/ethereum.json @@ -1,73 +1,76 @@ { - "coin": { - "name": "Ethereum", - "shortcut": "ETH", - "label": "Ethereum", - "alias": "ethereum" - }, - "ports": { - "backend_rpc": 8036, - "backend_message_queue": 0, - "backend_p2p": 38336, - "backend_http": 8136, - "backend_authrpc": 8536, - "blockbook_internal": 9036, - "blockbook_public": 9136 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "1.11.2-73b01f40", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz", - "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --syncmode full --txlookuplimit 0 --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --ws.api \"eth,net,web3,debug,txpool\" --http --http.port {{.Ports.BackendHttp}} --http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": true, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz.asc" - } + "coin": { + "name": "Ethereum", + "shortcut": "ETH", + "label": "Ethereum", + "alias": "ethereum" + }, + "ports": { + "backend_rpc": 8036, + "backend_message_queue": 0, + "backend_p2p": 38336, + "backend_http": 8136, + "backend_authrpc": 8536, + "blockbook_internal": 9036, + "blockbook_public": 9136 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "3.2.1", + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_amd64.tar.gz", + "verification_type": "sha256", + "verification_source": "8b5444988667721f2b2ef1ab3098139c31f722492992939c110813408c39dc7c", + "extract_command": "tar -C backend --strip-components=1 -xf", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/erigon --chain mainnet --snap.keepblocks --db.size.limit 15TB --db.pagesize 16KB --prune.mode full --externalcl --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/erigon --port {{.Ports.BackendP2P}} --ws --ws.port {{.Ports.BackendRPC}} --http --http.port {{.Ports.BackendRPC}} --http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} --private.api.addr \"\" --torrent.port {{.Ports.BackendHttp}} --log.dir.path {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log.dir.prefix {{.Coin.Alias}}'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_arm64.tar.gz", + "verification_source": "19a91709dc3ddbe947c4f81e70cb1de49044954e21f441e9ea46b3696f21b57f" + } + } + }, + "blockbook": { + "package_name": "blockbook-ethereum", + "system_user": "blockbook-ethereum", + "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, + "additional_params": { + "consensusNodeVersion": "http://localhost:7536/eth/v1/node/version", + "address_aliases": true, + "eip1559Fees": true, + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-ethereum", - "system_user": "blockbook-ethereum", - "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, - "additional_params": { - "consensusNodeVersion": "http://localhost:7536/eth/v1/node/version", - "mempoolTxTimeoutHours": 48, - "queryBackendOnMempoolResync": false, - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} +} \ No newline at end of file diff --git a/configs/coins/ethereum_archive.json b/configs/coins/ethereum_archive.json index 4ba2ac43fb..abe1fc4733 100644 --- a/configs/coins/ethereum_archive.json +++ b/configs/coins/ethereum_archive.json @@ -1,76 +1,79 @@ { - "coin": { - "name": "Ethereum Archive", - "shortcut": "ETH", - "label": "Ethereum", - "alias": "ethereum_archive" - }, - "ports": { - "backend_rpc": 8016, - "backend_message_queue": 0, - "backend_p2p": 38316, - "backend_http": 8116, - "backend_authrpc": 8516, - "blockbook_internal": 9016, - "blockbook_public": 9116 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-archive", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "1.11.2-73b01f40", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz", - "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --syncmode full --gcmode archive --txlookuplimit 0 --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --ws.api \"eth,net,web3,debug,txpool\" --http --http.port {{.Ports.BackendHttp}} --http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": true, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz.asc" - } + "coin": { + "name": "Ethereum Archive", + "shortcut": "ETH", + "label": "Ethereum", + "alias": "ethereum_archive" + }, + "ports": { + "backend_rpc": 8016, + "backend_message_queue": 0, + "backend_p2p": 38316, + "backend_http": 8116, + "backend_authrpc": 8516, + "blockbook_internal": 9016, + "blockbook_public": 9116 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-archive", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "3.2.1", + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_amd64.tar.gz", + "verification_type": "sha256", + "verification_source": "8b5444988667721f2b2ef1ab3098139c31f722492992939c110813408c39dc7c", + "extract_command": "tar -C backend --strip-components=1 -xf", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/erigon --chain mainnet --snap.keepblocks --db.size.limit 15TB --db.pagesize 16KB --prune.mode archive --externalcl --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/erigon --port {{.Ports.BackendP2P}} --ws --ws.port {{.Ports.BackendRPC}} --http --http.port {{.Ports.BackendRPC}} --http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} --private.api.addr \"\" --torrent.port {{.Ports.BackendHttp}} --log.dir.path {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log.dir.prefix {{.Coin.Alias}}'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_arm64.tar.gz", + "verification_source": "19a91709dc3ddbe947c4f81e70cb1de49044954e21f441e9ea46b3696f21b57f" + } + } + }, + "blockbook": { + "package_name": "blockbook-ethereum-archive", + "system_user": "blockbook-ethereum", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 600, + "additional_params": { + "consensusNodeVersion": "http://localhost:7516/eth/v1/node/version", + "address_aliases": true, + "eip1559Fees": true, + "alternative_estimate_fee": "infura", + "alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/1/suggestedGasFees\", \"periodSeconds\": 8}", + "mempoolTxTimeoutHours": 48, + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-ethereum-archive", - "system_user": "blockbook-ethereum", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "-workers=16", - "block_chain": { - "parse": true, - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 600, - "additional_params": { - "consensusNodeVersion": "http://localhost:7516/eth/v1/node/version", - "address_aliases": true, - "mempoolTxTimeoutHours": 48, - "processInternalTransactions": true, - "queryBackendOnMempoolResync": false, - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", - "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} +} \ No newline at end of file diff --git a/configs/coins/ethereum_archive_consensus.json b/configs/coins/ethereum_archive_consensus.json index c76976d824..e5f1f154a1 100644 --- a/configs/coins/ethereum_archive_consensus.json +++ b/configs/coins/ethereum_archive_consensus.json @@ -1,48 +1,48 @@ { - "coin": { - "name": "Ethereum Archive", - "shortcut": "ETH", - "label": "Ethereum", - "alias": "ethereum_archive_consensus", - "execution_alias": "ethereum_archive" - }, - "ports": { - "backend_rpc": 8016, - "backend_message_queue": 0, - "backend_p2p": 38316, - "backend_http": 8116, - "backend_authrpc": 8516, - "blockbook_internal": 9016, - "blockbook_public": 9116 - }, - "backend": { - "package_name": "backend-ethereum-archive-consensus", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "3.2.0", - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.2.0/beacon-chain-v3.2.0-linux-amd64", - "verification_type": "sha256", - "verification_source": "e57fed14bc15a62ab38a6605a8f93c2cf29fbd7a6333dd3ad72781c3778e36fc", - "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --mainnet --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=7516 --rpc-port=7517 --monitoring-port=7518 --p2p-tcp-port=3516 --p2p-udp-port=2516 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_archive/backend/geth/jwtsecret 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.1.2/beacon-chain-v3.1.2-linux-arm64", - "verification_source": "1701df47dbb6598a9215f82a313e1531c211bb912618dc3d0cd33e6e67c5ebb5" - } + "coin": { + "name": "Ethereum Archive", + "shortcut": "ETH", + "label": "Ethereum", + "alias": "ethereum_archive_consensus", + "execution_alias": "ethereum_archive" + }, + "ports": { + "backend_rpc": 8016, + "backend_message_queue": 0, + "backend_p2p": 38316, + "backend_http": 8116, + "backend_authrpc": 8516, + "blockbook_internal": 9016, + "blockbook_public": 9116 + }, + "backend": { + "package_name": "backend-ethereum-archive-consensus", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "6.1.2", + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-amd64", + "verification_type": "sha256", + "verification_source": "45d34c817db22e34ae12ebe733d281db76a349e3be439952f9e1dd50f10bc2b1", + "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --mainnet --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=7516 --rpc-port=7517 --monitoring-port=7518 --p2p-tcp-port=3516 --p2p-udp-port=2516 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_archive/backend/erigon/jwt.hex 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", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-arm64", + "verification_source": "2651f1407bb842e7f03dc00ba58990ee3345865cb5d474a3f76a968db5e57c02" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/ethereum_consensus.json b/configs/coins/ethereum_consensus.json index 437733e396..4288d87db7 100644 --- a/configs/coins/ethereum_consensus.json +++ b/configs/coins/ethereum_consensus.json @@ -1,48 +1,48 @@ { - "coin": { - "name": "Ethereum", - "shortcut": "ETH", - "label": "Ethereum", - "alias": "ethereum_consensus", - "execution_alias": "ethereum" - }, - "ports": { - "backend_rpc": 8036, - "backend_message_queue": 0, - "backend_p2p": 38336, - "backend_http": 8136, - "backend_authrpc": 8536, - "blockbook_internal": 9036, - "blockbook_public": 9136 - }, - "backend": { - "package_name": "backend-ethereum-consensus", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "3.2.0", - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.2.0/beacon-chain-v3.2.0-linux-amd64", - "verification_type": "sha256", - "verification_source": "e57fed14bc15a62ab38a6605a8f93c2cf29fbd7a6333dd3ad72781c3778e36fc", - "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --mainnet --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=7536 --rpc-port=7537 --monitoring-port=7538 --p2p-tcp-port=3536 --p2p-udp-port=2536 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum/backend/geth/jwtsecret 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.1.2/beacon-chain-v3.1.2-linux-arm64", - "verification_source": "1701df47dbb6598a9215f82a313e1531c211bb912618dc3d0cd33e6e67c5ebb5" - } + "coin": { + "name": "Ethereum", + "shortcut": "ETH", + "label": "Ethereum", + "alias": "ethereum_consensus", + "execution_alias": "ethereum" + }, + "ports": { + "backend_rpc": 8036, + "backend_message_queue": 0, + "backend_p2p": 38336, + "backend_http": 8136, + "backend_authrpc": 8536, + "blockbook_internal": 9036, + "blockbook_public": 9136 + }, + "backend": { + "package_name": "backend-ethereum-consensus", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "6.1.2", + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-amd64", + "verification_type": "sha256", + "verification_source": "45d34c817db22e34ae12ebe733d281db76a349e3be439952f9e1dd50f10bc2b1", + "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --mainnet --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=7536 --rpc-port=7537 --monitoring-port=7538 --p2p-tcp-port=3536 --p2p-udp-port=2536 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum/backend/erigon/jwt.hex 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", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-arm64", + "verification_source": "2651f1407bb842e7f03dc00ba58990ee3345865cb5d474a3f76a968db5e57c02" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/ethereum_testnet_goerli.json b/configs/coins/ethereum_testnet_goerli.json deleted file mode 100644 index b4bc2a5fe3..0000000000 --- a/configs/coins/ethereum_testnet_goerli.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "coin": { - "name": "Ethereum Testnet Goerli", - "shortcut": "tGOR", - "label": "Ethereum Goerli", - "alias": "ethereum_testnet_goerli" - }, - "ports": { - "backend_rpc": 18026, - "backend_message_queue": 0, - "backend_p2p": 48326, - "backend_http": 18126, - "backend_authrpc": 18526, - "blockbook_internal": 19026, - "blockbook_public": 19126 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-testnet-goerli", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "1.11.2-73b01f40", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz", - "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --goerli --syncmode full --txlookuplimit 0 --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --ws.api \"eth,net,web3,debug,txpool\" --http --http.port {{.Ports.BackendHttp}} -http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz.asc" - } - } - }, - "blockbook": { - "package_name": "blockbook-ethereum-testnet-goerli", - "system_user": "blockbook-ethereum", - "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": 3000, - "additional_params": { - "consensusNodeVersion": "http://localhost:17526/eth/v1/node/version", - "mempoolTxTimeoutHours": 12, - "queryBackendOnMempoolResync": false - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} diff --git a/configs/coins/ethereum_testnet_goerli_archive.json b/configs/coins/ethereum_testnet_goerli_archive.json deleted file mode 100644 index b444b33564..0000000000 --- a/configs/coins/ethereum_testnet_goerli_archive.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "coin": { - "name": "Ethereum Testnet Goerli Archive", - "shortcut": "tGOR", - "label": "Ethereum Goerli", - "alias": "ethereum_testnet_goerli_archive" - }, - "ports": { - "backend_rpc": 18006, - "backend_message_queue": 0, - "backend_p2p": 48306, - "backend_http": 18106, - "backend_authrpc": 18506, - "blockbook_internal": 19006, - "blockbook_public": 19106 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-testnet-goerli-archive", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "1.11.2-73b01f40", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz", - "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --goerli --syncmode full --gcmode archive --txlookuplimit 0 --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --ws.api \"eth,net,web3,debug,txpool\" --http --http.port {{.Ports.BackendHttp}} -http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz.asc" - } - } - }, - "blockbook": { - "package_name": "blockbook-ethereum-testnet-goerli-archive", - "system_user": "blockbook-ethereum", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "-workers=16", - "block_chain": { - "parse": true, - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 3000, - "additional_params": { - "consensusNodeVersion": "http://localhost:17506/eth/v1/node/version", - "address_aliases": true, - "mempoolTxTimeoutHours": 12, - "processInternalTransactions": true, - "queryBackendOnMempoolResync": false, - "fiat_rates-disabled": "coingecko", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", - "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} diff --git a/configs/coins/ethereum_testnet_goerli_archive_consensus.json b/configs/coins/ethereum_testnet_goerli_archive_consensus.json deleted file mode 100644 index 72ac980769..0000000000 --- a/configs/coins/ethereum_testnet_goerli_archive_consensus.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "coin": { - "name": "Ethereum Testnet Goerli Archive", - "shortcut": "tGOR", - "label": "Ethereum Goerli", - "alias": "ethereum_testnet_goerli_archive_consensus", - "execution_alias": "ethereum_testnet_goerli_archive" - }, - "ports": { - "backend_rpc": 18006, - "backend_message_queue": 0, - "backend_p2p": 48306, - "backend_http": 18106, - "backend_authrpc": 18506, - "blockbook_internal": 19006, - "blockbook_public": 19106 - }, - "backend": { - "package_name": "backend-ethereum-testnet-goerli-archive-consensus", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "3.2.0", - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.2.0/beacon-chain-v3.2.0-linux-amd64", - "verification_type": "sha256", - "verification_source": "e57fed14bc15a62ab38a6605a8f93c2cf29fbd7a6333dd3ad72781c3778e36fc", - "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --prater --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17506 --rpc-port=17507 --monitoring-port=17508 --p2p-tcp-port=13506 --p2p-udp-port=12506 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_goerli_archive/backend/geth/jwtsecret --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", - "postinst_script_template": "wget https://github.com/eth-clients/eth2-networks/raw/master/shared/prater/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", - "service_type": "simple", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.1.2/beacon-chain-v3.1.2-linux-arm64", - "verification_source": "1701df47dbb6598a9215f82a313e1531c211bb912618dc3d0cd33e6e67c5ebb5" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} diff --git a/configs/coins/ethereum_testnet_goerli_consensus.json b/configs/coins/ethereum_testnet_goerli_consensus.json deleted file mode 100644 index 49d0fc821e..0000000000 --- a/configs/coins/ethereum_testnet_goerli_consensus.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "coin": { - "name": "Ethereum Testnet Goerli", - "shortcut": "tGOR", - "label": "Ethereum Goerli", - "alias": "ethereum_testnet_goerli_consensus", - "execution_alias": "ethereum_testnet_goerli" - }, - "ports": { - "backend_rpc": 18026, - "backend_message_queue": 0, - "backend_p2p": 48326, - "backend_http": 18126, - "backend_authrpc": 18526, - "blockbook_internal": 19026, - "blockbook_public": 19126 - }, - "backend": { - "package_name": "backend-ethereum-testnet-goerli-consensus", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "3.2.0", - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.2.0/beacon-chain-v3.2.0-linux-amd64", - "verification_type": "sha256", - "verification_source": "e57fed14bc15a62ab38a6605a8f93c2cf29fbd7a6333dd3ad72781c3778e36fc", - "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --prater --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17526 --rpc-port=17527 --monitoring-port=17528 --p2p-tcp-port=13526 --p2p-udp-port=12526 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_goerli/backend/geth/jwtsecret --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", - "postinst_script_template": "wget https://github.com/eth-clients/eth2-networks/raw/master/shared/prater/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", - "service_type": "simple", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.1.2/beacon-chain-v3.1.2-linux-arm64", - "verification_source": "1701df47dbb6598a9215f82a313e1531c211bb912618dc3d0cd33e6e67c5ebb5" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} diff --git a/configs/coins/ethereum_testnet_hoodi.json b/configs/coins/ethereum_testnet_hoodi.json new file mode 100644 index 0000000000..21bbee5ab7 --- /dev/null +++ b/configs/coins/ethereum_testnet_hoodi.json @@ -0,0 +1,71 @@ +{ + "coin": { + "name": "Ethereum Testnet Hoodi", + "shortcut": "tHOD", + "label": "Ethereum Hoodi", + "alias": "ethereum_testnet_hoodi" + }, + "ports": { + "backend_rpc": 18006, + "backend_message_queue": 0, + "backend_p2p": 48306, + "backend_http": 18106, + "backend_authrpc": 18506, + "blockbook_internal": 19006, + "blockbook_public": 19106 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-testnet-hoodi", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "3.2.1", + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_amd64.tar.gz", + "verification_type": "sha256", + "verification_source": "8b5444988667721f2b2ef1ab3098139c31f722492992939c110813408c39dc7c", + "extract_command": "tar -C backend --strip-components=1 -xf", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/erigon --chain hoodi --snap.keepblocks --db.size.limit 15TB --db.pagesize 16KB --prune.mode full --externalcl --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/erigon --port {{.Ports.BackendP2P}} --ws --ws.port {{.Ports.BackendRPC}} --http --http.port {{.Ports.BackendRPC}} --http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} --private.api.addr \"\" --torrent.port {{.Ports.BackendHttp}} --log.dir.path {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log.dir.prefix {{.Coin.Alias}}'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_arm64.tar.gz", + "verification_source": "19a91709dc3ddbe947c4f81e70cb1de49044954e21f441e9ea46b3696f21b57f" + } + } + }, + "blockbook": { + "package_name": "blockbook-ethereum-testnet-hoodi", + "system_user": "blockbook-ethereum", + "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": 3000, + "additional_params": { + "consensusNodeVersion": "http://localhost:17506/eth/v1/node/version", + "eip1559Fees": true, + "mempoolTxTimeoutHours": 12, + "queryBackendOnMempoolResync": false + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/ethereum_testnet_hoodi_archive.json b/configs/coins/ethereum_testnet_hoodi_archive.json new file mode 100644 index 0000000000..b6c0f5a2de --- /dev/null +++ b/configs/coins/ethereum_testnet_hoodi_archive.json @@ -0,0 +1,77 @@ +{ + "coin": { + "name": "Ethereum Testnet Hoodi Archive", + "shortcut": "tHOD", + "label": "Ethereum Hoodi", + "alias": "ethereum_testnet_hoodi_archive" + }, + "ports": { + "backend_rpc": 18026, + "backend_message_queue": 0, + "backend_p2p": 48326, + "backend_http": 18126, + "backend_torrent": 18126, + "backend_authrpc": 18526, + "blockbook_internal": 19026, + "blockbook_public": 19126 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-testnet-hoodi-archive", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "3.2.1", + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_amd64.tar.gz", + "verification_type": "sha256", + "verification_source": "8b5444988667721f2b2ef1ab3098139c31f722492992939c110813408c39dc7c", + "extract_command": "tar -C backend --strip-components=1 -xf", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/erigon --chain hoodi --snap.keepblocks --db.size.limit 15TB --db.pagesize 16KB --prune.mode archive --externalcl --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/erigon --port {{.Ports.BackendP2P}} --ws --ws.port {{.Ports.BackendRPC}} --http --http.port {{.Ports.BackendRPC}} --http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} --private.api.addr \"\" --torrent.port {{.Ports.BackendHttp}} --log.dir.path {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log.dir.prefix {{.Coin.Alias}}'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_arm64.tar.gz", + "verification_source": "19a91709dc3ddbe947c4f81e70cb1de49044954e21f441e9ea46b3696f21b57f" + } + } + }, + "blockbook": { + "package_name": "blockbook-ethereum-testnet-hoodi-archive", + "system_user": "blockbook-ethereum", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 3000, + "additional_params": { + "consensusNodeVersion": "http://localhost:17526/eth/v1/node/version", + "address_aliases": true, + "eip1559Fees": true, + "mempoolTxTimeoutHours": 12, + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "fiat_rates-disabled": "coingecko", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/ethereum_testnet_hoodi_archive_consensus.json b/configs/coins/ethereum_testnet_hoodi_archive_consensus.json new file mode 100644 index 0000000000..8864249adc --- /dev/null +++ b/configs/coins/ethereum_testnet_hoodi_archive_consensus.json @@ -0,0 +1,52 @@ +{ + "coin": { + "name": "Ethereum Testnet Hoodi Archive", + "shortcut": "tHOD", + "label": "Ethereum Hoodi", + "alias": "ethereum_testnet_hoodi_archive_consensus", + "execution_alias": "ethereum_testnet_hoodi_archive" + }, + "ports": { + "backend_rpc": 18026, + "backend_message_queue": 0, + "backend_p2p": 48326, + "backend_http": 18126, + "backend_authrpc": 18526, + "blockbook_internal": 19026, + "blockbook_public": 19126 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-testnet-hoodi-archive-consensus", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "6.1.2", + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-amd64", + "verification_type": "sha256", + "verification_source": "45d34c817db22e34ae12ebe733d281db76a349e3be439952f9e1dd50f10bc2b1", + "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --hoodi --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17526 --rpc-port=17527 --monitoring-port=17528 --p2p-tcp-port=13626 --p2p-udp-port=12626 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_hoodi_archive/backend/erigon/jwt.hex --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "wget https://github.com/eth-clients/hoodi/raw/main/metadata/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-arm64", + "verification_source": "2651f1407bb842e7f03dc00ba58990ee3345865cb5d474a3f76a968db5e57c02" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/ethereum_testnet_hoodi_consensus.json b/configs/coins/ethereum_testnet_hoodi_consensus.json new file mode 100644 index 0000000000..1c50970658 --- /dev/null +++ b/configs/coins/ethereum_testnet_hoodi_consensus.json @@ -0,0 +1,52 @@ +{ + "coin": { + "name": "Ethereum Testnet Hoodi", + "shortcut": "tHOD", + "label": "Ethereum Hoodi", + "alias": "ethereum_testnet_hoodi_consensus", + "execution_alias": "ethereum_testnet_hoodi" + }, + "ports": { + "backend_rpc": 18006, + "backend_message_queue": 0, + "backend_p2p": 48306, + "backend_http": 18106, + "backend_authrpc": 18506, + "blockbook_internal": 19006, + "blockbook_public": 19106 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-testnet-hoodi-consensus", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "6.1.2", + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-amd64", + "verification_type": "sha256", + "verification_source": "45d34c817db22e34ae12ebe733d281db76a349e3be439952f9e1dd50f10bc2b1", + "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --hoodi --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17506 --rpc-port=17507 --monitoring-port=17508 --p2p-tcp-port=13506 --p2p-udp-port=12506 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_hoodi/backend/erigon/jwt.hex --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "wget https://github.com/eth-clients/holesky/raw/main/metadata/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-arm64", + "verification_source": "2651f1407bb842e7f03dc00ba58990ee3345865cb5d474a3f76a968db5e57c02" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/ethereum_testnet_ropsten.json b/configs/coins/ethereum_testnet_ropsten.json deleted file mode 100644 index ee91692ece..0000000000 --- a/configs/coins/ethereum_testnet_ropsten.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "coin": { - "name": "Ethereum Testnet Ropsten", - "shortcut": "tROP", - "label": "Ethereum Ropsten", - "alias": "ethereum_testnet_ropsten" - }, - "ports": { - "backend_rpc": 18036, - "backend_message_queue": 0, - "backend_p2p": 48336, - "backend_http": 18136, - "backend_authrpc": 18536, - "blockbook_internal": 19036, - "blockbook_public": 19136 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-testnet-ropsten", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "1.10.26-e5eb32ac", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.26-e5eb32ac.tar.gz", - "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.26-e5eb32ac.tar.gz.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --ropsten --syncmode full --txlookuplimit 0 --ipcdisable --cache 1024 --nat none --override.terminaltotaldifficulty 50000000000000000 --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --ws.api \"eth,net,web3,debug,txpool\" --http --http.port {{.Ports.BackendHttp}} -http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz.asc" - } - } - }, - "blockbook": { - "package_name": "blockbook-ethereum-testnet-ropsten", - "system_user": "blockbook-ethereum", - "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": 3000, - "additional_params": { - "consensusNodeVersion": "http://localhost:17536/eth/v1/node/version", - "mempoolTxTimeoutHours": 12, - "queryBackendOnMempoolResync": false - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} diff --git a/configs/coins/ethereum_testnet_ropsten_archive.json b/configs/coins/ethereum_testnet_ropsten_archive.json deleted file mode 100644 index 47408d1b27..0000000000 --- a/configs/coins/ethereum_testnet_ropsten_archive.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "coin": { - "name": "Ethereum Testnet Ropsten Archive", - "shortcut": "tROP", - "label": "Ethereum Ropsten", - "alias": "ethereum_testnet_ropsten_archive" - }, - "ports": { - "backend_rpc": 18016, - "backend_message_queue": 0, - "backend_p2p": 48316, - "backend_http": 18116, - "backend_authrpc": 18516, - "blockbook_internal": 19016, - "blockbook_public": 19116 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-testnet-ropsten-archive", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "1.10.26-e5eb32ac", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.26-e5eb32ac.tar.gz", - "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.26-e5eb32ac.tar.gz.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --ropsten --syncmode full --gcmode archive --txlookuplimit 0 --ipcdisable --cache 1024 --nat none --override.terminaltotaldifficulty 50000000000000000 --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --ws.api \"eth,net,web3,debug,txpool\" --http --http.port {{.Ports.BackendHttp}} -http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz.asc" - } - } - }, - "blockbook": { - "package_name": "blockbook-ethereum-testnet-ropsten-archive", - "system_user": "blockbook-ethereum", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "-workers=16", - "block_chain": { - "parse": true, - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 3000, - "additional_params": { - "consensusNodeVersion": "http://localhost:17516/eth/v1/node/version", - "address_aliases": true, - "mempoolTxTimeoutHours": 12, - "processInternalTransactions": true, - "queryBackendOnMempoolResync": false, - "fiat_rates-disabled": "coingecko", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", - "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} diff --git a/configs/coins/ethereum_testnet_ropsten_archive_consensus.json b/configs/coins/ethereum_testnet_ropsten_archive_consensus.json deleted file mode 100644 index a0137e7582..0000000000 --- a/configs/coins/ethereum_testnet_ropsten_archive_consensus.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "coin": { - "name": "Ethereum Testnet Ropsten Archive", - "shortcut": "tROP", - "label": "Ethereum Ropsten", - "alias": "ethereum_testnet_ropsten_archive_consensus", - "execution_alias": "ethereum_testnet_ropsten_archive" - }, - "ports": { - "backend_rpc": 18016, - "backend_message_queue": 0, - "backend_p2p": 48316, - "backend_http": 18116, - "backend_authrpc": 18516, - "blockbook_internal": 19016, - "blockbook_public": 19116 - }, - "backend": { - "package_name": "backend-ethereum-testnet-ropsten-archive-consensus", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "3.2.0", - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.2.0/beacon-chain-v3.2.0-linux-amd64", - "verification_type": "sha256", - "verification_source": "e57fed14bc15a62ab38a6605a8f93c2cf29fbd7a6333dd3ad72781c3778e36fc", - "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --ropsten --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17516 --rpc-port=17517 --monitoring-port=17518 --p2p-tcp-port=13516 --p2p-udp-port=12516 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_ropsten_archive/backend/geth/jwtsecret --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", - "postinst_script_template": "wget https://github.com/eth-clients/merge-testnets/raw/e4a6f0c181d24b28bc8651744f1d0e9ef74bda3f/ropsten-beacon-chain/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", - "service_type": "simple", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.1.2/beacon-chain-v3.1.2-linux-arm64", - "verification_source": "1701df47dbb6598a9215f82a313e1531c211bb912618dc3d0cd33e6e67c5ebb5" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} diff --git a/configs/coins/ethereum_testnet_ropsten_consensus.json b/configs/coins/ethereum_testnet_ropsten_consensus.json deleted file mode 100644 index 720784e21c..0000000000 --- a/configs/coins/ethereum_testnet_ropsten_consensus.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "coin": { - "name": "Ethereum Testnet Ropsten", - "shortcut": "tROP", - "label": "Ethereum Ropsten", - "alias": "ethereum_testnet_ropsten_consensus", - "execution_alias": "ethereum_testnet_ropsten" - }, - "ports": { - "backend_rpc": 18036, - "backend_message_queue": 0, - "backend_p2p": 48336, - "backend_http": 18136, - "backend_authrpc": 18536, - "blockbook_internal": 19036, - "blockbook_public": 19136 - }, - "backend": { - "package_name": "backend-ethereum-testnet-ropsten-consensus", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "3.2.0", - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.2.0/beacon-chain-v3.2.0-linux-amd64", - "verification_type": "sha256", - "verification_source": "e57fed14bc15a62ab38a6605a8f93c2cf29fbd7a6333dd3ad72781c3778e36fc", - "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --ropsten --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17536 --rpc-port=17537 --monitoring-port=17538 --p2p-tcp-port=13536 --p2p-udp-port=12536 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_ropsten/backend/geth/jwtsecret --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", - "postinst_script_template": "wget https://github.com/eth-clients/merge-testnets/raw/e4a6f0c181d24b28bc8651744f1d0e9ef74bda3f/ropsten-beacon-chain/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", - "service_type": "simple", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.1.2/beacon-chain-v3.1.2-linux-arm64", - "verification_source": "1701df47dbb6598a9215f82a313e1531c211bb912618dc3d0cd33e6e67c5ebb5" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} diff --git a/configs/coins/ethereum_testnet_sepolia.json b/configs/coins/ethereum_testnet_sepolia.json index 268fac2e43..18511edbec 100644 --- a/configs/coins/ethereum_testnet_sepolia.json +++ b/configs/coins/ethereum_testnet_sepolia.json @@ -1,70 +1,71 @@ { - "coin": { - "name": "Ethereum Testnet Sepolia", - "shortcut": "gSEP", - "label": "Ethereum Sepolia", - "alias": "ethereum_testnet_sepolia" - }, - "ports": { - "backend_rpc": 18076, - "backend_message_queue": 0, - "backend_p2p": 48376, - "backend_http": 18176, - "backend_authrpc": 18576, - "blockbook_internal": 19076, - "blockbook_public": 19176 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-testnet-sepolia", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "1.11.2-73b01f40", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz", - "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --sepolia --syncmode full --txlookuplimit 0 --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --ws.api \"eth,net,web3,debug,txpool\" --http --http.port {{.Ports.BackendHttp}} -http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz.asc" - } + "coin": { + "name": "Ethereum Testnet Sepolia", + "shortcut": "tSEP", + "label": "Ethereum Sepolia", + "alias": "ethereum_testnet_sepolia" + }, + "ports": { + "backend_rpc": 18076, + "backend_message_queue": 0, + "backend_p2p": 48376, + "backend_http": 18176, + "backend_authrpc": 18576, + "blockbook_internal": 19076, + "blockbook_public": 19176 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-testnet-sepolia", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "3.2.1", + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_amd64.tar.gz", + "verification_type": "sha256", + "verification_source": "8b5444988667721f2b2ef1ab3098139c31f722492992939c110813408c39dc7c", + "extract_command": "tar -C backend --strip-components=1 -xf", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/erigon --chain sepolia --snap.keepblocks --db.size.limit 15TB --db.pagesize 16KB --prune.mode full --externalcl --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/erigon --port {{.Ports.BackendP2P}} --ws --ws.port {{.Ports.BackendRPC}} --http --http.port {{.Ports.BackendRPC}} --http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} --private.api.addr \"\" --torrent.port {{.Ports.BackendHttp}} --log.dir.path {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log.dir.prefix {{.Coin.Alias}}'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_arm64.tar.gz", + "verification_source": "19a91709dc3ddbe947c4f81e70cb1de49044954e21f441e9ea46b3696f21b57f" + } + } + }, + "blockbook": { + "package_name": "blockbook-ethereum-testnet-sepolia", + "system_user": "blockbook-ethereum", + "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": 3000, + "additional_params": { + "consensusNodeVersion": "http://localhost:17576/eth/v1/node/version", + "eip1559Fees": true, + "mempoolTxTimeoutHours": 12, + "queryBackendOnMempoolResync": false + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-ethereum-testnet-sepolia", - "system_user": "blockbook-ethereum", - "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": 3000, - "additional_params": { - "consensusNodeVersion": "http://localhost:17576/eth/v1/node/version", - "mempoolTxTimeoutHours": 12, - "queryBackendOnMempoolResync": false - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} +} \ No newline at end of file diff --git a/configs/coins/ethereum_testnet_sepolia_archive.json b/configs/coins/ethereum_testnet_sepolia_archive.json index 8eb272bf45..809f3f6ce5 100644 --- a/configs/coins/ethereum_testnet_sepolia_archive.json +++ b/configs/coins/ethereum_testnet_sepolia_archive.json @@ -1,75 +1,77 @@ { - "coin": { - "name": "Ethereum Testnet Sepolia Archive", - "shortcut": "gSEP", - "label": "Ethereum Sepolia", - "alias": "ethereum_testnet_sepolia_archive" - }, - "ports": { - "backend_rpc": 18086, - "backend_message_queue": 0, - "backend_p2p": 48386, - "backend_http": 18186, - "backend_authrpc": 18586, - "blockbook_internal": 19086, - "blockbook_public": 19186 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-testnet-sepolia-archive", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "1.11.2-73b01f40", - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz", - "verification_type": "gpg", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.11.2-73b01f40.tar.gz.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/geth --sepolia --syncmode full --gcmode archive --txlookuplimit 0 --ipcdisable --cache 1024 --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --port {{.Ports.BackendP2P}} --ws --ws.addr 127.0.0.1 --ws.port {{.Ports.BackendRPC}} --ws.origins \"*\" --ws.api \"eth,net,web3,debug,txpool\" --http --http.port {{.Ports.BackendHttp}} -http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} 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", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz", - "verification_source": "https://gethstore.blob.core.windows.net/builds/geth-linux-arm64-1.10.26-e5eb32ac.tar.gz.asc" - } + "coin": { + "name": "Ethereum Testnet Sepolia Archive", + "shortcut": "tSEP", + "label": "Ethereum Sepolia", + "alias": "ethereum_testnet_sepolia_archive" + }, + "ports": { + "backend_rpc": 18086, + "backend_message_queue": 0, + "backend_p2p": 48386, + "backend_http": 18186, + "backend_torrent": 18186, + "backend_authrpc": 18586, + "blockbook_internal": 19086, + "blockbook_public": 19186 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-testnet-sepolia-archive", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "3.2.1", + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_amd64.tar.gz", + "verification_type": "sha256", + "verification_source": "8b5444988667721f2b2ef1ab3098139c31f722492992939c110813408c39dc7c", + "extract_command": "tar -C backend --strip-components=1 -xf", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/erigon --chain sepolia --snap.keepblocks --db.size.limit 15TB --db.pagesize 16KB --prune.mode archive --externalcl --nat none --datadir {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/erigon --port {{.Ports.BackendP2P}} --ws --ws.port {{.Ports.BackendRPC}} --http --http.port {{.Ports.BackendRPC}} --http.addr 127.0.0.1 --http.corsdomain \"*\" --http.vhosts \"*\" --http.api \"eth,net,web3,debug,txpool\" --authrpc.port {{.Ports.BackendAuthRpc}} --private.api.addr \"\" --torrent.port {{.Ports.BackendHttp}} --log.dir.path {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --log.dir.prefix {{.Coin.Alias}}'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/erigontech/erigon/releases/download/v3.2.1/erigon_v3.2.1_linux_arm64.tar.gz", + "verification_source": "19a91709dc3ddbe947c4f81e70cb1de49044954e21f441e9ea46b3696f21b57f" + } + } + }, + "blockbook": { + "package_name": "blockbook-ethereum-testnet-sepolia-archive", + "system_user": "blockbook-ethereum", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 3000, + "additional_params": { + "consensusNodeVersion": "http://localhost:17586/eth/v1/node/version", + "address_aliases": true, + "eip1559Fees": true, + "mempoolTxTimeoutHours": 12, + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "fiat_rates-disabled": "coingecko", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-ethereum-testnet-sepolia-archive", - "system_user": "blockbook-ethereum", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "-workers=16", - "block_chain": { - "parse": true, - "mempool_workers": 8, - "mempool_sub_workers": 2, - "block_addresses_to_keep": 3000, - "additional_params": { - "consensusNodeVersion": "http://localhost:17586/eth/v1/node/version", - "address_aliases": true, - "mempoolTxTimeoutHours": 12, - "processInternalTransactions": true, - "queryBackendOnMempoolResync": false, - "fiat_rates-disabled": "coingecko", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"ethereum\",\"platformIdentifier\": \"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", - "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } -} +} \ No newline at end of file diff --git a/configs/coins/ethereum_testnet_sepolia_archive_consensus.json b/configs/coins/ethereum_testnet_sepolia_archive_consensus.json index 2be9ce54ba..3455cc1fd6 100644 --- a/configs/coins/ethereum_testnet_sepolia_archive_consensus.json +++ b/configs/coins/ethereum_testnet_sepolia_archive_consensus.json @@ -1,52 +1,52 @@ { - "coin": { - "name": "Ethereum Testnet Sepolia Archive", - "shortcut": "gSEP", - "label": "Ethereum Sepolia", - "alias": "ethereum_testnet_sepolia_archive_consensus", - "execution_alias": "ethereum_testnet_sepolia_archive" - }, - "ports": { - "backend_rpc": 18086, - "backend_message_queue": 0, - "backend_p2p": 48386, - "backend_http": 18186, - "backend_authrpc": 18586, - "blockbook_internal": 19086, - "blockbook_public": 19186 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-testnet-sepolia-archive-consensus", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "3.2.0", - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.2.0/beacon-chain-v3.2.0-linux-amd64", - "verification_type": "sha256", - "verification_source": "e57fed14bc15a62ab38a6605a8f93c2cf29fbd7a6333dd3ad72781c3778e36fc", - "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --sepolia --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17586 --rpc-port=17587 --monitoring-port=17548 --p2p-tcp-port=13676 --p2p-udp-port=12676 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_sepolia_archive/backend/geth/jwtsecret --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", - "postinst_script_template": "wget https://github.com/eth-clients/merge-testnets/raw/302fe27afdc7a9d15b1766a0c0a9d64319140255/sepolia/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", - "service_type": "simple", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.1.2/beacon-chain-v3.1.2-linux-arm64", - "verification_source": "1701df47dbb6598a9215f82a313e1531c211bb912618dc3d0cd33e6e67c5ebb5" - } + "coin": { + "name": "Ethereum Testnet Sepolia Archive", + "shortcut": "tSEP", + "label": "Ethereum Sepolia", + "alias": "ethereum_testnet_sepolia_archive_consensus", + "execution_alias": "ethereum_testnet_sepolia_archive" + }, + "ports": { + "backend_rpc": 18086, + "backend_message_queue": 0, + "backend_p2p": 48386, + "backend_http": 18186, + "backend_authrpc": 18586, + "blockbook_internal": 19086, + "blockbook_public": 19186 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-testnet-sepolia-archive-consensus", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "6.1.2", + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-amd64", + "verification_type": "sha256", + "verification_source": "45d34c817db22e34ae12ebe733d281db76a349e3be439952f9e1dd50f10bc2b1", + "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --sepolia --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17586 --rpc-port=17587 --monitoring-port=17548 --p2p-tcp-port=13676 --p2p-udp-port=12676 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_sepolia_archive/backend/erigon/jwt.hex --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "wget https://github.com/eth-clients/sepolia/raw/main/metadata/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-arm64", + "verification_source": "2651f1407bb842e7f03dc00ba58990ee3345865cb5d474a3f76a968db5e57c02" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/ethereum_testnet_sepolia_consensus.json b/configs/coins/ethereum_testnet_sepolia_consensus.json index 635875b755..b26f323e3c 100644 --- a/configs/coins/ethereum_testnet_sepolia_consensus.json +++ b/configs/coins/ethereum_testnet_sepolia_consensus.json @@ -1,52 +1,52 @@ { - "coin": { - "name": "Ethereum Testnet Sepolia", - "shortcut": "gSEP", - "label": "Ethereum Sepolia", - "alias": "ethereum_testnet_sepolia_consensus", - "execution_alias": "ethereum_testnet_sepolia" - }, - "ports": { - "backend_rpc": 18076, - "backend_message_queue": 0, - "backend_p2p": 48376, - "backend_http": 18176, - "backend_authrpc": 18576, - "blockbook_internal": 19076, - "blockbook_public": 19176 - }, - "ipc": { - "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_timeout": 25 - }, - "backend": { - "package_name": "backend-ethereum-testnet-sepolia-consensus", - "package_revision": "satoshilabs-1", - "system_user": "ethereum", - "version": "3.2.0", - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.2.0/beacon-chain-v3.2.0-linux-amd64", - "verification_type": "sha256", - "verification_source": "e57fed14bc15a62ab38a6605a8f93c2cf29fbd7a6333dd3ad72781c3778e36fc", - "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", - "exclude_files": [], - "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --sepolia --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17576 --rpc-port=17577 --monitoring-port=17578 --p2p-tcp-port=13576 --p2p-udp-port=12576 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_sepolia/backend/geth/jwtsecret --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", - "postinst_script_template": "wget https://github.com/eth-clients/merge-testnets/raw/302fe27afdc7a9d15b1766a0c0a9d64319140255/sepolia/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", - "service_type": "simple", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "", - "client_config_file": "", - "platforms": { - "arm64": { - "binary_url": "https://github.com/prysmaticlabs/prysm/releases/download/v3.1.2/beacon-chain-v3.1.2-linux-arm64", - "verification_source": "1701df47dbb6598a9215f82a313e1531c211bb912618dc3d0cd33e6e67c5ebb5" - } + "coin": { + "name": "Ethereum Testnet Sepolia", + "shortcut": "tSEP", + "label": "Ethereum Sepolia", + "alias": "ethereum_testnet_sepolia_consensus", + "execution_alias": "ethereum_testnet_sepolia" + }, + "ports": { + "backend_rpc": 18076, + "backend_message_queue": 0, + "backend_p2p": 48376, + "backend_http": 18176, + "backend_authrpc": 18576, + "blockbook_internal": 19076, + "blockbook_public": 19176 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-ethereum-testnet-sepolia-consensus", + "package_revision": "satoshilabs-1", + "system_user": "ethereum", + "version": "6.1.2", + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-amd64", + "verification_type": "sha256", + "verification_source": "45d34c817db22e34ae12ebe733d281db76a349e3be439952f9e1dd50f10bc2b1", + "extract_command": "mv ${ARCHIVE} backend/beacon-chain && chmod +x backend/beacon-chain && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/beacon-chain --sepolia --accept-terms-of-use --execution-endpoint=http://localhost:{{.Ports.BackendAuthRpc}} --grpc-gateway-port=17576 --rpc-port=17577 --monitoring-port=17578 --p2p-tcp-port=13576 --p2p-udp-port=12576 --datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend --jwt-secret={{.Env.BackendDataPath}}/ethereum_testnet_sepolia/backend/erigon/jwt.hex --genesis-state={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz 2>>{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "wget https://github.com/eth-clients/holesky/raw/main/metadata/genesis.ssz -O {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/genesis.ssz", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/OffchainLabs/prysm/releases/download/v6.1.2/beacon-chain-v6.1.2-linux-arm64", + "verification_source": "2651f1407bb842e7f03dc00ba58990ee3345865cb5d474a3f76a968db5e57c02" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/firo.json b/configs/coins/firo.json index 1e29446998..26458b3ed4 100644 --- a/configs/coins/firo.json +++ b/configs/coins/firo.json @@ -22,10 +22,10 @@ "package_name": "backend-firo", "package_revision": "satoshilabs-1", "system_user": "firo", - "version": "0.14.11.1", - "binary_url": "https://github.com/firoorg/firo/releases/download/v0.14.11.1/firo-0.14.11.1-linux64.tar.gz", + "version": "0.14.15.0", + "binary_url": "https://github.com/firoorg/firo/releases/download/v0.14.15.0/firo-0.14.15.0-linux64.tar.gz", "verification_type": "sha256", - "verification_source": "8669ae8ce3356deee2512a4da133eab347c704cf47c865caf9ea10b46ba8b477", + "verification_source": "6a601e7c1aa0af4aee3b28a7fbd365a1d749d2203e8d042bcccf9f950072ecd9", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/firo-qt", diff --git a/configs/coins/flux.json b/configs/coins/flux.json index 887f8fa8a9..3bafdcf074 100644 --- a/configs/coins/flux.json +++ b/configs/coins/flux.json @@ -22,10 +22,10 @@ "package_name": "backend-flux", "package_revision": "satoshilabs-1", "system_user": "flux", - "version": "6.0.0", - "binary_url": "https://github.com/RunOnFlux/fluxd/releases/download/v6.0.0/Flux-amd64-v6.0.0.tar.gz", + "version": "9.0.0", + "binary_url": "https://github.com/RunOnFlux/fluxd/releases/download/v9.0.0/Flux-amd64-v9.0.0.tar.gz", "verification_type": "sha256", - "verification_source": "28717246a383018de8f6099a26afc3a4877da32f2d9531a3253b1664c22145e7", + "verification_source": "3d37ad5c769195c9ce6d6d0ee613eb9852de0cb9ee3779c9da7d9f9e51cd285e", "extract_command": "tar -C backend -xf", "exclude_files": [], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/fluxd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", @@ -39,10 +39,12 @@ "client_config_file": "bitcoin_like_client.conf", "additional_params": { "addnode": [ - "explorer.zel.cash", - "explorer2.zel.cash", - "explorer.zelcash.online", - "explorer-asia.zel.cash" + "explorer.runonflux.com", + "explorer.runonflux.io", + "blockbook.runonflux.com", + "blockbook.runonflux.io", + "explorer.flux.zelcore.io", + "blockbook.flux.zelcore.io" ] } }, diff --git a/configs/coins/fujicoin.json b/configs/coins/fujicoin.json index c3188e660d..82343c654f 100644 --- a/configs/coins/fujicoin.json +++ b/configs/coins/fujicoin.json @@ -1,71 +1,71 @@ { - "coin": { - "name": "Fujicoin", - "shortcut": "FJC", - "label": "Fujicoin", - "alias": "fujicoin" - }, - "ports": { - "backend_rpc": 8048, - "backend_message_queue": 38348, - "blockbook_internal": 9048, - "blockbook_public": 9148 - }, - "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-fujicoin", - "package_revision": "satoshilabs-1", - "system_user": "fujicoin", - "version": "22.0", - "binary_url": "https://download.fujicoin.org/fujicoin-v22.0/x86_64-linux-gnu/fujicoin-22.0-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "8aa699f3fbd6681391b90f744a25155d21a94f5ca63d6cc3b85172f3aca6e2a0", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/fujicoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/fujicoind -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" + "coin": { + "name": "Fujicoin", + "shortcut": "FJC", + "label": "Fujicoin", + "alias": "fujicoin" + }, + "ports": { + "backend_rpc": 8048, + "backend_message_queue": 38348, + "blockbook_internal": 9048, + "blockbook_public": 9148 + }, + "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-fujicoin", + "package_revision": "satoshilabs-1", + "system_user": "fujicoin", + "version": "22.0", + "binary_url": "https://download.fujicoin.org/fujicoin-v22.0/x86_64-linux-gnu/fujicoin-22.0-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "8aa699f3fbd6681391b90f744a25155d21a94f5ca63d6cc3b85172f3aca6e2a0", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/fujicoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/fujicoind -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-fujicoin", + "system_user": "blockbook-fujicoin", + "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": 75, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"fujicoin\", \"periodSeconds\": 600}" + } + } + }, + "meta": { + "package_maintainer": "Motty", + "package_maintainer_email": "fujicoin@gmail.com" } - }, - "blockbook": { - "package_name": "blockbook-fujicoin", - "system_user": "blockbook-fujicoin", - "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": 75, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"fujicoin\", \"periodSeconds\": 600}" - } - } - }, - "meta": { - "package_maintainer": "Motty", - "package_maintainer_email": "fujicoin@gmail.com" - } } diff --git a/configs/coins/groestlcoin.json b/configs/coins/groestlcoin.json index 5fdfd42bc4..367a2071c6 100644 --- a/configs/coins/groestlcoin.json +++ b/configs/coins/groestlcoin.json @@ -1,72 +1,83 @@ { - "coin": { - "name": "Groestlcoin", - "shortcut": "GRS", - "label": "Groestlcoin", - "alias": "groestlcoin" - }, - "ports": { - "backend_rpc": 8045, - "backend_message_queue": 38345, - "blockbook_internal": 9045, - "blockbook_public": 9145 - }, - "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-groestlcoin", - "package_revision": "satoshilabs-1", - "system_user": "groestlcoin", - "version": "24.0.1", - "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v24.0.1/groestlcoin-24.0.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "4b69743190e2697d7b7772bf6f63cde595d590ff6664abf15a7201dab2a6098b", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/groestlcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -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.conf", - "client_config_file": "bitcoin_client.conf", - "additional_params": { - "deprecatedrpc": "estimatefee", - "whitelist": "127.0.0.1" + "coin": { + "name": "Groestlcoin", + "shortcut": "GRS", + "label": "Groestlcoin", + "alias": "groestlcoin" + }, + "ports": { + "backend_rpc": 8045, + "backend_message_queue": 38345, + "blockbook_internal": 9045, + "blockbook_public": 9145 + }, + "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-groestlcoin", + "package_revision": "satoshilabs-1", + "system_user": "groestlcoin", + "version": "29.0", + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v29.0/groestlcoin-29.0-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "e0b3e3d96caf908060779c0d9964c777ccc4b7364af54404ff1768e018e56768", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/groestlcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -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.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v29.0/groestlcoin-29.0-aarch64-linux-gnu.tar.gz", + "verification_source": "43b67b0945eb63c26bf0106ce3e302d4fe0720900cd8658e84f5d7954899a2a8" + } + } + }, + "blockbook": { + "package_name": "blockbook-groestlcoin", + "system_user": "blockbook-groestlcoin", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-enablesubnewtx -extendedindex", + "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": 17, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"groestlcoin\", \"periodSeconds\": 900}", + "block_golomb_filter_p": 20, + "block_filter_scripts": "taproot-noordinals", + "block_filter_use_zeroed_key": true, + "mempool_golomb_filter_p": 20, + "mempool_filter_scripts": "taproot", + "mempool_filter_use_zeroed_key": false + } + } + }, + "meta": { + "package_maintainer": "Groestlcoin Development Team", + "package_maintainer_email": "jackie@groestlcoin.org" } - }, - "blockbook": { - "package_name": "blockbook-groestlcoin", - "system_user": "blockbook-groestlcoin", - "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": 17, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"groestlcoin\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "Groestlcoin Development Team", - "package_maintainer_email": "jackie@groestlcoin.org" - } } diff --git a/configs/coins/groestlcoin_regtest.json b/configs/coins/groestlcoin_regtest.json index c85b30bf82..4d6ae18c80 100644 --- a/configs/coins/groestlcoin_regtest.json +++ b/configs/coins/groestlcoin_regtest.json @@ -1,73 +1,73 @@ { - "coin": { - "name": "Groestlcoin Regtest", - "shortcut": "rGRS", - "label": "Groestlcoin Regtest", - "alias": "groestlcoin_regtest" - }, - "ports": { - "backend_rpc": 18046, - "backend_message_queue": 48346, - "blockbook_internal": 19046, - "blockbook_public": 19146 - }, - "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-groestlcoin-regtest", - "package_revision": "satoshilabs-1", - "system_user": "groestlcoin", - "version": "24.0.1", - "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v24.0.1/groestlcoin-24.0.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "4b69743190e2697d7b7772bf6f63cde595d590ff6664abf15a7201dab2a6098b", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/groestlcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -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/regtest/*.log", - "postinst_script_template": "", - "service_type": "forking", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "bitcoin_regtest.conf", - "client_config_file": "bitcoin_client.conf", - "additional_params": { - "deprecatedrpc": "estimatefee", - "whitelist": "127.0.0.1" + "coin": { + "name": "Groestlcoin Regtest", + "shortcut": "rGRS", + "label": "Groestlcoin Regtest", + "alias": "groestlcoin_regtest" }, - "platforms": { - "arm64": { - "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v24.0.1/groestlcoin-24.0.1-aarch64-linux-gnu.tar.gz", - "verification_source": "ca316c369728348406778c30b2b567bb2ede1ebcc87fb0305c0bed3dacae762b" - } - } - }, - "blockbook": { - "package_name": "blockbook-groestlcoin-regtest", - "system_user": "blockbook-groestlcoin", - "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 + "ports": { + "backend_rpc": 18046, + "backend_message_queue": 48346, + "blockbook_internal": 19046, + "blockbook_public": 19146 + }, + "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-groestlcoin-regtest", + "package_revision": "satoshilabs-1", + "system_user": "groestlcoin", + "version": "29.0", + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v29.0/groestlcoin-29.0-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "e0b3e3d96caf908060779c0d9964c777ccc4b7364af54404ff1768e018e56768", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/groestlcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -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/regtest/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "mainnet": false, + "protect_memory": true, + "server_config_file": "bitcoin_regtest.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v29.0/groestlcoin-29.0-aarch64-linux-gnu.tar.gz", + "verification_source": "43b67b0945eb63c26bf0106ce3e302d4fe0720900cd8658e84f5d7954899a2a8" + } + } + }, + "blockbook": { + "package_name": "blockbook-groestlcoin-regtest", + "system_user": "blockbook-groestlcoin", + "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": "Groestlcoin Development Team", + "package_maintainer_email": "jackie@groestlcoin.org" } - }, - "meta": { - "package_maintainer": "Groestlcoin Development Team", - "package_maintainer_email": "jackie@groestlcoin.org" - } } diff --git a/configs/coins/groestlcoin_signet.json b/configs/coins/groestlcoin_signet.json index 7859a6908b..2125aa4109 100644 --- a/configs/coins/groestlcoin_signet.json +++ b/configs/coins/groestlcoin_signet.json @@ -1,67 +1,73 @@ { - "coin": { - "name": "Groestlcoin Signet", - "shortcut": "sGRS", - "label": "Groestlcoin Signet", - "alias": "groestlcoin_signet" - }, - "ports": { - "backend_rpc": 18047, - "backend_message_queue": 48347, - "blockbook_internal": 19047, - "blockbook_public": 19147 - }, - "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-groestlcoin-signet", - "package_revision": "satoshilabs-1", - "system_user": "groestlcoin", - "version": "24.0.1", - "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v24.0.1/groestlcoin-24.0.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "4b69743190e2697d7b7772bf6f63cde595d590ff6664abf15a7201dab2a6098b", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/groestlcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -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/signet/*.log", - "postinst_script_template": "", - "service_type": "forking", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "bitcoin-signet.conf", - "client_config_file": "bitcoin_client.conf", - "additional_params": { - "deprecatedrpc": "estimatefee", - "whitelist": "127.0.0.1" + "coin": { + "name": "Groestlcoin Signet", + "shortcut": "sGRS", + "label": "Groestlcoin Signet", + "alias": "groestlcoin_signet" + }, + "ports": { + "backend_rpc": 18047, + "backend_message_queue": 48347, + "blockbook_internal": 19047, + "blockbook_public": 19147 + }, + "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-groestlcoin-signet", + "package_revision": "satoshilabs-1", + "system_user": "groestlcoin", + "version": "29.0", + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v29.0/groestlcoin-29.0-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "e0b3e3d96caf908060779c0d9964c777ccc4b7364af54404ff1768e018e56768", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/groestlcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -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/signet/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "bitcoin_signet.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v29.0/groestlcoin-29.0-aarch64-linux-gnu.tar.gz", + "verification_source": "43b67b0945eb63c26bf0106ce3e302d4fe0720900cd8658e84f5d7954899a2a8" + } + } + }, + "blockbook": { + "package_name": "blockbook-groestlcoin-signet", + "system_user": "blockbook-groestlcoin", + "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": "Groestlcoin Development Team", + "package_maintainer_email": "jackie@groestlcoin.org" } - }, - "blockbook": { - "package_name": "blockbook-groestlcoin-signet", - "system_user": "blockbook-groestlcoin", - "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 - } - }, - "meta": { - "package_maintainer": "Groestlcoin Development Team", - "package_maintainer_email": "jackie@groestlcoin.org" - } } diff --git a/configs/coins/groestlcoin_testnet.json b/configs/coins/groestlcoin_testnet.json index 05a67c2548..2b0e15aaec 100644 --- a/configs/coins/groestlcoin_testnet.json +++ b/configs/coins/groestlcoin_testnet.json @@ -1,67 +1,80 @@ { - "coin": { - "name": "Groestlcoin Testnet", - "shortcut": "tGRS", - "label": "Groestlcoin Testnet", - "alias": "groestlcoin_testnet" - }, - "ports": { - "backend_rpc": 18045, - "backend_message_queue": 48345, - "blockbook_internal": 19045, - "blockbook_public": 19145 - }, - "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-groestlcoin-testnet", - "package_revision": "satoshilabs-1", - "system_user": "groestlcoin", - "version": "24.0.1", - "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v24.0.1/groestlcoin-24.0.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "4b69743190e2697d7b7772bf6f63cde595d590ff6664abf15a7201dab2a6098b", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/groestlcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -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": "bitcoin.conf", - "client_config_file": "bitcoin_client.conf", - "additional_params": { - "deprecatedrpc": "estimatefee", - "whitelist": "127.0.0.1" + "coin": { + "name": "Groestlcoin Testnet", + "shortcut": "tGRS", + "label": "Groestlcoin Testnet", + "alias": "groestlcoin_testnet" + }, + "ports": { + "backend_rpc": 18045, + "backend_message_queue": 48345, + "blockbook_internal": 19045, + "blockbook_public": 19145 + }, + "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-groestlcoin-testnet", + "package_revision": "satoshilabs-1", + "system_user": "groestlcoin", + "version": "29.0", + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v29.0/groestlcoin-29.0-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "e0b3e3d96caf908060779c0d9964c777ccc4b7364af54404ff1768e018e56768", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/groestlcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -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": "bitcoin.conf", + "client_config_file": "bitcoin_client.conf", + "additional_params": { + "deprecatedrpc": "estimatefee" + }, + "platforms": { + "arm64": { + "binary_url": "https://github.com/Groestlcoin/groestlcoin/releases/download/v29.0/groestlcoin-29.0-aarch64-linux-gnu.tar.gz", + "verification_source": "43b67b0945eb63c26bf0106ce3e302d4fe0720900cd8658e84f5d7954899a2a8" + } + } + }, + "blockbook": { + "package_name": "blockbook-groestlcoin-testnet", + "system_user": "blockbook-groestlcoin", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-enablesubnewtx -extendedindex", + "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": { + "block_golomb_filter_p": 20, + "block_filter_scripts": "taproot-noordinals", + "block_filter_use_zeroed_key": true, + "mempool_golomb_filter_p": 20, + "mempool_filter_scripts": "taproot", + "mempool_filter_use_zeroed_key": false + } + } + }, + "meta": { + "package_maintainer": "Groestlcoin Development Team", + "package_maintainer_email": "jackie@groestlcoin.org" } - }, - "blockbook": { - "package_name": "blockbook-groestlcoin-testnet", - "system_user": "blockbook-groestlcoin", - "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 - } - }, - "meta": { - "package_maintainer": "Groestlcoin Development Team", - "package_maintainer_email": "jackie@groestlcoin.org" - } } diff --git a/configs/coins/litecoin.json b/configs/coins/litecoin.json index f965e4e813..026665c1df 100644 --- a/configs/coins/litecoin.json +++ b/configs/coins/litecoin.json @@ -1,77 +1,77 @@ { - "coin": { - "name": "Litecoin", - "shortcut": "LTC", - "label": "Litecoin", - "alias": "litecoin" - }, - "ports": { - "backend_rpc": 8034, - "backend_message_queue": 38334, - "blockbook_internal": 9034, - "blockbook_public": 9134 - }, - "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-litecoin", - "package_revision": "satoshilabs-1", - "system_user": "litecoin", - "version": "0.21.2.1", - "binary_url": "https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-x86_64-linux-gnu.tar.gz", - "verification_type": "gpg", - "verification_source": "https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-x86_64-linux-gnu.tar.gz.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/litecoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/litecoind -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" + "coin": { + "name": "Litecoin", + "shortcut": "LTC", + "label": "Litecoin", + "alias": "litecoin" }, - "platforms": { - "arm64": { - "binary_url": "https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-aarch64-linux-gnu.tar.gz", - "verification_source": "https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-aarch64-linux-gnu.tar.gz.asc" - } - } - }, - "blockbook": { - "package_name": "blockbook-litecoin", - "system_user": "blockbook-litecoin", - "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": 27108450, - "xpub_magic_segwit_p2sh": 28471030, - "xpub_magic_segwit_native": 78792518, - "slip44": 2, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"litecoin\", \"periodSeconds\": 900}" - } + "ports": { + "backend_rpc": 8034, + "backend_message_queue": 38334, + "blockbook_internal": 9034, + "blockbook_public": 9134 + }, + "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-litecoin", + "package_revision": "satoshilabs-1", + "system_user": "litecoin", + "version": "0.21.4", + "binary_url": "https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz", + "verification_type": "gpg", + "verification_source": "https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz.asc", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/litecoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/litecoind -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" + }, + "platforms": { + "arm64": { + "binary_url": "https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-aarch64-linux-gnu.tar.gz", + "verification_source": "https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-aarch64-linux-gnu.tar.gz.asc" + } + } + }, + "blockbook": { + "package_name": "blockbook-litecoin", + "system_user": "blockbook-litecoin", + "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": 27108450, + "xpub_magic_segwit_p2sh": 28471030, + "xpub_magic_segwit_native": 78792518, + "slip44": 2, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"litecoin\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/litecoin_testnet.json b/configs/coins/litecoin_testnet.json index fb23dbde04..0d0962b344 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.21.2.1", - "binary_url": "https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-x86_64-linux-gnu.tar.gz", + "version": "0.21.4", + "binary_url": "https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz", "verification_type": "gpg", - "verification_source": "https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-x86_64-linux-gnu.tar.gz.asc", + "verification_source": "https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/litecoin-qt" @@ -44,8 +44,8 @@ }, "platforms": { "arm64": { - "binary_url": "https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-aarch64-linux-gnu.tar.gz", - "verification_source": "https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-aarch64-linux-gnu.tar.gz.asc" + "binary_url": "https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-aarch64-linux-gnu.tar.gz", + "verification_source": "https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-aarch64-linux-gnu.tar.gz.asc" } } }, diff --git a/configs/coins/monacoin.json b/configs/coins/monacoin.json index 7cf2715ac8..39bbaba0e6 100644 --- a/configs/coins/monacoin.json +++ b/configs/coins/monacoin.json @@ -1,71 +1,71 @@ { - "coin": { - "name": "Monacoin", - "shortcut": "MONA", - "label": "Monacoin", - "alias": "monacoin" - }, - "ports": { - "backend_rpc": 8041, - "backend_message_queue": 38341, - "blockbook_internal": 9041, - "blockbook_public": 9141 - }, - "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-monacoin", - "package_revision": "satoshilabs-1", - "system_user": "monacoin", - "version": "0.20.3", - "binary_url": "https://github.com/monacoinproject/monacoin/releases/download/v0.20.3/monacoin-0.20.3-x86_64-linux-gnu.tar.gz", - "verification_type": "gpg-sha256", - "verification_source": "https://github.com/monacoinproject/monacoin/releases/download/v0.20.3/monacoin-0.20.3-signatures.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/monacoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/monacoind -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.conf", - "client_config_file": "bitcoin_like_client.conf", - "additional_params": { - "whitelist": "127.0.0.1" + "coin": { + "name": "Monacoin", + "shortcut": "MONA", + "label": "Monacoin", + "alias": "monacoin" + }, + "ports": { + "backend_rpc": 8041, + "backend_message_queue": 38341, + "blockbook_internal": 9041, + "blockbook_public": 9141 + }, + "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-monacoin", + "package_revision": "satoshilabs-1", + "system_user": "monacoin", + "version": "0.20.4", + "binary_url": "https://github.com/monacoinproject/monacoin/releases/download/v0.20.4/monacoin-0.20.4-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "94f8fe7400d23a9bad10af3dfc3f800e333be0aa4d61e5c8cfc5f338253d9451", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/monacoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/monacoind -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.conf", + "client_config_file": "bitcoin_like_client.conf", + "additional_params": { + "whitelist": "127.0.0.1" + } + }, + "blockbook": { + "package_name": "blockbook-monacoin", + "system_user": "blockbook-monacoin", + "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": 22, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"monacoin\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "wakiyamap", + "package_maintainer_email": "wakiyamap@gmail.com" } - }, - "blockbook": { - "package_name": "blockbook-monacoin", - "system_user": "blockbook-monacoin", - "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": 22, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"monacoin\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "wakiyamap", - "package_maintainer_email": "wakiyamap@gmail.com" - } } diff --git a/configs/coins/monacoin_testnet.json b/configs/coins/monacoin_testnet.json index c867057e7e..46d5826449 100644 --- a/configs/coins/monacoin_testnet.json +++ b/configs/coins/monacoin_testnet.json @@ -1,69 +1,69 @@ { - "coin": { - "name": "Monacoin Testnet", - "shortcut": "TMONA", - "label": "Monacoin Testnet", - "alias": "monacoin_testnet" - }, - "ports": { - "backend_rpc": 18041, - "backend_message_queue": 48341, - "blockbook_internal": 19041, - "blockbook_public": 19141 - }, - "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-monacoin-testnet", - "package_revision": "satoshilabs-1", - "system_user": "monacoin", - "version": "0.20.3", - "binary_url": "https://github.com/monacoinproject/monacoin/releases/download/v0.20.3/monacoin-0.20.3-x86_64-linux-gnu.tar.gz", - "verification_type": "gpg-sha256", - "verification_source": "https://github.com/monacoinproject/monacoin/releases/download/v0.20.3/monacoin-0.20.3-signatures.asc", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": [ - "bin/monacoin-qt" - ], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/monacoind -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/testnet4/*.log", - "postinst_script_template": "", - "service_type": "forking", - "service_additional_params_template": "", - "protect_memory": true, - "mainnet": false, - "server_config_file": "bitcoin.conf", - "client_config_file": "bitcoin_like_client.conf", - "additional_params": { - "whitelist": "127.0.0.1" + "coin": { + "name": "Monacoin Testnet", + "shortcut": "TMONA", + "label": "Monacoin Testnet", + "alias": "monacoin_testnet" + }, + "ports": { + "backend_rpc": 18041, + "backend_message_queue": 48341, + "blockbook_internal": 19041, + "blockbook_public": 19141 + }, + "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-monacoin-testnet", + "package_revision": "satoshilabs-1", + "system_user": "monacoin", + "version": "0.20.4", + "binary_url": "https://github.com/monacoinproject/monacoin/releases/download/v0.20.4/monacoin-0.20.4-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "94f8fe7400d23a9bad10af3dfc3f800e333be0aa4d61e5c8cfc5f338253d9451", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": [ + "bin/monacoin-qt" + ], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/monacoind -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/testnet4/*.log", + "postinst_script_template": "", + "service_type": "forking", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": false, + "server_config_file": "bitcoin.conf", + "client_config_file": "bitcoin_like_client.conf", + "additional_params": { + "whitelist": "127.0.0.1" + } + }, + "blockbook": { + "package_name": "blockbook-monacoin-testnet", + "system_user": "blockbook-monacoin", + "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": "wakiyamap", + "package_maintainer_email": "wakiyamap@gmail.com" } - }, - "blockbook": { - "package_name": "blockbook-monacoin-testnet", - "system_user": "blockbook-monacoin", - "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": "wakiyamap", - "package_maintainer_email": "wakiyamap@gmail.com" - } } diff --git a/configs/coins/namecoin.json b/configs/coins/namecoin.json index d9675283c5..e18f571169 100644 --- a/configs/coins/namecoin.json +++ b/configs/coins/namecoin.json @@ -1,74 +1,74 @@ { - "coin": { - "name": "Namecoin", - "shortcut": "NMC", - "label": "Namecoin", - "alias": "namecoin" - }, - "ports": { - "backend_rpc": 8039, - "backend_message_queue": 38339, - "blockbook_internal": 9039, - "blockbook_public": 9139 - }, - "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-namecoin", - "package_revision": "satoshilabs-1", - "system_user": "namecoin", - "version": "0.21.0.1", - "binary_url": "https://www.namecoin.org/files/namecoin-core/namecoin-core-0.21.0.1/namecoin-nc0.21.0.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "1e7f06030881fac5b8a6d33f497f1cab9a120189741ec81bc21e58d5cd93fa6f", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/namecoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/namecoind -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": { - "addnode": ["45.24.110.177:8334"], - "discover": 0, - "listenonion": 0, - "upnp": 0, - "whitelist": "127.0.0.1", - "whitelistrelay": 1 + "coin": { + "name": "Namecoin", + "shortcut": "NMC", + "label": "Namecoin", + "alias": "namecoin" + }, + "ports": { + "backend_rpc": 8039, + "backend_message_queue": 38339, + "blockbook_internal": 9039, + "blockbook_public": 9139 + }, + "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-namecoin", + "package_revision": "satoshilabs-1", + "system_user": "namecoin", + "version": "0.21.0.1", + "binary_url": "https://www.namecoin.org/files/namecoin-core/namecoin-core-0.21.0.1/namecoin-nc0.21.0.1-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "1e7f06030881fac5b8a6d33f497f1cab9a120189741ec81bc21e58d5cd93fa6f", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/namecoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/namecoind -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": { + "addnode": ["45.24.110.177:8334"], + "discover": 0, + "listenonion": 0, + "upnp": 0, + "whitelist": "127.0.0.1", + "whitelistrelay": 1 + } + }, + "blockbook": { + "package_name": "blockbook-namecoin", + "system_user": "blockbook-namecoin", + "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, + "slip44": 7, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"namecoin\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT Admin", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-namecoin", - "system_user": "blockbook-namecoin", - "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, - "slip44": 7, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"namecoin\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "IT Admin", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/omotenashicoin.json b/configs/coins/omotenashicoin.json index 4a27eab55e..d630bf6587 100644 --- a/configs/coins/omotenashicoin.json +++ b/configs/coins/omotenashicoin.json @@ -1,69 +1,69 @@ { - "coin": { - "name": "Omotenashicoin", - "shortcut": "MTNS", - "label": "Omotenashicoin", - "alias": "omotenashicoin" - }, - "ports": { - "blockbook_internal": 9094, - "blockbook_public": 9194, - "backend_rpc": 8094, - "backend_message_queue": 38394 - }, - "ipc": { - "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", - "rpc_user": "rpc", - "rpc_pass": "mtnsrpc", - "rpc_timeout": 25, - "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" - }, - "backend": { - "package_name": "backend-mtns", - "package_revision": "satoshilabs-1", - "system_user": "mtns", - "version": "1.7.3", - "binary_url": "https://github.com/omotenashicoin-project/OmotenashiCoin-HDwalletbinaries/raw/master/stable/omotenashicoin-x86_64-linux-gnu.tar.gz", - "verification_type": "", - "verification_source": "", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/omotenashicoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/omotenashicoind -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": false, - "mainnet": true, - "server_config_file": "bitcoin_like.conf", - "client_config_file": "bitcoin_like_client.conf", - "additional_params": { - "whitelist": "127.0.0.1" + "coin": { + "name": "Omotenashicoin", + "shortcut": "MTNS", + "label": "Omotenashicoin", + "alias": "omotenashicoin" + }, + "ports": { + "blockbook_internal": 9094, + "blockbook_public": 9194, + "backend_rpc": 8094, + "backend_message_queue": 38394 + }, + "ipc": { + "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_user": "rpc", + "rpc_pass": "mtnsrpc", + "rpc_timeout": 25, + "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" + }, + "backend": { + "package_name": "backend-mtns", + "package_revision": "satoshilabs-1", + "system_user": "mtns", + "version": "1.7.3", + "binary_url": "https://github.com/omotenashicoin-project/OmotenashiCoin-HDwalletbinaries/raw/master/stable/omotenashicoin-x86_64-linux-gnu.tar.gz", + "verification_type": "", + "verification_source": "", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/omotenashicoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/omotenashicoind -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": false, + "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-mtns", + "system_user": "blockbook-mtns", + "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": 61052245, + "slip44": 341, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"omotenashicoin\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "omotenashicoin dev", + "package_maintainer_email": "git@omotenashicoin.site" } - }, - "blockbook": { - "package_name": "blockbook-mtns", - "system_user": "blockbook-mtns", - "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": 61052245, - "slip44": 341, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"omotenashicoin\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "omotenashicoin dev", - "package_maintainer_email": "git@omotenashicoin.site" - } } diff --git a/configs/coins/optimism.json b/configs/coins/optimism.json new file mode 100644 index 0000000000..bc7cc8868f --- /dev/null +++ b/configs/coins/optimism.json @@ -0,0 +1,67 @@ +{ + "coin": { + "name": "Optimism", + "shortcut": "ETH", + "network": "OP", + "label": "Optimism", + "alias": "optimism" + }, + "ports": { + "backend_rpc": 8200, + "backend_p2p": 38400, + "backend_http": 8300, + "backend_authrpc": 8400, + "blockbook_internal": 9200, + "blockbook_public": 9300 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-optimism", + "package_revision": "satoshilabs-1", + "system_user": "optimism", + "version": "1.101315.1", + "binary_url": "https://github.com/ethereum-optimism/op-geth/archive/refs/tags/v1.101315.1.tar.gz", + "verification_type": "sha256", + "verification_source": "f0f31ef2982f87f9e3eb90f2b603f5fcd9d680e487d35f5bdcf5aeba290b153f", + "extract_command": "mkdir backend/source && tar -C backend/source --strip 1 -xf v1.101315.1.tar.gz && cd backend/source && make geth && mv build/bin/geth ../ && rm -rf ../source && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/optimism_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "optimism.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "openssl rand -hex 32 > {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/jwtsecret", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-optimism", + "system_user": "blockbook-optimism", + "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, + "additional_params": { + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"optimistic-ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/optimism_archive.json b/configs/coins/optimism_archive.json new file mode 100644 index 0000000000..3ae0e9d9d9 --- /dev/null +++ b/configs/coins/optimism_archive.json @@ -0,0 +1,72 @@ +{ + "coin": { + "name": "Optimism Archive", + "shortcut": "ETH", + "network": "OP", + "label": "Optimism", + "alias": "optimism_archive" + }, + "ports": { + "backend_rpc": 8202, + "backend_p2p": 38402, + "backend_http": 8302, + "backend_authrpc": 8402, + "blockbook_internal": 9202, + "blockbook_public": 9302 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-optimism-archive", + "package_revision": "satoshilabs-1", + "system_user": "optimism", + "version": "1.101315.1", + "binary_url": "https://github.com/ethereum-optimism/op-geth/archive/refs/tags/v1.101315.1.tar.gz", + "verification_type": "sha256", + "verification_source": "f0f31ef2982f87f9e3eb90f2b603f5fcd9d680e487d35f5bdcf5aeba290b153f", + "extract_command": "mkdir backend/source && tar -C backend/source --strip 1 -xf v1.101315.1.tar.gz && cd backend/source && make geth && mv build/bin/geth ../ && rm -rf ../source && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/optimism_archive_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "optimism_archive.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "openssl rand -hex 32 > {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/jwtsecret", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "blockbook": { + "package_name": "blockbook-optimism-archive", + "system_user": "blockbook-optimism", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 600, + "additional_params": { + "address_aliases": true, + "eip1559Fees": true, + "alternative_estimate_fee": "infura", + "alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/10/suggestedGasFees\", \"periodSeconds\": 20}", + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"ethereum\",\"platformIdentifier\": \"optimistic-ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/optimism_archive_legacy_geth.json b/configs/coins/optimism_archive_legacy_geth.json new file mode 100644 index 0000000000..7a9379d95f --- /dev/null +++ b/configs/coins/optimism_archive_legacy_geth.json @@ -0,0 +1,40 @@ +{ + "coin": { + "name": "Optimism Archive Legacy Geth", + "shortcut": "ETH", + "label": "Optimism", + "alias": "optimism_archive_legacy_geth" + }, + "ports": { + "backend_rpc": 8204, + "backend_http": 8304, + "backend_p2p": 38404, + "blockbook_internal": 9204, + "blockbook_public": 9304 + }, + "backend": { + "package_name": "backend-optimism-archive-legacy-geth", + "package_revision": "satoshilabs-1", + "system_user": "optimism", + "version": "0.5.31", + "binary_url": "https://github.com/ethereum-optimism/optimism-legacy/archive/refs/heads/develop.zip", + "verification_type": "sha256", + "verification_source": "367b32b3f4c1450a57fa57650a0abdfb74ae58c09123d94b161aaec90fd6b883", + "extract_command": "mkdir backend/source && unzip -d backend/source", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/optimism_archive_legacy_geth_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "optimism_archive_legacy_geth.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "cd {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/source/optimism-legacy-devlop/l2geth && make geth && mv build/bin/geth {{.Env.BackendInstallPath}}/{{.Coin.Alias}} && rm -rf {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/source", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/optimism_archive_op_node.json b/configs/coins/optimism_archive_op_node.json new file mode 100644 index 0000000000..a16c412246 --- /dev/null +++ b/configs/coins/optimism_archive_op_node.json @@ -0,0 +1,38 @@ +{ + "coin": { + "name": "Optimism Archive Op-Node", + "shortcut": "ETH", + "label": "Optimism", + "alias": "optimism_archive_op_node" + }, + "ports": { + "backend_rpc": 8203, + "blockbook_internal": 9203, + "blockbook_public": 9303 + }, + "backend": { + "package_name": "backend-optimism-archive-op-node", + "package_revision": "satoshilabs-1", + "system_user": "optimism", + "version": "1.7.6", + "binary_url": "https://github.com/ethereum-optimism/optimism/archive/refs/tags/op-node/v1.7.6.tar.gz", + "verification_type": "sha256", + "verification_source": "91384e4834f0d0776d1c3e19613b5c50a904f6e5814349e444d42d9e8be5a7ab", + "extract_command": "mkdir backend/source && tar -C backend/source --strip 1 -xf v1.7.6.tar.gz && cd backend/source/op-node && go build -o ../../op-node ./cmd && rm -rf ../../source && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/optimism_archive_op_node_exec.sh 2>&1 >> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "optimism_archive_op_node.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/optimism_op_node.json b/configs/coins/optimism_op_node.json new file mode 100644 index 0000000000..e2b6cc1740 --- /dev/null +++ b/configs/coins/optimism_op_node.json @@ -0,0 +1,38 @@ +{ + "coin": { + "name": "Optimism Op-Node", + "shortcut": "ETH", + "label": "Optimism", + "alias": "optimism_op_node" + }, + "ports": { + "backend_rpc": 8201, + "blockbook_internal": 9201, + "blockbook_public": 9301 + }, + "backend": { + "package_name": "backend-optimism-op-node", + "package_revision": "satoshilabs-1", + "system_user": "optimism", + "version": "1.7.6", + "binary_url": "https://github.com/ethereum-optimism/optimism/archive/refs/tags/op-node/v1.7.6.tar.gz", + "verification_type": "sha256", + "verification_source": "91384e4834f0d0776d1c3e19613b5c50a904f6e5814349e444d42d9e8be5a7ab", + "extract_command": "mkdir backend/source && tar -C backend/source --strip 1 -xf v1.7.6.tar.gz && cd backend/source/op-node && go build -o ../../op-node ./cmd && rm -rf ../../source && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/optimism_op_node_exec.sh 2>&1 >> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "optimism_op_node.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/pivx.json b/configs/coins/pivx.json index 96d1531f32..327d76ab34 100644 --- a/configs/coins/pivx.json +++ b/configs/coins/pivx.json @@ -22,19 +22,19 @@ "package_name": "backend-pivx", "package_revision": "satoshilabs-1", "system_user": "pivx", - "version": "4.0.0", - "binary_url": "https://github.com/PIVX-Project/PIVX/releases/download/v4.0.0/pivx-4.0.0-x86_64-linux-gnu.tar.gz", + "version": "5.6.1", + "binary_url": "https://github.com/PIVX-Project/PIVX/releases/download/v5.6.1/pivx-5.6.1-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "6cb1f608ec0e106ea6bbb455ec8b85c7cad05ca52ab43011d3db80557816b79e", + "verification_source": "6704625c63ff73da8c57f0fbb1dab6f1e4bd8f62c17467e05f52a64012a0ee2f", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/pivx-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/pivxd -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": "", + "postinst_script_template": "cd {{.Env.BackendInstallPath}}/{{.Coin.Alias}} && HOME={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/install-params.sh", "service_type": "forking", - "service_additional_params_template": "", + "service_additional_params_template": "Environment=\"HOME={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend\"", "protect_memory": false, "mainnet": true, "server_config_file": "bitcoin_like.conf", @@ -64,4 +64,4 @@ "package_maintainer": "rikardwissing", "package_maintainer_email": "rikard@coinid.org" } -} \ No newline at end of file +} diff --git a/configs/coins/pivx_testnet.json b/configs/coins/pivx_testnet.json index 325700d2a5..5a87334cf4 100644 --- a/configs/coins/pivx_testnet.json +++ b/configs/coins/pivx_testnet.json @@ -22,10 +22,10 @@ "package_name": "backend-pivx", "package_revision": "satoshilabs-1", "system_user": "pivx", - "version": "4.0.0", - "binary_url": "https://github.com/PIVX-Project/PIVX/releases/download/v4.0.0/pivx-4.0.0-x86_64-linux-gnu.tar.gz", + "version": "5.6.1", + "binary_url": "https://github.com/PIVX-Project/PIVX/releases/download/v5.6.1/pivx-5.6.1-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "6cb1f608ec0e106ea6bbb455ec8b85c7cad05ca52ab43011d3db80557816b79e", + "verification_source": "6704625c63ff73da8c57f0fbb1dab6f1e4bd8f62c17467e05f52a64012a0ee2f", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/pivx-qt" @@ -64,4 +64,4 @@ "package_maintainer": "PIVX team", "package_maintainer_email": "random.zebra@protonmail.com" } -} \ No newline at end of file +} diff --git a/configs/coins/polygon.json b/configs/coins/polygon.json new file mode 100644 index 0000000000..a9552c19f7 --- /dev/null +++ b/configs/coins/polygon.json @@ -0,0 +1,72 @@ +{ + "coin": { + "name": "Polygon", + "shortcut": "POL", + "network": "POL", + "label": "Polygon", + "alias": "polygon_bor" + }, + "ports": { + "backend_rpc": 8070, + "backend_p2p": 38370, + "backend_http": 8170, + "blockbook_internal": 9070, + "blockbook_public": 9170 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-polygon-bor", + "package_revision": "satoshilabs-1", + "system_user": "polygon", + "version": "2.2.9", + "binary_url": "https://github.com/maticnetwork/bor/releases/download/v2.2.9/bor-v2.2.9-amd64.deb", + "verification_type": "sha256", + "verification_source": "8125ae8f2c5e2485ba112e065bcbfa40468a113a41a3dfa34871dd239fd12f6e", + "extract_command": "mkdir -p backend && dpkg --fsys-tarfile ${ARCHIVE} | tar -xO ./usr/bin/bor > backend/bor && chmod +x backend/bor && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/polygon_bor_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "polygon_bor.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "wget https://raw.githubusercontent.com/maticnetwork/bor/v2.2.9/builder/files/genesis-mainnet-v1.json -O {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/genesis.json", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/maticnetwork/bor/releases/download/v2.2.9/bor-v2.2.9-arm64.deb", + "verification_source": "344bbd01a230250a43373ee559cb596bc8afb95026ce4aa9652c46077740414f" + } + } + }, + "blockbook": { + "package_name": "blockbook-polygon", + "system_user": "blockbook-polygon", + "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, + "additional_params": { + "mempoolTxTimeoutHours": 48, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"matic-network\",\"platformIdentifier\": \"polygon-pos\",\"platformVsCurrency\": \"usd\",\"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/polygon_archive.json b/configs/coins/polygon_archive.json new file mode 100644 index 0000000000..bfd19f9404 --- /dev/null +++ b/configs/coins/polygon_archive.json @@ -0,0 +1,78 @@ +{ + "coin": { + "name": "Polygon Archive", + "shortcut": "POL", + "network": "POL", + "label": "Polygon", + "alias": "polygon_archive_bor" + }, + "ports": { + "backend_rpc": 8072, + "backend_p2p": 38372, + "backend_http": 8172, + "blockbook_internal": 9072, + "blockbook_public": 9172 + }, + "ipc": { + "rpc_url_template": "ws://127.0.0.1:{{.Ports.BackendRPC}}", + "rpc_timeout": 25 + }, + "backend": { + "package_name": "backend-polygon-archive-bor", + "package_revision": "satoshilabs-1", + "system_user": "polygon", + "version": "2.2.9", + "binary_url": "https://github.com/maticnetwork/bor/releases/download/v2.2.9/bor-v2.2.9-amd64.deb", + "verification_type": "sha256", + "verification_source": "8125ae8f2c5e2485ba112e065bcbfa40468a113a41a3dfa34871dd239fd12f6e", + "extract_command": "mkdir -p backend && dpkg --fsys-tarfile ${ARCHIVE} | tar -xO ./usr/bin/bor > backend/bor && chmod +x backend/bor && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/polygon_archive_bor_exec.sh 2>> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "polygon_archive_bor.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "postinst_script_template": "wget https://raw.githubusercontent.com/maticnetwork/bor/v2.2.9/builder/files/genesis-mainnet-v1.json -O {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/genesis.json", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "", + "platforms": { + "arm64": { + "binary_url": "https://github.com/maticnetwork/bor/releases/download/v2.2.9/bor-v2.2.9-arm64.deb", + "verification_source": "344bbd01a230250a43373ee559cb596bc8afb95026ce4aa9652c46077740414f" + } + } + }, + "blockbook": { + "package_name": "blockbook-polygon-archive", + "system_user": "blockbook-polygon", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-workers=16", + "block_chain": { + "parse": true, + "mempool_workers": 8, + "mempool_sub_workers": 2, + "block_addresses_to_keep": 600, + "additional_params": { + "address_aliases": true, + "eip1559Fees": true, + "alternative_estimate_fee": "infura", + "alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/137/suggestedGasFees\", \"periodSeconds\": 8}", + "mempoolTxTimeoutHours": 48, + "processInternalTransactions": true, + "queryBackendOnMempoolResync": false, + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"matic-network\",\"platformIdentifier\": \"polygon-pos\",\"platformVsCurrency\": \"usd\",\"periodSeconds\": 900}", + "fourByteSignatures": "https://www.4byte.directory/api/v1/signatures/" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} \ No newline at end of file diff --git a/configs/coins/polygon_heimdall.json b/configs/coins/polygon_heimdall.json new file mode 100644 index 0000000000..7c05497569 --- /dev/null +++ b/configs/coins/polygon_heimdall.json @@ -0,0 +1,39 @@ +{ + "coin": { + "name": "Polygon Heimdall", + "shortcut": "MATIC", + "label": "Polygon", + "alias": "polygon_heimdall" + }, + "ports": { + "backend_rpc": 8071, + "backend_p2p": 38371, + "backend_http": 8171, + "blockbook_internal": 9071, + "blockbook_public": 9171 + }, + "backend": { + "package_name": "backend-polygon-heimdall", + "package_revision": "satoshilabs-1", + "system_user": "polygon", + "version": "0.2.16", + "binary_url": "https://github.com/0xPolygon/heimdall-v2/releases/download/v0.2.16/heimdall-v0.2.16-amd64.deb", + "verification_type": "sha256", + "verification_source": "1682bade3065065a4b660a162e06c843b4a3079af829cec300a05e9577c9389b", + "extract_command": "mkdir -p backend && dpkg --fsys-tarfile ${ARCHIVE} | tar -xO ./usr/bin/heimdalld > backend/heimdalld && chmod +x backend/heimdalld && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/polygon_heimdall_exec.sh 2>&1 >> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "polygon_heimdall.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/polygon_heimdall_archive.json b/configs/coins/polygon_heimdall_archive.json new file mode 100644 index 0000000000..96826703db --- /dev/null +++ b/configs/coins/polygon_heimdall_archive.json @@ -0,0 +1,39 @@ +{ + "coin": { + "name": "Polygon Archive Heimdall", + "shortcut": "MATIC", + "label": "Polygon", + "alias": "polygon_archive_heimdall" + }, + "ports": { + "backend_rpc": 8073, + "backend_p2p": 38373, + "backend_http": 8173, + "blockbook_internal": 9073, + "blockbook_public": 9173 + }, + "backend": { + "package_name": "backend-polygon-archive-heimdall", + "package_revision": "satoshilabs-1", + "system_user": "polygon", + "version": "0.2.16", + "binary_url": "https://github.com/0xPolygon/heimdall-v2/releases/download/v0.2.16/heimdall-v0.2.16-amd64.deb", + "verification_type": "sha256", + "verification_source": "1682bade3065065a4b660a162e06c843b4a3079af829cec300a05e9577c9389b", + "extract_command": "mkdir -p backend && dpkg --fsys-tarfile ${ARCHIVE} | tar -xO ./usr/bin/heimdalld > backend/heimdalld && chmod +x backend/heimdalld && echo", + "exclude_files": [], + "exec_command_template": "/bin/sh -c '{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/polygon_archive_heimdall_exec.sh 2>&1 >> {{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log'", + "exec_script": "polygon_archive_heimdall.sh", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/{{.Coin.Alias}}.log", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "", + "client_config_file": "" + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" + } +} diff --git a/configs/coins/qtum.json b/configs/coins/qtum.json index f8383ded24..7699106ac1 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": "22.1", - "binary_url": "https://github.com/qtumproject/qtum/releases/download/v22.1/qtum-22.1-x86_64-linux-gnu.tar.gz", + "version": "27.1", + "binary_url": "https://github.com/qtumproject/qtum/releases/download/v27.1/qtum-27.1-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "34f2c6ca10026cc1600cfb3fbc1e606b7f163a15d98781866be6fc34e7269ea0", + "verification_source": "0b1f612f0762184240c785c66b548f2dab8eed5e25481c635806ddf81807aa86", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/qtum-qt" diff --git a/configs/coins/qtum_testnet.json b/configs/coins/qtum_testnet.json index a374c8a493..ed1218de3b 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": "22.1", - "binary_url": "https://github.com/qtumproject/qtum/releases/download/v22.1/qtum-22.1-x86_64-linux-gnu.tar.gz", + "version": "27.1", + "binary_url": "https://github.com/qtumproject/qtum/releases/download/v27.1/qtum-27.1-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "34f2c6ca10026cc1600cfb3fbc1e606b7f163a15d98781866be6fc34e7269ea0", + "verification_source": "0b1f612f0762184240c785c66b548f2dab8eed5e25481c635806ddf81807aa86", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/qtum-qt" diff --git a/configs/coins/ravencoin.json b/configs/coins/ravencoin.json index 5fe9543c67..2805feff40 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": "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", + "version": "4.6.1.0", + "binary_url": "https://github.com/RavenProject/Ravencoin/releases/download/v4.6.1/raven-4.6.1-7864c39c2-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "5a86f806e2444c6e6d612fd315f3a1369521fe50863617d5f52c3b1c1e70af76", + "verification_source": "6c6ac6382cf594b218ec50dd9662892dc2d9a493ce151acb2d7feb500436c197", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/raven-qt" diff --git a/configs/coins/trezarcoin.json b/configs/coins/trezarcoin.json index 83fe5e5452..5d26312c88 100644 --- a/configs/coins/trezarcoin.json +++ b/configs/coins/trezarcoin.json @@ -1,69 +1,69 @@ { - "coin": { - "name": "Trezarcoin", - "shortcut": "TZC", - "label": "Trezarcoin", - "alias": "trezarcoin" - }, - "ports": { - "backend_rpc": 8096, - "backend_message_queue": 38396, - "blockbook_internal": 9096, - "blockbook_public": 9196 - }, - "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-trezarcoin", - "package_revision": "satoshilabs-1", - "system_user": "trezarcoin", - "version": "2.1.1", - "binary_url": "https://github.com/TrezarCoin/TrezarCoin/releases/download/v2.1.1.0/trezarcoin-2.1.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "4b41c4fecf36a870d6bb7298d85b211f61d9f2bcc6c1bef3167f3ef772bc6fdf", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/trezarcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/trezarcoind -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" + "coin": { + "name": "Trezarcoin", + "shortcut": "TZC", + "label": "Trezarcoin", + "alias": "trezarcoin" + }, + "ports": { + "backend_rpc": 8096, + "backend_message_queue": 38396, + "blockbook_internal": 9096, + "blockbook_public": 9196 + }, + "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-trezarcoin", + "package_revision": "satoshilabs-1", + "system_user": "trezarcoin", + "version": "2.1.1", + "binary_url": "https://github.com/TrezarCoin/TrezarCoin/releases/download/v2.1.1.0/trezarcoin-2.1.1-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "4b41c4fecf36a870d6bb7298d85b211f61d9f2bcc6c1bef3167f3ef772bc6fdf", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/trezarcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/trezarcoind -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-trezarcoin", + "system_user": "blockbook-trezarcoin", + "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": 27108450, + "slip44": 232, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"trezarcoin\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-trezarcoin", - "system_user": "blockbook-trezarcoin", - "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": 27108450, - "slip44": 232, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"trezarcoin\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "IT", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/vertcoin.json b/configs/coins/vertcoin.json index 23a3436512..a1444acb9e 100644 --- a/configs/coins/vertcoin.json +++ b/configs/coins/vertcoin.json @@ -1,71 +1,71 @@ { - "coin": { - "name": "Vertcoin", - "shortcut": "VTC", - "label": "Vertcoin", - "alias": "vertcoin" - }, - "ports": { - "backend_rpc": 8040, - "backend_message_queue": 38340, - "blockbook_internal": 9040, - "blockbook_public": 9140 - }, - "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-vertcoin", - "package_revision": "satoshilabs-1", - "system_user": "vertcoin", - "version": "22.1", - "binary_url": "https://github.com/vertcoin-project/vertcoin-core/releases/download/v22.1/vertcoin-22.1-x86_64-linux-gnu.tar.gz", - "verification_type": "sha256", - "verification_source": "aab3068e02d55128326801cdbcbfcb175be96291e024edf5ab12f3af6f4433c0", - "extract_command": "tar -C backend --strip 1 -xf", - "exclude_files": ["bin/vertcoin-qt"], - "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/vertcoind -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" + "coin": { + "name": "Vertcoin", + "shortcut": "VTC", + "label": "Vertcoin", + "alias": "vertcoin" + }, + "ports": { + "backend_rpc": 8040, + "backend_message_queue": 38340, + "blockbook_internal": 9040, + "blockbook_public": 9140 + }, + "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-vertcoin", + "package_revision": "satoshilabs-1", + "system_user": "vertcoin", + "version": "23.2", + "binary_url": "https://github.com/vertcoin-project/vertcoin-core/releases/download/v23.2/vertcoin-23.2-x86_64-linux-gnu.tar.gz", + "verification_type": "sha256", + "verification_source": "51d01d1c7e1307edc0a88f44c3bd73ae8e088633ae85c56b08855b50882ee876", + "extract_command": "tar -C backend --strip 1 -xf", + "exclude_files": ["bin/vertcoin-qt"], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/vertcoind -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-vertcoin", + "system_user": "blockbook-vertcoin", + "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": 1000, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "xpub_magic_segwit_native": 78792518, + "slip44": 28, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"vertcoin\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "Petr Kracik", + "package_maintainer_email": "petr.kracik@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-vertcoin", - "system_user": "blockbook-vertcoin", - "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": 1000, - "xpub_magic": 76067358, - "xpub_magic_segwit_p2sh": 77429938, - "xpub_magic_segwit_native": 78792518, - "slip44": 28, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"vertcoin\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "Petr Kracik", - "package_maintainer_email": "petr.kracik@satoshilabs.com" - } } diff --git a/configs/coins/vertcoin_testnet.json b/configs/coins/vertcoin_testnet.json index 680f30b705..c1e3bd6642 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": "22.1", - "binary_url": "https://github.com/vertcoin-project/vertcoin-core/releases/download/v22.1/vertcoin-22.1-x86_64-linux-gnu.tar.gz", + "version": "23.2", + "binary_url": "https://github.com/vertcoin-project/vertcoin-core/releases/download/v23.2/vertcoin-23.2-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "aab3068e02d55128326801cdbcbfcb175be96291e024edf5ab12f3af6f4433c0", + "verification_source": "51d01d1c7e1307edc0a88f44c3bd73ae8e088633ae85c56b08855b50882ee876", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/vertcoin-qt" diff --git a/configs/coins/viacoin.json b/configs/coins/viacoin.json index 95e7aecb5f..8799388b03 100644 --- a/configs/coins/viacoin.json +++ b/configs/coins/viacoin.json @@ -14,7 +14,7 @@ "ipc": { "rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}", "rpc_user": "rpc", - "rpc_pass": "rpcp", + "rpc_pass": "rpc", "rpc_timeout": 25, "message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}" }, @@ -22,10 +22,10 @@ "package_name": "backend-viacoin", "package_revision": "satoshilabs-1", "system_user": "viacoin", - "version": "1.14-beta-1", - "binary_url": "https://github.com/viacoin/viacoin/releases/download/v0.15.2/viacoin-0.15.2-x86_64-linux-gnu.tar.gz", + "version": "0.16.3", + "binary_url": "https://github.com/viacoin/viacoin/releases/download/v0.16.3/viacoin-0.16.3-x86_64-linux-gnu.tar.gz", "verification_type": "sha256", - "verification_source": "bdbd432645a8b4baadddb7169ea4bef3d03f80dc2ce53dce5783d8582ac63bab", + "verification_source": "4b84d8f1485d799fdff6cb4b1a316c00056b8869b53a702cd8ce2cc581bae59a", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ "bin/viacoin-qt" @@ -41,6 +41,7 @@ "client_config_file": "bitcoin_like_client.conf", "additional_params": { "discover": 0, + "deprecatedrpc": "estimatefee", "rpcthreads": 16, "upnp": 0, "whitelist": "127.0.0.1" @@ -62,11 +63,15 @@ "xpub_magic_segwit_p2sh": 77429938, "xpub_magic_segwit_native": 78792518, "slip44": 14, - "additional_params": {} + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"viacoin\", \"periodSeconds\": 900}" + } } }, "meta": { "package_maintainer": "Romano", - "package_maintainer_email": "romanornr@gmail.com" + "package_maintainer_email": "viacoin@protonmail.com" } } \ No newline at end of file diff --git a/configs/coins/zcash.json b/configs/coins/zcash.json index b080239f23..d2ec7ed719 100644 --- a/configs/coins/zcash.json +++ b/configs/coins/zcash.json @@ -1,69 +1,66 @@ { - "coin": { - "name": "Zcash", - "shortcut": "ZEC", - "label": "Zcash", - "alias": "zcash" - }, - "ports": { - "backend_rpc": 8032, - "backend_message_queue": 38332, - "blockbook_internal": 9032, - "blockbook_public": 9132 - }, - "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-zcash", - "package_revision": "satoshilabs-1", - "system_user": "zcash", - "version": "5.4.1", - "binary_url": "https://z.cash/downloads/zcash-5.4.1-linux64-debian-bullseye.tar.gz", - "verification_type": "sha256", - "verification_source": "237e35ae9c6751f66dfd0d0d93f2844664609cc32580077d5f055c8497568313", - "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", - "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", - "postinst_script_template": "HOME={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/zcash-fetch-params", - "service_type": "forking", - "service_additional_params_template": "Environment=\"HOME={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend\"", - "protect_memory": false, - "mainnet": true, - "server_config_file": "bitcoin_like.conf", - "client_config_file": "bitcoin_like_client.conf", - "additional_params": { - "addnode": ["mainnet.z.cash"] + "coin": { + "name": "Zcash", + "shortcut": "ZEC", + "label": "Zcash", + "alias": "zcash" + }, + "ports": { + "backend_rpc": 8032, + "backend_message_queue": 38332, + "blockbook_internal": 9032, + "blockbook_public": 9132 + }, + "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-zcash", + "package_revision": "satoshilabs-1", + "system_user": "zcash", + "version": "3.0.0", + "docker_image": "zfnd/zebra:3.0.0", + "verification_type": "docker", + "verification_source": "ec082c6c3fb26b1cbb4aa0f044406dc0cfbc8ce5f3c3e5ff5f9886d832becac9", + "extract_command": "mkdir backend/bin && docker cp extract:/usr/local/bin/zebrad backend/bin/zebrad", + "exclude_files": [], + "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/zebrad --config {{.Env.BackendInstallPath}}/{{.Coin.Alias}}/zcash.conf start", + "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", + "postinst_script_template": "", + "service_type": "simple", + "service_additional_params_template": "", + "protect_memory": true, + "mainnet": true, + "server_config_file": "zcash.conf", + "client_config_file": "bitcoin_like_client.conf" + }, + "blockbook": { + "package_name": "blockbook-zcash", + "system_user": "blockbook-zcash", + "internal_binding_template": ":{{.Ports.BlockbookInternal}}", + "public_binding_template": ":{{.Ports.BlockbookPublic}}", + "explorer_url": "", + "additional_params": "-resyncindexperiod=50000 -resyncmempoolperiod=3000", + "block_chain": { + "parse": true, + "mempool_workers": 4, + "mempool_sub_workers": 8, + "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 133, + "additional_params": { + "fiat_rates": "coingecko", + "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", + "fiat_rates_params": "{\"coin\": \"zcash\", \"periodSeconds\": 900}" + } + } + }, + "meta": { + "package_maintainer": "IT Admin", + "package_maintainer_email": "it@satoshilabs.com" } - }, - "blockbook": { - "package_name": "blockbook-zcash", - "system_user": "blockbook-zcash", - "internal_binding_template": ":{{.Ports.BlockbookInternal}}", - "public_binding_template": ":{{.Ports.BlockbookPublic}}", - "explorer_url": "", - "additional_params": "", - "block_chain": { - "parse": true, - "mempool_workers": 4, - "mempool_sub_workers": 8, - "block_addresses_to_keep": 300, - "xpub_magic": 76067358, - "slip44": 133, - "additional_params": { - "fiat_rates": "coingecko", - "fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH", - "fiat_rates_params": "{\"url\": \"https://api.coingecko.com/api/v3\", \"coin\": \"zcash\", \"periodSeconds\": 900}" - } - } - }, - "meta": { - "package_maintainer": "IT Admin", - "package_maintainer_email": "it@satoshilabs.com" - } } diff --git a/configs/coins/zcash_testnet.json b/configs/coins/zcash_testnet.json index 9fd3650173..14d5332e78 100644 --- a/configs/coins/zcash_testnet.json +++ b/configs/coins/zcash_testnet.json @@ -21,10 +21,10 @@ "backend": { "package_name": "backend-zcash-testnet", "package_revision": "satoshilabs-1", - "version": "5.4.1", - "binary_url": "https://z.cash/downloads/zcash-5.4.1-linux64-debian-bullseye.tar.gz", + "version": "6.2.0", + "binary_url": "https://download.z.cash/downloads/zcash-6.2.0-linux64-debian-bullseye.tar.gz", "verification_type": "sha256", - "verification_source": "237e35ae9c6751f66dfd0d0d93f2844664609cc32580077d5f055c8497568313", + "verification_source": "71cf378c27582a4b9f9d57cafc2b5a57a46e9e52a5eda33be112dc9790c64c6f", "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", @@ -39,7 +39,9 @@ "additional_params": { "addnode": [ "testnet.z.cash" - ] + + ], + "i-am-aware-zcashd-will-be-replaced-by-zebrad-and-zallet-in-2025": 1 } }, "blockbook": { diff --git a/configs/contract-fix/ethereum.json b/configs/contract-fix/ethereum.json new file mode 100644 index 0000000000..1856a35d44 --- /dev/null +++ b/configs/contract-fix/ethereum.json @@ -0,0 +1,35 @@ +[ + { + "standard": "ERC20", + "contract": "0xC19B6A4Ac7C7Cc24459F08984Bbd09664af17bD1", + "name": "Sensorium", + "symbol": "SENSO", + "decimals": 0, + "createdInBlock": 11098997 + }, + { + "standard": "ERC20", + "contract": "0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa", + "name": "mETH", + "symbol": "mETH", + "decimals": 18, + "createdInBlock": 18290587 + }, + { + "standard": "ERC20", + "contract": "0xE6829d9a7eE3040e1276Fa75293Bde931859e8fA", + "name": "cmETH", + "symbol": "cmETH", + "decimals": 18, + "createdInBlock": 20439180 + }, + { + "type": "ERC20", + "standard": "ERC20", + "contract": "0x6f40d4A6237C257fff2dB00FA0510DeEECd303eb", + "name": "Fluid", + "symbol": "FLUID", + "decimals": 18, + "createdInBlock": 12183236 + } +] diff --git a/configs/environ.json b/configs/environ.json index 529ac6404f..93c92a12f7 100644 --- a/configs/environ.json +++ b/configs/environ.json @@ -1,7 +1,7 @@ { - "version": "0.4.0", - "backend_install_path": "/opt/coins/nodes", - "backend_data_path": "/opt/coins/data", - "blockbook_install_path": "/opt/coins/blockbook", - "blockbook_data_path": "/opt/coins/data" + "version": "0.5.0", + "backend_install_path": "/opt/coins/nodes", + "backend_data_path": "/opt/coins/data", + "blockbook_install_path": "/opt/coins/blockbook", + "blockbook_data_path": "/opt/coins/data" } diff --git a/contrib/scripts/check-and-generate-port-registry.go b/contrib/scripts/check-and-generate-port-registry.go index 048a28f31d..9b2eebc6f8 100755 --- a/contrib/scripts/check-and-generate-port-registry.go +++ b/contrib/scripts/check-and-generate-port-registry.go @@ -1,4 +1,4 @@ -//usr/bin/go run $0 $@ ; exit +// usr/bin/go run $0 $@ ; exit package main import ( @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "math" "os" "path/filepath" @@ -36,15 +35,19 @@ type Config struct { Coin struct { Name string `json:"name"` Label string `json:"label"` + Alias string `json:"alias"` + } + Ports map[string]uint16 `json:"ports"` + Blockbook struct { + PackageName string `json:"package_name"` } - Ports map[string]uint16 `json:"ports"` } func checkPorts() int { ports := make(map[uint16][]string) status := 0 - files, err := ioutil.ReadDir(inputDir) + files, err := os.ReadDir(inputDir) if err != nil { panic(err) } @@ -69,21 +72,22 @@ func checkPorts() int { } if _, ok := v.Ports["blockbook_internal"]; !ok { - fmt.Printf("%s: missing blockbook_internal port\n", v.Coin.Name) + fmt.Printf("%s (%s): missing blockbook_internal port\n", v.Coin.Name, v.Coin.Alias) status = 1 } if _, ok := v.Ports["blockbook_public"]; !ok { - fmt.Printf("%s: missing blockbook_public port\n", v.Coin.Name) + fmt.Printf("%s (%s): missing blockbook_public port\n", v.Coin.Name, v.Coin.Alias) status = 1 } if _, ok := v.Ports["backend_rpc"]; !ok { - fmt.Printf("%s: missing backend_rpc port\n", v.Coin.Name) + fmt.Printf("%s (%s): missing backend_rpc port\n", v.Coin.Name, v.Coin.Alias) status = 1 } for _, port := range v.Ports { - if port > 0 { - ports[port] = append(ports[port], v.Coin.Name) + // ignore duplicities caused by configs that do not serve blockbook directly (consensus layers) + if port > 0 && v.Blockbook.PackageName == "" { + ports[port] = append(ports[port], v.Coin.Alias) } } } @@ -132,7 +136,7 @@ func main() { } func loadPortInfo(dir string) (PortInfoSlice, error) { - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) if err != nil { return nil, err } @@ -158,26 +162,31 @@ func loadPortInfo(dir string) (PortInfoSlice, error) { return nil, fmt.Errorf("%s: json: %s", path, err) } + // skip configs that do not have blockbook (consensus layers) + if v.Blockbook.PackageName == "" { + continue + } name := v.Coin.Label - if len(name) == 0 { + // exceptions when to use Name instead of Label so that the table looks good + if len(name) == 0 || strings.Contains(v.Coin.Name, "Ethereum") || strings.Contains(v.Coin.Name, "Archive") { name = v.Coin.Name } item := &PortInfo{CoinName: name, BackendServicePorts: map[string]uint16{}} - for k, v := range v.Ports { - if v == 0 { + for k, p := range v.Ports { + if p == 0 { continue } switch k { case "blockbook_internal": - item.BlockbookInternalPort = v + item.BlockbookInternalPort = p case "blockbook_public": - item.BlockbookPublicPort = v + item.BlockbookPublicPort = p case "backend_rpc": - item.BackendRPCPort = v + item.BackendRPCPort = p default: if len(k) > 8 && k[:8] == "backend_" { - item.BackendServicePorts[k[8:]] = v + item.BackendServicePorts[k[8:]] = p } } } @@ -233,10 +242,10 @@ func writeMarkdown(output string, slice PortInfoSlice) error { fmt.Fprintf(&buf, "# Registry of ports\n\n") - header := []string{"coin", "blockbook internal port", "blockbook public port", "backend rpc port", "backend service ports (zmq)"} + header := []string{"coin", "blockbook public", "blockbook internal", "backend rpc", "backend service ports (zmq)"} writeTable(&buf, header, slice) - fmt.Fprintf(&buf, "\n> NOTE: This document is generated from coin definitions in `configs/coins`.\n") + fmt.Fprintf(&buf, "\n> NOTE: This document is generated from coin definitions in `configs/coins` using command `go run contrib/scripts/check-and-generate-port-registry.go -w`.\n") out := os.Stdout if output != "stdout" { @@ -263,11 +272,11 @@ func writeTable(w io.Writer, header []string, slice PortInfoSlice) { for i, item := range slice { row := make([]string, len(header)) row[0] = item.CoinName - if item.BlockbookInternalPort > 0 { - row[1] = fmt.Sprintf("%d", item.BlockbookInternalPort) - } if item.BlockbookPublicPort > 0 { - row[2] = fmt.Sprintf("%d", item.BlockbookPublicPort) + row[1] = fmt.Sprintf("%d", item.BlockbookPublicPort) + } + if item.BlockbookInternalPort > 0 { + row[2] = fmt.Sprintf("%d", item.BlockbookInternalPort) } if item.BackendRPCPort > 0 { row[3] = fmt.Sprintf("%d", item.BackendRPCPort) @@ -284,6 +293,7 @@ func writeTable(w io.Writer, header []string, slice PortInfoSlice) { svcPorts = append(svcPorts, s) } + sort.Strings(svcPorts) row[4] = strings.Join(svcPorts, ", ") rows[i] = row @@ -294,7 +304,7 @@ func writeTable(w io.Writer, header []string, slice PortInfoSlice) { padding[column] = len(header[column]) for _, row := range rows { - padding[column] = maxInt(padding[column], len(row[column])) + padding[column] = max(padding[column], len(row[column])) } } @@ -312,13 +322,6 @@ func writeTable(w io.Writer, header []string, slice PortInfoSlice) { } } -func maxInt(a, b int) int { - if a > b { - return a - } - return b -} - func paddedRow(row []string, padding []int) []string { out := make([]string, len(row)) for i := 0; i < len(row); i++ { diff --git a/db/bulkconnect.go b/db/bulkconnect.go index b510fe3bfb..faa49632a4 100644 --- a/db/bulkconnect.go +++ b/db/bulkconnect.go @@ -27,8 +27,9 @@ type BulkConnect struct { bulkAddressesCount int ethBlockTxs []ethBlockTx txAddressesMap map[string]*TxAddresses + blockFilters map[string][]byte balances map[string]*AddrBalance - addressContracts map[string]*AddrContracts + addressContracts map[string]*unpackedAddrContracts height uint32 } @@ -40,6 +41,7 @@ const ( partialStoreBalances = maxBulkBalances / 10 maxBulkAddrContracts = 1200000 partialStoreAddrContracts = maxBulkAddrContracts / 10 + maxBlockFilters = 1000 ) // InitBulkConnect initializes bulk connect and switches DB to inconsistent state @@ -49,7 +51,8 @@ func (d *RocksDB) InitBulkConnect() (*BulkConnect, error) { chainType: d.chainParser.GetChainType(), txAddressesMap: make(map[string]*TxAddresses), balances: make(map[string]*AddrBalance), - addressContracts: make(map[string]*AddrContracts), + addressContracts: make(map[string]*unpackedAddrContracts), + blockFilters: make(map[string][]byte), } if err := d.SetInconsistentState(true); err != nil { return nil, err @@ -170,9 +173,26 @@ func (b *BulkConnect) storeBulkAddresses(wb *grocksdb.WriteBatch) error { return nil } +func (b *BulkConnect) storeBulkBlockFilters(wb *grocksdb.WriteBatch) error { + for blockHash, blockFilter := range b.blockFilters { + if err := b.d.storeBlockFilter(wb, blockHash, blockFilter); err != nil { + return err + } + } + b.blockFilters = make(map[string][]byte) + return nil +} + 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 { + gf, err := bchain.NewGolombFilter(b.d.is.BlockGolombFilterP, b.d.is.BlockFilterScripts, block.BlockHeader.Hash, b.d.is.BlockFilterUseZeroedKey) + if err != nil { + glog.Error("connectBlockBitcoinType golomb filter error ", err) + gf = nil + } else if gf != nil && !gf.Enabled { + gf = nil + } + if err := b.d.processAddressesBitcoinType(block, addresses, b.txAddressesMap, b.balances, gf); err != nil { return err } var storeAddressesChan, storeBalancesChan chan error @@ -199,8 +219,11 @@ func (b *BulkConnect) connectBlockBitcoinType(block *bchain.Block, storeBlockTxs addresses: addresses, }) b.bulkAddressesCount += len(addresses) + if gf != nil { + b.blockFilters[block.BlockHeader.Hash] = gf.Compute() + } // open WriteBatch only if going to write - if sa || b.bulkAddressesCount > maxBulkAddresses || storeBlockTxs { + if sa || b.bulkAddressesCount > maxBulkAddresses || storeBlockTxs || len(b.blockFilters) > maxBlockFilters { start := time.Now() wb := grocksdb.NewWriteBatch() defer wb.Destroy() @@ -215,6 +238,11 @@ func (b *BulkConnect) connectBlockBitcoinType(block *bchain.Block, storeBlockTxs return err } } + if len(b.blockFilters) > maxBlockFilters { + if err := b.storeBulkBlockFilters(wb); err != nil { + return err + } + } if err := b.d.WriteBatch(wb); err != nil { return err } @@ -236,12 +264,12 @@ func (b *BulkConnect) connectBlockBitcoinType(block *bchain.Block, storeBlockTxs } func (b *BulkConnect) storeAddressContracts(wb *grocksdb.WriteBatch, all bool) (int, error) { - var ac map[string]*AddrContracts + var ac map[string]*unpackedAddrContracts if all { ac = b.addressContracts - b.addressContracts = make(map[string]*AddrContracts) + b.addressContracts = make(map[string]*unpackedAddrContracts) } else { - ac = make(map[string]*AddrContracts) + ac = make(map[string]*unpackedAddrContracts) // store some random address contracts for k, a := range b.addressContracts { ac[k] = a @@ -251,7 +279,7 @@ func (b *BulkConnect) storeAddressContracts(wb *grocksdb.WriteBatch, all bool) ( } } } - if err := b.d.storeAddressContracts(wb, ac); err != nil { + if err := b.d.storeUnpackedAddressContracts(wb, ac); err != nil { return 0, err } return len(ac), nil @@ -380,10 +408,17 @@ func (b *BulkConnect) Close() error { } wb := grocksdb.NewWriteBatch() defer wb.Destroy() + if err := b.d.storeInternalDataEthereumType(wb, b.ethBlockTxs); err != nil { + return err + } + b.ethBlockTxs = b.ethBlockTxs[:0] bac := b.bulkAddressesCount if err := b.storeBulkAddresses(wb); err != nil { return err } + if err := b.storeBulkBlockFilters(wb); err != nil { + return err + } if err := b.d.WriteBatch(wb); err != nil { return err } @@ -403,19 +438,18 @@ func (b *BulkConnect) Close() error { return err } } - bt, err := b.d.loadBlockTimes() - if err != nil { - return err - } - avg := b.d.is.SetBlockTimes(bt) - if b.d.metrics != nil { - b.d.metrics.AvgBlockPeriod.Set(float64(avg)) - } - if err := b.d.SetInconsistentState(false); err != nil { return err } glog.Info("rocksdb: bulk connect closed, db set to open state") + + // set block times asynchronously (if not in unit test), it slows server startup for chains with large number of blocks + if b.d.is.Coin == "coin-unittest" { + b.d.setBlockTimes() + } else { + go b.d.setBlockTimes() + } + b.d = nil return nil } diff --git a/db/dboptions.go b/db/dboptions.go index 90f4b02eb2..47f8df55fc 100644 --- a/db/dboptions.go +++ b/db/dboptions.go @@ -2,6 +2,7 @@ package db // #include "rocksdb/c.h" import "C" +import "flag" import "github.com/linxGnu/grocksdb" /* @@ -38,6 +39,10 @@ func boolToChar(b bool) C.uchar { } */ +var ( + noCompression = flag.Bool("noCompression", false, "disable rocksdb compression when rocksdb library can't find compression library linked with binary") +) + func createAndSetDBOptions(bloomBits int, c *grocksdb.Cache, maxOpenFiles int) *grocksdb.Options { blockOpts := grocksdb.NewDefaultBlockBasedTableOptions() blockOpts.SetBlockSize(32 << 10) // 32kB @@ -57,6 +62,11 @@ func createAndSetDBOptions(bloomBits int, c *grocksdb.Cache, maxOpenFiles int) * opts.SetWriteBufferSize(1 << 27) // 128MB opts.SetMaxBytesForLevelBase(1 << 27) // 128MB opts.SetMaxOpenFiles(maxOpenFiles) - opts.SetCompression(grocksdb.LZ4HCCompression) + if *noCompression { + // resolve error rocksDB: Invalid argument: Compression type LZ4HC is not linked with the binary + opts.SetCompression(grocksdb.NoCompression) + } else { + opts.SetCompression(grocksdb.LZ4HCCompression) + } return opts } diff --git a/db/fiat.go b/db/fiat.go index f4392f2d34..dee8aa9442 100644 --- a/db/fiat.go +++ b/db/fiat.go @@ -2,8 +2,8 @@ package db import ( "encoding/binary" + "encoding/json" "math" - "sync" "time" vlq "github.com/bsm/go-vlq" @@ -16,9 +16,6 @@ import ( // FiatRatesTimeFormat is a format string for storing FiatRates timestamps in rocksdb const FiatRatesTimeFormat = "20060102150405" // YYYYMMDDhhmmss -var lastTickerInDB *common.CurrencyRatesTicker -var lastTickerInDBMux sync.Mutex - func packTimestamp(t *time.Time) []byte { return []byte(t.UTC().Format(FiatRatesTimeFormat)) } @@ -87,20 +84,6 @@ func unpackCurrencyRatesTicker(buf []byte) (*common.CurrencyRatesTicker, error) return &ticker, nil } -// FiatRatesConvertDate checks if the date is in correct format and returns the Time object. -// Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD -func FiatRatesConvertDate(date string) (*time.Time, error) { - for format := FiatRatesTimeFormat; len(format) >= 8; format = format[:len(format)-2] { - convertedDate, err := time.Parse(format, date) - if err == nil { - return &convertedDate, nil - } - } - msg := "Date \"" + date + "\" does not match any of available formats. " - msg += "Possible formats are: YYYYMMDDhhmmss, YYYYMMDDhhmm, YYYYMMDDhh, YYYYMMDD" - return nil, errors.New(msg) -} - // FiatRatesStoreTicker stores ticker data at the specified time func (d *RocksDB) FiatRatesStoreTicker(wb *grocksdb.WriteBatch, ticker *common.CurrencyRatesTicker) error { if len(ticker.Rates) == 0 { @@ -148,22 +131,6 @@ func (d *RocksDB) FiatRatesGetTicker(tickerTime *time.Time) (*common.CurrencyRat // FiatRatesFindTicker gets FiatRates data closest to the specified timestamp, of the base currency, vsCurrency or the token if specified func (d *RocksDB) FiatRatesFindTicker(tickerTime *time.Time, vsCurrency string, token string) (*common.CurrencyRatesTicker, error) { - currentTicker := d.is.GetCurrentTicker("", "") - lastTickerInDBMux.Lock() - dbTicker := lastTickerInDB - lastTickerInDBMux.Unlock() - if currentTicker != nil { - if !tickerTime.Before(currentTicker.Timestamp) || (dbTicker != nil && tickerTime.After(dbTicker.Timestamp)) { - f := true - if token != "" && currentTicker.TokenRates != nil { - _, f = currentTicker.TokenRates[token] - } - if f { - return currentTicker, nil - } - } - } - tickerTimeFormatted := tickerTime.UTC().Format(FiatRatesTimeFormat) it := d.db.NewIteratorCF(d.ro, d.cfh[cfFiatRates]) defer it.Close() @@ -181,6 +148,26 @@ func (d *RocksDB) FiatRatesFindTicker(tickerTime *time.Time, vsCurrency string, return nil, nil } +// FiatRatesGetAllTickers gets FiatRates data closest to the specified timestamp, of the base currency, vsCurrency or the token if specified +func (d *RocksDB) FiatRatesGetAllTickers(fn func(ticker *common.CurrencyRatesTicker) error) error { + it := d.db.NewIteratorCF(d.ro, d.cfh[cfFiatRates]) + defer it.Close() + + for it.SeekToFirst(); it.Valid(); it.Next() { + ticker, err := getTickerFromIterator(it, "", "") + if err != nil { + return err + } + if ticker == nil { + return errors.New("FiatRatesGetAllTickers got nil ticker") + } + if err = fn(ticker); err != nil { + return err + } + } + return nil +} + // FiatRatesFindLastTicker gets the last FiatRates record, of the base currency, vsCurrency or the token if specified func (d *RocksDB) FiatRatesFindLastTicker(vsCurrency string, token string) (*common.CurrencyRatesTicker, error) { it := d.db.NewIteratorCF(d.ro, d.cfh[cfFiatRates]) @@ -193,14 +180,33 @@ func (d *RocksDB) FiatRatesFindLastTicker(vsCurrency string, token string) (*com return nil, err } if ticker != nil { - // if without filter, store the ticker for later use - if vsCurrency == "" && token == "" { - lastTickerInDBMux.Lock() - lastTickerInDB = ticker - lastTickerInDBMux.Unlock() - } return ticker, nil } } return nil, nil } + +func (d *RocksDB) FiatRatesGetSpecialTickers(key string) (*[]common.CurrencyRatesTicker, error) { + val, err := d.db.GetCF(d.ro, d.cfh[cfDefault], []byte(key)) + if err != nil { + return nil, err + } + defer val.Free() + data := val.Data() + if data == nil { + return nil, nil + } + var tickers []common.CurrencyRatesTicker + if err := json.Unmarshal(data, &tickers); err != nil { + return nil, err + } + return &tickers, nil +} + +func (d *RocksDB) FiatRatesStoreSpecialTickers(key string, tickers *[]common.CurrencyRatesTicker) error { + data, err := json.Marshal(tickers) + if err != nil { + return err + } + return d.db.PutCF(d.wo, d.cfh[cfDefault], []byte(key), data) +} diff --git a/db/fiat_test.go b/db/fiat_test.go index 1e5f55fb70..e9ce5b4e75 100644 --- a/db/fiat_test.go +++ b/db/fiat_test.go @@ -17,22 +17,6 @@ func TestRocksTickers(t *testing.T) { }) defer closeAndDestroyRocksDB(t, d) - // Test valid formats - for _, date := range []string{"20190130", "2019013012", "201901301250", "20190130125030"} { - _, err := FiatRatesConvertDate(date) - if err != nil { - t.Errorf("%v", err) - } - } - - // Test invalid formats - for _, date := range []string{"01102019", "10201901", "", "abc", "20190130xxx"} { - _, err := FiatRatesConvertDate(date) - if err == nil { - t.Errorf("Wrongly-formatted date \"%v\" marked as valid!", date) - } - } - // Test storing & finding tickers pastKey, _ := time.Parse(FiatRatesTimeFormat, "20190627000000") futureKey, _ := time.Parse(FiatRatesTimeFormat, "20190630000000") @@ -158,22 +142,6 @@ func TestRocksTickers(t *testing.T) { t.Errorf("Ticker %v found unexpectedly for aud vsCurrency", ticker) } - ticker = d.is.GetCurrentTicker("", "") - if ticker != nil { - t.Errorf("FiatRatesGetCurrentTicker %v found unexpectedly", ticker) - } - - d.is.SetCurrentTicker(ticker1) - ticker = d.is.GetCurrentTicker("", "") - if err != nil { - t.Errorf("TestRocksTickers err: %+v", err) - } else if ticker == nil { - t.Errorf("Ticker not found") - } else if ticker.Timestamp.Format(FiatRatesTimeFormat) != ticker1.Timestamp.Format(FiatRatesTimeFormat) { - t.Errorf("Incorrect ticker found. Expected: %v, found: %+v", ticker1.Timestamp, ticker.Timestamp) - } - - d.is.SetCurrentTicker(nil) } func Test_packUnpackCurrencyRatesTicker(t *testing.T) { diff --git a/db/rocksdb.go b/db/rocksdb.go index 783cdd120e..9fa6517f09 100644 --- a/db/rocksdb.go +++ b/db/rocksdb.go @@ -22,7 +22,7 @@ import ( "github.com/trezor/blockbook/common" ) -const dbVersion = 6 +const dbVersion = 7 const packedHeightBytes = 4 const maxAddrDescLen = 1024 @@ -57,19 +57,25 @@ const ( addressBalanceDetailUTXOIndexed = 2 ) +const addrContractsCacheMinSize = 300_000 // limit for caching address contracts in memory to speed up indexing + // RocksDB handle type RocksDB struct { - path string - db *grocksdb.DB - wo *grocksdb.WriteOptions - ro *grocksdb.ReadOptions - cfh []*grocksdb.ColumnFamilyHandle - chainParser bchain.BlockChainParser - is *common.InternalState - metrics *common.Metrics - cache *grocksdb.Cache - maxOpenFiles int - cbs connectBlockStats + path string + db *grocksdb.DB + wo *grocksdb.WriteOptions + ro *grocksdb.ReadOptions + cfh []*grocksdb.ColumnFamilyHandle + chainParser bchain.BlockChainParser + is *common.InternalState + metrics *common.Metrics + cache *grocksdb.Cache + maxOpenFiles int + cbs connectBlockStats + extendedIndex bool + connectBlockMux sync.Mutex + addrContractsCacheMux sync.Mutex + addrContractsCache map[string]*unpackedAddrContracts } const ( @@ -82,6 +88,7 @@ const ( // BitcoinType cfAddressBalance cfTxAddresses + cfBlockFilter __break__ @@ -101,7 +108,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", "blockFilter"} var cfNamesEthereumType = []string{"addressContracts", "internalData", "contracts", "functionSignatures", "blockInternalDataErrors", "addressAliases"} func openDB(path string, c *grocksdb.Cache, openFiles int) (*grocksdb.DB, []*grocksdb.ColumnFamilyHandle, error) { @@ -126,7 +133,7 @@ func openDB(path string, c *grocksdb.Cache, openFiles int) (*grocksdb.DB, []*gro // 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, extendedIndex bool) (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...) @@ -135,6 +142,7 @@ func NewRocksDB(path string, cacheSize, maxOpenFiles int, parser bchain.BlockCha cfNames = append(cfNames, cfNamesBitcoinType...) } else if chainType == bchain.ChainEthereumType { cfNames = append(cfNames, cfNamesEthereumType...) + extendedIndex = false } else { return nil, errors.New("Unknown chain type") } @@ -146,7 +154,11 @@ func NewRocksDB(path string, cacheSize, maxOpenFiles int, parser bchain.BlockCha } wo := grocksdb.NewDefaultWriteOptions() ro := grocksdb.NewDefaultReadOptions() - return &RocksDB{path, db, wo, ro, cfh, parser, nil, metrics, c, maxOpenFiles, connectBlockStats{}}, nil + r := &RocksDB{path, db, wo, ro, cfh, parser, nil, metrics, c, maxOpenFiles, connectBlockStats{}, extendedIndex, sync.Mutex{}, sync.Mutex{}, make(map[string]*unpackedAddrContracts)} + if chainType == bchain.ChainEthereumType { + go r.periodicStoreAddrContractsCache() + } + return r, nil } func (d *RocksDB) closeDB() error { @@ -161,6 +173,10 @@ func (d *RocksDB) closeDB() error { // Close releases the RocksDB environment opened in NewRocksDB. func (d *RocksDB) Close() error { if d.db != nil { + // store cached address contracts + if d.chainParser.GetChainType() == bchain.ChainEthereumType { + d.storeAddrContractsCache() + } // store the internal state of the app if d.is != nil && d.is.DbState == common.DbStateOpen { d.is.DbState = common.DbStateClosed @@ -204,6 +220,11 @@ func (d *RocksDB) WriteBatch(wb *grocksdb.WriteBatch) error { return d.db.Write(d.wo, wb) } +// HasExtendedIndex returns true if the DB indexes input txids and spending data +func (d *RocksDB) HasExtendedIndex() bool { + return d.extendedIndex +} + // GetMemoryStats returns memory usage statistics as reported by RocksDB func (d *RocksDB) GetMemoryStats() string { var total, indexAndFilter, memtable uint64 @@ -325,6 +346,9 @@ const ( // ConnectBlock indexes addresses in the block and stores them in db func (d *RocksDB) ConnectBlock(block *bchain.Block) error { + d.connectBlockMux.Lock() + defer d.connectBlockMux.Unlock() + wb := grocksdb.NewWriteBatch() defer wb.Destroy() @@ -341,7 +365,14 @@ func (d *RocksDB) ConnectBlock(block *bchain.Block) error { if chainType == bchain.ChainBitcoinType { txAddressesMap := make(map[string]*TxAddresses) balances := make(map[string]*AddrBalance) - if err := d.processAddressesBitcoinType(block, addresses, txAddressesMap, balances); err != nil { + gf, err := bchain.NewGolombFilter(d.is.BlockGolombFilterP, d.is.BlockFilterScripts, block.BlockHeader.Hash, d.is.BlockFilterUseZeroedKey) + if err != nil { + glog.Error("ConnectBlock golomb filter error ", err) + gf = nil + } else if gf != nil && !gf.Enabled { + gf = nil + } + if err := d.processAddressesBitcoinType(block, addresses, txAddressesMap, balances, gf); err != nil { return err } if err := d.storeTxAddresses(wb, txAddressesMap); err != nil { @@ -353,13 +384,19 @@ func (d *RocksDB) ConnectBlock(block *bchain.Block) error { if err := d.storeAndCleanupBlockTxs(wb, block); err != nil { return err } + if gf != nil { + blockFilter := gf.Compute() + if err := d.storeBlockFilter(wb, block.BlockHeader.Hash, blockFilter); err != nil { + return err + } + } } else if chainType == bchain.ChainEthereumType { - addressContracts := make(map[string]*AddrContracts) + addressContracts := make(map[string]*unpackedAddrContracts) blockTxs, err := d.processAddressesEthereumType(block, addresses, addressContracts) if err != nil { return err } - if err := d.storeAddressContracts(wb, addressContracts); err != nil { + if err := d.storeUnpackedAddressContracts(wb, addressContracts); err != nil { return err } if err := d.storeInternalDataEthereumType(wb, blockTxs); err != nil { @@ -380,7 +417,7 @@ func (d *RocksDB) ConnectBlock(block *bchain.Block) error { if err := d.WriteBatch(wb); err != nil { return err } - avg := d.is.AppendBlockTime(uint32(block.Time)) + avg := d.is.SetBlockTime(block.Height, uint32(block.Time)) if d.metrics != nil { d.metrics.AvgBlockPeriod.Set(float64(avg)) } @@ -408,6 +445,9 @@ type outpoint struct { type TxInput struct { AddrDesc bchain.AddressDescriptor ValueSat big.Int + // extended index properties + Txid string + Vout uint32 } // Addresses converts AddressDescriptor of the input to array of strings @@ -420,6 +460,10 @@ type TxOutput struct { AddrDesc bchain.AddressDescriptor Spent bool ValueSat big.Int + // extended index properties + SpentTxid string + SpentIndex uint32 + SpentHeight uint32 } // Addresses converts AddressDescriptor of the output to array of strings @@ -432,6 +476,8 @@ type TxAddresses struct { Height uint32 Inputs []TxInput Outputs []TxOutput + // extended index properties + VSize uint32 } // Utxo holds information about unspent transaction output @@ -574,7 +620,7 @@ 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 addressesMap, txAddressesMap map[string]*TxAddresses, balances map[string]*AddrBalance, gf *bchain.GolombFilter) error { blockTxIDs := make([][]byte, len(block.Txs)) blockTxAddresses := make([]*TxAddresses, len(block.Txs)) // first process all outputs so that inputs can refer to txs in this block @@ -586,13 +632,21 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add } blockTxIDs[txi] = btxID ta := TxAddresses{Height: block.Height} + if d.extendedIndex { + if tx.VSize > 0 { + ta.VSize = uint32(tx.VSize) + } else { + ta.VSize = uint32(len(tx.Hex)) + } + } ta.Outputs = make([]TxOutput, len(tx.Vout)) txAddressesMap[string(btxID)] = &ta blockTxAddresses[txi] = &ta - for i, output := range tx.Vout { + for i := range tx.Vout { + output := &tx.Vout[i] tao := &ta.Outputs[i] tao.ValueSat = output.ValueSat - addrDesc, err := d.chainParser.GetAddrDescFromVout(&output) + addrDesc, err := d.chainParser.GetAddrDescFromVout(output) if err != nil || len(addrDesc) == 0 || len(addrDesc) > maxAddrDescLen { if err != nil { // do not log ErrAddressMissing, transactions can be without to address (for example eth contracts) @@ -604,6 +658,9 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add } continue } + if gf != nil { + gf.AddAddrDesc(addrDesc, tx) + } tao.AddrDesc = addrDesc if d.chainParser.IsAddrDescIndexable(addrDesc) { strAddrDesc := string(addrDesc) @@ -642,7 +699,8 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add ta := blockTxAddresses[txi] ta.Inputs = make([]TxInput, len(tx.Vin)) logged := false - for i, input := range tx.Vin { + for i := range tx.Vin { + input := &tx.Vin[i] tai := &ta.Inputs[i] btxID, err := d.chainParser.PackTxid(input.Txid) if err != nil { @@ -677,10 +735,20 @@ func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses add 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) } + if gf != nil { + gf.AddAddrDesc(spentOutput.AddrDesc, tx) + } tai.AddrDesc = spentOutput.AddrDesc tai.ValueSat = spentOutput.ValueSat // mark the output as spent in tx spentOutput.Spent = true + if d.extendedIndex { + spentOutput.SpentTxid = tx.Txid + spentOutput.SpentIndex = uint32(i) + spentOutput.SpentHeight = block.Height + tai.Txid = input.Txid + tai.Vout = input.Vout + } if len(spentOutput.AddrDesc) == 0 { if !logged { glog.V(1).Infof("rocksdb: height %d, tx %v, input tx %v vout %v skipping empty address", block.Height, tx.Txid, input.Txid, input.Vout) @@ -743,6 +811,24 @@ func addToAddressesMap(addresses addressesMap, strAddrDesc string, btxID []byte, return false } +func (d *RocksDB) getTxIndexesForAddressAndBlock(addrDesc bchain.AddressDescriptor, height uint32) ([]txIndexes, error) { + key := packAddressKey(addrDesc, height) + val, err := d.db.GetCF(d.ro, d.cfh[cfAddresses], key) + if err != nil { + return nil, err + } + defer val.Free() + // nil data means the key was not found in DB + if val.Data() == nil { + return nil, nil + } + rv, err := d.unpackTxIndexes(val.Data()) + if err != nil { + return nil, err + } + return rv, nil +} + func (d *RocksDB) storeAddresses(wb *grocksdb.WriteBatch, height uint32, addresses addressesMap) error { for addrDesc, txi := range addresses { ba := bchain.AddressDescriptor(addrDesc) @@ -757,7 +843,7 @@ func (d *RocksDB) storeTxAddresses(wb *grocksdb.WriteBatch, am map[string]*TxAdd varBuf := make([]byte, maxPackedBigintBytes) buf := make([]byte, 1024) for txID, ta := range am { - buf = packTxAddresses(ta, buf, varBuf) + buf = d.packTxAddresses(ta, buf, varBuf) wb.PutCF(d.cfh[cfTxAddresses], []byte(txID), buf) } return nil @@ -794,7 +880,7 @@ func (d *RocksDB) cleanupBlockTxs(wb *grocksdb.WriteBatch, block *bchain.Block) break } val.Free() - d.db.DeleteCF(d.wo, d.cfh[cfBlockTxs], key) + wb.DeleteCF(d.cfh[cfBlockTxs], key) } } return nil @@ -901,7 +987,7 @@ func (d *RocksDB) getTxAddresses(btxID []byte) (*TxAddresses, error) { if len(buf) < 3 { return nil, nil } - return unpackTxAddresses(buf) + return d.unpackTxAddresses(buf) } // GetTxAddresses returns TxAddresses for given txid or nil if not found @@ -932,34 +1018,63 @@ func (d *RocksDB) AddrDescForOutpoint(outpoint bchain.Outpoint) (bchain.AddressD return ta.Outputs[outpoint.Vout].AddrDesc, &ta.Outputs[outpoint.Vout].ValueSat } -func packTxAddresses(ta *TxAddresses, buf []byte, varBuf []byte) []byte { +func (d *RocksDB) packTxAddresses(ta *TxAddresses, buf []byte, varBuf []byte) []byte { buf = buf[:0] l := packVaruint(uint(ta.Height), varBuf) buf = append(buf, varBuf[:l]...) + if d.extendedIndex { + l = packVaruint(uint(ta.VSize), 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) + buf = d.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) + buf = d.appendTxOutput(&ta.Outputs[i], buf, varBuf) } return buf } -func appendTxInput(txi *TxInput, buf []byte, varBuf []byte) []byte { +func (d *RocksDB) 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]...) + var l int + if d.extendedIndex { + if txi.Txid == "" { + // coinbase transaction + la = ^la + } + l = packVarint(la, varBuf) + buf = append(buf, varBuf[:l]...) + buf = append(buf, txi.AddrDesc...) + l = packBigint(&txi.ValueSat, varBuf) + buf = append(buf, varBuf[:l]...) + if la >= 0 { + btxID, err := d.chainParser.PackTxid(txi.Txid) + if err != nil { + if err != bchain.ErrTxidMissing { + glog.Error("Cannot pack txid ", txi.Txid) + } + btxID = make([]byte, d.chainParser.PackedTxidLen()) + } + buf = append(buf, btxID...) + l = packVaruint(uint(txi.Vout), varBuf) + buf = append(buf, varBuf[:l]...) + } + } else { + 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 { +func (d *RocksDB) appendTxOutput(txo *TxOutput, buf []byte, varBuf []byte) []byte { la := len(txo.AddrDesc) if txo.Spent { la = ^la @@ -969,6 +1084,20 @@ func appendTxOutput(txo *TxOutput, buf []byte, varBuf []byte) []byte { buf = append(buf, txo.AddrDesc...) l = packBigint(&txo.ValueSat, varBuf) buf = append(buf, varBuf[:l]...) + if d.extendedIndex && txo.Spent { + btxID, err := d.chainParser.PackTxid(txo.SpentTxid) + if err != nil { + if err != bchain.ErrTxidMissing { + glog.Error("Cannot pack txid ", txo.SpentTxid) + } + btxID = make([]byte, d.chainParser.PackedTxidLen()) + } + buf = append(buf, btxID...) + l = packVaruint(uint(txo.SpentIndex), varBuf) + buf = append(buf, varBuf[:l]...) + l = packVaruint(uint(txo.SpentHeight), varBuf) + buf = append(buf, varBuf[:l]...) + } return buf } @@ -1020,7 +1149,7 @@ func packAddrBalance(ab *AddrBalance, buf, varBuf []byte) []byte { l = packBigint(&ab.BalanceSat, varBuf) buf = append(buf, varBuf[:l]...) for _, utxo := range ab.Utxos { - // if Vout < 0, utxo is marked as spent + // if Vout < 0, utxo is marked as spent and removed from the entry if utxo.Vout >= 0 { buf = append(buf, utxo.BtxID...) l = packVaruint(uint(utxo.Vout), varBuf) @@ -1034,34 +1163,62 @@ func packAddrBalance(ab *AddrBalance, buf, varBuf []byte) []byte { return buf } -func unpackTxAddresses(buf []byte) (*TxAddresses, error) { +func (d *RocksDB) unpackTxAddresses(buf []byte) (*TxAddresses, error) { ta := TxAddresses{} height, l := unpackVaruint(buf) ta.Height = uint32(height) + if d.extendedIndex { + vsize, ll := unpackVaruint(buf[l:]) + ta.VSize = uint32(vsize) + l += ll + } 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:]) + l += d.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:]) + l += d.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 (d *RocksDB) unpackTxInput(ti *TxInput, buf []byte) int { + if d.extendedIndex { + al, l := unpackVarint(buf) + var coinbase bool + if al < 0 { + coinbase = true + al = ^al + } + ti.AddrDesc = append([]byte(nil), buf[l:l+al]...) + al += l + ti.ValueSat, l = unpackBigint(buf[al:]) + al += l + if !coinbase { + l = d.chainParser.PackedTxidLen() + ti.Txid, _ = d.chainParser.UnpackTxid(buf[al : al+l]) + al += l + var i uint + i, l = unpackVaruint(buf[al:]) + ti.Vout = uint32(i) + al += l + } + return al + } else { + 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 { +func (d *RocksDB) unpackTxOutput(to *TxOutput, buf []byte) int { al, l := unpackVarint(buf) if al < 0 { to.Spent = true @@ -1070,7 +1227,20 @@ func unpackTxOutput(to *TxOutput, buf []byte) int { to.AddrDesc = append([]byte(nil), buf[l:l+al]...) al += l to.ValueSat, l = unpackBigint(buf[al:]) - return l + al + al += l + if d.extendedIndex && to.Spent { + l = d.chainParser.PackedTxidLen() + to.SpentTxid, _ = d.chainParser.UnpackTxid(buf[al : al+l]) + al += l + var i uint + i, l = unpackVaruint(buf[al:]) + al += l + to.SpentIndex = uint32(i) + i, l = unpackVaruint(buf[al:]) + to.SpentHeight = uint32(i) + al += l + } + return al } func (d *RocksDB) packTxIndexes(txi []txIndexes) []byte { @@ -1092,6 +1262,34 @@ func (d *RocksDB) packTxIndexes(txi []txIndexes) []byte { return buf } +func (d *RocksDB) unpackTxIndexes(buf []byte) ([]txIndexes, error) { + var retval []txIndexes + txidUnpackedLen := d.chainParser.PackedTxidLen() + for len(buf) > txidUnpackedLen { + btxID := make([]byte, txidUnpackedLen) + copy(btxID, buf[:txidUnpackedLen]) + indexes := make([]int32, 0, 16) + buf = buf[txidUnpackedLen:] + for { + index, l := unpackVarint32(buf) + indexes = append(indexes, index>>1) + buf = buf[l:] + if index&1 == 1 { + break + } + } + retval = append(retval, txIndexes{ + btxID: btxID, + indexes: indexes, + }) + } + // reverse the return values, packTxIndexes is storing it in reverse + for i, j := 0, len(retval)-1; i < j; i, j = i+1, j-1 { + retval[i], retval[j] = retval[j], retval[i] + } + return retval, nil +} + func (d *RocksDB) packOutpoints(outpoints []outpoint) []byte { buf := make([]byte, 0, 32) bvout := make([]byte, vlq.MaxLen32) @@ -1388,6 +1586,19 @@ func (d *RocksDB) disconnectTxAddressesOutputs(wb *grocksdb.WriteBatch, btxID [] return nil } +func (d *RocksDB) disconnectBlockFilter(wb *grocksdb.WriteBatch, height uint32) error { + blockHash, err := d.GetBlockHash(height) + if err != nil { + return err + } + blockHashBytes, err := hex.DecodeString(blockHash) + if err != nil { + return err + } + wb.DeleteCF(d.cfh[cfBlockFilter], blockHashBytes) + return nil +} + func (d *RocksDB) disconnectBlock(height uint32, blockTxs []blockTxs) error { wb := grocksdb.NewWriteBatch() defer wb.Destroy() @@ -1473,6 +1684,9 @@ func (d *RocksDB) disconnectBlock(height uint32, blockTxs []blockTxs) error { wb.DeleteCF(d.cfh[cfTransactions], b) wb.DeleteCF(d.cfh[cfTxAddresses], b) } + if err := d.disconnectBlockFilter(wb, height); err != nil { + return err + } return d.WriteBatch(wb) } @@ -1535,13 +1749,26 @@ func dirSize(path string) (int64, error) { return size, err } +// limit the number of size on disk calculations by restricting it to once a minute +var databaseSizeOnDisk int64 +var nextDatabaseSizeOnDisk time.Time +var databaseSizeOnDiskMux sync.Mutex + // DatabaseSizeOnDisk returns size of the database in bytes func (d *RocksDB) DatabaseSizeOnDisk() int64 { + databaseSizeOnDiskMux.Lock() + defer databaseSizeOnDiskMux.Unlock() + now := time.Now().UTC() + if now.Before(nextDatabaseSizeOnDisk) { + return databaseSizeOnDisk + } size, err := dirSize(d.path) if err != nil { glog.Warning("rocksdb: DatabaseSizeOnDisk: ", err) return 0 } + databaseSizeOnDisk = size + nextDatabaseSizeOnDisk = now.Add(60 * time.Second) return size } @@ -1637,6 +1864,101 @@ func (d *RocksDB) loadBlockTimes() ([]uint32, error) { return times, nil } +func (d *RocksDB) setBlockTimes() { + start := time.Now() + bt, err := d.loadBlockTimes() + if err != nil { + glog.Error("rocksdb: cannot load block times ", err) + return + } + avg := d.is.SetBlockTimes(bt) + if d.metrics != nil { + d.metrics.AvgBlockPeriod.Set(float64(avg)) + } + glog.Info("rocksdb: processed block times in ", time.Since(start)) +} + +func (d *RocksDB) migrateVersion5To6(sc, nc *common.InternalStateColumn) error { + // upgrade of DB 5 to 6 for BitcoinType coins is possible + // columns transactions and fiatRates must be cleared as they are not compatible + if d.chainParser.GetChainType() == bchain.ChainBitcoinType { + if nc.Name == "transactions" { + d.db.DeleteRangeCF(d.wo, d.cfh[cfTransactions], []byte{0}, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) + } else if nc.Name == "fiatRates" { + d.db.DeleteRangeCF(d.wo, d.cfh[cfFiatRates], []byte{0}, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) + } + glog.Infof("Column %s upgraded from v%d to v%d", nc.Name, sc.Version, dbVersion) + } else { + return errors.Errorf("DB version %v of column '%v' does not match the required version %v. DB is not compatible.", sc.Version, sc.Name, dbVersion) + } + return nil +} + +func (d *RocksDB) migrateAddrContractsToV7(approxRows int64) error { + glog.Info("MigrateAddrContracts: starting, will process approximately ", approxRows, " rows") + var row int64 + var seekKey []byte + // do not use cache + ro := grocksdb.NewDefaultReadOptions() + ro.SetFillCache(false) + for { + var addrDesc bchain.AddressDescriptor + it := d.db.NewIteratorCF(ro, d.cfh[cfAddressContracts]) + if row == 0 { + it.SeekToFirst() + } else { + glog.Info("MigrateAddrContracts: row ", row) + it.Seek(seekKey) + it.Next() + } + + wb := grocksdb.NewWriteBatch() + for count := 0; it.Valid() && count < refreshIterator; it.Next() { + addrDesc = append([]byte{}, it.Key().Data()...) + buf := it.Value().Data() + count++ + row++ + acs, err := unpackAddrContractsV6(buf, addrDesc) + if err != nil { + glog.Error(err, ", ", hex.EncodeToString(buf)) + acs = &AddrContracts{} + } + repacked := packAddrContracts(acs) + wb.PutCF(d.cfh[cfAddressContracts], addrDesc, repacked) + } + err := d.WriteBatch(wb) + wb.Destroy() + if err != nil { + return errors.Errorf("error storing repacked data %v", err) + } + + seekKey = addrDesc + valid := it.Valid() + it.Close() + if !valid { + break + } + } + glog.Info("MigrateAddrContracts: finished, migrated ", row, " rows") + return nil +} + +func (d *RocksDB) migrateVersion6To7(sc, nc *common.InternalStateColumn) error { + // DB v7 must migrate ethereum type column addressContracts + if d.chainParser.GetChainType() == bchain.ChainEthereumType { + if nc.Name == "addressContracts" { + err := d.migrateAddrContractsToV7(sc.Rows) + if err != nil { + return err + } + } else if nc.Name == "transactions" { + d.db.DeleteRangeCF(d.wo, d.cfh[cfTransactions], []byte{0}, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) + } + glog.Infof("Column %s migrated from v%d to v%d", nc.Name, sc.Version, dbVersion) + } + return nil +} + func (d *RocksDB) checkColumns(is *common.InternalState) ([]common.InternalStateColumn, error) { // make sure that column stats match the columns sc := is.DbColumns @@ -1648,15 +1970,16 @@ func (d *RocksDB) checkColumns(is *common.InternalState) ([]common.InternalState if sc[j].Name == nc[i].Name { // check the version of the column, if it does not match, the db is not compatible if sc[j].Version != dbVersion { - // upgrade of DB 5 to 6 for BitcoinType coins is possible - // columns transactions and fiatRates must be cleared as they are not compatible - if sc[j].Version == 5 && dbVersion == 6 && d.chainParser.GetChainType() == bchain.ChainBitcoinType { - if nc[i].Name == "transactions" { - d.db.DeleteRangeCF(d.wo, d.cfh[cfTransactions], []byte{0}, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) - } else if nc[i].Name == "fiatRates" { - d.db.DeleteRangeCF(d.wo, d.cfh[cfFiatRates], []byte{0}, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) + if sc[j].Version == 5 && dbVersion == 6 { + err := d.migrateVersion5To6(&sc[j], &nc[i]) + if err != nil { + return nil, err + } + } else if sc[j].Version == 6 && dbVersion == 7 { + err := d.migrateVersion6To7(&sc[j], &nc[i]) + if err != nil { + return nil, err } - glog.Infof("Column %s upgraded from v%d to v%d", nc[i].Name, sc[j].Version, dbVersion) } else { return nil, errors.Errorf("DB version %v of column '%v' does not match the required version %v. DB is not compatible.", sc[j].Version, sc[j].Name, dbVersion) } @@ -1673,7 +1996,7 @@ func (d *RocksDB) checkColumns(is *common.InternalState) ([]common.InternalState } // LoadInternalState loads from db internal state or initializes a new one if not yet stored -func (d *RocksDB) LoadInternalState(rpcCoin string) (*common.InternalState, error) { +func (d *RocksDB) LoadInternalState(config *common.Config) (*common.InternalState, error) { val, err := d.db.GetCF(d.ro, d.cfh[cfDefault], []byte(internalStateKey)) if err != nil { return nil, err @@ -1682,7 +2005,15 @@ func (d *RocksDB) LoadInternalState(rpcCoin string) (*common.InternalState, erro data := val.Data() var is *common.InternalState if len(data) == 0 { - is = &common.InternalState{Coin: rpcCoin, UtxoChecked: true} + is = &common.InternalState{ + Coin: config.CoinName, + UtxoChecked: true, + SortedAddressContracts: true, + ExtendedIndex: d.extendedIndex, + BlockGolombFilterP: config.BlockGolombFilterP, + BlockFilterScripts: config.BlockFilterScripts, + BlockFilterUseZeroedKey: config.BlockFilterUseZeroedKey, + } } else { is, err = common.UnpackInternalState(data) if err != nil { @@ -1691,9 +2022,21 @@ func (d *RocksDB) LoadInternalState(rpcCoin string) (*common.InternalState, erro // verify that the rpc coin matches DB coin // running it mismatched would corrupt the database if is.Coin == "" { - is.Coin = rpcCoin - } else if is.Coin != rpcCoin { - return nil, errors.Errorf("Coins do not match. DB coin %v, RPC coin %v", is.Coin, rpcCoin) + is.Coin = config.CoinName + } else if is.Coin != config.CoinName { + return nil, errors.Errorf("Coins do not match. DB coin %v, RPC coin %v", is.Coin, config.CoinName) + } + if is.ExtendedIndex != d.extendedIndex { + return nil, errors.Errorf("ExtendedIndex setting does not match. DB extendedIndex %v, extendedIndex in options %v", is.ExtendedIndex, d.extendedIndex) + } + if is.BlockGolombFilterP != config.BlockGolombFilterP { + return nil, errors.Errorf("BlockGolombFilterP does not match. DB BlockGolombFilterP %v, config BlockGolombFilterP %v", is.BlockGolombFilterP, config.BlockGolombFilterP) + } + if is.BlockFilterScripts != config.BlockFilterScripts { + return nil, errors.Errorf("BlockFilterScripts does not match. DB BlockFilterScripts %v, config BlockFilterScripts %v", is.BlockFilterScripts, config.BlockFilterScripts) + } + if is.BlockFilterUseZeroedKey != config.BlockFilterUseZeroedKey { + return nil, errors.Errorf("BlockFilterUseZeroedKey does not match. DB BlockFilterUseZeroedKey %v, config BlockFilterUseZeroedKey %v", is.BlockFilterUseZeroedKey, config.BlockFilterUseZeroedKey) } } nc, err := d.checkColumns(is) @@ -1701,15 +2044,14 @@ func (d *RocksDB) LoadInternalState(rpcCoin string) (*common.InternalState, erro return nil, err } is.DbColumns = nc - bt, err := d.loadBlockTimes() - if err != nil { - return nil, err - } - avg := is.SetBlockTimes(bt) - if d.metrics != nil { - d.metrics.AvgBlockPeriod.Set(float64(avg)) - } + d.is = is + // set block times asynchronously (if not in unit test), it slows server startup for chains with large number of blocks + if is.Coin == "coin-unittest" { + d.setBlockTimes() + } else { + go d.setBlockTimes() + } // after load, reset the synchronization data is.IsSynchronized = false is.IsMempoolSynchronized = false @@ -1725,6 +2067,14 @@ func (d *RocksDB) LoadInternalState(rpcCoin string) (*common.InternalState, erro glog.Infof("loaded %d address alias records", recordsCount) } + is.CoinShortcut = config.CoinShortcut + if config.CoinLabel == "" { + is.CoinLabel = config.CoinName + } else { + is.CoinLabel = config.CoinLabel + } + is.Network = config.Network + return is, nil } @@ -1794,13 +2144,13 @@ func (d *RocksDB) computeColumnSize(col int, stopCompute chan os.Signal) (int64, return 0, 0, 0, errors.New("Interrupted") default: } - key = it.Key().Data() + key = append([]byte{}, it.Key().Data()...) count++ rows++ keysSum += int64(len(key)) valuesSum += int64(len(it.Value().Data())) } - seekKey = append([]byte{}, key...) + seekKey = key valid := it.Valid() it.Close() if !valid { @@ -1975,7 +2325,7 @@ func (d *RocksDB) FixUtxos(stop chan os.Signal) error { return errors.New("Interrupted") default: } - addrDesc = it.Key().Data() + addrDesc = append([]byte{}, it.Key().Data()...) buf := it.Value().Data() count++ row++ @@ -2002,7 +2352,7 @@ func (d *RocksDB) FixUtxos(stop chan os.Signal) error { fixedCount++ } } - seekKey = append([]byte{}, addrDesc...) + seekKey = addrDesc valid := it.Valid() it.Close() if !valid { @@ -2013,6 +2363,32 @@ func (d *RocksDB) FixUtxos(stop chan os.Signal) error { return nil } +func (d *RocksDB) storeBlockFilter(wb *grocksdb.WriteBatch, blockHash string, blockFilter []byte) error { + blockHashBytes, err := hex.DecodeString(blockHash) + if err != nil { + return err + } + wb.PutCF(d.cfh[cfBlockFilter], blockHashBytes, blockFilter) + return nil +} + +func (d *RocksDB) GetBlockFilter(blockHash string) (string, error) { + blockHashBytes, err := hex.DecodeString(blockHash) + if err != nil { + return "", err + } + val, err := d.db.GetCF(d.ro, d.cfh[cfBlockFilter], blockHashBytes) + if err != nil { + return "", err + } + defer val.Free() + buf := val.Data() + if buf == nil { + return "", nil + } + return hex.EncodeToString(buf), nil +} + // Helpers func packAddressKey(addrDesc bchain.AddressDescriptor, height uint32) []byte { @@ -2139,6 +2515,10 @@ func packBigint(bi *big.Int, buf []byte) int { return fb + 1 } +func packedBigintLen(buf []byte) int { + return int(buf[0]) + 1 +} + func unpackBigint(buf []byte) (big.Int, int) { var r big.Int l := int(buf[0]) + 1 diff --git a/db/rocksdb_ethereumtype.go b/db/rocksdb_ethereumtype.go index b16429b330..7d59825ee8 100644 --- a/db/rocksdb_ethereumtype.go +++ b/db/rocksdb_ethereumtype.go @@ -4,7 +4,10 @@ import ( "bytes" "encoding/hex" "math/big" + "os" + "sort" "sync" + "time" vlq "github.com/bsm/go-vlq" "github.com/golang/glog" @@ -17,14 +20,101 @@ import ( const InternalTxIndexOffset = 1 const ContractIndexOffset = 2 +type AggregateFn = func(*big.Int, *big.Int) + +type Ids []big.Int + +func (s *Ids) sort() bool { + sorted := false + sort.Slice(*s, func(i, j int) bool { + isLess := (*s)[i].CmpAbs(&(*s)[j]) == -1 + if isLess == (i > j) { // it is necessary to swap - (id[i]j) or (id[i]>id[j] and i= 0 + }) +} + +// insert id in ascending order +func (s *Ids) insert(id big.Int) { + i := s.search(id) + if i == len(*s) { + *s = append(*s, id) + } else { + *s = append((*s)[:i+1], (*s)[i:]...) + (*s)[i] = id + } +} + +func (s *Ids) remove(id big.Int) { + i := s.search(id) + // remove id if found + if i < len(*s) && (*s)[i].CmpAbs(&id) == 0 { + *s = append((*s)[:i], (*s)[i+1:]...) + } +} + +type MultiTokenValues []bchain.MultiTokenValue + +func (s *MultiTokenValues) sort() bool { + sorted := false + sort.Slice(*s, func(i, j int) bool { + isLess := (*s)[i].Id.CmpAbs(&(*s)[j].Id) == -1 + if isLess == (i > j) { // it is necessary to swap - (id[i]j) or (id[i]>id[j] and i= 0 + }) +} + +func (s *MultiTokenValues) upsert(m bchain.MultiTokenValue, index int32, aggregate AggregateFn) { + i := s.search(m) + if i < len(*s) && (*s)[i].Id.CmpAbs(&m.Id) == 0 { + aggregate(&(*s)[i].Value, &m.Value) + // if transfer from, remove if the value is zero + if index < 0 && len((*s)[i].Value.Bits()) == 0 { + *s = append((*s)[:i], (*s)[i+1:]...) + } + return + } + if index >= 0 { + elem := bchain.MultiTokenValue{ + Id: m.Id, + Value: *new(big.Int).Set(&m.Value), + } + if i == len(*s) { + *s = append(*s, elem) + } else { + *s = append((*s)[:i+1], (*s)[i:]...) + (*s)[i] = elem + } + } +} + // AddrContract is Contract address with number of transactions done by given address type AddrContract struct { - Type bchain.TokenType + Standard bchain.TokenStandard Contract bchain.AddressDescriptor Txs uint - Value big.Int // single value of ERC20 - Ids []big.Int // multiple ERC721 tokens - MultiTokenValues []bchain.MultiTokenValue // multiple ERC1155 tokens + Value big.Int // single value of ERC20 + Ids Ids // multiple ERC721 tokens + MultiTokenValues MultiTokenValues // multiple ERC1155 tokens } // AddrContracts contains number of transactions and contracts for an address @@ -35,8 +125,8 @@ type AddrContracts struct { Contracts []AddrContract } -// packAddrContract packs AddrContracts into a byte buffer -func packAddrContracts(acs *AddrContracts) []byte { +// packAddrContracts packs AddrContracts into a byte buffer +func packAddrContractsV6(acs *AddrContracts) []byte { buf := make([]byte, 0, 128) varBuf := make([]byte, maxPackedBigintBytes) l := packVaruint(acs.TotalTxs, varBuf) @@ -47,12 +137,12 @@ func packAddrContracts(acs *AddrContracts) []byte { buf = append(buf, varBuf[:l]...) for _, ac := range acs.Contracts { buf = append(buf, ac.Contract...) - l = packVaruint(uint(ac.Type)+ac.Txs<<2, varBuf) + l = packVaruint(uint(ac.Standard)+ac.Txs<<2, varBuf) buf = append(buf, varBuf[:l]...) - if ac.Type == bchain.FungibleToken { + if ac.Standard == bchain.FungibleToken { l = packBigint(&ac.Value, varBuf) buf = append(buf, varBuf[:l]...) - } else if ac.Type == bchain.NonFungibleToken { + } else if ac.Standard == bchain.NonFungibleToken { l = packVaruint(uint(len(ac.Ids)), varBuf) buf = append(buf, varBuf[:l]...) for i := range ac.Ids { @@ -73,14 +163,114 @@ func packAddrContracts(acs *AddrContracts) []byte { return buf } -func unpackAddrContracts(buf []byte, addrDesc bchain.AddressDescriptor) (*AddrContracts, error) { +// packAddrContracts packs AddrContracts into a byte buffer +func packAddrContracts(acs *AddrContracts) []byte { + buf := make([]byte, 0, 8+len(acs.Contracts)*(eth.EthereumTypeAddressDescriptorLen+12)) + varBuf := make([]byte, maxPackedBigintBytes) + l := packVaruint(acs.TotalTxs, varBuf) + buf = append(buf, varBuf[:l]...) + l = packVaruint(acs.NonContractTxs, varBuf) + buf = append(buf, varBuf[:l]...) + l = packVaruint(acs.InternalTxs, varBuf) + buf = append(buf, varBuf[:l]...) + l = packVaruint(uint(len(acs.Contracts)), varBuf) + buf = append(buf, varBuf[:l]...) + for _, ac := range acs.Contracts { + buf = append(buf, ac.Contract...) + l = packVaruint(uint(ac.Standard)+ac.Txs<<2, varBuf) + buf = append(buf, varBuf[:l]...) + if ac.Standard == bchain.FungibleToken { + l = packBigint(&ac.Value, varBuf) + buf = append(buf, varBuf[:l]...) + } else if ac.Standard == bchain.NonFungibleToken { + l = packVaruint(uint(len(ac.Ids)), varBuf) + buf = append(buf, varBuf[:l]...) + for i := range ac.Ids { + l = packBigint(&ac.Ids[i], varBuf) + buf = append(buf, varBuf[:l]...) + } + } else { // bchain.ERC1155 + l = packVaruint(uint(len(ac.MultiTokenValues)), varBuf) + buf = append(buf, varBuf[:l]...) + for i := range ac.MultiTokenValues { + l = packBigint(&ac.MultiTokenValues[i].Id, varBuf) + buf = append(buf, varBuf[:l]...) + l = packBigint(&ac.MultiTokenValues[i].Value, varBuf) + buf = append(buf, varBuf[:l]...) + } + } + } + return buf +} + +func unpackAddrContractsV6(buf []byte, addrDesc bchain.AddressDescriptor) (acs *AddrContracts, err error) { + tt, l := unpackVaruint(buf) + buf = buf[l:] + nct, l := unpackVaruint(buf) + buf = buf[l:] + ict, l := unpackVaruint(buf) + buf = buf[l:] + c := make([]AddrContract, 0, len(buf)/30+4) + for len(buf) > 0 { + if len(buf) < eth.EthereumTypeAddressDescriptorLen { + return nil, errors.New("Invalid data stored in cfAddressContracts for AddrDesc " + addrDesc.String()) + } + contract := append(bchain.AddressDescriptor(nil), buf[:eth.EthereumTypeAddressDescriptorLen]...) + txs, l := unpackVaruint(buf[eth.EthereumTypeAddressDescriptorLen:]) + buf = buf[eth.EthereumTypeAddressDescriptorLen+l:] + standard := bchain.TokenStandard(txs & 3) + txs >>= 2 + ac := AddrContract{ + Standard: standard, + Contract: contract, + Txs: txs, + } + if standard == bchain.FungibleToken { + b, ll := unpackBigint(buf) + buf = buf[ll:] + ac.Value = b + } else { + len, ll := unpackVaruint(buf) + buf = buf[ll:] + if standard == bchain.NonFungibleToken { + ac.Ids = make(Ids, len) + for i := uint(0); i < len; i++ { + b, ll := unpackBigint(buf) + buf = buf[ll:] + ac.Ids[i] = b + } + } else { + ac.MultiTokenValues = make(MultiTokenValues, len) + for i := uint(0); i < len; i++ { + b, ll := unpackBigint(buf) + buf = buf[ll:] + ac.MultiTokenValues[i].Id = b + b, ll = unpackBigint(buf) + buf = buf[ll:] + ac.MultiTokenValues[i].Value = b + } + } + } + c = append(c, ac) + } + return &AddrContracts{ + TotalTxs: tt, + NonContractTxs: nct, + InternalTxs: ict, + Contracts: c, + }, nil +} + +func unpackAddrContracts(buf []byte, addrDesc bchain.AddressDescriptor) (acs *AddrContracts, err error) { tt, l := unpackVaruint(buf) buf = buf[l:] nct, l := unpackVaruint(buf) buf = buf[l:] ict, l := unpackVaruint(buf) buf = buf[l:] - c := make([]AddrContract, 0, 4) + cl, l := unpackVaruint(buf) + buf = buf[l:] + c := make([]AddrContract, 0, cl) for len(buf) > 0 { if len(buf) < eth.EthereumTypeAddressDescriptorLen { return nil, errors.New("Invalid data stored in cfAddressContracts for AddrDesc " + addrDesc.String()) @@ -88,29 +278,29 @@ func unpackAddrContracts(buf []byte, addrDesc bchain.AddressDescriptor) (*AddrCo contract := append(bchain.AddressDescriptor(nil), buf[:eth.EthereumTypeAddressDescriptorLen]...) txs, l := unpackVaruint(buf[eth.EthereumTypeAddressDescriptorLen:]) buf = buf[eth.EthereumTypeAddressDescriptorLen+l:] - ttt := bchain.TokenType(txs & 3) + standard := bchain.TokenStandard(txs & 3) txs >>= 2 ac := AddrContract{ - Type: ttt, + Standard: standard, Contract: contract, Txs: txs, } - if ttt == bchain.FungibleToken { + if standard == bchain.FungibleToken { b, ll := unpackBigint(buf) buf = buf[ll:] ac.Value = b } else { len, ll := unpackVaruint(buf) buf = buf[ll:] - if ttt == bchain.NonFungibleToken { - ac.Ids = make([]big.Int, len) + if standard == bchain.NonFungibleToken { + ac.Ids = make(Ids, len) for i := uint(0); i < len; i++ { b, ll := unpackBigint(buf) buf = buf[ll:] ac.Ids[i] = b } } else { - ac.MultiTokenValues = make([]bchain.MultiTokenValue, len) + ac.MultiTokenValues = make(MultiTokenValues, len) for i := uint(0); i < len; i++ { b, ll := unpackBigint(buf) buf = buf[ll:] @@ -158,7 +348,7 @@ func (d *RocksDB) GetAddrDescContracts(addrDesc bchain.AddressDescriptor) (*Addr return unpackAddrContracts(buf, addrDesc) } -func findContractInAddressContracts(contract bchain.AddressDescriptor, contracts []AddrContract) (int, bool) { +func findContractInAddressContracts(contract bchain.AddressDescriptor, contracts []unpackedAddrContract) (int, bool) { for i := range contracts { if bytes.Equal(contract, contracts[i].Contract) { return i, true @@ -209,8 +399,8 @@ func addToAddressesMapEthereumType(addresses addressesMap, strAddrDesc string, b return false } -func addToContract(c *AddrContract, contractIndex int, index int32, contract bchain.AddressDescriptor, transfer *bchain.TokenTransfer, addTxCount bool) int32 { - var aggregate func(*big.Int, *big.Int) +func addToContract(c *unpackedAddrContract, contractIndex int, index int32, contract bchain.AddressDescriptor, transfer *bchain.TokenTransfer, addTxCount bool) int32 { + var aggregate AggregateFn // index 0 is for ETH transfers, index 1 (InternalTxIndexOffset) is for internal transfers, contract indexes start with 2 (ContractIndexOffset) if index < 0 { index = ^int32(contractIndex + ContractIndexOffset) @@ -227,43 +417,17 @@ func addToContract(c *AddrContract, contractIndex int, index int32, contract bch s.Add(s, v) } } - if transfer.Type == bchain.FungibleToken { - aggregate(&c.Value, &transfer.Value) - } else if transfer.Type == bchain.NonFungibleToken { + if transfer.Standard == bchain.FungibleToken { + aggregate(c.Value.get(), &transfer.Value) + } else if transfer.Standard == bchain.NonFungibleToken { if index < 0 { - // remove token from the list - for i := range c.Ids { - if c.Ids[i].Cmp(&transfer.Value) == 0 { - c.Ids = append(c.Ids[:i], c.Ids[i+1:]...) - break - } - } + c.Ids.remove(transfer.Value) } else { - // add token to the list - c.Ids = append(c.Ids, transfer.Value) + c.Ids.insert(transfer.Value) } } else { // bchain.ERC1155 for _, t := range transfer.MultiTokenValues { - for i := range c.MultiTokenValues { - // find the token in the list - if c.MultiTokenValues[i].Id.Cmp(&t.Id) == 0 { - aggregate(&c.MultiTokenValues[i].Value, &t.Value) - // if transfer from, remove if the value is zero - if index < 0 && len(c.MultiTokenValues[i].Value.Bits()) == 0 { - c.MultiTokenValues = append(c.MultiTokenValues[:i], c.MultiTokenValues[i+1:]...) - } - goto nextTransfer - } - } - // if not found and transfer to, add to the list - // it is necessary to add a copy of the value so that subsequent calls to addToContract do not change the transfer value - if index >= 0 { - c.MultiTokenValues = append(c.MultiTokenValues, bchain.MultiTokenValue{ - Id: t.Id, - Value: *new(big.Int).Set(&t.Value), - }) - } - nextTransfer: + c.MultiTokenValues.upsert(t, index, aggregate) } } if addTxCount { @@ -272,17 +436,17 @@ func addToContract(c *AddrContract, contractIndex int, index int32, contract bch return index } -func (d *RocksDB) addToAddressesAndContractsEthereumType(addrDesc bchain.AddressDescriptor, btxID []byte, index int32, contract bchain.AddressDescriptor, transfer *bchain.TokenTransfer, addTxCount bool, addresses addressesMap, addressContracts map[string]*AddrContracts) error { +func (d *RocksDB) addToAddressesAndContractsEthereumType(addrDesc bchain.AddressDescriptor, btxID []byte, index int32, contract bchain.AddressDescriptor, transfer *bchain.TokenTransfer, addTxCount bool, addresses addressesMap, addressContracts map[string]*unpackedAddrContracts) error { var err error strAddrDesc := string(addrDesc) ac, e := addressContracts[strAddrDesc] if !e { - ac, err = d.GetAddrDescContracts(addrDesc) + ac, err = d.getUnpackedAddrDescContracts(addrDesc) if err != nil { return err } if ac == nil { - ac = &AddrContracts{} + ac = &unpackedAddrContracts{} } addressContracts[strAddrDesc] = ac d.cbs.balancesMiss++ @@ -304,9 +468,9 @@ func (d *RocksDB) addToAddressesAndContractsEthereumType(addrDesc bchain.Address contractIndex, found := findContractInAddressContracts(contract, ac.Contracts) if !found { contractIndex = len(ac.Contracts) - ac.Contracts = append(ac.Contracts, AddrContract{ + ac.Contracts = append(ac.Contracts, unpackedAddrContract{ Contract: contract, - Type: transfer.Type, + Standard: transfer.Standard, }) } c := &ac.Contracts[contractIndex] @@ -328,7 +492,7 @@ func (d *RocksDB) addToAddressesAndContractsEthereumType(addrDesc bchain.Address type ethBlockTxContract struct { from, to, contract bchain.AddressDescriptor - transferType bchain.TokenType + transferStandard bchain.TokenStandard value big.Int idValues []bchain.MultiTokenValue } @@ -353,7 +517,7 @@ type ethBlockTx struct { internalData *ethInternalData } -func (d *RocksDB) processBaseTxData(blockTx *ethBlockTx, tx *bchain.Tx, addresses addressesMap, addressContracts map[string]*AddrContracts) error { +func (d *RocksDB) processBaseTxData(blockTx *ethBlockTx, tx *bchain.Tx, addresses addressesMap, addressContracts map[string]*unpackedAddrContracts) error { var from, to bchain.AddressDescriptor var err error // there is only one output address in EthereumType transaction, store it in format txid 0 @@ -388,7 +552,23 @@ func (d *RocksDB) processBaseTxData(blockTx *ethBlockTx, tx *bchain.Tx, addresse return nil } -func (d *RocksDB) processInternalData(blockTx *ethBlockTx, tx *bchain.Tx, id *bchain.EthereumInternalData, addresses addressesMap, addressContracts map[string]*AddrContracts) error { +func (d *RocksDB) setAddressTxIndexesToAddressMap(addrDesc bchain.AddressDescriptor, height uint32, addresses addressesMap) error { + strAddrDesc := string(addrDesc) + _, found := addresses[strAddrDesc] + if !found { + txIndexes, err := d.getTxIndexesForAddressAndBlock(addrDesc, height) + if err != nil { + return err + } + if len(txIndexes) > 0 { + addresses[strAddrDesc] = txIndexes + } + } + return nil +} + +// existingBlock signals that internal data are reconnected to already indexed block after they failed during standard sync +func (d *RocksDB) processInternalData(blockTx *ethBlockTx, tx *bchain.Tx, id *bchain.EthereumInternalData, addresses addressesMap, addressContracts map[string]*unpackedAddrContracts, existingBlock bool) error { blockTx.internalData = ðInternalData{ internalType: id.Type, errorMsg: id.Error, @@ -404,6 +584,11 @@ func (d *RocksDB) processInternalData(blockTx *ethBlockTx, tx *bchain.Tx, id *bc blockTx.internalData.internalType = bchain.CALL } else { blockTx.internalData.contract = to + if existingBlock { + if err = d.setAddressTxIndexesToAddressMap(to, tx.BlockHeight, addresses); err != nil { + return err + } + } if err = d.addToAddressesAndContractsEthereumType(to, blockTx.btxID, internalTransferTo, nil, nil, true, addresses, addressContracts); err != nil { return err } @@ -422,6 +607,11 @@ func (d *RocksDB) processInternalData(blockTx *ethBlockTx, tx *bchain.Tx, id *bc glog.Warningf("rocksdb: processInternalData: %v, tx %v, internal transfer %d to", err, tx.Txid, i) } } else { + if existingBlock { + if err = d.setAddressTxIndexesToAddressMap(to, tx.BlockHeight, addresses); err != nil { + return err + } + } if err = d.addToAddressesAndContractsEthereumType(to, blockTx.btxID, internalTransferTo, nil, nil, true, addresses, addressContracts); err != nil { return err } @@ -433,6 +623,11 @@ func (d *RocksDB) processInternalData(blockTx *ethBlockTx, tx *bchain.Tx, id *bc glog.Warningf("rocksdb: processInternalData: %v, tx %v, internal transfer %d from", err, tx.Txid, i) } } else { + if existingBlock { + if err = d.setAddressTxIndexesToAddressMap(from, tx.BlockHeight, addresses); err != nil { + return err + } + } if err = d.addToAddressesAndContractsEthereumType(from, blockTx.btxID, internalTransferFrom, nil, nil, !bytes.Equal(from, to), addresses, addressContracts); err != nil { return err } @@ -445,7 +640,7 @@ func (d *RocksDB) processInternalData(blockTx *ethBlockTx, tx *bchain.Tx, id *bc return nil } -func (d *RocksDB) processContractTransfers(blockTx *ethBlockTx, tx *bchain.Tx, addresses addressesMap, addressContracts map[string]*AddrContracts) error { +func (d *RocksDB) processContractTransfers(blockTx *ethBlockTx, tx *bchain.Tx, addresses addressesMap, addressContracts map[string]*unpackedAddrContracts) error { tokenTransfers, err := d.chainParser.EthereumTypeGetTokenTransfersFromTx(tx) if err != nil { glog.Warningf("rocksdb: processContractTransfers %v, tx %v", err, tx.Txid) @@ -472,7 +667,7 @@ func (d *RocksDB) processContractTransfers(blockTx *ethBlockTx, tx *bchain.Tx, a return err } bc := &blockTx.contracts[i] - bc.transferType = t.Type + bc.transferStandard = t.Standard bc.from = from bc.to = to bc.contract = contract @@ -482,7 +677,7 @@ func (d *RocksDB) processContractTransfers(blockTx *ethBlockTx, tx *bchain.Tx, a return nil } -func (d *RocksDB) processAddressesEthereumType(block *bchain.Block, addresses addressesMap, addressContracts map[string]*AddrContracts) ([]ethBlockTx, error) { +func (d *RocksDB) processAddressesEthereumType(block *bchain.Block, addresses addressesMap, addressContracts map[string]*unpackedAddrContracts) ([]ethBlockTx, error) { blockTxs := make([]ethBlockTx, len(block.Txs)) for txi := range block.Txs { tx := &block.Txs[txi] @@ -498,7 +693,7 @@ func (d *RocksDB) processAddressesEthereumType(block *bchain.Block, addresses ad // process internal data eid, _ := tx.CoinSpecificData.(bchain.EthereumSpecificData) if eid.InternalData != nil { - if err = d.processInternalData(blockTx, tx, eid.InternalData, addresses, addressContracts); err != nil { + if err = d.processInternalData(blockTx, tx, eid.InternalData, addresses, addressContracts, false); err != nil { return nil, err } } @@ -510,6 +705,56 @@ func (d *RocksDB) processAddressesEthereumType(block *bchain.Block, addresses ad return blockTxs, nil } +// ReconnectInternalDataToBlockEthereumType adds missing internal data to the block and stores them in db +func (d *RocksDB) ReconnectInternalDataToBlockEthereumType(block *bchain.Block) error { + d.connectBlockMux.Lock() + defer d.connectBlockMux.Unlock() + + wb := grocksdb.NewWriteBatch() + defer wb.Destroy() + if d.chainParser.GetChainType() != bchain.ChainEthereumType { + return errors.New("Unsupported chain type") + } + + addresses := make(addressesMap) + addressContracts := make(map[string]*unpackedAddrContracts) + + // process internal data + blockTxs := make([]ethBlockTx, len(block.Txs)) + for txi := range block.Txs { + tx := &block.Txs[txi] + eid, _ := tx.CoinSpecificData.(bchain.EthereumSpecificData) + if eid.InternalData != nil { + btxID, err := d.chainParser.PackTxid(tx.Txid) + if err != nil { + return err + } + blockTx := &blockTxs[txi] + blockTx.btxID = btxID + tx.BlockHeight = block.Height + if err = d.processInternalData(blockTx, tx, eid.InternalData, addresses, addressContracts, true); err != nil { + return err + } + } + } + + if err := d.storeUnpackedAddressContracts(wb, addressContracts); err != nil { + return err + } + if err := d.storeInternalDataEthereumType(wb, blockTxs); err != nil { + return err + } + if err := d.storeAddresses(wb, block.Height, addresses); err != nil { + return err + } + // remove the block from the internal errors table + wb.DeleteCF(d.cfh[cfBlockInternalDataErrors], packUint(block.Height)) + if err := d.WriteBatch(wb); err != nil { + return err + } + return nil +} + var ethZeroAddress []byte = make([]byte, eth.EthereumTypeAddressDescriptorLen) func appendAddress(buf []byte, a bchain.AddressDescriptor) []byte { @@ -706,7 +951,7 @@ var cachedContractsMux sync.Mutex func packContractInfo(contractInfo *bchain.ContractInfo) []byte { buf := packString(contractInfo.Name) buf = append(buf, packString(contractInfo.Symbol)...) - buf = append(buf, packString(string(contractInfo.Type))...) + buf = append(buf, packString(string(contractInfo.Standard))...) varBuf := make([]byte, vlq.MaxLen64) l := packVaruint(uint(contractInfo.Decimals), varBuf) buf = append(buf, varBuf[:l]...) @@ -727,7 +972,8 @@ func unpackContractInfo(buf []byte) (*bchain.ContractInfo, error) { contractInfo.Symbol, l = unpackString(buf) buf = buf[l:] s, l = unpackString(buf) - contractInfo.Type = bchain.TokenTypeName(s) + contractInfo.Standard = bchain.TokenStandardName(s) + contractInfo.Type = bchain.TokenStandardName(s) buf = buf[l:] ui, l = unpackVaruint(buf) contractInfo.Decimals = int(ui) @@ -735,7 +981,7 @@ func unpackContractInfo(buf []byte) (*bchain.ContractInfo, error) { ui, l = unpackVaruint(buf) contractInfo.CreatedInBlock = uint32(ui) buf = buf[l:] - ui, l = unpackVaruint(buf) + ui, _ = unpackVaruint(buf) contractInfo.DestructedInBlock = uint32(ui) return &contractInfo, nil } @@ -748,9 +994,9 @@ func (d *RocksDB) GetContractInfoForAddress(address string) (*bchain.ContractInf return d.GetContractInfo(contract, "") } -// GetContractInfo gets contract from cache or DB and possibly updates the type from typeFromContext -// it is hard to guess the type of the contract using API, it is easier to set it the first time the contract is processed in a tx -func (d *RocksDB) GetContractInfo(contract bchain.AddressDescriptor, typeFromContext bchain.TokenTypeName) (*bchain.ContractInfo, error) { +// GetContractInfo gets contract from cache or DB and possibly updates the standard from standardFromContext +// it is hard to guess the standard of the contract using API, it is easier to set it the first time the contract is processed in a tx +func (d *RocksDB) GetContractInfo(contract bchain.AddressDescriptor, standardFromContext bchain.TokenStandardName) (*bchain.ContractInfo, error) { cacheKey := string(contract) cachedContractsMux.Lock() contractInfo, found := cachedContracts[cacheKey] @@ -765,15 +1011,19 @@ func (d *RocksDB) GetContractInfo(contract bchain.AddressDescriptor, typeFromCon if len(buf) == 0 { return nil, nil } - contractInfo, err = unpackContractInfo(buf) + contractInfo, _ = unpackContractInfo(buf) addresses, _, _ := d.chainParser.GetAddressesFromAddrDesc(contract) if len(addresses) > 0 { contractInfo.Contract = addresses[0] } - // if the type is specified and stored contractInfo has unknown type, set and store it - if typeFromContext != bchain.UnknownTokenType && contractInfo.Type == bchain.UnknownTokenType { - contractInfo.Type = typeFromContext + // if the standard is specified and stored contractInfo has unknown standard, set and store it + if standardFromContext != bchain.UnknownTokenStandard && contractInfo.Standard == bchain.UnknownTokenStandard { + contractInfo.Standard = standardFromContext + contractInfo.Type = standardFromContext err = d.db.PutCF(d.wo, d.cfh[cfContracts], contract, packContractInfo(contractInfo)) + if err != nil { + return nil, err + } } cachedContractsMux.Lock() cachedContracts[cacheKey] = contractInfo @@ -834,9 +1084,9 @@ func packBlockTx(buf []byte, blockTx *ethBlockTx) []byte { buf = appendAddress(buf, c.from) buf = appendAddress(buf, c.to) buf = appendAddress(buf, c.contract) - l = packVaruint(uint(c.transferType), varBuf) + l = packVaruint(uint(c.transferStandard), varBuf) buf = append(buf, varBuf[:l]...) - if c.transferType == bchain.MultiToken { + if c.transferStandard == bchain.MultiToken { l = packVaruint(uint(len(c.idValues)), varBuf) buf = append(buf, varBuf[:l]...) for i := range c.idValues { @@ -864,8 +1114,9 @@ func (d *RocksDB) storeAndCleanupBlockTxsEthereumType(wb *grocksdb.WriteBatch, b return d.cleanupBlockTxs(wb, block) } -func (d *RocksDB) storeBlockInternalDataErrorEthereumType(wb *grocksdb.WriteBatch, block *bchain.Block, message string) error { +func (d *RocksDB) StoreBlockInternalDataErrorEthereumType(wb *grocksdb.WriteBatch, block *bchain.Block, message string, retryCount uint8) error { key := packUint(block.Height) + // TODO: this supposes that Txid and block hash are the same size txid, err := d.chainParser.PackTxid(block.Hash) if err != nil { return err @@ -874,18 +1125,66 @@ func (d *RocksDB) storeBlockInternalDataErrorEthereumType(wb *grocksdb.WriteBatc buf := make([]byte, 0, len(txid)+len(m)+1) // the stored structure is txid+retry count (1 byte)+error message buf = append(buf, txid...) - buf = append(buf, 0) + buf = append(buf, retryCount) buf = append(buf, m...) wb.PutCF(d.cfh[cfBlockInternalDataErrors], key, buf) return nil } +type BlockInternalDataError struct { + Height uint32 + Hash string + Retries uint8 + ErrorMessage string +} + +func (d *RocksDB) unpackBlockInternalDataError(val []byte) (string, uint8, string, error) { + txidUnpackedLen := d.chainParser.PackedTxidLen() + var hash, message string + var retries uint8 + var err error + if len(val) > txidUnpackedLen+1 { + hash, err = d.chainParser.UnpackTxid(val[:txidUnpackedLen]) + if err != nil { + return "", 0, "", err + } + val = val[txidUnpackedLen:] + retries = val[0] + message = string(val[1:]) + } + return hash, retries, message, nil +} + +func (d *RocksDB) GetBlockInternalDataErrorsEthereumType() ([]BlockInternalDataError, error) { + retval := []BlockInternalDataError{} + if d.chainParser.GetChainType() == bchain.ChainEthereumType { + it := d.db.NewIteratorCF(d.ro, d.cfh[cfBlockInternalDataErrors]) + defer it.Close() + for it.SeekToFirst(); it.Valid(); it.Next() { + height := unpackUint(it.Key().Data()) + val := it.Value().Data() + hash, retires, message, err := d.unpackBlockInternalDataError(val) + if err != nil { + glog.Error("GetBlockInternalDataErrorsEthereumType height ", height, ", unpack error ", err) + continue + } + retval = append(retval, BlockInternalDataError{ + Height: height, + Hash: hash, + Retries: retires, + ErrorMessage: message, + }) + } + } + return retval, nil +} + func (d *RocksDB) storeBlockSpecificDataEthereumType(wb *grocksdb.WriteBatch, block *bchain.Block) error { blockSpecificData, _ := block.CoinSpecificData.(*bchain.EthereumBlockSpecificData) if blockSpecificData != nil { if blockSpecificData.InternalDataError != "" { glog.Info("storeBlockSpecificDataEthereumType ", block.Height, ": ", blockSpecificData.InternalDataError) - if err := d.storeBlockInternalDataErrorEthereumType(wb, block, blockSpecificData.InternalDataError); err != nil { + if err := d.StoreBlockInternalDataErrorEthereumType(wb, block, blockSpecificData.InternalDataError, 0); err != nil { return err } } @@ -949,9 +1248,9 @@ func unpackBlockTx(buf []byte, pos int) (*ethBlockTx, int, error) { return nil, 0, err } cc, l = unpackVaruint(buf[pos:]) - c.transferType = bchain.TokenType(cc) + c.transferStandard = bchain.TokenStandard(cc) pos += l - if c.transferType == bchain.MultiToken { + if c.transferStandard == bchain.MultiToken { cc, l = unpackVaruint(buf[pos:]) pos += l c.idValues = make([]bchain.MultiTokenValue, cc) @@ -998,7 +1297,7 @@ func (d *RocksDB) getBlockTxsEthereumType(height uint32) ([]ethBlockTx, error) { return bt, nil } -func (d *RocksDB) disconnectAddress(btxID []byte, internal bool, addrDesc bchain.AddressDescriptor, btxContract *ethBlockTxContract, addresses map[string]map[string]struct{}, contracts map[string]*AddrContracts) error { +func (d *RocksDB) disconnectAddress(btxID []byte, internal bool, addrDesc bchain.AddressDescriptor, btxContract *ethBlockTxContract, addresses map[string]map[string]struct{}, contracts map[string]*unpackedAddrContracts) error { var err error // do not process empty address if len(addrDesc) == 0 { @@ -1020,7 +1319,7 @@ func (d *RocksDB) disconnectAddress(btxID []byte, internal bool, addrDesc bchain } addrContracts, fc := contracts[s] if !fc { - addrContracts, err = d.GetAddrDescContracts(addrDesc) + addrContracts, err = d.getUnpackedAddrDescContracts(addrDesc) if err != nil { return err } @@ -1064,7 +1363,7 @@ func (d *RocksDB) disconnectAddress(btxID []byte, internal bool, addrDesc bchain index = transferTo } addToContract(addrContract, contractIndex, index, btxContract.contract, &bchain.TokenTransfer{ - Type: btxContract.transferType, + Standard: btxContract.transferStandard, Value: btxContract.value, MultiTokenValues: btxContract.idValues, }, false) @@ -1086,7 +1385,7 @@ func (d *RocksDB) disconnectAddress(btxID []byte, internal bool, addrDesc bchain return nil } -func (d *RocksDB) disconnectInternalData(btxID []byte, addresses map[string]map[string]struct{}, contracts map[string]*AddrContracts) error { +func (d *RocksDB) disconnectInternalData(btxID []byte, addresses map[string]map[string]struct{}, contracts map[string]*unpackedAddrContracts) error { internalData, err := d.getEthereumInternalData(btxID) if err != nil { return err @@ -1125,7 +1424,7 @@ func (d *RocksDB) disconnectInternalData(btxID []byte, addresses map[string]map[ return nil } -func (d *RocksDB) disconnectBlockTxsEthereumType(wb *grocksdb.WriteBatch, height uint32, blockTxs []ethBlockTx, contracts map[string]*AddrContracts) error { +func (d *RocksDB) disconnectBlockTxsEthereumType(wb *grocksdb.WriteBatch, height uint32, blockTxs []ethBlockTx, contracts map[string]*unpackedAddrContracts) error { glog.Info("Disconnecting block ", height, " containing ", len(blockTxs), " transactions") addresses := make(map[string]map[string]struct{}) for i := range blockTxs { @@ -1184,7 +1483,7 @@ func (d *RocksDB) DisconnectBlockRangeEthereumType(lower uint32, higher uint32) } wb := grocksdb.NewWriteBatch() defer wb.Destroy() - contracts := make(map[string]*AddrContracts) + contracts := make(map[string]*unpackedAddrContracts) for height := higher; height >= lower; height-- { if err := d.disconnectBlockTxsEthereumType(wb, height, blocks[height-lower], contracts); err != nil { return err @@ -1194,7 +1493,7 @@ func (d *RocksDB) DisconnectBlockRangeEthereumType(lower uint32, higher uint32) wb.DeleteCF(d.cfh[cfHeight], key) wb.DeleteCF(d.cfh[cfBlockInternalDataErrors], key) } - d.storeAddressContracts(wb, contracts) + d.storeUnpackedAddressContracts(wb, contracts) err := d.WriteBatch(wb) if err == nil { d.is.RemoveLastBlockTimes(int(higher-lower) + 1) @@ -1202,3 +1501,353 @@ func (d *RocksDB) DisconnectBlockRangeEthereumType(lower uint32, higher uint32) } return err } + +func (d *RocksDB) SortAddressContracts(stop chan os.Signal) error { + if d.chainParser.GetChainType() != bchain.ChainEthereumType { + glog.Info("SortAddressContracts: applicable only for ethereum type coins") + return nil + } + glog.Info("SortAddressContracts: starting") + // do not use cache + ro := grocksdb.NewDefaultReadOptions() + ro.SetFillCache(false) + it := d.db.NewIteratorCF(ro, d.cfh[cfAddressContracts]) + defer it.Close() + var rowCount, idsSortedCount, multiTokenValuesSortedCount int + for it.SeekToFirst(); it.Valid(); it.Next() { + select { + case <-stop: + return errors.New("SortAddressContracts: interrupted") + default: + } + rowCount++ + addrDesc := it.Key().Data() + buf := it.Value().Data() + if len(buf) > 0 { + ca, err := unpackAddrContracts(buf, addrDesc) + if err != nil { + glog.Error("failed to unpack AddrContracts for: ", hex.EncodeToString(addrDesc)) + } + update := false + for i := range ca.Contracts { + c := &ca.Contracts[i] + if sorted := c.Ids.sort(); sorted { + idsSortedCount++ + update = true + } + if sorted := c.MultiTokenValues.sort(); sorted { + multiTokenValuesSortedCount++ + update = true + } + } + if update { + if err := func() error { + wb := grocksdb.NewWriteBatch() + defer wb.Destroy() + buf := packAddrContracts(ca) + wb.PutCF(d.cfh[cfAddressContracts], addrDesc, buf) + return d.WriteBatch(wb) + }(); err != nil { + return errors.Errorf("failed to write cfAddressContracts for: %v: %v", addrDesc, err) + } + } + } + if rowCount%5000000 == 0 { + glog.Infof("SortAddressContracts: progress - scanned %d rows, sorted %d ids and %d multi token values", rowCount, idsSortedCount, multiTokenValuesSortedCount) + } + } + glog.Infof("SortAddressContracts: finished - scanned %d rows, sorted %d ids and %d multi token value", rowCount, idsSortedCount, multiTokenValuesSortedCount) + return nil +} + +type unpackedBigInt struct { + Slice []byte + Value *big.Int +} +type unpackedIds []unpackedBigInt + +type unpackedAddrContract struct { + Standard bchain.TokenStandard + Contract bchain.AddressDescriptor + Txs uint + Value unpackedBigInt // single value of ERC20 + Ids unpackedIds // multiple ERC721 tokens + MultiTokenValues unpackedMultiTokenValues // multiple ERC1155 tokens +} + +func (b *unpackedBigInt) get() *big.Int { + if b.Value == nil { + if len(b.Slice) == 0 { + b.Value = big.NewInt(0) + } else { + bi, _ := unpackBigint(b.Slice) + b.Value = &bi + } + } + return b.Value +} + +type unpackedAddrContracts struct { + Packed []byte + TotalTxs uint + NonContractTxs uint + InternalTxs uint + Contracts []unpackedAddrContract +} + +func (s *unpackedIds) search(id big.Int) int { + // attempt to find id using a binary search + return sort.Search(len(*s), func(i int) bool { + return (*s)[i].get().CmpAbs(&id) >= 0 + }) +} + +// insert id in ascending order +func (s *unpackedIds) insert(id big.Int) { + i := s.search(id) + if i == len(*s) { + *s = append(*s, unpackedBigInt{Value: &id}) + } else { + *s = append((*s)[:i+1], (*s)[i:]...) + (*s)[i] = unpackedBigInt{Value: &id} + } +} + +func (s *unpackedIds) remove(id big.Int) { + i := s.search(id) + // remove id if found + if i < len(*s) && (*s)[i].get().CmpAbs(&id) == 0 { + *s = append((*s)[:i], (*s)[i+1:]...) + } +} + +type unpackedMultiTokenValue struct { + Id unpackedBigInt + Value unpackedBigInt +} + +type unpackedMultiTokenValues []unpackedMultiTokenValue + +// search for multi token value using a binary seach on id +func (s *unpackedMultiTokenValues) search(m bchain.MultiTokenValue) int { + return sort.Search(len(*s), func(i int) bool { + return (*s)[i].Id.get().CmpAbs(&m.Id) >= 0 + }) +} + +func (s *unpackedMultiTokenValues) upsert(m bchain.MultiTokenValue, index int32, aggregate AggregateFn) { + i := s.search(m) + if i < len(*s) && (*s)[i].Id.get().CmpAbs(&m.Id) == 0 { + aggregate((*s)[i].Value.get(), &m.Value) + // if transfer from, remove if the value is zero + if index < 0 && len((*s)[i].Value.get().Bits()) == 0 { + *s = append((*s)[:i], (*s)[i+1:]...) + } + return + } + if index >= 0 { + elem := unpackedMultiTokenValue{ + Id: unpackedBigInt{Value: &m.Id}, + Value: unpackedBigInt{Value: new(big.Int).Set(&m.Value)}, + } + if i == len(*s) { + *s = append(*s, elem) + } else { + *s = append((*s)[:i+1], (*s)[i:]...) + (*s)[i] = elem + } + } +} + +// getUnpackedAddrDescContracts returns partially unpacked AddrContracts for given addrDesc +func (d *RocksDB) getUnpackedAddrDescContracts(addrDesc bchain.AddressDescriptor) (*unpackedAddrContracts, error) { + d.addrContractsCacheMux.Lock() + rv, found := d.addrContractsCache[string(addrDesc)] + d.addrContractsCacheMux.Unlock() + if found && rv != nil { + return rv, nil + } + val, err := d.db.GetCF(d.ro, d.cfh[cfAddressContracts], addrDesc) + if err != nil { + return nil, err + } + defer val.Free() + buf := val.Data() + if len(buf) == 0 { + return nil, nil + } + rv, err = partiallyUnpackAddrContracts(buf) + if err == nil && rv != nil && len(buf) > addrContractsCacheMinSize { + d.addrContractsCacheMux.Lock() + d.addrContractsCache[string(addrDesc)] = rv + d.addrContractsCacheMux.Unlock() + } + return rv, err +} + +// to speed up import of blocks, the unpacking of big ints is deferred to time when they are needed +func partiallyUnpackAddrContracts(buf []byte) (acs *unpackedAddrContracts, err error) { + // make copy of the slice to avoid subsequent allocation of smaller slices + buf = append([]byte{}, buf...) + index := 0 + tt, l := unpackVaruint(buf) + index += l + nct, l := unpackVaruint(buf[index:]) + index += l + ict, l := unpackVaruint(buf[index:]) + index += l + cl, l := unpackVaruint(buf[index:]) + index += l + c := make([]unpackedAddrContract, 0, cl) + for index < len(buf) { + contract := buf[index : index+eth.EthereumTypeAddressDescriptorLen] + index += eth.EthereumTypeAddressDescriptorLen + txs, l := unpackVaruint(buf[index:]) + index += l + standard := bchain.TokenStandard(txs & 3) + txs >>= 2 + ac := unpackedAddrContract{ + Standard: standard, + Contract: contract, + Txs: txs, + } + if standard == bchain.FungibleToken { + l := packedBigintLen(buf[index:]) + ac.Value = unpackedBigInt{Slice: buf[index : index+l]} + index += l + } else { + len, ll := unpackVaruint(buf[index:]) + index += ll + if standard == bchain.NonFungibleToken { + ac.Ids = make(unpackedIds, len) + for i := uint(0); i < len; i++ { + ll := packedBigintLen(buf[index:]) + ac.Ids[i] = unpackedBigInt{Slice: buf[index : index+ll]} + index += ll + } + } else { + ac.MultiTokenValues = make(unpackedMultiTokenValues, len) + for i := uint(0); i < len; i++ { + ll := packedBigintLen(buf[index:]) + ac.MultiTokenValues[i].Id = unpackedBigInt{Slice: buf[index : index+ll]} + index += ll + ll = packedBigintLen(buf[index:]) + ac.MultiTokenValues[i].Value = unpackedBigInt{Slice: buf[index : index+ll]} + index += ll + } + } + } + c = append(c, ac) + } + return &unpackedAddrContracts{ + Packed: buf, + TotalTxs: tt, + NonContractTxs: nct, + InternalTxs: ict, + Contracts: c, + }, nil +} + +// packUnpackedAddrContracts packs unpackedAddrContracts into a byte buffer +func packUnpackedAddrContracts(acs *unpackedAddrContracts) []byte { + buf := make([]byte, 0, len(acs.Packed)+eth.EthereumTypeAddressDescriptorLen+12) + varBuf := make([]byte, maxPackedBigintBytes) + l := packVaruint(acs.TotalTxs, varBuf) + buf = append(buf, varBuf[:l]...) + l = packVaruint(acs.NonContractTxs, varBuf) + buf = append(buf, varBuf[:l]...) + l = packVaruint(acs.InternalTxs, varBuf) + buf = append(buf, varBuf[:l]...) + l = packVaruint(uint(len(acs.Contracts)), varBuf) + buf = append(buf, varBuf[:l]...) + for _, ac := range acs.Contracts { + buf = append(buf, ac.Contract...) + l = packVaruint(uint(ac.Standard)+ac.Txs<<2, varBuf) + buf = append(buf, varBuf[:l]...) + if ac.Standard == bchain.FungibleToken { + if ac.Value.Value != nil { + l = packBigint(ac.Value.Value, varBuf) + buf = append(buf, varBuf[:l]...) + } else { + buf = append(buf, ac.Value.Slice...) + } + } else if ac.Standard == bchain.NonFungibleToken { + l = packVaruint(uint(len(ac.Ids)), varBuf) + buf = append(buf, varBuf[:l]...) + for i := range ac.Ids { + if ac.Ids[i].Value != nil { + l = packBigint(ac.Ids[i].Value, varBuf) + buf = append(buf, varBuf[:l]...) + } else { + buf = append(buf, ac.Ids[i].Slice...) + } + } + } else { // bchain.ERC1155 + l = packVaruint(uint(len(ac.MultiTokenValues)), varBuf) + buf = append(buf, varBuf[:l]...) + for i := range ac.MultiTokenValues { + if ac.MultiTokenValues[i].Id.Value != nil { + l = packBigint(ac.MultiTokenValues[i].Id.Value, varBuf) + buf = append(buf, varBuf[:l]...) + } else { + buf = append(buf, ac.MultiTokenValues[i].Id.Slice...) + } + if ac.MultiTokenValues[i].Value.Value != nil { + l = packBigint(ac.MultiTokenValues[i].Value.Value, varBuf) + buf = append(buf, varBuf[:l]...) + } else { + buf = append(buf, ac.MultiTokenValues[i].Value.Slice...) + } + } + } + } + return buf +} + +func (d *RocksDB) storeUnpackedAddressContracts(wb *grocksdb.WriteBatch, acm map[string]*unpackedAddrContracts) error { + for addrDesc, acs := range acm { + // address with 0 contracts is removed from db - happens on disconnect + if acs == nil || (acs.NonContractTxs == 0 && acs.InternalTxs == 0 && len(acs.Contracts) == 0) { + wb.DeleteCF(d.cfh[cfAddressContracts], bchain.AddressDescriptor(addrDesc)) + } else { + // do not store large address contracts found in cache + if _, found := d.addrContractsCache[addrDesc]; !found { + buf := packUnpackedAddrContracts(acs) + wb.PutCF(d.cfh[cfAddressContracts], bchain.AddressDescriptor(addrDesc), buf) + } + } + } + return nil +} + +func (d *RocksDB) writeContractsCache() { + wb := grocksdb.NewWriteBatch() + defer wb.Destroy() + d.addrContractsCacheMux.Lock() + for addrDesc, acs := range d.addrContractsCache { + buf := packUnpackedAddrContracts(acs) + wb.PutCF(d.cfh[cfAddressContracts], bchain.AddressDescriptor(addrDesc), buf) + } + d.addrContractsCacheMux.Unlock() + if err := d.WriteBatch(wb); err != nil { + glog.Error("writeContractsCache: failed to store addrContractsCache: ", err) + } +} + +func (d *RocksDB) storeAddrContractsCache() { + start := time.Now() + if len(d.addrContractsCache) > 0 { + d.writeContractsCache() + } + glog.Info("storeAddrContractsCache: store ", len(d.addrContractsCache), " entries in ", time.Since(start)) +} + +func (d *RocksDB) periodicStoreAddrContractsCache() { + period := time.Duration(5) * time.Minute + timer := time.NewTimer(period) + for { + <-timer.C + timer.Reset(period) + d.storeAddrContractsCache() + } +} diff --git a/db/rocksdb_ethereumtype_test.go b/db/rocksdb_ethereumtype_test.go index 8083f7eba7..23791bfccb 100644 --- a/db/rocksdb_ethereumtype_test.go +++ b/db/rocksdb_ethereumtype_test.go @@ -55,17 +55,17 @@ func verifyAfterEthereumTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect boo } if err := checkColumn(d, cfAddressContracts, []keyPair{ - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr3e, d.chainParser), "020102", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr3e, d.chainParser), "02010200", nil}, { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser), - "020100" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintFromStringToHex("10000000000000000000000"), nil, + "02010001" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintFromStringToHex("10000000000000000000000"), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr20, d.chainParser), - "010100" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintToHex(big.NewInt(0)), nil, + "01010001" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintToHex(big.NewInt(0)), nil, }, - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr9f, d.chainParser), "010002", nil}, - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser), "010101", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr9f, d.chainParser), "01000200", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser), "01010100", nil}, }); err != nil { { t.Fatal(err) @@ -177,51 +177,51 @@ func verifyAfterEthereumTypeBlock2(t *testing.T, d *RocksDB, wantBlockInternalDa if err := checkColumn(d, cfAddressContracts, []keyPair{ { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr20, d.chainParser), - "010100" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintToHex(big.NewInt(0)), nil, + "01010001" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintToHex(big.NewInt(0)), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr3e, d.chainParser), - "030202" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract6f, d.chainParser) + varuintToHex(1<<2+uint(bchain.MultiToken)) + varuintToHex(1) + bigintFromStringToHex("150") + bigintFromStringToHex("1"), nil, + "03020201" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract6f, d.chainParser) + varuintToHex(1<<2+uint(bchain.MultiToken)) + varuintToHex(1) + bigintFromStringToHex("150") + bigintFromStringToHex("1"), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr4b, d.chainParser), - "010101" + + "01010102" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser) + varuintToHex(2<<2+uint(bchain.FungibleToken)) + bigintFromStringToHex("8086") + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + varuintToHex(2<<2+uint(bchain.FungibleToken)) + bigintFromStringToHex("871180000950184"), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser), - "050300" + + "05030003" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + varuintToHex(2<<2+uint(bchain.FungibleToken)) + bigintFromStringToHex("10000000854307892726464") + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintFromStringToHex("0") + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintFromStringToHex("0"), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr5d, d.chainParser), - "010100" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract6f, d.chainParser) + varuintToHex(1<<2+uint(bchain.MultiToken)) + varuintToHex(2) + bigintFromStringToHex("1776") + bigintFromStringToHex("1") + bigintFromStringToHex("1898") + bigintFromStringToHex("10"), nil, + "01010001" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract6f, d.chainParser) + varuintToHex(1<<2+uint(bchain.MultiToken)) + varuintToHex(2) + bigintFromStringToHex("1776") + bigintFromStringToHex("1") + bigintFromStringToHex("1898") + bigintFromStringToHex("10"), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr7b, d.chainParser), - "020000" + + "02000003" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintFromStringToHex("0") + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser) + varuintToHex(1<<2+uint(bchain.FungibleToken)) + bigintFromStringToHex("7674999999999991915") + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContractCd, d.chainParser) + varuintToHex(1<<2+uint(bchain.NonFungibleToken)) + varuintToHex(1) + bigintFromStringToHex("1"), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr83, d.chainParser), - "010100" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContractCd, d.chainParser) + varuintToHex(1<<2+uint(bchain.NonFungibleToken)) + varuintToHex(0), nil, + "01010001" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContractCd, d.chainParser) + varuintToHex(1<<2+uint(bchain.NonFungibleToken)) + varuintToHex(0), nil, }, { dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrA3, d.chainParser), - "010000" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract6f, d.chainParser) + varuintToHex(1<<2+uint(bchain.MultiToken)) + varuintToHex(0), nil, + "01000001" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract6f, d.chainParser) + varuintToHex(1<<2+uint(bchain.MultiToken)) + varuintToHex(0), nil, }, - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr92, d.chainParser), "010100", nil}, - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr9f, d.chainParser), "030104", nil}, - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser), "010001", nil}, - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract47, d.chainParser), "010100", nil}, - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser), "020102", nil}, - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract6f, d.chainParser), "010100", nil}, - {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContractCd, d.chainParser), "010100", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr92, d.chainParser), "01010000", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr9f, d.chainParser), "03010400", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser), "01000100", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract47, d.chainParser), "01010000", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser), "02010200", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract6f, d.chainParser), "01010000", nil}, + {dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContractCd, d.chainParser), "01010000", nil}, }); err != nil { { t.Fatal(err) @@ -409,8 +409,8 @@ func TestRocksDB_Index_EthereumType(t *testing.T) { } verifyAfterEthereumTypeBlock1(t, d, false) - if len(d.is.BlockTimes) != 1 { - t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes)) + if len(d.is.BlockTimes) != 4321001 { + t.Fatal("Expecting is.BlockTimes 4321001, got ", len(d.is.BlockTimes)) } // connect 2nd block, simulate InternalDataError and AddressAlias @@ -421,8 +421,8 @@ func TestRocksDB_Index_EthereumType(t *testing.T) { verifyAfterEthereumTypeBlock2(t, d, true) block2.CoinSpecificData = nil - if len(d.is.BlockTimes) != 2 { - t.Fatal("Expecting is.BlockTimes 2, got ", len(d.is.BlockTimes)) + if len(d.is.BlockTimes) != 4321002 { + t.Fatal("Expecting is.BlockTimes 4321002, got ", len(d.is.BlockTimes)) } // get transactions for various addresses / low-high ranges @@ -551,8 +551,8 @@ func TestRocksDB_Index_EthereumType(t *testing.T) { } } - if len(d.is.BlockTimes) != 1 { - t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes)) + if len(d.is.BlockTimes) != 4321001 { + t.Fatal("Expecting is.BlockTimes 4321001, got ", len(d.is.BlockTimes)) } // connect block again and verify the state of db @@ -561,8 +561,8 @@ func TestRocksDB_Index_EthereumType(t *testing.T) { } verifyAfterEthereumTypeBlock2(t, d, false) - if len(d.is.BlockTimes) != 2 { - t.Fatal("Expecting is.BlockTimes 2, got ", len(d.is.BlockTimes)) + if len(d.is.BlockTimes) != 4321002 { + t.Fatal("Expecting is.BlockTimes 4321002, got ", len(d.is.BlockTimes)) } } @@ -729,6 +729,108 @@ func Test_packUnpackEthInternalData(t *testing.T) { } } +func generateAddrContracts(f, nf, nfc, m, mc int) []AddrContract { + parser := ethereumTestnetParser() + rv := make([]AddrContract, f+nf+m) + i := 0 + for ; i < f; i++ { + rv[i] = AddrContract{ + Standard: bchain.FungibleToken, + Contract: addressToAddrDesc(dbtestdata.EthAddrContract0d, parser), + Txs: uint(i + 100000), + Value: *big.NewInt(793201132 + int64(i*1000)), + } + } + for ; i < f+nf; i++ { + ids := make(Ids, nfc) + for j := 0; j < nfc; j++ { + ids[j] = *big.NewInt(int64(i*100000) + int64(j*100)) + } + rv[i] = AddrContract{ + Standard: bchain.NonFungibleToken, + Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), + Txs: uint(i + 100000), + Ids: ids, + } + } + for ; i < f+nf+m; i++ { + mtv := make(MultiTokenValues, mc) + for j := 0; j < nfc; j++ { + mtv[j] = bchain.MultiTokenValue{ + Id: *big.NewInt(int64(j)), + Value: *big.NewInt(4231521 + int64(i*1000000) + int64(j*1000)), + } + } + rv[i] = AddrContract{ + Standard: bchain.MultiToken, + Contract: addressToAddrDesc(dbtestdata.EthAddrContract4a, parser), + Txs: uint(i + 100000), + MultiTokenValues: mtv, + } + } + return rv +} + +var fungibleContracts = AddrContracts{ + TotalTxs: 3333330, + NonContractTxs: 2222220, + InternalTxs: 1111110, + Contracts: generateAddrContracts(100_000, 1, 1, 1, 1), +} +var packedFungibleContracts = packAddrContracts(&fungibleContracts) +var unpackedFungibleContracts, _ = partiallyUnpackAddrContracts(packedFungibleContracts) + +var mixedContracts = AddrContracts{ + TotalTxs: 3333330, + NonContractTxs: 2222220, + InternalTxs: 1111110, + Contracts: generateAddrContracts(100_000, 1, 1_000_000, 1, 1_000_000), +} +var packedMixedContracts = packAddrContracts(&mixedContracts) +var unpackedMixedContracts, _ = partiallyUnpackAddrContracts(packedMixedContracts) + +func Benchmark_packUnpackAddrContractsV6_Fungible(b *testing.B) { + for i := 0; i < b.N; i++ { + packed := packAddrContractsV6(&fungibleContracts) + unpackAddrContractsV6(packed, nil) + } +} + +func Benchmark_packUnpackAddrContracts_Fungible(b *testing.B) { + for i := 0; i < b.N; i++ { + packed := packAddrContracts(&fungibleContracts) + unpackAddrContracts(packed, nil) + } +} + +func Benchmark_packUnpackUnpackedkAddrContracts_Fungible(b *testing.B) { + for i := 0; i < b.N; i++ { + packed := packUnpackedAddrContracts(unpackedFungibleContracts) + partiallyUnpackAddrContracts(packed) + } +} + +func Benchmark_packUnpackAddrContractsV6_Mixed(b *testing.B) { + for i := 0; i < b.N; i++ { + packed := packAddrContractsV6(&mixedContracts) + unpackAddrContractsV6(packed, nil) + } +} + +func Benchmark_packUnpackAddrContracts_Mixed(b *testing.B) { + for i := 0; i < b.N; i++ { + packed := packAddrContracts(&mixedContracts) + unpackAddrContracts(packed, nil) + } +} + +func Benchmark_packUnpackUnpackedkAddrContracts_Mixed(b *testing.B) { + for i := 0; i < b.N; i++ { + packed := packUnpackedAddrContracts(unpackedMixedContracts) + partiallyUnpackAddrContracts(packed) + } +} + func Test_packUnpackAddrContracts(t *testing.T) { parser := ethereumTestnetParser() type args struct { @@ -756,16 +858,16 @@ func Test_packUnpackAddrContracts(t *testing.T) { InternalTxs: 8873, Contracts: []AddrContract{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract0d, parser), Txs: 8, Value: *big.NewInt(793201132), }, { - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), Txs: 41235, - Ids: []big.Int{ + Ids: Ids{ *big.NewInt(1), *big.NewInt(2), *big.NewInt(3), @@ -774,10 +876,10 @@ func Test_packUnpackAddrContracts(t *testing.T) { }, }, { - Type: bchain.MultiToken, + Standard: bchain.MultiToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract4a, parser), Txs: 64, - MultiTokenValues: []bchain.MultiTokenValue{ + MultiTokenValues: MultiTokenValues{ { Id: *big.NewInt(1), Value: *big.NewInt(1412341234), @@ -791,6 +893,24 @@ func Test_packUnpackAddrContracts(t *testing.T) { }, }, }, + { + name: "generated", + data: AddrContracts{ + TotalTxs: 3333330, + NonContractTxs: 2222220, + InternalTxs: 1111110, + Contracts: generateAddrContracts(10, 1, 1_000, 1, 1_000), + }, + }, + { + name: "huge", + data: AddrContracts{ + TotalTxs: 3333330, + NonContractTxs: 2222220, + InternalTxs: 1111110, + Contracts: generateAddrContracts(10000, 1, 1_000_000, 1, 1_000_000), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -830,8 +950,8 @@ func Test_addToContracts(t *testing.T) { index: 1, contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), transfer: &bchain.TokenTransfer{ - Type: bchain.FungibleToken, - Value: *big.NewInt(123456), + Standard: bchain.FungibleToken, + Value: *big.NewInt(123456), }, addTxCount: true, }, @@ -839,7 +959,7 @@ func Test_addToContracts(t *testing.T) { wantAddrContracts: &AddrContracts{ Contracts: []AddrContract{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), Txs: 1, Value: *big.NewInt(123456), @@ -853,8 +973,8 @@ func Test_addToContracts(t *testing.T) { index: ^1, contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), transfer: &bchain.TokenTransfer{ - Type: bchain.FungibleToken, - Value: *big.NewInt(23456), + Standard: bchain.FungibleToken, + Value: *big.NewInt(23456), }, addTxCount: true, }, @@ -862,7 +982,7 @@ func Test_addToContracts(t *testing.T) { wantAddrContracts: &AddrContracts{ Contracts: []AddrContract{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), Value: *big.NewInt(100000), Txs: 2, @@ -876,8 +996,8 @@ func Test_addToContracts(t *testing.T) { index: 1, contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), transfer: &bchain.TokenTransfer{ - Type: bchain.NonFungibleToken, - Value: *big.NewInt(1), + Standard: bchain.NonFungibleToken, + Value: *big.NewInt(1), }, addTxCount: true, }, @@ -885,16 +1005,16 @@ func Test_addToContracts(t *testing.T) { wantAddrContracts: &AddrContracts{ Contracts: []AddrContract{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), Value: *big.NewInt(100000), Txs: 2, }, { - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), Txs: 1, - Ids: []big.Int{*big.NewInt(1)}, + Ids: Ids{*big.NewInt(1)}, }, }, }, @@ -905,8 +1025,8 @@ func Test_addToContracts(t *testing.T) { index: 1, contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), transfer: &bchain.TokenTransfer{ - Type: bchain.NonFungibleToken, - Value: *big.NewInt(2), + Standard: bchain.NonFungibleToken, + Value: *big.NewInt(2), }, addTxCount: true, }, @@ -914,16 +1034,16 @@ func Test_addToContracts(t *testing.T) { wantAddrContracts: &AddrContracts{ Contracts: []AddrContract{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), Value: *big.NewInt(100000), Txs: 2, }, { - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), Txs: 2, - Ids: []big.Int{*big.NewInt(1), *big.NewInt(2)}, + Ids: Ids{*big.NewInt(1), *big.NewInt(2)}, }, }, }, @@ -934,8 +1054,8 @@ func Test_addToContracts(t *testing.T) { index: ^1, contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), transfer: &bchain.TokenTransfer{ - Type: bchain.NonFungibleToken, - Value: *big.NewInt(1), + Standard: bchain.NonFungibleToken, + Value: *big.NewInt(1), }, addTxCount: false, }, @@ -943,16 +1063,16 @@ func Test_addToContracts(t *testing.T) { wantAddrContracts: &AddrContracts{ Contracts: []AddrContract{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), Value: *big.NewInt(100000), Txs: 2, }, { - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), Txs: 2, - Ids: []big.Int{*big.NewInt(2)}, + Ids: Ids{*big.NewInt(2)}, }, }, }, @@ -963,7 +1083,7 @@ func Test_addToContracts(t *testing.T) { index: 1, contract: addressToAddrDesc(dbtestdata.EthAddrContractCd, parser), transfer: &bchain.TokenTransfer{ - Type: bchain.MultiToken, + Standard: bchain.MultiToken, MultiTokenValues: []bchain.MultiTokenValue{ { Id: *big.NewInt(11), @@ -977,22 +1097,22 @@ func Test_addToContracts(t *testing.T) { wantAddrContracts: &AddrContracts{ Contracts: []AddrContract{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), Value: *big.NewInt(100000), Txs: 2, }, { - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), Txs: 2, - Ids: []big.Int{*big.NewInt(2)}, + Ids: Ids{*big.NewInt(2)}, }, { - Type: bchain.MultiToken, + Standard: bchain.MultiToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContractCd, parser), Txs: 1, - MultiTokenValues: []bchain.MultiTokenValue{ + MultiTokenValues: MultiTokenValues{ { Id: *big.NewInt(11), Value: *big.NewInt(56789), @@ -1008,7 +1128,7 @@ func Test_addToContracts(t *testing.T) { index: 1, contract: addressToAddrDesc(dbtestdata.EthAddrContractCd, parser), transfer: &bchain.TokenTransfer{ - Type: bchain.MultiToken, + Standard: bchain.MultiToken, MultiTokenValues: []bchain.MultiTokenValue{ { Id: *big.NewInt(11), @@ -1026,22 +1146,22 @@ func Test_addToContracts(t *testing.T) { wantAddrContracts: &AddrContracts{ Contracts: []AddrContract{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), Value: *big.NewInt(100000), Txs: 2, }, { - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), Txs: 2, - Ids: []big.Int{*big.NewInt(2)}, + Ids: Ids{*big.NewInt(2)}, }, { - Type: bchain.MultiToken, + Standard: bchain.MultiToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContractCd, parser), Txs: 2, - MultiTokenValues: []bchain.MultiTokenValue{ + MultiTokenValues: MultiTokenValues{ { Id: *big.NewInt(11), Value: *big.NewInt(56900), @@ -1061,7 +1181,7 @@ func Test_addToContracts(t *testing.T) { index: ^1, contract: addressToAddrDesc(dbtestdata.EthAddrContractCd, parser), transfer: &bchain.TokenTransfer{ - Type: bchain.MultiToken, + Standard: bchain.MultiToken, MultiTokenValues: []bchain.MultiTokenValue{ { Id: *big.NewInt(11), @@ -1079,22 +1199,22 @@ func Test_addToContracts(t *testing.T) { wantAddrContracts: &AddrContracts{ Contracts: []AddrContract{ { - Type: bchain.FungibleToken, + Standard: bchain.FungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract47, parser), Value: *big.NewInt(100000), Txs: 2, }, { - Type: bchain.NonFungibleToken, + Standard: bchain.NonFungibleToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), Txs: 2, - Ids: []big.Int{*big.NewInt(2)}, + Ids: Ids{*big.NewInt(2)}, }, { - Type: bchain.MultiToken, + Standard: bchain.MultiToken, Contract: addressToAddrDesc(dbtestdata.EthAddrContractCd, parser), Txs: 3, - MultiTokenValues: []bchain.MultiTokenValue{ + MultiTokenValues: MultiTokenValues{ { Id: *big.NewInt(11), Value: *big.NewInt(56788), @@ -1107,17 +1227,24 @@ func Test_addToContracts(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - contractIndex, found := findContractInAddressContracts(tt.args.contract, addrContracts.Contracts) + // convert addrContracts to partially unpacked form which is used for block import + buf := packAddrContracts(addrContracts) + unpackedAddrContracts, _ := partiallyUnpackAddrContracts(buf) + // check logic + contractIndex, found := findContractInAddressContracts(tt.args.contract, unpackedAddrContracts.Contracts) if !found { - contractIndex = len(addrContracts.Contracts) - addrContracts.Contracts = append(addrContracts.Contracts, AddrContract{ + contractIndex = len(unpackedAddrContracts.Contracts) + unpackedAddrContracts.Contracts = append(unpackedAddrContracts.Contracts, unpackedAddrContract{ Contract: tt.args.contract, - Type: tt.args.transfer.Type, + Standard: tt.args.transfer.Standard, }) } - if got := addToContract(&addrContracts.Contracts[contractIndex], contractIndex, tt.args.index, tt.args.contract, tt.args.transfer, tt.args.addTxCount); got != tt.wantIndex { + if got := addToContract(&unpackedAddrContracts.Contracts[contractIndex], contractIndex, tt.args.index, tt.args.contract, tt.args.transfer, tt.args.addTxCount); got != tt.wantIndex { t.Errorf("addToContracts() = %v, want %v", got, tt.wantIndex) } + // convert from partially unpacked form to final form used by API + buf = packUnpackedAddrContracts(unpackedAddrContracts) + addrContracts, _ = unpackAddrContracts(buf, nil) if !reflect.DeepEqual(addrContracts, tt.wantAddrContracts) { t.Errorf("addToContracts() = %+v, want %+v", addrContracts, tt.wantAddrContracts) } @@ -1150,11 +1277,11 @@ func Test_packUnpackBlockTx(t *testing.T) { to: addressToAddrDesc(dbtestdata.EthAddr55, parser), contracts: []ethBlockTxContract{ { - from: addressToAddrDesc(dbtestdata.EthAddr20, parser), - to: addressToAddrDesc(dbtestdata.EthAddr5d, parser), - contract: addressToAddrDesc(dbtestdata.EthAddrContract4a, parser), - transferType: bchain.FungibleToken, - value: *big.NewInt(10000), + from: addressToAddrDesc(dbtestdata.EthAddr20, parser), + to: addressToAddrDesc(dbtestdata.EthAddr5d, parser), + contract: addressToAddrDesc(dbtestdata.EthAddrContract4a, parser), + transferStandard: bchain.FungibleToken, + value: *big.NewInt(10000), }, }, }, @@ -1168,24 +1295,24 @@ func Test_packUnpackBlockTx(t *testing.T) { to: addressToAddrDesc(dbtestdata.EthAddr55, parser), contracts: []ethBlockTxContract{ { - from: addressToAddrDesc(dbtestdata.EthAddr20, parser), - to: addressToAddrDesc(dbtestdata.EthAddr3e, parser), - contract: addressToAddrDesc(dbtestdata.EthAddrContract4a, parser), - transferType: bchain.FungibleToken, - value: *big.NewInt(987654321), + from: addressToAddrDesc(dbtestdata.EthAddr20, parser), + to: addressToAddrDesc(dbtestdata.EthAddr3e, parser), + contract: addressToAddrDesc(dbtestdata.EthAddrContract4a, parser), + transferStandard: bchain.FungibleToken, + value: *big.NewInt(987654321), }, { - from: addressToAddrDesc(dbtestdata.EthAddr4b, parser), - to: addressToAddrDesc(dbtestdata.EthAddr55, parser), - contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), - transferType: bchain.NonFungibleToken, - value: *big.NewInt(13), + from: addressToAddrDesc(dbtestdata.EthAddr4b, parser), + to: addressToAddrDesc(dbtestdata.EthAddr55, parser), + contract: addressToAddrDesc(dbtestdata.EthAddrContract6f, parser), + transferStandard: bchain.NonFungibleToken, + value: *big.NewInt(13), }, { - from: addressToAddrDesc(dbtestdata.EthAddr5d, parser), - to: addressToAddrDesc(dbtestdata.EthAddr7b, parser), - contract: addressToAddrDesc(dbtestdata.EthAddrContractCd, parser), - transferType: bchain.MultiToken, + from: addressToAddrDesc(dbtestdata.EthAddr5d, parser), + to: addressToAddrDesc(dbtestdata.EthAddr7b, parser), + contract: addressToAddrDesc(dbtestdata.EthAddrContractCd, parser), + transferStandard: bchain.MultiToken, idValues: []bchain.MultiTokenValue{ { Id: *big.NewInt(1234), @@ -1269,7 +1396,8 @@ func Test_packUnpackContractInfo(t *testing.T) { { name: "unknown", contractInfo: bchain.ContractInfo{ - Type: bchain.UnknownTokenType, + Type: bchain.UnknownTokenStandard, + Standard: bchain.UnknownTokenStandard, Name: "Test contract", Symbol: "TCT", Decimals: 18, @@ -1280,7 +1408,8 @@ func Test_packUnpackContractInfo(t *testing.T) { { name: "ERC20", contractInfo: bchain.ContractInfo{ - Type: bchain.ERC20TokenType, + Type: bchain.ERC20TokenStandard, + Standard: bchain.ERC20TokenStandard, Name: "GreenContract🟢", Symbol: "🟢", Decimals: 0, diff --git a/db/rocksdb_test.go b/db/rocksdb_test.go index 8a287403fd..aeb4c666af 100644 --- a/db/rocksdb_test.go +++ b/db/rocksdb_test.go @@ -5,7 +5,6 @@ package db import ( "encoding/binary" "encoding/hex" - "io/ioutil" "math/big" "os" "reflect" @@ -15,6 +14,7 @@ import ( vlq "github.com/bsm/go-vlq" "github.com/juju/errors" + "github.com/linxGnu/grocksdb" "github.com/martinboehm/btcutil/chaincfg" "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain/coins/btc" @@ -43,15 +43,15 @@ func bitcoinTestnetParser() *btc.BitcoinParser { } func setupRocksDB(t *testing.T, p bchain.BlockChainParser) *RocksDB { - tmp, err := ioutil.TempDir("", "testdb") + tmp, err := os.MkdirTemp("", "testdb") if err != nil { t.Fatal(err) } - d, err := NewRocksDB(tmp, 100000, -1, p, nil) + d, err := NewRocksDB(tmp, 100000, -1, p, nil, false) if err != nil { t.Fatal(err) } - is, err := d.LoadInternalState("coin-unittest") + is, err := d.LoadInternalState(&common.Config{CoinName: "coin-unittest"}) if err != nil { t.Fatal(err) } @@ -547,8 +547,8 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) { } verifyAfterBitcoinTypeBlock1(t, d, false) - if len(d.is.BlockTimes) != 1 { - t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes)) + if len(d.is.BlockTimes) != 225494 { + t.Fatal("Expecting is.BlockTimes 225494, got ", len(d.is.BlockTimes)) } // connect 2nd block - use some outputs from the 1st block as the inputs and 1 input uses tx from the same block @@ -558,8 +558,8 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) { } verifyAfterBitcoinTypeBlock2(t, d) - if len(d.is.BlockTimes) != 2 { - t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes)) + if len(d.is.BlockTimes) != 225495 { + t.Fatal("Expecting is.BlockTimes 225495, got ", len(d.is.BlockTimes)) } // get transactions for various addresses / low-high ranges @@ -667,8 +667,8 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) { } } - if len(d.is.BlockTimes) != 1 { - t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes)) + if len(d.is.BlockTimes) != 225494 { + t.Fatal("Expecting is.BlockTimes 225494, got ", len(d.is.BlockTimes)) } // connect block again and verify the state of db @@ -677,8 +677,8 @@ func TestRocksDB_Index_BitcoinType(t *testing.T) { } verifyAfterBitcoinTypeBlock2(t, d) - if len(d.is.BlockTimes) != 2 { - t.Fatal("Expecting is.BlockTimes 1, got ", len(d.is.BlockTimes)) + if len(d.is.BlockTimes) != 225495 { + t.Fatal("Expecting is.BlockTimes 225495, got ", len(d.is.BlockTimes)) } // test public methods for address balance and tx addresses @@ -802,6 +802,46 @@ func Test_BulkConnect_BitcoinType(t *testing.T) { } } +func Test_BlockFilter_GetAndStore(t *testing.T) { + d := setupRocksDB(t, &testBitcoinParser{ + BitcoinParser: bitcoinTestnetParser(), + }) + defer closeAndDestroyRocksDB(t, d) + + blockHash := "0000000000000003d0c9722718f8ee86c2cf394f9cd458edb1c854de2a7b1a91" + blockFilter := "042c6340895e413d8a811fa0" + blockFilterBytes, _ := hex.DecodeString(blockFilter) + + // Empty at the beginning + got, err := d.GetBlockFilter(blockHash) + if err != nil { + t.Fatal(err) + } + want := "" + if got != want { + t.Fatalf("GetBlockFilter(%s) = %s, want %s", blockHash, got, want) + } + + // Store the filter + wb := grocksdb.NewWriteBatch() + if err := d.storeBlockFilter(wb, blockHash, blockFilterBytes); err != nil { + t.Fatal(err) + } + if err := d.WriteBatch(wb); err != nil { + t.Fatal(err) + } + + // Get the filter + got, err = d.GetBlockFilter(blockHash) + if err != nil { + t.Fatal(err) + } + want = blockFilter + if got != want { + t.Fatalf("GetBlockFilter(%s) = %s, want %s", blockHash, got, want) + } +} + func Test_packBigint_unpackBigint(t *testing.T) { bigbig1, _ := big.NewInt(0).SetString("123456789123456789012345", 10) bigbig2, _ := big.NewInt(0).SetString("12345678912345678901234512389012345123456789123456789012345123456789123456789012345", 10) @@ -903,9 +943,10 @@ func addressToAddrDesc(addr string, parser bchain.BlockChainParser) []byte { func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { parser := bitcoinTestnetParser() tests := []struct { - name string - hex string - data *TxAddresses + name string + hex string + data *TxAddresses + rocksDB *RocksDB }{ { name: "1", @@ -930,6 +971,7 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { }, }, }, + rocksDB: &RocksDB{chainParser: parser, extendedIndex: false}, }, { name: "2", @@ -976,6 +1018,7 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { }, }, }, + rocksDB: &RocksDB{chainParser: parser, extendedIndex: false}, }, { name: "empty address", @@ -1000,6 +1043,7 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { }, }, }, + rocksDB: &RocksDB{chainParser: parser, extendedIndex: false}, }, { name: "empty", @@ -1008,18 +1052,111 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) { Inputs: []TxInput{}, Outputs: []TxOutput{}, }, + rocksDB: &RocksDB{chainParser: parser, extendedIndex: false}, + }, + { + name: "extendedIndex 1", + hex: "e0398241032ea9149eb21980dc9d413d8eac27314938b9da920ee53e8705021918f2c0c50c7ce2f5670fd52de738288299bd854a85ef1bb304f62f35ced1bd49a8a810002ea91409f70b896169c37981d2b54b371df0d81a136a2c870501dd7e28c0e96672c7fcc8da131427fcea7e841028614813496a56c11e8a6185c16861c495012ea914e371782582a4addb541362c55565d2cdf56f6498870501a1e35ec0ed308c72f9804dfeefdbb483ef8fd1e638180ad81d6b33f4b58d36d19162fa6d8106052fa9141d9ca71efa36d814424ea6ca1437e67287aebe348705012aadcac000b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa38400081ce8685592ea91424fbc77cdc62702ade74dcf989c15e5d3f9240bc870501664894c02fa914afbfb74ee994c7d45f6698738bc4226d065266f7870501a1e35ec0effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75ef17a1f4233276a914d2a37ce20ac9ec4f15dd05a7c6e8e9fbdb99850e88ac043b9943603376a9146b2044146a4438e6e5bfbc65f147afeb64d14fbb88ac05012a05f2007c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25a9956d8396f32a", + data: &TxAddresses{ + Height: 12345, + VSize: 321, + Inputs: []TxInput{ + { + AddrDesc: addressToAddrDesc("2N7iL7AvS4LViugwsdjTB13uN4T7XhV1bCP", parser), + ValueSat: *big.NewInt(9011000000), + Txid: "c50c7ce2f5670fd52de738288299bd854a85ef1bb304f62f35ced1bd49a8a810", + Vout: 0, + }, + { + AddrDesc: addressToAddrDesc("2Mt9v216YiNBAzobeNEzd4FQweHrGyuRHze", parser), + ValueSat: *big.NewInt(8011000000), + Txid: "e96672c7fcc8da131427fcea7e841028614813496a56c11e8a6185c16861c495", + Vout: 1, + }, + { + AddrDesc: addressToAddrDesc("2NDyqJpHvHnqNtL1F9xAeCWMAW8WLJmEMyD", parser), + ValueSat: *big.NewInt(7011000000), + Txid: "ed308c72f9804dfeefdbb483ef8fd1e638180ad81d6b33f4b58d36d19162fa6d", + Vout: 134, + }, + }, + Outputs: []TxOutput{ + { + AddrDesc: addressToAddrDesc("2MuwoFGwABMakU7DCpdGDAKzyj2nTyRagDP", parser), + ValueSat: *big.NewInt(5011000000), + Spent: true, + SpentTxid: dbtestdata.TxidB1T1, + SpentIndex: 0, + SpentHeight: 432112345, + }, + { + AddrDesc: addressToAddrDesc("2Mvcmw7qkGXNWzkfH1EjvxDcNRGL1Kf2tEM", parser), + ValueSat: *big.NewInt(6011000000), + }, + { + AddrDesc: addressToAddrDesc("2N9GVuX3XJGHS5MCdgn97gVezc6EgvzikTB", parser), + ValueSat: *big.NewInt(7011000000), + Spent: true, + SpentTxid: dbtestdata.TxidB1T2, + SpentIndex: 14231, + SpentHeight: 555555, + }, + { + AddrDesc: addressToAddrDesc("mzii3fuRSpExMLJEHdHveW8NmiX8MPgavk", parser), + ValueSat: *big.NewInt(999900000), + }, + { + AddrDesc: addressToAddrDesc("mqHPFTRk23JZm9W1ANuEFtwTYwxjESSgKs", parser), + ValueSat: *big.NewInt(5000000000), + Spent: true, + SpentTxid: dbtestdata.TxidB2T1, + SpentIndex: 674541, + SpentHeight: 6666666, + }, + }, + }, + rocksDB: &RocksDB{chainParser: parser, extendedIndex: true}, + }, + { + name: "extendedIndex empty address", + hex: "baef9a152d01010204d2020002162e010162fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db03e039", + data: &TxAddresses{ + Height: 123456789, + VSize: 45, + Inputs: []TxInput{ + { + AddrDesc: []byte(nil), + ValueSat: *big.NewInt(1234), + }, + }, + Outputs: []TxOutput{ + { + AddrDesc: []byte(nil), + ValueSat: *big.NewInt(5678), + }, + { + AddrDesc: []byte(nil), + ValueSat: *big.NewInt(98), + Spent: true, + SpentTxid: dbtestdata.TxidB2T4, + SpentIndex: 3, + SpentHeight: 12345, + }, + }, + }, + rocksDB: &RocksDB{chainParser: parser, extendedIndex: true}, }, } varBuf := make([]byte, maxPackedBigintBytes) buf := make([]byte, 1024) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := packTxAddresses(tt.data, buf, varBuf) + b := tt.rocksDB.packTxAddresses(tt.data, buf, varBuf) hex := hex.EncodeToString(b) if !reflect.DeepEqual(hex, tt.hex) { t.Errorf("packTxAddresses() = %v, want %v", hex, tt.hex) } - got1, err := unpackTxAddresses(b) + got1, err := tt.rocksDB.unpackTxAddresses(b) if err != nil { t.Errorf("unpackTxAddresses() error = %v", err) return @@ -1499,3 +1636,79 @@ func Test_packUnpackString(t *testing.T) { }) } } + +func TestRocksDB_packTxIndexes_unpackTxIndexes(t *testing.T) { + type args struct { + txi []txIndexes + } + tests := []struct { + name string + data []txIndexes + hex string + }{ + { + name: "1", + data: []txIndexes{ + { + btxID: hexToBytes(dbtestdata.TxidB1T1), + indexes: []int32{1}, + }, + }, + hex: "00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa384006", + }, + { + name: "2", + data: []txIndexes{ + { + btxID: hexToBytes(dbtestdata.TxidB1T1), + indexes: []int32{-2, 1, 3, 1234, -53241}, + }, + { + btxID: hexToBytes(dbtestdata.TxidB1T2), + indexes: []int32{-2, -1, 0, 1, 2, 3}, + }, + }, + hex: "effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac7507030004080e00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa384007040ca6488cff61", + }, + { + name: "3", + data: []txIndexes{ + { + btxID: hexToBytes(dbtestdata.TxidB2T1), + indexes: []int32{-2, 1, 3}, + }, + { + btxID: hexToBytes(dbtestdata.TxidB1T1), + indexes: []int32{-2, -1, 0, 1, 2, 3}, + }, + { + btxID: hexToBytes(dbtestdata.TxidB1T2), + indexes: []int32{-2}, + }, + }, + hex: "effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac750500b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa384007030004080e7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d2507040e", + }, + } + d := &RocksDB{ + chainParser: &testBitcoinParser{ + BitcoinParser: bitcoinTestnetParser(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := d.packTxIndexes(tt.data) + hex := hex.EncodeToString(b) + if !reflect.DeepEqual(hex, tt.hex) { + t.Errorf("packTxIndexes() = %v, want %v", hex, tt.hex) + } + got, err := d.unpackTxIndexes(b) + if err != nil { + t.Errorf("unpackTxIndexes() error = %v", err) + return + } + if !reflect.DeepEqual(got, tt.data) { + t.Errorf("unpackTxIndexes() = %+v, want %+v", got, tt.data) + } + }) + } +} diff --git a/db/sync.go b/db/sync.go index f04512ec10..e0ba75fc38 100644 --- a/db/sync.go +++ b/db/sync.go @@ -153,7 +153,8 @@ func (w *SyncWorker) resyncIndex(onNewBlock bchain.OnNewBlockFunc, initialSync b // if parallel operation is enabled and the number of blocks to be connected is large, // use parallel routine to load majority of blocks // use parallel sync only in case of initial sync because it puts the db to inconsistent state - if w.syncWorkers > 1 && initialSync { + // or in case of ChainEthereumType if the tip is farther + if w.syncWorkers > 1 && (initialSync || w.chain.GetChainParser().GetChainType() == bchain.ChainEthereumType) { remoteBestHeight, err := w.chain.GetBestBlockHeight() if err != nil { return err @@ -162,15 +163,30 @@ func (w *SyncWorker) resyncIndex(onNewBlock bchain.OnNewBlockFunc, initialSync b glog.Error("resync: error - remote best height ", remoteBestHeight, " less than sync start height ", w.startHeight) return errors.New("resync: remote best height error") } - if remoteBestHeight-w.startHeight > uint32(w.syncChunk) { - glog.Infof("resync: parallel sync of blocks %d-%d, using %d workers", w.startHeight, remoteBestHeight, w.syncWorkers) - err = w.ConnectBlocksParallel(w.startHeight, remoteBestHeight) - if err != nil { - return err + if initialSync { + if remoteBestHeight-w.startHeight > uint32(w.syncChunk) { + glog.Infof("resync: bulk sync of blocks %d-%d, using %d workers", w.startHeight, remoteBestHeight, w.syncWorkers) + err = w.BulkConnectBlocks(w.startHeight, remoteBestHeight) + if err != nil { + return err + } + // after parallel load finish the sync using standard way, + // new blocks may have been created in the meantime + return w.resyncIndex(onNewBlock, initialSync) + } + } + if w.chain.GetChainParser().GetChainType() == bchain.ChainEthereumType { + syncWorkers := uint32(4) + if remoteBestHeight-w.startHeight >= syncWorkers { + glog.Infof("resync: parallel sync of blocks %d-%d, using %d workers", w.startHeight, remoteBestHeight, syncWorkers) + err = w.ParallelConnectBlocks(onNewBlock, w.startHeight, remoteBestHeight, syncWorkers) + if err != nil { + return err + } + // after parallel load finish the sync using standard way, + // new blocks may have been created in the meantime + return w.resyncIndex(onNewBlock, initialSync) } - // after parallel load finish the sync using standard way, - // new blocks may have been created in the meantime - return w.resyncIndex(onNewBlock, initialSync) } } err = w.connectBlocks(onNewBlock, initialSync) @@ -184,7 +200,7 @@ func (w *SyncWorker) handleFork(localBestHeight uint32, localBestHash string, on // find forked blocks, disconnect them and then synchronize again var height uint32 hashes := []string{localBestHash} - for height = localBestHeight - 1; height >= 0; height-- { + for height = localBestHeight - 1; ; height-- { local, err := w.db.GetBlockHash(height) if err != nil { return err @@ -271,12 +287,140 @@ func (w *SyncWorker) connectBlocks(onNewBlock bchain.OnNewBlockFunc, initialSync return nil } -// ConnectBlocksParallel uses parallel goroutines to get data from blockchain daemon -func (w *SyncWorker) ConnectBlocksParallel(lower, higher uint32) error { - type hashHeight struct { - hash string - height uint32 +type hashHeight struct { + hash string + height uint32 +} + +// ParallelConnectBlocks uses parallel goroutines to get data from blockchain daemon but keeps Blockbook in +func (w *SyncWorker) ParallelConnectBlocks(onNewBlock bchain.OnNewBlockFunc, lower, higher uint32, syncWorkers uint32) error { + var err error + var wg sync.WaitGroup + bch := make([]chan *bchain.Block, syncWorkers) + for i := 0; i < int(syncWorkers); i++ { + bch[i] = make(chan *bchain.Block) } + hch := make(chan hashHeight, syncWorkers) + hchClosed := atomic.Value{} + hchClosed.Store(false) + writeBlockDone := make(chan struct{}) + terminating := make(chan struct{}) + writeBlockWorker := func() { + defer close(writeBlockDone) + lastBlock := lower - 1 + WriteBlockLoop: + for { + select { + case b := <-bch[(lastBlock+1)%syncWorkers]: + if b == nil { + // channel is closed and empty - work is done + break WriteBlockLoop + } + if b.Height != lastBlock+1 { + glog.Fatal("writeBlockWorker skipped block, expected block ", lastBlock+1, ", new block ", b.Height) + } + err := w.db.ConnectBlock(b) + if err != nil { + glog.Fatal("writeBlockWorker ", b.Height, " ", b.Hash, " error ", err) + } + + if onNewBlock != nil { + onNewBlock(b.Hash, b.Height) + } + w.metrics.BlockbookBestHeight.Set(float64(b.Height)) + + if b.Height > 0 && b.Height%1000 == 0 { + glog.Info("connected block ", b.Height, " ", b.Hash) + } + + lastBlock = b.Height + case <-terminating: + break WriteBlockLoop + } + } + if err != nil { + glog.Error("sync: ParallelConnectBlocks.Close error ", err) + } + glog.Info("WriteBlock exiting...") + } + for i := 0; i < int(syncWorkers); i++ { + wg.Add(1) + go w.getBlockWorker(i, syncWorkers, &wg, hch, bch, &hchClosed, terminating) + } + go writeBlockWorker() + var hash string +ConnectLoop: + for h := lower; h <= higher; { + select { + case <-w.chanOsSignal: + glog.Info("connectBlocksParallel interrupted at height ", h) + err = ErrOperationInterrupted + // signal all workers to terminate their loops (error loops are interrupted below) + close(terminating) + break ConnectLoop + default: + hash, err = w.chain.GetBlockHash(h) + if err != nil { + glog.Error("GetBlockHash error ", err) + w.metrics.IndexResyncErrors.With(common.Labels{"error": "failure"}).Inc() + time.Sleep(time.Millisecond * 500) + continue + } + hch <- hashHeight{hash, h} + h++ + } + } + close(hch) + // signal stop to workers that are in a error loop + hchClosed.Store(true) + // wait for workers and close bch that will stop writer loop + wg.Wait() + for i := 0; i < int(syncWorkers); i++ { + close(bch[i]) + } + <-writeBlockDone + return err +} + +func (w *SyncWorker) getBlockWorker(i int, syncWorkers uint32, wg *sync.WaitGroup, hch chan hashHeight, bch []chan *bchain.Block, hchClosed *atomic.Value, terminating chan struct{}) { + defer wg.Done() + var err error + var block *bchain.Block +GetBlockLoop: + for hh := range hch { + for { + block, err = w.chain.GetBlock(hh.hash, hh.height) + if err != nil { + // signal came while looping in the error loop + if hchClosed.Load() == true { + glog.Error("getBlockWorker ", i, " connect block error ", err, ". Exiting...") + return + } + if err == bchain.ErrBlockNotFound { + glog.Error("getBlockWorker ", i, " connect block ", hh.height, " ", hh.hash, " error ", err, ". Retrying...") + } else { + glog.Error("getBlockWorker ", i, " connect block error ", err, ". Retrying...") + } + w.metrics.IndexResyncErrors.With(common.Labels{"error": "failure"}).Inc() + time.Sleep(time.Millisecond * 500) + } else { + break + } + } + if w.dryRun { + continue + } + select { + case bch[hh.height%syncWorkers] <- block: + case <-terminating: + break GetBlockLoop + } + } + glog.Info("getBlockWorker ", i, " exiting...") +} + +// BulkConnectBlocks uses parallel goroutines to get data from blockchain daemon +func (w *SyncWorker) BulkConnectBlocks(lower, higher uint32) error { var err error var wg sync.WaitGroup bch := make([]chan *bchain.Block, w.syncWorkers) @@ -322,41 +466,9 @@ func (w *SyncWorker) ConnectBlocksParallel(lower, higher uint32) error { } glog.Info("WriteBlock exiting...") } - getBlockWorker := func(i int) { - defer wg.Done() - var err error - var block *bchain.Block - GetBlockLoop: - for hh := range hch { - for { - block, err = w.chain.GetBlock(hh.hash, hh.height) - if err != nil { - // signal came while looping in the error loop - if hchClosed.Load() == true { - glog.Error("getBlockWorker ", i, " connect block error ", err, ". Exiting...") - return - } - glog.Error("getBlockWorker ", i, " connect block error ", err, ". Retrying...") - w.metrics.IndexResyncErrors.With(common.Labels{"error": "failure"}).Inc() - time.Sleep(time.Millisecond * 500) - } else { - break - } - } - if w.dryRun { - continue - } - select { - case bch[hh.height%uint32(w.syncWorkers)] <- block: - case <-terminating: - break GetBlockLoop - } - } - glog.Info("getBlockWorker ", i, " exiting...") - } for i := 0; i < w.syncWorkers; i++ { wg.Add(1) - go getBlockWorker(i) + go w.getBlockWorker(i, uint32(w.syncWorkers), &wg, hch, bch, &hchClosed, terminating) } go writeBlockWorker() var hash string @@ -418,7 +530,6 @@ func (w *SyncWorker) getBlockChain(out chan blockResult, done chan struct{}) { hash := w.startHash height := w.startHeight prevHash := "" - // loop until error ErrBlockNotFound for { select { diff --git a/db/txcache.go b/db/txcache.go index 27012849fd..bcc89bc254 100644 --- a/db/txcache.go +++ b/db/txcache.go @@ -46,7 +46,7 @@ func (c *TxCache) GetTransaction(txid string) (*bchain.Tx, int, error) { } if tx != nil { // number of confirmations is not stored in cache, they change all the time - _, bestheight, _ := c.is.GetSyncState() + _, bestheight, _, _ := c.is.GetSyncState() tx.Confirmations = bestheight - h + 1 c.metrics.TxCacheEfficiency.With(common.Labels{"status": "hit"}).Inc() return tx, int(h), nil diff --git a/docs/api.md b/docs/api.md index 4b0cf795ad..d0fb05fab3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,68 +8,74 @@ API V2 is the current version of API. It can be used with all coin types that Bl Common principles used in API V2: -- all crypto amounts are transferred as strings, in the lowest denomination (satoshis, wei, ...), without decimal point -- empty fields are omitted. Empty field is a string of value _null_ or _""_, a number of value _0_, an object of value _null_ or an array without elements. The reason for this is that the interface serves many different coins which use only subset of the fields. Sometimes this principle can lead to slightly confusing results, for example when transaction version is 0, the field _version_ is omitted. +- all crypto amounts are transferred as strings, in the lowest denomination (satoshis, wei, ...), without decimal point +- empty fields are omitted. Empty field is a string of value _null_ or _""_, a number of value _0_, an object of value _null_ or an array without elements. The reason for this is that the interface serves many different coins which use only subset of the fields. Sometimes this principle can lead to slightly confusing results, for example when transaction version is 0, the field _version_ is omitted. + +See all the referred types (`typescript` interfaces) in the [blockbook-api.ts](../blockbook-api.ts) file. ### REST API The following methods are supported: -- [Status](#status) -- [Get block hash](#get-block-hash) -- [Get transaction](#get-transaction) -- [Get transaction specific](#get-transaction-specific) -- [Get address](#get-address) -- [Get xpub](#get-xpub) -- [Get utxo](#get-utxo) -- [Get block](#get-block) -- [Send transaction](#send-transaction) -- [Tickers list](#tickers-list) -- [Tickers](#tickers) -- [Balance history](#balance-history) +- [Status](#status) +- [Get block hash](#get-block-hash) +- [Get transaction](#get-transaction) +- [Get transaction specific](#get-transaction-specific) +- [Get address](#get-address) +- [Get xpub](#get-xpub) +- [Get utxo](#get-utxo) +- [Get block](#get-block) +- [Send transaction](#send-transaction) +- [Tickers list](#tickers-list) +- [Tickers](#tickers) +- [Balance history](#balance-history) #### Status page Status page returns current status of Blockbook and connected backend. ``` -GET /api +GET /api/status ``` -Response: +Response (`SystemInfo` type): + + ```javascript { "blockbook": { "coin": "Bitcoin", - "host": "blockbook", - "version": "0.4.0", - "gitCommit": "3d9ad91", - "buildTime": "2019-05-17T14:34:00+00:00", + "network": "BTC", + "host": "backend5", + "version": "0.5.0", + "gitCommit": "a0960c8e", + "buildTime": "2024-08-08T12:32:50+00:00", "syncMode": true, "initialSync": false, "inSync": true, - "bestHeight": 577261, - "lastBlockTime": "2019-05-22T18:03:33.547762973+02:00", + "bestHeight": 860730, + "lastBlockTime": "2024-09-10T08:19:04.471017534Z", "inSyncMempool": true, - "lastMempoolTime": "2019-05-22T18:10:10.27929383+02:00", - "mempoolSize": 17348, + "lastMempoolTime": "2024-09-10T08:42:39.38871351Z", + "mempoolSize": 232021, "decimals": 8, - "dbSize": 191887866502, - "about": "Blockbook - blockchain indexer for Trezor wallet https://trezor.io/. Do not use for any other purpose." + "dbSize": 761283489075, + "hasFiatRates": true, + "currentFiatRatesTime": "2024-09-10T08:42:00.898792419Z", + "historicalFiatRatesTime": "2024-09-10T00:00:00Z", + "about": "Blockbook - blockchain indexer for Trezor Suite https://trezor.io/trezor-suite. Do not use for any other purpose." }, "backend": { "chain": "main", - "blocks": 577261, - "headers": 577261, - "bestBlockHash": "0000000000000000000ca8c902aa58b3118a7f35d093e25a07f17bcacd91cabf", - "difficulty": "6704632680587.417", - "sizeOnDisk": 250504188580, - "version": "180000", - "subversion": "/Satoshi:0.18.0/", - "protocolVersion": "70015", - "timeOffset": 0, - "warnings": "" + "blocks": 860730, + "headers": 860730, + "bestBlockHash": "00000000000000000000effeb0c4460480e6a347deab95332c63007a68646ee5", + "difficulty": "89471664776970.77", + "sizeOnDisk": 681584532221, + "version": "270100", + "subversion": "/Satoshi:27.1.0/", + "protocolVersion": "70016" } } ``` @@ -82,9 +88,11 @@ GET /api/v2/block-index/ Response: + + ```javascript { - "blockHash": "ed8f3af8c10ca70a136901c6dd3adf037f0aea8a93fbe9e80939214034300f1e" + "blockHash": "0000000000000000000b7b8574bc6fd285825ec2dbcbeca149121fc05b0c828c" } ``` @@ -98,117 +106,183 @@ Get transaction returns "normalized" data about transaction, which has the same GET /api/v2/tx/ ``` -Response for Bitcoin-type coins, confirmed transaction: +Response for Bitcoin-type coins, confirmed transaction (`Tx` type): + + ```javascript { - "txid": "9e2bc8fbd40af17a6564831f84aef0cab2046d4bad19e91c09d21bff2c851851", - "version": 1, + "txid": "8c1e3dec662d1f2a5e322ccef5eca263f98eb16723c6f990be0c88c1db113fb1", + "version": 2, + "lockTime": 860729, "vin": [ { - "txid": "f124e6999bf67e710b9e8a8ac4dbb08a64aa9c264120cf98793455e36a531615", - "vout": 2, - "sequence": 4294967295, + "txid": "0eb7b574373de2c88d0dc1444f49947c681d0437d21361f9ebb4dd09c62f2a66", + "vout": 1, + "sequence": 4294967293, "n": 0, "addresses": [ - "DDhUv8JZGmSxKYV95NLnbRTUKni9cDZD3S" + "bc1qmgwnfjlda4ns3g6g3yz74w6scnn9yu2ts82yyc" ], "isAddress": true, - "value": "55795108999999", - "hex": "473...2c7ec77bb982" + "value": "10106300" } ], "vout": [ { - "value": "55585679000000", + "value": "175000", "n": 0, - "hex": "76a914feaca9d9fa7120c7c587c00c639bb18d40faadd388ac", + "hex": "76a914ecc999d554eaa3efa5e871c28f58b549c36ec51788ac", "addresses": [ - "DUMh1rPrXTrCN2Z9EHsLPg7b78rACHB2h7" + "1Nb1ykSD7J5k4RFjJQGsrD9gxBE6jzfNa9" ], "isAddress": true }, { - "value": "209329999999", + "value": "9888100", "n": 1, - "hex": "76a914ea8984be785868391d92f49c14933f47c152ea0a88ac", + "hex": "001496f152a0919487624bf4f13f46f0d20fa10d9acc", "addresses": [ - "DSXDQ6rnwLX47WFRnemctoXPHA9pLMxqXn" + "bc1qjmc49gy3jjrkyjl57yl5duxjp7ssmxkvh5t2q5" ], "isAddress": true } ], - "blockHash": "78d1f3de899a10dd2e580704226ebf9508e95e1706f177fc9c31c47f245d2502", - "blockHeight": 2647927, + "blockHash": "00000000000000000000effeb0c4460480e6a347deab95332c63007a68646ee5", + "blockHeight": 860730, "confirmations": 1, - "blockTime": 1553088212, - "size": 234, - "vsize": 153, - "value": "55795008999999", - "valueIn": "55795108999999", - "fees": "100000000", - "hex": "0100000...0011000" + "blockTime": 1725956288, + "size": 225, + "vsize": 144, + "value": "10063100", + "valueIn": "10106300", + "fees": "43200", + "hex": "02000000000101662a2fc609ddb4ebf96113d237041d687c94494f44c10d8dc8e23d3774b5b70e0100000000fdffffff0298ab0200000000001976a914ecc999d554eaa3efa5e871c28f58b549c36ec51788ac64e196000000000016001496f152a0919487624bf4f13f46f0d20fa10d9acc0247304402202bb0591180cdbbe0f639af6eb21abdb993fc5a667b09e6392d5c11b025a9187102201ef2e84fc91a5d2c6fbbc9f943482d230256a3640f8ecb83c1f3f17242cf011001210314f03889e1667feb696ee280625943195189cfabe46d54204d987f631fe6892739220d00" } ``` -Response for Bitcoin-type coins, unconfirmed transaction (_blockHeight_: -1, _confirmations_: 0, mining estimates _confirmationETABlocks_ and _confirmationETASeconds_): +Response for Bitcoin-type coins, unconfirmed transaction: + +Special fields: + +- _blockHeight_: -1 +- _confirmations_: 0 +- _confirmationETABlocks_: number +- _confirmationETASeconds_: number + + ```javascript { - "txid": "cd8ec77174e426070d0a50779232bba7312b712e2c6843d82d963d7076c61366", + "txid": "73b1ad97194e426031e5c692869de2d83dc2ff6033fc6f0ab5514345f92eaf0d", "version": 2, "vin": [ { - "txid": "47687cc4abb58d815168686465a38113a0608b2568a6d6480129d197e653f6dc", - "sequence": 4294967295, + "txid": "bccbebb64b1613ada74eefa96753088a80fefa53a10e42c66eef1899371bc096", "n": 0, - "addresses": ["bc1qka0gpenex558g8gpxmpx247mwhw695k6a7yhs4"], + "addresses": [ + "bc1q9lh77es6m8ztr7muwcec00ewn8fxakpl9jwv8y" + ], "isAddress": true, - "value": "1983687" + "value": "371042" } ], "vout": [ { - "value": "3106", + "value": "293135", "n": 0, - "hex": "0020d7da4868055fde790a8581637ab81c216e17a3f8a099283da6c4a27419ffa539", + "hex": "0014aafd7386f99f4b508ec05ee8f7edc2e07126620a", "addresses": [ - "bc1q6ldys6q9tl08jz59s93h4wquy9hp0glc5zvjs0dxcj38gx0l55uspu8x86" + "bc1q4t7h8phena94prkqtm500mwzupcjvcs2akcdy9" ], "isAddress": true }, { - "value": "1979101", + "value": "74022", "n": 1, - "hex": "0014381be30ca46ddf378ef69ebc4a601bd6ff30b754", - "addresses": ["bc1q8qd7xr9ydh0n0rhkn67y5cqm6mlnpd65dcyeeg"], + "hex": "0014a3de0fbba89c17d43093164ea955bad65bc260bf", + "addresses": [ + "bc1q500qlwagnstagvynze82j4d66eduyc9lf64ksh" + ], "isAddress": true } ], "blockHeight": -1, "confirmations": 0, - "confirmationETABlocks": 3, - "confirmationETASeconds": 2055, - "blockTime": 1675270935, - "size": 234, - "vsize": 153, - "value": "1982207", - "valueIn": "1983687", - "fees": "1480", - "hex": "020000000001...b18f00000000" + "confirmationETABlocks": 1, + "confirmationETASeconds": 619, + "blockTime": 1725959035, + "size": 222, + "vsize": 141, + "value": "367157", + "valueIn": "371042", + "fees": "3885", + "hex": "0200000000010196c01b379918ef6ec6420ea153fafe808a085367a9ef4ea7ad13164bb6ebcbbc000000000000000000020f79040000000000160014aafd7386f99f4b508ec05ee8f7edc2e07126620a2621010000000000160014a3de0fbba89c17d43093164ea955bad65bc260bf0247304402204a5bdf8a8d19b0a19044b0c0de3ced92b92e8d0c629ffca83178c85a608f719e02203841d40dd92db48715f9f41a732e139ac3cc7696a23adc87136bd8037a594e9f012102824a5e7b878f8d63887bdcb1b0982cdb0b375068b3798c4c96799476a19a389e00000000", + "rbf": true, + "coinSpecificData": { + "txid": "73b1ad97194e426031e5c692869de2d83dc2ff6033fc6f0ab5514345f92eaf0d", + "hash": "91deb6a9d0f5a37e2e83d1e602ba14cd9811fd3605f582154c9bd1337f7f4c8a", + "version": 2, + "size": 222, + "vsize": 141, + "weight": 561, + "locktime": 0, + "vin": [ + { + "txid": "bccbebb64b1613ada74eefa96753088a80fefa53a10e42c66eef1899371bc096", + "vout": 0, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "304402204a5bdf8a8d19b0a19044b0c0de3ced92b92e8d0c629ffca83178c85a608f719e02203841d40dd92db48715f9f41a732e139ac3cc7696a23adc87136bd8037a594e9f01", + "02824a5e7b878f8d63887bdcb1b0982cdb0b375068b3798c4c96799476a19a389e" + ], + "sequence": 0 + } + ], + "vout": [ + { + "value": 0.00293135, + "n": 0, + "scriptPubKey": { + "asm": "0 aafd7386f99f4b508ec05ee8f7edc2e07126620a", + "desc": "addr(bc1q4t7h8phena94prkqtm500mwzupcjvcs2akcdy9)#qmxeweuu", + "hex": "0014aafd7386f99f4b508ec05ee8f7edc2e07126620a", + "address": "bc1q4t7h8phena94prkqtm500mwzupcjvcs2akcdy9", + "type": "witness_v0_keyhash" + } + }, + { + "value": 0.00074022, + "n": 1, + "scriptPubKey": { + "asm": "0 a3de0fbba89c17d43093164ea955bad65bc260bf", + "desc": "addr(bc1q500qlwagnstagvynze82j4d66eduyc9lf64ksh)#mynfp6xy", + "hex": "0014a3de0fbba89c17d43093164ea955bad65bc260bf", + "address": "bc1q500qlwagnstagvynze82j4d66eduyc9lf64ksh", + "type": "witness_v0_keyhash" + } + } + ], + "hex": "0200000000010196c01b379918ef6ec6420ea153fafe808a085367a9ef4ea7ad13164bb6ebcbbc000000000000000000020f79040000000000160014aafd7386f99f4b508ec05ee8f7edc2e07126620a2621010000000000160014a3de0fbba89c17d43093164ea955bad65bc260bf0247304402204a5bdf8a8d19b0a19044b0c0de3ced92b92e8d0c629ffca83178c85a608f719e02203841d40dd92db48715f9f41a732e139ac3cc7696a23adc87136bd8037a594e9f012102824a5e7b878f8d63887bdcb1b0982cdb0b375068b3798c4c96799476a19a389e00000000" + } } ``` Response for Ethereum-type coins. Data of the transaction consist of: -- always only one _vin_, only one _vout_ -- an array of _tokenTransfers_ (ERC20, ERC721 or ERC1155) -- _ethereumSpecific_ data - - _type_ (returned only for contract creation - value `1` and destruction value `2`) - - _status_ (`1` OK, `0` Failure, `-1` pending), potential _error_ message, _gasLimit_, _gasUsed_, _gasPrice_, _nonce_, input _data_ - - parsed input data in the field _parsedData_, if a match with the 4byte directory was found - - internal transfers (type `0` transfer, type `1` contract creation, type `2` contract destruction) -- _addressAliases_ - maps addresses in the transaction to names from contract or ENS. Only addresses with known names are returned. +- always only one _vin_, only one _vout_ +- an array of _tokenTransfers_ (ERC20, ERC721 or ERC1155) +- _ethereumSpecific_ data + - _type_ (returned only for contract creation - value `1` and destruction value `2`) + - _status_ (`1` OK, `0` Failure, `-1` pending), potential _error_ message, _gasLimit_, _gasUsed_, _gasPrice_, _nonce_, input _data_ + - parsed input data in the field _parsedData_, if a match with the 4byte directory was found + - internal transfers (type `0` transfer, type `1` contract creation, type `2` contract destruction) +- _addressAliases_ - maps addresses in the transaction to names from contract or ENS. Only addresses with known names are returned. + + ```javascript { @@ -272,6 +346,9 @@ Response for Ethereum-type coins. Data of the transaction consist of: "gasLimit": 550941, "gasUsed": 434686, "gasPrice": "44035608242", + "maxPriorityFeePerGas": "44035608243", + "maxFeePerGas": "44035608244", + "baseFeePerGas": "2035608244", "data": "0xac9650d800000000000000000000", "parsedData": { "methodId": "0xfa2b068f", @@ -311,8 +388,8 @@ Response for Ethereum-type coins. Data of the transaction consist of: 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. +- 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. #### Get transaction specific @@ -324,10 +401,14 @@ GET /api/v2/tx-specific/ Example response: + + ```javascript { "hex": "040000808...8e6e73cb009", "txid": "7a0a0ff6f67bac2a856c7296382b69151949878de6fb0d01a8efa197182b2913", + "authdigest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "size": 1809, "overwintered": true, "version": 4, "versiongroupid": "892f2085", @@ -337,6 +418,7 @@ Example response: "vout": [], "vjoinsplit": [], "valueBalance": 0, + "valueBalanceZat": 0, "vShieldedSpend": [ { "cv": "50258bfa65caa9f42f4448b9194840c7da73afc8159faf7358140bfd0f237962", @@ -367,7 +449,8 @@ Example response: ], "bindingSig": "bc018af8808387...5130bb382ad8e6e73cb009", "blockhash": "0000000001c4aa394e796dd1b82e358f114535204f6f5b6cf4ad58dc439c47af", - "confirmations": 5222, + "height": 495665, + "confirmations": 2145803, "time": 1552301566, "blocktime": 1552301566 } @@ -383,43 +466,46 @@ GET /api/v2/address/
[?page=&pageSize=&from=&t The optional query parameters: -- _page_: specifies page of returned transactions, starting from 1. If out of range, Blockbook returns the closest possible page. -- _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 address balances, without any transactions - - _tokens_: _basic_ + tokens belonging to the address (applicable only to some coins) - - _tokenBalances_: _basic_ + tokens with balances + belonging to the address (applicable only to some coins) - - _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) -- _secondary_: specifies secondary (fiat) currency in which the token and total balances are returned in addition to crypto values - -Example response for bitcoin type coin, _details_ set to _txids_: +- _page_: specifies page of returned transactions, starting from 1. If out of range, Blockbook returns the closest possible page. +- _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 address balances, without any transactions + - _tokens_: _basic_ + tokens belonging to the address (applicable only to some coins) + - _tokenBalances_: _basic_ + tokens with balances + belonging to the address (applicable only to some coins) + - _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) +- _secondary_: specifies secondary (fiat) currency in which the token and total balances are returned in addition to crypto values + +Example response for bitcoin type coin, _details_ set to _txids_ (`Address` type): + + ```javascript { "page": 1, "totalPages": 1, "itemsOnPage": 1000, - "address": "D5Z7XrtJNg7hAtznSDMXvfiFmMYphwuWz7", - "balance": "2432468097999991", - "totalReceived": "3992283916999979", - "totalSent": "1559815818999988", + "address": "bc1q0wd209cv5k9pd9mhk7nspacywcj038xxdhnt5u", + "balance": "4225100", + "totalReceived": "4225100", + "totalSent": "0", "unconfirmedBalance": "0", "unconfirmedTxs": 0, - "txs": 3, + "txs": 2, "txids": [ - "461dd46d5d6f56d765f82e60e6bf0727a3a1d1cb8c4144373d805b152a21d308", - "bdb5b47603c5d174eae3384c368068c8e9d2183b398ed0e31d125defa4447a10", - "5c1d2686d70d82bd8e84b5d3dc4bd0e8485e28cdc865336db6a5e40b2098277d" + "0db6010dc0815a4bdaa505bd1ccc851056b0d53c7e4ea7af39c4d648a2c0c019", + "7532920ddc506218337cceac978cce9c7f98e27ad3226dee55f3e934e0b32e80" ] } ``` Example response for ethereum type coin, _details_ set to _tokenBalances_ and _secondary_ set to _usd_. The _baseValue_ is value of the token in the base currency (ETH), _secondaryValue_ is value of the token in specified _secondary_ currency: + + ```javascript { "address": "0x2df3951b2037bA620C20Ed0B73CCF45Ea473e83B", @@ -457,25 +543,25 @@ Returns balances and transactions of an xpub or output descriptor, applicable on 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 +- Xpubs - 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. + 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. -- Output descriptors +- Output descriptors - Output descriptors are in the form `([][//*])[#checkum]`, for example `pkh([5c9e228d/44'/0'/0']xpub6BgBgses...Mj92pReUsQ/<0;1>/*)#abcd` + Output descriptors are in the form `([][//*])[#checksum]`, for example `pkh([5c9e228d/44'/0'/0']xpub6BgBgses...Mj92pReUsQ/<0;1>/*)#abcd` - Parameters `type` and `xpub` are mandatory, the rest is optional + Parameters `type` and `xpub` are mandatory, the rest is optional - Blockbook supports a limited set of `type`s: + Blockbook supports a limited set of `type`s: - - BIP44: `pkh(xpub)` - - BIP49: `sh(wpkh(xpub))` - - BIP84: `wpkh(xpub)` - - BIP86 (Taproot single key): `tr(xpub)` + - 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>`. + 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. @@ -485,22 +571,22 @@ GET /api/v2/xpub/[?page=&pageSize=&from=[?confirmed=true] ``` -Response: +Response (`Utxo[]` type): ```javascript [ - { - txid: "13d26cd939bf5d155b1c60054e02d9c9b832a85e6ec4f2411be44b6b5a2842e9", - vout: 0, - value: "1422303206539", - confirmations: 0, - lockTime: 2648100, - }, - { - txid: "a79e396a32e10856c97b95f43da7e9d2b9a11d446f7638dbd75e5e7603128cac", - vout: 1, - value: "39748685", - height: 2648043, - confirmations: 47, - coinbase: true, - }, - { - txid: "de4f379fdc3ea9be063e60340461a014f372a018d70c3db35701654e7066b3ef", - vout: 0, - value: "122492339065", - height: 2646043, - confirmations: 2047, - }, - { - txid: "9e8eb9b3d2e8e4b5d6af4c43a9196dfc55a05945c8675904d8c61f404ea7b1e9", - vout: 0, - value: "142771322208", - height: 2644885, - confirmations: 3205, - }, + { + txid: '13d26cd939bf5d155b1c60054e02d9c9b832a85e6ec4f2411be44b6b5a2842e9', + vout: 0, + value: '1422303206539', + confirmations: 0, + lockTime: 2648100, + }, + { + txid: 'a79e396a32e10856c97b95f43da7e9d2b9a11d446f7638dbd75e5e7603128cac', + vout: 1, + value: '39748685', + height: 2648043, + confirmations: 47, + coinbase: true, + }, + { + txid: 'de4f379fdc3ea9be063e60340461a014f372a018d70c3db35701654e7066b3ef', + vout: 0, + value: '122492339065', + height: 2646043, + confirmations: 2047, + }, + { + txid: '9e8eb9b3d2e8e4b5d6af4c43a9196dfc55a05945c8675904d8c61f404ea7b1e9', + vout: 0, + value: '142771322208', + height: 2644885, + confirmations: 3205, + }, ]; ``` @@ -606,7 +692,7 @@ Returns information about block with transactions, subject to paging. GET /api/v2/block/ ``` -Response: +Response (`Block` type): ```javascript { @@ -735,9 +821,9 @@ GET /api/v2/tickers-list/?timestamp= The query parameters: -- _timestamp_: specifies a Unix timestamp to return available tickers for. +- _timestamp_: specifies a Unix timestamp to return available tickers for. -Example response: +Example response (`AvailableVsCurrencies` type): ```javascript { @@ -760,10 +846,10 @@ GET /api/v2/tickers/[?currency=×tamp=] The optional query parameters: -- _currency_: specifies a currency of returned rate ("usd", "eur", "eth"...). If not specified, all available currencies will be returned. -- _timestamp_: a Unix timestamp that specifies a date to return currency rates for. If not specified, the last available rate will be returned. +- _currency_: specifies a currency of returned rate ("usd", "eur", "eth"...). If not specified, all available currencies will be returned. +- _timestamp_: a Unix timestamp that specifies a date to return currency rates for. If not specified, the last available rate will be returned. -Example response (no parameters): +Example response (no parameters, `FiatTicker` type): ```javascript { @@ -807,15 +893,15 @@ GET /api/v2/balancehistory/?from=&to=[&fiatcur Query parameters: -- _from_: specifies a start date as a Unix timestamp -- _to_: specifies an end date as a Unix timestamp +- _from_: specifies a start date as a Unix timestamp +- _to_: specifies an end date as a Unix timestamp The optional query parameters: -- _fiatcurrency_: if specified, the response will contain secondary (fiat) rate at the time of transaction. If not, all available currencies will be returned. -- _groupBy_: an interval in seconds, to group results by. Default is 3600 seconds. +- _fiatcurrency_: if specified, the response will contain secondary (fiat) rate at the time of transaction. If not, all available currencies will be returned. +- _groupBy_: an interval in seconds, to group results by. Default is 3600 seconds. -Example response (_fiatcurrency_ not specified): +Example response (_fiatcurrency_ not specified, `BalanceHistory[]` type): ```javascript [ @@ -850,26 +936,26 @@ Example response (fiatcurrency=usd): ```javascript [ - { - time: 1578391200, - txs: 5, - received: "5000000", - sent: "0", - sentToSelf: "0", - rates: { - usd: 7855.9, + { + time: 1578391200, + txs: 5, + received: '5000000', + sent: '0', + sentToSelf: '0', + rates: { + usd: 7855.9, + }, }, - }, - { - time: 1578488400, - txs: 1, - received: "0", - sent: "5000000", - sentToSelf: "0", - rates: { - usd: 8283.11, + { + time: 1578488400, + txs: 1, + received: '0', + sent: '5000000', + sentToSelf: '0', + rates: { + usd: 8283.11, + }, }, - }, ]; ``` @@ -877,16 +963,16 @@ Example response (fiatcurrency=usd&groupBy=172800): ```javascript [ - { - time: 1578355200, - txs: 6, - received: "5000000", - sent: "5000000", - sentToSelf: "0", - rates: { - usd: 7734.45, + { + time: 1578355200, + txs: 6, + received: '5000000', + sent: '5000000', + sentToSelf: '0', + rates: { + usd: 7734.45, + }, }, - }, ]; ``` @@ -898,26 +984,28 @@ Websocket interface is provided at `/websocket/`. The interface can be explored The websocket interface provides the following requests: -- getInfo -- getBlockHash -- getAccountInfo -- getAccountUtxo -- getTransaction -- getTransactionSpecific -- getBalanceHistory -- getCurrentFiatRates -- getFiatRatesTickersList -- getFiatRatesForTimestamps -- estimateFee -- sendTransaction -- ping +- getInfo +- getBlockHash +- getAccountInfo +- getAccountUtxo +- getTransaction +- getTransactionSpecific +- getBalanceHistory +- getCurrentFiatRates +- getFiatRatesTickersList +- getFiatRatesForTimestamps +- getMempoolFilters +- getBlockFilter +- estimateFee +- sendTransaction +- ping The client can subscribe to the following events: -- `subscribeNewBlock` - new block added to blockchain -- `subscribeNewTransaction` - new transaction added to blockchain (all addresses) -- `subscribeAddresses` - new transaction for a given address (list of addresses) added to mempool -- `subscribeFiatRates` - new currency rate ticker +- `subscribeNewBlock` - new block added to blockchain +- `subscribeNewTransaction` - new transaction added to blockchain (all addresses) +- `subscribeAddresses` - new transaction for a given address (list of addresses) added to mempool +- `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. @@ -925,7 +1013,7 @@ The subscribeNewTransaction event is not enabled by default. To enable support, _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 +Websocket communication format (`WsReq` type) ```javascript { @@ -949,7 +1037,7 @@ Example for subscribing to an address (or multiple addresses) ## Legacy API V1 -The legacy API is a compatible subset of API provided by **Bitcore Insight**. It is supported only Bitcoin-type coins. The details of the REST/socket.io requests can be found in the Insight's documentation. +The legacy API is a compatible subset of API provided by **Bitcore Insight**. It is supported only for Bitcoin-type coins. The details of the REST/socket.io requests can be found in the Insight's documentation. ### REST API @@ -970,4 +1058,4 @@ 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 (as of Blockbook v0.4.0) also accessible without the _/v1/_ prefix, however in the future versions the version less access will be removed. +The legacy API is currently (as of Blockbook v0.5.0) also accessible without the _/v1/_ prefix, however in the future versions the version-less access will be removed. diff --git a/docs/build.md b/docs/build.md index 3623d50f26..9d39d3f1fa 100644 --- a/docs/build.md +++ b/docs/build.md @@ -11,7 +11,7 @@ Manual build require additional dependencies that are described in appropriate s ## Build in Docker environment All build operations run in Docker container in order to keep build environment isolated. Makefile in root of repository -define few targets used for building, testing and packaging of Blockbook. With Docker image definitions and Debian +defines few targets used for building, testing and packaging of Blockbook. With Docker image definitions and Debian package templates in *build/docker* and *build/templates* respectively, they are only inputs that make build process. Docker build images are created at first execution of Makefile and that information is persisted. (Actually there are @@ -137,7 +137,7 @@ Blockbook versioning is much simpler. There is only one version defined in *conf ### Back-end building -Because we don't keep back-end archives inside out repository we download them during build process. Build steps +Because we don't keep back-end archives inside our repository we download them during build process. Build steps are these: download, verify and extract archive, prepare distribution and make package. All configuration keys described below are in coin definition file in *configs/coins*. @@ -153,7 +153,7 @@ have signed sha256 sums and some don't care about verification at all. So there could be *gpg*, *gpg-sha256* or *sha256* and chooses particular method. *gpg* type require file with digital sign and maintainer's public key imported in Docker build image (see below). Sign -file is downloaded from URL defined in *backend.verification_source*. Than is passed to gpg in order to verify archvie. +file is downloaded from URL defined in *backend.verification_source*. Than is passed to gpg in order to verify archive. *gpg-sha256* type require signed checksum file and maintainer's public key imported in Docker build image (see below). Checksum file is downloaded from URL defined in *backend.verification_source*. Then is verified by gpg and passed to @@ -191,7 +191,7 @@ like macOS or Windows, please adapt the instructions to your target system. Setup go environment (use newer version of go as available) ``` -wget https://golang.org/dl/go1.19.linux-amd64.tar.gz && tar xf go1.19.linux-amd64.tar.gz +wget https://golang.org/dl/go1.22.8.linux-amd64.tar.gz && tar xf go1.22.8.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 @@ -201,15 +201,15 @@ export PATH=$PATH:$GOPATH/bin ``` Install RocksDB: https://github.com/facebook/rocksdb/blob/master/INSTALL.md -and compile the static_lib and tools. Optionally, consider adding `PORTABLE=1` before the +and compile the static_lib and tools. Optionally, consider adding `PORTABLE=1` before the make command to create a portable binary. ``` sudo apt-get update && sudo apt-get install -y \ - build-essential git wget pkg-config libzmq3-dev libgflags-dev libsnappy-dev zlib1g-dev libzstd-dev libbz2-dev liblz4-dev + build-essential git wget pkg-config libzmq3-dev libgflags-dev libsnappy-dev zlib1g-dev libzstd-dev libbz2-dev liblz4-dev git clone https://github.com/facebook/rocksdb.git cd rocksdb -git checkout v7.5.3 +git checkout v9.10.0 CFLAGS=-fPIC CXXFLAGS=-fPIC make release ``` diff --git a/docs/config.md b/docs/config.md index b4e14296c8..5e413e38b2 100644 --- a/docs/config.md +++ b/docs/config.md @@ -32,7 +32,7 @@ Good examples of coin configuration are * `backend_*` – Additional back-end ports can be documented here. Actually the only purpose is to get them to port table (prefix is removed and rest of string is used as note). * `blockbook_internal` – Blockbook's internal port that is used for metric collecting, debugging etc. - * `blockbook_public` – Blockbook's public port that is used to comunicate with Trezor wallet (via Socket.IO). + * `blockbook_public` – Blockbook's public port that is used to communicate with Trezor wallet (via Socket.IO). * `ipc` – Defines how Blockbook connects its back-end service. * `rpc_url_template` – Template that defines URL of back-end RPC service. See note on templates below. @@ -82,7 +82,7 @@ Good examples of coin configuration are * `explorer_url` – URL of blockchain explorer. Leave empty for internal explorer. * `additional_params` – Additional params of exec command (see [Dogecoin definition](/configs/coins/dogecoin.json)). * `block_chain` – Configuration of BlockChain type that ensures communication with back-end service. All options - must be tweaked for each individual coin separely. + must be tweaked for each individual coin separately. * `parse` – Use binary parser for block decoding if *true* else call verbose back-end RPC method that returns JSON. Note that verbose method is slow and not every coin support it. However there are coin implementations that don't support binary parsing (e.g. ZCash). @@ -112,4 +112,4 @@ to alter built-in text that is specific for Trezor. Text fields that could be up * [tos_link](/build/text/tos_link) – A link to Terms of service shown as the footer on the Explorer pages. Text data are stored as plain text files in *build/text* directory and are embedded to binary during build. A change of -theese files is mean for a private purpose and PRs that would update them won't be accepted. +these files is meant for a private purpose and PRs that would update them won't be accepted. diff --git a/docs/env.md b/docs/env.md new file mode 100644 index 0000000000..8d95dc985c --- /dev/null +++ b/docs/env.md @@ -0,0 +1,11 @@ +# Environment variables + +Some behavior of Blockbook can be modified by environment variables. The variables usually start with a coin shortcut to allow to run multiple Blockbooks on a single server. + +- `_WS_GETACCOUNTINFO_LIMIT` - Limits the number of `getAccountInfo` requests per websocket connection to reduce server abuse. Accepts number as input. + +- `_STAKING_POOL_CONTRACT` - The pool name and contract used for Ethereum staking. The format of the variable is `/`. If missing, staking support is disabled. + +- `COINGECKO_API_KEY` or `_COINGECKO_API_KEY` - API key for making requests to CoinGecko in the paid tier. + +- `_ALLOWED_RPC_CALL_TO` - Addresses to which `rpcCall` websocket requests can be made, as a comma-separated list. If omitted, `rpcCall` is enabled for all addresses. diff --git a/docs/ports.md b/docs/ports.md index 9b010b39cf..18bed15cab 100644 --- a/docs/ports.md +++ b/docs/ports.md @@ -1,76 +1,93 @@ # Registry of ports -| coin | blockbook internal port | blockbook public port | backend rpc port | backend service ports (zmq) | -|------------------------|-------------------------|-----------------------|------------------|-----------------------------| -| Bitcoin | 9030 | 9130 | 8030 | 38330 | -| Bitcoin Cash | 9031 | 9131 | 8031 | 38331 | -| Zcash | 9032 | 9132 | 8032 | 38332 | -| Dash | 9033 | 9133 | 8033 | 38333 | -| Litecoin | 9034 | 9134 | 8034 | 38334 | -| Bitcoin Gold | 9035 | 9135 | 8035 | 38335 | -| Ethereum | 9036 | 9136 | 8036 | 38336 p2p, 8136 http | -| Ethereum Classic | 9037 | 9137 | 8037 | | -| Dogecoin | 9038 | 9138 | 8038 | 38338 | -| Namecoin | 9039 | 9139 | 8039 | 38339 | -| Vertcoin | 9040 | 9140 | 8040 | 38340 | -| Monacoin | 9041 | 9141 | 8041 | 38341 | -| DigiByte | 9042 | 9142 | 8042 | 38342 | -| Myriad | 9043 | 9143 | 8043 | 38343 | -| GameCredits | 9044 | 9144 | 8044 | 38344 | -| Groestlcoin | 9045 | 9145 | 8045 | 38345 | -| Bitcoin Cash SV | 9046 | 9146 | 8046 | 38346 | -| Liquid | 9047 | 9147 | 8047 | 38347 | -| Fujicoin | 9048 | 9148 | 8048 | 38348 | -| PIVX | 9049 | 9149 | 8049 | 38349 | -| Firo | 9050 | 9150 | 8050 | 38350 | -| Koto | 9051 | 9151 | 8051 | 38351 | -| Bellcoin | 9052 | 9152 | 8052 | 38352 | -| NULS | 9053 | 9153 | 8053 | 38353 | -| Bitcore | 9054 | 9154 | 8054 | 38354 | -| Viacoin | 9055 | 9155 | 8055 | 38355 | -| VIPSTARCOIN | 9056 | 9156 | 8056 | 38356 | -| MonetaryUnit | 9057 | 9157 | 8057 | 38357 | -| Flux | 9058 | 9158 | 8058 | 38358 | -| Ravencoin | 9059 | 9159 | 8059 | 38359 | -| Ritocoin | 9060 | 9160 | 8060 | 38360 | -| Decred | 9061 | 9161 | 8061 | 38361 | -| SnowGem | 9062 | 9162 | 8062 | 38362 | -| Flo | 9066 | 9166 | 8066 | 38366 | -| Polis | 9067 | 9167 | 8067 | 38367 | -| Qtum | 9088 | 9188 | 8088 | 38388 | -| Divi Project | 9089 | 9189 | 8089 | 38389 | -| CPUchain | 9090 | 9190 | 8090 | 38390 | -| DeepOnion | 9091 | 9191 | 8091 | 38391 | -| Unobtanium | 9092 | 9192 | 65535 | 38392 | -| Omotenashicoin | 9094 | 9194 | 8094 | 38394 | -| BitZeny | 9095 | 9195 | 8095 | 38395 | -| Trezarcoin | 9096 | 9196 | 8096 | 38396 | -| eCash | 9097 | 9197 | 8097 | 38397 | -| Avalanche | 9098 | 9198 | 8098 | 38398 p2p | -| Avalanche Archive | 9099 | 9199 | 8099 | 38399 p2p | -| Bitcoin Signet | 19020 | 19120 | 18020 | 48320 | -| Bitcoin Regtest | 19021 | 19121 | 18021 | 48321 | -| Ethereum Goerli | 19026 | 19126 | 18026 | 48326 p2p | -| Ethereum Sepolia | 19176 | 19176 | 18076 | 48376 p2p | -| 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 | -| Bitcoin Gold Testnet | 19035 | 19135 | 18035 | 48335 | -| Ethereum Ropsten | 19036 | 19136 | 18036 | 48336 p2p | -| Dogecoin Testnet | 19038 | 19138 | 18038 | 48338 | -| Vertcoin Testnet | 19040 | 19140 | 18040 | 48340 | -| Monacoin Testnet | 19041 | 19141 | 18041 | 48341 | -| DigiByte Testnet | 19042 | 19142 | 18042 | 48342 | -| Groestlcoin Testnet | 19045 | 19145 | 18045 | 48345 | -| Groestlcoin Regtest | 19046 | 19146 | 18046 | 48346 | -| Groestlcoin Signet | 19047 | 19147 | 18047 | 48347 | -| PIVX Testnet | 19049 | 19149 | 18049 | 48349 | -| Koto Testnet | 19051 | 19151 | 18051 | 48351 | -| Decred Testnet | 19061 | 19161 | 18061 | 48361 | -| Flo Testnet | 19066 | 19166 | 18066 | 48366 | -| Qtum Testnet | 19088 | 19188 | 18088 | 48388 | -| Omotenashicoin Testnet | 19089 | 19189 | 18089 | 48389 | +| coin | blockbook public | blockbook internal | backend rpc | backend service ports (zmq) | +|----------------------------------|------------------|--------------------|-------------|-----------------------------------------------------| +| Ethereum Archive | 9116 | 9016 | 8016 | 38316 p2p, 8116 http, 8516 authrpc | +| Bitcoin | 9130 | 9030 | 8030 | 38330 | +| Bitcoin Cash | 9131 | 9031 | 8031 | 38331 | +| Zcash | 9132 | 9032 | 8032 | 38332 | +| Dash | 9133 | 9033 | 8033 | 38333 | +| Litecoin | 9134 | 9034 | 8034 | 38334 | +| Bitcoin Gold | 9135 | 9035 | 8035 | 38335 | +| Ethereum | 9136 | 9036 | 8036 | 38336 p2p, 8136 http, 8536 authrpc | +| Ethereum Classic | 9137 | 9037 | 8037 | 38337 p2p, 8137 http | +| Dogecoin | 9138 | 9038 | 8038 | 38338 | +| Namecoin | 9139 | 9039 | 8039 | 38339 | +| Vertcoin | 9140 | 9040 | 8040 | 38340 | +| Monacoin | 9141 | 9041 | 8041 | 38341 | +| DigiByte | 9142 | 9042 | 8042 | 38342 | +| Myriad | 9143 | 9043 | 8043 | 38343 | +| GameCredits | 9144 | 9044 | 8044 | 38344 | +| Groestlcoin | 9145 | 9045 | 8045 | 38345 | +| Bitcoin Cash SV | 9146 | 9046 | 8046 | 38346 | +| Liquid | 9147 | 9047 | 8047 | 38347 | +| Fujicoin | 9148 | 9048 | 8048 | 38348 | +| PIVX | 9149 | 9049 | 8049 | 38349 | +| Firo | 9150 | 9050 | 8050 | 38350 | +| Koto | 9151 | 9051 | 8051 | 38351 | +| Bellcoin | 9152 | 9052 | 8052 | 38352 | +| NULS | 9153 | 9053 | 8053 | 38353 | +| Bitcore | 9154 | 9054 | 8054 | 38354 | +| Viacoin | 9155 | 9055 | 8055 | 38355 | +| VIPSTARCOIN | 9156 | 9056 | 8056 | 38356 | +| MonetaryUnit | 9157 | 9057 | 8057 | 38357 | +| Flux | 9158 | 9058 | 8058 | 38358 | +| Ravencoin | 9159 | 9059 | 8059 | 38359 | +| Ritocoin | 9160 | 9060 | 8060 | 38360 | +| Decred | 9161 | 9061 | 8061 | 38361 | +| SnowGem | 9162 | 9062 | 8062 | 38362 | +| BNB Smart Chain | 9164 | 9064 | 8064 | 38364 p2p, 8164 http | +| BNB Smart Chain Archive | 9165 | 9065 | 8065 | 38365 p2p, 8165 http | +| Flo | 9166 | 9066 | 8066 | 38366 | +| Polis | 9167 | 9067 | 8067 | 38367 | +| Polygon | 9170 | 9070 | 8070 | 38370 p2p, 8170 http | +| Polygon Archive | 9172 | 9072 | 8072 | 38372 p2p, 8172 http | +| Qtum | 9188 | 9088 | 8088 | 38388 | +| Divi Project | 9189 | 9089 | 8089 | 38389 | +| CPUchain | 9190 | 9090 | 8090 | 38390 | +| DeepOnion | 9191 | 9091 | 8091 | 38391 | +| Unobtanium | 9192 | 9092 | 65535 | 38392 | +| Omotenashicoin | 9194 | 9094 | 8094 | 38394 | +| BitZeny | 9195 | 9095 | 8095 | 38395 | +| Trezarcoin | 9196 | 9096 | 8096 | 38396 | +| eCash | 9197 | 9097 | 8097 | 38397 | +| Avalanche | 9198 | 9098 | 8098 | 38398 p2p | +| Avalanche Archive | 9199 | 9099 | 8099 | 38399 p2p | +| Optimism | 9300 | 9200 | 8200 | 38400 p2p, 8300 http, 8400 authrpc | +| Optimism Archive | 9302 | 9202 | 8202 | 38402 p2p, 8302 http, 8402 authrpc | +| Arbitrum | 9305 | 9205 | 8205 | 38405 p2p, 8305 http | +| Arbitrum Archive | 9306 | 9206 | 8306 | 38406 p2p | +| Arbitrum Nova | 9307 | 9207 | 8207 | 38407 p2p, 8307 http | +| Arbitrum Nova Archive | 9308 | 9208 | 8308 | 38408 p2p | +| Base | 9309 | 9209 | 8309 | 38409 p2p, 8209 http, 8409 authrpc | +| Base Archive | 9311 | 9211 | 8211 | 38411 p2p, 8311 http, 8411 authrpc | +| Bitcoin Signet | 19120 | 19020 | 18020 | 48320 | +| Bitcoin Regtest | 19121 | 19021 | 18021 | 48321 | +| Bitcoin Testnet4 | 19129 | 19029 | 18029 | 48329 | +| Bitcoin Testnet | 19130 | 19030 | 18030 | 48330 | +| Bitcoin Cash Testnet | 19131 | 19031 | 18031 | 48331 | +| Zcash Testnet | 19132 | 19032 | 18032 | 48332 | +| Dash Testnet | 19133 | 19033 | 18033 | 48333 | +| Litecoin Testnet | 19134 | 19034 | 18034 | 48334 | +| Bitcoin Gold Testnet | 19135 | 19035 | 18035 | 48335 | +| Dogecoin Testnet | 19138 | 19038 | 18038 | 48338 | +| Vertcoin Testnet | 19140 | 19040 | 18040 | 48340 | +| Monacoin Testnet | 19141 | 19041 | 18041 | 48341 | +| DigiByte Testnet | 19142 | 19042 | 18042 | 48342 | +| Groestlcoin Testnet | 19145 | 19045 | 18045 | 48345 | +| Groestlcoin Regtest | 19146 | 19046 | 18046 | 48346 | +| Groestlcoin Signet | 19147 | 19047 | 18047 | 48347 | +| PIVX Testnet | 19149 | 19049 | 18049 | 48349 | +| Koto Testnet | 19151 | 19051 | 18051 | 48351 | +| Decred Testnet | 19161 | 19061 | 18061 | 48361 | +| Flo Testnet | 19166 | 19066 | 18066 | 48366 | +| Ethereum Testnet Holesky | 19116 | 19016 | 18016 | 18116 http, 18516 authrpc, 48316 p2p | +| Ethereum Testnet Holesky Archive | 19136 | 19036 | 18036 | 18136 http, 18136 torrent, 18536 authrpc, 48336 p2p | +| Ethereum Testnet Hoodi | 19106 | 19006 | 18006 | 18106 http, 18506 authrpc, 48306 p2p | +| Ethereum Testnet Hoodi Archive | 19126 | 19026 | 18026 | 18126 http, 18126 torrent, 18526 authrpc, 48326 p2p | +| Ethereum Testnet Sepolia | 19176 | 19076 | 18076 | 18176 http, 18576 authrpc, 48376 p2p | +| Ethereum Testnet Sepolia Archive | 19186 | 19086 | 18086 | 18186 http, 18186 torrent, 18586 authrpc, 48386 p2p | +| Qtum Testnet | 19188 | 19088 | 18088 | 48388 | +| Omotenashicoin Testnet | 19189 | 19089 | 18089 | 48389 | -> NOTE: This document is generated from coin definitions in `configs/coins`. +> NOTE: This document is generated from coin definitions in `configs/coins` using command `go run contrib/scripts/check-and-generate-port-registry.go -w`. diff --git a/docs/rocksdb.md b/docs/rocksdb.md index ddc2356f93..3a230085e9 100644 --- a/docs/rocksdb.md +++ b/docs/rocksdb.md @@ -25,7 +25,7 @@ **Database structure:** -The database structure described here is of Blockbook version **0.4.0** (internal data format version 6). +The database structure described here is of Blockbook version **0.5.0** (internal data format version 7). The database structure for **Bitcoin type** and **Ethereum type** coins is different. Column families used for both types: @@ -100,7 +100,7 @@ Column families used only by **Ethereum type** coins: and array of _contracts_ with _number of transfers_ of given address. ``` - (addrDesc []byte) -> (total_txs vuint)+(non-contract_txs vuint)+(internal_txs vuint)+ + (addrDesc []byte) -> (total_txs vuint)+(non-contract_txs vuint)+(internal_txs vuint)+(contracts vuint)+ []((contractAddrDesc []byte)+(type+4*nr_transfers vuint))+ <(value bigInt) if ERC20> or <(nr_values vuint)+[](id bigInt) if ERC721> or diff --git a/docs/testing.md b/docs/testing.md index aa6de9ee7d..639bd3a8ec 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 @@ -34,7 +34,7 @@ components of Blockbook, it is mandatory that coin implementations have these in implemented in packages `blockbook/tests/rpc` and `blockbook/tests/sync` and both of them are declarative. For each coin there are test definition that enables particular tests of test suite and *testdata* file that contains test fixtures. -Not every coin implementation support full set of back-end API so it is necessary define which tests of test suite +Not every coin implementation supports full set of back-end API so it is necessary to define which tests of test suite are able to run. That is done in test definition file *blockbook/tests/tests.json*. Configuration is hierarchical and test implementations call each level as separate subtest. Go's *test* command allows filter tests to run by `-run` flag. It perfectly fits with layered test definitions. For example, you can: diff --git a/fiat/coingecko.go b/fiat/coingecko.go index 1f5087c392..7e79dd4222 100644 --- a/fiat/coingecko.go +++ b/fiat/coingecko.go @@ -3,9 +3,10 @@ package fiat import ( "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "net/url" + "os" "strconv" "strings" "time" @@ -16,9 +17,15 @@ import ( "github.com/trezor/blockbook/db" ) +const ( + DefaultHTTPTimeout = 15 * time.Second + DefaultThrottleDelayMs = 100 // 100 ms delay between requests +) + // Coingecko is a structure that implements RatesDownloaderInterface type Coingecko struct { url string + apiKey string coin string platformIdentifier string platformVsCurrency string @@ -30,6 +37,7 @@ type Coingecko struct { db *db.RocksDB updatingCurrent bool updatingTokens bool + metrics *common.Metrics } // simpleSupportedVSCurrencies https://api.coingecko.com/api/v3/simple/supported_vs_currencies @@ -51,32 +59,56 @@ type marketChartPrices struct { } // NewCoinGeckoDownloader creates a coingecko structure that implements the RatesDownloaderInterface -func NewCoinGeckoDownloader(db *db.RocksDB, url string, coin string, platformIdentifier string, platformVsCurrency string, allowedVsCurrencies string, timeFormat string, throttleDown bool) RatesDownloaderInterface { - var throttlingDelayMs int +func NewCoinGeckoDownloader(db *db.RocksDB, network string, url string, coin string, platformIdentifier string, platformVsCurrency string, allowedVsCurrencies string, timeFormat string, metrics *common.Metrics, throttleDown bool) RatesDownloaderInterface { + throttlingDelayMs := 0 // No delay by default if throttleDown { - throttlingDelayMs = 100 + throttlingDelayMs = DefaultThrottleDelayMs } - httpTimeout := 15 * time.Second - allowedVsCurrenciesMap := make(map[string]struct{}) - if len(allowedVsCurrencies) > 0 { - for _, c := range strings.Split(strings.ToLower(allowedVsCurrencies), ",") { - allowedVsCurrenciesMap[c] = struct{}{} + + allowedVsCurrenciesMap := getAllowedVsCurrenciesMap(allowedVsCurrencies) + + apiKey := os.Getenv(strings.ToUpper(network) + "_COINGECKO_API_KEY") + if apiKey == "" { + apiKey = os.Getenv("COINGECKO_API_KEY") + } + + // use default address if not overridden, with respect to existence of apiKey + if url == "" { + if apiKey != "" { + url = "https://pro-api.coingecko.com/api/v3/" + } else { + url = "https://api.coingecko.com/api/v3" } } + glog.Info("Coingecko downloader url ", url) + return &Coingecko{ url: url, + apiKey: apiKey, coin: coin, platformIdentifier: platformIdentifier, platformVsCurrency: platformVsCurrency, allowedVsCurrencies: allowedVsCurrenciesMap, - httpTimeout: httpTimeout, + httpTimeout: DefaultHTTPTimeout, timeFormat: timeFormat, httpClient: &http.Client{ - Timeout: httpTimeout, + Timeout: DefaultHTTPTimeout, }, db: db, throttlingDelay: time.Duration(throttlingDelayMs) * time.Millisecond, + metrics: metrics, + } +} + +// getAllowedVsCurrenciesMap returns a map of allowed vs currencies +func getAllowedVsCurrenciesMap(currenciesString string) map[string]struct{} { + allowedVsCurrenciesMap := make(map[string]struct{}) + if len(currenciesString) > 0 { + for _, c := range strings.Split(strings.ToLower(currenciesString), ",") { + allowedVsCurrenciesMap[c] = struct{}{} + } } + return allowedVsCurrenciesMap } // doReq HTTP client @@ -86,7 +118,7 @@ func doReq(req *http.Request, client *http.Client) ([]byte, error) { return nil, err } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } @@ -97,7 +129,7 @@ func doReq(req *http.Request, client *http.Client) ([]byte, error) { } // makeReq HTTP request helper - will retry the call after 1 minute on error -func (cg *Coingecko) makeReq(url string) ([]byte, error) { +func (cg *Coingecko) makeReq(url string, endpoint string) ([]byte, error) { for { // glog.Infof("Coingecko makeReq %v", url) req, err := http.NewRequest("GET", url, nil) @@ -105,14 +137,26 @@ func (cg *Coingecko) makeReq(url string) ([]byte, error) { return nil, err } req.Header.Set("Content-Type", "application/json") + if cg.apiKey != "" { + req.Header.Set("x-cg-pro-api-key", cg.apiKey) + } resp, err := doReq(req, cg.httpClient) if err == nil { + if cg.metrics != nil { + cg.metrics.CoingeckoRequests.With(common.Labels{"endpoint": endpoint, "status": "success"}).Inc() + } return resp, err } - if err.Error() != "error code: 1015" && !strings.Contains(strings.ToLower(err.Error()), "exceeded the rate limit") { + if err.Error() != "error code: 1015" && !strings.Contains(strings.ToLower(err.Error()), "exceeded the rate limit") && !strings.Contains(strings.ToLower(err.Error()), "throttled") { + if cg.metrics != nil { + cg.metrics.CoingeckoRequests.With(common.Labels{"endpoint": endpoint, "status": "error"}).Inc() + } glog.Errorf("Coingecko makeReq %v error %v", url, err) return nil, err } + if cg.metrics != nil { + cg.metrics.CoingeckoRequests.With(common.Labels{"endpoint": endpoint, "status": "throttle"}).Inc() + } // if there is a throttling error, wait 60 seconds and retry glog.Warningf("Coingecko makeReq %v error %v, will retry in 60 seconds", url, err) time.Sleep(60 * time.Second) @@ -122,7 +166,7 @@ func (cg *Coingecko) makeReq(url string) ([]byte, error) { // SimpleSupportedVSCurrencies /simple/supported_vs_currencies func (cg *Coingecko) simpleSupportedVSCurrencies() (simpleSupportedVSCurrencies, error) { url := cg.url + "/simple/supported_vs_currencies" - resp, err := cg.makeReq(url) + resp, err := cg.makeReq(url, "supported_vs_currencies") if err != nil { return nil, err } @@ -153,7 +197,7 @@ func (cg *Coingecko) simplePrice(ids []string, vsCurrencies []string) (*map[stri params.Add("vs_currencies", vsCurrenciesParam) url := fmt.Sprintf("%s/simple/price?%s", cg.url, params.Encode()) - resp, err := cg.makeReq(url) + resp, err := cg.makeReq(url, "simple/price") if err != nil { return nil, err } @@ -176,7 +220,7 @@ func (cg *Coingecko) coinsList() (coinList, error) { } params.Add("include_platform", platform) url := fmt.Sprintf("%s/coins/list?%s", cg.url, params.Encode()) - resp, err := cg.makeReq(url) + resp, err := cg.makeReq(url, "coins/list") if err != nil { return nil, err } @@ -190,18 +234,20 @@ func (cg *Coingecko) coinsList() (coinList, error) { } // coinMarketChart /coins/{id}/market_chart?vs_currency={usd, eur, jpy, etc.}&days={1,14,30,max} -func (cg *Coingecko) coinMarketChart(id string, vs_currency string, days string) (*marketChartPrices, error) { +func (cg *Coingecko) coinMarketChart(id string, vs_currency string, days string, daily bool) (*marketChartPrices, error) { if len(id) == 0 || len(vs_currency) == 0 || len(days) == 0 { return nil, fmt.Errorf("id, vs_currency, and days is required") } params := url.Values{} - params.Add("interval", "daily") + if daily { + params.Add("interval", "daily") + } params.Add("vs_currency", vs_currency) params.Add("days", days) url := fmt.Sprintf("%s/coins/%s/market_chart?%s", cg.url, id, params.Encode()) - resp, err := cg.makeReq(url) + resp, err := cg.makeReq(url, "market_chart") if err != nil { return nil, err } @@ -241,6 +287,7 @@ func (cg *Coingecko) platformIds() error { return nil } +// CurrentTickers returns the latest exchange rates func (cg *Coingecko) CurrentTickers() (*common.CurrencyRatesTicker, error) { cg.updatingCurrent = true defer func() { cg.updatingCurrent = false }() @@ -296,6 +343,45 @@ func (cg *Coingecko) CurrentTickers() (*common.CurrencyRatesTicker, error) { return &newTickers, nil } +func (cg *Coingecko) getHighGranularityTickers(days string) (*[]common.CurrencyRatesTicker, error) { + mc, err := cg.coinMarketChart(cg.coin, highGranularityVsCurrency, days, false) + if err != nil { + return nil, err + } + if len(mc.Prices) < 2 { + return nil, nil + } + // ignore the last point, it is not in granularity + tickers := make([]common.CurrencyRatesTicker, len(mc.Prices)-1) + for i, p := range mc.Prices[:len(mc.Prices)-1] { + var timestamp uint + timestamp = uint(p[0]) + if timestamp > 100000000000 { + // convert timestamp from milliseconds to seconds + timestamp /= 1000 + } + rate := float32(p[1]) + u := time.Unix(int64(timestamp), 0).UTC() + ticker := common.CurrencyRatesTicker{ + Timestamp: u, + Rates: make(map[string]float32), + } + ticker.Rates[highGranularityVsCurrency] = rate + tickers[i] = ticker + } + return &tickers, nil +} + +// HourlyTickers returns the array of the exchange rates in hourly granularity +func (cg *Coingecko) HourlyTickers() (*[]common.CurrencyRatesTicker, error) { + return cg.getHighGranularityTickers("90") +} + +// FiveMinutesTickers returns the array of the exchange rates in five minutes granularity +func (cg *Coingecko) FiveMinutesTickers() (*[]common.CurrencyRatesTicker, error) { + return cg.getHighGranularityTickers("1") +} + func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*common.CurrencyRatesTicker, coinId string, vsCurrency string, token string) (bool, error) { lastTicker, err := cg.db.FiatRatesFindLastTicker(vsCurrency, token) if err != nil { @@ -312,7 +398,7 @@ func (cg *Coingecko) getHistoricalTicker(tickersToUpdate map[uint]*common.Curren } days = strconv.Itoa(d) } - mc, err := cg.coinMarketChart(coinId, vsCurrency, days) + mc, err := cg.coinMarketChart(coinId, vsCurrency, days, true) if err != nil { return false, err } diff --git a/fiat/fiat_rates.go b/fiat/fiat_rates.go index 4719bece01..8a2d4bd464 100644 --- a/fiat/fiat_rates.go +++ b/fiat/fiat_rates.go @@ -6,6 +6,7 @@ import ( "fmt" "math/rand" "strings" + "sync" "time" "github.com/golang/glog" @@ -13,29 +14,66 @@ import ( "github.com/trezor/blockbook/db" ) +const currentTickersKey = "CurrentTickers" +const hourlyTickersKey = "HourlyTickers" +const fiveMinutesTickersKey = "FiveMinutesTickers" + +const highGranularityVsCurrency = "usd" + +const secondsInDay = 24 * 60 * 60 +const secondsInHour = 60 * 60 +const secondsInFiveMinutes = 5 * 60 + // OnNewFiatRatesTicker is used to send notification about a new FiatRates ticker type OnNewFiatRatesTicker func(ticker *common.CurrencyRatesTicker) -// RatesDownloaderInterface provides method signatures for specific fiat rates downloaders +// RatesDownloaderInterface provides method signatures for a specific fiat rates downloader type RatesDownloaderInterface interface { CurrentTickers() (*common.CurrencyRatesTicker, error) + HourlyTickers() (*[]common.CurrencyRatesTicker, error) + FiveMinutesTickers() (*[]common.CurrencyRatesTicker, error) UpdateHistoricalTickers() error UpdateHistoricalTokenTickers() error } -// RatesDownloader stores FiatRates API parameters -type RatesDownloader struct { - periodSeconds int64 - db *db.RocksDB - timeFormat string - callbackOnNewTicker OnNewFiatRatesTicker - downloader RatesDownloaderInterface - downloadTokens bool +// FiatRates is used to fetch and refresh fiat rates +type FiatRates struct { + Enabled bool + periodSeconds int64 + db *db.RocksDB + timeFormat string + callbackOnNewTicker OnNewFiatRatesTicker + downloader RatesDownloaderInterface + downloadTokens bool + provider string + allowedVsCurrencies string + mux sync.RWMutex + currentTicker *common.CurrencyRatesTicker + hourlyTickers map[int64]*common.CurrencyRatesTicker + hourlyTickersFrom int64 + hourlyTickersTo int64 + fiveMinutesTickers map[int64]*common.CurrencyRatesTicker + fiveMinutesTickersFrom int64 + fiveMinutesTickersTo int64 + dailyTickers map[int64]*common.CurrencyRatesTicker + dailyTickersFrom int64 + dailyTickersTo int64 } -// NewFiatRatesDownloader initializes the downloader for FiatRates API. -func NewFiatRatesDownloader(db *db.RocksDB, apiType string, params string, allowedVsCurrencies string, callback OnNewFiatRatesTicker) (*RatesDownloader, error) { - var rd = &RatesDownloader{} +// NewFiatRates initializes the FiatRates handler +func NewFiatRates(db *db.RocksDB, config *common.Config, metrics *common.Metrics, callback OnNewFiatRatesTicker) (*FiatRates, error) { + + var fr = &FiatRates{ + provider: config.FiatRates, + allowedVsCurrencies: config.FiatRatesVsCurrencies, + } + + if config.FiatRates == "" || config.FiatRatesParams == "" { + glog.Infof("FiatRates config is empty, not downloading fiat rates") + fr.Enabled = false + return fr, nil + } + type fiatRatesParams struct { URL string `json:"url"` Coin string `json:"coin"` @@ -44,98 +82,397 @@ func NewFiatRatesDownloader(db *db.RocksDB, apiType string, params string, allow PeriodSeconds int64 `json:"periodSeconds"` } rdParams := &fiatRatesParams{} - err := json.Unmarshal([]byte(params), &rdParams) + err := json.Unmarshal([]byte(config.FiatRatesParams), &rdParams) if err != nil { return nil, err } - if rdParams.URL == "" || rdParams.PeriodSeconds == 0 { - return nil, errors.New("Missing parameters") + if rdParams.PeriodSeconds == 0 { + return nil, errors.New("missing parameters") } - rd.timeFormat = "02-01-2006" // Layout string for FiatRates date formatting (DD-MM-YYYY) - rd.periodSeconds = rdParams.PeriodSeconds // Time period for syncing the latest market data - if rd.periodSeconds < 60 { // minimum is one minute - rd.periodSeconds = 60 + fr.timeFormat = "02-01-2006" // Layout string for FiatRates date formatting (DD-MM-YYYY) + fr.periodSeconds = rdParams.PeriodSeconds // Time period for syncing the latest market data + if fr.periodSeconds < 60 { // minimum is one minute + fr.periodSeconds = 60 } - rd.db = db - rd.callbackOnNewTicker = callback - rd.downloadTokens = rdParams.PlatformIdentifier != "" && rdParams.PlatformVsCurrency != "" - if rd.downloadTokens { + fr.db = db + fr.callbackOnNewTicker = callback + fr.downloadTokens = rdParams.PlatformIdentifier != "" && rdParams.PlatformVsCurrency != "" + if fr.downloadTokens { common.TickerRecalculateTokenRate = strings.ToLower(db.GetInternalState().CoinShortcut) != rdParams.PlatformVsCurrency common.TickerTokenVsCurrency = rdParams.PlatformVsCurrency } - is := rd.db.GetInternalState() - if apiType == "coingecko" { + is := fr.db.GetInternalState() + if fr.provider == "coingecko" { throttle := true if callback == nil { // a small hack - in tests the callback is not used, therefore there is no delay slowing down the test throttle = false } - rd.downloader = NewCoinGeckoDownloader(db, rdParams.URL, rdParams.Coin, rdParams.PlatformIdentifier, rdParams.PlatformVsCurrency, allowedVsCurrencies, rd.timeFormat, throttle) + fr.downloader = NewCoinGeckoDownloader(db, db.GetInternalState().GetNetwork(), rdParams.URL, rdParams.Coin, rdParams.PlatformIdentifier, rdParams.PlatformVsCurrency, fr.allowedVsCurrencies, fr.timeFormat, metrics, throttle) if is != nil { is.HasFiatRates = true - is.HasTokenFiatRates = rd.downloadTokens - } + is.HasTokenFiatRates = fr.downloadTokens + fr.Enabled = true + + if err := fr.loadDailyTickers(); err != nil { + return nil, err + } + currentTickers, err := db.FiatRatesGetSpecialTickers(currentTickersKey) + if err != nil { + glog.Error("FiatRatesDownloader: get CurrentTickers from DB error ", err) + } + if currentTickers != nil && len(*currentTickers) > 0 { + fr.currentTicker = &(*currentTickers)[0] + } + + hourlyTickers, err := db.FiatRatesGetSpecialTickers(hourlyTickersKey) + if err != nil { + glog.Error("FiatRatesDownloader: get HourlyTickers from DB error ", err) + } + fr.hourlyTickers, fr.hourlyTickersFrom, fr.hourlyTickersTo = fr.tickersToMap(hourlyTickers, secondsInHour) + + fiveMinutesTickers, err := db.FiatRatesGetSpecialTickers(fiveMinutesTickersKey) + if err != nil { + glog.Error("FiatRatesDownloader: get FiveMinutesTickers from DB error ", err) + } + fr.fiveMinutesTickers, fr.fiveMinutesTickersFrom, fr.fiveMinutesTickersTo = fr.tickersToMap(fiveMinutesTickers, secondsInFiveMinutes) + + } } else { - return nil, fmt.Errorf("NewFiatRatesDownloader: incorrect API type %q", apiType) + return nil, fmt.Errorf("unknown provider %q", fr.provider) + } + fr.logTickersInfo() + return fr, nil +} + +// GetCurrentTicker returns current ticker +func (fr *FiatRates) GetCurrentTicker(vsCurrency string, token string) *common.CurrencyRatesTicker { + fr.mux.RLock() + currentTicker := fr.currentTicker + fr.mux.RUnlock() + if currentTicker != nil && common.IsSuitableTicker(currentTicker, vsCurrency, token) { + return currentTicker + } + return nil +} + +// getTokenTickersForTimestamps returns tickers for slice of timestamps, that contain requested vsCurrency and token +func (fr *FiatRates) getTokenTickersForTimestamps(timestamps []int64, vsCurrency string, token string) (*[]*common.CurrencyRatesTicker, error) { + currentTicker := fr.GetCurrentTicker("", token) + tickers := make([]*common.CurrencyRatesTicker, len(timestamps)) + var prevTicker *common.CurrencyRatesTicker + var prevTs int64 + var err error + for i, t := range timestamps { + // check if the token is available in the current ticker - if not, return nil ticker instead of wasting time in costly DB searches + if currentTicker != nil { + var ticker *common.CurrencyRatesTicker + date := time.Unix(t, 0) + // if previously found ticker is newer than this one (token tickers may not be in DB for every day), skip search in DB + if prevTicker != nil && t >= prevTs && !date.After(prevTicker.Timestamp) { + ticker = prevTicker + prevTs = t + } else { + ticker, err = fr.db.FiatRatesFindTicker(&date, vsCurrency, token) + if err != nil { + return nil, err + } + prevTicker = ticker + prevTs = t + } + // if ticker not found in DB, use current ticker + if ticker == nil { + tickers[i] = currentTicker + prevTicker = currentTicker + prevTs = t + } else { + tickers[i] = ticker + } + } + } + return &tickers, nil +} + +// GetTickersForTimestamps returns tickers for slice of timestamps, that contain requested vsCurrency and token +func (fr *FiatRates) GetTickersForTimestamps(timestamps []int64, vsCurrency string, token string) (*[]*common.CurrencyRatesTicker, error) { + if !fr.Enabled { + return nil, nil + } + // token rates are not in memory, them load from DB + if token != "" { + return fr.getTokenTickersForTimestamps(timestamps, vsCurrency, token) + } + fr.mux.RLock() + defer fr.mux.RUnlock() + tickers := make([]*common.CurrencyRatesTicker, len(timestamps)) + var prevTicker *common.CurrencyRatesTicker + var prevTs int64 + for i, t := range timestamps { + dailyTs := ceilUnix(t, secondsInDay) + // use higher granularity only for non daily timestamps + if t != dailyTs { + if t >= fr.fiveMinutesTickersFrom && t <= fr.fiveMinutesTickersTo { + if ticker, found := fr.fiveMinutesTickers[ceilUnix(t, secondsInFiveMinutes)]; found && ticker != nil { + if common.IsSuitableTicker(ticker, vsCurrency, token) { + tickers[i] = ticker + continue + } + } + } + if t >= fr.hourlyTickersFrom && t <= fr.hourlyTickersTo { + if ticker, found := fr.hourlyTickers[ceilUnix(t, secondsInHour)]; found && ticker != nil { + if common.IsSuitableTicker(ticker, vsCurrency, token) { + tickers[i] = ticker + continue + } + } + } + } + if prevTicker != nil && t >= prevTs && t <= prevTicker.Timestamp.Unix() { + tickers[i] = prevTicker + continue + } else { + var found bool + if dailyTs < fr.dailyTickersFrom { + dailyTs = fr.dailyTickersFrom + } + var ticker *common.CurrencyRatesTicker + for ; dailyTs <= fr.dailyTickersTo; dailyTs += secondsInDay { + if ticker, found = fr.dailyTickers[dailyTs]; found && ticker != nil { + if common.IsSuitableTicker(ticker, vsCurrency, token) { + tickers[i] = ticker + prevTicker = ticker + prevTs = t + break + } else { + found = false + } + } + } + if !found { + tickers[i] = fr.currentTicker + prevTicker = fr.currentTicker + prevTs = t + } + } + } + return &tickers, nil +} +func (fr *FiatRates) logTickersInfo() { + glog.Infof("fiat rates %s handler, %d (%s - %s) daily tickers, %d (%s - %s) hourly tickers, %d (%s - %s) 5 minute tickers", fr.provider, + len(fr.dailyTickers), time.Unix(fr.dailyTickersFrom, 0).Format("2006-01-02"), time.Unix(fr.dailyTickersTo, 0).Format("2006-01-02"), + len(fr.hourlyTickers), time.Unix(fr.hourlyTickersFrom, 0).Format("2006-01-02 15:04"), time.Unix(fr.hourlyTickersTo, 0).Format("2006-01-02 15:04"), + len(fr.fiveMinutesTickers), time.Unix(fr.fiveMinutesTickersFrom, 0).Format("2006-01-02 15:04"), time.Unix(fr.fiveMinutesTickersTo, 0).Format("2006-01-02 15:04")) +} + +func roundTimeUnix(t time.Time, granularity int64) int64 { + return roundUnix(t.UTC().Unix(), granularity) +} + +func roundUnix(t int64, granularity int64) int64 { + unix := t + (granularity >> 1) + return unix - unix%granularity +} + +func ceilUnix(t int64, granularity int64) int64 { + unix := t + (granularity - 1) + return unix - unix%granularity +} + +// loadDailyTickers loads daily tickers to cache +func (fr *FiatRates) loadDailyTickers() error { + fr.mux.Lock() + defer fr.mux.Unlock() + fr.dailyTickers = make(map[int64]*common.CurrencyRatesTicker) + err := fr.db.FiatRatesGetAllTickers(func(ticker *common.CurrencyRatesTicker) error { + normalizedTime := roundTimeUnix(ticker.Timestamp, secondsInDay) + if normalizedTime == fr.dailyTickersFrom { + // there are multiple tickers on the first day, use only the first one + return nil + } + // remove token rates from cache to save memory (tickers with token rates are hundreds of kb big) + ticker.TokenRates = nil + if len(fr.dailyTickers) > 0 { + // check that there is a ticker for every day, if missing, set it from current value if missing + prevTime := normalizedTime + for { + prevTime -= secondsInDay + if _, found := fr.dailyTickers[prevTime]; found { + break + } + fr.dailyTickers[prevTime] = ticker + } + } else { + fr.dailyTickersFrom = normalizedTime + } + fr.dailyTickers[normalizedTime] = ticker + fr.dailyTickersTo = normalizedTime + return nil + }) + return err +} + +// setCurrentTicker sets current ticker +func (fr *FiatRates) setCurrentTicker(t *common.CurrencyRatesTicker) { + fr.mux.Lock() + defer fr.mux.Unlock() + fr.currentTicker = t + fr.db.FiatRatesStoreSpecialTickers(currentTickersKey, &[]common.CurrencyRatesTicker{*t}) +} + +func (fr *FiatRates) tickersToMap(tickers *[]common.CurrencyRatesTicker, granularitySeconds int64) (map[int64]*common.CurrencyRatesTicker, int64, int64) { + if tickers == nil || len(*tickers) == 0 { + return make(map[int64]*common.CurrencyRatesTicker), 0, 0 + } + m := make(map[int64]*common.CurrencyRatesTicker, len(*tickers)) + from := int64(0) + to := int64(0) + for i := range *tickers { + ticker := (*tickers)[i] + normalizedTime := roundTimeUnix(ticker.Timestamp, granularitySeconds) + dailyTime := roundTimeUnix(ticker.Timestamp, secondsInDay) + dailyTicker, found := fr.dailyTickers[dailyTime] + if !found { + // if not found in historical tickers, use current ticker + dailyTicker = fr.currentTicker + } + if dailyTicker != nil { + // high granularity tickers are loaded only in one currency, add other currencies based on daily rate between fiat currencies + vsRate, foundVs := ticker.Rates[highGranularityVsCurrency] + dailyVsRate, foundDaily := dailyTicker.Rates[highGranularityVsCurrency] + if foundDaily && dailyVsRate != 0 && foundVs && vsRate != 0 { + for currency, rate := range dailyTicker.Rates { + if currency != highGranularityVsCurrency { + ticker.Rates[currency] = vsRate * rate / dailyVsRate + } + } + } + } + if len(m) > 0 { + if normalizedTime == from { + // there are multiple normalized tickers for the first entry, skip + continue + } + // check that there is a ticker for each period, set it from current value if missing + prevTime := normalizedTime + for { + prevTime -= granularitySeconds + if _, found := m[prevTime]; found { + break + } + m[prevTime] = &ticker + } + } else { + from = normalizedTime + } + m[normalizedTime] = &ticker + to = normalizedTime } - return rd, nil + return m, from, to +} + +// setHourlyTickers sets hourly tickers +func (fr *FiatRates) setHourlyTickers(t *[]common.CurrencyRatesTicker) { + fr.db.FiatRatesStoreSpecialTickers(hourlyTickersKey, t) + fr.mux.Lock() + defer fr.mux.Unlock() + fr.hourlyTickers, fr.hourlyTickersFrom, fr.hourlyTickersTo = fr.tickersToMap(t, secondsInHour) } -// Run periodically downloads current (every 15 minutes) and historical (once a day) tickers -func (rd *RatesDownloader) Run() error { +// setFiveMinutesTickers sets five minutes tickers +func (fr *FiatRates) setFiveMinutesTickers(t *[]common.CurrencyRatesTicker) { + fr.db.FiatRatesStoreSpecialTickers(fiveMinutesTickersKey, t) + fr.mux.Lock() + defer fr.mux.Unlock() + fr.fiveMinutesTickers, fr.fiveMinutesTickersFrom, fr.fiveMinutesTickersTo = fr.tickersToMap(t, secondsInFiveMinutes) +} + +// RunDownloader periodically downloads current (every 15 minutes) and historical (once a day) tickers +func (fr *FiatRates) RunDownloader() error { + glog.Infof("Starting %v FiatRates downloader...", fr.provider) var lastHistoricalTickers time.Time - is := rd.db.GetInternalState() - tickerFromIs := is.GetCurrentTicker("", "") + is := fr.db.GetInternalState() + tickerFromIs := fr.GetCurrentTicker("", "") firstRun := true for { unix := time.Now().Unix() - next := unix + rd.periodSeconds - next -= next % rd.periodSeconds + next := unix + fr.periodSeconds + next -= next % fr.periodSeconds // skip waiting for the period for the first run if there are no tickerFromIs or they are too old - if !firstRun || (tickerFromIs != nil && next-tickerFromIs.Timestamp.Unix() < rd.periodSeconds) { + if !firstRun || (tickerFromIs != nil && next-tickerFromIs.Timestamp.Unix() < fr.periodSeconds) { // wait for the next run with a slight random value to avoid too many request at the same time - next += int64(rand.Intn(12)) + next += int64(rand.Intn(3)) time.Sleep(time.Duration(next-unix) * time.Second) } firstRun = false - tickers, err := rd.downloader.CurrentTickers() - if err != nil || tickers == nil { + + // load current tickers + currentTicker, err := fr.downloader.CurrentTickers() + if err != nil || currentTicker == nil { glog.Error("FiatRatesDownloader: CurrentTickers error ", err) } else { - is.SetCurrentTicker(tickers) + fr.setCurrentTicker(currentTicker) glog.Info("FiatRatesDownloader: CurrentTickers updated") - if rd.callbackOnNewTicker != nil { - rd.callbackOnNewTicker(tickers) + if fr.callbackOnNewTicker != nil { + fr.callbackOnNewTicker(currentTicker) } } - now := time.Now().UTC() + + // load hourly tickers, it is necessary to wait about 1 hour to prepare the tickers + if time.Now().UTC().Unix() >= fr.hourlyTickersTo+secondsInHour+secondsInHour { + hourlyTickers, err := fr.downloader.HourlyTickers() + if err != nil || hourlyTickers == nil { + glog.Error("FiatRatesDownloader: HourlyTickers error ", err) + } else { + fr.setHourlyTickers(hourlyTickers) + glog.Info("FiatRatesDownloader: HourlyTickers updated") + } + } + + // load five minute tickers, it is necessary to wait about 10 minutes to prepare the tickers + if time.Now().UTC().Unix() >= fr.fiveMinutesTickersTo+3*secondsInFiveMinutes { + fiveMinutesTickers, err := fr.downloader.FiveMinutesTickers() + if err != nil || fiveMinutesTickers == nil { + glog.Error("FiatRatesDownloader: FiveMinutesTickers error ", err) + } else { + fr.setFiveMinutesTickers(fiveMinutesTickers) + glog.Info("FiatRatesDownloader: FiveMinutesTickers updated") + } + } + // once a day, 1 hour after UTC midnight (to let the provider prepare historical rates) update historical tickers + now := time.Now().UTC() if (now.YearDay() != lastHistoricalTickers.YearDay() || now.Year() != lastHistoricalTickers.Year()) && now.Hour() > 0 { - err = rd.downloader.UpdateHistoricalTickers() + err = fr.downloader.UpdateHistoricalTickers() if err != nil { glog.Error("FiatRatesDownloader: UpdateHistoricalTickers error ", err) } else { lastHistoricalTickers = time.Now().UTC() - ticker, err := rd.db.FiatRatesFindLastTicker("", "") - if err != nil || ticker == nil { - glog.Error("FiatRatesDownloader: FiatRatesFindLastTicker error ", err) + if err = fr.loadDailyTickers(); err != nil { + glog.Error("FiatRatesDownloader: loadDailyTickers error ", err) } else { - glog.Infof("FiatRatesDownloader: UpdateHistoricalTickers finished, last ticker from %v", ticker.Timestamp) - if is != nil { - is.HistoricalFiatRatesTime = ticker.Timestamp + ticker, found := fr.dailyTickers[fr.dailyTickersTo] + if !found || ticker == nil { + glog.Error("FiatRatesDownloader: dailyTickers not loaded") + } else { + glog.Infof("FiatRatesDownloader: UpdateHistoricalTickers finished, last ticker from %v", ticker.Timestamp) + fr.logTickersInfo() + if is != nil { + is.HistoricalFiatRatesTime = ticker.Timestamp + } } } - if rd.downloadTokens { + if fr.downloadTokens { // UpdateHistoricalTokenTickers in a goroutine, it can take quite some time as there are many tokens go func() { - err := rd.downloader.UpdateHistoricalTokenTickers() + err := fr.downloader.UpdateHistoricalTokenTickers() if err != nil { glog.Error("FiatRatesDownloader: UpdateHistoricalTokenTickers error ", err) } else { glog.Info("FiatRatesDownloader: UpdateHistoricalTokenTickers finished") if is != nil { - is.HistoricalTokenFiatRatesTime = time.Now() + is.HistoricalTokenFiatRatesTime = time.Now().UTC() } } }() diff --git a/fiat/fiat_rates_test.go b/fiat/fiat_rates_test.go index 417c960be1..fad58acfca 100644 --- a/fiat/fiat_rates_test.go +++ b/fiat/fiat_rates_test.go @@ -3,9 +3,8 @@ package fiat import ( - "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "net/http/httptest" "os" @@ -31,16 +30,16 @@ func TestMain(m *testing.M) { os.Exit(c) } -func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *common.InternalState, string) { - tmp, err := ioutil.TempDir("", "testdb") +func setupRocksDB(t *testing.T, parser bchain.BlockChainParser, config *common.Config) (*db.RocksDB, *common.InternalState, string) { + tmp, err := os.MkdirTemp("", "testdb") if err != nil { t.Fatal(err) } - d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil) + d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil, false) if err != nil { t.Fatal(err) } - is, err := d.LoadInternalState("fakecoin") + is, err := d.LoadInternalState(config) if err != nil { t.Fatal(err) } @@ -75,7 +74,7 @@ func getFiatRatesMockData(name string) (string, error) { glog.Errorf("Cannot open file %v", filename) return "", err } - b, err := ioutil.ReadAll(mockFile) + b, err := io.ReadAll(mockFile) if err != nil { glog.Errorf("Cannot read file %v", filename) return "", err @@ -84,11 +83,6 @@ func getFiatRatesMockData(name string) (string, error) { } func TestFiatRates(t *testing.T) { - d, _, tmp := setupRocksDB(t, &testBitcoinParser{ - BitcoinParser: bitcoinTestnetParser(), - }) - defer closeAndDestroyRocksDB(t, d, tmp) - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var err error var mockData string @@ -130,168 +124,160 @@ func TestFiatRates(t *testing.T) { })) defer mockServer.Close() - // mocked CoinGecko API - configJSON := `{"fiat_rates": "coingecko", "fiat_rates_params": "{\"url\": \"` + mockServer.URL + `\", \"coin\": \"ethereum\",\"platformIdentifier\":\"ethereum\",\"platformVsCurrency\": \"eth\",\"periodSeconds\": 60}"}` - - type fiatRatesConfig struct { - FiatRates string `json:"fiat_rates"` - FiatRatesParams string `json:"fiat_rates_params"` + // config with mocked CoinGecko API + config := common.Config{ + CoinName: "fakecoin", + FiatRates: "coingecko", + FiatRatesParams: `{"url": "` + mockServer.URL + `", "coin": "ethereum","platformIdentifier": "ethereum","platformVsCurrency": "eth","periodSeconds": 60}`, } - var config fiatRatesConfig - err := json.Unmarshal([]byte(configJSON), &config) + d, _, tmp := setupRocksDB(t, &testBitcoinParser{ + BitcoinParser: bitcoinTestnetParser(), + }, &config) + defer closeAndDestroyRocksDB(t, d, tmp) + + fiatRates, err := NewFiatRates(d, &config, nil, nil) if err != nil { - t.Fatalf("Error parsing config: %v", err) + t.Fatalf("FiatRates init error: %v", err) } - if config.FiatRates == "" || config.FiatRatesParams == "" { - t.Fatalf("Error parsing FiatRates config - empty parameter") + // get current tickers + currentTickers, err := fiatRates.downloader.CurrentTickers() + if err != nil { + t.Fatalf("Error in CurrentTickers: %v", err) return } - fiatRates, err := NewFiatRatesDownloader(d, config.FiatRates, config.FiatRatesParams, "", nil) - if err != nil { - t.Fatalf("FiatRates init error: %v", err) + if currentTickers == nil { + t.Fatalf("CurrentTickers returned nil value") + return } - if config.FiatRates == "coingecko" { - // get current tickers - currentTickers, err := fiatRates.downloader.CurrentTickers() - if err != nil { - t.Fatalf("Error in CurrentTickers: %v", err) - return - } - if currentTickers == nil { - t.Fatalf("CurrentTickers returned nil value") - return - } - - wantCurrentTickers := common.CurrencyRatesTicker{ - Rates: map[string]float32{ - "aed": 8447.1, - "ars": 268901, - "aud": 3314.36, - "btc": 0.07531005, - "eth": 1, - "eur": 2182.99, - "ltc": 29.097696, - "usd": 2299.72, - }, - TokenRates: map[string]float32{ - "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 5.58195e-07, - "0x906710835d1ae85275eb770f06873340ca54274b": 1.39852e-10, - }, - Timestamp: currentTickers.Timestamp, - } - if !reflect.DeepEqual(currentTickers, &wantCurrentTickers) { - t.Fatalf("CurrentTickers() = %v, want %v", *currentTickers, wantCurrentTickers) - } + wantCurrentTickers := common.CurrencyRatesTicker{ + Rates: map[string]float32{ + "aed": 8447.1, + "ars": 268901, + "aud": 3314.36, + "btc": 0.07531005, + "eth": 1, + "eur": 2182.99, + "ltc": 29.097696, + "usd": 2299.72, + }, + TokenRates: map[string]float32{ + "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 5.58195e-07, + "0x906710835d1ae85275eb770f06873340ca54274b": 1.39852e-10, + }, + Timestamp: currentTickers.Timestamp, + } + if !reflect.DeepEqual(currentTickers, &wantCurrentTickers) { + t.Fatalf("CurrentTickers() = %v, want %v", *currentTickers, wantCurrentTickers) + } - ticker, err := fiatRates.db.FiatRatesFindLastTicker("usd", "") - if err != nil { - t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) - } - if ticker != nil { - t.Fatalf("FiatRatesFindLastTicker found unexpected data") - } + ticker, err := fiatRates.db.FiatRatesFindLastTicker("usd", "") + if err != nil { + t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) + } + if ticker != nil { + t.Fatalf("FiatRatesFindLastTicker found unexpected data") + } - // update historical tickers for the first time - err = fiatRates.downloader.UpdateHistoricalTickers() - if err != nil { - t.Fatalf("UpdateHistoricalTickers 1st pass failed with error: %v", err) - } - err = fiatRates.downloader.UpdateHistoricalTokenTickers() - if err != nil { - t.Fatalf("UpdateHistoricalTokenTickers 1st pass failed with error: %v", err) - } + // update historical tickers for the first time + err = fiatRates.downloader.UpdateHistoricalTickers() + if err != nil { + t.Fatalf("UpdateHistoricalTickers 1st pass failed with error: %v", err) + } + err = fiatRates.downloader.UpdateHistoricalTokenTickers() + if err != nil { + t.Fatalf("UpdateHistoricalTokenTickers 1st pass failed with error: %v", err) + } - ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "") - if err != nil || ticker == nil { - t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) - } - wantTicker := common.CurrencyRatesTicker{ - Rates: map[string]float32{ - "aed": 241272.48, - "ars": 241272.48, - "aud": 241272.48, - "btc": 241272.48, - "eth": 241272.48, - "eur": 241272.48, - "ltc": 241272.48, - "usd": 1794.5397, - }, - TokenRates: map[string]float32{ - "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.161734e+07, - "0x906710835d1ae85275eb770f06873340ca54274b": 4.161734e+07, - }, - Timestamp: time.Unix(1654732800, 0).UTC(), - } - if !reflect.DeepEqual(ticker, &wantTicker) { - t.Fatalf("UpdateHistoricalTickers(usd) 1st pass = %v, want %v", *ticker, wantTicker) - } + ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "") + if err != nil || ticker == nil { + t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) + } + wantTicker := common.CurrencyRatesTicker{ + Rates: map[string]float32{ + "aed": 241272.48, + "ars": 241272.48, + "aud": 241272.48, + "btc": 241272.48, + "eth": 241272.48, + "eur": 241272.48, + "ltc": 241272.48, + "usd": 1794.5397, + }, + TokenRates: map[string]float32{ + "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.161734e+07, + "0x906710835d1ae85275eb770f06873340ca54274b": 4.161734e+07, + }, + Timestamp: time.Unix(1654732800, 0).UTC(), + } + if !reflect.DeepEqual(ticker, &wantTicker) { + t.Fatalf("UpdateHistoricalTickers(usd) 1st pass = %v, want %v", *ticker, wantTicker) + } - ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "") - if err != nil || ticker == nil { - t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) - } - wantTicker = common.CurrencyRatesTicker{ - Rates: map[string]float32{ - "aed": 240402.97, - "ars": 240402.97, - "aud": 240402.97, - "btc": 240402.97, - "eth": 240402.97, - "eur": 240402.97, - "ltc": 240402.97, - }, - TokenRates: map[string]float32{ - "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07, - "0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07, - }, - Timestamp: time.Unix(1654819200, 0).UTC(), - } - if !reflect.DeepEqual(ticker, &wantTicker) { - t.Fatalf("UpdateHistoricalTickers(eur) 1st pass = %v, want %v", *ticker, wantTicker) - } + ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "") + if err != nil || ticker == nil { + t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) + } + wantTicker = common.CurrencyRatesTicker{ + Rates: map[string]float32{ + "aed": 240402.97, + "ars": 240402.97, + "aud": 240402.97, + "btc": 240402.97, + "eth": 240402.97, + "eur": 240402.97, + "ltc": 240402.97, + }, + TokenRates: map[string]float32{ + "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07, + "0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07, + }, + Timestamp: time.Unix(1654819200, 0).UTC(), + } + if !reflect.DeepEqual(ticker, &wantTicker) { + t.Fatalf("UpdateHistoricalTickers(eur) 1st pass = %v, want %v", *ticker, wantTicker) + } - // update historical tickers for the second time - err = fiatRates.downloader.UpdateHistoricalTickers() - if err != nil { - t.Fatalf("UpdateHistoricalTickers 2nd pass failed with error: %v", err) - } - err = fiatRates.downloader.UpdateHistoricalTokenTickers() - if err != nil { - t.Fatalf("UpdateHistoricalTokenTickers 2nd pass failed with error: %v", err) - } - ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "") - if err != nil || ticker == nil { - t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) - } - wantTicker = common.CurrencyRatesTicker{ - Rates: map[string]float32{ - "aed": 240402.97, - "ars": 240402.97, - "aud": 240402.97, - "btc": 240402.97, - "eth": 240402.97, - "eur": 240402.97, - "ltc": 240402.97, - "usd": 1788.4183, - }, - TokenRates: map[string]float32{ - "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07, - "0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07, - }, - Timestamp: time.Unix(1654819200, 0).UTC(), - } - if !reflect.DeepEqual(ticker, &wantTicker) { - t.Fatalf("UpdateHistoricalTickers(usd) 2nd pass = %v, want %v", *ticker, wantTicker) - } - ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "") - if err != nil || ticker == nil { - t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) - } - if !reflect.DeepEqual(ticker, &wantTicker) { - t.Fatalf("UpdateHistoricalTickers(eur) 2nd pass = %v, want %v", *ticker, wantTicker) - } + // update historical tickers for the second time + err = fiatRates.downloader.UpdateHistoricalTickers() + if err != nil { + t.Fatalf("UpdateHistoricalTickers 2nd pass failed with error: %v", err) + } + err = fiatRates.downloader.UpdateHistoricalTokenTickers() + if err != nil { + t.Fatalf("UpdateHistoricalTokenTickers 2nd pass failed with error: %v", err) + } + ticker, err = fiatRates.db.FiatRatesFindLastTicker("usd", "") + if err != nil || ticker == nil { + t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) + } + wantTicker = common.CurrencyRatesTicker{ + Rates: map[string]float32{ + "aed": 240402.97, + "ars": 240402.97, + "aud": 240402.97, + "btc": 240402.97, + "eth": 240402.97, + "eur": 240402.97, + "ltc": 240402.97, + "usd": 1788.4183, + }, + TokenRates: map[string]float32{ + "0x5e9997684d061269564f94e5d11ba6ce6fa9528c": 4.1464476e+07, + "0x906710835d1ae85275eb770f06873340ca54274b": 4.1464476e+07, + }, + Timestamp: time.Unix(1654819200, 0).UTC(), + } + if !reflect.DeepEqual(ticker, &wantTicker) { + t.Fatalf("UpdateHistoricalTickers(usd) 2nd pass = %v, want %v", *ticker, wantTicker) + } + ticker, err = fiatRates.db.FiatRatesFindLastTicker("eur", "") + if err != nil || ticker == nil { + t.Fatalf("FiatRatesFindLastTicker failed with error: %v", err) + } + if !reflect.DeepEqual(ticker, &wantTicker) { + t.Fatalf("UpdateHistoricalTickers(eur) 2nd pass = %v, want %v", *ticker, wantTicker) } } diff --git a/go.mod b/go.mod index aa12c96fc2..0b44d03671 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,10 @@ module github.com/trezor/blockbook -go 1.19 +go 1.25.0 require ( - github.com/Groestlcoin/go-groestl-hash v0.0.0-20181012171753-790653ac190c // indirect - github.com/ava-labs/avalanchego v1.9.7 - github.com/ava-labs/coreth v0.11.6 + github.com/ava-labs/avalanchego v1.14.0 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 @@ -16,99 +13,75 @@ require ( 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.26 - github.com/golang/glog v1.0.0 - github.com/golang/protobuf v1.5.2 - github.com/gorilla/websocket v1.4.2 + github.com/ethereum/go-ethereum v1.16.7 + github.com/golang/glog v1.2.1 + 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/linxGnu/grocksdb v1.7.7 + github.com/linxGnu/grocksdb v1.9.8 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/pirk/ecashaddr-converter v0.0.0-20220121162910-c6cb45163b29 github.com/pirk/ecashutil v0.0.0-20220124103933-d37f548d249e - github.com/prometheus/client_golang v1.13.0 + github.com/prometheus/client_golang v1.23.2 github.com/schancel/cashaddr-converter v0.0.0-20181111022653-4769e7add95a - golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d - google.golang.org/protobuf v1.28.1 - gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect + github.com/tkrajina/typescriptify-golang-structs v0.1.11 + golang.org/x/crypto v0.43.0 + google.golang.org/protobuf v1.36.10 ) require ( - github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/Groestlcoin/go-groestl-hash v0.0.0-20181012171753-790653ac190c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/PiRK/cashaddr-converter v0.0.0-20220121162910-c6cb45163b29 // indirect - github.com/VictoriaMetrics/fastcache v1.10.0 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/aead/siphash v1.0.1 // indirect github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect - github.com/cenkalti/backoff/v4 v4.1.3 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dchest/siphash v1.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect + github.com/dchest/blake256 v1.0.0 // indirect + github.com/dchest/siphash v1.2.3 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/base58 v1.0.3 // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/crypto/blake256 v1.1.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/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/decred/dcrd/wire v1.4.0 // indirect github.com/decred/slog v1.1.0 // indirect - github.com/go-logr/logr v1.2.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-stack/stack v1.8.0 // indirect - github.com/golang/mock v1.6.0 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.2.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect + github.com/ethereum/c-kzg-4844 v1.0.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/gorilla/rpc v1.2.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.12.0 // indirect - github.com/holiman/uint256 v1.2.0 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect - github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - github.com/rjeczalik/notify v0.9.2 // indirect - github.com/rs/cors v1.7.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect + github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b // indirect + github.com/kkdai/bstream v0.0.0-20171226095907-f71540b9dfdc // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/stretchr/testify v1.8.1 // indirect - github.com/supranational/blst v0.3.11-0.20220920110316-f72618070295 // indirect - github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect - github.com/tklauser/go-sysconf v0.3.5 // indirect - github.com/tklauser/numcpus v0.2.2 // indirect - github.com/yusufpapurcu/wmi v1.2.2 // indirect - go.opentelemetry.io/otel v1.11.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.0 // indirect - go.opentelemetry.io/otel/sdk v1.11.0 // indirect - go.opentelemetry.io/otel/trace v1.11.0 // indirect - go.opentelemetry.io/proto/otlp v0.19.0 // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect - gonum.org/v1/gonum v0.11.0 // indirect - google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect - google.golang.org/grpc v1.50.1 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect - gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tkrajina/go-reflector v0.5.5 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect ) // replace github.com/martinboehm/btcutil => ../btcutil diff --git a/go.sum b/go.sum index 548d00ef43..c7e0c7d0d0 100644 --- a/go.sum +++ b/go.sum @@ -1,73 +1,28 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= +github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= 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/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/PiRK/cashaddr-converter v0.0.0-20220121162910-c6cb45163b29 h1:B11BryeZQ1LrAzzM0lCpblwleB7SyxPfvN2AsNbyvQc= github.com/PiRK/cashaddr-converter v0.0.0-20220121162910-c6cb45163b29/go.mod h1:+39XiGr9m9TPY49sG4XIH5CVaRxHGFWT0U4MOY6dy3o= -github.com/VictoriaMetrics/fastcache v1.10.0 h1:5hDJnLsKLpnUEToub7ETuRu8RCkb40woBZAUiKonXzY= -github.com/VictoriaMetrics/fastcache v1.10.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 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/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 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= -github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/ava-labs/avalanchego v1.9.7 h1:f2vS8jUBZmrqPcfU5NEa7dSHXbKfTB0EyjcCyvqxqPw= -github.com/ava-labs/avalanchego v1.9.7/go.mod h1:ckdSQHeoRN6PmQ3TLgWAe6Kh9tFpU4Lu6MgDW4GrU/Q= -github.com/ava-labs/coreth v0.11.6 h1:kMCHfb37k4UyxkHwoUuciXC92eyIeowB/EKv15XKQ6s= -github.com/ava-labs/coreth v0.11.6/go.mod h1:xgjjJdl50zhHlWPP+3Ux5LxfvFcbSG60tGK6QUkFDhI= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -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/ava-labs/avalanchego v1.14.0 h1:0j314N1fEwstKSymvyhvvxi8Hr752xc6MQvjq6kGIJY= +github.com/ava-labs/avalanchego v1.14.0/go.mod h1:7sYTcQknONY5x5qzS+GrN+UtyB8kX7Q5ClHhGj1DgXg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 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/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= -github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= 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= @@ -79,44 +34,55 @@ github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= -github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA= +github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= +github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= +github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/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/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= 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/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 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= @@ -125,8 +91,8 @@ github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1 h1:V6eqU1crZzuoFT4KG2LhaU5xDSdkHu 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/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 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= @@ -140,163 +106,81 @@ github.com/decred/dcrd/wire v1.4.0 h1:KmSo6eTQIvhXS0fLBQ/l7hG7QLcSJQKSwSyzSqJYDk 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/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ethereum/go-ethereum v1.10.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s= -github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= -github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-ethereum v1.15.5 h1:Fo2TbBWC61lWVkFw9tsMoHCNX1ndpuaQBRJ8H6xLUPo= +github.com/ethereum/go-ethereum v1.15.5/go.mod h1:1LG2LnMOx2yPRHR/S+xuipXH29vPr6BIH6GElD8N/fo= +github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= +github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/getsentry/sentry-go v0.35.0 h1:+FJNlnjJsZMG3g0/rmmP7GiKjQoUF5EXfEtBwtPtkzY= +github.com/getsentry/sentry-go v0.35.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -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/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/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/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.12.0 h1:kr3j8iIMR4ywO/O0rvksXaJvauGGCMg2zAZIiNZ9uIQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.12.0/go.mod h1:ummNFgdgLhhX7aIiy35vVmQNS0rWXknfPE0qe6fmFXg= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= -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/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= -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/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/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/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 h1:I1QApI4r4SG8Hh45H0yRjVnThWRn1oOwod76rrAe5KE= github.com/kkdai/bstream v0.0.0-20171226095907-f71540b9dfdc/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/linxGnu/grocksdb v1.7.7 h1:b6o8gagb4FL+P55qUzPchBR/C0u1lWjJOWQSWbhvTWg= -github.com/linxGnu/grocksdb v1.7.7/go.mod h1:0hTf+iA+GOr0jDX4CgIYyJZxqOH9XlBh6KVj8+zmF34= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/linxGnu/grocksdb v1.9.8 h1:vOIKv9/+HKiqJAElJIEYv3ZLcihRxyP7Suu/Mu8Dxjs= +github.com/linxGnu/grocksdb v1.9.8/go.mod h1:C3CNe9UYc9hlEM2pC82AqiGS3LRW537u9LFV4wIZuHk= 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= @@ -308,514 +192,136 @@ github.com/martinboehm/btcutil v0.0.0-20211010173611-6ef1889c1819 h1:ra2UymMEDhR 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/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= 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/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/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= -github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg= github.com/pebbe/zmq4 v1.2.1 h1:jrXQW3mD8Si2mcSY/8VBs2nNkK/sKCOEM0rHAfxyc8c= github.com/pebbe/zmq4 v1.2.1/go.mod h1:7N4y5R18zBiu3l0vajMUWQgZyjv464prE8RCyBcmnZM= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pirk/ecashaddr-converter v0.0.0-20220121162910-c6cb45163b29 h1:awILOeL107zIYvPB1zhkz6ZTp0AaMpLGMoV16DMairA= github.com/pirk/ecashaddr-converter v0.0.0-20220121162910-c6cb45163b29/go.mod h1:ATZjpmb9u55Kcrd5M/ca/40H73BZLhduMzCmGwpfWw0= github.com/pirk/ecashutil v0.0.0-20220124103933-d37f548d249e h1:WrnL52yXO0jNpHC7UbthJl9mnHPHY7bW3xzmWIuWzh8= github.com/pirk/ecashutil v0.0.0-20220124103933-d37f548d249e/go.mod h1:y/B3gomTdd1s23RvcBij/X738fcTobeupT30EhV6nPE= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= -github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= -github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/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/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969 h1:Oo2KZNP70KE0+IUJSidPj/BFS/RXNHmKIJOdckzml2E= 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/supranational/blst v0.3.11-0.20220920110316-f72618070295 h1:rVKS9JjtqE4/PscoIsP46sRnJhfq8YFbjlk0fUJTRnY= -github.com/supranational/blst v0.3.11-0.20220920110316-f72618070295/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= +github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= -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/tyler-smith/go-bip39 v1.0.2 h1:+t3w+KwLXO6154GNJY+qUtIxLTmFjfUmpguQT1OlOT8= -github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= -github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk= -go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.0 h1:0dly5et1i/6Th3WHn0M6kYiJfFNzhhxanrJ0bOfnjEo= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.0/go.mod h1:+Lq4/WkdCkjbGcBMVHHg2apTbv8oMBf29QCnyCCJjNQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.0 h1:eyJ6njZmH16h9dOKCi7lMswAnGsSOwgTqWzfxqcuNr8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.0/go.mod h1:FnDp7XemjN3oZ3xGunnfOUTVwd2XcvLbtRAuOSU3oc8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.0 h1:j2RFV0Qdt38XQ2Jvi4WIsQ56w8T7eSirYbMw19VXRDg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.0/go.mod h1:pILgiTEtrqvZpoiuGdblDgS5dbIaTgDrkIuKfEFkt+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.0 h1:v29I/NbVp7LXQYMFZhU6q17D0jSEbYOAVONlrO1oH5s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.0/go.mod h1:/RpLsmbQLDO1XCbWAM4S6TSwj8FKwwgyKKyqtvVfAnw= -go.opentelemetry.io/otel/sdk v1.11.0 h1:ZnKIL9V9Ztaq+ME43IUi/eo22mNsb6a7tGfzaOWB5fo= -go.opentelemetry.io/otel/sdk v1.11.0/go.mod h1:REusa8RsyKaq0OlyangWXaw97t2VogoO4SSEeKkSTAk= -go.opentelemetry.io/otel/trace v1.11.0 h1:20U/Vj42SX+mASlXLmSGBg6jpI1jQtv682lZtTAOVFI= -go.opentelemetry.io/otel/trace v1.11.0/go.mod h1:nyYjis9jy0gytE9LXGU+/m1sHTKbRY0fX0hulNNDP1U= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= +github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/tkrajina/typescriptify-golang-structs v0.1.11 h1:zEIVczF/iWgs4eTY7NQqbBe23OVlFVk9sWLX/FDYi4Q= +github.com/tkrajina/typescriptify-golang-structs v0.1.11/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-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-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 h1:rxKZ2gOnYxjfmakvUUqh9Gyb6KXfrj7JWTxORTYqb0E= -golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= +golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-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-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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.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/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= -gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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/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.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= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/server/html_templates.go b/server/html_templates.go new file mode 100644 index 0000000000..b9c9d62e3e --- /dev/null +++ b/server/html_templates.go @@ -0,0 +1,403 @@ +package server + +import ( + "encoding/json" + "fmt" + "html" + "html/template" + "math/big" + "net/http" + "runtime/debug" + "strconv" + "strings" + "time" + + "github.com/golang/glog" + "github.com/trezor/blockbook/api" + "github.com/trezor/blockbook/common" +) + +type tpl int + +const ( + noTpl = tpl(iota) + errorTpl + errorInternalTpl +) + +// htmlTemplateHandler is a handle to public http server +type htmlTemplates[TD any] struct { + metrics *common.Metrics + templates []*template.Template + debug bool + newTemplateData func(r *http.Request) *TD + newTemplateDataWithError func(error *api.APIError, r *http.Request) *TD + parseTemplates func() []*template.Template + postHtmlTemplateHandler func(data *TD, w http.ResponseWriter, r *http.Request) +} + +func (s *htmlTemplates[TD]) jsonHandler(handler func(r *http.Request, apiVersion int) (interface{}, error), apiVersion int) func(w http.ResponseWriter, r *http.Request) { + type jsonError struct { + 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(handlerName, " recovered from panic: ", e) + debug.PrintStack() + if s.debug { + data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError} + } else { + data = jsonError{"Internal server error", http.StatusInternalServerError} + } + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if e, isError := data.(jsonError); isError { + w.WriteHeader(e.HTTPStatus) + } + err = json.NewEncoder(w).Encode(data) + if err != nil { + glog.Warning("json encode ", err) + } + if s.metrics != nil { + s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec() + } + }() + if s.metrics != nil { + 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 { + if apiErr.Public { + data = jsonError{apiErr.Error(), http.StatusBadRequest} + } else { + data = jsonError{apiErr.Error(), http.StatusInternalServerError} + } + } else { + if err != nil { + glog.Error(handlerName, " error: ", err) + } + if s.debug { + if data != nil { + data = jsonError{fmt.Sprintf("Internal server error: %v, data %+v", err, data), http.StatusInternalServerError} + } else { + data = jsonError{fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError} + } + } else { + data = jsonError{"Internal server error", http.StatusInternalServerError} + } + } + } + } +} + +func (s *htmlTemplates[TD]) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TD, error)) func(w http.ResponseWriter, r *http.Request) { + handlerName := getFunctionName(handler) + return func(w http.ResponseWriter, r *http.Request) { + var t tpl + var data *TD + var err error + defer func() { + if e := recover(); e != nil { + glog.Error(handlerName, " recovered from panic: ", e) + debug.PrintStack() + t = errorInternalTpl + if s.debug { + data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprint("Internal server error: recovered from panic ", e)}, r) + } else { + data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r) + } + } + // noTpl means the handler completely handled the request + if t != noTpl { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // return 500 Internal Server Error with errorInternalTpl + if t == errorInternalTpl { + w.WriteHeader(http.StatusInternalServerError) + } + if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil { + glog.Error(err) + } + } + if s.metrics != nil { + s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec() + } + }() + if s.metrics != nil { + s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc() + } + if s.debug { + // reload templates on each request + // to reflect changes during development + s.templates = s.parseTemplates() + } + t, data, err = handler(w, r) + if err != nil || (data == nil && t != noTpl) { + t = errorInternalTpl + if apiErr, ok := err.(*api.APIError); ok { + data = s.newTemplateDataWithError(apiErr, r) + if apiErr.Public { + t = errorTpl + } + } else { + if err != nil { + glog.Error(handlerName, " error: ", err) + } + if s.debug { + data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprintf("Internal server error: %v, data %+v", err, data)}, r) + } else { + data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r) + } + } + } + if s.postHtmlTemplateHandler != nil { + s.postHtmlTemplateHandler(data, w, r) + } + + } +} + +func relativeTimeUnit(d int64) string { + var u string + if d < 60 { + if d == 1 { + u = " sec" + } else { + u = " secs" + } + } else if d < 3600 { + d /= 60 + if d == 1 { + u = " min" + } else { + u = " mins" + } + } else if d < 3600*24 { + d /= 3600 + if d == 1 { + u = " hour" + } else { + u = " hours" + } + } else { + d /= 3600 * 24 + if d == 1 { + u = " day" + } else { + u = " days" + } + } + return strconv.FormatInt(d, 10) + u +} + +func relativeTime(d int64) string { + r := relativeTimeUnit(d) + if d > 3600*24 { + d = d % (3600 * 24) + if d >= 3600 { + r += " " + relativeTimeUnit(d) + } + } else if d > 3600 { + d = d % 3600 + if d >= 60 { + r += " " + relativeTimeUnit(d) + } + } + return r +} + +func unixTimeSpan(ut int64) template.HTML { + t := time.Unix(ut, 0) + return timeSpan(&t) +} + +var timeNow = time.Now + +func timeSpan(t *time.Time) template.HTML { + if t == nil { + return "" + } + u := t.Unix() + if u <= 0 { + return "" + } + d := timeNow().Unix() - u + f := t.UTC().Format("2006-01-02 15:04:05") + if d < 0 { + return template.HTML(f) + } + r := relativeTime(d) + return template.HTML(`` + r + " ago") +} + +func toJSON(data interface{}) string { + json, err := json.Marshal(data) + if err != nil { + return "" + } + return string(json) +} + +func formatAmountWithDecimals(a *api.Amount, d int) string { + if a == nil { + return "0" + } + return a.DecimalString(d) +} + +func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) { + rv.WriteString(`") + i := strings.IndexByte(amount, '.') + if i < 0 { + appendSeparatedNumberSpans(rv, amount, "nc") + } else { + appendSeparatedNumberSpans(rv, amount[:i], "nc") + rv.WriteString(`.`) + rv.WriteString(``) + appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns") + rv.WriteString("") + } + if shortcut != "" { + rv.WriteString(" ") + rv.WriteString(html.EscapeString(shortcut)) + } + rv.WriteString("") +} + +func appendAmountSpanBitcoinType(rv *strings.Builder, class, amount, shortcut, txDate string) { + if amount == "0" { + appendAmountSpan(rv, class, amount, shortcut, txDate) + return + } + rv.WriteString(`") + i := strings.IndexByte(amount, '.') + var decimals string + if i < 0 { + appendSeparatedNumberSpans(rv, amount, "nc") + decimals = "00000000" + } else { + appendSeparatedNumberSpans(rv, amount[:i], "nc") + decimals = amount[i+1:] + "00000000" + } + rv.WriteString(`.`) + rv.WriteString(``) + rv.WriteString(decimals[:2]) + rv.WriteString(``) + rv.WriteString(decimals[2:5]) + rv.WriteString("") + rv.WriteString(``) + rv.WriteString(decimals[5:8]) + rv.WriteString("") + rv.WriteString("") + if shortcut != "" { + rv.WriteString(" ") + rv.WriteString(html.EscapeString(shortcut)) + } + rv.WriteString("") +} + +func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) { + rv.WriteString(``) +} + +func formatInt(i int) template.HTML { + return formatInt64(int64(i)) +} + +func formatUint32(i uint32) template.HTML { + return formatInt64(int64(i)) +} + +func appendSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) { + if len(s) > 0 && s[0] == '-' { + s = s[1:] + rv.WriteByte('-') + } + t := (len(s) - 1) / 3 + if t <= 0 { + rv.WriteString(s) + } else { + t *= 3 + rv.WriteString(s[:len(s)-t]) + for i := len(s) - t; i < len(s); i += 3 { + rv.WriteString(``) + rv.WriteString(s[i : i+3]) + rv.WriteString("") + } + } +} + +func appendLeftSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) { + l := len(s) + if l <= 3 { + rv.WriteString(s) + } else { + rv.WriteString(s[:3]) + for i := 3; i < len(s); i += 3 { + rv.WriteString(``) + e := i + 3 + if e > l { + e = l + } + rv.WriteString(s[i:e]) + rv.WriteString("") + } + } +} + +func formatInt64(i int64) template.HTML { + s := strconv.FormatInt(i, 10) + var rv strings.Builder + appendSeparatedNumberSpans(&rv, s, "ns") + return template.HTML(rv.String()) +} + +func formatBigInt(i *big.Int) template.HTML { + if i == nil { + return "" + } + s := i.String() + var rv strings.Builder + appendSeparatedNumberSpans(&rv, s, "ns") + return template.HTML(rv.String()) +} diff --git a/server/html_templates_test.go b/server/html_templates_test.go new file mode 100644 index 0000000000..eca70f3652 --- /dev/null +++ b/server/html_templates_test.go @@ -0,0 +1,257 @@ +//go:build unittest + +package server + +import ( + "html/template" + "reflect" + "strings" + "testing" + "time" +) + +func Test_formatInt64(t *testing.T) { + tests := []struct { + name string + n int64 + want template.HTML + }{ + {"1", 1, "1"}, + {"13", 13, "13"}, + {"123", 123, "123"}, + {"1234", 1234, `1234`}, + {"91234", 91234, `91234`}, + {"891234", 891234, `891234`}, + {"7891234", 7891234, `7891234`}, + {"67891234", 67891234, `67891234`}, + {"567891234", 567891234, `567891234`}, + {"4567891234", 4567891234, `4567891234`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := formatInt64(tt.n); !reflect.DeepEqual(got, tt.want) { + t.Errorf("formatInt64() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_formatTime(t *testing.T) { + timeNow = fixedTimeNow + tests := []struct { + name string + want template.HTML + }{ + { + name: "2020-12-23 15:16:17", + want: `630 days 21 hours ago`, + }, + { + name: "2022-08-23 11:12:13", + want: `23 days 1 hour ago`, + }, + { + name: "2022-09-14 11:12:13", + want: `1 day 1 hour ago`, + }, + { + name: "2022-09-14 14:12:13", + want: `22 hours 31 mins ago`, + }, + { + name: "2022-09-15 09:33:26", + want: `3 hours 10 mins ago`, + }, + { + name: "2022-09-15 12:23:56", + want: `20 mins ago`, + }, + { + name: "2022-09-15 12:24:07", + want: `19 mins ago`, + }, + { + name: "2022-09-15 12:43:21", + want: `35 secs ago`, + }, + { + name: "2022-09-15 12:43:56", + want: `0 secs ago`, + }, + { + name: "2022-09-16 12:43:56", + want: `2022-09-16 12:43:56`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tm, _ := time.Parse("2006-01-02 15:04:05", tt.name) + if got := timeSpan(&tm); !reflect.DeepEqual(got, tt.want) { + t.Errorf("formatTime() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_appendAmountSpan(t *testing.T) { + tests := []struct { + name string + class string + amount string + shortcut string + txDate string + want string + }{ + { + name: "prim-amt 1.23456789 BTC", + class: "prim-amt", + amount: "1.23456789", + shortcut: "BTC", + want: `1.23456789 BTC`, + }, + { + name: "prim-amt 1432134.23456 BTC", + class: "prim-amt", + amount: "1432134.23456", + shortcut: "BTC", + want: `1432134.23456 BTC`, + }, + { + name: "sec-amt 1 EUR", + class: "sec-amt", + amount: "1", + shortcut: "EUR", + want: `1 EUR`, + }, + { + name: "sec-amt -1 EUR", + class: "sec-amt", + amount: "-1", + shortcut: "EUR", + want: `-1 EUR`, + }, + { + name: "sec-amt 432109.23 EUR", + class: "sec-amt", + amount: "432109.23", + shortcut: "EUR", + want: `432109.23 EUR`, + }, + { + name: "sec-amt -432109.23 EUR", + class: "sec-amt", + amount: "-432109.23", + shortcut: "EUR", + want: `-432109.23 EUR`, + }, + { + name: "sec-amt 43141.29 EUR", + class: "sec-amt", + amount: "43141.29", + shortcut: "EUR", + txDate: "2022-03-14", + want: `43141.29 EUR`, + }, + { + name: "sec-amt -43141.29 EUR", + class: "sec-amt", + amount: "-43141.29", + shortcut: "EUR", + txDate: "2022-03-14", + want: `-43141.29 EUR`, + }, + { + name: "prim-amt 1.23456789 BTC", + class: "prim-amt", + amount: "1.23456789", + shortcut: "alert(1)", + want: `1.23456789 <javascript>alert(1)</javascript>`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var rv strings.Builder + appendAmountSpan(&rv, tt.class, tt.amount, tt.shortcut, tt.txDate) + if got := rv.String(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("appendAmountSpan() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_appendAmountSpanBitcoinType(t *testing.T) { + tests := []struct { + name string + class string + amount string + shortcut string + txDate string + want string + }{ + { + name: "prim-amt 1.23456789 BTC", + class: "prim-amt", + amount: "1.23456789", + shortcut: "BTC", + want: `1.23456789 BTC`, + }, + { + name: "prim-amt 1432134.23456 BTC", + class: "prim-amt", + amount: "1432134.23456", + shortcut: "BTC", + want: `1432134.23456000 BTC`, + }, + { + name: "prim-amt 1 BTC", + class: "prim-amt", + amount: "1", + shortcut: "BTC", + want: `1.00000000 BTC`, + }, + { + name: "prim-amt 0 BTC", + class: "prim-amt", + amount: "0", + shortcut: "BTC", + want: `0 BTC`, + }, + { + name: "prim-amt 34.2 BTC", + class: "prim-amt", + amount: "34.2", + shortcut: "BTC", + want: `34.20000000 BTC`, + }, + { + name: "prim-amt -34.2345678 BTC", + class: "prim-amt", + amount: "-34.2345678", + shortcut: "BTC", + want: `-34.23456780 BTC`, + }, + { + name: "prim-amt -1234.2345 BTC", + class: "prim-amt", + amount: "-1234.2345", + shortcut: "BTC", + want: `-1234.23450000 BTC`, + }, + { + name: "prim-amt -123.23 BTC", + class: "prim-amt", + amount: "-123.23", + shortcut: "BTC", + want: `-123.23000000 BTC`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var rv strings.Builder + appendAmountSpanBitcoinType(&rv, tt.class, tt.amount, tt.shortcut, tt.txDate) + if got := rv.String(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("appendAmountSpanBitcoinType() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/server/internal.go b/server/internal.go index 92d0dbd843..e440fbd8e6 100644 --- a/server/internal.go +++ b/server/internal.go @@ -4,18 +4,27 @@ import ( "context" "encoding/json" "fmt" + "html/template" + "io" "net/http" + "path/filepath" + "sort" + "strconv" + "strings" "github.com/golang/glog" + "github.com/juju/errors" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/trezor/blockbook/api" "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/common" "github.com/trezor/blockbook/db" + "github.com/trezor/blockbook/fiat" ) // InternalServer is handle to internal http server type InternalServer struct { + htmlTemplates[InternalTemplateData] https *http.Server certFiles string db *db.RocksDB @@ -28,8 +37,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, metrics *common.Metrics, is *common.InternalState) (*InternalServer, error) { - api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) +func NewInternalServer(binding, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates) (*InternalServer, error) { + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates) if err != nil { return nil, err } @@ -41,6 +50,9 @@ func NewInternalServer(binding, certFiles string, db *db.RocksDB, chain bchain.B Handler: serveMux, } s := &InternalServer{ + htmlTemplates: htmlTemplates[InternalTemplateData]{ + debug: true, + }, https: https, certFiles: certFiles, db: db, @@ -51,11 +63,22 @@ func NewInternalServer(binding, certFiles string, db *db.RocksDB, chain bchain.B is: is, api: api, } + s.htmlTemplates.newTemplateData = s.newTemplateData + s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError + s.htmlTemplates.parseTemplates = s.parseTemplates + s.templates = s.parseTemplates() serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/"))) + serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) serveMux.HandleFunc(path+"metrics", promhttp.Handler().ServeHTTP) serveMux.HandleFunc(path, s.index) - + serveMux.HandleFunc(path+"admin", s.htmlTemplateHandler(s.adminIndex)) + serveMux.HandleFunc(path+"admin/ws-limit-exceeding-ips", s.htmlTemplateHandler(s.wsLimitExceedingIPs)) + if s.chainParser.GetChainType() == bchain.ChainEthereumType { + serveMux.HandleFunc(path+"admin/internal-data-errors", s.htmlTemplateHandler(s.internalDataErrors)) + serveMux.HandleFunc(path+"admin/contract-info", s.htmlTemplateHandler(s.contractInfoPage)) + serveMux.HandleFunc(path+"admin/contract-info/", s.jsonHandler(s.apiContractInfo, 0)) + } return s, nil } @@ -97,3 +120,155 @@ func (s *InternalServer) index(w http.ResponseWriter, r *http.Request) { w.Write(buf) } + +const ( + adminIndexTpl = iota + errorInternalTpl + 1 + adminInternalErrorsTpl + adminLimitExceedingIPSTpl + adminContractInfoTpl + + internalTplCount +) + +// WsLimitExceedingIP is used to transfer data to the templates +type WsLimitExceedingIP struct { + IP string + Count int +} + +// InternalTemplateData is used to transfer data to the templates +type InternalTemplateData struct { + CoinName string + CoinShortcut string + CoinLabel string + ChainType bchain.ChainType + Error *api.APIError + InternalDataErrors []db.BlockInternalDataError + RefetchingInternalData bool + WsGetAccountInfoLimit int + WsLimitExceedingIPs []WsLimitExceedingIP +} + +func (s *InternalServer) newTemplateData(r *http.Request) *InternalTemplateData { + t := &InternalTemplateData{ + CoinName: s.is.Coin, + CoinShortcut: s.is.CoinShortcut, + CoinLabel: s.is.CoinLabel, + ChainType: s.chainParser.GetChainType(), + } + return t +} + +func (s *InternalServer) newTemplateDataWithError(error *api.APIError, r *http.Request) *InternalTemplateData { + td := s.newTemplateData(r) + td.Error = error + return td +} + +func (s *InternalServer) parseTemplates() []*template.Template { + templateFuncMap := template.FuncMap{ + "formatUint32": formatUint32, + } + createTemplate := func(filenames ...string) *template.Template { + if len(filenames) == 0 { + panic("Missing templates") + } + return template.Must(template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap).ParseFiles(filenames...)) + } + t := make([]*template.Template, internalTplCount) + t[errorTpl] = createTemplate("./static/internal_templates/error.html", "./static/internal_templates/base.html") + t[errorInternalTpl] = createTemplate("./static/internal_templates/error.html", "./static/internal_templates/base.html") + t[adminIndexTpl] = createTemplate("./static/internal_templates/index.html", "./static/internal_templates/base.html") + t[adminInternalErrorsTpl] = createTemplate("./static/internal_templates/block_internal_data_errors.html", "./static/internal_templates/base.html") + t[adminLimitExceedingIPSTpl] = createTemplate("./static/internal_templates/ws_limit_exceeding_ips.html", "./static/internal_templates/base.html") + t[adminContractInfoTpl] = createTemplate("./static/internal_templates/contract_info.html", "./static/internal_templates/base.html") + return t +} + +func (s *InternalServer) adminIndex(w http.ResponseWriter, r *http.Request) (tpl, *InternalTemplateData, error) { + data := s.newTemplateData(r) + return adminIndexTpl, data, nil +} + +func (s *InternalServer) internalDataErrors(w http.ResponseWriter, r *http.Request) (tpl, *InternalTemplateData, error) { + if r.Method == http.MethodPost { + err := s.api.RefetchInternalData() + if err != nil { + return errorTpl, nil, err + } + } + data := s.newTemplateData(r) + internalErrors, err := s.db.GetBlockInternalDataErrorsEthereumType() + if err != nil { + return errorTpl, nil, err + } + data.InternalDataErrors = internalErrors + data.RefetchingInternalData = s.api.IsRefetchingInternalData() + return adminInternalErrorsTpl, data, nil +} + +func (s *InternalServer) wsLimitExceedingIPs(w http.ResponseWriter, r *http.Request) (tpl, *InternalTemplateData, error) { + if r.Method == http.MethodPost { + s.is.ResetWsLimitExceedingIPs() + } + data := s.newTemplateData(r) + ips := make([]WsLimitExceedingIP, 0, len(s.is.WsLimitExceedingIPs)) + for k, v := range s.is.WsLimitExceedingIPs { + ips = append(ips, WsLimitExceedingIP{k, v}) + } + sort.Slice(ips, func(i, j int) bool { + return ips[i].Count > ips[j].Count + }) + data.WsLimitExceedingIPs = ips + data.WsGetAccountInfoLimit = s.is.WsGetAccountInfoLimit + return adminLimitExceedingIPSTpl, data, nil +} + +func (s *InternalServer) contractInfoPage(w http.ResponseWriter, r *http.Request) (tpl, *InternalTemplateData, error) { + data := s.newTemplateData(r) + return adminContractInfoTpl, data, nil +} + +func (s *InternalServer) apiContractInfo(r *http.Request, apiVersion int) (interface{}, error) { + if r.Method == http.MethodPost { + return s.updateContracts(r) + } + var contractAddress string + i := strings.LastIndexByte(r.URL.Path, '/') + if i > 0 { + contractAddress = r.URL.Path[i+1:] + } + if len(contractAddress) == 0 { + return nil, api.NewAPIError("Missing contract address", true) + } + + contractInfo, valid, err := s.api.GetContractInfo(contractAddress, bchain.UnknownTokenStandard) + if err != nil { + return nil, api.NewAPIError(err.Error(), true) + } + if !valid { + return nil, api.NewAPIError("Not a contract", true) + } + return contractInfo, nil +} + +func (s *InternalServer) updateContracts(r *http.Request) (interface{}, error) { + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, api.NewAPIError("Cannot get request body", true) + } + var contractInfos []bchain.ContractInfo + err = json.Unmarshal(data, &contractInfos) + if err != nil { + return nil, errors.Annotatef(err, "Cannot unmarshal body to array of ContractInfo objects") + } + for i := range contractInfos { + c := &contractInfos[i] + err := s.db.StoreContractInfo(c) + if err != nil { + return nil, api.NewAPIError("Error updating contract "+c.Contract+" "+err.Error(), true) + } + + } + return "{\"success\":\"Updated " + strconv.Itoa(len(contractInfos)) + " contracts\"}", nil +} diff --git a/server/public.go b/server/public.go index cd57179c43..e54a307c71 100644 --- a/server/public.go +++ b/server/public.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "html" "html/template" "io" "math/big" @@ -14,7 +15,6 @@ import ( "reflect" "regexp" "runtime" - "runtime/debug" "sort" "strconv" "strings" @@ -25,6 +25,7 @@ import ( "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/common" "github.com/trezor/blockbook/db" + "github.com/trezor/blockbook/fiat" ) const txsOnPage = 25 @@ -40,42 +41,43 @@ const ( apiV2 ) -// PublicServer is a handle to public http server +// PublicServer provides public http server functionality type PublicServer struct { - binding string - certFiles string - socketio *SocketIoServer - websocket *WebsocketServer - https *http.Server - db *db.RocksDB - txCache *db.TxCache - chain bchain.BlockChain - chainParser bchain.BlockChainParser - mempool bchain.Mempool - api *api.Worker - explorerURL string - internalExplorer bool - metrics *common.Metrics - is *common.InternalState - templates []*template.Template - debug bool + htmlTemplates[TemplateData] + binding string + certFiles string + socketio *SocketIoServer + websocket *WebsocketServer + https *http.Server + db *db.RocksDB + txCache *db.TxCache + chain bchain.BlockChain + chainParser bchain.BlockChainParser + mempool bchain.Mempool + api *api.Worker + explorerURL string + internalExplorer bool + is *common.InternalState + fiatRates *fiat.FiatRates + useSatsAmountFormat bool + isFullInterface bool } // 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, enableSubNewTx 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, fiatRates *fiat.FiatRates, debugMode bool) (*PublicServer, error) { - api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates) if err != nil { return nil, err } - socketio, err := NewSocketIoServer(db, chain, mempool, txCache, metrics, is) + socketio, err := NewSocketIoServer(db, chain, mempool, txCache, metrics, is, fiatRates) if err != nil { return nil, err } - websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is, enableSubNewTx) + websocket, err := NewWebsocketServer(db, chain, mempool, txCache, metrics, is, fiatRates) if err != nil { return nil, err } @@ -88,23 +90,31 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch } s := &PublicServer{ - binding: binding, - certFiles: certFiles, - https: https, - api: api, - socketio: socketio, - websocket: websocket, - db: db, - txCache: txCache, - chain: chain, - chainParser: chain.GetChainParser(), - mempool: mempool, - explorerURL: explorerURL, - internalExplorer: explorerURL == "", - metrics: metrics, - is: is, - debug: debugMode, - } + htmlTemplates: htmlTemplates[TemplateData]{ + metrics: metrics, + debug: debugMode, + }, + binding: binding, + certFiles: certFiles, + https: https, + api: api, + socketio: socketio, + websocket: websocket, + db: db, + txCache: txCache, + chain: chain, + chainParser: chain.GetChainParser(), + mempool: mempool, + explorerURL: explorerURL, + internalExplorer: explorerURL == "", + is: is, + fiatRates: fiatRates, + useSatsAmountFormat: chain.GetChainParser().GetChainType() == bchain.ChainBitcoinType && chain.GetChainParser().AmountDecimals() == 8, + } + s.htmlTemplates.newTemplateData = s.newTemplateData + s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError + s.htmlTemplates.parseTemplates = s.parseTemplates + s.htmlTemplates.postHtmlTemplateHandler = s.postHtmlTemplateHandler s.templates = s.parseTemplates() // map only basic functions, the rest is enabled by method MapFullPublicInterface @@ -175,8 +185,10 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1)) } serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex, apiDefault)) + serveMux.HandleFunc(path+"api/block-filters/", s.jsonHandler(s.apiBlockFilters, apiDefault)) serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiDefault)) serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault)) + serveMux.HandleFunc(path+"api/rawtx/", s.jsonHandler(s.apiRawTx, apiDefault)) serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault)) serveMux.HandleFunc(path+"api/xpub/", s.jsonHandler(s.apiXpub, apiDefault)) serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiUtxo, apiDefault)) @@ -187,6 +199,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/balancehistory/", s.jsonHandler(s.apiBalanceHistory, apiDefault)) // v2 format serveMux.HandleFunc(path+"api/v2/block-index/", s.jsonHandler(s.apiBlockIndex, apiV2)) + serveMux.HandleFunc(path+"api/v2/block-filters/", s.jsonHandler(s.apiBlockFilters, apiV2)) 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)) @@ -205,6 +218,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.Handle(path+"socket.io/", s.socketio.GetHandler()) // websocket interface serveMux.Handle(path+"websocket", s.websocket.GetHandler()) + s.isFullInterface = true } // Close closes the server @@ -278,62 +292,6 @@ func getFunctionName(i interface{}) string { return name } -func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) (interface{}, error), apiVersion int) func(w http.ResponseWriter, r *http.Request) { - type jsonError struct { - 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(handlerName, " recovered from panic: ", e) - debug.PrintStack() - if s.debug { - data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError} - } else { - data = jsonError{"Internal server error", http.StatusInternalServerError} - } - } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if e, isError := data.(jsonError); isError { - w.WriteHeader(e.HTTPStatus) - } - err = json.NewEncoder(w).Encode(data) - 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 { - if apiErr.Public { - data = jsonError{apiErr.Error(), http.StatusBadRequest} - } else { - data = jsonError{apiErr.Error(), http.StatusInternalServerError} - } - } else { - if err != nil { - glog.Error(handlerName, " error: ", err) - } - if s.debug { - if data != nil { - data = jsonError{fmt.Sprintf("Internal server error: %v, data %+v", err, data), http.StatusInternalServerError} - } else { - data = jsonError{fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError} - } - } else { - data = jsonError{"Internal server error", http.StatusInternalServerError} - } - } - } - } -} - func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData { t := &TemplateData{ CoinName: s.is.Coin, @@ -343,8 +301,13 @@ func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData { InternalExplorer: s.internalExplorer && !s.is.InitialSync, TOSLink: api.Text.TOSLink, } + if t.ChainType == bchain.ChainEthereumType { + t.FungibleTokenName = bchain.EthereumTokenStandardMap[bchain.FungibleToken] + t.NonFungibleTokenName = bchain.EthereumTokenStandardMap[bchain.NonFungibleToken] + t.MultiTokenName = bchain.EthereumTokenStandardMap[bchain.MultiToken] + } if !s.debug { - t.Minified = ".min.2" + t.Minified = ".min.4" } if s.is.HasFiatRates { // get the secondary coin and if it should be shown either from query parameters "secondary" and "use_secondary" @@ -368,10 +331,10 @@ func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData { secondary = "usd" } } - ticker := s.is.GetCurrentTicker(secondary, "") + ticker := s.fiatRates.GetCurrentTicker(secondary, "") if ticker == nil && secondary != "usd" { secondary = "usd" - ticker = s.is.GetCurrentTicker(secondary, "") + ticker = s.fiatRates.GetCurrentTicker(secondary, "") } if ticker != nil { t.SecondaryCoin = strings.ToUpper(secondary) @@ -391,82 +354,14 @@ func (s *PublicServer) newTemplateData(r *http.Request) *TemplateData { return t } -func (s *PublicServer) newTemplateDataWithError(text string, r *http.Request) *TemplateData { +func (s *PublicServer) newTemplateDataWithError(error *api.APIError, r *http.Request) *TemplateData { td := s.newTemplateData(r) - td.Error = &api.APIError{Text: text} + td.Error = error return td } -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(handlerName, " recovered from panic: ", e) - debug.PrintStack() - t = errorInternalTpl - if s.debug { - data = s.newTemplateDataWithError(fmt.Sprint("Internal server error: recovered from panic ", e), r) - } else { - data = s.newTemplateDataWithError("Internal server error", r) - } - } - // noTpl means the handler completely handled the request - if t != noTpl { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - // return 500 Internal Server Error with errorInternalTpl - if t == errorInternalTpl { - w.WriteHeader(http.StatusInternalServerError) - } - if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil { - 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 - s.templates = s.parseTemplates() - } - t, data, err = handler(w, r) - if err != nil || (data == nil && t != noTpl) { - t = errorInternalTpl - if apiErr, ok := err.(*api.APIError); ok { - data = s.newTemplateData(r) - data.Error = apiErr - if apiErr.Public { - t = errorTpl - } - } else { - if err != nil { - glog.Error(handlerName, " error: ", err) - } - if s.debug { - data = s.newTemplateDataWithError(fmt.Sprintf("Internal server error: %v, data %+v", err, data), r) - } else { - data = s.newTemplateDataWithError("Internal server error", r) - } - } - } - // if SecondaryCoin is specified, set secondary_coin cookie - if data != nil && data.SecondaryCoin != "" { - http.SetCookie(w, &http.Cookie{Name: secondaryCoinCookieName, Value: data.SecondaryCoin + "=" + strconv.FormatBool(data.UseSecondaryCoin), Path: "/"}) - } - } -} - -type tpl int - const ( - noTpl = tpl(iota) - errorTpl - errorInternalTpl - indexTpl + indexTpl = iota + errorInternalTpl + 1 txTpl addressTpl xpubTpl @@ -476,7 +371,7 @@ const ( mempoolTpl nftDetailTpl - tplCount + publicTplCount ) // TemplateData is used to transfer data to the templates @@ -486,6 +381,9 @@ type TemplateData struct { CoinLabel string InternalExplorer bool ChainType bchain.ChainType + FungibleTokenName bchain.TokenStandardName + NonFungibleTokenName bchain.TokenStandardName + MultiTokenName bchain.TokenStandardName Address *api.Address AddrStr string Tx *api.Tx @@ -582,7 +480,7 @@ func (s *PublicServer) parseTemplates() []*template.Template { return t } } - t := make([]*template.Template, tplCount) + t := make([]*template.Template, publicTplCount) t[errorTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html") t[errorInternalTpl] = createTemplate("./static/templates/error.html", "./static/templates/base.html") t[indexTpl] = createTemplate("./static/templates/index.html", "./static/templates/base.html") @@ -603,85 +501,12 @@ func (s *PublicServer) parseTemplates() []*template.Template { return t } -func relativeTimeUnit(d int64) string { - var u string - if d < 60 { - if d == 1 { - u = " sec" - } else { - u = " secs" - } - } else if d < 3600 { - d /= 60 - if d == 1 { - u = " min" - } else { - u = " mins" - } - } else if d < 3600*24 { - d /= 3600 - if d == 1 { - u = " hour" - } else { - u = " hours" - } - } else { - d /= 3600 * 24 - if d == 1 { - u = " day" - } else { - u = " days" - } - } - return strconv.FormatInt(d, 10) + u -} - -func relativeTime(d int64) string { - r := relativeTimeUnit(d) - if d > 3600*24 { - d = d % (3600 * 24) - if d >= 3600 { - r += " " + relativeTimeUnit(d) - } - } else if d > 3600 { - d = d % 3600 - if d >= 60 { - r += " " + relativeTimeUnit(d) - } +func (s *PublicServer) postHtmlTemplateHandler(data *TemplateData, w http.ResponseWriter, r *http.Request) { + // // if SecondaryCoin is specified, set secondary_coin cookie + if data != nil && data.SecondaryCoin != "" { + http.SetCookie(w, &http.Cookie{Name: secondaryCoinCookieName, Value: data.SecondaryCoin + "=" + strconv.FormatBool(data.UseSecondaryCoin), Path: "/"}) } - return r -} -func unixTimeSpan(ut int64) template.HTML { - t := time.Unix(ut, 0) - return timeSpan(&t) -} - -var timeNow = time.Now - -func timeSpan(t *time.Time) template.HTML { - if t == nil { - return "" - } - u := t.Unix() - if u <= 0 { - return "" - } - d := timeNow().Unix() - u - f := t.UTC().Format("2006-01-02 15:04:05") - if d < 0 { - return template.HTML(f) - } - r := relativeTime(d) - return template.HTML(`` + r + " ago") -} - -func toJSON(data interface{}) string { - json, err := json.Marshal(data) - if err != nil { - return "" - } - return string(json) } func (s *PublicServer) formatAmount(a *api.Amount) string { @@ -691,68 +516,15 @@ func (s *PublicServer) formatAmount(a *api.Amount) string { return s.chainParser.AmountToDecimalString((*big.Int)(a)) } -func formatAmountWithDecimals(a *api.Amount, d int) string { - if a == nil { - return "0" - } - return a.DecimalString(d) -} - -func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) { - rv.WriteString(`") - i := strings.IndexByte(amount, '.') - if i < 0 { - appendSeparatedNumberSpans(rv, amount, "nc") - } else { - appendSeparatedNumberSpans(rv, amount[:i], "nc") - rv.WriteString(`.`) - rv.WriteString(``) - appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns") - rv.WriteString("") - } - if shortcut != "" { - rv.WriteString(" ") - rv.WriteString(shortcut) - } - rv.WriteString("") -} - -func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) { - rv.WriteString(``) -} - -func formatSecondaryAmount(a float64, td *TemplateData) string { - if td.SecondaryCoin == "BTC" || td.SecondaryCoin == "ETH" { - return strconv.FormatFloat(a, 'f', 6, 64) - } - return strconv.FormatFloat(a, 'f', 2, 64) -} - func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes string) template.HTML { primary := s.formatAmount(a) var rv strings.Builder appendAmountWrapperSpan(&rv, primary, td.CoinShortcut, classes) - appendAmountSpan(&rv, "prim-amt", primary, td.CoinShortcut, "") + if s.useSatsAmountFormat { + appendAmountSpanBitcoinType(&rv, "prim-amt", primary, td.CoinShortcut, "") + } else { + appendAmountSpan(&rv, "prim-amt", primary, td.CoinShortcut, "") + } if td.SecondaryCoin != "" { p, err := strconv.ParseFloat(primary, 64) if err == nil { @@ -763,7 +535,11 @@ func (s *PublicServer) amountSpan(a *api.Amount, td *TemplateData, classes strin if td.TxTicker == nil { date := time.Unix(td.Tx.Blocktime, 0).UTC() secondary := strings.ToLower(td.SecondaryCoin) - ticker, _ := s.db.FiatRatesFindTicker(&date, secondary, "") + var ticker *common.CurrencyRatesTicker + tickers, err := s.fiatRates.GetTickersForTimestamps([]int64{int64(td.Tx.Blocktime)}, "", "") + if err == nil && tickers != nil && len(*tickers) > 0 { + ticker = (*tickers)[0] + } if ticker != nil { td.TxSecondaryCoinRate = float64(ticker.Rates[secondary]) // the ticker is from the midnight, valid for the whole day before @@ -890,70 +666,11 @@ func (s *PublicServer) summaryValuesSpan(baseValue float64, secondaryValue float return template.HTML(rv.String()) } -func formatInt(i int) template.HTML { - return formatInt64(int64(i)) -} - -func formatUint32(i uint32) template.HTML { - return formatInt64(int64(i)) -} - -func appendSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) { - if len(s) > 0 && s[0] == '-' { - s = s[1:] - rv.WriteByte('-') - } - t := (len(s) - 1) / 3 - if t <= 0 { - rv.WriteString(s) - } else { - t *= 3 - rv.WriteString(s[:len(s)-t]) - for i := len(s) - t; i < len(s); i += 3 { - rv.WriteString(``) - rv.WriteString(s[i : i+3]) - rv.WriteString("") - } - } -} - -func appendLeftSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) { - l := len(s) - if l <= 3 { - rv.WriteString(s) - } else { - rv.WriteString(s[:3]) - for i := 3; i < len(s); i += 3 { - rv.WriteString(``) - e := i + 3 - if e > l { - e = l - } - rv.WriteString(s[i:e]) - rv.WriteString("") - } - } -} - -func formatInt64(i int64) template.HTML { - s := strconv.FormatInt(i, 10) - var rv strings.Builder - appendSeparatedNumberSpans(&rv, s, "ns") - return template.HTML(rv.String()) -} - -func formatBigInt(i *big.Int) template.HTML { - if i == nil { - return "" +func formatSecondaryAmount(a float64, td *TemplateData) string { + if td.SecondaryCoin == "BTC" || td.SecondaryCoin == "ETH" { + return strconv.FormatFloat(a, 'f', 6, 64) } - s := i.String() - var rv strings.Builder - appendSeparatedNumberSpans(&rv, s, "ns") - return template.HTML(rv.String()) + return strconv.FormatFloat(a, 'f', 2, 64) } func getAddressAlias(a string, td *TemplateData) *api.AddressAlias { @@ -988,11 +705,11 @@ func addressAliasSpan(a string, td *TemplateData) template.HTML { rv.WriteString(a) } else { rv.WriteString(``) - rv.WriteString(alias.Alias) + rv.WriteString(html.EscapeString(alias.Alias)) } rv.WriteString("") return template.HTML(rv.String()) @@ -1028,10 +745,10 @@ func isOwnAddress(td *TemplateData, a string) bool { } // called from template, returns count of token transfers of given type in a tx -func tokenTransfersCount(tx *api.Tx, t bchain.TokenTypeName) int { +func tokenTransfersCount(tx *api.Tx, t bchain.TokenStandardName) int { count := 0 for i := range tx.TokenTransfers { - if tx.TokenTransfers[i].Type == t { + if tx.TokenTransfers[i].Standard == t { count++ } } @@ -1039,10 +756,10 @@ func tokenTransfersCount(tx *api.Tx, t bchain.TokenTypeName) int { } // called from template, returns count of tokens in array of given type -func tokenCount(tokens []api.Token, t bchain.TokenTypeName) int { +func tokenCount(tokens []api.Token, t bchain.TokenStandardName) int { count := 0 for i := range tokens { - if tokens[i].Type == t { + if tokens[i].Standard == t { count++ } } @@ -1284,6 +1001,11 @@ func (s *PublicServer) explorerBlock(w http.ResponseWriter, r *http.Request) (tp } func (s *PublicServer) explorerIndex(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { + if !s.isFullInterface && r.URL.Path != "/" { + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte("Service unavailable")) + return noTpl, nil, nil + } var si *api.SystemInfo var err error s.metrics.ExplorerViews.With(common.Labels{"action": "index"}).Inc() @@ -1338,7 +1060,7 @@ func (s *PublicServer) explorerSendTx(w http.ResponseWriter, r *http.Request) (t } hex := r.FormValue("hex") if len(hex) > 0 { - res, err := s.chain.SendRawTransaction(hex) + res, err := s.chain.SendRawTransaction(hex, false) if err != nil { data.SendTxHex = hex data.Error = &api.APIError{Text: err.Error(), Public: true} @@ -1428,6 +1150,9 @@ func getPagingRange(page int, total int) ([]int, int, int) { } func (s *PublicServer) apiIndex(r *http.Request, apiVersion int) (interface{}, error) { + if !s.isFullInterface && r.URL.Path != "/api/" { + return nil, api.NewAPIError("Service unavailable", false) + } s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc() return s.api.GetSystemInfo(false) } @@ -1458,6 +1183,96 @@ func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface }, nil } +func (s *PublicServer) apiBlockFilters(r *http.Request, apiVersion int) (interface{}, error) { + // Define return type + type blockFilterResult struct { + BlockHash string `json:"blockHash"` + Filter string `json:"filter"` + } + type resBlockFilters struct { + ParamP uint8 `json:"P"` + ParamM uint64 `json:"M"` + ZeroedKey bool `json:"zeroedKey"` + BlockFilters map[int]blockFilterResult `json:"blockFilters"` + } + + // Parse parameters + lastN, ec := strconv.Atoi(r.URL.Query().Get("lastN")) + if ec != nil { + lastN = 0 + } + from, ec := strconv.Atoi(r.URL.Query().Get("from")) + if ec != nil { + from = 0 + } + to, ec := strconv.Atoi(r.URL.Query().Get("to")) + if ec != nil { + to = 0 + } + scriptType := r.URL.Query().Get("scriptType") + if scriptType != s.is.BlockFilterScripts { + return nil, api.NewAPIError(fmt.Sprintf("Invalid scriptType %s. Use %s", scriptType, s.is.BlockFilterScripts), true) + } + // NOTE: technically, we are also accepting "m: uint64" param, but we do not use it currently + + // Sanity checks + if lastN == 0 && from == 0 && to == 0 { + return nil, api.NewAPIError("Missing parameters", true) + } + if from > to { + return nil, api.NewAPIError("Invalid parameters - from > to", true) + } + + // Best height is needed more than once + bestHeight, _, err := s.db.GetBestBlock() + if err != nil { + glog.Error(err) + return nil, err + } + + // Modify to/from if needed + if lastN > 0 { + // Get data for last N blocks + to = int(bestHeight) + from = to - lastN + 1 + } else { + // Get data for specified from-to range + // From will always stay the same (even if 0) + // To will be the best block if not specified + if to == 0 { + to = int(bestHeight) + } + } + + handleBlockFiltersResultFromTo := func(fromHeight int, toHeight int) (interface{}, error) { + blockFiltersMap := make(map[int]blockFilterResult) + for i := fromHeight; i <= toHeight; i++ { + blockHash, err := s.db.GetBlockHash(uint32(i)) + if err != nil { + glog.Error(err) + return nil, err + } + blockFilter, err := s.db.GetBlockFilter(blockHash) + if err != nil { + glog.Error(err) + return nil, err + } + blockFiltersMap[i] = blockFilterResult{ + BlockHash: blockHash, + Filter: blockFilter, + } + } + return resBlockFilters{ + ParamP: s.is.BlockGolombFilterP, + ParamM: bchain.GetGolombParamM(s.is.BlockGolombFilterP), + ZeroedKey: s.is.BlockFilterUseZeroedKey, + BlockFilters: blockFiltersMap, + }, nil + } + + return handleBlockFiltersResultFromTo(from, to) +} + func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) { var txid string i := strings.LastIndexByte(r.URL.Path, '/') @@ -1485,6 +1300,19 @@ func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, erro return tx, err } +func (s *PublicServer) apiRawTx(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 "", api.NewAPIError("Missing txid", true) + } + s.metrics.ExplorerViews.With(common.Labels{"action": "api-raw-tx"}).Inc() + return s.api.GetRawTransaction(txid) +} + func (s *PublicServer) apiTxSpecific(r *http.Request, apiVersion int) (interface{}, error) { var txid string i := strings.LastIndexByte(r.URL.Path, '/') @@ -1682,7 +1510,7 @@ func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{}, } } if len(hex) > 0 { - res.Result, err = s.chain.SendRawTransaction(hex) + res.Result, err = s.chain.SendRawTransaction(hex, false) if err != nil { return nil, api.NewAPIError(err.Error(), true) } diff --git a/server/public_ethereumtype_test.go b/server/public_ethereumtype_test.go index 9094ca1751..f63f8558f0 100644 --- a/server/public_ethereumtype_test.go +++ b/server/public_ethereumtype_test.go @@ -7,11 +7,13 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/golang/glog" "github.com/linxGnu/grocksdb" "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain/coins/eth" + "github.com/trezor/blockbook/common" "github.com/trezor/blockbook/db" "github.com/trezor/blockbook/tests/dbtestdata" ) @@ -24,7 +26,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Address address7b.eth

0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b

0.000000000123450123 FAKE

Confirmed
Balance0.000000000123450123 FAKE
Transactions2
Non-contract Transactions0
Internal Transactions0
Nonce123
ContractQuantityValueTransfers#
Contract 130.000000001000123013 S131
Contract 740.001000123074 S741
ContractTokensTransfers#
Contract 20511

Transactions

ERC721 Token Transfers
`, + `Trezor Fake Coin Explorer

Address address7b.eth

0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b

0.000000000123450123 FAKE
0.00 USD

Confirmed
Balance0.000000000123450123 FAKE0.00 USD
Transactions2
Non-contract Transactions0
Internal Transactions0
Nonce123
ContractQuantityValueTransfers#
Contract 130.000000001000123013 S13-1
Contract 740.001000123074 S74-1
ContractTokensTransfers#
Contract 20511

Transactions

ERC721 Token Transfers
ERC20 Token Transfers
address7b.eth
 
871.180000950184 S74-
 
address7b.eth
7.674999999999991915 S13-
`, }, }, { @@ -33,7 +35,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Address

0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e

0.000000000123450093 FAKE

Confirmed
Balance0.000000000123450093 FAKE
Transactions1
Non-contract Transactions1
Internal Transactions0
Nonce93
ContractTokensTransfers#
Contract 1111 S111 of ID 1776, 10 S111 of ID 18981

Transactions

0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
 
0 FAKE
ERC1155 Token Transfers
 
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
1 S111 of ID 1776, 10 S111 of ID 1898
`, + `Trezor Fake Coin Explorer

Address

0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e

0.000000000123450093 FAKE
0.00 USD

Confirmed
Balance0.000000000123450093 FAKE0.00 USD
Transactions1
Non-contract Transactions1
Internal Transactions0
Nonce93
ContractTokensTransfers#
Contract 1111 S111 of ID 1776, 10 S111 of ID 18981

Transactions

0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
 
0 FAKE0.00 USD0.00 USD
ERC1155 Token Transfers
 
0x5Dc6288b35E0807A3d6fEB89b3a2Ff4aB773168e
1 S111 of ID 1776, 10 S111 of ID 1898
`, }, }, { @@ -42,14 +44,14 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Transaction

0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101
In BlockUnconfirmed
StatusSuccess
Value0 FAKE
Gas Used / Limit52025 / 78037
Gas Price0.00000004 FAKE (40 Gwei)
Fees0.002081 FAKE
RBFON
ERC20 Token Transfers
Input Data

0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000
transfer(address, uint256)
#TypeData
0address0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f
1uint25610000000000000000000000
Raw Transaction
`, + `Trezor Fake Coin Explorer

Transaction

0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101
In BlockUnconfirmed
StatusSuccess
Value0 FAKE0.00 USD0.00 USD
Gas Used / Limit52025 / 78037
Gas Price0.00000004 FAKE0.00 USD0.00 USD (40 Gwei)
Max Priority Fee Per Gas0.000000040000000001 FAKE0.00 USD0.00 USD (40.000000001 Gwei)
Max Fee Per Gas0.000000040000000002 FAKE0.00 USD0.00 USD (40.000000002 Gwei)
Base Fee Per Gas0.000000040000000003 FAKE0.00 USD0.00 USD (40.000000003 Gwei)
Fees0.002081 FAKE4.16 USD18.55 USD
RBFON
Nonce208
 
0 FAKE0.00 USD0.00 USD
ERC20 Token Transfers
Input Data

0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000
transfer(address, uint256)
#TypeData
0address0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f
1uint25610000000000000000000000
`, }, }, { name: "explorerTokenDetail " + dbtestdata.EthAddr7b, r: newGetRequest(ts.URL + "/nft/" + dbtestdata.EthAddrContractCd + "/" + "1"), status: http.StatusOK, contentType: "text/html; charset=utf-8", - body: []string{`Trezor Fake Coin Explorer

NFT Token Detail

Token ID1
Contract0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9
Contract 205
Contract typeERC20
`}, + body: []string{`Trezor Fake Coin Explorer

NFT Token Detail

Token ID1
Contract0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9
Contract 205
StandardERC20
`}, }, { name: "apiIndex", @@ -70,7 +72,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","balance":"123450075","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":1,"nonTokenTxs":1,"internalTxs":1,"txids":["0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2"],"nonce":"75","tokens":[{"type":"ERC20","name":"Contract 13","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","transfers":2,"symbol":"S13","decimals":18,"balance":"1000075013"},{"type":"ERC20","name":"Contract 74","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","transfers":2,"symbol":"S74","decimals":12,"balance":"1000075074"}]}`, + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","balance":"123450075","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":1,"nonTokenTxs":1,"internalTxs":1,"txids":["0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2"],"nonce":"75","tokens":[{"type":"ERC20","standard":"ERC20","name":"Contract 13","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","transfers":2,"symbol":"S13","decimals":18,"balance":"1000075013"},{"type":"ERC20","standard":"ERC20","name":"Contract 74","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","transfers":2,"symbol":"S74","decimals":12,"balance":"1000075074"}]}`, }, }, { @@ -79,7 +81,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b","balance":"123450123","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"0xca7628be5c80cda77163729ec63d218ee868a399d827a4682a478c6f48a6e22a","vin":[{"n":0,"addresses":["0x837E3f699d85a4b0B99894567e9233dFB1DcB081"],"isAddress":true}],"vout":[{"value":"0","n":0,"addresses":["0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9"],"isAddress":true}],"blockHeight":-1,"confirmations":0,"blockTime":0,"value":"0","fees":"87945000410410","rbf":true,"coinSpecificData":{"tx":{"nonce":"0x2","gasPrice":"0x59682f07","gas":"0x173a9","to":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","value":"0x0","input":"0x23b872dd000000000000000000000000837e3f699d85a4b0b99894567e9233dfb1dcb0810000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000000000000000000000000000000000000000000001","hash":"0xca7628be5c80cda77163729ec63d218ee868a399d827a4682a478c6f48a6e22a","blockNumber":"0xb33b9f","from":"0x837E3f699d85a4b0B99894567e9233dFB1DcB081","transactionIndex":"0x1"},"receipt":{"gasUsed":"0xe506","status":"0x1","logs":[{"address":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","topics":["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925","0x000000000000000000000000837e3f699d85a4b0b99894567e9233dfb1dcb081","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000001"],"data":"0x"},{"address":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000837e3f699d85a4b0b99894567e9233dfb1dcb081","0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b","0x0000000000000000000000000000000000000000000000000000000000000001"],"data":"0x"}]}},"tokenTransfers":[{"type":"ERC721","from":"0x837E3f699d85a4b0B99894567e9233dFB1DcB081","to":"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b","contract":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","name":"Contract 205","symbol":"S205","decimals":18,"value":"1"}],"ethereumSpecific":{"status":1,"nonce":2,"gasLimit":95145,"gasUsed":58630,"gasPrice":"1500000007","data":"0x23b872dd000000000000000000000000837e3f699d85a4b0b99894567e9233dfb1dcb0810000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000000000000000000000000000000000000000000001","parsedData":{"methodId":"0x23b872dd","name":""}}},{"txid":"0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2","vin":[{"n":0,"addresses":["0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D"],"isAddress":true}],"vout":[{"value":"0","n":0,"addresses":["0x479CC461fEcd078F766eCc58533D6F69580CF3AC"],"isAddress":true}],"blockHeight":-1,"confirmations":0,"blockTime":0,"value":"0","fees":"216368000000000","rbf":true,"coinSpecificData":{"tx":{"nonce":"0x1df76","gasPrice":"0x3b9aca00","gas":"0x3d090","to":"0x479CC461fEcd078F766eCc58533D6F69580CF3AC","value":"0x0","input":"0x4f15078700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000a5ef5a7656bfb0000000000000000000000000000000000000000000000000000004ba78398d5c5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfe0b9579b4ecf7a2801880f644009a324671a79754ea57c3a103c6e70d3dbef6ba69a08000000000000000000000000000000000000000000000000004f937d86afb90000000000000000000000000000000000000000000000000ab280fd8037d500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfb784b7c1f3fbe8b75484603ab8adc58aaee3a46245a6579fac7077b5570018b4e0d4eb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000308fd0e798ac00000000000000000000000000000000000000000000000006a8313d60b1f606b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000001b00000000000000000000000000000000000000000000000000000000000000029de0ccec59e8948e3d905b40e5542335ebc1eb4674db517d2f6392ec7fdeb3d45f3449d313ee2589819c6c79eb1c1b047adae68565c1608e3a1d1d70823febb0000000000000000000000000000000000000000000000000000000000000000234d06fe17f1202e8b07177a30eb64d14adc08cdb3fa1b3e3e0bea0f9672c02175b77c01c51d3c7e460723b27ecbc7801fd6482559a8c9999593f9a4d149c7384","hash":"0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2","blockNumber":"0x41eee9","from":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","transactionIndex":"0x24"},"internalData":{"type":1,"contract":"0d0f936ee4c93e25944694d6c121de94d9760f11","transfers":[{"type":0,"from":"4bda106325c335df99eab7fe363cac8a0ba2a24d","to":"9f4981531fda132e83c44680787dfa7ee31e4f8d","value":1000010},{"type":2,"from":"4af4114f73d1c1c903ac9e0361b379d1291808a2","to":"9f4981531fda132e83c44680787dfa7ee31e4f8d","value":1000011}],"Error":""},"receipt":{"gasUsed":"0x34d30","status":"0x1","logs":[{"address":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f","0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d"],"data":"0x0000000000000000000000000000000000000000000000006a8313d60b1f8001"},{"address":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d","0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f"],"data":"0x000000000000000000000000000000000000000000000000000308fd0e798ac0"},{"address":"0x479CC461fEcd078F766eCc58533D6F69580CF3AC","topics":["0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3","0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f","0x0000000000000000000000000000000000000000000000000000000000000000","0x5af266c0a89a07c1917deaa024414577e6c3c31c8907d079e13eb448c082594f"],"data":"0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000000000000000000000000006a8313d60b1f8001000000000000000000000000000000000000000000000000000308fd0e798ac0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e083a16f4b092c5729a49f9c3ed3cc171bb3d3d0c22e20b1de6063c32f399ac"},{"address":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b","0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d"],"data":"0x00000000000000000000000000000000000000000000000000031855667df7a8"},{"address":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d","0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b"],"data":"0x0000000000000000000000000000000000000000000000006a8313d60b1f606b"},{"address":"0x479CC461fEcd078F766eCc58533D6F69580CF3AC","topics":["0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3","0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b","0x0000000000000000000000000000000000000000000000000000000000000000","0xb0b69dad58df6032c3b266e19b1045b19c87acd2c06fb0c598090f44b8e263aa"],"data":"0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f1100000000000000000000000000000000000000000000000000031855667df7a80000000000000000000000000000000000000000000000006a8313d60b1f606b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f2b0d62c44ed08f2a5adef40c875d20310a42a9d4f488bd26323256fe01c7f48"}]}},"tokenTransfers":[{"type":"ERC20","from":"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f","to":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","name":"Contract 13","symbol":"S13","decimals":18,"value":"7675000000000000001"},{"type":"ERC20","from":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","to":"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","name":"Contract 74","symbol":"S74","decimals":12,"value":"854307892726464"},{"type":"ERC20","from":"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b","to":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","name":"Contract 74","symbol":"S74","decimals":12,"value":"871180000950184"},{"type":"ERC20","from":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","to":"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","name":"Contract 13","symbol":"S13","decimals":18,"value":"7674999999999991915"}],"ethereumSpecific":{"status":1,"nonce":122742,"gasLimit":250000,"gasUsed":216368,"gasPrice":"1000000000","data":"0x4f15078700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000a5ef5a7656bfb0000000000000000000000000000000000000000000000000000004ba78398d5c5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfe0b9579b4ecf7a2801880f644009a324671a79754ea57c3a103c6e70d3dbef6ba69a08000000000000000000000000000000000000000000000000004f937d86afb90000000000000000000000000000000000000000000000000ab280fd8037d500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfb784b7c1f3fbe8b75484603ab8adc58aaee3a46245a6579fac7077b5570018b4e0d4eb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000308fd0e798ac00000000000000000000000000000000000000000000000006a8313d60b1f606b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000001b00000000000000000000000000000000000000000000000000000000000000029de0ccec59e8948e3d905b40e5542335ebc1eb4674db517d2f6392ec7fdeb3d45f3449d313ee2589819c6c79eb1c1b047adae68565c1608e3a1d1d70823febb0000000000000000000000000000000000000000000000000000000000000000234d06fe17f1202e8b07177a30eb64d14adc08cdb3fa1b3e3e0bea0f9672c02175b77c01c51d3c7e460723b27ecbc7801fd6482559a8c9999593f9a4d149c7384","parsedData":{"methodId":"0x4f150787","name":""}}}],"nonce":"123","tokens":[{"type":"ERC20","name":"Contract 13","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","transfers":1,"symbol":"S13","decimals":18,"balance":"1000123013"},{"type":"ERC721","name":"Contract 205","contract":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","transfers":1,"symbol":"S205","decimals":18,"ids":["1"]},{"type":"ERC20","name":"Contract 74","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","transfers":1,"symbol":"S74","decimals":12,"balance":"1000123074"}],"addressAliases":{"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b":{"Type":"ENS","Alias":"address7b.eth"},"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9":{"Type":"Contract","Alias":"Contract 205"}}}`, + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b","balance":"123450123","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"transactions":[{"txid":"0xca7628be5c80cda77163729ec63d218ee868a399d827a4682a478c6f48a6e22a","vin":[{"n":0,"addresses":["0x837E3f699d85a4b0B99894567e9233dFB1DcB081"],"isAddress":true}],"vout":[{"value":"0","n":0,"addresses":["0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9"],"isAddress":true}],"blockHeight":-1,"confirmations":0,"blockTime":0,"value":"0","fees":"87945000410410","rbf":true,"coinSpecificData":{"tx":{"nonce":"0x2","gasPrice":"0x59682f07","gas":"0x173a9","to":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","value":"0x0","input":"0x23b872dd000000000000000000000000837e3f699d85a4b0b99894567e9233dfb1dcb0810000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000000000000000000000000000000000000000000001","hash":"0xca7628be5c80cda77163729ec63d218ee868a399d827a4682a478c6f48a6e22a","blockNumber":"0xb33b9f","from":"0x837E3f699d85a4b0B99894567e9233dFB1DcB081","transactionIndex":"0x1"},"receipt":{"gasUsed":"0xe506","status":"0x1","logs":[{"address":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","topics":["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925","0x000000000000000000000000837e3f699d85a4b0b99894567e9233dfb1dcb081","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000001"],"data":"0x"},{"address":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000837e3f699d85a4b0b99894567e9233dfb1dcb081","0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b","0x0000000000000000000000000000000000000000000000000000000000000001"],"data":"0x"}]}},"tokenTransfers":[{"type":"ERC721","standard":"ERC721","from":"0x837E3f699d85a4b0B99894567e9233dFB1DcB081","to":"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b","contract":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","name":"Contract 205","symbol":"S205","decimals":18,"value":"1"}],"ethereumSpecific":{"status":1,"nonce":2,"gasLimit":95145,"gasUsed":58630,"gasPrice":"1500000007","data":"0x23b872dd000000000000000000000000837e3f699d85a4b0b99894567e9233dfb1dcb0810000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000000000000000000000000000000000000000000001","parsedData":{"methodId":"0x23b872dd","name":""}}},{"txid":"0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2","vin":[{"n":0,"addresses":["0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D"],"isAddress":true}],"vout":[{"value":"0","n":0,"addresses":["0x479CC461fEcd078F766eCc58533D6F69580CF3AC"],"isAddress":true}],"blockHeight":-1,"confirmations":0,"blockTime":0,"value":"0","fees":"216368000000000","rbf":true,"coinSpecificData":{"tx":{"nonce":"0x1df76","gasPrice":"0x3b9aca00","gas":"0x3d090","to":"0x479CC461fEcd078F766eCc58533D6F69580CF3AC","value":"0x0","input":"0x4f15078700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000a5ef5a7656bfb0000000000000000000000000000000000000000000000000000004ba78398d5c5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfe0b9579b4ecf7a2801880f644009a324671a79754ea57c3a103c6e70d3dbef6ba69a08000000000000000000000000000000000000000000000000004f937d86afb90000000000000000000000000000000000000000000000000ab280fd8037d500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfb784b7c1f3fbe8b75484603ab8adc58aaee3a46245a6579fac7077b5570018b4e0d4eb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000308fd0e798ac00000000000000000000000000000000000000000000000006a8313d60b1f606b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000001b00000000000000000000000000000000000000000000000000000000000000029de0ccec59e8948e3d905b40e5542335ebc1eb4674db517d2f6392ec7fdeb3d45f3449d313ee2589819c6c79eb1c1b047adae68565c1608e3a1d1d70823febb0000000000000000000000000000000000000000000000000000000000000000234d06fe17f1202e8b07177a30eb64d14adc08cdb3fa1b3e3e0bea0f9672c02175b77c01c51d3c7e460723b27ecbc7801fd6482559a8c9999593f9a4d149c7384","hash":"0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2","blockNumber":"0x41eee9","from":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","transactionIndex":"0x24"},"internalData":{"type":1,"contract":"0d0f936ee4c93e25944694d6c121de94d9760f11","transfers":[{"type":0,"from":"4bda106325c335df99eab7fe363cac8a0ba2a24d","to":"9f4981531fda132e83c44680787dfa7ee31e4f8d","value":1000010},{"type":2,"from":"4af4114f73d1c1c903ac9e0361b379d1291808a2","to":"9f4981531fda132e83c44680787dfa7ee31e4f8d","value":1000011}],"Error":""},"receipt":{"gasUsed":"0x34d30","status":"0x1","logs":[{"address":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f","0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d"],"data":"0x0000000000000000000000000000000000000000000000006a8313d60b1f8001"},{"address":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d","0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f"],"data":"0x000000000000000000000000000000000000000000000000000308fd0e798ac0"},{"address":"0x479CC461fEcd078F766eCc58533D6F69580CF3AC","topics":["0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3","0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f","0x0000000000000000000000000000000000000000000000000000000000000000","0x5af266c0a89a07c1917deaa024414577e6c3c31c8907d079e13eb448c082594f"],"data":"0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000000000000000000000000006a8313d60b1f8001000000000000000000000000000000000000000000000000000308fd0e798ac0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e083a16f4b092c5729a49f9c3ed3cc171bb3d3d0c22e20b1de6063c32f399ac"},{"address":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b","0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d"],"data":"0x00000000000000000000000000000000000000000000000000031855667df7a8"},{"address":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d","0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b"],"data":"0x0000000000000000000000000000000000000000000000006a8313d60b1f606b"},{"address":"0x479CC461fEcd078F766eCc58533D6F69580CF3AC","topics":["0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3","0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b","0x0000000000000000000000000000000000000000000000000000000000000000","0xb0b69dad58df6032c3b266e19b1045b19c87acd2c06fb0c598090f44b8e263aa"],"data":"0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f1100000000000000000000000000000000000000000000000000031855667df7a80000000000000000000000000000000000000000000000006a8313d60b1f606b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f2b0d62c44ed08f2a5adef40c875d20310a42a9d4f488bd26323256fe01c7f48"}]}},"tokenTransfers":[{"type":"ERC20","standard":"ERC20","from":"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f","to":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","name":"Contract 13","symbol":"S13","decimals":18,"value":"7675000000000000001"},{"type":"ERC20","standard":"ERC20","from":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","to":"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","name":"Contract 74","symbol":"S74","decimals":12,"value":"854307892726464"},{"type":"ERC20","standard":"ERC20","from":"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b","to":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","name":"Contract 74","symbol":"S74","decimals":12,"value":"871180000950184"},{"type":"ERC20","standard":"ERC20","from":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","to":"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","name":"Contract 13","symbol":"S13","decimals":18,"value":"7674999999999991915"}],"ethereumSpecific":{"status":1,"nonce":122742,"gasLimit":250000,"gasUsed":216368,"gasPrice":"1000000000","data":"0x4f15078700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000000000000004af4114f73d1c1c903ac9e0361b379d1291808a20000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000a5ef5a7656bfb0000000000000000000000000000000000000000000000000000004ba78398d5c5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfe0b9579b4ecf7a2801880f644009a324671a79754ea57c3a103c6e70d3dbef6ba69a08000000000000000000000000000000000000000000000000004f937d86afb90000000000000000000000000000000000000000000000000ab280fd8037d500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000166cfb784b7c1f3fbe8b75484603ab8adc58aaee3a46245a6579fac7077b5570018b4e0d4eb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000308fd0e798ac00000000000000000000000000000000000000000000000006a8313d60b1f606b0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000001b00000000000000000000000000000000000000000000000000000000000000029de0ccec59e8948e3d905b40e5542335ebc1eb4674db517d2f6392ec7fdeb3d45f3449d313ee2589819c6c79eb1c1b047adae68565c1608e3a1d1d70823febb0000000000000000000000000000000000000000000000000000000000000000234d06fe17f1202e8b07177a30eb64d14adc08cdb3fa1b3e3e0bea0f9672c02175b77c01c51d3c7e460723b27ecbc7801fd6482559a8c9999593f9a4d149c7384","parsedData":{"methodId":"0x4f150787","name":""}}}],"nonce":"123","tokens":[{"type":"ERC20","standard":"ERC20","name":"Contract 13","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","transfers":1,"symbol":"S13","decimals":18,"balance":"1000123013"},{"type":"ERC721","standard":"ERC721","name":"Contract 205","contract":"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9","transfers":1,"symbol":"S205","decimals":18,"ids":["1"]},{"type":"ERC20","standard":"ERC20","name":"Contract 74","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","transfers":1,"symbol":"S74","decimals":12,"balance":"1000123074"}],"addressAliases":{"0x7B62EB7fe80350DC7EC945C0B73242cb9877FB1b":{"Type":"ENS","Alias":"address7b.eth"},"0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9":{"Type":"Contract","Alias":"Contract 205"}}}`, }, }, { @@ -88,7 +90,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"txid":"0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101","vin":[{"n":0,"addresses":["0x20cD153de35D469BA46127A0C8F18626b59a256A"],"isAddress":true}],"vout":[{"value":"0","n":0,"addresses":["0x4af4114F73d1c1C903aC9E0361b379D1291808A2"],"isAddress":true}],"blockHeight":-1,"confirmations":0,"blockTime":0,"value":"0","fees":"2081000000000000","rbf":true,"coinSpecificData":{"tx":{"nonce":"0xd0","gasPrice":"0x9502f9000","gas":"0x130d5","to":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","value":"0x0","input":"0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000","hash":"0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101","blockNumber":"0x41eee8","from":"0x20cD153de35D469BA46127A0C8F18626b59a256A","transactionIndex":"0x0"},"internalData":{"type":0,"transfers":[{"type":1,"from":"9f4981531fda132e83c44680787dfa7ee31e4f8d","to":"4af4114f73d1c1c903ac9e0361b379d1291808a2","value":1000000},{"type":0,"from":"3e3a3d69dc66ba10737f531ed088954a9ec89d97","to":"9f4981531fda132e83c44680787dfa7ee31e4f8d","value":1000001},{"type":0,"from":"3e3a3d69dc66ba10737f531ed088954a9ec89d97","to":"3e3a3d69dc66ba10737f531ed088954a9ec89d97","value":1000002}],"Error":""},"receipt":{"gasUsed":"0xcb39","status":"0x1","logs":[{"address":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x00000000000000000000000020cd153de35d469ba46127a0c8f18626b59a256a","0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f"],"data":"0x00000000000000000000000000000000000000000000021e19e0c9bab2400000"}]}},"tokenTransfers":[{"type":"ERC20","from":"0x20cD153de35D469BA46127A0C8F18626b59a256A","to":"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","name":"Contract 74","symbol":"S74","decimals":12,"value":"10000000000000000000000"}],"ethereumSpecific":{"status":1,"nonce":208,"gasLimit":78037,"gasUsed":52025,"gasPrice":"40000000000","data":"0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000","parsedData":{"methodId":"0xa9059cbb","name":"Transfer","function":"transfer(address, uint256)","params":[{"type":"address","values":["0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f"]},{"type":"uint256","values":["10000000000000000000000"]}]}},"addressAliases":{"0x20cD153de35D469BA46127A0C8F18626b59a256A":{"Type":"ENS","Alias":"address20.eth"},"0x4af4114F73d1c1C903aC9E0361b379D1291808A2":{"Type":"Contract","Alias":"Contract 74"}}}`, + `{"txid":"0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101","vin":[{"n":0,"addresses":["0x20cD153de35D469BA46127A0C8F18626b59a256A"],"isAddress":true}],"vout":[{"value":"0","n":0,"addresses":["0x4af4114F73d1c1C903aC9E0361b379D1291808A2"],"isAddress":true}],"blockHeight":-1,"confirmations":0,"blockTime":0,"value":"0","fees":"2081000000000000","rbf":true,"coinSpecificData":{"tx":{"nonce":"0xd0","gasPrice":"0x9502f9000","maxPriorityFeePerGas":"0x9502f9001","maxFeePerGas":"0x9502f9002","baseFeePerGas":"0x9502f9003","gas":"0x130d5","to":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","value":"0x0","input":"0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000","hash":"0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101","blockNumber":"0x41eee8","from":"0x20cD153de35D469BA46127A0C8F18626b59a256A","transactionIndex":"0x0"},"internalData":{"type":0,"transfers":[{"type":1,"from":"9f4981531fda132e83c44680787dfa7ee31e4f8d","to":"4af4114f73d1c1c903ac9e0361b379d1291808a2","value":1000000},{"type":0,"from":"3e3a3d69dc66ba10737f531ed088954a9ec89d97","to":"9f4981531fda132e83c44680787dfa7ee31e4f8d","value":1000001},{"type":0,"from":"3e3a3d69dc66ba10737f531ed088954a9ec89d97","to":"3e3a3d69dc66ba10737f531ed088954a9ec89d97","value":1000002}],"Error":""},"receipt":{"gasUsed":"0xcb39","status":"0x1","logs":[{"address":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x00000000000000000000000020cd153de35d469ba46127a0c8f18626b59a256a","0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f"],"data":"0x00000000000000000000000000000000000000000000021e19e0c9bab2400000"}]}},"tokenTransfers":[{"type":"ERC20","standard":"ERC20","from":"0x20cD153de35D469BA46127A0C8F18626b59a256A","to":"0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","name":"Contract 74","symbol":"S74","decimals":12,"value":"10000000000000000000000"}],"ethereumSpecific":{"status":1,"nonce":208,"gasLimit":78037,"gasUsed":52025,"gasPrice":"40000000000","maxPriorityFeePerGas":"40000000001","maxFeePerGas":"40000000002","baseFeePerGas":"40000000003","data":"0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000","parsedData":{"methodId":"0xa9059cbb","name":"Transfer","function":"transfer(address, uint256)","params":[{"type":"address","values":["0x555Ee11FBDDc0E49A9bAB358A8941AD95fFDB48f"]},{"type":"uint256","values":["10000000000000000000000"]}]}},"addressAliases":{"0x20cD153de35D469BA46127A0C8F18626b59a256A":{"Type":"ENS","Alias":"address20.eth"},"0x4af4114F73d1c1C903aC9E0361b379D1291808A2":{"Type":"Contract","Alias":"Contract 74"}}}`, }, }, { @@ -97,7 +99,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1574344800,"rates":{"usd":7814.5}}`, + `{"ts":1574380800,"rates":{"usd":7914.5}}`, }, }, { @@ -106,16 +108,16 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1574344800,"rates":{"usd":6251.6}}`, + `{"ts":1574380800,"rates":{"usd":1.2}}`, }, }, { name: "apiFiatRates get token rate by timestamp for all currencies", r: newGetRequest(ts.URL + "/api/v2/tickers?timestamp=1574340000&token=0xA4DD6Bc15Be95Af55f0447555c8b6aA3088562f3"), - status: http.StatusBadRequest, + status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"error":"Rates for token only for a single currency"}`, + `{"ts":1574380800,"rates":{"eur":1.0816754,"usd":1.2}}`, }, }, { @@ -132,6 +134,38 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { performHttpTests(tests, t, ts) } +var websocketTestsEthereumType = []websocketTest{ + { + name: "websocket getInfo", + req: websocketReq{ + Method: "getInfo", + }, + want: `{"id":"0","data":{"name":"Fakecoin","shortcut":"FAKE","network":"FAKE","decimals":18,"version":"unknown","bestHeight":4321001,"bestHash":"0x2b57e15e93a0ed197417a34c2498b7187df79099572c04a6b6e6ff418f74e6ee","block0Hash":"","testnet":true,"backend":{"version":"001001","subversion":"/Fakecoin:0.0.1/"}}}`, + }, + { + name: "websocket rpcCall", + req: websocketReq{ + Method: "rpcCall", + Params: WsRpcCallReq{ + To: "0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9", + Data: "0x4567", + }, + }, + want: `{"id":"1","data":{"data":"0x4567abcd"}}`, + }, + { + name: "websocket sendTransaction hex format", + req: websocketReq{ + Method: "sendTransaction", + Params: WsSendTransactionReq{ + Hex: "123456", + DisableAlternativeRPC: true, + }, + }, + want: `{"id":"2","data":{"result":"9876"}}`, + }, +} + func initEthereumTypeDB(d *db.RocksDB) error { // add 0xa9059cbb transfer(address,uint256) signature wb := grocksdb.NewWriteBatch() @@ -147,7 +181,7 @@ func initEthereumTypeDB(d *db.RocksDB) error { // initTestFiatRatesEthereumType initializes test data for /api/v2/tickers endpoint func initTestFiatRatesEthereumType(d *db.RocksDB) error { - if err := insertFiatRate("20180320020000", map[string]float32{ + if err := insertFiatRate("20180320000000", map[string]float32{ "usd": 2000.0, "eur": 1300.0, }, map[string]float32{ @@ -156,7 +190,7 @@ func initTestFiatRatesEthereumType(d *db.RocksDB) error { }, d); err != nil { return err } - if err := insertFiatRate("20180320030000", map[string]float32{ + if err := insertFiatRate("20180321000000", map[string]float32{ "usd": 2001.0, "eur": 1301.0, }, map[string]float32{ @@ -165,7 +199,7 @@ func initTestFiatRatesEthereumType(d *db.RocksDB) error { }, d); err != nil { return err } - if err := insertFiatRate("20180320040000", map[string]float32{ + if err := insertFiatRate("20180322000000", map[string]float32{ "usd": 2002.0, "eur": 1302.0, }, map[string]float32{ @@ -174,7 +208,7 @@ func initTestFiatRatesEthereumType(d *db.RocksDB) error { }, d); err != nil { return err } - if err := insertFiatRate("20180321055521", map[string]float32{ + if err := insertFiatRate("20180323000000", map[string]float32{ "usd": 2003.0, "eur": 1303.0, }, map[string]float32{ @@ -183,7 +217,7 @@ func initTestFiatRatesEthereumType(d *db.RocksDB) error { }, d); err != nil { return err } - if err := insertFiatRate("20191121140000", map[string]float32{ + if err := insertFiatRate("20190321000000", map[string]float32{ "usd": 7814.5, "eur": 7100.0, }, map[string]float32{ @@ -193,14 +227,31 @@ func initTestFiatRatesEthereumType(d *db.RocksDB) error { }, d); err != nil { return err } - return insertFiatRate("20191121143015", map[string]float32{ + if err := insertFiatRate("20191122000000", map[string]float32{ "usd": 7914.5, "eur": 7134.1, }, map[string]float32{ "0xdac17f958d2ee523a2206206994597c13d831ec7": 7914.1, "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": 599.0, "0xa4dd6bc15be95af55f0447555c8b6aa3088562f3": 1.2, - }, d) + }, d); err != nil { + return err + } + + return d.FiatRatesStoreSpecialTickers("CurrentTickers", &[]common.CurrencyRatesTicker{ + { + Timestamp: time.Unix(1592821931, 0), + Rates: map[string]float32{ + "usd": 8914.5, + "eur": 8134.1, + }, + TokenRates: map[string]float32{ + "0xdac17f958d2ee523a2206206994597c13d831ec7": 8914.1, + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": 899.0, + "0xa4dd6bc15be95af55f0447555c8b6aa3088562f3": 8.2, + }, + }, + }) } func Test_PublicServer_EthereumType(t *testing.T) { @@ -211,7 +262,7 @@ func Test_PublicServer_EthereumType(t *testing.T) { glog.Fatal("fakechain: ", err) } - s, dbpath := setupPublicHTTPServer(parser, chain, t) + s, dbpath := setupPublicHTTPServer(parser, chain, t, false) defer closeAndDestroyPublicServer(t, s, dbpath) s.ConnectFullPublicInterface() // take the handler of the public server and pass it to the test server @@ -219,4 +270,5 @@ func Test_PublicServer_EthereumType(t *testing.T) { defer ts.Close() httpTestsEthereumType(t, ts) + runWebsocketTests(t, ts, websocketTestsEthereumType) } diff --git a/server/public_test.go b/server/public_test.go index 7957f5d5d6..bea3e69024 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -4,13 +4,11 @@ package server import ( "encoding/json" - "html/template" - "io/ioutil" + "io" "net/http" "net/http/httptest" "net/url" "os" - "reflect" "strconv" "strings" "testing" @@ -26,6 +24,7 @@ import ( "github.com/trezor/blockbook/bchain/coins/btc" "github.com/trezor/blockbook/common" "github.com/trezor/blockbook/db" + "github.com/trezor/blockbook/fiat" "github.com/trezor/blockbook/tests/dbtestdata" ) @@ -39,16 +38,16 @@ func TestMain(m *testing.M) { os.Exit(c) } -func setupRocksDB(parser bchain.BlockChainParser, chain bchain.BlockChain, t *testing.T) (*db.RocksDB, *common.InternalState, string) { - tmp, err := ioutil.TempDir("", "testdb") +func setupRocksDB(parser bchain.BlockChainParser, chain bchain.BlockChain, t *testing.T, extendedIndex bool, config *common.Config) (*db.RocksDB, *common.InternalState, string) { + tmp, err := os.MkdirTemp("", "testdb") if err != nil { t.Fatal(err) } - d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil) + d, err := db.NewRocksDB(tmp, 100000, -1, parser, nil, extendedIndex) if err != nil { t.Fatal(err) } - is, err := d.LoadInternalState("fakecoin") + is, err := d.LoadInternalState(config) if err != nil { t.Fatal(err) } @@ -95,17 +94,27 @@ func setupRocksDB(parser bchain.BlockChainParser, chain bchain.BlockChain, t *te var metrics *common.Metrics -func setupPublicHTTPServer(parser bchain.BlockChainParser, chain bchain.BlockChain, t *testing.T) (*PublicServer, string) { - d, is, path := setupRocksDB(parser, chain, t) - // setup internal state and match BestHeight to test data - is.Coin = "Fakecoin" - is.CoinLabel = "Fake Coin" - is.CoinShortcut = "FAKE" +func setupPublicHTTPServer(parser bchain.BlockChainParser, chain bchain.BlockChain, t *testing.T, extendedIndex bool) (*PublicServer, string) { + // config with mocked CoinGecko API + config := common.Config{ + CoinName: "Fakecoin", + CoinLabel: "Fake Coin", + CoinShortcut: "FAKE", + FiatRates: "coingecko", + FiatRatesParams: `{"url": "none", "coin": "ethereum","platformIdentifier": "ethereum","platformVsCurrency": "usd","periodSeconds": 60}`, + } + + // add block golomb filters with extended index + if extendedIndex { + config.BlockGolombFilterP = 20 + } + + d, is, path := setupRocksDB(parser, chain, t, extendedIndex, &config) var err error // metrics can be setup only once if metrics == nil { - metrics, err = common.GetMetrics("Fakecoin") + metrics, err = common.GetMetrics("Fakecoin" + strconv.FormatBool(extendedIndex)) if err != nil { glog.Fatal("metrics: ", err) } @@ -122,8 +131,13 @@ func setupPublicHTTPServer(parser bchain.BlockChainParser, chain bchain.BlockCha glog.Fatal("txCache: ", err) } + fiatRates, err := fiat.NewFiatRates(d, &config, nil, nil) + if err != nil { + glog.Fatal("fiatRates ", err) + } + // s.Run is never called, binding can be to any port - s, err := NewPublicServer("localhost:12345", "", d, chain, mempool, txCache, "", metrics, is, false, false) + s, err := NewPublicServer("localhost:12345", "", d, chain, mempool, txCache, "", metrics, is, fiatRates, false) if err != nil { t.Fatal(err) } @@ -169,12 +183,12 @@ func newPostRequest(u string, body string) *http.Request { } func insertFiatRate(date string, rates map[string]float32, tokenRates map[string]float32, d *db.RocksDB) error { - convertedDate, err := db.FiatRatesConvertDate(date) + convertedDate, err := time.Parse("20060102150405", date) if err != nil { return err } ticker := &common.CurrencyRatesTicker{ - Timestamp: *convertedDate, + Timestamp: convertedDate, Rates: rates, TokenRates: tokenRates, } @@ -188,37 +202,37 @@ func insertFiatRate(date string, rates map[string]float32, tokenRates map[string // initTestFiatRates initializes test data for /api/v2/tickers endpoint func initTestFiatRates(d *db.RocksDB) error { - if err := insertFiatRate("20180320020000", map[string]float32{ + if err := insertFiatRate("20180320000000", map[string]float32{ "usd": 2000.0, "eur": 1300.0, }, nil, d); err != nil { return err } - if err := insertFiatRate("20180320030000", map[string]float32{ + if err := insertFiatRate("20180321000000", map[string]float32{ "usd": 2001.0, "eur": 1301.0, }, nil, d); err != nil { return err } - if err := insertFiatRate("20180320040000", map[string]float32{ + if err := insertFiatRate("20180322000000", map[string]float32{ "usd": 2002.0, "eur": 1302.0, }, nil, d); err != nil { return err } - if err := insertFiatRate("20180321055521", map[string]float32{ + if err := insertFiatRate("20180324000000", map[string]float32{ "usd": 2003.0, "eur": 1303.0, }, nil, d); err != nil { return err } - if err := insertFiatRate("20191121140000", map[string]float32{ + if err := insertFiatRate("20191121000000", map[string]float32{ "usd": 7814.5, "eur": 7100.0, }, nil, d); err != nil { return err } - return insertFiatRate("20191121143015", map[string]float32{ + return insertFiatRate("20191122000000", map[string]float32{ "usd": 7914.5, "eur": 7134.1, }, nil, d) @@ -246,7 +260,7 @@ func performHttpTests(tests []httpTests, t *testing.T, ts *httptest.Server) { if resp.Header["Content-Type"][0] != tt.contentType { t.Errorf("Content-Type = %v, want %v", resp.Header["Content-Type"][0], tt.contentType) } - bb, err := ioutil.ReadAll(resp.Body) + bb, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } @@ -269,7 +283,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Transaction

fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input0 FAKE
Total Output13.60030331 FAKE
Fees0 FAKE
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
Raw Transaction
`, + `Trezor Fake Coin Explorer

Transaction

fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input0 FAKE
Total Output13.60030331 FAKE
Fees0 FAKE
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -278,7 +292,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Address

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz

0.00012345 FAKE

Confirmed
Total Received0.0002469 FAKE
Total Sent0.00012345 FAKE
Final Balance0.00012345 FAKE
No. Transactions2

Transactions

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs
 
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`, + `Trezor Fake Coin Explorer

Address

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz

0.00012345 FAKE

Confirmed
Total Received0.00024690 FAKE
Total Sent0.00012345 FAKE
Final Balance0.00012345 FAKE
No. Transactions2

Transactions

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs
 
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`, }, }, { @@ -287,7 +301,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Transaction

3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input3172.83951062 FAKE
Total Output3172.83951 FAKE
Fees0.00000062 FAKE
Raw Transaction
`, + `Trezor Fake Coin Explorer

Transaction

3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input3172.83951062 FAKE
Total Output3172.83951000 FAKE
Fees0.00000062 FAKE
`, }, }, { @@ -296,7 +310,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Error

Transaction not found

`, + `Trezor Fake Coin Explorer

Error

Transaction not found

`, }, }, { @@ -305,7 +319,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Blocks

HeightHashTimestampTransactionsSize
22549400000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b61639 days 11 hours ago42345678
2254930000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e29971640 days 9 hours ago21234567
`, + `Trezor Fake Coin Explorer

Blocks

HeightHashTimestampTransactionsSize
22549400000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b61639 days 11 hours ago42345678
2254930000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e29971640 days 9 hours ago21234567
`, }, }, { @@ -314,7 +328,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, + `Trezor Fake Coin Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -323,8 +337,8 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Application status

Synchronization with backend is disabled, the state of index is not up to date.

`, + `Trezor Fake Coin Explorer

Application status

Synchronization with backend is disabled, the state of index is not up to date.

Blockbook

CoinFakecoin
Host
Version / Commit / Buildunknown / unknown / unknown
Synchronized
true
Last Block225494
Last Block Update`, - `
Mempool in Sync
false
Last Mempool Update
Transactions in Mempool0
Size On Disk
`, `

Blockbook

CoinFakecoin
Host
Version / Commit / Buildunknown / unknown / unknown
Synchronized
true
Last Block225494
Last Block Update`, + `
Mempool in Sync
false
Last Mempool Update
Transactions in Mempool0
Current Fiat rates

Backend

Chainfakecoin
Version001001
Subversion/Fakecoin:0.0.1/
Last Block2
Difficulty
Blockbook - blockchain indexer for Trezor Suite https://trezor.io/trezor-suite. Do not use for any other purpose.
`, }, }, @@ -334,7 +348,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, + `Trezor Fake Coin Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -343,7 +357,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, + `Trezor Fake Coin Explorer

Block

225494
00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
Transactions4
Height225494
Confirmations1
Timestamp1639 days 11 hours ago
Size (bytes)2345678
Version
Merkle Root
Nonce
Bits
Difficulty

Transactions

 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -352,7 +366,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Transaction

fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input0 FAKE
Total Output13.60030331 FAKE
Fees0 FAKE
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
Raw Transaction
`, + `Trezor Fake Coin Explorer

Transaction

fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db
Mined Time1639 days 11 hours ago
In Block00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6
In Block Height225494
Total Input0 FAKE
Total Output13.60030331 FAKE
Fees0 FAKE
No Inputs (Newly Generated Coins)
 
Unparsed address0 FAKE×
`, }, }, { @@ -361,7 +375,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Address

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz

0.00012345 FAKE

Confirmed
Total Received0.0002469 FAKE
Total Sent0.00012345 FAKE
Final Balance0.00012345 FAKE
No. Transactions2

Transactions

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs
 
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`, + `Trezor Fake Coin Explorer

Address

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz

0.00012345 FAKE

Confirmed
Total Received0.00024690 FAKE
Total Sent0.00012345 FAKE
Final Balance0.00012345 FAKE
No. Transactions2

Transactions

mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
 
OP_RETURN 2020f1686f6a200 FAKE×
No Inputs
 
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE
mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz0.00012345 FAKE×
`, }, }, { @@ -370,7 +384,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

XPUB

upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q

1186.419755 FAKE

Confirmed
Total Received1186.41975501 FAKE
Total Sent0.00000001 FAKE
Final Balance1186.419755 FAKE
No. Transactions2
Used XPUB Addresses2
XPUB Addresses with Balance
AddressBalanceTxsPath
2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu1186.419755 FAKE1m/49'/1'/33'/1/3

Transactions

`, + `Trezor Fake Coin Explorer

XPUB

upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q

1186.419755 FAKE

Confirmed
Total Received1186.41975501 FAKE
Total Sent0.00000001 FAKE
Final Balance1186.41975500 FAKE
No. Transactions2
Used XPUB Addresses2
XPUB Addresses with Balance
AddressBalanceTxsPath
2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu1186.41975500 FAKE1m/49'/1'/33'/1/3

Transactions

`, }, }, { @@ -379,7 +393,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

XPUB

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

0 FAKE

Confirmed
Total Received0 FAKE
Total Sent0 FAKE
Final Balance0 FAKE
No. Transactions0
Used XPUB Addresses0
XPUB Addresses with Balance
No addresses
`, + `Trezor Fake Coin Explorer

XPUB

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

0 FAKE

Confirmed
Total Received0 FAKE
Total Sent0 FAKE
Final Balance0 FAKE
No. Transactions0
Used XPUB Addresses0
XPUB Addresses with Balance
No addresses
`, }, }, { @@ -388,7 +402,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Error

No matching records found for '1234'

`, + `Trezor Fake Coin Explorer

Error

No matching records found for '1234'

`, }, }, { @@ -397,7 +411,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Send Raw Transaction

`, + `Trezor Fake Coin Explorer

Send Raw Transaction

`, }, }, { @@ -406,7 +420,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "text/html; charset=utf-8", body: []string{ - `Trezor Fake Coin Explorer

Send Raw Transaction

Invalid data
`, + `Trezor Fake Coin Explorer

Send Raw Transaction

Invalid data
`, }, }, { @@ -486,12 +500,12 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { }, }, { - name: "apiFiatRates missing currency", + name: "apiFiatRates all currencies", r: newGetRequest(ts.URL + "/api/v2/tickers"), status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1574346615,"rates":{"eur":7134.1,"usd":7914.5}}`, + `{"ts":1574380800,"rates":{"eur":7134.1,"usd":7914.5}}`, }, }, { @@ -500,16 +514,16 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1574346615,"rates":{"usd":7914.5}}`, + `{"ts":1574380800,"rates":{"usd":7914.5}}`, }, }, { name: "apiFiatRates get rate by exact timestamp", - r: newGetRequest(ts.URL + "/api/v2/tickers?currency=usd×tamp=1574344800"), + r: newGetRequest(ts.URL + "/api/v2/tickers?currency=usd×tamp=1521545531"), status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1574344800,"rates":{"usd":7814.5}}`, + `{"ts":1521590400,"rates":{"usd":2001}}`, }, }, { @@ -545,25 +559,25 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1574344800,"rates":{"eur":7100}}`, + `{"ts":1574380800,"rates":{"eur":7134.1}`, }, }, { name: "apiMultiFiatRates all currencies", - r: newGetRequest(ts.URL + "/api/v2/multi-tickers?timestamp=1574344800,1574346615"), + r: newGetRequest(ts.URL + "/api/v2/multi-tickers?timestamp=1574344800,1521677000"), 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}}]`, + `[{"ts":1574380800,"rates":{"eur":7134.1,"usd":7914.5}},{"ts":1521849600,"rates":{"eur":1303,"usd":2003}}]`, }, }, { name: "apiMultiFiatRates get EUR rate", - r: newGetRequest(ts.URL + "/api/v2/multi-tickers?timestamp=1574344800,1574346615¤cy=eur"), + r: newGetRequest(ts.URL + "/api/v2/multi-tickers?timestamp=1521545531,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}}]`, + `[{"ts":1521590400,"rates":{"eur":1301}},{"ts":1574380800,"rates":{"eur":7134.1}}]`, }, }, { @@ -572,7 +586,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1521511200,"rates":{"usd":2000}}`, + `{"ts":1521504000,"rates":{"usd":2000}}`, }, }, { @@ -581,7 +595,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1521611721,"rates":{"usd":2003}}`, + `{"ts":1521676800,"rates":{"usd":2002}}`, }, }, { @@ -590,7 +604,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1574346615,"rates":{"eur":7134.1}}`, + `{"ts":1574380800,"rates":{"eur":7134.1}}`, }, }, { @@ -608,7 +622,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"ts":1574346615,"available_currencies":["eur","usd"]}`, + `{"ts":1574380800,"available_currencies":["eur","usd"]}`, }, }, { @@ -662,7 +676,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":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"],"usedTokens":2,"tokens":[{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"}]}`, + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"addrTxCount":3,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"],"usedTokens":2,"tokens":[{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"}]}`, }, }, { @@ -671,7 +685,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":"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":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"}]}`, + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"addrTxCount":3,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"],"usedTokens":2,"tokens":[{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"}]}`, }, }, { @@ -680,7 +694,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":"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}]}`, + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"addrTxCount":3,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"],"usedTokens":2,"tokens":[{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuWrWMzoBt8VDFNvPmpJf42M1GTUs85fPx","path":"m/49'/1'/33'/0/6","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuVZ2Ca6Da9zmYynt49Rx7uikAgubGcymF","path":"m/49'/1'/33'/0/7","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzRGWDUmrPP9HwYu4B43QGCTLwoop5cExa","path":"m/49'/1'/33'/0/8","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5C9EEWJzyBXhpyPHqa3UNed73Amsi5b3L","path":"m/49'/1'/33'/0/9","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzNawz2zjwq1L85GDE3YydEJGJYfXxaWkk","path":"m/49'/1'/33'/0/10","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N7NdeuAMgL57WE7QCeV2gTWi2Um8iAu5dA","path":"m/49'/1'/33'/0/11","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8JQEP6DSHEZHNsSDPA1gHMUq9YFndhkfV","path":"m/49'/1'/33'/0/12","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mvbn3YXqKZVpQKugaoQrfjSYPvz76RwZkC","path":"m/49'/1'/33'/0/13","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8MRNxCfwUY9TSW27X9ooGYtqgrGCfLRHx","path":"m/49'/1'/33'/0/14","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6HvwrHC113KYZAmCtJ9XJNWgaTcnFunCM","path":"m/49'/1'/33'/0/15","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEo3oNyHUoi7rmRWee7wki37jxPWsWCopJ","path":"m/49'/1'/33'/0/16","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mzm5KY8qdFbDHsQfy4akXbFvbR3FAwDuVo","path":"m/49'/1'/33'/0/17","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NGMwftmQCogp6XZNGvgiybz3WZysvsJzqC","path":"m/49'/1'/33'/0/18","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N3fJrrefndYjLGycvFFfYgevpZtcRKCkRD","path":"m/49'/1'/33'/0/19","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N1T7TnHBwfdpBoyw53EGUL7vuJmb2mU6jF","path":"m/49'/1'/33'/0/20","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N7HexL4dyAQc7Th4iqcCW4hZuyiZsLWf74","path":"m/49'/1'/33'/1/9","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NF6X5FDGWrQj4nQrfP6hA77zB5WAc1DGup","path":"m/49'/1'/33'/1/10","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4ZRPdvc7BVioBTohy4F6QtxreqcjNj26b","path":"m/49'/1'/33'/1/11","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mtfho1rLmevh4qTnkYWxZEFCWteDMtTcUF","path":"m/49'/1'/33'/1/12","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NFUCphKYvmMcNZRZrF261mRX6iADVB9Qms","path":"m/49'/1'/33'/1/13","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5kBNMB8qgxE4Y4f8J19fScsE49J4aNvoJ","path":"m/49'/1'/33'/1/14","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NANWCaefhCKdXMcW8NbZnnrFRDvhJN2wPy","path":"m/49'/1'/33'/1/15","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NFHw7Yo2Bz8D2wGAYHW9qidbZFLpfJ72qB","path":"m/49'/1'/33'/1/16","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NBDSsBgy5PpFniLCb1eAFHcSxgxwPSDsZa","path":"m/49'/1'/33'/1/17","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NDWCSQHogc7sCuc2WoYt9PX2i2i6a5k6dX","path":"m/49'/1'/33'/1/18","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8vNyDP7iSDjm3BKpXrbDjAxyphqfvnJz8","path":"m/49'/1'/33'/1/19","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4tFKLurSbMusAyq1tv4tzymVjveAFV1Vb","path":"m/49'/1'/33'/1/20","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NBx5WwjAr2cH6Yqrp3Vsf957HtRKwDUVdX","path":"m/49'/1'/33'/1/21","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NBu1seHTaFhQxbcW5L5BkZzqFLGmZqpxsa","path":"m/49'/1'/33'/1/22","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NCDLoea22jGsXuarfT1n2QyCUh6RFhAPnT","path":"m/49'/1'/33'/1/23","transfers":0,"decimals":8}]}`, }, }, { @@ -689,7 +703,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":"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}]}`, + `{"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","standard":"XPUBAddress","name":"tb1pswrqtykue8r89t9u4rprjs0gt4qzkdfuursfnvqaa3f2yql07zmq8s8a5u","path":"m/86'/1'/0'/0/0","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"tb1p8tvmvsvhsee73rhym86wt435qrqm92psfsyhy6a3n5gw455znnpqm8wald","path":"m/86'/1'/0'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"tb1p537ddhyuydg5c2v75xxmn6ac64yz4xns2x0gpdcwj5vzzzgrywlqlqwk43","path":"m/86'/1'/0'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"tb1pn2d0yjeedavnkd8z8lhm566p0f2utm3lgvxrsdehnl94y34txmts5s7t4c","path":"m/86'/1'/0'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"tb1p0pnd6ue5vryymvd28aeq3kdz6rmsdjqrq6eespgtg8wdgnxjzjksujhq4u","path":"m/86'/1'/0'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"tb1p29gpmd96hhgf7wj2vs03ca7x2xx39g8t6e0p55h2d5ssqs4fsj8qtx00wc","path":"m/86'/1'/0'/1/2","transfers":0,"decimals":8}]}`, }, }, { @@ -698,7 +712,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"usedTokens":2}`, + `{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"addrTxCount":3,"usedTokens":2}`, }, }, { @@ -707,7 +721,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"usedTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8}]}`, + `{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"addrTxCount":3,"usedTokens":2,"tokens":[{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8}]}`, }, }, { @@ -716,7 +730,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { status: http.StatusOK, contentType: "application/json; charset=utf-8", body: []string{ - `{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"usedTokens":2,"tokens":[{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"}]}`, + `{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"addrTxCount":3,"usedTokens":2,"tokens":[{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"}]}`, }, }, { @@ -725,7 +739,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,"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}]}`, + `{"page":1,"totalPages":1,"itemsOnPage":3,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"addrTxCount":3,"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","standard":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8}]}`, }, }, { @@ -779,7 +793,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","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"0","sent":"12345","sentToSelf":"0","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":1302,"usd":2002}}]`, }, }, { @@ -788,7 +802,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","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","sentToSelf":"9000","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":1302,"usd":2002}}]`, }, }, { @@ -797,7 +811,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","sentToSelf":"0","rates":{"eur":1301}},{"time":1521594000,"txs":1,"received":"9000","sent":"9876","sentToSelf":"9000","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":1302}}]`, }, }, { @@ -815,7 +829,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","sentToSelf":"0","rates":{"eur":1301,"usd":2001}},{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","sentToSelf":"118641975500","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":1302,"usd":2002}}]`, }, }, { @@ -842,7 +856,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","sentToSelf":"118641975500","rates":{"eur":1303,"usd":2003}}]`, + `[{"time":1521594000,"txs":1,"received":"118641975500","sent":"1","sentToSelf":"118641975500","rates":{"eur":1302,"usd":2002}}]`, }, }, { @@ -1004,438 +1018,492 @@ func socketioTestsBitcoinType(t *testing.T, ts *httptest.Server) { } } -func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { - type websocketReq struct { - ID string `json:"id"` - Method string `json:"method"` - Params interface{} `json:"params,omitempty"` - } - type websocketResp struct { - ID string `json:"id"` - } - url := strings.Replace(ts.URL, "http://", "ws://", 1) + "/websocket" - s, _, err := websocket.DefaultDialer.Dial(url, nil) - if err != nil { - t.Fatal(err) - } - defer s.Close() +type websocketReq struct { + ID string `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` +} +type websocketResp struct { + ID string `json:"id"` +} - tests := []struct { - name string - req websocketReq - want string - }{ - { - name: "websocket getInfo", - req: websocketReq{ - Method: "getInfo", - }, - 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", - req: websocketReq{ - Method: "getBlockHash", - Params: map[string]interface{}{ - "height": 225494, - }, - }, - want: `{"id":"1","data":{"hash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6"}}`, - }, - { - name: "websocket getAccountInfo xpub txs", - req: websocketReq{ - Method: "getAccountInfo", - Params: map[string]interface{}{ - "descriptor": dbtestdata.Xpub, - "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,"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 address", - req: websocketReq{ - Method: "getAccountInfo", - Params: map[string]interface{}{ - "descriptor": dbtestdata.Addr4, - "details": "txids", - }, - }, - 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 gap", - req: websocketReq{ - Method: "getAccountInfo", - Params: map[string]interface{}{ - "descriptor": dbtestdata.Xpub, - "details": "tokens", - "tokens": "derived", - "gap": 10, - }, - }, - want: `{"id":"4","data":{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"usedTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8},{"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":"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},{"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}]}}`, - }, - { - name: "websocket getAccountUtxo", - req: websocketReq{ - Method: "getAccountUtxo", - Params: map[string]interface{}{ - "descriptor": dbtestdata.Addr1, - }, - }, - want: `{"id":"5","data":[{"txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","vout":0,"value":"100000000","height":225493,"confirmations":2}]}`, - }, - { - name: "websocket getAccountUtxo", - req: websocketReq{ - Method: "getAccountUtxo", - Params: map[string]interface{}{ - "descriptor": dbtestdata.Addr4, - }, - }, - want: `{"id":"6","data":[]}`, - }, - { - name: "websocket getTransaction", - req: websocketReq{ - Method: "getTransaction", - Params: map[string]interface{}{ - "txid": dbtestdata.TxidB2T2, - }, - }, - want: `{"id":"7","data":{"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"}}`, - }, - { - name: "websocket getTransaction", - req: websocketReq{ - Method: "getTransaction", - Params: map[string]interface{}{ - "txid": "not a tx", - }, - }, - want: `{"id":"8","data":{"error":{"message":"Transaction 'not a tx' not found"}}}`, - }, - { - name: "websocket getTransactionSpecific", - req: websocketReq{ - Method: "getTransactionSpecific", - Params: map[string]interface{}{ - "txid": dbtestdata.TxidB2T2, - }, - }, - want: `{"id":"9","data":{"hex":"","txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","version":0,"locktime":0,"vin":[{"coinbase":"","txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":0,"scriptSig":{"hex":""},"sequence":0,"addresses":null},{"coinbase":"","txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":1,"scriptSig":{"hex":""},"sequence":0,"addresses":null}],"vout":[{"ValueSat":118641975500,"value":0,"n":0,"scriptPubKey":{"hex":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","addresses":null}},{"ValueSat":198641975500,"value":0,"n":1,"scriptPubKey":{"hex":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","addresses":null}}],"confirmations":1,"time":1521595678,"blocktime":1521595678,"vsize":400}}`, - }, - { - name: "websocket estimateFee", - req: websocketReq{ - Method: "estimateFee", - Params: map[string]interface{}{ - "blocks": []int{2, 5, 10, 20}, - "specific": map[string]interface{}{ - "conservative": false, - "txsize": 1234, - }, - }, - }, - 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{ - Method: "sendTransaction", - Params: map[string]interface{}{ - "hex": "123456", - }, - }, - want: `{"id":"12","data":{"result":"9876"}}`, - }, - { - name: "websocket subscribeNewBlock", - req: websocketReq{ - Method: "subscribeNewBlock", - }, - want: `{"id":"13","data":{"subscribed":true}}`, - }, - { - name: "websocket unsubscribeNewBlock", - req: websocketReq{ - Method: "unsubscribeNewBlock", - }, - want: `{"id":"14","data":{"subscribed":false}}`, - }, - { - name: "websocket subscribeAddresses", - req: websocketReq{ - Method: "subscribeAddresses", - Params: map[string]interface{}{ - "addresses": []string{dbtestdata.Addr1, dbtestdata.Addr2}, - }, - }, - want: `{"id":"15","data":{"subscribed":true}}`, - }, - { - name: "websocket unsubscribeAddresses", - req: websocketReq{ - Method: "unsubscribeAddresses", - }, - want: `{"id":"16","data":{"subscribed":false}}`, - }, - { - name: "websocket ping", - req: websocketReq{ - Method: "ping", - }, - want: `{"id":"17","data":{}}`, - }, - { - name: "websocket getCurrentFiatRates all currencies", - req: websocketReq{ - Method: "getCurrentFiatRates", - Params: map[string]interface{}{ - "currencies": []string{}, - }, - }, - want: `{"id":"18","data":{"ts":1574346615,"rates":{"eur":7134.1,"usd":7914.5}}}`, - }, - { - name: "websocket getCurrentFiatRates usd", - req: websocketReq{ - Method: "getCurrentFiatRates", - Params: map[string]interface{}{ - "currencies": []string{"usd"}, - }, - }, - want: `{"id":"19","data":{"ts":1574346615,"rates":{"usd":7914.5}}}`, - }, - { - name: "websocket getCurrentFiatRates eur", - req: websocketReq{ - Method: "getCurrentFiatRates", - Params: map[string]interface{}{ - "currencies": []string{"eur"}, - }, - }, - want: `{"id":"20","data":{"ts":1574346615,"rates":{"eur":7134.1}}}`, - }, - { - name: "websocket getCurrentFiatRates incorrect currency", - req: websocketReq{ - Method: "getCurrentFiatRates", - Params: map[string]interface{}{ - "currencies": []string{"does-not-exist"}, - }, - }, - want: `{"id":"21","data":{"error":{"message":"No tickers found!"}}}`, - }, - { - name: "websocket getFiatRatesForTimestamps missing date", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "currencies": []string{"usd"}, - }, - }, - want: `{"id":"22","data":{"error":{"message":"No timestamps provided"}}}`, - }, - { - name: "websocket getFiatRatesForTimestamps incorrect date", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "currencies": []string{"usd"}, - "timestamps": []string{"yesterday"}, - }, - }, - want: `{"id":"23","data":{"error":{"message":"json: cannot unmarshal string into Go struct field .timestamps of type int64"}}}`, - }, - { - name: "websocket getFiatRatesForTimestamps empty currency", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "timestamps": []int64{7885693815}, - "currencies": []string{""}, - }, - }, - want: `{"id":"24","data":{"tickers":[{"ts":7885693815,"rates":{}}]}}`, - }, - { - name: "websocket getFiatRatesForTimestamps incorrect (future) date", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "currencies": []string{"usd"}, - "timestamps": []int64{7885693815}, - }, - }, - want: `{"id":"25","data":{"tickers":[{"ts":7885693815,"rates":{"usd":-1}}]}}`, - }, - { - name: "websocket getFiatRatesForTimestamps exact date", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "currencies": []string{"usd"}, - "timestamps": []int64{1574346615}, - }, - }, - want: `{"id":"26","data":{"tickers":[{"ts":1574346615,"rates":{"usd":7914.5}}]}}`, - }, - { - name: "websocket getFiatRatesForTimestamps closest date, eur", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "currencies": []string{"eur"}, - "timestamps": []int64{1521507600}, - }, - }, - want: `{"id":"27","data":{"tickers":[{"ts":1521511200,"rates":{"eur":1300}}]}}`, - }, - { - name: "websocket getFiatRatesForTimestamps multiple timestamps usd", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "currencies": []string{"usd"}, - "timestamps": []int64{1570346615, 1574346615}, - }, - }, - want: `{"id":"28","data":{"tickers":[{"ts":1574344800,"rates":{"usd":7814.5}},{"ts":1574346615,"rates":{"usd":7914.5}}]}}`, - }, - { - name: "websocket getFiatRatesForTimestamps multiple timestamps eur", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "currencies": []string{"eur"}, - "timestamps": []int64{1570346615, 1574346615}, - }, - }, - want: `{"id":"29","data":{"tickers":[{"ts":1574344800,"rates":{"eur":7100}},{"ts":1574346615,"rates":{"eur":7134.1}}]}}`, - }, - { - name: "websocket getFiatRatesForTimestamps multiple timestamps with an error", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "currencies": []string{"usd"}, - "timestamps": []int64{1570346615, 1574346615, 2000000000}, - }, - }, - 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", - req: websocketReq{ - Method: "getFiatRatesForTimestamps", - Params: map[string]interface{}{ - "currencies": []string{"usd"}, - "timestamps": []int64{7832854800, 2000000000}, - }, - }, - want: `{"id":"31","data":{"tickers":[{"ts":7832854800,"rates":{"usd":-1}},{"ts":2000000000,"rates":{"usd":-1}}]}}`, - }, - { - name: "websocket getTickersList", - req: websocketReq{ - Method: "getFiatRatesTickersList", - Params: map[string]interface{}{ - "timestamp": 1570346615, - }, - }, - want: `{"id":"32","data":{"ts":1574344800,"available_currencies":["eur","usd"]}}`, - }, - { - name: "websocket getBalanceHistory Addr2", - req: websocketReq{ - Method: "getBalanceHistory", - Params: map[string]interface{}{ - "descriptor": "mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz", - }, - }, - 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", - req: websocketReq{ - Method: "getBalanceHistory", - Params: map[string]interface{}{ - "descriptor": dbtestdata.Xpub, - }, - }, - 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]", - req: websocketReq{ - Method: "getBalanceHistory", - Params: map[string]interface{}{ - "descriptor": dbtestdata.Xpub, - "from": 1521504000, - "to": 1521590400, - "currencies": []string{"usd"}, - }, - }, - 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]", - req: websocketReq{ - Method: "getBalanceHistory", - Params: map[string]interface{}{ - "descriptor": dbtestdata.Xpub, - "from": 1521504000, - "to": 1521590400, - "currencies": []string{"usd", "eur", "incorrect"}, +type websocketTest struct { + name string + req websocketReq + want string +} + +var websocketTestsBitcoinType = []websocketTest{ + { + name: "websocket getInfo", + req: websocketReq{ + Method: "getInfo", + }, + want: `{"id":"0","data":{"name":"Fakecoin","shortcut":"FAKE","network":"FAKE","decimals":8,"version":"unknown","bestHeight":225494,"bestHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","block0Hash":"","testnet":true,"backend":{"version":"001001","subversion":"/Fakecoin:0.0.1/"}}}`, + }, + { + name: "websocket getBlockHash", + req: websocketReq{ + Method: "getBlockHash", + Params: map[string]interface{}{ + "height": 225494, + }, + }, + want: `{"id":"1","data":{"hash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6"}}`, + }, + { + name: "websocket getAccountInfo xpub txs", + req: websocketReq{ + Method: "getAccountInfo", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Xpub, + "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,"addrTxCount":3,"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","standard":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuWrWMzoBt8VDFNvPmpJf42M1GTUs85fPx","path":"m/49'/1'/33'/0/6","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuVZ2Ca6Da9zmYynt49Rx7uikAgubGcymF","path":"m/49'/1'/33'/0/7","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzRGWDUmrPP9HwYu4B43QGCTLwoop5cExa","path":"m/49'/1'/33'/0/8","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5C9EEWJzyBXhpyPHqa3UNed73Amsi5b3L","path":"m/49'/1'/33'/0/9","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzNawz2zjwq1L85GDE3YydEJGJYfXxaWkk","path":"m/49'/1'/33'/0/10","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N7NdeuAMgL57WE7QCeV2gTWi2Um8iAu5dA","path":"m/49'/1'/33'/0/11","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8JQEP6DSHEZHNsSDPA1gHMUq9YFndhkfV","path":"m/49'/1'/33'/0/12","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mvbn3YXqKZVpQKugaoQrfjSYPvz76RwZkC","path":"m/49'/1'/33'/0/13","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8MRNxCfwUY9TSW27X9ooGYtqgrGCfLRHx","path":"m/49'/1'/33'/0/14","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6HvwrHC113KYZAmCtJ9XJNWgaTcnFunCM","path":"m/49'/1'/33'/0/15","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEo3oNyHUoi7rmRWee7wki37jxPWsWCopJ","path":"m/49'/1'/33'/0/16","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mzm5KY8qdFbDHsQfy4akXbFvbR3FAwDuVo","path":"m/49'/1'/33'/0/17","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NGMwftmQCogp6XZNGvgiybz3WZysvsJzqC","path":"m/49'/1'/33'/0/18","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N3fJrrefndYjLGycvFFfYgevpZtcRKCkRD","path":"m/49'/1'/33'/0/19","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N1T7TnHBwfdpBoyw53EGUL7vuJmb2mU6jF","path":"m/49'/1'/33'/0/20","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N7HexL4dyAQc7Th4iqcCW4hZuyiZsLWf74","path":"m/49'/1'/33'/1/9","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NF6X5FDGWrQj4nQrfP6hA77zB5WAc1DGup","path":"m/49'/1'/33'/1/10","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4ZRPdvc7BVioBTohy4F6QtxreqcjNj26b","path":"m/49'/1'/33'/1/11","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mtfho1rLmevh4qTnkYWxZEFCWteDMtTcUF","path":"m/49'/1'/33'/1/12","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NFUCphKYvmMcNZRZrF261mRX6iADVB9Qms","path":"m/49'/1'/33'/1/13","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5kBNMB8qgxE4Y4f8J19fScsE49J4aNvoJ","path":"m/49'/1'/33'/1/14","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NANWCaefhCKdXMcW8NbZnnrFRDvhJN2wPy","path":"m/49'/1'/33'/1/15","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NFHw7Yo2Bz8D2wGAYHW9qidbZFLpfJ72qB","path":"m/49'/1'/33'/1/16","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NBDSsBgy5PpFniLCb1eAFHcSxgxwPSDsZa","path":"m/49'/1'/33'/1/17","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NDWCSQHogc7sCuc2WoYt9PX2i2i6a5k6dX","path":"m/49'/1'/33'/1/18","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8vNyDP7iSDjm3BKpXrbDjAxyphqfvnJz8","path":"m/49'/1'/33'/1/19","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4tFKLurSbMusAyq1tv4tzymVjveAFV1Vb","path":"m/49'/1'/33'/1/20","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NBx5WwjAr2cH6Yqrp3Vsf957HtRKwDUVdX","path":"m/49'/1'/33'/1/21","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NBu1seHTaFhQxbcW5L5BkZzqFLGmZqpxsa","path":"m/49'/1'/33'/1/22","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NCDLoea22jGsXuarfT1n2QyCUh6RFhAPnT","path":"m/49'/1'/33'/1/23","transfers":0,"decimals":8}]}}`, + }, + { + name: "websocket getAccountInfo address", + req: websocketReq{ + Method: "getAccountInfo", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Addr4, + "details": "txids", + }, + }, + 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 gap", + req: websocketReq{ + Method: "getAccountInfo", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Xpub, + "details": "tokens", + "tokens": "derived", + "gap": 10, + }, + }, + want: `{"id":"4","data":{"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":3,"addrTxCount":3,"usedTokens":2,"tokens":[{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuWrWMzoBt8VDFNvPmpJf42M1GTUs85fPx","path":"m/49'/1'/33'/0/6","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuVZ2Ca6Da9zmYynt49Rx7uikAgubGcymF","path":"m/49'/1'/33'/0/7","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzRGWDUmrPP9HwYu4B43QGCTLwoop5cExa","path":"m/49'/1'/33'/0/8","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5C9EEWJzyBXhpyPHqa3UNed73Amsi5b3L","path":"m/49'/1'/33'/0/9","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzNawz2zjwq1L85GDE3YydEJGJYfXxaWkk","path":"m/49'/1'/33'/0/10","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N7HexL4dyAQc7Th4iqcCW4hZuyiZsLWf74","path":"m/49'/1'/33'/1/9","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NF6X5FDGWrQj4nQrfP6hA77zB5WAc1DGup","path":"m/49'/1'/33'/1/10","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4ZRPdvc7BVioBTohy4F6QtxreqcjNj26b","path":"m/49'/1'/33'/1/11","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mtfho1rLmevh4qTnkYWxZEFCWteDMtTcUF","path":"m/49'/1'/33'/1/12","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NFUCphKYvmMcNZRZrF261mRX6iADVB9Qms","path":"m/49'/1'/33'/1/13","transfers":0,"decimals":8}]}}`, + }, + { + name: "websocket getAccountUtxo", + req: websocketReq{ + Method: "getAccountUtxo", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Addr1, + }, + }, + want: `{"id":"5","data":[{"txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","vout":0,"value":"100000000","height":225493,"confirmations":2}]}`, + }, + { + name: "websocket getAccountUtxo", + req: websocketReq{ + Method: "getAccountUtxo", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Addr4, + }, + }, + want: `{"id":"6","data":[]}`, + }, + { + name: "websocket getTransaction", + req: websocketReq{ + Method: "getTransaction", + Params: map[string]interface{}{ + "txid": dbtestdata.TxidB2T2, + }, + }, + want: `{"id":"7","data":{"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"}}`, + }, + { + name: "websocket getTransaction", + req: websocketReq{ + Method: "getTransaction", + Params: map[string]interface{}{ + "txid": "not a tx", + }, + }, + want: `{"id":"8","data":{"error":{"message":"Transaction 'not a tx' not found"}}}`, + }, + { + name: "websocket getTransactionSpecific", + req: websocketReq{ + Method: "getTransactionSpecific", + Params: map[string]interface{}{ + "txid": dbtestdata.TxidB2T2, + }, + }, + want: `{"id":"9","data":{"hex":"","txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","version":0,"locktime":0,"vin":[{"coinbase":"","txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":0,"scriptSig":{"hex":""},"sequence":0,"addresses":null},{"coinbase":"","txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":1,"scriptSig":{"hex":""},"sequence":0,"addresses":null}],"vout":[{"ValueSat":118641975500,"value":0,"n":0,"scriptPubKey":{"hex":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","addresses":null}},{"ValueSat":198641975500,"value":0,"n":1,"scriptPubKey":{"hex":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","addresses":null}}],"confirmations":1,"time":1521595678,"blocktime":1521595678,"vsize":400}}`, + }, + { + name: "websocket estimateFee", + req: websocketReq{ + Method: "estimateFee", + Params: map[string]interface{}{ + "blocks": []int{2, 5, 10, 20}, + "specific": map[string]interface{}{ + "conservative": false, + "txsize": 1234, }, }, - 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=[]", - req: websocketReq{ - Method: "getBalanceHistory", - Params: map[string]interface{}{ - "descriptor": dbtestdata.Xpub, - "from": 1521504000, - "to": 1521590400, - "currencies": []string{}, + 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":"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."}}`, }, + 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{ + Method: "sendTransaction", + Params: map[string]interface{}{ + "hex": "123456", + }, + }, + want: `{"id":"12","data":{"result":"9876"}}`, + }, + { + name: "websocket subscribeNewBlock", + req: websocketReq{ + Method: "subscribeNewBlock", + }, + want: `{"id":"13","data":{"subscribed":true}}`, + }, + { + name: "websocket unsubscribeNewBlock", + req: websocketReq{ + Method: "unsubscribeNewBlock", + }, + want: `{"id":"14","data":{"subscribed":false}}`, + }, + { + name: "websocket subscribeAddresses", + req: websocketReq{ + Method: "subscribeAddresses", + Params: map[string]interface{}{ + "addresses": []string{dbtestdata.Addr1, dbtestdata.Addr2}, + }, + }, + want: `{"id":"15","data":{"subscribed":true}}`, + }, + { + name: "websocket unsubscribeAddresses", + req: websocketReq{ + Method: "unsubscribeAddresses", + }, + want: `{"id":"16","data":{"subscribed":false}}`, + }, + { + name: "websocket ping", + req: websocketReq{ + Method: "ping", + }, + want: `{"id":"17","data":{}}`, + }, + { + name: "websocket getCurrentFiatRates all currencies", + req: websocketReq{ + Method: "getCurrentFiatRates", + Params: map[string]interface{}{ + "currencies": []string{}, + }, + }, + want: `{"id":"18","data":{"ts":1574380800,"rates":{"eur":7134.1,"usd":7914.5}}}`, + }, + { + name: "websocket getCurrentFiatRates usd", + req: websocketReq{ + Method: "getCurrentFiatRates", + Params: map[string]interface{}{ + "currencies": []string{"usd"}, + }, + }, + want: `{"id":"19","data":{"ts":1574380800,"rates":{"usd":7914.5}}}`, + }, + { + name: "websocket getCurrentFiatRates eur", + req: websocketReq{ + Method: "getCurrentFiatRates", + Params: map[string]interface{}{ + "currencies": []string{"eur"}, + }, + }, + want: `{"id":"20","data":{"ts":1574380800,"rates":{"eur":7134.1}}}`, + }, + { + name: "websocket getCurrentFiatRates incorrect currency", + req: websocketReq{ + Method: "getCurrentFiatRates", + Params: map[string]interface{}{ + "currencies": []string{"does-not-exist"}, + }, + }, + want: `{"id":"21","data":{"error":{"message":"No tickers found!"}}}`, + }, + { + name: "websocket getFiatRatesForTimestamps missing date", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "currencies": []string{"usd"}, + }, + }, + want: `{"id":"22","data":{"error":{"message":"No timestamps provided"}}}`, + }, + { + name: "websocket getFiatRatesForTimestamps incorrect date", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "currencies": []string{"usd"}, + "timestamps": []string{"yesterday"}, + }, + }, + want: `{"id":"23","data":{"error":{"message":"json: cannot unmarshal string into Go struct field WsFiatRatesForTimestampsReq.timestamps of type int64"}}}`, + }, + { + name: "websocket getFiatRatesForTimestamps empty currency", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "timestamps": []int64{7885693815}, + "currencies": []string{""}, + }, + }, + want: `{"id":"24","data":{"tickers":[{"ts":7885693815,"rates":{}}]}}`, + }, + { + name: "websocket getFiatRatesForTimestamps incorrect (future) date", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "currencies": []string{"usd"}, + "timestamps": []int64{7885693815}, + }, + }, + want: `{"id":"25","data":{"tickers":[{"ts":7885693815,"rates":{"usd":-1}}]}}`, + }, + { + name: "websocket getFiatRatesForTimestamps exact date", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "currencies": []string{"usd"}, + "timestamps": []int64{1574380800}, + }, + }, + want: `{"id":"26","data":{"tickers":[{"ts":1574380800,"rates":{"usd":7914.5}}]}}`, + }, + { + name: "websocket getFiatRatesForTimestamps closest date, eur", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "currencies": []string{"eur"}, + "timestamps": []int64{1521507600}, + }, + }, + want: `{"id":"27","data":{"tickers":[{"ts":1521590400,"rates":{"eur":1301}}]}}`, + }, + { + name: "websocket getFiatRatesForTimestamps multiple timestamps usd", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "currencies": []string{"usd"}, + "timestamps": []int64{1570346615, 1574346615}, + }, + }, + want: `{"id":"28","data":{"tickers":[{"ts":1574294400,"rates":{"usd":7814.5}},{"ts":1574380800,"rates":{"usd":7914.5}}]}}`, + }, + { + name: "websocket getFiatRatesForTimestamps multiple timestamps eur", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "currencies": []string{"eur"}, + "timestamps": []int64{1570346615, 1574346615}, + }, + }, + want: `{"id":"29","data":{"tickers":[{"ts":1574294400,"rates":{"eur":7100}},{"ts":1574380800,"rates":{"eur":7134.1}}]}}`, + }, + { + name: "websocket getFiatRatesForTimestamps multiple timestamps with an error", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "currencies": []string{"usd"}, + "timestamps": []int64{1570346615, 1574346615, 2000000000}, + }, + }, + want: `{"id":"30","data":{"tickers":[{"ts":1574294400,"rates":{"usd":7814.5}},{"ts":1574380800,"rates":{"usd":7914.5}},{"ts":2000000000,"rates":{"usd":-1}}]}}`, + }, + { + name: "websocket getFiatRatesForTimestamps multiple errors", + req: websocketReq{ + Method: "getFiatRatesForTimestamps", + Params: map[string]interface{}{ + "currencies": []string{"usd"}, + "timestamps": []int64{7832854800, 2000000000}, + }, + }, + want: `{"id":"31","data":{"tickers":[{"ts":7832854800,"rates":{"usd":-1}},{"ts":2000000000,"rates":{"usd":-1}}]}}`, + }, + { + name: "websocket getTickersList", + req: websocketReq{ + Method: "getFiatRatesTickersList", + Params: map[string]interface{}{ + "timestamp": 1570346615, + }, + }, + want: `{"id":"32","data":{"ts":1574294400,"available_currencies":["eur","usd"]}}`, + }, + { + name: "websocket getBalanceHistory Addr2", + req: websocketReq{ + Method: "getBalanceHistory", + Params: map[string]interface{}{ + "descriptor": "mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz", + }, + }, + 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":1302,"usd":2002}}]}`, + }, + { + name: "websocket getBalanceHistory xpub", + req: websocketReq{ + Method: "getBalanceHistory", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Xpub, + }, + }, + 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":1302,"usd":2002}}]}`, + }, + { + name: "websocket getBalanceHistory xpub from=1521504000&to=1521590400 currencies=[usd]", + req: websocketReq{ + Method: "getBalanceHistory", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Xpub, + "from": 1521504000, + "to": 1521590400, + "currencies": []string{"usd"}, + }, + }, + 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]", + req: websocketReq{ + Method: "getBalanceHistory", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Xpub, + "from": 1521504000, + "to": 1521590400, + "currencies": []string{"usd", "eur", "incorrect"}, + }, + }, + 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=[]", + req: websocketReq{ + Method: "getBalanceHistory", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Xpub, + "from": 1521504000, + "to": 1521590400, + "currencies": []string{}, + }, + }, + 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."}}`, + }, + { + name: "websocket getBlock", + req: websocketReq{ + Method: "getBlock", + Params: map[string]interface{}{ + "id": "00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6", + }, + }, + want: `{"id":"40","data":{"error":{"message":"Not supported"}}}`, + }, + { + name: "websocket getMempoolFilters", + req: websocketReq{ + Method: "getMempoolFilters", + Params: map[string]interface{}{ + "scriptType": "", + }, + }, + want: `{"id":"41","data":{"P":0,"M":1,"zeroedKey":false,"entries":{}}}`, + }, + { + name: "websocket getMempoolFilters invalid type", + req: websocketReq{ + Method: "getMempoolFilters", + Params: map[string]interface{}{ + "scriptType": "invalid", + }, + }, + want: `{"id":"42","data":{"error":{"message":"Unsupported script filter invalid"}}}`, + }, + { + name: "websocket getBlockFilter", + req: websocketReq{ + Method: "getBlockFilter", + Params: map[string]interface{}{ + "blockHash": "abcd", + }, + }, + want: `{"id":"43","data":{"P":0,"M":1,"zeroedKey":false,"blockFilter":""}}`, + }, + { + name: "websocket rpcCall", + req: websocketReq{ + Method: "rpcCall", + Params: WsRpcCallReq{ + To: "0x123", + Data: "0x456", + }, + }, + want: `{"id":"44","data":{"error":{"message":"not supported"}}}`, + }, +} + +func runWebsocketTests(t *testing.T, ts *httptest.Server, tests []websocketTest) { + url := strings.Replace(ts.URL, "http://", "ws://", 1) + "/websocket" + s, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + t.Fatal(err) } + defer s.Close() // send all requests at once for i, tt := range tests { @@ -1489,7 +1557,7 @@ func fixedTimeNow() time.Time { return time.Date(2022, 9, 15, 12, 43, 56, 0, time.UTC) } -func Test_PublicServer_BitcoinType(t *testing.T) { +func setupChain(t *testing.T) (bchain.BlockChainParser, bchain.BlockChain) { timeNow = fixedTimeNow parser := btc.NewBitcoinParser( btc.GetChainParams("test"), @@ -1505,8 +1573,13 @@ func Test_PublicServer_BitcoinType(t *testing.T) { if err != nil { glog.Fatal("fakechain: ", err) } + return parser, chain +} + +func Test_PublicServer_BitcoinType(t *testing.T) { + parser, chain := setupChain(t) - s, dbpath := setupPublicHTTPServer(parser, chain, t) + s, dbpath := setupPublicHTTPServer(parser, chain, t, false) defer closeAndDestroyPublicServer(t, s, dbpath) s.ConnectFullPublicInterface() // take the handler of the public server and pass it to the test server @@ -1515,167 +1588,186 @@ func Test_PublicServer_BitcoinType(t *testing.T) { httpTestsBitcoinType(t, ts) socketioTestsBitcoinType(t, ts) - websocketTestsBitcoinType(t, ts) + runWebsocketTests(t, ts, websocketTestsBitcoinType) } -func Test_formatInt64(t *testing.T) { +func httpTestsBitcoinTypeExtendedIndex(t *testing.T, ts *httptest.Server) { tests := []struct { - name string - n int64 - want template.HTML + name string + r *http.Request + status int + contentType string + body []string }{ - {"1", 1, "1"}, - {"13", 13, "13"}, - {"123", 123, "123"}, - {"1234", 1234, `1234`}, - {"91234", 91234, `91234`}, - {"891234", 891234, `891234`}, - {"7891234", 7891234, `7891234`}, - {"67891234", 67891234, `67891234`}, - {"567891234", 567891234, `567891234`}, - {"4567891234", 4567891234, `4567891234`}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := formatInt64(tt.n); !reflect.DeepEqual(got, tt.want) { - t.Errorf("formatInt64() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_formatTime(t *testing.T) { - timeNow = fixedTimeNow - tests := []struct { - name string - want template.HTML - }{ - { - name: "2020-12-23 15:16:17", - want: `630 days 21 hours ago`, - }, - { - name: "2022-08-23 11:12:13", - want: `23 days 1 hour ago`, - }, - { - name: "2022-09-14 11:12:13", - want: `1 day 1 hour ago`, - }, { - name: "2022-09-14 14:12:13", - want: `22 hours 31 mins ago`, - }, - { - name: "2022-09-15 09:33:26", - want: `3 hours 10 mins ago`, + name: "apiIndex", + r: newGetRequest(ts.URL + "/api"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"blockbook":{"coin":"Fakecoin"`, + `"bestHeight":225494`, + `"decimals":8`, + `"backend":{"chain":"fakecoin","blocks":2,"headers":2,"bestBlockHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6"`, + `"version":"001001","subversion":"/Fakecoin:0.0.1/"`, + }, }, { - name: "2022-09-15 12:23:56", - want: `20 mins ago`, + name: "apiTx v2", + r: newGetRequest(ts.URL + "/api/v2/tx/7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"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,"spentTxId":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","spentHeight":225494,"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"}`, + }, }, { - name: "2022-09-15 12:24:07", - want: `19 mins ago`, + name: "apiAddress v2 details=txs", + r: newGetRequest(ts.URL + "/api/v2/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw?details=txs"), + 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,"isOwn":true,"value":"1234567890123"},{"txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","vout":1,"n":1,"addresses":["mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz"],"isAddress":true,"value":"12345"}],"vout":[{"value":"317283951061","n":0,"spent":true,"spentTxId":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","spentHeight":225494,"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,"spentTxId":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","spentHeight":225494,"hex":"76a914a08eae93007f22668ab5e4a9c83c8cd1c325e3e088ac","addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true,"isOwn":true},{"value":"1","n":1,"spent":true,"spentTxId":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","spentIndex":1,"spentHeight":225494,"hex":"a91452724c5178682f70e0ba31c6ec0633755a3b41d987","addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true},{"value":"9876","n":2,"spent":true,"spentTxId":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","spentHeight":225494,"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"isAddress":true}],"blockHash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","blockHeight":225493,"confirmations":2,"blockTime":1521515026,"value":"1234567900000","valueIn":"0","fees":"0"}]}`, + }, }, { - name: "2022-09-15 12:43:21", - want: `35 secs ago`, + name: "apiGetBlock", + r: newGetRequest(ts.URL + "/api/v2/block/225493"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"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,"spentTxId":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","spentIndex":1,"spentHeight":225494,"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,"spentTxId":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","spentHeight":225494,"addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true},{"value":"1","n":1,"spent":true,"spentTxId":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","spentIndex":1,"spentHeight":225494,"addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true},{"value":"9876","n":2,"spent":true,"spentTxId":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","spentHeight":225494,"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"isAddress":true}],"blockHash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","blockHeight":225493,"confirmations":2,"blockTime":1521515026,"value":"1234567900000","valueIn":"0","fees":"0"}]}`, + }, }, { - name: "2022-09-15 12:43:56", - want: `0 secs ago`, + name: "apiBlockFilters", + r: newGetRequest(ts.URL + "/api/v2/block-filters?lastN=2"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"P":20,"M":1048576,"zeroedKey":false,"blockFilters":{"225493":{"blockHash":"0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997","filter":"050079b0d468a27502af2ac08f2fc0"},"225494":{"blockHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","filter":"0a0195bc0a550129e827a9ba4aa44287840cc73d0c27d16832059690"}}}`, + }, }, { - name: "2022-09-16 12:43:56", - want: `2022-09-16 12:43:56`, + name: "apiBlockFilters scriptType=taproot", + r: newGetRequest(ts.URL + "/api/v2/block-filters?lastN=2&scriptType=taproot"), + status: http.StatusBadRequest, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"error":"Invalid scriptType taproot. Use "}`, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tm, _ := time.Parse("2006-01-02 15:04:05", tt.name) - if got := timeSpan(&tm); !reflect.DeepEqual(got, tt.want) { - t.Errorf("formatTime() = %v, want %v", got, tt.want) + resp, err := http.DefaultClient.Do(tt.r) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != tt.status { + t.Errorf("StatusCode = %v, want %v", resp.StatusCode, tt.status) + } + if resp.Header["Content-Type"][0] != tt.contentType { + t.Errorf("Content-Type = %v, want %v", resp.Header["Content-Type"][0], tt.contentType) + } + bb, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + b := string(bb) + for _, c := range tt.body { + if !strings.Contains(b, c) { + t.Errorf("got %v, want to contain %v", b, c) + break + } } }) } } -func Test_appendAmountSpan(t *testing.T) { - tests := []struct { - name string - class string - amount string - shortcut string - txDate string - want string - }{ - { - name: "prim-amt 1.23456789 BTC", - class: "prim-amt", - amount: "1.23456789", - shortcut: "BTC", - want: `1.23456789 BTC`, - }, - { - name: "prim-amt 1432134.23456 BTC", - class: "prim-amt", - amount: "1432134.23456", - shortcut: "BTC", - want: `1432134.23456 BTC`, - }, - { - name: "sec-amt 1 EUR", - class: "sec-amt", - amount: "1", - shortcut: "EUR", - want: `1 EUR`, - }, - { - name: "sec-amt -1 EUR", - class: "sec-amt", - amount: "-1", - shortcut: "EUR", - want: `-1 EUR`, - }, - { - name: "sec-amt 432109.23 EUR", - class: "sec-amt", - amount: "432109.23", - shortcut: "EUR", - want: `432109.23 EUR`, - }, - { - name: "sec-amt -432109.23 EUR", - class: "sec-amt", - amount: "-432109.23", - shortcut: "EUR", - want: `-432109.23 EUR`, - }, - { - name: "sec-amt 43141.29 EUR", - class: "sec-amt", - amount: "43141.29", - shortcut: "EUR", - txDate: "2022-03-14", - want: `43141.29 EUR`, - }, - { - name: "sec-amt -43141.29 EUR", - class: "sec-amt", - amount: "-43141.29", - shortcut: "EUR", - txDate: "2022-03-14", - want: `-43141.29 EUR`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var rv strings.Builder - appendAmountSpan(&rv, tt.class, tt.amount, tt.shortcut, tt.txDate) - if got := rv.String(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("formatTime() = %v, want %v", got, tt.want) - } - }) - } +var websocketTestsBitcoinTypeExtendedIndex = []websocketTest{ + { + name: "websocket getInfo", + req: websocketReq{ + Method: "getInfo", + }, + want: `{"id":"0","data":{"name":"Fakecoin","shortcut":"FAKE","network":"FAKE","decimals":8,"version":"unknown","bestHeight":225494,"bestHash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","block0Hash":"","testnet":true,"backend":{"version":"001001","subversion":"/Fakecoin:0.0.1/"}}}`, + }, + { + name: "websocket getBlockHash", + req: websocketReq{ + Method: "getBlockHash", + Params: map[string]interface{}{ + "height": 225494, + }, + }, + want: `{"id":"1","data":{"hash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6"}}`, + }, + { + name: "websocket getAccountInfo xpub txs", + req: websocketReq{ + Method: "getAccountInfo", + Params: map[string]interface{}{ + "descriptor": dbtestdata.Xpub, + "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,"addrTxCount":3,"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,"spentTxId":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","spentHeight":225494,"hex":"76a914a08eae93007f22668ab5e4a9c83c8cd1c325e3e088ac","addresses":["mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"],"isAddress":true},{"value":"1","n":1,"spent":true,"spentTxId":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","spentIndex":1,"spentHeight":225494,"hex":"a91452724c5178682f70e0ba31c6ec0633755a3b41d987","addresses":["2MzmAKayJmja784jyHvRUW1bXPget1csRRG"],"isAddress":true,"isOwn":true},{"value":"9876","n":2,"spent":true,"spentTxId":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","spentHeight":225494,"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","standard":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuWrWMzoBt8VDFNvPmpJf42M1GTUs85fPx","path":"m/49'/1'/33'/0/6","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MuVZ2Ca6Da9zmYynt49Rx7uikAgubGcymF","path":"m/49'/1'/33'/0/7","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzRGWDUmrPP9HwYu4B43QGCTLwoop5cExa","path":"m/49'/1'/33'/0/8","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5C9EEWJzyBXhpyPHqa3UNed73Amsi5b3L","path":"m/49'/1'/33'/0/9","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzNawz2zjwq1L85GDE3YydEJGJYfXxaWkk","path":"m/49'/1'/33'/0/10","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N7NdeuAMgL57WE7QCeV2gTWi2Um8iAu5dA","path":"m/49'/1'/33'/0/11","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8JQEP6DSHEZHNsSDPA1gHMUq9YFndhkfV","path":"m/49'/1'/33'/0/12","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mvbn3YXqKZVpQKugaoQrfjSYPvz76RwZkC","path":"m/49'/1'/33'/0/13","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8MRNxCfwUY9TSW27X9ooGYtqgrGCfLRHx","path":"m/49'/1'/33'/0/14","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6HvwrHC113KYZAmCtJ9XJNWgaTcnFunCM","path":"m/49'/1'/33'/0/15","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEo3oNyHUoi7rmRWee7wki37jxPWsWCopJ","path":"m/49'/1'/33'/0/16","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mzm5KY8qdFbDHsQfy4akXbFvbR3FAwDuVo","path":"m/49'/1'/33'/0/17","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NGMwftmQCogp6XZNGvgiybz3WZysvsJzqC","path":"m/49'/1'/33'/0/18","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N3fJrrefndYjLGycvFFfYgevpZtcRKCkRD","path":"m/49'/1'/33'/0/19","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N1T7TnHBwfdpBoyw53EGUL7vuJmb2mU6jF","path":"m/49'/1'/33'/0/20","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N7HexL4dyAQc7Th4iqcCW4hZuyiZsLWf74","path":"m/49'/1'/33'/1/9","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NF6X5FDGWrQj4nQrfP6hA77zB5WAc1DGup","path":"m/49'/1'/33'/1/10","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4ZRPdvc7BVioBTohy4F6QtxreqcjNj26b","path":"m/49'/1'/33'/1/11","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2Mtfho1rLmevh4qTnkYWxZEFCWteDMtTcUF","path":"m/49'/1'/33'/1/12","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NFUCphKYvmMcNZRZrF261mRX6iADVB9Qms","path":"m/49'/1'/33'/1/13","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N5kBNMB8qgxE4Y4f8J19fScsE49J4aNvoJ","path":"m/49'/1'/33'/1/14","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NANWCaefhCKdXMcW8NbZnnrFRDvhJN2wPy","path":"m/49'/1'/33'/1/15","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NFHw7Yo2Bz8D2wGAYHW9qidbZFLpfJ72qB","path":"m/49'/1'/33'/1/16","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NBDSsBgy5PpFniLCb1eAFHcSxgxwPSDsZa","path":"m/49'/1'/33'/1/17","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NDWCSQHogc7sCuc2WoYt9PX2i2i6a5k6dX","path":"m/49'/1'/33'/1/18","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N8vNyDP7iSDjm3BKpXrbDjAxyphqfvnJz8","path":"m/49'/1'/33'/1/19","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2N4tFKLurSbMusAyq1tv4tzymVjveAFV1Vb","path":"m/49'/1'/33'/1/20","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NBx5WwjAr2cH6Yqrp3Vsf957HtRKwDUVdX","path":"m/49'/1'/33'/1/21","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NBu1seHTaFhQxbcW5L5BkZzqFLGmZqpxsa","path":"m/49'/1'/33'/1/22","transfers":0,"decimals":8},{"type":"XPUBAddress","standard":"XPUBAddress","name":"2NCDLoea22jGsXuarfT1n2QyCUh6RFhAPnT","path":"m/49'/1'/33'/1/23","transfers":0,"decimals":8}]}}`, + }, + { + name: "websocket getBlockFilter", + req: websocketReq{ + Method: "getBlockFilter", + Params: map[string]interface{}{ + "blockHash": "0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997", + }, + }, + want: `{"id":"3","data":{"P":20,"M":1048576,"zeroedKey":false,"blockFilter":"050079b0d468a27502af2ac08f2fc0"}}`, + }, + { + name: "websocket getBlockFiltersBatch bestKnownBlockHash 1st block", + req: websocketReq{ + Method: "getBlockFiltersBatch", + Params: map[string]interface{}{ + "bestKnownBlockHash": "0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997", + }, + }, + want: `{"id":"4","data":{"P":20,"M":1048576,"zeroedKey":false,"blockFiltersBatch":["225494:00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6:0a0195bc0a550129e827a9ba4aa44287840cc73d0c27d16832059690"]}}`, + }, + { + name: "websocket getBlockFiltersBatch bestKnownBlockHash 2nd block", + req: websocketReq{ + Method: "getBlockFiltersBatch", + Params: map[string]interface{}{ + "bestKnownBlockHash": "00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6", + }, + }, + want: `{"id":"5","data":{"P":20,"M":1048576,"zeroedKey":false,"blockFiltersBatch":[]}}`, + }, + { + name: "websocket getBlockFiltersBatch bestKnownBlockHash 1st block, unsupported script type", + req: websocketReq{ + Method: "getBlockFiltersBatch", + Params: map[string]interface{}{ + "bestKnownBlockHash": "0000000076fbbed90fd75b0e18856aa35baa984e9c9d444cf746ad85e94e2997", + "scriptType": "unsupported", + }, + }, + want: `{"id":"6","data":{"error":{"message":"Unsupported script type unsupported"}}}`, + }, +} + +func Test_PublicServer_BitcoinType_ExtendedIndex(t *testing.T) { + parser, chain := setupChain(t) + + s, dbpath := setupPublicHTTPServer(parser, chain, t, true) + defer closeAndDestroyPublicServer(t, s, dbpath) + s.ConnectFullPublicInterface() + // take the handler of the public server and pass it to the test server + ts := httptest.NewServer(s.https.Handler) + defer ts.Close() + + httpTestsBitcoinTypeExtendedIndex(t, ts) + runWebsocketTests(t, ts, websocketTestsBitcoinTypeExtendedIndex) } diff --git a/server/socketio.go b/server/socketio.go index 5919db4522..c606eb1ac9 100644 --- a/server/socketio.go +++ b/server/socketio.go @@ -17,6 +17,7 @@ import ( "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/common" "github.com/trezor/blockbook/db" + "github.com/trezor/blockbook/fiat" ) // SocketIoServer is handle to SocketIoServer @@ -33,8 +34,8 @@ 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, metrics, is) +func NewSocketIoServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates) (*SocketIoServer, error) { + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates) if err != nil { return nil, err } @@ -174,7 +175,9 @@ func (s *SocketIoServer) onMessage(c *gosocketio.Channel, req map[string]json.Ra 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 + defer func() { + s.metrics.SocketIOReqDuration.With(common.Labels{"method": method}).Observe(float64(time.Since(t)) / 1e3) // in microseconds + }() f, ok := onMessageHandlers[method] if ok { rv, err = f(s, params) @@ -584,7 +587,7 @@ type resultGetInfo struct { } func (s *SocketIoServer) getInfo() (res resultGetInfo, err error) { - _, height, _ := s.is.GetSyncState() + _, height, _, _ := s.is.GetSyncState() res.Result.Blocks = int(height) res.Result.Testnet = s.chain.IsTestnet() res.Result.Network = s.chain.GetNetworkName() @@ -638,7 +641,7 @@ func (s *SocketIoServer) getDetailedTransaction(txid string) (res resultGetDetai } func (s *SocketIoServer) sendTransaction(tx string) (res resultSendTransaction, err error) { - txid, err := s.chain.SendRawTransaction(tx) + txid, err := s.chain.SendRawTransaction(tx, false) if err != nil { return res, err } diff --git a/server/websocket.go b/server/websocket.go index facaaa42df..9a6d09cba3 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -4,6 +4,7 @@ import ( "encoding/json" "math/big" "net/http" + "os" "runtime/debug" "strconv" "strings" @@ -18,6 +19,7 @@ import ( "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/common" "github.com/trezor/blockbook/db" + "github.com/trezor/blockbook/fiat" ) const upgradeFailed = "Upgrade failed: " @@ -34,31 +36,17 @@ var ( connectionCounter uint64 ) -type websocketReq struct { - ID string `json:"id"` - Method string `json:"method"` - Params json.RawMessage `json:"params"` -} - -type websocketRes struct { - ID string `json:"id"` - Data interface{} `json:"data"` -} - type websocketChannel struct { - id uint64 - conn *websocket.Conn - out chan *websocketRes - ip string - requestHeader http.Header - alive bool - aliveLock sync.Mutex - addrDescs []string // subscribed address descriptors as strings -} - -type fiatRatesSubscription struct { - Currency string `json:"currency"` - Tokens []string `json:"tokens"` + id uint64 + conn *websocket.Conn + out chan *WsRes + ip string + requestHeader http.Header + alive bool + aliveLock sync.Mutex + addrDescs []string // subscribed address descriptors as strings + getAddressInfoDescriptorsMux sync.Mutex + getAddressInfoDescriptors map[string]struct{} } // WebsocketServer is a handle to websocket server @@ -83,11 +71,12 @@ type WebsocketServer struct { fiatRatesSubscriptions map[string]map[*websocketChannel]string fiatRatesTokenSubscriptions map[*websocketChannel][]string fiatRatesSubscriptionsLock sync.Mutex + allowedRpcCallTo map[string]struct{} } // 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, enableSubNewTx bool) (*WebsocketServer, error) { - api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is) +func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates) (*WebsocketServer, error) { + api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates) if err != nil { return nil, err } @@ -97,9 +86,10 @@ func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain. } s := &WebsocketServer{ upgrader: &websocket.Upgrader{ - ReadBufferSize: 1024 * 32, - WriteBufferSize: 1024 * 32, - CheckOrigin: checkOrigin, + ReadBufferSize: 1024 * 32, + WriteBufferSize: 1024 * 32, + CheckOrigin: checkOrigin, + EnableCompression: true, }, db: db, txCache: txCache, @@ -111,12 +101,20 @@ func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain. api: api, block0hash: b0, newBlockSubscriptions: make(map[*websocketChannel]string), - newTransactionEnabled: enableSubNewTx, + newTransactionEnabled: is.EnableSubNewTx, newTransactionSubscriptions: make(map[*websocketChannel]string), addressSubscriptions: make(map[string]map[*websocketChannel]string), fiatRatesSubscriptions: make(map[string]map[*websocketChannel]string), fiatRatesTokenSubscriptions: make(map[*websocketChannel][]string), } + envRpcCall := os.Getenv(strings.ToUpper(is.GetNetwork()) + "_ALLOWED_RPC_CALL_TO") + if envRpcCall != "" { + s.allowedRpcCallTo = make(map[string]struct{}) + for _, c := range strings.Split(envRpcCall, ",") { + s.allowedRpcCallTo[strings.ToLower(c)] = struct{}{} + } + glog.Info("Support of rpcCall for these contracts: ", envRpcCall) + } return s, nil } @@ -126,7 +124,11 @@ func checkOrigin(r *http.Request) bool { } func getIP(r *http.Request) string { - ip := r.Header.Get("X-Real-Ip") + ip := r.Header.Get("cf-connecting-ip") + if ip != "" { + return ip + } + ip = r.Header.Get("X-Real-Ip") if ip != "" { return ip } @@ -136,22 +138,25 @@ func getIP(r *http.Request) string { // ServeHTTP sets up handler of websocket channel func (s *WebsocketServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { - http.Error(w, upgradeFailed+ErrorMethodNotAllowed.Error(), 503) + http.Error(w, upgradeFailed+ErrorMethodNotAllowed.Error(), http.StatusServiceUnavailable) return } conn, err := s.upgrader.Upgrade(w, r, nil) if err != nil { - http.Error(w, upgradeFailed+err.Error(), 503) + http.Error(w, upgradeFailed+err.Error(), http.StatusServiceUnavailable) return } c := &websocketChannel{ id: atomic.AddUint64(&connectionCounter, 1), conn: conn, - out: make(chan *websocketRes, outChannelSize), + out: make(chan *WsRes, outChannelSize), ip: getIP(r), requestHeader: r.Header, alive: true, } + if s.is.WsGetAccountInfoLimit > 0 { + c.getAddressInfoDescriptors = make(map[string]struct{}) + } go s.inputLoop(c) go s.outputLoop(c) s.onConnect(c) @@ -162,11 +167,13 @@ func (s *WebsocketServer) GetHandler() http.Handler { return s } -func (s *WebsocketServer) closeChannel(c *websocketChannel) { +func (s *WebsocketServer) closeChannel(c *websocketChannel) bool { if c.CloseOut() { c.conn.Close() s.onDisconnect(c) + return true } + return false } func (c *websocketChannel) CloseOut() bool { @@ -184,7 +191,7 @@ func (c *websocketChannel) CloseOut() bool { return false } -func (c *websocketChannel) DataOut(data *websocketRes) { +func (c *websocketChannel) DataOut(data *WsRes) { c.aliveLock.Lock() defer c.aliveLock.Unlock() if c.alive { @@ -215,7 +222,7 @@ func (s *WebsocketServer) inputLoop(c *websocketChannel) { } switch t { case websocket.TextMessage: - var req websocketReq + var req WsReq err := json.Unmarshal(d, &req) if err != nil { glog.Error("Error parsing message from ", c.id, ", ", string(d), ", ", err) @@ -229,7 +236,6 @@ func (s *WebsocketServer) inputLoop(c *websocketChannel) { return case websocket.PingMessage: c.conn.WriteControl(websocket.PongMessage, nil, time.Now().Add(defaultTimeout)) - break case websocket.CloseMessage: s.closeChannel(c) return @@ -270,46 +276,62 @@ func (s *WebsocketServer) onDisconnect(c *websocketChannel) { s.metrics.WebsocketClients.Dec() } -var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *websocketReq) (interface{}, error){ - "getAccountInfo": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { +var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *WsReq) (interface{}, error){ + "getAccountInfo": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { r, err := unmarshalGetAccountInfoRequest(req.Params) if err == nil { + if s.is.WsGetAccountInfoLimit > 0 { + c.getAddressInfoDescriptorsMux.Lock() + c.getAddressInfoDescriptors[r.Descriptor] = struct{}{} + l := len(c.getAddressInfoDescriptors) + c.getAddressInfoDescriptorsMux.Unlock() + if l > s.is.WsGetAccountInfoLimit { + if s.closeChannel(c) { + glog.Info("Client ", c.id, " exceeded getAddressInfo limit, ", c.ip) + s.is.AddWsLimitExceedingIP(c.ip) + } + return + } + } rv, err = s.getAccountInfo(r) } return }, - "getInfo": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "getInfo": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.getInfo() }, - "getBlockHash": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Height int `json:"height"` - }{} + "getBlockHash": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsBlockHashReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getBlockHash(r.Height) } return }, - "getAccountUtxo": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Descriptor string `json:"descriptor"` - }{} + "getBlock": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + if !s.is.ExtendedIndex { + return nil, errors.New("Not supported") + } + r := WsBlockReq{} + err = json.Unmarshal(req.Params, &r) + if r.PageSize == 0 { + r.PageSize = 1000000 + } + if err == nil { + rv, err = s.getBlock(r.Id, r.Page, r.PageSize) + } + return + }, + "getAccountUtxo": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsAccountUtxoReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getAccountUtxo(r.Descriptor) } return }, - "getBalanceHistory": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Descriptor string `json:"descriptor"` - From int64 `json:"from"` - To int64 `json:"to"` - Currencies []string `json:"currencies"` - Gap int `json:"gap"` - GroupBy uint32 `json:"groupBy"` - }{} + "getBalanceHistory": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsBalanceHistoryReq{} err = json.Unmarshal(req.Params, &r) if err == nil { if r.From <= 0 { @@ -328,63 +350,93 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs } return }, - "getTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Txid string `json:"txid"` - }{} + "getTransaction": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsTransactionReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getTransaction(r.Txid) } return }, - "getTransactionSpecific": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Txid string `json:"txid"` - }{} + "getTransactionSpecific": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsTransactionSpecificReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getTransactionSpecific(r.Txid) } return }, - "estimateFee": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - return s.estimateFee(c, req.Params) + "estimateFee": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + return s.estimateFee(req.Params) + }, + "longTermFeeRate": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + return s.longTermFeeRate() }, - "sendTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Hex string `json:"hex"` - }{} + "sendTransaction": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsSendTransactionReq{} err = json.Unmarshal(req.Params, &r) if err == nil { - rv, err = s.sendTransaction(r.Hex) + rv, err = s.sendTransaction(r.Hex, r.DisableAlternativeRPC) } return }, - "subscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + + "getMempoolFilters": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsMempoolFiltersReq{} + err = json.Unmarshal(req.Params, &r) + if err == nil { + rv, err = s.getMempoolFilters(&r) + } + return + }, + "getBlockFilter": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsBlockFilterReq{} + err = json.Unmarshal(req.Params, &r) + if err == nil { + rv, err = s.getBlockFilter(&r) + } + return + }, + "getBlockFiltersBatch": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsBlockFiltersBatchReq{} + err = json.Unmarshal(req.Params, &r) + if err == nil { + rv, err = s.getBlockFiltersBatch(&r) + } + return + }, + "rpcCall": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsRpcCallReq{} + err = json.Unmarshal(req.Params, &r) + if err == nil { + rv, err = s.rpcCall(&r) + } + return + }, + "subscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.subscribeNewBlock(c, req) }, - "unsubscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "unsubscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.unsubscribeNewBlock(c) }, - "subscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "subscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.subscribeNewTransaction(c, req) }, - "unsubscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "unsubscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.unsubscribeNewTransaction(c) }, - "subscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "subscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { ad, err := s.unmarshalAddresses(req.Params) if err == nil { rv, err = s.subscribeAddresses(c, ad, req) } return }, - "unsubscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "unsubscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.unsubscribeAddresses(c) }, - "subscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - var r fiatRatesSubscription + "subscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + var r WsSubscribeFiatRatesReq err = json.Unmarshal(req.Params, &r) if err != nil { return nil, err @@ -395,41 +447,31 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs } return s.subscribeFiatRates(c, &r, req) }, - "unsubscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "unsubscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.unsubscribeFiatRates(c) }, - "ping": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "ping": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { r := struct{}{} return r, nil }, - "getCurrentFiatRates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Currencies []string `json:"currencies"` - Token string `json:"token"` - }{} + "getCurrentFiatRates": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsCurrentFiatRatesReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getCurrentFiatRates(r.Currencies, r.Token) } return }, - "getFiatRatesForTimestamps": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Timestamps []int64 `json:"timestamps"` - Currencies []string `json:"currencies"` - Token string `json:"token"` - }{} + "getFiatRatesForTimestamps": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsFiatRatesForTimestampsReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getFiatRatesForTimestamps(r.Timestamps, r.Currencies, r.Token) } return }, - "getFiatRatesTickersList": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Timestamp int64 `json:"timestamp"` - Token string `json:"token"` - }{} + "getFiatRatesTickersList": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsFiatRatesTickersListReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getAvailableVsCurrencies(r.Timestamp, r.Token) @@ -438,7 +480,7 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs }, } -func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { +func (s *WebsocketServer) onRequest(c *websocketChannel, req *WsReq) { var err error var data interface{} defer func() { @@ -451,7 +493,7 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { } // nil data means no response if data != nil { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: req.ID, Data: data, }) @@ -460,7 +502,9 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { }() 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 + defer func() { + 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) @@ -481,21 +525,8 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { } } -type accountInfoReq struct { - Descriptor string `json:"descriptor"` - Details string `json:"details"` - Tokens string `json:"tokens"` - PageSize int `json:"pageSize"` - Page int `json:"page"` - FromHeight int `json:"from"` - ToHeight int `json:"to"` - ContractFilter string `json:"contractFilter"` - SecondaryCurrency string `json:"secondaryCurrency"` - Gap int `json:"gap"` -} - -func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) { - var r accountInfoReq +func unmarshalGetAccountInfoRequest(params []byte) (*WsAccountInfoReq, error) { + var r WsAccountInfoReq err := json.Unmarshal(params, &r) if err != nil { return nil, err @@ -503,7 +534,7 @@ func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) { return &r, nil } -func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, err error) { +func (s *WebsocketServer) getAccountInfo(req *WsAccountInfoReq) (res *api.Address, err error) { var opt api.AccountDetails switch req.Details { case "tokens": @@ -545,7 +576,7 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, return a, nil } -func (s *WebsocketServer) getAccountUtxo(descriptor string) (interface{}, error) { +func (s *WebsocketServer) getAccountUtxo(descriptor string) (api.Utxos, error) { utxo, err := s.api.GetXpubUtxo(descriptor, false, 0) if err != nil { return s.api.GetAddressUtxo(descriptor, false) @@ -553,7 +584,7 @@ func (s *WebsocketServer) getAccountUtxo(descriptor string) (interface{}, error) return utxo, nil } -func (s *WebsocketServer) getTransaction(txid string) (interface{}, error) { +func (s *WebsocketServer) getTransaction(txid string) (*api.Tx, error) { return s.api.GetTransaction(txid, false, false) } @@ -561,40 +592,24 @@ func (s *WebsocketServer) getTransactionSpecific(txid string) (interface{}, erro return s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid}) } -func (s *WebsocketServer) getInfo() (interface{}, error) { +func (s *WebsocketServer) getInfo() (*WsInfoRes, 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"` - ConsensusVersion string `json:"consensus_version,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"` - Backend backendInfo `json:"backend"` - } - return &info{ + return &WsInfoRes{ Name: s.is.Coin, Shortcut: s.is.CoinShortcut, + Network: s.is.GetNetwork(), Decimals: s.chainParser.AmountDecimals(), BestHeight: int(height), BestHash: hash, Version: vi.Version, Block0Hash: s.block0hash, Testnet: s.chain.IsTestnet(), - Backend: backendInfo{ + Backend: WsBackendInfo{ Version: bi.Version, Subversion: bi.Subversion, ConsensusVersion: bi.ConsensusVersion, @@ -603,35 +618,54 @@ func (s *WebsocketServer) getInfo() (interface{}, error) { }, nil } -func (s *WebsocketServer) getBlockHash(height int) (interface{}, error) { +func (s *WebsocketServer) getBlockHash(height int) (*WsBlockHashRes, error) { h, err := s.db.GetBlockHash(uint32(height)) if err != nil { return nil, err } - type hash struct { - Hash string `json:"hash"` - } - return &hash{ + return &WsBlockHashRes{ Hash: h, }, nil } -func (s *WebsocketServer) estimateFee(c *websocketChannel, params []byte) (interface{}, error) { - type estimateFeeReq struct { - Blocks []int `json:"blocks"` - Specific map[string]interface{} `json:"specific"` +func (s *WebsocketServer) getBlock(id string, page, pageSize int) (interface{}, error) { + block, err := s.api.GetBlock(id, page, pageSize) + if err != nil { + return nil, err } - type estimateFeeRes struct { - FeePerTx string `json:"feePerTx,omitempty"` - FeePerUnit string `json:"feePerUnit,omitempty"` - FeeLimit string `json:"feeLimit,omitempty"` + return block, nil +} + +func eip1559FeesToApi(fee *bchain.Eip1559Fee) *api.Eip1559Fee { + if fee == nil { + return nil + } + apiFee := api.Eip1559Fee{} + apiFee.MaxFeePerGas = (*api.Amount)(fee.MaxFeePerGas) + apiFee.MaxPriorityFeePerGas = (*api.Amount)(fee.MaxPriorityFeePerGas) + apiFee.MaxWaitTimeEstimate = fee.MaxWaitTimeEstimate + apiFee.MinWaitTimeEstimate = fee.MinWaitTimeEstimate + return &apiFee +} + +func eip1559FeeRangeToApi(feeRange []*big.Int) []*api.Amount { + if feeRange == nil { + return nil } - var r estimateFeeReq + apiFeeRange := make([]*api.Amount, len(feeRange)) + for i := range feeRange { + apiFeeRange[i] = (*api.Amount)(feeRange[i]) + } + return apiFeeRange +} + +func (s *WebsocketServer) estimateFee(params []byte) (interface{}, error) { + var r WsEstimateFeeReq err := json.Unmarshal(params, &r) if err != nil { return nil, err } - res := make([]estimateFeeRes, len(r.Blocks)) + res := make([]WsEstimateFeeRes, len(r.Blocks)) if s.chainParser.GetChainType() == bchain.ChainEthereumType { gas, err := s.chain.EthereumTypeEstimateGas(r.Specific) if err != nil { @@ -646,11 +680,32 @@ func (s *WebsocketServer) estimateFee(c *websocketChannel, params []byte) (inter if err != nil { return nil, err } + feePerTx := new(big.Int) + feePerTx.Mul(&fee, new(big.Int).SetUint64(gas)) + eip1559, err := s.chain.EthereumTypeGetEip1559Fees() + if err != nil { + return nil, err + } + var eip1559Api *api.Eip1559Fees + if eip1559 != nil { + eip1559Api = &api.Eip1559Fees{} + eip1559Api.BaseFeePerGas = (*api.Amount)(eip1559.BaseFeePerGas) + eip1559Api.Instant = eip1559FeesToApi(eip1559.Instant) + eip1559Api.High = eip1559FeesToApi(eip1559.High) + eip1559Api.Medium = eip1559FeesToApi(eip1559.Medium) + eip1559Api.Low = eip1559FeesToApi(eip1559.Low) + eip1559Api.NetworkCongestion = eip1559.NetworkCongestion + eip1559Api.BaseFeeTrend = eip1559.BaseFeeTrend + eip1559Api.PriorityFeeTrend = eip1559.PriorityFeeTrend + eip1559Api.LatestPriorityFeeRange = eip1559FeeRangeToApi(eip1559.LatestPriorityFeeRange) + eip1559Api.HistoricalBaseFeeRange = eip1559FeeRangeToApi(eip1559.HistoricalBaseFeeRange) + eip1559Api.HistoricalPriorityFeeRange = eip1559FeeRangeToApi(eip1559.HistoricalPriorityFeeRange) + } for i := range r.Blocks { res[i].FeePerUnit = fee.String() res[i].FeeLimit = sg - fee.Mul(&fee, new(big.Int).SetUint64(gas)) - res[i].FeePerTx = fee.String() + res[i].FeePerTx = feePerTx.String() + res[i].Eip1559 = eip1559Api } } else { conservative := true @@ -686,8 +741,19 @@ func (s *WebsocketServer) estimateFee(c *websocketChannel, params []byte) (inter return res, nil } -func (s *WebsocketServer) sendTransaction(tx string) (res resultSendTransaction, err error) { - txid, err := s.chain.SendRawTransaction(tx) +func (s *WebsocketServer) longTermFeeRate() (res interface{}, err error) { + feeRate, err := s.chain.LongTermFeeRate() + if err != nil { + return nil, err + } + return WsLongTermFeeRateRes{ + FeePerUnit: feeRate.FeePerUnit.String(), + Blocks: feeRate.Blocks, + }, nil +} + +func (s *WebsocketServer) sendTransaction(tx string, disableAlternativeRPC bool) (res resultSendTransaction, err error) { + txid, err := s.chain.SendRawTransaction(tx, disableAlternativeRPC) if err != nil { return res, err } @@ -695,6 +761,83 @@ func (s *WebsocketServer) sendTransaction(tx string) (res resultSendTransaction, return } +func (s *WebsocketServer) getMempoolFilters(r *WsMempoolFiltersReq) (res interface{}, err error) { + type resMempoolFilters struct { + ParamP uint8 `json:"P"` + ParamM uint64 `json:"M"` + ZeroedKey bool `json:"zeroedKey"` + Entries map[string]string `json:"entries"` + } + filterEntries, err := s.mempool.GetTxidFilterEntries(r.ScriptType, r.FromTimestamp) + if err != nil { + return nil, err + } + return resMempoolFilters{ + ParamP: s.is.BlockGolombFilterP, + ParamM: bchain.GetGolombParamM(s.is.BlockGolombFilterP), + ZeroedKey: filterEntries.UsedZeroedKey, + Entries: filterEntries.Entries, + }, nil +} + +func (s *WebsocketServer) getBlockFilter(r *WsBlockFilterReq) (res interface{}, err error) { + type resBlockFilter struct { + ParamP uint8 `json:"P"` + ParamM uint64 `json:"M"` + ZeroedKey bool `json:"zeroedKey"` + BlockFilter string `json:"blockFilter"` + } + if s.is.BlockFilterScripts != r.ScriptType { + return nil, errors.Errorf("Unsupported script type %s", r.ScriptType) + } + blockFilter, err := s.db.GetBlockFilter(r.BlockHash) + if err != nil { + return nil, err + } + return resBlockFilter{ + ParamP: s.is.BlockGolombFilterP, + ParamM: bchain.GetGolombParamM(s.is.BlockGolombFilterP), + ZeroedKey: s.is.BlockFilterUseZeroedKey, + BlockFilter: blockFilter, + }, nil +} + +func (s *WebsocketServer) getBlockFiltersBatch(r *WsBlockFiltersBatchReq) (res interface{}, err error) { + type resBlockFiltersBatch struct { + ParamP uint8 `json:"P"` + ParamM uint64 `json:"M"` + ZeroedKey bool `json:"zeroedKey"` + BlockFiltersBatch []string `json:"blockFiltersBatch"` + } + if s.is.BlockFilterScripts != r.ScriptType { + return nil, errors.Errorf("Unsupported script type %s", r.ScriptType) + } + blockFiltersBatch, err := s.api.GetBlockFiltersBatch(r.BlockHash, r.PageSize) + if err != nil { + return nil, err + } + return resBlockFiltersBatch{ + ParamP: s.is.BlockGolombFilterP, + ParamM: bchain.GetGolombParamM(s.is.BlockGolombFilterP), + ZeroedKey: s.is.BlockFilterUseZeroedKey, + BlockFiltersBatch: blockFiltersBatch, + }, nil +} + +func (s *WebsocketServer) rpcCall(r *WsRpcCallReq) (*WsRpcCallRes, error) { + if s.allowedRpcCallTo != nil { + _, ok := s.allowedRpcCallTo[strings.ToLower(r.To)] + if !ok { + return nil, errors.New("Not supported") + } + } + data, err := s.chain.EthereumTypeRpcCall(r.Data, r.To, r.From) + if err != nil { + return nil, err + } + return &WsRpcCallRes{Data: data}, nil +} + type subscriptionResponse struct { Subscribed bool `json:"subscribed"` } @@ -703,7 +846,7 @@ type subscriptionResponseMessage struct { Message string `json:"message"` } -func (s *WebsocketServer) subscribeNewBlock(c *websocketChannel, req *websocketReq) (res interface{}, err error) { +func (s *WebsocketServer) subscribeNewBlock(c *websocketChannel, req *WsReq) (res interface{}, err error) { s.newBlockSubscriptionsLock.Lock() defer s.newBlockSubscriptionsLock.Unlock() s.newBlockSubscriptions[c] = req.ID @@ -719,7 +862,7 @@ func (s *WebsocketServer) unsubscribeNewBlock(c *websocketChannel) (res interfac return &subscriptionResponse{false}, nil } -func (s *WebsocketServer) subscribeNewTransaction(c *websocketChannel, req *websocketReq) (res interface{}, err error) { +func (s *WebsocketServer) subscribeNewTransaction(c *websocketChannel, req *WsReq) (res interface{}, err error) { s.newTransactionSubscriptionsLock.Lock() defer s.newTransactionSubscriptionsLock.Unlock() if !s.newTransactionEnabled { @@ -742,9 +885,7 @@ func (s *WebsocketServer) unsubscribeNewTransaction(c *websocketChannel) (res in } func (s *WebsocketServer) unmarshalAddresses(params []byte) ([]string, error) { - r := struct { - Addresses []string `json:"addresses"` - }{} + r := WsSubscribeAddressesReq{} err := json.Unmarshal(params, &r) if err != nil { return nil, err @@ -760,7 +901,7 @@ func (s *WebsocketServer) unmarshalAddresses(params []byte) ([]string, error) { return rv, nil } -// unsubscribe addresses without addressSubscriptionsLock - can be called only from subscribeAddresses and unsubscribeAddresses +// doUnsubscribeAddresses 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] @@ -778,7 +919,7 @@ func (s *WebsocketServer) doUnsubscribeAddresses(c *websocketChannel) { c.addrDescs = nil } -func (s *WebsocketServer) subscribeAddresses(c *websocketChannel, addrDesc []string, req *websocketReq) (res interface{}, err error) { +func (s *WebsocketServer) subscribeAddresses(c *websocketChannel, addrDesc []string, req *WsReq) (res interface{}, err error) { s.addressSubscriptionsLock.Lock() defer s.addressSubscriptionsLock.Unlock() // unsubscribe all previous subscriptions @@ -805,7 +946,7 @@ func (s *WebsocketServer) unsubscribeAddresses(c *websocketChannel) (res interfa return &subscriptionResponse{false}, nil } -// unsubscribe fiat rates without fiatRatesSubscriptionsLock - can be called only from subscribeFiatRates and unsubscribeFiatRates +// doUnsubscribeFiatRates 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 { @@ -821,7 +962,7 @@ func (s *WebsocketServer) doUnsubscribeFiatRates(c *websocketChannel) { } // subscribeFiatRates subscribes all FiatRates subscriptions by this channel -func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, d *fiatRatesSubscription, req *websocketReq) (res interface{}, err error) { +func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, d *WsSubscribeFiatRatesReq, req *WsReq) (res interface{}, err error) { s.fiatRatesSubscriptionsLock.Lock() defer s.fiatRatesSubscriptionsLock.Unlock() // unsubscribe all previous subscriptions @@ -865,7 +1006,7 @@ func (s *WebsocketServer) onNewBlockAsync(hash string, height uint32) { Hash: hash, } for c, id := range s.newBlockSubscriptions { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &data, }) @@ -882,7 +1023,7 @@ func (s *WebsocketServer) sendOnNewTx(tx *api.Tx) { s.newTransactionSubscriptionsLock.Lock() defer s.newTransactionSubscriptionsLock.Unlock() for c, id := range s.newTransactionSubscriptions { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &tx, }) @@ -910,7 +1051,7 @@ func (s *WebsocketServer) sendOnNewTxAddr(stringAddressDescriptor string, tx *ap as, ok := s.addressSubscriptions[stringAddressDescriptor] if ok { for c, id := range as { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &data, }) @@ -1012,12 +1153,12 @@ func (s *WebsocketServer) broadcastTicker(currency string, rates map[string]floa dataWithTokens.TokenRates[token] = rate } } - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &dataWithTokens, }) } else { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &data, }) @@ -1037,17 +1178,17 @@ func (s *WebsocketServer) OnNewFiatRatesTicker(ticker *common.CurrencyRatesTicke s.broadcastTicker(allFiatRates, ticker.Rates, nil) } -func (s *WebsocketServer) getCurrentFiatRates(currencies []string, token string) (interface{}, error) { +func (s *WebsocketServer) getCurrentFiatRates(currencies []string, token string) (*api.FiatTicker, error) { ret, err := s.api.GetCurrentFiatRates(currencies, strings.ToLower(token)) return ret, err } -func (s *WebsocketServer) getFiatRatesForTimestamps(timestamps []int64, currencies []string, token string) (interface{}, error) { +func (s *WebsocketServer) getFiatRatesForTimestamps(timestamps []int64, currencies []string, token string) (*api.FiatTickers, error) { ret, err := s.api.GetFiatRatesForTimestamps(timestamps, currencies, strings.ToLower(token)) return ret, err } -func (s *WebsocketServer) getAvailableVsCurrencies(timestamp int64, token string) (interface{}, error) { +func (s *WebsocketServer) getAvailableVsCurrencies(timestamp int64, token string) (*api.AvailableVsCurrencies, error) { ret, err := s.api.GetAvailableVsCurrencies(timestamp, strings.ToLower(token)) return ret, err } diff --git a/server/ws_types.go b/server/ws_types.go new file mode 100644 index 0000000000..3f17f6cea7 --- /dev/null +++ b/server/ws_types.go @@ -0,0 +1,188 @@ +package server + +import ( + "encoding/json" + + "github.com/trezor/blockbook/api" +) + +// WsReq represents a generic WebSocket request with an ID, method, and raw parameters. +type WsReq struct { + ID string `json:"id" ts_doc:"Unique request identifier."` + Method string `json:"method" ts_type:"'getAccountInfo' | 'getInfo' | 'getBlockHash'| 'getBlock' | 'getAccountUtxo' | 'getBalanceHistory' | 'getTransaction' | 'getTransactionSpecific' | 'estimateFee' | 'sendTransaction' | 'subscribeNewBlock' | 'unsubscribeNewBlock' | 'subscribeNewTransaction' | 'unsubscribeNewTransaction' | 'subscribeAddresses' | 'unsubscribeAddresses' | 'subscribeFiatRates' | 'unsubscribeFiatRates' | 'ping' | 'getCurrentFiatRates' | 'getFiatRatesForTimestamps' | 'getFiatRatesTickersList' | 'getMempoolFilters'" ts_doc:"Requested method name."` + Params json.RawMessage `json:"params" ts_type:"any" ts_doc:"Parameters for the requested method in raw JSON format."` +} + +// WsRes represents a generic WebSocket response with an ID and arbitrary data. +type WsRes struct { + ID string `json:"id" ts_doc:"Corresponding request identifier."` + Data interface{} `json:"data" ts_doc:"Payload of the response, structure depends on the request."` +} + +// WsAccountInfoReq carries parameters for the 'getAccountInfo' method. +type WsAccountInfoReq struct { + Descriptor string `json:"descriptor" ts_doc:"Address or XPUB descriptor to query."` + Details string `json:"details,omitempty" ts_type:"'basic' | 'tokens' | 'tokenBalances' | 'txids' | 'txslight' | 'txs'" ts_doc:"Level of detail to retrieve about the account."` + Tokens string `json:"tokens,omitempty" ts_type:"'derived' | 'used' | 'nonzero'" ts_doc:"Which tokens to include in the account info."` + PageSize int `json:"pageSize,omitempty" ts_doc:"Number of items per page, if paging is used."` + Page int `json:"page,omitempty" ts_doc:"Requested page index, if paging is used."` + FromHeight int `json:"from,omitempty" ts_doc:"Starting block height for transaction filtering."` + ToHeight int `json:"to,omitempty" ts_doc:"Ending block height for transaction filtering."` + ContractFilter string `json:"contractFilter,omitempty" ts_doc:"Filter by specific contract address (for token data)."` + SecondaryCurrency string `json:"secondaryCurrency,omitempty" ts_doc:"Currency code to convert values into (e.g. 'USD')."` + Gap int `json:"gap,omitempty" ts_doc:"Gap limit for XPUB scanning, if relevant."` +} + +// WsBackendInfo holds extended info about the connected backend node. +type WsBackendInfo struct { + Version string `json:"version,omitempty" ts_doc:"Backend version string."` + Subversion string `json:"subversion,omitempty" ts_doc:"Backend sub-version string."` + ConsensusVersion string `json:"consensus_version,omitempty" ts_doc:"Consensus protocol version in use."` + Consensus interface{} `json:"consensus,omitempty" ts_doc:"Additional consensus details, structure depends on blockchain."` +} + +// WsInfoRes is returned by 'getInfo' requests, containing basic blockchain info. +type WsInfoRes struct { + Name string `json:"name" ts_doc:"Human-readable blockchain name."` + Shortcut string `json:"shortcut" ts_doc:"Short code for the blockchain (e.g. BTC, ETH)."` + Network string `json:"network" ts_doc:"Network identifier (e.g. mainnet, testnet)."` + Decimals int `json:"decimals" ts_doc:"Number of decimals in the base unit of the coin."` + Version string `json:"version" ts_doc:"Version of the blockbook or backend service."` + BestHeight int `json:"bestHeight" ts_doc:"Current best chain height according to the backend."` + BestHash string `json:"bestHash" ts_doc:"Block hash of the best (latest) block."` + Block0Hash string `json:"block0Hash" ts_doc:"Genesis block hash or identifier."` + Testnet bool `json:"testnet" ts_doc:"Indicates if this is a test network."` + Backend WsBackendInfo `json:"backend" ts_doc:"Additional backend-related information."` +} + +// WsBlockHashReq holds a single integer for querying the block hash at that height. +type WsBlockHashReq struct { + Height int `json:"height" ts_doc:"Block height for which the hash is requested."` +} + +// WsBlockHashRes returns the block hash for a requested height. +type WsBlockHashRes struct { + Hash string `json:"hash" ts_doc:"Block hash at the requested height."` +} + +// WsBlockReq is used to request details of a block (by ID) with paging options. +type WsBlockReq struct { + Id string `json:"id" ts_doc:"Block identifier (hash)."` + PageSize int `json:"pageSize,omitempty" ts_doc:"Number of transactions per page in the block."` + Page int `json:"page,omitempty" ts_doc:"Page index to retrieve if multiple pages of transactions are available."` +} + +// WsAccountUtxoReq is used to request unspent transaction outputs (UTXOs) for a given xpub/address. +type WsAccountUtxoReq struct { + Descriptor string `json:"descriptor" ts_doc:"Address or XPUB descriptor to retrieve UTXOs for."` +} + +// WsBalanceHistoryReq is used to retrieve a historical balance chart or intervals for an account. +type WsBalanceHistoryReq struct { + Descriptor string `json:"descriptor" ts_doc:"Address or XPUB descriptor to query history for."` + From int64 `json:"from,omitempty" ts_doc:"Unix timestamp from which to start the history."` + To int64 `json:"to,omitempty" ts_doc:"Unix timestamp at which to end the history."` + Currencies []string `json:"currencies,omitempty" ts_doc:"List of currency codes for which to fetch exchange rates at each interval."` + Gap int `json:"gap,omitempty" ts_doc:"Gap limit for XPUB scanning, if relevant."` + GroupBy uint32 `json:"groupBy,omitempty" ts_doc:"Size of each aggregated time window in seconds."` +} + +// WsTransactionReq requests details for a specific transaction by its txid. +type WsTransactionReq struct { + Txid string `json:"txid" ts_doc:"Transaction ID to retrieve details for."` +} + +// WsMempoolFiltersReq requests mempool filters for scripts of a specific type, after a given timestamp. +type WsMempoolFiltersReq struct { + ScriptType string `json:"scriptType" ts_doc:"Type of script we are filtering for (e.g., P2PKH, P2SH)."` + FromTimestamp uint32 `json:"fromTimestamp" ts_doc:"Only retrieve filters for mempool txs after this timestamp."` + ParamM uint64 `json:"M,omitempty" ts_doc:"Optional parameter for certain filter logic (e.g., n-bloom)."` +} + +// WsBlockFilterReq requests a filter for a given block hash and script type. +type WsBlockFilterReq struct { + ScriptType string `json:"scriptType" ts_doc:"Type of script filter (e.g., P2PKH, P2SH)."` + BlockHash string `json:"blockHash" ts_doc:"Block hash for which we want the filter."` + ParamM uint64 `json:"M,omitempty" ts_doc:"Optional parameter for certain filter logic."` +} + +// WsBlockFiltersBatchReq is used to request batch filters for consecutive blocks. +type WsBlockFiltersBatchReq struct { + ScriptType string `json:"scriptType" ts_doc:"Type of script filter (e.g., P2PKH, P2SH)."` + BlockHash string `json:"bestKnownBlockHash" ts_doc:"Hash of the latest known block. Filters will be retrieved backward from here."` + PageSize int `json:"pageSize,omitempty" ts_doc:"Number of block filters per request."` + ParamM uint64 `json:"M,omitempty" ts_doc:"Optional parameter for certain filter logic."` +} + +// WsTransactionSpecificReq requests blockchain-specific transaction info that might go beyond standard fields. +type WsTransactionSpecificReq struct { + Txid string `json:"txid" ts_doc:"Transaction ID for the detailed blockchain-specific data."` +} + +// WsEstimateFeeReq requests an estimation of transaction fees for a set of blocks or with specific parameters. +type WsEstimateFeeReq struct { + Blocks []int `json:"blocks,omitempty" ts_doc:"Block confirmations targets for which fees should be estimated."` + Specific map[string]interface{} `json:"specific,omitempty" ts_type:"{conservative?: boolean; txsize?: number; from?: string; to?: string; data?: string; value?: string;}" ts_doc:"Additional chain-specific parameters (e.g. for Ethereum)."` +} + +// WsEstimateFeeRes is returned in response to a fee estimation request. +type WsEstimateFeeRes struct { + FeePerTx string `json:"feePerTx,omitempty" ts_doc:"Estimated total fee per transaction, if relevant."` + FeePerUnit string `json:"feePerUnit,omitempty" ts_doc:"Estimated fee per unit (sat/byte, Wei/gas, etc.)."` + FeeLimit string `json:"feeLimit,omitempty" ts_doc:"Max fee limit for blockchains like Ethereum."` + Eip1559 *api.Eip1559Fees `json:"eip1559,omitempty"` +} + +// WsLongTermFeeRateRes is returned in response to a long term fee rate request. +type WsLongTermFeeRateRes struct { + FeePerUnit string `json:"feePerUnit" ts_doc:"Long term fee rate (in sat/kByte)."` + Blocks uint64 `json:"blocks" ts_doc:"Amount of blocks used for the long term fee rate estimation."` +} + +// WsSendTransactionReq is used to broadcast a transaction to the network. +type WsSendTransactionReq struct { + Hex string `json:"hex,omitempty" ts_doc:"Hex-encoded transaction data to broadcast (string format)."` + DisableAlternativeRPC bool `json:"disableAlternativeRpc" ts_doc:"Use alternative RPC method to broadcast transaction."` +} + +// WsSubscribeAddressesReq is used to subscribe to updates on a list of addresses. +type WsSubscribeAddressesReq struct { + Addresses []string `json:"addresses" ts_doc:"List of addresses to subscribe for updates (e.g., new transactions)."` +} + +// WsSubscribeFiatRatesReq subscribes to updates of fiat rates for a specific currency or set of tokens. +type WsSubscribeFiatRatesReq struct { + Currency string `json:"currency,omitempty" ts_doc:"Fiat currency code (e.g. 'USD')."` + Tokens []string `json:"tokens,omitempty" ts_doc:"List of token symbols or IDs to get fiat rates for."` +} + +// WsCurrentFiatRatesReq requests the current fiat rates for specified currencies (and optionally a token). +type WsCurrentFiatRatesReq struct { + Currencies []string `json:"currencies,omitempty" ts_doc:"List of fiat currencies, e.g. ['USD','EUR']."` + Token string `json:"token,omitempty" ts_doc:"Token symbol or ID if asking for token fiat rates (e.g. 'ETH')."` +} + +// WsFiatRatesForTimestampsReq requests historical fiat rates for given timestamps. +type WsFiatRatesForTimestampsReq struct { + Timestamps []int64 `json:"timestamps" ts_doc:"List of Unix timestamps for which to retrieve fiat rates."` + Currencies []string `json:"currencies,omitempty" ts_doc:"List of fiat currencies, e.g. ['USD','EUR']."` + Token string `json:"token,omitempty" ts_doc:"Token symbol or ID if asking for token fiat rates."` +} + +// WsFiatRatesTickersListReq requests a list of tickers for a given timestamp (and possibly a token). +type WsFiatRatesTickersListReq struct { + Timestamp int64 `json:"timestamp,omitempty" ts_doc:"Timestamp for which the list of available tickers is needed."` + Token string `json:"token,omitempty" ts_doc:"Token symbol or ID if asking for token-specific fiat rates."` +} + +// WsRpcCallReq is used for raw RPC calls (for example, on an Ethereum-like backend). +type WsRpcCallReq struct { + From string `json:"from,omitempty" ts_doc:"Address from which the RPC call is originated (if relevant)."` + To string `json:"to" ts_doc:"Contract or address to which the RPC call is made."` + Data string `json:"data" ts_doc:"Hex-encoded call data (function signature + parameters)."` +} + +// WsRpcCallRes returns the result of an RPC call in hex form. +type WsRpcCallRes struct { + Data string `json:"data" ts_doc:"Hex-encoded return data from the call."` +} diff --git a/shell.nix b/shell.nix index 89b2034ce5..ee60043428 100644 --- a/shell.nix +++ b/shell.nix @@ -11,6 +11,7 @@ stdenv.mkDerivation { snappy zeromq zlib + gcc ]; shellHook = '' export CGO_LDFLAGS="-L${stdenv.cc.cc.lib}/lib -lrocksdb -lz -lbz2 -lsnappy -llz4 -lm -lstdc++" diff --git a/static/css/bootstrap.5.2.2.min.css b/static/css/bootstrap.5.2.2.min.css new file mode 100644 index 0000000000..1359b3b721 --- /dev/null +++ b/static/css/bootstrap.5.2.2.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.2.2 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors + * Copyright 2011-2022 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid var(--bs-border-color);border-radius:.375rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:var(--bs-table-color);vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:2px solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#bacbe6;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#cbccce;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#bcd0c7;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#badce3;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#e6dbb9;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#dfc2c4;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#dfe0e1;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#373b3e;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:calc(1.5em + .75rem + 2px);padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:.375rem}.form-control-color::-webkit-color-swatch{border-radius:.375rem}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + 2px)}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + 2px)}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.25rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.5rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;width:100%;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.375rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.375rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.375rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:#212529;--bs-btn-bg:transparent;--bs-btn-border-width:1px;--bs-btn-border-color:transparent;--bs-btn-border-radius:0.375rem;--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:none;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:0.5rem}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:0.25rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:#212529;--bs-dropdown-bg:#fff;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:0.375rem;--bs-dropdown-border-width:1px;--bs-dropdown-inner-border-radius:calc(0.375rem - 1px);--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-dropdown-link-color:#212529;--bs-dropdown-link-hover-color:#1e2125;--bs-dropdown-link-hover-bg:#e9ecef;--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:.375rem}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:#6c757d;display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:1px;--bs-nav-tabs-border-color:#dee2e6;--bs-nav-tabs-border-radius:0.375rem;--bs-nav-tabs-link-hover-border-color:#e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color:#495057;--bs-nav-tabs-link-active-bg:#fff;--bs-nav-tabs-link-active-border-color:#dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));background:0 0;border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:0.375rem;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{background:0 0;border:0;border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(0, 0, 0, 0.55);--bs-navbar-hover-color:rgba(0, 0, 0, 0.7);--bs-navbar-disabled-color:rgba(0, 0, 0, 0.3);--bs-navbar-active-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-hover-color:rgba(0, 0, 0, 0.9);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(0, 0, 0, 0.1);--bs-navbar-toggler-border-radius:0.375rem;--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .show>.nav-link{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-border-width:1px;--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:0.375rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(0.375rem - 1px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(0, 0, 0, 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#fff;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:#212529;--bs-accordion-bg:#fff;--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:1px;--bs-accordion-border-radius:0.375rem;--bs-accordion-inner-border-radius:calc(0.375rem - 1px);--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:#212529;--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color:#86b7fe;--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:#0c63e4;--bs-accordion-active-bg:#e7f1ff}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button,.accordion-flush .accordion-item .accordion-button.collapsed{border-radius:0}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:#6c757d;--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#6c757d;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:#fff;--bs-pagination-border-width:1px;--bs-pagination-border-color:#dee2e6;--bs-pagination-border-radius:0.375rem;--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:#e9ecef;--bs-pagination-hover-border-color:#dee2e6;--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:#e9ecef;--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:#6c757d;--bs-pagination-disabled-bg:#fff;--bs-pagination-disabled-border-color:#dee2e6;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:0.5rem}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:0.25rem}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:0.375rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:1px solid var(--bs-alert-border-color);--bs-alert-border-radius:0.375rem;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:#084298;--bs-alert-bg:#cfe2ff;--bs-alert-border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{--bs-alert-color:#41464b;--bs-alert-bg:#e2e3e5;--bs-alert-border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{--bs-alert-color:#0f5132;--bs-alert-bg:#d1e7dd;--bs-alert-border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{--bs-alert-color:#055160;--bs-alert-bg:#cff4fc;--bs-alert-border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{--bs-alert-color:#664d03;--bs-alert-bg:#fff3cd;--bs-alert-border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{--bs-alert-color:#842029;--bs-alert-bg:#f8d7da;--bs-alert-border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{--bs-alert-color:#636464;--bs-alert-bg:#fefefe;--bs-alert-border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{--bs-alert-color:#141619;--bs-alert-bg:#d3d3d4;--bs-alert-border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:#e9ecef;--bs-progress-border-radius:0.375rem;--bs-progress-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:#212529;--bs-list-group-bg:#fff;--bs-list-group-border-color:rgba(0, 0, 0, 0.125);--bs-list-group-border-width:1px;--bs-list-group-border-radius:0.375rem;--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#495057;--bs-list-group-action-hover-color:#495057;--bs-list-group-action-hover-bg:#f8f9fa;--bs-list-group-action-active-color:#212529;--bs-list-group-action-active-bg:#e9ecef;--bs-list-group-disabled-color:#6c757d;--bs-list-group-disabled-bg:#fff;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(255, 255, 255, 0.85);--bs-toast-border-width:1px;--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:0.375rem;--bs-toast-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-toast-header-color:#6c757d;--bs-toast-header-bg:rgba(255, 255, 255, 0.85);--bs-toast-header-border-color:rgba(0, 0, 0, 0.05);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:#fff;--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:1px;--bs-modal-border-radius:0.5rem;--bs-modal-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-modal-inner-border-radius:calc(0.5rem - 1px);--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:1px;--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:#fff;--bs-tooltip-bg:#000;--bs-tooltip-border-radius:0.375rem;--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;padding:var(--bs-tooltip-arrow-height);margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:#fff;--bs-popover-border-width:1px;--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:0.5rem;--bs-popover-inner-border-radius:calc(0.5rem - 1px);--bs-popover-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color: ;--bs-popover-header-bg:#f0f0f0;--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:#212529;--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color: ;--bs-offcanvas-bg:#fff;--bs-offcanvas-border-width:1px;--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:575.98px){.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}}@media (max-width:575.98px){.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:767.98px){.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}}@media (max-width:767.98px){.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:991.98px){.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}}@media (max-width:991.98px){.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1199.98px){.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}}@media (max-width:1199.98px){.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}}@media (max-width:1399.98px){.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(13,110,253,var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(108,117,125,var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(25,135,84,var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(13,202,240,var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(255,193,7,var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(220,53,69,var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(248,249,250,var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(33,37,41,var(--bs-bg-opacity,1))!important}.link-primary{color:#0d6efd!important}.link-primary:focus,.link-primary:hover{color:#0a58ca!important}.link-secondary{color:#6c757d!important}.link-secondary:focus,.link-secondary:hover{color:#565e64!important}.link-success{color:#198754!important}.link-success:focus,.link-success:hover{color:#146c43!important}.link-info{color:#0dcaf0!important}.link-info:focus,.link-info:hover{color:#3dd5f3!important}.link-warning{color:#ffc107!important}.link-warning:focus,.link-warning:hover{color:#ffcd39!important}.link-danger{color:#dc3545!important}.link-danger:focus,.link-danger:hover{color:#b02a37!important}.link-light{color:#f8f9fa!important}.link-light:focus,.link-light:hover{color:#f9fafb!important}.link-dark{color:#212529!important}.link-dark:focus,.link-dark:hover{color:#1a1e21!important}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-1{--bs-border-width:1px}.border-2{--bs-border-width:2px}.border-3{--bs-border-width:3px}.border-4{--bs-border-width:4px}.border-5{--bs-border-width:5px}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-semibold{font-weight:600!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-2xl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css index b02f915c58..e708ac188c 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -200,6 +200,10 @@ span.btn-paging:hover { color: #757575; } +.btn-paging.active:hover { + background-color: white; +} + .paging-group { border: 1px solid #e2e2e2; border-radius: 0.5rem; @@ -694,4 +698,4 @@ span.btn-paging:hover { .btn { --bs-btn-font-size: 1rem; } -} \ No newline at end of file +} diff --git a/static/css/main.min.2.css b/static/css/main.min.2.css deleted file mode 100644 index dbafe0842b..0000000000 --- a/static/css/main.min.2.css +++ /dev/null @@ -1 +0,0 @@ -@import "TTHoves/TTHoves.css";* {margin: 0;padding: 0;outline: none;font-family: "TT Hoves", -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;}html, body {height: 100%;}body {min-height: 100%;margin: 0;background: linear-gradient(to bottom, #f6f6f6 360px, #e5e5e5 0), #e5e5e5;background-repeat: no-repeat;}a {color: #00854d;text-decoration: none;}a:hover {color: #00854d;text-decoration: underline;}select {border-radius: 0.5rem;padding-left: 0.5rem;border: 1px solid #ced4da;color: var(--bs-body-color);min-height: 45px;}#header {position: fixed;top: 0;left: 0;width: 100%;margin: 0;padding-bottom: 0;padding-top: 0;background-color: white;border-bottom: 1px solid #f6f6f6;z-index: 10;}#header a {color: var(--bs-navbar-brand-color);}#header a:hover {color: var(--bs-navbar-brand-hover-color);}#header .navbar {--bs-navbar-padding-y: 0.7rem;}#header .form-control-lg {font-size: 1rem;padding: 0.75rem 1rem;}#header .container {min-height: 50px;}#header .btn.dropdown-toggle {padding-right: 0;}#header .dropdown-menu {--bs-dropdown-min-width: 13rem;}#header .dropdown-menu[data-bs-popper] {left: initial;right: 0;}#header .dropdown-menu.show {display: flex;}.form-control:focus {outline: 0;box-shadow: none;border-color: #00854d;}.base-value {color: #757575 !important;padding-left: 0.5rem;font-weight: normal;}.badge {vertical-align: middle;text-transform: uppercase;letter-spacing: 0.15em;--bs-badge-padding-x: 0.8rem;--bs-badge-font-weight: normal;--bs-badge-border-radius: 0.6rem;}.bg-secondary {background-color: #757575 !important;}.accordion {--bs-accordion-border-radius: 10px;--bs-accordion-inner-border-radius: calc(10px - 1px);--bs-accordion-color: var(--bs-body-color);--bs-accordion-active-color: var(--bs-body-color);--bs-accordion-active-bg: white;--bs-accordion-btn-active-icon: url("data:image/svg+xml,");}.accordion-button:focus {outline: 0;box-shadow: none;}.accordion-body {letter-spacing: -0.01em;}.bb-group {border: 0.6rem solid #f6f6f6;background-color: #f6f6f6;border-radius: 0.5rem;position: relative;display: inline-flex;vertical-align: middle;}.bb-group>.btn {--bs-btn-padding-x: 0.5rem;--bs-btn-padding-y: 0.22rem;--bs-btn-border-radius: 0.3rem;--bs-btn-border-width: 0;color: #545454;}.bb-group>.btn-check:checked+.btn, .bb-group .btn.active {color: black;font-weight: bold;background-color: white;}.paging {display: flex;}.paging .bb-group>.btn {min-width: 2rem;margin-left: 0.1rem;margin-right: 0.1rem;}.paging .bb-group>.btn:hover {background-color: white;}.paging a {text-decoration: none;}.btn-paging {--bs-btn-color: #757575;--bs-btn-border-color: #e2e2e2;--bs-btn-hover-color: black;--bs-btn-hover-bg: #f6f6f6;--bs-btn-hover-border-color: #e2e2e2;--bs-btn-focus-shadow-rgb: 108, 117, 125;--bs-btn-active-color: #fff;--bs-btn-active-bg: #e2e2e2;--bs-btn-active-border-color: #e2e2e2;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-gradient: none;--bs-btn-padding-y: 0.75rem;--bs-btn-padding-x: 1.1rem;--bs-btn-border-radius: 0.5rem;--bs-btn-font-weight: bold;background-color: #f6f6f6;}span.btn-paging {cursor: initial;}span.btn-paging:hover {color: #757575;}.paging-group {border: 1px solid #e2e2e2;border-radius: 0.5rem;}.paging-group>.bb-group {border: 0.53rem solid #f6f6f6;}#wrap {min-height: 100%;height: auto;padding: 112px 0 75px 0;margin: 0 auto -56px;}#footer {background-color: black;color: #757575;height: 56px;overflow: hidden;}.navbar-form {width: 60%;}.navbar-form button {margin-left: -50px;position: relative;}.search-icon {width: 16px;height: 16px;position: absolute;top: 16px;background-size: cover;background-image: url("data:image/svg+xml, %3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.24976 12.5C10.1493 12.5 12.4998 10.1495 12.4998 7.25C12.4998 4.35051 10.1493 2 7.24976 2C4.35026 2 1.99976 4.35051 1.99976 7.25C1.99976 10.1495 4.35026 12.5 7.24976 12.5Z' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3Cpath d='M10.962 10.9625L13.9996 14.0001' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E");}.navbar-form ::placeholder {color: #e2e2e2;}.ellipsis {overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.data-table {table-layout: fixed;overflow-wrap: anywhere;margin-left: 8px;margin-top: 2rem;margin-bottom: 2rem;width: calc(100% - 16px);}.data-table thead {padding-bottom: 20px;}.table.data-table> :not(caption)>*>* {padding: 0.8rem 0.8rem;background-color: var(--bs-table-bg);border-bottom-width: 1px;box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);}.table.data-table>thead>*>* {padding-bottom: 1.5rem;}.table.data-table>*>*:last-child>* {border-bottom: none;}.data-table thead, .data-table thead tr, .data-table thead th {color: #757575;border: none;font-weight: normal;}.data-table tbody th {color: #757575;font-weight: normal;}.data-table tbody {background: white;border-radius: 8px;box-shadow: 0 0 0 8px white;}.data-table h3, .data-table h5, .data-table h6 {margin-bottom: 0;}.data-table h3, .data-table h5 {color: var(--bs-body-color);}.accordion .table.data-table>thead>*>* {padding-bottom: 0;}.info-table tbody {display: inline-table;width: 100%;}.info-table td {font-weight: bold;}.info-table tr>td:first-child {font-weight: normal;color: #757575;}.ns:before {content: " ";}.nc:before {content: ",";}.trezor-logo {width: 128px;height: 32px;position: absolute;top: 16px;background-size: cover;background-image: url("data:image/svg+xml,%3Csvg style='width: 128px%3B' version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 163.7 41.9' space='preserve'%3E%3Cpolygon points='101.1 12.8 118.2 12.8 118.2 17.3 108.9 29.9 118.2 29.9 118.2 35.2 101.1 35.2 101.1 30.7 110.4 18.1 101.1 18.1'%3E%3C/polygon%3E%3Cpath d='M158.8 26.9c2.1-0.8 4.3-2.9 4.3-6.6c0-4.5-3.1-7.4-7.7-7.4h-10.5v22.3h5.8v-7.5h2.2l4.1 7.5h6.7L158.8 26.9z M154.7 22.5h-4V18h4c1.5 0 2.5 0.9 2.5 2.2C157.2 21.6 156.2 22.5 154.7 22.5z'%3E%3C/path%3E%3Cpath d='M130.8 12.5c-6.8 0-11.6 4.9-11.6 11.5s4.9 11.5 11.6 11.5s11.7-4.9 11.7-11.5S137.6 12.5 130.8 12.5z M130.8 30.3c-3.4 0-5.7-2.6-5.7-6.3c0-3.8 2.3-6.3 5.7-6.3c3.4 0 5.8 2.6 5.8 6.3C136.6 27.7 134.2 30.3 130.8 30.3z'%3E%3C/path%3E%3Cpolygon points='82.1 12.8 98.3 12.8 98.3 18 87.9 18 87.9 21.3 98 21.3 98 26.4 87.9 26.4 87.9 30 98.3 30 98.3 35.2 82.1 35.2'%3E%3C/polygon%3E%3Cpath d='M24.6 9.7C24.6 4.4 20 0 14.4 0S4.2 4.4 4.2 9.7v3.1H0v22.3h0l14.4 6.7l14.4-6.7h0V12.9h-4.2V9.7z M9.4 9.7c0-2.5 2.2-4.5 5-4.5s5 2 5 4.5v3.1H9.4V9.7z M23 31.5l-8.6 4l-8.6-4V18.1H23V31.5z'%3E%3C/path%3E%3Cpath d='M79.4 20.3c0-4.5-3.1-7.4-7.7-7.4H61.2v22.3H67v-7.5h2.2l4.1 7.5H80l-4.9-8.3C77.2 26.1 79.4 24 79.4 20.3z M71 22.5h-4V18h4c1.5 0 2.5 0.9 2.5 2.2C73.5 21.6 72.5 22.5 71 22.5z'%3E%3C/path%3E%3Cpolygon points='40.5 12.8 58.6 12.8 58.6 18.1 52.4 18.1 52.4 35.2 46.6 35.2 46.6 18.1 40.5 18.1'%3E%3C/polygon%3E%3C/svg%3E");}.copyable::before, .copied::before {width: 18px;height: 16px;margin: 3px -18px;content: "";position: absolute;background-size: cover;}.copyable::before {display: none;cursor: copy;background-image: url("data:image/svg+xml,%3Csvg width='18' height='16' viewBox='0 0 18 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.5 10.4996H13.5V2.49963H5.5V5.49963' stroke='%2300854D' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M10.4998 5.49976H2.49976V13.4998H10.4998V5.49976Z' stroke='%2300854D' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");}.copyable:hover::before {display: inline-block;}.copied::before {transition: all 0.4s ease;transform: scale(1.2);background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='16' viewBox='-30 -30 330 330'%3E%3Cpath d='M 30,180 90,240 240,30' style='stroke:%2300854D;stroke-width:32;fill:none'/%3E%3C/svg%3E");}.h-data {letter-spacing: 0.12em;font-weight: normal !important;}.tx-detail {background: #f6f6f6;color: #757575;border-radius: 10px;box-shadow: 0 0 0 10px white;width: calc(100% - 20px);margin-left: 10px;margin-top: 3rem;overflow-wrap: break-word;}.tx-detail:first-child {margin-top: 1rem;}.tx-detail:last-child {margin-bottom: 2rem;}.tx-detail span.ellipsis, .tx-detail a.ellipsis {display: block;float: left;max-width: 100%;}.tx-detail>.head, .tx-detail>.footer {padding: 1.5rem;--bs-gutter-x: 0;}.tx-detail>.head {border-radius: 10px 10px 0 0;}.tx-detail .txid {font-size: 106%;letter-spacing: -0.01em;}.tx-detail>.body {padding: 0 1.5rem;--bs-gutter-x: 0;letter-spacing: -0.01em;}.tx-detail>.subhead {padding: 1.5rem 1.5rem 0.4rem 1.5rem;--bs-gutter-x: 0;letter-spacing: 0.1em;text-transform: uppercase;color: var(--bs-body-color);}.tx-detail>.subhead-2 {padding: 0.3rem 1.5rem 0 1.5rem;--bs-gutter-x: 0;font-size: .875em;color: var(--bs-body-color);}.tx-in .col-12, .tx-out .col-12, .tx-addr .col-12 {background-color: white;padding: 1.2rem 1.3rem;border-bottom: 1px solid #f6f6f6;}.amt-out {padding: 1.2rem 0 1.2rem 1rem;text-align: right;overflow-wrap: break-word;}.tx-in .col-12:last-child, .tx-out .col-12:last-child {border-bottom: none;}.tx-own {background-color: #fff9e3 !important;}.tx-amt {float: right !important;}.spent {color: #dc3545 !important;}.unspent {color: #28a745 !important;}.outpoint {color: #757575 !important;}.spent, .unspent, .outpoint {display: inline-block;text-align: right;min-width: 18px;text-decoration: none !important;}.octicon {height: 24px;width: 24px;margin-left: -12px;margin-top: 19px;position: absolute;background-size: cover;background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 4.5L16.5 12L9 19.5' stroke='%23AFAFAF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");}.txvalue {color: var(--bs-body-color);font-weight: bold;}.txerror {color: #c51f13;}.txerror a, .txerror .txvalue {color: #c51f13;}.txerror .copyable::before, .txerror .copied::before {filter: invert(86%) sepia(43%) saturate(732%) hue-rotate(367deg) brightness(84%);}.tx-amt .amt:hover, .tx-amt.amt:hover, .amt-out>.amt:hover {color: var(--bs-body-color);}.prim-amt {display: initial;}.sec-amt {display: none;}.csec-amt {display: none;}.base-amt {display: none;}.cbase-amt {display: none;}.tooltip {--bs-tooltip-opacity: 1;--bs-tooltip-max-width: 380px;--bs-tooltip-bg: #fff;--bs-tooltip-color: var(--bs-body-color);--bs-tooltip-padding-x: 1rem;--bs-tooltip-padding-y: 0.8rem;filter: drop-shadow(0px 24px 64px rgba(22, 27, 45, 0.25));}.l-tooltip {text-align: start;display: inline-block;}.l-tooltip .prim-amt, .l-tooltip .sec-amt, .l-tooltip .csec-amt, .l-tooltip .base-amt, .l-tooltip .cbase-amt {display: initial;float: right;}.l-tooltip .amt-time {padding-right: 3rem;float: left;}.amt-dec {font-size: 95%;}.unconfirmed {color: white;background-color: #c51e13;padding: 0.7rem 1.2rem;border-radius: 1.4rem;}.json {word-wrap: break-word;font-size: smaller;background: #002b31;border-radius: 8px;}#raw {padding: 1.5rem 2rem;color: #ffffff;letter-spacing: 0.02em;}#raw .string {color: #2bca87;}#raw .number, #raw .boolean {color: #efc941;}#raw .null {color: red;}@media (max-width: 768px) {body {font-size: 0.8rem;background: linear-gradient(to bottom, #f6f6f6 500px, #e5e5e5 0), #e5e5e5;}.container {padding-left: 2px;padding-right: 2px;}.accordion-body {padding: var(--bs-accordion-body-padding-y) 0;}.octicon {scale: 60% !important;margin-top: -2px;}.unconfirmed {padding: 0.1rem 0.8rem;}.btn {--bs-btn-font-size: 0.8rem;}}@media (max-width: 991px) {#header .container {min-height: 40px;}#header .dropdown-menu[data-bs-popper] {left: 0;right: initial;}.trezor-logo {top: 10px;}.octicon {scale: 80%;}.table.data-table>:not(caption)>*>* {padding: 0.8rem 0.4rem;}.tx-in .col-12, .tx-out .col-12, .tx-addr .col-12 {padding: 0.7rem 1.1rem;}.amt-out {padding: 0.7rem 0 0.7rem 1rem }}@media (min-width: 769px) {body {font-size: 0.9rem;}.btn {--bs-btn-font-size: 0.9rem;}}@media (min-width: 1200px) {.h1, h1 {font-size: 2.4rem;}body {font-size: 1rem;}.btn {--bs-btn-font-size: 1rem;}} \ No newline at end of file diff --git a/static/css/main.min.4.css b/static/css/main.min.4.css new file mode 100644 index 0000000000..54dd88a23d --- /dev/null +++ b/static/css/main.min.4.css @@ -0,0 +1 @@ +@import "TTHoves/TTHoves.css";* {margin: 0;padding: 0;outline: none;font-family: "TT Hoves", -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;}html, body {height: 100%;}body {min-height: 100%;margin: 0;background: linear-gradient(to bottom, #f6f6f6 360px, #e5e5e5 0), #e5e5e5;background-repeat: no-repeat;}a {color: #00854d;text-decoration: none;}a:hover {color: #00854d;text-decoration: underline;}select {border-radius: 0.5rem;padding-left: 0.5rem;border: 1px solid #ced4da;color: var(--bs-body-color);min-height: 45px;}#header {position: fixed;top: 0;left: 0;width: 100%;margin: 0;padding-bottom: 0;padding-top: 0;background-color: white;border-bottom: 1px solid #f6f6f6;z-index: 10;}#header a {color: var(--bs-navbar-brand-color);}#header a:hover {color: var(--bs-navbar-brand-hover-color);}#header .navbar {--bs-navbar-padding-y: 0.7rem;}#header .form-control-lg {font-size: 1rem;padding: 0.75rem 1rem;}#header .container {min-height: 50px;}#header .btn.dropdown-toggle {padding-right: 0;}#header .dropdown-menu {--bs-dropdown-min-width: 13rem;}#header .dropdown-menu[data-bs-popper] {left: initial;right: 0;}#header .dropdown-menu.show {display: flex;}.form-control:focus {outline: 0;box-shadow: none;border-color: #00854d;}.base-value {color: #757575 !important;padding-left: 0.5rem;font-weight: normal;}.badge {vertical-align: middle;text-transform: uppercase;letter-spacing: 0.15em;--bs-badge-padding-x: 0.8rem;--bs-badge-font-weight: normal;--bs-badge-border-radius: 0.6rem;}.bg-secondary {background-color: #757575 !important;}.accordion {--bs-accordion-border-radius: 10px;--bs-accordion-inner-border-radius: calc(10px - 1px);--bs-accordion-color: var(--bs-body-color);--bs-accordion-active-color: var(--bs-body-color);--bs-accordion-active-bg: white;--bs-accordion-btn-active-icon: url("data:image/svg+xml,");}.accordion-button:focus {outline: 0;box-shadow: none;}.accordion-body {letter-spacing: -0.01em;}.bb-group {border: 0.6rem solid #f6f6f6;background-color: #f6f6f6;border-radius: 0.5rem;position: relative;display: inline-flex;vertical-align: middle;}.bb-group>.btn {--bs-btn-padding-x: 0.5rem;--bs-btn-padding-y: 0.22rem;--bs-btn-border-radius: 0.3rem;--bs-btn-border-width: 0;color: #545454;}.bb-group>.btn-check:checked+.btn, .bb-group .btn.active {color: black;font-weight: bold;background-color: white;}.paging {display: flex;}.paging .bb-group>.btn {min-width: 2rem;margin-left: 0.1rem;margin-right: 0.1rem;}.paging .bb-group>.btn:hover {background-color: white;}.paging a {text-decoration: none;}.btn-paging {--bs-btn-color: #757575;--bs-btn-border-color: #e2e2e2;--bs-btn-hover-color: black;--bs-btn-hover-bg: #f6f6f6;--bs-btn-hover-border-color: #e2e2e2;--bs-btn-focus-shadow-rgb: 108, 117, 125;--bs-btn-active-color: #fff;--bs-btn-active-bg: #e2e2e2;--bs-btn-active-border-color: #e2e2e2;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-gradient: none;--bs-btn-padding-y: 0.75rem;--bs-btn-padding-x: 1.1rem;--bs-btn-border-radius: 0.5rem;--bs-btn-font-weight: bold;background-color: #f6f6f6;}span.btn-paging {cursor: initial;}span.btn-paging:hover {color: #757575;}.btn-paging.active:hover {background-color: white;}.paging-group {border: 1px solid #e2e2e2;border-radius: 0.5rem;}.paging-group>.bb-group {border: 0.53rem solid #f6f6f6;}#wrap {min-height: 100%;height: auto;padding: 112px 0 75px 0;margin: 0 auto -56px;}#footer {background-color: black;color: #757575;height: 56px;overflow: hidden;}.navbar-form {width: 60%;}.navbar-form button {margin-left: -50px;position: relative;}.search-icon {width: 16px;height: 16px;position: absolute;top: 16px;background-size: cover;background-image: url("data:image/svg+xml, %3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.24976 12.5C10.1493 12.5 12.4998 10.1495 12.4998 7.25C12.4998 4.35051 10.1493 2 7.24976 2C4.35026 2 1.99976 4.35051 1.99976 7.25C1.99976 10.1495 4.35026 12.5 7.24976 12.5Z' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3Cpath d='M10.962 10.9625L13.9996 14.0001' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' /%3E%3C/svg%3E");}.navbar-form ::placeholder {color: #e2e2e2;}.ellipsis {overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.data-table {table-layout: fixed;overflow-wrap: anywhere;margin-left: 8px;margin-top: 2rem;margin-bottom: 2rem;width: calc(100% - 16px);}.data-table thead {padding-bottom: 20px;}.table.data-table> :not(caption)>*>* {padding: 0.8rem 0.8rem;background-color: var(--bs-table-bg);border-bottom-width: 1px;box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);}.table.data-table>thead>*>* {padding-bottom: 1.5rem;}.table.data-table>*>*:last-child>* {border-bottom: none;}.data-table thead, .data-table thead tr, .data-table thead th {color: #757575;border: none;font-weight: normal;}.data-table tbody th {color: #757575;font-weight: normal;}.data-table tbody {background: white;border-radius: 8px;box-shadow: 0 0 0 8px white;}.data-table h3, .data-table h5, .data-table h6 {margin-bottom: 0;}.data-table h3, .data-table h5 {color: var(--bs-body-color);}.accordion .table.data-table>thead>*>* {padding-bottom: 0;}.info-table tbody {display: inline-table;width: 100%;}.info-table td {font-weight: bold;}.info-table tr>td:first-child {font-weight: normal;color: #757575;}.ns:before {content: " ";}.nc:before {content: ",";}.trezor-logo {width: 128px;height: 32px;position: absolute;top: 16px;background-size: cover;background-image: url("data:image/svg+xml,%3Csvg style='width: 128px%3B' version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 163.7 41.9' space='preserve'%3E%3Cpolygon points='101.1 12.8 118.2 12.8 118.2 17.3 108.9 29.9 118.2 29.9 118.2 35.2 101.1 35.2 101.1 30.7 110.4 18.1 101.1 18.1'%3E%3C/polygon%3E%3Cpath d='M158.8 26.9c2.1-0.8 4.3-2.9 4.3-6.6c0-4.5-3.1-7.4-7.7-7.4h-10.5v22.3h5.8v-7.5h2.2l4.1 7.5h6.7L158.8 26.9z M154.7 22.5h-4V18h4c1.5 0 2.5 0.9 2.5 2.2C157.2 21.6 156.2 22.5 154.7 22.5z'%3E%3C/path%3E%3Cpath d='M130.8 12.5c-6.8 0-11.6 4.9-11.6 11.5s4.9 11.5 11.6 11.5s11.7-4.9 11.7-11.5S137.6 12.5 130.8 12.5z M130.8 30.3c-3.4 0-5.7-2.6-5.7-6.3c0-3.8 2.3-6.3 5.7-6.3c3.4 0 5.8 2.6 5.8 6.3C136.6 27.7 134.2 30.3 130.8 30.3z'%3E%3C/path%3E%3Cpolygon points='82.1 12.8 98.3 12.8 98.3 18 87.9 18 87.9 21.3 98 21.3 98 26.4 87.9 26.4 87.9 30 98.3 30 98.3 35.2 82.1 35.2'%3E%3C/polygon%3E%3Cpath d='M24.6 9.7C24.6 4.4 20 0 14.4 0S4.2 4.4 4.2 9.7v3.1H0v22.3h0l14.4 6.7l14.4-6.7h0V12.9h-4.2V9.7z M9.4 9.7c0-2.5 2.2-4.5 5-4.5s5 2 5 4.5v3.1H9.4V9.7z M23 31.5l-8.6 4l-8.6-4V18.1H23V31.5z'%3E%3C/path%3E%3Cpath d='M79.4 20.3c0-4.5-3.1-7.4-7.7-7.4H61.2v22.3H67v-7.5h2.2l4.1 7.5H80l-4.9-8.3C77.2 26.1 79.4 24 79.4 20.3z M71 22.5h-4V18h4c1.5 0 2.5 0.9 2.5 2.2C73.5 21.6 72.5 22.5 71 22.5z'%3E%3C/path%3E%3Cpolygon points='40.5 12.8 58.6 12.8 58.6 18.1 52.4 18.1 52.4 35.2 46.6 35.2 46.6 18.1 40.5 18.1'%3E%3C/polygon%3E%3C/svg%3E");}.copyable::before, .copied::before {width: 18px;height: 16px;margin: 3px -18px;content: "";position: absolute;background-size: cover;}.copyable::before {display: none;cursor: copy;background-image: url("data:image/svg+xml,%3Csvg width='18' height='16' viewBox='0 0 18 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.5 10.4996H13.5V2.49963H5.5V5.49963' stroke='%2300854D' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M10.4998 5.49976H2.49976V13.4998H10.4998V5.49976Z' stroke='%2300854D' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");}.copyable:hover::before {display: inline-block;}.copied::before {transition: all 0.4s ease;transform: scale(1.2);background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='16' viewBox='-30 -30 330 330'%3E%3Cpath d='M 30,180 90,240 240,30' style='stroke:%2300854D;stroke-width:32;fill:none'/%3E%3C/svg%3E");}.h-data {letter-spacing: 0.12em;font-weight: normal !important;}.tx-detail {background: #f6f6f6;color: #757575;border-radius: 10px;box-shadow: 0 0 0 10px white;width: calc(100% - 20px);margin-left: 10px;margin-top: 3rem;overflow-wrap: break-word;}.tx-detail:first-child {margin-top: 1rem;}.tx-detail:last-child {margin-bottom: 2rem;}.tx-detail span.ellipsis, .tx-detail a.ellipsis {display: block;float: left;max-width: 100%;}.tx-detail>.head, .tx-detail>.footer {padding: 1.5rem;--bs-gutter-x: 0;}.tx-detail>.head {border-radius: 10px 10px 0 0;}.tx-detail .txid {font-size: 106%;letter-spacing: -0.01em;}.tx-detail>.body {padding: 0 1.5rem;--bs-gutter-x: 0;letter-spacing: -0.01em;}.tx-detail>.subhead {padding: 1.5rem 1.5rem 0.4rem 1.5rem;--bs-gutter-x: 0;letter-spacing: 0.1em;text-transform: uppercase;color: var(--bs-body-color);}.tx-detail>.subhead-2 {padding: 0.3rem 1.5rem 0 1.5rem;--bs-gutter-x: 0;font-size: .875em;color: var(--bs-body-color);}.tx-in .col-12, .tx-out .col-12, .tx-addr .col-12 {background-color: white;padding: 1.2rem 1.3rem;border-bottom: 1px solid #f6f6f6;}.amt-out {padding: 1.2rem 0 1.2rem 1rem;text-align: right;overflow-wrap: break-word;}.tx-in .col-12:last-child, .tx-out .col-12:last-child {border-bottom: none;}.tx-own {background-color: #fff9e3 !important;}.tx-amt {float: right !important;}.spent {color: #dc3545 !important;}.unspent {color: #28a745 !important;}.outpoint {color: #757575 !important;}.spent, .unspent, .outpoint {display: inline-block;text-align: right;min-width: 18px;text-decoration: none !important;}.octicon {height: 24px;width: 24px;margin-left: -12px;margin-top: 19px;position: absolute;background-size: cover;background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 4.5L16.5 12L9 19.5' stroke='%23AFAFAF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");}.txvalue {color: var(--bs-body-color);font-weight: bold;}.txerror {color: #c51f13;}.txerror a, .txerror .txvalue {color: #c51f13;}.txerror .copyable::before, .txerror .copied::before {filter: invert(86%) sepia(43%) saturate(732%) hue-rotate(367deg) brightness(84%);}.tx-amt .amt:hover, .tx-amt.amt:hover, .amt-out>.amt:hover {color: var(--bs-body-color);}.prim-amt {display: initial;}.sec-amt {display: none;}.csec-amt {display: none;}.base-amt {display: none;}.cbase-amt {display: none;}.tooltip {--bs-tooltip-opacity: 1;--bs-tooltip-max-width: 380px;--bs-tooltip-bg: #fff;--bs-tooltip-color: var(--bs-body-color);--bs-tooltip-padding-x: 1rem;--bs-tooltip-padding-y: 0.8rem;filter: drop-shadow(0px 24px 64px rgba(22, 27, 45, 0.25));}.l-tooltip {text-align: start;display: inline-block;}.l-tooltip .prim-amt, .l-tooltip .sec-amt, .l-tooltip .csec-amt, .l-tooltip .base-amt, .l-tooltip .cbase-amt {display: initial;float: right;}.l-tooltip .amt-time {padding-right: 3rem;float: left;}.amt-dec {font-size: 95%;}.unconfirmed {color: white;background-color: #c51e13;padding: 0.7rem 1.2rem;border-radius: 1.4rem;}.json {word-wrap: break-word;font-size: smaller;background: #002b31;border-radius: 8px;}#raw {padding: 1.5rem 2rem;color: #ffffff;letter-spacing: 0.02em;}#raw .string {color: #2bca87;}#raw .number, #raw .boolean {color: #efc941;}#raw .null {color: red;}@media (max-width: 768px) {body {font-size: 0.8rem;background: linear-gradient(to bottom, #f6f6f6 500px, #e5e5e5 0), #e5e5e5;}.container {padding-left: 2px;padding-right: 2px;}.accordion-body {padding: var(--bs-accordion-body-padding-y) 0;}.octicon {scale: 60% !important;margin-top: -2px;}.unconfirmed {padding: 0.1rem 0.8rem;}.btn {--bs-btn-font-size: 0.8rem;}}@media (max-width: 991px) {#header .container {min-height: 40px;}#header .dropdown-menu[data-bs-popper] {left: 0;right: initial;}.trezor-logo {top: 10px;}.octicon {scale: 80%;}.table.data-table>:not(caption)>*>* {padding: 0.8rem 0.4rem;}.tx-in .col-12, .tx-out .col-12, .tx-addr .col-12 {padding: 0.7rem 1.1rem;}.amt-out {padding: 0.7rem 0 0.7rem 1rem }}@media (min-width: 769px) {body {font-size: 0.9rem;}.btn {--bs-btn-font-size: 0.9rem;}}@media (min-width: 1200px) {.h1, h1 {font-size: 2.4rem;}body {font-size: 1rem;}.btn {--bs-btn-font-size: 1rem;}} \ No newline at end of file diff --git a/static/internal_templates/base.html b/static/internal_templates/base.html new file mode 100644 index 0000000000..86d6fd40ef --- /dev/null +++ b/static/internal_templates/base.html @@ -0,0 +1,28 @@ + + + + + + + + Blockbook {{.CoinLabel}} Internal Admin + + + + +
+
+ {{- template "specific" . -}} +
+
+ + + \ No newline at end of file diff --git a/static/internal_templates/block_internal_data_errors.html b/static/internal_templates/block_internal_data_errors.html new file mode 100644 index 0000000000..2301f94362 --- /dev/null +++ b/static/internal_templates/block_internal_data_errors.html @@ -0,0 +1,35 @@ +{{define "specific"}} +

Blocks with errors from fetching internal data

+
+
Count: {{len .InternalDataErrors}}
+
+ {{if .RefetchingInternalData}}Fetching...{{else}} +
+ +
+ {{end}} +
+ +
+ + + + + + + + + + + {{range $e := .InternalDataErrors}} + + + + + + + {{end}} + +
HeightHashRetriesError Message
{{formatUint32 $e.Height}}{{$e.Hash}}{{$e.Retries}}{{$e.ErrorMessage}}
+
+{{end}} \ No newline at end of file diff --git a/static/internal_templates/contract_info.html b/static/internal_templates/contract_info.html new file mode 100644 index 0000000000..57cbfece24 --- /dev/null +++ b/static/internal_templates/contract_info.html @@ -0,0 +1,39 @@ +{{define "specific"}} {{if eq .ChainType 1}} + +
+
+
+ +
+
+ +
+
+
+
+ To update contract, use POST request to /admin/contract-info/ endpoint. Example: +
+
+            curl -k -v  \
+            'https://<internaladdress>/admin/contract-info/' \
+            -H 'Content-Type: application/json' \
+            --data '[{ContractInfo},{ContractInfo},...]'        
+        
+
+
+{{else}} Not supported {{end}}{{end}} diff --git a/static/internal_templates/error.html b/static/internal_templates/error.html new file mode 100644 index 0000000000..0b75378bcf --- /dev/null +++ b/static/internal_templates/error.html @@ -0,0 +1,4 @@ +{{define "specific"}} +

Error

+

{{.Error.Text}}

+{{end}} \ No newline at end of file diff --git a/static/internal_templates/index.html b/static/internal_templates/index.html new file mode 100644 index 0000000000..7a94bce8f0 --- /dev/null +++ b/static/internal_templates/index.html @@ -0,0 +1,14 @@ +{{define "specific"}} + +{{if eq .ChainType 1}} + + +{{end}}{{end}} diff --git a/static/internal_templates/ws_limit_exceeding_ips.html b/static/internal_templates/ws_limit_exceeding_ips.html new file mode 100644 index 0000000000..081431fb1b --- /dev/null +++ b/static/internal_templates/ws_limit_exceeding_ips.html @@ -0,0 +1,29 @@ +{{define "specific"}} +

IP addresses disconnected for exceeding websocket limit

+
+
Distinct ip addresses that exceeded limit of {{.WsGetAccountInfoLimit}} requests since last reset: {{len .WsLimitExceedingIPs}}
+
+
+ +
+
+ +
+ + + + + + + + + {{range $d := .WsLimitExceedingIPs}} + + + + + {{end}} + +
IPCount
{{$d.IP}}{{$d.Count}}
+
+{{end}} \ No newline at end of file diff --git a/static/js/bootstrap.bundle.5.2.2.min.js b/static/js/bootstrap.bundle.5.2.2.min.js new file mode 100644 index 0000000000..1d138863be --- /dev/null +++ b/static/js/bootstrap.bundle.5.2.2.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.2.2 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},m=t=>{"function"==typeof t&&t()},_=(e,i,n=!0)=>{if(!n)return void m(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),m(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=N(t);return C.has(o)||(o=t),[n,s,o]}function D(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return j(s,{delegateTarget:r}),n.oneOff&&P.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return j(n,{delegateTarget:t}),i.oneOff&&P.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function S(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function I(t,e,i,n){const s=e[i]||{};for(const o of Object.keys(s))if(o.includes(n)){const n=s[o];S(t,e,i,n.callable,n.delegationSelector)}}function N(t){return t=t.replace(y,""),T[t]||t}const P={on(t,e,i,n){D(t,e,i,n,!1)},one(t,e,i,n){D(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))I(t,l,i,e.slice(1));for(const i of Object.keys(c)){const n=i.replace(w,"");if(!a||e.includes(n)){const e=c[i];S(t,l,r,e.callable,e.delegationSelector)}}}else{if(!Object.keys(c).length)return;S(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==N(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());let l=new Event(e,{bubbles:o,cancelable:!0});return l=j(l,i),a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function j(t,e){for(const[i,n]of Object.entries(e||{}))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}const M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};function $(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function W(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const B={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${W(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${W(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=$(t.dataset[n])}return e},getDataAttribute:(t,e)=>$(t.getAttribute(`data-bs-${W(e)}`))};class F{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?B.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?B.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const n of Object.keys(e)){const s=e[n],r=t[n],a=o(r)?"element":null==(i=r)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(a))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}var i}}class z extends F{constructor(t,e){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(e),H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),P.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.2.2"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const q=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;P.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class R extends z{static get NAME(){return"alert"}close(){if(P.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),P.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=R.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}q(R,"close"),g(R);const V='[data-bs-toggle="button"]';class K extends z{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=K.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}P.on(document,"click.bs.button.data-api",V,(t=>{t.preventDefault();const e=t.target.closest(V);K.getOrCreateInstance(e).toggle()})),g(K);const Q={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))}},X={endCallback:null,leftCallback:null,rightCallback:null},Y={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class U extends F{constructor(t,e){super(),this._element=t,t&&U.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return X}static get DefaultType(){return Y}static get NAME(){return"swipe"}dispose(){P.off(this._element,".bs.swipe")}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),m(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&m(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(P.on(this._element,"pointerdown.bs.swipe",(t=>this._start(t))),P.on(this._element,"pointerup.bs.swipe",(t=>this._end(t))),this._element.classList.add("pointer-event")):(P.on(this._element,"touchstart.bs.swipe",(t=>this._start(t))),P.on(this._element,"touchmove.bs.swipe",(t=>this._move(t))),P.on(this._element,"touchend.bs.swipe",(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const G="next",J="prev",Z="left",tt="right",et="slid.bs.carousel",it="carousel",nt="active",st={ArrowLeft:tt,ArrowRight:Z},ot={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},rt={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class at extends z{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=Q.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===it&&this.cycle()}static get Default(){return ot}static get DefaultType(){return rt}static get NAME(){return"carousel"}next(){this._slide(G)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(J)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?P.one(this._element,et,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void P.one(this._element,et,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?G:J;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&P.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(P.on(this._element,"mouseenter.bs.carousel",(()=>this.pause())),P.on(this._element,"mouseleave.bs.carousel",(()=>this._maybeEnableCycle()))),this._config.touch&&U.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of Q.find(".carousel-item img",this._element))P.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(Z)),rightCallback:()=>this._slide(this._directionToOrder(tt)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new U(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=st[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=Q.findOne(".active",this._indicatorsElement);e.classList.remove(nt),e.removeAttribute("aria-current");const i=Q.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(nt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===G,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>P.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r("slide.bs.carousel").defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(nt),i.classList.remove(nt,c,l),this._isSliding=!1,r(et)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return Q.findOne(".active.carousel-item",this._element)}_getItems(){return Q.find(".carousel-item",this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===Z?J:G:t===Z?G:J}_orderToDirection(t){return p()?t===J?Z:tt:t===J?tt:Z}static jQueryInterface(t){return this.each((function(){const e=at.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}P.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",(function(t){const e=n(this);if(!e||!e.classList.contains(it))return;t.preventDefault();const i=at.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===B.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),P.on(window,"load.bs.carousel.data-api",(()=>{const t=Q.find('[data-bs-ride="carousel"]');for(const e of t)at.getOrCreateInstance(e)})),g(at);const lt="show",ct="collapse",ht="collapsing",dt='[data-bs-toggle="collapse"]',ut={parent:null,toggle:!0},ft={parent:"(null|element)",toggle:"boolean"};class pt extends z{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const n=Q.find(dt);for(const t of n){const e=i(t),n=Q.find(e).filter((t=>t===this._element));null!==e&&n.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return ut}static get DefaultType(){return ft}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>pt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(P.trigger(this._element,"show.bs.collapse").defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[e]="",P.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(P.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);for(const t of this._triggerArray){const e=n(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),P.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(dt);for(const e of t){const t=n(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=Q.find(":scope .collapse .collapse",this._config.parent);return Q.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}P.on(document,"click.bs.collapse.data-api",dt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this),n=Q.find(e);for(const t of n)pt.getOrCreateInstance(t,{toggle:!1}).toggle()})),g(pt);var gt="top",mt="bottom",_t="right",bt="left",vt="auto",yt=[gt,mt,_t,bt],wt="start",At="end",Et="clippingParents",Tt="viewport",Ct="popper",Ot="reference",xt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+At])}),[]),kt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+At])}),[]),Lt="beforeRead",Dt="read",St="afterRead",It="beforeMain",Nt="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",$t=[Lt,Dt,St,It,Nt,Pt,jt,Mt,Ht];function Wt(t){return t?(t.nodeName||"").toLowerCase():null}function Bt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function Ft(t){return t instanceof Bt(t).Element||t instanceof Element}function zt(t){return t instanceof Bt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Bt(t).ShadowRoot||t instanceof ShadowRoot)}const Rt={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Wt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Wt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Vt(t){return t.split("-")[0]}var Kt=Math.max,Qt=Math.min,Xt=Math.round;function Yt(){var t=navigator.userAgentData;return null!=t&&t.brands?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ut(){return!/^((?!chrome|android).)*safari/i.test(Yt())}function Gt(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&zt(t)&&(s=t.offsetWidth>0&&Xt(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&Xt(n.height)/t.offsetHeight||1);var r=(Ft(t)?Bt(t):window).visualViewport,a=!Ut()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Jt(t){var e=Gt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Zt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function te(t){return Bt(t).getComputedStyle(t)}function ee(t){return["table","td","th"].indexOf(Wt(t))>=0}function ie(t){return((Ft(t)?t.ownerDocument:t.document)||window.document).documentElement}function ne(t){return"html"===Wt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||ie(t)}function se(t){return zt(t)&&"fixed"!==te(t).position?t.offsetParent:null}function oe(t){for(var e=Bt(t),i=se(t);i&&ee(i)&&"static"===te(i).position;)i=se(i);return i&&("html"===Wt(i)||"body"===Wt(i)&&"static"===te(i).position)?e:i||function(t){var e=/firefox/i.test(Yt());if(/Trident/i.test(Yt())&&zt(t)&&"fixed"===te(t).position)return null;var i=ne(t);for(qt(i)&&(i=i.host);zt(i)&&["html","body"].indexOf(Wt(i))<0;){var n=te(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function re(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function ae(t,e,i){return Kt(t,Qt(e,i))}function le(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ce(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const he={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Vt(i.placement),l=re(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return le("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ce(t,yt))}(s.padding,i),d=Jt(o),u="y"===l?gt:bt,f="y"===l?mt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],g=r[l]-i.rects.reference[l],m=oe(o),_=m?"y"===l?m.clientHeight||0:m.clientWidth||0:0,b=p/2-g/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=ae(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Zt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function de(t){return t.split("-")[1]}var ue={top:"auto",right:"auto",bottom:"auto",left:"auto"};function fe(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,g=void 0===p?0:p,m="function"==typeof h?h({x:f,y:g}):{x:f,y:g};f=m.x,g=m.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=bt,y=gt,w=window;if(c){var A=oe(i),E="clientHeight",T="clientWidth";A===Bt(i)&&"static"!==te(A=ie(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===gt||(s===bt||s===_t)&&o===At)&&(y=mt,g-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,g*=l?1:-1),s!==bt&&(s!==gt&&s!==mt||o!==At)||(v=_t,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&ue),x=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:Xt(e*n)/n||0,y:Xt(i*n)/n||0}}({x:f,y:g}):{x:f,y:g};return f=x.x,g=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+g+"px)":"translate3d("+f+"px, "+g+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?g+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const pe={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Vt(e.placement),variation:de(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,fe(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,fe(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ge={passive:!0};const me={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Bt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ge)})),a&&l.addEventListener("resize",i.update,ge),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ge)})),a&&l.removeEventListener("resize",i.update,ge)}},data:{}};var _e={left:"right",right:"left",bottom:"top",top:"bottom"};function be(t){return t.replace(/left|right|bottom|top/g,(function(t){return _e[t]}))}var ve={start:"end",end:"start"};function ye(t){return t.replace(/start|end/g,(function(t){return ve[t]}))}function we(t){var e=Bt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ae(t){return Gt(ie(t)).left+we(t).scrollLeft}function Ee(t){var e=te(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Te(t){return["html","body","#document"].indexOf(Wt(t))>=0?t.ownerDocument.body:zt(t)&&Ee(t)?t:Te(ne(t))}function Ce(t,e){var i;void 0===e&&(e=[]);var n=Te(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Bt(n),r=s?[o].concat(o.visualViewport||[],Ee(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ce(ne(r)))}function Oe(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function xe(t,e,i){return e===Tt?Oe(function(t,e){var i=Bt(t),n=ie(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ut();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ae(t),y:l}}(t,i)):Ft(e)?function(t,e){var i=Gt(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Oe(function(t){var e,i=ie(t),n=we(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=Kt(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=Kt(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ae(t),l=-n.scrollTop;return"rtl"===te(s||i).direction&&(a+=Kt(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(ie(t)))}function ke(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Vt(s):null,r=s?de(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case gt:e={x:a,y:i.y-n.height};break;case mt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?re(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case At:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Et:a,c=i.rootBoundary,h=void 0===c?Tt:c,d=i.elementContext,u=void 0===d?Ct:d,f=i.altBoundary,p=void 0!==f&&f,g=i.padding,m=void 0===g?0:g,_=le("number"!=typeof m?m:ce(m,yt)),b=u===Ct?Ot:Ct,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Ce(ne(t)),i=["absolute","fixed"].indexOf(te(t).position)>=0&&zt(t)?oe(t):t;return Ft(i)?e.filter((function(t){return Ft(t)&&Zt(t,i)&&"body"!==Wt(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=xe(t,i,n);return e.top=Kt(s.top,e.top),e.right=Qt(s.right,e.right),e.bottom=Qt(s.bottom,e.bottom),e.left=Kt(s.left,e.left),e}),xe(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(Ft(y)?y:y.contextElement||ie(t.elements.popper),l,h,r),A=Gt(t.elements.reference),E=ke({reference:A,element:v,strategy:"absolute",placement:s}),T=Oe(Object.assign({},v,E)),C=u===Ct?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Ct&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[_t,mt].indexOf(t)>=0?1:-1,i=[gt,mt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function De(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?kt:l,h=de(n),d=h?a?xt:xt.filter((function(t){return de(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=Le(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Vt(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const Se={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,g=i.allowedAutoPlacements,m=e.options.placement,_=Vt(m),b=l||(_!==m&&p?function(t){if(Vt(t)===vt)return[];var e=be(t);return[ye(t),e,ye(e)]}(m):[be(m)]),v=[m].concat(b).reduce((function(t,i){return t.concat(Vt(i)===vt?De(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:g}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,D=L?"width":"height",S=Le(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),I=L?k?_t:bt:k?mt:gt;y[D]>w[D]&&(I=be(I));var N=be(I),P=[];if(o&&P.push(S[x]<=0),a&&P.push(S[I]<=0,S[N]<=0),P.every((function(t){return t}))){T=O,E=!1;break}A.set(O,P)}if(E)for(var j=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function Ie(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Ne(t){return[gt,_t,mt,bt].some((function(e){return t[e]>=0}))}const Pe={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=Le(e,{elementContext:"reference"}),a=Le(e,{altBoundary:!0}),l=Ie(r,n),c=Ie(a,s,o),h=Ne(l),d=Ne(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},je={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=kt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Vt(t),s=[bt,gt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Me={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ke({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},He={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,g=void 0===p?0:p,m=Le(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Vt(e.placement),b=de(e.placement),v=!b,y=re(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof g?g(Object.assign({},e.rects,{placement:e.placement})):g,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,D="y"===y?gt:bt,S="y"===y?mt:_t,I="y"===y?"height":"width",N=A[y],P=N+m[D],j=N-m[S],M=f?-T[I]/2:0,H=b===wt?E[I]:T[I],$=b===wt?-T[I]:-E[I],W=e.elements.arrow,B=f&&W?Jt(W):{width:0,height:0},F=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=F[D],q=F[S],R=ae(0,E[I],B[I]),V=v?E[I]/2-M-R-z-O.mainAxis:H-R-z-O.mainAxis,K=v?-E[I]/2+M+R+q+O.mainAxis:$+R+q+O.mainAxis,Q=e.elements.arrow&&oe(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=N+K-Y,G=ae(f?Qt(P,N+V-Y-X):P,N,f?Kt(j,U):j);A[y]=G,k[y]=G-N}if(a){var J,Z="x"===y?gt:bt,tt="x"===y?mt:_t,et=A[w],it="y"===w?"height":"width",nt=et+m[Z],st=et-m[tt],ot=-1!==[gt,bt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=ae(t,e,i);return n>i?i:n}(at,et,lt):ae(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function $e(t,e,i){void 0===i&&(i=!1);var n,s,o=zt(e),r=zt(e)&&function(t){var e=t.getBoundingClientRect(),i=Xt(e.width)/t.offsetWidth||1,n=Xt(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=ie(e),l=Gt(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==Wt(e)||Ee(a))&&(c=(n=e)!==Bt(n)&&zt(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:we(n)),zt(e)?((h=Gt(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ae(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function We(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Fe(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(B.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=Q.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=Q.find(ti);for(const i of e){const e=hi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Xe,Ye].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ze)?this:Q.prev(this,Ze)[0]||Q.next(this,Ze)[0]||Q.findOne(Ze,t.delegateTarget.parentNode),o=hi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}P.on(document,Ge,Ze,hi.dataApiKeydownHandler),P.on(document,Ge,ei,hi.dataApiKeydownHandler),P.on(document,Ue,hi.clearMenus),P.on(document,"keyup.bs.dropdown.data-api",hi.clearMenus),P.on(document,Ue,Ze,(function(t){t.preventDefault(),hi.getOrCreateInstance(this).toggle()})),g(hi);const di=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",ui=".sticky-top",fi="padding-right",pi="margin-right";class gi{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,fi,(e=>e+t)),this._setElementAttributes(di,fi,(e=>e+t)),this._setElementAttributes(ui,pi,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,fi),this._resetElementAttributes(di,fi),this._resetElementAttributes(ui,pi)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&B.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=B.getDataAttribute(t,e);null!==i?(B.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of Q.find(t,this._element))e(i)}}const mi="show",_i="mousedown.bs.backdrop",bi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},vi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class yi extends F{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return bi}static get DefaultType(){return vi}static get NAME(){return"backdrop"}show(t){if(!this._config.isVisible)return void m(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(mi),this._emulateAnimation((()=>{m(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(mi),this._emulateAnimation((()=>{this.dispose(),m(t)}))):m(t)}dispose(){this._isAppended&&(P.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),P.on(t,_i,(()=>{m(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const wi=".bs.focustrap",Ai="backward",Ei={autofocus:!0,trapElement:null},Ti={autofocus:"boolean",trapElement:"element"};class Ci extends F{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Ei}static get DefaultType(){return Ti}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),P.off(document,wi),P.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),P.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,P.off(document,wi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=Q.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===Ai?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ai:"forward")}}const Oi="hidden.bs.modal",xi="show.bs.modal",ki="modal-open",Li="show",Di="modal-static",Si={backdrop:!0,focus:!0,keyboard:!0},Ii={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ni extends z{constructor(t,e){super(t,e),this._dialog=Q.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new gi,this._addEventListeners()}static get Default(){return Si}static get DefaultType(){return Ii}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||P.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(ki),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(P.trigger(this._element,"hide.bs.modal").defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Li),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){for(const t of[window,this._dialog])P.off(t,".bs.modal");this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new yi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ci({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=Q.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(Li),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,P.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){P.on(this._element,"keydown.dismiss.bs.modal",(t=>{if("Escape"===t.key)return this._config.keyboard?(t.preventDefault(),void this.hide()):void this._triggerBackdropTransition()})),P.on(window,"resize.bs.modal",(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),P.on(this._element,"mousedown.dismiss.bs.modal",(t=>{P.one(this._element,"click.dismiss.bs.modal",(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(ki),this._resetAdjustments(),this._scrollBar.reset(),P.trigger(this._element,Oi)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(P.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Di)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Di),this._queueCallback((()=>{this._element.classList.remove(Di),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Ni.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}P.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),P.one(e,xi,(t=>{t.defaultPrevented||P.one(e,Oi,(()=>{a(this)&&this.focus()}))}));const i=Q.findOne(".modal.show");i&&Ni.getInstance(i).hide(),Ni.getOrCreateInstance(e).toggle(this)})),q(Ni),g(Ni);const Pi="show",ji="showing",Mi="hiding",Hi=".offcanvas.show",$i="hidePrevented.bs.offcanvas",Wi="hidden.bs.offcanvas",Bi={backdrop:!0,keyboard:!0,scroll:!1},Fi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class zi extends z{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Bi}static get DefaultType(){return Fi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||P.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new gi).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(ji),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Pi),this._element.classList.remove(ji),P.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(P.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Mi),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Pi,Mi),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new gi).reset(),P.trigger(this._element,Wi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new yi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():P.trigger(this._element,$i)}:null})}_initializeFocusTrap(){return new Ci({trapElement:this._element})}_addEventListeners(){P.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():P.trigger(this._element,$i))}))}static jQueryInterface(t){return this.each((function(){const e=zi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}P.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;P.one(e,Wi,(()=>{a(this)&&this.focus()}));const i=Q.findOne(Hi);i&&i!==e&&zi.getInstance(i).hide(),zi.getOrCreateInstance(e).toggle(this)})),P.on(window,"load.bs.offcanvas.data-api",(()=>{for(const t of Q.find(Hi))zi.getOrCreateInstance(t).show()})),P.on(window,"resize.bs.offcanvas",(()=>{for(const t of Q.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&zi.getOrCreateInstance(t).hide()})),q(zi),g(zi);const qi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Ri=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Vi=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Ki=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!qi.has(i)||Boolean(Ri.test(t.nodeValue)||Vi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Qi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Xi={allowList:Qi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Yi={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Ui={entry:"(string|element|function|null)",selector:"(string|element)"};class Gi extends F{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Ui)}_setContent(t,e,i){const n=Q.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Ki(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return"function"==typeof t?t(this):t}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Ji=new Set(["sanitize","allowList","sanitizeFn"]),Zi="fade",tn="show",en=".modal",nn="hide.bs.modal",sn="hover",on="focus",rn={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},an={allowList:Qi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,0],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ln={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cn extends z{constructor(t,e){if(void 0===Ke)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return an}static get DefaultType(){return ln}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),P.off(this._element.closest(en),nn,this._hideModalHandler),this.tip&&this.tip.remove(),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=P.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this.tip&&(this.tip.remove(),this.tip=null);const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),P.trigger(this._element,this.constructor.eventName("inserted"))),this._popper?this._popper.update():this._popper=this._createPopper(i),i.classList.add(tn),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.on(t,"mouseover",h);this._queueCallback((()=>{P.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(!this._isShown())return;if(P.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented)return;const t=this._getTipElement();if(t.classList.remove(tn),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||t.remove(),this._element.removeAttribute("aria-describedby"),P.trigger(this._element,this.constructor.eventName("hidden")),this._disposePopper())}),this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(Zi,tn),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(Zi),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Gi({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Zi)}_isShown(){return this.tip&&this.tip.classList.contains(tn)}_createPopper(t){const e="function"==typeof this._config.placement?this._config.placement.call(this,t,this._element):this._config.placement,i=rn[e.toUpperCase()];return Ve(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)P.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===sn?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===sn?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");P.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?on:sn]=!0,e._enter()})),P.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?on:sn]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},P.on(this._element.closest(en),nn,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=B.getDataAttributes(this._element);for(const t of Object.keys(e))Ji.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=cn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(cn);const hn={...cn.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},dn={...cn.DefaultType,content:"(null|string|element|function)"};class un extends cn{static get Default(){return hn}static get DefaultType(){return dn}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn="click.bs.scrollspy",pn="active",gn="[href]",mn={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},_n={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class bn extends z{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return mn}static get DefaultType(){return _n}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(P.off(this._config.target,fn),P.on(this._config.target,fn,gn,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=Q.find(gn,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=Q.findOne(e.hash,this._element);a(t)&&(this._targetLinks.set(e.hash,e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(pn),this._activateParents(t),P.trigger(this._element,"activate.bs.scrollspy",{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))Q.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(pn);else for(const e of Q.parents(t,".nav, .list-group"))for(const t of Q.prev(e,".nav-link, .nav-item > .nav-link, .list-group-item"))t.classList.add(pn)}_clearActiveClass(t){t.classList.remove(pn);const e=Q.find("[href].active",t);for(const t of e)t.classList.remove(pn)}static jQueryInterface(t){return this.each((function(){const e=bn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(window,"load.bs.scrollspy.data-api",(()=>{for(const t of Q.find('[data-bs-spy="scroll"]'))bn.getOrCreateInstance(t)})),g(bn);const vn="ArrowLeft",yn="ArrowRight",wn="ArrowUp",An="ArrowDown",En="active",Tn="fade",Cn="show",On='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',xn=`.nav-link:not(.dropdown-toggle), .list-group-item:not(.dropdown-toggle), [role="tab"]:not(.dropdown-toggle), ${On}`;class kn extends z{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),P.on(this._element,"keydown.bs.tab",(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?P.trigger(e,"hide.bs.tab",{relatedTarget:t}):null;P.trigger(t,"show.bs.tab",{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(En),this._activate(n(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),P.trigger(t,"shown.bs.tab",{relatedTarget:e})):t.classList.add(Cn)}),t,t.classList.contains(Tn)))}_deactivate(t,e){t&&(t.classList.remove(En),t.blur(),this._deactivate(n(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),P.trigger(t,"hidden.bs.tab",{relatedTarget:e})):t.classList.remove(Cn)}),t,t.classList.contains(Tn)))}_keydown(t){if(![vn,yn,wn,An].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=[yn,An].includes(t.key),i=b(this._getChildren().filter((t=>!l(t))),t.target,e,!0);i&&(i.focus({preventScroll:!0}),kn.getOrCreateInstance(i).show())}_getChildren(){return Q.find(xn,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=n(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`#${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=Q.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",En),n(".dropdown-menu",Cn),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(En)}_getInnerElement(t){return t.matches(xn)?t:Q.findOne(xn,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=kn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(document,"click.bs.tab",On,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||kn.getOrCreateInstance(this).show()})),P.on(window,"load.bs.tab",(()=>{for(const t of Q.find('.active[data-bs-toggle="tab"], .active[data-bs-toggle="pill"], .active[data-bs-toggle="list"]'))kn.getOrCreateInstance(t)})),g(kn);const Ln="hide",Dn="show",Sn="showing",In={animation:"boolean",autohide:"boolean",delay:"number"},Nn={animation:!0,autohide:!0,delay:5e3};class Pn extends z{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Nn}static get DefaultType(){return In}static get NAME(){return"toast"}show(){P.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Ln),d(this._element),this._element.classList.add(Dn,Sn),this._queueCallback((()=>{this._element.classList.remove(Sn),P.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(P.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(Sn),this._queueCallback((()=>{this._element.classList.add(Ln),this._element.classList.remove(Sn,Dn),P.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Dn),super.dispose()}isShown(){return this._element.classList.contains(Dn)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){P.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),P.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),P.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),P.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Pn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return q(Pn),g(Pn),{Alert:R,Button:K,Carousel:at,Collapse:pt,Dropdown:hi,Modal:Ni,Offcanvas:zi,Popover:un,ScrollSpy:bn,Tab:kn,Toast:Pn,Tooltip:cn}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 3efffd4704..b1d31408f8 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -28,10 +28,10 @@ function syntaxHighlight(json) { } function getCoinCookie() { - return document.cookie - .split("; ") - .find((row) => row.startsWith("secondary_coin=")) - ?.split("="); + if(hasSecondary) return document.cookie + .split("; ") + .find((row) => row.startsWith("secondary_coin=")) + ?.split("="); } function changeCSSStyle(selector, cssProp, cssVal) { @@ -85,6 +85,79 @@ function addressAliasTooltip() { return `${type}
${address}
`; } +function handleTxPage(rawData, txId) { + const rawOutput = document.getElementById('raw'); + const rawButton = document.getElementById('raw-button'); + const rawHexButton = document.getElementById('raw-hex-button'); + + rawOutput.innerHTML = syntaxHighlight(rawData); + + let isShowingHexData = false; + + const memoizedResponses = {}; + + async function getTransactionHex(txId) { + // BTC-like coins have a 'hex' field in the raw data + if (rawData['hex']) { + return rawData['hex']; + } + if (memoizedResponses[txId]) { + return memoizedResponses[txId]; + } + const fetchedData = await fetchTransactionHex(txId); + memoizedResponses[txId] = fetchedData; + return fetchedData; + } + + async function fetchTransactionHex(txId) { + const response = await fetch(`/api/rawtx/${txId}`); + if (!response.ok) { + throw new Error(`Error fetching data: ${response.status}`); + } + const txHex = await response.text(); + const hexWithoutQuotes = txHex.replace(/"/g, ''); + return hexWithoutQuotes; + } + + function updateButtonStyles() { + if (isShowingHexData) { + rawButton.classList.add('active'); + rawButton.style.fontWeight = 'normal'; + rawHexButton.classList.remove('active'); + rawHexButton.style.fontWeight = 'bold'; + } else { + rawButton.classList.remove('active'); + rawButton.style.fontWeight = 'bold'; + rawHexButton.classList.add('active'); + rawHexButton.style.fontWeight = 'normal'; + } + } + + updateButtonStyles(); + + rawHexButton.addEventListener('click', async () => { + if (!isShowingHexData) { + try { + const txHex = await getTransactionHex(txId); + rawOutput.textContent = txHex; + } catch (error) { + console.error('Error fetching raw transaction hex:', error); + rawOutput.textContent = `Error fetching raw transaction hex: ${error.message}`; + } + isShowingHexData = true; + updateButtonStyles(); + } + }); + + rawButton.addEventListener('click', () => { + if (isShowingHexData) { + rawOutput.innerHTML = syntaxHighlight(rawData); + isShowingHexData = false; + updateButtonStyles(); + } + }); +} + window.addEventListener("DOMContentLoaded", () => { const a = getCoinCookie(); if (a?.length === 3) { @@ -127,7 +200,8 @@ window.addEventListener("DOMContentLoaded", () => { if (e.clientX < e.target.getBoundingClientRect().x) { let t = e.target.getAttribute("cc"); if (!t) t = e.target.innerText; - navigator.clipboard.writeText(t); + const textToCopy = t.trim(); + navigator.clipboard.writeText(textToCopy); e.target.className = e.target.className.replace("copyable", "copied"); setTimeout( () => diff --git a/static/js/main.min.2.js b/static/js/main.min.2.js deleted file mode 100644 index 271d4598ff..0000000000 --- a/static/js/main.min.2.js +++ /dev/null @@ -1 +0,0 @@ -function syntaxHighlight(t){return(t=(t=JSON.stringify(t,void 0,2)).replace(/&/g,"&").replace(//g,">")).length>1e6?`${t}`:t.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,t=>{let e="number";return/^"/.test(t)?e=/:$/.test(t)?"key":"string":/true|false/.test(t)?e="boolean":/null/.test(t)&&(e="null"),`${t}`})}function getCoinCookie(){return document.cookie.split("; ").find(t=>t.startsWith("secondary_coin="))?.split("=")}function changeCSSStyle(t,e,l){let a=document.all?"rules":"cssRules";for(i=0,len=document.styleSheets[1][a].length;i`;if(a){let n=a.getAttribute("tm");n||(n="now"),r+=`${n}${a.outerHTML}
`}if(s&&(r+=`now${s.outerHTML}
`),e){let o=e.getAttribute("tm");o||(o="now"),r+=`${o}${e.outerHTML}
`}return l&&(r+=`now${l.outerHTML}
`),`${r}`}function addressAliasTooltip(){let t=this.getAttribute("alias-type"),e=this.getAttribute("cc");return`${t}
${e}
`}window.addEventListener("DOMContentLoaded",()=>{let t=getCoinCookie();t?.length===3&&("true"===t[2]&&(changeCSSStyle(".prim-amt","display","none"),changeCSSStyle(".sec-amt","display","initial")),document.querySelectorAll(".amt").forEach(t=>new bootstrap.Tooltip(t,{title:amountTooltip,html:!0}))),document.querySelectorAll("[alias-type]").forEach(t=>new bootstrap.Tooltip(t,{title:addressAliasTooltip,html:!0})),document.querySelectorAll("[tt]").forEach(t=>new bootstrap.Tooltip(t,{title:t.getAttribute("tt")})),document.querySelectorAll("#header .bb-group>.btn-check").forEach(t=>t.addEventListener("click",t=>{let e=getCoinCookie(),l="secondary-coin"===t.target.id;e?.length===3&&"true"===e[2]!==l&&(document.cookie=`${e[0]}=${e[1]}=${l}; Path=/`,changeCSSStyle(".prim-amt","display",l?"none":"initial"),changeCSSStyle(".sec-amt","display",l?"initial":"none"))})),document.querySelectorAll(".copyable").forEach(t=>t.addEventListener("click",t=>{if(t.clientXt.target.className=t.target.className.replace("copied","copyable"),1e3),t.preventDefault()}}))}); \ No newline at end of file diff --git a/static/js/main.min.4.js b/static/js/main.min.4.js new file mode 100644 index 0000000000..5e237185ab --- /dev/null +++ b/static/js/main.min.4.js @@ -0,0 +1 @@ +function syntaxHighlight(t){return(t=(t=JSON.stringify(t,void 0,2)).replace(/&/g,"&").replace(//g,">")).length>1e6?`${t}`:t.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,(t=>{let e="number";return/^"/.test(t)?e=/:$/.test(t)?"key":"string":/true|false/.test(t)?e="boolean":/null/.test(t)&&(e="null"),`${t}`}))}function getCoinCookie(){if(hasSecondary)return document.cookie.split("; ").find((t=>t.startsWith("secondary_coin=")))?.split("=")}function changeCSSStyle(t,e,n){const a=document.all?"rules":"cssRules";for(i=0,len=document.styleSheets[1][a].length;i`;if(a){let t=a.getAttribute("tm");t||(t="now"),i+=`${t}${a.outerHTML}
`}if(o&&(i+=`now${o.outerHTML}
`),e){let t=e.getAttribute("tm");t||(t="now"),i+=`${t}${e.outerHTML}
`}return n&&(i+=`now${n.outerHTML}
`),`${i}`}function addressAliasTooltip(){return`${this.getAttribute("alias-type")}
${this.getAttribute("cc")}
`}function handleTxPage(t,e){const n=document.getElementById("raw"),a=document.getElementById("raw-button"),o=document.getElementById("raw-hex-button");n.innerHTML=syntaxHighlight(t);let i=!1;const r={};async function s(e){if(t.hex)return t.hex;if(r[e])return r[e];const n=await async function(t){const e=await fetch(`/api/rawtx/${t}`);if(!e.ok)throw new Error(`Error fetching data: ${e.status}`);const n=await e.text();return n.replace(/"/g,"")}(e);return r[e]=n,n}function l(){i?(a.classList.add("active"),a.style.fontWeight="normal",o.classList.remove("active"),o.style.fontWeight="bold"):(a.classList.remove("active"),a.style.fontWeight="bold",o.classList.add("active"),o.style.fontWeight="normal")}l(),o.addEventListener("click",(async()=>{if(!i){try{const t=await s(e);n.textContent=t}catch(t){console.error("Error fetching raw transaction hex:",t),n.textContent=`Error fetching raw transaction hex: ${t.message}`}i=!0,l()}})),a.addEventListener("click",(()=>{i&&(n.innerHTML=syntaxHighlight(t),i=!1,l())}))}window.addEventListener("DOMContentLoaded",(()=>{const t=getCoinCookie();3===t?.length&&("true"===t[2]&&(changeCSSStyle(".prim-amt","display","none"),changeCSSStyle(".sec-amt","display","initial")),document.querySelectorAll(".amt").forEach((t=>new bootstrap.Tooltip(t,{title:amountTooltip,html:!0})))),document.querySelectorAll("[alias-type]").forEach((t=>new bootstrap.Tooltip(t,{title:addressAliasTooltip,html:!0}))),document.querySelectorAll("[tt]").forEach((t=>new bootstrap.Tooltip(t,{title:t.getAttribute("tt")}))),document.querySelectorAll("#header .bb-group>.btn-check").forEach((t=>t.addEventListener("click",(t=>{const e=getCoinCookie(),n="secondary-coin"===t.target.id;3===e?.length&&"true"===e[2]!==n&&(document.cookie=`${e[0]}=${e[1]}=${n}; Path=/`,changeCSSStyle(".prim-amt","display",n?"none":"initial"),changeCSSStyle(".sec-amt","display",n?"initial":"none"))})))),document.querySelectorAll(".copyable").forEach((t=>t.addEventListener("click",(t=>{if(t.clientXt.target.className=t.target.className.replace("copied","copyable")),1e3),t.preventDefault()}}))))})); \ No newline at end of file diff --git a/static/templates/address.html b/static/templates/address.html index 2ae5bb301e..d2bc9772e6 100644 --- a/static/templates/address.html +++ b/static/templates/address.html @@ -51,10 +51,10 @@

{{$addr.Nonce}} {{if $addr.ContractInfo}} - {{if $addr.ContractInfo.Type}} + {{if $addr.ContractInfo.Standard}} - Contract type - {{$addr.ContractInfo.Type}} + Standard + {{$addr.ContractInfo.Standard}} {{end}} {{if $addr.ContractInfo.CreatedInBlock}} @@ -109,13 +109,13 @@

{{end}} {{if eq .ChainType 1}} -{{if tokenCount $addr.Tokens "ERC20"}} +{{if tokenCount $addr.Tokens .FungibleTokenName}}
@@ -131,7 +131,7 @@
{{summaryValuesSpa Transfers# {{range $t := $addr.Tokens}} - {{if eq $t.Type "ERC20"}} + {{if eq $t.Standard $.FungibleTokenName}} {{if $t.Name}}{{$t.Name}}{{else}}{{$t.Contract}}{{end}} {{formattedAmountSpan $t.BalanceSat $t.Decimals $t.Symbol $data "copyable"}} @@ -147,13 +147,13 @@
{{summaryValuesSpa
{{end}} -{{if tokenCount $addr.Tokens "ERC721"}} +{{if tokenCount $addr.Tokens .NonFungibleTokenName}}
@@ -167,7 +167,7 @@
ERC721 Tokens {{toke Transfers# {{range $t := $addr.Tokens}} - {{if eq $t.Type "ERC721"}} + {{if eq $t.Standard $.NonFungibleTokenName}} {{if $t.Name}}{{$t.Name}}{{else}}{{$t.Contract}}{{end}} @@ -184,13 +184,13 @@
ERC721 Tokens {{toke
{{end}} -{{if tokenCount $addr.Tokens "ERC1155"}} +{{if tokenCount $addr.Tokens .MultiTokenName}}
@@ -204,7 +204,7 @@
ERC1155 Tokens {{tok Transfers# {{range $t := $addr.Tokens}} - {{if eq $t.Type "ERC1155"}} + {{if eq $t.Standard $.MultiTokenName}} {{if $t.Name}}{{$t.Name}}{{else}}{{$t.Contract}}{{end}} @@ -221,6 +221,60 @@
ERC1155 Tokens {{tok
{{end}} +{{if $addr.StakingPools }} +
+
+
+ +
+
+
+ {{range $sp := $addr.StakingPools}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$sp.Name}} {{$sp.Contract}}
Pending Balance{{amountSpan $sp.PendingBalance $data "copyable"}}
Pending Deposited Balance{{amountSpan $sp.PendingDepositedBalance $data "copyable"}}
Deposited Balance{{amountSpan $sp.DepositedBalance $data "copyable"}}
Withdrawal Total Amount{{amountSpan $sp.WithdrawTotalAmount $data "copyable"}}
Claimable Amount{{amountSpan $sp.ClaimableAmount $data "copyable"}}
Restaked Reward{{amountSpan $sp.RestakedReward $data "copyable"}}
Autocompound Balance{{amountSpan $sp.AutocompoundBalance $data "copyable"}}
+ {{end}} +
+
+
+
+{{end}} {{end}} {{if or $addr.Transactions $addr.Filter}}
@@ -234,18 +288,18 @@

Transactions

{{range $t := $addr.Tokens}} - {{if eq $t.Type "ERC20"}} - + {{if eq $t.Standard $.FungibleTokenName}} + {{end}} {{end}} {{range $t := $addr.Tokens}} - {{if eq $t.Type "ERC721"}} - + {{if eq $t.Standard $.NonFungibleTokenName}} + {{end}} {{end}} {{range $t := $addr.Tokens}} - {{if eq $t.Type "ERC1155"}} - + {{if eq $t.Standard $.MultiTokenName}} + {{end}} {{end}} {{end}} diff --git a/static/templates/base.html b/static/templates/base.html index 86f2631e52..9156d494e1 100644 --- a/static/templates/base.html +++ b/static/templates/base.html @@ -4,9 +4,10 @@ - + - + + @@ -53,7 +54,7 @@ {{range $c := .SecondaryCurrencies}} {{end}} -
+

{{end}}
diff --git a/static/templates/index.html b/static/templates/index.html index 36d81fe5cc..3454943f35 100644 --- a/static/templates/index.html +++ b/static/templates/index.html @@ -64,6 +64,12 @@

{{$data.ContractInfo.Contract}}
{{$data.ContractInfo.Name}} - Contract type - {{$data.ContractInfo.Type}} + Standard + {{$data.ContractInfo.Standard}} diff --git a/static/templates/tx.html b/static/templates/tx.html index 396d6c14de..4ca57b8013 100644 --- a/static/templates/tx.html +++ b/static/templates/tx.html @@ -51,6 +51,42 @@
{{$tx.Txid}}Gas Price {{amountSpan $tx.EthereumSpecific.GasPrice $data "copyable"}} ({{amountSatsSpan $tx.EthereumSpecific.GasPrice $data "copyable"}} Gwei) + {{if $tx.EthereumSpecific.MaxPriorityFeePerGas}} + + Max Priority Fee Per Gas + {{amountSpan $tx.EthereumSpecific.MaxPriorityFeePerGas $data "copyable"}} ({{amountSatsSpan $tx.EthereumSpecific.MaxPriorityFeePerGas $data "copyable"}} Gwei) + + {{end}} + {{if $tx.EthereumSpecific.MaxFeePerGas}} + + Max Fee Per Gas + {{amountSpan $tx.EthereumSpecific.MaxFeePerGas $data "copyable"}} ({{amountSatsSpan $tx.EthereumSpecific.MaxFeePerGas $data "copyable"}} Gwei) + + {{end}} + {{if $tx.EthereumSpecific.BaseFeePerGas}} + + Base Fee Per Gas + {{amountSpan $tx.EthereumSpecific.BaseFeePerGas $data "copyable"}} ({{amountSatsSpan $tx.EthereumSpecific.BaseFeePerGas $data "copyable"}} Gwei) + + {{end}} + {{if $tx.EthereumSpecific.L1GasUsed}} + + L1 Gas Used + {{formatBigInt $tx.EthereumSpecific.L1GasUsed}} + + {{end}} + {{if $tx.EthereumSpecific.L1GasPrice}} + + L1 Gas Price + {{amountSpan $tx.EthereumSpecific.L1GasPrice $data "copyable"}} ({{amountSatsSpan $tx.EthereumSpecific.L1GasPrice $data "copyable"}} Gwei) + + {{end}} + {{if $tx.EthereumSpecific.L1FeeScalar}} + + L1 Fee Scalar + {{$tx.EthereumSpecific.L1FeeScalar}} + + {{end}} {{else}} Total Input @@ -99,6 +135,12 @@
{{$tx.Txid}} {{end}} + {{if $tx.EthereumSpecific}} + + Nonce + {{$tx.EthereumSpecific.Nonce}} + + {{end}}
@@ -158,13 +200,19 @@
{{if $tx.EthereumSpecific.ParsedData.Name}}{{$tx.EthereumSpecif {{end}} {{end}}
-
Raw Transaction
-
-

+    
+    
+    
+

     
-
{{end}} diff --git a/static/templates/txdetail.html b/static/templates/txdetail.html index a5f53758fd..3472323e6d 100644 --- a/static/templates/txdetail.html +++ b/static/templates/txdetail.html @@ -59,8 +59,8 @@