diff --git a/.cargo/config b/.cargo/config.toml similarity index 99% rename from .cargo/config rename to .cargo/config.toml index c81c668c82..5704896780 100644 --- a/.cargo/config +++ b/.cargo/config.toml @@ -8,14 +8,14 @@ rustflags = [ "-Zshare-generics=y" ] # [target.x86_64-unknown-linux-gnu] # linker = "clang" # rustflags = [ "-Clink-arg=-fuse-ld=lld" ] -# +# # # `brew install llvm` # [target.x86_64-apple-darwin] # rustflags = [ # "-C", # "link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld", # ] -# +# # [target.aarch64-apple-darwin] # rustflags = [ # "-C", diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 57683d5a17..8156a6934b 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/debian:buster-slim +FROM docker.io/debian:10 MAINTAINER Onur Özkan @@ -10,6 +10,7 @@ RUN apt-get install -y \ ca-certificates \ curl \ wget \ + unzip \ gnupg RUN ln -s /usr/bin/python3 /bin/python @@ -26,11 +27,11 @@ RUN ./llvm.sh 16 RUN rm ./llvm.sh -RUN ln -s /usr/bin/clang-16 /usr/bin/clang - ENV AR=/usr/bin/llvm-ar-16 ENV CC=/usr/bin/clang-16 +RUN ln -s /usr/bin/clang-16 /usr/bin/clang + RUN mkdir -m 0755 -p /etc/apt/keyrings RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg @@ -47,6 +48,9 @@ RUN apt-get install -y \ containerd.io \ docker-buildx-plugin -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --profile minimal --default-toolchain nightly-2023-06-01 -y + +RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.1/protoc-3.20.1-linux-x86_64.zip +RUN unzip protoc-3.20.1-linux-x86_64.zip && mv ./include/google /usr/include/google ENV PATH="/root/.cargo/bin:$PATH" diff --git a/.docker/Dockerfile.ci-container b/.docker/Dockerfile.ci-container index d68cecd225..c56e686888 100644 --- a/.docker/Dockerfile.ci-container +++ b/.docker/Dockerfile.ci-container @@ -14,9 +14,14 @@ RUN apt-get install -y \ gnupg \ git \ zip \ + python3 \ + python3-pip \ + python-dev \ + libffi-dev \ sudo -RUN ln -s /usr/bin/python3 /bin/python +RUN pip3 install --upgrade pip +RUN pip3 install paramiko RUN apt install -y \ software-properties-common \ diff --git a/.docker/Dockerfile.dev-release b/.docker/Dockerfile.dev-release index 3167c38a59..74abcb02e3 100644 --- a/.docker/Dockerfile.dev-release +++ b/.docker/Dockerfile.dev-release @@ -1,5 +1,6 @@ FROM docker.io/debian:stable-slim -WORKDIR /mm2 +WORKDIR /kdf +COPY target/release/kdf /usr/local/bin/kdf COPY target/release/mm2 /usr/local/bin/mm2 EXPOSE 7783 -CMD ["mm2"] +CMD ["kdf"] diff --git a/.docker/Dockerfile.release b/.docker/Dockerfile.release index 8de75301d6..d9d8d51325 100644 --- a/.docker/Dockerfile.release +++ b/.docker/Dockerfile.release @@ -1,5 +1,6 @@ FROM docker.io/debian:stable-slim -WORKDIR /mm2 +WORKDIR /kdf +COPY target/release/kdf /usr/local/bin/kdf COPY target/release/mm2 /usr/local/bin/mm2 EXPOSE 7783 -CMD ["mm2"] \ No newline at end of file +CMD ["kdf"] \ No newline at end of file diff --git a/.docker/Dockerfile.ubuntu.ci b/.docker/Dockerfile.ubuntu.ci index 5e86dac8f2..23110a6761 100644 --- a/.docker/Dockerfile.ubuntu.ci +++ b/.docker/Dockerfile.ubuntu.ci @@ -18,4 +18,4 @@ RUN \ chmod -R 777 /root ENV PATH="/root/.cargo/bin:${PATH}" -WORKDIR /mm2 \ No newline at end of file +WORKDIR /kdf \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/config/addrbook.json b/.docker/container-state/atom-testnet-data/config/addrbook.json new file mode 100644 index 0000000000..9f75ad40ef --- /dev/null +++ b/.docker/container-state/atom-testnet-data/config/addrbook.json @@ -0,0 +1,4 @@ +{ + "key": "9c1e658a8e070bbd7e7cfcaa", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/config/app.toml b/.docker/container-state/atom-testnet-data/config/app.toml new file mode 100644 index 0000000000..af33d27cbf --- /dev/null +++ b/.docker/container-state/atom-testnet-data/config/app.toml @@ -0,0 +1,263 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +############################################################################### +### Base Configuration ### +############################################################################### + +# The minimum gas prices a validator is willing to accept for processing a +# transaction. A transaction's fees must meet the minimum of any denomination +# specified in this config (e.g. 0.25token1,0.0001token2). +minimum-gas-prices = "0.0025uatom" + +# default: the last 362880 states are kept, pruning at 10 block intervals +# nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node) +# everything: 2 latest states will be kept; pruning at 10 block intervals. +# custom: allow pruning options to be manually specified through 'pruning-keep-recent', and 'pruning-interval' +pruning = "default" + +# These are applied if and only if the pruning strategy is custom. +pruning-keep-recent = "0" +pruning-interval = "0" + +# HaltHeight contains a non-zero block height at which a node will gracefully +# halt and shutdown that can be used to assist upgrades and testing. +# +# Note: Commitment of state will be attempted on the corresponding block. +halt-height = 0 + +# HaltTime contains a non-zero minimum block time (in Unix seconds) at which +# a node will gracefully halt and shutdown that can be used to assist upgrades +# and testing. +# +# Note: Commitment of state will be attempted on the corresponding block. +halt-time = 0 + +# MinRetainBlocks defines the minimum block height offset from the current +# block being committed, such that all blocks past this offset are pruned +# from Tendermint. It is used as part of the process of determining the +# ResponseCommit.RetainHeight value during ABCI Commit. A value of 0 indicates +# that no blocks should be pruned. +# +# This configuration value is only responsible for pruning Tendermint blocks. +# It has no bearing on application state pruning which is determined by the +# "pruning-*" configurations. +# +# Note: Tendermint block pruning is dependant on this parameter in conunction +# with the unbonding (safety threshold) period, state pruning and state sync +# snapshot parameters to determine the correct minimum value of +# ResponseCommit.RetainHeight. +min-retain-blocks = 0 + +# InterBlockCache enables inter-block caching. +inter-block-cache = true + +# IndexEvents defines the set of events in the form {eventType}.{attributeKey}, +# which informs Tendermint what to index. If empty, all events will be indexed. +# +# Example: +# ["message.sender", "message.recipient"] +index-events = [] + +# IavlCacheSize set the size of the iavl tree cache (in number of nodes). +iavl-cache-size = 781250 + +# IAVLDisableFastNode enables or disables the fast node feature of IAVL. +# Default is false. +iavl-disable-fastnode = false + +# IAVLLazyLoading enable/disable the lazy loading of iavl store. +# Default is false. +iavl-lazy-loading = false + +# AppDBBackend defines the database backend type to use for the application and snapshots DBs. +# An empty string indicates that a fallback will be used. +# The fallback is the db_backend value set in Tendermint's config.toml. +app-db-backend = "" + +############################################################################### +### Telemetry Configuration ### +############################################################################### + +[telemetry] + +# Prefixed with keys to separate services. +service-name = "" + +# Enabled enables the application telemetry functionality. When enabled, +# an in-memory sink is also enabled by default. Operators may also enabled +# other sinks such as Prometheus. +enabled = false + +# Enable prefixing gauge values with hostname. +enable-hostname = false + +# Enable adding hostname to labels. +enable-hostname-label = false + +# Enable adding service to labels. +enable-service-label = false + +# PrometheusRetentionTime, when positive, enables a Prometheus metrics sink. +prometheus-retention-time = 0 + +# GlobalLabels defines a global set of name/value label tuples applied to all +# metrics emitted using the wrapper functions defined in telemetry package. +# +# Example: +# [["chain_id", "cosmoshub-1"]] +global-labels = [ +] + +############################################################################### +### API Configuration ### +############################################################################### + +[api] + +# Enable defines if the API server should be enabled. +enable = true + +# Swagger defines if swagger documentation should automatically be registered. +swagger = true + +# Address defines the API server to listen on. +address = "tcp://localhost:1318" + +# MaxOpenConnections defines the number of maximum open connections. +max-open-connections = 1000 + +# RPCReadTimeout defines the Tendermint RPC read timeout (in seconds). +rpc-read-timeout = 10 + +# RPCWriteTimeout defines the Tendermint RPC write timeout (in seconds). +rpc-write-timeout = 0 + +# RPCMaxBodyBytes defines the Tendermint maximum request body (in bytes). +rpc-max-body-bytes = 1000000 + +# EnableUnsafeCORS defines if CORS should be enabled (unsafe - use it at your own risk). +enabled-unsafe-cors = false + +############################################################################### +### Rosetta Configuration ### +############################################################################### + +[rosetta] + +# Enable defines if the Rosetta API server should be enabled. +enable = false + +# Address defines the Rosetta API server to listen on. +address = ":8080" + +# Network defines the name of the blockchain that will be returned by Rosetta. +blockchain = "app" + +# Network defines the name of the network that will be returned by Rosetta. +network = "network" + +# Retries defines the number of retries when connecting to the node before failing. +retries = 3 + +# Offline defines if Rosetta server should run in offline mode. +offline = false + +# EnableDefaultSuggestedFee defines if the server should suggest fee by default. +# If 'construction/medata' is called without gas limit and gas price, +# suggested fee based on gas-to-suggest and denom-to-suggest will be given. +enable-fee-suggestion = false + +# GasToSuggest defines gas limit when calculating the fee +gas-to-suggest = 200000 + +# DenomToSuggest defines the defult denom for fee suggestion. +# Price must be in minimum-gas-prices. +denom-to-suggest = "uatom" + +############################################################################### +### gRPC Configuration ### +############################################################################### + +[grpc] + +# Enable defines if the gRPC server should be enabled. +enable = true + +# Address defines the gRPC server address to bind to. +address = "localhost:9090" + +# MaxRecvMsgSize defines the max message size in bytes the server can receive. +# The default value is 10MB. +max-recv-msg-size = "10485760" + +# MaxSendMsgSize defines the max message size in bytes the server can send. +# The default value is math.MaxInt32. +max-send-msg-size = "2147483647" + +############################################################################### +### gRPC Web Configuration ### +############################################################################### + +[grpc-web] + +# GRPCWebEnable defines if the gRPC-web should be enabled. +# NOTE: gRPC must also be enabled, otherwise, this configuration is a no-op. +enable = true + +# Address defines the gRPC-web server address to bind to. +address = "localhost:9091" + +# EnableUnsafeCORS defines if CORS should be enabled (unsafe - use it at your own risk). +enable-unsafe-cors = false + +############################################################################### +### State Sync Configuration ### +############################################################################### + +# State sync snapshots allow other nodes to rapidly join the network without replaying historical +# blocks, instead downloading and applying a snapshot of the application state at a given height. +[state-sync] + +# snapshot-interval specifies the block interval at which local state sync snapshots are +# taken (0 to disable). +snapshot-interval = 1000 + +# snapshot-keep-recent specifies the number of recent snapshots to keep and serve (0 to keep all). +snapshot-keep-recent = 10 + +############################################################################### +### Store / State Streaming ### +############################################################################### + +[store] +streamers = [] + +[streamers] +[streamers.file] +keys = ["*", ] +write_dir = "" +prefix = "" + +# output-metadata specifies if output the metadata file which includes the abci request/responses +# during processing the block. +output-metadata = "true" + +# stop-node-on-error specifies if propagate the file streamer errors to consensus state machine. +stop-node-on-error = "true" + +# fsync specifies if call fsync after writing the files. +fsync = "false" + +############################################################################### +### Mempool ### +############################################################################### + +[mempool] +# Setting max-txs to 0 will allow for a unbounded amount of transactions in the mempool. +# Setting max_txs to negative 1 (-1) will disable transactions from being inserted into the mempool. +# Setting max_txs to a positive number (> 0) will limit the number of transactions in the mempool, by the specified amount. +# +# Note, this configuration only applies to SDK built-in app-side mempool +# implementations. +max-txs = 5000 diff --git a/.docker/container-state/atom-testnet-data/config/client.toml b/.docker/container-state/atom-testnet-data/config/client.toml new file mode 100644 index 0000000000..a066773456 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/config/client.toml @@ -0,0 +1,17 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +############################################################################### +### Client Configuration ### +############################################################################### + +# The network chain ID +chain-id = "cosmoshub-testnet" +# The keyring's backend, where the keys are stored (os|file|kwallet|pass|test|memory) +keyring-backend = "test" +# CLI output format (text|json) +output = "text" +# : to Tendermint RPC interface for this chain +node = "tcp://0.0.0.0:26658" +# Transaction broadcasting mode (sync|async) +broadcast-mode = "sync" diff --git a/.docker/container-state/atom-testnet-data/config/config.toml b/.docker/container-state/atom-testnet-data/config/config.toml new file mode 100644 index 0000000000..31c8c4633a --- /dev/null +++ b/.docker/container-state/atom-testnet-data/config/config.toml @@ -0,0 +1,492 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +# NOTE: Any path below can be absolute (e.g. "/var/myawesomeapp/data") or +# relative to the home directory (e.g. "data"). The home directory is +# "$HOME/.cometbft" by default, but could be changed via $CMTHOME env variable +# or --home cmd flag. + +####################################################################### +### Main Base Config Options ### +####################################################################### + +# TCP or UNIX socket address of the ABCI application, +# or the name of an ABCI application compiled in with the CometBFT binary +proxy_app = "tcp://127.0.0.1:26658" + +# A custom human readable name for this node +moniker = "nimda" + +# If this node is many blocks behind the tip of the chain, BlockSync +# allows them to catchup quickly by downloading blocks in parallel +# and verifying their commits +# +# Deprecated: this key will be removed and BlockSync will be enabled +# unconditionally in the next major release. +block_sync = true + +# Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb +# * goleveldb (github.com/syndtr/goleveldb - most popular implementation) +# - pure go +# - stable +# * cleveldb (uses levigo wrapper) +# - fast +# - requires gcc +# - use cleveldb build tag (go build -tags cleveldb) +# * boltdb (uses etcd's fork of bolt - github.com/etcd-io/bbolt) +# - EXPERIMENTAL +# - may be faster is some use-cases (random reads - indexer) +# - use boltdb build tag (go build -tags boltdb) +# * rocksdb (uses github.com/tecbot/gorocksdb) +# - EXPERIMENTAL +# - requires gcc +# - use rocksdb build tag (go build -tags rocksdb) +# * badgerdb (uses github.com/dgraph-io/badger) +# - EXPERIMENTAL +# - use badgerdb build tag (go build -tags badgerdb) +db_backend = "goleveldb" + +# Database directory +db_dir = "data" + +# Output level for logging, including package level options +log_level = "info" + +# Output format: 'plain' (colored text) or 'json' +log_format = "plain" + +##### additional base config options ##### + +# Path to the JSON file containing the initial validator set and other meta data +genesis_file = "config/genesis.json" + +# Path to the JSON file containing the private key to use as a validator in the consensus protocol +priv_validator_key_file = "config/priv_validator_key.json" + +# Path to the JSON file containing the last sign state of a validator +priv_validator_state_file = "data/priv_validator_state.json" + +# TCP or UNIX socket address for CometBFT to listen on for +# connections from an external PrivValidator process +priv_validator_laddr = "" + +# Path to the JSON file containing the private key to use for node authentication in the p2p protocol +node_key_file = "config/node_key.json" + +# Mechanism to connect to the ABCI application: socket | grpc +abci = "socket" + +# If true, query the ABCI app on connecting to a new peer +# so the app can decide if we should keep the connection or not +filter_peers = false + + +####################################################################### +### Advanced Configuration Options ### +####################################################################### + +####################################################### +### RPC Server Configuration Options ### +####################################################### +[rpc] + +# TCP or UNIX socket address for the RPC server to listen on +laddr = "tcp://0.0.0.0:26658" + +# A list of origins a cross-domain request can be executed from +# Default value '[]' disables cors support +# Use '["*"]' to allow any origin +cors_allowed_origins = [] + +# A list of methods the client is allowed to use with cross-domain requests +cors_allowed_methods = ["HEAD", "GET", "POST", ] + +# A list of non simple headers the client is allowed to use with cross-domain requests +cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time", ] + +# TCP or UNIX socket address for the gRPC server to listen on +# NOTE: This server only supports /broadcast_tx_commit +grpc_laddr = "" + +# Maximum number of simultaneous connections. +# Does not include RPC (HTTP&WebSocket) connections. See max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +grpc_max_open_connections = 900 + +# Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool +unsafe = false + +# Maximum number of simultaneous connections (including WebSocket). +# Does not include gRPC connections. See grpc_max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +max_open_connections = 900 + +# Maximum number of unique clientIDs that can /subscribe +# If you're using /broadcast_tx_commit, set to the estimated maximum number +# of broadcast_tx_commit calls per block. +max_subscription_clients = 100 + +# Maximum number of unique queries a given client can /subscribe to +# If you're using GRPC (or Local RPC client) and /broadcast_tx_commit, set to +# the estimated # maximum number of broadcast_tx_commit calls per block. +max_subscriptions_per_client = 5 + +# Experimental parameter to specify the maximum number of events a node will +# buffer, per subscription, before returning an error and closing the +# subscription. Must be set to at least 100, but higher values will accommodate +# higher event throughput rates (and will use more memory). +experimental_subscription_buffer_size = 200 + +# Experimental parameter to specify the maximum number of RPC responses that +# can be buffered per WebSocket client. If clients cannot read from the +# WebSocket endpoint fast enough, they will be disconnected, so increasing this +# parameter may reduce the chances of them being disconnected (but will cause +# the node to use more memory). +# +# Must be at least the same as "experimental_subscription_buffer_size", +# otherwise connections could be dropped unnecessarily. This value should +# ideally be somewhat higher than "experimental_subscription_buffer_size" to +# accommodate non-subscription-related RPC responses. +experimental_websocket_write_buffer_size = 200 + +# If a WebSocket client cannot read fast enough, at present we may +# silently drop events instead of generating an error or disconnecting the +# client. +# +# Enabling this experimental parameter will cause the WebSocket connection to +# be closed instead if it cannot read fast enough, allowing for greater +# predictability in subscription behavior. +experimental_close_on_slow_client = false + +# How long to wait for a tx to be committed during /broadcast_tx_commit. +# WARNING: Using a value larger than 10s will result in increasing the +# global HTTP write timeout, which applies to all connections and endpoints. +# See https://github.com/tendermint/tendermint/issues/3435 +timeout_broadcast_tx_commit = "10s" + +# Maximum size of request body, in bytes +max_body_bytes = 1000000 + +# Maximum size of request header, in bytes +max_header_bytes = 1048576 + +# The path to a file containing certificate that is used to create the HTTPS server. +# Might be either absolute path or path related to CometBFT's config directory. +# If the certificate is signed by a certificate authority, +# the certFile should be the concatenation of the server's certificate, any intermediates, +# and the CA's certificate. +# NOTE: both tls_cert_file and tls_key_file must be present for CometBFT to create HTTPS server. +# Otherwise, HTTP server is run. +tls_cert_file = "" + +# The path to a file containing matching private key that is used to create the HTTPS server. +# Might be either absolute path or path related to CometBFT's config directory. +# NOTE: both tls-cert-file and tls-key-file must be present for CometBFT to create HTTPS server. +# Otherwise, HTTP server is run. +tls_key_file = "" + +# pprof listen address (https://golang.org/pkg/net/http/pprof) +pprof_laddr = "localhost:6061" + +####################################################### +### P2P Configuration Options ### +####################################################### +[p2p] + +# Address to listen for incoming connections +laddr = "tcp://0.0.0.0:26655" + +# Address to advertise to peers for them to dial. If empty, will use the same +# port as the laddr, and will introspect on the listener to figure out the +# address. IP and port are required. Example: 159.89.10.97:26655 +external_address = "" + +# Comma separated list of seed nodes to connect to +seeds = "" + +# Comma separated list of nodes to keep persistent connections to +persistent_peers = "" + +# Path to address book +addr_book_file = "config/addrbook.json" + +# Set true for strict address routability rules +# Set false for private or local networks +addr_book_strict = true + +# Maximum number of inbound peers +max_num_inbound_peers = 40 + +# Maximum number of outbound peers to connect to, excluding persistent peers +max_num_outbound_peers = 10 + +# List of node IDs, to which a connection will be (re)established ignoring any existing limits +unconditional_peer_ids = "" + +# Maximum pause when redialing a persistent peer (if zero, exponential backoff is used) +persistent_peers_max_dial_period = "0s" + +# Time to wait before flushing messages out on the connection +flush_throttle_timeout = "100ms" + +# Maximum size of a message packet payload, in bytes +max_packet_msg_payload_size = 1024 + +# Rate at which packets can be sent, in bytes/second +send_rate = 5120000 + +# Rate at which packets can be received, in bytes/second +recv_rate = 5120000 + +# Set true to enable the peer-exchange reactor +pex = true + +# Seed mode, in which node constantly crawls the network and looks for +# peers. If another node asks it for addresses, it responds and disconnects. +# +# Does not work if the peer-exchange reactor is disabled. +seed_mode = false + +# Comma separated list of peer IDs to keep private (will not be gossiped to other peers) +private_peer_ids = "" + +# Toggle to disable guard against peers connecting from the same ip. +allow_duplicate_ip = false + +# Peer connection configuration. +handshake_timeout = "20s" +dial_timeout = "3s" + +####################################################### +### Mempool Configuration Option ### +####################################################### +[mempool] + +# Mempool version to use: +# 1) "v0" - (default) FIFO mempool. +# 2) "v1" - prioritized mempool (deprecated; will be removed in the next release). +version = "v0" + +# The type of mempool for this node to use. +# +# Possible types: +# - "flood" : concurrent linked list mempool with flooding gossip protocol +# (default) +# - "nop" : nop-mempool (short for no operation; the ABCI app is responsible +# for storing, disseminating and proposing txs). "create_empty_blocks=false" is +# not supported. +type = "flood" + +recheck = true +broadcast = true +wal_dir = "" + +# Maximum number of transactions in the mempool +size = 5000 + +# Limit the total size of all txs in the mempool. +# This only accounts for raw transactions (e.g. given 1MB transactions and +# max_txs_bytes=5MB, mempool will only accept 5 transactions). +max_txs_bytes = 1073741824 + +# Size of the cache (used to filter transactions we saw earlier) in transactions +cache_size = 10000 + +# Do not remove invalid transactions from the cache (default: false) +# Set to true if it's not possible for any invalid transaction to become valid +# again in the future. +keep-invalid-txs-in-cache = false + +# Maximum size of a single transaction. +# NOTE: the max size of a tx transmitted over the network is {max_tx_bytes}. +max_tx_bytes = 1048576 + +# Maximum size of a batch of transactions to send to a peer +# Including space needed by encoding (one varint per transaction). +# XXX: Unused due to https://github.com/tendermint/tendermint/issues/5796 +max_batch_bytes = 0 + +# ttl-duration, if non-zero, defines the maximum amount of time a transaction +# can exist for in the mempool. +# +# Note, if ttl-num-blocks is also defined, a transaction will be removed if it +# has existed in the mempool at least ttl-num-blocks number of blocks or if it's +# insertion time into the mempool is beyond ttl-duration. +ttl-duration = "0s" + +# ttl-num-blocks, if non-zero, defines the maximum number of blocks a transaction +# can exist for in the mempool. +# +# Note, if ttl-duration is also defined, a transaction will be removed if it +# has existed in the mempool at least ttl-num-blocks number of blocks or if +# it's insertion time into the mempool is beyond ttl-duration. +ttl-num-blocks = 0 + +# Experimental parameters to limit gossiping txs to up to the specified number of peers. +# This feature is only available for the default mempool (version config set to "v0"). +# We use two independent upper values for persistent and non-persistent peers. +# Unconditional peers are not affected by this feature. +# If we are connected to more than the specified number of persistent peers, only send txs to +# ExperimentalMaxGossipConnectionsToPersistentPeers of them. If one of those +# persistent peers disconnects, activate another persistent peer. +# Similarly for non-persistent peers, with an upper limit of +# ExperimentalMaxGossipConnectionsToNonPersistentPeers. +# If set to 0, the feature is disabled for the corresponding group of peers, that is, the +# number of active connections to that group of peers is not bounded. +# For non-persistent peers, if enabled, a value of 10 is recommended based on experimental +# performance results using the default P2P configuration. +experimental_max_gossip_connections_to_persistent_peers = 0 +experimental_max_gossip_connections_to_non_persistent_peers = 0 + +####################################################### +### State Sync Configuration Options ### +####################################################### +[statesync] +# State sync rapidly bootstraps a new node by discovering, fetching, and restoring a state machine +# snapshot from peers instead of fetching and replaying historical blocks. Requires some peers in +# the network to take and serve state machine snapshots. State sync is not attempted if the node +# has any local state (LastBlockHeight > 0). The node will have a truncated block history, +# starting from the height of the snapshot. +enable = false + +# RPC servers (comma-separated) for light client verification of the synced state machine and +# retrieval of state data for node bootstrapping. Also needs a trusted height and corresponding +# header hash obtained from a trusted source, and a period during which validators can be trusted. +# +# For Cosmos SDK-based chains, trust_period should usually be about 2/3 of the unbonding time (~2 +# weeks) during which they can be financially punished (slashed) for misbehavior. +rpc_servers = "" +trust_height = 0 +trust_hash = "" +trust_period = "168h0m0s" + +# Time to spend discovering snapshots before initiating a restore. +discovery_time = "15s" + +# Temporary directory for state sync snapshot chunks, defaults to the OS tempdir (typically /tmp). +# Will create a new, randomly named directory within, and remove it when done. +temp_dir = "" + +# The timeout duration before re-requesting a chunk, possibly from a different +# peer (default: 1 minute). +chunk_request_timeout = "10s" + +# The number of concurrent chunk fetchers to run (default: 1). +chunk_fetchers = "4" + +####################################################### +### Block Sync Configuration Options ### +####################################################### +[blocksync] + +# Block Sync version to use: +# +# In v0.37, v1 and v2 of the block sync protocols were deprecated. +# Please use v0 instead. +# +# 1) "v0" - the default block sync implementation +version = "v0" + +####################################################### +### Consensus Configuration Options ### +####################################################### +[consensus] + +wal_file = "data/cs.wal/wal" + +# How long we wait for a proposal block before prevoting nil +timeout_propose = "3s" +# How much timeout_propose increases with each round +timeout_propose_delta = "500ms" +# How long we wait after receiving +2/3 prevotes for “anything” (ie. not a single block or nil) +timeout_prevote = "1s" +# How much the timeout_prevote increases with each round +timeout_prevote_delta = "500ms" +# How long we wait after receiving +2/3 precommits for “anything” (ie. not a single block or nil) +timeout_precommit = "1s" +# How much the timeout_precommit increases with each round +timeout_precommit_delta = "500ms" +# How long we wait after committing a block, before starting on the new +# height (this gives us a chance to receive some more precommits, even +# though we already have +2/3). +timeout_commit = "5s" + +# How many blocks to look back to check existence of the node's consensus votes before joining consensus +# When non-zero, the node will panic upon restart +# if the same consensus key was used to sign {double_sign_check_height} last blocks. +# So, validators should stop the state machine, wait for some blocks, and then restart the state machine to avoid panic. +double_sign_check_height = 0 + +# Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) +skip_timeout_commit = false + +# EmptyBlocks mode and possible interval between empty blocks +create_empty_blocks = true +create_empty_blocks_interval = "0s" + +# Reactor sleep duration parameters +peer_gossip_sleep_duration = "100ms" +peer_query_maj23_sleep_duration = "2s" + +####################################################### +### Storage Configuration Options ### +####################################################### +[storage] + +# Set to true to discard ABCI responses from the state store, which can save a +# considerable amount of disk space. Set to false to ensure ABCI responses are +# persisted. ABCI responses are required for /block_results RPC queries, and to +# reindex events in the command-line tool. +discard_abci_responses = false + +####################################################### +### Transaction Indexer Configuration Options ### +####################################################### +[tx_index] + +# What indexer to use for transactions +# +# The application will set which txs to index. In some cases a node operator will be able +# to decide which txs to index based on configuration set in the application. +# +# Options: +# 1) "null" +# 2) "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). +# - When "kv" is chosen "tx.height" and "tx.hash" will always be indexed. +# 3) "psql" - the indexer services backed by PostgreSQL. +# When "kv" or "psql" is chosen "tx.height" and "tx.hash" will always be indexed. +indexer = "kv" + +# The PostgreSQL connection configuration, the connection format: +# postgresql://:@:/? +psql-conn = "" + +####################################################### +### Instrumentation Configuration Options ### +####################################################### +[instrumentation] + +# When true, Prometheus metrics are served under /metrics on +# PrometheusListenAddr. +# Check out the documentation for the list of available metrics. +prometheus = false + +# Address to listen for Prometheus collector(s) connections +prometheus_listen_addr = ":26660" + +# Maximum number of simultaneous connections. +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +max_open_connections = 3 + +# Instrumentation namespace +namespace = "cometbft" diff --git a/.docker/container-state/atom-testnet-data/config/genesis.json b/.docker/container-state/atom-testnet-data/config/genesis.json new file mode 100644 index 0000000000..c6f9d33015 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/config/genesis.json @@ -0,0 +1,544 @@ +{ + "genesis_time": "2024-06-03T11:43:33.263437606Z", + "chain_id": "cosmoshub-testnet", + "initial_height": "1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + }, + "version": { + "app": "0" + } + }, + "app_hash": "", + "app_state": { + "07-tendermint": null, + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "cosmos15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89k6d4n0", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "cosmos1yfygf0zr5s69ces9r0h72hqv23nkqz9nc2lvt2", + "pub_key": null, + "account_number": "1", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "cosmos10tfc28dmn2m5qdrmg5ycjyqq7lyu7y8ledc8tc", + "pub_key": null, + "account_number": "2", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "cosmos150evuj4j7k9kgu38e453jdv9m3u0ft2n53flg6", + "pub_key": null, + "account_number": "3", + "sequence": "0" + } + ] + }, + "authz": { + "authorization": [] + }, + "bank": { + "params": { + "send_enabled": [], + "default_send_enabled": true + }, + "balances": [ + { + "address": "cosmos1yfygf0zr5s69ces9r0h72hqv23nkqz9nc2lvt2", + "coins": [] + }, + { + "address": "cosmos10tfc28dmn2m5qdrmg5ycjyqq7lyu7y8ledc8tc", + "coins": [ + { + "denom": "uatom", + "amount": "12345" + } + ] + }, + { + "address": "cosmos15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89k6d4n0", + "coins": [ + { + "denom": "uatom", + "amount": "10000000000" + } + ] + }, + { + "address": "cosmos150evuj4j7k9kgu38e453jdv9m3u0ft2n53flg6", + "coins": [ + { + "denom": "uatom", + "amount": "10000000000" + } + ] + } + ], + "supply": [ + { + "denom": "uatom", + "amount": "20000012345" + } + ], + "denom_metadata": [ + { + "description": "The native staking token of Cosmoshub.", + "denom_units": [ + { + "denom": "uatom", + "exponent": 0, + "aliases": [ + "microatom" + ] + }, + { + "denom": "matom", + "exponent": 3, + "aliases": [ + "milliatom" + ] + }, + { + "denom": "atom", + "exponent": 6, + "aliases": [] + } + ], + "base": "uatom", + "display": "atom", + "name": "", + "symbol": "", + "uri": "", + "uri_hash": "" + } + ], + "send_enabled": [] + }, + "capability": { + "index": "1", + "owners": [] + }, + "consensus": null, + "crisis": { + "constant_fee": { + "denom": "uatom", + "amount": "1000" + } + }, + "distribution": { + "params": { + "community_tax": "0.020000000000000000", + "base_proposer_reward": "0.000000000000000000", + "bonus_proposer_reward": "0.000000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "evidence": { + "evidence": [] + }, + "feegrant": { + "allowances": [] + }, + "feeibc": { + "identified_fees": [], + "fee_enabled_channels": [], + "registered_payees": [], + "registered_counterparty_payees": [], + "forward_relayers": [] + }, + "genutil": { + "gen_txs": [ + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "nimda", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.050000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "0", + "delegator_address": "cosmos15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89k6d4n0", + "validator_address": "cosmosvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89nweqlu", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "jJu9A07VzP62GblYgmbBHed8DlmFNVg2jEaM0p6FLQU=" + }, + "value": { + "denom": "uatom", + "amount": "200000000" + } + } + ], + "memo": "07cf84bfb3d35e62409e6637f11edc414edd6f6b@192.168.1.24:26656", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [ + { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AsgJRkBRhm9bOTsGtcByi+fBbdjEp6BB59AiFHDqDRN0" + }, + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "sequence": "0" + } + ], + "fee": { + "amount": [ + { + "denom": "uatom", + "amount": "2500" + } + ], + "gas_limit": "200000", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [ + "RIr7bzYEV6f6LBAdzSwx7FrDg5Ya6HloX6su6p4jPWcxgAH9ChduQrEpSlsdEPgqQ31l2xjkQLeqqm3Hn6eCGA==" + ] + } + ] + }, + "globalfee": { + "params": { + "minimum_gas_prices": [], + "bypass_min_fee_msg_types": [ + "/ibc.core.channel.v1.MsgRecvPacket", + "/ibc.core.channel.v1.MsgAcknowledgement", + "/ibc.core.client.v1.MsgUpdateClient", + "/ibc.core.channel.v1.MsgTimeout", + "/ibc.core.channel.v1.MsgTimeoutOnClose" + ], + "max_total_bypass_min_fee_msg_gas_usage": "1000000" + } + }, + "gov": { + "starting_proposal_id": "1", + "deposits": [], + "votes": [], + "proposals": [], + "deposit_params": null, + "voting_params": null, + "tally_params": null, + "params": { + "min_deposit": [ + { + "denom": "uatom", + "amount": "10000000" + } + ], + "max_deposit_period": "172800s", + "voting_period": "172800s", + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto_threshold": "0.334000000000000000", + "min_initial_deposit_ratio": "0.000000000000000000", + "burn_vote_quorum": false, + "burn_proposal_deposit_prevote": false, + "burn_vote_veto": true, + "min_deposit_ratio": "0.010000000000000000" + } + }, + "ibc": { + "client_genesis": { + "clients": [], + "clients_consensus": [], + "clients_metadata": [], + "params": { + "allowed_clients": [ + "06-solomachine", + "07-tendermint", + "09-localhost" + ] + }, + "create_localhost": false, + "next_client_sequence": "0" + }, + "connection_genesis": { + "connections": [], + "client_connection_paths": [], + "next_connection_sequence": "0", + "params": { + "max_expected_time_per_block": "30000000000" + } + }, + "channel_genesis": { + "channels": [], + "acknowledgements": [], + "commitments": [], + "receipts": [], + "send_sequences": [], + "recv_sequences": [], + "ack_sequences": [], + "next_channel_sequence": "0" + } + }, + "interchainaccounts": { + "controller_genesis_state": { + "active_channels": [], + "interchain_accounts": [], + "ports": [], + "params": { + "controller_enabled": true + } + }, + "host_genesis_state": { + "active_channels": [], + "interchain_accounts": [], + "port": "icahost", + "params": { + "host_enabled": true, + "allow_messages": [ + "*" + ] + } + } + }, + "metaprotocols": {}, + "mint": { + "minter": { + "inflation": "0.130000000000000000", + "annual_provisions": "0.000000000000000000" + }, + "params": { + "mint_denom": "uatom", + "inflation_rate_change": "0.130000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "goal_bonded": "0.670000000000000000", + "blocks_per_year": "6311520" + } + }, + "packetfowardmiddleware": { + "params": { + "fee_percentage": "0.000000000000000000" + }, + "in_flight_packets": {} + }, + "params": null, + "provider": { + "valset_update_id": "1", + "consumer_states": [], + "unbonding_ops": [], + "mature_unbonding_ops": null, + "valset_update_id_to_height": [], + "consumer_addition_proposals": [], + "consumer_removal_proposals": [], + "params": { + "template_client": { + "chain_id": "", + "trust_level": { + "numerator": "1", + "denominator": "3" + }, + "trusting_period": "0s", + "unbonding_period": "0s", + "max_clock_drift": "10s", + "frozen_height": { + "revision_number": "0", + "revision_height": "0" + }, + "latest_height": { + "revision_number": "0", + "revision_height": "0" + }, + "proof_specs": [ + { + "leaf_spec": { + "hash": "SHA256", + "prehash_key": "NO_HASH", + "prehash_value": "SHA256", + "length": "VAR_PROTO", + "prefix": "AA==" + }, + "inner_spec": { + "child_order": [ + 0, + 1 + ], + "child_size": 33, + "min_prefix_length": 4, + "max_prefix_length": 12, + "empty_child": null, + "hash": "SHA256" + }, + "max_depth": 0, + "min_depth": 0, + "prehash_key_before_comparison": false + }, + { + "leaf_spec": { + "hash": "SHA256", + "prehash_key": "NO_HASH", + "prehash_value": "SHA256", + "length": "VAR_PROTO", + "prefix": "AA==" + }, + "inner_spec": { + "child_order": [ + 0, + 1 + ], + "child_size": 32, + "min_prefix_length": 1, + "max_prefix_length": 1, + "empty_child": null, + "hash": "SHA256" + }, + "max_depth": 0, + "min_depth": 0, + "prehash_key_before_comparison": false + } + ], + "upgrade_path": [ + "upgrade", + "upgradedIBCState" + ], + "allow_update_after_expiry": false, + "allow_update_after_misbehaviour": false + }, + "trusting_period_fraction": "0.66", + "ccv_timeout_period": "2419200s", + "init_timeout_period": "604800s", + "vsc_timeout_period": "3024000s", + "slash_meter_replenish_period": "3600s", + "slash_meter_replenish_fraction": "0.05", + "consumer_reward_denom_registration_fee": { + "denom": "uatom", + "amount": "10000000" + }, + "blocks_per_epoch": "600" + }, + "validator_consumer_pubkeys": [], + "validators_by_consumer_addr": [], + "consumer_addrs_to_prune": [], + "init_timeout_timestamps": [], + "exported_vsc_send_timestamps": [] + }, + "ratelimit": { + "params": {}, + "rate_limits": [], + "whitelisted_address_pairs": [], + "blacklisted_denoms": [], + "pending_send_packet_sequence_numbers": [], + "hour_epoch": { + "epoch_number": "0", + "duration": "3600s", + "epoch_start_time": "0001-01-01T00:00:00Z", + "epoch_start_height": "0" + } + }, + "slashing": { + "params": { + "signed_blocks_window": "100", + "min_signed_per_window": "0.500000000000000000", + "downtime_jail_duration": "600s", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": [], + "missed_blocks": [] + }, + "staking": { + "params": { + "unbonding_time": "1814400s", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 10000, + "bond_denom": "uatom", + "min_commission_rate": "0.000000000000000000", + "validator_bond_factor": "-1.000000000000000000", + "global_liquid_staking_cap": "1.000000000000000000", + "validator_liquid_staking_cap": "1.000000000000000000" + }, + "last_total_power": "0", + "last_validator_powers": [], + "validators": [], + "delegations": [], + "unbonding_delegations": [], + "redelegations": [], + "exported": false, + "tokenize_share_records": [], + "last_tokenize_share_record_id": "0", + "tokenize_share_locks": [] + }, + "transfer": { + "port_id": "transfer", + "denom_traces": [], + "params": { + "send_enabled": true, + "receive_enabled": true + }, + "total_escrowed": [] + }, + "upgrade": {}, + "vesting": {}, + "htlc": { + "params": { + "asset_params": [] + }, + "htlcs": [], + "supplies": [], + "previous_block_time": "1970-01-01T00:00:01Z" + } + } +} \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/config/gentx/gentx-07cf84bfb3d35e62409e6637f11edc414edd6f6b.json b/.docker/container-state/atom-testnet-data/config/gentx/gentx-07cf84bfb3d35e62409e6637f11edc414edd6f6b.json new file mode 100644 index 0000000000..13879cb222 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/config/gentx/gentx-07cf84bfb3d35e62409e6637f11edc414edd6f6b.json @@ -0,0 +1 @@ +{"body":{"messages":[{"@type":"/cosmos.staking.v1beta1.MsgCreateValidator","description":{"moniker":"nimda","identity":"","website":"","security_contact":"","details":""},"commission":{"rate":"0.050000000000000000","max_rate":"0.200000000000000000","max_change_rate":"0.010000000000000000"},"min_self_delegation":"0","delegator_address":"cosmos15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89k6d4n0","validator_address":"cosmosvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89nweqlu","pubkey":{"@type":"/cosmos.crypto.ed25519.PubKey","key":"jJu9A07VzP62GblYgmbBHed8DlmFNVg2jEaM0p6FLQU="},"value":{"denom":"uatom","amount":"200000000"}}],"memo":"07cf84bfb3d35e62409e6637f11edc414edd6f6b@192.168.1.24:26656","timeout_height":"0","extension_options":[],"non_critical_extension_options":[]},"auth_info":{"signer_infos":[{"public_key":{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"AsgJRkBRhm9bOTsGtcByi+fBbdjEp6BB59AiFHDqDRN0"},"mode_info":{"single":{"mode":"SIGN_MODE_DIRECT"}},"sequence":"0"}],"fee":{"amount":[{"denom":"uatom","amount":"2500"}],"gas_limit":"200000","payer":"","granter":""},"tip":null},"signatures":["RIr7bzYEV6f6LBAdzSwx7FrDg5Ya6HloX6su6p4jPWcxgAH9ChduQrEpSlsdEPgqQ31l2xjkQLeqqm3Hn6eCGA=="]} diff --git a/.docker/container-state/atom-testnet-data/config/node_key.json b/.docker/container-state/atom-testnet-data/config/node_key.json new file mode 100644 index 0000000000..4e2f33b9fb --- /dev/null +++ b/.docker/container-state/atom-testnet-data/config/node_key.json @@ -0,0 +1 @@ +{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"fFxhUeVktUmntyAYZqCpOnV4LdkXCgV1x+516mKwOY9PIQQAxxZvRUZiWMNdpcFVV7U9q8XoIrbi0oH9EOU6IQ=="}} \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/config/priv_validator_key.json b/.docker/container-state/atom-testnet-data/config/priv_validator_key.json new file mode 100644 index 0000000000..2b76bcb996 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/config/priv_validator_key.json @@ -0,0 +1,11 @@ +{ + "address": "3E49B197AC395DB2BD999B4C3F35F1B6B36508D0", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "jJu9A07VzP62GblYgmbBHed8DlmFNVg2jEaM0p6FLQU=" + }, + "priv_key": { + "type": "tendermint/PrivKeyEd25519", + "value": "/FmlJVv/qhnR0lfYXgngRFR2C5jA1NtNElR4xa0N28eMm70DTtXM/rYZuViCZsEd53wOWYU1WDaMRozSnoUtBQ==" + } +} \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000122.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000122.ldb new file mode 100644 index 0000000000..5979f2fdb6 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000122.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000123.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000123.ldb new file mode 100644 index 0000000000..b03a2ddeb6 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000123.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000124.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000124.ldb new file mode 100644 index 0000000000..415499ade2 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000124.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000125.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000125.ldb new file mode 100644 index 0000000000..90619e3797 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000125.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000126.log b/.docker/container-state/atom-testnet-data/data/application.db/000126.log new file mode 100644 index 0000000000..69d85e6d61 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000126.log differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/CURRENT b/.docker/container-state/atom-testnet-data/data/application.db/CURRENT new file mode 100644 index 0000000000..224d52afe7 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/application.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000127 diff --git a/.docker/container-state/atom-testnet-data/data/application.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/application.db/CURRENT.bak new file mode 100644 index 0000000000..2b465edb20 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/application.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000121 diff --git a/.docker/container-state/atom-testnet-data/data/application.db/LOCK b/.docker/container-state/atom-testnet-data/data/application.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/atom-testnet-data/data/application.db/LOG b/.docker/container-state/atom-testnet-data/data/application.db/LOG new file mode 100644 index 0000000000..40eb63ea2c --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/application.db/LOG @@ -0,0 +1,472 @@ +=============== Jun 3, 2024 (+03) =============== +14:44:32.470783 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:44:32.472928 db@open opening +14:44:32.473060 version@stat F·[] S·0B[] Sc·[] +14:44:32.473634 db@janitor F·2 G·0 +14:44:32.473644 db@open done T·708.125µs +=============== Jun 6, 2024 (+03) =============== +18:04:49.030249 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:04:49.030388 version@stat F·[] S·0B[] Sc·[] +18:04:49.030400 db@open opening +18:04:49.030434 journal@recovery F·1 +18:04:49.030820 journal@recovery recovering @1 +18:04:49.038914 memdb@flush created L0@2 N·8801 S·458KiB "s/1,v430":"s/p..hts,v590" +18:04:49.039037 version@stat F·[1] S·458KiB[458KiB] Sc·[0.25] +18:04:49.041764 db@janitor F·3 G·0 +18:04:49.041784 db@open done T·11.378459ms +=============== Jun 6, 2024 (+03) =============== +18:09:26.383098 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:09:26.383228 version@stat F·[1] S·458KiB[458KiB] Sc·[0.25] +18:09:26.383240 db@open opening +18:09:26.383267 journal@recovery F·1 +18:09:26.385208 journal@recovery recovering @3 +18:09:26.393123 memdb@flush created L0@5 N·8851 S·474KiB "s/100,v15842":"s/p..hts,v8954" +18:09:26.393259 version@stat F·[2] S·932KiB[932KiB] Sc·[0.50] +18:09:26.396002 db@janitor F·4 G·0 +18:09:26.396016 db@open done T·12.770381ms +18:09:31.452362 table@compaction L0·2 -> L1·0 S·932KiB Q·17654 +18:09:31.458728 table@build created L1@8 N·13251 S·882KiB "s/1,v430":"s/p..hts,v17651" +18:09:31.458755 version@stat F·[0 1] S·882KiB[0B 882KiB] Sc·[0.00 0.01] +18:09:31.459341 table@compaction committed F-1 S-49KiB Ke·0 D·4401 T·6.95821ms +18:09:31.459478 table@remove removed @2 +=============== Jun 6, 2024 (+03) =============== +19:18:28.670453 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:18:28.670587 version@stat F·[0 1] S·882KiB[0B 882KiB] Sc·[0.00 0.01] +19:18:28.670603 db@open opening +19:18:28.670631 journal@recovery F·1 +19:18:28.670706 journal@recovery recovering @6 +19:18:28.675997 memdb@flush created L0@9 N·4727 S·258KiB "s/112,v17816":"s/p..hts,v17815" +19:18:28.676113 version@stat F·[1 1] S·1MiB[258KiB 882KiB] Sc·[0.25 0.01] +19:18:28.678710 db@janitor F·5 G·1 +19:18:28.678716 db@janitor removing table-5 +19:18:28.678854 db@open done T·8.247273ms +=============== Jun 6, 2024 (+03) =============== +19:29:57.913638 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:29:57.913762 version@stat F·[1 1] S·1MiB[258KiB 882KiB] Sc·[0.25 0.01] +19:29:57.913776 db@open opening +19:29:57.913805 journal@recovery F·1 +19:29:57.913883 journal@recovery recovering @10 +19:29:57.915623 memdb@flush created L0@12 N·756 S·39KiB "s/139,v22557":"s/p..hts,v22556" +19:29:57.915929 version@stat F·[2 1] S·1MiB[297KiB 882KiB] Sc·[0.50 0.01] +19:29:57.918802 db@janitor F·5 G·0 +19:29:57.918815 db@open done T·5.034214ms +19:30:02.970800 table@compaction L0·2 -> L1·1 S·1MiB Q·23139 +19:30:02.979924 table@build created L1@15 N·17454 S·1MiB "s/1,v430":"s/p..hts,v23136" +19:30:02.979953 version@stat F·[0 1] S·1MiB[0B 1MiB] Sc·[0.00 0.01] +19:30:02.980537 table@compaction committed F-2 S-4KiB Ke·0 D·1280 T·9.713575ms +19:30:02.980645 table@remove removed @9 +19:30:02.980828 table@remove removed @8 +=============== Jun 10, 2024 (UTC) =============== +10:09:48.888722 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:48.888843 version@stat F·[0 1] S·1MiB[0B 1MiB] Sc·[0.00 0.01] +10:09:48.888855 db@open opening +10:09:48.888888 journal@recovery F·1 +10:09:48.889077 journal@recovery recovering @13 +10:09:48.891277 memdb@flush created L0@16 N·1329 S·68KiB "s/143,v23304":"s/p..hts,v23303" +10:09:48.891428 version@stat F·[1 1] S·1MiB[68KiB 1MiB] Sc·[0.25 0.01] +10:09:48.894794 db@janitor F·5 G·1 +10:09:48.894800 db@janitor removing table-12 +10:09:48.894839 db@open done T·5.979788ms +10:10:19.001548 table@compaction L0·1 -> L1·1 S·1MiB Q·25336 +10:10:19.010338 table@build created L1@19 N·18470 S·1MiB "s/1,v430":"s/p..hts,v24466" +10:10:19.010358 version@stat F·[0 1] S·1MiB[0B 1MiB] Sc·[0.00 0.01] +10:10:19.010930 table@compaction committed F-1 S+1KiB Ke·0 D·313 T·9.363036ms +10:10:19.011217 table@remove removed @15 +=============== Jun 10, 2024 (UTC) =============== +10:19:47.864242 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.864404 version@stat F·[0 1] S·1MiB[0B 1MiB] Sc·[0.00 0.01] +10:19:47.864417 db@open opening +10:19:47.864455 journal@recovery F·1 +10:19:47.864543 journal@recovery recovering @17 +10:19:47.883750 memdb@flush created L0@20 N·19720 S·1MiB "s/151,v24659":"s/p..hts,v24658" +10:19:47.883883 version@stat F·[1 1] S·2MiB[1MiB 1MiB] Sc·[0.25 0.01] +10:19:47.886887 db@janitor F·5 G·1 +10:19:47.886895 db@janitor removing table-16 +10:19:47.886941 db@open done T·22.519639ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.739618 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.739758 version@stat F·[1 1] S·2MiB[1MiB 1MiB] Sc·[0.25 0.01] +10:20:11.739793 db@open opening +10:20:11.739833 journal@recovery F·1 +10:20:11.739918 journal@recovery recovering @21 +10:20:11.740384 version@stat F·[1 1] S·2MiB[1MiB 1MiB] Sc·[0.25 0.01] +10:20:11.743444 db@janitor F·4 G·0 +10:20:11.743466 db@open done T·3.657641ms +10:21:01.860122 table@compaction L0·1 -> L1·1 S·2MiB Q·45701 +10:21:01.873375 table@build created L1@25 N·28916 S·2MiB "s/1,v430":"s/k..D\xaa\x83,v20141" +10:21:01.875580 table@build created L1@26 N·4609 S·215KiB "s/k..\x06\vf,v1010":"s/p..hts,v44187" +10:21:01.875601 version@stat F·[0 2] S·2MiB[0B 2MiB] Sc·[0.00 0.02] +10:21:01.876175 table@compaction committed F~ S-46KiB Ke·0 D·4665 T·16.036575ms +10:21:01.876420 table@remove removed @20 +10:21:01.876646 table@remove removed @19 +=============== Jun 10, 2024 (UTC) =============== +10:23:28.279773 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.279915 version@stat F·[0 2] S·2MiB[0B 2MiB] Sc·[0.00 0.02] +10:23:28.279928 db@open opening +10:23:28.279963 journal@recovery F·1 +10:23:28.280049 journal@recovery recovering @23 +10:23:28.286016 memdb@flush created L0@27 N·5710 S·307KiB "s/268,v44357":"s/p..hts,v44356" +10:23:28.286191 version@stat F·[1 2] S·2MiB[307KiB 2MiB] Sc·[0.25 0.02] +10:23:28.289566 db@janitor F·5 G·0 +10:23:28.289579 db@open done T·9.646421ms +10:24:13.402908 table@compaction L0·1 -> L1·2 S·2MiB Q·51243 +10:24:13.416567 table@build created L1@30 N·30507 S·2MiB "s/1,v430":"s/k..6L|,v16959" +10:24:13.420157 table@build created L1@31 N·7398 S·526KiB "s/k..\x9e\x02\xd1,v7074":"s/p..hts,v49898" +10:24:13.420180 version@stat F·[0 2] S·2MiB[0B 2MiB] Sc·[0.00 0.03] +10:24:13.420757 table@compaction committed F-1 S+4KiB Ke·0 D·1330 T·17.82593ms +10:24:13.421167 table@remove removed @25 +10:24:13.421239 table@remove removed @26 +=============== Jun 10, 2024 (UTC) =============== +10:27:08.453702 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.453825 version@stat F·[0 2] S·2MiB[0B 2MiB] Sc·[0.00 0.03] +10:27:08.453837 db@open opening +10:27:08.453874 journal@recovery F·1 +10:27:08.455857 journal@recovery recovering @28 +10:27:08.461852 memdb@flush created L0@32 N·7019 S·388KiB "s/302,v50066":"s/p..hts,v50065" +10:27:08.462783 version@stat F·[1 2] S·2MiB[388KiB 2MiB] Sc·[0.25 0.03] +10:27:08.465641 db@janitor F·6 G·1 +10:27:08.465655 db@janitor removing table-27 +10:27:08.465750 db@open done T·11.90829ms +10:28:18.630807 table@compaction L0·1 -> L1·2 S·2MiB Q·59266 +10:28:18.645204 table@build created L1@35 N·32525 S·2MiB "s/1,v430":"s/k..\xe8\x0fr,v31522" +10:28:18.650417 table@build created L1@36 N·10781 S·895KiB "s/k..\x1fE\xf7,v42971":"s/p..hts,v56918" +10:28:18.650437 version@stat F·[0 2] S·2MiB[0B 2MiB] Sc·[0.00 0.03] +10:28:18.651013 table@compaction committed F-1 S-18KiB Ke·0 D·1618 T·20.181509ms +10:28:18.651419 table@remove removed @30 +10:28:18.651531 table@remove removed @31 +=============== Jun 10, 2024 (UTC) =============== +10:30:28.072433 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.072563 version@stat F·[0 2] S·2MiB[0B 2MiB] Sc·[0.00 0.03] +10:30:28.072576 db@open opening +10:30:28.072619 journal@recovery F·1 +10:30:28.072705 journal@recovery recovering @33 +10:30:28.076122 memdb@flush created L0@37 N·3354 S·181KiB "s/343,v57088":"s/p..hts,v57087" +10:30:28.076417 version@stat F·[1 2] S·3MiB[181KiB 2MiB] Sc·[0.25 0.03] +10:30:28.080919 db@janitor F·6 G·1 +10:30:28.080934 db@janitor removing table-32 +10:30:28.081042 db@open done T·8.460381ms +10:31:13.198757 table@compaction L0·1 -> L1·2 S·3MiB Q·61619 +10:31:13.212967 table@build created L1@40 N·33817 S·2MiB "s/1,v430":"s/k..101,v15999" +10:31:13.219101 table@build created L1@41 N·12061 S·1MiB "s/k..102,v16164":"s/p..hts,v60273" +10:31:13.219122 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.03] +10:31:13.219696 table@compaction committed F-1 S+1KiB Ke·0 D·782 T·20.920426ms +10:31:13.220094 table@remove removed @35 +10:31:13.220302 table@remove removed @36 +=============== Jun 10, 2024 (UTC) =============== +11:11:44.643599 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.643714 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.03] +11:11:44.643726 db@open opening +11:11:44.643777 journal@recovery F·1 +11:11:44.643861 journal@recovery recovering @38 +11:11:44.652096 memdb@flush created L0@42 N·8401 S·461KiB "s/363,v60443":"s/p..hts,v60442" +11:11:44.654048 version@stat F·[1 2] S·3MiB[461KiB 3MiB] Sc·[0.25 0.03] +11:11:44.656876 db@janitor F·6 G·1 +11:11:44.656883 db@janitor removing table-37 +11:11:44.656946 db@open done T·13.21517ms +11:12:14.752800 table@compaction L0·1 -> L1·2 S·3MiB Q·69618 +11:12:14.767156 table@build created L1@45 N·33052 S·2MiB "s/1,v430":"s/k..!%\x05,v33355" +11:12:14.776213 table@build created L1@46 N·19272 S·1MiB "s/k..A.f,v33358":"s/p..hts,v68675" +11:12:14.776266 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +11:12:14.778223 table@compaction committed F-1 S-19KiB Ke·0 D·1955 T·25.395841ms +11:12:14.778633 table@remove removed @40 +11:12:14.778843 table@remove removed @41 +=============== Jun 10, 2024 (UTC) =============== +11:15:00.429927 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.430050 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +11:15:00.430063 db@open opening +11:15:00.430099 journal@recovery F·1 +11:15:00.430194 journal@recovery recovering @43 +11:15:00.434598 memdb@flush created L0@47 N·4626 S·267KiB "s/413,v68845":"s/p..hts,v68844" +11:15:00.435140 version@stat F·[1 2] S·3MiB[267KiB 3MiB] Sc·[0.25 0.04] +11:15:00.438447 db@janitor F·6 G·1 +11:15:00.438463 db@janitor removing table-42 +11:15:00.438584 db@open done T·8.515761ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.263340 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.263473 version@stat F·[1 2] S·3MiB[267KiB 3MiB] Sc·[0.25 0.04] +11:21:08.263485 db@open opening +11:21:08.263520 journal@recovery F·1 +11:21:08.263603 journal@recovery recovering @48 +11:21:08.264212 version@stat F·[1 2] S·3MiB[267KiB 3MiB] Sc·[0.25 0.04] +11:21:08.266901 db@janitor F·5 G·0 +11:21:08.266919 db@open done T·3.422808ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.199114 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.199268 version@stat F·[1 2] S·3MiB[267KiB 3MiB] Sc·[0.25 0.04] +11:22:28.199281 db@open opening +11:22:28.199319 journal@recovery F·1 +11:22:28.199409 journal@recovery recovering @50 +11:22:28.200482 memdb@flush created L0@52 N·174 S·10KiB "s/437,v73478":"s/p..hts,v73477" +11:22:28.200769 version@stat F·[2 2] S·3MiB[277KiB 3MiB] Sc·[0.50 0.04] +11:22:28.204055 db@janitor F·6 G·0 +11:22:28.204075 db@open done T·4.78312ms +11:22:33.277462 table@compaction L0·2 -> L1·2 S·3MiB Q·73480 +11:22:33.291989 table@build created L1@55 N·32831 S·2MiB "s/1,v430":"s/k..\xca\xec\x89,v36389" +11:22:33.302726 table@build created L1@56 N·23270 S·1MiB "s/k..\x82<\x01,v33691":"s/p..hts,v73477" +11:22:33.302751 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +11:22:33.303326 table@compaction committed F-2 S-5KiB Ke·0 D·1023 T·25.824435ms +11:22:33.303436 table@remove removed @47 +11:22:33.303808 table@remove removed @45 +11:22:33.304122 table@remove removed @46 +=============== Jun 10, 2024 (UTC) =============== +11:23:15.294049 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.294165 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +11:23:15.294178 db@open opening +11:23:15.294219 journal@recovery F·1 +11:23:15.294309 journal@recovery recovering @53 +11:23:15.295725 memdb@flush created L0@57 N·871 S·46KiB "s/438,v73653":"s/p..hts,v73652" +11:23:15.295980 version@stat F·[1 2] S·3MiB[46KiB 3MiB] Sc·[0.25 0.04] +11:23:15.299082 db@janitor F·6 G·1 +11:23:15.299096 db@janitor removing table-52 +11:23:15.299134 db@open done T·4.952161ms +11:23:50.404822 table@compaction L0·1 -> L1·2 S·3MiB Q·75396 +11:23:50.419850 table@build created L1@60 N·32875 S·2MiB "s/1,v430":"s/k..\x93\xe0],v32335" +11:23:50.430534 table@build created L1@61 N·23901 S·1MiB "s/k..\x13<\xea,v17425":"s/p..hts,v74349" +11:23:50.430556 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +11:23:50.431136 table@compaction committed F-1 S+1KiB Ke·0 D·196 T·26.29764ms +11:23:50.431536 table@remove removed @55 +11:23:50.431867 table@remove removed @56 +=============== Jun 10, 2024 (UTC) =============== +11:29:08.654580 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.654713 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +11:29:08.654727 db@open opening +11:29:08.654764 journal@recovery F·1 +11:29:08.654851 journal@recovery recovering @58 +11:29:08.665426 memdb@flush created L0@62 N·11434 S·635KiB "s/443,v74525":"s/p..hts,v74524" +11:29:08.665566 version@stat F·[1 2] S·4MiB[635KiB 3MiB] Sc·[0.25 0.04] +11:29:08.668438 db@janitor F·6 G·1 +11:29:08.668448 db@janitor removing table-57 +11:29:08.668492 db@open done T·13.759515ms +11:29:53.787682 table@compaction L0·1 -> L1·2 S·4MiB Q·87164 +11:29:53.802279 table@build created L1@65 N·30758 S·2MiB "s/1,v430":"s/k..\xd3\x01\xbd,v36705" +11:29:53.814393 table@build created L1@66 N·25657 S·2MiB "s/k..\x00\xe8\xc7,v36704":"s/k..t[\xd5,v10496" +11:29:53.818240 table@build created L1@67 N·9215 S·408KiB "s/k..\x1fa\x1c,v3122":"s/p..hts,v85784" +11:29:53.818266 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.04] +11:29:53.818845 table@compaction committed F~ S-3KiB Ke·0 D·2580 T·31.139681ms +11:29:53.819258 table@remove removed @60 +11:29:53.819625 table@remove removed @61 +=============== Jun 10, 2024 (UTC) =============== +11:30:42.338644 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.338765 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.04] +11:30:42.338779 db@open opening +11:30:42.338817 journal@recovery F·1 +11:30:42.339033 journal@recovery recovering @63 +11:30:42.342593 memdb@flush created L0@68 N·2936 S·159KiB "s/509,v85960":"s/p..hts,v85959" +11:30:42.342747 version@stat F·[1 3] S·4MiB[159KiB 4MiB] Sc·[0.25 0.04] +11:30:42.345611 db@janitor F·7 G·1 +11:30:42.345622 db@janitor removing table-62 +11:30:42.345772 db@open done T·6.987419ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.600264 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.600395 version@stat F·[1 3] S·4MiB[159KiB 4MiB] Sc·[0.25 0.04] +11:30:55.600410 db@open opening +11:30:55.600449 journal@recovery F·1 +11:30:55.600584 journal@recovery recovering @69 +11:30:55.602155 memdb@flush created L0@71 N·174 S·10KiB "s/526,v88897":"s/p..hts,v88896" +11:30:55.602318 version@stat F·[2 3] S·4MiB[169KiB 4MiB] Sc·[0.50 0.04] +11:30:55.604943 db@janitor F·7 G·0 +11:30:55.604959 db@open done T·4.543228ms +11:31:00.674202 table@compaction L0·2 -> L1·3 S·4MiB Q·88899 +11:31:00.688956 table@build created L1@74 N·30690 S·2MiB "s/1,v430":"s/k..\x9aNH,v23211" +11:31:00.701699 table@build created L1@75 N·26683 S·2MiB "s/k..\xf7\xfcj,v78951":"s/k..T\xea(,v68803" +11:31:00.706363 table@build created L1@76 N·10663 S·569KiB "s/k..4\xaa\x9c,v83159":"s/p..hts,v88896" +11:31:00.706391 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.05] +11:31:00.706972 table@compaction committed F-2 S-9KiB Ke·0 D·704 T·32.741325ms +11:31:00.707066 table@remove removed @68 +11:31:00.707443 table@remove removed @65 +11:31:00.707913 table@remove removed @66 +11:31:00.708008 table@remove removed @67 +=============== Jun 10, 2024 (UTC) =============== +11:36:36.118534 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.118658 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.05] +11:36:36.118672 db@open opening +11:36:36.118712 journal@recovery F·1 +11:36:36.118796 journal@recovery recovering @72 +11:36:36.128330 memdb@flush created L0@77 N·10436 S·584KiB "s/527,v89072":"s/p..hts,v89071" +11:36:36.128457 version@stat F·[1 3] S·5MiB[584KiB 4MiB] Sc·[0.25 0.05] +11:36:36.131274 db@janitor F·7 G·1 +11:36:36.131284 db@janitor removing table-71 +11:36:36.131323 db@open done T·12.644866ms +11:37:16.245879 table@compaction L0·1 -> L1·3 S·5MiB Q·100545 +11:37:16.260038 table@build created L1@80 N·28695 S·2MiB "s/1,v430":"s/k..\x96g\xfe,v88424" +11:37:16.274075 table@build created L1@81 N·31724 S·2MiB "s/k..\xb4\xc9-,v88423":"s/k..\x14f\xab,v88687" +11:37:16.281200 table@build created L1@82 N·15707 S·1MiB "s/k..\xc0\x14\xad,v59055":"s/p..hts,v99333" +11:37:16.281223 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.05] +11:37:16.281805 table@compaction committed F-1 S-18KiB Ke·0 D·2346 T·35.907911ms +11:37:16.282217 table@remove removed @74 +11:37:16.282629 table@remove removed @75 +11:37:16.282802 table@remove removed @76 +=============== Jun 10, 2024 (UTC) =============== +12:01:05.009469 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:05.009641 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.05] +12:01:05.009656 db@open opening +12:01:05.009696 journal@recovery F·1 +12:01:05.009781 journal@recovery recovering @78 +12:01:05.018430 memdb@flush created L0@83 N·8756 S·488KiB "s/587,v99509":"s/p..hts,v99508" +12:01:05.018561 version@stat F·[1 3] S·5MiB[488KiB 5MiB] Sc·[0.25 0.05] +12:01:05.021399 db@janitor F·7 G·1 +12:01:05.021409 db@janitor removing table-77 +12:01:05.021553 db@open done T·11.890965ms +=============== Jun 10, 2024 (UTC) =============== +12:03:00.000946 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:00.001079 version@stat F·[1 3] S·5MiB[488KiB 5MiB] Sc·[0.25 0.05] +12:03:00.001093 db@open opening +12:03:00.001133 journal@recovery F·1 +12:03:00.001225 journal@recovery recovering @84 +12:03:00.002416 memdb@flush created L0@86 N·531 S·28KiB "s/637,v108268":"s/p..hts,v108267" +12:03:00.002624 version@stat F·[2 3] S·5MiB[516KiB 5MiB] Sc·[0.50 0.05] +12:03:00.005937 db@janitor F·7 G·0 +12:03:00.005952 db@open done T·4.853053ms +12:03:05.077109 table@compaction L0·2 -> L1·3 S·5MiB Q·108625 +12:03:05.091524 table@build created L1@89 N·27160 S·2MiB "s/1,v430":"s/k..p\xf6%,v38370" +12:03:05.107522 table@build created L1@90 N·35970 S·2MiB "s/k..\xcc8$,v38365":"s/k..\x89\xee\x89,v19481" +12:03:05.117642 table@build created L1@91 N·20210 S·1MiB "s/k..\xa4u-,v36431":"s/p..hts,v108622" +12:03:05.117667 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.06] +12:03:05.118245 table@compaction committed F-2 S-11KiB Ke·0 D·2073 T·41.114952ms +12:03:05.118396 table@remove removed @83 +12:03:05.118803 table@remove removed @80 +12:03:05.119233 table@remove removed @81 +12:03:05.119442 table@remove removed @82 +=============== Jun 10, 2024 (UTC) =============== +12:03:19.709632 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.709765 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.06] +12:03:19.709779 db@open opening +12:03:19.709820 journal@recovery F·1 +12:03:19.709955 journal@recovery recovering @87 +12:03:19.711380 memdb@flush created L0@92 N·347 S·18KiB "s/640,v108797":"s/p..hts,v108795" +12:03:19.713412 version@stat F·[1 3] S·5MiB[18KiB 5MiB] Sc·[0.25 0.06] +12:03:19.716005 db@janitor F·7 G·1 +12:03:19.716015 db@janitor removing table-86 +12:03:19.716059 db@open done T·6.274876ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.803627 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.803757 version@stat F·[1 3] S·5MiB[18KiB 5MiB] Sc·[0.25 0.06] +12:04:23.803771 db@open opening +12:04:23.803811 journal@recovery F·1 +12:04:23.804004 journal@recovery recovering @93 +12:04:23.805335 memdb@flush created L0@95 N·174 S·10KiB "s/642,v109146":"s/p..hts,v109145" +12:04:23.805498 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:04:23.808108 db@janitor F·7 G·0 +12:04:23.808123 db@open done T·4.347018ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.886704 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.886839 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:07:46.886859 db@open opening +12:07:46.886900 journal@recovery F·1 +12:07:46.887029 journal@recovery recovering @96 +12:07:46.888107 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:07:46.890947 db@janitor F·7 G·0 +12:07:46.890962 db@open done T·4.097266ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.108588 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.108714 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:08:02.108729 db@open opening +12:08:02.108769 journal@recovery F·1 +12:08:02.108858 journal@recovery recovering @98 +12:08:02.109121 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:08:02.111934 db@janitor F·7 G·0 +12:08:02.111955 db@open done T·3.214158ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.433921 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.434059 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:27:21.434073 db@open opening +12:27:21.434110 journal@recovery F·1 +12:27:21.434202 journal@recovery recovering @100 +12:27:21.434741 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:27:21.438483 db@janitor F·7 G·0 +12:27:21.438506 db@open done T·4.420938ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.807711 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.807846 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:56:43.807861 db@open opening +12:56:43.807901 journal@recovery F·1 +12:56:43.808190 journal@recovery recovering @102 +12:56:43.809050 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:56:43.811721 db@janitor F·7 G·0 +12:56:43.811735 db@open done T·3.867954ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.382438 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.382574 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:57:38.382588 db@open opening +12:57:38.382631 journal@recovery F·1 +12:57:38.382875 journal@recovery recovering @104 +12:57:38.383858 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:57:38.386695 db@janitor F·7 G·0 +12:57:38.386709 db@open done T·4.114956ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.472246 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.472393 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:58:01.472411 db@open opening +12:58:01.472470 journal@recovery F·1 +12:58:01.472580 journal@recovery recovering @106 +12:58:01.472931 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:58:01.476286 db@janitor F·7 G·0 +12:58:01.476305 db@open done T·3.882034ms +=============== Jun 10, 2024 (UTC) =============== +12:58:48.995014 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:48.995158 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:58:48.995172 db@open opening +12:58:48.995220 journal@recovery F·1 +12:58:48.995307 journal@recovery recovering @108 +12:58:48.995915 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:58:48.998938 db@janitor F·7 G·0 +12:58:48.998959 db@open done T·3.773962ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.131412 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.131553 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:59:05.131566 db@open opening +12:59:05.131605 journal@recovery F·1 +12:59:05.131693 journal@recovery recovering @110 +12:59:05.132151 version@stat F·[2 3] S·5MiB[29KiB 5MiB] Sc·[0.50 0.06] +12:59:05.135904 db@janitor F·7 G·0 +12:59:05.135925 db@open done T·4.347538ms +12:59:10.211730 table@compaction L0·2 -> L1·3 S·5MiB Q·109148 +12:59:10.226056 table@build created L1@114 N·27001 S·2MiB "s/1,v430":"s/k..\xe0\xbe\x02,v34482" +12:59:10.243823 table@build created L1@115 N·36275 S·2MiB "s/k..8\xac\x87,v34652":"s/k..{OD,v100676" +12:59:10.253723 table@build created L1@116 N·20467 S·1MiB "s/k..\x87\xe7\xf7,v34071":"s/p..hts,v109145" +12:59:10.253746 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.06] +12:59:10.254320 table@compaction committed F-2 S+2KiB Ke·0 D·118 T·42.56914ms +12:59:10.254386 table@remove removed @95 +12:59:10.254424 table@remove removed @92 +12:59:10.254797 table@remove removed @89 +12:59:10.255281 table@remove removed @90 +12:59:10.255568 table@remove removed @91 +=============== Jun 10, 2024 (UTC) =============== +12:59:32.406943 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.407078 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.06] +12:59:32.407091 db@open opening +12:59:32.407127 journal@recovery F·1 +12:59:32.407218 journal@recovery recovering @112 +12:59:32.408386 memdb@flush created L0@117 N·176 S·10KiB "s/643,v109323":"s/p..hts,v109322" +12:59:32.408771 version@stat F·[1 3] S·5MiB[10KiB 5MiB] Sc·[0.25 0.06] +12:59:32.413762 db@janitor F·6 G·0 +12:59:32.413783 db@open done T·6.679628ms +=============== Jul 3, 2024 (UTC) =============== +17:52:58.343546 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.343680 version@stat F·[1 3] S·5MiB[10KiB 5MiB] Sc·[0.25 0.06] +17:52:58.343692 db@open opening +17:52:58.343730 journal@recovery F·1 +17:52:58.343813 journal@recovery recovering @118 +17:52:58.344805 version@stat F·[1 3] S·5MiB[10KiB 5MiB] Sc·[0.25 0.06] +17:52:58.347468 db@janitor F·6 G·0 +17:52:58.347493 db@open done T·3.787981ms +17:53:38.463406 table@compaction L0·1 -> L1·3 S·5MiB Q·110576 +17:53:38.476640 table@build created L1@122 N·27036 S·2MiB "s/1,v430":"s/k..\xe7\x85\xfc,v34490" +17:53:38.490737 table@build created L1@123 N·36290 S·2MiB "s/k..\x05u\xf4,v34491":"s/k..PX7,v35580" +17:53:38.499972 table@build created L1@124 N·20554 S·1MiB "s/k..\xef<&,v26322":"s/p..hts,v109322" +17:53:38.499994 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.06] +17:53:38.500573 table@compaction committed F-1 S+769B Ke·0 D·39 T·37.149557ms +17:53:38.500641 table@remove removed @117 +17:53:38.501044 table@remove removed @114 +17:53:38.501495 table@remove removed @115 +17:53:38.501847 table@remove removed @116 +=============== Jul 3, 2024 (UTC) =============== +17:59:01.697660 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.697817 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.06] +17:59:01.697829 db@open opening +17:59:01.697866 journal@recovery F·1 +17:59:01.697956 journal@recovery recovering @120 +17:59:01.710172 memdb@flush created L0@125 N·12904 S·739KiB "s/644,v109500":"s/p..hts,v109499" +17:59:01.710322 version@stat F·[1 3] S·6MiB[739KiB 5MiB] Sc·[0.25 0.06] +17:59:01.715180 db@janitor F·6 G·0 +17:59:01.715196 db@open done T·17.361833ms diff --git a/.docker/container-state/atom-testnet-data/data/application.db/MANIFEST-000127 b/.docker/container-state/atom-testnet-data/data/application.db/MANIFEST-000127 new file mode 100644 index 0000000000..28e3f506f3 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/MANIFEST-000127 differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/000093.ldb b/.docker/container-state/atom-testnet-data/data/blockstore.db/000093.ldb new file mode 100644 index 0000000000..8ddf4ae7fb Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/blockstore.db/000093.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/000096.ldb b/.docker/container-state/atom-testnet-data/data/blockstore.db/000096.ldb new file mode 100644 index 0000000000..854fba8071 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/blockstore.db/000096.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/000097.log b/.docker/container-state/atom-testnet-data/data/blockstore.db/000097.log new file mode 100644 index 0000000000..b5adbff40d Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/blockstore.db/000097.log differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT b/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT new file mode 100644 index 0000000000..95395b28bd --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000098 diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT.bak new file mode 100644 index 0000000000..b993e6cda5 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000095 diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/LOCK b/.docker/container-state/atom-testnet-data/data/blockstore.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/LOG b/.docker/container-state/atom-testnet-data/data/blockstore.db/LOG new file mode 100644 index 0000000000..969bf1ea7a --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/blockstore.db/LOG @@ -0,0 +1,386 @@ +=============== Jun 3, 2024 (+03) =============== +14:44:32.488873 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:44:32.490713 db@open opening +14:44:32.490847 version@stat F·[] S·0B[] Sc·[] +14:44:32.491453 db@janitor F·2 G·0 +14:44:32.491467 db@open done T·748.076µs +=============== Jun 6, 2024 (+03) =============== +18:04:49.061128 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:04:49.061216 version@stat F·[] S·0B[] Sc·[] +18:04:49.061227 db@open opening +18:04:49.061250 journal@recovery F·1 +18:04:49.061332 journal@recovery recovering @1 +18:04:49.062551 memdb@flush created L0@2 N·336 S·51KiB "BH:..f4e,v333":"blo..ore,v6" +18:04:49.062842 version@stat F·[1] S·51KiB[51KiB] Sc·[0.25] +18:04:49.066134 db@janitor F·3 G·0 +18:04:49.066148 db@open done T·4.916763ms +=============== Jun 6, 2024 (+03) =============== +18:09:26.413084 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:09:26.413194 version@stat F·[1] S·51KiB[51KiB] Sc·[0.25] +18:09:26.413205 db@open opening +18:09:26.413234 journal@recovery F·1 +18:09:26.413325 journal@recovery recovering @3 +18:09:26.414515 memdb@flush created L0@5 N·330 S·52KiB "BH:..e15,v628":"blo..ore,v343" +18:09:26.414886 version@stat F·[2] S·104KiB[104KiB] Sc·[0.50] +18:09:26.417663 db@janitor F·4 G·0 +18:09:26.417674 db@open done T·4.464969ms +=============== Jun 6, 2024 (+03) =============== +19:18:28.698110 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:18:28.698209 version@stat F·[2] S·104KiB[104KiB] Sc·[0.50] +19:18:28.698222 db@open opening +19:18:28.698250 journal@recovery F·1 +19:18:28.698341 journal@recovery recovering @6 +19:18:28.699364 memdb@flush created L0@8 N·162 S·27KiB "BH:..67b,v821":"blo..ore,v674" +19:18:28.699631 version@stat F·[3] S·132KiB[132KiB] Sc·[0.75] +19:18:28.702337 db@janitor F·5 G·0 +19:18:28.702348 db@open done T·4.123446ms +=============== Jun 6, 2024 (+03) =============== +19:29:57.938543 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:29:57.938619 version@stat F·[3] S·132KiB[132KiB] Sc·[0.75] +19:29:57.938629 db@open opening +19:29:57.938657 journal@recovery F·1 +19:29:57.938742 journal@recovery recovering @9 +19:29:57.939513 memdb@flush created L0@11 N·24 S·3KiB "BH:..6a1,v846":"blo..ore,v837" +19:29:57.939637 version@stat F·[4] S·135KiB[135KiB] Sc·[1.00] +19:29:57.942112 db@janitor F·6 G·0 +19:29:57.942119 db@open done T·3.487601ms +19:29:57.942146 table@compaction L0·4 -> L1·0 S·135KiB Q·856 +19:29:57.945014 table@build created L1@14 N·711 S·134KiB "BH:..e15,v628":"blo..ore,v855" +19:29:57.945035 version@stat F·[0 1] S·134KiB[0B 134KiB] Sc·[0.00 0.00] +19:29:57.945615 table@compaction committed F-3 S-1007B Ke·0 D·141 T·3.45401ms +19:29:57.945672 table@remove removed @8 +19:29:57.945706 table@remove removed @5 +19:29:57.945736 table@remove removed @2 +=============== Jun 10, 2024 (UTC) =============== +10:09:48.936150 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:48.936310 version@stat F·[0 1] S·134KiB[0B 134KiB] Sc·[0.00 0.00] +10:09:48.936325 db@open opening +10:09:48.936357 journal@recovery F·1 +10:09:48.936452 journal@recovery recovering @12 +10:09:48.937368 memdb@flush created L0@15 N·48 S·7KiB "BH:..6c1,v889":"blo..ore,v862" +10:09:48.937516 version@stat F·[1 1] S·141KiB[7KiB 134KiB] Sc·[0.25 0.00] +10:09:48.940107 db@janitor F·5 G·1 +10:09:48.940114 db@janitor removing table-11 +10:09:48.940160 db@open done T·3.831502ms +=============== Jun 10, 2024 (UTC) =============== +10:19:47.919317 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.919437 version@stat F·[1 1] S·141KiB[7KiB 134KiB] Sc·[0.25 0.00] +10:19:47.919453 db@open opening +10:19:47.919481 journal@recovery F·1 +10:19:47.919558 journal@recovery recovering @16 +10:19:47.921218 memdb@flush created L0@18 N·702 S·109KiB "BH:..c56,v1346":"blo..ore,v911" +10:19:47.921345 version@stat F·[2 1] S·251KiB[116KiB 134KiB] Sc·[0.50 0.00] +10:19:47.924050 db@janitor F·5 G·0 +10:19:47.924060 db@open done T·4.603129ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.776562 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.776673 version@stat F·[2 1] S·251KiB[116KiB 134KiB] Sc·[0.50 0.00] +10:20:11.776684 db@open opening +10:20:11.776713 journal@recovery F·1 +10:20:11.777346 journal@recovery recovering @19 +10:20:11.778263 version@stat F·[2 1] S·251KiB[116KiB 134KiB] Sc·[0.50 0.00] +10:20:11.780908 db@janitor F·5 G·0 +10:20:11.780918 db@open done T·4.229455ms +=============== Jun 10, 2024 (UTC) =============== +10:23:28.323489 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.323583 version@stat F·[2 1] S·251KiB[116KiB 134KiB] Sc·[0.50 0.00] +10:23:28.323595 db@open opening +10:23:28.323627 journal@recovery F·1 +10:23:28.323711 journal@recovery recovering @21 +10:23:28.324718 memdb@flush created L0@23 N·204 S·31KiB "BH:..b5f,v1617":"blo..ore,v1614" +10:23:28.324835 version@stat F·[3 1] S·283KiB[148KiB 134KiB] Sc·[0.75 0.00] +10:23:28.327449 db@janitor F·6 G·0 +10:23:28.327461 db@open done T·3.861793ms +=============== Jun 10, 2024 (UTC) =============== +10:27:08.500585 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.500746 version@stat F·[3 1] S·283KiB[148KiB 134KiB] Sc·[0.75 0.00] +10:27:08.500758 db@open opening +10:27:08.500793 journal@recovery F·1 +10:27:08.501923 journal@recovery recovering @24 +10:27:08.503154 memdb@flush created L0@26 N·246 S·39KiB "BH:..a29,v1840":"blo..ore,v1819" +10:27:08.503295 version@stat F·[4 1] S·323KiB[188KiB 134KiB] Sc·[1.00 0.00] +10:27:08.505975 db@janitor F·7 G·0 +10:27:08.505988 db@open done T·5.226534ms +10:27:08.506015 table@compaction L0·4 -> L1·1 S·323KiB Q·2060 +10:27:08.510618 table@build created L1@29 N·1711 S·322KiB "BH:..c56,v1346":"blo..ore,v2059" +10:27:08.510638 version@stat F·[0 1] S·322KiB[0B 322KiB] Sc·[0.00 0.00] +10:27:08.511249 table@compaction committed F-4 S-107B Ke·0 D·200 T·5.214384ms +10:27:08.511348 table@remove removed @23 +10:27:08.511400 table@remove removed @18 +10:27:08.511431 table@remove removed @15 +10:27:08.511480 table@remove removed @14 +=============== Jun 10, 2024 (UTC) =============== +10:30:28.116246 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.116355 version@stat F·[0 1] S·322KiB[0B 322KiB] Sc·[0.00 0.00] +10:30:28.116372 db@open opening +10:30:28.116418 journal@recovery F·1 +10:30:28.116502 journal@recovery recovering @27 +10:30:28.117379 memdb@flush created L0@30 N·120 S·19KiB "BH:..dcb,v2099":"blo..ore,v2066" +10:30:28.117497 version@stat F·[1 1] S·342KiB[19KiB 322KiB] Sc·[0.25 0.00] +10:30:28.120089 db@janitor F·5 G·1 +10:30:28.120096 db@janitor removing table-26 +10:30:28.120134 db@open done T·3.757482ms +=============== Jun 10, 2024 (UTC) =============== +11:11:44.693199 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.693313 version@stat F·[1 1] S·342KiB[19KiB 322KiB] Sc·[0.25 0.00] +11:11:44.693325 db@open opening +11:11:44.693356 journal@recovery F·1 +11:11:44.693437 journal@recovery recovering @31 +11:11:44.694528 memdb@flush created L0@33 N·300 S·46KiB "BH:..2f2,v2400":"blo..ore,v2187" +11:11:44.694644 version@stat F·[2 1] S·388KiB[65KiB 322KiB] Sc·[0.50 0.00] +11:11:44.697251 db@janitor F·5 G·0 +11:11:44.697263 db@open done T·3.934983ms +11:13:15.541536 table@compaction L0·2 -> L1·1 S·388KiB Q·2590 +11:13:15.544998 table@build created L1@36 N·2061 S·388KiB "BH:..c56,v1346":"blo..ore,v2481" +11:13:15.545019 version@stat F·[0 1] S·388KiB[0B 388KiB] Sc·[0.00 0.00] +11:13:15.545611 table@compaction committed F-2 S-832B Ke·0 D·70 T·4.050233ms +11:13:15.545675 table@remove removed @30 +11:13:15.545763 table@remove removed @29 +=============== Jun 10, 2024 (UTC) =============== +11:15:00.477161 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.477270 version@stat F·[0 1] S·388KiB[0B 388KiB] Sc·[0.00 0.00] +11:15:00.477284 db@open opening +11:15:00.477314 journal@recovery F·1 +11:15:00.477397 journal@recovery recovering @34 +11:15:00.478341 memdb@flush created L0@37 N·144 S·31KiB "BH:..2ce,v2497":"blo..ore,v2488" +11:15:00.478455 version@stat F·[1 1] S·419KiB[31KiB 388KiB] Sc·[0.25 0.00] +11:15:00.480996 db@janitor F·5 G·1 +11:15:00.481003 db@janitor removing table-33 +11:15:00.481043 db@open done T·3.754491ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.303989 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.304092 version@stat F·[1 1] S·419KiB[31KiB 388KiB] Sc·[0.25 0.00] +11:21:08.304103 db@open opening +11:21:08.304132 journal@recovery F·1 +11:21:08.304223 journal@recovery recovering @38 +11:21:08.304359 version@stat F·[1 1] S·419KiB[31KiB 388KiB] Sc·[0.25 0.00] +11:21:08.306977 db@janitor F·4 G·0 +11:21:08.306988 db@open done T·2.880913ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.244211 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.244337 version@stat F·[1 1] S·419KiB[31KiB 388KiB] Sc·[0.25 0.00] +11:22:28.244353 db@open opening +11:22:28.244391 journal@recovery F·1 +11:22:28.244497 journal@recovery recovering @40 +11:22:28.245313 memdb@flush created L0@42 N·6 S·953B "BH:..2f0,v2630":"blo..ore,v2633" +11:22:28.245443 version@stat F·[2 1] S·420KiB[31KiB 388KiB] Sc·[0.50 0.00] +11:22:28.247973 db@janitor F·5 G·0 +11:22:28.247983 db@open done T·3.62546ms +=============== Jun 10, 2024 (UTC) =============== +11:23:15.336431 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.336541 version@stat F·[2 1] S·420KiB[31KiB 388KiB] Sc·[0.50 0.00] +11:23:15.336553 db@open opening +11:23:15.336583 journal@recovery F·1 +11:23:15.336660 journal@recovery recovering @43 +11:23:15.337523 memdb@flush created L0@45 N·30 S·3KiB "BH:..cbb,v2643":"blo..ore,v2640" +11:23:15.337641 version@stat F·[3 1] S·423KiB[35KiB 388KiB] Sc·[0.75 0.00] +11:23:15.340336 db@janitor F·6 G·0 +11:23:15.340348 db@open done T·3.790991ms +=============== Jun 10, 2024 (UTC) =============== +11:29:08.705083 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.705174 version@stat F·[3 1] S·423KiB[35KiB 388KiB] Sc·[0.75 0.00] +11:29:08.705194 db@open opening +11:29:08.705235 journal@recovery F·1 +11:29:08.705313 journal@recovery recovering @46 +11:29:08.706537 memdb@flush created L0@48 N·396 S·61KiB "BH:..1b8,v2968":"blo..ore,v2671" +11:29:08.706659 version@stat F·[4 1] S·485KiB[97KiB 388KiB] Sc·[1.00 0.00] +11:29:08.709326 db@janitor F·7 G·0 +11:29:08.709338 db@open done T·4.136295ms +11:29:08.709366 table@compaction L0·4 -> L1·1 S·485KiB Q·3062 +11:29:08.713655 table@build created L1@51 N·2541 S·486KiB "BH:..c56,v1346":"blo..ore,v3061" +11:29:08.713678 version@stat F·[0 1] S·486KiB[0B 486KiB] Sc·[0.00 0.00] +11:29:08.715230 table@compaction committed F-4 S+616B Ke·0 D·96 T·5.842169ms +11:29:08.715295 table@remove removed @45 +11:29:08.715332 table@remove removed @42 +11:29:08.715447 table@remove removed @37 +11:29:08.715550 table@remove removed @36 +=============== Jun 10, 2024 (UTC) =============== +11:30:42.383930 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.384026 version@stat F·[0 1] S·486KiB[0B 486KiB] Sc·[0.00 0.00] +11:30:42.384039 db@open opening +11:30:42.384068 journal@recovery F·1 +11:30:42.384145 journal@recovery recovering @49 +11:30:42.385044 memdb@flush created L0@52 N·102 S·16KiB "BH:..c64,v3107":"blo..ore,v3068" +11:30:42.385164 version@stat F·[1 1] S·502KiB[16KiB 486KiB] Sc·[0.25 0.00] +11:30:42.387750 db@janitor F·5 G·1 +11:30:42.387758 db@janitor removing table-48 +11:30:42.387801 db@open done T·3.759432ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.643138 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.643248 version@stat F·[1 1] S·502KiB[16KiB 486KiB] Sc·[0.25 0.00] +11:30:55.643264 db@open opening +11:30:55.643296 journal@recovery F·1 +11:30:55.643389 journal@recovery recovering @53 +11:30:55.644261 memdb@flush created L0@55 N·6 S·961B "BH:..031,v3168":"blo..ore,v3171" +11:30:55.644377 version@stat F·[2 1] S·503KiB[17KiB 486KiB] Sc·[0.50 0.00] +11:30:55.647123 db@janitor F·5 G·0 +11:30:55.647137 db@open done T·3.868243ms +=============== Jun 10, 2024 (UTC) =============== +11:36:36.169004 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.169220 version@stat F·[2 1] S·503KiB[17KiB 486KiB] Sc·[0.50 0.00] +11:36:36.169245 db@open opening +11:36:36.169298 journal@recovery F·1 +11:36:36.169422 journal@recovery recovering @56 +11:36:36.170654 memdb@flush created L0@58 N·360 S·56KiB "BH:..acb,v3439":"blo..ore,v3178" +11:36:36.171816 version@stat F·[3 1] S·560KiB[74KiB 486KiB] Sc·[0.75 0.00] +11:36:36.174552 db@janitor F·6 G·0 +11:36:36.174567 db@open done T·5.315235ms +=============== Jun 10, 2024 (UTC) =============== +12:01:05.060492 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:05.060613 version@stat F·[3 1] S·560KiB[74KiB 486KiB] Sc·[0.75 0.00] +12:01:05.060632 db@open opening +12:01:05.060677 journal@recovery F·1 +12:01:05.060769 journal@recovery recovering @59 +12:01:05.061925 memdb@flush created L0@61 N·300 S·46KiB "BH:..fe3,v3794":"blo..ore,v3539" +12:01:05.062094 version@stat F·[4 1] S·607KiB[120KiB 486KiB] Sc·[1.00 0.00] +12:01:05.065257 db@janitor F·7 G·0 +12:01:05.065272 db@open done T·4.633571ms +12:01:05.065310 table@compaction L0·4 -> L1·1 S·607KiB Q·3834 +12:01:05.070497 table@build created L1@64 N·3181 S·605KiB "BH:..c56,v1346":"blo..ore,v3833" +12:01:05.070521 version@stat F·[0 1] S·605KiB[0B 605KiB] Sc·[0.00 0.01] +12:01:05.071073 table@compaction committed F-4 S-2KiB Ke·0 D·128 T·5.740311ms +12:01:05.071142 table@remove removed @58 +12:01:05.071177 table@remove removed @55 +12:01:05.071219 table@remove removed @52 +12:01:05.071335 table@remove removed @51 +=============== Jun 10, 2024 (UTC) =============== +12:03:00.046263 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:00.046410 version@stat F·[0 1] S·605KiB[0B 605KiB] Sc·[0.00 0.01] +12:03:00.046426 db@open opening +12:03:00.046461 journal@recovery F·1 +12:03:00.046563 journal@recovery recovering @62 +12:03:00.047387 memdb@flush created L0@65 N·18 S·2KiB "BH:..3b0,v3843":"blo..ore,v3840" +12:03:00.047524 version@stat F·[1 1] S·607KiB[2KiB 605KiB] Sc·[0.25 0.01] +12:03:00.050072 db@janitor F·5 G·1 +12:03:00.050081 db@janitor removing table-61 +12:03:00.050125 db@open done T·3.693892ms +=============== Jun 10, 2024 (UTC) =============== +12:03:19.754783 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.754881 version@stat F·[1 1] S·607KiB[2KiB 605KiB] Sc·[0.25 0.01] +12:03:19.754895 db@open opening +12:03:19.754926 journal@recovery F·1 +12:03:19.755014 journal@recovery recovering @66 +12:03:19.755813 memdb@flush created L0@68 N·12 S·1KiB "BH:..067,v3862":"blo..ore,v3859" +12:03:19.755990 version@stat F·[2 1] S·608KiB[3KiB 605KiB] Sc·[0.50 0.01] +12:03:19.759200 db@janitor F·5 G·0 +12:03:19.759219 db@open done T·4.318108ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.847650 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.847761 version@stat F·[2 1] S·608KiB[3KiB 605KiB] Sc·[0.50 0.01] +12:04:23.847777 db@open opening +12:04:23.847810 journal@recovery F·1 +12:04:23.848051 journal@recovery recovering @69 +12:04:23.849318 memdb@flush created L0@71 N·6 S·987B "BH:..960,v3869":"blo..ore,v3872" +12:04:23.849590 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:04:23.852801 db@janitor F·6 G·0 +12:04:23.852816 db@open done T·5.034254ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.929784 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.929882 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:07:46.929895 db@open opening +12:07:46.929928 journal@recovery F·1 +12:07:46.931774 journal@recovery recovering @72 +12:07:46.931913 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:07:46.935410 db@janitor F·6 G·0 +12:07:46.935423 db@open done T·5.523909ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.153961 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.154072 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:08:02.154087 db@open opening +12:08:02.154122 journal@recovery F·1 +12:08:02.154210 journal@recovery recovering @74 +12:08:02.154367 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:08:02.157005 db@janitor F·6 G·0 +12:08:02.157017 db@open done T·2.924626ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.480718 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.480823 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:27:21.480837 db@open opening +12:27:21.480873 journal@recovery F·1 +12:27:21.480960 journal@recovery recovering @76 +12:27:21.481104 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:27:21.485083 db@janitor F·6 G·0 +12:27:21.485095 db@open done T·4.253597ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.851141 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.851239 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:56:43.851254 db@open opening +12:56:43.851286 journal@recovery F·1 +12:56:43.851381 journal@recovery recovering @78 +12:56:43.851571 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:56:43.855100 db@janitor F·6 G·0 +12:56:43.855112 db@open done T·3.853263ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.425919 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.426025 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:57:38.426040 db@open opening +12:57:38.426072 journal@recovery F·1 +12:57:38.426161 journal@recovery recovering @80 +12:57:38.426313 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:57:38.428964 db@janitor F·6 G·0 +12:57:38.428977 db@open done T·2.931815ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.520107 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.520223 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:58:01.520238 db@open opening +12:58:01.520276 journal@recovery F·1 +12:58:01.520358 journal@recovery recovering @82 +12:58:01.520492 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:58:01.523157 db@janitor F·6 G·0 +12:58:01.523170 db@open done T·2.926926ms +=============== Jun 10, 2024 (UTC) =============== +12:58:49.040153 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:49.040284 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:58:49.040302 db@open opening +12:58:49.040337 journal@recovery F·1 +12:58:49.040426 journal@recovery recovering @84 +12:58:49.040585 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:58:49.043662 db@janitor F·6 G·0 +12:58:49.043680 db@open done T·3.373349ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.178695 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.178791 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:59:05.178805 db@open opening +12:59:05.178844 journal@recovery F·1 +12:59:05.178937 journal@recovery recovering @86 +12:59:05.179077 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:59:05.182753 db@janitor F·6 G·0 +12:59:05.182765 db@open done T·3.954894ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.457946 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.458049 version@stat F·[3 1] S·609KiB[4KiB 605KiB] Sc·[0.75 0.01] +12:59:32.458062 db@open opening +12:59:32.458095 journal@recovery F·1 +12:59:32.458194 journal@recovery recovering @88 +12:59:32.458994 memdb@flush created L0@90 N·6 S·969B "BH:..67e,v3876":"blo..ore,v3879" +12:59:32.459122 version@stat F·[4 1] S·610KiB[5KiB 605KiB] Sc·[1.00 0.01] +12:59:32.461936 db@janitor F·7 G·0 +12:59:32.461950 db@open done T·3.882674ms +12:59:32.461975 table@compaction L0·4 -> L1·1 S·610KiB Q·3880 +12:59:32.466693 table@build created L1@93 N·3216 S·611KiB "BH:..c56,v1346":"blo..ore,v3879" +12:59:32.466712 version@stat F·[0 1] S·611KiB[0B 611KiB] Sc·[0.00 0.01] +12:59:32.469342 table@compaction committed F-4 S+1KiB Ke·0 D·7 T·7.345334ms +12:59:32.469407 table@remove removed @71 +12:59:32.469441 table@remove removed @68 +12:59:32.469472 table@remove removed @65 +12:59:32.469610 table@remove removed @64 +=============== Jul 3, 2024 (UTC) =============== +17:52:58.389900 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.390016 version@stat F·[0 1] S·611KiB[0B 611KiB] Sc·[0.00 0.01] +17:52:58.390030 db@open opening +17:52:58.390060 journal@recovery F·1 +17:52:58.390141 journal@recovery recovering @91 +17:52:58.390270 version@stat F·[0 1] S·611KiB[0B 611KiB] Sc·[0.00 0.01] +17:52:58.392841 db@janitor F·4 G·1 +17:52:58.392849 db@janitor removing table-90 +17:52:58.392880 db@open done T·2.845504ms +=============== Jul 3, 2024 (UTC) =============== +17:59:01.756021 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.756195 version@stat F·[0 1] S·611KiB[0B 611KiB] Sc·[0.00 0.01] +17:59:01.756211 db@open opening +17:59:01.756249 journal@recovery F·1 +17:59:01.756409 journal@recovery recovering @94 +17:59:01.758513 memdb@flush created L0@96 N·414 S·72KiB "BH:..19a,v4003":"blo..ore,v3886" +17:59:01.758668 version@stat F·[1 1] S·684KiB[72KiB 611KiB] Sc·[0.25 0.01] +17:59:01.761454 db@janitor F·4 G·0 +17:59:01.761470 db@open done T·5.254513ms diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/MANIFEST-000098 b/.docker/container-state/atom-testnet-data/data/blockstore.db/MANIFEST-000098 new file mode 100644 index 0000000000..a7d73d0ef2 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/blockstore.db/MANIFEST-000098 differ diff --git a/.docker/container-state/atom-testnet-data/data/cs.wal/wal b/.docker/container-state/atom-testnet-data/data/cs.wal/wal new file mode 100644 index 0000000000..61a997fc44 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/cs.wal/wal differ diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/000068.log b/.docker/container-state/atom-testnet-data/data/evidence.db/000068.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT b/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT new file mode 100644 index 0000000000..5893b8f83b --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000069 diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT.bak new file mode 100644 index 0000000000..0094dacbb8 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000067 diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/LOCK b/.docker/container-state/atom-testnet-data/data/evidence.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/LOG b/.docker/container-state/atom-testnet-data/data/evidence.db/LOG new file mode 100644 index 0000000000..5ab563f5bc --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/evidence.db/LOG @@ -0,0 +1,312 @@ +=============== Jun 3, 2024 (+03) =============== +14:44:32.509654 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:44:32.511481 db@open opening +14:44:32.511607 version@stat F·[] S·0B[] Sc·[] +14:44:32.512206 db@janitor F·2 G·0 +14:44:32.512222 db@open done T·734.856µs +=============== Jun 6, 2024 (+03) =============== +18:04:49.080272 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:04:49.080340 version@stat F·[] S·0B[] Sc·[] +18:04:49.080350 db@open opening +18:04:49.080377 journal@recovery F·1 +18:04:49.080469 journal@recovery recovering @1 +18:04:49.080588 version@stat F·[] S·0B[] Sc·[] +18:04:49.083092 db@janitor F·2 G·0 +18:04:49.083102 db@open done T·2.748954ms +=============== Jun 6, 2024 (+03) =============== +18:09:26.436024 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:09:26.436087 version@stat F·[] S·0B[] Sc·[] +18:09:26.436097 db@open opening +18:09:26.436125 journal@recovery F·1 +18:09:26.436219 journal@recovery recovering @2 +18:09:26.436339 version@stat F·[] S·0B[] Sc·[] +18:09:26.440379 db@janitor F·2 G·0 +18:09:26.440390 db@open done T·4.288777ms +=============== Jun 6, 2024 (+03) =============== +19:18:28.712802 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:18:28.712862 version@stat F·[] S·0B[] Sc·[] +19:18:28.712871 db@open opening +19:18:28.712895 journal@recovery F·1 +19:18:28.712989 journal@recovery recovering @4 +19:18:28.713116 version@stat F·[] S·0B[] Sc·[] +19:18:28.715549 db@janitor F·2 G·0 +19:18:28.715559 db@open done T·2.685744ms +=============== Jun 6, 2024 (+03) =============== +19:29:57.954497 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:29:57.954568 version@stat F·[] S·0B[] Sc·[] +19:29:57.954580 db@open opening +19:29:57.954621 journal@recovery F·1 +19:29:57.954732 journal@recovery recovering @6 +19:29:57.954867 version@stat F·[] S·0B[] Sc·[] +19:29:57.958415 db@janitor F·2 G·0 +19:29:57.958429 db@open done T·3.844423ms +=============== Jun 10, 2024 (UTC) =============== +10:09:48.953791 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:48.953853 version@stat F·[] S·0B[] Sc·[] +10:09:48.953863 db@open opening +10:09:48.953887 journal@recovery F·1 +10:09:48.953967 journal@recovery recovering @8 +10:09:48.954082 version@stat F·[] S·0B[] Sc·[] +10:09:48.956576 db@janitor F·2 G·0 +10:09:48.956585 db@open done T·2.718252ms +=============== Jun 10, 2024 (UTC) =============== +10:19:47.937791 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.937852 version@stat F·[] S·0B[] Sc·[] +10:19:47.937862 db@open opening +10:19:47.937889 journal@recovery F·1 +10:19:47.937963 journal@recovery recovering @10 +10:19:47.938077 version@stat F·[] S·0B[] Sc·[] +10:19:47.940744 db@janitor F·2 G·0 +10:19:47.940754 db@open done T·2.889024ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.788441 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.788536 version@stat F·[] S·0B[] Sc·[] +10:20:11.788548 db@open opening +10:20:11.788589 journal@recovery F·1 +10:20:11.788670 journal@recovery recovering @12 +10:20:11.788927 version@stat F·[] S·0B[] Sc·[] +10:20:11.791609 db@janitor F·2 G·0 +10:20:11.791619 db@open done T·3.067276ms +=============== Jun 10, 2024 (UTC) =============== +10:23:28.338513 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.338581 version@stat F·[] S·0B[] Sc·[] +10:23:28.338593 db@open opening +10:23:28.338623 journal@recovery F·1 +10:23:28.338701 journal@recovery recovering @14 +10:23:28.338821 version@stat F·[] S·0B[] Sc·[] +10:23:28.341447 db@janitor F·2 G·0 +10:23:28.341457 db@open done T·2.859444ms +=============== Jun 10, 2024 (UTC) =============== +10:27:08.517200 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.517262 version@stat F·[] S·0B[] Sc·[] +10:27:08.517272 db@open opening +10:27:08.517297 journal@recovery F·1 +10:27:08.517572 journal@recovery recovering @16 +10:27:08.520411 version@stat F·[] S·0B[] Sc·[] +10:27:08.524138 db@janitor F·2 G·0 +10:27:08.524148 db@open done T·6.872488ms +=============== Jun 10, 2024 (UTC) =============== +10:30:28.131018 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.131081 version@stat F·[] S·0B[] Sc·[] +10:30:28.131091 db@open opening +10:30:28.131116 journal@recovery F·1 +10:30:28.132992 journal@recovery recovering @18 +10:30:28.133111 version@stat F·[] S·0B[] Sc·[] +10:30:28.135680 db@janitor F·2 G·0 +10:30:28.135692 db@open done T·4.597388ms +=============== Jun 10, 2024 (UTC) =============== +11:11:44.709294 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.709384 version@stat F·[] S·0B[] Sc·[] +11:11:44.709399 db@open opening +11:11:44.709429 journal@recovery F·1 +11:11:44.709514 journal@recovery recovering @20 +11:11:44.709646 version@stat F·[] S·0B[] Sc·[] +11:11:44.712300 db@janitor F·2 G·0 +11:11:44.712313 db@open done T·2.909154ms +=============== Jun 10, 2024 (UTC) =============== +11:15:00.492747 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.492807 version@stat F·[] S·0B[] Sc·[] +11:15:00.492817 db@open opening +11:15:00.492844 journal@recovery F·1 +11:15:00.494716 journal@recovery recovering @22 +11:15:00.494843 version@stat F·[] S·0B[] Sc·[] +11:15:00.497465 db@janitor F·2 G·0 +11:15:00.497477 db@open done T·4.656458ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.314888 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.314951 version@stat F·[] S·0B[] Sc·[] +11:21:08.314961 db@open opening +11:21:08.314989 journal@recovery F·1 +11:21:08.316843 journal@recovery recovering @24 +11:21:08.318743 version@stat F·[] S·0B[] Sc·[] +11:21:08.321282 db@janitor F·2 G·0 +11:21:08.321291 db@open done T·6.326252ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.256835 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.256941 version@stat F·[] S·0B[] Sc·[] +11:22:28.256955 db@open opening +11:22:28.256996 journal@recovery F·1 +11:22:28.259268 journal@recovery recovering @26 +11:22:28.261218 version@stat F·[] S·0B[] Sc·[] +11:22:28.265181 db@janitor F·2 G·0 +11:22:28.265201 db@open done T·8.241029ms +=============== Jun 10, 2024 (UTC) =============== +11:23:15.352058 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.352127 version@stat F·[] S·0B[] Sc·[] +11:23:15.352138 db@open opening +11:23:15.352170 journal@recovery F·1 +11:23:15.353995 journal@recovery recovering @28 +11:23:15.354128 version@stat F·[] S·0B[] Sc·[] +11:23:15.356631 db@janitor F·2 G·0 +11:23:15.356643 db@open done T·4.500648ms +=============== Jun 10, 2024 (UTC) =============== +11:29:08.722698 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.722765 version@stat F·[] S·0B[] Sc·[] +11:29:08.722776 db@open opening +11:29:08.722805 journal@recovery F·1 +11:29:08.722880 journal@recovery recovering @30 +11:29:08.723005 version@stat F·[] S·0B[] Sc·[] +11:29:08.725629 db@janitor F·2 G·0 +11:29:08.725646 db@open done T·2.865544ms +=============== Jun 10, 2024 (UTC) =============== +11:30:42.405075 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.405179 version@stat F·[] S·0B[] Sc·[] +11:30:42.405200 db@open opening +11:30:42.405235 journal@recovery F·1 +11:30:42.405312 journal@recovery recovering @32 +11:30:42.405442 version@stat F·[] S·0B[] Sc·[] +11:30:42.408071 db@janitor F·2 G·0 +11:30:42.408085 db@open done T·2.879154ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.658003 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.658072 version@stat F·[] S·0B[] Sc·[] +11:30:55.658084 db@open opening +11:30:55.658111 journal@recovery F·1 +11:30:55.658199 journal@recovery recovering @34 +11:30:55.658334 version@stat F·[] S·0B[] Sc·[] +11:30:55.662651 db@janitor F·2 G·0 +11:30:55.662663 db@open done T·4.574568ms +=============== Jun 10, 2024 (UTC) =============== +11:36:36.186291 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.186359 version@stat F·[] S·0B[] Sc·[] +11:36:36.186371 db@open opening +11:36:36.186398 journal@recovery F·1 +11:36:36.186474 journal@recovery recovering @36 +11:36:36.186679 version@stat F·[] S·0B[] Sc·[] +11:36:36.189757 db@janitor F·2 G·0 +11:36:36.189773 db@open done T·3.397518ms +=============== Jun 10, 2024 (UTC) =============== +12:01:05.075088 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:05.075156 version@stat F·[] S·0B[] Sc·[] +12:01:05.075168 db@open opening +12:01:05.075206 journal@recovery F·1 +12:01:05.075344 journal@recovery recovering @38 +12:01:05.076359 version@stat F·[] S·0B[] Sc·[] +12:01:05.078935 db@janitor F·2 G·0 +12:01:05.078946 db@open done T·3.773483ms +=============== Jun 10, 2024 (UTC) =============== +12:03:00.061063 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:00.061170 version@stat F·[] S·0B[] Sc·[] +12:03:00.061191 db@open opening +12:03:00.061236 journal@recovery F·1 +12:03:00.063367 journal@recovery recovering @40 +12:03:00.063504 version@stat F·[] S·0B[] Sc·[] +12:03:00.066048 db@janitor F·2 G·0 +12:03:00.066060 db@open done T·4.861773ms +=============== Jun 10, 2024 (UTC) =============== +12:03:19.769218 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.769288 version@stat F·[] S·0B[] Sc·[] +12:03:19.769300 db@open opening +12:03:19.769329 journal@recovery F·1 +12:03:19.769683 journal@recovery recovering @42 +12:03:19.769952 version@stat F·[] S·0B[] Sc·[] +12:03:19.773238 db@janitor F·2 G·0 +12:03:19.773252 db@open done T·3.947185ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.863959 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.864027 version@stat F·[] S·0B[] Sc·[] +12:04:23.864040 db@open opening +12:04:23.864070 journal@recovery F·1 +12:04:23.865852 journal@recovery recovering @44 +12:04:23.867851 version@stat F·[] S·0B[] Sc·[] +12:04:23.870459 db@janitor F·2 G·0 +12:04:23.870470 db@open done T·6.425726ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.944645 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.944731 version@stat F·[] S·0B[] Sc·[] +12:07:46.944746 db@open opening +12:07:46.944789 journal@recovery F·1 +12:07:46.944888 journal@recovery recovering @46 +12:07:46.945043 version@stat F·[] S·0B[] Sc·[] +12:07:46.947664 db@janitor F·2 G·0 +12:07:46.947679 db@open done T·2.927556ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.165399 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.165472 version@stat F·[] S·0B[] Sc·[] +12:08:02.165483 db@open opening +12:08:02.165513 journal@recovery F·1 +12:08:02.167359 journal@recovery recovering @48 +12:08:02.169235 version@stat F·[] S·0B[] Sc·[] +12:08:02.171915 db@janitor F·2 G·0 +12:08:02.171939 db@open done T·6.450667ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.495936 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.496008 version@stat F·[] S·0B[] Sc·[] +12:27:21.496022 db@open opening +12:27:21.496051 journal@recovery F·1 +12:27:21.497870 journal@recovery recovering @50 +12:27:21.498007 version@stat F·[] S·0B[] Sc·[] +12:27:21.500619 db@janitor F·2 G·0 +12:27:21.500632 db@open done T·4.604ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.864647 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.864707 version@stat F·[] S·0B[] Sc·[] +12:56:43.864719 db@open opening +12:56:43.864747 journal@recovery F·1 +12:56:43.864972 journal@recovery recovering @52 +12:56:43.865922 version@stat F·[] S·0B[] Sc·[] +12:56:43.869073 db@janitor F·2 G·0 +12:56:43.869083 db@open done T·4.360008ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.438744 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.438806 version@stat F·[] S·0B[] Sc·[] +12:57:38.438818 db@open opening +12:57:38.438848 journal@recovery F·1 +12:57:38.438932 journal@recovery recovering @54 +12:57:38.439056 version@stat F·[] S·0B[] Sc·[] +12:57:38.441571 db@janitor F·2 G·0 +12:57:38.441582 db@open done T·2.760494ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.531422 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.531489 version@stat F·[] S·0B[] Sc·[] +12:58:01.531501 db@open opening +12:58:01.531529 journal@recovery F·1 +12:58:01.533438 journal@recovery recovering @56 +12:58:01.535416 version@stat F·[] S·0B[] Sc·[] +12:58:01.537931 db@janitor F·2 G·0 +12:58:01.537943 db@open done T·6.438126ms +=============== Jun 10, 2024 (UTC) =============== +12:58:49.052411 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:49.052487 version@stat F·[] S·0B[] Sc·[] +12:58:49.052500 db@open opening +12:58:49.052529 journal@recovery F·1 +12:58:49.054491 journal@recovery recovering @58 +12:58:49.056525 version@stat F·[] S·0B[] Sc·[] +12:58:49.059108 db@janitor F·2 G·0 +12:58:49.059123 db@open done T·6.619018ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.193570 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.193641 version@stat F·[] S·0B[] Sc·[] +12:59:05.193655 db@open opening +12:59:05.193683 journal@recovery F·1 +12:59:05.195651 journal@recovery recovering @60 +12:59:05.197669 version@stat F·[] S·0B[] Sc·[] +12:59:05.200219 db@janitor F·2 G·0 +12:59:05.200236 db@open done T·6.576927ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.475710 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.475804 version@stat F·[] S·0B[] Sc·[] +12:59:32.475819 db@open opening +12:59:32.475846 journal@recovery F·1 +12:59:32.478047 journal@recovery recovering @62 +12:59:32.478227 version@stat F·[] S·0B[] Sc·[] +12:59:32.481158 db@janitor F·2 G·0 +12:59:32.481169 db@open done T·5.346007ms +=============== Jul 3, 2024 (UTC) =============== +17:52:58.403602 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.403672 version@stat F·[] S·0B[] Sc·[] +17:52:58.403689 db@open opening +17:52:58.403719 journal@recovery F·1 +17:52:58.405639 journal@recovery recovering @64 +17:52:58.405780 version@stat F·[] S·0B[] Sc·[] +17:52:58.408336 db@janitor F·2 G·0 +17:52:58.408348 db@open done T·4.655669ms +=============== Jul 3, 2024 (UTC) =============== +17:59:01.773333 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.773412 version@stat F·[] S·0B[] Sc·[] +17:59:01.773427 db@open opening +17:59:01.773458 journal@recovery F·1 +17:59:01.773619 journal@recovery recovering @66 +17:59:01.774745 version@stat F·[] S·0B[] Sc·[] +17:59:01.777793 db@janitor F·2 G·0 +17:59:01.777805 db@open done T·4.373546ms diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/MANIFEST-000069 b/.docker/container-state/atom-testnet-data/data/evidence.db/MANIFEST-000069 new file mode 100644 index 0000000000..3833569575 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/evidence.db/MANIFEST-000069 differ diff --git a/.docker/container-state/atom-testnet-data/data/priv_validator_state.json b/.docker/container-state/atom-testnet-data/data/priv_validator_state.json new file mode 100644 index 0000000000..99546706e3 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/priv_validator_state.json @@ -0,0 +1,7 @@ +{ + "height": "717", + "round": 0, + "step": 3, + "signature": "XLwGIxJpiVU0AYl646G7zqXk0v/ihlfAwKP5BvHmMfy6UQCzeW/rjtuVoS+2i7KT0RVUfXc7dvGYOnpuAvSTAA==", + "signbytes": "76080211CD0200000000000022480A20911EABA0078B62D4B55F3B349F0719F53B0D39EEE4064EA626437B86D219A37F122408011220AB2248EBDDC8ACA32BCF90D60E19DA9B4A42F2DD6A8957FA3D7337C1AC73151A2A0C08FEA296B40610F7CFDA82033211636F736D6F736875622D746573746E6574" +} \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/000068.log b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/000068.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT new file mode 100644 index 0000000000..5893b8f83b --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000069 diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT.bak new file mode 100644 index 0000000000..0094dacbb8 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000067 diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/LOCK b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/LOG b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/LOG new file mode 100644 index 0000000000..2638185812 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/LOG @@ -0,0 +1,312 @@ +=============== Jun 3, 2024 (+03) =============== +14:44:32.475411 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:44:32.477260 db@open opening +14:44:32.477695 version@stat F·[] S·0B[] Sc·[] +14:44:32.478344 db@janitor F·2 G·0 +14:44:32.478355 db@open done T·1.087999ms +=============== Jun 6, 2024 (+03) =============== +18:04:49.043230 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:04:49.043300 version@stat F·[] S·0B[] Sc·[] +18:04:49.043310 db@open opening +18:04:49.043338 journal@recovery F·1 +18:04:49.043415 journal@recovery recovering @1 +18:04:49.043536 version@stat F·[] S·0B[] Sc·[] +18:04:49.046065 db@janitor F·2 G·0 +18:04:49.046074 db@open done T·2.760574ms +=============== Jun 6, 2024 (+03) =============== +18:09:26.397237 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:09:26.397298 version@stat F·[] S·0B[] Sc·[] +18:09:26.397308 db@open opening +18:09:26.397338 journal@recovery F·1 +18:09:26.397413 journal@recovery recovering @2 +18:09:26.397529 version@stat F·[] S·0B[] Sc·[] +18:09:26.400092 db@janitor F·2 G·0 +18:09:26.400102 db@open done T·2.789214ms +=============== Jun 6, 2024 (+03) =============== +19:18:28.680139 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:18:28.680197 version@stat F·[] S·0B[] Sc·[] +19:18:28.680205 db@open opening +19:18:28.680229 journal@recovery F·1 +19:18:28.680307 journal@recovery recovering @4 +19:18:28.680423 version@stat F·[] S·0B[] Sc·[] +19:18:28.682861 db@janitor F·2 G·0 +19:18:28.682869 db@open done T·2.661124ms +=============== Jun 6, 2024 (+03) =============== +19:29:57.920102 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:29:57.920159 version@stat F·[] S·0B[] Sc·[] +19:29:57.920167 db@open opening +19:29:57.920190 journal@recovery F·1 +19:29:57.922094 journal@recovery recovering @6 +19:29:57.922230 version@stat F·[] S·0B[] Sc·[] +19:29:57.924641 db@janitor F·2 G·0 +19:29:57.924650 db@open done T·4.480109ms +=============== Jun 10, 2024 (UTC) =============== +10:09:48.896199 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:48.896267 version@stat F·[] S·0B[] Sc·[] +10:09:48.896277 db@open opening +10:09:48.896314 journal@recovery F·1 +10:09:48.896391 journal@recovery recovering @8 +10:09:48.896519 version@stat F·[] S·0B[] Sc·[] +10:09:48.899646 db@janitor F·2 G·0 +10:09:48.899653 db@open done T·3.372298ms +=============== Jun 10, 2024 (UTC) =============== +10:19:47.888266 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.888339 version@stat F·[] S·0B[] Sc·[] +10:19:47.888350 db@open opening +10:19:47.888377 journal@recovery F·1 +10:19:47.888455 journal@recovery recovering @10 +10:19:47.888724 version@stat F·[] S·0B[] Sc·[] +10:19:47.891797 db@janitor F·2 G·0 +10:19:47.891807 db@open done T·3.453589ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.744814 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.744888 version@stat F·[] S·0B[] Sc·[] +10:20:11.744899 db@open opening +10:20:11.744930 journal@recovery F·1 +10:20:11.746835 journal@recovery recovering @12 +10:20:11.746968 version@stat F·[] S·0B[] Sc·[] +10:20:11.749659 db@janitor F·2 G·0 +10:20:11.749670 db@open done T·4.76632ms +=============== Jun 10, 2024 (UTC) =============== +10:23:28.290887 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.290956 version@stat F·[] S·0B[] Sc·[] +10:23:28.290967 db@open opening +10:23:28.290992 journal@recovery F·1 +10:23:28.291072 journal@recovery recovering @14 +10:23:28.291223 version@stat F·[] S·0B[] Sc·[] +10:23:28.293997 db@janitor F·2 G·0 +10:23:28.294009 db@open done T·3.037896ms +=============== Jun 10, 2024 (UTC) =============== +10:27:08.467062 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.467128 version@stat F·[] S·0B[] Sc·[] +10:27:08.467139 db@open opening +10:27:08.467165 journal@recovery F·1 +10:27:08.467379 journal@recovery recovering @16 +10:27:08.469369 version@stat F·[] S·0B[] Sc·[] +10:27:08.472017 db@janitor F·2 G·0 +10:27:08.472029 db@open done T·4.885871ms +=============== Jun 10, 2024 (UTC) =============== +10:30:28.082344 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.082423 version@stat F·[] S·0B[] Sc·[] +10:30:28.082434 db@open opening +10:30:28.082460 journal@recovery F·1 +10:30:28.084262 journal@recovery recovering @18 +10:30:28.084397 version@stat F·[] S·0B[] Sc·[] +10:30:28.086961 db@janitor F·2 G·0 +10:30:28.086969 db@open done T·4.531538ms +=============== Jun 10, 2024 (UTC) =============== +11:11:44.658330 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.658400 version@stat F·[] S·0B[] Sc·[] +11:11:44.658414 db@open opening +11:11:44.658441 journal@recovery F·1 +11:11:44.658519 journal@recovery recovering @20 +11:11:44.658644 version@stat F·[] S·0B[] Sc·[] +11:11:44.661228 db@janitor F·2 G·0 +11:11:44.661238 db@open done T·2.820283ms +=============== Jun 10, 2024 (UTC) =============== +11:15:00.439924 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.439987 version@stat F·[] S·0B[] Sc·[] +11:15:00.440001 db@open opening +11:15:00.440029 journal@recovery F·1 +11:15:00.441685 journal@recovery recovering @22 +11:15:00.443588 version@stat F·[] S·0B[] Sc·[] +11:15:00.446135 db@janitor F·2 G·0 +11:15:00.446146 db@open done T·6.140941ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.268198 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.268266 version@stat F·[] S·0B[] Sc·[] +11:21:08.268277 db@open opening +11:21:08.268302 journal@recovery F·1 +11:21:08.270129 journal@recovery recovering @24 +11:21:08.270268 version@stat F·[] S·0B[] Sc·[] +11:21:08.272787 db@janitor F·2 G·0 +11:21:08.272796 db@open done T·4.515027ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.205329 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.205394 version@stat F·[] S·0B[] Sc·[] +11:22:28.205403 db@open opening +11:22:28.205430 journal@recovery F·1 +11:22:28.207319 journal@recovery recovering @26 +11:22:28.207454 version@stat F·[] S·0B[] Sc·[] +11:22:28.210837 db@janitor F·2 G·0 +11:22:28.210847 db@open done T·5.440115ms +=============== Jun 10, 2024 (UTC) =============== +11:23:15.300491 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.300554 version@stat F·[] S·0B[] Sc·[] +11:23:15.300568 db@open opening +11:23:15.300594 journal@recovery F·1 +11:23:15.302393 journal@recovery recovering @28 +11:23:15.302518 version@stat F·[] S·0B[] Sc·[] +11:23:15.305063 db@janitor F·2 G·0 +11:23:15.305073 db@open done T·4.501347ms +=============== Jun 10, 2024 (UTC) =============== +11:29:08.669833 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.669907 version@stat F·[] S·0B[] Sc·[] +11:29:08.669919 db@open opening +11:29:08.669948 journal@recovery F·1 +11:29:08.670026 journal@recovery recovering @30 +11:29:08.670336 version@stat F·[] S·0B[] Sc·[] +11:29:08.673082 db@janitor F·2 G·0 +11:29:08.673099 db@open done T·3.175877ms +=============== Jun 10, 2024 (UTC) =============== +11:30:42.347315 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.347389 version@stat F·[] S·0B[] Sc·[] +11:30:42.347404 db@open opening +11:30:42.347434 journal@recovery F·1 +11:30:42.347509 journal@recovery recovering @32 +11:30:42.347634 version@stat F·[] S·0B[] Sc·[] +11:30:42.350309 db@janitor F·2 G·0 +11:30:42.350319 db@open done T·2.910614ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.606170 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.606265 version@stat F·[] S·0B[] Sc·[] +11:30:55.606281 db@open opening +11:30:55.606319 journal@recovery F·1 +11:30:55.606421 journal@recovery recovering @34 +11:30:55.606576 version@stat F·[] S·0B[] Sc·[] +11:30:55.609590 db@janitor F·2 G·0 +11:30:55.609602 db@open done T·3.316178ms +=============== Jun 10, 2024 (UTC) =============== +11:36:36.132677 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.132767 version@stat F·[] S·0B[] Sc·[] +11:36:36.132781 db@open opening +11:36:36.132811 journal@recovery F·1 +11:36:36.132895 journal@recovery recovering @36 +11:36:36.133060 version@stat F·[] S·0B[] Sc·[] +11:36:36.135669 db@janitor F·2 G·0 +11:36:36.135678 db@open done T·2.891694ms +=============== Jun 10, 2024 (UTC) =============== +12:01:05.022802 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:05.022873 version@stat F·[] S·0B[] Sc·[] +12:01:05.022887 db@open opening +12:01:05.022914 journal@recovery F·1 +12:01:05.022991 journal@recovery recovering @38 +12:01:05.023111 version@stat F·[] S·0B[] Sc·[] +12:01:05.025774 db@janitor F·2 G·0 +12:01:05.025784 db@open done T·2.892525ms +=============== Jun 10, 2024 (UTC) =============== +12:03:00.007287 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:00.007362 version@stat F·[] S·0B[] Sc·[] +12:03:00.007374 db@open opening +12:03:00.007403 journal@recovery F·1 +12:03:00.009173 journal@recovery recovering @40 +12:03:00.009326 version@stat F·[] S·0B[] Sc·[] +12:03:00.011929 db@janitor F·2 G·0 +12:03:00.011941 db@open done T·4.56198ms +=============== Jun 10, 2024 (UTC) =============== +12:03:19.717291 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.717361 version@stat F·[] S·0B[] Sc·[] +12:03:19.717373 db@open opening +12:03:19.717401 journal@recovery F·1 +12:03:19.717484 journal@recovery recovering @42 +12:03:19.717611 version@stat F·[] S·0B[] Sc·[] +12:03:19.720112 db@janitor F·2 G·0 +12:03:19.720123 db@open done T·2.746074ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.809510 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.809653 version@stat F·[] S·0B[] Sc·[] +12:04:23.809666 db@open opening +12:04:23.809697 journal@recovery F·1 +12:04:23.809785 journal@recovery recovering @44 +12:04:23.809927 version@stat F·[] S·0B[] Sc·[] +12:04:23.812577 db@janitor F·2 G·0 +12:04:23.812591 db@open done T·2.919426ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.892377 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.892460 version@stat F·[] S·0B[] Sc·[] +12:07:46.892473 db@open opening +12:07:46.892501 journal@recovery F·1 +12:07:46.892581 journal@recovery recovering @46 +12:07:46.892712 version@stat F·[] S·0B[] Sc·[] +12:07:46.895287 db@janitor F·2 G·0 +12:07:46.895300 db@open done T·2.822145ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.113224 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.113300 version@stat F·[] S·0B[] Sc·[] +12:08:02.113310 db@open opening +12:08:02.113335 journal@recovery F·1 +12:08:02.115124 journal@recovery recovering @48 +12:08:02.115264 version@stat F·[] S·0B[] Sc·[] +12:08:02.117935 db@janitor F·2 G·0 +12:08:02.117944 db@open done T·4.630711ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.439818 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.439895 version@stat F·[] S·0B[] Sc·[] +12:27:21.439908 db@open opening +12:27:21.439934 journal@recovery F·1 +12:27:21.441724 journal@recovery recovering @50 +12:27:21.441856 version@stat F·[] S·0B[] Sc·[] +12:27:21.444635 db@janitor F·2 G·0 +12:27:21.444647 db@open done T·4.734541ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.813189 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.813261 version@stat F·[] S·0B[] Sc·[] +12:56:43.813274 db@open opening +12:56:43.813301 journal@recovery F·1 +12:56:43.813390 journal@recovery recovering @52 +12:56:43.813515 version@stat F·[] S·0B[] Sc·[] +12:56:43.816070 db@janitor F·2 G·0 +12:56:43.816079 db@open done T·2.800785ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.388069 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.388136 version@stat F·[] S·0B[] Sc·[] +12:57:38.388149 db@open opening +12:57:38.388177 journal@recovery F·1 +12:57:38.388277 journal@recovery recovering @54 +12:57:38.388405 version@stat F·[] S·0B[] Sc·[] +12:57:38.390999 db@janitor F·2 G·0 +12:57:38.391009 db@open done T·2.855815ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.477760 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.477835 version@stat F·[] S·0B[] Sc·[] +12:58:01.477847 db@open opening +12:58:01.477875 journal@recovery F·1 +12:58:01.479806 journal@recovery recovering @56 +12:58:01.481861 version@stat F·[] S·0B[] Sc·[] +12:58:01.485382 db@janitor F·2 G·0 +12:58:01.485393 db@open done T·7.540906ms +=============== Jun 10, 2024 (UTC) =============== +12:58:49.000209 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:49.000280 version@stat F·[] S·0B[] Sc·[] +12:58:49.000293 db@open opening +12:58:49.000320 journal@recovery F·1 +12:58:49.002268 journal@recovery recovering @58 +12:58:49.002409 version@stat F·[] S·0B[] Sc·[] +12:58:49.004946 db@janitor F·2 G·0 +12:58:49.004957 db@open done T·4.658861ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.137398 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.137461 version@stat F·[] S·0B[] Sc·[] +12:59:05.137473 db@open opening +12:59:05.137500 journal@recovery F·1 +12:59:05.139458 journal@recovery recovering @60 +12:59:05.139596 version@stat F·[] S·0B[] Sc·[] +12:59:05.142252 db@janitor F·2 G·0 +12:59:05.142263 db@open done T·4.785572ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.415003 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.415075 version@stat F·[] S·0B[] Sc·[] +12:59:32.415092 db@open opening +12:59:32.415119 journal@recovery F·1 +12:59:32.417105 journal@recovery recovering @62 +12:59:32.417251 version@stat F·[] S·0B[] Sc·[] +12:59:32.420493 db@janitor F·2 G·0 +12:59:32.420505 db@open done T·5.407497ms +=============== Jul 3, 2024 (UTC) =============== +17:52:58.348722 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.348793 version@stat F·[] S·0B[] Sc·[] +17:52:58.348811 db@open opening +17:52:58.348841 journal@recovery F·1 +17:52:58.350768 journal@recovery recovering @64 +17:52:58.350929 version@stat F·[] S·0B[] Sc·[] +17:52:58.355593 db@janitor F·2 G·0 +17:52:58.355602 db@open done T·6.787586ms +=============== Jul 3, 2024 (UTC) =============== +17:59:01.716570 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.716652 version@stat F·[] S·0B[] Sc·[] +17:59:01.716664 db@open opening +17:59:01.716693 journal@recovery F·1 +17:59:01.716779 journal@recovery recovering @66 +17:59:01.716921 version@stat F·[] S·0B[] Sc·[] +17:59:01.720432 db@janitor F·2 G·0 +17:59:01.720442 db@open done T·3.774542ms diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/MANIFEST-000069 b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/MANIFEST-000069 new file mode 100644 index 0000000000..3833569575 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/MANIFEST-000069 differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/000113.ldb b/.docker/container-state/atom-testnet-data/data/state.db/000113.ldb new file mode 100644 index 0000000000..4775c1b3ee Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/state.db/000113.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/000114.ldb b/.docker/container-state/atom-testnet-data/data/state.db/000114.ldb new file mode 100644 index 0000000000..fb954e1eb4 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/state.db/000114.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/000115.log b/.docker/container-state/atom-testnet-data/data/state.db/000115.log new file mode 100644 index 0000000000..5eec6803eb Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/state.db/000115.log differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/CURRENT b/.docker/container-state/atom-testnet-data/data/state.db/CURRENT new file mode 100644 index 0000000000..2b6390e186 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/state.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000116 diff --git a/.docker/container-state/atom-testnet-data/data/state.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/state.db/CURRENT.bak new file mode 100644 index 0000000000..b59a6ba248 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/state.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000112 diff --git a/.docker/container-state/atom-testnet-data/data/state.db/LOCK b/.docker/container-state/atom-testnet-data/data/state.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/atom-testnet-data/data/state.db/LOG b/.docker/container-state/atom-testnet-data/data/state.db/LOG new file mode 100644 index 0000000000..2c9dc925a4 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/state.db/LOG @@ -0,0 +1,443 @@ +=============== Jun 3, 2024 (+03) =============== +14:44:32.491582 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:44:32.493405 db@open opening +14:44:32.493614 version@stat F·[] S·0B[] Sc·[] +14:44:32.494820 db@janitor F·2 G·0 +14:44:32.494828 db@open done T·1.416442ms +=============== Jun 6, 2024 (+03) =============== +18:04:49.066239 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:04:49.066302 version@stat F·[] S·0B[] Sc·[] +18:04:49.066312 db@open opening +18:04:49.066337 journal@recovery F·1 +18:04:49.068180 journal@recovery recovering @1 +18:04:49.069352 memdb@flush created L0@2 N·286 S·52KiB "abc..y:1,v7":"val..y:9,v39" +18:04:49.071268 version@stat F·[1] S·52KiB[52KiB] Sc·[0.25] +18:04:49.073814 db@janitor F·3 G·0 +18:04:49.073824 db@open done T·7.508465ms +=============== Jun 6, 2024 (+03) =============== +18:09:26.417763 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:09:26.417850 version@stat F·[1] S·52KiB[52KiB] Sc·[0.25] +18:09:26.417860 db@open opening +18:09:26.417886 journal@recovery F·1 +18:09:26.419730 journal@recovery recovering @3 +18:09:26.420961 memdb@flush created L0@5 N·276 S·49KiB "abc..100,v504":"val..:99,v491" +18:09:26.421071 version@stat F·[2] S·102KiB[102KiB] Sc·[0.50] +18:09:26.423761 db@janitor F·4 G·0 +18:09:26.423776 db@open done T·5.912201ms +=============== Jun 6, 2024 (+03) =============== +19:18:28.702459 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:18:28.702524 version@stat F·[2] S·102KiB[102KiB] Sc·[0.50] +19:18:28.702533 db@open opening +19:18:28.702559 journal@recovery F·1 +19:18:28.704423 journal@recovery recovering @6 +19:18:28.705342 memdb@flush created L0@8 N·136 S·29KiB "abc..112,v566":"val..140,v698" +19:18:28.705454 version@stat F·[3] S·132KiB[132KiB] Sc·[0.75] +19:18:28.707895 db@janitor F·5 G·0 +19:18:28.707904 db@open done T·5.368658ms +=============== Jun 6, 2024 (+03) =============== +19:29:57.942395 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:29:57.942467 version@stat F·[3] S·132KiB[132KiB] Sc·[0.75] +19:29:57.942476 db@open opening +19:29:57.942504 journal@recovery F·1 +19:29:57.943531 journal@recovery recovering @9 +19:29:57.944488 memdb@flush created L0@11 N·21 S·5KiB "abc..139,v703":"val..144,v720" +19:29:57.946450 version@stat F·[4] S·137KiB[137KiB] Sc·[1.00] +19:29:57.948955 db@janitor F·6 G·0 +19:29:57.948966 db@open done T·6.487446ms +19:29:57.949037 table@compaction L0·4 -> L1·0 S·137KiB Q·723 +19:29:57.950927 table@build created L1@14 N·433 S·59KiB "abc..y:1,v7":"val..:99,v491" +19:29:57.950976 version@stat F·[0 1] S·59KiB[0B 59KiB] Sc·[0.00 0.00] +19:29:57.951995 table@compaction committed F-3 S-78KiB Ke·0 D·286 T·2.948286ms +19:29:57.952048 table@remove removed @8 +19:29:57.952125 table@remove removed @5 +19:29:57.952159 table@remove removed @2 +=============== Jun 10, 2024 (UTC) =============== +10:09:48.940254 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:48.940330 version@stat F·[0 1] S·59KiB[0B 59KiB] Sc·[0.00 0.00] +10:09:48.940340 db@open opening +10:09:48.940368 journal@recovery F·1 +10:09:48.940464 journal@recovery recovering @12 +10:09:48.941238 memdb@flush created L0@15 N·41 S·7KiB "abc..143,v725":"val..152,v762" +10:09:48.941531 version@stat F·[1 1] S·66KiB[7KiB 59KiB] Sc·[0.25 0.00] +10:09:48.944883 db@janitor F·5 G·1 +10:09:48.944890 db@janitor removing table-11 +10:09:48.944923 db@open done T·4.579157ms +10:12:34.186166 table@compaction L0·1 -> L1·1 S·66KiB Q·926 +10:12:34.187897 table@build created L1@18 N·457 S·61KiB "abc..y:1,v7":"val..:99,v491" +10:12:34.187919 version@stat F·[0 1] S·61KiB[0B 61KiB] Sc·[0.00 0.00] +10:12:34.189017 table@compaction committed F-1 S-4KiB Ke·0 D·17 T·2.830493ms +10:12:34.189085 table@remove removed @14 +=============== Jun 10, 2024 (UTC) =============== +10:19:47.924141 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.924233 version@stat F·[0 1] S·61KiB[0B 61KiB] Sc·[0.00 0.00] +10:19:47.924245 db@open opening +10:19:47.924272 journal@recovery F·1 +10:19:47.924395 journal@recovery recovering @16 +10:19:47.926846 memdb@flush created L0@19 N·586 S·100KiB "abc..151,v767":"val..269,v1349" +10:19:47.928660 version@stat F·[1 1] S·162KiB[100KiB 61KiB] Sc·[0.25 0.00] +10:19:47.931422 db@janitor F·5 G·1 +10:19:47.931429 db@janitor removing table-15 +10:19:47.931467 db@open done T·7.218481ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.781006 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.781130 version@stat F·[1 1] S·162KiB[100KiB 61KiB] Sc·[0.25 0.00] +10:20:11.781142 db@open opening +10:20:11.781176 journal@recovery F·1 +10:20:11.781282 journal@recovery recovering @20 +10:20:11.782033 memdb@flush created L0@22 N·1 S·141B "off..Key,v1353":"off..Key,v1353" +10:20:11.782147 version@stat F·[2 1] S·162KiB[100KiB 61KiB] Sc·[0.50 0.00] +10:20:11.784717 db@janitor F·5 G·0 +10:20:11.784729 db@open done T·3.58287ms +10:22:57.025785 table@compaction L0·2 -> L1·1 S·162KiB Q·1515 +10:22:57.027672 table@build created L1@25 N·808 S·101KiB "abc..y:1,v7":"val..:99,v491" +10:22:57.027692 version@stat F·[0 1] S·101KiB[0B 101KiB] Sc·[0.00 0.00] +10:22:57.028787 table@compaction committed F-2 S-60KiB Ke·0 D·236 T·2.981815ms +10:22:57.028860 table@remove removed @19 +10:22:57.028904 table@remove removed @18 +=============== Jun 10, 2024 (UTC) =============== +10:23:28.327557 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.327626 version@stat F·[0 1] S·101KiB[0B 101KiB] Sc·[0.00 0.00] +10:23:28.327637 db@open opening +10:23:28.327666 journal@recovery F·1 +10:23:28.327797 journal@recovery recovering @23 +10:23:28.329167 memdb@flush created L0@26 N·171 S·29KiB "abc..268,v1356":"val..303,v1523" +10:23:28.331040 version@stat F·[1 1] S·131KiB[29KiB 101KiB] Sc·[0.25 0.00] +10:23:28.333638 db@janitor F·5 G·1 +10:23:28.333646 db@janitor removing table-22 +10:23:28.333680 db@open done T·6.038971ms +10:26:13.556090 table@compaction L0·1 -> L1·1 S·131KiB Q·1687 +10:26:13.559311 table@build created L1@29 N·910 S·113KiB "abc..y:1,v7":"val..:99,v491" +10:26:13.559335 version@stat F·[0 1] S·113KiB[0B 113KiB] Sc·[0.00 0.00] +10:26:13.560452 table@compaction committed F-1 S-17KiB Ke·0 D·69 T·4.339276ms +10:26:13.560543 table@remove removed @25 +=============== Jun 10, 2024 (UTC) =============== +10:27:08.506132 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.506230 version@stat F·[0 1] S·113KiB[0B 113KiB] Sc·[0.00 0.00] +10:27:08.506243 db@open opening +10:27:08.506274 journal@recovery F·1 +10:27:08.506368 journal@recovery recovering @27 +10:27:08.507543 memdb@flush created L0@30 N·206 S·38KiB "abc..302,v1528":"val..344,v1730" +10:27:08.507655 version@stat F·[1 1] S·151KiB[38KiB 113KiB] Sc·[0.25 0.00] +10:27:08.512040 db@janitor F·5 G·1 +10:27:08.512047 db@janitor removing table-26 +10:27:08.512086 db@open done T·5.839899ms +=============== Jun 10, 2024 (UTC) =============== +10:30:28.120211 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.120282 version@stat F·[1 1] S·151KiB[38KiB 113KiB] Sc·[0.25 0.00] +10:30:28.120292 db@open opening +10:30:28.120319 journal@recovery F·1 +10:30:28.120407 journal@recovery recovering @31 +10:30:28.121322 memdb@flush created L0@33 N·101 S·17KiB "abc..343,v1735":"val..364,v1832" +10:30:28.121427 version@stat F·[2 1] S·169KiB[56KiB 113KiB] Sc·[0.50 0.00] +10:30:28.124021 db@janitor F·5 G·0 +10:30:28.124031 db@open done T·3.734811ms +10:32:58.327355 table@compaction L0·2 -> L1·1 S·169KiB Q·1981 +10:32:58.329351 table@build created L1@36 N·1093 S·135KiB "abc..y:1,v7":"val..:99,v491" +10:32:58.329372 version@stat F·[0 1] S·135KiB[0B 135KiB] Sc·[0.00 0.00] +10:32:58.330455 table@compaction committed F-2 S-33KiB Ke·0 D·124 T·3.080357ms +10:32:58.330520 table@remove removed @30 +10:32:58.330570 table@remove removed @29 +=============== Jun 10, 2024 (UTC) =============== +11:11:44.697359 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.697432 version@stat F·[0 1] S·135KiB[0B 135KiB] Sc·[0.00 0.00] +11:11:44.697443 db@open opening +11:11:44.697472 journal@recovery F·1 +11:11:44.697661 journal@recovery recovering @34 +11:11:44.699193 memdb@flush created L0@37 N·251 S·43KiB "abc..363,v1837":"val..414,v2084" +11:11:44.701040 version@stat F·[1 1] S·179KiB[43KiB 135KiB] Sc·[0.25 0.00] +11:11:44.703734 db@janitor F·5 G·1 +11:11:44.703742 db@janitor removing table-33 +11:11:44.703778 db@open done T·6.331643ms +11:12:41.510444 table@compaction L0·1 -> L1·1 S·179KiB Q·2143 +11:12:41.514879 table@build created L1@40 N·1243 S·152KiB "abc..y:1,v7":"val..:99,v491" +11:12:41.514902 version@stat F·[0 1] S·152KiB[0B 152KiB] Sc·[0.00 0.00] +11:12:41.515487 table@compaction committed F-1 S-26KiB Ke·0 D·101 T·5.017072ms +11:12:41.515573 table@remove removed @36 +=============== Jun 10, 2024 (UTC) =============== +11:15:00.481126 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.481205 version@stat F·[0 1] S·152KiB[0B 152KiB] Sc·[0.00 0.00] +11:15:00.481223 db@open opening +11:15:00.481254 journal@recovery F·1 +11:15:00.481345 journal@recovery recovering @38 +11:15:00.482402 memdb@flush created L0@41 N·121 S·44KiB "abc..413,v2089":"val..438,v2206" +11:15:00.482509 version@stat F·[1 1] S·197KiB[44KiB 152KiB] Sc·[0.25 0.00] +11:15:00.485050 db@janitor F·5 G·1 +11:15:00.485057 db@janitor removing table-37 +11:15:00.485097 db@open done T·3.870602ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.307060 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.307126 version@stat F·[1 1] S·197KiB[44KiB 152KiB] Sc·[0.25 0.00] +11:21:08.307136 db@open opening +11:21:08.307163 journal@recovery F·1 +11:21:08.307262 journal@recovery recovering @42 +11:21:08.307965 memdb@flush created L0@44 N·1 S·141B "off..Key,v2210":"off..Key,v2210" +11:21:08.308073 version@stat F·[2 1] S·197KiB[44KiB 152KiB] Sc·[0.50 0.00] +11:21:08.310609 db@janitor F·5 G·0 +11:21:08.310619 db@open done T·3.478859ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.248069 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.248144 version@stat F·[2 1] S·197KiB[44KiB 152KiB] Sc·[0.50 0.00] +11:22:28.248155 db@open opening +11:22:28.248194 journal@recovery F·1 +11:22:28.248271 journal@recovery recovering @45 +11:22:28.248990 memdb@flush created L0@47 N·6 S·1KiB "abc..437,v2213":"val..439,v2215" +11:22:28.249101 version@stat F·[3 1] S·198KiB[46KiB 152KiB] Sc·[0.75 0.00] +11:22:28.252120 db@janitor F·6 G·0 +11:22:28.252130 db@open done T·3.971993ms +=============== Jun 10, 2024 (UTC) =============== +11:23:15.340458 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.340532 version@stat F·[3 1] S·198KiB[46KiB 152KiB] Sc·[0.75 0.00] +11:23:15.340542 db@open opening +11:23:15.340572 journal@recovery F·1 +11:23:15.340661 journal@recovery recovering @48 +11:23:15.341449 memdb@flush created L0@50 N·26 S·5KiB "abc..438,v2220":"val..444,v2242" +11:23:15.341565 version@stat F·[4 1] S·203KiB[51KiB 152KiB] Sc·[1.00 0.00] +11:23:15.344531 db@janitor F·7 G·0 +11:23:15.344549 db@open done T·4.003893ms +11:23:15.344567 table@compaction L0·4 -> L1·1 S·203KiB Q·2245 +11:23:15.347824 table@build created L1@53 N·1333 S·174KiB "abc..y:1,v7":"val..:99,v491" +11:23:15.347850 version@stat F·[0 1] S·174KiB[0B 174KiB] Sc·[0.00 0.00] +11:23:15.348412 table@compaction committed F-4 S-29KiB Ke·0 D·64 T·3.829602ms +11:23:15.348473 table@remove removed @47 +11:23:15.348504 table@remove removed @44 +11:23:15.348541 table@remove removed @41 +11:23:15.348595 table@remove removed @40 +=============== Jun 10, 2024 (UTC) =============== +11:29:08.709730 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.709913 version@stat F·[0 1] S·174KiB[0B 174KiB] Sc·[0.00 0.00] +11:29:08.709928 db@open opening +11:29:08.709989 journal@recovery F·1 +11:29:08.712637 journal@recovery recovering @51 +11:29:08.714167 memdb@flush created L0@54 N·331 S·56KiB "abc..443,v2247":"val..510,v2574" +11:29:08.714305 version@stat F·[1 1] S·231KiB[56KiB 174KiB] Sc·[0.25 0.00] +11:29:08.717361 db@janitor F·5 G·1 +11:29:08.717369 db@janitor removing table-50 +11:29:08.717406 db@open done T·7.467142ms +=============== Jun 10, 2024 (UTC) =============== +11:30:42.387877 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.388005 version@stat F·[1 1] S·231KiB[56KiB 174KiB] Sc·[0.25 0.00] +11:30:42.388020 db@open opening +11:30:42.388052 journal@recovery F·1 +11:30:42.388272 journal@recovery recovering @55 +11:30:42.389671 memdb@flush created L0@57 N·86 S·15KiB "abc..509,v2579":"val..527,v2661" +11:30:42.391571 version@stat F·[2 1] S·246KiB[72KiB 174KiB] Sc·[0.50 0.00] +11:30:42.400050 db@janitor F·5 G·0 +11:30:42.400066 db@open done T·12.040961ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.647230 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.647311 version@stat F·[2 1] S·246KiB[72KiB 174KiB] Sc·[0.50 0.00] +11:30:55.647324 db@open opening +11:30:55.647355 journal@recovery F·1 +11:30:55.647502 journal@recovery recovering @58 +11:30:55.648559 memdb@flush created L0@60 N·6 S·1KiB "abc..526,v2666":"val..528,v2668" +11:30:55.648784 version@stat F·[3 1] S·248KiB[73KiB 174KiB] Sc·[0.75 0.00] +11:30:55.651370 db@janitor F·6 G·0 +11:30:55.651383 db@open done T·4.054084ms +11:33:25.878916 table@compaction L0·3 -> L1·1 S·248KiB Q·2817 +11:33:25.881634 table@build created L1@63 N·1585 S·203KiB "abc..y:1,v7":"val..:99,v491" +11:33:25.881656 version@stat F·[0 1] S·203KiB[0B 203KiB] Sc·[0.00 0.00] +11:33:25.882248 table@compaction committed F-3 S-44KiB Ke·0 D·171 T·3.312488ms +11:33:25.882310 table@remove removed @57 +11:33:25.882406 table@remove removed @54 +11:33:25.882470 table@remove removed @53 +=============== Jun 10, 2024 (UTC) =============== +11:36:36.174685 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.174762 version@stat F·[0 1] S·203KiB[0B 203KiB] Sc·[0.00 0.00] +11:36:36.174775 db@open opening +11:36:36.174805 journal@recovery F·1 +11:36:36.176685 journal@recovery recovering @61 +11:36:36.177965 memdb@flush created L0@64 N·301 S·52KiB "abc..527,v2673":"val..588,v2970" +11:36:36.178103 version@stat F·[1 1] S·255KiB[52KiB 203KiB] Sc·[0.25 0.00] +11:36:36.180802 db@janitor F·5 G·1 +11:36:36.180812 db@janitor removing table-60 +11:36:36.180845 db@open done T·6.065931ms +11:39:21.416169 table@compaction L0·1 -> L1·1 S·255KiB Q·3134 +11:39:21.419332 table@build created L1@67 N·1765 S·224KiB "abc..y:1,v7":"val..:99,v491" +11:39:21.419354 version@stat F·[0 1] S·224KiB[0B 224KiB] Sc·[0.00 0.00] +11:39:21.420882 table@compaction committed F-1 S-31KiB Ke·0 D·121 T·4.69133ms +11:39:21.420976 table@remove removed @63 +=============== Jun 10, 2024 (UTC) =============== +12:01:05.065456 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:05.065554 version@stat F·[0 1] S·224KiB[0B 224KiB] Sc·[0.00 0.00] +12:01:05.065566 db@open opening +12:01:05.065600 journal@recovery F·1 +12:01:05.065686 journal@recovery recovering @65 +12:01:05.066980 memdb@flush created L0@68 N·251 S·43KiB "abc..587,v2975":"val..638,v3222" +12:01:05.067116 version@stat F·[1 1] S·267KiB[43KiB 224KiB] Sc·[0.25 0.00] +12:01:05.069794 db@janitor F·5 G·1 +12:01:05.069803 db@janitor removing table-64 +12:01:05.069856 db@open done T·4.285068ms +=============== Jun 10, 2024 (UTC) =============== +12:03:00.050214 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:00.050294 version@stat F·[1 1] S·267KiB[43KiB 224KiB] Sc·[0.25 0.00] +12:03:00.050308 db@open opening +12:03:00.050339 journal@recovery F·1 +12:03:00.050425 journal@recovery recovering @69 +12:03:00.051162 memdb@flush created L0@71 N·16 S·2KiB "abc..637,v3227":"val..641,v3239" +12:03:00.051282 version@stat F·[2 1] S·270KiB[46KiB 224KiB] Sc·[0.50 0.00] +12:03:00.053885 db@janitor F·5 G·0 +12:03:00.053900 db@open done T·3.587902ms +=============== Jun 10, 2024 (UTC) =============== +12:03:19.759315 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.759390 version@stat F·[2 1] S·270KiB[46KiB 224KiB] Sc·[0.50 0.00] +12:03:19.759402 db@open opening +12:03:19.759435 journal@recovery F·1 +12:03:19.761256 journal@recovery recovering @72 +12:03:19.761978 memdb@flush created L0@74 N·11 S·2KiB "abc..640,v3244":"val..643,v3251" +12:03:19.762093 version@stat F·[3 1] S·272KiB[48KiB 224KiB] Sc·[0.75 0.00] +12:03:19.764654 db@janitor F·6 G·0 +12:03:19.764670 db@open done T·5.263086ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.852929 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.853006 version@stat F·[3 1] S·272KiB[48KiB 224KiB] Sc·[0.75 0.00] +12:04:23.853018 db@open opening +12:04:23.853052 journal@recovery F·1 +12:04:23.853131 journal@recovery recovering @75 +12:04:23.853850 memdb@flush created L0@77 N·6 S·1KiB "abc..642,v3256":"val..644,v3258" +12:04:23.853976 version@stat F·[4 1] S·274KiB[49KiB 224KiB] Sc·[1.00 0.00] +12:04:23.856578 db@janitor F·7 G·0 +12:04:23.856589 db@open done T·3.567482ms +12:04:23.856623 table@compaction L0·4 -> L1·1 S·274KiB Q·3261 +12:04:23.861057 table@build created L1@80 N·1933 S·243KiB "abc..y:1,v7":"val..:99,v491" +12:04:23.861100 version@stat F·[0 1] S·243KiB[0B 243KiB] Sc·[0.00 0.00] +12:04:23.862613 table@compaction committed F-4 S-30KiB Ke·0 D·116 T·5.965383ms +12:04:23.862722 table@remove removed @74 +12:04:23.862756 table@remove removed @71 +12:04:23.862795 table@remove removed @68 +12:04:23.862866 table@remove removed @67 +=============== Jun 10, 2024 (UTC) =============== +12:07:46.935525 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.935601 version@stat F·[0 1] S·243KiB[0B 243KiB] Sc·[0.00 0.00] +12:07:46.935612 db@open opening +12:07:46.935642 journal@recovery F·1 +12:07:46.935887 journal@recovery recovering @78 +12:07:46.937468 memdb@flush created L0@81 N·1 S·141B "off..Key,v3262":"off..Key,v3262" +12:07:46.937734 version@stat F·[1 1] S·243KiB[141B 243KiB] Sc·[0.25 0.00] +12:07:46.940329 db@janitor F·5 G·1 +12:07:46.940338 db@janitor removing table-77 +12:07:46.940372 db@open done T·4.754782ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.157116 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.157193 version@stat F·[1 1] S·243KiB[141B 243KiB] Sc·[0.25 0.00] +12:08:02.157206 db@open opening +12:08:02.157234 journal@recovery F·1 +12:08:02.157313 journal@recovery recovering @82 +12:08:02.158010 memdb@flush created L0@84 N·1 S·141B "off..Key,v3264":"off..Key,v3264" +12:08:02.158127 version@stat F·[2 1] S·243KiB[282B 243KiB] Sc·[0.50 0.00] +12:08:02.160671 db@janitor F·5 G·0 +12:08:02.160685 db@open done T·3.475091ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.485205 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.485280 version@stat F·[2 1] S·243KiB[282B 243KiB] Sc·[0.50 0.00] +12:27:21.485292 db@open opening +12:27:21.485322 journal@recovery F·1 +12:27:21.485414 journal@recovery recovering @85 +12:27:21.486106 memdb@flush created L0@87 N·1 S·141B "off..Key,v3266":"off..Key,v3266" +12:27:21.486222 version@stat F·[3 1] S·243KiB[423B 243KiB] Sc·[0.75 0.00] +12:27:21.488835 db@janitor F·6 G·0 +12:27:21.488846 db@open done T·3.549981ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.855222 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.855290 version@stat F·[3 1] S·243KiB[423B 243KiB] Sc·[0.75 0.00] +12:56:43.855302 db@open opening +12:56:43.855333 journal@recovery F·1 +12:56:43.857248 journal@recovery recovering @88 +12:56:43.857943 memdb@flush created L0@90 N·1 S·141B "off..Key,v3268":"off..Key,v3268" +12:56:43.858064 version@stat F·[4 1] S·244KiB[564B 243KiB] Sc·[1.00 0.00] +12:56:43.860558 db@janitor F·7 G·0 +12:56:43.860568 db@open done T·5.262286ms +12:56:43.860590 table@compaction L0·4 -> L1·1 S·244KiB Q·3269 +12:56:43.863695 table@build created L1@93 N·1933 S·243KiB "abc..y:1,v7":"val..:99,v491" +12:56:43.863715 version@stat F·[0 1] S·243KiB[0B 243KiB] Sc·[0.00 0.00] +12:56:43.865868 table@compaction committed F-4 S-564B Ke·0 D·4 T·5.258606ms +12:56:43.865939 table@remove removed @87 +12:56:43.866030 table@remove removed @84 +12:56:43.866063 table@remove removed @81 +12:56:43.866214 table@remove removed @80 +=============== Jun 10, 2024 (UTC) =============== +12:57:38.429067 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.429136 version@stat F·[0 1] S·243KiB[0B 243KiB] Sc·[0.00 0.00] +12:57:38.429149 db@open opening +12:57:38.429177 journal@recovery F·1 +12:57:38.429369 journal@recovery recovering @91 +12:57:38.430151 memdb@flush created L0@94 N·1 S·141B "off..Key,v3270":"off..Key,v3270" +12:57:38.430364 version@stat F·[1 1] S·243KiB[141B 243KiB] Sc·[0.25 0.00] +12:57:38.432954 db@janitor F·5 G·1 +12:57:38.432969 db@janitor removing table-90 +12:57:38.433003 db@open done T·3.849903ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.523272 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.523344 version@stat F·[1 1] S·243KiB[141B 243KiB] Sc·[0.25 0.00] +12:58:01.523357 db@open opening +12:58:01.523384 journal@recovery F·1 +12:58:01.523485 journal@recovery recovering @95 +12:58:01.524177 memdb@flush created L0@97 N·1 S·141B "off..Key,v3272":"off..Key,v3272" +12:58:01.524295 version@stat F·[2 1] S·243KiB[282B 243KiB] Sc·[0.50 0.00] +12:58:01.526870 db@janitor F·5 G·0 +12:58:01.526886 db@open done T·3.525131ms +=============== Jun 10, 2024 (UTC) =============== +12:58:49.043806 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:49.043878 version@stat F·[2 1] S·243KiB[282B 243KiB] Sc·[0.50 0.00] +12:58:49.043891 db@open opening +12:58:49.043921 journal@recovery F·1 +12:58:49.044000 journal@recovery recovering @98 +12:58:49.044711 memdb@flush created L0@100 N·1 S·141B "off..Key,v3274":"off..Key,v3274" +12:58:49.044831 version@stat F·[3 1] S·243KiB[423B 243KiB] Sc·[0.75 0.00] +12:58:49.047427 db@janitor F·6 G·0 +12:58:49.047442 db@open done T·3.546651ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.182866 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.182939 version@stat F·[3 1] S·243KiB[423B 243KiB] Sc·[0.75 0.00] +12:59:05.182949 db@open opening +12:59:05.182978 journal@recovery F·1 +12:59:05.183078 journal@recovery recovering @101 +12:59:05.183778 memdb@flush created L0@103 N·1 S·141B "off..Key,v3276":"off..Key,v3276" +12:59:05.183898 version@stat F·[4 1] S·244KiB[564B 243KiB] Sc·[1.00 0.00] +12:59:05.187343 db@janitor F·7 G·0 +12:59:05.187358 db@open done T·4.405238ms +12:59:05.187383 table@compaction L0·4 -> L1·1 S·244KiB Q·3277 +12:59:05.191222 table@build created L1@106 N·1933 S·243KiB "abc..y:1,v7":"val..:99,v491" +12:59:05.191248 version@stat F·[0 1] S·243KiB[0B 243KiB] Sc·[0.00 0.00] +12:59:05.192786 table@compaction committed F-4 S-564B Ke·0 D·4 T·5.387817ms +12:59:05.192898 table@remove removed @100 +12:59:05.192932 table@remove removed @97 +12:59:05.192960 table@remove removed @94 +12:59:05.193036 table@remove removed @93 +=============== Jun 10, 2024 (UTC) =============== +12:59:32.462104 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.462234 version@stat F·[0 1] S·243KiB[0B 243KiB] Sc·[0.00 0.00] +12:59:32.462248 db@open opening +12:59:32.462279 journal@recovery F·1 +12:59:32.462364 journal@recovery recovering @104 +12:59:32.463150 memdb@flush created L0@107 N·6 S·1KiB "abc..643,v3279":"val..645,v3281" +12:59:32.463288 version@stat F·[1 1] S·244KiB[1KiB 243KiB] Sc·[0.25 0.00] +12:59:32.467320 db@janitor F·5 G·1 +12:59:32.467329 db@janitor removing table-103 +12:59:32.467362 db@open done T·5.109474ms +=============== Jul 3, 2024 (UTC) =============== +17:52:58.392967 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.393039 version@stat F·[1 1] S·244KiB[1KiB 243KiB] Sc·[0.25 0.00] +17:52:58.393050 db@open opening +17:52:58.393077 journal@recovery F·1 +17:52:58.393169 journal@recovery recovering @108 +17:52:58.393872 memdb@flush created L0@110 N·1 S·141B "off..Key,v3285":"off..Key,v3285" +17:52:58.393983 version@stat F·[2 1] S·244KiB[1KiB 243KiB] Sc·[0.50 0.00] +17:52:58.396491 db@janitor F·5 G·0 +17:52:58.396505 db@open done T·3.451089ms +17:55:33.613329 table@compaction L0·2 -> L1·1 S·244KiB Q·3437 +17:55:33.616483 table@build created L1@113 N·1936 S·243KiB "abc..y:1,v7":"val..:99,v491" +17:55:33.616505 version@stat F·[0 1] S·243KiB[0B 243KiB] Sc·[0.00 0.00] +17:55:33.618025 table@compaction committed F-2 S-1KiB Ke·0 D·4 T·4.673628ms +17:55:33.618089 table@remove removed @107 +17:55:33.618160 table@remove removed @106 +=============== Jul 3, 2024 (UTC) =============== +17:59:01.761567 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.761644 version@stat F·[0 1] S·243KiB[0B 243KiB] Sc·[0.00 0.00] +17:59:01.761656 db@open opening +17:59:01.761687 journal@recovery F·1 +17:59:01.761781 journal@recovery recovering @111 +17:59:01.763581 memdb@flush created L0@114 N·346 S·83KiB "abc..644,v3288":"val..714,v3630" +17:59:01.763748 version@stat F·[1 1] S·327KiB[83KiB 243KiB] Sc·[0.25 0.00] +17:59:01.766489 db@janitor F·5 G·1 +17:59:01.766499 db@janitor removing table-110 +17:59:01.766531 db@open done T·4.87064ms diff --git a/.docker/container-state/atom-testnet-data/data/state.db/MANIFEST-000116 b/.docker/container-state/atom-testnet-data/data/state.db/MANIFEST-000116 new file mode 100644 index 0000000000..41565f0c98 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/state.db/MANIFEST-000116 differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000070.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000070.ldb new file mode 100644 index 0000000000..13cc2bd346 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000070.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000071.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000071.ldb new file mode 100644 index 0000000000..c7dcbe51d4 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000071.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000090.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000090.ldb new file mode 100644 index 0000000000..32bb2004b9 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000090.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000095.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000095.ldb new file mode 100644 index 0000000000..f22e4b3e9b Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000095.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000096.log b/.docker/container-state/atom-testnet-data/data/tx_index.db/000096.log new file mode 100644 index 0000000000..a57e507e95 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000096.log differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT b/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT new file mode 100644 index 0000000000..af00d34b29 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000097 diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT.bak new file mode 100644 index 0000000000..0ab25fa07f --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000094 diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/LOCK b/.docker/container-state/atom-testnet-data/data/tx_index.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/LOG b/.docker/container-state/atom-testnet-data/data/tx_index.db/LOG new file mode 100644 index 0000000000..58ce5d3c39 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/tx_index.db/LOG @@ -0,0 +1,379 @@ +=============== Jun 3, 2024 (+03) =============== +14:44:32.496020 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +14:44:32.497868 db@open opening +14:44:32.498022 version@stat F·[] S·0B[] Sc·[] +14:44:32.498623 db@janitor F·2 G·0 +14:44:32.498647 db@open done T·772.676µs +=============== Jun 6, 2024 (+03) =============== +18:04:49.075260 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:04:49.075321 version@stat F·[] S·0B[] Sc·[] +18:04:49.075331 db@open opening +18:04:49.075355 journal@recovery F·1 +18:04:49.075436 journal@recovery recovering @1 +18:04:49.077025 memdb@flush created L0@2 N·1612 S·25KiB "blo..\x00\x01\x81,v1":"blo..\x01\xc2\xca,v1594" +18:04:49.077135 version@stat F·[1] S·25KiB[25KiB] Sc·[0.25] +18:04:49.079674 db@janitor F·3 G·0 +18:04:49.079684 db@open done T·4.348848ms +=============== Jun 6, 2024 (+03) =============== +18:09:26.424183 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +18:09:26.424253 version@stat F·[1] S·25KiB[25KiB] Sc·[0.25] +18:09:26.424264 db@open opening +18:09:26.424289 journal@recovery F·1 +18:09:26.424367 journal@recovery recovering @3 +18:09:26.426040 memdb@flush created L0@5 N·1637 S·29KiB "01\x11..\x9f\xa8\xa6,v2873":"\x93NV..\x16b\x1d,v2323" +18:09:26.426155 version@stat F·[2] S·54KiB[54KiB] Sc·[0.50] +18:09:26.435684 db@janitor F·4 G·0 +18:09:26.435696 db@open done T·11.427739ms +=============== Jun 6, 2024 (+03) =============== +19:18:28.708310 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:18:28.708381 version@stat F·[2] S·54KiB[54KiB] Sc·[0.50] +19:18:28.708390 db@open opening +19:18:28.708417 journal@recovery F·1 +19:18:28.708491 journal@recovery recovering @6 +19:18:28.709672 memdb@flush created L0@8 N·875 S·18KiB ":cU..\xdaRE,v3772":"\xef\xb9<..\x9a\x1b\x86,v4043" +19:18:28.709802 version@stat F·[3] S·73KiB[73KiB] Sc·[0.75] +19:18:28.712224 db@janitor F·5 G·0 +19:18:28.712232 db@open done T·3.839274ms +=============== Jun 6, 2024 (+03) =============== +19:29:57.949413 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +19:29:57.949480 version@stat F·[3] S·73KiB[73KiB] Sc·[0.75] +19:29:57.949489 db@open opening +19:29:57.949516 journal@recovery F·1 +19:29:57.949600 journal@recovery recovering @9 +19:29:57.950423 memdb@flush created L0@11 N·141 S·4KiB "\x1dU\xbd..\xdfV\x85,v4210":"tx...s$7,v4197" +19:29:57.950541 version@stat F·[4] S·77KiB[77KiB] Sc·[1.00] +19:29:57.953844 db@janitor F·6 G·0 +19:29:57.953856 db@open done T·4.364458ms +19:29:57.953873 table@compaction L0·4 -> L1·0 S·77KiB Q·4269 +19:29:57.956040 table@build created L1@14 N·4265 S·81KiB "\x1dU\xbd..\xdfV\x85,v4210":"\xef\xb9<..\x9a\x1b\x86,v4043" +19:29:57.956057 version@stat F·[0 1] S·81KiB[0B 81KiB] Sc·[0.00 0.00] +19:29:57.957148 table@compaction committed F-3 S+3KiB Ke·0 D·0 T·3.266569ms +19:29:57.957193 table@remove removed @8 +19:29:57.957269 table@remove removed @5 +19:29:57.957299 table@remove removed @2 +=============== Jun 10, 2024 (UTC) =============== +10:09:48.945690 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:48.945769 version@stat F·[0 1] S·81KiB[0B 81KiB] Sc·[0.00 0.00] +10:09:48.945780 db@open opening +10:09:48.945810 journal@recovery F·1 +10:09:48.947674 journal@recovery recovering @12 +10:09:48.948524 memdb@flush created L0@15 N·232 S·4KiB "blo..\x01\xc0\x8f,v4270":"blo..\x01\xc0`,v4483" +10:09:48.950420 version@stat F·[1 1] S·85KiB[4KiB 81KiB] Sc·[0.25 0.00] +10:09:48.953250 db@janitor F·5 G·1 +10:09:48.953256 db@janitor removing table-11 +10:09:48.953288 db@open done T·7.503901ms +=============== Jun 10, 2024 (UTC) =============== +10:19:47.931822 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.931895 version@stat F·[1 1] S·85KiB[4KiB 81KiB] Sc·[0.25 0.00] +10:19:47.931905 db@open opening +10:19:47.931933 journal@recovery F·1 +10:19:47.932025 journal@recovery recovering @16 +10:19:47.934684 memdb@flush created L0@18 N·3393 S·58KiB "blo..\x01\xc0\x97,v4503":"blo..\x01\xc5\xe9,v7877" +10:19:47.934801 version@stat F·[2 1] S·143KiB[62KiB 81KiB] Sc·[0.50 0.00] +10:19:47.937476 db@janitor F·5 G·0 +10:19:47.937486 db@open done T·5.579207ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.785141 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.785227 version@stat F·[2 1] S·143KiB[62KiB 81KiB] Sc·[0.50 0.00] +10:20:11.785239 db@open opening +10:20:11.785268 journal@recovery F·1 +10:20:11.785350 journal@recovery recovering @19 +10:20:11.785477 version@stat F·[2 1] S·143KiB[62KiB 81KiB] Sc·[0.50 0.00] +10:20:11.788064 db@janitor F·5 G·0 +10:20:11.788074 db@open done T·2.830793ms +=============== Jun 10, 2024 (UTC) =============== +10:23:28.334052 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.334123 version@stat F·[2 1] S·143KiB[62KiB 81KiB] Sc·[0.50 0.00] +10:23:28.334134 db@open opening +10:23:28.334166 journal@recovery F·1 +10:23:28.334267 journal@recovery recovering @21 +10:23:28.335538 memdb@flush created L0@23 N·986 S·16KiB "blo..\x01\xc1\f,v7897":"blo..\x01\xc1\xb2,v8864" +10:23:28.335649 version@stat F·[3 1] S·159KiB[78KiB 81KiB] Sc·[0.75 0.00] +10:23:28.338205 db@janitor F·6 G·0 +10:23:28.338216 db@open done T·4.078524ms +=============== Jun 10, 2024 (UTC) =============== +10:27:08.512447 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.512528 version@stat F·[3 1] S·159KiB[78KiB 81KiB] Sc·[0.75 0.00] +10:27:08.512539 db@open opening +10:27:08.512569 journal@recovery F·1 +10:27:08.512654 journal@recovery recovering @24 +10:27:08.514153 memdb@flush created L0@26 N·1228 S·22KiB "\tW\x04..'%z,v9477":"\xe4\xc1 ..\xbe\xbd\xab,v9879" +10:27:08.514282 version@stat F·[4 1] S·182KiB[100KiB 81KiB] Sc·[1.00 0.00] +10:27:08.516900 db@janitor F·7 G·0 +10:27:08.516914 db@open done T·4.371717ms +10:27:08.516927 table@compaction L0·4 -> L1·1 S·182KiB Q·10112 +10:27:08.521707 table@build created L1@29 N·10104 S·181KiB "\tW\x04..'%z,v9477":"\xef\xb9<..\x9a\x1b\x86,v4043" +10:27:08.521732 version@stat F·[0 1] S·181KiB[0B 181KiB] Sc·[0.00 0.00] +10:27:08.522816 table@compaction committed F-4 S-1KiB Ke·0 D·0 T·5.87477ms +10:27:08.522880 table@remove removed @23 +10:27:08.522964 table@remove removed @18 +10:27:08.522998 table@remove removed @15 +10:27:08.523039 table@remove removed @14 +=============== Jun 10, 2024 (UTC) =============== +10:30:28.124491 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.124570 version@stat F·[0 1] S·181KiB[0B 181KiB] Sc·[0.00 0.00] +10:30:28.124581 db@open opening +10:30:28.124611 journal@recovery F·1 +10:30:28.124736 journal@recovery recovering @27 +10:30:28.126183 memdb@flush created L0@30 N·580 S·9KiB "blo..\x01\xc1W,v10113":"blo..\x01\xc0\xfc,v10674" +10:30:28.128128 version@stat F·[1 1] S·190KiB[9KiB 181KiB] Sc·[0.25 0.00] +10:30:28.130689 db@janitor F·5 G·1 +10:30:28.130696 db@janitor removing table-26 +10:30:28.130734 db@open done T·6.149542ms +=============== Jun 10, 2024 (UTC) =============== +11:11:44.704217 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.704299 version@stat F·[1 1] S·190KiB[9KiB 181KiB] Sc·[0.25 0.00] +11:11:44.704312 db@open opening +11:11:44.704346 journal@recovery F·1 +11:11:44.704425 journal@recovery recovering @31 +11:11:44.705964 memdb@flush created L0@33 N·1450 S·23KiB "blo..\x01\xc1k,v10694":"blo..\x01\u0082,v12125" +11:11:44.706086 version@stat F·[2 1] S·214KiB[33KiB 181KiB] Sc·[0.50 0.00] +11:11:44.708711 db@janitor F·5 G·0 +11:11:44.708721 db@open done T·4.405607ms +=============== Jun 10, 2024 (UTC) =============== +11:15:00.485497 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.485602 version@stat F·[2 1] S·214KiB[33KiB 181KiB] Sc·[0.50 0.00] +11:15:00.485613 db@open opening +11:15:00.485647 journal@recovery F·1 +11:15:00.485799 journal@recovery recovering @34 +11:15:00.487788 memdb@flush created L0@36 N·835 S·36KiB "\x04\x92\xf2..\v,\x8e,v12512":"\xba[\x1e..SVa,v12657" +11:15:00.489774 version@stat F·[3 1] S·251KiB[70KiB 181KiB] Sc·[0.75 0.00] +11:15:00.492389 db@janitor F·6 G·0 +11:15:00.492402 db@open done T·6.785426ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.310961 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.311037 version@stat F·[3 1] S·251KiB[70KiB 181KiB] Sc·[0.75 0.00] +11:21:08.311050 db@open opening +11:21:08.311081 journal@recovery F·1 +11:21:08.311156 journal@recovery recovering @37 +11:21:08.311393 version@stat F·[3 1] S·251KiB[70KiB 181KiB] Sc·[0.75 0.00] +11:21:08.314600 db@janitor F·6 G·0 +11:21:08.314610 db@open done T·3.55552ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.252574 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.252649 version@stat F·[3 1] S·251KiB[70KiB 181KiB] Sc·[0.75 0.00] +11:22:28.252659 db@open opening +11:22:28.252687 journal@recovery F·1 +11:22:28.252764 journal@recovery recovering @39 +11:22:28.253465 memdb@flush created L0@41 N·29 S·925B "blo..\x01\xc1\xb5,v12981":"blo..\x00\x01\x85,v12991" +11:22:28.253635 version@stat F·[4 1] S·252KiB[70KiB 181KiB] Sc·[1.00 0.00] +11:22:28.256514 db@janitor F·7 G·0 +11:22:28.256526 db@open done T·3.863542ms +11:22:28.256561 table@compaction L0·4 -> L1·1 S·252KiB Q·13010 +11:22:28.262691 table@build created L1@44 N·12998 S·253KiB "\x04\x92\xf2..\v,\x8e,v12512":"\xef\xb9<..\x9a\x1b\x86,v4043" +11:22:28.262719 version@stat F·[0 1] S·253KiB[0B 253KiB] Sc·[0.00 0.00] +11:22:28.263937 table@compaction committed F-4 S+1KiB Ke·0 D·0 T·7.351261ms +11:22:28.264032 table@remove removed @36 +11:22:28.264080 table@remove removed @33 +11:22:28.264127 table@remove removed @30 +11:22:28.264218 table@remove removed @29 +=============== Jun 10, 2024 (UTC) =============== +11:23:15.344988 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.345083 version@stat F·[0 1] S·253KiB[0B 253KiB] Sc·[0.00 0.00] +11:23:15.345098 db@open opening +11:23:15.345137 journal@recovery F·1 +11:23:15.345324 journal@recovery recovering @42 +11:23:15.346996 memdb@flush created L0@45 N·145 S·2KiB "blo..\x01\xc1\xb6,v13011":"blo..\x00\x01\xb9,v13137" +11:23:15.348877 version@stat F·[1 1] S·256KiB[2KiB 253KiB] Sc·[0.25 0.00] +11:23:15.351409 db@janitor F·5 G·1 +11:23:15.351416 db@janitor removing table-41 +11:23:15.351450 db@open done T·6.348053ms +=============== Jun 10, 2024 (UTC) =============== +11:29:08.717753 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.717824 version@stat F·[1 1] S·256KiB[2KiB 253KiB] Sc·[0.25 0.00] +11:29:08.717836 db@open opening +11:29:08.717865 journal@recovery F·1 +11:29:08.717942 journal@recovery recovering @46 +11:29:08.719709 memdb@flush created L0@48 N·1914 S·31KiB "blo..\x01\xc1\xbb,v13157":"blo..\x01\xc3R,v15052" +11:29:08.719821 version@stat F·[2 1] S·287KiB[34KiB 253KiB] Sc·[0.50 0.00] +11:29:08.722425 db@janitor F·5 G·0 +11:29:08.722436 db@open done T·4.596628ms +=============== Jun 10, 2024 (UTC) =============== +11:30:42.400488 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.400575 version@stat F·[2 1] S·287KiB[34KiB 253KiB] Sc·[0.50 0.00] +11:30:42.400587 db@open opening +11:30:42.400620 journal@recovery F·1 +11:30:42.400705 journal@recovery recovering @49 +11:30:42.401777 memdb@flush created L0@51 N·493 S·8KiB "blo..\x01\xc1\xfd,v15072":"blo..\x01\xc0\xd5,v15546" +11:30:42.401897 version@stat F·[3 1] S·296KiB[42KiB 253KiB] Sc·[0.75 0.00] +11:30:42.404520 db@janitor F·6 G·0 +11:30:42.404533 db@open done T·3.942623ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.651815 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.651923 version@stat F·[3 1] S·296KiB[42KiB 253KiB] Sc·[0.75 0.00] +11:30:55.651938 db@open opening +11:30:55.651978 journal@recovery F·1 +11:30:55.653912 journal@recovery recovering @52 +11:30:55.654664 memdb@flush created L0@54 N·29 S·921B "blo..\x01\xc2\x0e,v15566":"blo..\x00\x01\x85,v15576" +11:30:55.654798 version@stat F·[4 1] S·296KiB[43KiB 253KiB] Sc·[1.00 0.00] +11:30:55.657351 db@janitor F·7 G·0 +11:30:55.657367 db@open done T·5.424386ms +11:30:55.657388 table@compaction L0·4 -> L1·1 S·296KiB Q·15595 +11:30:55.663657 table@build created L1@57 N·15579 S·297KiB "\x04\x92\xf2..\v,\x8e,v12512":"\xef\xb9<..\x9a\x1b\x86,v4043" +11:30:55.663681 version@stat F·[0 1] S·297KiB[0B 297KiB] Sc·[0.00 0.00] +11:30:55.665265 table@compaction committed F-4 S+908B Ke·0 D·0 T·7.858166ms +11:30:55.665342 table@remove removed @51 +11:30:55.665392 table@remove removed @48 +11:30:55.665434 table@remove removed @45 +11:30:55.665529 table@remove removed @44 +=============== Jun 10, 2024 (UTC) =============== +11:36:36.181241 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.181320 version@stat F·[0 1] S·297KiB[0B 297KiB] Sc·[0.00 0.00] +11:36:36.181333 db@open opening +11:36:36.181362 journal@recovery F·1 +11:36:36.181439 journal@recovery recovering @55 +11:36:36.183160 memdb@flush created L0@58 N·1740 S·28KiB "blo..\x01\xc2\x0f,v15596":"blo..\x01\xc3\x04,v17317" +11:36:36.183288 version@stat F·[1 1] S·326KiB[28KiB 297KiB] Sc·[0.25 0.00] +11:36:36.185945 db@janitor F·5 G·1 +11:36:36.185953 db@janitor removing table-54 +11:36:36.185986 db@open done T·4.648749ms +=============== Jun 10, 2024 (UTC) =============== +12:01:05.070270 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:05.070349 version@stat F·[1 1] S·326KiB[28KiB 297KiB] Sc·[0.25 0.00] +12:01:05.070361 db@open opening +12:01:05.070391 journal@recovery F·1 +12:01:05.070472 journal@recovery recovering @59 +12:01:05.072116 memdb@flush created L0@61 N·1450 S·23KiB "blo..\x01\xc2K,v17337":"blo..\x01\u0082,v18768" +12:01:05.072239 version@stat F·[2 1] S·350KiB[52KiB 297KiB] Sc·[0.50 0.00] +12:01:05.074775 db@janitor F·5 G·0 +12:01:05.074788 db@open done T·4.42315ms +=============== Jun 10, 2024 (UTC) =============== +12:03:00.054328 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:00.054420 version@stat F·[2 1] S·350KiB[52KiB 297KiB] Sc·[0.50 0.00] +12:03:00.054439 db@open opening +12:03:00.054485 journal@recovery F·1 +12:03:00.054723 journal@recovery recovering @62 +12:03:00.056228 memdb@flush created L0@64 N·87 S·1KiB "blo..\x01\xc2},v18788":"blo..\x00\x01\x9f,v18856" +12:03:00.058037 version@stat F·[3 1] S·352KiB[54KiB 297KiB] Sc·[0.75 0.00] +12:03:00.060585 db@janitor F·6 G·0 +12:03:00.060598 db@open done T·6.153555ms +=============== Jun 10, 2024 (UTC) =============== +12:03:19.765209 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.765311 version@stat F·[3 1] S·352KiB[54KiB 297KiB] Sc·[0.75 0.00] +12:03:19.765325 db@open opening +12:03:19.765360 journal@recovery F·1 +12:03:19.765469 journal@recovery recovering @65 +12:03:19.766245 memdb@flush created L0@67 N·58 S·1KiB "blo..\x01\u0080,v18876":"blo..\x00\x01\x92,v18915" +12:03:19.766367 version@stat F·[4 1] S·353KiB[55KiB 297KiB] Sc·[1.00 0.00] +12:03:19.768909 db@janitor F·7 G·0 +12:03:19.768920 db@open done T·3.590622ms +12:03:19.768942 table@compaction L0·4 -> L1·1 S·353KiB Q·18934 +12:03:19.777063 table@build created L1@70 N·18914 S·352KiB "\x04\x92\xf2..\v,\x8e,v12512":"\xef\xb9<..\x9a\x1b\x86,v4043" +12:03:19.777089 version@stat F·[0 1] S·352KiB[0B 352KiB] Sc·[0.00 0.00] +12:03:19.777658 table@compaction committed F-4 S-400B Ke·0 D·0 T·8.701467ms +12:03:19.777720 table@remove removed @64 +12:03:19.777758 table@remove removed @61 +12:03:19.777793 table@remove removed @58 +12:03:19.777877 table@remove removed @57 +=============== Jun 10, 2024 (UTC) =============== +12:04:23.857145 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.857250 version@stat F·[0 1] S·352KiB[0B 352KiB] Sc·[0.00 0.00] +12:04:23.857265 db@open opening +12:04:23.857322 journal@recovery F·1 +12:04:23.857406 journal@recovery recovering @68 +12:04:23.858158 memdb@flush created L0@71 N·29 S·932B "blo..\x01\u0082,v18935":"blo..\x00\x01\x85,v18945" +12:04:23.858466 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:04:23.863301 db@janitor F·5 G·1 +12:04:23.863310 db@janitor removing table-67 +12:04:23.863344 db@open done T·6.073803ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.940724 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.940829 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:07:46.940845 db@open opening +12:07:46.940891 journal@recovery F·1 +12:07:46.941015 journal@recovery recovering @72 +12:07:46.941174 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:07:46.944342 db@janitor F·4 G·0 +12:07:46.944357 db@open done T·3.506111ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.161058 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.161130 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:08:02.161142 db@open opening +12:08:02.161170 journal@recovery F·1 +12:08:02.161261 journal@recovery recovering @74 +12:08:02.161772 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:08:02.165071 db@janitor F·4 G·0 +12:08:02.165085 db@open done T·3.938975ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.489214 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.489293 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:27:21.489306 db@open opening +12:27:21.489335 journal@recovery F·1 +12:27:21.489533 journal@recovery recovering @76 +12:27:21.492693 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:27:21.495334 db@janitor F·4 G·0 +12:27:21.495349 db@open done T·6.038382ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.860894 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.860958 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:56:43.860969 db@open opening +12:56:43.861000 journal@recovery F·1 +12:56:43.861094 journal@recovery recovering @78 +12:56:43.861237 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:56:43.864305 db@janitor F·4 G·0 +12:56:43.864316 db@open done T·3.342909ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.433435 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.433507 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:57:38.433523 db@open opening +12:57:38.433555 journal@recovery F·1 +12:57:38.435466 journal@recovery recovering @80 +12:57:38.435598 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:57:38.438132 db@janitor F·4 G·0 +12:57:38.438146 db@open done T·4.61711ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.527259 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.527331 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:58:01.527344 db@open opening +12:58:01.527371 journal@recovery F·1 +12:58:01.527448 journal@recovery recovering @82 +12:58:01.528206 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:58:01.530847 db@janitor F·4 G·0 +12:58:01.530860 db@open done T·3.51254ms +=============== Jun 10, 2024 (UTC) =============== +12:58:49.047851 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:49.047923 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:58:49.047936 db@open opening +12:58:49.047971 journal@recovery F·1 +12:58:49.048062 journal@recovery recovering @84 +12:58:49.048387 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:58:49.051827 db@janitor F·4 G·0 +12:58:49.051843 db@open done T·3.900364ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.188209 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.188325 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:59:05.188346 db@open opening +12:59:05.188403 journal@recovery F·1 +12:59:05.188521 journal@recovery recovering @86 +12:59:05.188747 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:59:05.192881 db@janitor F·4 G·0 +12:59:05.192895 db@open done T·4.542609ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.467734 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.467839 version@stat F·[1 1] S·353KiB[932B 352KiB] Sc·[0.25 0.00] +12:59:32.467852 db@open opening +12:59:32.467882 journal@recovery F·1 +12:59:32.468080 journal@recovery recovering @88 +12:59:32.469891 memdb@flush created L0@90 N·29 S·925B "blo..\x01\u0083,v18965":"blo..\x00\x01\x85,v18975" +12:59:32.472164 version@stat F·[2 1] S·354KiB[1KiB 352KiB] Sc·[0.50 0.00] +12:59:32.475396 db@janitor F·5 G·0 +12:59:32.475411 db@open done T·7.555145ms +=============== Jul 3, 2024 (UTC) =============== +17:52:58.396919 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.396991 version@stat F·[2 1] S·354KiB[1KiB 352KiB] Sc·[0.50 0.00] +17:52:58.397003 db@open opening +17:52:58.397031 journal@recovery F·1 +17:52:58.397205 journal@recovery recovering @91 +17:52:58.400312 version@stat F·[2 1] S·354KiB[1KiB 352KiB] Sc·[0.50 0.00] +17:52:58.402997 db@janitor F·5 G·0 +17:52:58.403009 db@open done T·6.0015ms +=============== Jul 3, 2024 (UTC) =============== +17:59:01.767011 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.767114 version@stat F·[2 1] S·354KiB[1KiB 352KiB] Sc·[0.50 0.00] +17:59:01.767128 db@open opening +17:59:01.767161 journal@recovery F·1 +17:59:01.767237 journal@recovery recovering @93 +17:59:01.769440 memdb@flush created L0@95 N·2140 S·59KiB "\x12\xf5\xa5..b\x01[,v20843":"\xe0\x1e\x98..\x9a\x10\x81,v20696" +17:59:01.769561 version@stat F·[3 1] S·414KiB[61KiB 352KiB] Sc·[0.75 0.00] +17:59:01.773003 db@janitor F·6 G·0 +17:59:01.773016 db@open done T·5.882999ms diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/MANIFEST-000097 b/.docker/container-state/atom-testnet-data/data/tx_index.db/MANIFEST-000097 new file mode 100644 index 0000000000..bbde422037 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/MANIFEST-000097 differ diff --git a/.docker/container-state/atom-testnet-data/data/write-file-atomic-03223747255274497650 b/.docker/container-state/atom-testnet-data/data/write-file-atomic-03223747255274497650 new file mode 100644 index 0000000000..bc534f0b06 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/data/write-file-atomic-03223747255274497650 @@ -0,0 +1,7 @@ +{ + "height": "343", + "round": 0, + "step": 3, + "signature": "MY8ah2Oe2uLdMiP3JJQ++r2nUP3Nf3E/SdU0oumWpOFQpsLgFnJusiKgbbuUq1vlfEzZvNzC+Iq1Q4x6Ma80Bw==", + "signbytes": "76080211570100000000000022480A202E5A85F34706FA52E68449B0D7C4096A8AA01C74C0F4F774E793501209B7D13A122408011220C688A67C51EDAFDCF4036BB86B89E5916C3A84D5FBACB7D32BD011EB31F597312A0C08F2A99BB306109884F3A2023211636F736D6F736875622D746573746E6574" +} \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/keyring-test/224884bc43a4345c66051befe55c0c54676008b3.address b/.docker/container-state/atom-testnet-data/keyring-test/224884bc43a4345c66051befe55c0c54676008b3.address new file mode 100644 index 0000000000..80ea7eb88a --- /dev/null +++ b/.docker/container-state/atom-testnet-data/keyring-test/224884bc43a4345c66051befe55c0c54676008b3.address @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0wMyAxNDo0MzozMy40NzUxMDYyMzIgKzAzMDAgKzAzIG09KzAuMDUwMjQzNDYzIiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiNDVFd1ZvLXBsUTg5TWhwbiJ9.D2_MVk-uV5oSHZ_1fTMPaInNKJYV8mq4j4vLRDpXpcc6Y64UeMhTrg.8ObGr4ch02Ueij_w._dys4Xkk15-eBFn0Hp1ND1D5a6x1qeiEZd4nDk9zH7nYX8pHVTI0aMf-eMBPwyrvNlP4HR7kP32aMfzOG5-KGfjVTMvFU1uzvddktQMAADiDII_anlp9SHFQ2OE5ElafEb0TrM42_nKO52NIjBoR11BnrlklbNSe7Z_5_aJFtkvPeI3M6JMgL8-3HvlrVxh1jONNTfzI6BsMRu9oyz7czzjt_CCBykZiB3MORpZXUiAdvQ.fXxi52nksYKozG5ZgB-I8w \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/keyring-test/a36b04d45a23d962b67cd9c82b3065c45c0a80e5.address b/.docker/container-state/atom-testnet-data/keyring-test/a36b04d45a23d962b67cd9c82b3065c45c0a80e5.address new file mode 100644 index 0000000000..f973909eb2 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/keyring-test/a36b04d45a23d962b67cd9c82b3065c45c0a80e5.address @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0wMyAxNDo0MzozMy40MTYyMDA1NjkgKzAzMDAgKzAzIG09KzAuMDUwODg5Njg4IiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiYXQ4QWxwUzVuVWFGWlZRUSJ9.X2pj9isOPsF7cKkoyVas6VcJB-m_AAxbA701Kmh3Yk7VwaxdQ5Qo2g.vI2ySGJ5ddMTDiHb.zahJ5cHOvxC54v4JT96sst89jXwIdMIfgsGQpxLyNMRthst7imfGyxy2TJALHbZnZ8kHSkKzV5iCCmc2PseV1QDd9uYzzqPyaNj_4521XaublQSQVwKx7dWx_ZO_m4L4TohqiH9RGXgWWXULmPDpbPeHR-IQCXQQCpcCt-xgYnu2NQlpsHjVJEu6GnHadSBGTQSpmlJOEBWBrCo3GWMDh2BZNmGZtpHjetCFjvUtWQydRFkO6M4.LGozg-bJyZPFpLp6GSFo1g \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/keyring-test/alice.info b/.docker/container-state/atom-testnet-data/keyring-test/alice.info new file mode 100644 index 0000000000..6190860f95 --- /dev/null +++ b/.docker/container-state/atom-testnet-data/keyring-test/alice.info @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0wMyAxNDo0MzozMy40MTQzMjMyMjQgKzAzMDAgKzAzIG09KzAuMDQ5MDEyMzUzIiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiVUtiMERCV3QxSUdVM1BVcSJ9.3PC7McaHjy5hj0TMb0_ehGBkaet2VOJv-ct2O3sQu1X-tG70EozO5Q.dhHoSV0VHixAut1x.51GHbUPRH0hP7gwT5Cn4wjSIQ6QkeeR-E5Wt5E9H3oalsPj71PeZBr_xcLVs76RYcHo_9EgDZWVROMMGqNPW_3lvOIGcSdL3xY-XWFvC-Lk80PwQTlG-IVrlVjzfYX1q4iCSA1IVZ_5N6G-7Ru0vWsLrkzjm4A_0PbFKLnyIDxEvm1hyvt36EdpALB1OjHApG6WdL6XGaOw-ENNngGgaRuB0zxXpaFtoEaUargiSc9L-0wxODppRVbeHxbprNbQ3LTD2YT_gL-ckLQ2GWn83I4NeAGZNQLdBtqV_FXYecsUVRTsBT5x_GZA_eI2VxPUda6q-4pNdnK2F2cz7z2INJUXZkvXJLGAMWW71uzi4VOFSKvHWorAC_dGBhdzduOI2z-FyI4u_r6mgshHzG98ustvS0V5r9v6_REdPPVlbrB61sg-tSEzOFvGSDKE.SC8KcfH62fAttZk07w1PKg \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/keyring-test/bob.info b/.docker/container-state/atom-testnet-data/keyring-test/bob.info new file mode 100644 index 0000000000..8791ab5e6c --- /dev/null +++ b/.docker/container-state/atom-testnet-data/keyring-test/bob.info @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0wMyAxNDo0MzozMy40NzMyNDIwNTcgKzAzMDAgKzAzIG09KzAuMDQ4Mzc5Mjk4IiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiMXdoTEJ3Z1dJMGJNWmk3ZSJ9.AEebIffKKSlCliXJlq3ges1jhA5W-oSThTZRBXwR79ZQX_CkrQM_xg.pp8DeTHU5c_aDLe8.3y585dIrkDKD9NwhjfoQ3f_Wxt3VyZXgn-bS_FBbkHPyUi64yrSOLpxMZy8YWIUHLtJntofm0bgOfHvouwu2nwG73_mUJTB0EXNIBg0ynK_7dlkf_dlVWxX-dfzXnpNpvD94FJLfRbMeNLMWzdIpp7ggq-j2CbudBIjHTTXDv_nNi9Xy4kqu6endAy1XfThCR2KYaTNzBzBnad0RkT_rIsa7tOgo_huYyeik0ZujVt9__jEYjTgyML9fL-c_bAHdaqCEhvWQKxEYyfIZFm_rQFQgqVR0UYWGDfRdYRechQxE3CHOGQ-O-FnSp-ZQzoIq_t2X7bS4qG6nciADdfLtNXdCyUKjujp5zYx5NcoE2YYi6qacrjozsLIXWPlP0jf2Gx7pn1ljfOd7Pzs1O8c58UBQXsjNvtcqWKgnFmz6JEI9uuoVtGsAQz66.YDdLNtYlQPh8lNC7GfN82g \ No newline at end of file diff --git a/.docker/container-state/ibc-relayer-data/config/config.lock b/.docker/container-state/ibc-relayer-data/config/config.lock new file mode 100755 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/ibc-relayer-data/config/config.yaml b/.docker/container-state/ibc-relayer-data/config/config.yaml new file mode 100755 index 0000000000..b31a94b30c --- /dev/null +++ b/.docker/container-state/ibc-relayer-data/config/config.yaml @@ -0,0 +1,72 @@ +global: + api-listen-addr: :5183 + timeout: 10s + memo: "" + light-cache-size: 20 + log-level: "" + ics20-memo-limit: 0 + max-receiver-size: 0 +chains: + atom: + type: cosmos + value: + key-directory: /home/nimda/.relayer/keys/cosmoshub-testnet + key: test2 + chain-id: cosmoshub-testnet + rpc-addr: http://127.0.0.1:26658 + account-prefix: cosmos + keyring-backend: test + gas-adjustment: 1.8 + gas-prices: 0.5uatom + min-gas-amount: 0 + max-gas-amount: 0 + debug: true + timeout: 20s + block-timeout: "" + output-format: json + sign-mode: direct + extra-codecs: [] + coin-type: null + signing-algorithm: "" + broadcast-mode: batch + min-loop-duration: 0s + extension-options: [] + feegrants: null + nucleus: + type: cosmos + value: + key-directory: /home/nimda/.relayer/keys/nucleus-testnet + key: test1 + chain-id: nucleus-testnet + rpc-addr: http://127.0.0.1:26657 + account-prefix: nuc + keyring-backend: test + gas-adjustment: 1.8 + gas-prices: 0.5unucl + min-gas-amount: 0 + max-gas-amount: 0 + debug: true + timeout: 20s + block-timeout: "" + output-format: json + sign-mode: direct + extra-codecs: [] + coin-type: null + signing-algorithm: "" + broadcast-mode: batch + min-loop-duration: 0s + extension-options: [] + feegrants: null +paths: + nucleus-atom: + src: + chain-id: nucleus-testnet + client-id: 07-tendermint-1 + connection-id: connection-1 + dst: + chain-id: cosmoshub-testnet + client-id: 07-tendermint-1 + connection-id: connection-1 + src-channel-filter: + rule: "" + channel-list: [] diff --git a/.docker/container-state/ibc-relayer-data/keys/cosmoshub-testnet/keyring-test/b93a76d5dd5607a7512be7209a8fa9470038a1c1.address b/.docker/container-state/ibc-relayer-data/keys/cosmoshub-testnet/keyring-test/b93a76d5dd5607a7512be7209a8fa9470038a1c1.address new file mode 100755 index 0000000000..180a543e54 --- /dev/null +++ b/.docker/container-state/ibc-relayer-data/keys/cosmoshub-testnet/keyring-test/b93a76d5dd5607a7512be7209a8fa9470038a1c1.address @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0xMCAxMzoxODoyNC40OTM4ODA1NDcgKzAzMDAgKzAzIG09KzAuMDQzODQ1NDYwIiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiOUZyZVdPcTktbUdqejR3TSJ9.quAlH3Hr-wRWG4Njljd5_2NxlKUjjUBbxSbyolHvb6lj7de9zbiO3w.RWefar5xuM4KP6Hy.1lXXgeQih0ibaSNmXUWgz08L7v9ASYaIfkCeQgqlyO7UPnI6QvymXE44npML0tzjCLSvMuFGtSALgdQYVLr2NdbSDJ0CTJI0Eo26ll5H6t6IFWB4vhSENAg_5EvFgzIyllR7cVvMMjdt_O4QpjZG9CZIGxGZLBF1FdDNQke-vX5HbZOyf-zq9LDb-79bKj4tt1M8HaFw4LX1-UY503XzGHauFW4JoZx2PzNHkwCBhK4q5ygYT-8.7qJKM-Kvhl3jPk9QpE3FUQ \ No newline at end of file diff --git a/.docker/container-state/ibc-relayer-data/keys/cosmoshub-testnet/keyring-test/test2.info b/.docker/container-state/ibc-relayer-data/keys/cosmoshub-testnet/keyring-test/test2.info new file mode 100755 index 0000000000..1164b8c6a6 --- /dev/null +++ b/.docker/container-state/ibc-relayer-data/keys/cosmoshub-testnet/keyring-test/test2.info @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0xMCAxMzoxODoyNC40OTAxNDcyMzUgKzAzMDAgKzAzIG09KzAuMDQwMTEyMTU4IiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiZmJvNnpfaWpxc3loX2VOcyJ9.TvymOktT7IZ1BsPqGiIsi6Fn6be1jZL8hqw46fph1j2NhxP_1yq0cA.XLrFWk8BXL_WZ2aA.01hpPXfBtMMmjwdJgSprKCo_bFDTaRsZ5j9jxF5vVjDYKHjE0D2JFkDlhFaxNujvoEfnvreNTxhAHs9N7zKgVPlTXRK-PK0SoMeg3EBsmaPf4VIbpdQDXNtjP9MZmeta7KZ7Fur3kSGAmh8vGaVkKlep-3JCYNAbsXyympfpw5bgZNSjn380QEQ3C_SoyupcRm3Xb6NZgGYGPlfXDHdzEbao0pMopZN-OZFSobOEzdQv3CCNdvQa9vsO3lpyWKlwB7S3CxQDvTuWwnFIDA-p7Y19QoXp_i17lYRc73CDGubR8EFgHXfQub4du6RUZzRWhn8mThIZ-EqrWbN1YPmWNMlCiYSozhakAfpdc6v34ksTZjTEd5rSG7hQCfOaosK2XASnsAD9KtzOAs1UrYschCoSYRrDEgqv4miGrL-Vm0myNBVpP7tTEU5VkIU.Ko-RDxf7wgzDSz2tf5N67g \ No newline at end of file diff --git a/.docker/container-state/ibc-relayer-data/keys/nucleus-testnet/keyring-test/528769d0cfb20433088d2112cfce79ac4123f0c5.address b/.docker/container-state/ibc-relayer-data/keys/nucleus-testnet/keyring-test/528769d0cfb20433088d2112cfce79ac4123f0c5.address new file mode 100755 index 0000000000..bf29dd8d61 --- /dev/null +++ b/.docker/container-state/ibc-relayer-data/keys/nucleus-testnet/keyring-test/528769d0cfb20433088d2112cfce79ac4123f0c5.address @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0xMCAxMzoxODozMy41NjIyOTY1NDUgKzAzMDAgKzAzIG09KzAuMDQzODYyMzgwIiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiNUc3M3FJUFR4T29sWmZiMyJ9.-NzHhAimX8YJmZ62HjfwZ6HObP76uh9Li8OgK7CD63XTZLsktWRRcg.887WuM1rWe5dtD-3.kcUfpyEa6T3KhffcEThO5VHV_lOn7KL4HrVJV5gLX3bTS3nRH2eS4y0bo9ZBsXkuUjiPv7kAcFWTDbeGFn12C70Gd81YlBjT2lra-xsOS64n_BBgMYuk38BnAg5kFgQ5iqq4GCBtOQtYNoLx07AHXID3Wc3gXtZp_OB3JdfemibQMQFC8t2WQH3M0ndZCnM3GaqzdwZt_N1X4TE5luyNq-3llRTxg2WuTmhGQre90ygUIVocuRw.2WX1GaPqNiGxZSPqNfRIog \ No newline at end of file diff --git a/.docker/container-state/ibc-relayer-data/keys/nucleus-testnet/keyring-test/test1.info b/.docker/container-state/ibc-relayer-data/keys/nucleus-testnet/keyring-test/test1.info new file mode 100755 index 0000000000..84af982a42 --- /dev/null +++ b/.docker/container-state/ibc-relayer-data/keys/nucleus-testnet/keyring-test/test1.info @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0xMCAxMzoxODozMy41NTg2NDU1NzQgKzAzMDAgKzAzIG09KzAuMDQwMjExNDA5IiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoienJNclpwdUhJM1VTd21pZCJ9.Xp7j59CIMe3Ytjudw5fejiLzSbMSPppSrjnP-zci5CNlgQ0wkHmfhQ.B4tCZcVPF0lfEFsw.bHIRYqPYroRbvOqz5gac7w3K5M-uORPQvgyE6GF5c_-Nxu2OhCRr7anGVPxmYp3p3whOp5ky9GoVgnle7a4nUR9JPkgv94xI-bYgTZ7kTipuvom20vsL1zc-xqGSYLjw8skhVMZ0thkOQa8spuqMXtB3YJdPF43rA0U2DBGq8rCvoy8mDdLfuYCqJXSULKxjIlMK8zRFGp5RDM3IXknrsUk-yyfR5Bs9WUE0R3hrsnL0SU4r6QjhkpKrvBziKPYVrgul9bix68x1ZC8Pb0bZObhhmCw2CPH_wHscVT6hsBDzRmpsU4OvIYZJbMmRPpYm_ylTB4TInhZGlmOwJXk_Yv0X_ojlXxjB08prJKgufxSkAG8tkvmQW_bLmDDxPnRvH_q9mHEpwBGLP4es3_sAu8mEtb-uwwZlx4DWY_tepJcXOLDDeTp1J79GZWg.RkIc0UsZDYMh3AexHloGMA \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/addrbook.json b/.docker/container-state/nucleus-testnet-data/config/addrbook.json new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/addrbook.json @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/app.toml b/.docker/container-state/nucleus-testnet-data/config/app.toml new file mode 100644 index 0000000000..a01507cd51 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/app.toml @@ -0,0 +1,245 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +############################################################################### +### Base Configuration ### +############################################################################### + +# The minimum gas prices a validator is willing to accept for processing a +# transaction. A transaction's fees must meet the minimum of any denomination +# specified in this config (e.g. 0.25token1;0.0001token2). +minimum-gas-prices = "0.0025unucl" + +# default: the last 362880 states are kept, pruning at 10 block intervals +# nothing: all historic states will be saved, nothing will be deleted (i.e. archiving node) +# everything: 2 latest states will be kept; pruning at 10 block intervals. +# custom: allow pruning options to be manually specified through 'pruning-keep-recent', and 'pruning-interval' +pruning = "default" + +# These are applied if and only if the pruning strategy is custom. +pruning-keep-recent = "0" +pruning-interval = "0" + +# HaltHeight contains a non-zero block height at which a node will gracefully +# halt and shutdown that can be used to assist upgrades and testing. +# +# Note: Commitment of state will be attempted on the corresponding block. +halt-height = 0 + +# HaltTime contains a non-zero minimum block time (in Unix seconds) at which +# a node will gracefully halt and shutdown that can be used to assist upgrades +# and testing. +# +# Note: Commitment of state will be attempted on the corresponding block. +halt-time = 0 + +# MinRetainBlocks defines the minimum block height offset from the current +# block being committed, such that all blocks past this offset are pruned +# from Tendermint. It is used as part of the process of determining the +# ResponseCommit.RetainHeight value during ABCI Commit. A value of 0 indicates +# that no blocks should be pruned. +# +# This configuration value is only responsible for pruning Tendermint blocks. +# It has no bearing on application state pruning which is determined by the +# "pruning-*" configurations. +# +# Note: Tendermint block pruning is dependant on this parameter in conunction +# with the unbonding (safety threshold) period, state pruning and state sync +# snapshot parameters to determine the correct minimum value of +# ResponseCommit.RetainHeight. +min-retain-blocks = 0 + +# InterBlockCache enables inter-block caching. +inter-block-cache = true + +# IndexEvents defines the set of events in the form {eventType}.{attributeKey}, +# which informs Tendermint what to index. If empty, all events will be indexed. +# +# Example: +# ["message.sender", "message.recipient"] +index-events = [] + +# IavlCacheSize set the size of the iavl tree cache. +# Default cache size is 50mb. +iavl-cache-size = 781250 + +# IavlDisableFastNode enables or disables the fast node feature of IAVL. +# Default is false. +iavl-disable-fastnode = false + +# AppDBBackend defines the database backend type to use for the application and snapshots DBs. +# An empty string indicates that a fallback will be used. +# First fallback is the deprecated compile-time types.DBBackend value. +# Second fallback (if the types.DBBackend also isn't set), is the db-backend value set in Tendermint's config.toml. +app-db-backend = "" + +############################################################################### +### Telemetry Configuration ### +############################################################################### + +[telemetry] + +# Prefixed with keys to separate services. +service-name = "" + +# Enabled enables the application telemetry functionality. When enabled, +# an in-memory sink is also enabled by default. Operators may also enabled +# other sinks such as Prometheus. +enabled = false + +# Enable prefixing gauge values with hostname. +enable-hostname = false + +# Enable adding hostname to labels. +enable-hostname-label = false + +# Enable adding service to labels. +enable-service-label = false + +# PrometheusRetentionTime, when positive, enables a Prometheus metrics sink. +prometheus-retention-time = 0 + +# GlobalLabels defines a global set of name/value label tuples applied to all +# metrics emitted using the wrapper functions defined in telemetry package. +# +# Example: +# [["chain_id", "cosmoshub-1"]] +global-labels = [ +] + +############################################################################### +### API Configuration ### +############################################################################### + +[api] + +# Enable defines if the API server should be enabled. +enable = true + +# Swagger defines if swagger documentation should automatically be registered. +swagger = true + +# Address defines the API server to listen on. +address = "tcp://0.0.0.0:1317" + +# MaxOpenConnections defines the number of maximum open connections. +max-open-connections = 1000 + +# RPCReadTimeout defines the Tendermint RPC read timeout (in seconds). +rpc-read-timeout = 10 + +# RPCWriteTimeout defines the Tendermint RPC write timeout (in seconds). +rpc-write-timeout = 0 + +# RPCMaxBodyBytes defines the Tendermint maximum response body (in bytes). +rpc-max-body-bytes = 1000000 + +# EnableUnsafeCORS defines if CORS should be enabled (unsafe - use it at your own risk). +enabled-unsafe-cors = true + +############################################################################### +### Rosetta Configuration ### +############################################################################### + +[rosetta] + +# Enable defines if the Rosetta API server should be enabled. +enable = false + +# Address defines the Rosetta API server to listen on. +address = ":8080" + +# Network defines the name of the blockchain that will be returned by Rosetta. +blockchain = "app" + +# Network defines the name of the network that will be returned by Rosetta. +network = "network" + +# Retries defines the number of retries when connecting to the node before failing. +retries = 3 + +# Offline defines if Rosetta server should run in offline mode. +offline = false + +# EnableDefaultSuggestedFee defines if the server should suggest fee by default. +# If 'construction/medata' is called without gas limit and gas price, +# suggested fee based on gas-to-suggest and denom-to-suggest will be given. +enable-fee-suggestion = false + +# GasToSuggest defines gas limit when calculating the fee +gas-to-suggest = 200000 + +# DenomToSuggest defines the defult denom for fee suggestion. +# Price must be in minimum-gas-prices. +denom-to-suggest = "unucl" + +############################################################################### +### gRPC Configuration ### +############################################################################### + +[grpc] + +# Enable defines if the gRPC server should be enabled. +enable = true + +# Address defines the gRPC server address to bind to. +address = "0.0.0.0:9090" + +# MaxRecvMsgSize defines the max message size in bytes the server can receive. +# The default value is 10MB. +max-recv-msg-size = "10485760" + +# MaxSendMsgSize defines the max message size in bytes the server can send. +# The default value is math.MaxInt32. +max-send-msg-size = "2147483647" + +############################################################################### +### gRPC Web Configuration ### +############################################################################### + +[grpc-web] + +# GRPCWebEnable defines if the gRPC-web should be enabled. +# NOTE: gRPC must also be enabled, otherwise, this configuration is a no-op. +enable = true + +# Address defines the gRPC-web server address to bind to. +address = "0.0.0.0:9091" + +# EnableUnsafeCORS defines if CORS should be enabled (unsafe - use it at your own risk). +enable-unsafe-cors = true + +############################################################################### +### State Sync Configuration ### +############################################################################### + +# State sync snapshots allow other nodes to rapidly join the network without replaying historical +# blocks, instead downloading and applying a snapshot of the application state at a given height. +[state-sync] + +# snapshot-interval specifies the block interval at which local state sync snapshots are +# taken (0 to disable). +snapshot-interval = 0 + +# snapshot-keep-recent specifies the number of recent snapshots to keep and serve (0 to keep all). +snapshot-keep-recent = 2 + +############################################################################### +### Store / State Streaming ### +############################################################################### + +[store] +streamers = [] + +[streamers] +[streamers.file] +keys = ["*", ] +write_dir = "" +prefix = "" + +[wasm] +# This is the maximum sdk gas (wasm and storage) that we allow for any x/wasm "smart" queries +query_gas_limit = 300000 +# This is the number of wasm vm instances we keep cached in memory for speed-up +# Warning: this is currently unstable and may lead to crashes, best to keep for 0 unless testing locally +lru_size = 0 diff --git a/.docker/container-state/nucleus-testnet-data/config/client.toml b/.docker/container-state/nucleus-testnet-data/config/client.toml new file mode 100644 index 0000000000..4de71d43a6 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/client.toml @@ -0,0 +1,17 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +############################################################################### +### Client Configuration ### +############################################################################### + +# The network chain ID +chain-id = "nucleus-testnet" +# The keyring's backend, where the keys are stored (os|file|kwallet|pass|test|memory) +keyring-backend = "test" +# CLI output format (text|json) +output = "text" +# : to Tendermint RPC interface for this chain +node = "tcp://localhost:26657" +# Transaction broadcasting mode (sync|async|block) +broadcast-mode = "sync" diff --git a/.docker/container-state/nucleus-testnet-data/config/config.toml b/.docker/container-state/nucleus-testnet-data/config/config.toml new file mode 100644 index 0000000000..8292219e1f --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/config.toml @@ -0,0 +1,466 @@ +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +# NOTE: Any path below can be absolute (e.g. "/var/myawesomeapp/data") or +# relative to the home directory (e.g. "data"). The home directory is +# "$HOME/.tendermint" by default, but could be changed via $TMHOME env variable +# or --home cmd flag. + +####################################################################### +### Main Base Config Options ### +####################################################################### + +# TCP or UNIX socket address of the ABCI application, +# or the name of an ABCI application compiled in with the Tendermint binary +proxy_app = "tcp://127.0.0.1:26658" + +# A custom human readable name for this node +moniker = "nimda" + +# If this node is many blocks behind the tip of the chain, FastSync +# allows them to catchup quickly by downloading blocks in parallel +# and verifying their commits +fast_sync = true + +# Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb +# * goleveldb (github.com/syndtr/goleveldb - most popular implementation) +# - pure go +# - stable +# * cleveldb (uses levigo wrapper) +# - fast +# - requires gcc +# - use cleveldb build tag (go build -tags cleveldb) +# * boltdb (uses etcd's fork of bolt - github.com/etcd-io/bbolt) +# - EXPERIMENTAL +# - may be faster is some use-cases (random reads - indexer) +# - use boltdb build tag (go build -tags boltdb) +# * rocksdb (uses github.com/tecbot/gorocksdb) +# - EXPERIMENTAL +# - requires gcc +# - use rocksdb build tag (go build -tags rocksdb) +# * badgerdb (uses github.com/dgraph-io/badger) +# - EXPERIMENTAL +# - use badgerdb build tag (go build -tags badgerdb) +db_backend = "goleveldb" + +# Database directory +db_dir = "data" + +# Output level for logging, including package level options +log_level = "info" + +# Output format: 'plain' (colored text) or 'json' +log_format = "plain" + +##### additional base config options ##### + +# Path to the JSON file containing the initial validator set and other meta data +genesis_file = "config/genesis.json" + +# Path to the JSON file containing the private key to use as a validator in the consensus protocol +priv_validator_key_file = "config/priv_validator_key.json" + +# Path to the JSON file containing the last sign state of a validator +priv_validator_state_file = "data/priv_validator_state.json" + +# TCP or UNIX socket address for Tendermint to listen on for +# connections from an external PrivValidator process +priv_validator_laddr = "" + +# Path to the JSON file containing the private key to use for node authentication in the p2p protocol +node_key_file = "config/node_key.json" + +# Mechanism to connect to the ABCI application: socket | grpc +abci = "socket" + +# If true, query the ABCI app on connecting to a new peer +# so the app can decide if we should keep the connection or not +filter_peers = false + + +####################################################################### +### Advanced Configuration Options ### +####################################################################### + +####################################################### +### RPC Server Configuration Options ### +####################################################### +[rpc] + +# TCP or UNIX socket address for the RPC server to listen on +laddr = "tcp://0.0.0.0:26657" + +# A list of origins a cross-domain request can be executed from +# Default value '[]' disables cors support +# Use '["*"]' to allow any origin +cors_allowed_origins = [] + +# A list of methods the client is allowed to use with cross-domain requests +cors_allowed_methods = ["HEAD", "GET", "POST", ] + +# A list of non simple headers the client is allowed to use with cross-domain requests +cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time", ] + +# TCP or UNIX socket address for the gRPC server to listen on +# NOTE: This server only supports /broadcast_tx_commit +grpc_laddr = "" + +# Maximum number of simultaneous connections. +# Does not include RPC (HTTP&WebSocket) connections. See max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +grpc_max_open_connections = 900 + +# Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool +unsafe = false + +# Maximum number of simultaneous connections (including WebSocket). +# Does not include gRPC connections. See grpc_max_open_connections +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +# Should be < {ulimit -Sn} - {MaxNumInboundPeers} - {MaxNumOutboundPeers} - {N of wal, db and other open files} +# 1024 - 40 - 10 - 50 = 924 = ~900 +max_open_connections = 900 + +# Maximum number of unique clientIDs that can /subscribe +# If you're using /broadcast_tx_commit, set to the estimated maximum number +# of broadcast_tx_commit calls per block. +max_subscription_clients = 100 + +# Maximum number of unique queries a given client can /subscribe to +# If you're using GRPC (or Local RPC client) and /broadcast_tx_commit, set to +# the estimated # maximum number of broadcast_tx_commit calls per block. +max_subscriptions_per_client = 5 + +# Experimental parameter to specify the maximum number of events a node will +# buffer, per subscription, before returning an error and closing the +# subscription. Must be set to at least 100, but higher values will accommodate +# higher event throughput rates (and will use more memory). +experimental_subscription_buffer_size = 200 + +# Experimental parameter to specify the maximum number of RPC responses that +# can be buffered per WebSocket client. If clients cannot read from the +# WebSocket endpoint fast enough, they will be disconnected, so increasing this +# parameter may reduce the chances of them being disconnected (but will cause +# the node to use more memory). +# +# Must be at least the same as "experimental_subscription_buffer_size", +# otherwise connections could be dropped unnecessarily. This value should +# ideally be somewhat higher than "experimental_subscription_buffer_size" to +# accommodate non-subscription-related RPC responses. +experimental_websocket_write_buffer_size = 200 + +# If a WebSocket client cannot read fast enough, at present we may +# silently drop events instead of generating an error or disconnecting the +# client. +# +# Enabling this experimental parameter will cause the WebSocket connection to +# be closed instead if it cannot read fast enough, allowing for greater +# predictability in subscription behaviour. +experimental_close_on_slow_client = false + +# How long to wait for a tx to be committed during /broadcast_tx_commit. +# WARNING: Using a value larger than 10s will result in increasing the +# global HTTP write timeout, which applies to all connections and endpoints. +# See https://github.com/tendermint/tendermint/issues/3435 +timeout_broadcast_tx_commit = "10s" + +# Maximum size of request body, in bytes +max_body_bytes = 1000000 + +# Maximum size of request header, in bytes +max_header_bytes = 1048576 + +# The path to a file containing certificate that is used to create the HTTPS server. +# Might be either absolute path or path related to Tendermint's config directory. +# If the certificate is signed by a certificate authority, +# the certFile should be the concatenation of the server's certificate, any intermediates, +# and the CA's certificate. +# NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. +# Otherwise, HTTP server is run. +tls_cert_file = "" + +# The path to a file containing matching private key that is used to create the HTTPS server. +# Might be either absolute path or path related to Tendermint's config directory. +# NOTE: both tls-cert-file and tls-key-file must be present for Tendermint to create HTTPS server. +# Otherwise, HTTP server is run. +tls_key_file = "" + +# pprof listen address (https://golang.org/pkg/net/http/pprof) +pprof_laddr = "localhost:6060" + +####################################################### +### P2P Configuration Options ### +####################################################### +[p2p] + +# Address to listen for incoming connections +laddr = "tcp://0.0.0.0:26656" + +# Address to advertise to peers for them to dial +# If empty, will use the same port as the laddr, +# and will introspect on the listener or use UPnP +# to figure out the address. ip and port are required +# example: 159.89.10.97:26656 +external_address = "" + +# Comma separated list of seed nodes to connect to +seeds = "" + +# Comma separated list of nodes to keep persistent connections to +persistent_peers = "" + +# UPNP port forwarding +upnp = false + +# Path to address book +addr_book_file = "config/addrbook.json" + +# Set true for strict address routability rules +# Set false for private or local networks +addr_book_strict = true + +# Maximum number of inbound peers +max_num_inbound_peers = 40 + +# Maximum number of outbound peers to connect to, excluding persistent peers +max_num_outbound_peers = 10 + +# List of node IDs, to which a connection will be (re)established ignoring any existing limits +unconditional_peer_ids = "" + +# Maximum pause when redialing a persistent peer (if zero, exponential backoff is used) +persistent_peers_max_dial_period = "0s" + +# Time to wait before flushing messages out on the connection +flush_throttle_timeout = "100ms" + +# Maximum size of a message packet payload, in bytes +max_packet_msg_payload_size = 1024 + +# Rate at which packets can be sent, in bytes/second +send_rate = 5120000 + +# Rate at which packets can be received, in bytes/second +recv_rate = 5120000 + +# Set true to enable the peer-exchange reactor +pex = true + +# Seed mode, in which node constantly crawls the network and looks for +# peers. If another node asks it for addresses, it responds and disconnects. +# +# Does not work if the peer-exchange reactor is disabled. +seed_mode = false + +# Comma separated list of peer IDs to keep private (will not be gossiped to other peers) +private_peer_ids = "" + +# Toggle to disable guard against peers connecting from the same ip. +allow_duplicate_ip = false + +# Peer connection configuration. +handshake_timeout = "20s" +dial_timeout = "3s" + +####################################################### +### Mempool Configuration Option ### +####################################################### +[mempool] + +# Mempool version to use: +# 1) "v0" - (default) FIFO mempool. +# 2) "v1" - prioritized mempool. +version = "v0" + +recheck = true +broadcast = true +wal_dir = "" + +# Maximum number of transactions in the mempool +size = 5000 + +# Limit the total size of all txs in the mempool. +# This only accounts for raw transactions (e.g. given 1MB transactions and +# max_txs_bytes=5MB, mempool will only accept 5 transactions). +max_txs_bytes = 1073741824 + +# Size of the cache (used to filter transactions we saw earlier) in transactions +cache_size = 10000 + +# Do not remove invalid transactions from the cache (default: false) +# Set to true if it's not possible for any invalid transaction to become valid +# again in the future. +keep-invalid-txs-in-cache = false + +# Maximum size of a single transaction. +# NOTE: the max size of a tx transmitted over the network is {max_tx_bytes}. +max_tx_bytes = 1048576 + +# Maximum size of a batch of transactions to send to a peer +# Including space needed by encoding (one varint per transaction). +# XXX: Unused due to https://github.com/tendermint/tendermint/issues/5796 +max_batch_bytes = 0 + +# ttl-duration, if non-zero, defines the maximum amount of time a transaction +# can exist for in the mempool. +# +# Note, if ttl-num-blocks is also defined, a transaction will be removed if it +# has existed in the mempool at least ttl-num-blocks number of blocks or if it's +# insertion time into the mempool is beyond ttl-duration. +ttl-duration = "0s" + +# ttl-num-blocks, if non-zero, defines the maximum number of blocks a transaction +# can exist for in the mempool. +# +# Note, if ttl-duration is also defined, a transaction will be removed if it +# has existed in the mempool at least ttl-num-blocks number of blocks or if +# it's insertion time into the mempool is beyond ttl-duration. +ttl-num-blocks = 0 + +####################################################### +### State Sync Configuration Options ### +####################################################### +[statesync] +# State sync rapidly bootstraps a new node by discovering, fetching, and restoring a state machine +# snapshot from peers instead of fetching and replaying historical blocks. Requires some peers in +# the network to take and serve state machine snapshots. State sync is not attempted if the node +# has any local state (LastBlockHeight > 0). The node will have a truncated block history, +# starting from the height of the snapshot. +enable = false + +# RPC servers (comma-separated) for light client verification of the synced state machine and +# retrieval of state data for node bootstrapping. Also needs a trusted height and corresponding +# header hash obtained from a trusted source, and a period during which validators can be trusted. +# +# For Cosmos SDK-based chains, trust_period should usually be about 2/3 of the unbonding time (~2 +# weeks) during which they can be financially punished (slashed) for misbehavior. +rpc_servers = "" +trust_height = 0 +trust_hash = "" +trust_period = "168h0m0s" + +# Time to spend discovering snapshots before initiating a restore. +discovery_time = "15s" + +# Temporary directory for state sync snapshot chunks, defaults to the OS tempdir (typically /tmp). +# Will create a new, randomly named directory within, and remove it when done. +temp_dir = "" + +# The timeout duration before re-requesting a chunk, possibly from a different +# peer (default: 1 minute). +chunk_request_timeout = "10s" + +# The number of concurrent chunk fetchers to run (default: 1). +chunk_fetchers = "4" + +####################################################### +### Fast Sync Configuration Connections ### +####################################################### +[fastsync] + +# Fast Sync version to use: +# 1) "v0" (default) - the legacy fast sync implementation +# 2) "v1" - refactor of v0 version for better testability +# 2) "v2" - complete redesign of v0, optimized for testability & readability +version = "v0" + +####################################################### +### Consensus Configuration Options ### +####################################################### +[consensus] + +wal_file = "data/cs.wal/wal" + +# How long we wait for a proposal block before prevoting nil +timeout_propose = "3s" +# How much timeout_propose increases with each round +timeout_propose_delta = "500ms" +# How long we wait after receiving +2/3 prevotes for “anything” (ie. not a single block or nil) +timeout_prevote = "1s" +# How much the timeout_prevote increases with each round +timeout_prevote_delta = "500ms" +# How long we wait after receiving +2/3 precommits for “anything” (ie. not a single block or nil) +timeout_precommit = "1s" +# How much the timeout_precommit increases with each round +timeout_precommit_delta = "500ms" +# How long we wait after committing a block, before starting on the new +# height (this gives us a chance to receive some more precommits, even +# though we already have +2/3). +timeout_commit = "5s" + +# How many blocks to look back to check existence of the node's consensus votes before joining consensus +# When non-zero, the node will panic upon restart +# if the same consensus key was used to sign {double_sign_check_height} last blocks. +# So, validators should stop the state machine, wait for some blocks, and then restart the state machine to avoid panic. +double_sign_check_height = 0 + +# Make progress as soon as we have all the precommits (as if TimeoutCommit = 0) +skip_timeout_commit = false + +# EmptyBlocks mode and possible interval between empty blocks +create_empty_blocks = true +create_empty_blocks_interval = "0s" + +# Reactor sleep duration parameters +peer_gossip_sleep_duration = "100ms" +peer_query_maj23_sleep_duration = "2s" + +####################################################### +### Storage Configuration Options ### +####################################################### +[storage] + +# Set to true to discard ABCI responses from the state store, which can save a +# considerable amount of disk space. Set to false to ensure ABCI responses are +# persisted. ABCI responses are required for /block_results RPC queries, and to +# reindex events in the command-line tool. +discard_abci_responses = false + +####################################################### +### Transaction Indexer Configuration Options ### +####################################################### +[tx_index] + +# What indexer to use for transactions +# +# The application will set which txs to index. In some cases a node operator will be able +# to decide which txs to index based on configuration set in the application. +# +# Options: +# 1) "null" +# 2) "kv" (default) - the simplest possible indexer, backed by key-value storage (defaults to levelDB; see DBBackend). +# - When "kv" is chosen "tx.height" and "tx.hash" will always be indexed. +# 3) "psql" - the indexer services backed by PostgreSQL. +# When "kv" or "psql" is chosen "tx.height" and "tx.hash" will always be indexed. +indexer = "kv" + +# The PostgreSQL connection configuration, the connection format: +# postgresql://:@:/? +psql-conn = "" + +####################################################### +### Instrumentation Configuration Options ### +####################################################### +[instrumentation] + +# When true, Prometheus metrics are served under /metrics on +# PrometheusListenAddr. +# Check out the documentation for the list of available metrics. +prometheus = false + +# Address to listen for Prometheus collector(s) connections +prometheus_listen_addr = ":26660" + +# Maximum number of simultaneous connections. +# If you want to accept a larger number than the default, make sure +# you increase your OS limits. +# 0 - unlimited. +max_open_connections = 3 + +# Instrumentation namespace +namespace = "tendermint" diff --git a/.docker/container-state/nucleus-testnet-data/config/genesis.json b/.docker/container-state/nucleus-testnet-data/config/genesis.json new file mode 100644 index 0000000000..57d87a8ef0 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/genesis.json @@ -0,0 +1,463 @@ +{ + "genesis_time": "2024-06-04T10:09:21.465680574Z", + "chain_id": "nucleus-testnet", + "initial_height": "1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + }, + "version": {} + }, + "app_hash": "", + "app_state": { + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "nuc15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89hzvgjk", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "nuc1yfygf0zr5s69ces9r0h72hqv23nkqz9nej732n", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "nuc1qqwg79cpe2c9jtxklu8lxl30hj3qfzd3rzgs3x", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "nuc1e0rx87mdj79zejewuc4jg7ql9ud2286g7x3t2z", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "nuc1erfnkjsmalkwtvj44qnfr2drfzdt4n9ledw63y", + "pub_key": null, + "account_number": "0", + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "address": "nuc1z609g9z8ef4jlkcfgg36c2ljgcmna50l77n078", + "pub_key": null, + "account_number": "0", + "sequence": "0" + } + ] + }, + "authz": { + "authorization": [] + }, + "bank": { + "params": { + "send_enabled": [], + "default_send_enabled": true + }, + "balances": [ + { + "address": "nuc1qqwg79cpe2c9jtxklu8lxl30hj3qfzd3rzgs3x", + "coins": [ + { + "denom": "ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C", + "amount": "10000000000" + }, + { + "denom": "unucl", + "amount": "10000000000" + } + ] + }, + { + "address": "nuc1z609g9z8ef4jlkcfgg36c2ljgcmna50l77n078", + "coins": [ + { + "denom": "ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C", + "amount": "10000000000" + }, + { + "denom": "unucl", + "amount": "10000000000" + } + ] + }, + { + "address": "nuc1yfygf0zr5s69ces9r0h72hqv23nkqz9nej732n", + "coins": [] + }, + { + "address": "nuc15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89hzvgjk", + "coins": [ + { + "denom": "unucl", + "amount": "10000000000" + } + ] + }, + { + "address": "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr", + "coins": [ + { + "denom": "ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C", + "amount": "10000000000" + }, + { + "denom": "unucl", + "amount": "10000000000" + } + ] + }, + { + "address": "nuc1erfnkjsmalkwtvj44qnfr2drfzdt4n9ledw63y", + "coins": [ + { + "denom": "ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C", + "amount": "10000000000" + }, + { + "denom": "unucl", + "amount": "10000000000" + } + ] + }, + { + "address": "nuc1e0rx87mdj79zejewuc4jg7ql9ud2286g7x3t2z", + "coins": [ + { + "denom": "ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C", + "amount": "10000000000" + }, + { + "denom": "unucl", + "amount": "10000000000" + } + ] + } + ], + "supply": [], + "denom_metadata": [ + { + "description": "The native staking token of Nucleus.", + "denom_units": [ + { + "denom": "unucl", + "exponent": 0, + "aliases": [ + "micronucl" + ] + }, + { + "denom": "mnucl", + "exponent": 3, + "aliases": [ + "millinucl" + ] + }, + { + "denom": "nucl", + "exponent": 6, + "aliases": [] + } + ], + "base": "unucl", + "display": "nucl", + "name": "", + "symbol": "", + "uri": "", + "uri_hash": "" + } + ] + }, + "capability": { + "index": "1", + "owners": [] + }, + "crisis": { + "constant_fee": { + "denom": "unucl", + "amount": "1000" + } + }, + "distribution": { + "params": { + "community_tax": "0.020000000000000000", + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "evidence": { + "evidence": [] + }, + "feegrant": { + "allowances": [] + }, + "genutil": { + "gen_txs": [ + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "nimda", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.050000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "nuc15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89hzvgjk", + "validator_address": "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "lw88sSAfwL1TDk4KEXxW/hZvYNxFEDUuI6LrS+tqsdo=" + }, + "value": { + "denom": "unucl", + "amount": "200000000" + } + } + ], + "memo": "12c7b58d43fe064e67fda22a2bd0598151308e32@192.168.1.24:26656", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [ + { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AsgJRkBRhm9bOTsGtcByi+fBbdjEp6BB59AiFHDqDRN0" + }, + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "sequence": "0" + } + ], + "fee": { + "amount": [ + { + "denom": "unucl", + "amount": "2500" + } + ], + "gas_limit": "200000", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [ + "yHNLaI5W2JnH5fa3Wn9Ma/YNt4HpeZkfCgRrl7EyX8svAKex8qDKJSeEl6zfUJSDiRYkkuPu67BOed8XUexA4w==" + ] + } + ] + }, + "gov": { + "starting_proposal_id": "1", + "deposits": [], + "votes": [], + "proposals": [], + "deposit_params": { + "min_deposit": [ + { + "denom": "unucl", + "amount": "10000000" + } + ], + "max_deposit_period": "172800s" + }, + "voting_params": { + "voting_period": "172800s" + }, + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto_threshold": "0.334000000000000000" + } + }, + "group": { + "group_seq": "0", + "groups": [], + "group_members": [], + "group_policy_seq": "0", + "group_policies": [], + "proposal_seq": "0", + "proposals": [], + "votes": [] + }, + "htlc": { + "params": { + "asset_params": [] + }, + "htlcs": [], + "supplies": [], + "previous_block_time": "1970-01-01T00:00:01Z" + }, + "ibc": { + "client_genesis": { + "clients": [], + "clients_consensus": [], + "clients_metadata": [], + "params": { + "allowed_clients": [ + "06-solomachine", + "07-tendermint" + ] + }, + "create_localhost": false, + "next_client_sequence": "0" + }, + "connection_genesis": { + "connections": [], + "client_connection_paths": [], + "next_connection_sequence": "0", + "params": { + "max_expected_time_per_block": "30000000000" + } + }, + "channel_genesis": { + "channels": [], + "acknowledgements": [], + "commitments": [], + "receipts": [], + "send_sequences": [], + "recv_sequences": [], + "ack_sequences": [], + "next_channel_sequence": "0" + } + }, + "interchainaccounts": { + "controller_genesis_state": { + "active_channels": [], + "interchain_accounts": [], + "ports": [], + "params": { + "controller_enabled": true + } + }, + "host_genesis_state": { + "active_channels": [], + "interchain_accounts": [], + "port": "icahost", + "params": { + "host_enabled": true, + "allow_messages": [] + } + } + }, + "mint": { + "minter": { + "inflation": "0.130000000000000000", + "annual_provisions": "0.000000000000000000" + }, + "params": { + "mint_denom": "unucl", + "inflation_rate_change": "0.130000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "goal_bonded": "0.670000000000000000", + "blocks_per_year": "6311520" + } + }, + "nucleus": { + "params": {} + }, + "params": null, + "slashing": { + "params": { + "signed_blocks_window": "100", + "min_signed_per_window": "0.500000000000000000", + "downtime_jail_duration": "600s", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": [], + "missed_blocks": [] + }, + "staking": { + "params": { + "unbonding_time": "1814400s", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 10000, + "bond_denom": "unucl", + "min_commission_rate": "0.000000000000000000" + }, + "last_total_power": "0", + "last_validator_powers": [], + "validators": [], + "delegations": [], + "unbonding_delegations": [], + "redelegations": [], + "exported": false + }, + "transfer": { + "port_id": "transfer", + "denom_traces": [], + "params": { + "send_enabled": true, + "receive_enabled": true + } + }, + "upgrade": {}, + "vesting": {} + } +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/gentx/gentx-12c7b58d43fe064e67fda22a2bd0598151308e32.json b/.docker/container-state/nucleus-testnet-data/config/gentx/gentx-12c7b58d43fe064e67fda22a2bd0598151308e32.json new file mode 100644 index 0000000000..3de3e2b2be --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/gentx/gentx-12c7b58d43fe064e67fda22a2bd0598151308e32.json @@ -0,0 +1 @@ +{"body":{"messages":[{"@type":"/cosmos.staking.v1beta1.MsgCreateValidator","description":{"moniker":"nimda","identity":"","website":"","security_contact":"","details":""},"commission":{"rate":"0.050000000000000000","max_rate":"0.200000000000000000","max_change_rate":"0.010000000000000000"},"min_self_delegation":"1","delegator_address":"nuc15d4sf4z6y0vk9dnum8yzkvr9c3wq4q89hzvgjk","validator_address":"nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu","pubkey":{"@type":"/cosmos.crypto.ed25519.PubKey","key":"lw88sSAfwL1TDk4KEXxW/hZvYNxFEDUuI6LrS+tqsdo="},"value":{"denom":"unucl","amount":"200000000"}}],"memo":"12c7b58d43fe064e67fda22a2bd0598151308e32@192.168.1.24:26656","timeout_height":"0","extension_options":[],"non_critical_extension_options":[]},"auth_info":{"signer_infos":[{"public_key":{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"AsgJRkBRhm9bOTsGtcByi+fBbdjEp6BB59AiFHDqDRN0"},"mode_info":{"single":{"mode":"SIGN_MODE_DIRECT"}},"sequence":"0"}],"fee":{"amount":[{"denom":"unucl","amount":"2500"}],"gas_limit":"200000","payer":"","granter":""},"tip":null},"signatures":["yHNLaI5W2JnH5fa3Wn9Ma/YNt4HpeZkfCgRrl7EyX8svAKex8qDKJSeEl6zfUJSDiRYkkuPu67BOed8XUexA4w=="]} diff --git a/.docker/container-state/nucleus-testnet-data/config/node_key.json b/.docker/container-state/nucleus-testnet-data/config/node_key.json new file mode 100644 index 0000000000..1d8587df8d --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/node_key.json @@ -0,0 +1 @@ +{"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"RFJZUn1lEIM490WPhRFp/ktmWxFIfp9ncLc9N8qwlYeGOnl+VaXhCKZ8yt1ad73NxnTkPfjoJ2L5FSvezlvAAA=="}} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/priv_validator_key.json b/.docker/container-state/nucleus-testnet-data/config/priv_validator_key.json new file mode 100644 index 0000000000..83e5195aca --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/priv_validator_key.json @@ -0,0 +1,11 @@ +{ + "address": "E0ADD4CA24ED5ED685BAFEAC1897CE2E63CF2B5E", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "lw88sSAfwL1TDk4KEXxW/hZvYNxFEDUuI6LrS+tqsdo=" + }, + "priv_key": { + "type": "tendermint/PrivKeyEd25519", + "value": "8H+X2GroxcbOntGYOPzKDqoQsA2du++m7UDtJzrf18uXDzyxIB/AvVMOTgoRfFb+Fm9g3EUQNS4joutL62qx2g==" + } +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-01554913484981479804 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-01554913484981479804 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-01554913484981479804 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-03684770830230437769 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-03684770830230437769 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-03684770830230437769 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-05951320143323737291 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-05951320143323737291 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-05951320143323737291 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-06122022161526377170 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-06122022161526377170 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-06122022161526377170 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-07519249100848775322 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-07519249100848775322 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-07519249100848775322 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-1868122991755536167 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-1868122991755536167 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-1868122991755536167 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-5942702362113201644 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-5942702362113201644 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-5942702362113201644 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-6555242139890704645 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-6555242139890704645 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-6555242139890704645 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-7440457262784007142 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-7440457262784007142 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-7440457262784007142 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-7683025105498428194 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-7683025105498428194 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-7683025105498428194 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000124.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000124.ldb new file mode 100644 index 0000000000..626e16207a Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000124.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000125.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000125.ldb new file mode 100644 index 0000000000..3590e1577d Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000125.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000126.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000126.ldb new file mode 100644 index 0000000000..c9145d6ba1 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000126.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000127.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000127.ldb new file mode 100644 index 0000000000..d35ee8f42a Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000127.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000130.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000130.ldb new file mode 100644 index 0000000000..70efc2fb72 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000130.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000131.log b/.docker/container-state/nucleus-testnet-data/data/application.db/000131.log new file mode 100644 index 0000000000..84e0725816 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000131.log differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT new file mode 100644 index 0000000000..c39c670731 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000132 diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT.bak new file mode 100644 index 0000000000..ea072ca932 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000129 diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/LOCK b/.docker/container-state/nucleus-testnet-data/data/application.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/LOG b/.docker/container-state/nucleus-testnet-data/data/application.db/LOG new file mode 100644 index 0000000000..a806f60292 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/application.db/LOG @@ -0,0 +1,463 @@ +=============== Jun 4, 2024 (UTC) =============== +11:02:17.153019 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:02:17.155047 db@open opening +11:02:17.155230 version@stat F·[] S·0B[] Sc·[] +11:02:17.155840 db@janitor F·2 G·0 +11:02:17.155867 db@open done T·813.26µs +=============== Jun 6, 2024 (UTC) =============== +05:23:16.989830 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:23:16.989931 version@stat F·[] S·0B[] Sc·[] +05:23:16.989941 db@open opening +05:23:16.989964 journal@recovery F·1 +05:23:16.990199 journal@recovery recovering @1 +05:23:17.014526 memdb@flush created L0@2 N·26530 S·1MiB "s/1,v499":"s/p..hts,v630" +05:23:17.014655 version@stat F·[1] S·1MiB[1MiB] Sc·[0.25] +05:23:17.017692 db@janitor F·3 G·0 +05:23:17.017711 db@open done T·27.764757ms +=============== Jun 10, 2024 (UTC) =============== +10:09:48.947179 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:48.947301 version@stat F·[1] S·1MiB[1MiB] Sc·[0.25] +10:09:48.947313 db@open opening +10:09:48.947342 journal@recovery F·1 +10:09:48.947436 journal@recovery recovering @3 +10:09:48.975881 memdb@flush created L0@5 N·30645 S·1MiB "s/218,v26651":"s/p..hts,v26650" +10:09:48.976009 version@stat F·[2] S·3MiB[3MiB] Sc·[0.50] +10:09:48.979287 db@janitor F·4 G·0 +10:09:48.979305 db@open done T·31.98773ms +10:09:54.039001 table@compaction L0·2 -> L1·0 S·3MiB Q·57177 +10:09:54.054819 table@build created L1@8 N·32441 S·2MiB "s/1,v499":"s/k..&\x88},v26655" +10:09:54.059930 table@build created L1@9 N·10204 S·917KiB "s/k..Y\x9dN,v39456":"s/p..hts,v57174" +10:09:54.059953 version@stat F·[0 2] S·2MiB[0B 2MiB] Sc·[0.00 0.03] +10:09:54.060525 table@compaction committed F~ S-146KiB Ke·0 D·14530 T·21.507495ms +10:09:54.060815 table@remove removed @2 +=============== Jun 10, 2024 (UTC) =============== +10:19:47.804621 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.804763 version@stat F·[0 2] S·2MiB[0B 2MiB] Sc·[0.00 0.03] +10:19:47.804776 db@open opening +10:19:47.804807 journal@recovery F·1 +10:19:47.804989 journal@recovery recovering @6 +10:19:47.817928 memdb@flush created L0@10 N·14431 S·793KiB "s/465,v57299":"s/p..hts,v57298" +10:19:47.818058 version@stat F·[1 2] S·3MiB[793KiB 2MiB] Sc·[0.25 0.03] +10:19:47.820971 db@janitor F·6 G·1 +10:19:47.820979 db@janitor removing table-5 +10:19:47.821329 db@open done T·16.547569ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.668697 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.668820 version@stat F·[1 2] S·3MiB[793KiB 2MiB] Sc·[0.25 0.03] +10:20:11.668832 db@open opening +10:20:11.668862 journal@recovery F·1 +10:20:11.669038 journal@recovery recovering @11 +10:20:11.669815 version@stat F·[1 2] S·3MiB[793KiB 2MiB] Sc·[0.25 0.03] +10:20:11.672599 db@janitor F·5 G·0 +10:20:11.672617 db@open done T·3.779802ms +10:20:56.772142 table@compaction L0·1 -> L1·2 S·3MiB Q·72609 +10:20:56.785966 table@build created L1@15 N·28801 S·2MiB "s/1,v499":"s/k..\x00\x01\xd9,v58209" +10:20:56.796512 table@build created L1@16 N·24636 S·1MiB "s/k..\x00\x01\xda,v58391":"s/p..hts,v71606" +10:20:56.796534 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +10:20:56.797107 table@compaction committed F-1 S-26KiB Ke·0 D·3639 T·24.946359ms +10:20:56.797326 table@remove removed @10 +10:20:56.797749 table@remove removed @8 +10:20:56.797968 table@remove removed @9 +=============== Jun 10, 2024 (UTC) =============== +10:23:28.214423 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.214548 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +10:23:28.214560 db@open opening +10:23:28.214588 journal@recovery F·1 +10:23:28.214783 journal@recovery recovering @13 +10:23:28.218995 memdb@flush created L0@17 N·4323 S·239KiB "s/582,v71733":"s/p..hts,v71732" +10:23:28.219113 version@stat F·[1 2] S·3MiB[239KiB 3MiB] Sc·[0.25 0.04] +10:23:28.221785 db@janitor F·5 G·0 +10:23:28.221797 db@open done T·7.232311ms +10:24:48.367494 table@compaction L0·1 -> L1·2 S·3MiB Q·77796 +10:24:48.381066 table@build created L1@20 N·27688 S·2MiB "s/1,v499":"s/k..G\xdf?,v49145" +10:24:48.392891 table@build created L1@21 N·29007 S·1MiB "s/k..m\x9az,v49258":"s/p..hts,v75930" +10:24:48.392911 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +10:24:48.393500 table@compaction committed F-1 S-6KiB Ke·0 D·1065 T·25.988629ms +10:24:48.393922 table@remove removed @15 +10:24:48.394285 table@remove removed @16 +=============== Jun 10, 2024 (UTC) =============== +10:27:08.387537 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.387676 version@stat F·[0 2] S·3MiB[0B 3MiB] Sc·[0.00 0.04] +10:27:08.387689 db@open opening +10:27:08.387720 journal@recovery F·1 +10:27:08.387920 journal@recovery recovering @18 +10:27:08.392859 memdb@flush created L0@22 N·5238 S·287KiB "s/616,v76057":"s/p..hts,v76056" +10:27:08.392989 version@stat F·[1 2] S·4MiB[287KiB 3MiB] Sc·[0.25 0.04] +10:27:08.395685 db@janitor F·6 G·1 +10:27:08.395692 db@janitor removing table-17 +10:27:08.395775 db@open done T·8.079478ms +10:27:35.621747 table@compaction L0·1 -> L1·2 S·4MiB Q·81796 +10:27:35.635407 table@build created L1@25 N·26268 S·2MiB "s/1,v499":"s/k..C`\xd4,v16345" +10:27:35.648036 table@build created L1@26 N·30133 S·2MiB "s/k..\xe4,\",v16342":"s/k..\xf8J\xe6,v54390" +10:27:35.649902 table@build created L1@27 N·4226 S·139KiB "s/k..\xfe\x9c\x94,v54392":"s/p..hts,v81169" +10:27:35.649925 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.04] +10:27:35.650493 table@compaction committed F~ S-3KiB Ke·0 D·1306 T·28.727581ms +10:27:35.650893 table@remove removed @20 +10:27:35.651281 table@remove removed @21 +=============== Jun 10, 2024 (UTC) =============== +10:30:28.003560 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.003680 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.04] +10:30:28.003693 db@open opening +10:30:28.003727 journal@recovery F·1 +10:30:28.003923 journal@recovery recovering @23 +10:30:28.007014 memdb@flush created L0@28 N·2460 S·135KiB "s/658,v81296":"s/p..hts,v81295" +10:30:28.007134 version@stat F·[1 3] S·4MiB[135KiB 4MiB] Sc·[0.25 0.04] +10:30:28.009778 db@janitor F·7 G·1 +10:30:28.009786 db@janitor removing table-22 +10:30:28.009884 db@open done T·6.185242ms +10:31:48.160595 table@compaction L0·1 -> L1·3 S·4MiB Q·85502 +10:31:48.173851 table@build created L1@31 N·25661 S·2MiB "s/1,v499":"s/k..\v0\xf2,v1959" +10:31:48.186308 table@build created L1@32 N·29638 S·2MiB "s/k..\x87\x87\xf7,v1957":"s/k..\x99,\x1c,v18646" +10:31:48.189180 table@build created L1@33 N·7189 S·270KiB "s/k...\x86\xde,v18643":"s/p..hts,v83630" +10:31:48.189210 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.04] +10:31:48.189782 table@compaction committed F-1 S-3KiB Ke·0 D·599 T·29.167911ms +10:31:48.190223 table@remove removed @25 +10:31:48.190608 table@remove removed @26 +10:31:48.190657 table@remove removed @27 +=============== Jun 10, 2024 (UTC) =============== +11:11:44.577240 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.577369 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.04] +11:11:44.577432 db@open opening +11:11:44.577518 journal@recovery F·1 +11:11:44.577726 journal@recovery recovering @29 +11:11:44.583326 memdb@flush created L0@34 N·6241 S·343KiB "s/677,v83757":"s/p..hts,v83756" +11:11:44.583448 version@stat F·[1 3] S·4MiB[343KiB 4MiB] Sc·[0.25 0.04] +11:11:44.586146 db@janitor F·7 G·1 +11:11:44.586154 db@janitor removing table-28 +11:11:44.586222 db@open done T·8.779793ms +11:12:19.677457 table@compaction L0·1 -> L1·3 S·4MiB Q·90624 +11:12:19.690805 table@build created L1@37 N·25669 S·2MiB "s/1,v499":"s/k..S\x93t,v12167" +11:12:19.703741 table@build created L1@38 N·30785 S·2MiB "s/k..\xda\x15\x1f,v78135":"s/k..^JZ,v21323" +11:12:19.708261 table@build created L1@39 N·10720 S·612KiB "s/k..\x035\xd4,v43808":"s/p..hts,v89872" +11:12:19.708290 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.05] +11:12:19.708865 table@compaction committed F-1 S-2KiB Ke·0 D·1555 T·31.384841ms +11:12:19.709277 table@remove removed @31 +11:12:19.709690 table@remove removed @32 +11:12:19.709815 table@remove removed @33 +=============== Jun 10, 2024 (UTC) =============== +11:15:00.368723 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.368844 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.05] +11:15:00.368858 db@open opening +11:15:00.368892 journal@recovery F·1 +11:15:00.368995 journal@recovery recovering @35 +11:15:00.372581 memdb@flush created L0@40 N·3461 S·198KiB "s/727,v89999":"s/p..hts,v89998" +11:15:00.372719 version@stat F·[1 3] S·4MiB[198KiB 4MiB] Sc·[0.25 0.05] +11:15:00.376163 db@janitor F·7 G·1 +11:15:00.376173 db@janitor removing table-34 +11:15:00.376313 db@open done T·7.447901ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.195628 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.195754 version@stat F·[1 3] S·4MiB[198KiB 4MiB] Sc·[0.25 0.05] +11:21:08.195767 db@open opening +11:21:08.195798 journal@recovery F·1 +11:21:08.195975 journal@recovery recovering @41 +11:21:08.196426 version@stat F·[1 3] S·4MiB[198KiB 4MiB] Sc·[0.25 0.05] +11:21:08.199202 db@janitor F·6 G·0 +11:21:08.199220 db@open done T·3.448269ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.257213 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.257369 version@stat F·[1 3] S·4MiB[198KiB 4MiB] Sc·[0.25 0.05] +11:22:28.257384 db@open opening +11:22:28.257428 journal@recovery F·1 +11:22:28.257547 journal@recovery recovering @43 +11:22:28.258465 memdb@flush created L0@45 N·125 S·7KiB "s/751,v93461":"s/p..hts,v93460" +11:22:28.258779 version@stat F·[2 3] S·4MiB[206KiB 4MiB] Sc·[0.50 0.05] +11:22:28.262873 db@janitor F·7 G·0 +11:22:28.262892 db@open done T·5.501516ms +11:22:33.315933 table@compaction L0·2 -> L1·3 S·4MiB Q·93463 +11:22:33.329447 table@build created L1@48 N·25736 S·2MiB "s/1,v499":"s/k..@\x9fi,v27903" +11:22:33.343087 table@build created L1@49 N·31857 S·2MiB "s/k..^\x8f\\,v30966":"s/k..\az\xa0,v42094" +11:22:33.348593 table@build created L1@50 N·12352 S·812KiB "s/k..e.\xff,v90474":"s/p..hts,v93460" +11:22:33.348622 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.05] +11:22:33.349202 table@compaction committed F-2 S-3KiB Ke·0 D·815 T·33.250577ms +11:22:33.349295 table@remove removed @40 +11:22:33.349660 table@remove removed @37 +11:22:33.350060 table@remove removed @38 +11:22:33.350239 table@remove removed @39 +=============== Jun 10, 2024 (UTC) =============== +11:23:15.225956 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.226075 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.05] +11:23:15.226088 db@open opening +11:23:15.226123 journal@recovery F·1 +11:23:15.226222 journal@recovery recovering @46 +11:23:15.227403 memdb@flush created L0@51 N·625 S·33KiB "s/752,v93587":"s/p..hts,v93586" +11:23:15.227683 version@stat F·[1 3] S·4MiB[33KiB 4MiB] Sc·[0.25 0.05] +11:23:15.230467 db@janitor F·7 G·1 +11:23:15.230483 db@janitor removing table-45 +11:23:15.230525 db@open done T·4.432437ms +11:23:45.311352 table@compaction L0·1 -> L1·3 S·4MiB Q·94715 +11:23:45.325053 table@build created L1@54 N·25698 S·2MiB "s/1,v499":"s/k..\xb4G\xad,v42131" +11:23:45.338343 table@build created L1@55 N·31986 S·2MiB "s/k..4\xfaD,v56281":"s/k..E\xe4T,v35729" +11:23:45.343864 table@build created L1@56 N·12731 S·857KiB "s/k..\xcba\xc7,v65071":"s/p..hts,v94086" +11:23:45.343889 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.05] +11:23:45.344461 table@compaction committed F-1 S+7KiB Ke·0 D·155 T·33.091217ms +11:23:45.344880 table@remove removed @48 +11:23:45.345336 table@remove removed @49 +11:23:45.345490 table@remove removed @50 +=============== Jun 10, 2024 (UTC) =============== +11:29:08.589479 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.589612 version@stat F·[0 3] S·4MiB[0B 4MiB] Sc·[0.00 0.05] +11:29:08.589628 db@open opening +11:29:08.589660 journal@recovery F·1 +11:29:08.589860 journal@recovery recovering @52 +11:29:08.597140 memdb@flush created L0@57 N·8233 S·457KiB "s/757,v94213":"s/p..hts,v94212" +11:29:08.597275 version@stat F·[1 3] S·5MiB[457KiB 4MiB] Sc·[0.25 0.05] +11:29:08.600093 db@janitor F·7 G·1 +11:29:08.600103 db@janitor removing table-51 +11:29:08.600151 db@open done T·10.518089ms +11:29:53.705206 table@compaction L0·1 -> L1·3 S·5MiB Q·103324 +11:29:53.720168 table@build created L1@60 N·25880 S·2MiB "s/1,v499":"s/k..\x03ړ,v94566" +11:29:53.734810 table@build created L1@61 N·34394 S·2MiB "s/k..\xeaχ,v17268":"s/k..\x17\xaf\xe4,v42101" +11:29:53.742516 table@build created L1@62 N·16321 S·1MiB "s/k..=X7,v94523":"s/p..hts,v102320" +11:29:53.742548 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.05] +11:29:53.743118 table@compaction committed F-1 S-18KiB Ke·0 D·2053 T·37.884368ms +11:29:53.743548 table@remove removed @54 +11:29:53.743948 table@remove removed @55 +11:29:53.744162 table@remove removed @56 +=============== Jun 10, 2024 (UTC) =============== +11:30:42.272443 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.272566 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.05] +11:30:42.272580 db@open opening +11:30:42.272613 journal@recovery F·1 +11:30:42.272709 journal@recovery recovering @58 +11:30:42.275163 memdb@flush created L0@63 N·2126 S·114KiB "s/823,v102447":"s/p..hts,v102446" +11:30:42.275287 version@stat F·[1 3] S·5MiB[114KiB 5MiB] Sc·[0.25 0.05] +11:30:42.277900 db@janitor F·7 G·1 +11:30:42.277909 db@janitor removing table-57 +11:30:42.278025 db@open done T·5.440236ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.658631 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.658763 version@stat F·[1 3] S·5MiB[114KiB 5MiB] Sc·[0.25 0.05] +11:30:55.658776 db@open opening +11:30:55.658809 journal@recovery F·1 +11:30:55.658898 journal@recovery recovering @64 +11:30:55.660051 memdb@flush created L0@66 N·126 S·8KiB "s/840,v104575":"s/p..hts,v104573" +11:30:55.660321 version@stat F·[2 3] S·5MiB[122KiB 5MiB] Sc·[0.50 0.05] +11:30:55.665424 db@janitor F·7 G·0 +11:30:55.665451 db@open done T·6.666506ms +11:31:00.721511 table@compaction L0·2 -> L1·3 S·5MiB Q·104577 +11:31:00.735564 table@build created L1@69 N·25918 S·2MiB "s/1,v499":"s/k..\xf6\x12\xf1,v90096" +11:31:00.750734 table@build created L1@70 N·35025 S·2MiB "s/k..|\x7f\x16,v12531":"s/k..3\xfa\x1f,v12954" +11:31:00.759099 table@build created L1@71 N·17344 S·1MiB "s/k..\x80iJ,v82942":"s/p..hts,v104574" +11:31:00.759122 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.05] +11:31:00.759701 table@compaction committed F-2 S+4KiB Ke·0 D·560 T·38.1687ms +11:31:00.759781 table@remove removed @63 +11:31:00.760155 table@remove removed @60 +11:31:00.760594 table@remove removed @61 +11:31:00.760890 table@remove removed @62 +=============== Jun 10, 2024 (UTC) =============== +11:36:36.044137 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.044283 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.05] +11:36:36.044302 db@open opening +11:36:36.044363 journal@recovery F·1 +11:36:36.044531 journal@recovery recovering @67 +11:36:36.051282 memdb@flush created L0@72 N·7488 S·410KiB "s/841,v104701":"s/p..hts,v104700" +11:36:36.051415 version@stat F·[1 3] S·5MiB[410KiB 5MiB] Sc·[0.25 0.05] +11:36:36.054655 db@janitor F·7 G·1 +11:36:36.054664 db@janitor removing table-66 +11:36:36.054706 db@open done T·10.398727ms +11:37:21.163801 table@compaction L0·1 -> L1·3 S·5MiB Q·113064 +11:37:21.177861 table@build created L1@75 N·26109 S·2MiB "s/1,v499":"s/k.._\xc2H,v105793" +11:37:21.193638 table@build created L1@76 N·37117 S·2MiB "s/k..\x15\x00Z,v48886":"s/k../B\xcf,v30518" +11:37:21.203733 table@build created L1@77 N·20683 S·1MiB "s/k..\xd5cc,v98974":"s/p..hts,v112063" +11:37:21.203758 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.06] +11:37:21.204327 table@compaction committed F-1 S-7KiB Ke·0 D·1866 T·40.506749ms +11:37:21.204748 table@remove removed @69 +11:37:21.205181 table@remove removed @70 +11:37:21.205507 table@remove removed @71 +=============== Jun 10, 2024 (UTC) =============== +12:01:04.928335 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:04.928474 version@stat F·[0 3] S·5MiB[0B 5MiB] Sc·[0.00 0.06] +12:01:04.928493 db@open opening +12:01:04.928537 journal@recovery F·1 +12:01:04.928634 journal@recovery recovering @73 +12:01:04.934234 memdb@flush created L0@78 N·6235 S·344KiB "s/901,v112188":"s/p..hts,v112187" +12:01:04.934551 version@stat F·[1 3] S·6MiB[344KiB 5MiB] Sc·[0.25 0.06] +12:01:04.937539 db@janitor F·7 G·1 +12:01:04.937550 db@janitor removing table-72 +12:01:04.937667 db@open done T·9.167731ms +=============== Jun 10, 2024 (UTC) =============== +12:02:59.924405 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:02:59.924546 version@stat F·[1 3] S·6MiB[344KiB 5MiB] Sc·[0.25 0.06] +12:02:59.924564 db@open opening +12:02:59.924601 journal@recovery F·1 +12:02:59.924874 journal@recovery recovering @79 +12:02:59.926525 memdb@flush created L0@81 N·375 S·19KiB "s/951,v118426":"s/p..hts,v118425" +12:02:59.926683 version@stat F·[2 3] S·6MiB[364KiB 5MiB] Sc·[0.50 0.06] +12:02:59.929424 db@janitor F·7 G·0 +12:02:59.929446 db@open done T·4.875913ms +12:03:04.985928 table@compaction L0·2 -> L1·3 S·6MiB Q·118678 +12:03:05.000541 table@build created L1@84 N·26214 S·2MiB "s/1,v499":"s/k..Og\xc3,v45444" +12:03:05.017051 table@build created L1@85 N·39244 S·2MiB "s/k..\x86\x8f\x81,v23591":"s/k..705,v87236" +12:03:05.027971 table@build created L1@86 N·19330 S·2MiB "s/k..706,v87333":"s/k..\x80\x15\xaf,v105420" +12:03:05.029777 table@build created L1@87 N·4083 S·111KiB "s/k..\xc90A,v105423":"s/p..hts,v118675" +12:03:05.029803 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +12:03:05.030919 table@compaction committed F-1 S-7KiB Ke·0 D·1648 T·44.962277ms +12:03:05.031043 table@remove removed @78 +12:03:05.031427 table@remove removed @75 +12:03:05.031838 table@remove removed @76 +12:03:05.032198 table@remove removed @77 +=============== Jun 10, 2024 (UTC) =============== +12:03:19.578037 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.578172 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +12:03:19.578197 db@open opening +12:03:19.578235 journal@recovery F·1 +12:03:19.578450 journal@recovery recovering @82 +12:03:19.585879 memdb@flush created L0@88 N·250 S·13KiB "s/954,v118802":"s/p..hts,v118801" +12:03:19.586024 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:03:19.589565 db@janitor F·8 G·1 +12:03:19.589581 db@janitor removing table-81 +12:03:19.589635 db@open done T·11.431501ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.511322 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.511484 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:04:23.511498 db@open opening +12:04:23.511539 journal@recovery F·1 +12:04:23.511708 journal@recovery recovering @89 +12:04:23.513097 memdb@flush created L0@91 N·125 S·7KiB "s/956,v119053":"s/p..hts,v119052" +12:04:23.515002 version@stat F·[2 4] S·6MiB[21KiB 6MiB] Sc·[0.50 0.06] +12:04:23.517725 db@janitor F·8 G·0 +12:04:23.517743 db@open done T·6.239135ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.637779 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.637910 version@stat F·[2 4] S·6MiB[21KiB 6MiB] Sc·[0.50 0.06] +12:07:46.637926 db@open opening +12:07:46.637967 journal@recovery F·1 +12:07:46.638064 journal@recovery recovering @92 +12:07:46.638407 version@stat F·[2 4] S·6MiB[21KiB 6MiB] Sc·[0.50 0.06] +12:07:46.642735 db@janitor F·8 G·0 +12:07:46.642764 db@open done T·4.823572ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.035051 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.035205 version@stat F·[2 4] S·6MiB[21KiB 6MiB] Sc·[0.50 0.06] +12:08:02.035223 db@open opening +12:08:02.035260 journal@recovery F·1 +12:08:02.035480 journal@recovery recovering @94 +12:08:02.035726 version@stat F·[2 4] S·6MiB[21KiB 6MiB] Sc·[0.50 0.06] +12:08:02.038668 db@janitor F·8 G·0 +12:08:02.038693 db@open done T·3.457031ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.378533 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.378667 version@stat F·[2 4] S·6MiB[21KiB 6MiB] Sc·[0.50 0.06] +12:27:21.378681 db@open opening +12:27:21.378720 journal@recovery F·1 +12:27:21.378935 journal@recovery recovering @96 +12:27:21.379433 version@stat F·[2 4] S·6MiB[21KiB 6MiB] Sc·[0.50 0.06] +12:27:21.383910 db@janitor F·8 G·0 +12:27:21.383942 db@open done T·5.247126ms +12:27:21.497937 table@compaction L0·2 -> L1·4 S·6MiB Q·119055 +12:27:21.514083 table@build created L1@100 N·26210 S·2MiB "s/1,v499":"s/k..\xf0.^,v28181" +12:27:21.531654 table@build created L1@101 N·39393 S·2MiB "s/k..c\xa8\xc9,v81378":"s/k..658,v81198" +12:27:21.542467 table@build created L1@102 N·18895 S·2MiB "s/k..659,v81357":"s/k..%U\xa1,v99302" +12:27:21.544653 table@build created L1@103 N·4655 S·136KiB "s/k..\xbfZx,v99304":"s/p..hts,v119052" +12:27:21.544690 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +12:27:21.546405 table@compaction committed F-2 S+1KiB Ke·0 D·93 T·48.446441ms +12:27:21.546477 table@remove removed @91 +12:27:21.546513 table@remove removed @88 +12:27:21.546937 table@remove removed @84 +12:27:21.547413 table@remove removed @85 +12:27:21.547838 table@remove removed @86 +12:27:21.547886 table@remove removed @87 +=============== Jun 10, 2024 (UTC) =============== +12:56:43.749314 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.749428 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +12:56:43.749442 db@open opening +12:56:43.749474 journal@recovery F·1 +12:56:43.749678 journal@recovery recovering @98 +12:56:43.750993 memdb@flush created L0@104 N·250 S·13KiB "s/957,v119179":"s/p..hts,v119178" +12:56:43.751146 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:56:43.754258 db@janitor F·7 G·0 +12:56:43.754276 db@open done T·4.828302ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.320984 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.321104 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:57:38.321118 db@open opening +12:57:38.321153 journal@recovery F·1 +12:57:38.321256 journal@recovery recovering @105 +12:57:38.321538 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:57:38.324489 db@janitor F·7 G·0 +12:57:38.324510 db@open done T·3.38598ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.414316 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.414448 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:58:01.414461 db@open opening +12:58:01.414496 journal@recovery F·1 +12:58:01.414755 journal@recovery recovering @107 +12:58:01.416967 memdb@flush created L0@109 N·125 S·8KiB "s/959,v119430":"s/p..hts,v119429" +12:58:01.417101 version@stat F·[2 4] S·6MiB[21KiB 6MiB] Sc·[0.50 0.06] +12:58:01.420682 db@janitor F·8 G·0 +12:58:01.420702 db@open done T·6.232364ms +12:58:06.474695 table@compaction L0·2 -> L1·4 S·6MiB Q·119432 +12:58:06.488984 table@build created L1@112 N·26268 S·2MiB "s/1,v499":"s/k..F\rz,v16445" +12:58:06.505239 table@build created L1@113 N·39510 S·2MiB "s/k..\x9eE\xab,v60804":"s/k..637,v78665" +12:58:06.515776 table@build created L1@114 N·18673 S·2MiB "s/k..638,v78730":"s/k..p\x05U,v96071" +12:58:06.517908 table@build created L1@115 N·4984 S·150KiB "s/k..\xa7M\xf6,v96065":"s/p..hts,v119429" +12:58:06.517931 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +12:58:06.518521 table@compaction committed F-2 S-5KiB Ke·0 D·93 T·43.804781ms +12:58:06.518585 table@remove removed @104 +12:58:06.518960 table@remove removed @100 +12:58:06.519357 table@remove removed @101 +12:58:06.519779 table@remove removed @102 +12:58:06.519830 table@remove removed @103 +=============== Jun 10, 2024 (UTC) =============== +12:58:48.938350 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:48.938471 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +12:58:48.938486 db@open opening +12:58:48.938523 journal@recovery F·1 +12:58:48.938615 journal@recovery recovering @110 +12:58:48.939691 memdb@flush created L0@116 N·251 S·13KiB "s/960,v119557":"s/p..hts,v119555" +12:58:48.939982 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:58:48.943323 db@janitor F·8 G·1 +12:58:48.943340 db@janitor removing table-109 +12:58:48.943385 db@open done T·4.892942ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.179741 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.179892 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:59:05.179912 db@open opening +12:59:05.179961 journal@recovery F·1 +12:59:05.180083 journal@recovery recovering @117 +12:59:05.180459 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:59:05.185454 db@janitor F·7 G·0 +12:59:05.185482 db@open done T·5.556219ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.364226 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.364382 version@stat F·[1 4] S·6MiB[13KiB 6MiB] Sc·[0.25 0.06] +12:59:32.364397 db@open opening +12:59:32.364436 journal@recovery F·1 +12:59:32.364628 journal@recovery recovering @119 +12:59:32.365864 memdb@flush created L0@121 N·125 S·7KiB "s/962,v119808":"s/p..hts,v119807" +12:59:32.366062 version@stat F·[2 4] S·6MiB[21KiB 6MiB] Sc·[0.50 0.06] +12:59:32.369313 db@janitor F·8 G·0 +12:59:32.369333 db@open done T·4.930333ms +12:59:32.482085 table@compaction L0·2 -> L1·4 S·6MiB Q·119810 +12:59:32.496446 table@build created L1@124 N·26224 S·2MiB "s/1,v499":"s/k..\xa0\x02\xe4,v4174" +12:59:32.512989 table@build created L1@125 N·39687 S·2MiB "s/k..\xc6*$,v92946":"s/k..579,v71349" +12:59:32.524159 table@build created L1@126 N·18356 S·2MiB "s/k..P58,v6928":"s/k..l\xf4#,v90644" +12:59:32.526466 table@build created L1@127 N·5450 S·172KiB "s/k..:p\xda,v90642":"s/p..hts,v119807" +12:59:32.526491 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +12:59:32.527069 table@compaction committed F-2 S-1KiB Ke·0 D·94 T·44.96445ms +12:59:32.527130 table@remove removed @116 +12:59:32.527519 table@remove removed @112 +12:59:32.527940 table@remove removed @113 +12:59:32.528413 table@remove removed @114 +12:59:32.528468 table@remove removed @115 +=============== Jul 3, 2024 (UTC) =============== +17:52:58.287231 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.287362 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +17:52:58.287372 db@open opening +17:52:58.287410 journal@recovery F·1 +17:52:58.287601 journal@recovery recovering @122 +17:52:58.288202 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +17:52:58.291869 db@janitor F·7 G·1 +17:52:58.291876 db@janitor removing table-121 +17:52:58.291918 db@open done T·4.541677ms +=============== Jul 3, 2024 (UTC) =============== +17:59:01.655850 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.655977 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +17:59:01.655990 db@open opening +17:59:01.656024 journal@recovery F·1 +17:59:01.656280 journal@recovery recovering @128 +17:59:01.664160 memdb@flush created L0@130 N·9211 S·526KiB "s/1000,v124559":"s/p..hts,v119933" +17:59:01.664278 version@stat F·[1 4] S·6MiB[526KiB 6MiB] Sc·[0.25 0.06] +17:59:01.667135 db@janitor F·7 G·0 +17:59:01.667155 db@open done T·11.159872ms diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/MANIFEST-000132 b/.docker/container-state/nucleus-testnet-data/data/application.db/MANIFEST-000132 new file mode 100644 index 0000000000..50463c21d5 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/MANIFEST-000132 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000089.ldb b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000089.ldb new file mode 100644 index 0000000000..139161de2b Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000089.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000090.log b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000090.log new file mode 100644 index 0000000000..39218b184a Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000090.log differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000092.ldb b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000092.ldb new file mode 100644 index 0000000000..f0670e0093 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000092.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT new file mode 100644 index 0000000000..00f4669871 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000091 diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT.bak new file mode 100644 index 0000000000..948a0b647f --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000088 diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/LOCK b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/LOG b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/LOG new file mode 100644 index 0000000000..1b30a4e429 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/LOG @@ -0,0 +1,391 @@ +=============== Jun 4, 2024 (UTC) =============== +11:02:17.167192 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:02:17.169057 db@open opening +11:02:17.169205 version@stat F·[] S·0B[] Sc·[] +11:02:17.169819 db@janitor F·2 G·0 +11:02:17.169829 db@open done T·763.83µs +11:20:27.051134 db@close closing +11:20:27.051182 db@close done T·46.48µs +=============== Jun 6, 2024 (UTC) =============== +05:23:17.029456 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:23:17.029539 version@stat F·[] S·0B[] Sc·[] +05:23:17.029549 db@open opening +05:23:17.029574 journal@recovery F·1 +05:23:17.029663 journal@recovery recovering @1 +05:23:17.031961 memdb@flush created L0@2 N·1302 S·207KiB "BH:..c5c,v363":"blo..ore,v6" +05:23:17.032081 version@stat F·[1] S·207KiB[207KiB] Sc·[0.25] +05:23:17.034612 db@janitor F·3 G·0 +05:23:17.034624 db@open done T·5.07141ms +05:43:54.555408 db@close closing +05:43:54.555457 db@close done T·47.84µs +=============== Jun 10, 2024 (UTC) =============== +10:09:48.994026 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:48.994092 version@stat F·[1] S·207KiB[207KiB] Sc·[0.25] +10:09:48.994103 db@open opening +10:09:48.994131 journal@recovery F·1 +10:09:48.994739 journal@recovery recovering @3 +10:09:48.996990 memdb@flush created L0@5 N·1482 S·230KiB "BH:..100,v2722":"blo..ore,v1309" +10:09:48.998841 version@stat F·[2] S·438KiB[438KiB] Sc·[0.50] +10:09:49.001475 db@janitor F·4 G·0 +10:09:49.001485 db@open done T·7.37854ms +10:19:38.331932 db@close closing +10:19:38.331979 db@close done T·46.88µs +=============== Jun 10, 2024 (UTC) =============== +10:19:47.837374 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.837488 version@stat F·[2] S·438KiB[438KiB] Sc·[0.50] +10:19:47.837500 db@open opening +10:19:47.837533 journal@recovery F·1 +10:19:47.839083 journal@recovery recovering @6 +10:19:47.840597 memdb@flush created L0@8 N·702 S·108KiB "BH:..92c,v3053":"blo..ore,v2792" +10:19:47.842733 version@stat F·[3] S·547KiB[547KiB] Sc·[0.75] +10:19:47.845431 db@janitor F·5 G·0 +10:19:47.845458 db@open done T·7.953897ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.689661 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.689755 version@stat F·[3] S·547KiB[547KiB] Sc·[0.75] +10:20:11.689767 db@open opening +10:20:11.689805 journal@recovery F·1 +10:20:11.691702 journal@recovery recovering @9 +10:20:11.693746 version@stat F·[3] S·547KiB[547KiB] Sc·[0.75] +10:20:11.696414 db@janitor F·5 G·0 +10:20:11.696425 db@open done T·6.652796ms +10:23:04.294984 db@close closing +10:23:04.295033 db@close done T·49.151µs +=============== Jun 10, 2024 (UTC) =============== +10:23:28.239002 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.239174 version@stat F·[3] S·547KiB[547KiB] Sc·[0.75] +10:23:28.239203 db@open opening +10:23:28.239244 journal@recovery F·1 +10:23:28.241209 journal@recovery recovering @11 +10:23:28.242267 memdb@flush created L0@13 N·204 S·32KiB "BH:..7ed,v3636":"blo..ore,v3495" +10:23:28.242434 version@stat F·[4] S·579KiB[579KiB] Sc·[1.00] +10:23:28.245238 db@janitor F·6 G·0 +10:23:28.245267 db@open done T·6.058471ms +10:23:28.245334 table@compaction L0·4 -> L1·0 S·579KiB Q·3694 +10:23:28.250870 table@build created L1@16 N·3076 S·577KiB "BH:..c5c,v363":"blo..ore,v3693" +10:23:28.250893 version@stat F·[0 1] S·577KiB[0B 577KiB] Sc·[0.00 0.01] +10:23:28.252525 table@compaction committed F-3 S-1KiB Ke·0 D·614 T·7.16534ms +10:23:28.252612 table@remove removed @8 +10:23:28.252686 table@remove removed @5 +10:23:28.252750 table@remove removed @2 +10:26:58.662937 db@close closing +10:26:58.662982 db@close done T·44.621µs +=============== Jun 10, 2024 (UTC) =============== +10:27:08.413936 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.414062 version@stat F·[0 1] S·577KiB[0B 577KiB] Sc·[0.00 0.01] +10:27:08.414073 db@open opening +10:27:08.414112 journal@recovery F·1 +10:27:08.416106 journal@recovery recovering @14 +10:27:08.417170 memdb@flush created L0@17 N·252 S·39KiB "BH:..892,v3853":"blo..ore,v3700" +10:27:08.417318 version@stat F·[1 1] S·617KiB[39KiB 577KiB] Sc·[0.25 0.01] +10:27:08.419976 db@janitor F·5 G·1 +10:27:08.419983 db@janitor removing table-13 +10:27:08.420037 db@open done T·5.95856ms +10:28:46.710119 db@close closing +10:28:46.710164 db@close done T·43.77µs +=============== Jun 10, 2024 (UTC) =============== +10:30:28.027998 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.028106 version@stat F·[1 1] S·617KiB[39KiB 577KiB] Sc·[0.25 0.01] +10:30:28.028124 db@open opening +10:30:28.028170 journal@recovery F·1 +10:30:28.029934 journal@recovery recovering @18 +10:30:28.030846 memdb@flush created L0@20 N·114 S·18KiB "BH:..399,v3950":"blo..ore,v3953" +10:30:28.032879 version@stat F·[2 1] S·635KiB[58KiB 577KiB] Sc·[0.50 0.01] +10:30:28.035574 db@janitor F·5 G·0 +10:30:28.035586 db@open done T·7.456743ms +10:34:40.140392 db@close closing +10:34:40.140456 db@close done T·63.16µs +=============== Jun 10, 2024 (UTC) =============== +11:11:44.605592 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.605707 version@stat F·[2 1] S·635KiB[58KiB 577KiB] Sc·[0.50 0.01] +11:11:44.605723 db@open opening +11:11:44.605775 journal@recovery F·1 +11:11:44.607629 journal@recovery recovering @21 +11:11:44.608741 memdb@flush created L0@23 N·300 S·46KiB "BH:..33c,v4293":"blo..ore,v4068" +11:11:44.610678 version@stat F·[3 1] S·682KiB[104KiB 577KiB] Sc·[0.75 0.01] +11:11:44.613493 db@janitor F·6 G·0 +11:11:44.613509 db@open done T·7.777625ms +11:13:21.544611 table@compaction L0·3 -> L1·1 S·682KiB Q·4477 +11:13:21.549117 table@build created L1@26 N·3631 S·681KiB "BH:..c5c,v363":"blo..ore,v4362" +11:13:21.549139 version@stat F·[0 1] S·681KiB[0B 681KiB] Sc·[0.00 0.01] +11:13:21.549722 table@compaction committed F-3 S-927B Ke·0 D·111 T·5.090632ms +11:13:21.549781 table@remove removed @20 +11:13:21.549819 table@remove removed @17 +11:13:21.549946 table@remove removed @16 +11:13:46.210519 db@close closing +11:13:46.210605 db@close done T·82.181µs +=============== Jun 10, 2024 (UTC) =============== +11:15:00.395160 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.395289 version@stat F·[0 1] S·681KiB[0B 681KiB] Sc·[0.00 0.01] +11:15:00.395304 db@open opening +11:15:00.395335 journal@recovery F·1 +11:15:00.397149 journal@recovery recovering @24 +11:15:00.398119 memdb@flush created L0@27 N·144 S·28KiB "BH:..02c,v4498":"blo..ore,v4369" +11:15:00.398262 version@stat F·[1 1] S·710KiB[28KiB 681KiB] Sc·[0.25 0.01] +11:15:00.400949 db@janitor F·5 G·1 +11:15:00.400956 db@janitor removing table-23 +11:15:00.401012 db@open done T·5.704517ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.218334 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.218427 version@stat F·[1 1] S·710KiB[28KiB 681KiB] Sc·[0.25 0.01] +11:21:08.218443 db@open opening +11:21:08.218475 journal@recovery F·1 +11:21:08.220368 journal@recovery recovering @28 +11:21:08.222289 version@stat F·[1 1] S·710KiB[28KiB 681KiB] Sc·[0.25 0.01] +11:21:08.224916 db@janitor F·4 G·0 +11:21:08.224931 db@open done T·6.481994ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.286946 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.287033 version@stat F·[1 1] S·710KiB[28KiB 681KiB] Sc·[0.25 0.01] +11:22:28.287043 db@open opening +11:22:28.287071 journal@recovery F·1 +11:22:28.287370 journal@recovery recovering @30 +11:22:28.288112 memdb@flush created L0@32 N·6 S·963B "BH:..686,v4511":"blo..ore,v4514" +11:22:28.288229 version@stat F·[2 1] S·711KiB[29KiB 681KiB] Sc·[0.50 0.01] +11:22:28.290768 db@janitor F·5 G·0 +11:22:28.290778 db@open done T·3.732711ms +11:22:57.093632 db@close closing +11:22:57.093684 db@close done T·51.87µs +=============== Jun 10, 2024 (UTC) =============== +11:23:15.251147 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.251266 version@stat F·[2 1] S·711KiB[29KiB 681KiB] Sc·[0.50 0.01] +11:23:15.251279 db@open opening +11:23:15.251309 journal@recovery F·1 +11:23:15.251613 journal@recovery recovering @33 +11:23:15.252445 memdb@flush created L0@35 N·30 S·3KiB "BH:..674,v4524":"blo..ore,v4521" +11:23:15.252583 version@stat F·[3 1] S·715KiB[33KiB 681KiB] Sc·[0.75 0.01] +11:23:15.255175 db@janitor F·6 G·0 +11:23:15.255196 db@open done T·3.913293ms +11:28:46.931016 db@close closing +11:28:46.931081 db@close done T·64.521µs +=============== Jun 10, 2024 (UTC) =============== +11:29:08.621812 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.621915 version@stat F·[3 1] S·715KiB[33KiB 681KiB] Sc·[0.75 0.01] +11:29:08.621928 db@open opening +11:29:08.621962 journal@recovery F·1 +11:29:08.623929 journal@recovery recovering @36 +11:29:08.625168 memdb@flush created L0@38 N·396 S·61KiB "BH:..882,v4657":"blo..ore,v4552" +11:29:08.625451 version@stat F·[4 1] S·776KiB[94KiB 681KiB] Sc·[1.00 0.01] +11:29:08.628275 db@janitor F·7 G·0 +11:29:08.628290 db@open done T·6.357054ms +11:29:08.628317 table@compaction L0·4 -> L1·1 S·776KiB Q·4943 +11:29:08.634076 table@build created L1@41 N·4111 S·776KiB "BH:..c5c,v363":"blo..ore,v4942" +11:29:08.634098 version@stat F·[0 1] S·776KiB[0B 776KiB] Sc·[0.00 0.01] +11:29:08.635339 table@compaction committed F-4 S+187B Ke·0 D·96 T·7.003189ms +11:29:08.635404 table@remove removed @35 +11:29:08.635436 table@remove removed @32 +11:29:08.635473 table@remove removed @27 +11:29:08.635616 table@remove removed @26 +11:30:37.043712 db@close closing +11:30:37.043766 db@close done T·53.58µs +=============== Jun 10, 2024 (UTC) =============== +11:30:42.302109 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.302249 version@stat F·[0 1] S·776KiB[0B 776KiB] Sc·[0.00 0.01] +11:30:42.302268 db@open opening +11:30:42.302314 journal@recovery F·1 +11:30:42.302608 journal@recovery recovering @39 +11:30:42.303604 memdb@flush created L0@42 N·102 S·16KiB "BH:..42a,v5036":"blo..ore,v4949" +11:30:42.303790 version@stat F·[1 1] S·792KiB[16KiB 776KiB] Sc·[0.25 0.01] +11:30:42.312329 db@janitor F·5 G·1 +11:30:42.312341 db@janitor removing table-38 +11:30:42.312405 db@open done T·10.130265ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.689994 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.690084 version@stat F·[1 1] S·792KiB[16KiB 776KiB] Sc·[0.25 0.01] +11:30:55.690098 db@open opening +11:30:55.690130 journal@recovery F·1 +11:30:55.690436 journal@recovery recovering @43 +11:30:55.691219 memdb@flush created L0@45 N·6 S·958B "BH:..90d,v5049":"blo..ore,v5052" +11:30:55.693168 version@stat F·[2 1] S·793KiB[17KiB 776KiB] Sc·[0.50 0.01] +11:30:55.695733 db@janitor F·5 G·0 +11:30:55.695744 db@open done T·5.641957ms +11:35:56.722570 db@close closing +11:35:56.722627 db@close done T·56.51µs +=============== Jun 10, 2024 (UTC) =============== +11:36:36.076466 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.076569 version@stat F·[2 1] S·793KiB[17KiB 776KiB] Sc·[0.50 0.01] +11:36:36.076587 db@open opening +11:36:36.076617 journal@recovery F·1 +11:36:36.078605 journal@recovery recovering @46 +11:36:36.079833 memdb@flush created L0@48 N·360 S·56KiB "BH:..7f2,v5146":"blo..ore,v5059" +11:36:36.080080 version@stat F·[3 1] S·850KiB[73KiB 776KiB] Sc·[0.75 0.01] +11:36:36.082842 db@janitor F·6 G·0 +11:36:36.082856 db@open done T·6.265422ms +11:40:48.235708 db@close closing +11:40:48.235769 db@close done T·60.051µs +=============== Jun 10, 2024 (UTC) =============== +12:01:04.961874 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:04.961988 version@stat F·[3 1] S·850KiB[73KiB 776KiB] Sc·[0.75 0.01] +12:01:04.962005 db@open opening +12:01:04.962052 journal@recovery F·1 +12:01:04.962165 journal@recovery recovering @49 +12:01:04.963449 memdb@flush created L0@51 N·300 S·46KiB "BH:..60a,v5591":"blo..ore,v5420" +12:01:04.963613 version@stat F·[4 1] S·896KiB[120KiB 776KiB] Sc·[1.00 0.01] +12:01:04.966322 db@janitor F·7 G·0 +12:01:04.966337 db@open done T·4.326219ms +12:01:04.966376 table@compaction L0·4 -> L1·1 S·896KiB Q·5715 +12:01:04.973198 table@build created L1@54 N·4751 S·897KiB "BH:..c5c,v363":"blo..ore,v5714" +12:01:04.973224 version@stat F·[0 1] S·897KiB[0B 897KiB] Sc·[0.00 0.01] +12:01:04.974775 table@compaction committed F-4 S+406B Ke·0 D·128 T·8.360764ms +12:01:04.974845 table@remove removed @48 +12:01:04.974878 table@remove removed @45 +12:01:04.974912 table@remove removed @42 +12:01:04.975077 table@remove removed @41 +12:01:23.716813 db@close closing +12:01:23.716866 db@close done T·52.111µs +=============== Jun 10, 2024 (UTC) =============== +12:02:59.952861 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:02:59.952973 version@stat F·[0 1] S·897KiB[0B 897KiB] Sc·[0.00 0.01] +12:02:59.952986 db@open opening +12:02:59.953018 journal@recovery F·1 +12:02:59.954882 journal@recovery recovering @52 +12:02:59.955700 memdb@flush created L0@55 N·18 S·2KiB "BH:..d21,v5718":"blo..ore,v5721" +12:02:59.955835 version@stat F·[1 1] S·899KiB[2KiB 897KiB] Sc·[0.25 0.01] +12:02:59.958450 db@janitor F·5 G·1 +12:02:59.958461 db@janitor removing table-51 +12:02:59.958507 db@open done T·5.515799ms +=============== Jun 10, 2024 (UTC) =============== +12:03:19.612706 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.612822 version@stat F·[1 1] S·899KiB[2KiB 897KiB] Sc·[0.25 0.01] +12:03:19.612837 db@open opening +12:03:19.612868 journal@recovery F·1 +12:03:19.614715 journal@recovery recovering @56 +12:03:19.615550 memdb@flush created L0@58 N·12 S·1KiB "BH:..fdd,v5737":"blo..ore,v5740" +12:03:19.615710 version@stat F·[2 1] S·900KiB[3KiB 897KiB] Sc·[0.50 0.01] +12:03:19.618942 db@janitor F·5 G·0 +12:03:19.618957 db@open done T·6.114524ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.542876 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.543017 version@stat F·[2 1] S·900KiB[3KiB 897KiB] Sc·[0.50 0.01] +12:04:23.543037 db@open opening +12:04:23.543093 journal@recovery F·1 +12:04:23.543219 journal@recovery recovering @59 +12:04:23.544112 memdb@flush created L0@61 N·6 S·958B "BH:..ea0,v5750":"blo..ore,v5753" +12:04:23.544448 version@stat F·[3 1] S·901KiB[4KiB 897KiB] Sc·[0.75 0.01] +12:04:23.547433 db@janitor F·6 G·0 +12:04:23.547453 db@open done T·4.409409ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.666287 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.666385 version@stat F·[3 1] S·901KiB[4KiB 897KiB] Sc·[0.75 0.01] +12:07:46.666397 db@open opening +12:07:46.666428 journal@recovery F·1 +12:07:46.666516 journal@recovery recovering @62 +12:07:46.666654 version@stat F·[3 1] S·901KiB[4KiB 897KiB] Sc·[0.75 0.01] +12:07:46.670100 db@janitor F·6 G·0 +12:07:46.670122 db@open done T·3.720013ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.061159 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.061341 version@stat F·[3 1] S·901KiB[4KiB 897KiB] Sc·[0.75 0.01] +12:08:02.061358 db@open opening +12:08:02.061395 journal@recovery F·1 +12:08:02.063219 journal@recovery recovering @64 +12:08:02.063403 version@stat F·[3 1] S·901KiB[4KiB 897KiB] Sc·[0.75 0.01] +12:08:02.066748 db@janitor F·6 G·0 +12:08:02.066758 db@open done T·5.394877ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.417846 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.418053 version@stat F·[3 1] S·901KiB[4KiB 897KiB] Sc·[0.75 0.01] +12:27:21.418074 db@open opening +12:27:21.418132 journal@recovery F·1 +12:27:21.420620 journal@recovery recovering @66 +12:27:21.420876 version@stat F·[3 1] S·901KiB[4KiB 897KiB] Sc·[0.75 0.01] +12:27:21.423677 db@janitor F·6 G·0 +12:27:21.423712 db@open done T·5.630459ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.777795 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.777986 version@stat F·[3 1] S·901KiB[4KiB 897KiB] Sc·[0.75 0.01] +12:56:43.778006 db@open opening +12:56:43.778062 journal@recovery F·1 +12:56:43.778197 journal@recovery recovering @68 +12:56:43.779023 memdb@flush created L0@70 N·12 S·1KiB "BH:..7fc,v5763":"blo..ore,v5760" +12:56:43.779334 version@stat F·[4 1] S·903KiB[6KiB 897KiB] Sc·[1.00 0.01] +12:56:43.782135 db@janitor F·7 G·0 +12:56:43.782149 db@open done T·4.137736ms +12:56:43.782180 table@compaction L0·4 -> L1·1 S·903KiB Q·5767 +12:56:43.791584 table@build created L1@73 N·4791 S·903KiB "BH:..c5c,v363":"blo..ore,v5766" +12:56:43.791609 version@stat F·[0 1] S·903KiB[0B 903KiB] Sc·[0.00 0.01] +12:56:43.793192 table@compaction committed F-4 S-54B Ke·0 D·8 T·10.985056ms +12:56:43.793334 table@remove removed @61 +12:56:43.793377 table@remove removed @58 +12:56:43.793418 table@remove removed @55 +12:56:43.793662 table@remove removed @54 +=============== Jun 10, 2024 (UTC) =============== +12:57:38.349938 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.350097 version@stat F·[0 1] S·903KiB[0B 903KiB] Sc·[0.00 0.01] +12:57:38.350118 db@open opening +12:57:38.350163 journal@recovery F·1 +12:57:38.350486 journal@recovery recovering @71 +12:57:38.350653 version@stat F·[0 1] S·903KiB[0B 903KiB] Sc·[0.00 0.01] +12:57:38.353367 db@janitor F·4 G·1 +12:57:38.353378 db@janitor removing table-70 +12:57:38.353420 db@open done T·3.295768ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.444508 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.444615 version@stat F·[0 1] S·903KiB[0B 903KiB] Sc·[0.00 0.01] +12:58:01.444630 db@open opening +12:58:01.444660 journal@recovery F·1 +12:58:01.446613 journal@recovery recovering @74 +12:58:01.447398 memdb@flush created L0@76 N·6 S·958B "BH:..6b7,v5770":"blo..ore,v5773" +12:58:01.447667 version@stat F·[1 1] S·903KiB[958B 903KiB] Sc·[0.25 0.01] +12:58:01.450613 db@janitor F·4 G·0 +12:58:01.450627 db@open done T·5.992352ms +=============== Jun 10, 2024 (UTC) =============== +12:58:48.967138 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:48.967225 version@stat F·[1 1] S·903KiB[958B 903KiB] Sc·[0.25 0.01] +12:58:48.967239 db@open opening +12:58:48.967269 journal@recovery F·1 +12:58:48.967356 journal@recovery recovering @77 +12:58:48.968077 memdb@flush created L0@79 N·12 S·1KiB "BH:..45e,v5783":"blo..ore,v5780" +12:58:48.968206 version@stat F·[2 1] S·905KiB[2KiB 903KiB] Sc·[0.50 0.01] +12:58:48.972449 db@janitor F·5 G·0 +12:58:48.972466 db@open done T·5.222185ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.212420 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.212508 version@stat F·[2 1] S·905KiB[2KiB 903KiB] Sc·[0.50 0.01] +12:59:05.212521 db@open opening +12:59:05.212553 journal@recovery F·1 +12:59:05.212641 journal@recovery recovering @80 +12:59:05.212778 version@stat F·[2 1] S·905KiB[2KiB 903KiB] Sc·[0.50 0.01] +12:59:05.216088 db@janitor F·5 G·0 +12:59:05.216102 db@open done T·3.574251ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.394691 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.394801 version@stat F·[2 1] S·905KiB[2KiB 903KiB] Sc·[0.50 0.01] +12:59:32.394814 db@open opening +12:59:32.394845 journal@recovery F·1 +12:59:32.394945 journal@recovery recovering @82 +12:59:32.395725 memdb@flush created L0@84 N·6 S·963B "BH:..f72,v5790":"blo..ore,v5793" +12:59:32.395975 version@stat F·[3 1] S·906KiB[3KiB 903KiB] Sc·[0.75 0.01] +12:59:32.398691 db@janitor F·6 G·0 +12:59:32.398707 db@open done T·3.888023ms +=============== Jul 3, 2024 (UTC) =============== +17:52:58.314317 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.314484 version@stat F·[3 1] S·906KiB[3KiB 903KiB] Sc·[0.75 0.01] +17:52:58.314500 db@open opening +17:52:58.314539 journal@recovery F·1 +17:52:58.316433 journal@recovery recovering @85 +17:52:58.316614 version@stat F·[3 1] S·906KiB[3KiB 903KiB] Sc·[0.75 0.01] +17:52:58.319264 db@janitor F·6 G·0 +17:52:58.319291 db@open done T·4.786289ms +17:58:44.101809 db@close closing +17:58:44.101855 db@close done T·46.011µs +=============== Jul 3, 2024 (UTC) =============== +17:59:01.692437 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.692555 version@stat F·[3 1] S·906KiB[3KiB 903KiB] Sc·[0.75 0.01] +17:59:01.692569 db@open opening +17:59:01.692603 journal@recovery F·1 +17:59:01.694560 journal@recovery recovering @87 +17:59:01.695928 memdb@flush created L0@89 N·414 S·71KiB "BH:..1bf,v6121":"blo..ore,v5800" +17:59:01.696078 version@stat F·[4 1] S·977KiB[74KiB 903KiB] Sc·[1.00 0.01] +17:59:01.698791 db@janitor F·7 G·0 +17:59:01.698808 db@open done T·6.231022ms +17:59:01.698849 table@compaction L0·4 -> L1·1 S·977KiB Q·6209 +17:59:01.706606 table@build created L1@92 N·5156 S·979KiB "BH:..c5c,v363":"blo..ore,v6208" +17:59:01.706633 version@stat F·[0 1] S·979KiB[0B 979KiB] Sc·[0.00 0.01] +17:59:01.707731 table@compaction committed F-4 S+1KiB Ke·0 D·73 T·8.851353ms +17:59:01.707829 table@remove removed @84 +17:59:01.707863 table@remove removed @79 +17:59:01.707892 table@remove removed @76 +17:59:01.708096 table@remove removed @73 +17:59:27.006678 db@close closing +17:59:27.006727 db@close done T·48.84µs diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/MANIFEST-000091 b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/MANIFEST-000091 new file mode 100644 index 0000000000..849bb8a758 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/MANIFEST-000091 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/cs.wal/wal b/.docker/container-state/nucleus-testnet-data/data/cs.wal/wal new file mode 100644 index 0000000000..1e135caef0 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/cs.wal/wal differ diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/000062.log b/.docker/container-state/nucleus-testnet-data/data/evidence.db/000062.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT new file mode 100644 index 0000000000..e8c02667ae --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000063 diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT.bak new file mode 100644 index 0000000000..ebafc63b8e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000061 diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/LOCK b/.docker/container-state/nucleus-testnet-data/data/evidence.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/LOG b/.docker/container-state/nucleus-testnet-data/data/evidence.db/LOG new file mode 100644 index 0000000000..bba04859e3 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/evidence.db/LOG @@ -0,0 +1,285 @@ +=============== Jun 4, 2024 (UTC) =============== +11:02:17.187082 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:02:17.188914 db@open opening +11:02:17.189040 version@stat F·[] S·0B[] Sc·[] +11:02:17.189650 db@janitor F·2 G·0 +11:02:17.189659 db@open done T·738.62µs +=============== Jun 6, 2024 (UTC) =============== +05:23:17.053111 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:23:17.053173 version@stat F·[] S·0B[] Sc·[] +05:23:17.053182 db@open opening +05:23:17.053205 journal@recovery F·1 +05:23:17.053281 journal@recovery recovering @1 +05:23:17.053412 version@stat F·[] S·0B[] Sc·[] +05:23:17.055873 db@janitor F·2 G·0 +05:23:17.055889 db@open done T·2.702971ms +=============== Jun 10, 2024 (UTC) =============== +10:09:49.023169 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:49.023239 version@stat F·[] S·0B[] Sc·[] +10:09:49.023249 db@open opening +10:09:49.023275 journal@recovery F·1 +10:09:49.023363 journal@recovery recovering @2 +10:09:49.023493 version@stat F·[] S·0B[] Sc·[] +10:09:49.026195 db@janitor F·2 G·0 +10:09:49.026206 db@open done T·2.953584ms +=============== Jun 10, 2024 (UTC) =============== +10:19:47.860561 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.860624 version@stat F·[] S·0B[] Sc·[] +10:19:47.860635 db@open opening +10:19:47.860667 journal@recovery F·1 +10:19:47.860754 journal@recovery recovering @4 +10:19:47.860899 version@stat F·[] S·0B[] Sc·[] +10:19:47.863633 db@janitor F·2 G·0 +10:19:47.863647 db@open done T·3.008235ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.703815 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.703905 version@stat F·[] S·0B[] Sc·[] +10:20:11.703917 db@open opening +10:20:11.703945 journal@recovery F·1 +10:20:11.705878 journal@recovery recovering @6 +10:20:11.706028 version@stat F·[] S·0B[] Sc·[] +10:20:11.708583 db@janitor F·2 G·0 +10:20:11.708596 db@open done T·4.675399ms +=============== Jun 10, 2024 (UTC) =============== +10:23:28.256992 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.257065 version@stat F·[] S·0B[] Sc·[] +10:23:28.257076 db@open opening +10:23:28.257105 journal@recovery F·1 +10:23:28.257199 journal@recovery recovering @8 +10:23:28.257354 version@stat F·[] S·0B[] Sc·[] +10:23:28.259997 db@janitor F·2 G·0 +10:23:28.260016 db@open done T·2.934545ms +=============== Jun 10, 2024 (UTC) =============== +10:27:08.430669 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.430731 version@stat F·[] S·0B[] Sc·[] +10:27:08.430741 db@open opening +10:27:08.430766 journal@recovery F·1 +10:27:08.430848 journal@recovery recovering @10 +10:27:08.430991 version@stat F·[] S·0B[] Sc·[] +10:27:08.433790 db@janitor F·2 G·0 +10:27:08.433810 db@open done T·3.064686ms +=============== Jun 10, 2024 (UTC) =============== +10:30:28.047713 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.047802 version@stat F·[] S·0B[] Sc·[] +10:30:28.047815 db@open opening +10:30:28.047854 journal@recovery F·1 +10:30:28.047961 journal@recovery recovering @12 +10:30:28.048120 version@stat F·[] S·0B[] Sc·[] +10:30:28.050790 db@janitor F·2 G·0 +10:30:28.050801 db@open done T·2.981765ms +=============== Jun 10, 2024 (UTC) =============== +11:11:44.624807 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.624892 version@stat F·[] S·0B[] Sc·[] +11:11:44.624907 db@open opening +11:11:44.624945 journal@recovery F·1 +11:11:44.625058 journal@recovery recovering @14 +11:11:44.625230 version@stat F·[] S·0B[] Sc·[] +11:11:44.627948 db@janitor F·2 G·0 +11:11:44.627959 db@open done T·3.047036ms +=============== Jun 10, 2024 (UTC) =============== +11:15:00.412843 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.412930 version@stat F·[] S·0B[] Sc·[] +11:15:00.412942 db@open opening +11:15:00.412971 journal@recovery F·1 +11:15:00.413060 journal@recovery recovering @16 +11:15:00.413215 version@stat F·[] S·0B[] Sc·[] +11:15:00.415839 db@janitor F·2 G·0 +11:15:00.415851 db@open done T·2.904874ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.234535 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.234617 version@stat F·[] S·0B[] Sc·[] +11:21:08.234632 db@open opening +11:21:08.234671 journal@recovery F·1 +11:21:08.234779 journal@recovery recovering @18 +11:21:08.234941 version@stat F·[] S·0B[] Sc·[] +11:21:08.237567 db@janitor F·2 G·0 +11:21:08.237584 db@open done T·2.947235ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.299878 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.299971 version@stat F·[] S·0B[] Sc·[] +11:22:28.299984 db@open opening +11:22:28.300023 journal@recovery F·1 +11:22:28.302041 journal@recovery recovering @20 +11:22:28.302197 version@stat F·[] S·0B[] Sc·[] +11:22:28.304730 db@janitor F·2 G·0 +11:22:28.304742 db@open done T·4.754419ms +=============== Jun 10, 2024 (UTC) =============== +11:23:15.265595 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.265664 version@stat F·[] S·0B[] Sc·[] +11:23:15.265674 db@open opening +11:23:15.265698 journal@recovery F·1 +11:23:15.265775 journal@recovery recovering @22 +11:23:15.265902 version@stat F·[] S·0B[] Sc·[] +11:23:15.268365 db@janitor F·2 G·0 +11:23:15.268375 db@open done T·2.697262ms +=============== Jun 10, 2024 (UTC) =============== +11:29:08.639966 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.640035 version@stat F·[] S·0B[] Sc·[] +11:29:08.640049 db@open opening +11:29:08.640077 journal@recovery F·1 +11:29:08.640316 journal@recovery recovering @24 +11:29:08.640704 version@stat F·[] S·0B[] Sc·[] +11:29:08.643423 db@janitor F·2 G·0 +11:29:08.643437 db@open done T·3.382309ms +=============== Jun 10, 2024 (UTC) =============== +11:30:42.323978 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.324044 version@stat F·[] S·0B[] Sc·[] +11:30:42.324055 db@open opening +11:30:42.324083 journal@recovery F·1 +11:30:42.324168 journal@recovery recovering @26 +11:30:42.324302 version@stat F·[] S·0B[] Sc·[] +11:30:42.326923 db@janitor F·2 G·0 +11:30:42.326934 db@open done T·2.875554ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.706903 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.706987 version@stat F·[] S·0B[] Sc·[] +11:30:55.707004 db@open opening +11:30:55.707033 journal@recovery F·1 +11:30:55.707112 journal@recovery recovering @28 +11:30:55.707250 version@stat F·[] S·0B[] Sc·[] +11:30:55.709798 db@janitor F·2 G·0 +11:30:55.709811 db@open done T·2.802093ms +=============== Jun 10, 2024 (UTC) =============== +11:36:36.096066 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.096134 version@stat F·[] S·0B[] Sc·[] +11:36:36.096146 db@open opening +11:36:36.096173 journal@recovery F·1 +11:36:36.096373 journal@recovery recovering @30 +11:36:36.096813 version@stat F·[] S·0B[] Sc·[] +11:36:36.099532 db@janitor F·2 G·0 +11:36:36.099551 db@open done T·3.400128ms +=============== Jun 10, 2024 (UTC) =============== +12:01:04.980476 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:04.980556 version@stat F·[] S·0B[] Sc·[] +12:01:04.980571 db@open opening +12:01:04.980604 journal@recovery F·1 +12:01:04.980714 journal@recovery recovering @32 +12:01:04.980988 version@stat F·[] S·0B[] Sc·[] +12:01:04.983784 db@janitor F·2 G·0 +12:01:04.983795 db@open done T·3.218849ms +=============== Jun 10, 2024 (UTC) =============== +12:02:59.970127 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:02:59.970222 version@stat F·[] S·0B[] Sc·[] +12:02:59.970239 db@open opening +12:02:59.970280 journal@recovery F·1 +12:02:59.970391 journal@recovery recovering @34 +12:02:59.970578 version@stat F·[] S·0B[] Sc·[] +12:02:59.973860 db@janitor F·2 G·0 +12:02:59.973876 db@open done T·3.630602ms +=============== Jun 10, 2024 (UTC) =============== +12:03:19.630533 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.630602 version@stat F·[] S·0B[] Sc·[] +12:03:19.630614 db@open opening +12:03:19.630641 journal@recovery F·1 +12:03:19.630721 journal@recovery recovering @36 +12:03:19.630848 version@stat F·[] S·0B[] Sc·[] +12:03:19.633451 db@janitor F·2 G·0 +12:03:19.633470 db@open done T·2.852455ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.559233 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.559349 version@stat F·[] S·0B[] Sc·[] +12:04:23.559367 db@open opening +12:04:23.559411 journal@recovery F·1 +12:04:23.559671 journal@recovery recovering @38 +12:04:23.560103 version@stat F·[] S·0B[] Sc·[] +12:04:23.563554 db@janitor F·2 G·0 +12:04:23.563570 db@open done T·4.197407ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.679721 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.679789 version@stat F·[] S·0B[] Sc·[] +12:07:46.679801 db@open opening +12:07:46.679829 journal@recovery F·1 +12:07:46.679909 journal@recovery recovering @40 +12:07:46.680047 version@stat F·[] S·0B[] Sc·[] +12:07:46.682611 db@janitor F·2 G·0 +12:07:46.682621 db@open done T·2.815235ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.074814 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.074917 version@stat F·[] S·0B[] Sc·[] +12:08:02.074933 db@open opening +12:08:02.074966 journal@recovery F·1 +12:08:02.075076 journal@recovery recovering @42 +12:08:02.075231 version@stat F·[] S·0B[] Sc·[] +12:08:02.077960 db@janitor F·2 G·0 +12:08:02.077978 db@open done T·3.038487ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.431883 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.432009 version@stat F·[] S·0B[] Sc·[] +12:27:21.432023 db@open opening +12:27:21.432054 journal@recovery F·1 +12:27:21.432163 journal@recovery recovering @44 +12:27:21.432338 version@stat F·[] S·0B[] Sc·[] +12:27:21.436666 db@janitor F·2 G·0 +12:27:21.436680 db@open done T·4.651501ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.794221 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.794281 version@stat F·[] S·0B[] Sc·[] +12:56:43.794292 db@open opening +12:56:43.794322 journal@recovery F·1 +12:56:43.794573 journal@recovery recovering @46 +12:56:43.794994 version@stat F·[] S·0B[] Sc·[] +12:56:43.797799 db@janitor F·2 G·0 +12:56:43.797809 db@open done T·3.513421ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.361625 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.361689 version@stat F·[] S·0B[] Sc·[] +12:57:38.361700 db@open opening +12:57:38.361725 journal@recovery F·1 +12:57:38.363627 journal@recovery recovering @48 +12:57:38.363765 version@stat F·[] S·0B[] Sc·[] +12:57:38.366313 db@janitor F·2 G·0 +12:57:38.366324 db@open done T·4.621091ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.458920 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.459002 version@stat F·[] S·0B[] Sc·[] +12:58:01.459016 db@open opening +12:58:01.459053 journal@recovery F·1 +12:58:01.459168 journal@recovery recovering @50 +12:58:01.459840 version@stat F·[] S·0B[] Sc·[] +12:58:01.463073 db@janitor F·2 G·0 +12:58:01.463087 db@open done T·4.065715ms +=============== Jun 10, 2024 (UTC) =============== +12:58:48.984610 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:48.984672 version@stat F·[] S·0B[] Sc·[] +12:58:48.984685 db@open opening +12:58:48.984711 journal@recovery F·1 +12:58:48.984791 journal@recovery recovering @52 +12:58:48.984935 version@stat F·[] S·0B[] Sc·[] +12:58:48.987578 db@janitor F·2 G·0 +12:58:48.987590 db@open done T·2.900696ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.226799 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.226858 version@stat F·[] S·0B[] Sc·[] +12:59:05.226870 db@open opening +12:59:05.226897 journal@recovery F·1 +12:59:05.226977 journal@recovery recovering @54 +12:59:05.227118 version@stat F·[] S·0B[] Sc·[] +12:59:05.229637 db@janitor F·2 G·0 +12:59:05.229649 db@open done T·2.774174ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.412547 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.412623 version@stat F·[] S·0B[] Sc·[] +12:59:32.412676 db@open opening +12:59:32.412712 journal@recovery F·1 +12:59:32.412975 journal@recovery recovering @56 +12:59:32.413331 version@stat F·[] S·0B[] Sc·[] +12:59:32.416748 db@janitor F·2 G·0 +12:59:32.416762 db@open done T·4.081375ms +=============== Jul 3, 2024 (UTC) =============== +17:52:58.329151 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.329241 version@stat F·[] S·0B[] Sc·[] +17:52:58.329256 db@open opening +17:52:58.329285 journal@recovery F·1 +17:52:58.329384 journal@recovery recovering @58 +17:52:58.329519 version@stat F·[] S·0B[] Sc·[] +17:52:58.332058 db@janitor F·2 G·0 +17:52:58.332074 db@open done T·2.812453ms +=============== Jul 3, 2024 (UTC) =============== +17:59:01.713169 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.713237 version@stat F·[] S·0B[] Sc·[] +17:59:01.713249 db@open opening +17:59:01.713276 journal@recovery F·1 +17:59:01.713370 journal@recovery recovering @60 +17:59:01.713504 version@stat F·[] S·0B[] Sc·[] +17:59:01.716861 db@janitor F·2 G·0 +17:59:01.716876 db@open done T·3.62282ms diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/MANIFEST-000063 b/.docker/container-state/nucleus-testnet-data/data/evidence.db/MANIFEST-000063 new file mode 100644 index 0000000000..f5b9b4efb8 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/evidence.db/MANIFEST-000063 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/priv_validator_state.json b/.docker/container-state/nucleus-testnet-data/data/priv_validator_state.json new file mode 100644 index 0000000000..8bf82338a8 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/priv_validator_state.json @@ -0,0 +1,7 @@ +{ + "height": "1036", + "round": 0, + "step": 3, + "signature": "v27Rs6us+6Q7cj+aLxVHFzzu3VuuAnLMflQ/rhuAh4CgOngnQ5pvY25d3yFM32fBXneJEm0bRYy/30RDgYtEDA==", + "signbytes": "740802110C0400000000000022480A20893D8ECF312013CEA3102503BF9E39C5C78EB6AEAFA0DE202529F944D899793B122408011220D72D5FD098990DF6F3EC52E4602F660D611936C04D5DD307585531367D52F82C2A0C08FEA296B4061082BDE6E502320F6E75636C6575732D746573746E6574" +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/000062.log b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/000062.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT new file mode 100644 index 0000000000..e8c02667ae --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000063 diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT.bak new file mode 100644 index 0000000000..ebafc63b8e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000061 diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/LOCK b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/LOG b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/LOG new file mode 100644 index 0000000000..f1dec582ce --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/LOG @@ -0,0 +1,285 @@ +=============== Jun 4, 2024 (UTC) =============== +11:02:17.160862 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:02:17.162765 db@open opening +11:02:17.164575 version@stat F·[] S·0B[] Sc·[] +11:02:17.165193 db@janitor F·2 G·0 +11:02:17.165200 db@open done T·2.42891ms +=============== Jun 6, 2024 (UTC) =============== +05:23:17.021523 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:23:17.021605 version@stat F·[] S·0B[] Sc·[] +05:23:17.021614 db@open opening +05:23:17.021640 journal@recovery F·1 +05:23:17.021803 journal@recovery recovering @1 +05:23:17.022954 version@stat F·[] S·0B[] Sc·[] +05:23:17.025420 db@janitor F·2 G·0 +05:23:17.025429 db@open done T·3.81236ms +=============== Jun 10, 2024 (UTC) =============== +10:09:48.982992 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:48.983060 version@stat F·[] S·0B[] Sc·[] +10:09:48.983070 db@open opening +10:09:48.983107 journal@recovery F·1 +10:09:48.983198 journal@recovery recovering @2 +10:09:48.983503 version@stat F·[] S·0B[] Sc·[] +10:09:48.986335 db@janitor F·2 G·0 +10:09:48.986347 db@open done T·3.273207ms +=============== Jun 10, 2024 (UTC) =============== +10:19:47.825155 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.825238 version@stat F·[] S·0B[] Sc·[] +10:19:47.825251 db@open opening +10:19:47.825278 journal@recovery F·1 +10:19:47.825359 journal@recovery recovering @4 +10:19:47.825768 version@stat F·[] S·0B[] Sc·[] +10:19:47.828609 db@janitor F·2 G·0 +10:19:47.828624 db@open done T·3.369679ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.676466 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.676528 version@stat F·[] S·0B[] Sc·[] +10:20:11.676538 db@open opening +10:20:11.676563 journal@recovery F·1 +10:20:11.676652 journal@recovery recovering @6 +10:20:11.676774 version@stat F·[] S·0B[] Sc·[] +10:20:11.679974 db@janitor F·2 G·0 +10:20:11.679984 db@open done T·3.442729ms +=============== Jun 10, 2024 (UTC) =============== +10:23:28.225439 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.225500 version@stat F·[] S·0B[] Sc·[] +10:23:28.225510 db@open opening +10:23:28.225534 journal@recovery F·1 +10:23:28.225612 journal@recovery recovering @8 +10:23:28.225858 version@stat F·[] S·0B[] Sc·[] +10:23:28.229607 db@janitor F·2 G·0 +10:23:28.229617 db@open done T·4.103634ms +=============== Jun 10, 2024 (UTC) =============== +10:27:08.399412 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.399485 version@stat F·[] S·0B[] Sc·[] +10:27:08.399496 db@open opening +10:27:08.399521 journal@recovery F·1 +10:27:08.399601 journal@recovery recovering @10 +10:27:08.399820 version@stat F·[] S·0B[] Sc·[] +10:27:08.404066 db@janitor F·2 G·0 +10:27:08.404080 db@open done T·4.580069ms +=============== Jun 10, 2024 (UTC) =============== +10:30:28.013547 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.013654 version@stat F·[] S·0B[] Sc·[] +10:30:28.013666 db@open opening +10:30:28.013698 journal@recovery F·1 +10:30:28.013791 journal@recovery recovering @12 +10:30:28.013945 version@stat F·[] S·0B[] Sc·[] +10:30:28.016558 db@janitor F·2 G·0 +10:30:28.016571 db@open done T·2.900484ms +=============== Jun 10, 2024 (UTC) =============== +11:11:44.589857 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.589970 version@stat F·[] S·0B[] Sc·[] +11:11:44.589985 db@open opening +11:11:44.590018 journal@recovery F·1 +11:11:44.590110 journal@recovery recovering @14 +11:11:44.590348 version@stat F·[] S·0B[] Sc·[] +11:11:44.593982 db@janitor F·2 G·0 +11:11:44.593996 db@open done T·4.005713ms +=============== Jun 10, 2024 (UTC) =============== +11:15:00.380028 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.380099 version@stat F·[] S·0B[] Sc·[] +11:15:00.380109 db@open opening +11:15:00.380136 journal@recovery F·1 +11:15:00.380229 journal@recovery recovering @16 +11:15:00.380384 version@stat F·[] S·0B[] Sc·[] +11:15:00.383015 db@janitor F·2 G·0 +11:15:00.383037 db@open done T·2.921814ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.202896 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.203014 version@stat F·[] S·0B[] Sc·[] +11:21:08.203027 db@open opening +11:21:08.203063 journal@recovery F·1 +11:21:08.203151 journal@recovery recovering @18 +11:21:08.203297 version@stat F·[] S·0B[] Sc·[] +11:21:08.205886 db@janitor F·2 G·0 +11:21:08.205897 db@open done T·2.865774ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.267111 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.267194 version@stat F·[] S·0B[] Sc·[] +11:22:28.267211 db@open opening +11:22:28.267251 journal@recovery F·1 +11:22:28.269348 journal@recovery recovering @20 +11:22:28.271266 version@stat F·[] S·0B[] Sc·[] +11:22:28.273866 db@janitor F·2 G·0 +11:22:28.273881 db@open done T·6.665886ms +=============== Jun 10, 2024 (UTC) =============== +11:23:15.234286 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.234355 version@stat F·[] S·0B[] Sc·[] +11:23:15.234365 db@open opening +11:23:15.234391 journal@recovery F·1 +11:23:15.236240 journal@recovery recovering @22 +11:23:15.236377 version@stat F·[] S·0B[] Sc·[] +11:23:15.238935 db@janitor F·2 G·0 +11:23:15.238946 db@open done T·4.577368ms +=============== Jun 10, 2024 (UTC) =============== +11:29:08.603809 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.603877 version@stat F·[] S·0B[] Sc·[] +11:29:08.603891 db@open opening +11:29:08.603922 journal@recovery F·1 +11:29:08.604005 journal@recovery recovering @24 +11:29:08.604260 version@stat F·[] S·0B[] Sc·[] +11:29:08.607136 db@janitor F·2 G·0 +11:29:08.607152 db@open done T·3.256208ms +=============== Jun 10, 2024 (UTC) =============== +11:30:42.282203 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.282303 version@stat F·[] S·0B[] Sc·[] +11:30:42.282316 db@open opening +11:30:42.282344 journal@recovery F·1 +11:30:42.284184 journal@recovery recovering @26 +11:30:42.284358 version@stat F·[] S·0B[] Sc·[] +11:30:42.287694 db@janitor F·2 G·0 +11:30:42.287706 db@open done T·5.385445ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.669352 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.669425 version@stat F·[] S·0B[] Sc·[] +11:30:55.669437 db@open opening +11:30:55.669465 journal@recovery F·1 +11:30:55.671338 journal@recovery recovering @28 +11:30:55.671494 version@stat F·[] S·0B[] Sc·[] +11:30:55.675253 db@janitor F·2 G·0 +11:30:55.675272 db@open done T·5.825019ms +=============== Jun 10, 2024 (UTC) =============== +11:36:36.058381 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.058454 version@stat F·[] S·0B[] Sc·[] +11:36:36.058468 db@open opening +11:36:36.058498 journal@recovery F·1 +11:36:36.058575 journal@recovery recovering @30 +11:36:36.058874 version@stat F·[] S·0B[] Sc·[] +11:36:36.061701 db@janitor F·2 G·0 +11:36:36.061715 db@open done T·3.241157ms +=============== Jun 10, 2024 (UTC) =============== +12:01:04.941417 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:04.941487 version@stat F·[] S·0B[] Sc·[] +12:01:04.941499 db@open opening +12:01:04.941526 journal@recovery F·1 +12:01:04.943164 journal@recovery recovering @32 +12:01:04.943305 version@stat F·[] S·0B[] Sc·[] +12:01:04.946000 db@janitor F·2 G·0 +12:01:04.946011 db@open done T·4.50795ms +=============== Jun 10, 2024 (UTC) =============== +12:02:59.933301 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:02:59.933378 version@stat F·[] S·0B[] Sc·[] +12:02:59.933391 db@open opening +12:02:59.933419 journal@recovery F·1 +12:02:59.933504 journal@recovery recovering @34 +12:02:59.933639 version@stat F·[] S·0B[] Sc·[] +12:02:59.936268 db@janitor F·2 G·0 +12:02:59.936283 db@open done T·2.887636ms +=============== Jun 10, 2024 (UTC) =============== +12:03:19.593709 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.593780 version@stat F·[] S·0B[] Sc·[] +12:03:19.593792 db@open opening +12:03:19.593819 journal@recovery F·1 +12:03:19.593901 journal@recovery recovering @36 +12:03:19.594034 version@stat F·[] S·0B[] Sc·[] +12:03:19.596646 db@janitor F·2 G·0 +12:03:19.596665 db@open done T·2.867925ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.521701 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.521775 version@stat F·[] S·0B[] Sc·[] +12:04:23.521787 db@open opening +12:04:23.521818 journal@recovery F·1 +12:04:23.521905 journal@recovery recovering @38 +12:04:23.522047 version@stat F·[] S·0B[] Sc·[] +12:04:23.525587 db@janitor F·2 G·0 +12:04:23.525599 db@open done T·3.808243ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.646403 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.646479 version@stat F·[] S·0B[] Sc·[] +12:07:46.646491 db@open opening +12:07:46.646522 journal@recovery F·1 +12:07:46.648326 journal@recovery recovering @40 +12:07:46.648463 version@stat F·[] S·0B[] Sc·[] +12:07:46.651146 db@janitor F·2 G·0 +12:07:46.651157 db@open done T·4.661321ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.042365 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.042432 version@stat F·[] S·0B[] Sc·[] +12:08:02.042442 db@open opening +12:08:02.042475 journal@recovery F·1 +12:08:02.042558 journal@recovery recovering @42 +12:08:02.042679 version@stat F·[] S·0B[] Sc·[] +12:08:02.045282 db@janitor F·2 G·0 +12:08:02.045297 db@open done T·2.848245ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.390931 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.391048 version@stat F·[] S·0B[] Sc·[] +12:27:21.391068 db@open opening +12:27:21.391111 journal@recovery F·1 +12:27:21.391230 journal@recovery recovering @44 +12:27:21.391421 version@stat F·[] S·0B[] Sc·[] +12:27:21.394216 db@janitor F·2 G·0 +12:27:21.394232 db@open done T·3.158397ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.758036 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.758118 version@stat F·[] S·0B[] Sc·[] +12:56:43.758132 db@open opening +12:56:43.758159 journal@recovery F·1 +12:56:43.758252 journal@recovery recovering @46 +12:56:43.758481 version@stat F·[] S·0B[] Sc·[] +12:56:43.761326 db@janitor F·2 G·0 +12:56:43.761341 db@open done T·3.203428ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.328344 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.328411 version@stat F·[] S·0B[] Sc·[] +12:57:38.328421 db@open opening +12:57:38.328447 journal@recovery F·1 +12:57:38.330433 journal@recovery recovering @48 +12:57:38.330573 version@stat F·[] S·0B[] Sc·[] +12:57:38.333314 db@janitor F·2 G·0 +12:57:38.333323 db@open done T·4.898373ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.424360 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.424428 version@stat F·[] S·0B[] Sc·[] +12:58:01.424439 db@open opening +12:58:01.424465 journal@recovery F·1 +12:58:01.424555 journal@recovery recovering @50 +12:58:01.424862 version@stat F·[] S·0B[] Sc·[] +12:58:01.427853 db@janitor F·2 G·0 +12:58:01.427866 db@open done T·3.42119ms +=============== Jun 10, 2024 (UTC) =============== +12:58:48.947445 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:48.947531 version@stat F·[] S·0B[] Sc·[] +12:58:48.947544 db@open opening +12:58:48.947575 journal@recovery F·1 +12:58:48.949608 journal@recovery recovering @52 +12:58:48.949761 version@stat F·[] S·0B[] Sc·[] +12:58:48.952378 db@janitor F·2 G·0 +12:58:48.952391 db@open done T·4.842882ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.190019 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.190081 version@stat F·[] S·0B[] Sc·[] +12:59:05.190092 db@open opening +12:59:05.190125 journal@recovery F·1 +12:59:05.192033 journal@recovery recovering @54 +12:59:05.194101 version@stat F·[] S·0B[] Sc·[] +12:59:05.196707 db@janitor F·2 G·0 +12:59:05.196719 db@open done T·6.621927ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.373105 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.373178 version@stat F·[] S·0B[] Sc·[] +12:59:32.373198 db@open opening +12:59:32.373227 journal@recovery F·1 +12:59:32.373311 journal@recovery recovering @56 +12:59:32.373559 version@stat F·[] S·0B[] Sc·[] +12:59:32.376912 db@janitor F·2 G·0 +12:59:32.376924 db@open done T·3.720422ms +=============== Jul 3, 2024 (UTC) =============== +17:52:58.295705 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.295775 version@stat F·[] S·0B[] Sc·[] +17:52:58.295785 db@open opening +17:52:58.295817 journal@recovery F·1 +17:52:58.295900 journal@recovery recovering @58 +17:52:58.296023 version@stat F·[] S·0B[] Sc·[] +17:52:58.298922 db@janitor F·2 G·0 +17:52:58.298936 db@open done T·3.146896ms +=============== Jul 3, 2024 (UTC) =============== +17:59:01.670860 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.670987 version@stat F·[] S·0B[] Sc·[] +17:59:01.670999 db@open opening +17:59:01.671030 journal@recovery F·1 +17:59:01.671128 journal@recovery recovering @60 +17:59:01.671281 version@stat F·[] S·0B[] Sc·[] +17:59:01.674069 db@janitor F·2 G·0 +17:59:01.674079 db@open done T·3.076606ms diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/MANIFEST-000063 b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/MANIFEST-000063 new file mode 100644 index 0000000000..f5b9b4efb8 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/MANIFEST-000063 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/000088.ldb b/.docker/container-state/nucleus-testnet-data/data/state.db/000088.ldb new file mode 100644 index 0000000000..72caa9fb6f Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/state.db/000088.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/000091.ldb b/.docker/container-state/nucleus-testnet-data/data/state.db/000091.ldb new file mode 100644 index 0000000000..ce41e60f12 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/state.db/000091.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/000092.log b/.docker/container-state/nucleus-testnet-data/data/state.db/000092.log new file mode 100644 index 0000000000..7aa8691b72 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/state.db/000092.log differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT new file mode 100644 index 0000000000..f60e23b00c --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000093 diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT.bak new file mode 100644 index 0000000000..2f2c868af7 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000090 diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/LOCK b/.docker/container-state/nucleus-testnet-data/data/state.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/LOG b/.docker/container-state/nucleus-testnet-data/data/state.db/LOG new file mode 100644 index 0000000000..c811eadd21 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/state.db/LOG @@ -0,0 +1,396 @@ +=============== Jun 4, 2024 (UTC) =============== +11:02:17.169926 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:02:17.171758 db@open opening +11:02:17.171929 version@stat F·[] S·0B[] Sc·[] +11:02:17.172614 db@janitor F·2 G·0 +11:02:17.172624 db@open done T·859.55µs +11:20:27.051196 db@close closing +11:20:27.051229 db@close done T·32.611µs +=============== Jun 6, 2024 (UTC) =============== +05:23:17.034700 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:23:17.034755 version@stat F·[] S·0B[] Sc·[] +05:23:17.034764 db@open opening +05:23:17.034786 journal@recovery F·1 +05:23:17.034963 journal@recovery recovering @1 +05:23:17.038139 memdb@flush created L0@2 N·1090 S·217KiB "abc..y:1,v6":"val..:99,v488" +05:23:17.040121 version@stat F·[1] S·217KiB[217KiB] Sc·[0.25] +05:23:17.042809 db@janitor F·3 G·0 +05:23:17.042819 db@open done T·8.051963ms +05:43:54.555468 db@close closing +05:43:54.555490 db@close done T·22.451µs +=============== Jun 10, 2024 (UTC) =============== +10:09:49.001598 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:49.001771 version@stat F·[1] S·217KiB[217KiB] Sc·[0.25] +10:09:49.001787 db@open opening +10:09:49.001834 journal@recovery F·1 +10:09:49.001967 journal@recovery recovering @3 +10:09:49.006540 memdb@flush created L0@5 N·1235 S·238KiB "abc..218,v1092":"val..466,v2324" +10:09:49.006846 version@stat F·[2] S·456KiB[456KiB] Sc·[0.50] +10:09:49.010108 db@janitor F·4 G·0 +10:09:49.010120 db@open done T·8.329177ms +10:17:29.618645 table@compaction L0·2 -> L1·0 S·456KiB Q·2782 +10:17:29.622427 table@build created L1@8 N·1398 S·192KiB "abc..y:1,v6":"val..:99,v488" +10:17:29.622451 version@stat F·[0 1] S·192KiB[0B 192KiB] Sc·[0.00 0.00] +10:17:29.623984 table@compaction committed F-1 S-263KiB Ke·0 D·927 T·5.320955ms +10:17:29.624090 table@remove removed @2 +10:19:38.331989 db@close closing +10:19:38.332018 db@close done T·29.511µs +=============== Jun 10, 2024 (UTC) =============== +10:19:47.845603 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.845689 version@stat F·[0 1] S·192KiB[0B 192KiB] Sc·[0.00 0.00] +10:19:47.845699 db@open opening +10:19:47.845727 journal@recovery F·1 +10:19:47.845811 journal@recovery recovering @6 +10:19:47.848260 memdb@flush created L0@9 N·585 S·107KiB "abc..465,v2328":"val..583,v2910" +10:19:47.848595 version@stat F·[1 1] S·300KiB[107KiB 192KiB] Sc·[0.25 0.00] +10:19:47.851412 db@janitor F·5 G·1 +10:19:47.851418 db@janitor removing table-5 +10:19:47.851496 db@open done T·5.792829ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.696554 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.696634 version@stat F·[1 1] S·300KiB[107KiB 192KiB] Sc·[0.25 0.00] +10:20:11.696646 db@open opening +10:20:11.696673 journal@recovery F·1 +10:20:11.696758 journal@recovery recovering @10 +10:20:11.696889 version@stat F·[1 1] S·300KiB[107KiB 192KiB] Sc·[0.25 0.00] +10:20:11.699600 db@janitor F·4 G·0 +10:20:11.699614 db@open done T·2.964835ms +10:23:04.295043 db@close closing +10:23:04.295073 db@close done T·29.89µs +=============== Jun 10, 2024 (UTC) =============== +10:23:28.245438 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.245925 version@stat F·[1 1] S·300KiB[107KiB 192KiB] Sc·[0.25 0.00] +10:23:28.245945 db@open opening +10:23:28.246022 journal@recovery F·1 +10:23:28.246220 journal@recovery recovering @12 +10:23:28.247738 memdb@flush created L0@14 N·170 S·33KiB "abc..582,v2914":"val..617,v3081" +10:23:28.248222 version@stat F·[2 1] S·333KiB[140KiB 192KiB] Sc·[0.50 0.00] +10:23:28.251598 db@janitor F·5 G·0 +10:23:28.251610 db@open done T·5.660847ms +10:26:58.662998 db@close closing +10:26:58.663040 db@close done T·41.29µs +=============== Jun 10, 2024 (UTC) =============== +10:27:08.420118 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.420212 version@stat F·[2 1] S·333KiB[140KiB 192KiB] Sc·[0.50 0.00] +10:27:08.420223 db@open opening +10:27:08.420254 journal@recovery F·1 +10:27:08.420427 journal@recovery recovering @15 +10:27:08.421948 memdb@flush created L0@17 N·210 S·39KiB "abc..616,v3085":"val..659,v3292" +10:27:08.422255 version@stat F·[3 1] S·372KiB[179KiB 192KiB] Sc·[0.75 0.00] +10:27:08.425139 db@janitor F·6 G·0 +10:27:08.425151 db@open done T·4.924371ms +10:28:46.710173 db@close closing +10:28:46.710215 db@close done T·41.64µs +=============== Jun 10, 2024 (UTC) =============== +10:30:28.035683 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.035773 version@stat F·[3 1] S·372KiB[179KiB 192KiB] Sc·[0.75 0.00] +10:30:28.035785 db@open opening +10:30:28.035824 journal@recovery F·1 +10:30:28.035921 journal@recovery recovering @18 +10:30:28.036916 memdb@flush created L0@20 N·95 S·19KiB "abc..658,v3296":"val..678,v3388" +10:30:28.037233 version@stat F·[4 1] S·392KiB[199KiB 192KiB] Sc·[1.00 0.00] +10:30:28.039918 db@janitor F·7 G·0 +10:30:28.039932 db@open done T·4.142944ms +10:30:28.039957 table@compaction L0·4 -> L1·1 S·392KiB Q·3391 +10:30:28.044217 table@build created L1@23 N·2034 S·273KiB "abc..y:1,v6":"val..:99,v488" +10:30:28.044258 version@stat F·[0 1] S·273KiB[0B 273KiB] Sc·[0.00 0.00] +10:30:28.045300 table@compaction committed F-4 S-118KiB Ke·0 D·424 T·5.324424ms +10:30:28.045359 table@remove removed @17 +10:30:28.045459 table@remove removed @14 +10:30:28.045509 table@remove removed @9 +10:30:28.045570 table@remove removed @8 +10:34:40.140467 db@close closing +10:34:40.140495 db@close done T·28.811µs +=============== Jun 10, 2024 (UTC) =============== +11:11:44.613640 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.613730 version@stat F·[0 1] S·273KiB[0B 273KiB] Sc·[0.00 0.00] +11:11:44.613741 db@open opening +11:11:44.613768 journal@recovery F·1 +11:11:44.613936 journal@recovery recovering @21 +11:11:44.615591 memdb@flush created L0@24 N·250 S·46KiB "abc..677,v3392":"val..728,v3639" +11:11:44.615920 version@stat F·[1 1] S·320KiB[46KiB 273KiB] Sc·[0.25 0.00] +11:11:44.618540 db@janitor F·5 G·1 +11:11:44.618547 db@janitor removing table-20 +11:11:44.618585 db@open done T·4.83984ms +11:12:55.522384 table@compaction L0·1 -> L1·1 S·320KiB Q·3712 +11:12:55.525587 table@build created L1@27 N·2184 S·292KiB "abc..y:1,v6":"val..:99,v488" +11:12:55.525608 version@stat F·[0 1] S·292KiB[0B 292KiB] Sc·[0.00 0.00] +11:12:55.526197 table@compaction committed F-1 S-28KiB Ke·0 D·100 T·3.792921ms +11:12:55.526304 table@remove removed @23 +11:13:46.210625 db@close closing +11:13:46.210682 db@close done T·56.52µs +=============== Jun 10, 2024 (UTC) =============== +11:15:00.401087 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.401167 version@stat F·[0 1] S·292KiB[0B 292KiB] Sc·[0.00 0.00] +11:15:00.401179 db@open opening +11:15:00.401215 journal@recovery F·1 +11:15:00.401300 journal@recovery recovering @25 +11:15:00.402497 memdb@flush created L0@28 N·120 S·44KiB "abc..727,v3643":"val..752,v3760" +11:15:00.402737 version@stat F·[1 1] S·336KiB[44KiB 292KiB] Sc·[0.25 0.00] +11:15:00.405667 db@janitor F·5 G·1 +11:15:00.405675 db@janitor removing table-24 +11:15:00.405715 db@open done T·4.533188ms +=============== Jun 10, 2024 (UTC) =============== +11:21:08.225008 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.225077 version@stat F·[1 1] S·336KiB[44KiB 292KiB] Sc·[0.25 0.00] +11:21:08.225087 db@open opening +11:21:08.225120 journal@recovery F·1 +11:21:08.225204 journal@recovery recovering @29 +11:21:08.225515 version@stat F·[1 1] S·336KiB[44KiB 292KiB] Sc·[0.25 0.00] +11:21:08.228249 db@janitor F·4 G·0 +11:21:08.228259 db@open done T·3.163386ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.290853 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.290916 version@stat F·[1 1] S·336KiB[44KiB 292KiB] Sc·[0.25 0.00] +11:22:28.290927 db@open opening +11:22:28.290952 journal@recovery F·1 +11:22:28.291031 journal@recovery recovering @31 +11:22:28.291738 memdb@flush created L0@33 N·5 S·1KiB "abc..751,v3764":"val..753,v3766" +11:22:28.291843 version@stat F·[2 1] S·338KiB[45KiB 292KiB] Sc·[0.50 0.00] +11:22:28.294374 db@janitor F·5 G·0 +11:22:28.294388 db@open done T·3.457658ms +11:22:57.093696 db@close closing +11:22:57.093729 db@close done T·32.36µs +=============== Jun 10, 2024 (UTC) =============== +11:23:15.255337 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.255429 version@stat F·[2 1] S·338KiB[45KiB 292KiB] Sc·[0.50 0.00] +11:23:15.255440 db@open opening +11:23:15.255468 journal@recovery F·1 +11:23:15.255632 journal@recovery recovering @34 +11:23:15.256593 memdb@flush created L0@36 N·25 S·5KiB "abc..752,v3770":"val..758,v3792" +11:23:15.256919 version@stat F·[3 1] S·343KiB[51KiB 292KiB] Sc·[0.75 0.00] +11:23:15.259471 db@janitor F·6 G·0 +11:23:15.259483 db@open done T·4.039333ms +11:27:45.632206 table@compaction L0·3 -> L1·1 S·343KiB Q·4060 +11:27:45.635970 table@build created L1@39 N·2274 S·314KiB "abc..y:1,v6":"val..:99,v488" +11:27:45.635994 version@stat F·[0 1] S·314KiB[0B 314KiB] Sc·[0.00 0.00] +11:27:45.637490 table@compaction committed F-3 S-29KiB Ke·0 D·60 T·5.261094ms +11:27:45.637552 table@remove removed @33 +11:27:45.637596 table@remove removed @28 +11:27:45.637689 table@remove removed @27 +11:28:46.931096 db@close closing +11:28:46.931135 db@close done T·38.64µs +=============== Jun 10, 2024 (UTC) =============== +11:29:08.628446 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.628530 version@stat F·[0 1] S·314KiB[0B 314KiB] Sc·[0.00 0.00] +11:29:08.628543 db@open opening +11:29:08.628575 journal@recovery F·1 +11:29:08.628662 journal@recovery recovering @37 +11:29:08.630178 memdb@flush created L0@40 N·330 S·60KiB "abc..757,v3796":"val..824,v4123" +11:29:08.630297 version@stat F·[1 1] S·375KiB[60KiB 314KiB] Sc·[0.25 0.00] +11:29:08.633799 db@janitor F·5 G·1 +11:29:08.633809 db@janitor removing table-36 +11:29:08.633844 db@open done T·5.296244ms +11:30:37.043777 db@close closing +11:30:37.043826 db@close done T·46.751µs +=============== Jun 10, 2024 (UTC) =============== +11:30:42.312510 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.312612 version@stat F·[1 1] S·375KiB[60KiB 314KiB] Sc·[0.25 0.00] +11:30:42.312630 db@open opening +11:30:42.312676 journal@recovery F·1 +11:30:42.312783 journal@recovery recovering @41 +11:30:42.313896 memdb@flush created L0@43 N·85 S·16KiB "abc..823,v4127":"val..841,v4209" +11:30:42.314203 version@stat F·[2 1] S·391KiB[77KiB 314KiB] Sc·[0.50 0.00] +11:30:42.316961 db@janitor F·5 G·0 +11:30:42.316978 db@open done T·4.342496ms +=============== Jun 10, 2024 (UTC) =============== +11:30:55.695865 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.695992 version@stat F·[2 1] S·391KiB[77KiB 314KiB] Sc·[0.50 0.00] +11:30:55.696007 db@open opening +11:30:55.696041 journal@recovery F·1 +11:30:55.696135 journal@recovery recovering @44 +11:30:55.698557 memdb@flush created L0@46 N·5 S·1KiB "abc..840,v4213":"val..842,v4215" +11:30:55.698730 version@stat F·[3 1] S·392KiB[78KiB 314KiB] Sc·[0.75 0.00] +11:30:55.701328 db@janitor F·6 G·0 +11:30:55.701348 db@open done T·5.337075ms +11:35:56.722638 db@close closing +11:35:56.722688 db@close done T·49.24µs +=============== Jun 10, 2024 (UTC) =============== +11:36:36.082968 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.083044 version@stat F·[3 1] S·392KiB[78KiB 314KiB] Sc·[0.75 0.00] +11:36:36.083056 db@open opening +11:36:36.083087 journal@recovery F·1 +11:36:36.083363 journal@recovery recovering @47 +11:36:36.085402 memdb@flush created L0@49 N·300 S·54KiB "abc..841,v4219":"val..902,v4516" +11:36:36.085536 version@stat F·[4 1] S·447KiB[133KiB 314KiB] Sc·[1.00 0.00] +11:36:36.088497 db@janitor F·7 G·0 +11:36:36.088511 db@open done T·5.450696ms +11:36:36.088549 table@compaction L0·4 -> L1·1 S·447KiB Q·4519 +11:36:36.093288 table@build created L1@52 N·2706 S·369KiB "abc..y:1,v6":"val..:99,v488" +11:36:36.093313 version@stat F·[0 1] S·369KiB[0B 369KiB] Sc·[0.00 0.00] +11:36:36.094811 table@compaction committed F-4 S-78KiB Ke·0 D·288 T·6.229052ms +11:36:36.094881 table@remove removed @46 +11:36:36.094975 table@remove removed @43 +11:36:36.095038 table@remove removed @40 +11:36:36.095147 table@remove removed @39 +11:40:48.235780 db@close closing +11:40:48.235814 db@close done T·33.03µs +=============== Jun 10, 2024 (UTC) =============== +12:01:04.967260 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:04.967353 version@stat F·[0 1] S·369KiB[0B 369KiB] Sc·[0.00 0.00] +12:01:04.967366 db@open opening +12:01:04.967398 journal@recovery F·1 +12:01:04.969218 journal@recovery recovering @50 +12:01:04.970473 memdb@flush created L0@53 N·250 S·46KiB "abc..901,v4520":"val..952,v4767" +12:01:04.970598 version@stat F·[1 1] S·416KiB[46KiB 369KiB] Sc·[0.25 0.00] +12:01:04.974974 db@janitor F·5 G·1 +12:01:04.974982 db@janitor removing table-49 +12:01:04.975024 db@open done T·7.653087ms +12:01:23.716876 db@close closing +12:01:23.716904 db@close done T·27.41µs +=============== Jun 10, 2024 (UTC) =============== +12:02:59.958593 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:02:59.958685 version@stat F·[1 1] S·416KiB[46KiB 369KiB] Sc·[0.25 0.00] +12:02:59.958697 db@open opening +12:02:59.958725 journal@recovery F·1 +12:02:59.958811 journal@recovery recovering @54 +12:02:59.959590 memdb@flush created L0@56 N·15 S·2KiB "abc..951,v4771":"val..955,v4783" +12:02:59.959926 version@stat F·[2 1] S·419KiB[49KiB 369KiB] Sc·[0.50 0.00] +12:02:59.962721 db@janitor F·5 G·0 +12:02:59.962740 db@open done T·4.037675ms +=============== Jun 10, 2024 (UTC) =============== +12:03:19.619068 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.619159 version@stat F·[2 1] S·419KiB[49KiB 369KiB] Sc·[0.50 0.00] +12:03:19.619171 db@open opening +12:03:19.619209 journal@recovery F·1 +12:03:19.619293 journal@recovery recovering @57 +12:03:19.620092 memdb@flush created L0@59 N·10 S·2KiB "abc..954,v4787":"val..957,v4794" +12:03:19.620371 version@stat F·[3 1] S·421KiB[52KiB 369KiB] Sc·[0.75 0.00] +12:03:19.623516 db@janitor F·6 G·0 +12:03:19.623536 db@open done T·4.359919ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.547604 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.547706 version@stat F·[3 1] S·421KiB[52KiB 369KiB] Sc·[0.75 0.00] +12:04:23.547724 db@open opening +12:04:23.547777 journal@recovery F·1 +12:04:23.550018 journal@recovery recovering @60 +12:04:23.550785 memdb@flush created L0@62 N·5 S·1KiB "abc..956,v4798":"val..958,v4800" +12:04:23.550942 version@stat F·[4 1] S·423KiB[53KiB 369KiB] Sc·[1.00 0.00] +12:04:23.554488 db@janitor F·7 G·0 +12:04:23.554502 db@open done T·6.771829ms +12:04:23.554540 table@compaction L0·4 -> L1·1 S·423KiB Q·4803 +12:04:23.560321 table@build created L1@65 N·2874 S·390KiB "abc..y:1,v6":"val..:99,v488" +12:04:23.560358 version@stat F·[0 1] S·390KiB[0B 390KiB] Sc·[0.00 0.00] +12:04:23.561465 table@compaction committed F-4 S-32KiB Ke·0 D·112 T·6.830041ms +12:04:23.561559 table@remove removed @59 +12:04:23.561683 table@remove removed @56 +12:04:23.561737 table@remove removed @53 +12:04:23.561844 table@remove removed @52 +=============== Jun 10, 2024 (UTC) =============== +12:07:46.670266 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.670379 version@stat F·[0 1] S·390KiB[0B 390KiB] Sc·[0.00 0.00] +12:07:46.670400 db@open opening +12:07:46.670446 journal@recovery F·1 +12:07:46.670707 journal@recovery recovering @63 +12:07:46.671008 version@stat F·[0 1] S·390KiB[0B 390KiB] Sc·[0.00 0.00] +12:07:46.673726 db@janitor F·4 G·1 +12:07:46.673744 db@janitor removing table-62 +12:07:46.673784 db@open done T·3.3777ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.066883 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.066971 version@stat F·[0 1] S·390KiB[0B 390KiB] Sc·[0.00 0.00] +12:08:02.066983 db@open opening +12:08:02.067013 journal@recovery F·1 +12:08:02.067205 journal@recovery recovering @66 +12:08:02.067764 version@stat F·[0 1] S·390KiB[0B 390KiB] Sc·[0.00 0.00] +12:08:02.070467 db@janitor F·3 G·0 +12:08:02.070483 db@open done T·3.495051ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.423944 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.424164 version@stat F·[0 1] S·390KiB[0B 390KiB] Sc·[0.00 0.00] +12:27:21.424182 db@open opening +12:27:21.424225 journal@recovery F·1 +12:27:21.424560 journal@recovery recovering @68 +12:27:21.425123 version@stat F·[0 1] S·390KiB[0B 390KiB] Sc·[0.00 0.00] +12:27:21.427788 db@janitor F·3 G·0 +12:27:21.427802 db@open done T·3.608771ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.782384 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.782466 version@stat F·[0 1] S·390KiB[0B 390KiB] Sc·[0.00 0.00] +12:56:43.782481 db@open opening +12:56:43.782514 journal@recovery F·1 +12:56:43.784553 journal@recovery recovering @70 +12:56:43.785342 memdb@flush created L0@72 N·10 S·2KiB "abc..957,v4804":"val..960,v4811" +12:56:43.785469 version@stat F·[1 1] S·393KiB[2KiB 390KiB] Sc·[0.25 0.00] +12:56:43.788122 db@janitor F·4 G·0 +12:56:43.788135 db@open done T·5.649129ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.353522 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.353605 version@stat F·[1 1] S·393KiB[2KiB 390KiB] Sc·[0.25 0.00] +12:57:38.353620 db@open opening +12:57:38.353659 journal@recovery F·1 +12:57:38.353761 journal@recovery recovering @73 +12:57:38.353914 version@stat F·[1 1] S·393KiB[2KiB 390KiB] Sc·[0.25 0.00] +12:57:38.356549 db@janitor F·4 G·0 +12:57:38.356566 db@open done T·2.941386ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.450715 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.450788 version@stat F·[1 1] S·393KiB[2KiB 390KiB] Sc·[0.25 0.00] +12:58:01.450800 db@open opening +12:58:01.450834 journal@recovery F·1 +12:58:01.451013 journal@recovery recovering @75 +12:58:01.451751 memdb@flush created L0@77 N·5 S·1KiB "abc..959,v4815":"val..961,v4817" +12:58:01.451877 version@stat F·[2 1] S·394KiB[3KiB 390KiB] Sc·[0.50 0.00] +12:58:01.454428 db@janitor F·5 G·0 +12:58:01.454441 db@open done T·3.635832ms +=============== Jun 10, 2024 (UTC) =============== +12:58:48.972542 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:48.972608 version@stat F·[2 1] S·394KiB[3KiB 390KiB] Sc·[0.50 0.00] +12:58:48.972619 db@open opening +12:58:48.972645 journal@recovery F·1 +12:58:48.972799 journal@recovery recovering @78 +12:58:48.973750 memdb@flush created L0@80 N·10 S·2KiB "abc..960,v4821":"val..963,v4828" +12:58:48.975745 version@stat F·[3 1] S·396KiB[6KiB 390KiB] Sc·[0.75 0.00] +12:58:48.978310 db@janitor F·6 G·0 +12:58:48.978328 db@open done T·5.706739ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.216181 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.216268 version@stat F·[3 1] S·396KiB[6KiB 390KiB] Sc·[0.75 0.00] +12:59:05.216281 db@open opening +12:59:05.216313 journal@recovery F·1 +12:59:05.216396 journal@recovery recovering @81 +12:59:05.216630 version@stat F·[3 1] S·396KiB[6KiB 390KiB] Sc·[0.75 0.00] +12:59:05.219289 db@janitor F·6 G·0 +12:59:05.219303 db@open done T·3.017166ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.398825 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.398920 version@stat F·[3 1] S·396KiB[6KiB 390KiB] Sc·[0.75 0.00] +12:59:32.398933 db@open opening +12:59:32.398966 journal@recovery F·1 +12:59:32.401293 journal@recovery recovering @83 +12:59:32.402150 memdb@flush created L0@85 N·5 S·1KiB "abc..962,v4832":"val..964,v4834" +12:59:32.402298 version@stat F·[4 1] S·398KiB[7KiB 390KiB] Sc·[1.00 0.00] +12:59:32.404889 db@janitor F·7 G·0 +12:59:32.404903 db@open done T·5.965162ms +12:59:32.404938 table@compaction L0·4 -> L1·1 S·398KiB Q·4837 +12:59:32.410153 table@build created L1@88 N·2892 S·392KiB "abc..y:1,v6":"val..:99,v488" +12:59:32.410177 version@stat F·[0 1] S·392KiB[0B 392KiB] Sc·[0.00 0.00] +12:59:32.411742 table@compaction committed F-4 S-5KiB Ke·0 D·12 T·6.775389ms +12:59:32.411819 table@remove removed @80 +12:59:32.411922 table@remove removed @77 +12:59:32.411960 table@remove removed @72 +12:59:32.412071 table@remove removed @65 +=============== Jul 3, 2024 (UTC) =============== +17:52:58.319428 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.319516 version@stat F·[0 1] S·392KiB[0B 392KiB] Sc·[0.00 0.00] +17:52:58.319528 db@open opening +17:52:58.319556 journal@recovery F·1 +17:52:58.319644 journal@recovery recovering @86 +17:52:58.319923 version@stat F·[0 1] S·392KiB[0B 392KiB] Sc·[0.00 0.00] +17:52:58.323031 db@janitor F·4 G·1 +17:52:58.323039 db@janitor removing table-85 +17:52:58.323074 db@open done T·3.54148ms +17:58:44.101865 db@close closing +17:58:44.101899 db@close done T·33.25µs +=============== Jul 3, 2024 (UTC) =============== +17:59:01.699027 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.699336 version@stat F·[0 1] S·392KiB[0B 392KiB] Sc·[0.00 0.00] +17:59:01.699350 db@open opening +17:59:01.699408 journal@recovery F·1 +17:59:01.700331 journal@recovery recovering @89 +17:59:01.701961 memdb@flush created L0@91 N·345 S·87KiB "abc..000,v5023":"val..999,v5010" +17:59:01.702080 version@stat F·[1 1] S·480KiB[87KiB 392KiB] Sc·[0.25 0.00] +17:59:01.705270 db@janitor F·4 G·0 +17:59:01.705289 db@open done T·5.934209ms +17:59:27.006738 db@close closing +17:59:27.006773 db@close done T·35.621µs diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/MANIFEST-000093 b/.docker/container-state/nucleus-testnet-data/data/state.db/MANIFEST-000093 new file mode 100644 index 0000000000..f044b3f1c5 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/state.db/MANIFEST-000093 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000078.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000078.ldb new file mode 100644 index 0000000000..3c915bd371 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000078.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000079.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000079.ldb new file mode 100644 index 0000000000..d228e9d6d5 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000079.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000084.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000084.ldb new file mode 100644 index 0000000000..dbe9644aa0 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000084.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000089.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000089.ldb new file mode 100644 index 0000000000..c1388b6fb7 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000089.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000090.log b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000090.log new file mode 100644 index 0000000000..0897ff3654 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000090.log differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT new file mode 100644 index 0000000000..00f4669871 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT @@ -0,0 +1 @@ +MANIFEST-000091 diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT.bak new file mode 100644 index 0000000000..948a0b647f --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT.bak @@ -0,0 +1 @@ +MANIFEST-000088 diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/LOCK b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/LOCK new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/LOG b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/LOG new file mode 100644 index 0000000000..6f949c5d7c --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/LOG @@ -0,0 +1,352 @@ +=============== Jun 4, 2024 (UTC) =============== +11:02:17.174284 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:02:17.176196 db@open opening +11:02:17.178035 version@stat F·[] S·0B[] Sc·[] +11:02:17.178648 db@janitor F·2 G·0 +11:02:17.178656 db@open done T·2.45342ms +=============== Jun 6, 2024 (UTC) =============== +05:23:17.043151 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:23:17.043211 version@stat F·[] S·0B[] Sc·[] +05:23:17.043220 db@open opening +05:23:17.043243 journal@recovery F·1 +05:23:17.044979 journal@recovery recovering @1 +05:23:17.050073 memdb@flush created L0@2 N·7767 S·122KiB "\x17\xc7\v..\x0f\x89\xe4,v7242":"\xe2\x98\xcc..\xed\xa6\xd1,v3296" +05:23:17.050182 version@stat F·[1] S·122KiB[122KiB] Sc·[0.25] +05:23:17.052762 db@janitor F·3 G·0 +05:23:17.052770 db@open done T·9.548125ms +=============== Jun 10, 2024 (UTC) =============== +10:09:49.011112 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:09:49.011205 version@stat F·[1] S·122KiB[122KiB] Sc·[0.25] +10:09:49.011217 db@open opening +10:09:49.011244 journal@recovery F·1 +10:09:49.011426 journal@recovery recovering @3 +10:09:49.017663 memdb@flush created L0@5 N·8783 S·135KiB "blo..\x01\xc0\xda,v7769":"\xea\xa0\\..ʞ\xcc,v16411" +10:09:49.019152 version@stat F·[2] S·257KiB[257KiB] Sc·[0.50] +10:09:49.022013 db@janitor F·4 G·0 +10:09:49.022025 db@open done T·10.804059ms +=============== Jun 10, 2024 (UTC) =============== +10:19:47.851934 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:19:47.852029 version@stat F·[2] S·257KiB[257KiB] Sc·[0.50] +10:19:47.852041 db@open opening +10:19:47.852071 journal@recovery F·1 +10:19:47.854038 journal@recovery recovering @6 +10:19:47.857092 memdb@flush created L0@8 N·4095 S·59KiB "blo..\x01\xc1\xd1,v16553":"blo..k\x00\x01,v20623" +10:19:47.857269 version@stat F·[3] S·317KiB[317KiB] Sc·[0.75] +10:19:47.860203 db@janitor F·5 G·0 +10:19:47.860216 db@open done T·8.170569ms +=============== Jun 10, 2024 (UTC) =============== +10:20:11.700021 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:20:11.700094 version@stat F·[3] S·317KiB[317KiB] Sc·[0.75] +10:20:11.700104 db@open opening +10:20:11.700134 journal@recovery F·1 +10:20:11.700362 journal@recovery recovering @9 +10:20:11.700777 version@stat F·[3] S·317KiB[317KiB] Sc·[0.75] +10:20:11.703476 db@janitor F·5 G·0 +10:20:11.703490 db@open done T·3.381668ms +=============== Jun 10, 2024 (UTC) =============== +10:23:28.252008 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:23:28.252086 version@stat F·[3] S·317KiB[317KiB] Sc·[0.75] +10:23:28.252097 db@open opening +10:23:28.252131 journal@recovery F·1 +10:23:28.252233 journal@recovery recovering @11 +10:23:28.253604 memdb@flush created L0@13 N·1214 S·18KiB "blo..\x01\xc2F,v20649":"\xf2~\xe4..\x05\xa4\xc4,v21827" +10:23:28.253719 version@stat F·[4] S·335KiB[335KiB] Sc·[1.00] +10:23:28.256305 db@janitor F·6 G·0 +10:23:28.256317 db@open done T·4.216796ms +10:23:28.256346 table@compaction L0·4 -> L1·0 S·335KiB Q·21863 +10:23:28.262948 table@build created L1@16 N·18180 S·307KiB "\x17\xc7\v..\x0f\x89\xe4,v7242":"\xf2~\xe4..\x05\xa4\xc4,v21827" +10:23:28.263025 version@stat F·[0 1] S·307KiB[0B 307KiB] Sc·[0.00 0.00] +10:23:28.263636 table@compaction committed F-3 S-27KiB Ke·0 D·3679 T·7.276661ms +10:23:28.263708 table@remove removed @8 +10:23:28.263765 table@remove removed @5 +10:23:28.263814 table@remove removed @2 +=============== Jun 10, 2024 (UTC) =============== +10:27:08.425591 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:27:08.425679 version@stat F·[0 1] S·307KiB[0B 307KiB] Sc·[0.00 0.00] +10:27:08.425689 db@open opening +10:27:08.425716 journal@recovery F·1 +10:27:08.425794 journal@recovery recovering @14 +10:27:08.427349 memdb@flush created L0@17 N·1470 S·20KiB "blo..\x01\xc2h,v21864":"blo..k\x00\x01,v23309" +10:27:08.427458 version@stat F·[1 1] S·328KiB[20KiB 307KiB] Sc·[0.25 0.00] +10:27:08.430053 db@janitor F·5 G·1 +10:27:08.430059 db@janitor removing table-13 +10:27:08.430093 db@open done T·4.402037ms +=============== Jun 10, 2024 (UTC) =============== +10:30:28.040468 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +10:30:28.040547 version@stat F·[1 1] S·328KiB[20KiB 307KiB] Sc·[0.25 0.00] +10:30:28.040558 db@open opening +10:30:28.040587 journal@recovery F·1 +10:30:28.042484 journal@recovery recovering @18 +10:30:28.043684 memdb@flush created L0@20 N·684 S·11KiB "blo..\x01\u0092,v23335":"\x8aq\x93..4\x8a\x01,v23563" +10:30:28.043794 version@stat F·[2 1] S·339KiB[31KiB 307KiB] Sc·[0.50 0.00] +10:30:28.047375 db@janitor F·5 G·0 +10:30:28.047387 db@open done T·6.825167ms +=============== Jun 10, 2024 (UTC) =============== +11:11:44.618970 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:11:44.619059 version@stat F·[2 1] S·339KiB[31KiB 307KiB] Sc·[0.50 0.00] +11:11:44.619069 db@open opening +11:11:44.619102 journal@recovery F·1 +11:11:44.619201 journal@recovery recovering @21 +11:11:44.621160 memdb@flush created L0@23 N·1750 S·24KiB "blo..\x01¥,v24020":"blo..k\x00\x01,v25745" +11:11:44.621310 version@stat F·[3 1] S·364KiB[56KiB 307KiB] Sc·[0.75 0.00] +11:11:44.624054 db@janitor F·6 G·0 +11:11:44.624071 db@open done T·4.997421ms +=============== Jun 10, 2024 (UTC) =============== +11:15:00.406082 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:15:00.406168 version@stat F·[3 1] S·364KiB[56KiB 307KiB] Sc·[0.75 0.00] +11:15:00.406179 db@open opening +11:15:00.406218 journal@recovery F·1 +11:15:00.406430 journal@recovery recovering @24 +11:15:00.407797 memdb@flush created L0@26 N·947 S·33KiB "\tr\xf8..pSV,v26111":"\xec.\xac..\x95\xd6=,v26445" +11:15:00.409639 version@stat F·[4 1] S·397KiB[89KiB 307KiB] Sc·[1.00 0.00] +11:15:00.412219 db@janitor F·7 G·0 +11:15:00.412231 db@open done T·6.04823ms +11:15:00.412270 table@compaction L0·4 -> L1·1 S·397KiB Q·26718 +11:15:00.421972 table@build created L1@29 N·22236 S·395KiB "\tr\xf8..pSV,v26111":"\xf2~\xe4..\x05\xa4\xc4,v21827" +11:15:00.421998 version@stat F·[0 1] S·395KiB[0B 395KiB] Sc·[0.00 0.00] +11:15:00.422579 table@compaction committed F-4 S-2KiB Ke·0 D·795 T·10.284235ms +11:15:00.422646 table@remove removed @23 +11:15:00.422683 table@remove removed @20 +11:15:00.422717 table@remove removed @17 +11:15:00.422799 table@remove removed @16 +=============== Jun 10, 2024 (UTC) =============== +11:21:08.228635 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:21:08.228712 version@stat F·[0 1] S·395KiB[0B 395KiB] Sc·[0.00 0.00] +11:21:08.228722 db@open opening +11:21:08.228751 journal@recovery F·1 +11:21:08.229030 journal@recovery recovering @27 +11:21:08.231032 version@stat F·[0 1] S·395KiB[0B 395KiB] Sc·[0.00 0.00] +11:21:08.233701 db@janitor F·4 G·1 +11:21:08.233712 db@janitor removing table-26 +11:21:08.233762 db@open done T·5.036372ms +=============== Jun 10, 2024 (UTC) =============== +11:22:28.294744 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:22:28.294812 version@stat F·[0 1] S·395KiB[0B 395KiB] Sc·[0.00 0.00] +11:22:28.294823 db@open opening +11:22:28.294852 journal@recovery F·1 +11:22:28.295021 journal@recovery recovering @30 +11:22:28.296000 memdb@flush created L0@32 N·35 S·992B "blo..\x01\xc2\xef,v26719":"blo..k\x00\x01,v26729" +11:22:28.296389 version@stat F·[1 1] S·396KiB[992B 395KiB] Sc·[0.25 0.00] +11:22:28.298957 db@janitor F·4 G·0 +11:22:28.298970 db@open done T·4.143054ms +=============== Jun 10, 2024 (UTC) =============== +11:23:15.259916 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:23:15.260005 version@stat F·[1 1] S·396KiB[992B 395KiB] Sc·[0.25 0.00] +11:23:15.260017 db@open opening +11:23:15.260048 journal@recovery F·1 +11:23:15.261938 journal@recovery recovering @33 +11:23:15.262754 memdb@flush created L0@35 N·175 S·2KiB "blo..\x01\xc2\xf0,v26755":"blo..k\x00\x01,v26905" +11:23:15.262866 version@stat F·[2 1] S·399KiB[3KiB 395KiB] Sc·[0.50 0.00] +11:23:15.265329 db@janitor F·5 G·0 +11:23:15.265341 db@open done T·5.319964ms +=============== Jun 10, 2024 (UTC) =============== +11:29:08.634321 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:29:08.634397 version@stat F·[2 1] S·399KiB[3KiB 395KiB] Sc·[0.50 0.00] +11:29:08.634410 db@open opening +11:29:08.634447 journal@recovery F·1 +11:29:08.634524 journal@recovery recovering @36 +11:29:08.636530 memdb@flush created L0@38 N·2310 S·32KiB "blo..\x01\xc2\xf5,v26931":"blo..k\x00\x01,v29216" +11:29:08.636644 version@stat F·[3 1] S·432KiB[36KiB 395KiB] Sc·[0.75 0.00] +11:29:08.639364 db@janitor F·6 G·0 +11:29:08.639377 db@open done T·4.962372ms +=============== Jun 10, 2024 (UTC) =============== +11:30:42.317380 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:42.317458 version@stat F·[3 1] S·432KiB[36KiB 395KiB] Sc·[0.75 0.00] +11:30:42.317471 db@open opening +11:30:42.317506 journal@recovery F·1 +11:30:42.317764 journal@recovery recovering @39 +11:30:42.318864 memdb@flush created L0@41 N·595 S·8KiB "blo..\x01\xc37,v29242":"blo..k\x00\x01,v29812" +11:30:42.320783 version@stat F·[4 1] S·440KiB[45KiB 395KiB] Sc·[1.00 0.00] +11:30:42.323381 db@janitor F·7 G·0 +11:30:42.323395 db@open done T·5.920359ms +11:30:42.323415 table@compaction L0·4 -> L1·1 S·440KiB Q·29837 +11:30:42.332306 table@build created L1@44 N·24817 S·436KiB "\tr\xf8..pSV,v26111":"\xf2~\xe4..\x05\xa4\xc4,v21827" +11:30:42.332329 version@stat F·[0 1] S·436KiB[0B 436KiB] Sc·[0.00 0.00] +11:30:42.338634 table@compaction committed F-4 S-4KiB Ke·0 D·534 T·15.198827ms +11:30:42.338710 table@remove removed @38 +11:30:42.338745 table@remove removed @35 +11:30:42.338775 table@remove removed @32 +11:30:42.338885 table@remove removed @29 +=============== Jun 10, 2024 (UTC) =============== +11:30:55.701766 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:30:55.701846 version@stat F·[0 1] S·436KiB[0B 436KiB] Sc·[0.00 0.00] +11:30:55.701860 db@open opening +11:30:55.701891 journal@recovery F·1 +11:30:55.702099 journal@recovery recovering @42 +11:30:55.703313 memdb@flush created L0@45 N·35 S·989B "blo..\x01\xc3H,v29838":"blo..k\x00\x01,v29848" +11:30:55.703661 version@stat F·[1 1] S·437KiB[989B 436KiB] Sc·[0.25 0.00] +11:30:55.706204 db@janitor F·5 G·1 +11:30:55.706214 db@janitor removing table-41 +11:30:55.706249 db@open done T·4.385017ms +=============== Jun 10, 2024 (UTC) =============== +11:36:36.089192 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +11:36:36.089306 version@stat F·[1 1] S·437KiB[989B 436KiB] Sc·[0.25 0.00] +11:36:36.089320 db@open opening +11:36:36.089352 journal@recovery F·1 +11:36:36.089445 journal@recovery recovering @46 +11:36:36.091405 memdb@flush created L0@48 N·2100 S·29KiB "blo..\x01\xc3I,v29874":"blo..k\x00\x01,v31949" +11:36:36.091532 version@stat F·[2 1] S·467KiB[30KiB 436KiB] Sc·[0.50 0.00] +11:36:36.095700 db@janitor F·5 G·0 +11:36:36.095715 db@open done T·6.390813ms +=============== Jun 10, 2024 (UTC) =============== +12:01:04.975418 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:01:04.975503 version@stat F·[2 1] S·467KiB[30KiB 436KiB] Sc·[0.50 0.00] +12:01:04.975517 db@open opening +12:01:04.975549 journal@recovery F·1 +12:01:04.975645 journal@recovery recovering @49 +12:01:04.977423 memdb@flush created L0@51 N·1750 S·24KiB "blo..\x01Å,v31975":"blo..k\x00\x01,v33700" +12:01:04.977537 version@stat F·[3 1] S·491KiB[55KiB 436KiB] Sc·[0.75 0.00] +12:01:04.980160 db@janitor F·6 G·0 +12:01:04.980173 db@open done T·4.651841ms +=============== Jun 10, 2024 (UTC) =============== +12:02:59.963267 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:02:59.963359 version@stat F·[3 1] S·491KiB[55KiB 436KiB] Sc·[0.75 0.00] +12:02:59.963372 db@open opening +12:02:59.963406 journal@recovery F·1 +12:02:59.963680 journal@recovery recovering @52 +12:02:59.964573 memdb@flush created L0@54 N·105 S·2KiB "blo..\x01÷,v33726":"blo..k\x00\x01,v33806" +12:02:59.966426 version@stat F·[4 1] S·493KiB[57KiB 436KiB] Sc·[1.00 0.00] +12:02:59.969351 db@janitor F·7 G·0 +12:02:59.969369 db@open done T·5.991293ms +12:02:59.969398 table@compaction L0·4 -> L1·1 S·493KiB Q·33831 +12:02:59.980814 table@build created L1@57 N·28123 S·487KiB "\tr\xf8..pSV,v26111":"\xf2~\xe4..\x05\xa4\xc4,v21827" +12:02:59.980843 version@stat F·[0 1] S·487KiB[0B 487KiB] Sc·[0.00 0.00] +12:02:59.981432 table@compaction committed F-4 S-6KiB Ke·0 D·684 T·12.014736ms +12:02:59.981524 table@remove removed @51 +12:02:59.981577 table@remove removed @48 +12:02:59.981619 table@remove removed @45 +12:02:59.981766 table@remove removed @44 +=============== Jun 10, 2024 (UTC) =============== +12:03:19.623990 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:03:19.624078 version@stat F·[0 1] S·487KiB[0B 487KiB] Sc·[0.00 0.00] +12:03:19.624091 db@open opening +12:03:19.624122 journal@recovery F·1 +12:03:19.624431 journal@recovery recovering @55 +12:03:19.625258 memdb@flush created L0@58 N·70 S·1KiB "blo..\x01ú,v33832":"blo..k\x00\x01,v33877" +12:03:19.627206 version@stat F·[1 1] S·488KiB[1KiB 487KiB] Sc·[0.25 0.00] +12:03:19.629930 db@janitor F·5 G·1 +12:03:19.629939 db@janitor removing table-54 +12:03:19.629974 db@open done T·5.877682ms +=============== Jun 10, 2024 (UTC) =============== +12:04:23.554964 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:04:23.555042 version@stat F·[1 1] S·488KiB[1KiB 487KiB] Sc·[0.25 0.00] +12:04:23.555054 db@open opening +12:04:23.555092 journal@recovery F·1 +12:04:23.555194 journal@recovery recovering @59 +12:04:23.555943 memdb@flush created L0@61 N·35 S·986B "blo..\x01ü,v33903":"blo..k\x00\x01,v33913" +12:04:23.556098 version@stat F·[2 1] S·489KiB[2KiB 487KiB] Sc·[0.50 0.00] +12:04:23.558875 db@janitor F·5 G·0 +12:04:23.558888 db@open done T·3.829644ms +=============== Jun 10, 2024 (UTC) =============== +12:07:46.674194 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:07:46.674325 version@stat F·[2 1] S·489KiB[2KiB 487KiB] Sc·[0.50 0.00] +12:07:46.674343 db@open opening +12:07:46.674391 journal@recovery F·1 +12:07:46.676322 journal@recovery recovering @62 +12:07:46.676495 version@stat F·[2 1] S·489KiB[2KiB 487KiB] Sc·[0.50 0.00] +12:07:46.679126 db@janitor F·5 G·0 +12:07:46.679139 db@open done T·4.790762ms +=============== Jun 10, 2024 (UTC) =============== +12:08:02.070916 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:08:02.071015 version@stat F·[2 1] S·489KiB[2KiB 487KiB] Sc·[0.50 0.00] +12:08:02.071028 db@open opening +12:08:02.071062 journal@recovery F·1 +12:08:02.071327 journal@recovery recovering @64 +12:08:02.071493 version@stat F·[2 1] S·489KiB[2KiB 487KiB] Sc·[0.50 0.00] +12:08:02.074133 db@janitor F·5 G·0 +12:08:02.074159 db@open done T·3.125978ms +=============== Jun 10, 2024 (UTC) =============== +12:27:21.428181 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:27:21.428282 version@stat F·[2 1] S·489KiB[2KiB 487KiB] Sc·[0.50 0.00] +12:27:21.428295 db@open opening +12:27:21.428325 journal@recovery F·1 +12:27:21.428582 journal@recovery recovering @66 +12:27:21.428741 version@stat F·[2 1] S·489KiB[2KiB 487KiB] Sc·[0.50 0.00] +12:27:21.431302 db@janitor F·5 G·0 +12:27:21.431312 db@open done T·3.012646ms +=============== Jun 10, 2024 (UTC) =============== +12:56:43.788508 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:56:43.788576 version@stat F·[2 1] S·489KiB[2KiB 487KiB] Sc·[0.50 0.00] +12:56:43.788588 db@open opening +12:56:43.788625 journal@recovery F·1 +12:56:43.788708 journal@recovery recovering @68 +12:56:43.789495 memdb@flush created L0@70 N·70 S·1KiB "blo..\x01ý,v33939":"blo..k\x00\x01,v33984" +12:56:43.789609 version@stat F·[3 1] S·491KiB[3KiB 487KiB] Sc·[0.75 0.00] +12:56:43.793896 db@janitor F·6 G·0 +12:56:43.793908 db@open done T·5.315456ms +=============== Jun 10, 2024 (UTC) =============== +12:57:38.356927 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:57:38.357011 version@stat F·[3 1] S·491KiB[3KiB 487KiB] Sc·[0.75 0.00] +12:57:38.357027 db@open opening +12:57:38.357075 journal@recovery F·1 +12:57:38.357287 journal@recovery recovering @71 +12:57:38.357967 version@stat F·[3 1] S·491KiB[3KiB 487KiB] Sc·[0.75 0.00] +12:57:38.361003 db@janitor F·6 G·0 +12:57:38.361017 db@open done T·3.984844ms +=============== Jun 10, 2024 (UTC) =============== +12:58:01.454825 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:01.454905 version@stat F·[3 1] S·491KiB[3KiB 487KiB] Sc·[0.75 0.00] +12:58:01.454919 db@open opening +12:58:01.454958 journal@recovery F·1 +12:58:01.455047 journal@recovery recovering @73 +12:58:01.455784 memdb@flush created L0@75 N·35 S·985B "blo..\x01ÿ,v34010":"blo..k\x00\x01,v34020" +12:58:01.455897 version@stat F·[4 1] S·492KiB[4KiB 487KiB] Sc·[1.00 0.00] +12:58:01.458473 db@janitor F·7 G·0 +12:58:01.458486 db@open done T·3.560491ms +12:58:01.458514 table@compaction L0·4 -> L1·1 S·492KiB Q·34045 +12:58:01.469129 table@build created L1@78 N·28297 S·490KiB "\tr\xf8..pSV,v26111":"\xf2~\xe4..\x05\xa4\xc4,v21827" +12:58:01.469153 version@stat F·[0 1] S·490KiB[0B 490KiB] Sc·[0.00 0.00] +12:58:01.469731 table@compaction committed F-4 S-2KiB Ke·0 D·36 T·11.200588ms +12:58:01.469799 table@remove removed @70 +12:58:01.469837 table@remove removed @61 +12:58:01.469869 table@remove removed @58 +12:58:01.469990 table@remove removed @57 +=============== Jun 10, 2024 (UTC) =============== +12:58:48.978728 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:58:48.978800 version@stat F·[0 1] S·490KiB[0B 490KiB] Sc·[0.00 0.00] +12:58:48.978816 db@open opening +12:58:48.978849 journal@recovery F·1 +12:58:48.980846 journal@recovery recovering @76 +12:58:48.981597 memdb@flush created L0@79 N·70 S·1KiB "blo..\x01\xc3\xc0,v34046":"blo..k\x00\x01,v34091" +12:58:48.981707 version@stat F·[1 1] S·491KiB[1KiB 490KiB] Sc·[0.25 0.00] +12:58:48.984274 db@janitor F·5 G·1 +12:58:48.984283 db@janitor removing table-75 +12:58:48.984317 db@open done T·5.495648ms +=============== Jun 10, 2024 (UTC) =============== +12:59:05.219764 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:05.219845 version@stat F·[1 1] S·491KiB[1KiB 490KiB] Sc·[0.25 0.00] +12:59:05.219860 db@open opening +12:59:05.219891 journal@recovery F·1 +12:59:05.221853 journal@recovery recovering @80 +12:59:05.223847 version@stat F·[1 1] S·491KiB[1KiB 490KiB] Sc·[0.25 0.00] +12:59:05.226424 db@janitor F·4 G·0 +12:59:05.226442 db@open done T·6.577477ms +=============== Jun 10, 2024 (UTC) =============== +12:59:32.405516 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +12:59:32.405608 version@stat F·[1 1] S·491KiB[1KiB 490KiB] Sc·[0.25 0.00] +12:59:32.405621 db@open opening +12:59:32.405666 journal@recovery F·1 +12:59:32.405757 journal@recovery recovering @82 +12:59:32.406528 memdb@flush created L0@84 N·35 S·989B "blo..\x01\xc3\xc2,v34117":"blo..k\x00\x01,v34127" +12:59:32.406659 version@stat F·[2 1] S·492KiB[2KiB 490KiB] Sc·[0.50 0.00] +12:59:32.411831 db@janitor F·5 G·0 +12:59:32.411845 db@open done T·6.219235ms +=============== Jul 3, 2024 (UTC) =============== +17:52:58.323595 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:52:58.323693 version@stat F·[2 1] S·492KiB[2KiB 490KiB] Sc·[0.50 0.00] +17:52:58.323706 db@open opening +17:52:58.323743 journal@recovery F·1 +17:52:58.323963 journal@recovery recovering @85 +17:52:58.325969 version@stat F·[2 1] S·492KiB[2KiB 490KiB] Sc·[0.50 0.00] +17:52:58.328526 db@janitor F·5 G·0 +17:52:58.328541 db@open done T·4.8287ms +=============== Jul 3, 2024 (UTC) =============== +17:59:01.705778 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +17:59:01.705947 version@stat F·[2 1] S·492KiB[2KiB 490KiB] Sc·[0.50 0.00] +17:59:01.705959 db@open opening +17:59:01.705991 journal@recovery F·1 +17:59:01.706077 journal@recovery recovering @87 +17:59:01.708294 memdb@flush created L0@89 N·2541 S·58KiB "\x1c\xa7\x1b..\x9a\xa4\xe0,v36448":"\x85\x00\xda..\x95K\xe3,v36113" +17:59:01.708419 version@stat F·[3 1] S·550KiB[60KiB 490KiB] Sc·[0.75 0.00] +17:59:01.712571 db@janitor F·6 G·0 +17:59:01.712584 db@open done T·6.619955ms diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/MANIFEST-000091 b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/MANIFEST-000091 new file mode 100644 index 0000000000..d75cffd4ec Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/MANIFEST-000091 differ diff --git a/.docker/container-state/nucleus-testnet-data/keyring-test/224884bc43a4345c66051befe55c0c54676008b3.address b/.docker/container-state/nucleus-testnet-data/keyring-test/224884bc43a4345c66051befe55c0c54676008b3.address new file mode 100644 index 0000000000..94bb305001 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/keyring-test/224884bc43a4345c66051befe55c0c54676008b3.address @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0wNCAxMzowOToyMS42Mzk3ODAzNzUgKzAzMDAgKzAzIG09KzAuMDM4NzA5ODE1IiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiS2ZkaHFCNGJUUjVWT2ZZZyJ9.mUCckjmMF6wtTNjM5kNeXGYNCBVmZCT0HmHLloJNhIXAn5rws_mi0A.aj5fZAGdGl57m-7v.zsLX47A302loDj0lK94kh7xTVlC2sq_Ry4juzGZf3rE8KyDBx-SI_kNbkMfm6aVKrxZpnxdTmdhPhj8M3ayX7BqW7EjuKjSlLFwkkjJyT4hS4B2_uy5AP-ljUp5x_0SWf96pFPSHZjTT5ilN84jmRJdoiYHvcJtW1HwjzjHfGXUj33T1UqOFU0IoELChE-FKb11TfsIf-d52J8XLcVpw6Rxv__RWB-vxm7dyvQLDZSFrOA.lM0bzqqyCCkurzSN8RR_Vw \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/keyring-test/a36b04d45a23d962b67cd9c82b3065c45c0a80e5.address b/.docker/container-state/nucleus-testnet-data/keyring-test/a36b04d45a23d962b67cd9c82b3065c45c0a80e5.address new file mode 100644 index 0000000000..978c76fe1b --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/keyring-test/a36b04d45a23d962b67cd9c82b3065c45c0a80e5.address @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0wNCAxMzowOToyMS41OTA4MTk0MjggKzAzMDAgKzAzIG09KzAuMDM4MDM1Mzg5IiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoiX3BkeDU2N0hPNVExZU9oZSJ9.hjs12iz7OwHZNtX4IRoX2J7bxJKZ4ZdUwNYU3HMiQsQianl-H8lJZA.bwpDKReJ9-id82O8.fE3UOdwIdY9v6RLeBuDdOVIrDuYR2nA_5opEZ9WQNiuUfgHiOnP4l0u3U9t_PUYujKd6keq9_FgTa1XVdau1odWhfwpXrYDZgMw63PaSf8OSxezmBgjYklSeVbJEnThy1A_u2Ie72SK632dWzmfdBRxHRW8tTv4HROrCOEFn3iNQdhZS82xU2b6uq2Nl9h_euzt1ddGLWy5OhtEwivI7jalpKy43_G8pHwrifartXR3PKXEBYEI.TtEeC1L4Nio85Emo_9UrHQ \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/keyring-test/alice.info b/.docker/container-state/nucleus-testnet-data/keyring-test/alice.info new file mode 100644 index 0000000000..f8c106187b --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/keyring-test/alice.info @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0wNCAxMzowOToyMS41ODczMjk0OSArMDMwMCArMDMgbT0rMC4wMzQ1NDU0NTEiLCJlbmMiOiJBMjU2R0NNIiwicDJjIjo4MTkyLCJwMnMiOiJTYkpvLUNBV0F5YXl6VUppIn0.A4qJ3OdE7CcD4ovRhGXw0jELmfD41e6CEtsTSMoLaL5jBWpENwGXEg.OO53V2D2KZgvhxDG.MPmzfeTKAALGeDUryBGi-mBnj_pWWD-HOK6mx57Ue4uhLmTXaJhmj5ol13o8Xa_nBEs7DY6L8HGo28wPkLIPNLEhhXQ5Qaes9S-TVrcphTJ1K2sf07CrNVXso3R9uU0V_lKkz765Ar4Iuf5vxOft84aou_6qSD5BDtnDSV0RdbnEXDVXuL5acadUMBPBwuddV9NNbHH_uqFVx67CDUJXqDs-XFPVqElGcfuzwtFEQHw2Od6u5TyQE6_xphiidoQoyXDOBbQfQOBtuXlCUvLtClGDXotoyx8FJM5Ns5gpgCKq8Rp8oMpiNOnrt74ag0xGwXFhy6hD8WIlJCNcJZ0UV0KcuL7JO6WSvmHtI1XWoA82RFP6I2P2UleJJsXEVtb44UC2pz0MuhuCA--79yTcdYkkIA5z5fKm4AVwLFAZySbnilIRWABawkI8jSE.QDha2U3XXGLpYDpBOvY2DQ \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/keyring-test/bob.info b/.docker/container-state/nucleus-testnet-data/keyring-test/bob.info new file mode 100644 index 0000000000..04ca589cb8 --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/keyring-test/bob.info @@ -0,0 +1 @@ +eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjcmVhdGVkIjoiMjAyNC0wNi0wNCAxMzowOToyMS42MzYwOTQyNjUgKzAzMDAgKzAzIG09KzAuMDM1MDIzNjk1IiwiZW5jIjoiQTI1NkdDTSIsInAyYyI6ODE5MiwicDJzIjoickljRWhKdXhUdmlQQklYaiJ9.f2yI5y6rWVCBiXAJ2pe7gNAKApYpF47FIU0J7GDzxHM-RJ1uZB_GhQ.TzUGK_soy1SV8Z4a.ROxRJJdi8_2CrMhR7LCG9OaJJvyAF5fu_m_sF-aUAwvxC47IgAGodzKPuw_JCbg6xMmRF0t36vUiuyC5690DOOc_YjVS0t7ZzLt0_7dC-FvLF3Wz8PrJH1h1LZ4PWTAwLymNqBDGoLaM72K9xWNV3iUbxdBYlYfCLGH5bNg3TsIkm4sOcsm0gPZptaNdxYMfkX8iXiyOruaELUq3hJoqN3Qbz6UnEpZ1vZzjOKTm5XK4hxOEuoAq75D-GmjmA80vqiHHiTrARhQQTeMw2IpuUpkd4MIKecyf0JlJ_pGKOjZ5pIw0AGns7NEOqLJlYUE_EldWSUSSq4-FPu2uGMsKh2g4cSoXa4bqW0scYm28h_IW9rFzfPIU-L_pGTrziAmWiDJjcbS2ml-xl3uUid6nM34CeN1rqbn18zc54BBADGmM4U8Oty9ucNK1.NE7JvnAisuaiVQqJZbvUow \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 5e4e3785e1..778ad13875 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,8 @@ cmake-build-debug !/target/release/mm2 !/target/debug/mm2 +!/target/release/kdf +!/target/debug/kdf /mm2src/*/target /build @@ -17,6 +19,8 @@ cmake-build-debug /js/*.wasm /js/mm2 /js/mm2.exe +/js/kdf +/js/kdf.exe /wasm-build.log # Opt out from history in order to speed the `COPY .` up. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 440838e30b..9d788ecc55 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Bug report -about: Marketmaker 1.0 bug report +about: Komodo DeFi Framework bug report --- @@ -9,10 +9,10 @@ A clear and concise description of what the bug is. **Please answer following questions and attach requested info - it'll help to solve issue faster** - What OS do you use? -- What marketmaker version do you run? +- What KDF version do you run? - Attach your coins.json config. - Provide your enable script with response. - Provide other curl scripts (with responses) which were executed prior to error. -- Attach full marketmaker console logs (start collecting right after marketmaker execution). +- Attach full KDF console logs (start collecting right after KDF execution). - ***Make sure that you don't send your passphrase, userpass and privkeys. Your funds might be stolen if you reveal this info publicly!*** - Provide info for all nodes involved (e.g. if error occurs during atomic swap you should provide info for both Bob and Alice). diff --git a/.github/actions/deps-install/action.yml b/.github/actions/deps-install/action.yml new file mode 100644 index 0000000000..25ed15bf50 --- /dev/null +++ b/.github/actions/deps-install/action.yml @@ -0,0 +1,61 @@ +name: Install Dependencies +description: Install non-cargo dependencies in an OS-agnostic way + +inputs: + deps: + description: "Dependencies to install (format: ('list' 'of' 'dependencies'))." + required: true + temp: + description: "A temporary directory path that can be used to store the installed binaries if needed." + +# NOTE: Don't install binaries in the project directory because the directory might be checked out later. +runs: + using: 'composite' + steps: + - name: Install protoc (Linux) + env: + TMP: ${{ inputs.temp || runner.temp }} + if: runner.os == 'Linux' && contains(inputs.deps, 'protoc') + shell: bash + run: | + wget https://github.com/protocolbuffers/protobuf/releases/download/v25.3/protoc-25.3-linux-x86_64.zip + unzip protoc-25.3-linux-x86_64 -d "$TMP/protobuf" + echo "$TMP/protobuf/bin" >> $GITHUB_PATH + + - name: Install protoc (MacOS) + env: + TMP: ${{ inputs.temp || runner.temp }} + if: runner.os == 'macOS' && contains(inputs.deps, 'protoc') + shell: bash + run: | + wget https://github.com/protocolbuffers/protobuf/releases/download/v25.3/protoc-25.3-osx-x86_64.zip + unzip protoc-25.3-osx-x86_64.zip -d "$TMP/protobuf" + echo "$TMP/protobuf/bin" >> $GITHUB_PATH + + - name: Install protoc (Windows) + env: + TMP: ${{ inputs.temp || runner.temp }} + if: runner.os == 'Windows' && contains(inputs.deps, 'protoc') + shell: powershell + run: | + Invoke-WebRequest -Uri https://github.com/protocolbuffers/protobuf/releases/download/v25.3/protoc-25.3-win64.zip -OutFile protoc-25.3-win64.zip + 7z x protoc-25.3-win64.zip -o"$TMP\protobuf" + echo "$TMP\protobuf\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Install libudev (Linux) + if: runner.os == 'Linux' && contains(inputs.deps, 'libudev') + shell: bash + run: | + sudo apt-get update -y + sudo apt-get install -y libudev-dev + + - name: Install python3 + uses: actions/setup-python@v5 + if: contains(inputs.deps, 'python3') + with: + python-version: 3 + + - name: Install `paramiko` pip package + if: contains(inputs.deps, 'paramiko') + shell: bash + run: pip install paramiko diff --git a/.github/workflows/adex-cli.yml b/.github/workflows/adex-cli.yml index 1265d28d46..32b62f04ed 100644 --- a/.github/workflows/adex-cli.yml +++ b/.github/workflows/adex-cli.yml @@ -1,4 +1,4 @@ -name: adex-cli +name: Adex CLI on: [push] concurrency: @@ -10,6 +10,7 @@ env: jobs: code-check: + name: Code Checks timeout-minutes: 60 runs-on: ${{ matrix.os }} strategy: @@ -18,87 +19,19 @@ jobs: steps: - uses: actions/checkout@v3 - - name: pre scripts for ci container - run: | - git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework - echo "/bin" >> $GITHUB_PATH - echo "/usr/bin" >> $GITHUB_PATH - echo "/root/.cargo/bin" >> $GITHUB_PATH - - - name: Calculate commit hash for PR commit - if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - - name: Calculate commit hash for merge commit - if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache uses: ./.github/actions/cargo-cache - name: Start checking code format and lint + continue-on-error: true run: | cargo fmt --manifest-path ./mm2src/adex_cli/Cargo.toml --all -- --check cargo clippy --manifest-path ./mm2src/adex_cli/Cargo.toml --all-targets --all-features -- --D warnings - test: - timeout-minutes: 60 - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - steps: - - uses: actions/checkout@v3 - - - name: pre scripts for ci container + - name: Start building run: | - git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework - echo "/bin" >> $GITHUB_PATH - echo "/usr/bin" >> $GITHUB_PATH - echo "/root/.cargo/bin" >> $GITHUB_PATH - - - name: Calculate commit hash for PR commit - if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - - name: Calculate commit hash for merge commit - if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - - name: Cargo cache - uses: ./.github/actions/cargo-cache + cargo build --manifest-path ./mm2src/adex_cli/Cargo.toml - name: Start testing run: | cargo test --manifest-path ./mm2src/adex_cli/Cargo.toml --no-fail-fast - - build: - timeout-minutes: 60 - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - steps: - - uses: actions/checkout@v3 - - - name: pre scripts for ci container - run: | - git config --global --add safe.directory /__w/komodo-defi-framework/komodo-defi-framework - echo "/bin" >> $GITHUB_PATH - echo "/usr/bin" >> $GITHUB_PATH - echo "/root/.cargo/bin" >> $GITHUB_PATH - - - name: Calculate commit hash for PR commit - if: github.event_name == 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV - - - name: Calculate commit hash for merge commit - if: github.event_name != 'pull_request' - run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - - name: Cargo cache - uses: ./.github/actions/cargo-cache - - - name: Start building - run: | - cargo build --manifest-path ./mm2src/adex_cli/Cargo.toml diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 4130b783ee..a4fce83cbe 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -30,8 +30,13 @@ jobs: - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -48,9 +53,9 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --bin mm2 --release + cargo build --release - - name: Compress build output + - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} @@ -60,18 +65,23 @@ jobs: mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output + - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + run: | + NAME="kdf_$COMMIT_HASH-linux-x86-64.zip" + zip $NAME target/release/kdf -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" - name: Login to dockerhub if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev' @@ -81,9 +91,9 @@ jobs: if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev' run: | CONTAINER_TAG="dev-$COMMIT_HASH" - docker build -t komodoofficial/atomicdexapi:"$CONTAINER_TAG" -t komodoofficial/atomicdexapi:dev-latest -f .docker/Dockerfile.dev-release . - docker push komodoofficial/atomicdexapi:"$CONTAINER_TAG" - docker push komodoofficial/atomicdexapi:dev-latest + docker build -t komodoofficial/komodo-defi-framework:"$CONTAINER_TAG" -t komodoofficial/komodo-defi-framework:dev-latest -f .docker/Dockerfile.dev-release . + docker push komodoofficial/komodo-defi-framework:"$CONTAINER_TAG" + docker push komodoofficial/komodo-defi-framework:dev-latest mac-x86-64: timeout-minutes: 60 @@ -92,8 +102,14 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + rustup target add x86_64-apple-darwin + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc', 'python3', 'paramiko') - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -110,9 +126,9 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --bin mm2 --release --target x86_64-apple-darwin + cargo build --release --target x86_64-apple-darwin - - name: Compress build output + - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} @@ -122,18 +138,84 @@ jobs: mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output + - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} - uses: garygrossgarten/github-action-scp@v0.8.0 + run: | + NAME="kdf_$COMMIT_HASH-mac-x86-64.zip" + zip $NAME target/x86_64-apple-darwin/release/kdf -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + + mac-arm64: + timeout-minutes: 60 + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + rustup target add aarch64-apple-darwin + + - name: Install build deps + uses: ./.github/actions/deps-install with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + deps: ('protoc', 'python3', 'paramiko') + + - name: Calculate commit hash for PR commit + if: github.event_name == 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Cargo cache + uses: ./.github/actions/cargo-cache + + - name: Build + run: | + rm -f ./MM_VERSION + echo $COMMIT_HASH > ./MM_VERSION + cargo build --release --target aarch64-apple-darwin + + - name: Compress mm2 build output + env: + AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.AVAILABLE != '' }} + run: | + NAME="mm2_$COMMIT_HASH-mac-arm64.zip" + zip $NAME target/aarch64-apple-darwin/release/mm2 -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Compress kdf build output + env: + AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.AVAILABLE != '' }} + run: | + NAME="kdf_$COMMIT_HASH-mac-arm64.zip" + zip $NAME target/aarch64-apple-darwin/release/kdf -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" win-x86-64: timeout-minutes: 60 @@ -142,8 +224,13 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc', 'python3', 'paramiko') - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -162,9 +249,9 @@ jobs: remove-item "./MM_VERSION" } echo $Env:COMMIT_HASH > ./MM_VERSION - cargo build --bin mm2 --release + cargo build --release - - name: Compress build output + - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} @@ -174,18 +261,23 @@ jobs: mkdir $Env:BRANCH_NAME mv $NAME ./$Env:BRANCH_NAME/ - - name: Upload output + - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + run: | + $NAME="kdf_$Env:COMMIT_HASH-win-x86-64.zip" + 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll + mv $NAME ./$Env:BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" mac-dylib-x86-64: timeout-minutes: 60 @@ -194,8 +286,14 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01-x86_64-apple-darwin --no-self-update --profile=minimal + rustup default nightly-2023-06-01-x86_64-apple-darwin + rustup target add x86_64-apple-darwin + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc', 'python3', 'paramiko') - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -214,29 +312,35 @@ jobs: echo $COMMIT_HASH > ./MM_VERSION cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress build output + - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | NAME="mm2_$COMMIT_HASH-mac-dylib-x86-64.zip" - mv target/x86_64-apple-darwin/release/libmm2lib.a target/x86_64-apple-darwin/release/libmm2.a + cp target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libmm2.a zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output + - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + run: | + NAME="kdf_$COMMIT_HASH-mac-dylib-x86-64.zip" + mv target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libkdf.a + zip $NAME target/x86_64-apple-darwin/release/libkdf.a -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" wasm: timeout-minutes: 60 @@ -252,10 +356,15 @@ jobs: echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add wasm32-unknown-unknown - name: Install wasm-pack @@ -283,23 +392,19 @@ jobs: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | - NAME="mm2_$COMMIT_HASH-wasm.zip" + NAME="kdf_$COMMIT_HASH-wasm.zip" (cd ./target/target-wasm-release && zip -r - .) > $NAME mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output + - name: Upload build artifact env: - AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} - if: ${{ env.AVAILABLE != '' }} - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" ios-aarch64: timeout-minutes: 60 @@ -308,10 +413,15 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add aarch64-apple-ios + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc', 'python3', 'paramiko') + - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV @@ -329,29 +439,35 @@ jobs: echo $COMMIT_HASH > ./MM_VERSION cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress build output + - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | NAME="mm2_$COMMIT_HASH-ios-aarch64.zip" - mv target/aarch64-apple-ios/release/libmm2lib.a target/aarch64-apple-ios/release/libmm2.a + cp target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libmm2.a zip $NAME target/aarch64-apple-ios/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output + - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + run: | + NAME="kdf_$COMMIT_HASH-ios-aarch64.zip" + mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libkdf.a + zip $NAME target/aarch64-apple-ios/release/libkdf.a -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" android-aarch64: timeout-minutes: 60 @@ -369,10 +485,15 @@ jobs: - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add aarch64-linux-android + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + - name: Setup NDK run: ./scripts/ci/android-ndk.sh x86 23 @@ -395,29 +516,35 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress build output + - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | NAME="mm2_$COMMIT_HASH-android-aarch64.zip" - mv target/aarch64-linux-android/release/libmm2lib.a target/aarch64-linux-android/release/libmm2.a - zip $NAME target/aarch64-linux-android/release/libmm2.a -j + cp target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libmm2.a + zip $NAME target/aarch64-linux-android/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output + - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + run: | + NAME="kdf_$COMMIT_HASH-android-aarch64.zip" + mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libkdf.a + zip $NAME target/aarch64-linux-android/release/libkdf.a -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" android-armv7: timeout-minutes: 60 @@ -435,10 +562,15 @@ jobs: - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add armv7-linux-androideabi + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + - name: Setup NDK run: ./scripts/ci/android-ndk.sh x86 23 @@ -461,29 +593,35 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress build output + - name: Compress mm2 build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} run: | NAME="mm2_$COMMIT_HASH-android-armv7.zip" - mv target/armv7-linux-androideabi/release/libmm2lib.a target/armv7-linux-androideabi/release/libmm2.a - zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j + cp target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libmm2.a + zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output + - name: Compress kdf build output env: AVAILABLE: ${{ secrets.FILE_SERVER_KEY }} if: ${{ env.AVAILABLE != '' }} - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + run: | + NAME="kdf_$COMMIT_HASH-android-armv7.zip" + mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libkdf.a + zip $NAME target/armv7-linux-androideabi/release/libkdf.a -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" deployment-commitment: if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev' diff --git a/.github/workflows/fmt-and-lint.yml b/.github/workflows/fmt-and-lint.yml index 093cdb823b..f5ea217eee 100644 --- a/.github/workflows/fmt-and-lint.yml +++ b/.github/workflows/fmt-and-lint.yml @@ -7,6 +7,7 @@ concurrency: jobs: fmt-and-lint: + name: x86 Format and Lint Checks timeout-minutes: 45 runs-on: ${{ matrix.os }} strategy: @@ -14,39 +15,47 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v3 + - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal --component rustfmt clippy - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal --component rustfmt clippy + rustup default nightly-2023-06-01 - - name: Install OS dependencies - run: | - sudo apt-get update - sudo apt-get -y install libudev-dev - if: matrix.os == 'ubuntu-latest' + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc' 'libudev-dev') - name: Cargo cache uses: ./.github/actions/cargo-cache - name: fmt check + # Format checks aren't OS dependant. + if: matrix.os == 'ubuntu-latest' run: cargo fmt -- --check - - name: x86-64 code lint + - name: clippy lint run: cargo clippy --all-targets --all-features -- --D warnings wasm-lint: + name: Wasm Lint Checks timeout-minutes: 45 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal --component clippy - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal --component clippy + rustup default nightly-2023-06-01 rustup target add wasm32-unknown-unknown + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + - name: Cargo cache uses: ./.github/actions/cargo-cache - - name: wasm code lint + - name: clippy lint run: cargo clippy --target wasm32-unknown-unknown -- --D warnings diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 0a7474dd08..59e176a5e0 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -1,4 +1,4 @@ -name: "PR Lint" +name: PR Lint on: pull_request: @@ -45,11 +45,9 @@ jobs: fi - name: Check PR labels - if: > - (contains(toJson(github.event.pull_request.labels.*.name), 'under review') == false && - contains(toJson(github.event.pull_request.labels.*.name), 'in progress') == false) || - (contains(toJson(github.event.pull_request.labels.*.name), 'under review') == true && - contains(toJson(github.event.pull_request.labels.*.name), 'in progress') == true) + env: + LABEL_NAMES: ${{ toJson(github.event.pull_request.labels.*.name) }} + if: contains(env.LABEL_NAMES, 'under review') == contains(env.LABEL_NAMES, 'in progress') run: | - echo "PR must have "exactly one" of these labels: [ 'under review', 'in progress' ]." + echo "PR must have "exactly one" of these labels: ['under review', 'in progress']." exit 1 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index d93777a1a8..a74a589d10 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -30,8 +30,13 @@ jobs: - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -48,34 +53,39 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --bin mm2 --release + cargo build --release - - name: Compress build output + - name: Compress mm2 build output run: | NAME="mm2_$COMMIT_HASH-linux-x86-64.zip" zip $NAME target/release/mm2 -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + - name: Compress kdf build output + run: | + NAME="kdf_$COMMIT_HASH-linux-x86-64.zip" + zip $NAME target/release/kdf -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" - name: Login to dockerhub run: docker login --username ${{ secrets.DOCKER_HUB_USERNAME }} --password ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} docker.io - name: Build and push container image run: | - export CONTAINER_TAG=$(./target/release/mm2 --version | awk '{print $3}') - docker build -t komodoofficial/atomicdexapi:"$CONTAINER_TAG" -t komodoofficial/atomicdexapi:main-latest -f .docker/Dockerfile.release . - docker push komodoofficial/atomicdexapi:"$CONTAINER_TAG" - docker push komodoofficial/atomicdexapi:main-latest + export CONTAINER_TAG=$(./target/release/kdf --version | awk '{print $3}') + docker build -t komodoofficial/komodo-defi-framework:"$CONTAINER_TAG" -t komodoofficial/komodo-defi-framework:main-latest -f .docker/Dockerfile.release . + docker push komodoofficial/komodo-defi-framework:"$CONTAINER_TAG" + docker push komodoofficial/komodo-defi-framework:main-latest mac-x86-64: timeout-minutes: 60 @@ -84,8 +94,13 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc', 'python3', 'paramiko') - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -102,24 +117,84 @@ jobs: run: | rm -f ./MM_VERSION echo $COMMIT_HASH > ./MM_VERSION - cargo build --bin mm2 --release --target x86_64-apple-darwin + cargo build --release --target x86_64-apple-darwin - - name: Compress build output + - name: Compress mm2 build output run: | NAME="mm2_$COMMIT_HASH-mac-x86-64.zip" zip $NAME target/x86_64-apple-darwin/release/mm2 -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output - uses: garygrossgarten/github-action-scp@v0.8.0 + - name: Compress kdf build output + run: | + NAME="kdf_$COMMIT_HASH-mac-x86-64.zip" + zip $NAME target/x86_64-apple-darwin/release/kdf -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" + + mac-arm64: + timeout-minutes: 60 + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + rustup target add aarch64-apple-darwin + + - name: Install build deps + uses: ./.github/actions/deps-install with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + deps: ('protoc', 'python3', 'paramiko') + + - name: Calculate commit hash for PR commit + if: github.event_name == 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV + + - name: Calculate commit hash for merge commit + if: github.event_name != 'pull_request' + run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV + + - name: Cargo cache + uses: ./.github/actions/cargo-cache + + - name: Build + run: | + rm -f ./MM_VERSION + echo $COMMIT_HASH > ./MM_VERSION + cargo build --release --target aarch64-apple-darwin + + - name: Compress mm2 build output + run: | + NAME="mm2_$COMMIT_HASH-mac-arm64.zip" + zip $NAME target/aarch64-apple-darwin/release/mm2 -j + mkdir $BRANCH_NAME + mv $NAME ./$BRANCH_NAME/ + + - name: Compress kdf build output + run: | + NAME="kdf_$COMMIT_HASH-mac-arm64.zip" + zip $NAME target/aarch64-apple-darwin/release/kdf -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" win-x86-64: timeout-minutes: 60 @@ -128,8 +203,13 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc', 'python3', 'paramiko') - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -148,24 +228,29 @@ jobs: remove-item "./MM_VERSION" } echo $Env:COMMIT_HASH > ./MM_VERSION - cargo build --bin mm2 --release + cargo build --release - - name: Compress build output + - name: Compress mm2 build output run: | $NAME="mm2_$Env:COMMIT_HASH-win-x86-64.zip" 7z a $NAME .\target\release\mm2.exe .\target\release\*.dll mkdir $Env:BRANCH_NAME mv $NAME ./$Env:BRANCH_NAME/ - - name: Upload output - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + - name: Compress kdf build output + run: | + $NAME="kdf_$Env:COMMIT_HASH-win-x86-64.zip" + 7z a $NAME .\target\release\kdf.exe .\target\release\*.dll + mv $NAME ./$Env:BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" mac-dylib-x86-64: timeout-minutes: 60 @@ -174,8 +259,13 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc', 'python3', 'paramiko') - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -194,23 +284,29 @@ jobs: echo $COMMIT_HASH > ./MM_VERSION cargo rustc --target x86_64-apple-darwin --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress build output + - name: Compress mm2 build output run: | NAME="mm2_$COMMIT_HASH-mac-dylib-x86-64.zip" - mv target/x86_64-apple-darwin/release/libmm2lib.a target/x86_64-apple-darwin/release/libmm2.a + cp target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libmm2.a zip $NAME target/x86_64-apple-darwin/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + - name: Compress kdf build output + run: | + NAME="kdf_$COMMIT_HASH-mac-dylib-x86-64.zip" + mv target/x86_64-apple-darwin/release/libkdflib.a target/x86_64-apple-darwin/release/libkdf.a + zip $NAME target/x86_64-apple-darwin/release/libkdf.a -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" wasm: timeout-minutes: 60 @@ -226,10 +322,15 @@ jobs: echo "/usr/bin" >> $GITHUB_PATH echo "/root/.cargo/bin" >> $GITHUB_PATH + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add wasm32-unknown-unknown - name: Install wasm-pack @@ -254,20 +355,19 @@ jobs: - name: Compress build output run: | - NAME="mm2_$COMMIT_HASH-wasm.zip" + NAME="kdf_$COMMIT_HASH-wasm.zip" (cd ./target/target-wasm-release && zip -r - .) > $NAME mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" ios-aarch64: timeout-minutes: 60 @@ -276,10 +376,15 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add aarch64-apple-ios + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc', 'python3', 'paramiko') + - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 ${{ github.event.pull_request.head.sha }})" >> $GITHUB_ENV @@ -297,23 +402,29 @@ jobs: echo $COMMIT_HASH > ./MM_VERSION cargo rustc --target aarch64-apple-ios --lib --release --package mm2_bin_lib --crate-type=staticlib - - name: Compress build output + - name: Compress mm2 build output run: | NAME="mm2_$COMMIT_HASH-ios-aarch64.zip" - mv target/aarch64-apple-ios/release/libmm2lib.a target/aarch64-apple-ios/release/libmm2.a + mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libmm2.a zip $NAME target/aarch64-apple-ios/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + - name: Compress kdf build output + run: | + NAME="kdf_$COMMIT_HASH-ios-aarch64.zip" + mv target/aarch64-apple-ios/release/libkdflib.a target/aarch64-apple-ios/release/libkdf.a + zip $NAME target/aarch64-apple-ios/release/libkdf.a -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" android-aarch64: timeout-minutes: 60 @@ -331,10 +442,15 @@ jobs: - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add aarch64-linux-android + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + - name: Setup NDK run: ./scripts/ci/android-ndk.sh x86 23 @@ -357,23 +473,29 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_aarch64_linux_android=aarch64-linux-android21-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang cargo rustc --target=aarch64-linux-android --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress build output + - name: Compress mm2 build output run: | NAME="mm2_$COMMIT_HASH-android-aarch64.zip" - mv target/aarch64-linux-android/release/libmm2lib.a target/aarch64-linux-android/release/libmm2.a - zip $NAME target/aarch64-linux-android/release/libmm2.a -j + mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libmm2.a + zip $NAME target/aarch64-linux-android/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + - name: Compress kdf build output + run: | + NAME="kdf_$COMMIT_HASH-android-aarch64.zip" + mv target/aarch64-linux-android/release/libkdflib.a target/aarch64-linux-android/release/libkdf.a + zip $NAME target/aarch64-linux-android/release/libkdf.a -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" android-armv7: timeout-minutes: 60 @@ -391,10 +513,15 @@ jobs: - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add armv7-linux-androideabi + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + - name: Setup NDK run: ./scripts/ci/android-ndk.sh x86 23 @@ -417,20 +544,26 @@ jobs: export PATH=$PATH:/android-ndk/bin CC_armv7_linux_androideabi=armv7a-linux-androideabi21-clang CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang cargo rustc --target=armv7-linux-androideabi --lib --release --crate-type=staticlib --package mm2_bin_lib - - name: Compress build output + - name: Compress mm2 build output run: | NAME="mm2_$COMMIT_HASH-android-armv7.zip" - mv target/armv7-linux-androideabi/release/libmm2lib.a target/armv7-linux-androideabi/release/libmm2.a - zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j + mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libmm2.a + zip $NAME target/armv7-linux-androideabi/release/libmm2.a -j mkdir $BRANCH_NAME mv $NAME ./$BRANCH_NAME/ - - name: Upload output - uses: garygrossgarten/github-action-scp@v0.8.0 - with: - host: ${{ secrets.FILE_SERVER_HOST }} - username: ${{ secrets.FILE_SERVER_USERNAME }} - port: ${{ secrets.FILE_SERVER_PORT }} - privateKey: ${{ secrets.FILE_SERVER_KEY }} - local: ${{ env.BRANCH_NAME }} - remote: "/uploads/${{ env.BRANCH_NAME }}" + - name: Compress kdf build output + run: | + NAME="kdf_$COMMIT_HASH-android-armv7.zip" + mv target/armv7-linux-androideabi/release/libkdflib.a target/armv7-linux-androideabi/release/libkdf.a + zip $NAME target/armv7-linux-androideabi/release/libkdf.a -j + mv $NAME ./$BRANCH_NAME/ + + - name: Upload build artifact + env: + FILE_SERVER_HOST: ${{ secrets.FILE_SERVER_HOST }} + FILE_SERVER_USERNAME: ${{ secrets.FILE_SERVER_USERNAME }} + FILE_SERVER_PORT: ${{ secrets.FILE_SERVER_PORT }} + FILE_SERVER_KEY: ${{ secrets.FILE_SERVER_KEY }} + if: ${{ env.FILE_SERVER_KEY != '' }} + run: python3 ./scripts/ci/upload_artifact.py "${{ env.BRANCH_NAME }}" "/uploads/${{ env.BRANCH_NAME }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 382fd15bd8..19cb9c5d67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,8 @@ name: Test -on: [push] +on: + push: + branches-ignore: + - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -22,15 +25,19 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') - name: Cargo cache uses: ./.github/actions/cargo-cache - name: Test run: | - # wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash cargo test --bins --lib --no-fail-fast mac-x86-64-unit: @@ -46,17 +53,20 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 - rustup target add x86_64-apple-darwin + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') - name: Cargo cache uses: ./.github/actions/cargo-cache - name: Test run: | - # wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash - cargo test --bins --lib --target x86_64-apple-darwin --no-fail-fast + cargo test --bins --lib --no-fail-fast win-x86-64-unit: timeout-minutes: 90 @@ -71,33 +81,22 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') - name: Cargo cache uses: ./.github/actions/cargo-cache - name: Test run: | - # Invoke-WebRequest -Uri https://github.com/KomodoPlatform/komodo/raw/d456be35acd1f8584e1e4f971aea27bd0644d5c5/zcutil/wget64.exe -OutFile \wget64.exe - # Invoke-WebRequest -Uri https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.bat -OutFile \cmd.bat && \cmd.bat - - # Set Path in variable - # $config = "C:\ProgramData\Docker\config\daemon.json" - - # $json = Get-Content $config | Out-String | ConvertFrom-Json - # $json | Add-Member -Type NoteProperty -Name 'experimental' -Value $True - # $json | ConvertTo-Json | Set-Content $config - - # Check the file content - # type $config - - # Restart-Service docker - # Get-Service docker - cargo test --bins --lib --no-fail-fast - linux-x86-64-mm2-integration: + linux-x86-64-kdf-integration: timeout-minutes: 90 runs-on: ubuntu-latest env: @@ -110,8 +109,45 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + + - name: Cargo cache + uses: ./.github/actions/cargo-cache + + - name: Test + run: | + wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash + cargo test --test 'mm2_tests_main' --no-fail-fast + + mac-x86-64-kdf-integration: + timeout-minutes: 90 + runs-on: macos-latest + env: + BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_MACOS }} + BOB_USERPASS: ${{ secrets.BOB_USERPASS_MACOS }} + ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_MACOS }} + ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_MACOS }} + TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + run: | + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + + - name: Set loopback address + run: ./scripts/ci/lo0_config.sh - name: Cargo cache uses: ./.github/actions/cargo-cache @@ -121,29 +157,7 @@ jobs: wget -O - https://raw.githubusercontent.com/KomodoPlatform/komodo/master/zcutil/fetch-params-alt.sh | bash cargo test --test 'mm2_tests_main' --no-fail-fast - # https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits - # https://github.com/KomodoPlatform/atomicDEX-API/actions/runs/4419618128/jobs/7748266141#step:4:1790 - # mac-x86-64-mm2-integration: - # timeout-minutes: 90 - # runs-on: macos-latest - # env: - # BOB_PASSPHRASE: ${{ secrets.BOB_PASSPHRASE_MACOS }} - # BOB_USERPASS: ${{ secrets.BOB_USERPASS_MACOS }} - # ALICE_PASSPHRASE: ${{ secrets.ALICE_PASSPHRASE_MACOS }} - # ALICE_USERPASS: ${{ secrets.ALICE_USERPASS_MACOS }} - # TELEGRAM_API_KEY: ${{ secrets.TELEGRAM_API_KEY }} - # steps: - # - uses: actions/checkout@v3 - # - name: Install toolchain - # run: | - # rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - # rustup default nightly-2022-10-29 - # rustup target add x86_64-apple-darwin - - # - name: Test - # run: cargo test --test 'mm2_tests_main' --target x86_64-apple-darwin - - win-x86-64-mm2-integration: + win-x86-64-kdf-integration: timeout-minutes: 90 runs-on: windows-latest env: @@ -156,8 +170,13 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') - name: Cargo cache uses: ./.github/actions/cargo-cache @@ -181,8 +200,13 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 + + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') - name: Cargo cache uses: ./.github/actions/cargo-cache @@ -205,10 +229,15 @@ jobs: - uses: actions/checkout@v3 - name: Install toolchain run: | - rustup toolchain install nightly-2022-10-29 --no-self-update --profile=minimal - rustup default nightly-2022-10-29 + rustup toolchain install nightly-2023-06-01 --no-self-update --profile=minimal + rustup default nightly-2023-06-01 rustup target add wasm32-unknown-unknown + - name: Install build deps + uses: ./.github/actions/deps-install + with: + deps: ('protoc') + - name: Install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh @@ -224,4 +253,4 @@ jobs: uses: ./.github/actions/cargo-cache - name: Test - run: WASM_BINDGEN_TEST_TIMEOUT=360 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main + run: WASM_BINDGEN_TEST_TIMEOUT=480 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main diff --git a/.github/workflows/validate-mm2-version.yml b/.github/workflows/validate-mm2-version.yml index fd8c588c81..4abda09e96 100644 --- a/.github/workflows/validate-mm2-version.yml +++ b/.github/workflows/validate-mm2-version.yml @@ -1,4 +1,4 @@ -name: Validate mm2 version +name: Validate kdf version on: pull_request: diff --git a/.gitignore b/.gitignore index db90f0d053..42f63e5115 100755 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ scripts/mm2/seed/unparsed.txt /js/mm2 /js/mm2.exe /js/.mm2.* +/js/kdf +/js/kdf.exe +/js/.kdf.* # Rust artefacts /MM_DATETIME @@ -39,6 +42,8 @@ scripts/mm2/seed/unparsed.txt /MM_VERSION /MM_VERSION.tmp /target +/targettest +/clippytarget # Subcrate artefacts /mm2src/*/target @@ -49,6 +54,7 @@ scripts/mm2/seed/unparsed.txt /.vscode/settings.json /.vscode/tasks.json /.vscode/c_cpp_properties.json +/.vscode/launch.json # IDE artefacts /.vscode/ipch @@ -67,4 +73,10 @@ MM2.json [._]sw[a-p] # mergetool -*.orig \ No newline at end of file +*.orig + +# Ignore containers runtime directories for dockerized tests +# This directory contains temporary data used by Docker containers during tests execution. +# It is recreated from container-state data each time test containers are started, +# and should not be tracked in version control. +.docker/container-runtime/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 42724876e3..0000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "mm2", - "type": "cppvsdbg", - "request": "launch", - "program": "${workspaceRoot}/target/debug/mm2.exe", - "args": ["{\"gui\":\"nogui\",\"client\":1, \"passphrase\":\"123\", \"coins\":\"BTC,KMD\"}"], - "stopAtEntry": false, - "cwd": "${workspaceRoot}", - "environment": [], - "externalConsole": false - }, - { - "name": "test", - "type": "cppvsdbg", - "request": "launch", - "program": "${workspaceRoot}/target/debug/mm2-62664bdf1981f49d.exe", - "args": ["peers_dht"], - "stopAtEntry": false, - "cwd": "${workspaceRoot}", - "environment": [ - {"name": "LOCAL_THREAD_MM", "value": "1"} - ], - "externalConsole": false - }, - { - "name": "test-trade", - "type": "cppvsdbg", - "request": "launch", - // Waiting for https://github.com/rust-lang/cargo/issues/1924. - "program": "${workspaceRoot}/target/debug/mm2-e4371a8467308969.exe", - "args": ["trade_beer_pizza", "--nocapture", "--ignored"], - "stopAtEntry": false, - "cwd": "${workspaceRoot}", - // Use ".env.client" to set PASSPHRASE and USERPASS for Alice, - // ".env.seed" to set PASSPHRASE and USERPASS for Bob. - "environment": [ - {"name": "LOCAL_THREAD_MM", "value": "alice"}, - ], - "externalConsole": false - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e61f3274cc..20a2424397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,97 @@ +## v2.1.0-beta - 2024-07-31 + +**Features:** +- Seed Management [#1939](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1939) + - Seed generation, encryption, and storage features were introduced, including a new `get_mnemonic` API. [#2014](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2014) +- A new `sign_raw_transaction` rpc was added for UTXO and EVM coins, this will facilitate air-gapped wallet implementation in the future. [#1930](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1930) + +**Enhancements/Fixes:** +- Event Streaming [#1901](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1901) + - Balance event streaming for Electrum clients was implemented. [#2013](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2013) + - Balance events for UTXOs were introduced. + - Electrum notification receiving bug was fixed. + - Balance event streaming for EVM was added. [#2041](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2041) + - Error events were introduced. [#2041](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2041) + - Heartbeats were introduced to notify about streaming channel health. [#2058](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2058) + - Balance event streaming for ARRR/Pirate was added. [#2076](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2076) +- Trading Protocol Upgrade [#1895](https://github.com/KomodoPlatform/atomicDEX-API/issues/1895) + - *Important note:* Seednodes update is needed to support and rebroadcast new swap protocol messages. [#2015](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2015) + - WASM storage for upgraded swaps introduced. [#2015](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2015) + - Migration of old swaps data was added. [#2015](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2015) + - Swaps now automatically kickstart on MM2 reload. [#2015](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2015) + - File lock for swaps added, preventing the same swap from starting in different processes. [#2015](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2015) + - `my_swap_status`, `my_recent_swaps` V2 RPCs introduced. [#2015](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2015) + - Upgraded swaps data now accessible through V1 RPCs. [#2015](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2015) + - Locked amount handling for UTXO swaps implemented. [#2046](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2046) + - Conditional wait for maker payment confirmation was added before signing funding tx spend preimage on taker's side. [#2046](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2046) + - `active_swaps` V2 RPC introduced. [#2046](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2046) + - Handling `accept_only_from` for swap messages (validation of the sender) was implemented. [#2046](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2046) + - `swap_uuid` for swap v2 messages was added to avoid reuse of the messages generated for other swaps. [#2046](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2046) + - Maker payment immediate refund path handling was implemented. [#2046](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2046) +- KMD Burn [#2010](https://github.com/KomodoPlatform/komodo-defi-framework/issues/2010) + - KMD dex fee burn for upgraded swaps was added. [#2046](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2046) +- Hardware Wallet [#964](https://github.com/KomodoPlatform/atomicDEX-API/issues/964) + - Trezor now supports SegWit for withdrawals. [#1984](https://github.com/KomodoPlatform/atomicDEX-API/pull/1984) + - Trezor support was added for EVM coins/tokens using task manager activation methods. [#1962](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1962) + - Support for unsigned Tendermint transactions using Ledger's Keplr extension was added, excluding HTLC transactions and swap operations. [#2148](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2148) +- NFT integration [#900](https://github.com/KomodoPlatform/atomicDEX-API/issues/900) + - A new `clear_nft_db` RPC for NFT data management was added. This enables selective (based on a chain) or complete NFT DB data clearance. [#2039](https://github.com/KomodoPlatform/atomicDEX-API/pull/2039) + - NFT can now be enabled using `enable_eth_with_tokens` or `enable_nft`, similar to `enable_erc20`. [#2049](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2049) + - NFT swaps V2 POC was shown, which includes a NFT maker payment test using the dockerized Geth dev node. [#2084](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2084) + - `komodo-defi-proxy` support for NFT feature was added. [#2100](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2100) + - Additional checks were added for malicious `token_uri` links. [#2100](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2100) + - `clear_all` parameter in `clear_nft_db` RPC is now optional (default: `false`). [#2100](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2100) +- WASM Worker + - Improved environment detection to ensure the correct method is used for accessing the IndexedDB factory, accommodating both window and worker contexts. [#1953](https://github.com/KomodoPlatform/atomicDEX-API/pull/1953), [#2131](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2131) + - SharedWorker support was added, allowing any worker path in `event_stream_configuration` with a default to `event_streaming_worker.js`. [#2080](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2080) +- Simple Maker Bot [#1065](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1065) + - Maker bot was updated to support multiple price URLs. [#2027](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2027) + - `testcoin` was added to provider options to allow testing the maker bot using test chains assets. [#2161](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2161) +- IndexedDB + - Cursor implementation was fixed, ensuring stable iteration over items. [#2028](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2028) + - Advanced cursor filtering features were added, including limit, offset, and a fix for `where_` condition/option. [#2066](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2066) +- Swap Stats DB + - `stats_swaps` table now includes GUI and MM2 version data used for a swap. [#2061](https://github.com/KomodoPlatform/atomicDEX-API/pull/2061) +- P2P Layer + - Added `max_concurrent_connections` to MM2 config to control the maximum number of concurrent connections for Gossipsub. [#2063](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2063) +- ARRR/Pirate [#927](https://github.com/KomodoPlatform/komodo-defi-framework/issues/927) + - ARRR/Pirate wallet and Dex operations now work in browser environments / WASM. [#1957](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1957), [#2077](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2077) + - Syncing and activation improvements were made, including stopping sync status after main sync and refining `first_sync_block` handling. [#2089](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2089) +- EVM Transport + - ETH websocket transport was introduced. `komodo-defi-proxy` signed messages were also supported for websocket transport. [#2058](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2058) +- Tendermint integration [#1432](https://github.com/KomodoPlatform/atomicDEX-API/issues/1432) + - Nucleus chain support was introduced as an alternative HTLC backend to Iris. [#2079](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2079) + - Tendermint fee calculation was fixed to use `get_receiver_trade_fee` in platform coin. [#2106](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2106) + - Pubkey-only mode for Tendermint protocol was introduced, allowing use of any external wallet for wallet and swap operations. [#2088](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2088) + - `ibc_withdraw` RPC was removed, and `withdraw` was refactored to support IBC transfers by automatically finding IBC channels. [#2088](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2088) + - Transaction history handling was enhanced to support base64 encoded transaction values for Cosmos-based networks, preventing missing transactions in the history table. [#2133](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2133) + - The precision of max amount handling was improved for Tendermint withdraw operations by simulating the transaction and removing the estimated fee. [#2155](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2155) + - Account sequence numbers are now resolved locally, incorrect sequence numbers from cached responses are also avoided. [#2164](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2164) +- HD Wallet [#1838](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1838) + - Full UTXO and EVM HD wallet functionalities were implemented. [#1962](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1962) +- Swap watchers [#1431](https://github.com/KomodoPlatform/atomicDEX-API/issues/1431) + - UTXO swaps were fixed to apply events that occurred while the taker was offline, such as maker spending or watcher refunding the taker payment. [#2114](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2114) +- Fees Improvements [#1848](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1848) + - EIP-1559 gas fee estimator and RPCs were added for ETH, including priority fee support for withdrawals and swaps, and improved gas limit for swap transactions. [#2051](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2051) + - `gas_limit` parameter can be used in coins config to override default gas limit values. [#2137](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2137) + - Default `gas_limit` values now ensure that Proxied ERC20 tokens have sufficient gas. [#2137](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2137) +- Rust Toolchain [#1972](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1972) + - Toolchain was upgraded to Rust toolchain version 1.72 nightly (nightly-2023-06-01). [#2149](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2149) + - rust-analyzer was added into the workspace toolchain. [#2179](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2179) +- CI Builds + - MacOS builds for Apple Silicon are now provided through the CI pipeline. [#2163](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2163) +- Miscellaneous + - BCH block header deserialization was fixed to match BTC's handling of `KAWPOW` version headers. [#2099](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2099) + - Implemented root application directory `.kdf` under `$HOME` to consolidate all runtime files, enhancing user experience by following standard UNIX practices. [#2102](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2102) + - Memory usage was improved a bit through preallocation optimizations. [#2098](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2098) + - Swaps and orders file handling was enhanced to use `.tmp` files to avoid concurrent reading/writing issues. [#2118](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2118) + - UTXO P2PK balance is now shown as part of the P2PKH/Legacy address balance and can be spent in withdrawals and swaps. [#2053](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2053) + - `wallet-only` restriction was removed from `max_maker_vol` RPC, enabling its use for wallet-only mode assets. [#2153](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2153) + +**NB - Backwards compatibility breaking changes:** +- Renamed `mm2` binaries to `kdf`, while providing backward-compatible copies with `mm2` naming; WASM binaries use `kdf` naming only, which is a breaking change. [#2126](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2126) + + ## v2.0.0-beta - 2023-12-15 **Features:** - KMD Burn [#2010](https://github.com/KomodoPlatform/komodo-defi-framework/issues/2010) diff --git a/Cargo.lock b/Cargo.lock index 5981ce776e..755ad68862 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "922b33332f54fc0ad13fa3e514601e8d30fb54e1f3eadc36643f6526db645621" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -49,9 +49,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if 1.0.0", - "cipher", - "cpufeatures 0.2.1", - "opaque-debug 0.3.0", + "cipher 0.3.0", + "cpufeatures 0.2.11", + "opaque-debug", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.4.4", + "cpufeatures 0.2.11", ] [[package]] @@ -61,11 +72,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" dependencies = [ "aead", - "aes", - "cipher", + "aes 0.7.5", + "cipher 0.3.0", "ctr", "ghash", - "subtle 2.4.0", + "subtle", ] [[package]] @@ -138,6 +149,19 @@ version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" +[[package]] +name = "argon2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.11", + "password-hash", + "zeroize", +] + [[package]] name = "arrayref" version = "0.3.6" @@ -237,8 +261,8 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -250,13 +274,13 @@ checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" [[package]] name = "async-trait" -version = "0.1.52" +version = "0.1.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +checksum = "531b97fb4cd3dfdce92c35dedbfdc1f0b9d8091c8ca943d6dae340ef5012d514" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 1.0.95", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -297,45 +321,45 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.17" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", "bitflags", "bytes 1.4.0", "futures-util", - "http 0.2.7", + "http 0.2.12", "http-body 0.4.5", "hyper", - "itoa 1.0.1", + "itoa 1.0.10", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite 0.2.9", + "rustversion", "serde", "sync_wrapper", - "tokio", "tower", - "tower-http", "tower-layer", "tower-service", ] [[package]] name = "axum-core" -version = "0.2.9" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", "bytes 1.4.0", "futures-util", - "http 0.2.7", + "http 0.2.12", "http-body 0.4.5", "mime", + "rustversion", "tower-layer", "tower-service", ] @@ -363,9 +387,9 @@ checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" [[package]] name = "base16ct" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base32" @@ -379,15 +403,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" -[[package]] -name = "base64" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -dependencies = [ - "byteorder", -] - [[package]] name = "base64" version = "0.11.0" @@ -408,9 +423,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -441,7 +456,7 @@ dependencies = [ "num_cpus", "pairing", "rand_core 0.5.1", - "subtle 2.4.0", + "subtle", ] [[package]] @@ -477,7 +492,18 @@ dependencies = [ "ripemd160", "secp256k1 0.20.3", "sha2 0.9.9", - "subtle 2.4.0", + "subtle", + "zeroize", +] + +[[package]] +name = "bip39" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" +dependencies = [ + "bitcoin_hashes", + "rand_core 0.6.4", "zeroize", ] @@ -507,7 +533,7 @@ dependencies = [ "ripemd160", "serialization", "sha-1", - "sha2 0.9.9", + "sha2 0.10.7", "sha3 0.9.1", "siphasher", ] @@ -598,18 +624,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding 0.1.5", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" @@ -617,7 +631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ "block-padding 0.2.1", - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -626,34 +640,24 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] -name = "block-modes" -version = "0.8.1" +name = "block-padding" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" -dependencies = [ - "block-padding 0.2.1", - "cipher", -] +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "block-padding" -version = "0.1.5" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "byte-tools", + "generic-array", ] -[[package]] -name = "block-padding" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" - [[package]] name = "blocking" version = "0.4.6" @@ -678,7 +682,17 @@ dependencies = [ "group 0.8.0", "pairing", "rand_core 0.5.1", - "subtle 2.4.0", + "subtle", +] + +[[package]] +name = "bollard-stubs" +version = "1.42.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" +dependencies = [ + "serde", + "serde_with", ] [[package]] @@ -700,7 +714,7 @@ dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", "proc-macro-crate 0.1.5", - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", "syn 1.0.95", ] @@ -710,8 +724,8 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -721,8 +735,8 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -766,12 +780,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c1bf4a04a88c54f589125563643d773f3254b5c38571395e2b591c693bbc81" -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "bytemuck" version = "1.8.0" @@ -787,8 +795,8 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -813,6 +821,9 @@ name = "bytes" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] [[package]] name = "bzip2" @@ -852,6 +863,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "cc" version = "1.0.74" @@ -880,8 +900,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" dependencies = [ "cfg-if 1.0.0", - "cipher", - "cpufeatures 0.2.1", + "cipher 0.3.0", + "cpufeatures 0.2.11", "zeroize", ] @@ -893,7 +913,7 @@ checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" dependencies = [ "aead", "chacha20", - "cipher", + "cipher 0.3.0", "poly1305", "zeroize", ] @@ -932,7 +952,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.5", + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", ] [[package]] @@ -944,7 +974,7 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", @@ -968,15 +998,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "cmake" -version = "0.1.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" -dependencies = [ - "cc", -] - [[package]] name = "codespan-reporting" version = "0.11.1" @@ -994,12 +1015,13 @@ dependencies = [ "async-std", "async-trait", "base58", - "base64 0.10.1", + "base64 0.21.7", "bincode", "bip32", "bitcoin", "bitcoin_hashes", "bitcrypto", + "blake2b_simd", "byteorder", "bytes 0.4.12", "cfg-if 1.0.0", @@ -1013,23 +1035,27 @@ dependencies = [ "dirs", "ed25519-dalek", "ed25519-dalek-bip32 0.2.0", - "enum_from", + "enum_derives", "ethabi", "ethcore-transaction", "ethereum-types", "ethkey", + "ff 0.8.0", "futures 0.1.29", "futures 0.3.28", + "futures-ticker", "futures-util", "group 0.8.0", "gstuff", - "hex 0.4.3", - "http 0.2.7", + "hex", + "http 0.2.12", "hyper", - "hyper-rustls", + "hyper-rustls 0.24.2", + "instant", "itertools", "js-sys", "jsonrpc-core", + "jubjub", "keys", "lazy_static", "libc", @@ -1059,12 +1085,14 @@ dependencies = [ "protobuf", "rand 0.7.3", "regex", + "reqwest", "rlp", "rmp-serde", "rpc", "rpc_task", "rust-ini", - "rustls 0.20.4", + "rustls 0.21.10", + "satomic-swap", "script", "secp256k1 0.20.3", "secp256k1 0.24.3", @@ -1075,7 +1103,7 @@ dependencies = [ "serde_json", "serialization", "serialization_derive", - "sha2 0.9.9", + "sha2 0.10.7", "sha3 0.9.1", "solana-client", "solana-sdk", @@ -1083,27 +1111,29 @@ dependencies = [ "spl-associated-token-account", "spl-token", "spv_validation", - "tendermint-config", "tendermint-rpc", - "tiny-bip39", + "time 0.3.20", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-tungstenite-wasm", "tonic", "tonic-build", + "tower-service", "url", "utxo_signer", "uuid 1.2.2", + "wagyu-zcash-parameters", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", "web3", - "webpki-roots 0.22.3", + "webpki-roots 0.25.4", "winapi", "zbase32", "zcash_client_backend", "zcash_client_sqlite", + "zcash_extras", "zcash_primitives", "zcash_proofs", ] @@ -1119,7 +1149,7 @@ dependencies = [ "derive_more", "ethereum-types", "futures 0.3.28", - "hex 0.4.3", + "hex", "lightning", "lightning-background-processor", "lightning-invoice", @@ -1137,6 +1167,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "url", ] [[package]] @@ -1160,11 +1191,11 @@ dependencies = [ "futures 0.3.28", "futures-timer", "gstuff", - "hex 0.4.3", - "http 0.2.7", + "hex", + "http 0.2.12", "http-body 0.1.0", "hyper", - "hyper-rustls", + "hyper-rustls 0.24.2", "instant", "itertools", "js-sys", @@ -1177,6 +1208,7 @@ dependencies = [ "primitive-types", "rand 0.7.3", "regex", + "rustc-hash", "ser_error", "ser_error_derive", "serde", @@ -1184,7 +1216,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_repr", - "sha2 0.9.9", + "sha2 0.10.7", "shared_ref_counter", "tokio", "uuid 1.2.2", @@ -1250,9 +1282,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.7.1" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" @@ -1287,9 +1319,9 @@ dependencies = [ [[package]] name = "cosmos-sdk-proto" -version = "0.12.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ca04d3795c18023c221a2143b29de9c70668ecb22d17783bc02ee780c6c404" +checksum = "73c9d2043a9e617b0d602fbc0a0ecd621568edbf3a9774890a6d562389bd8e1c" dependencies = [ "prost", "prost-types", @@ -1298,18 +1330,16 @@ dependencies = [ [[package]] name = "cosmrs" -version = "0.7.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6989fdb6267eccb52762530b79ce0b385f4eaeb8b786522a95512e9bebb268c2" +checksum = "af13955d6f356272e6def9ff5e2450a7650df536d8934f47052a20c76513d2f6" dependencies = [ "cosmos-sdk-proto", "ecdsa", "eyre", "getrandom 0.2.9", "k256", - "prost", - "prost-types", - "rand_core 0.6.3", + "rand_core 0.6.4", "serde", "serde_json", "subtle-encoding", @@ -1328,9 +1358,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.1" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -1425,7 +1455,7 @@ dependencies = [ "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", - "memoffset 0.5.4", + "memoffset 0.5.6", "scopeguard", ] @@ -1493,18 +1523,26 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" name = "crypto" version = "1.0.0" dependencies = [ + "aes 0.8.3", + "argon2", "arrayref", "async-trait", + "base64 0.21.7", "bip32", + "bip39", "bitcrypto", "bs58 0.4.0", + "cbc", + "cfg-if 1.0.0", + "cipher 0.4.4", "common", "derive_more", "enum-primitive-derive", - "enum_from", + "enum_derives", "futures 0.3.28", - "hex 0.4.3", - "http 0.2.7", + "hex", + "hmac 0.12.1", + "http 0.2.12", "hw_common", "keys", "lazy_static", @@ -1524,52 +1562,44 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "tiny-bip39", + "sha2 0.10.7", + "tokio", "trezor", "wasm-bindgen-test", "web3", + "zeroize", ] [[package]] name = "crypto-bigint" -version = "0.3.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array 0.14.5", - "rand_core 0.6.3", - "subtle 2.4.0", + "generic-array", + "rand_core 0.6.4", + "subtle", "zeroize", ] [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.5", + "generic-array", "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" -dependencies = [ - "generic-array 0.12.4", - "subtle 1.0.0", -] - [[package]] name = "crypto-mac" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" dependencies = [ - "generic-array 0.14.5", - "subtle 2.4.0", + "generic-array", + "subtle", ] [[package]] @@ -1578,8 +1608,8 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" dependencies = [ - "generic-array 0.14.5", - "subtle 2.4.0", + "generic-array", + "subtle", ] [[package]] @@ -1588,8 +1618,8 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.5", - "subtle 2.4.0", + "generic-array", + "subtle", ] [[package]] @@ -1613,7 +1643,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" dependencies = [ - "cipher", + "cipher 0.3.0", ] [[package]] @@ -1636,7 +1666,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "subtle 2.4.0", + "subtle", "zeroize", ] @@ -1650,7 +1680,20 @@ dependencies = [ "fiat-crypto", "packed_simd_2", "platforms", - "subtle 2.4.0", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", "zeroize", ] @@ -1675,8 +1718,8 @@ dependencies = [ "cc", "codespan-reporting", "lazy_static", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "scratch", "syn 1.0.95", ] @@ -1693,8 +1736,43 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 1.0.95", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.69", + "quote 1.0.33", + "strsim 0.10.0", + "syn 1.0.95", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote 1.0.33", "syn 1.0.95", ] @@ -1742,7 +1820,7 @@ dependencies = [ "common", "crossbeam-channel 0.5.1", "futures 0.3.28", - "hex 0.4.3", + "hex", "log", "rusqlite", "sql-builder", @@ -1750,23 +1828,14 @@ dependencies = [ "uuid 1.2.2", ] -[[package]] -name = "debug_stub_derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496b7f8a2f853313c3ca370641d7ff3e42c32974fdccda8f0684599ed0a3ff6b" -dependencies = [ - "quote 0.3.15", - "syn 0.11.11", -] - [[package]] name = "der" -version = "0.5.1" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", + "zeroize", ] [[package]] @@ -1790,8 +1859,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -1801,8 +1870,8 @@ version = "0.99.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -1818,22 +1887,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -1843,8 +1903,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.2", + "const-oid", "crypto-common", - "subtle 2.4.0", + "subtle", ] [[package]] @@ -1939,14 +2000,16 @@ checksum = "5caaa75cbd2b960ff1e5392d2cfb1f44717fffe12fc1f32b7b5d1267f99732a6" [[package]] name = "ecdsa" -version = "0.13.4" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", + "digest 0.10.7", "elliptic-curve", "rfc6979", - "signature", + "signature 2.2.0", + "spki", ] [[package]] @@ -1955,7 +2018,30 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ - "signature", + "signature 1.4.0", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", ] [[package]] @@ -1965,7 +2051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek 3.2.0", - "ed25519", + "ed25519 1.5.2", "rand 0.7.3", "serde", "sha2 0.9.9", @@ -2011,19 +2097,20 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "elliptic-curve" -version = "0.11.12" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "der", - "ff 0.11.1", - "generic-array 0.14.5", - "group 0.11.0", - "rand_core 0.6.3", + "digest 0.10.7", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "pkcs8", + "rand_core 0.6.4", "sec1", - "subtle 2.4.0", + "subtle", "zeroize", ] @@ -2055,8 +2142,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ "heck", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -2067,17 +2154,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c375b9c5eadb68d0a6efee2999fef292f45854c3444c86f09d8ab086ba942b0e" dependencies = [ "num-traits", - "quote 1.0.28", + "quote 1.0.33", "syn 1.0.95", ] [[package]] -name = "enum_from" +name = "enum_derives" version = "0.1.0" dependencies = [ "itertools", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -2097,12 +2184,18 @@ dependencies = [ [[package]] name = "equihash" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" dependencies = [ "blake2b_simd", "byteorder", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.2.8" @@ -2142,7 +2235,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4966fba78396ff92db3b817ee71143eccd98acf0f876b8d600e585a670c5d1b" dependencies = [ "ethereum-types", - "hex 0.4.3", + "hex", "once_cell", "regex", "serde", @@ -2168,7 +2261,7 @@ dependencies = [ [[package]] name = "ethcore-transaction" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" dependencies = [ "ethereum-types", "ethkey", @@ -2195,7 +2288,7 @@ dependencies = [ [[package]] name = "ethkey" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" dependencies = [ "byteorder", "edit-distance", @@ -2242,18 +2335,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", "synstructure", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -2289,17 +2376,17 @@ checksum = "01646e077d4ebda82b73f1bca002ea1e91561a77df2431a9e79729bcc31950ef" dependencies = [ "bitvec 0.18.5", "rand_core 0.5.1", - "subtle 2.4.0", + "subtle", ] [[package]] name = "ff" -version = "0.11.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core 0.6.3", - "subtle 2.4.0", + "rand_core 0.6.4", + "subtle", ] [[package]] @@ -2388,12 +2475,12 @@ dependencies = [ [[package]] name = "fpe" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd910db5f9ca4dc3116f8c46367825807aa2b942f72565f16b4be0b208a00a9e" +checksum = "26c4b37de5ae15812a764c958297cfc50f5c010438f60c6ce75d11b802abd404" dependencies = [ - "block-modes", - "cipher", + "cbc", + "cipher 0.4.4", "libm 0.2.7", "num-bigint", "num-integer", @@ -2510,31 +2597,30 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] name = "futures-rustls" -version = "0.21.1" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1387e07917c711fb4ee4f48ea0adb04a3c9739e53ef85bf43ae1edc2937a8b" +checksum = "e01fe9932a224b72b45336d96040aa86386d674a31d0af27d800ea7bc8ca97fe" dependencies = [ "futures-io", - "rustls 0.19.1", - "webpki 0.21.3", + "rustls 0.20.4", + "webpki", ] [[package]] name = "futures-rustls" -version = "0.22.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01fe9932a224b72b45336d96040aa86386d674a31d0af27d800ea7bc8ca97fe" +checksum = "35bd3cf68c183738046838e300353e4716c674dc5e56890de4826801a6622a28" dependencies = [ "futures-io", - "rustls 0.20.4", - "webpki 0.22.0", + "rustls 0.21.10", ] [[package]] @@ -2591,22 +2677,14 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "generic-array" -version = "0.14.5" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "serde", "typenum", "version_check", + "zeroize", ] [[package]] @@ -2650,7 +2728,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bbd60caa311237d508927dbba7594b483db3ef05faa55172fcf89b1bcda7853" dependencies = [ - "opaque-debug 0.3.0", + "opaque-debug", "polyval", ] @@ -2681,7 +2759,7 @@ checksum = "2432787a9b8f0d58dca43fe2240399479b7582dc8afa2126dc7652b864029e47" dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -2693,18 +2771,18 @@ dependencies = [ "byteorder", "ff 0.8.0", "rand_core 0.5.1", - "subtle 2.4.0", + "subtle", ] [[package]] name = "group" -version = "0.11.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.11.1", - "rand_core 0.6.3", - "subtle 2.4.0", + "ff 0.13.0", + "rand_core 0.6.4", + "subtle", ] [[package]] @@ -2719,17 +2797,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.19" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes 1.4.0", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.7", - "indexmap", + "http 0.2.12", + "indexmap 2.2.3", "slab", "tokio", "tokio-util", @@ -2778,6 +2856,12 @@ dependencies = [ "ahash 0.8.3", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "hashlink" version = "0.8.2" @@ -2787,6 +2871,30 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.7", + "bytes 1.4.0", + "headers-core", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + [[package]] name = "heck" version = "0.4.0" @@ -2802,12 +2910,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hex" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" - [[package]] name = "hex" version = "0.4.3" @@ -2838,21 +2940,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84f2a5541afe0725f0b95619d6af614f48c1b176385b8aa30918cfb8c4bfafc8" dependencies = [ "hmac 0.11.0", - "rand_core 0.6.3", + "rand_core 0.6.4", "sha2 0.9.9", "zeroize", ] -[[package]] -name = "hmac" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" -dependencies = [ - "crypto-mac 0.7.0", - "digest 0.8.1", -] - [[package]] name = "hmac" version = "0.8.1" @@ -2899,7 +2991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" dependencies = [ "digest 0.9.0", - "generic-array 0.14.5", + "generic-array", "hmac 0.8.1", ] @@ -2927,13 +3019,13 @@ dependencies = [ [[package]] name = "http" -version = "0.2.7" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes 1.4.0", "fnv", - "itoa 1.0.1", + "itoa 1.0.10", ] [[package]] @@ -2955,16 +3047,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes 1.4.0", - "http 0.2.7", + "http 0.2.12", "pin-project-lite 0.2.9", ] -[[package]] -name = "http-range-header" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" - [[package]] name = "httparse" version = "1.8.0" @@ -3015,11 +3101,11 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 0.2.7", + "http 0.2.12", "http-body 0.4.5", "httparse", "httpdate", - "itoa 1.0.1", + "itoa 1.0.10", "pin-project-lite 0.2.9", "socket2 0.4.9", "tokio", @@ -3034,12 +3120,26 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ - "http 0.2.7", + "http 0.2.12", "hyper", "rustls 0.20.4", "tokio", - "tokio-rustls", - "webpki-roots 0.22.3", + "tokio-rustls 0.23.2", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper", + "rustls 0.21.10", + "tokio", + "tokio-rustls 0.24.1", + "webpki-roots 0.25.4", ] [[package]] @@ -3078,6 +3178,12 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -3151,8 +3257,8 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -3178,6 +3284,16 @@ dependencies = [ "hashbrown 0.12.1", ] +[[package]] +name = "indexmap" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + [[package]] name = "indicatif" version = "0.16.2" @@ -3190,6 +3306,16 @@ dependencies = [ "regex", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding 0.3.3", + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -3256,9 +3382,9 @@ checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jemalloc-sys" @@ -3325,20 +3451,21 @@ dependencies = [ "ff 0.8.0", "group 0.8.0", "rand_core 0.5.1", - "subtle 2.4.0", + "subtle", ] [[package]] name = "k256" -version = "0.10.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19c3a5e0a0b8450278feda242592512e09f61c72e018b8cd5c859482802daf2d" +checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" dependencies = [ "cfg-if 1.0.0", "ecdsa", "elliptic-curve", - "sec1", - "sha2 0.9.9", + "once_cell", + "sha2 0.10.7", + "signature 2.2.0", ] [[package]] @@ -3404,9 +3531,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libloading" @@ -3433,7 +3560,7 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libp2p" version = "0.52.1" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -3465,7 +3592,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3476,7 +3603,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3487,7 +3614,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "either", "fnv", @@ -3514,7 +3641,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "futures 0.3.28", "libp2p-core", @@ -3528,7 +3655,7 @@ dependencies = [ [[package]] name = "libp2p-floodsub" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "asynchronous-codec", "cuckoofilter", @@ -3548,10 +3675,10 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.45.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "asynchronous-codec", - "base64 0.21.2", + "base64 0.21.7", "byteorder", "bytes 1.4.0", "either", @@ -3579,7 +3706,7 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "asynchronous-codec", "either", @@ -3619,7 +3746,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.44.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "data-encoding", "futures 0.3.28", @@ -3639,7 +3766,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.13.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "instant", "libp2p-core", @@ -3655,7 +3782,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "bytes 1.4.0", "curve25519-dalek 3.2.0", @@ -3679,7 +3806,7 @@ dependencies = [ [[package]] name = "libp2p-ping" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "either", "futures 0.3.28", @@ -3696,7 +3823,7 @@ dependencies = [ [[package]] name = "libp2p-request-response" version = "0.25.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "async-trait", "futures 0.3.28", @@ -3713,7 +3840,7 @@ dependencies = [ [[package]] name = "libp2p-swarm" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "either", "fnv", @@ -3735,19 +3862,19 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.33.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "heck", "proc-macro-warning", - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] name = "libp2p-tcp" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "futures 0.3.28", "futures-timer", @@ -3763,7 +3890,7 @@ dependencies = [ [[package]] name = "libp2p-wasm-ext" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "futures 0.3.28", "js-sys", @@ -3776,7 +3903,7 @@ dependencies = [ [[package]] name = "libp2p-websocket" version = "0.42.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "either", "futures 0.3.28", @@ -3795,7 +3922,7 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.44.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "either", "futures 0.3.28", @@ -3852,7 +3979,7 @@ checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" dependencies = [ "crunchy", "digest 0.9.0", - "subtle 2.4.0", + "subtle", ] [[package]] @@ -3863,7 +3990,7 @@ checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" dependencies = [ "crunchy", "digest 0.9.0", - "subtle 2.4.0", + "subtle", ] [[package]] @@ -4075,9 +4202,9 @@ checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] name = "matchit" -version = "0.5.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "maybe-uninit" @@ -4088,7 +4215,7 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "mem" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" [[package]] name = "memchr" @@ -4107,9 +4234,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" dependencies = [ "autocfg 1.1.0", ] @@ -4151,9 +4278,9 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" dependencies = [ - "base64 0.21.2", + "base64 0.21.7", "hyper", - "indexmap", + "indexmap 1.9.3", "ipnet", "metrics", "metrics-util", @@ -4169,9 +4296,9 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -4184,7 +4311,7 @@ dependencies = [ "crossbeam-epoch 0.9.5", "crossbeam-utils 0.8.16", "hashbrown 0.13.2", - "indexmap", + "indexmap 1.9.3", "metrics", "num_cpus", "ordered-float", @@ -4231,7 +4358,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "2.0.0-beta" +version = "2.1.0-beta" dependencies = [ "chrono", "common", @@ -4256,25 +4383,32 @@ name = "mm2_core" version = "0.1.0" dependencies = [ "arrayref", + "async-std", "async-trait", "cfg-if 1.0.0", "common", "db_common", "derive_more", "futures 0.3.28", - "futures-rustls 0.21.1", "gstuff", - "hex 0.4.3", + "hex", + "instant", "lazy_static", + "mm2_err_handle", "mm2_event_stream", "mm2_metrics", "mm2_rpc", "primitives", "rand 0.7.3", + "rustls 0.21.10", + "ser_error", + "ser_error_derive", "serde", "serde_json", "shared_ref_counter", + "tokio", "uuid 1.2.2", + "wasm-bindgen-test", ] [[package]] @@ -4284,9 +4418,9 @@ dependencies = [ "async-trait", "common", "derive_more", - "enum_from", + "enum_derives", "futures 0.3.28", - "hex 0.4.3", + "hex", "itertools", "js-sys", "lazy_static", @@ -4311,7 +4445,7 @@ dependencies = [ "common", "derive_more", "futures 0.1.29", - "http 0.2.7", + "http 0.2.12", "itertools", "ser_error", "ser_error_derive", @@ -4325,8 +4459,8 @@ version = "0.1.0" dependencies = [ "ethabi", "ethkey", - "hex 0.4.3", - "indexmap", + "hex", + "indexmap 1.9.3", "itertools", "mm2_err_handle", "secp256k1 0.20.3", @@ -4355,7 +4489,7 @@ version = "0.1.0" dependencies = [ "async-trait", "common", - "http 0.2.7", + "http 0.2.12", "mm2_err_handle", "mm2_net", "serde", @@ -4422,16 +4556,18 @@ dependencies = [ "dirs", "either", "enum-primitive-derive", - "enum_from", + "enum_derives", + "ethabi", + "ethcore-transaction", "ethereum-types", "futures 0.1.29", "futures 0.3.28", - "futures-rustls 0.21.1", + "futures-rustls 0.24.0", "gstuff", "hash-db", "hash256-std-hasher", - "hex 0.4.3", - "http 0.2.7", + "hex", + "http 0.2.12", "hw_common", "hyper", "instant", @@ -4464,10 +4600,12 @@ dependencies = [ "rand 0.7.3", "rcgen", "regex", + "rlp", "rmp-serde", "rpc", "rpc_task", - "rustls 0.20.4", + "rustc-hex", + "rustls 0.21.10", "rustls-pemfile 1.0.2", "script", "secp256k1 0.20.3", @@ -4490,6 +4628,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", + "web3", "winapi", ] @@ -4520,12 +4659,12 @@ dependencies = [ name = "mm2_metrics" version = "0.1.0" dependencies = [ - "base64 0.10.1", + "base64 0.21.7", "common", "derive_more", "futures 0.3.28", "hyper", - "hyper-rustls", + "hyper-rustls 0.24.2", "itertools", "metrics", "metrics-exporter-prometheus", @@ -4542,6 +4681,7 @@ version = "0.1.0" dependencies = [ "async-stream", "async-trait", + "base64 0.21.7", "bytes 1.4.0", "cfg-if 1.0.0", "common", @@ -4550,7 +4690,9 @@ dependencies = [ "futures 0.3.28", "futures-util", "gstuff", - "http 0.2.7", + "http 0.2.12", + "http-body 0.4.5", + "httparse", "hyper", "js-sys", "lazy_static", @@ -4560,13 +4702,17 @@ dependencies = [ "mm2_p2p", "mm2_state_machine", "parking_lot 0.12.0", + "pin-project", "prost", "rand 0.7.3", - "rustls 0.20.4", + "rustls 0.21.10", "serde", "serde_json", + "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", + "tonic", + "tower-service", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -4596,9 +4742,10 @@ dependencies = [ "derive_more", "env_logger", "futures 0.3.28", - "futures-rustls 0.21.1", + "futures-rustls 0.22.1", + "futures-rustls 0.24.0", "futures-ticker", - "hex 0.4.3", + "hex", "instant", "lazy_static", "libp2p", @@ -4609,9 +4756,9 @@ dependencies = [ "secp256k1 0.20.3", "serde", "serde_bytes", - "sha2 0.9.9", + "sha2 0.10.7", "smallvec 1.6.1", - "syn 2.0.23", + "syn 2.0.38", "tokio", "void", ] @@ -4624,7 +4771,7 @@ dependencies = [ "derive_more", "futures 0.3.28", "gstuff", - "http 0.2.7", + "http 0.2.12", "mm2_err_handle", "mm2_number", "rpc", @@ -4656,7 +4803,7 @@ dependencies = [ "db_common", "futures 0.3.28", "gstuff", - "http 0.2.7", + "http 0.2.12", "lazy_static", "mm2_core", "mm2_io", @@ -4688,8 +4835,8 @@ version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3048ef3680533a27f9f8e7d6a0bce44dc61e4895ea0f42709337fa1c8616fefe" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -4742,7 +4889,7 @@ checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" [[package]] name = "multistream-select" version = "0.13.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -4888,8 +5035,8 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -4952,20 +5099,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" dependencies = [ "proc-macro-crate 1.1.3", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "number_prefix" version = "0.4.0" @@ -4987,12 +5125,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - [[package]] name = "opaque-debug" version = "0.3.0" @@ -5033,8 +5165,8 @@ checksum = "44a0b52c2cbaef7dffa5fec1a43274afe8bd2a644fa9fc50a9ef4ff0269b1257" dependencies = [ "Inflector", "proc-macro-error", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -5079,8 +5211,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c45ed1f39709f5a89338fab50e59816b2e8815f5bb58276e7ddf9afd495f73f8" dependencies = [ "proc-macro-crate 1.1.3", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -5108,7 +5240,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f557c32c6d268a07c921471619c0295f5efad3a0e76d4f97a05c091a51d110b2" dependencies = [ - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", "syn 1.0.95", "synstructure", ] @@ -5189,6 +5321,17 @@ dependencies = [ "windows-sys 0.32.0", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.7" @@ -5230,8 +5373,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" dependencies = [ "peg-runtime", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", ] [[package]] @@ -5262,7 +5405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 1.9.3", ] [[package]] @@ -5280,9 +5423,9 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -5305,13 +5448,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs8" -version = "0.8.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", - "zeroize", ] [[package]] @@ -5349,7 +5491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fe800695325da85083cd23b56826fccb2e2dc29b218e7811a6f33bc93f414be" dependencies = [ "cpufeatures 0.1.4", - "opaque-debug 0.3.0", + "opaque-debug", "universal-hash", ] @@ -5361,7 +5503,7 @@ checksum = "e597450cbf209787f0e6de80bf3795c6b2356a380ee87837b545aded8dbc1823" dependencies = [ "cfg-if 1.0.0", "cpufeatures 0.1.4", - "opaque-debug 0.3.0", + "opaque-debug", "universal-hash", ] @@ -5383,7 +5525,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28f53e8b192565862cf99343194579a022eb9c7dd3a8d03134734803c7b3125" dependencies = [ - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", "syn 1.0.95", ] @@ -5437,8 +5579,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", "version_check", ] @@ -5449,8 +5591,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "version_check", ] @@ -5460,9 +5602,9 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70550716265d1ec349c41f70dd4f964b4fd88394efe4405f0c1da679c4799a07" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -5476,9 +5618,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -5490,7 +5632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c2f43e8969d51935d2a7284878ae053ba30034cd563f673cde37ba5205685e" dependencies = [ "dtoa", - "itoa 1.0.1", + "itoa 1.0.10", "parking_lot 0.12.0", "prometheus-client-derive-encode", ] @@ -5501,16 +5643,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b6a5217beb0ad503ee7fa752d451c905113d70721b937126158f3106a48cc1" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] [[package]] name = "prost" -version = "0.10.3" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc03e116981ff7d8da8e5c220e374587b98d294af7ba7dd7fda761158f00086f" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes 1.4.0", "prost-derive", @@ -5518,46 +5660,45 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.10.4" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae5a4388762d5815a9fc0dea33c56b021cdc8dde0c55e0c9ca57197254b0cab" +checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" dependencies = [ "bytes 1.4.0", - "cfg-if 1.0.0", - "cmake", "heck", "itertools", "lazy_static", "log", "multimap", "petgraph", + "prettyplease", "prost", "prost-types", "regex", + "syn 1.0.95", "tempfile", "which", ] [[package]] name = "prost-derive" -version = "0.10.1" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", "itertools", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] [[package]] name = "prost-types" -version = "0.10.1" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" dependencies = [ - "bytes 1.4.0", "prost", ] @@ -5629,7 +5770,7 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "asynchronous-codec", "bytes 1.4.0", @@ -5666,11 +5807,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", ] [[package]] @@ -5742,7 +5883,7 @@ checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", "rand_hc 0.3.1", ] @@ -5773,7 +5914,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -5802,9 +5943,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.9", ] @@ -5833,7 +5974,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -5867,6 +6008,7 @@ dependencies = [ "libc", "rand_core 0.4.2", "rdrand", + "wasm-bindgen", "winapi", ] @@ -5939,8 +6081,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", - "ring", - "time 0.3.11", + "ring 0.16.20", + "time 0.3.20", "yasna", ] @@ -6004,8 +6146,8 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c523ccaed8ac4b0288948849a350b37d3035827413c458b6a40ddb614bb4f72" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -6038,10 +6180,10 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 0.2.7", + "http 0.2.12", "http-body 0.4.5", "hyper", - "hyper-rustls", + "hyper-rustls 0.23.0", "ipnet", "js-sys", "lazy_static", @@ -6055,7 +6197,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls", + "tokio-rustls 0.23.2", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -6076,13 +6218,12 @@ dependencies = [ [[package]] name = "rfc6979" -version = "0.1.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "crypto-bigint", - "hmac 0.11.0", - "zeroize", + "hmac 0.12.1", + "subtle", ] [[package]] @@ -6094,12 +6235,35 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +dependencies = [ + "cc", + "getrandom 0.2.9", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "ripemd160" version = "0.9.1" @@ -6108,7 +6272,7 @@ checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -6222,7 +6386,7 @@ dependencies = [ "hashlink", "libsqlite3-sys", "smallvec 1.6.1", - "time 0.3.11", + "time 0.3.20", ] [[package]] @@ -6309,27 +6473,26 @@ dependencies = [ [[package]] name = "rustls" -version = "0.19.1" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" dependencies = [ - "base64 0.13.0", "log", - "ring", - "sct 0.6.0", - "webpki 0.21.3", + "ring 0.16.20", + "sct", + "webpki", ] [[package]] name = "rustls" -version = "0.20.4" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring", - "sct 0.7.0", - "webpki 0.22.0", + "ring 0.17.3", + "rustls-webpki 0.101.7", + "sct", ] [[package]] @@ -6359,7 +6522,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.2", + "base64 0.21.7", ] [[package]] @@ -6368,20 +6531,30 @@ version = "0.100.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.3", + "untrusted 0.9.0", ] [[package]] name = "rustversion" -version = "1.0.6" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rw-stream-sink" version = "0.4.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.2#15dd3a3c32b1d9b393cbb108f479675cacf71b99" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" dependencies = [ "futures 0.3.28", "pin-project", @@ -6403,6 +6576,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "satomic-swap" +version = "0.1.0" +source = "git+https://github.com/KomodoPlatform/satomic-swap.git?rev=413e472#413e4725a97f2c4d5d34101b3d2c49009c95cb28" +dependencies = [ + "solana-program", +] + [[package]] name = "scale-info" version = "2.1.2" @@ -6422,8 +6603,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e334bb10a245e28e5fd755cabcafd96cfcd167c99ae63a46924ca8d8703a3c" dependencies = [ "proc-macro-crate 1.1.3", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -6468,36 +6649,27 @@ dependencies = [ "serialization", ] -[[package]] -name = "sct" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sct" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] name = "sec1" -version = "0.2.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ + "base16ct", "der", - "generic-array 0.14.5", + "generic-array", "pkcs8", - "subtle 2.4.0", + "subtle", "zeroize", ] @@ -6615,17 +6787,17 @@ dependencies = [ name = "ser_error_derive" version = "0.1.0" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "ser_error", "syn 1.0.95", ] [[package]] name = "serde" -version = "1.0.164" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] @@ -6652,13 +6824,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.164" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -6667,8 +6839,8 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ - "indexmap", - "itoa 1.0.1", + "indexmap 1.9.3", + "itoa 1.0.10", "ryu", "serde", ] @@ -6679,30 +6851,52 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] [[package]] name = "serde_urlencoded" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 0.4.6", + "itoa 1.0.10", "ryu", "serde", ] +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 1.0.95", +] + [[package]] name = "serde_yaml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" dependencies = [ - "indexmap", + "indexmap 1.9.3", "ryu", "serde", "yaml-rust", @@ -6735,21 +6929,20 @@ checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.2.1", + "cpufeatures 0.2.11", "digest 0.9.0", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] -name = "sha2" -version = "0.8.2" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", + "cfg-if 1.0.0", + "cpufeatures 0.2.11", + "digest 0.10.7", ] [[package]] @@ -6760,9 +6953,9 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.2.1", + "cpufeatures 0.2.11", "digest 0.9.0", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -6772,7 +6965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if 1.0.0", - "cpufeatures 0.2.1", + "cpufeatures 0.2.11", "digest 0.10.7", ] @@ -6785,7 +6978,7 @@ dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", "keccak", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -6819,9 +7012,15 @@ name = "signature" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.9.0", - "rand_core 0.6.3", + "digest 0.10.7", + "rand_core 0.6.4", ] [[package]] @@ -6891,11 +7090,11 @@ dependencies = [ "blake2", "chacha20poly1305", "curve25519-dalek 4.0.0-rc.1", - "rand_core 0.6.3", - "ring", + "rand_core 0.6.4", + "ring 0.16.20", "rustc_version 0.4.0", "sha2 0.10.7", - "subtle 2.4.0", + "subtle", ] [[package]] @@ -7144,7 +7343,7 @@ checksum = "a5f69a79200f5ba439eb8b790c5e00beab4d1fae4da69ce023c69c6ac1b55bf1" dependencies = [ "bs58 0.4.0", "bv", - "generic-array 0.14.5", + "generic-array", "log", "memmap2", "rustc_version 0.4.0", @@ -7162,8 +7361,8 @@ version = "1.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402fffb54bf5d335e6df26fc1719feecfbd7a22fafdf6649fe78380de3c47384" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "rustc_version 0.4.0", "syn 1.0.95", ] @@ -7425,7 +7624,7 @@ dependencies = [ "digest 0.9.0", "ed25519-dalek", "ed25519-dalek-bip32 0.1.1", - "generic-array 0.14.5", + "generic-array", "hmac 0.11.0", "itertools", "js-sys", @@ -7464,8 +7663,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c834b4e02ac911b13c13aed08b3f847e722f6be79d31b1c660c1dbd2dee83cdb" dependencies = [ "bs58 0.4.0", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "rustversion", "syn 1.0.95", ] @@ -7588,8 +7787,8 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d676664972e22a0796176e81e7bec41df461d1edf52090955cdab55f2c956ff2" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -7618,8 +7817,8 @@ checksum = "22ecb916b9664ed9f90abef0ff5a3e61454c1efea5861b2997e03f39b59b955f" dependencies = [ "Inflector", "proc-macro-crate 1.1.3", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", ] @@ -7686,11 +7885,17 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" -version = "0.5.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -7745,7 +7950,7 @@ dependencies = [ "serde", "serde_json", "serialization", - "sha2 0.9.9", + "sha2 0.10.7", "test_helpers", ] @@ -7766,8 +7971,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f9799e6d412271cb2414597581128b03f3285f260ea49f5363d07df6a332b3e" dependencies = [ "Inflector", - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "serde", "serde_json", "unicode-xid 0.2.0", @@ -7792,10 +7997,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] -name = "subtle" -version = "1.0.0" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" @@ -7812,6 +8017,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + [[package]] name = "symlink" version = "0.1.0" @@ -7846,19 +8057,19 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.23" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "unicode-ident", ] @@ -7883,8 +8094,8 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", "unicode-xid 0.2.0", ] @@ -7927,99 +8138,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "tc_cli_client" -version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "log", - "serde", - "serde_derive", - "serde_json", - "tc_core", -] - -[[package]] -name = "tc_coblox_bitcoincore" -version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "hex 0.3.2", - "hmac 0.7.1", - "log", - "rand 0.7.3", - "sha2 0.8.2", - "tc_core", -] - -[[package]] -name = "tc_core" -version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "debug_stub_derive", - "log", -] - -[[package]] -name = "tc_dynamodb_local" -version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "log", - "tc_core", -] - -[[package]] -name = "tc_elasticmq" -version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "tc_core", -] - -[[package]] -name = "tc_generic" -version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "tc_core", -] - -[[package]] -name = "tc_parity_parity" -version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "log", - "tc_core", -] - -[[package]] -name = "tc_postgres" -version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "log", - "tc_core", -] - -[[package]] -name = "tc_redis" -version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "tc_core", -] - -[[package]] -name = "tc_trufflesuite_ganachecli" -version = "0.4.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" -dependencies = [ - "tc_core", -] - [[package]] name = "tempfile" version = "3.4.0" @@ -8035,14 +8153,14 @@ dependencies = [ [[package]] name = "tendermint" -version = "0.23.7" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca881fa4dedd2b46334f13be7fbc8cc1549ba4be5a833fe4e73d1a1baaf7949" +checksum = "3f0a7d05cf78524782337f8edd55cbc578d159a16ad4affe2135c92f7dbac7f0" dependencies = [ - "async-trait", "bytes 1.4.0", - "ed25519", - "ed25519-dalek", + "digest 0.10.7", + "ed25519 2.2.3", + "ed25519-consensus", "flex-error", "futures 0.3.28", "k256", @@ -8050,25 +8168,25 @@ dependencies = [ "once_cell", "prost", "prost-types", - "ripemd160", + "ripemd", "serde", "serde_bytes", "serde_json", "serde_repr", - "sha2 0.9.9", - "signature", - "subtle 2.4.0", + "sha2 0.10.7", + "signature 2.2.0", + "subtle", "subtle-encoding", "tendermint-proto", - "time 0.3.11", + "time 0.3.20", "zeroize", ] [[package]] name = "tendermint-config" -version = "0.23.7" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c56ee93f4e9b7e7daba86d171f44572e91b741084384d0ae00df7991873dfd" +checksum = "71a72dbbea6dde12045d261f2c70c0de039125675e8a026c8d5ad34522756372" dependencies = [ "flex-error", "serde", @@ -8080,9 +8198,9 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.23.7" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71f925d74903f4abbdc4af0110635a307b3cb05b175fdff4a7247c14a4d0874" +checksum = "c0cec054567d16d85e8c3f6a3139963d1a66d9d3051ed545d31562550e9bcc3d" dependencies = [ "bytes 1.4.0", "flex-error", @@ -8093,29 +8211,32 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.11", + "time 0.3.20", ] [[package]] name = "tendermint-rpc" -version = "0.23.7" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13e63f57ee05a1e927887191c76d1b139de9fa40c180b9f8727ee44377242a6" +checksum = "d119d83a130537fc4a98c3c9eb6899ebe857fea4860400a61675bfb5f0b35129" dependencies = [ + "async-trait", "bytes 1.4.0", "flex-error", "getrandom 0.2.9", "peg", "pin-project", + "semver 1.0.6", "serde", "serde_bytes", "serde_json", + "subtle", "subtle-encoding", "tendermint", "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.11", + "time 0.3.20", "url", "uuid 0.8.2", "walkdir", @@ -8144,24 +8265,24 @@ dependencies = [ name = "test_helpers" version = "0.1.0" dependencies = [ - "hex 0.4.3", + "hex", ] [[package]] name = "testcontainers" -version = "0.7.0" -source = "git+https://github.com/KomodoPlatform/mm2-testcontainers-rs.git#65e738093488f1b37185af0f1d3cf0ffd23e840d" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" dependencies = [ - "tc_cli_client", - "tc_coblox_bitcoincore", - "tc_core", - "tc_dynamodb_local", - "tc_elasticmq", - "tc_generic", - "tc_parity_parity", - "tc_postgres", - "tc_redis", - "tc_trufflesuite_ganachecli", + "bollard-stubs", + "futures 0.3.28", + "hex", + "hmac 0.12.1", + "log", + "rand 0.8.4", + "serde", + "serde_json", + "sha2 0.10.7", ] [[package]] @@ -8188,9 +8309,9 @@ version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -8205,21 +8326,31 @@ dependencies = [ [[package]] name = "time" -version = "0.3.11" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ - "itoa 1.0.1", - "libc", - "num_threads", + "itoa 1.0.10", + "js-sys", + "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] [[package]] name = "tiny-bip39" @@ -8318,9 +8449,9 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -8331,7 +8462,17 @@ checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" dependencies = [ "rustls 0.20.4", "tokio", - "webpki 0.22.0", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.10", + "tokio", ] [[package]] @@ -8356,9 +8497,9 @@ dependencies = [ "rustls 0.20.4", "rustls-native-certs", "tokio", - "tokio-rustls", + "tokio-rustls 0.23.2", "tungstenite", - "webpki 0.22.0", + "webpki", ] [[package]] @@ -8368,7 +8509,7 @@ source = "git+https://github.com/KomodoPlatform/tokio-tungstenite-wasm?rev=d20ab dependencies = [ "futures-channel", "futures-util", - "http 0.2.7", + "http 0.2.12", "httparse", "js-sys", "thiserror", @@ -8403,50 +8544,47 @@ dependencies = [ [[package]] name = "tonic" -version = "0.7.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be9d60db39854b30b835107500cf0aca0b0d14d6e1c3de124217c23a29c2ddb" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.13.0", + "base64 0.21.7", "bytes 1.4.0", "flate2", "futures-core", "futures-util", "h2", - "http 0.2.7", + "http 0.2.12", "http-body 0.4.5", "hyper", "hyper-timeout", "percent-encoding", "pin-project", "prost", - "prost-derive", "rustls-pemfile 1.0.2", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-stream", - "tokio-util", "tower", "tower-layer", "tower-service", "tracing", - "tracing-futures", - "webpki-roots 0.22.3", + "webpki-roots 0.23.1", ] [[package]] name = "tonic-build" -version = "0.7.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9263bf4c9bfaae7317c1c2faf7f18491d2fe476f70c414b73bf5d445b00ffa1" +checksum = "a6fdaae4c2c638bb70fe42803a26fbd6fc6ac8c72f5c59f67ecc2a2dcabf4b07" dependencies = [ "prettyplease", - "proc-macro2 1.0.63", + "proc-macro2 1.0.69", "prost-build", - "quote 1.0.28", + "quote 1.0.33", "syn 1.0.95", ] @@ -8458,7 +8596,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 1.9.3", "pin-project", "pin-project-lite 0.2.9", "rand 0.8.4", @@ -8470,30 +8608,11 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-http" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d342c6d58709c0a6d48d48dabbb62d4ef955cf5f0f3bbfd845838e7ae88dbae" -dependencies = [ - "bitflags", - "bytes 1.4.0", - "futures-core", - "futures-util", - "http 0.2.7", - "http-body 0.4.5", - "http-range-header", - "pin-project-lite 0.2.9", - "tower", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" @@ -8508,7 +8627,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", - "log", "pin-project-lite 0.2.9", "tracing-attributes", "tracing-core", @@ -8520,9 +8638,9 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", ] [[package]] @@ -8534,28 +8652,23 @@ dependencies = [ "once_cell", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "trezor" version = "0.1.1" dependencies = [ + "async-std", "async-trait", "bip32", "byteorder", "common", "derive_more", + "ethcore-transaction", + "ethereum-types", + "ethkey", "futures 0.3.28", "hw_common", "js-sys", + "lazy_static", "mm2_err_handle", "prost", "rand 0.7.3", @@ -8659,7 +8772,7 @@ dependencies = [ "base64 0.13.0", "byteorder", "bytes 1.4.0", - "http 0.2.7", + "http 0.2.12", "httparse", "log", "rand 0.8.4", @@ -8668,7 +8781,7 @@ dependencies = [ "thiserror", "url", "utf-8", - "webpki 0.22.0", + "webpki", "webpki-roots 0.22.3", ] @@ -8686,14 +8799,14 @@ checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0" dependencies = [ "byteorder", "crunchy", - "hex 0.4.3", + "hex", "static_assertions", ] [[package]] name = "unexpected" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" [[package]] name = "unicode-bidi" @@ -8749,8 +8862,8 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" dependencies = [ - "generic-array 0.14.5", - "subtle 2.4.0", + "generic-array", + "subtle", ] [[package]] @@ -8769,6 +8882,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "uriparse" version = "0.6.3" @@ -8807,7 +8926,7 @@ dependencies = [ "common", "crypto", "derive_more", - "hex 0.4.3", + "hex", "keys", "mm2_err_handle", "primitives", @@ -8863,6 +8982,56 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "wagyu-zcash-parameters" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c904628658374e651288f000934c33ef738b2d8b3e65d4100b70b395dbe2bb" +dependencies = [ + "wagyu-zcash-parameters-1", + "wagyu-zcash-parameters-2", + "wagyu-zcash-parameters-3", + "wagyu-zcash-parameters-4", + "wagyu-zcash-parameters-5", + "wagyu-zcash-parameters-6", +] + +[[package]] +name = "wagyu-zcash-parameters-1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bf2e21bb027d3f8428c60d6a720b54a08bf6ce4e6f834ef8e0d38bb5695da8" + +[[package]] +name = "wagyu-zcash-parameters-2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616ab2e51e74cc48995d476e94de810fb16fc73815f390bf2941b046cc9ba2c" + +[[package]] +name = "wagyu-zcash-parameters-3" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14da1e2e958ff93c0830ee68e91884069253bf3462a67831b02b367be75d6147" + +[[package]] +name = "wagyu-zcash-parameters-4" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f058aeef03a2070e8666ffb5d1057d8bb10313b204a254a6e6103eb958e9a6d6" + +[[package]] +name = "wagyu-zcash-parameters-5" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffe916b30e608c032ae1b734f02574a3e12ec19ab5cc5562208d679efe4969d" + +[[package]] +name = "wagyu-zcash-parameters-6" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b6d5a78adc3e8f198e9cd730f219a695431467f7ec29dcfc63ade885feebe1" + [[package]] name = "waker-fn" version = "1.1.0" @@ -8921,9 +9090,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -8945,7 +9114,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote 1.0.28", + "quote 1.0.33", "wasm-bindgen-macro-support", ] @@ -8955,9 +9124,9 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", - "syn 2.0.23", + "proc-macro2 1.0.69", + "quote 1.0.33", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8988,8 +9157,8 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c2e18093f11c19ca4e188c177fecc7c372304c311189f12c2f9bea5b7324ac7" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", ] [[package]] @@ -9005,16 +9174,19 @@ dependencies = [ [[package]] name = "web3" version = "0.19.0" -source = "git+https://github.com/KomodoPlatform/rust-web3?tag=v0.19.0#ec5e72a5c95e3935ea0c9ab77b501e3926686fa9" +source = "git+https://github.com/KomodoPlatform/rust-web3?tag=v0.20.0#01de1d732e61c920cfb2fb1533db7d7110c8a457" dependencies = [ "arrayvec 0.7.1", + "base64 0.13.0", + "bytes 1.4.0", "derive_more", "ethabi", "ethereum-types", "futures 0.3.28", "futures-timer", "getrandom 0.2.9", - "hex 0.4.3", + "headers", + "hex", "idna", "js-sys", "jsonrpc-core", @@ -9022,33 +9194,25 @@ dependencies = [ "parking_lot 0.12.0", "pin-project", "rand 0.8.4", + "reqwest", "rlp", "serde", "serde-wasm-bindgen", "serde_json", "tiny-keccak 2.0.2", + "url", "wasm-bindgen", "wasm-bindgen-futures", ] -[[package]] -name = "webpki" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -9057,7 +9221,7 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" dependencies = [ - "webpki 0.22.0", + "webpki", ] [[package]] @@ -9066,9 +9230,15 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "rustls-webpki", + "rustls-webpki 0.100.1", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "wepoll-sys-stjepang" version = "1.0.6" @@ -9448,7 +9618,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.11", + "time 0.3.20", ] [[package]] @@ -9460,23 +9630,24 @@ checksum = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f" [[package]] name = "zcash_client_backend" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" dependencies = [ + "async-trait", "base64 0.13.0", "bech32", "bls12_381", "bs58 0.4.0", "ff 0.8.0", "group 0.8.0", - "hex 0.4.3", + "hex", "jubjub", "nom", "percent-encoding", "protobuf", "protobuf-codegen-pure", "rand_core 0.5.1", - "subtle 2.4.0", - "time 0.3.11", + "subtle", + "time 0.3.20", "zcash_note_encryption", "zcash_primitives", ] @@ -9484,8 +9655,9 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" dependencies = [ + "async-trait", "bech32", "bs58 0.4.0", "ff 0.8.0", @@ -9495,7 +9667,25 @@ dependencies = [ "protobuf", "rand_core 0.5.1", "rusqlite", - "time 0.3.11", + "time 0.3.20", + "tokio", + "zcash_client_backend", + "zcash_extras", + "zcash_primitives", +] + +[[package]] +name = "zcash_extras" +version = "0.1.0" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" +dependencies = [ + "async-trait", + "ff 0.8.0", + "group 0.8.0", + "jubjub", + "protobuf", + "rand_core 0.5.1", + "time 0.3.20", "zcash_client_backend", "zcash_primitives", ] @@ -9503,7 +9693,7 @@ dependencies = [ [[package]] name = "zcash_note_encryption" version = "0.0.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" dependencies = [ "blake2b_simd", "byteorder", @@ -9511,15 +9701,15 @@ dependencies = [ "ff 0.8.0", "group 0.8.0", "rand_core 0.5.1", - "subtle 2.4.0", + "subtle", ] [[package]] name = "zcash_primitives" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" dependencies = [ - "aes", + "aes 0.8.3", "bitvec 0.18.5", "blake2b_simd", "blake2s_simd", @@ -9531,7 +9721,7 @@ dependencies = [ "fpe", "funty 1.1.0", "group 0.8.0", - "hex 0.4.3", + "hex", "jubjub", "lazy_static", "log", @@ -9540,14 +9730,14 @@ dependencies = [ "ripemd160", "secp256k1 0.20.3", "sha2 0.9.9", - "subtle 2.4.0", + "subtle", "zcash_note_encryption", ] [[package]] name = "zcash_proofs" version = "0.5.0" -source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.3.0#443fef0cf301b04375f76128e7436b4de02d1c4d" +source = "git+https://github.com/KomodoPlatform/librustzcash.git?tag=k-1.4.1#e92443a7bbd1c5e92e00e6deb45b5a33af14cea4" dependencies = [ "bellman", "blake2b_simd", @@ -9564,9 +9754,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -9577,8 +9767,8 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ - "proc-macro2 1.0.63", - "quote 1.0.28", + "proc-macro2 1.0.69", + "quote 1.0.33", "syn 1.0.95", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index ac0439b17b..4de3f093e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "mm2src/common/shared_ref_counter", "mm2src/crypto", "mm2src/db_common", - "mm2src/derives/enum_from", + "mm2src/derives/enum_derives", "mm2src/derives/ser_error_derive", "mm2src/derives/ser_error", "mm2src/hw_common", @@ -36,7 +36,6 @@ members = [ "mm2src/mm2_p2p", "mm2src/mm2_rpc", "mm2src/mm2_state_machine", - "mm2src/mm2_test_helpers", "mm2src/rpc_task", "mm2src/trezor", ] @@ -46,6 +45,7 @@ exclude = [ "mm2src/floodsub", "mm2src/gossipsub", "mm2src/mm2_libp2p", + "mm2src/mm2_test_helpers", ] # https://doc.rust-lang.org/beta/cargo/reference/features.html#feature-resolver-version-2 @@ -70,4 +70,7 @@ debug = 1 debug-assertions = false panic = 'unwind' incremental = true -codegen-units = 256 \ No newline at end of file +codegen-units = 256 + +[profile.release.package.mocktopus] +opt-level = 1 # TODO: MIR fails on optimizing this dependency, remove that.. \ No newline at end of file diff --git a/README.md b/README.md index b22bdb5856..ff05f9d501 100755 --- a/README.md +++ b/README.md @@ -64,13 +64,6 @@ For a curated list of Komodo DeFi Framework based projects and resources, check - Buy/sell from the orderbook, or create maker orders - Configure automated ["makerbot" trading](https://developers.komodoplatform.com/basic-docs/atomicdex-api-20/start_simple_market_maker_bot.html) with periodic price updates and optional [telegram](https://telegram.org/) alerts - -## System Requirements - -- 64-bit MacOS, Windows, or Linux operating system -- 2GB of free RAM (or more) -- User account with admin/root privileges - ## Building from source ### On Host System: @@ -96,13 +89,13 @@ If you want to build from source without installing prerequisites to your host s Build the image: ```sh -docker build -t mm2-build-container -f .docker/Dockerfile . +docker build -t kdf-build-container -f .docker/Dockerfile . ``` Bind source code into container and compile it: ```sh -docker run -v "$(pwd)":/app -w /app mm2-build-container cargo build +docker run -v "$(pwd)":/app -w /app kdf-build-container cargo build ``` Just like building it on your host system, you will now have the target directory containing the build files. @@ -129,11 +122,11 @@ For example: The coins file contains information about the coins and tokens you want to trade. A regularly updated version is maintained in the [Komodo Platform coins repository](https://github.com/KomodoPlatform/coins/blob/master/coins). Pull Requests to add any coins not yet included are welcome. -To facilitate interoperability with the `mm2` service, there is the `adex-cli` command line utility. It provides a questionnaire initialization mode to set up the configuration and obtain the proper coin set through the internet. It can also be used to start or stop the service. +To facilitate interoperability with the `kdf` service, there is the `adex-cli` command line utility. It provides a questionnaire initialization mode to set up the configuration and obtain the proper coin set through the internet. It can also be used to start or stop the service. ## Usage -To launch the Komodo DeFi Framework, run `./mm2` (or `mm2.exe` in Windows) +To launch the Komodo DeFi Framework, run `./kdf` (or `kdf.exe` in Windows) To activate a coin: ```bash @@ -192,7 +185,7 @@ Refer to the [Komodo Developer Docs](https://developers.komodoplatform.com/basic ## Disclaimer -This repository contains the `work in progress` code of the brand new Komodo DeFi Framework (mm2) built mainly on Rust. +This repository contains the `work in progress` code of the brand new Komodo DeFi Framework (kdf) built mainly on Rust. The current state can be considered as a alpha version. **WARNING: Use with test coins only or with assets which value does not exceed an amount you are willing to lose. This is alpha stage software! ** diff --git a/docs/ANDROID_CROSS_ON_M1_MAC.md b/docs/ANDROID_CROSS_ON_M1_MAC.md index 666e7be92c..1233c05588 100644 --- a/docs/ANDROID_CROSS_ON_M1_MAC.md +++ b/docs/ANDROID_CROSS_ON_M1_MAC.md @@ -1,4 +1,4 @@ -## Cross-compiling MM for Android on M1 Mac +## Cross-compiling MM for Android on Apple Silicon 1. Ensure that your terminal is added to `Developer tools` in MacOS Security & Privacy settings. 2. The cross-compilation requires Android NDK version 21. Custom brew cask file is located at the root of this repo. diff --git a/docs/DEV_ENVIRONMENT.md b/docs/DEV_ENVIRONMENT.md index f6185b7249..5e4f6d1659 100644 --- a/docs/DEV_ENVIRONMENT.md +++ b/docs/DEV_ENVIRONMENT.md @@ -36,7 +36,7 @@ ``` sudo ln -s $(which podman) /usr/bin/docker ``` -9. Try `cargo test --features "native run-docker-tests" --all -- --test-threads=16`. +9. Try `cargo test --all --features run-docker-tests -- --test-threads=16`. ## Running WASM tests @@ -61,12 +61,12 @@ ``` CC=/usr/local/opt/llvm/bin/clang AR=/usr/local/opt/llvm/bin/llvm-ar wasm-pack test --firefox --headless mm2src/mm2_main ``` - - for OSX users (M1): + - for OSX users (Apple Silicon): ``` CC=/opt/homebrew/opt/llvm/bin/clang AR=/opt/homebrew/opt/llvm/bin/llvm-ar wasm-pack test --firefox --headless mm2src/mm2_main ``` Please note `CC` and `AR` must be specified in the same line as `wasm-pack test mm2src/mm2_main`. -#### Running specific WASM tests with Cargo
+#### Running specific WASM tests with Cargo
- Install `wasm-bindgen-cli`:
Make sure you have wasm-bindgen-cli installed with a version that matches the one specified in your Cargo.toml file. You can install it using Cargo with the following command: diff --git a/docs/WASM_BUILD.md b/docs/WASM_BUILD.md index 13d568dffc..eb62fa7731 100644 --- a/docs/WASM_BUILD.md +++ b/docs/WASM_BUILD.md @@ -25,7 +25,7 @@ To build WASM release binary run one of the following commands according to your ``` CC=/usr/local/opt/llvm/bin/clang AR=/usr/local/opt/llvm/bin/llvm-ar wasm-pack build mm2src/mm2_bin_lib --target web --out-dir wasm_build/deps/pkg/ ``` -- for OSX users (M1): +- for OSX users (Apple Silicon): ``` CC=/opt/homebrew/opt/llvm/bin/clang AR=/opt/homebrew/opt/llvm/bin/llvm-ar wasm-pack build mm2src/mm2_bin_lib --target web --out-dir wasm_build/deps/pkg/ ``` diff --git a/mm2src/adex_cli/Cargo.lock b/mm2src/adex_cli/Cargo.lock index 8aed234b0b..4de83cbd97 100644 --- a/mm2src/adex_cli/Cargo.lock +++ b/mm2src/adex_cli/Cargo.lock @@ -34,7 +34,7 @@ dependencies = [ "mm2_rpc", "passwords", "rpc", - "rustls 0.20.4", + "rustls", "serde", "serde_json", "sysinfo", @@ -158,6 +158,125 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.0", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite 1.13.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg 1.1.0", + "cfg-if 1.0.0", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling", + "rustix", + "slab", + "socket2", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-process" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +dependencies = [ + "async-io", + "async-lock", + "autocfg 1.1.0", + "blocking", + "cfg-if 1.0.0", + "event-listener", + "futures-lite 1.13.0", + "rustix", + "signal-hook", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.52" @@ -169,6 +288,12 @@ dependencies = [ "syn 1.0.95", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -218,24 +343,9 @@ checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" [[package]] name = "base64" -version = "0.10.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -dependencies = [ - "byteorder", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bech32" @@ -281,7 +391,7 @@ dependencies = [ "ripemd160", "serialization", "sha-1", - "sha2", + "sha2 0.10.8", "sha3", "siphasher", ] @@ -325,12 +435,37 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "block-padding" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "blocking" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand 2.1.0", + "futures-io", + "futures-lite 1.13.0", + "piper", + "tracing", +] + [[package]] name = "bumpalo" version = "3.12.0" @@ -367,9 +502,9 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.74" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" +checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" [[package]] name = "cfg-if" @@ -515,6 +650,7 @@ dependencies = [ "primitive-types", "rand 0.7.3", "regex", + "rustc-hash", "ser_error", "ser_error_derive", "serde", @@ -522,7 +658,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_repr", - "sha2", + "sha2 0.10.8", "tokio", "uuid", "wasm-bindgen", @@ -532,6 +668,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -633,13 +778,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" -dependencies = [ - "cfg-if 1.0.0", - "lazy_static", -] +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -672,6 +813,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-mac" version = "0.8.0" @@ -731,10 +882,13 @@ name = "db_common" version = "0.1.0" dependencies = [ "common", + "crossbeam-channel", + "futures 0.3.28", "hex", "log", "rusqlite", "sql-builder", + "tokio", "uuid", ] @@ -758,6 +912,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] + [[package]] name = "directories" version = "5.0.1" @@ -829,6 +993,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.2" @@ -880,7 +1050,7 @@ dependencies = [ [[package]] name = "ethkey" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" dependencies = [ "byteorder", "edit-distance", @@ -895,6 +1065,12 @@ dependencies = [ "tiny-keccak 1.4.4", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -907,6 +1083,21 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "findshlibs" version = "0.5.0" @@ -1003,25 +1194,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] -name = "futures-macro" -version = "0.3.28" +name = "futures-lite" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "proc-macro2", - "quote 1.0.27", - "syn 2.0.16", + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", ] [[package]] -name = "futures-rustls" -version = "0.21.1" +name = "futures-lite" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1387e07917c711fb4ee4f48ea0adb04a3c9739e53ef85bf43ae1edc2937a8b" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ + "fastrand 2.1.0", + "futures-core", "futures-io", - "rustls 0.19.1", - "webpki 0.21.4", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] @@ -1085,9 +1293,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if 1.0.0", "libc", @@ -1100,14 +1308,26 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "groestl" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2432787a9b8f0d58dca43fe2240399479b7582dc8afa2126dc7652b864029e47" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "opaque-debug", ] @@ -1123,9 +1343,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.19" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes 1.4.0", "fnv", @@ -1133,7 +1353,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.9", - "indexmap", + "indexmap 2.0.1", "slab", "tokio", "tokio-util", @@ -1208,7 +1428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" dependencies = [ "crypto-mac", - "digest", + "digest 0.9.0", ] [[package]] @@ -1309,14 +1529,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ + "futures-util", "http 0.2.9", "hyper", "log", - "rustls 0.20.4", + "rustls", "rustls-native-certs", "tokio", "tokio-rustls", @@ -1395,6 +1616,16 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "inquire" version = "0.6.2" @@ -1517,6 +1748,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1525,9 +1765,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libsqlite3-sys" @@ -1575,11 +1815,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" dependencies = [ - "cfg-if 1.0.0", + "value-bag", ] [[package]] @@ -1600,7 +1840,7 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "mem" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" [[package]] name = "memchr" @@ -1634,9 +1874,9 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" dependencies = [ - "base64 0.21.2", + "base64", "hyper", - "indexmap", + "indexmap 1.9.3", "ipnet", "metrics", "metrics-util", @@ -1667,7 +1907,7 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "hashbrown 0.13.1", - "indexmap", + "indexmap 1.9.3", "metrics", "num_cpus", "ordered-float", @@ -1702,25 +1942,32 @@ name = "mm2_core" version = "0.1.0" dependencies = [ "arrayref", + "async-std", "async-trait", "cfg-if 1.0.0", "common", "db_common", "derive_more", "futures 0.3.28", - "futures-rustls", "gstuff", "hex", + "instant", "lazy_static", + "mm2_err_handle", "mm2_event_stream", "mm2_metrics", "mm2_rpc", "primitives", "rand 0.7.3", + "rustls", + "ser_error", + "ser_error_derive", "serde", "serde_json", "shared_ref_counter", + "tokio", "uuid", + "wasm-bindgen-test", ] [[package]] @@ -1756,7 +2003,7 @@ dependencies = [ name = "mm2_metrics" version = "0.1.0" dependencies = [ - "base64 0.10.1", + "base64", "common", "derive_more", "futures 0.3.28", @@ -1777,6 +2024,7 @@ name = "mm2_net" version = "0.1.0" dependencies = [ "async-trait", + "base64", "bytes 1.4.0", "cfg-if 1.0.0", "common", @@ -1786,19 +2034,25 @@ dependencies = [ "futures-util", "gstuff", "http 0.2.9", + "http-body 0.4.5", + "httparse", "hyper", "js-sys", "lazy_static", "mm2_core", "mm2_err_handle", "mm2_state_machine", + "pin-project", "prost", "rand 0.7.3", - "rustls 0.20.4", + "rustls", "serde", "serde_json", + "thiserror", "tokio", "tokio-rustls", + "tonic", + "tower-service", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -1993,6 +2247,12 @@ dependencies = [ "syn 1.0.95", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.0" @@ -2055,6 +2315,32 @@ dependencies = [ "crypto-mac", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote 1.0.27", + "syn 2.0.16", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -2067,12 +2353,39 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" +dependencies = [ + "atomic-waker", + "fastrand 2.1.0", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg 1.1.0", + "bitflags", + "cfg-if 1.0.0", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + [[package]] name = "portable-atomic" version = "1.3.2" @@ -2126,18 +2439,18 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.10.4" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes 1.4.0", "prost-derive", @@ -2145,9 +2458,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.10.1" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", "itertools", @@ -2313,7 +2626,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.14", ] [[package]] @@ -2365,6 +2678,7 @@ dependencies = [ "libc", "rand_core 0.4.2", "rdrand", + "wasm-bindgen", "winapi", ] @@ -2488,7 +2802,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.14", "redox_syscall 0.2.10", ] @@ -2518,20 +2832,35 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "getrandom 0.2.14", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + [[package]] name = "ripemd160" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "opaque-debug", ] @@ -2618,27 +2947,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" -dependencies = [ - "base64 0.13.1", - "log", - "ring", - "sct 0.6.1", - "webpki 0.21.4", -] - -[[package]] -name = "rustls" -version = "0.20.4" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring", - "sct 0.7.0", - "webpki 0.22.0", + "ring 0.17.8", + "rustls-webpki", + "sct", ] [[package]] @@ -2659,7 +2975,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] @@ -2709,24 +3035,14 @@ dependencies = [ "serialization", ] -[[package]] -name = "sct" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sct" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -2859,7 +3175,7 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ - "indexmap", + "indexmap 1.9.3", "itoa 1.0.1", "ryu", "serde", @@ -2900,10 +3216,10 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] @@ -2913,21 +3229,32 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha3" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.9.0", + "digest 0.9.0", "keccak", "opaque-debug", ] @@ -3018,6 +3345,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "sql-builder" version = "3.1.1" @@ -3167,7 +3500,7 @@ dependencies = [ "pbkdf2", "rand 0.7.3", "rustc-hash", - "sha2", + "sha2 0.9.9", "thiserror", "unicode-normalization", "wasm-bindgen", @@ -3248,13 +3581,23 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.20.4", + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", "tokio", - "webpki 0.22.0", ] [[package]] @@ -3280,6 +3623,34 @@ dependencies = [ "serde", ] +[[package]] +name = "tonic" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +dependencies = [ + "async-trait", + "base64", + "bytes 1.4.0", + "futures-core", + "futures-util", + "http 0.2.9", + "http-body 0.4.5", + "percent-encoding", + "pin-project", + "prost", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -3288,11 +3659,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.34" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if 1.0.0", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3300,22 +3670,22 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.20" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote 1.0.27", - "syn 1.0.95", + "syn 2.0.16", ] [[package]] name = "tracing-core" -version = "0.1.26" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] @@ -3381,6 +3751,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "utf8parse" version = "0.2.1" @@ -3393,11 +3769,17 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.14", "rand 0.8.5", "serde", ] +[[package]] +name = "value-bag" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" + [[package]] name = "vcpkg" version = "0.2.15" @@ -3410,6 +3792,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "want" version = "0.3.0" @@ -3532,34 +3920,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki 0.22.0", -] +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "winapi" @@ -3638,6 +4003,15 @@ dependencies = [ "windows-targets 0.48.1", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-targets" version = "0.42.1" @@ -3668,6 +4042,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3680,6 +4069,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -3698,6 +4093,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -3716,6 +4117,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -3734,6 +4141,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -3752,6 +4165,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3764,6 +4183,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -3782,6 +4207,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "wyz" version = "0.5.1" diff --git a/mm2src/adex_cli/Cargo.toml b/mm2src/adex_cli/Cargo.toml index b09916344e..d2b38a4cba 100644 --- a/mm2src/adex_cli/Cargo.toml +++ b/mm2src/adex_cli/Cargo.toml @@ -16,17 +16,17 @@ directories = "5.0" env_logger = "0.7.1" http = "0.2" hyper = { version = "0.14.26", features = ["client", "http2", "tcp"] } -hyper-rustls = "=0.23.0" +hyper-rustls = "0.24.0" gstuff = { version = "=0.7.4" , features = [ "nightly" ]} inquire = "0.6" itertools = "0.10" -log = "0.4" +log = "0.4.21" mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc"} passwords = "3.1" rpc = { path = "../mm2_bitcoin/rpc" } -rustls = { version = "=0.20.4", features = [ "dangerous_configuration" ] } +rustls = { version = "0.21", features = [ "dangerous_configuration" ] } serde = "1.0" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } sysinfo = "0.28" diff --git a/mm2src/adex_cli/src/rpc_data.rs b/mm2src/adex_cli/src/rpc_data.rs index f8e1329453..a3146cbe47 100644 --- a/mm2src/adex_cli/src/rpc_data.rs +++ b/mm2src/adex_cli/src/rpc_data.rs @@ -3,7 +3,7 @@ //! *Note: it's expected that the following data types will be moved to mm2_rpc::data when mm2 is refactored to be able to handle them* //! -use mm2_rpc::data::legacy::{ElectrumProtocol, GasStationPricePolicy, UtxoMergeParams}; +use mm2_rpc::data::legacy::{ElectrumProtocol, UtxoMergeParams}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] @@ -23,12 +23,6 @@ pub(crate) struct EnableRequest { #[serde(skip_serializing_if = "Option::is_none")] fallback_swap_contract: Option, #[serde(skip_serializing_if = "Option::is_none")] - gas_station_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - gas_station_decimals: Option, - #[serde(skip_serializing_if = "Option::is_none")] - gas_station_policy: Option, - #[serde(skip_serializing_if = "Option::is_none")] mm2: Option, #[serde(default)] tx_history: bool, diff --git a/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs b/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs index 36a864b5f8..7bc46126ba 100644 --- a/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs +++ b/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs @@ -347,10 +347,11 @@ pub(crate) fn get_status() { .filter(|line| line.contains("PID")) .last() { - let pid = found - .trim() + let chars = found.trim(); + + let pid = chars .matches(char::is_numeric) - .fold(String::default(), |mut pid, ch| { + .fold(String::with_capacity(chars.len()), |mut pid, ch| { pid.push_str(ch); pid }); diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index b2ccc8c227..1c1fe1637d 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -13,10 +13,16 @@ enable-solana = [ "dep:solana-sdk", "dep:solana-transaction-status", "dep:spl-token", - "dep:spl-associated-token-account" + "dep:spl-associated-token-account", + "dep:satomic-swap" +] +enable-sia = [ + "dep:reqwest", + "blake2b_simd" ] default = [] run-docker-tests = [] +for-tests = [] [lib] name = "coins" @@ -26,32 +32,33 @@ doctest = false [dependencies] async-std = { version = "1.5", features = ["unstable"] } async-trait = "0.1.52" -base64 = "0.10.0" +base64 = "0.21.2" base58 = "0.2.0" bip32 = { version = "0.2.2", default-features = false, features = ["alloc", "secp256k1-ffi"] } bitcoin_hashes = "0.11" bitcrypto = { path = "../mm2_bitcoin/crypto" } +blake2b_simd = { version = "0.5.10", optional = true } byteorder = "1.3" bytes = "0.4" cfg-if = "1.0" chain = { path = "../mm2_bitcoin/chain" } common = { path = "../common" } -cosmrs = { version = "0.7", default-features = false } +cosmrs = { version = "0.14.0", default-features = false } crossbeam = "0.8" crypto = { path = "../crypto" } db_common = { path = "../db_common" } derive_more = "0.99" ed25519-dalek = "1.0.1" -enum_from = { path = "../derives/enum_from" } +enum_derives = { path = "../derives/enum_derives" } ethabi = { version = "17.0.0" } -ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } +ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } +ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } # Waiting for https://github.com/rust-lang/rust/issues/54725 to use on Stable. #enum_dispatch = "0.1" -tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", features = ["rustls-tls-native-roots"]} futures01 = { version = "0.1", package = "futures" } futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } +futures-ticker = "0.0.3" # using select macro requires the crate to be named futures, compilation failed with futures03 name futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } group = "0.8.0" @@ -77,14 +84,16 @@ mocktopus = "0.8.0" num-traits = "0.2" parking_lot = { version = "0.12.0", features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } -prost = "0.10" +prost = "0.11" protobuf = "2.20" rand = { version = "0.7", features = ["std", "small_rng"] } regex = "1" +reqwest = { version = "0.11.9", default-features = false, features = ["json"], optional = true } rlp = { version = "0.5" } rmp-serde = "0.14.3" rpc = { path = "../mm2_bitcoin/rpc" } rpc_task = { path = "../rpc_task" } +satomic-swap = { git = "https://github.com/KomodoPlatform/satomic-swap.git", rev = "413e472", optional = true } script = { path = "../mm2_bitcoin/script" } secp256k1 = { version = "0.20" } ser_error = { path = "../derives/ser_error" } @@ -95,21 +104,21 @@ serde_json = { version = "1", features = ["preserve_order", "raw_value"] } serialization = { path = "../mm2_bitcoin/serialization" } serialization_derive = { path = "../mm2_bitcoin/serialization_derive" } spv_validation = { path = "../mm2_bitcoin/spv_validation" } -sha2 = "0.9" +sha2 = "0.10" sha3 = "0.9" utxo_signer = { path = "utxo_signer" } # using the same version as cosmrs -tendermint-rpc = { version = "=0.23.7", default-features = false } -tiny-bip39 = "0.8.0" +tendermint-rpc = { version = "0.32.0", default-features = false } +tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", features = ["rustls-tls-native-roots"]} url = { version = "2.2.2", features = ["serde"] } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } # One of web3 dependencies is the old `tokio-uds 0.1.7` which fails cross-compiling to ARM. # We don't need the default web3 features at all since we added our own web3 transport using shared HYPER instance. -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.19.0", default-features = false } +web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false } zbase32 = "0.1.2" -zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } -zcash_primitives = { features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } -zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } +zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } +zcash_extras = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } +zcash_primitives = {features = ["transparent-inputs"], git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } [target.'cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))'.dependencies] bincode = { version = "1.3.3", default-features = false, optional = true } @@ -121,14 +130,23 @@ spl-token = { version = "3", optional = true } spl-associated-token-account = { version = "1", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] +blake2b_simd = "0.5" +ff = "0.8" +futures-util = "0.3" +instant = "0.1.12" +jubjub = "0.5.1" js-sys = { version = "0.3.27" } mm2_db = { path = "../mm2_db" } mm2_metamask = { path = "../mm2_metamask" } mm2_test_helpers = { path = "../mm2_test_helpers" } +time = { version = "0.3.20", features = ["wasm-bindgen"] } +tonic = { version = "0.9", default-features = false, features = ["prost", "codegen", "gzip"] } +tower-service = "0.3" wasm-bindgen = "0.2.86" wasm-bindgen-futures = { version = "0.4.1" } wasm-bindgen-test = { version = "0.3.2" } web-sys = { version = "0.3.55", features = ["console", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Window"] } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1", default-features = false, features = ["local-prover"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dirs = { version = "1" } @@ -136,20 +154,21 @@ bitcoin = "0.29" hyper = { version = "0.14.26", features = ["client", "http2", "server", "tcp"] } # using webpki-tokio to avoid rejecting valid certificates # got "invalid certificate: UnknownIssuer" for https://ropsten.infura.io on iOS using default-features -hyper-rustls = { version = "0.23", default-features = false, features = ["http1", "http2", "webpki-tokio"] } +hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "http2", "webpki-tokio"] } +instant = { version = "0.1.12", features = ["wasm-bindgen"] } lightning = "0.0.113" lightning-background-processor = "0.0.113" lightning-invoice = { version = "0.21.0", features = ["serde"] } lightning-net-tokio = "0.0.113" rust-ini = { version = "0.13" } -rustls = { version = "0.20", features = ["dangerous_configuration"] } +rustls = { version = "0.21", features = ["dangerous_configuration"] } secp256k1v24 = { version = "0.24", package = "secp256k1" } -tendermint-config = { version = "0.23.7", default-features = false } tokio = { version = "1.20" } -tokio-rustls = { version = "0.23" } -tonic = { version = "0.7", features = ["tls", "tls-webpki-roots", "compression"] } -webpki-roots = { version = "0.22" } -zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.3.0" } +tokio-rustls = { version = "0.24" } +tonic = { version = "0.9", features = ["tls", "tls-webpki-roots", "gzip"] } +webpki-roots = { version = "0.25" } +zcash_client_sqlite = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } +zcash_proofs = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1", default-features = false, features = ["local-prover", "multicore"] } [target.'cfg(windows)'.dependencies] winapi = "0.3" @@ -157,6 +176,9 @@ winapi = "0.3" [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wagyu-zcash-parameters = { version = "0.2" } + [build-dependencies] -prost-build = { version = "0.10.4", default-features = false } -tonic-build = { version = "0.7", features = ["prost", "compression"] } +prost-build = { version = "0.11", default-features = false } +tonic-build = { version = "0.9", default-features = false, features = ["prost"] } diff --git a/mm2src/coins/coin_balance.rs b/mm2src/coins/coin_balance.rs index 8dca163419..f3ba916c33 100644 --- a/mm2src/coins/coin_balance.rs +++ b/mm2src/coins/coin_balance.rs @@ -1,24 +1,26 @@ -use crate::hd_pubkey::HDXPubExtractor; -use crate::hd_wallet::{HDAccountOps, HDAddressId, HDWalletCoinOps, HDWalletOps, NewAccountCreatingError, - NewAddressDerivingError}; -use crate::{BalanceError, BalanceResult, CoinBalance, CoinWithDerivationMethod, DerivationMethod, HDAddress, - MarketCoinOps}; +use crate::hd_wallet::{HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, HDCoinHDAccount, + HDPathAccountToAddressId, HDWalletCoinOps, HDWalletOps, HDXPubExtractor, + NewAccountCreationError, NewAddressDerivingError}; +use crate::{BalanceError, BalanceResult, CoinBalance, CoinBalanceMap, CoinWithDerivationMethod, DerivationMethod, + IguanaBalanceOps, MarketCoinOps}; use async_trait::async_trait; use common::log::{debug, info}; use crypto::{Bip44Chain, RpcDerivationPath}; -use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; #[cfg(test)] use mocktopus::macros::*; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::ops::Range; use std::{fmt, iter}; pub type AddressIdRange = Range; +pub(crate) type HDBalanceAddress = <::HDAddressScanner as HDAddressBalanceScanner>::Address; +pub(crate) type HDWalletBalanceObject = ::BalanceObject; +#[derive(Display)] pub enum EnableCoinBalanceError { NewAddressDerivingError(NewAddressDerivingError), - NewAccountCreatingError(NewAccountCreatingError), + NewAccountCreationError(NewAccountCreationError), BalanceError(BalanceError), } @@ -26,85 +28,129 @@ impl From for EnableCoinBalanceError { fn from(e: NewAddressDerivingError) -> Self { EnableCoinBalanceError::NewAddressDerivingError(e) } } -impl From for EnableCoinBalanceError { - fn from(e: NewAccountCreatingError) -> Self { EnableCoinBalanceError::NewAccountCreatingError(e) } +impl From for EnableCoinBalanceError { + fn from(e: NewAccountCreationError) -> Self { EnableCoinBalanceError::NewAccountCreationError(e) } } impl From for EnableCoinBalanceError { fn from(e: BalanceError) -> Self { EnableCoinBalanceError::BalanceError(e) } } +/// `BalanceObjectOps` should be implemented for a type that represents balance/s of a wallet. +/// For instance, if the wallet is for a platform coin and its tokens, the implementing type should be able to return the balances of the coin and its associated tokens. +pub trait BalanceObjectOps { + /// Creates a new balance object. + fn new() -> Self + where + Self: Sized; + + /// Adds another balance object to the current balance object. + fn add(&mut self, other: Self) + where + Self: Sized; + + /// Returns the total balance for the specified ticker. + /// If the balance object doesn't contain the specified ticker, it should return `None`. + fn get_total_for_ticker(&self, ticker: &str) -> Option; +} + +/// Encapsulates the balance of a specific wallet. +/// It provides two variants: `Iguana` and `HD`, each representing a different type of wallet. +/// This enum is used to abstract the differences between these two types of wallets, allowing for more generic operations on the balances. #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(tag = "wallet_type")] -pub enum CoinBalanceReport { - Iguana(IguanaWalletBalance), - HD(HDWalletBalance), +pub enum CoinBalanceReport +where + BalanceObject: BalanceObjectOps, +{ + Iguana(IguanaWalletBalance), + HD(HDWalletBalance), } -impl CoinBalanceReport { - /// Returns a map where the key is address, and the value is the address's total balance [`CoinBalance::total`]. - pub fn to_addresses_total_balances(&self) -> HashMap { +impl CoinBalanceReport +where + BalanceObject: BalanceObjectOps, +{ + /// Returns a map where the key is address, and the value is the address's total balance for the specified ticker. + pub fn to_addresses_total_balances(&self, ticker: &str) -> HashMap> { match self { CoinBalanceReport::Iguana(IguanaWalletBalance { ref address, ref balance, - }) => iter::once((address.clone(), balance.get_total())).collect(), + }) => iter::once((address.clone(), balance.get_total_for_ticker(ticker))).collect(), CoinBalanceReport::HD(HDWalletBalance { ref accounts }) => accounts .iter() .flat_map(|account_balance| { - account_balance - .addresses - .iter() - .map(|addr_balance| (addr_balance.address.clone(), addr_balance.balance.get_total())) + account_balance.addresses.iter().map(|addr_balance| { + ( + addr_balance.address.clone(), + addr_balance.balance.get_total_for_ticker(ticker), + ) + }) }) .collect(), } } } +/// `IguanaWalletBalance` represents the balance of an Iguana wallet. +/// The BalanceObject generic parameter can be any type that represents the balance/s of a single address. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct IguanaWalletBalance { +pub struct IguanaWalletBalance { pub address: String, - pub balance: CoinBalance, + pub balance: BalanceObject, } +/// Represents the balance of an HD wallet. +/// `BalanceObject` is a generic parameter which can be any type represent balance/s. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct HDWalletBalance { - pub accounts: Vec, +pub struct HDWalletBalance { + pub accounts: Vec>, } +/// Represents the balance of a single account in an HD wallet. +/// `BalanceObject` is a generic parameter which can be any type represent balance/s. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct HDAccountBalance { +pub struct HDAccountBalance { pub account_index: u32, pub derivation_path: RpcDerivationPath, - pub total_balance: CoinBalance, - pub addresses: Vec, + pub total_balance: BalanceObject, + pub addresses: Vec>, +} + +/// Encapsulates the balance of an account in an HD wallet. +/// It provides two variants: `Single` and `Map`, each representing a different type of balance. +/// `Single` is used when the balance is for one coin, while `Map` is used when the balance is for multiple coins. +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum HDAccountBalanceEnum { + Single(HDAccountBalance), + Map(HDAccountBalance), } +/// Represents the balance of a single address in an HD wallet. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct HDAddressBalance { +pub struct HDAddressBalance { pub address: String, pub derivation_path: RpcDerivationPath, pub chain: Bip44Chain, - pub balance: CoinBalance, + pub balance: BalanceObject, } #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum EnableCoinScanPolicy { /// Don't scan for new addresses. DoNotScan, /// Scan for new addresses if the coin HD wallet hasn't been enabled *only*. /// In other words, scan for new addresses if there were no HD accounts in the HD wallet storage. + #[default] ScanIfNewWallet, /// Scan for new addresses even if the coin HD wallet has been enabled before. Scan, } -impl Default for EnableCoinScanPolicy { - fn default() -> Self { EnableCoinScanPolicy::ScanIfNewWallet } -} - #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct EnabledCoinBalanceParams { #[serde(default)] @@ -112,25 +158,30 @@ pub struct EnabledCoinBalanceParams { pub min_addresses_number: Option, } +/// `CoinBalanceReportOps` provides methods for getting the balance of a coin for different types of wallets. #[async_trait] pub trait CoinBalanceReportOps { - async fn coin_balance_report(&self) -> BalanceResult; + /// Represents the balance of a coin or a coin and its associated tokens for a certain wallet.. + type BalanceObject: BalanceObjectOps; + /// Returns the balance of a coin or a coin and its associated tokens for a certain wallet. + async fn coin_balance_report(&self) -> BalanceResult>; } #[async_trait] impl CoinBalanceReportOps for Coin where - Coin: CoinWithDerivationMethod::HDWallet> + Coin: CoinWithDerivationMethod + HDWalletBalanceOps - + MarketCoinOps + + IguanaBalanceOps> + Sync, - ::Address: fmt::Display + Sync, + HDCoinAddress: fmt::Display + Sync, { - async fn coin_balance_report(&self) -> BalanceResult { + type BalanceObject = HDWalletBalanceObject; + + async fn coin_balance_report(&self) -> BalanceResult> { match self.derivation_method() { DerivationMethod::SingleAddress(my_address) => self - .my_balance() - .compat() + .iguana_balances() .await .map(|balance| { CoinBalanceReport::Iguana(IguanaWalletBalance { @@ -149,56 +200,66 @@ where #[async_trait] pub trait EnableCoinBalanceOps { + type BalanceObject: BalanceObjectOps; + async fn enable_coin_balance( &self, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor; + XPubExtractor: HDXPubExtractor + Send; } #[async_trait] impl EnableCoinBalanceOps for Coin where - Coin: CoinWithDerivationMethod::HDWallet> + Coin: CoinWithDerivationMethod + HDWalletBalanceOps - + MarketCoinOps + + IguanaBalanceOps> + Sync, - ::Address: fmt::Display + Sync, + HDCoinAddress: fmt::Display + Sync, { + type BalanceObject = HDWalletBalanceObject; + async fn enable_coin_balance( &self, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { match self.derivation_method() { DerivationMethod::SingleAddress(my_address) => self - .my_balance() - .compat() + .iguana_balances() .await .map(|balance| { CoinBalanceReport::Iguana(IguanaWalletBalance { - address: my_address.to_string(), + address: self.address_formatter()(my_address), balance, }) }) .mm_err(EnableCoinBalanceError::from), DerivationMethod::HDWallet(hd_wallet) => self - .enable_hd_wallet(hd_wallet, xpub_extractor, params) + .enable_hd_wallet(hd_wallet, xpub_extractor, params, path_to_address) .await .map(CoinBalanceReport::HD), } } } +/// `HDWalletBalanceOps` provides different methods related to the balance of an HD wallet. #[async_trait] pub trait HDWalletBalanceOps: HDWalletCoinOps { - type HDAddressScanner: HDAddressBalanceScanner
; + /// The type of the scanner that will be used to scan for balances in an HD wallet. + type HDAddressScanner: HDAddressBalanceScanner
> + Sync; + /// Represents a balance in an HD wallet. + type BalanceObject: BalanceObjectOps + Clone + Send; + /// Returns the scanner of balances for the HD wallet. async fn produce_hd_address_scanner(&self) -> BalanceResult; /// Requests balances of already known addresses, and if it's prescribed by [`EnableCoinParams::scan_policy`], @@ -207,33 +268,41 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { async fn enable_hd_wallet( &self, hd_wallet: &Self::HDWallet, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor; + XPubExtractor: HDXPubExtractor + Send; /// Scans for the new addresses of the specified `hd_account` using the given `address_scanner`. /// Returns balances of the new addresses. async fn scan_for_new_addresses( &self, hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, + hd_account: &mut HDCoinHDAccount, address_scanner: &Self::HDAddressScanner, gap_limit: u32, - ) -> BalanceResult>; + ) -> BalanceResult>>; /// Requests balances of every activated HD account. - async fn all_accounts_balances(&self, hd_wallet: &Self::HDWallet) -> BalanceResult> { + async fn all_accounts_balances( + &self, + hd_wallet: &Self::HDWallet, + ) -> BalanceResult>> { let accounts = hd_wallet.get_accounts().await; let mut result = Vec::with_capacity(accounts.len()); for (_account_id, hd_account) in accounts { let addresses = self.all_known_addresses_balances(&hd_account).await?; - let total_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { - total + addr_balance.balance.clone() - }); + let total_balance = addresses + .iter() + .fold(Self::BalanceObject::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total + }); + let account_balance = HDAccountBalance { account_index: hd_account.account_id(), derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), @@ -248,17 +317,19 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { } /// Requests balances of every known addresses of the given `hd_account`. - async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult>; + async fn all_known_addresses_balances( + &self, + hd_account: &HDCoinHDAccount, + ) -> BalanceResult>>; /// Requests balances of known addresses of the given `address_ids` addresses at the specified `chain`. async fn known_addresses_balances_with_ids( &self, - hd_account: &Self::HDAccount, + hd_account: &HDCoinHDAccount, chain: Bip44Chain, address_ids: Ids, - ) -> BalanceResult> + ) -> BalanceResult>> where - Self::Address: fmt::Display + Clone, Ids: Iterator + Send, { let address_ids = address_ids.map(|address_id| HDAddressId { chain, address_id }); @@ -268,13 +339,7 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { .derive_addresses(hd_account, address_ids) .await? .into_iter() - .map( - |HDAddress { - address, - derivation_path, - .. - }| (address, derivation_path), - ) + .map(|hd_address| (hd_address.address(), hd_address.derivation_path().clone())) .unzip(); let balances = self @@ -286,7 +351,7 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { // So we can zip the derivation paths with the pairs `(Address, CoinBalance)`. .zip(der_paths) .map(|((address, balance), derivation_path)| HDAddressBalance { - address: address.to_string(), + address: self.address_formatter()(&address), derivation_path: RpcDerivationPath(derivation_path), chain, balance, @@ -298,22 +363,22 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { /// Requests balance of the given `address`. /// This function is expected to be more efficient than ['HDWalletBalanceOps::is_address_used'] in most cases /// since many of RPC clients allow us to request the address balance without the history. - async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult; + async fn known_address_balance(&self, address: &HDBalanceAddress) -> BalanceResult; /// Requests balances of the given `addresses`. /// The pairs `(Address, CoinBalance)` are guaranteed to be in the same order in which they were requested. async fn known_addresses_balances( &self, - addresses: Vec, - ) -> BalanceResult>; + addresses: Vec>, + ) -> BalanceResult, Self::BalanceObject)>>; /// Checks if the address has been used by the user by checking if the transaction history of the given `address` is not empty. /// Please note the function can return zero balance even if the address has been used before. async fn is_address_used( &self, - address: &Self::Address, + address: &HDBalanceAddress, address_scanner: &Self::HDAddressScanner, - ) -> BalanceResult> { + ) -> BalanceResult> { if !address_scanner.is_address_used(address).await? { return Ok(AddressBalanceStatus::NotUsed); } @@ -321,13 +386,21 @@ pub trait HDWalletBalanceOps: HDWalletCoinOps { let balance = self.known_address_balance(address).await?; Ok(AddressBalanceStatus::Used(balance)) } + + // Todo: should probably be moved to a separate trait. Addresses should be HashSet too + /// Prepares addresses for real time balance streaming if coin balance event is enabled. + async fn prepare_addresses_for_balance_stream_if_enabled(&self, addresses: HashSet) + -> MmResult<(), String>; } +/// `HDAddressBalanceScanner` trait provides different methods related to scanning for balances in an HD wallet. #[async_trait] #[cfg_attr(test, mockable)] -pub trait HDAddressBalanceScanner: Sync { - type Address; +pub trait HDAddressBalanceScanner { + /// The type of address that the scanner will be scanning for. + type Address: Send + Sync; + /// Checks if the given `address` has been used before. async fn is_address_used(&self, address: &Self::Address) -> BalanceResult; } @@ -338,19 +411,21 @@ pub enum AddressBalanceStatus { pub mod common_impl { use super::*; - use crate::hd_wallet::{HDAccountOps, HDWalletOps}; + use crate::hd_wallet::{create_new_account, ExtractExtendedPubkey, HDAccountOps, HDAccountStorageOps, HDAddressOps, + HDCoinExtendedPubkey, HDWalletOps}; pub(crate) async fn enable_hd_account( coin: &Coin, hd_wallet: &Coin::HDWallet, - hd_account: &mut Coin::HDAccount, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, address_scanner: &Coin::HDAddressScanner, scan_new_addresses: bool, min_addresses_number: Option, - ) -> MmResult + ) -> MmResult>, EnableCoinBalanceError> where Coin: HDWalletBalanceOps + MarketCoinOps + Sync, - Coin::Address: fmt::Display, + HDCoinAddress: fmt::Display, { let gap_limit = hd_wallet.gap_limit(); let mut addresses = coin.all_known_addresses_balances(hd_account).await?; @@ -362,12 +437,16 @@ pub mod common_impl { } if let Some(min_addresses_number) = min_addresses_number { - gen_new_addresses(coin, hd_wallet, hd_account, &mut addresses, min_addresses_number).await? + gen_new_addresses(coin, hd_wallet, hd_account, chain, &mut addresses, min_addresses_number).await? } - let total_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { - total + addr_balance.balance.clone() - }); + let total_balance = addresses + .iter() + .fold(HDWalletBalanceObject::::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total + }); + let account_balance = HDAccountBalance { account_index: hd_account.account_id(), derivation_path: RpcDerivationPath(hd_account.account_derivation_path()), @@ -381,13 +460,18 @@ pub mod common_impl { pub(crate) async fn enable_hd_wallet( coin: &Coin, hd_wallet: &Coin::HDWallet, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult>, EnableCoinBalanceError> where - Coin: HDWalletBalanceOps + MarketCoinOps + Sync, - Coin::Address: fmt::Display, - XPubExtractor: HDXPubExtractor, + Coin: ExtractExtendedPubkey> + + HDWalletBalanceOps + + MarketCoinOps + + Sync, + HDCoinAddress: fmt::Display, + XPubExtractor: HDXPubExtractor + Send, + HDCoinHDAccount: HDAccountStorageOps, { let mut accounts = hd_wallet.get_accounts_mut().await; let address_scanner = coin.produce_hd_address_scanner().await?; @@ -396,7 +480,7 @@ pub mod common_impl { accounts: Vec::with_capacity(accounts.len() + 1), }; - if accounts.is_empty() { + if accounts.get(&path_to_address.account_id).is_none() { // Is seems that we couldn't find any HD account from the HD wallet storage. drop(accounts); info!( @@ -405,7 +489,8 @@ pub mod common_impl { ); // Create new HD account. - let mut new_account = coin.create_new_account(hd_wallet, xpub_extractor).await?; + let mut new_account = + create_new_account(coin, hd_wallet, xpub_extractor, Some(path_to_address.account_id)).await?; let scan_new_addresses = matches!( params.scan_policy, EnableCoinScanPolicy::ScanIfNewWallet | EnableCoinScanPolicy::Scan @@ -415,11 +500,13 @@ pub mod common_impl { coin, hd_wallet, &mut new_account, + path_to_address.chain, &address_scanner, scan_new_addresses, - params.min_addresses_number, + params.min_addresses_number.max(Some(path_to_address.address_id + 1)), ) .await?; + // Todo: The enabled address should be indicated in the response. result.accounts.push(account_balance); return Ok(result); } @@ -430,14 +517,23 @@ pub mod common_impl { coin.ticker() ); let scan_new_addresses = matches!(params.scan_policy, EnableCoinScanPolicy::Scan); - for (_account_id, hd_account) in accounts.iter_mut() { + for (account_id, hd_account) in accounts.iter_mut() { + let min_addresses_number = if *account_id == path_to_address.account_id { + // The account for the enabled address is already indexed. + // But in case the address index is larger than the number of derived addresses, + // we need to derive new addresses to make sure that the enabled address is indexed. + params.min_addresses_number.max(Some(path_to_address.address_id + 1)) + } else { + params.min_addresses_number + }; let account_balance = enable_hd_account( coin, hd_wallet, hd_account, + path_to_address.chain, &address_scanner, scan_new_addresses, - params.min_addresses_number, + min_addresses_number, ) .await?; result.accounts.push(account_balance); @@ -450,15 +546,15 @@ pub mod common_impl { async fn gen_new_addresses( coin: &Coin, hd_wallet: &Coin::HDWallet, - hd_account: &mut Coin::HDAccount, - result_addresses: &mut Vec, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, + result_addresses: &mut Vec>>, min_addresses_number: u32, ) -> MmResult<(), EnableCoinBalanceError> where Coin: HDWalletBalanceOps + MarketCoinOps + Sync, - Coin::Address: fmt::Display, { - let max_addresses_number = hd_wallet.address_limit(); + let max_addresses_number = hd_account.address_limit(); if min_addresses_number >= max_addresses_number { return MmError::err(EnableCoinBalanceError::NewAddressDerivingError( NewAddressDerivingError::AddressLimitReached { max_addresses_number }, @@ -473,7 +569,6 @@ pub mod common_impl { } let to_generate = min_addresses_number - actual_addresses_number; - let chain = hd_wallet.default_receiver_chain(); let ticker = coin.ticker(); let account_id = hd_account.account_id(); info!("Generate '{to_generate}' addresses: ticker={ticker} account_id={account_id}, chain={chain:?}"); @@ -481,19 +576,15 @@ pub mod common_impl { let mut new_addresses = Vec::with_capacity(to_generate); let mut addresses_to_request = Vec::with_capacity(to_generate); for _ in 0..to_generate { - let HDAddress { - address, - derivation_path, - .. - } = coin.generate_new_address(hd_wallet, hd_account, chain).await?; + let hd_address = coin.generate_new_address(hd_wallet, hd_account, chain).await?; new_addresses.push(HDAddressBalance { - address: address.to_string(), - derivation_path: RpcDerivationPath(derivation_path), + address: coin.address_formatter()(&hd_address.address()), + derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, - balance: CoinBalance::default(), + balance: HDWalletBalanceObject::::new(), }); - addresses_to_request.push(address); + addresses_to_request.push(hd_address.address().clone()); } let to_extend = coin diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index c9672082c7..34b0c46486 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -1,5 +1,7 @@ -use crate::{eth::Web3RpcError, my_tx_history_v2::MyTxHistoryErrorV2, utxo::rpc_clients::UtxoRpcError, DelegationError, - NumConversError, TxHistoryError, UnexpectedDerivationMethod, WithdrawError}; +use crate::eth::nft_swap_v2::errors::{Erc721FunctionError, HtlcParamsError, PaymentStatusErr, PrepareTxDataError}; +use crate::eth::{EthAssocTypesError, EthNftAssocTypesError, Web3RpcError}; +use crate::{utxo::rpc_clients::UtxoRpcError, NumConversError, UnexpectedDerivationMethod}; +use enum_derives::EnumFromStringify; use futures01::Future; use mm2_err_handle::prelude::MmError; use spv_validation::helpers_validation::SPVError; @@ -7,13 +9,25 @@ use std::num::TryFromIntError; /// Helper type used as result for swap payment validation function(s) pub type ValidatePaymentFut = Box> + Send>; +/// Helper type used as result for swap payment validation function(s) +pub type ValidatePaymentResult = Result>; /// Enum covering possible error cases of swap payment validation -#[derive(Debug, Display)] +#[derive(Debug, Display, EnumFromStringify)] pub enum ValidatePaymentError { /// Should be used to indicate internal MM2 state problems (e.g., DB errors, etc.). + #[from_stringify( + "EthAssocTypesError", + "Erc721FunctionError", + "EthNftAssocTypesError", + "NumConversError", + "UnexpectedDerivationMethod", + "keys::Error", + "PrepareTxDataError" + )] InternalError(String), /// Problem with deserializing the transaction, or one of the transaction parts is invalid. + #[from_stringify("rlp::DecoderError", "serialization::Error")] TxDeserializationError(String), /// One of the input parameters is invalid. InvalidParameter(String), @@ -26,6 +40,7 @@ pub enum ValidatePaymentError { /// Payment transaction is in unexpected state. E.g., `Uninitialized` instead of `Sent` for ETH payment. UnexpectedPaymentState(String), /// Transport (RPC) error. + #[from_stringify("web3::Error")] Transport(String), /// Transaction has wrong properties, for example, it has been sent to a wrong address. WrongPaymentTx(String), @@ -33,32 +48,14 @@ pub enum ValidatePaymentError { WatcherRewardError(String), /// Input payment timelock overflows the type used by specific coin. TimelockOverflow(TryFromIntError), -} - -impl From for ValidatePaymentError { - fn from(err: rlp::DecoderError) -> Self { Self::TxDeserializationError(err.to_string()) } -} - -impl From for ValidatePaymentError { - fn from(err: web3::Error) -> Self { Self::Transport(err.to_string()) } -} - -impl From for ValidatePaymentError { - fn from(err: NumConversError) -> Self { Self::InternalError(err.to_string()) } + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, } impl From for ValidatePaymentError { fn from(err: SPVError) -> Self { Self::SPVError(err) } } -impl From for ValidatePaymentError { - fn from(err: serialization::Error) -> Self { Self::TxDeserializationError(err.to_string()) } -} - -impl From for ValidatePaymentError { - fn from(err: UnexpectedDerivationMethod) -> Self { Self::InternalError(err.to_string()) } -} - impl From for ValidatePaymentError { fn from(err: UtxoRpcError) -> Self { match err { @@ -74,39 +71,38 @@ impl From for ValidatePaymentError { match e { Web3RpcError::Transport(tr) => ValidatePaymentError::Transport(tr), Web3RpcError::InvalidResponse(resp) => ValidatePaymentError::InvalidRpcResponse(resp), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { - ValidatePaymentError::InternalError(internal) - }, + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => ValidatePaymentError::InternalError(internal), + Web3RpcError::NftProtocolNotSupported => ValidatePaymentError::NftProtocolNotSupported, } } } -#[derive(Debug, Display)] -pub enum MyAddressError { - UnexpectedDerivationMethod(String), - InternalError(String), -} - -impl From for MyAddressError { - fn from(err: UnexpectedDerivationMethod) -> Self { Self::UnexpectedDerivationMethod(err.to_string()) } -} - -impl From for WithdrawError { - fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } -} - -impl From for UtxoRpcError { - fn from(err: MyAddressError) -> Self { Self::Internal(err.to_string()) } -} - -impl From for DelegationError { - fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } +impl From for ValidatePaymentError { + fn from(err: PaymentStatusErr) -> Self { + match err { + PaymentStatusErr::Transport(e) => Self::Transport(e), + PaymentStatusErr::AbiError(e) + | PaymentStatusErr::Internal(e) + | PaymentStatusErr::TxDeserializationError(e) => Self::InternalError(e), + } + } } -impl From for TxHistoryError { - fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } +impl From for ValidatePaymentError { + fn from(err: HtlcParamsError) -> Self { + match err { + HtlcParamsError::WrongPaymentTx(e) => ValidatePaymentError::WrongPaymentTx(e), + HtlcParamsError::TxDeserializationError(e) => ValidatePaymentError::TxDeserializationError(e), + } + } } -impl From for MyTxHistoryErrorV2 { - fn from(err: MyAddressError) -> Self { Self::Internal(err.to_string()) } +#[derive(Debug, Display, EnumFromStringify)] +pub enum MyAddressError { + #[from_stringify("UnexpectedDerivationMethod")] + UnexpectedDerivationMethod(String), + InternalError(String), } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 90e2f90ffb..b5f5b0ecde 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright © 2023 Pampex LTD and TillyHK LTD * + * Copyright © 2023 Pampex LTD and TillyHK LTD * * * * See the CONTRIBUTOR-LICENSE-AGREEMENT, COPYING, LICENSE-COPYRIGHT-NOTICE * * and DEVELOPER-CERTIFICATE-OF-ORIGIN files in the LEGAL directory in * @@ -7,7 +7,7 @@ * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * - * Komodo DeFi Framework software, including this file may be copied, modified, propagated* + * Komodo DeFi Framework software, including this file may be copied, modified, propagated * * or distributed except according to the terms contained in the * * LICENSE-COPYRIGHT-NOTICE file. * * * @@ -21,41 +21,70 @@ // Copyright © 2023 Pampex LTD and TillyHK LTD. All rights reserved. // use super::eth::Action::{Call, Create}; +use super::watcher_common::{validate_watcher_reward, REWARD_GAS_AMOUNT}; +use super::*; +use crate::coin_balance::{EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, + HDWalletBalance, HDWalletBalanceOps}; +use crate::eth::eth_rpc::ETH_RPC_REQUEST_TIMEOUT; +use crate::eth::web3_transport::websocket_transport::{WebsocketTransport, WebsocketTransportNode}; +use crate::hd_wallet::{HDAccountOps, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, HDPathAccountToAddressId, + HDWalletCoinOps, HDXPubExtractor}; use crate::lp_price::get_base_price_in_rel; -use crate::nft::nft_structs::{ContractType, ConvertChain, TransactionNftDetails, WithdrawErc1155, WithdrawErc721}; -use crate::{DexFee, ValidateWatcherSpendInput, WatcherSpendType}; +use crate::nft::nft_errors::ParseContractTypeError; +use crate::nft::nft_structs::{ContractType, ConvertChain, NftInfo, TransactionNftDetails, WithdrawErc1155, + WithdrawErc721}; +use crate::nft::WithdrawNftResult; +use crate::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; +use crate::rpc_command::get_new_address::{GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, + GetNewAddressRpcOps}; +use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; +use crate::rpc_command::init_account_balance::{InitAccountBalanceParams, InitAccountBalanceRpcOps}; +use crate::rpc_command::init_create_account::{CreateAccountRpcError, CreateAccountState, CreateNewAccountParams, + InitCreateAccountRpcOps}; +use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, ScanAddressesParams, + ScanAddressesResponse}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandleShared}; +use crate::rpc_command::{account_balance, get_new_address, init_account_balance, init_create_account, + init_scan_for_new_addresses}; +use crate::{coin_balance, scan_for_new_addresses_impl, BalanceResult, CoinWithDerivationMethod, DerivationMethod, + DexFee, Eip1559Ops, MakerNftSwapOpsV2, ParseCoinAssocTypes, ParseNftAssocTypes, PayForGasParams, + PrivKeyPolicy, RefundMakerPaymentArgs, RpcCommonOps, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, + ToBytes, ValidateNftMakerPaymentArgs, ValidateWatcherSpendInput, WatcherSpendType}; use async_trait::async_trait; use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; use common::custom_futures::timeout::FutureTimerExt; -use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError, Timer}; +use common::executor::{abortable_queue::AbortableQueue, AbortOnDropHandle, AbortSettings, AbortableSystem, + AbortedError, SpawnAbortable, Timer}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; use common::{get_utc_timestamp, now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; -#[cfg(target_arch = "wasm32")] -use common::{now_ms, wait_until_ms}; use crypto::privkey::key_pair_from_secret; -use crypto::{CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy, StandardHDCoinAddress}; +use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairPolicy}; use derive_more::Display; -use enum_from::EnumFromStringify; +use enum_derives::EnumFromStringify; use ethabi::{Contract, Function, Token}; -pub use ethcore_transaction::SignedTransaction as SignedEthTx; -use ethcore_transaction::{Action, Transaction as UnSignedEthTx, UnverifiedTransaction}; +use ethcore_transaction::tx_builders::TxBuilderError; +use ethcore_transaction::{Action, TransactionWrapper, TransactionWrapperBuilder as UnSignedEthTxBuilder, + UnverifiedEip1559Transaction, UnverifiedEip2930Transaction, UnverifiedLegacyTransaction, + UnverifiedTransactionWrapper}; +pub use ethcore_transaction::{SignedTransaction as SignedEthTx, TxType}; use ethereum_types::{Address, H160, H256, U256}; -use ethkey::{public_to_address, KeyPair, Public, Signature}; -use ethkey::{sign, verify_address}; +use ethkey::{public_to_address, sign, verify_address, KeyPair, Public, Signature}; use futures::compat::Future01CompatExt; -use futures::future::{join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; +use futures::future::{join, join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; use futures01::Future; -use http::StatusCode; +use http::Uri; +use instant::Instant; +use keys::Public as HtlcPubKey; use mm2_core::mm_ctx::{MmArc, MmWeak}; -use mm2_err_handle::prelude::*; -use mm2_net::transport::{slurp_url, GuiAuthValidation, GuiAuthValidationGenerator, SlurpError}; +use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; +use mm2_net::transport::{KomodefiProxyAuthValidation, ProxyAuthValidationGenerator}; use mm2_number::bigdecimal_custom::CheckedDivision; -use mm2_number::{BigDecimal, MmNumber}; -use mm2_rpc::data::legacy::GasStationPricePolicy; +use mm2_number::{BigDecimal, BigUint, MmNumber}; #[cfg(test)] use mocktopus::macros::*; use rand::seq::SliceRandom; +use rlp::{DecoderError, Encodable, RlpStream}; use rpc::v1::types::Bytes as BytesJson; use secp256k1::PublicKey; use serde_json::{self as json, Value as Json}; @@ -64,23 +93,23 @@ use sha3::{Digest, Keccak256}; use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::ops::Deref; -#[cfg(not(target_arch = "wasm32"))] use std::path::PathBuf; +use std::str::from_utf8; use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::{Arc, Mutex}; +use std::time::Duration; use web3::types::{Action as TraceAction, BlockId, BlockNumber, Bytes, CallRequest, FilterBuilder, Log, Trace, TraceFilterBuilder, Transaction as Web3Transaction, TransactionId, U64}; use web3::{self, Web3}; -use web3_transport::{http_transport::HttpTransportNode, EthFeeHistoryNamespace, Web3Transport}; cfg_wasm32! { + use common::{now_ms, wait_until_ms}; use crypto::MetamaskArc; use ethereum_types::{H264, H520}; use mm2_metamask::MetamaskError; use web3::types::TransactionRequest; } -use super::watcher_common::{validate_watcher_reward, REWARD_GAS_AMOUNT}; use super::{coin_conf, lp_coinfind_or_err, AsyncMutex, BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, CoinProtocol, CoinTransportMetrics, CoinsContext, ConfirmPaymentInput, EthValidateFeeArgs, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, @@ -89,42 +118,62 @@ use super::{coin_conf, lp_coinfind_or_err, AsyncMutex, BalanceError, BalanceFut, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, RewardTarget, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, - SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, - TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, - TransactionEnum, TransactionErr, TransactionFut, TransactionType, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, - ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, VerificationError, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult, EARLY_CONFIRMATION_ERR_LOG, - INVALID_CONTRACT_ADDRESS_ERR_LOG, INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, - INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG}; + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignEthTransactionParams, + SignRawTransactionEnum, SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, + SwapOps, SwapTxFeePolicy, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, + TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, TransactionEnum, TransactionErr, + TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, + WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, + WithdrawRequest, WithdrawResult, EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, + INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG}; pub use rlp; +cfg_native! { + use std::path::PathBuf; +} +mod eth_balance_events; +mod eth_rpc; #[cfg(test)] mod eth_tests; #[cfg(target_arch = "wasm32")] mod eth_wasm_tests; +#[cfg(any(test, target_arch = "wasm32"))] mod for_tests; +pub(crate) mod nft_swap_v2; mod web3_transport; +use web3_transport::{http_transport::HttpTransportNode, Web3Transport}; + +pub mod eth_hd_wallet; +use eth_hd_wallet::EthHDWallet; #[path = "eth/v2_activation.rs"] pub mod v2_activation; -use crate::nft::{find_wallet_nft_amount, WithdrawNftResult}; use v2_activation::{build_address_and_priv_key_policy, EthActivationV2Error}; +mod eth_withdraw; +use eth_withdraw::{EthWithdraw, InitEthWithdraw, StandardEthWithdraw}; + mod nonce; -use crate::{PrivKeyPolicy, TransactionResult, WithdrawFrom}; use nonce::ParityNonce; +mod eip1559_gas_fee; +pub(crate) use eip1559_gas_fee::FeePerGasEstimated; +use eip1559_gas_fee::{BlocknativeGasApiCaller, FeePerGasSimpleEstimator, GasApiConfig, GasApiProvider, + InfuraGasApiCaller}; + /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol /// Dev chain (195.201.137.5:8565) contract address: 0x83965C539899cC0F918552e5A26915de40ee8852 /// Ropsten: https://ropsten.etherscan.io/address/0x7bc1bbdd6a0a722fc9bffc49c921b685ecb84b94 /// ETH mainnet: https://etherscan.io/address/0x8500AFc0bc5214728082163326C2FF0C73f4a871 -const SWAP_CONTRACT_ABI: &str = include_str!("eth/swap_contract_abi.json"); +pub const SWAP_CONTRACT_ABI: &str = include_str!("eth/swap_contract_abi.json"); /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md -const ERC20_ABI: &str = include_str!("eth/erc20_abi.json"); +pub const ERC20_ABI: &str = include_str!("eth/erc20_abi.json"); /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md const ERC721_ABI: &str = include_str!("eth/erc721_abi.json"); /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md const ERC1155_ABI: &str = include_str!("eth/erc1155_abi.json"); +const NFT_SWAP_CONTRACT_ABI: &str = include_str!("eth/nft_swap_contract_abi.json"); +const NFT_MAKER_SWAP_V2_ABI: &str = include_str!("eth/nft_maker_swap_v2_abi.json"); + /// Payment states from etomic swap smart contract: https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol#L5 pub enum PaymentState { Uninitialized, @@ -132,16 +181,33 @@ pub enum PaymentState { Spent, Refunded, } -// Ethgasstation API returns response in 10^8 wei units. So 10 from their API mean 1 gwei -const ETH_GAS_STATION_DECIMALS: u8 = 8; -const GAS_PRICE_PERCENT: u64 = 10; + +#[allow(dead_code)] +pub(crate) enum MakerPaymentStateV2 { + Uninitialized, + PaymentSent, + TakerSpent, + MakerRefunded, +} + +#[allow(dead_code)] +pub(crate) enum TakerPaymentStateV2 { + Uninitialized, + PaymentSent, + TakerApproved, + MakerSpent, + TakerRefunded, +} + /// It can change 12.5% max each block according to https://www.blocknative.com/blog/eip-1559-fees const BASE_BLOCK_FEE_DIFF_PCT: u64 = 13; const DEFAULT_LOGS_BLOCK_RANGE: u64 = 1000; const DEFAULT_REQUIRED_CONFIRMATIONS: u8 = 1; -const ETH_DECIMALS: u8 = 18; +pub(crate) const ETH_DECIMALS: u8 = 18; + +pub(crate) const ETH_GWEI_DECIMALS: u8 = 9; /// Take into account that the dynamic fee may increase by 3% during the swap. const GAS_PRICE_APPROXIMATION_PERCENT_ON_START_SWAP: u64 = 3; @@ -157,77 +223,180 @@ const GAS_PRICE_APPROXIMATION_PERCENT_ON_ORDER_ISSUE: u64 = 5; /// - it may increase by 3% during the swap. const GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE: u64 = 7; -const ETH_GAS: u64 = 150_000; +/// Heuristic default gas limits for withdraw and swap operations (including extra margin value for possible changes in opcodes cost) +pub mod gas_limit { + /// Gas limit for sending coins + pub const ETH_SEND_COINS: u64 = 21_000; + /// Gas limit for transfer ERC20 tokens + /// TODO: maybe this is too much and 150K is okay + pub const ETH_SEND_ERC20: u64 = 210_000; + /// Gas limit for swap payment tx with coins + /// real values are approx 48,6K by etherscan + pub const ETH_PAYMENT: u64 = 65_000; + /// Gas limit for swap payment tx with ERC20 tokens + /// real values are 98,9K for ERC20 and 135K for ERC-1967 proxied ERC20 contracts (use 'gas_limit' override in coins to tune) + pub const ERC20_PAYMENT: u64 = 150_000; + /// Gas limit for swap receiver spend tx with coins + /// real values are 40,7K + pub const ETH_RECEIVER_SPEND: u64 = 65_000; + /// Gas limit for swap receiver spend tx with ERC20 tokens + /// real values are 72,8K + pub const ERC20_RECEIVER_SPEND: u64 = 150_000; + /// Gas limit for swap refund tx with coins + pub const ETH_SENDER_REFUND: u64 = 100_000; + /// Gas limit for swap refund tx with with ERC20 tokens + pub const ERC20_SENDER_REFUND: u64 = 150_000; + /// Gas limit for other operations + pub const ETH_MAX_TRADE_GAS: u64 = 150_000; +} + +/// Coin conf param to override default gas limits +#[derive(Deserialize)] +#[serde(default)] +pub struct EthGasLimit { + /// Gas limit for sending coins + pub eth_send_coins: u64, + /// Gas limit for sending ERC20 tokens + pub eth_send_erc20: u64, + /// Gas limit for swap payment tx with coins + pub eth_payment: u64, + /// Gas limit for swap payment tx with ERC20 tokens + pub erc20_payment: u64, + /// Gas limit for swap receiver spend tx with coins + pub eth_receiver_spend: u64, + /// Gas limit for swap receiver spend tx with ERC20 tokens + pub erc20_receiver_spend: u64, + /// Gas limit for swap refund tx with coins + pub eth_sender_refund: u64, + /// Gas limit for swap refund tx with with ERC20 tokens + pub erc20_sender_refund: u64, + /// Gas limit for other operations + pub eth_max_trade_gas: u64, +} + +impl Default for EthGasLimit { + fn default() -> Self { + EthGasLimit { + eth_send_coins: gas_limit::ETH_SEND_COINS, + eth_send_erc20: gas_limit::ETH_SEND_ERC20, + eth_payment: gas_limit::ETH_PAYMENT, + erc20_payment: gas_limit::ERC20_PAYMENT, + eth_receiver_spend: gas_limit::ETH_RECEIVER_SPEND, + erc20_receiver_spend: gas_limit::ERC20_RECEIVER_SPEND, + eth_sender_refund: gas_limit::ETH_SENDER_REFUND, + erc20_sender_refund: gas_limit::ERC20_SENDER_REFUND, + eth_max_trade_gas: gas_limit::ETH_MAX_TRADE_GAS, + } + } +} + +/// Lifetime of generated signed message for proxy-auth requests +const PROXY_AUTH_SIGNED_MESSAGE_LIFETIME_SEC: i64 = 90; -/// Lifetime of generated signed message for gui-auth requests -const GUI_AUTH_SIGNED_MESSAGE_LIFETIME_SEC: i64 = 90; +/// Max transaction type according to EIP-2718 +const ETH_MAX_TX_TYPE: u64 = 0x7f; lazy_static! { pub static ref SWAP_CONTRACT: Contract = Contract::load(SWAP_CONTRACT_ABI.as_bytes()).unwrap(); pub static ref ERC20_CONTRACT: Contract = Contract::load(ERC20_ABI.as_bytes()).unwrap(); pub static ref ERC721_CONTRACT: Contract = Contract::load(ERC721_ABI.as_bytes()).unwrap(); pub static ref ERC1155_CONTRACT: Contract = Contract::load(ERC1155_ABI.as_bytes()).unwrap(); + pub static ref NFT_SWAP_CONTRACT: Contract = Contract::load(NFT_SWAP_CONTRACT_ABI.as_bytes()).unwrap(); + pub static ref NFT_MAKER_SWAP_V2: Contract = Contract::load(NFT_MAKER_SWAP_V2_ABI.as_bytes()).unwrap(); } +pub type EthDerivationMethod = DerivationMethod; pub type Web3RpcFut = Box> + Send>; pub type Web3RpcResult = Result>; -pub type GasStationResult = Result>; type EthPrivKeyPolicy = PrivKeyPolicy; -type GasDetails = (U256, U256); -#[derive(Debug, Display)] -pub enum GasStationReqErr { - #[display(fmt = "Transport '{}' error: {}", uri, error)] - Transport { - uri: String, - error: String, - }, - #[display(fmt = "Invalid response: {}", _0)] - InvalidResponse(String), - Internal(String), +#[macro_export] +macro_rules! wei_from_gwei_decimal { + ($big_decimal: expr) => { + $crate::eth::wei_from_big_decimal($big_decimal, $crate::eth::ETH_GWEI_DECIMALS) + }; } -impl From for GasStationReqErr { - fn from(e: serde_json::Error) -> Self { GasStationReqErr::InvalidResponse(e.to_string()) } +#[macro_export] +macro_rules! wei_to_gwei_decimal { + ($gwei: expr) => { + $crate::eth::u256_to_big_decimal($gwei, $crate::eth::ETH_GWEI_DECIMALS) + }; } -impl From for GasStationReqErr { - fn from(e: SlurpError) -> Self { - let error = e.to_string(); - match e { - SlurpError::ErrorDeserializing { .. } => GasStationReqErr::InvalidResponse(error), - SlurpError::Transport { uri, .. } | SlurpError::Timeout { uri, .. } => { - GasStationReqErr::Transport { uri, error } - }, - SlurpError::Internal(_) | SlurpError::InvalidRequest(_) => GasStationReqErr::Internal(error), +#[derive(Clone, Debug)] +pub(crate) struct LegacyGasPrice { + pub(crate) gas_price: U256, +} + +#[derive(Clone, Debug)] +pub(crate) struct Eip1559FeePerGas { + pub(crate) max_fee_per_gas: U256, + pub(crate) max_priority_fee_per_gas: U256, +} + +/// Internal structure describing how transaction pays for gas unit: +/// either legacy gas price or EIP-1559 fee per gas +#[derive(Clone, Debug)] +pub(crate) enum PayForGasOption { + Legacy(LegacyGasPrice), + Eip1559(Eip1559FeePerGas), +} + +impl PayForGasOption { + fn get_gas_price(&self) -> Option { + match self { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => Some(*gas_price), + PayForGasOption::Eip1559(..) => None, + } + } + + fn get_fee_per_gas(&self) -> (Option, Option) { + match self { + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }) => (Some(*max_fee_per_gas), Some(*max_priority_fee_per_gas)), + PayForGasOption::Legacy(..) => (None, None), } } } -#[derive(Debug, Display)] +impl TryFrom for PayForGasOption { + type Error = MmError; + + fn try_from(param: PayForGasParams) -> Result { + match param { + PayForGasParams::Legacy(legacy) => Ok(Self::Legacy(LegacyGasPrice { + gas_price: wei_from_gwei_decimal!(&legacy.gas_price)?, + })), + PayForGasParams::Eip1559(eip1559) => Ok(Self::Eip1559(Eip1559FeePerGas { + max_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_priority_fee_per_gas)?, + })), + } + } +} + +type GasDetails = (U256, PayForGasOption); + +#[derive(Debug, Display, EnumFromStringify)] pub enum Web3RpcError { #[display(fmt = "Transport: {}", _0)] Transport(String), + #[from_stringify("serde_json::Error")] #[display(fmt = "Invalid response: {}", _0)] InvalidResponse(String), #[display(fmt = "Timeout: {}", _0)] Timeout(String), #[display(fmt = "Internal: {}", _0)] Internal(String), -} - -impl From for Web3RpcError { - fn from(err: GasStationReqErr) -> Self { - match err { - GasStationReqErr::Transport { .. } => Web3RpcError::Transport(err.to_string()), - GasStationReqErr::InvalidResponse(err) => Web3RpcError::InvalidResponse(err), - GasStationReqErr::Internal(err) => Web3RpcError::Internal(err), - } - } -} - -impl From for Web3RpcError { - fn from(e: serde_json::Error) -> Self { Web3RpcError::InvalidResponse(e.to_string()) } + #[display(fmt = "Invalid gas api provider config: {}", _0)] + InvalidGasApiConfig(String), + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, + #[display(fmt = "Number conversion: {}", _0)] + NumConversError(String), } impl From for Web3RpcError { @@ -245,16 +414,16 @@ impl From for Web3RpcError { } } -impl From for RawTransactionError { - fn from(e: web3::Error) -> Self { RawTransactionError::Transport(e.to_string()) } -} - impl From for RawTransactionError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => RawTransactionError::Transport(tr), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { - RawTransactionError::InternalError(internal) + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => RawTransactionError::InternalError(internal), + Web3RpcError::NftProtocolNotSupported => { + RawTransactionError::InternalError("Nft Protocol is not supported yet!".to_string()) }, } } @@ -268,6 +437,10 @@ impl From for Web3RpcError { } } +impl From for Web3RpcError { + fn from(e: UnexpectedDerivationMethod) -> Self { Web3RpcError::Internal(e.to_string()) } +} + #[cfg(target_arch = "wasm32")] impl From for Web3RpcError { fn from(e: MetamaskError) -> Self { @@ -278,6 +451,10 @@ impl From for Web3RpcError { } } +impl From for Web3RpcError { + fn from(e: NumConversError) -> Self { Web3RpcError::NumConversError(e.to_string()) } +} + impl From for WithdrawError { fn from(e: ethabi::Error) -> Self { // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. @@ -294,13 +471,19 @@ impl From for WithdrawError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(err) | Web3RpcError::InvalidResponse(err) => WithdrawError::Transport(err), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { - WithdrawError::InternalError(internal) - }, + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => WithdrawError::InternalError(internal), + Web3RpcError::NftProtocolNotSupported => WithdrawError::NftProtocolNotSupported, } } } +impl From for WithdrawError { + fn from(e: ethcore_transaction::Error) -> Self { WithdrawError::SigningError(e.to_string()) } +} + impl From for TradePreimageError { fn from(e: web3::Error) -> Self { TradePreimageError::Transport(e.to_string()) } } @@ -309,9 +492,11 @@ impl From for TradePreimageError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(err) | Web3RpcError::InvalidResponse(err) => TradePreimageError::Transport(err), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { - TradePreimageError::InternalError(internal) - }, + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => TradePreimageError::InternalError(internal), + Web3RpcError::NftProtocolNotSupported => TradePreimageError::NftProtocolNotSupported, } } } @@ -340,11 +525,25 @@ impl From for BalanceError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => BalanceError::Transport(tr), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => BalanceError::Internal(internal), + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => BalanceError::Internal(internal), + Web3RpcError::NftProtocolNotSupported => { + BalanceError::Internal("Nft Protocol is not supported yet!".to_string()) + }, } } } +impl From for TransactionErr { + fn from(e: TxBuilderError) -> Self { TransactionErr::Plain(e.to_string()) } +} + +impl From for TransactionErr { + fn from(e: ethcore_transaction::Error) -> Self { TransactionErr::Plain(e.to_string()) } +} + #[derive(Debug, Deserialize, Serialize)] struct SavedTraces { /// ETH traces for my_address @@ -365,13 +564,19 @@ struct SavedErc20Events { latest_block: U64, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum EthCoinType { /// Ethereum itself or it's forks: ETC/others Eth, /// ERC20 token with smart contract address /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md - Erc20 { platform: String, token_addr: Address }, + Erc20 { + platform: String, + token_addr: Address, + }, + Nft { + platform: String, + }, } /// An alternative to `crate::PrivKeyBuildPolicy`, typical only for ETH coin. @@ -380,6 +585,7 @@ pub enum EthPrivKeyBuildPolicy { GlobalHDAccount(GlobalHDAccountArc), #[cfg(target_arch = "wasm32")] Metamask(MetamaskArc), + Trezor, } impl EthPrivKeyBuildPolicy { @@ -398,47 +604,82 @@ impl EthPrivKeyBuildPolicy { } } -impl TryFrom for EthPrivKeyBuildPolicy { - type Error = PrivKeyPolicyNotAllowed; - - /// Converts `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` - /// taking into account that ETH doesn't support `Trezor` yet. - fn try_from(policy: PrivKeyBuildPolicy) -> Result { +impl From for EthPrivKeyBuildPolicy { + fn from(policy: PrivKeyBuildPolicy) -> Self { match policy { - PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(EthPrivKeyBuildPolicy::IguanaPrivKey(iguana)), - PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => Ok(EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd)), - PrivKeyBuildPolicy::Trezor => Err(PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => EthPrivKeyBuildPolicy::IguanaPrivKey(iguana), + PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd), + PrivKeyBuildPolicy::Trezor => EthPrivKeyBuildPolicy::Trezor, } } } +/// Gas fee estimator loop context, runs a loop to estimate max fee and max priority fee per gas according to EIP-1559 for the next block +/// +/// This FeeEstimatorContext handles rpc requests which start and stop gas fee estimation loop and handles the loop itself. +/// FeeEstimatorContext keeps the latest estimated gas fees to return them on rpc request +pub(crate) struct FeeEstimatorContext { + /// Latest estimated gas fee values + pub(crate) estimated_fees: Arc>, + /// Handler for estimator loop graceful shutdown + pub(crate) abort_handler: AsyncMutex>, +} + +/// Gas fee estimator creation state +pub(crate) enum FeeEstimatorState { + /// Gas fee estimation not supported for this coin + CoinNotSupported, + /// Platform coin required to be enabled for gas fee estimation for this coin + PlatformCoinRequired, + /// Fee estimator created, use simple internal estimator + Simple(AsyncMutex), + /// Fee estimator created, use provider or simple internal estimator (if provider fails) + Provider(AsyncMutex), +} + /// pImpl idiom. pub struct EthCoinImpl { ticker: String, pub coin_type: EthCoinType, - priv_key_policy: EthPrivKeyPolicy, - my_address: Address, + pub(crate) priv_key_policy: EthPrivKeyPolicy, + /// Either an Iguana address or a 'EthHDWallet' instance. + /// Arc is used to use the same hd wallet from platform coin if we need to. + /// This allows the reuse of the same derived accounts/addresses of the + /// platform coin for tokens and vice versa. + derivation_method: Arc, sign_message_prefix: Option, swap_contract_address: Address, fallback_swap_contract: Option
, contract_supports_watchers: bool, - pub(crate) web3: Web3, - /// The separate web3 instances kept to get nonce, will replace the web3 completely soon - web3_instances: Vec, + web3_instances: AsyncMutex>, decimals: u8, - gas_station_url: Option, - gas_station_decimals: u8, - gas_station_policy: GasStationPricePolicy, history_sync_state: Mutex, required_confirmations: AtomicU64, + swap_txfee_policy: Mutex, + max_eth_tx_type: Option, /// Coin needs access to the context in order to reuse the logging and shutdown facilities. /// Using a weak reference by default in order to avoid circular references and leaks. pub ctx: MmWeak, - chain_id: Option, + chain_id: u64, + /// The name of the coin with which Trezor wallet associates this asset. + trezor_coin: Option, /// the block range used for eth_getLogs logs_block_range: u64, - nonce_lock: Arc>, + /// A mapping of Ethereum addresses to their respective nonce locks. + /// This is used to ensure that only one transaction is sent at a time per address. + /// Each address is associated with an `AsyncMutex` which is locked when a transaction is being created and sent, + /// and unlocked once the transaction is confirmed. This prevents nonce conflicts when multiple transactions + /// are initiated concurrently from the same address. + address_nonce_locks: Arc>>>>, erc20_tokens_infos: Arc>>, + /// Stores information about NFTs owned by the user. Each entry in the HashMap is uniquely identified by a composite key + /// consisting of the token address and token ID, separated by a comma. This field is essential for tracking the NFT assets + /// information (chain & contract type, amount etc.), where ownership and amount, in ERC1155 case, might change over time. + pub nfts_infos: Arc>>, + /// Context for eth fee per gas estimator loop. Created if coin supports fee per gas estimation + pub(crate) platform_fee_estimator_state: Arc, + /// Config provided gas limits for swap and send transactions + pub(crate) gas_limit: EthGasLimit, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation /// and on [`MmArc::stop`]. pub abortable_system: AbortableQueue, @@ -450,9 +691,13 @@ pub struct Web3Instance { is_parity: bool, } +/// Information about a token that follows the ERC20 protocol on an EVM-based network. #[derive(Clone, Debug)] pub struct Erc20TokenInfo { + /// The contract address of the token on the EVM-based network. pub token_address: Address, + /// The number of decimal places the token uses. + /// This represents the smallest unit that the token can be divided into. pub decimals: u8, } @@ -468,93 +713,32 @@ pub enum EthAddressFormat { MixedCase, } -#[cfg_attr(test, mockable)] -async fn make_gas_station_request(url: &str) -> GasStationResult { - let resp = slurp_url(url).await?; - if resp.0 != StatusCode::OK { - let error = format!("Gas price request failed with status code {}", resp.0); - return MmError::err(GasStationReqErr::Transport { - uri: url.to_owned(), - error, - }); - } - let result: GasStationData = json::from_slice(&resp.2)?; - Ok(result) +/// get tx type from pay_for_gas_option +/// currently only type2 and legacy supported +/// if for Eth Classic we also want support for type 1 then use a fn +#[macro_export] +macro_rules! tx_type_from_pay_for_gas_option { + ($pay_for_gas_option: expr) => { + if matches!($pay_for_gas_option, PayForGasOption::Eip1559(..)) { + ethcore_transaction::TxType::Type2 + } else { + ethcore_transaction::TxType::Legacy + } + }; } impl EthCoinImpl { - /// Gets Transfer events from ERC20 smart contract `addr` between `from_block` and `to_block` - fn erc20_transfer_events( - &self, - contract: Address, - from_addr: Option
, - to_addr: Option
, - from_block: BlockNumber, - to_block: BlockNumber, - limit: Option, - ) -> Box, Error = String> + Send> { - let contract_event = try_fus!(ERC20_CONTRACT.event("Transfer")); - let topic0 = Some(vec![contract_event.signature()]); - let topic1 = from_addr.map(|addr| vec![addr.into()]); - let topic2 = to_addr.map(|addr| vec![addr.into()]); - let mut filter = FilterBuilder::default() - .topics(topic0, topic1, topic2, None) - .from_block(from_block) - .to_block(to_block) - .address(vec![contract]); - - if let Some(l) = limit { - filter = filter.limit(l); - } - - Box::new( - self.web3 - .eth() - .logs(filter.build()) - .compat() - .map_err(|e| ERRL!("{}", e)), - ) - } - - /// Gets ETH traces from ETH node between addresses in `from_block` and `to_block` - fn eth_traces( - &self, - from_addr: Vec
, - to_addr: Vec
, - from_block: BlockNumber, - to_block: BlockNumber, - limit: Option, - ) -> Box, Error = String> + Send> { - let mut filter = TraceFilterBuilder::default() - .from_address(from_addr) - .to_address(to_addr) - .from_block(from_block) - .to_block(to_block); - - if let Some(l) = limit { - filter = filter.count(l); - } - - Box::new( - self.web3 - .trace() - .filter(filter.build()) - .compat() - .map_err(|e| ERRL!("{}", e)), - ) - } - #[cfg(not(target_arch = "wasm32"))] - fn eth_traces_path(&self, ctx: &MmArc) -> PathBuf { + fn eth_traces_path(&self, ctx: &MmArc, my_address: Address) -> PathBuf { ctx.dbdir() .join("TRANSACTIONS") - .join(format!("{}_{:#02x}_trace.json", self.ticker, self.my_address)) + .join(format!("{}_{:#02x}_trace.json", self.ticker, my_address)) } /// Load saved ETH traces from local DB #[cfg(not(target_arch = "wasm32"))] - fn load_saved_traces(&self, ctx: &MmArc) -> Option { - let content = gstuff::slurp(&self.eth_traces_path(ctx)); + fn load_saved_traces(&self, ctx: &MmArc, my_address: Address) -> Option { + let content = gstuff::slurp(&self.eth_traces_path(ctx, my_address)); if content.is_empty() { None } else { @@ -567,54 +751,54 @@ impl EthCoinImpl { /// Load saved ETH traces from local DB #[cfg(target_arch = "wasm32")] - fn load_saved_traces(&self, _ctx: &MmArc) -> Option { + fn load_saved_traces(&self, _ctx: &MmArc, _my_address: Address) -> Option { common::panic_w("'load_saved_traces' is not implemented in WASM"); unreachable!() } /// Store ETH traces to local DB #[cfg(not(target_arch = "wasm32"))] - fn store_eth_traces(&self, ctx: &MmArc, traces: &SavedTraces) { + fn store_eth_traces(&self, ctx: &MmArc, my_address: Address, traces: &SavedTraces) { let content = json::to_vec(traces).unwrap(); - let tmp_file = format!("{}.tmp", self.eth_traces_path(ctx).display()); + let tmp_file = format!("{}.tmp", self.eth_traces_path(ctx, my_address).display()); std::fs::write(&tmp_file, content).unwrap(); - std::fs::rename(tmp_file, self.eth_traces_path(ctx)).unwrap(); + std::fs::rename(tmp_file, self.eth_traces_path(ctx, my_address)).unwrap(); } /// Store ETH traces to local DB #[cfg(target_arch = "wasm32")] - fn store_eth_traces(&self, _ctx: &MmArc, _traces: &SavedTraces) { + fn store_eth_traces(&self, _ctx: &MmArc, _my_address: Address, _traces: &SavedTraces) { common::panic_w("'store_eth_traces' is not implemented in WASM"); unreachable!() } #[cfg(not(target_arch = "wasm32"))] - fn erc20_events_path(&self, ctx: &MmArc) -> PathBuf { + fn erc20_events_path(&self, ctx: &MmArc, my_address: Address) -> PathBuf { ctx.dbdir() .join("TRANSACTIONS") - .join(format!("{}_{:#02x}_events.json", self.ticker, self.my_address)) + .join(format!("{}_{:#02x}_events.json", self.ticker, my_address)) } /// Store ERC20 events to local DB #[cfg(not(target_arch = "wasm32"))] - fn store_erc20_events(&self, ctx: &MmArc, events: &SavedErc20Events) { + fn store_erc20_events(&self, ctx: &MmArc, my_address: Address, events: &SavedErc20Events) { let content = json::to_vec(events).unwrap(); - let tmp_file = format!("{}.tmp", self.erc20_events_path(ctx).display()); + let tmp_file = format!("{}.tmp", self.erc20_events_path(ctx, my_address).display()); std::fs::write(&tmp_file, content).unwrap(); - std::fs::rename(tmp_file, self.erc20_events_path(ctx)).unwrap(); + std::fs::rename(tmp_file, self.erc20_events_path(ctx, my_address)).unwrap(); } /// Store ERC20 events to local DB #[cfg(target_arch = "wasm32")] - fn store_erc20_events(&self, _ctx: &MmArc, _events: &SavedErc20Events) { + fn store_erc20_events(&self, _ctx: &MmArc, _my_address: Address, _events: &SavedErc20Events) { common::panic_w("'store_erc20_events' is not implemented in WASM"); unreachable!() } /// Load saved ERC20 events from local DB #[cfg(not(target_arch = "wasm32"))] - fn load_saved_erc20_events(&self, ctx: &MmArc) -> Option { - let content = gstuff::slurp(&self.erc20_events_path(ctx)); + fn load_saved_erc20_events(&self, ctx: &MmArc, my_address: Address) -> Option { + let content = gstuff::slurp(&self.erc20_events_path(ctx, my_address)); if content.is_empty() { None } else { @@ -627,37 +811,21 @@ impl EthCoinImpl { /// Load saved ERC20 events from local DB #[cfg(target_arch = "wasm32")] - fn load_saved_erc20_events(&self, _ctx: &MmArc) -> Option { + fn load_saved_erc20_events(&self, _ctx: &MmArc, _my_address: Address) -> Option { common::panic_w("'load_saved_erc20_events' is not implemented in WASM"); unreachable!() } /// The id used to differentiate payments on Etomic swap smart contract - fn etomic_swap_id(&self, time_lock: u32, secret_hash: &[u8]) -> Vec { - let mut input = vec![]; - input.extend_from_slice(&time_lock.to_le_bytes()); + pub(crate) fn etomic_swap_id(&self, time_lock: u32, secret_hash: &[u8]) -> Vec { + let timelock_bytes = time_lock.to_le_bytes(); + + let mut input = Vec::with_capacity(timelock_bytes.len() + secret_hash.len()); + input.extend_from_slice(&timelock_bytes); input.extend_from_slice(secret_hash); sha256(&input).to_vec() } - /// Gets `SenderRefunded` events from etomic swap smart contract since `from_block` - fn refund_events( - &self, - swap_contract_address: Address, - from_block: u64, - to_block: u64, - ) -> Box, Error = String> + Send> { - let contract_event = try_fus!(SWAP_CONTRACT.event("SenderRefunded")); - let filter = FilterBuilder::default() - .topics(Some(vec![contract_event.signature()]), None, None, None) - .from_block(BlockNumber::Number(from_block.into())) - .to_block(BlockNumber::Number(to_block.into())) - .address(vec![swap_contract_address]) - .build(); - - Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) - } - /// Try to parse address from string. pub fn address_from_str(&self, address: &str) -> Result { Ok(try_s!(valid_addr_from_str(address))) @@ -666,7 +834,7 @@ impl EthCoinImpl { pub fn erc20_token_address(&self) -> Option
{ match self.coin_type { EthCoinType::Erc20 { token_addr, .. } => Some(token_addr), - EthCoinType::Eth => None, + EthCoinType::Eth | EthCoinType::Nft { .. } => None, } } @@ -694,8 +862,6 @@ async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> async fn get_tx_hex_by_hash_impl(coin: EthCoin, tx_hash: H256) -> RawTransactionResult { let web3_tx = coin - .web3 - .eth() .transaction(TransactionId::Hash(tx_hash)) .await? .or_mm_err(|| RawTransactionError::HashNotExist(tx_hash.to_string()))?; @@ -706,170 +872,19 @@ async fn get_tx_hex_by_hash_impl(coin: EthCoin, tx_hash: H256) -> RawTransaction } async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { - let to_addr = coin - .address_from_str(&req.to) - .map_to_mm(WithdrawError::InvalidAddress)?; - let (my_balance, my_address, key_pair) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { - let raw_priv_key = coin - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; - let key_pair = KeyPair::from_secret_slice(raw_priv_key.as_slice()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let address = key_pair.address(); - let balance = coin.address_balance(address).compat().await?; - (balance, address, key_pair) - }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for EVM!".to_string(), - )) - }, - None => ( - coin.my_balance().compat().await?, - coin.my_address, - coin.priv_key_policy.activated_key_or_err()?.clone(), - ), - }; - let my_balance_dec = u256_to_big_decimal(my_balance, coin.decimals)?; - - let (mut wei_amount, dec_amount) = if req.max { - (my_balance, my_balance_dec.clone()) - } else { - let wei_amount = wei_from_big_decimal(&req.amount, coin.decimals)?; - (wei_amount, req.amount.clone()) - }; - if wei_amount > my_balance { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: my_balance_dec.clone(), - required: dec_amount, - }); - }; - let (mut eth_value, data, call_addr, fee_coin) = match &coin.coin_type { - EthCoinType::Eth => (wei_amount, vec![], to_addr, coin.ticker()), - EthCoinType::Erc20 { platform, token_addr } => { - let function = ERC20_CONTRACT.function("transfer")?; - let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(wei_amount)])?; - (0.into(), data, *token_addr, platform.as_str()) - }, - }; - let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; - - let (gas, gas_price) = - get_eth_gas_details(&coin, req.fee, eth_value, data.clone().into(), call_addr, req.max).await?; - let total_fee = gas * gas_price; - let total_fee_dec = u256_to_big_decimal(total_fee, coin.decimals)?; - - if req.max && coin.coin_type == EthCoinType::Eth { - if eth_value < total_fee || wei_amount < total_fee { - return MmError::err(WithdrawError::AmountTooLow { - amount: eth_value_dec, - threshold: total_fee_dec, - }); - } - eth_value -= total_fee; - wei_amount -= total_fee; - }; - - let (tx_hash, tx_hex) = match coin.priv_key_policy { - EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { - // Todo: nonce_lock is still global for all addresses but this needs to be per address - let _nonce_lock = coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(my_address, coin.web3_instances.clone()) - .compat() - .timeout_secs(30.) - .await? - .map_to_mm(WithdrawError::Transport)?; - - let tx = UnSignedEthTx { - nonce, - value: eth_value, - action: Action::Call(call_addr), - data, - gas, - gas_price, - }; - - let signed = tx.sign(key_pair.secret(), coin.chain_id); - let bytes = rlp::encode(&signed); - - (signed.hash, BytesJson::from(bytes.to_vec())) - }, - EthPrivKeyPolicy::Trezor => { - return MmError::err(WithdrawError::UnsupportedError( - "Trezor is not supported for EVM yet!".to_string(), - )) - }, - #[cfg(target_arch = "wasm32")] - EthPrivKeyPolicy::Metamask(_) => { - if !req.broadcast { - let error = "Set 'broadcast' to generate, sign and broadcast a transaction with MetaMask".to_string(); - return MmError::err(WithdrawError::BroadcastExpected(error)); - } - - let tx_to_send = TransactionRequest { - from: coin.my_address, - to: Some(to_addr), - gas: Some(gas), - gas_price: Some(gas_price), - value: Some(eth_value), - data: Some(data.clone().into()), - nonce: None, - ..TransactionRequest::default() - }; - - // Wait for 10 seconds for the transaction to appear on the RPC node. - let wait_rpc_timeout = 10_000; - let check_every = 1.; - - // Please note that this method may take a long time - // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = coin.web3.eth().send_transaction(tx_to_send).await?; - - let signed_tx = coin - .wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) - .await?; - let tx_hex = signed_tx - .map(|tx| BytesJson::from(rlp::encode(&tx).to_vec())) - // Return an empty `tx_hex` if the transaction is still not appeared on the RPC node. - .unwrap_or_default(); - (tx_hash, tx_hex) - }, - }; - - let tx_hash_bytes = BytesJson::from(tx_hash.0.to_vec()); - let tx_hash_str = format!("{:02x}", tx_hash_bytes); + StandardEthWithdraw::new(coin.clone(), req)?.build().await +} - let amount_decimal = u256_to_big_decimal(wei_amount, coin.decimals)?; - let mut spent_by_me = amount_decimal.clone(); - let received_by_me = if to_addr == my_address { - amount_decimal.clone() - } else { - 0.into() - }; - let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; - if coin.coin_type == EthCoinType::Eth { - spent_by_me += &fee_details.total_fee; - } - Ok(TransactionDetails { - to: vec![checksum_address(&format!("{:#02x}", to_addr))], - from: vec![checksum_address(&format!("{:#02x}", my_address))], - total_amount: amount_decimal, - my_balance_change: &received_by_me - &spent_by_me, - spent_by_me, - received_by_me, - tx_hex, - tx_hash: tx_hash_str, - block_height: 0, - fee_details: Some(fee_details.into()), - coin: coin.ticker.clone(), - internal_id: vec![].into(), - timestamp: now_sec(), - kmd_rewards: None, - transaction_type: Default::default(), - memo: None, - }) +#[async_trait] +impl InitWithdrawCoin for EthCoin { + async fn init_withdraw( + &self, + ctx: MmArc, + req: WithdrawRequest, + task_handle: WithdrawTaskHandleShared, + ) -> Result> { + InitEthWithdraw::new(ctx, self.clone(), req, task_handle)?.build().await + } } /// `withdraw_erc1155` function returns details of `ERC-1155` transaction including tx hex, @@ -877,16 +892,11 @@ async fn withdraw_impl(coin: EthCoin, req: WithdrawRequest) -> WithdrawResult { pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> WithdrawNftResult { let coin = lp_coinfind_or_err(&ctx, withdraw_type.chain.to_ticker()).await?; let (to_addr, token_addr, eth_coin) = - get_valid_nft_add_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; - let my_address = eth_coin.my_address()?; - - let wallet_amount = find_wallet_nft_amount( - &ctx, - &withdraw_type.chain, - withdraw_type.token_address.to_lowercase(), - withdraw_type.token_id.clone(), - ) - .await?; + get_valid_nft_addr_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; + + let token_id_str = &withdraw_type.token_id.to_string(); + let wallet_amount = eth_coin.erc1155_balance(token_addr, token_id_str).await?; + let amount_dec = if withdraw_type.max { wallet_amount.clone() } else { @@ -902,17 +912,16 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit }); } + let my_address = eth_coin.derivation_method.single_addr_or_err().await?; let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { EthCoinType::Eth => { let function = ERC1155_CONTRACT.function("safeTransferFrom")?; - let token_id_u256 = U256::from_dec_str(&withdraw_type.token_id.to_string()) - .map_err(|e| format!("{:?}", e)) - .map_to_mm(NumConversError::new)?; - let amount_u256 = U256::from_dec_str(&amount_dec.to_string()) - .map_err(|e| format!("{:?}", e)) - .map_to_mm(NumConversError::new)?; + let token_id_u256 = + U256::from_dec_str(token_id_str).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; + let amount_u256 = + U256::from_dec_str(&amount_dec.to_string()).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; let data = function.encode_input(&[ - Token::Address(eth_coin.my_address), + Token::Address(my_address), Token::Address(to_addr), Token::Uint(token_id_u256), Token::Uint(amount_u256), @@ -925,41 +934,46 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit "Erc20 coin type doesnt support withdraw nft".to_owned(), )) }, + EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), }; - let (gas, gas_price) = get_eth_gas_details( + let (gas, pay_for_gas_option) = get_eth_gas_details_from_withdraw_fee( ð_coin, withdraw_type.fee, eth_value, data.clone().into(), + my_address, call_addr, false, ) .await?; - let _nonce_lock = eth_coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) + let address_lock = eth_coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let (nonce, _) = eth_coin + .clone() + .get_addr_nonce(my_address) .compat() .timeout_secs(30.) .await? .map_to_mm(WithdrawError::Transport)?; - let tx = UnSignedEthTx { - nonce, - value: eth_value, - action: Action::Call(call_addr), - data, - gas, - gas_price, - }; - + let tx_type = tx_type_from_pay_for_gas_option!(pay_for_gas_option); + if !eth_coin.is_tx_type_supported(&tx_type) { + return MmError::err(WithdrawError::TxTypeNotSupported); + } + let tx_builder = UnSignedEthTxBuilder::new(tx_type, nonce, gas, Action::Call(call_addr), eth_value, data); + let tx_builder = tx_builder_with_pay_for_gas_option(ð_coin, tx_builder, &pay_for_gas_option)?; + let tx = tx_builder + .build() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, eth_coin.chain_id); + let signed = tx.sign(secret, Some(eth_coin.chain_id))?; let signed_bytes = rlp::encode(&signed); - let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), - tx_hash: format!("{:02x}", signed.tx_hash()), - from: vec![my_address], + tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), + from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc1155, token_address: withdraw_type.token_address, @@ -979,17 +993,26 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> WithdrawNftResult { let coin = lp_coinfind_or_err(&ctx, withdraw_type.chain.to_ticker()).await?; let (to_addr, token_addr, eth_coin) = - get_valid_nft_add_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; - let my_address = eth_coin.my_address()?; + get_valid_nft_addr_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; + + let token_id_str = &withdraw_type.token_id.to_string(); + let token_owner = eth_coin.erc721_owner(token_addr, token_id_str).await?; + let my_address = eth_coin.derivation_method.single_addr_or_err().await?; + if token_owner != my_address { + return MmError::err(WithdrawError::MyAddressNotNftOwner { + my_address: eth_addr_to_hex(&my_address), + token_owner: eth_addr_to_hex(&token_owner), + }); + } + let my_address = eth_coin.derivation_method.single_addr_or_err().await?; let (eth_value, data, call_addr, fee_coin) = match eth_coin.coin_type { EthCoinType::Eth => { let function = ERC721_CONTRACT.function("safeTransferFrom")?; let token_id_u256 = U256::from_dec_str(&withdraw_type.token_id.to_string()) - .map_err(|e| format!("{:?}", e)) - .map_to_mm(NumConversError::new)?; + .map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; let data = function.encode_input(&[ - Token::Address(eth_coin.my_address), + Token::Address(my_address), Token::Address(to_addr), Token::Uint(token_id_u256), ])?; @@ -1000,41 +1023,48 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd "Erc20 coin type doesnt support withdraw nft".to_owned(), )) }, + // TODO: start to use NFT GLOBAL TOKEN for withdraw + EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), }; - let (gas, gas_price) = get_eth_gas_details( + let (gas, pay_for_gas_option) = get_eth_gas_details_from_withdraw_fee( ð_coin, withdraw_type.fee, eth_value, data.clone().into(), + my_address, call_addr, false, ) .await?; - let _nonce_lock = eth_coin.nonce_lock.lock().await; - let (nonce, _) = get_addr_nonce(eth_coin.my_address, eth_coin.web3_instances.clone()) + + let address_lock = eth_coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let (nonce, _) = eth_coin + .clone() + .get_addr_nonce(my_address) .compat() .timeout_secs(30.) .await? .map_to_mm(WithdrawError::Transport)?; - let tx = UnSignedEthTx { - nonce, - value: eth_value, - action: Action::Call(call_addr), - data, - gas, - gas_price, - }; - + let tx_type = tx_type_from_pay_for_gas_option!(pay_for_gas_option); + if !eth_coin.is_tx_type_supported(&tx_type) { + return MmError::err(WithdrawError::TxTypeNotSupported); + } + let tx_builder = UnSignedEthTxBuilder::new(tx_type, nonce, gas, Action::Call(call_addr), eth_value, data); + let tx_builder = tx_builder_with_pay_for_gas_option(ð_coin, tx_builder, &pay_for_gas_option)?; + let tx = tx_builder + .build() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, eth_coin.chain_id); + let signed = tx.sign(secret, Some(eth_coin.chain_id))?; let signed_bytes = rlp::encode(&signed); - let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), - tx_hash: format!("{:02x}", signed.tx_hash()), - from: vec![my_address], + tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), + from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc721, token_address: withdraw_type.token_address, @@ -1058,7 +1088,7 @@ impl Deref for EthCoin { #[async_trait] impl SwapOps for EthCoin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let address = try_tx_fus!(addr_from_raw_pubkey(fee_addr)); Box::new( @@ -1084,32 +1114,34 @@ impl SwapOps for EthCoin { ) } - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - Box::new( - self.spend_hash_time_locked_payment(maker_spends_payment_args) - .map(TransactionEnum::from), - ) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_hash_time_locked_payment(maker_spends_payment_args) + .await + .map(TransactionEnum::from) } - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - Box::new( - self.spend_hash_time_locked_payment(taker_spends_payment_args) - .map(TransactionEnum::from), - ) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_hash_time_locked_payment(taker_spends_payment_args) + .await + .map(TransactionEnum::from) } async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { self.refund_hash_time_locked_payment(taker_refunds_payment_args) - .map(TransactionEnum::from) - .compat() .await + .map(TransactionEnum::from) } async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { self.refund_hash_time_locked_payment(maker_refunds_payment_args) - .map(TransactionEnum::from) - .compat() .await + .map(TransactionEnum::from) } fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()> { @@ -1118,7 +1150,7 @@ impl SwapOps for EthCoin { _ => panic!(), }; validate_fee_impl(self.clone(), EthValidateFeeArgs { - fee_tx_hash: &tx.hash, + fee_tx_hash: &tx.tx_hash(), expected_sender: validate_fee_args.expected_sender, fee_addr: validate_fee_args.fee_addr, amount: &validate_fee_args.dex_fee.fee_amount().into(), @@ -1128,13 +1160,13 @@ impl SwapOps for EthCoin { } #[inline] - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - self.validate_payment(input) + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + self.validate_payment(input).compat().await } #[inline] - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - self.validate_payment(input) + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + self.validate_payment(input).compat().await } fn check_if_my_payment_sent( @@ -1183,8 +1215,6 @@ impl SwapOps for EthCoin { Some(event) => { let transaction = try_s!( selfi - .web3 - .eth() .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) .await ); @@ -1245,14 +1275,14 @@ impl SwapOps for EthCoin { spend_tx: &[u8], watcher_reward: bool, ) -> Result, String> { - let unverified: UnverifiedTransaction = try_s!(rlp::decode(spend_tx)); + let unverified: UnverifiedTransactionWrapper = try_s!(rlp::decode(spend_tx)); let function_name = get_function_name("receiverSpend", watcher_reward); let function = try_s!(SWAP_CONTRACT.function(&function_name)); // Validate contract call; expected to be receiverSpend. // https://www.4byte.directory/signatures/?bytes4_signature=02ed292b. let expected_signature = function.short_signature(); - let actual_signature = &unverified.data[0..4]; + let actual_signature = &unverified.unsigned().data()[0..4]; if actual_signature != expected_signature { return ERR!( "Expected 'receiverSpend' contract call signature: {:?}, found {:?}", @@ -1261,7 +1291,7 @@ impl SwapOps for EthCoin { ); }; - let tokens = try_s!(decode_contract_call(function, &unverified.data)); + let tokens = try_s!(decode_contract_call(function, unverified.unsigned().data())); if tokens.len() < 3 { return ERR!("Invalid arguments in 'receiverSpend' call: {:?}", tokens); } @@ -1432,7 +1462,7 @@ impl WatcherOps for EthCoin { _secret_hash: &[u8], _swap_unique_data: &[u8], ) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_payment_tx)); + let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(maker_payment_tx)); let signed = try_tx_fus!(SignedEthTx::new(tx)); let fut = async move { Ok(TransactionEnum::from(signed)) }; @@ -1448,7 +1478,7 @@ impl WatcherOps for EthCoin { _swap_contract_address: &Option, _swap_unique_data: &[u8], ) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_payment_tx)); + let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(taker_payment_tx)); let signed = try_tx_fus!(SignedEthTx::new(tx)); let fut = async move { Ok(TransactionEnum::from(signed)) }; @@ -1481,14 +1511,14 @@ impl WatcherOps for EthCoin { .watcher_reward .clone() .ok_or_else(|| ValidatePaymentError::WatcherRewardError("Watcher reward not found".to_string()))); - let expected_reward_amount = try_f!(wei_from_big_decimal(&watcher_reward.amount, self.decimals)); + let expected_reward_amount = try_f!(wei_from_big_decimal(&watcher_reward.amount, ETH_DECIMALS)); let expected_swap_contract_address = try_f!(input .swap_contract_address .try_to_address() .map_to_mm(ValidatePaymentError::InvalidParameter)); - let unsigned: UnverifiedTransaction = try_f!(rlp::decode(&input.payment_tx)); + let unsigned: UnverifiedTransactionWrapper = try_f!(rlp::decode(&input.payment_tx)); let tx = try_f!(SignedEthTx::new(unsigned) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))); @@ -1510,9 +1540,9 @@ impl WatcherOps for EthCoin { let trade_amount = try_f!(wei_from_big_decimal(&(input.amount), decimals)); let fut = async move { - match tx.action { + match tx.unsigned().action() { Call(contract_address) => { - if contract_address != expected_swap_contract_address { + if *contract_address != expected_swap_contract_address { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Transaction {:?} was sent to wrong address, expected {:?}", contract_address, expected_swap_contract_address, @@ -1550,7 +1580,7 @@ impl WatcherOps for EthCoin { .function(&function_name) .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; - let decoded = decode_contract_call(function, &tx.data) + let decoded = decode_contract_call(function, tx.unsigned().data()) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; let swap_id_input = get_function_input_data(&decoded, function, 0) @@ -1588,11 +1618,12 @@ impl WatcherOps for EthCoin { ))); } + let my_address = selfi.derivation_method.single_addr_or_err().await?; let sender_input = get_function_input_data(&decoded, function, 4) .map_to_mm(ValidatePaymentError::TxDeserializationError)?; let expected_sender = match input.spend_type { WatcherSpendType::MakerPaymentSpend => maker_addr, - WatcherSpendType::TakerPaymentRefund => selfi.my_address, + WatcherSpendType::TakerPaymentRefund => my_address, }; if sender_input != Token::Address(expected_sender) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( @@ -1605,7 +1636,7 @@ impl WatcherOps for EthCoin { let receiver_input = get_function_input_data(&decoded, function, 5) .map_to_mm(ValidatePaymentError::TxDeserializationError)?; let expected_receiver = match input.spend_type { - WatcherSpendType::MakerPaymentSpend => selfi.my_address, + WatcherSpendType::MakerPaymentSpend => my_address, WatcherSpendType::TakerPaymentRefund => maker_addr, }; if receiver_input != Token::Address(expected_receiver) { @@ -1646,10 +1677,10 @@ impl WatcherOps for EthCoin { ))); } - if tx.value != U256::zero() { + if tx.unsigned().value() != U256::zero() { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Transaction value arg {:?} is invalid, expected 0", - tx.value + tx.unsigned().value() ))); } @@ -1659,10 +1690,12 @@ impl WatcherOps for EthCoin { .map_to_mm(ValidatePaymentError::TxDeserializationError)?; let total_amount = match input.spend_type { WatcherSpendType::MakerPaymentSpend => { - if let RewardTarget::None = watcher_reward.reward_target { - trade_amount - } else { + if !matches!(watcher_reward.reward_target, RewardTarget::None) + || watcher_reward.send_contract_reward_on_spend + { trade_amount + expected_reward_amount + } else { + trade_amount } }, WatcherSpendType::TakerPaymentRefund => trade_amount + expected_reward_amount, @@ -1709,6 +1742,7 @@ impl WatcherOps for EthCoin { ))); } }, + EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), } Ok(()) @@ -1717,7 +1751,7 @@ impl WatcherOps for EthCoin { } fn watcher_validate_taker_payment(&self, input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - let unsigned: UnverifiedTransaction = try_f!(rlp::decode(&input.payment_tx)); + let unsigned: UnverifiedTransactionWrapper = try_f!(rlp::decode(&input.payment_tx)); let tx = try_f!(SignedEthTx::new(unsigned) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))); @@ -1737,10 +1771,9 @@ impl WatcherOps for EthCoin { }; let expected_swap_contract_address = self.swap_contract_address; let fallback_swap_contract = self.fallback_swap_contract; - let decimals = self.decimals; let fut = async move { - let tx_from_rpc = selfi.web3.eth().transaction(TransactionId::Hash(tx.hash)).await?; + let tx_from_rpc = selfi.transaction(TransactionId::Hash(tx.tx_hash())).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx)) @@ -1781,7 +1814,7 @@ impl WatcherOps for EthCoin { .get_taker_watcher_reward(&input.maker_coin, None, None, None, input.wait_until) .await .map_err(|err| ValidatePaymentError::WatcherRewardError(err.into_inner().to_string()))?; - let expected_reward_amount = wei_from_big_decimal(&watcher_reward.amount, decimals)?; + let expected_reward_amount = wei_from_big_decimal(&watcher_reward.amount, ETH_DECIMALS)?; match &selfi.coin_type { EthCoinType::Eth => { @@ -1950,6 +1983,7 @@ impl WatcherOps for EthCoin { ))); } }, + EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), } Ok(()) @@ -1961,10 +1995,10 @@ impl WatcherOps for EthCoin { &self, input: WatcherSearchForSwapTxSpendInput<'_>, ) -> Result, String> { - let unverified: UnverifiedTransaction = try_s!(rlp::decode(input.tx)); + let unverified: UnverifiedTransactionWrapper = try_s!(rlp::decode(input.tx)); let tx = try_s!(SignedEthTx::new(unverified)); - let swap_contract_address = match tx.action { - Call(address) => address, + let swap_contract_address = match tx.unsigned().action() { + Call(address) => *address, Create => return Err(ERRL!("Invalid payment action: the payment action cannot be create")), }; @@ -1992,7 +2026,6 @@ impl WatcherOps for EthCoin { RewardTarget::PaymentSender }; - let is_exact_amount = reward_amount.is_some(); let amount = match reward_amount { Some(amount) => amount, None => self.get_watcher_reward_amount(wait_until).await?, @@ -2002,7 +2035,7 @@ impl WatcherOps for EthCoin { Ok(WatcherReward { amount, - is_exact_amount, + is_exact_amount: false, reward_target, send_contract_reward_on_spend, }) @@ -2043,6 +2076,11 @@ impl WatcherOps for EthCoin { })? } }, + EthCoinType::Nft { .. } => { + return MmError::err(WatcherRewardError::InternalError( + "Nft Protocol is not supported yet!".to_string(), + )) + }, } }, }; @@ -2058,15 +2096,22 @@ impl WatcherOps for EthCoin { } } +#[async_trait] #[cfg_attr(test, mockable)] +#[async_trait] impl MarketCoinOps for EthCoin { fn ticker(&self) -> &str { &self.ticker[..] } fn my_address(&self) -> MmResult { - Ok(checksum_address(&format!("{:#02x}", self.my_address))) + match self.derivation_method() { + DerivationMethod::SingleAddress(my_address) => Ok(display_eth_address(my_address)), + DerivationMethod::HDWallet(_) => MmError::err(MyAddressError::UnexpectedDerivationMethod( + "'my_address' is deprecated for HD wallets".to_string(), + )), + } } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { match self.priv_key_policy { EthPrivKeyPolicy::Iguana(ref key_pair) | EthPrivKeyPolicy::HDWallet { @@ -2076,7 +2121,19 @@ impl MarketCoinOps for EthCoin { let uncompressed_without_prefix = hex::encode(key_pair.public()); Ok(format!("04{}", uncompressed_without_prefix)) }, - EthPrivKeyPolicy::Trezor => MmError::err(UnexpectedDerivationMethod::Trezor), + EthPrivKeyPolicy::Trezor => { + let public_key = self + .deref() + .derivation_method + .hd_wallet() + .ok_or(UnexpectedDerivationMethod::ExpectedHDWallet)? + .get_enabled_address() + .await + .ok_or_else(|| UnexpectedDerivationMethod::InternalError("no enabled address".to_owned()))? + .pubkey(); + let uncompressed_without_prefix = hex::encode(public_key); + Ok(format!("04{}", uncompressed_without_prefix)) + }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(ref metamask_policy) => { Ok(format!("{:02x}", metamask_policy.public_key_uncompressed)) @@ -2120,7 +2177,7 @@ impl MarketCoinOps for EthCoin { fn my_balance(&self) -> BalanceFut { let decimals = self.decimals; let fut = self - .my_balance() + .get_balance() .and_then(move |result| Ok(u256_to_big_decimal(result, decimals)?)) .map(|spendable| CoinBalance { spendable, @@ -2139,7 +2196,7 @@ impl MarketCoinOps for EthCoin { fn platform_ticker(&self) -> &str { match &self.coin_type { EthCoinType::Eth => self.ticker(), - EthCoinType::Erc20 { platform, .. } => platform, + EthCoinType::Erc20 { platform, .. } | EthCoinType::Nft { platform } => platform, } } @@ -2148,25 +2205,39 @@ impl MarketCoinOps for EthCoin { tx = &tx[2..]; } let bytes = try_fus!(hex::decode(tx)); - Box::new( - self.web3 - .eth() - .send_raw_transaction(bytes.into()) - .compat() - .map(|res| format!("{:02x}", res)) - .map_err(|e| ERRL!("{}", e)), - ) + + let coin = self.clone(); + + let fut = async move { + coin.send_raw_transaction(bytes.into()) + .await + .map(|res| format!("{:02x}", res)) + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) } fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { - Box::new( - self.web3 - .eth() - .send_raw_transaction(tx.into()) - .compat() + let coin = self.clone(); + + let tx = tx.to_owned(); + let fut = async move { + coin.send_raw_transaction(tx.into()) + .await .map(|res| format!("{:02x}", res)) - .map_err(|e| ERRL!("{}", e)), - ) + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) + } + + async fn sign_raw_tx(&self, args: &SignRawTransactionRequest) -> RawTransactionResult { + if let SignRawTransactionEnum::ETH(eth_args) = &args.tx { + sign_raw_eth_tx(self, eth_args).await + } else { + MmError::err(RawTransactionError::InvalidParam("eth type expected".to_string())) + } } fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { @@ -2184,9 +2255,9 @@ impl MarketCoinOps for EthCoin { status.status(&[&self.ticker], "Waiting for confirmations…"); status.deadline(input.wait_until * 1000); - let unsigned: UnverifiedTransaction = try_fus!(rlp::decode(&input.payment_tx)); + let unsigned: UnverifiedTransactionWrapper = try_fus!(rlp::decode(&input.payment_tx)); let tx = try_fus!(SignedEthTx::new(unsigned)); - let tx_hash = tx.hash(); + let tx_hash = tx.tx_hash(); let required_confirms = U64::from(input.confirmations); let check_every = input.check_every as f64; @@ -2257,13 +2328,13 @@ impl MarketCoinOps for EthCoin { } fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { - let unverified: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.tx_bytes)); + let unverified: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(args.tx_bytes)); let tx = try_tx_fus!(SignedEthTx::new(unverified)); let swap_contract_address = match args.swap_contract_address { Some(addr) => try_tx_fus!(addr.try_to_address()), - None => match tx.action { - Call(address) => address, + None => match tx.unsigned().action() { + Call(address) => *address, Create => { return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( "Invalid payment action: the payment action cannot be create" @@ -2275,10 +2346,15 @@ impl MarketCoinOps for EthCoin { let func_name = match self.coin_type { EthCoinType::Eth => get_function_name("ethPayment", args.watcher_reward), EthCoinType::Erc20 { .. } => get_function_name("erc20Payment", args.watcher_reward), + EthCoinType::Nft { .. } => { + return Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))) + }, }; let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&func_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &tx.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, tx.unsigned().data())); let id = match decoded.first() { Some(Token::FixedBytes(bytes)) => bytes.clone(), invalid_token => { @@ -2328,7 +2404,7 @@ impl MarketCoinOps for EthCoin { if let Some(event) = found { if let Some(tx_hash) = event.transaction_hash { - let transaction = match selfi.web3.eth().transaction(TransactionId::Hash(tx_hash)).await { + let transaction = match selfi.transaction(TransactionId::Hash(tx_hash)).await { Ok(Some(t)) => t, Ok(None) => { info!("Tx {} not found yet", tx_hash); @@ -2359,14 +2435,16 @@ impl MarketCoinOps for EthCoin { } fn current_block(&self) -> Box + Send> { - Box::new( - self.web3 - .eth() - .block_number() - .compat() + let coin = self.clone(); + + let fut = async move { + coin.block_number() + .await .map(|res| res.as_u64()) - .map_err(|e| ERRL!("{}", e)), - ) + .map_err(|e| ERRL!("{}", e)) + }; + + Box::new(fut.boxed().compat()) } fn display_priv_key(&self) -> Result { @@ -2376,9 +2454,9 @@ impl MarketCoinOps for EthCoin { activated_key: ref key_pair, .. } => Ok(format!("{:#02x}", key_pair.secret())), - EthPrivKeyPolicy::Trezor => ERR!("'display_priv_key' doesn't support Trezor yet!"), + EthPrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] - EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support MetaMask"), + EthPrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' is not supported for MetaMask"), } } @@ -2390,70 +2468,96 @@ impl MarketCoinOps for EthCoin { let pow = self.decimals as u32; MmNumber::from(1) / MmNumber::from(10u64.pow(pow)) } + + fn is_trezor(&self) -> bool { self.priv_key_policy.is_trezor() } } pub fn signed_eth_tx_from_bytes(bytes: &[u8]) -> Result { - let tx: UnverifiedTransaction = try_s!(rlp::decode(bytes)); + let tx: UnverifiedTransactionWrapper = try_s!(rlp::decode(bytes)); let signed = try_s!(SignedEthTx::new(tx)); Ok(signed) } +type AddressNonceLocks = Mutex>>>>; + // We can use a nonce lock shared between tokens using the same platform coin and the platform itself. // For example, ETH/USDT-ERC20 should use the same lock, but it will be different for BNB/USDT-BEP20. +// This lock is used to ensure that only one transaction is sent at a time per address. lazy_static! { - static ref NONCE_LOCK: Mutex>>> = Mutex::new(HashMap::new()); + static ref NONCE_LOCK: AddressNonceLocks = Mutex::new(HashMap::new()); } type EthTxFut = Box + Send + 'static>; +/// Signs an Eth transaction using `key_pair`. +/// +/// This method polls for the latest nonce from the RPC nodes and uses it for the transaction to be signed. +/// A `nonce_lock` is returned so that the caller doesn't release it until the transaction is sent and the +/// address nonce is updated on RPC nodes. +#[allow(clippy::too_many_arguments)] +async fn sign_transaction_with_keypair<'a>( + coin: &'a EthCoin, + key_pair: &KeyPair, + value: U256, + action: Action, + data: Vec, + gas: U256, + pay_for_gas_option: &PayForGasOption, + from_address: Address, +) -> Result<(SignedEthTx, Vec), TransactionErr> { + info!(target: "sign", "get_addr_nonce…"); + let (nonce, web3_instances_with_latest_nonce) = try_tx_s!(coin.clone().get_addr_nonce(from_address).compat().await); + let tx_type = tx_type_from_pay_for_gas_option!(pay_for_gas_option); + if !coin.is_tx_type_supported(&tx_type) { + return Err(TransactionErr::Plain("Eth transaction type not supported".into())); + } + let tx_builder = UnSignedEthTxBuilder::new(tx_type, nonce, gas, action, value, data); + let tx_builder = tx_builder_with_pay_for_gas_option(coin, tx_builder, pay_for_gas_option) + .map_err(|e| TransactionErr::Plain(e.get_inner().to_string()))?; + let tx = tx_builder.build()?; + + Ok(( + tx.sign(key_pair.secret(), Some(coin.chain_id))?, + web3_instances_with_latest_nonce, + )) +} + +/// Sign and send eth transaction with provided keypair, +/// This fn is primarily for swap transactions so it uses swap tx fee policy async fn sign_and_send_transaction_with_keypair( - ctx: MmArc, coin: &EthCoin, key_pair: &KeyPair, + address: Address, value: U256, action: Action, data: Vec, gas: U256, ) -> Result { - let mut status = ctx.log.status_handle(); - macro_rules! tags { - () => { - &[&"sign-and-send"] - }; - } - let _nonce_lock = coin.nonce_lock.lock().await; - status.status(tags!(), "get_addr_nonce…"); - let (nonce, web3_instances_with_latest_nonce) = try_tx_s!( - get_addr_nonce(coin.my_address, coin.web3_instances.clone()) - .compat() + info!(target: "sign-and-send", "get_gas_price…"); + let pay_for_gas_option = try_tx_s!( + coin.get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) .await ); - status.status(tags!(), "get_gas_price…"); - let gas_price = try_tx_s!(coin.get_gas_price().compat().await); - - let tx = UnSignedEthTx { - nonce, - gas_price, - gas, - action, - value, - data, - }; - - let signed = tx.sign(key_pair.secret(), coin.chain_id); + let address_lock = coin.get_address_lock(address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let (signed, web3_instances_with_latest_nonce) = + sign_transaction_with_keypair(coin, key_pair, value, action, data, gas, &pay_for_gas_option, address).await?; let bytes = Bytes(rlp::encode(&signed).to_vec()); - status.status(tags!(), "send_raw_transaction…"); + info!(target: "sign-and-send", "send_raw_transaction…"); let futures = web3_instances_with_latest_nonce .into_iter() .map(|web3_instance| web3_instance.web3.eth().send_raw_transaction(bytes.clone())); try_tx_s!(select_ok(futures).await.map_err(|e| ERRL!("{}", e)), signed); - status.status(tags!(), "get_addr_nonce…"); - coin.wait_for_addr_nonce_increase(coin.my_address, nonce).await; + info!(target: "sign-and-send", "wait_for_tx_appears_on_rpc…"); + coin.wait_for_addr_nonce_increase(address, signed.unsigned().nonce()) + .await; Ok(signed) } +/// Sign and send eth transaction with metamask API, +/// This fn is primarily for swap transactions so it uses swap tx fee policy #[cfg(target_arch = "wasm32")] async fn sign_and_send_transaction_with_metamask( coin: EthCoin, @@ -2467,13 +2571,20 @@ async fn sign_and_send_transaction_with_metamask( Action::Call(to) => Some(to), }; - let gas_price = try_tx_s!(coin.get_gas_price().compat().await); - + let pay_for_gas_option = try_tx_s!( + coin.get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await + ); + let my_address = try_tx_s!(coin.derivation_method.single_addr_or_err().await); + let gas_price = pay_for_gas_option.get_gas_price(); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); let tx_to_send = TransactionRequest { - from: coin.my_address, + from: my_address, to, gas: Some(gas), - gas_price: Some(gas_price), + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, value: Some(value), data: Some(data.clone().into()), nonce: None, @@ -2487,7 +2598,7 @@ async fn sign_and_send_transaction_with_metamask( // Please note that this method may take a long time // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. - let tx_hash = try_tx_s!(coin.web3.eth().send_transaction(tx_to_send).await); + let tx_hash = try_tx_s!(coin.send_transaction(tx_to_send).await); let maybe_signed_tx = try_tx_s!( coin.wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) @@ -2502,7 +2613,198 @@ async fn sign_and_send_transaction_with_metamask( } } +/// Sign eth transaction +async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> RawTransactionResult { + let value = wei_from_big_decimal(args.value.as_ref().unwrap_or(&BigDecimal::from(0)), coin.decimals)?; + let action = if let Some(to) = &args.to { + Call(Address::from_str(to).map_to_mm(|err| RawTransactionError::InvalidParam(err.to_string()))?) + } else { + Create + }; + let data = hex::decode(args.data.as_ref().unwrap_or(&String::from("")))?; + match coin.priv_key_policy { + // TODO: use zeroise for privkey + EthPrivKeyPolicy::Iguana(ref key_pair) + | EthPrivKeyPolicy::HDWallet { + activated_key: ref key_pair, + .. + } => { + let my_address = coin + .derivation_method + .single_addr_or_err() + .await + .mm_err(|e| RawTransactionError::InternalError(e.to_string()))?; + let address_lock = coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let pay_for_gas_option = if let Some(ref pay_for_gas) = args.pay_for_gas { + pay_for_gas.clone().try_into()? + } else { + // use legacy gas_price() if not set + info!(target: "sign-and-send", "get_gas_price…"); + let gas_price = coin.get_gas_price().await?; + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) + }; + sign_transaction_with_keypair( + coin, + key_pair, + value, + action, + data, + args.gas_limit, + &pay_for_gas_option, + my_address, + ) + .await + .map(|(signed_tx, _)| RawTransactionRes { + tx_hex: signed_tx.tx_hex().into(), + }) + .map_to_mm(|err| RawTransactionError::TransactionError(err.get_plain_text_format())) + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => MmError::err(RawTransactionError::InvalidParam( + "sign raw eth tx not implemented for Metamask".into(), + )), + EthPrivKeyPolicy::Trezor => MmError::err(RawTransactionError::InvalidParam( + "sign raw eth tx not implemented for Trezor".into(), + )), + } +} + +#[async_trait] +impl RpcCommonOps for EthCoin { + type RpcClient = Web3Instance; + type Error = Web3RpcError; + + async fn get_live_client(&self) -> Result { + let mut clients = self.web3_instances.lock().await; + + // try to find first live client + for (i, client) in clients.clone().into_iter().enumerate() { + if let Web3Transport::Websocket(socket_transport) = &client.web3.transport() { + socket_transport.maybe_spawn_connection_loop(self.clone()); + }; + + if !client.web3.transport().is_last_request_failed() { + // Bring the live client to the front of rpc_clients + clients.rotate_left(i); + return Ok(client); + } + + match client + .web3 + .web3() + .client_version() + .timeout(ETH_RPC_REQUEST_TIMEOUT) + .await + { + Ok(Ok(_)) => { + // Bring the live client to the front of rpc_clients + clients.rotate_left(i); + return Ok(client); + }, + Ok(Err(rpc_error)) => { + debug!("Could not get client version on: {:?}. Error: {}", &client, rpc_error); + + if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { + socket_transport.stop_connection_loop().await; + }; + }, + Err(timeout_error) => { + debug!( + "Client version timeout exceed on: {:?}. Error: {}", + &client, timeout_error + ); + + if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { + socket_transport.stop_connection_loop().await; + }; + }, + }; + } + + return Err(Web3RpcError::Transport( + "All the current rpc nodes are unavailable.".to_string(), + )); + } +} + impl EthCoin { + pub(crate) async fn web3(&self) -> Result, Web3RpcError> { + self.get_live_client().await.map(|t| t.web3) + } + + /// Gets `SenderRefunded` events from etomic swap smart contract since `from_block` + fn refund_events( + &self, + swap_contract_address: Address, + from_block: u64, + to_block: u64, + ) -> Box, Error = String> + Send> { + let contract_event = try_fus!(SWAP_CONTRACT.event("SenderRefunded")); + let filter = FilterBuilder::default() + .topics(Some(vec![contract_event.signature()]), None, None, None) + .from_block(BlockNumber::Number(from_block.into())) + .to_block(BlockNumber::Number(to_block.into())) + .address(vec![swap_contract_address]) + .build(); + + let coin = self.clone(); + + let fut = async move { coin.logs(filter).await.map_err(|e| ERRL!("{}", e)) }; + + Box::new(fut.boxed().compat()) + } + + /// Gets ETH traces from ETH node between addresses in `from_block` and `to_block` + async fn eth_traces( + &self, + from_addr: Vec
, + to_addr: Vec
, + from_block: BlockNumber, + to_block: BlockNumber, + limit: Option, + ) -> Web3RpcResult> { + let mut filter = TraceFilterBuilder::default() + .from_address(from_addr) + .to_address(to_addr) + .from_block(from_block) + .to_block(to_block); + if let Some(l) = limit { + filter = filter.count(l); + } + drop_mutability!(filter); + + self.trace_filter(filter.build()).await.map_to_mm(Web3RpcError::from) + } + + /// Gets Transfer events from ERC20 smart contract `addr` between `from_block` and `to_block` + async fn erc20_transfer_events( + &self, + contract: Address, + from_addr: Option
, + to_addr: Option
, + from_block: BlockNumber, + to_block: BlockNumber, + limit: Option, + ) -> Web3RpcResult> { + let contract_event = ERC20_CONTRACT.event("Transfer")?; + let topic0 = Some(vec![contract_event.signature()]); + let topic1 = from_addr.map(|addr| vec![addr.into()]); + let topic2 = to_addr.map(|addr| vec![addr.into()]); + + let mut filter = FilterBuilder::default() + .topics(topic0, topic1, topic2, None) + .from_block(from_block) + .to_block(to_block) + .address(vec![contract]); + if let Some(l) = limit { + filter = filter.limit(l); + } + drop_mutability!(filter); + + self.logs(filter.build()).await.map_to_mm(Web3RpcError::from) + } + /// Downloads and saves ETH transaction history of my_address, relies on Parity trace_filter API /// https://wiki.parity.io/JSONRPC-trace-module#trace_filter, this requires tracing to be enabled /// in node config. Other ETH clients (Geth, etc.) are `not` supported (yet). @@ -2514,6 +2816,17 @@ impl EthCoin { // Also the Parity RPC server seem to get stuck while request in running (other requests performance is also lowered). let delta = U64::from(1000); + let my_address = match self.derivation_method.single_addr_or_err().await { + Ok(addr) => addr, + Err(e) => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Error on getting my address: {}", e), + ); + return; + }, + }; let mut success_iteration = 0i32; loop { if ctx.is_stopping() { @@ -2528,7 +2841,7 @@ impl EthCoin { }; } - let current_block = match self.web3.eth().block_number().await { + let current_block = match self.block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -2541,7 +2854,7 @@ impl EthCoin { }, }; - let mut saved_traces = match self.load_saved_traces(ctx) { + let mut saved_traces = match self.load_saved_traces(ctx, my_address) { Some(traces) => traces, None => SavedTraces { traces: vec![], @@ -2577,13 +2890,12 @@ impl EthCoin { let from_traces_before_earliest = match self .eth_traces( - vec![self.my_address], + vec![my_address], vec![], BlockNumber::Number(before_earliest), BlockNumber::Number(saved_traces.earliest_block), None, ) - .compat() .await { Ok(traces) => traces, @@ -2601,12 +2913,11 @@ impl EthCoin { let to_traces_before_earliest = match self .eth_traces( vec![], - vec![self.my_address], + vec![my_address], BlockNumber::Number(before_earliest), BlockNumber::Number(saved_traces.earliest_block), None, ) - .compat() .await { Ok(traces) => traces, @@ -2633,19 +2944,18 @@ impl EthCoin { } else { 0.into() }; - self.store_eth_traces(ctx, &saved_traces); + self.store_eth_traces(ctx, my_address, &saved_traces); } if current_block > saved_traces.latest_block { let from_traces_after_latest = match self .eth_traces( - vec![self.my_address], + vec![my_address], vec![], BlockNumber::Number(saved_traces.latest_block + 1), BlockNumber::Number(current_block), None, ) - .compat() .await { Ok(traces) => traces, @@ -2663,12 +2973,11 @@ impl EthCoin { let to_traces_after_latest = match self .eth_traces( vec![], - vec![self.my_address], + vec![my_address], BlockNumber::Number(saved_traces.latest_block + 1), BlockNumber::Number(current_block), None, ) - .compat() .await { Ok(traces) => traces, @@ -2691,7 +3000,7 @@ impl EthCoin { saved_traces.traces.extend(to_traces_after_latest); saved_traces.latest_block = current_block; - self.store_eth_traces(ctx, &saved_traces); + self.store_eth_traces(ctx, my_address, &saved_traces); } saved_traces.traces.sort_by(|a, b| b.block_number.cmp(&a.block_number)); for trace in saved_traces.traces { @@ -2711,8 +3020,6 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.request.count", 1, "coin" => self.ticker.clone(), "method" => "tx_detail_by_hash"); let web3_tx = match self - .web3 - .eth() .transaction(TransactionId::Hash(trace.transaction_hash.unwrap())) .await { @@ -2744,12 +3051,7 @@ impl EthCoin { mm_counter!(ctx.metrics, "tx.history.response.count", 1, "coin" => self.ticker.clone(), "method" => "tx_detail_by_hash"); - let receipt = match self - .web3 - .eth() - .transaction_receipt(trace.transaction_hash.unwrap()) - .await - { + let receipt = match self.transaction_receipt(trace.transaction_hash.unwrap()).await { Ok(r) => r, Err(e) => { ctx.log.log( @@ -2767,15 +3069,30 @@ impl EthCoin { let fee_coin = match &self.coin_type { EthCoinType::Eth => self.ticker(), EthCoinType::Erc20 { platform, .. } => platform.as_str(), + EthCoinType::Nft { .. } => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Error on getting fee coin: Nft Protocol is not supported yet!"), + ); + continue; + }, }; let fee_details: Option = match receipt { Some(r) => { let gas_used = r.gas_used.unwrap_or_default(); let gas_price = web3_tx.gas_price.unwrap_or_default(); - // It's relatively safe to unwrap `EthTxFeeDetails::new` as it may fail - // due to `u256_to_big_decimal` only. + // TODO: create and use EthTxFeeDetails::from(web3_tx) + // It's relatively safe to unwrap `EthTxFeeDetails::new` as it may fail due to `u256_to_big_decimal` only. // Also TX history is not used by any GUI and has significant disadvantages. - Some(EthTxFeeDetails::new(gas_used, gas_price, fee_coin).unwrap()) + Some( + EthTxFeeDetails::new( + gas_used, + PayForGasOption::Legacy(LegacyGasPrice { gas_price }), + fee_coin, + ) + .unwrap(), + ) }, None => None, }; @@ -2784,7 +3101,7 @@ impl EthCoin { let mut received_by_me = 0.into(); let mut spent_by_me = 0.into(); - if call_data.from == self.my_address { + if call_data.from == my_address { // ETH transfer is actually happening only if no error occurred if trace.error.is_none() { spent_by_me = total_amount.clone(); @@ -2794,7 +3111,7 @@ impl EthCoin { } } - if call_data.to == self.my_address { + if call_data.to == my_address { // ETH transfer is actually happening only if no error occurred if trace.error.is_none() { received_by_me = total_amount.clone(); @@ -2803,8 +3120,6 @@ impl EthCoin { let raw = signed_tx_from_web3_tx(web3_tx).unwrap(); let block = match self - .web3 - .eth() .block(BlockId::Number(BlockNumber::Number(trace.block_number.into()))) .await { @@ -2824,13 +3139,15 @@ impl EthCoin { spent_by_me, received_by_me, total_amount, - to: vec![checksum_address(&format!("{:#02x}", call_data.to))], - from: vec![checksum_address(&format!("{:#02x}", call_data.from))], + to: vec![display_eth_address(&call_data.to)], + from: vec![display_eth_address(&call_data.from)], coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: trace.block_number, - tx_hash: format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), - tx_hex: BytesJson(rlp::encode(&raw).to_vec()), + tx: TransactionData::new_signed( + BytesJson(rlp::encode(&raw).to_vec()), + format!("{:02x}", BytesJson(raw.tx_hash_as_bytes().to_vec())), + ), internal_id, timestamp: block.timestamp.into_or_max(), kmd_rewards: None, @@ -2873,6 +3190,17 @@ impl EthCoin { async fn process_erc20_history(&self, token_addr: H160, ctx: &MmArc) { let delta = U64::from(10000); + let my_address = match self.derivation_method.single_addr_or_err().await { + Ok(addr) => addr, + Err(e) => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Error on getting my address: {}", e), + ); + return; + }, + }; let mut success_iteration = 0i32; loop { if ctx.is_stopping() { @@ -2887,7 +3215,7 @@ impl EthCoin { }; } - let current_block = match self.web3.eth().block_number().await { + let current_block = match self.block_number().await { Ok(block) => block, Err(e) => { ctx.log.log( @@ -2900,7 +3228,7 @@ impl EthCoin { }, }; - let mut saved_events = match self.load_saved_erc20_events(ctx) { + let mut saved_events = match self.load_saved_erc20_events(ctx, my_address) { Some(events) => events, None => SavedErc20Events { events: vec![], @@ -2925,13 +3253,12 @@ impl EthCoin { let from_events_before_earliest = match self .erc20_transfer_events( token_addr, - Some(self.my_address), + Some(my_address), None, BlockNumber::Number(before_earliest), BlockNumber::Number(saved_events.earliest_block - 1), None, ) - .compat() .await { Ok(events) => events, @@ -2950,12 +3277,11 @@ impl EthCoin { .erc20_transfer_events( token_addr, None, - Some(self.my_address), + Some(my_address), BlockNumber::Number(before_earliest), BlockNumber::Number(saved_events.earliest_block - 1), None, ) - .compat() .await { Ok(events) => events, @@ -2981,20 +3307,19 @@ impl EthCoin { } else { 0.into() }; - self.store_erc20_events(ctx, &saved_events); + self.store_erc20_events(ctx, my_address, &saved_events); } if current_block > saved_events.latest_block { let from_events_after_latest = match self .erc20_transfer_events( token_addr, - Some(self.my_address), + Some(my_address), None, BlockNumber::Number(saved_events.latest_block + 1), BlockNumber::Number(current_block), None, ) - .compat() .await { Ok(events) => events, @@ -3013,12 +3338,11 @@ impl EthCoin { .erc20_transfer_events( token_addr, None, - Some(self.my_address), + Some(my_address), BlockNumber::Number(saved_events.latest_block + 1), BlockNumber::Number(current_block), None, ) - .compat() .await { Ok(events) => events, @@ -3040,7 +3364,7 @@ impl EthCoin { saved_events.events.extend(from_events_after_latest); saved_events.events.extend(to_events_after_latest); saved_events.latest_block = current_block; - self.store_erc20_events(ctx, &saved_events); + self.store_erc20_events(ctx, my_address, &saved_events); } let all_events: HashMap<_, _> = saved_events @@ -3078,11 +3402,11 @@ impl EthCoin { let from_addr = H160::from(event.topics[1]); let to_addr = H160::from(event.topics[2]); - if from_addr == self.my_address { + if from_addr == my_address { spent_by_me = total_amount.clone(); } - if to_addr == self.my_address { + if to_addr == my_address { received_by_me = total_amount.clone(); } @@ -3090,8 +3414,6 @@ impl EthCoin { "coin" => self.ticker.clone(), "client" => "ethereum", "method" => "tx_detail_by_hash"); let web3_tx = match self - .web3 - .eth() .transaction(TransactionId::Hash(event.transaction_hash.unwrap())) .await { @@ -3125,12 +3447,7 @@ impl EthCoin { }, }; - let receipt = match self - .web3 - .eth() - .transaction_receipt(event.transaction_hash.unwrap()) - .await - { + let receipt = match self.transaction_receipt(event.transaction_hash.unwrap()).await { Ok(r) => r, Err(e) => { ctx.log.log( @@ -3148,6 +3465,14 @@ impl EthCoin { let fee_coin = match &self.coin_type { EthCoinType::Eth => self.ticker(), EthCoinType::Erc20 { platform, .. } => platform.as_str(), + EthCoinType::Nft { .. } => { + ctx.log.log( + "", + &[&"tx_history", &self.ticker], + &ERRL!("Error on getting fee coin: Nft Protocol is not supported yet!"), + ); + continue; + }, }; let fee_details = match receipt { Some(r) => { @@ -3156,17 +3481,19 @@ impl EthCoin { // It's relatively safe to unwrap `EthTxFeeDetails::new` as it may fail // due to `u256_to_big_decimal` only. // Also TX history is not used by any GUI and has significant disadvantages. - Some(EthTxFeeDetails::new(gas_used, gas_price, fee_coin).unwrap()) + Some( + EthTxFeeDetails::new( + gas_used, + PayForGasOption::Legacy(LegacyGasPrice { gas_price }), + fee_coin, + ) + .unwrap(), + ) }, None => None, }; let block_number = event.block_number.unwrap(); - let block = match self - .web3 - .eth() - .block(BlockId::Number(BlockNumber::Number(block_number))) - .await - { + let block = match self.block(BlockId::Number(BlockNumber::Number(block_number))).await { Ok(Some(b)) => b, Ok(None) => { ctx.log.log( @@ -3192,13 +3519,15 @@ impl EthCoin { spent_by_me, received_by_me, total_amount, - to: vec![checksum_address(&format!("{:#02x}", to_addr))], - from: vec![checksum_address(&format!("{:#02x}", from_addr))], + to: vec![display_eth_address(&to_addr)], + from: vec![display_eth_address(&from_addr)], coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: block_number.as_u64(), - tx_hash: format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), - tx_hex: BytesJson(rlp::encode(&raw).to_vec()), + tx: TransactionData::new_signed( + BytesJson(rlp::encode(&raw).to_vec()), + format!("{:02x}", BytesJson(raw.tx_hash_as_bytes().to_vec())), + ), internal_id: BytesJson(internal_id.to_vec()), timestamp: block.timestamp.into_or_max(), kmd_rewards: None, @@ -3234,12 +3563,39 @@ impl EthCoin { } } } + + /// Returns tx type as number if this type supported by this coin + fn is_tx_type_supported(&self, tx_type: &TxType) -> bool { + let tx_type_as_num = match tx_type { + TxType::Legacy => 0_u64, + TxType::Type1 => 1_u64, + TxType::Type2 => 2_u64, + TxType::Invalid => return false, + }; + let max_tx_type = self.max_eth_tx_type.unwrap_or(0_u64); + tx_type_as_num <= max_tx_type + } + + /// Retrieves the lock associated with a given address. + /// + /// This function is used to ensure that only one transaction is sent at a time per address. + /// If the address does not have an associated lock, a new one is created and stored. + async fn get_address_lock(&self, address: String) -> Arc> { + let address_lock = { + let mut lock = self.address_nonce_locks.lock().await; + lock.entry(address) + .or_insert_with(|| Arc::new(AsyncMutex::new(()))) + .clone() + }; + address_lock + } } #[cfg_attr(test, mockable)] impl EthCoin { - fn sign_and_send_transaction(&self, value: U256, action: Action, data: Vec, gas: U256) -> EthTxFut { - let ctx = try_tx_fus!(MmArc::from_weak(&self.ctx).ok_or("!ctx")); + /// Sign and send eth transaction. + /// This function is primarily for swap transactions so internally it relies on the swap tx fee policy + pub fn sign_and_send_transaction(&self, value: U256, action: Action, data: Vec, gas: U256) -> EthTxFut { let coin = self.clone(); let fut = async move { match coin.priv_key_policy { @@ -3247,8 +3603,15 @@ impl EthCoin { | EthPrivKeyPolicy::HDWallet { activated_key: ref key_pair, .. - } => sign_and_send_transaction_with_keypair(ctx, &coin, key_pair, value, action, data, gas).await, - EthPrivKeyPolicy::Trezor => Err(TransactionErr::Plain(ERRL!("Trezor is not supported for EVM yet!"))), + } => { + let address = coin + .derivation_method + .single_addr_or_err() + .await + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + sign_and_send_transaction_with_keypair(&coin, key_pair, address, value, action, data, gas).await + }, + EthPrivKeyPolicy::Trezor => Err(TransactionErr::Plain(ERRL!("Trezor is not supported for swaps yet!"))), #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => { sign_and_send_transaction_with_metamask(coin, value, action, data, gas).await @@ -3260,7 +3623,12 @@ impl EthCoin { pub fn send_to_address(&self, address: Address, value: U256) -> EthTxFut { match &self.coin_type { - EthCoinType::Eth => self.sign_and_send_transaction(value, Action::Call(address), vec![], U256::from(21000)), + EthCoinType::Eth => self.sign_and_send_transaction( + value, + Action::Call(address), + vec![], + U256::from(self.gas_limit.eth_send_coins), + ), EthCoinType::Erc20 { platform: _, token_addr, @@ -3268,8 +3636,16 @@ impl EthCoin { let abi = try_tx_fus!(Contract::load(ERC20_ABI.as_bytes())); let function = try_tx_fus!(abi.function("transfer")); let data = try_tx_fus!(function.encode_input(&[Token::Address(address), Token::Uint(value)])); - self.sign_and_send_transaction(0.into(), Action::Call(*token_addr), data, U256::from(210_000)) + self.sign_and_send_transaction( + 0.into(), + Action::Call(*token_addr), + data, + U256::from(self.gas_limit.eth_send_erc20), + ) }, + EthCoinType::Nft { .. } => Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))), } } @@ -3280,7 +3656,6 @@ impl EthCoin { let trade_amount = try_tx_fus!(wei_from_big_decimal(&args.amount, self.decimals)); let time_lock = U256::from(args.time_lock); - let gas = U256::from(ETH_GAS); let secret_hash = if args.secret_hash.len() == 32 { ripemd160(args.secret_hash).to_vec() @@ -3297,7 +3672,7 @@ impl EthCoin { let data = match &args.watcher_reward { Some(reward) => { let reward_amount = try_tx_fus!(wei_from_big_decimal(&reward.amount, self.decimals)); - if !matches!(reward.reward_target, RewardTarget::None) { + if !matches!(reward.reward_target, RewardTarget::None) || reward.send_contract_reward_on_spend { value += reward_amount; } @@ -3318,7 +3693,7 @@ impl EthCoin { Token::Uint(time_lock), ])), }; - + let gas = U256::from(self.gas_limit.eth_payment); self.sign_and_send_transaction(value, Action::Call(swap_contract_address), data, gas) }, EthCoinType::Erc20 { @@ -3335,14 +3710,33 @@ impl EthCoin { let mut value = U256::from(0); let mut amount = trade_amount; + debug!("Using watcher reward {:?} for swap payment", args.watcher_reward); + let data = match args.watcher_reward { Some(reward) => { - let reward_amount = try_tx_fus!(wei_from_big_decimal(&reward.amount, self.decimals)); - - match reward.reward_target { - RewardTarget::Contract | RewardTarget::PaymentSender => value += reward_amount, - RewardTarget::PaymentSpender => amount += reward_amount, - _ => (), + let reward_amount = match reward.reward_target { + RewardTarget::Contract | RewardTarget::PaymentSender => { + let eth_reward_amount = try_tx_fus!(wei_from_big_decimal(&reward.amount, ETH_DECIMALS)); + value += eth_reward_amount; + eth_reward_amount + }, + RewardTarget::PaymentSpender => { + let token_reward_amount = + try_tx_fus!(wei_from_big_decimal(&reward.amount, self.decimals)); + amount += token_reward_amount; + token_reward_amount + }, + _ => { + // TODO tests passed without this change, need to research on how it worked + if reward.send_contract_reward_on_spend { + let eth_reward_amount = + try_tx_fus!(wei_from_big_decimal(&reward.amount, ETH_DECIMALS)); + value += eth_reward_amount; + eth_reward_amount + } else { + 0.into() + } + }, }; try_tx_fus!(function.encode_input(&[ @@ -3370,6 +3764,7 @@ impl EthCoin { }; let wait_for_required_allowance_until = args.wait_for_confirmation_until; + let gas = U256::from(self.gas_limit.erc20_payment); let arc = self.clone(); Box::new(allowance_fut.and_then(move |allowed| -> EthTxFut { @@ -3387,14 +3782,14 @@ impl EthCoin { .map_err(move |e| { TransactionErr::Plain(ERRL!( "Allowed value was not updated in time after sending approve transaction {:02x}: {}", - approved.tx_hash(), + approved.tx_hash_as_bytes(), e )) }) .and_then(move |_| { arc.sign_and_send_transaction( value, - Action::Call(swap_contract_address), + Call(swap_contract_address), data, gas, ) @@ -3404,18 +3799,21 @@ impl EthCoin { } else { Box::new(arc.sign_and_send_transaction( value, - Action::Call(swap_contract_address), + Call(swap_contract_address), data, gas, )) } })) }, + EthCoinType::Nft { .. } => Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))), } } fn watcher_spends_hash_time_locked_payment(&self, input: SendMakerPaymentSpendPreimageInput) -> EthTxFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(input.preimage)); + let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(input.preimage)); let payment = try_tx_fus!(SignedEthTx::new(tx)); let function_name = get_function_name("receiverSpend", input.watcher_reward); @@ -3423,8 +3821,8 @@ impl EthCoin { let clone = self.clone(); let secret_vec = input.secret.to_vec(); let taker_addr = addr_from_raw_pubkey(input.taker_pub).unwrap(); - let swap_contract_address = match payment.action { - Call(address) => address, + let swap_contract_address = match payment.unsigned().action() { + Call(address) => *address, Create => { return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( "Invalid payment action: the payment action cannot be create" @@ -3437,7 +3835,7 @@ impl EthCoin { EthCoinType::Eth => { let function_name = get_function_name("ethPayment", watcher_reward); let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, payment.unsigned().data())); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); @@ -3453,7 +3851,7 @@ impl EthCoin { )))); } - let value = payment.value; + let value = payment.unsigned().value(); let reward_target = try_tx_fus!(get_function_input_data(&decoded, payment_func, 4)); let sends_contract_reward = try_tx_fus!(get_function_input_data(&decoded, payment_func, 5)); let watcher_reward_amount = try_tx_fus!(get_function_input_data(&decoded, payment_func, 6)); @@ -3472,9 +3870,9 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(swap_contract_address), + Call(swap_contract_address), data, - U256::from(ETH_GAS), + U256::from(clone.gas_limit.eth_receiver_spend), ) }), ) @@ -3486,7 +3884,7 @@ impl EthCoin { let function_name = get_function_name("erc20Payment", watcher_reward); let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, payment.unsigned().data())); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); @@ -3520,18 +3918,21 @@ impl EthCoin { ])); clone.sign_and_send_transaction( 0.into(), - Action::Call(swap_contract_address), + Call(swap_contract_address), data, - U256::from(ETH_GAS), + U256::from(clone.gas_limit.erc20_receiver_spend), ) }), ) }, + EthCoinType::Nft { .. } => Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))), } } fn watcher_refunds_hash_time_locked_payment(&self, args: RefundPaymentArgs) -> EthTxFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.payment_tx)); + let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(args.payment_tx)); let payment = try_tx_fus!(SignedEthTx::new(tx)); let function_name = get_function_name("senderRefund", true); @@ -3539,8 +3940,8 @@ impl EthCoin { let clone = self.clone(); let taker_addr = addr_from_raw_pubkey(args.other_pubkey).unwrap(); - let swap_contract_address = match payment.action { - Call(address) => address, + let swap_contract_address = match payment.unsigned().action() { + Call(address) => *address, Create => { return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( "Invalid payment action: the payment action cannot be create" @@ -3552,7 +3953,7 @@ impl EthCoin { EthCoinType::Eth => { let function_name = get_function_name("ethPayment", true); let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, payment.unsigned().data())); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let receiver_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); let hash_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 2)); @@ -3570,7 +3971,7 @@ impl EthCoin { )))); } - let value = payment.value; + let value = payment.unsigned().value(); let reward_target = try_tx_fus!(get_function_input_data(&decoded, payment_func, 4)); let sends_contract_reward = try_tx_fus!(get_function_input_data(&decoded, payment_func, 5)); let reward_amount = try_tx_fus!(get_function_input_data(&decoded, payment_func, 6)); @@ -3589,9 +3990,9 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(swap_contract_address), + Call(swap_contract_address), data, - U256::from(ETH_GAS), + U256::from(clone.gas_limit.eth_sender_refund), ) }), ) @@ -3603,7 +4004,7 @@ impl EthCoin { let function_name = get_function_name("erc20Payment", true); let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, payment.unsigned().data())); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); let receiver_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 3)); @@ -3640,248 +4041,262 @@ impl EthCoin { clone.sign_and_send_transaction( 0.into(), - Action::Call(swap_contract_address), + Call(swap_contract_address), data, - U256::from(ETH_GAS), + U256::from(clone.gas_limit.erc20_sender_refund), ) }), ) }, + EthCoinType::Nft { .. } => Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + )))), } } - fn spend_hash_time_locked_payment(&self, args: SpendPaymentArgs) -> EthTxFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.other_payment_tx)); - let payment = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(args.swap_contract_address.try_to_address()); + async fn spend_hash_time_locked_payment<'a>( + &self, + args: SpendPaymentArgs<'a>, + ) -> Result { + let tx: UnverifiedTransactionWrapper = try_tx_s!(rlp::decode(args.other_payment_tx)); + let payment = try_tx_s!(SignedEthTx::new(tx)); + let my_address = try_tx_s!(self.derivation_method.single_addr_or_err().await); + let swap_contract_address = try_tx_s!(args.swap_contract_address.try_to_address()); let function_name = get_function_name("receiverSpend", args.watcher_reward); - let spend_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let spend_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let clone = self.clone(); let secret_vec = args.secret.to_vec(); let watcher_reward = args.watcher_reward; match self.coin_type { EthCoinType::Eth => { let function_name = get_function_name("ethPayment", watcher_reward); - let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); + let decoded = try_tx_s!(decode_contract_call(payment_func, payment.unsigned().data())); - let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new( - state_f - .map_err(TransactionErr::Plain) - .and_then(move |state| -> EthTxFut { - if state != U256::from(PaymentState::Sent as u8) { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - )))); - } + let state = try_tx_s!( + self.payment_status(swap_contract_address, decoded[0].clone()) + .compat() + .await + ); + if state != U256::from(PaymentState::Sent as u8) { + return Err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + ))); + } - let data = if watcher_reward { - try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(payment.value), - Token::FixedBytes(secret_vec), - Token::Address(Address::default()), - Token::Address(payment.sender()), - Token::Address(clone.my_address), - decoded[4].clone(), - decoded[5].clone(), - decoded[6].clone(), - ])) - } else { - try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(payment.value), - Token::FixedBytes(secret_vec), - Token::Address(Address::default()), - Token::Address(payment.sender()), - ])) - }; + let data = if watcher_reward { + try_tx_s!(spend_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(payment.unsigned().value()), + Token::FixedBytes(secret_vec), + Token::Address(Address::default()), + Token::Address(payment.sender()), + Token::Address(my_address), + decoded[4].clone(), + decoded[5].clone(), + decoded[6].clone(), + ])) + } else { + try_tx_s!(spend_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(payment.unsigned().value()), + Token::FixedBytes(secret_vec), + Token::Address(Address::default()), + Token::Address(payment.sender()), + ])) + }; - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(ETH_GAS), - ) - }), + self.sign_and_send_transaction( + 0.into(), + Call(swap_contract_address), + data, + U256::from(self.gas_limit.eth_receiver_spend), ) + .compat() + .await }, EthCoinType::Erc20 { platform: _, token_addr, } => { let function_name = get_function_name("erc20Payment", watcher_reward); - let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); - let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); + let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - Box::new( - state_f - .map_err(TransactionErr::Plain) - .and_then(move |state| -> EthTxFut { - if state != U256::from(PaymentState::Sent as u8) { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - )))); - } - let data = if watcher_reward { - try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - Token::FixedBytes(secret_vec), - Token::Address(token_addr), - Token::Address(payment.sender()), - Token::Address(clone.my_address), - decoded[6].clone(), - decoded[7].clone(), - decoded[8].clone(), - ])) - } else { - try_tx_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - Token::FixedBytes(secret_vec), - Token::Address(token_addr), - Token::Address(payment.sender()), - ])) - }; + let decoded = try_tx_s!(decode_contract_call(payment_func, payment.unsigned().data())); + let state = try_tx_s!( + self.payment_status(swap_contract_address, decoded[0].clone()) + .compat() + .await + ); + if state != U256::from(PaymentState::Sent as u8) { + return Err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + ))); + } - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(ETH_GAS), - ) - }), + let data = if watcher_reward { + try_tx_s!(spend_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + Token::FixedBytes(secret_vec), + Token::Address(token_addr), + Token::Address(payment.sender()), + Token::Address(my_address), + decoded[6].clone(), + decoded[7].clone(), + decoded[8].clone(), + ])) + } else { + try_tx_s!(spend_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + Token::FixedBytes(secret_vec), + Token::Address(token_addr), + Token::Address(payment.sender()), + ])) + }; + + self.sign_and_send_transaction( + 0.into(), + Call(swap_contract_address), + data, + U256::from(self.gas_limit.erc20_receiver_spend), ) + .compat() + .await }, + EthCoinType::Nft { .. } => Err(TransactionErr::ProtocolNotSupported(ERRL!( + "Nft Protocol is not supported!" + ))), } } - fn refund_hash_time_locked_payment(&self, args: RefundPaymentArgs) -> EthTxFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.payment_tx)); - let payment = try_tx_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_tx_fus!(args.swap_contract_address.try_to_address()); + async fn refund_hash_time_locked_payment<'a>( + &self, + args: RefundPaymentArgs<'a>, + ) -> Result { + let tx: UnverifiedTransactionWrapper = try_tx_s!(rlp::decode(args.payment_tx)); + let payment = try_tx_s!(SignedEthTx::new(tx)); + let my_address = try_tx_s!(self.derivation_method.single_addr_or_err().await); + let swap_contract_address = try_tx_s!(args.swap_contract_address.try_to_address()); let function_name = get_function_name("senderRefund", args.watcher_reward); - let refund_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let refund_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); let watcher_reward = args.watcher_reward; - let clone = self.clone(); - match self.coin_type { EthCoinType::Eth => { let function_name = get_function_name("ethPayment", watcher_reward); - let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_s!(decode_contract_call(payment_func, payment.unsigned().data())); - let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new( - state_f - .map_err(TransactionErr::Plain) - .and_then(move |state| -> EthTxFut { - if state != U256::from(PaymentState::Sent as u8) { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - )))); - } + let state = try_tx_s!( + self.payment_status(swap_contract_address, decoded[0].clone()) + .compat() + .await + ); + if state != U256::from(PaymentState::Sent as u8) { + return Err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + ))); + } - let value = payment.value; - let data = if watcher_reward { - try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - decoded[2].clone(), - Token::Address(Address::default()), - Token::Address(clone.my_address), - decoded[1].clone(), - decoded[4].clone(), - decoded[5].clone(), - decoded[6].clone(), - ])) - } else { - try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - decoded[2].clone(), - Token::Address(Address::default()), - decoded[1].clone(), - ])) - }; + let value = payment.unsigned().value(); + let data = if watcher_reward { + try_tx_s!(refund_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + decoded[2].clone(), + Token::Address(Address::default()), + Token::Address(my_address), + decoded[1].clone(), + decoded[4].clone(), + decoded[5].clone(), + decoded[6].clone(), + ])) + } else { + try_tx_s!(refund_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + decoded[2].clone(), + Token::Address(Address::default()), + decoded[1].clone(), + ])) + }; - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(ETH_GAS), - ) - }), + self.sign_and_send_transaction( + 0.into(), + Call(swap_contract_address), + data, + U256::from(self.gas_limit.eth_sender_refund), ) + .compat() + .await }, EthCoinType::Erc20 { platform: _, token_addr, } => { let function_name = get_function_name("erc20Payment", watcher_reward); - let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); + let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); - let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new( - state_f - .map_err(TransactionErr::Plain) - .and_then(move |state| -> EthTxFut { - if state != U256::from(PaymentState::Sent as u8) { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - )))); - } + let decoded = try_tx_s!(decode_contract_call(payment_func, payment.unsigned().data())); + let state = try_tx_s!( + self.payment_status(swap_contract_address, decoded[0].clone()) + .compat() + .await + ); + if state != U256::from(PaymentState::Sent as u8) { + return Err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + ))); + } - let data = if watcher_reward { - try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - decoded[4].clone(), - Token::Address(token_addr), - Token::Address(clone.my_address), - decoded[3].clone(), - decoded[6].clone(), - decoded[7].clone(), - decoded[8].clone(), - ])) - } else { - try_tx_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - decoded[4].clone(), - Token::Address(token_addr), - decoded[3].clone(), - ])) - }; + let data = if watcher_reward { + try_tx_s!(refund_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + decoded[4].clone(), + Token::Address(token_addr), + Token::Address(my_address), + decoded[3].clone(), + decoded[6].clone(), + decoded[7].clone(), + decoded[8].clone(), + ])) + } else { + try_tx_s!(refund_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + decoded[4].clone(), + Token::Address(token_addr), + decoded[3].clone(), + ])) + }; - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(ETH_GAS), - ) - }), + self.sign_and_send_transaction( + 0.into(), + Call(swap_contract_address), + data, + U256::from(self.gas_limit.erc20_sender_refund), ) + .compat() + .await }, + EthCoinType::Nft { .. } => Err(TransactionErr::ProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + ))), } } @@ -3889,12 +4304,12 @@ impl EthCoin { let coin = self.clone(); let fut = async move { match coin.coin_type { - EthCoinType::Eth => Ok(coin.web3.eth().balance(address, Some(BlockNumber::Latest)).await?), + EthCoinType::Eth => Ok(coin.balance(address, Some(BlockNumber::Latest)).await?), EthCoinType::Erc20 { ref token_addr, .. } => { let function = ERC20_CONTRACT.function("balanceOf")?; let data = function.encode_input(&[Token::Address(address)])?; - let res = coin.call_request(*token_addr, None, Some(data.into())).await?; + let res = coin.call_request(address, *token_addr, None, Some(data.into())).await?; let decoded = function.decode_output(&res.0)?; match decoded[0] { Token::Uint(number) => Ok(number), @@ -3904,19 +4319,37 @@ impl EthCoin { }, } }, + EthCoinType::Nft { .. } => { + MmError::err(BalanceError::Internal("Nft Protocol is not supported yet!".to_string())) + }, } }; Box::new(fut.boxed().compat()) } - fn my_balance(&self) -> BalanceFut { self.address_balance(self.my_address) } + fn get_balance(&self) -> BalanceFut { + let coin = self.clone(); + let fut = async move { + let my_address = coin.derivation_method.single_addr_or_err().await?; + coin.address_balance(my_address).compat().await + }; + Box::new(fut.boxed().compat()) + } - pub async fn get_tokens_balance_list(&self) -> Result, MmError> { + pub async fn get_tokens_balance_list_for_address( + &self, + address: Address, + ) -> Result> { let coin = || self; - let mut requests = Vec::new(); - for (token_ticker, info) in self.get_erc_tokens_infos() { + + let tokens = self.get_erc_tokens_infos(); + let mut requests = Vec::with_capacity(tokens.len()); + + for (token_ticker, info) in tokens { let fut = async move { - let balance_as_u256 = coin().get_token_balance_by_address(info.token_address).await?; + let balance_as_u256 = coin() + .get_token_balance_for_address(address, info.token_address) + .await?; let balance_as_big_decimal = u256_to_big_decimal(balance_as_u256, info.decimals)?; let balance = CoinBalance::new(balance_as_big_decimal); Ok((token_ticker, balance)) @@ -3927,11 +4360,21 @@ impl EthCoin { try_join_all(requests).await.map(|res| res.into_iter().collect()) } - async fn get_token_balance_by_address(&self, token_address: Address) -> Result> { - let coin = self.clone(); + pub async fn get_tokens_balance_list(&self) -> Result> { + let my_address = self.derivation_method.single_addr_or_err().await?; + self.get_tokens_balance_list_for_address(my_address).await + } + + async fn get_token_balance_for_address( + &self, + address: Address, + token_address: Address, + ) -> Result> { let function = ERC20_CONTRACT.function("balanceOf")?; - let data = function.encode_input(&[Token::Address(coin.my_address)])?; - let res = coin.call_request(token_address, None, Some(data.into())).await?; + let data = function.encode_input(&[Token::Address(address)])?; + let res = self + .call_request(address, token_address, None, Some(data.into())) + .await?; let decoded = function.decode_output(&res.0)?; match decoded[0] { @@ -3943,9 +4386,77 @@ impl EthCoin { } } - fn estimate_gas(&self, req: CallRequest) -> Box + Send> { + async fn get_token_balance(&self, token_address: Address) -> Result> { + let my_address = self.derivation_method.single_addr_or_err().await?; + self.get_token_balance_for_address(my_address, token_address).await + } + + async fn erc1155_balance(&self, token_addr: Address, token_id: &str) -> MmResult { + let wallet_amount_uint = match self.coin_type { + EthCoinType::Eth | EthCoinType::Nft { .. } => { + let function = ERC1155_CONTRACT.function("balanceOf")?; + let token_id_u256 = + U256::from_dec_str(token_id).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; + let my_address = self.derivation_method.single_addr_or_err().await?; + let data = function.encode_input(&[Token::Address(my_address), Token::Uint(token_id_u256)])?; + let result = self + .call_request(my_address, token_addr, None, Some(data.into())) + .await?; + let decoded = function.decode_output(&result.0)?; + match decoded[0] { + Token::Uint(number) => number, + _ => { + let error = format!("Expected U256 as balanceOf result but got {:?}", decoded); + return MmError::err(BalanceError::InvalidResponse(error)); + }, + } + }, + EthCoinType::Erc20 { .. } => { + return MmError::err(BalanceError::Internal( + "Erc20 coin type doesnt support Erc1155 standard".to_owned(), + )) + }, + }; + let wallet_amount = u256_to_big_decimal(wallet_amount_uint, self.decimals)?; + Ok(wallet_amount) + } + + async fn erc721_owner(&self, token_addr: Address, token_id: &str) -> MmResult { + let owner_address = match self.coin_type { + EthCoinType::Eth | EthCoinType::Nft { .. } => { + let function = ERC721_CONTRACT.function("ownerOf")?; + let token_id_u256 = + U256::from_dec_str(token_id).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; + let data = function.encode_input(&[Token::Uint(token_id_u256)])?; + let my_address = self.derivation_method.single_addr_or_err().await?; + let result = self + .call_request(my_address, token_addr, None, Some(data.into())) + .await?; + let decoded = function.decode_output(&result.0)?; + match decoded[0] { + Token::Address(owner) => owner, + _ => { + let error = format!("Expected Address as ownerOf result but got {:?}", decoded); + return MmError::err(GetNftInfoError::InvalidResponse(error)); + }, + } + }, + EthCoinType::Erc20 { .. } => { + return MmError::err(GetNftInfoError::Internal( + "Erc20 coin type doesnt support Erc721 standard".to_owned(), + )) + }, + }; + Ok(owner_address) + } + + fn estimate_gas_wrapper(&self, req: CallRequest) -> Box + Send> { + let coin = self.clone(); + // always using None block number as old Geth version accept only single argument in this RPC - Box::new(self.web3.eth().estimate_gas(req, None).compat()) + let fut = async move { coin.estimate_gas(req, None).await }; + + Box::new(fut.boxed().compat()) } /// Estimates how much gas is necessary to allow the contract call to complete. @@ -3958,38 +4469,48 @@ impl EthCoin { /// /// Also, note that the contract call has to be initiated by my wallet address, /// because [`CallRequest::from`] is set to [`EthCoinImpl::my_address`]. - fn estimate_gas_for_contract_call(&self, contract_addr: Address, call_data: Bytes) -> Web3RpcFut { + async fn estimate_gas_for_contract_call(&self, contract_addr: Address, call_data: Bytes) -> Web3RpcResult { let coin = self.clone(); - Box::new(coin.get_gas_price().and_then(move |gas_price| { - let eth_value = U256::zero(); - let estimate_gas_req = CallRequest { - value: Some(eth_value), - data: Some(call_data), - from: Some(coin.my_address), - to: Some(contract_addr), - gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), - ..CallRequest::default() - }; - coin.estimate_gas(estimate_gas_req).map_to_mm_fut(Web3RpcError::from) - })) + let my_address = coin.derivation_method.single_addr_or_err().await?; + let fee_policy_for_estimate = get_swap_fee_policy_for_estimate(self.get_swap_transaction_fee_policy()); + let pay_for_gas_option = coin.get_swap_pay_for_gas_option(fee_policy_for_estimate).await?; + let eth_value = U256::zero(); + let estimate_gas_req = CallRequest { + value: Some(eth_value), + data: Some(call_data), + from: Some(my_address), + to: Some(contract_addr), + ..CallRequest::default() + }; + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + let estimate_gas_req = call_request_with_pay_for_gas_option(estimate_gas_req, pay_for_gas_option); + coin.estimate_gas_wrapper(estimate_gas_req) + .compat() + .await + .map_to_mm(Web3RpcError::from) } fn eth_balance(&self) -> BalanceFut { - Box::new( - self.web3 - .eth() - .balance(self.my_address, Some(BlockNumber::Latest)) - .compat() - .map_to_mm_fut(BalanceError::from), - ) + let coin = self.clone(); + let fut = async move { + let my_address = coin.derivation_method.single_addr_or_err().await?; + coin.balance(my_address, Some(BlockNumber::Latest)) + .await + .map_to_mm(BalanceError::from) + }; + Box::new(fut.boxed().compat()) } - async fn call_request(&self, to: Address, value: Option, data: Option) -> Result { + pub(crate) async fn call_request( + &self, + from: Address, + to: Address, + value: Option, + data: Option, + ) -> Result { let request = CallRequest { - from: Some(self.my_address), + from: Some(from), to: Some(to), gas: None, gas_price: None, @@ -3998,10 +4519,7 @@ impl EthCoin { ..CallRequest::default() }; - self.web3 - .eth() - .call(request, Some(BlockId::Number(BlockNumber::Latest))) - .await + self.call(request, Some(BlockId::Number(BlockNumber::Latest))).await } fn allowance(&self, spender: Address) -> Web3RpcFut { @@ -4013,9 +4531,12 @@ impl EthCoin { )), EthCoinType::Erc20 { ref token_addr, .. } => { let function = ERC20_CONTRACT.function("allowance")?; - let data = function.encode_input(&[Token::Address(coin.my_address), Token::Address(spender)])?; + let my_address = coin.derivation_method.single_addr_or_err().await?; + let data = function.encode_input(&[Token::Address(my_address), Token::Address(spender)])?; - let res = coin.call_request(*token_addr, None, Some(data.into())).await?; + let res = coin + .call_request(my_address, *token_addr, None, Some(data.into())) + .await?; let decoded = function.decode_output(&res.0)?; match decoded[0] { @@ -4026,6 +4547,7 @@ impl EthCoin { }, } }, + EthCoinType::Nft { .. } => MmError::err(Web3RpcError::NftProtocolNotSupported), } }; Box::new(fut.boxed().compat()) @@ -4071,17 +4593,21 @@ impl EthCoin { let token_addr = match coin.coin_type { EthCoinType::Eth => return TX_PLAIN_ERR!("'approve' is expected to be call for ERC20 coins only"), EthCoinType::Erc20 { token_addr, .. } => token_addr, + EthCoinType::Nft { .. } => { + return Err(TransactionErr::ProtocolNotSupported(ERRL!( + "Nft Protocol is not supported yet!" + ))) + }, }; let function = try_tx_s!(ERC20_CONTRACT.function("approve")); let data = try_tx_s!(function.encode_input(&[Token::Address(spender), Token::Uint(amount)])); let gas_limit = try_tx_s!( coin.estimate_gas_for_contract_call(token_addr, Bytes::from(data.clone())) - .compat() .await ); - coin.sign_and_send_transaction(0.into(), Action::Call(token_addr), data, gas_limit) + coin.sign_and_send_transaction(0.into(), Call(token_addr), data, gas_limit) .compat() .await }; @@ -4103,7 +4629,10 @@ impl EthCoin { .address(vec![swap_contract_address]) .build(); - Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) + let coin = self.clone(); + + let fut = async move { coin.logs(filter).await.map_err(|e| ERRL!("{}", e)) }; + Box::new(fut.boxed().compat()) } /// Gets `ReceiverSpent` events from etomic swap smart contract since `from_block` @@ -4121,7 +4650,10 @@ impl EthCoin { .address(vec![swap_contract_address]) .build(); - Box::new(self.web3.eth().logs(filter).compat().map_err(|e| ERRL!("{}", e))) + let coin = self.clone(); + + let fut = async move { coin.logs(filter).await.map_err(|e| ERRL!("{}", e)) }; + Box::new(fut.boxed().compat()) } fn validate_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { @@ -4130,7 +4662,7 @@ impl EthCoin { .try_to_address() .map_to_mm(ValidatePaymentError::InvalidParameter)); - let unsigned: UnverifiedTransaction = try_f!(rlp::decode(&input.payment_tx)); + let unsigned: UnverifiedTransactionWrapper = try_f!(rlp::decode(&input.payment_tx)); let tx = try_f!(SignedEthTx::new(unsigned) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))); @@ -4162,9 +4694,9 @@ impl EthCoin { ))); } - let tx_from_rpc = selfi.web3.eth().transaction(TransactionId::Hash(tx.hash)).await?; + let tx_from_rpc = selfi.transaction(TransactionId::Hash(tx.tx_hash())).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { - ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx.hash)) + ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx.tx_hash())) })?; if tx_from_rpc.from != Some(sender) { @@ -4174,6 +4706,7 @@ impl EthCoin { ))); } + let my_address = selfi.derivation_method.single_addr_or_err().await?; match &selfi.coin_type { EthCoinType::Eth => { let mut expected_value = trade_amount; @@ -4200,11 +4733,11 @@ impl EthCoin { ))); } - if decoded[1] != Token::Address(selfi.my_address) { + if decoded[1] != Token::Address(my_address) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx receiver arg {:?} is invalid, expected {:?}", decoded[1], - Token::Address(selfi.my_address) + Token::Address(my_address) ))); } @@ -4251,7 +4784,11 @@ impl EthCoin { )?; match watcher_reward.reward_target { - RewardTarget::None | RewardTarget::PaymentReceiver => (), + RewardTarget::None | RewardTarget::PaymentReceiver => { + if watcher_reward.send_contract_reward_on_spend { + expected_value += actual_reward_amount + } + }, RewardTarget::PaymentSender | RewardTarget::PaymentSpender | RewardTarget::Contract => { expected_value += actual_reward_amount }, @@ -4300,11 +4837,11 @@ impl EthCoin { ))); } - if decoded[3] != Token::Address(selfi.my_address) { + if decoded[3] != Token::Address(my_address) { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx receiver arg {:?} is invalid, expected {:?}", decoded[3], - Token::Address(selfi.my_address), + Token::Address(my_address), ))); } @@ -4339,7 +4876,23 @@ impl EthCoin { ))); } - let expected_reward_amount = wei_from_big_decimal(&watcher_reward.amount, decimals)?; + let expected_reward_amount = match watcher_reward.reward_target { + RewardTarget::Contract | RewardTarget::PaymentSender => { + wei_from_big_decimal(&watcher_reward.amount, ETH_DECIMALS)? + }, + RewardTarget::PaymentSpender => { + wei_from_big_decimal(&watcher_reward.amount, selfi.decimals)? + }, + _ => { + // TODO tests passed without this change, need to research on how it worked + if watcher_reward.send_contract_reward_on_spend { + wei_from_big_decimal(&watcher_reward.amount, ETH_DECIMALS)? + } else { + 0.into() + } + }, + }; + let actual_reward_amount = get_function_input_data(&decoded, function, 8) .map_to_mm(ValidatePaymentError::TxDeserializationError)? .into_uint() @@ -4360,7 +4913,11 @@ impl EthCoin { expected_value += actual_reward_amount }, RewardTarget::PaymentSpender => expected_amount += actual_reward_amount, - _ => (), + _ => { + if watcher_reward.send_contract_reward_on_spend { + expected_value += actual_reward_amount + } + }, }; if decoded[1] != Token::Uint(expected_amount) { @@ -4374,10 +4931,11 @@ impl EthCoin { if tx_from_rpc.value != expected_value { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Payment tx value arg {:?} is invalid, expected {:?}", - tx_from_rpc.value, trade_amount + tx_from_rpc.value, expected_value ))); } }, + EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), } Ok(()) @@ -4395,9 +4953,18 @@ impl EthCoin { let data = try_fus!(function.encode_input(&[token])); let coin = self.clone(); - let fut = async move { coin.call_request(swap_contract_address, None, Some(data.into())).await }; + let fut = async move { + let my_address = coin + .derivation_method + .single_addr_or_err() + .await + .map_err(|e| ERRL!("{}", e))?; + coin.call_request(my_address, swap_contract_address, None, Some(data.into())) + .await + .map_err(|e| ERRL!("{}", e)) + }; - Box::new(fut.boxed().compat().map_err(|e| ERRL!("{}", e)).and_then(move |bytes| { + Box::new(fut.boxed().compat().and_then(move |bytes| { let decoded_tokens = try_s!(function.decode_output(&bytes.0)); let state = decoded_tokens .get(2) @@ -4417,16 +4984,17 @@ impl EthCoin { search_from_block: u64, watcher_reward: bool, ) -> Result, String> { - let unverified: UnverifiedTransaction = try_s!(rlp::decode(tx)); + let unverified: UnverifiedTransactionWrapper = try_s!(rlp::decode(tx)); let tx = try_s!(SignedEthTx::new(unverified)); let func_name = match self.coin_type { EthCoinType::Eth => get_function_name("ethPayment", watcher_reward), EthCoinType::Erc20 { .. } => get_function_name("erc20Payment", watcher_reward), + EthCoinType::Nft { .. } => return ERR!("Nft Protocol is not supported yet!"), }; let payment_func = try_s!(SWAP_CONTRACT.function(&func_name)); - let decoded = try_s!(decode_contract_call(payment_func, &tx.data)); + let decoded = try_s!(decode_contract_call(payment_func, tx.unsigned().data())); let id = match decoded.first() { Some(Token::FixedBytes(bytes)) => bytes.clone(), invalid_token => return ERR!("Expected Token::FixedBytes, got {:?}", invalid_token), @@ -4452,8 +5020,7 @@ impl EthCoin { if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = match try_s!(self.web3.eth().transaction(TransactionId::Hash(tx_hash)).await) - { + let transaction = match try_s!(self.transaction(TransactionId::Hash(tx_hash)).await) { Some(t) => t, None => { return ERR!("Found ReceiverSpent event, but transaction {:02x} is missing", tx_hash) @@ -4478,8 +5045,7 @@ impl EthCoin { if let Some(event) = found { match event.transaction_hash { Some(tx_hash) => { - let transaction = match try_s!(self.web3.eth().transaction(TransactionId::Hash(tx_hash)).await) - { + let transaction = match try_s!(self.transaction(TransactionId::Hash(tx_hash)).await) { Some(t) => t, None => { return ERR!("Found SenderRefunded event, but transaction {:02x} is missing", tx_hash) @@ -4504,52 +5070,39 @@ impl EthCoin { } pub async fn get_watcher_reward_amount(&self, wait_until: u64) -> Result> { - let gas_price = repeatable!(async { self.get_gas_price().compat().await.retry_on_err() }) - .until_s(wait_until) - .repeat_every_secs(10.) - .await - .map_err(|_| WatcherRewardError::RPCError("Error getting the gas price".to_string()))?; + let pay_for_gas_option = repeatable!(async { + self.get_swap_pay_for_gas_option(self.get_swap_transaction_fee_policy()) + .await + .retry_on_err() + }) + .until_s(wait_until) + .repeat_every_secs(10.) + .await + .map_err(|_| WatcherRewardError::RPCError("Error getting the gas price".to_string()))?; - let gas_cost_wei = U256::from(REWARD_GAS_AMOUNT) * gas_price; - let gas_cost_eth = - u256_to_big_decimal(gas_cost_wei, 18).map_err(|e| WatcherRewardError::InternalError(e.to_string()))?; + let gas_cost_wei = calc_total_fee(U256::from(REWARD_GAS_AMOUNT), &pay_for_gas_option) + .map_err(|e| WatcherRewardError::InternalError(e.to_string()))?; + let gas_cost_eth = u256_to_big_decimal(gas_cost_wei, ETH_DECIMALS) + .map_err(|e| WatcherRewardError::InternalError(e.to_string()))?; Ok(gas_cost_eth) } /// Get gas price - pub fn get_gas_price(&self) -> Web3RpcFut { + pub async fn get_gas_price(&self) -> Web3RpcResult { let coin = self.clone(); - let fut = async move { - // TODO refactor to error_log_passthrough once simple maker bot is merged - let gas_station_price = match &coin.gas_station_url { - Some(url) => { - match GasStationData::get_gas_price(url, coin.gas_station_decimals, coin.gas_station_policy.clone()) - .compat() - .await - { - Ok(from_station) => Some(increase_by_percent_one_gwei(from_station, GAS_PRICE_PERCENT)), - Err(e) => { - error!("Error {} on request to gas station url {}", e, url); - None - }, - } - }, - None => None, - }; - - let eth_gas_price = match coin.web3.eth().gas_price().await { + let eth_gas_price_fut = async { + match coin.gas_price().await { Ok(eth_gas) => Some(eth_gas), Err(e) => { error!("Error {} on eth_gasPrice request", e); None }, - }; + } + } + .boxed(); - let fee_history_namespace: EthFeeHistoryNamespace<_> = coin.web3.api(); - let eth_fee_history_price = match fee_history_namespace - .eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[]) - .await - { + let eth_fee_history_price_fut = async { + match coin.eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[]).await { Ok(res) => res .base_fee_per_gas .first() @@ -4558,16 +5111,83 @@ impl EthCoin { debug!("Error {} on eth_feeHistory request", e); None }, - }; + } + } + .boxed(); + + let (eth_gas_price, eth_fee_history_price) = join(eth_gas_price_fut, eth_fee_history_price_fut).await; + // on editions < 2021 the compiler will resolve array.into_iter() as (&array).into_iter() + // https://doc.rust-lang.org/edition-guide/rust-2021/IntoIterator-for-arrays.html#details + IntoIterator::into_iter([eth_gas_price, eth_fee_history_price]) + .flatten() + .max() + .or_mm_err(|| Web3RpcError::Internal("All requests failed".into())) + } - // on editions < 2021 the compiler will resolve array.into_iter() as (&array).into_iter() - // https://doc.rust-lang.org/edition-guide/rust-2021/IntoIterator-for-arrays.html#details - IntoIterator::into_iter([gas_station_price, eth_gas_price, eth_fee_history_price]) - .flatten() - .max() - .or_mm_err(|| Web3RpcError::Internal("All requests failed".into())) + /// Get gas base fee and suggest priority tip fees for the next block (see EIP-1559) + pub async fn get_eip1559_gas_fee(&self) -> Web3RpcResult { + let coin = self.clone(); + let history_estimator_fut = FeePerGasSimpleEstimator::estimate_fee_by_history(&coin); + let ctx = + MmArc::from_weak(&coin.ctx).ok_or_else(|| MmError::new(Web3RpcError::Internal("ctx is null".into())))?; + let gas_api_conf = ctx.conf["gas_api"].clone(); + if gas_api_conf.is_null() { + debug!("No eth gas api provider config, using only history estimator"); + return history_estimator_fut + .await + .map_err(|e| MmError::new(Web3RpcError::Internal(e.to_string()))); + } + let gas_api_conf: GasApiConfig = json::from_value(gas_api_conf) + .map_err(|e| MmError::new(Web3RpcError::InvalidGasApiConfig(e.to_string())))?; + let provider_estimator_fut = match gas_api_conf.provider { + GasApiProvider::Infura => InfuraGasApiCaller::fetch_infura_fee_estimation(&gas_api_conf.url).boxed(), + GasApiProvider::Blocknative => { + BlocknativeGasApiCaller::fetch_blocknative_fee_estimation(&gas_api_conf.url).boxed() + }, }; - Box::new(fut.boxed().compat()) + provider_estimator_fut + .or_else(|provider_estimator_err| { + debug!( + "Call to eth gas api provider failed {}, using internal fee estimator", + provider_estimator_err + ); + history_estimator_fut.map_err(move |history_estimator_err| { + MmError::new(Web3RpcError::Internal(format!( + "All gas api requests failed, provider estimator error: {}, history estimator error: {}", + provider_estimator_err, history_estimator_err + ))) + }) + }) + .await + } + + async fn get_swap_pay_for_gas_option(&self, swap_fee_policy: SwapTxFeePolicy) -> Web3RpcResult { + let coin = self.clone(); + match swap_fee_policy { + SwapTxFeePolicy::Internal => { + let gas_price = coin.get_gas_price().await?; + Ok(PayForGasOption::Legacy(LegacyGasPrice { gas_price })) + }, + SwapTxFeePolicy::Low | SwapTxFeePolicy::Medium | SwapTxFeePolicy::High => { + let fee_per_gas = coin.get_eip1559_gas_fee().await?; + let pay_result = match swap_fee_policy { + SwapTxFeePolicy::Low => PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas: fee_per_gas.low.max_fee_per_gas, + max_priority_fee_per_gas: fee_per_gas.low.max_priority_fee_per_gas, + }), + SwapTxFeePolicy::Medium => PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas: fee_per_gas.medium.max_fee_per_gas, + max_priority_fee_per_gas: fee_per_gas.medium.max_priority_fee_per_gas, + }), + _ => PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas: fee_per_gas.high.max_fee_per_gas, + max_priority_fee_per_gas: fee_per_gas.high.max_priority_fee_per_gas, + }), + }; + Ok(pay_result) + }, + SwapTxFeePolicy::Unsupported => Err(MmError::new(Web3RpcError::Internal("swap fee policy not set".into()))), + } } /// Checks every second till at least one ETH node recognizes that nonce is increased. @@ -4582,11 +5202,11 @@ impl EthCoin { /// The function is endless, we just keep looping in case of a transport error hoping it will go away. async fn wait_for_addr_nonce_increase(&self, addr: Address, prev_nonce: U256) { repeatable!(async { - match get_addr_nonce(addr, self.web3_instances.clone()).compat().await { + match self.clone().get_addr_nonce(addr).compat().await { Ok((new_nonce, _)) if new_nonce > prev_nonce => Ready(()), Ok((_nonce, _)) => Retry(()), Err(e) => { - error!("Error getting {} {} nonce: {}", self.ticker(), self.my_address, e); + error!("Error getting {} {} nonce: {}", self.ticker(), addr, e); Retry(()) }, } @@ -4607,7 +5227,7 @@ impl EthCoin { ) -> Web3RpcResult> { let wait_until = wait_until_ms(wait_rpc_timeout_ms); while now_ms() < wait_until { - let maybe_tx = self.web3.eth().transaction(TransactionId::Hash(tx_hash)).await?; + let maybe_tx = self.transaction(TransactionId::Hash(tx_hash)).await?; if let Some(tx) = maybe_tx { let signed_tx = signed_tx_from_web3_tx(tx).map_to_mm(Web3RpcError::InvalidResponse)?; return Ok(Some(signed_tx)); @@ -4636,7 +5256,7 @@ impl EthCoin { ))); } - let web3_receipt = match selfi.web3.eth().transaction_receipt(payment_hash).await { + let web3_receipt = match selfi.transaction_receipt(payment_hash).await { Ok(r) => r, Err(e) => { error!( @@ -4684,7 +5304,7 @@ impl EthCoin { ))); } - match selfi.web3.eth().block_number().await { + match selfi.block_number().await { Ok(current_block) => { if current_block >= block_number { break Ok(()); @@ -4704,30 +5324,134 @@ impl EthCoin { }; Box::new(fut.boxed().compat()) } + + async fn spawn_balance_stream_if_enabled(&self, ctx: &MmArc) -> Result<(), String> { + if let Some(stream_config) = &ctx.event_stream_configuration { + if let EventInitStatus::Failed(err) = EventBehaviour::spawn_if_active(self.clone(), stream_config).await { + return ERR!("Failed spawning balance events. Error: {}", err); + } + } + + Ok(()) + } + + /// Requests the nonce from all available nodes and returns the highest nonce available with the list of nodes that returned the highest nonce. + /// Transactions will be sent using the nodes that returned the highest nonce. + pub fn get_addr_nonce( + self, + addr: Address, + ) -> Box), Error = String> + Send> { + const TMP_SOCKET_DURATION: Duration = Duration::from_secs(300); + + let fut = async move { + let mut errors: u32 = 0; + let web3_instances = self.web3_instances.lock().await.to_vec(); + loop { + let (futures, web3_instances): (Vec<_>, Vec<_>) = web3_instances + .iter() + .map(|instance| { + if let Web3Transport::Websocket(socket_transport) = instance.web3.transport() { + socket_transport.maybe_spawn_temporary_connection_loop( + self.clone(), + Instant::now() + TMP_SOCKET_DURATION, + ); + }; + + if instance.is_parity { + let parity: ParityNonce<_> = instance.web3.api(); + (Either::Left(parity.parity_next_nonce(addr)), instance.clone()) + } else { + ( + Either::Right(instance.web3.eth().transaction_count(addr, Some(BlockNumber::Pending))), + instance.clone(), + ) + } + }) + .unzip(); + + let nonces: Vec<_> = join_all(futures) + .await + .into_iter() + .zip(web3_instances.into_iter()) + .filter_map(|(nonce_res, instance)| match nonce_res { + Ok(n) => Some((n, instance)), + Err(e) => { + error!("Error getting nonce for addr {:?}: {}", addr, e); + None + }, + }) + .collect(); + if nonces.is_empty() { + // all requests errored + errors += 1; + if errors > 5 { + return ERR!("Couldn't get nonce after 5 errored attempts, aborting"); + } + } else { + let max = nonces + .iter() + .map(|(n, _)| *n) + .max() + .expect("nonces should not be empty!"); + break Ok(( + max, + nonces + .into_iter() + .filter_map(|(n, instance)| if n == max { Some(instance) } else { None }) + .collect(), + )); + } + Timer::sleep(1.).await + } + }; + Box::new(Box::pin(fut).compat()) + } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct EthTxFeeDetails { pub coin: String, pub gas: u64, - /// WEI units per 1 gas + /// Gas price in ETH per gas unit + /// if 'max_fee_per_gas' and 'max_priority_fee_per_gas' are used we set 'gas_price' as 'max_fee_per_gas' for compatibility with GUI pub gas_price: BigDecimal, + /// Max fee per gas in ETH per gas unit + pub max_fee_per_gas: Option, + /// Max priority fee per gas in ETH per gas unit + pub max_priority_fee_per_gas: Option, pub total_fee: BigDecimal, } impl EthTxFeeDetails { - pub(crate) fn new(gas: U256, gas_price: U256, coin: &str) -> NumConversResult { - let total_fee = gas * gas_price; + pub(crate) fn new(gas: U256, pay_for_gas_option: PayForGasOption, coin: &str) -> NumConversResult { + let total_fee = calc_total_fee(gas, &pay_for_gas_option)?; // Fees are always paid in ETH, can use 18 decimals by default let total_fee = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; + let (gas_price, max_fee_per_gas, max_priority_fee_per_gas) = match pay_for_gas_option { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => (gas_price, None, None), + // Using max_fee_per_gas as estimated gas_price value for compatibility in caller not expecting eip1559 fee per gas values. + // Normally the caller should pay attention to presence of max_fee_per_gas and max_priority_fee_per_gas in the result: + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }) => (max_fee_per_gas, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)), + }; let gas_price = u256_to_big_decimal(gas_price, ETH_DECIMALS)?; - + let (max_fee_per_gas, max_priority_fee_per_gas) = match (max_fee_per_gas, max_priority_fee_per_gas) { + (Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => ( + Some(u256_to_big_decimal(max_fee_per_gas, ETH_DECIMALS)?), + Some(u256_to_big_decimal(max_priority_fee_per_gas, ETH_DECIMALS)?), + ), + (_, _) => (None, None), + }; let gas_u64 = u64::try_from(gas).map_to_mm(|e| NumConversError::new(e.to_string()))?; Ok(EthTxFeeDetails { coin: coin.to_owned(), gas: gas_u64, gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, total_fee, }) } @@ -4800,6 +5524,7 @@ impl MmCoin for EthCoin { match coin.coin_type { EthCoinType::Eth => coin.process_eth_history(&ctx).await, EthCoinType::Erc20 { ref token_addr, .. } => coin.process_erc20_history(*token_addr, &ctx).await, + EthCoinType::Nft {..} => return Err(()) } Ok(()) }; @@ -4812,20 +5537,27 @@ impl MmCoin for EthCoin { fn get_trade_fee(&self) -> Box + Send> { let coin = self.clone(); Box::new( - self.get_gas_price() - .map_err(|e| e.to_string()) - .and_then(move |gas_price| { - let fee = gas_price * U256::from(ETH_GAS); - let fee_coin = match &coin.coin_type { - EthCoinType::Eth => &coin.ticker, - EthCoinType::Erc20 { platform, .. } => platform, - }; - Ok(TradeFee { - coin: fee_coin.into(), - amount: try_s!(u256_to_big_decimal(fee, ETH_DECIMALS)).into(), - paid_from_trading_vol: false, - }) - }), + async move { + let pay_for_gas_option = coin + .get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await + .map_err(|e| e.to_string())?; + + let fee = calc_total_fee(U256::from(coin.gas_limit.eth_max_trade_gas), &pay_for_gas_option) + .map_err(|e| e.to_string())?; + let fee_coin = match &coin.coin_type { + EthCoinType::Eth => &coin.ticker, + EthCoinType::Erc20 { platform, .. } => platform, + EthCoinType::Nft { .. } => return ERR!("Nft Protocol is not supported yet!"), + }; + Ok(TradeFee { + coin: fee_coin.into(), + amount: try_s!(u256_to_big_decimal(fee, ETH_DECIMALS)).into(), + paid_from_trading_vol: false, + }) + } + .boxed() + .compat(), ) } @@ -4833,15 +5565,23 @@ impl MmCoin for EthCoin { &self, value: TradePreimageValue, stage: FeeApproxStage, + include_refund_fee: bool, ) -> TradePreimageResult { - let gas_price = self.get_gas_price().compat().await?; - let gas_price = increase_gas_price_by_stage(gas_price, &stage); + let pay_for_gas_option = self + .get_swap_pay_for_gas_option(self.get_swap_transaction_fee_policy()) + .await?; + let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage); let gas_limit = match self.coin_type { EthCoinType::Eth => { - // this gas_limit includes gas for `ethPayment` and `senderRefund` contract calls - U256::from(300_000) + // this gas_limit includes gas for `ethPayment` and optionally `senderRefund` contract calls + if include_refund_fee { + U256::from(self.gas_limit.eth_payment) + U256::from(self.gas_limit.eth_sender_refund) + } else { + U256::from(self.gas_limit.eth_payment) + } }, EthCoinType::Erc20 { token_addr, .. } => { + let mut gas = U256::from(self.gas_limit.erc20_payment); let value = match value { TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { wei_from_big_decimal(&value, self.decimals)? @@ -4852,28 +5592,31 @@ impl MmCoin for EthCoin { // estimate gas for the `approve` contract call // Pass a dummy spender. Let's use `my_address`. - let spender = self.my_address; + let spender = self.derivation_method.single_addr_or_err().await?; let approve_function = ERC20_CONTRACT.function("approve")?; let approve_data = approve_function.encode_input(&[Token::Address(spender), Token::Uint(value)])?; let approve_gas_limit = self .estimate_gas_for_contract_call(token_addr, Bytes::from(approve_data)) - .compat() .await?; - // this gas_limit includes gas for `approve`, `erc20Payment` and `senderRefund` contract calls - U256::from(300_000) + approve_gas_limit - } else { - // this gas_limit includes gas for `erc20Payment` and `senderRefund` contract calls - U256::from(300_000) + // this gas_limit includes gas for `approve`, `erc20Payment` contract calls + gas += approve_gas_limit; } + // add 'senderRefund' gas if requested + if include_refund_fee { + gas += U256::from(self.gas_limit.erc20_sender_refund); + } + gas }, + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; - let total_fee = gas_limit * gas_price; + let total_fee = calc_total_fee(gas_limit, &pay_for_gas_option)?; let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; let fee_coin = match &self.coin_type { EthCoinType::Eth => &self.ticker, EthCoinType::Erc20 { platform, .. } => platform, + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; Ok(TradeFee { coin: fee_coin.into(), @@ -4885,14 +5628,22 @@ impl MmCoin for EthCoin { fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { let coin = self.clone(); let fut = async move { - let gas_price = coin.get_gas_price().compat().await?; - let gas_price = increase_gas_price_by_stage(gas_price, &stage); - let total_fee = gas_price * U256::from(ETH_GAS); - let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; - let fee_coin = match &coin.coin_type { - EthCoinType::Eth => &coin.ticker, - EthCoinType::Erc20 { platform, .. } => platform, + let pay_for_gas_option = coin + .get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await?; + let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage); + let (fee_coin, total_fee) = match &coin.coin_type { + EthCoinType::Eth => ( + &coin.ticker, + calc_total_fee(U256::from(coin.gas_limit.eth_receiver_spend), &pay_for_gas_option)?, + ), + EthCoinType::Erc20 { platform, .. } => ( + platform, + calc_total_fee(U256::from(coin.gas_limit.erc20_receiver_spend), &pay_for_gas_option)?, + ), + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; + let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; Ok(TradeFee { coin: fee_coin.into(), amount: amount.into(), @@ -4919,26 +5670,28 @@ impl MmCoin for EthCoin { let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(dex_fee_amount)])?; (0.into(), data, token_addr, platform) }, + EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; - let gas_price = self.get_gas_price().compat().await?; - let gas_price = increase_gas_price_by_stage(gas_price, &stage); + let my_address = self.derivation_method.single_addr_or_err().await?; + let fee_policy_for_estimate = get_swap_fee_policy_for_estimate(self.get_swap_transaction_fee_policy()); + let pay_for_gas_option = self.get_swap_pay_for_gas_option(fee_policy_for_estimate).await?; + let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage); let estimate_gas_req = CallRequest { value: Some(eth_value), data: Some(data.clone().into()), - from: Some(self.my_address), + from: Some(my_address), to: Some(*call_addr), gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), ..CallRequest::default() }; - + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + let estimate_gas_req = call_request_with_pay_for_gas_option(estimate_gas_req, pay_for_gas_option.clone()); // Please note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. // Ideally we should determine the case when we have the insufficient balance and return `TradePreimageError::NotSufficientBalance` error. - let gas_limit = self.estimate_gas(estimate_gas_req).compat().await?; - let total_fee = gas_limit * gas_price; + let gas_limit = self.estimate_gas_wrapper(estimate_gas_req).compat().await?; + let total_fee = calc_total_fee(gas_limit, &pay_for_gas_option)?; let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; Ok(TradeFee { coin: fee_coin.into(), @@ -5025,14 +5778,15 @@ impl TryToAddress for Option { } } -pub trait GuiAuthMessages { - fn gui_auth_sign_message_hash(message: String) -> Option<[u8; 32]>; - fn generate_gui_auth_signed_validation(generator: GuiAuthValidationGenerator) - -> SignatureResult; +pub trait KomodoDefiAuthMessages { + fn proxy_auth_sign_message_hash(message: String) -> Option<[u8; 32]>; + fn generate_proxy_auth_signed_validation( + generator: ProxyAuthValidationGenerator, + ) -> SignatureResult; } -impl GuiAuthMessages for EthCoin { - fn gui_auth_sign_message_hash(message: String) -> Option<[u8; 32]> { +impl KomodoDefiAuthMessages for EthCoin { + fn proxy_auth_sign_message_hash(message: String) -> Option<[u8; 32]> { let message_prefix = "atomicDEX Auth Ethereum Signed Message:\n"; let prefix_len = CompactInteger::from(message_prefix.len()); @@ -5045,16 +5799,16 @@ impl GuiAuthMessages for EthCoin { Some(keccak256(&stream.out()).take()) } - fn generate_gui_auth_signed_validation( - generator: GuiAuthValidationGenerator, - ) -> SignatureResult { - let timestamp_message = get_utc_timestamp() + GUI_AUTH_SIGNED_MESSAGE_LIFETIME_SEC; + fn generate_proxy_auth_signed_validation( + generator: ProxyAuthValidationGenerator, + ) -> SignatureResult { + let timestamp_message = get_utc_timestamp() + PROXY_AUTH_SIGNED_MESSAGE_LIFETIME_SEC; - let message_hash = - EthCoin::gui_auth_sign_message_hash(timestamp_message.to_string()).ok_or(SignatureError::PrefixNotFound)?; + let message_hash = EthCoin::proxy_auth_sign_message_hash(timestamp_message.to_string()) + .ok_or(SignatureError::PrefixNotFound)?; let signature = sign(&generator.secret, &H256::from(message_hash))?; - Ok(GuiAuthValidation { + Ok(KomodefiProxyAuthValidation { coin_ticker: generator.coin_ticker, address: generator.address, timestamp_message, @@ -5075,7 +5829,7 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - let fut = async move { let expected_value = wei_from_big_decimal(&amount, coin.decimals)?; - let tx_from_rpc = coin.web3.eth().transaction(TransactionId::Hash(fee_tx_hash)).await?; + let tx_from_rpc = coin.transaction(TransactionId::Hash(fee_tx_hash)).await?; let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", fee_tx_hash)) @@ -5158,6 +5912,7 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - }, } }, + EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), } Ok(()) @@ -5204,11 +5959,13 @@ fn display_u256_with_decimal_point(number: U256, decimals: u8) -> String { string.trim_end_matches('0').into() } +/// Converts 'number' to value with decimal point and shifts it left by 'decimals' places pub fn u256_to_big_decimal(number: U256, decimals: u8) -> NumConversResult { let string = display_u256_with_decimal_point(number, decimals); Ok(string.parse::()?) } +/// Shifts 'number' with decimal point right by 'decimals' places and converts it to U256 value pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResult { let mut amount = amount.to_string(); let dot = amount.find(|c| c == '.'); @@ -5225,76 +5982,134 @@ pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResu } else { amount.insert_str(amount.len(), &"0".repeat(decimals)); } - U256::from_dec_str(&amount) - .map_err(|e| format!("{:?}", e)) - .map_to_mm(NumConversError::new) + U256::from_dec_str(&amount).map_to_mm(|e| NumConversError::new(format!("{:?}", e))) } impl Transaction for SignedEthTx { fn tx_hex(&self) -> Vec { rlp::encode(self).to_vec() } - fn tx_hash(&self) -> BytesJson { self.hash.0.to_vec().into() } + fn tx_hash_as_bytes(&self) -> BytesJson { self.tx_hash().as_bytes().into() } } fn signed_tx_from_web3_tx(transaction: Web3Transaction) -> Result { - let r = transaction.r.ok_or_else(|| ERRL!("'Transaction::r' is not set"))?; - let s = transaction.s.ok_or_else(|| ERRL!("'Transaction::s' is not set"))?; - let v = transaction - .v - .ok_or_else(|| ERRL!("'Transaction::v' is not set"))? - .as_u64(); - let gas_price = transaction - .gas_price - .ok_or_else(|| ERRL!("'Transaction::gas_price' is not set"))?; - - let unverified = UnverifiedTransaction { - r, - s, - v, - hash: transaction.hash, - unsigned: UnSignedEthTx { - data: transaction.input.0, - gas_price, - gas: transaction.gas, - value: transaction.value, - nonce: transaction.nonce, - action: match transaction.to { - Some(addr) => Action::Call(addr), - None => Action::Create, - }, + // Local function to map the access list + fn map_access_list(web3_access_list: &Option>) -> ethcore_transaction::AccessList { + match web3_access_list { + Some(list) => ethcore_transaction::AccessList( + list.iter() + .map(|item| ethcore_transaction::AccessListItem { + address: item.address, + storage_keys: item.storage_keys.clone(), + }) + .collect(), + ), + None => ethcore_transaction::AccessList(vec![]), + } + } + + // Define transaction types + let type_0: ethereum_types::U64 = 0.into(); + let type_1: ethereum_types::U64 = 1.into(); + let type_2: ethereum_types::U64 = 2.into(); + + // Determine the transaction type + let tx_type = match transaction.transaction_type { + None => TxType::Legacy, + Some(t) if t == type_0 => TxType::Legacy, + Some(t) if t == type_1 => TxType::Type1, + Some(t) if t == type_2 => TxType::Type2, + _ => return Err(ERRL!("'Transaction::transaction_type' unsupported")), + }; + + // Determine the action based on the presence of 'to' field + let action = match transaction.to { + Some(addr) => Action::Call(addr), + None => Action::Create, + }; + + // Initialize the transaction builder + let tx_builder = UnSignedEthTxBuilder::new( + tx_type.clone(), + transaction.nonce, + transaction.gas, + action, + transaction.value, + transaction.input.0, + ); + + // Modify the builder based on the transaction type + let tx_builder = match tx_type { + TxType::Legacy => { + let gas_price = transaction + .gas_price + .ok_or_else(|| ERRL!("'Transaction::gas_price' is not set"))?; + tx_builder.with_gas_price(gas_price) + }, + TxType::Type1 => { + let gas_price = transaction + .gas_price + .ok_or_else(|| ERRL!("'Transaction::gas_price' is not set"))?; + let chain_id = transaction + .chain_id + .ok_or_else(|| ERRL!("'Transaction::chain_id' is not set"))? + .to_string() + .parse() + .map_err(|e: std::num::ParseIntError| e.to_string())?; + tx_builder + .with_gas_price(gas_price) + .with_chain_id(chain_id) + .with_access_list(map_access_list(&transaction.access_list)) }, + TxType::Type2 => { + let max_fee_per_gas = transaction + .max_fee_per_gas + .ok_or_else(|| ERRL!("'Transaction::max_fee_per_gas' is not set"))?; + let max_priority_fee_per_gas = transaction + .max_priority_fee_per_gas + .ok_or_else(|| ERRL!("'Transaction::max_priority_fee_per_gas' is not set"))?; + let chain_id = transaction + .chain_id + .ok_or_else(|| ERRL!("'Transaction::chain_id' is not set"))? + .to_string() + .parse() + .map_err(|e: std::num::ParseIntError| e.to_string())?; + tx_builder + .with_priority_fee_per_gas(max_fee_per_gas, max_priority_fee_per_gas) + .with_chain_id(chain_id) + .with_access_list(map_access_list(&transaction.access_list)) + }, + TxType::Invalid => return Err(ERRL!("Internal error: 'tx_type' invalid")), }; - Ok(try_s!(SignedEthTx::new(unverified))) -} + // Build the unsigned transaction + let unsigned = tx_builder.build().map_err(|err| err.to_string())?; -#[derive(Deserialize, Debug, Serialize)] -pub struct GasStationData { - // matic gas station average fees is named standard, using alias to support both format. - #[serde(alias = "average", alias = "standard")] - average: MmNumber, - fast: MmNumber, -} + // Extract signature components + let r = transaction.r.ok_or_else(|| ERRL!("'Transaction::r' is not set"))?; + let s = transaction.s.ok_or_else(|| ERRL!("'Transaction::s' is not set"))?; + let v = transaction + .v + .ok_or_else(|| ERRL!("'Transaction::v' is not set"))? + .as_u64(); -impl GasStationData { - fn average_gwei(&self, decimals: u8, gas_price_policy: GasStationPricePolicy) -> NumConversResult { - let gas_price = match gas_price_policy { - GasStationPricePolicy::MeanAverageFast => ((&self.average + &self.fast) / MmNumber::from(2)).into(), - GasStationPricePolicy::Average => self.average.to_decimal(), - }; - wei_from_big_decimal(&gas_price, decimals) - } + // Create the signed transaction + let unverified = match unsigned { + TransactionWrapper::Legacy(unsigned) => UnverifiedTransactionWrapper::Legacy( + UnverifiedLegacyTransaction::new_with_network_v(unsigned, r, s, v, transaction.hash) + .map_err(|err| ERRL!("'Transaction::new' error {}", err.to_string()))?, + ), + TransactionWrapper::Eip2930(unsigned) => UnverifiedTransactionWrapper::Eip2930( + UnverifiedEip2930Transaction::new(unsigned, r, s, v, transaction.hash) + .map_err(|err| ERRL!("'Transaction::new' error {}", err.to_string()))?, + ), + TransactionWrapper::Eip1559(unsigned) => UnverifiedTransactionWrapper::Eip1559( + UnverifiedEip1559Transaction::new(unsigned, r, s, v, transaction.hash) + .map_err(|err| ERRL!("'Transaction::new' error {}", err.to_string()))?, + ), + }; - fn get_gas_price(uri: &str, decimals: u8, gas_price_policy: GasStationPricePolicy) -> Web3RpcFut { - let uri = uri.to_owned(); - let fut = async move { - make_gas_station_request(&uri) - .await? - .average_gwei(decimals, gas_price_policy) - .mm_err(|e| Web3RpcError::Internal(e.0)) - }; - Box::new(fut.boxed().compat()) - } + // Return the signed transaction + Ok(try_s!(SignedEthTx::new(unverified))) } async fn get_token_decimals(web3: &Web3, token_addr: Address) -> Result { @@ -5372,9 +6187,43 @@ fn rpc_event_handlers_for_eth_transport(ctx: &MmArc, ticker: String) -> Vec Result, String> { + fn check_max_eth_tx_type_conf(conf: &Json) -> Result, String> { + if !conf["max_eth_tx_type"].is_null() { + let max_eth_tx_type = conf["max_eth_tx_type"] + .as_u64() + .ok_or_else(|| "max_eth_tx_type in coins is invalid".to_string())?; + if max_eth_tx_type > ETH_MAX_TX_TYPE { + return Err("max_eth_tx_type in coins is too big".to_string()); + } + Ok(Some(max_eth_tx_type)) + } else { + Ok(None) + } + } + + match &coin_type { + EthCoinType::Eth => check_max_eth_tx_type_conf(conf), + EthCoinType::Erc20 { platform, .. } | EthCoinType::Nft { platform } => { + let coin_max_eth_tx_type = check_max_eth_tx_type_conf(conf)?; + // Normally we suppose max_eth_tx_type is in platform coin but also try to get it from tokens for tests to work: + if let Some(coin_max_eth_tx_type) = coin_max_eth_tx_type { + Ok(Some(coin_max_eth_tx_type)) + } else { + let platform_coin = lp_coinfind_or_err(ctx, platform).await; + match platform_coin { + Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin.max_eth_tx_type), + _ => Ok(None), + } + } + }, + } +} + #[inline] -fn new_nonce_lock() -> Arc> { Arc::new(AsyncMutex::new(())) } +fn new_nonce_lock() -> HashMap>> { HashMap::new() } +/// Activate eth coin or erc20 token from coin config and private key build policy pub async fn eth_coin_from_conf_and_request( ctx: &MmArc, ticker: &str, @@ -5393,15 +6242,6 @@ pub async fn eth_coin_from_conf_and_request( let mut rng = small_rng(); urls.as_mut_slice().shuffle(&mut rng); - let mut nodes = vec![]; - for url in urls.iter() { - nodes.push(HttpTransportNode { - uri: try_s!(url.parse()), - gui_auth: false, - }); - } - drop_mutability!(nodes); - let swap_contract_address: Address = try_s!(json::from_value(req["swap_contract_address"].clone())); if swap_contract_address == Address::default() { return ERR!("swap_contract_address can't be zero address"); @@ -5414,25 +6254,62 @@ pub async fn eth_coin_from_conf_and_request( } let contract_supports_watchers = req["contract_supports_watchers"].as_bool().unwrap_or_default(); - let path_to_address = try_s!(json::from_value::>( + let path_to_address = try_s!(json::from_value::>( req["path_to_address"].clone() )) .unwrap_or_default(); - let (my_address, key_pair) = - try_s!(build_address_and_priv_key_policy(conf, priv_key_policy, &path_to_address).await); + let (key_pair, derivation_method) = + try_s!(build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, &path_to_address, None).await); let mut web3_instances = vec![]; let event_handlers = rpc_event_handlers_for_eth_transport(ctx, ticker.to_string()); - for node in nodes.iter() { - let transport = Web3Transport::new_http(vec![node.clone()], event_handlers.clone()); + for url in urls.iter() { + let uri: Uri = try_s!(url.parse()); + + let transport = match uri.scheme_str() { + Some("ws") | Some("wss") => { + const TMP_SOCKET_CONNECTION: Duration = Duration::from_secs(20); + + let node = WebsocketTransportNode { + uri: uri.clone(), + gui_auth: false, + }; + let websocket_transport = WebsocketTransport::with_event_handlers(node, event_handlers.clone()); + + // Temporarily start the connection loop (we close the connection once we have the client version below). + // Ideally, it would be much better to not do this workaround, which requires a lot of refactoring or + // dropping websocket support on parity nodes. + let fut = websocket_transport + .clone() + .start_connection_loop(Some(Instant::now() + TMP_SOCKET_CONNECTION)); + let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", uri)); + ctx.spawner().spawn_with_settings(fut, settings); + + Web3Transport::Websocket(websocket_transport) + }, + Some("http") | Some("https") => { + let node = HttpTransportNode { uri, gui_auth: false }; + + Web3Transport::new_http_with_event_handlers(node, event_handlers.clone()) + }, + _ => { + return ERR!( + "Invalid node address '{}'. Only http(s) and ws(s) nodes are supported", + uri + ); + }, + }; + let web3 = Web3::new(transport); let version = match web3.web3().client_version().await { Ok(v) => v, Err(e) => { - error!("Couldn't get client version for url {}: {}", node.uri, e); + error!("Couldn't get client version for url {}: {}", url, e); + continue; }, }; + web3_instances.push(Web3Instance { web3, is_parity: version.contains("Parity") || version.contains("parity"), @@ -5443,9 +6320,6 @@ pub async fn eth_coin_from_conf_and_request( return ERR!("Failed to get client version for all urls"); } - let transport = Web3Transport::new_http(nodes, event_handlers); - let web3 = Web3::new(transport); - let (coin_type, decimals) = match protocol { CoinProtocol::ETH => (EthCoinType::Eth, ETH_DECIMALS), CoinProtocol::ERC20 { @@ -5454,12 +6328,22 @@ pub async fn eth_coin_from_conf_and_request( } => { let token_addr = try_s!(valid_addr_from_str(&contract_address)); let decimals = match conf["decimals"].as_u64() { - None | Some(0) => try_s!(get_token_decimals(&web3, token_addr).await), + None | Some(0) => try_s!( + get_token_decimals( + &web3_instances + .first() + .expect("web3_instances can't be empty in ETH activation") + .web3, + token_addr + ) + .await + ), Some(d) => d as u8, }; (EthCoinType::Erc20 { platform, token_addr }, decimals) }, - _ => return ERR!("Expect ETH or ERC20 protocol"), + CoinProtocol::NFT { platform } => (EthCoinType::Nft { platform }, ETH_DECIMALS), + _ => return ERR!("Expect ETH, ERC20 or NFT protocol"), }; // param from request should override the config @@ -5478,32 +6362,41 @@ pub async fn eth_coin_from_conf_and_request( let sign_message_prefix: Option = json::from_value(conf["sign_message_prefix"].clone()).unwrap_or(None); + let chain_id = try_s!(conf["chain_id"] + .as_u64() + .ok_or_else(|| format!("chain_id is not set for {}", ticker))); + + let trezor_coin: Option = json::from_value(conf["trezor_coin"].clone()).unwrap_or(None); + let initial_history_state = if req["tx_history"].as_bool().unwrap_or(false) { HistorySyncState::NotStarted } else { HistorySyncState::NotEnabled }; - let gas_station_decimals: Option = try_s!(json::from_value(req["gas_station_decimals"].clone())); - let gas_station_policy: GasStationPricePolicy = - json::from_value(req["gas_station_policy"].clone()).unwrap_or_default(); - let key_lock = match &coin_type { EthCoinType::Eth => String::from(ticker), - EthCoinType::Erc20 { ref platform, .. } => String::from(platform), + EthCoinType::Erc20 { platform, .. } | EthCoinType::Nft { platform } => String::from(platform), }; - let mut map = NONCE_LOCK.lock().unwrap(); - - let nonce_lock = map.entry(key_lock).or_insert_with(new_nonce_lock).clone(); + let address_nonce_locks = { + let mut map = NONCE_LOCK.lock().unwrap(); + Arc::new(AsyncMutex::new( + map.entry(key_lock).or_insert_with(new_nonce_lock).clone(), + )) + }; // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to `ETH` coin will be aborted as well. let abortable_system = try_s!(ctx.abortable_system.create_subsystem()); + let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; + let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; + let gas_limit = extract_gas_limit_from_conf(conf)?; + let coin = EthCoinImpl { priv_key_policy: key_pair, - my_address, + derivation_method: Arc::new(derivation_method), coin_type, sign_message_prefix, swap_contract_address, @@ -5511,26 +6404,32 @@ pub async fn eth_coin_from_conf_and_request( contract_supports_watchers, decimals, ticker: ticker.into(), - gas_station_url: try_s!(json::from_value(req["gas_station_url"].clone())), - gas_station_decimals: gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), - gas_station_policy, - web3, - web3_instances, + web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(initial_history_state), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), + max_eth_tx_type, ctx: ctx.weak(), required_confirmations, - chain_id: conf["chain_id"].as_u64(), + chain_id, + trezor_coin, logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), - nonce_lock, + address_nonce_locks, erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), + platform_fee_estimator_state, + gas_limit, abortable_system, }; - Ok(EthCoin(Arc::new(coin))) + + let coin = EthCoin(Arc::new(coin)); + coin.spawn_balance_stream_if_enabled(ctx).await?; + + Ok(coin) } /// Displays the address in mixed-case checksum form /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md -fn checksum_address(addr: &str) -> String { +pub fn checksum_address(addr: &str) -> String { let mut addr = addr.to_lowercase(); if addr.starts_with("0x") { addr.replace_range(..2, ""); @@ -5561,74 +6460,15 @@ fn checksum_address(addr: &str) -> String { /// `eth_addr_to_hex` converts Address to hex format. /// Note: the result will be in lowercase. -pub(crate) fn eth_addr_to_hex(address: &Address) -> String { format!("{:#02x}", address) } +pub fn eth_addr_to_hex(address: &Address) -> String { format!("{:#02x}", address) } /// Checks that input is valid mixed-case checksum form address /// The input must be 0x prefixed hex string fn is_valid_checksum_addr(addr: &str) -> bool { addr == checksum_address(addr) } -/// Requests the nonce from all available nodes and returns the highest nonce available with the list of nodes that returned the highest nonce. -/// Transactions will be sent using the nodes that returned the highest nonce. -#[cfg_attr(test, mockable)] -fn get_addr_nonce( - addr: Address, - web3s: Vec, -) -> Box), Error = String> + Send> { - let fut = async move { - let mut errors: u32 = 0; - loop { - let (futures, web3s): (Vec<_>, Vec<_>) = web3s - .iter() - .map(|web3| { - if web3.is_parity { - let parity: ParityNonce<_> = web3.web3.api(); - (Either::Left(parity.parity_next_nonce(addr)), web3.clone()) - } else { - ( - Either::Right(web3.web3.eth().transaction_count(addr, Some(BlockNumber::Pending))), - web3.clone(), - ) - } - }) - .unzip(); - - let nonces: Vec<_> = join_all(futures) - .await - .into_iter() - .zip(web3s.into_iter()) - .filter_map(|(nonce_res, web3)| match nonce_res { - Ok(n) => Some((n, web3)), - Err(e) => { - error!("Error getting nonce for addr {:?}: {}", addr, e); - None - }, - }) - .collect(); - if nonces.is_empty() { - // all requests errored - errors += 1; - if errors > 5 { - return ERR!("Couldn't get nonce after 5 errored attempts, aborting"); - } - } else { - let max = nonces - .iter() - .map(|(n, _)| *n) - .max() - .expect("nonces should not be empty!"); - break Ok(( - max, - nonces - .into_iter() - .filter_map(|(n, web3)| if n == max { Some(web3) } else { None }) - .collect(), - )); - } - Timer::sleep(1.).await - } - }; - Box::new(Box::pin(fut).compat()) -} +/// `display_eth_address` converts Address to mixed-case checksum form. +#[inline] +pub fn display_eth_address(addr: &Address) -> String { checksum_address(ð_addr_to_hex(addr)) } fn increase_by_percent_one_gwei(num: U256, percent: u64) -> U256 { let one_gwei = U256::from(10u64.pow(9)); @@ -5640,33 +6480,41 @@ fn increase_by_percent_one_gwei(num: U256, percent: u64) -> U256 { } } -fn increase_gas_price_by_stage(gas_price: U256, level: &FeeApproxStage) -> U256 { - match level { - FeeApproxStage::WithoutApprox => gas_price, - FeeApproxStage::StartSwap => { - increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_START_SWAP) - }, - FeeApproxStage::OrderIssue => { - increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_ORDER_ISSUE) - }, - FeeApproxStage::TradePreimage => { - increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE) - }, - FeeApproxStage::WatcherPreimage => { - increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_WATCHER_PREIMAGE) - }, +fn increase_gas_price_by_stage(pay_for_gas_option: PayForGasOption, level: &FeeApproxStage) -> PayForGasOption { + if let PayForGasOption::Legacy(LegacyGasPrice { gas_price }) = pay_for_gas_option { + let new_gas_price = match level { + FeeApproxStage::WithoutApprox => gas_price, + FeeApproxStage::StartSwap => { + increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_START_SWAP) + }, + FeeApproxStage::OrderIssue => { + increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_ORDER_ISSUE) + }, + FeeApproxStage::TradePreimage => { + increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE) + }, + FeeApproxStage::WatcherPreimage => { + increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_WATCHER_PREIMAGE) + }, + }; + PayForGasOption::Legacy(LegacyGasPrice { + gas_price: new_gas_price, + }) + } else { + pay_for_gas_option } } +/// Represents errors that can occur while retrieving an Ethereum address. #[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] pub enum GetEthAddressError { - PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), EthActivationV2Error(EthActivationV2Error), Internal(String), } -impl From for GetEthAddressError { - fn from(e: PrivKeyPolicyNotAllowed) -> Self { GetEthAddressError::PrivKeyPolicyNotAllowed(e) } +impl From for GetEthAddressError { + fn from(e: UnexpectedDerivationMethod) -> Self { GetEthAddressError::UnexpectedDerivationMethod(e) } } impl From for GetEthAddressError { @@ -5677,42 +6525,54 @@ impl From for GetEthAddressError { fn from(e: CryptoCtxError) -> Self { GetEthAddressError::Internal(e.to_string()) } } +// Todo: `get_eth_address` should be removed since NFT is now part of the coins ctx. /// `get_eth_address` returns wallet address for coin with `ETH` protocol type. /// Note: result address has mixed-case checksum form. pub async fn get_eth_address( ctx: &MmArc, conf: &Json, ticker: &str, - path_to_address: &StandardHDCoinAddress, + path_to_address: &HDPathAccountToAddressId, ) -> MmResult { - let priv_key_policy = PrivKeyBuildPolicy::detect_priv_key_policy(ctx)?; - // Convert `PrivKeyBuildPolicy` to `EthPrivKeyBuildPolicy` if it's possible. - let priv_key_policy = EthPrivKeyBuildPolicy::try_from(priv_key_policy)?; + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let priv_key_policy = if crypto_ctx.hw_ctx().is_some() { + PrivKeyBuildPolicy::Trezor + } else { + PrivKeyBuildPolicy::detect_priv_key_policy(ctx)? + } + .into(); - let (my_address, ..) = build_address_and_priv_key_policy(conf, priv_key_policy, path_to_address).await?; - let wallet_address = checksum_address(&format!("{:#02x}", my_address)); + let (_, derivation_method) = + build_address_and_priv_key_policy(ctx, ticker, conf, priv_key_policy, path_to_address, None).await?; + let my_address = match derivation_method { + EthDerivationMethod::SingleAddress(my_address) => my_address, + EthDerivationMethod::HDWallet(_) => { + return Err(MmError::new(GetEthAddressError::UnexpectedDerivationMethod( + UnexpectedDerivationMethod::UnsupportedError("HDWallet is not supported for NFT yet!".to_owned()), + ))); + }, + }; Ok(MyWalletAddress { coin: ticker.to_owned(), - wallet_address, + wallet_address: display_eth_address(&my_address), }) } +/// Errors encountered while validating Ethereum addresses for NFT withdrawal. #[derive(Display)] pub enum GetValidEthWithdrawAddError { - #[display(fmt = "My address {} and from address {} mismatch", my_address, from)] - AddressMismatchError { - my_address: String, - from: String, - }, + /// The specified coin does not support NFT withdrawal. #[display(fmt = "{} coin doesn't support NFT withdrawing", coin)] - CoinDoesntSupportNftWithdraw { - coin: String, - }, + CoinDoesntSupportNftWithdraw { coin: String }, + /// The provided address is invalid. InvalidAddress(String), } -fn get_valid_nft_add_to_withdraw( +/// Validates Ethereum addresses for NFT withdrawal. +/// Returns a tuple of valid `to` address, `token` address, and `EthCoin` instance on success. +/// Errors if the coin doesn't support NFT withdrawal or if the addresses are invalid. +fn get_valid_nft_addr_to_withdraw( coin_enum: MmCoinEnum, to: &str, token_add: &str, @@ -5739,6 +6599,8 @@ pub enum EthGasDetailsErr { Internal(String), #[display(fmt = "Transport: {}", _0)] Transport(String), + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, } impl From for EthGasDetailsErr { @@ -5749,51 +6611,453 @@ impl From for EthGasDetailsErr { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => EthGasDetailsErr::Transport(tr), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => EthGasDetailsErr::Internal(internal), + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => EthGasDetailsErr::Internal(internal), + Web3RpcError::NftProtocolNotSupported => EthGasDetailsErr::NftProtocolNotSupported, } } } -async fn get_eth_gas_details( +async fn get_eth_gas_details_from_withdraw_fee( eth_coin: &EthCoin, fee: Option, eth_value: U256, data: Bytes, + sender_address: Address, call_addr: Address, fungible_max: bool, ) -> MmResult { - match fee { + let pay_for_gas_option = match fee { Some(WithdrawFee::EthGas { gas_price, gas }) => { - let gas_price = wei_from_big_decimal(&gas_price, 9)?; - Ok((gas.into(), gas_price)) + let gas_price = wei_from_big_decimal(&gas_price, ETH_GWEI_DECIMALS)?; + return Ok((gas.into(), PayForGasOption::Legacy(LegacyGasPrice { gas_price }))); + }, + Some(WithdrawFee::EthGasEip1559 { + max_fee_per_gas, + max_priority_fee_per_gas, + gas_option: gas_limit, + }) => { + let max_fee_per_gas = wei_from_big_decimal(&max_fee_per_gas, ETH_GWEI_DECIMALS)?; + let max_priority_fee_per_gas = wei_from_big_decimal(&max_priority_fee_per_gas, ETH_GWEI_DECIMALS)?; + match gas_limit { + EthGasLimitOption::Set(gas) => { + return Ok(( + gas.into(), + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }), + )) + }, + EthGasLimitOption::Calc => + // go to gas estimate code + { + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }) + }, + } }, Some(fee_policy) => { let error = format!("Expected 'EthGas' fee type, found {:?}", fee_policy); - MmError::err(EthGasDetailsErr::InvalidFeePolicy(error)) + return MmError::err(EthGasDetailsErr::InvalidFeePolicy(error)); }, None => { - let gas_price = eth_coin.get_gas_price().compat().await?; - // covering edge case by deducting the standard transfer fee when we want to max withdraw ETH - let eth_value_for_estimate = if fungible_max && eth_coin.coin_type == EthCoinType::Eth { - eth_value - gas_price * U256::from(21000) - } else { - eth_value - }; - let estimate_gas_req = CallRequest { - value: Some(eth_value_for_estimate), - data: Some(data), - from: Some(eth_coin.my_address), - to: Some(call_addr), - gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), - ..CallRequest::default() - }; - // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. - // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. - let gas_limit = eth_coin.estimate_gas(estimate_gas_req).compat().await?; - Ok((gas_limit, gas_price)) + // If WithdrawFee not set use legacy gas price (?) + let gas_price = eth_coin.get_gas_price().await?; + // go to gas estimate code + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) }, + }; + + // covering edge case by deducting the standard transfer fee when we want to max withdraw ETH + let eth_value_for_estimate = if fungible_max && eth_coin.coin_type == EthCoinType::Eth { + eth_value - calc_total_fee(U256::from(eth_coin.gas_limit.eth_send_coins), &pay_for_gas_option)? + } else { + eth_value + }; + + let gas_price = pay_for_gas_option.get_gas_price(); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); + let estimate_gas_req = CallRequest { + value: Some(eth_value_for_estimate), + data: Some(data), + from: Some(sender_address), + to: Some(call_addr), + gas: None, + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + gas_price, + max_priority_fee_per_gas, + max_fee_per_gas, + ..CallRequest::default() + }; + // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. + // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. + let gas_limit = eth_coin.estimate_gas_wrapper(estimate_gas_req).compat().await?; + Ok((gas_limit, pay_for_gas_option)) +} + +/// Calc estimated total gas fee or price +fn calc_total_fee(gas: U256, pay_for_gas_option: &PayForGasOption) -> NumConversResult { + match *pay_for_gas_option { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => gas + .checked_mul(gas_price) + .or_mm_err(|| NumConversError("total fee overflow".into())), + PayForGasOption::Eip1559(Eip1559FeePerGas { max_fee_per_gas, .. }) => gas + .checked_mul(max_fee_per_gas) + .or_mm_err(|| NumConversError("total fee overflow".into())), + } +} + +#[allow(clippy::result_large_err)] +fn tx_builder_with_pay_for_gas_option( + eth_coin: &EthCoin, + tx_builder: UnSignedEthTxBuilder, + pay_for_gas_option: &PayForGasOption, +) -> MmResult { + let tx_builder = match *pay_for_gas_option { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => tx_builder.with_gas_price(gas_price), + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_priority_fee_per_gas, + max_fee_per_gas, + }) => tx_builder + .with_priority_fee_per_gas(max_fee_per_gas, max_priority_fee_per_gas) + .with_chain_id(eth_coin.chain_id), + }; + Ok(tx_builder) +} + +/// convert fee policy for gas estimate requests +fn get_swap_fee_policy_for_estimate(swap_fee_policy: SwapTxFeePolicy) -> SwapTxFeePolicy { + match swap_fee_policy { + SwapTxFeePolicy::Internal => SwapTxFeePolicy::Internal, + // always use 'high' for estimate to avoid max_fee_per_gas less than base_fee errors: + SwapTxFeePolicy::Low | SwapTxFeePolicy::Medium | SwapTxFeePolicy::High => SwapTxFeePolicy::High, + SwapTxFeePolicy::Unsupported => SwapTxFeePolicy::Unsupported, + } +} + +fn call_request_with_pay_for_gas_option(call_request: CallRequest, pay_for_gas_option: PayForGasOption) -> CallRequest { + match pay_for_gas_option { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => CallRequest { + gas_price: Some(gas_price), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + ..call_request + }, + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }) => CallRequest { + gas_price: None, + max_fee_per_gas: Some(max_fee_per_gas), + max_priority_fee_per_gas: Some(max_priority_fee_per_gas), + ..call_request + }, + } +} + +impl ToBytes for Signature { + fn to_bytes(&self) -> Vec { self.to_vec() } +} + +impl ToBytes for SignedEthTx { + fn to_bytes(&self) -> Vec { + let mut stream = RlpStream::new(); + self.rlp_append(&mut stream); + // Handle potential panicking. + if stream.is_finished() { + Vec::from(stream.out()) + } else { + // TODO: Consider returning Result, Error> in future refactoring for better error handling. + warn!("RlpStream was not finished; returning an empty Vec as a fail-safe."); + vec![] + } + } +} + +#[derive(Debug, Display)] +pub enum EthAssocTypesError { + InvalidHexString(String), + TxParseError(String), + ParseSignatureError(String), + KeysError(keys::Error), +} + +impl From for EthAssocTypesError { + fn from(e: DecoderError) -> Self { EthAssocTypesError::TxParseError(e.to_string()) } +} + +impl From for EthAssocTypesError { + fn from(e: keys::Error) -> Self { EthAssocTypesError::KeysError(e) } +} + +#[derive(Debug, Display)] +pub enum EthNftAssocTypesError { + Utf8Error(String), + ParseContractTypeError(ParseContractTypeError), + ParseTokenContractError(String), +} + +impl From for EthNftAssocTypesError { + fn from(e: ParseContractTypeError) -> Self { EthNftAssocTypesError::ParseContractTypeError(e) } +} + +#[async_trait] +impl ParseCoinAssocTypes for EthCoin { + type Address = Address; + type AddressParseError = MmError; + type Pubkey = HtlcPubKey; + type PubkeyParseError = MmError; + type Tx = SignedEthTx; + type TxParseError = MmError; + type Preimage = SignedEthTx; + type PreimageParseError = MmError; + type Sig = Signature; + type SigParseError = MmError; + + async fn my_addr(&self) -> Self::Address { + match self.derivation_method() { + DerivationMethod::SingleAddress(addr) => *addr, + // Todo: Expect should not fail but we need to handle it properly + DerivationMethod::HDWallet(hd_wallet) => hd_wallet + .get_enabled_address() + .await + .expect("Getting enabled address should not fail!") + .address(), + } + } + + fn parse_address(&self, address: &str) -> Result { + Address::from_str(address).map_to_mm(|e| EthAssocTypesError::InvalidHexString(e.to_string())) + } + + fn parse_pubkey(&self, pubkey: &[u8]) -> Result { + HtlcPubKey::from_slice(pubkey).map_to_mm(EthAssocTypesError::from) + } + + fn parse_tx(&self, tx: &[u8]) -> Result { + let unverified: UnverifiedTransactionWrapper = rlp::decode(tx).map_err(EthAssocTypesError::from)?; + SignedEthTx::new(unverified).map_to_mm(|e| EthAssocTypesError::TxParseError(e.to_string())) + } + + fn parse_preimage(&self, tx: &[u8]) -> Result { self.parse_tx(tx) } + + fn parse_signature(&self, sig: &[u8]) -> Result { + if sig.len() != 65 { + return MmError::err(EthAssocTypesError::ParseSignatureError( + "Signature slice is not 65 bytes long".to_string(), + )); + }; + + let mut arr = [0; 65]; + arr.copy_from_slice(sig); + Ok(Signature::from(arr)) // Assuming `Signature::from([u8; 65])` exists + } +} + +impl ToBytes for Address { + fn to_bytes(&self) -> Vec { self.0.to_vec() } +} + +impl ToBytes for BigUint { + fn to_bytes(&self) -> Vec { self.to_bytes_be() } +} + +impl ToBytes for ContractType { + fn to_bytes(&self) -> Vec { self.to_string().into_bytes() } +} + +impl ParseNftAssocTypes for EthCoin { + type ContractAddress = Address; + type TokenId = BigUint; + type ContractType = ContractType; + type NftAssocTypesError = MmError; + + fn parse_contract_address( + &self, + contract_address: &[u8], + ) -> Result { + contract_address + .try_to_address() + .map_to_mm(EthNftAssocTypesError::ParseTokenContractError) + } + + fn parse_token_id(&self, token_id: &[u8]) -> Result { + Ok(BigUint::from_bytes_be(token_id)) + } + + fn parse_contract_type(&self, contract_type: &[u8]) -> Result { + let contract_str = from_utf8(contract_type).map_err(|e| EthNftAssocTypesError::Utf8Error(e.to_string()))?; + ContractType::from_str(contract_str).map_to_mm(EthNftAssocTypesError::from) + } +} + +#[async_trait] +impl MakerNftSwapOpsV2 for EthCoin { + async fn send_nft_maker_payment_v2( + &self, + args: SendNftMakerPaymentArgs<'_, Self>, + ) -> Result { + self.send_nft_maker_payment_v2_impl(args).await + } + + async fn validate_nft_maker_payment_v2( + &self, + args: ValidateNftMakerPaymentArgs<'_, Self>, + ) -> ValidatePaymentResult<()> { + self.validate_nft_maker_payment_v2_impl(args).await + } + + async fn spend_nft_maker_payment_v2( + &self, + args: SpendNftMakerPaymentArgs<'_, Self>, + ) -> Result { + self.spend_nft_maker_payment_v2_impl(args).await + } + + async fn refund_nft_maker_payment_v2_timelock( + &self, + args: RefundPaymentArgs<'_>, + ) -> Result { + self.refund_nft_maker_payment_v2_timelock_impl(args).await + } + + async fn refund_nft_maker_payment_v2_secret( + &self, + _args: RefundMakerPaymentArgs<'_, Self>, + ) -> Result { + todo!() + } +} + +impl CoinWithPrivKeyPolicy for EthCoin { + type KeyPair = KeyPair; + + fn priv_key_policy(&self) -> &PrivKeyPolicy { &self.priv_key_policy } +} + +impl CoinWithDerivationMethod for EthCoin { + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet> { &self.derivation_method } +} + +#[async_trait] +impl IguanaBalanceOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn iguana_balances(&self) -> BalanceResult { + let platform_balance = self.my_balance().compat().await?; + let token_balances = self.get_tokens_balance_list().await?; + let mut balances = CoinBalanceMap::new(); + balances.insert(self.ticker().to_string(), platform_balance); + balances.extend(token_balances.into_iter()); + Ok(balances) + } +} + +#[async_trait] +impl GetNewAddressRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + async fn get_new_address_rpc_without_conf( + &self, + params: GetNewAddressParams, + ) -> MmResult, GetNewAddressRpcError> { + get_new_address::common_impl::get_new_address_rpc_without_conf(self, params).await + } + + async fn get_new_address_rpc( + &self, + params: GetNewAddressParams, + confirm_address: &ConfirmAddress, + ) -> MmResult, GetNewAddressRpcError> + where + ConfirmAddress: HDConfirmAddress, + { + get_new_address::common_impl::get_new_address_rpc(self, params, confirm_address).await + } +} + +#[async_trait] +impl AccountBalanceRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn account_balance_rpc( + &self, + params: AccountBalanceParams, + ) -> MmResult, HDAccountBalanceRpcError> { + account_balance::common_impl::account_balance_rpc(self, params).await + } +} + +#[async_trait] +impl InitAccountBalanceRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn init_account_balance_rpc( + &self, + params: InitAccountBalanceParams, + ) -> MmResult, HDAccountBalanceRpcError> { + init_account_balance::common_impl::init_account_balance_rpc(self, params).await + } +} + +#[async_trait] +impl InitScanAddressesRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn init_scan_for_new_addresses_rpc( + &self, + params: ScanAddressesParams, + ) -> MmResult, HDAccountBalanceRpcError> { + init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await + } +} + +#[async_trait] +impl InitCreateAccountRpcOps for EthCoin { + type BalanceObject = CoinBalanceMap; + + async fn init_create_account_rpc( + &self, + params: CreateNewAccountParams, + state: CreateAccountState, + xpub_extractor: Option, + ) -> MmResult, CreateAccountRpcError> + where + XPubExtractor: HDXPubExtractor + Send, + { + init_create_account::common_impl::init_create_new_account_rpc(self, params, state, xpub_extractor).await + } + + async fn revert_creating_account(&self, account_id: u32) { + init_create_account::common_impl::revert_creating_account(self, account_id).await + } +} + +/// Converts and extended public key derived using BIP32 to an Ethereum public key. +pub fn pubkey_from_extended(extended_pubkey: &Secp256k1ExtendedPublicKey) -> Public { + let serialized = extended_pubkey.public_key().serialize_uncompressed(); + let mut pubkey_uncompressed = Public::default(); + pubkey_uncompressed.as_mut().copy_from_slice(&serialized[1..]); + pubkey_uncompressed +} + +fn extract_gas_limit_from_conf(coin_conf: &Json) -> Result { + if coin_conf["gas_limit"].is_null() { + Ok(Default::default()) + } else { + json::from_value(coin_conf["gas_limit"].clone()).map_err(|e| e.to_string()) + } +} + +impl Eip1559Ops for EthCoin { + fn get_swap_transaction_fee_policy(&self) -> SwapTxFeePolicy { self.swap_txfee_policy.lock().unwrap().clone() } + + fn set_swap_transaction_fee_policy(&self, swap_txfee_policy: SwapTxFeePolicy) { + *self.swap_txfee_policy.lock().unwrap() = swap_txfee_policy } } diff --git a/mm2src/coins/eth/eip1559_gas_fee.rs b/mm2src/coins/eth/eip1559_gas_fee.rs new file mode 100644 index 0000000000..4d33781f39 --- /dev/null +++ b/mm2src/coins/eth/eip1559_gas_fee.rs @@ -0,0 +1,499 @@ +//! Provides estimations of base and priority fee per gas or fetch estimations from a gas api provider + +use super::web3_transport::FeeHistoryResult; +use super::{Web3RpcError, Web3RpcResult}; +use crate::{wei_from_gwei_decimal, wei_to_gwei_decimal, EthCoin, NumConversError}; +use ethereum_types::U256; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::or_mm_error::OrMmError; +use mm2_number::BigDecimal; +use num_traits::FromPrimitive; +use std::convert::TryFrom; +use url::Url; +use web3::types::BlockNumber; + +pub(crate) use gas_api::BlocknativeGasApiCaller; +pub(crate) use gas_api::InfuraGasApiCaller; + +use gas_api::{BlocknativeBlockPricesResponse, InfuraFeePerGas}; + +const FEE_PER_GAS_LEVELS: usize = 3; + +/// Indicates which provider was used to get fee per gas estimations +#[derive(Clone, Debug)] +pub enum EstimationSource { + /// filled by default values + Empty, + /// internal simple estimator + Simple, + Infura, + Blocknative, +} + +impl ToString for EstimationSource { + fn to_string(&self) -> String { + match self { + EstimationSource::Empty => "empty".into(), + EstimationSource::Simple => "simple".into(), + EstimationSource::Infura => "infura".into(), + EstimationSource::Blocknative => "blocknative".into(), + } + } +} + +impl Default for EstimationSource { + fn default() -> Self { Self::Empty } +} + +enum PriorityLevelId { + Low = 0, + Medium = 1, + High = 2, +} + +/// Supported gas api providers +#[derive(Deserialize)] +pub enum GasApiProvider { + Infura, + Blocknative, +} + +#[derive(Deserialize)] +pub struct GasApiConfig { + /// gas api provider name to use + pub provider: GasApiProvider, + /// gas api provider or proxy base url (scheme, host and port without the relative part) + pub url: Url, +} + +/// Priority level estimated max fee per gas +#[derive(Clone, Debug, Default)] +pub struct FeePerGasLevel { + /// estimated max priority tip fee per gas in wei + pub max_priority_fee_per_gas: U256, + /// estimated max fee per gas in wei + pub max_fee_per_gas: U256, + /// estimated transaction min wait time in mempool in ms for this priority level + pub min_wait_time: Option, + /// estimated transaction max wait time in mempool in ms for this priority level + pub max_wait_time: Option, +} + +/// Internal struct for estimated fee per gas for several priority levels, in wei +/// low/medium/high levels are supported +#[derive(Default, Debug, Clone)] +pub struct FeePerGasEstimated { + /// base fee for the next block in wei + pub base_fee: U256, + /// estimated low priority fee + pub low: FeePerGasLevel, + /// estimated medium priority fee + pub medium: FeePerGasLevel, + /// estimated high priority fee + pub high: FeePerGasLevel, + /// which estimator used + pub source: EstimationSource, + /// base trend (up or down) + pub base_fee_trend: String, + /// priority trend (up or down) + pub priority_fee_trend: String, +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(infura_fees: InfuraFeePerGas) -> Result { + Ok(Self { + base_fee: wei_from_gwei_decimal!(&infura_fees.estimated_base_fee)?, + low: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.low.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.low.suggested_max_priority_fee_per_gas)?, + min_wait_time: Some(infura_fees.low.min_wait_time_estimate), + max_wait_time: Some(infura_fees.low.max_wait_time_estimate), + }, + medium: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.medium.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal!( + &infura_fees.medium.suggested_max_priority_fee_per_gas + )?, + min_wait_time: Some(infura_fees.medium.min_wait_time_estimate), + max_wait_time: Some(infura_fees.medium.max_wait_time_estimate), + }, + high: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.high.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.high.suggested_max_priority_fee_per_gas)?, + min_wait_time: Some(infura_fees.high.min_wait_time_estimate), + max_wait_time: Some(infura_fees.high.max_wait_time_estimate), + }, + source: EstimationSource::Infura, + base_fee_trend: infura_fees.base_fee_trend, + priority_fee_trend: infura_fees.priority_fee_trend, + }) + } +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(block_prices: BlocknativeBlockPricesResponse) -> Result { + if block_prices.block_prices.is_empty() { + return Ok(FeePerGasEstimated::default()); + } + if block_prices.block_prices[0].estimated_prices.len() < FEE_PER_GAS_LEVELS { + return Ok(FeePerGasEstimated::default()); + } + Ok(Self { + base_fee: wei_from_gwei_decimal!(&block_prices.block_prices[0].base_fee_per_gas)?, + low: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[2].max_fee_per_gas + )?, + max_priority_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[2].max_priority_fee_per_gas + )?, + min_wait_time: None, + max_wait_time: None, + }, + medium: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[1].max_fee_per_gas + )?, + max_priority_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[1].max_priority_fee_per_gas + )?, + min_wait_time: None, + max_wait_time: None, + }, + high: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[0].max_fee_per_gas + )?, + max_priority_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[0].max_priority_fee_per_gas + )?, + min_wait_time: None, + max_wait_time: None, + }, + source: EstimationSource::Blocknative, + base_fee_trend: String::default(), + priority_fee_trend: String::default(), + }) + } +} + +/// Simple priority fee per gas estimator based on fee history +/// normally used if gas api provider is not available +pub(crate) struct FeePerGasSimpleEstimator {} + +impl FeePerGasSimpleEstimator { + // TODO: add minimal max fee and priority fee + /// depth to look for fee history to estimate priority fees + const FEE_PRIORITY_DEPTH: u64 = 5u64; + + /// percentiles to pass to eth_feeHistory + const HISTORY_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [25.0, 50.0, 75.0]; + + /// percentile to predict next base fee over historical rewards + const BASE_FEE_PERCENTILE: f64 = 75.0; + + /// percentiles to calc max priority fee over historical rewards + const PRIORITY_FEE_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [50.0, 50.0, 50.0]; + + /// adjustment for max fee per gas picked up by sampling + const ADJUST_MAX_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.1, 1.175, 1.25]; // 1.25 assures max_fee_per_gas will be over next block base_fee + + /// adjustment for max priority fee picked up by sampling + const ADJUST_MAX_PRIORITY_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.0, 1.0, 1.0]; + + /// block depth for eth_feeHistory + pub fn history_depth() -> u64 { Self::FEE_PRIORITY_DEPTH } + + /// percentiles for priority rewards obtained with eth_feeHistory + pub fn history_percentiles() -> &'static [f64] { &Self::HISTORY_PERCENTILES } + + /// percentile for vector + fn percentile_of(v: &[U256], percent: f64) -> U256 { + let mut v_mut = v.to_owned(); + v_mut.sort(); + + // validate bounds: + let percent = if percent > 100.0 { 100.0 } else { percent }; + let percent = if percent < 0.0 { 0.0 } else { percent }; + + let value_pos = ((v_mut.len() - 1) as f64 * percent / 100.0).round() as usize; + v_mut[value_pos] + } + + /// Estimate simplified gas priority fees based on fee history + pub async fn estimate_fee_by_history(coin: &EthCoin) -> Web3RpcResult { + let res: Result = coin + .eth_fee_history( + U256::from(Self::history_depth()), + BlockNumber::Latest, + Self::history_percentiles(), + ) + .await; + + match res { + Ok(fee_history) => Ok(Self::calculate_with_history(&fee_history)?), + Err(_) => MmError::err(Web3RpcError::Internal("Eth requests failed".into())), + } + } + + fn predict_base_fee(base_fees: &[U256]) -> U256 { Self::percentile_of(base_fees, Self::BASE_FEE_PERCENTILE) } + + fn priority_fee_for_level( + level: PriorityLevelId, + base_fee: BigDecimal, + fee_history: &FeeHistoryResult, + ) -> Web3RpcResult { + let level_index = level as usize; + let level_rewards = fee_history + .priority_rewards + .as_ref() + .or_mm_err(|| Web3RpcError::Internal("expected reward in eth_feeHistory".into()))? + .iter() + .map(|rewards| rewards.get(level_index).copied().unwrap_or_else(|| U256::from(0))) + .collect::>(); + + // Calculate the max priority fee per gas based on the rewards percentile. + let max_priority_fee_per_gas = Self::percentile_of(&level_rewards, Self::PRIORITY_FEE_PERCENTILES[level_index]); + // Convert the priority fee to BigDecimal gwei, falling back to 0 on error. + let max_priority_fee_per_gas_gwei = + wei_to_gwei_decimal!(max_priority_fee_per_gas).unwrap_or_else(|_| BigDecimal::from(0)); + + // Calculate the max fee per gas by adjusting the base fee and adding the priority fee. + let adjust_max_fee = + BigDecimal::from_f64(Self::ADJUST_MAX_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); + let adjust_max_priority_fee = + BigDecimal::from_f64(Self::ADJUST_MAX_PRIORITY_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); + + // TODO: consider use checked ops + let max_fee_per_gas_dec = base_fee * adjust_max_fee + max_priority_fee_per_gas_gwei * adjust_max_priority_fee; + + Ok(FeePerGasLevel { + max_priority_fee_per_gas, + max_fee_per_gas: wei_from_gwei_decimal!(&max_fee_per_gas_dec)?, + // TODO: Consider adding default wait times if applicable (and mark them as uncertain). + min_wait_time: None, + max_wait_time: None, + }) + } + + /// estimate priority fees by fee history + fn calculate_with_history(fee_history: &FeeHistoryResult) -> Web3RpcResult { + // For estimation of max fee and max priority fee we use latest block base_fee but adjusted. + // Apparently for this simple fee estimator for assured high priority we should assume + // that the real base_fee may go up by 1,25 (i.e. if the block is full). This is covered by high priority ADJUST_MAX_FEE multiplier + let latest_base_fee = fee_history + .base_fee_per_gas + .first() + .cloned() + .unwrap_or_else(|| U256::from(0)); + let latest_base_fee_dec = wei_to_gwei_decimal!(latest_base_fee).unwrap_or_else(|_| BigDecimal::from(0)); + + // The predicted base fee is not used for calculating eip1559 values here and is provided for other purposes + // (f.e if the caller would like to do own estimates of max fee and max priority fee) + let predicted_base_fee = Self::predict_base_fee(&fee_history.base_fee_per_gas); + Ok(FeePerGasEstimated { + base_fee: predicted_base_fee, + low: Self::priority_fee_for_level(PriorityLevelId::Low, latest_base_fee_dec.clone(), fee_history)?, + medium: Self::priority_fee_for_level(PriorityLevelId::Medium, latest_base_fee_dec.clone(), fee_history)?, + high: Self::priority_fee_for_level(PriorityLevelId::High, latest_base_fee_dec, fee_history)?, + source: EstimationSource::Simple, + base_fee_trend: String::default(), + priority_fee_trend: String::default(), + }) + } +} + +mod gas_api { + use std::convert::TryInto; + + use super::FeePerGasEstimated; + use crate::eth::{Web3RpcError, Web3RpcResult}; + use http::StatusCode; + use mm2_err_handle::mm_error::MmError; + use mm2_err_handle::prelude::*; + use mm2_net::transport::slurp_url_with_headers; + use mm2_number::BigDecimal; + use serde_json::{self as json}; + use url::Url; + + lazy_static! { + /// API key for testing + static ref INFURA_GAS_API_AUTH_TEST: String = std::env::var("INFURA_GAS_API_AUTH_TEST").unwrap_or_default(); + } + + #[derive(Clone, Debug, Deserialize)] + pub(crate) struct InfuraFeePerGasLevel { + #[serde(rename = "suggestedMaxPriorityFeePerGas")] + pub suggested_max_priority_fee_per_gas: BigDecimal, + #[serde(rename = "suggestedMaxFeePerGas")] + pub suggested_max_fee_per_gas: BigDecimal, + #[serde(rename = "minWaitTimeEstimate")] + pub min_wait_time_estimate: u32, + #[serde(rename = "maxWaitTimeEstimate")] + pub max_wait_time_estimate: u32, + } + + /// Infura gas api response + /// see https://docs.infura.io/api/infura-expansion-apis/gas-api/api-reference/gasprices-type2 + #[allow(dead_code)] + #[derive(Debug, Deserialize)] + pub(crate) struct InfuraFeePerGas { + pub low: InfuraFeePerGasLevel, + pub medium: InfuraFeePerGasLevel, + pub high: InfuraFeePerGasLevel, + #[serde(rename = "estimatedBaseFee")] + pub estimated_base_fee: BigDecimal, + #[serde(rename = "networkCongestion")] + pub network_congestion: BigDecimal, + #[serde(rename = "latestPriorityFeeRange")] + pub latest_priority_fee_range: Vec, + #[serde(rename = "historicalPriorityFeeRange")] + pub historical_priority_fee_range: Vec, + #[serde(rename = "historicalBaseFeeRange")] + pub historical_base_fee_range: Vec, + #[serde(rename = "priorityFeeTrend")] + pub priority_fee_trend: String, // we are not using enum here bcz values not mentioned in docs could be received + #[serde(rename = "baseFeeTrend")] + pub base_fee_trend: String, + } + + /// Infura gas api provider caller + #[allow(dead_code)] + pub(crate) struct InfuraGasApiCaller {} + + #[allow(dead_code)] + impl InfuraGasApiCaller { + const INFURA_GAS_FEES_ENDPOINT: &'static str = "networks/1/suggestedGasFees"; // Support only main chain + + fn get_infura_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { + let mut url = base_url.clone(); + url.set_path(Self::INFURA_GAS_FEES_ENDPOINT); + let headers = vec![("Authorization", INFURA_GAS_API_AUTH_TEST.as_str())]; + (url, headers) + } + + async fn make_infura_gas_api_request( + url: &Url, + headers: Vec<(&'static str, &'static str)>, + ) -> Result> { + let resp = slurp_url_with_headers(url.as_str(), headers) + .await + .mm_err(|e| e.to_string())?; + if resp.0 != StatusCode::OK { + let error = format!("{} failed with status code {}", url, resp.0); + return MmError::err(error); + } + let estimated_fees = json::from_slice(&resp.2).map_to_mm(|e| e.to_string())?; + Ok(estimated_fees) + } + + /// Fetch fee per gas estimations from infura provider + pub async fn fetch_infura_fee_estimation(base_url: &Url) -> Web3RpcResult { + let (url, headers) = Self::get_infura_gas_api_url(base_url); + let infura_estimated_fees = Self::make_infura_gas_api_request(&url, headers) + .await + .mm_err(Web3RpcError::Transport)?; + infura_estimated_fees.try_into().mm_err(Into::into) + } + } + + lazy_static! { + /// API key for testing + static ref BLOCKNATIVE_GAS_API_AUTH_TEST: String = std::env::var("BLOCKNATIVE_GAS_API_AUTH_TEST").unwrap_or_default(); + } + + #[allow(dead_code)] + #[derive(Clone, Debug, Deserialize)] + pub(crate) struct BlocknativeBlockPrices { + #[serde(rename = "blockNumber")] + pub block_number: u32, + #[serde(rename = "estimatedTransactionCount")] + pub estimated_transaction_count: u32, + #[serde(rename = "baseFeePerGas")] + pub base_fee_per_gas: BigDecimal, + #[serde(rename = "estimatedPrices")] + pub estimated_prices: Vec, + } + + #[allow(dead_code)] + #[derive(Clone, Debug, Deserialize)] + pub(crate) struct BlocknativeEstimatedPrices { + pub confidence: u32, + pub price: BigDecimal, + #[serde(rename = "maxPriorityFeePerGas")] + pub max_priority_fee_per_gas: BigDecimal, + #[serde(rename = "maxFeePerGas")] + pub max_fee_per_gas: BigDecimal, + } + + /// Blocknative gas prices response + /// see https://docs.blocknative.com/gas-prediction/gas-platform + #[allow(dead_code)] + #[derive(Debug, Deserialize)] + pub(crate) struct BlocknativeBlockPricesResponse { + pub system: String, + pub network: String, + pub unit: String, + #[serde(rename = "maxPrice")] + pub max_price: BigDecimal, + #[serde(rename = "currentBlockNumber")] + pub current_block_number: u32, + #[serde(rename = "msSinceLastBlock")] + pub ms_since_last_block: u32, + #[serde(rename = "blockPrices")] + pub block_prices: Vec, + } + + /// Blocknative gas api provider caller + #[allow(dead_code)] + pub(crate) struct BlocknativeGasApiCaller {} + + #[allow(dead_code)] + impl BlocknativeGasApiCaller { + const BLOCKNATIVE_GAS_PRICES_ENDPOINT: &'static str = "gasprices/blockprices"; + const BLOCKNATIVE_GAS_PRICES_LOW: &'static str = "10"; + const BLOCKNATIVE_GAS_PRICES_MEDIUM: &'static str = "50"; + const BLOCKNATIVE_GAS_PRICES_HIGH: &'static str = "90"; + + fn get_blocknative_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { + let mut url = base_url.clone(); + url.set_path(Self::BLOCKNATIVE_GAS_PRICES_ENDPOINT); + url.query_pairs_mut() + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_LOW) + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_MEDIUM) + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_HIGH) + .append_pair("withBaseFees", "true"); + + let headers = vec![("Authorization", BLOCKNATIVE_GAS_API_AUTH_TEST.as_str())]; + (url, headers) + } + + async fn make_blocknative_gas_api_request( + url: &Url, + headers: Vec<(&'static str, &'static str)>, + ) -> Result> { + let resp = slurp_url_with_headers(url.as_str(), headers) + .await + .mm_err(|e| e.to_string())?; + if resp.0 != StatusCode::OK { + let error = format!("{} failed with status code {}", url, resp.0); + return MmError::err(error); + } + let block_prices = json::from_slice(&resp.2).map_err(|e| e.to_string())?; + Ok(block_prices) + } + + /// Fetch fee per gas estimations from blocknative provider + pub async fn fetch_blocknative_fee_estimation(base_url: &Url) -> Web3RpcResult { + let (url, headers) = Self::get_blocknative_gas_api_url(base_url); + let block_prices = Self::make_blocknative_gas_api_request(&url, headers) + .await + .mm_err(Web3RpcError::Transport)?; + block_prices.try_into().mm_err(Into::into) + } + } +} diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs new file mode 100644 index 0000000000..231aa68507 --- /dev/null +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -0,0 +1,240 @@ +use async_trait::async_trait; +use common::{executor::{AbortSettings, SpawnAbortable, Timer}, + log, Future01CompatExt}; +use ethereum_types::Address; +use futures::{channel::oneshot::{self, Receiver, Sender}, + stream::FuturesUnordered, + StreamExt}; +use instant::Instant; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; +use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, + ErrorEventName, Event, EventName, EventStreamConfiguration}; +use mm2_number::BigDecimal; +use std::collections::{HashMap, HashSet}; + +use super::EthCoin; +use crate::{eth::{u256_to_big_decimal, Erc20TokenInfo}, + BalanceError, CoinWithDerivationMethod, MmCoin}; + +struct BalanceData { + ticker: String, + address: String, + balance: BigDecimal, +} + +struct BalanceFetchError { + ticker: String, + address: String, + error: MmError, +} + +type BalanceResult = Result; + +/// This implementation differs from others, as they immediately return +/// an error if any of the requests fails. This one completes all futures +/// and returns their results individually. +async fn get_all_balance_results_concurrently(coin: &EthCoin, addresses: HashSet
) -> Vec { + let mut tokens = coin.get_erc_tokens_infos(); + // Workaround for performance purposes. + // + // Unlike tokens, the platform coin length is constant (=1). Instead of creating a generic + // type and mapping the platform coin and the entire token list (which can grow at any time), we map + // the platform coin to Erc20TokenInfo so that we can use the token list right away without + // additional mapping. + tokens.insert(coin.ticker.clone(), Erc20TokenInfo { + // This is a dummy value, since there is no token address for the platform coin. + // In the fetch_balance function, we check if the token_ticker is equal to this + // coin's ticker to avoid using token_address to fetch the balance + // and to use address_balance instead. + token_address: Address::default(), + decimals: coin.decimals, + }); + drop_mutability!(tokens); + + let mut all_jobs = FuturesUnordered::new(); + + for address in addresses { + let jobs = tokens.iter().map(|(token_ticker, info)| { + let coin = coin.clone(); + let token_ticker = token_ticker.clone(); + let info = info.clone(); + async move { fetch_balance(&coin, address, token_ticker, &info).await } + }); + + all_jobs.extend(jobs); + } + + all_jobs.collect().await +} + +async fn fetch_balance( + coin: &EthCoin, + address: Address, + token_ticker: String, + info: &Erc20TokenInfo, +) -> Result { + let (balance_as_u256, decimals) = if token_ticker == coin.ticker { + ( + coin.address_balance(address) + .compat() + .await + .map_err(|error| BalanceFetchError { + ticker: token_ticker.clone(), + address: address.to_string(), + error, + })?, + coin.decimals, + ) + } else { + ( + coin.get_token_balance(info.token_address) + .await + .map_err(|error| BalanceFetchError { + ticker: token_ticker.clone(), + address: address.to_string(), + error, + })?, + info.decimals, + ) + }; + + let balance_as_big_decimal = u256_to_big_decimal(balance_as_u256, decimals).map_err(|e| BalanceFetchError { + ticker: token_ticker.clone(), + address: address.to_string(), + error: e.into(), + })?; + + Ok(BalanceData { + ticker: token_ticker, + address: address.to_string(), + balance: balance_as_big_decimal, + }) +} + +#[async_trait] +impl EventBehaviour for EthCoin { + fn event_name() -> EventName { EventName::CoinBalance } + + fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } + + async fn handle(self, interval: f64, tx: oneshot::Sender) { + const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; + + async fn start_polling(coin: EthCoin, ctx: MmArc, interval: f64) { + async fn sleep_remaining_time(interval: f64, now: Instant) { + // If the interval is x seconds, + // our goal is to broadcast changed balances every x seconds. + // To achieve this, we need to subtract the time complexity of each iteration. + // Given that an iteration already takes 80% of the interval, + // this will lead to inconsistency in the events. + let remaining_time = interval - now.elapsed().as_secs_f64(); + // Not worth to make a call for less than `0.1` durations + if remaining_time >= 0.1 { + Timer::sleep(remaining_time).await; + } + } + + let mut cache: HashMap> = HashMap::new(); + + loop { + let now = Instant::now(); + + let addresses = match coin.all_addresses().await { + Ok(addresses) => addresses, + Err(e) => { + log::error!("Failed getting addresses for {}. Error: {}", coin.ticker, e); + let e = serde_json::to_value(e).expect("Serialization shouldn't fail."); + ctx.stream_channel_controller + .broadcast(Event::new( + format!("{}:{}", EthCoin::error_event_name(), coin.ticker), + e.to_string(), + )) + .await; + sleep_remaining_time(interval, now).await; + continue; + }, + }; + + let mut balance_updates = vec![]; + for result in get_all_balance_results_concurrently(&coin, addresses).await { + match result { + Ok(res) => { + if Some(&res.balance) == cache.get(&res.ticker).and_then(|map| map.get(&res.address)) { + continue; + } + + balance_updates.push(json!({ + "ticker": res.ticker, + "address": res.address, + "balance": { "spendable": res.balance, "unspendable": BigDecimal::default() } + })); + cache + .entry(res.ticker.clone()) + .or_insert_with(HashMap::new) + .insert(res.address, res.balance); + }, + Err(err) => { + log::error!( + "Failed getting balance for '{}:{}' with {interval} interval. Error: {}", + err.ticker, + err.address, + err.error + ); + let e = serde_json::to_value(err.error).expect("Serialization shouldn't fail."); + ctx.stream_channel_controller + .broadcast(Event::new( + format!("{}:{}:{}", EthCoin::error_event_name(), err.ticker, err.address), + e.to_string(), + )) + .await; + }, + }; + } + + if !balance_updates.is_empty() { + ctx.stream_channel_controller + .broadcast(Event::new( + EthCoin::event_name().to_string(), + json!(balance_updates).to_string(), + )) + .await; + } + + sleep_remaining_time(interval, now).await; + } + } + + let ctx = match MmArc::from_weak(&self.ctx) { + Some(ctx) => ctx, + None => { + let msg = "MM context must have been initialized already."; + tx.send(EventInitStatus::Failed(msg.to_owned())) + .expect(RECEIVER_DROPPED_MSG); + panic!("{}", msg); + }, + }; + + tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); + + start_polling(self, ctx, interval).await + } + + async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { + if let Some(event) = config.get_event(&Self::event_name()) { + log::info!("{} event is activated for {}", Self::event_name(), self.ticker,); + + let (tx, rx): (Sender, Receiver) = oneshot::channel(); + let fut = self.clone().handle(event.stream_interval_seconds, tx); + let settings = + AbortSettings::info_on_abort(format!("{} event is stopped for {}.", Self::event_name(), self.ticker)); + self.spawner().spawn_with_settings(fut, settings); + + rx.await.unwrap_or_else(|e| { + EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) + }) + } else { + EventInitStatus::Inactive + } + } +} diff --git a/mm2src/coins/eth/eth_hd_wallet.rs b/mm2src/coins/eth/eth_hd_wallet.rs new file mode 100644 index 0000000000..ceff4ccb79 --- /dev/null +++ b/mm2src/coins/eth/eth_hd_wallet.rs @@ -0,0 +1,180 @@ +use super::*; +use crate::coin_balance::HDAddressBalanceScanner; +use crate::hd_wallet::{ExtractExtendedPubkey, HDAccount, HDAddress, HDExtractPubkeyError, HDWallet, HDXPubExtractor, + TrezorCoinError}; +use async_trait::async_trait; +use bip32::DerivationPath; +use crypto::Secp256k1ExtendedPublicKey; +use ethereum_types::{Address, Public}; + +pub type EthHDAddress = HDAddress; +pub type EthHDAccount = HDAccount; +pub type EthHDWallet = HDWallet; + +#[async_trait] +impl ExtractExtendedPubkey for EthCoin { + type ExtendedPublicKey = Secp256k1ExtendedPublicKey; + + async fn extract_extended_pubkey( + &self, + xpub_extractor: Option, + derivation_path: DerivationPath, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Send, + { + extract_extended_pubkey_impl(self, xpub_extractor, derivation_path).await + } +} + +#[async_trait] +impl HDWalletCoinOps for EthCoin { + type HDWallet = EthHDWallet; + + fn address_formatter(&self) -> fn(&Address) -> String { display_eth_address } + + fn address_from_extended_pubkey( + &self, + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, + ) -> EthHDAddress { + let pubkey = pubkey_from_extended(extended_pubkey); + let address = public_to_address(&pubkey); + EthHDAddress { + address, + pubkey, + derivation_path, + } + } + + fn trezor_coin(&self) -> MmResult { + self.trezor_coin.clone().or_mm_err(|| { + let ticker = self.ticker(); + let error = format!("'{ticker}' coin has 'trezor_coin' field as `None` in the coins config"); + TrezorCoinError::Internal(error) + }) + } +} + +impl HDCoinWithdrawOps for EthCoin {} + +#[async_trait] +#[cfg_attr(test, mockable)] +impl HDAddressBalanceScanner for EthCoin { + type Address = Address; + + async fn is_address_used(&self, address: &Self::Address) -> BalanceResult { + // Count calculates the number of transactions sent from the address whether it's for ERC20 or ETH. + // If the count is greater than 0, then the address is used. + // If the count is 0, then we check for the balance of the address to make sure there was no received transactions. + let count = self.transaction_count(*address, None).await?; + if count > U256::zero() { + return Ok(true); + } + + // We check for platform balance only first to reduce the number of requests to the node. + // If this is a token added using init_token, then we check for this token balance only, and + // we don't check for platform balance or other tokens that was added before. + let platform_balance = self.address_balance(*address).compat().await?; + if !platform_balance.is_zero() { + return Ok(true); + } + + // This is done concurrently which increases the cost of the requests to the node. but it's better than doing it sequentially to reduce the time. + let token_balance_map = self.get_tokens_balance_list_for_address(*address).await?; + Ok(token_balance_map.values().any(|balance| !balance.get_total().is_zero())) + } +} + +#[async_trait] +impl HDWalletBalanceOps for EthCoin { + type HDAddressScanner = Self; + type BalanceObject = CoinBalanceMap; + + async fn produce_hd_address_scanner(&self) -> BalanceResult { Ok(self.clone()) } + + async fn enable_hd_wallet( + &self, + hd_wallet: &Self::HDWallet, + xpub_extractor: Option, + params: EnabledCoinBalanceParams, + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> + where + XPubExtractor: HDXPubExtractor + Send, + { + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params, path_to_address).await + } + + async fn scan_for_new_addresses( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut EthHDAccount, + address_scanner: &Self::HDAddressScanner, + gap_limit: u32, + ) -> BalanceResult>> { + scan_for_new_addresses_impl( + self, + hd_wallet, + hd_account, + address_scanner, + Bip44Chain::External, + gap_limit, + ) + .await + } + + async fn all_known_addresses_balances( + &self, + hd_account: &EthHDAccount, + ) -> BalanceResult>> { + let external_addresses = hd_account + .known_addresses_number(Bip44Chain::External) + // A UTXO coin should support both [`Bip44Chain::External`] and [`Bip44Chain::Internal`]. + .mm_err(|e| BalanceError::Internal(e.to_string()))?; + + self.known_addresses_balances_with_ids(hd_account, Bip44Chain::External, 0..external_addresses) + .await + } + + async fn known_address_balance(&self, address: &Address) -> BalanceResult { + let balance = self + .address_balance(*address) + .and_then(move |result| Ok(u256_to_big_decimal(result, self.decimals())?)) + .compat() + .await?; + + let coin_balance = CoinBalance { + spendable: balance, + unspendable: BigDecimal::from(0), + }; + + let mut balances = CoinBalanceMap::new(); + balances.insert(self.ticker().to_string(), coin_balance); + let token_balances = self.get_tokens_balance_list_for_address(*address).await?; + balances.extend(token_balances); + Ok(balances) + } + + async fn known_addresses_balances( + &self, + addresses: Vec
, + ) -> BalanceResult> { + let mut balance_futs = Vec::new(); + for address in addresses { + let fut = async move { + let balance = self.known_address_balance(&address).await?; + Ok((address, balance)) + }; + balance_futs.push(fut); + } + try_join_all(balance_futs).await + } + + async fn prepare_addresses_for_balance_stream_if_enabled( + &self, + _addresses: HashSet, + ) -> MmResult<(), String> { + Ok(()) + } +} diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs new file mode 100644 index 0000000000..922e219fbd --- /dev/null +++ b/mm2src/coins/eth/eth_rpc.rs @@ -0,0 +1,455 @@ +//! This module serves as an abstraction layer for Ethereum RPCs. +//! Unlike the built-in functions in web3, this module dynamically +//! rotates through all transports in case of failures. + +use super::web3_transport::FeeHistoryResult; +use super::{web3_transport::Web3Transport, EthCoin}; +use common::{custom_futures::timeout::FutureTimerExt, log::debug}; +use instant::Duration; +use serde_json::Value; +use web3::types::{Address, Block, BlockId, BlockNumber, Bytes, CallRequest, FeeHistory, Filter, Log, Proof, SyncState, + Trace, TraceFilter, Transaction, TransactionId, TransactionReceipt, TransactionRequest, Work, H256, + H520, H64, U256, U64}; +use web3::{helpers, Transport}; + +pub(crate) const ETH_RPC_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); + +impl EthCoin { + async fn try_rpc_send(&self, method: &str, params: Vec) -> Result { + let mut clients = self.web3_instances.lock().await; + + let mut error = web3::Error::Unreachable; + for (i, client) in clients.clone().into_iter().enumerate() { + let execute_fut = match client.web3.transport() { + Web3Transport::Http(http) => http.execute(method, params.clone()), + Web3Transport::Websocket(socket) => { + socket.maybe_spawn_connection_loop(self.clone()); + socket.execute(method, params.clone()) + }, + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.execute(method, params.clone()), + }; + + match execute_fut.timeout(ETH_RPC_REQUEST_TIMEOUT).await { + Ok(Ok(r)) => { + // Bring the live client to the front of rpc_clients + clients.rotate_left(i); + return Ok(r); + }, + Ok(Err(err)) => { + debug!("Request on '{method}' failed. Error: {err}"); + error = err; + + if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { + socket_transport.stop_connection_loop().await; + }; + }, + Err(timeout_error) => { + debug!("Timeout exceed for '{method}' request. Error: {timeout_error}",); + + if let Web3Transport::Websocket(socket_transport) = client.web3.transport() { + socket_transport.stop_connection_loop().await; + }; + }, + }; + } + + Err(error) + } +} + +#[allow(dead_code)] +impl EthCoin { + /// Get list of available accounts. + pub(crate) async fn accounts(&self) -> Result, web3::Error> { + self.try_rpc_send("eth_accounts", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get current block number + pub(crate) async fn block_number(&self) -> Result { + self.try_rpc_send("eth_blockNumber", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Call a constant method of contract without changing the state of the blockchain. + pub(crate) async fn call(&self, req: CallRequest, block: Option) -> Result { + let req = helpers::serialize(&req); + let block = helpers::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into())); + + self.try_rpc_send("eth_call", vec![req, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get coinbase address + pub(crate) async fn coinbase(&self) -> Result { + self.try_rpc_send("eth_coinbase", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Compile LLL + pub(crate) async fn compile_lll(&self, code: String) -> Result { + let code = helpers::serialize(&code); + self.try_rpc_send("eth_compileLLL", vec![code]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Compile Solidity + pub(crate) async fn compile_solidity(&self, code: String) -> Result { + let code = helpers::serialize(&code); + self.try_rpc_send("eth_compileSolidity", vec![code]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Compile Serpent + pub(crate) async fn compile_serpent(&self, code: String) -> Result { + let code = helpers::serialize(&code); + self.try_rpc_send("eth_compileSerpent", vec![code]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Call a contract without changing the state of the blockchain to estimate gas usage. + pub(crate) async fn estimate_gas(&self, req: CallRequest, block: Option) -> Result { + let req = helpers::serialize(&req); + + let args = match block { + Some(block) => vec![req, helpers::serialize(&block)], + None => vec![req], + }; + + self.try_rpc_send("eth_estimateGas", args) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get current recommended gas price + pub(crate) async fn gas_price(&self) -> Result { + self.try_rpc_send("eth_gasPrice", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Returns a collection of historical gas information. This can be used for evaluating the max_fee_per_gas + /// and max_priority_fee_per_gas to send the future transactions. + pub(crate) async fn fee_history( + &self, + block_count: U256, + newest_block: BlockNumber, + reward_percentiles: Option>, + ) -> Result { + let block_count = helpers::serialize(&block_count); + let newest_block = helpers::serialize(&newest_block); + let reward_percentiles = helpers::serialize(&reward_percentiles); + + self.try_rpc_send("eth_feeHistory", vec![block_count, newest_block, reward_percentiles]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get balance of given address + pub(crate) async fn balance(&self, address: Address, block: Option) -> Result { + let address = helpers::serialize(&address); + let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + + self.try_rpc_send("eth_getBalance", vec![address, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get all logs matching a given filter object + pub(crate) async fn logs(&self, filter: Filter) -> Result, web3::Error> { + let filter = helpers::serialize(&filter); + self.try_rpc_send("eth_getLogs", vec![filter]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get block details with transaction hashes. + pub(crate) async fn block(&self, block: BlockId) -> Result>, web3::Error> { + let include_txs = helpers::serialize(&false); + + let result = match block { + BlockId::Hash(hash) => { + let hash = helpers::serialize(&hash); + self.try_rpc_send("eth_getBlockByHash", vec![hash, include_txs]) + }, + BlockId::Number(num) => { + let num = helpers::serialize(&num); + self.try_rpc_send("eth_getBlockByNumber", vec![num, include_txs]) + }, + }; + + result.await.and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get block details with full transaction objects. + pub(crate) async fn block_with_txs(&self, block: BlockId) -> Result>, web3::Error> { + let include_txs = helpers::serialize(&true); + + let result = match block { + BlockId::Hash(hash) => { + let hash = helpers::serialize(&hash); + self.try_rpc_send("eth_getBlockByHash", vec![hash, include_txs]) + }, + BlockId::Number(num) => { + let num = helpers::serialize(&num); + self.try_rpc_send("eth_getBlockByNumber", vec![num, include_txs]) + }, + }; + + result.await.and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get number of transactions in block + pub(crate) async fn block_transaction_count(&self, block: BlockId) -> Result, web3::Error> { + let result = match block { + BlockId::Hash(hash) => { + let hash = helpers::serialize(&hash); + self.try_rpc_send("eth_getBlockTransactionCountByHash", vec![hash]) + }, + BlockId::Number(num) => { + let num = helpers::serialize(&num); + + self.try_rpc_send("eth_getBlockTransactionCountByNumber", vec![num]) + }, + }; + + result.await.and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get code under given address + pub(crate) async fn code(&self, address: Address, block: Option) -> Result { + let address = helpers::serialize(&address); + let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + + self.try_rpc_send("eth_getCode", vec![address, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get supported compilers + pub(crate) async fn compilers(&self) -> Result, web3::Error> { + self.try_rpc_send("eth_getCompilers", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get chain id + pub(crate) async fn chain_id(&self) -> Result { + self.try_rpc_send("eth_chainId", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get available user accounts. This method is only available in the browser. With MetaMask, + /// this will cause the popup that prompts the user to allow or deny access to their accounts + /// to your app. + pub(crate) async fn request_accounts(&self) -> Result, web3::Error> { + self.try_rpc_send("eth_requestAccounts", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get storage entry + pub(crate) async fn storage( + &self, + address: Address, + idx: U256, + block: Option, + ) -> Result { + let address = helpers::serialize(&address); + let idx = helpers::serialize(&idx); + let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + + self.try_rpc_send("eth_getStorageAt", vec![address, idx, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get nonce + pub(crate) async fn transaction_count( + &self, + address: Address, + block: Option, + ) -> Result { + let address = helpers::serialize(&address); + let block = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + + self.try_rpc_send("eth_getTransactionCount", vec![address, block]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get transaction + pub(crate) async fn transaction(&self, id: TransactionId) -> Result, web3::Error> { + let result = match id { + TransactionId::Hash(hash) => { + let hash = helpers::serialize(&hash); + self.try_rpc_send("eth_getTransactionByHash", vec![hash]) + }, + TransactionId::Block(BlockId::Hash(hash), index) => { + let hash = helpers::serialize(&hash); + let idx = helpers::serialize(&index); + self.try_rpc_send("eth_getTransactionByBlockHashAndIndex", vec![hash, idx]) + }, + TransactionId::Block(BlockId::Number(number), index) => { + let number = helpers::serialize(&number); + let idx = helpers::serialize(&index); + self.try_rpc_send("eth_getTransactionByBlockNumberAndIndex", vec![number, idx]) + }, + }; + + result.await.and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get transaction receipt + pub(crate) async fn transaction_receipt(&self, hash: H256) -> Result, web3::Error> { + let hash = helpers::serialize(&hash); + + self.try_rpc_send("eth_getTransactionReceipt", vec![hash]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get work package + pub(crate) async fn work(&self) -> Result { + self.try_rpc_send("eth_getWork", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get hash rate + pub(crate) async fn hashrate(&self) -> Result { + self.try_rpc_send("eth_hashrate", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get mining status + pub(crate) async fn mining(&self) -> Result { + self.try_rpc_send("eth_mining", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Start new block filter + pub(crate) async fn new_block_filter(&self) -> Result { + self.try_rpc_send("eth_newBlockFilter", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Start new pending transaction filter + pub(crate) async fn new_pending_transaction_filter(&self) -> Result { + self.try_rpc_send("eth_newPendingTransactionFilter", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Start new pending transaction filter + pub(crate) async fn protocol_version(&self) -> Result { + self.try_rpc_send("eth_protocolVersion", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Sends a rlp-encoded signed transaction + pub(crate) async fn send_raw_transaction(&self, rlp: Bytes) -> Result { + let rlp = helpers::serialize(&rlp); + self.try_rpc_send("eth_sendRawTransaction", vec![rlp]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Sends a transaction transaction + pub(crate) async fn send_transaction(&self, tx: TransactionRequest) -> Result { + let tx = helpers::serialize(&tx); + self.try_rpc_send("eth_sendTransaction", vec![tx]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Signs a hash of given data + pub(crate) async fn sign(&self, address: Address, data: Bytes) -> Result { + let address = helpers::serialize(&address); + let data = helpers::serialize(&data); + self.try_rpc_send("eth_sign", vec![address, data]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Submit hashrate of external miner + pub(crate) async fn submit_hashrate(&self, rate: U256, id: H256) -> Result { + let rate = helpers::serialize(&rate); + let id = helpers::serialize(&id); + self.try_rpc_send("eth_submitHashrate", vec![rate, id]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Submit work of external miner + pub(crate) async fn submit_work(&self, nonce: H64, pow_hash: H256, mix_hash: H256) -> Result { + let nonce = helpers::serialize(&nonce); + let pow_hash = helpers::serialize(&pow_hash); + let mix_hash = helpers::serialize(&mix_hash); + self.try_rpc_send("eth_submitWork", vec![nonce, pow_hash, mix_hash]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Get syncing status + pub(crate) async fn syncing(&self) -> Result { + self.try_rpc_send("eth_syncing", vec![]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Returns the account- and storage-values of the specified account including the Merkle-proof. + pub(crate) async fn proof( + &self, + address: Address, + keys: Vec, + block: Option, + ) -> Result, web3::Error> { + let add = helpers::serialize(&address); + let ks = helpers::serialize(&keys); + let blk = helpers::serialize(&block.unwrap_or(BlockNumber::Latest)); + self.try_rpc_send("eth_getProof", vec![add, ks, blk]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + pub(crate) async fn eth_fee_history( + &self, + count: U256, + block: BlockNumber, + reward_percentiles: &[f64], + ) -> Result { + let count = helpers::serialize(&count); + let block = helpers::serialize(&block); + let reward_percentiles = helpers::serialize(&reward_percentiles); + let params = vec![count, block, reward_percentiles]; + + self.try_rpc_send("eth_feeHistory", params) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } + + /// Return traces matching the given filter + /// + /// See [TraceFilterBuilder](../types/struct.TraceFilterBuilder.html) + pub(crate) async fn trace_filter(&self, filter: TraceFilter) -> Result, web3::Error> { + let filter = helpers::serialize(&filter); + + self.try_rpc_send("trace_filter", vec![filter]) + .await + .and_then(|t| serde_json::from_value(t).map_err(Into::into)) + } +} diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 152f68436f..9332594931 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,13 +1,12 @@ use super::*; +use crate::eth::for_tests::{eth_coin_for_test, eth_coin_from_keypair}; use crate::{DexFee, IguanaPrivKey}; -use common::{block_on, now_sec, wait_until_sec}; -use crypto::privkey::key_pair_from_seed; +use common::{block_on, now_sec}; +#[cfg(not(target_arch = "wasm32"))] use ethkey::{Generator, Random}; -use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; -use mm2_test_helpers::{for_tests::{eth_jst_testnet_conf, eth_testnet_conf, ETH_DEV_NODE, ETH_DEV_NODES, - ETH_DEV_SWAP_CONTRACT, ETH_DEV_TOKEN_CONTRACT, ETH_MAINNET_NODE, - ETH_MAINNET_SWAP_CONTRACT}, - get_passphrase}; +use mm2_core::mm_ctx::MmCtxBuilder; +use mm2_test_helpers::for_tests::{ETH_MAINNET_CHAIN_ID, ETH_MAINNET_NODE, ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, + ETH_SEPOLIA_TOKEN_CONTRACT}; use mocktopus::mocking::*; /// The gas price for the tests @@ -18,164 +17,16 @@ const GAS_PRICE_APPROXIMATION_ON_START_SWAP: u64 = 51_500_000_000; const GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE: u64 = 52_500_000_000; // `GAS_PRICE` increased by 7% const GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE: u64 = 53_500_000_000; +// old way to add some extra gas to the returned value from gas station (non-existent now), still used in tests +const GAS_PRICE_PERCENT: u64 = 10; const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; -lazy_static! { - static ref ETH_DISTRIBUTOR: EthCoin = eth_distributor(); - static ref JST_DISTRIBUTOR: EthCoin = jst_distributor(); - static ref MM_CTX: MmArc = MmCtxBuilder::new().into_mm_arc(); -} - fn check_sum(addr: &str, expected: &str) { let actual = checksum_address(addr); assert_eq!(expected, actual); } -pub fn eth_distributor() -> EthCoin { - let req = json!({ - "method": "enable", - "coin": "ETH", - "urls": ETH_DEV_NODES, - "swap_contract_address": ETH_DEV_SWAP_CONTRACT, - }); - let seed = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); - let keypair = key_pair_from_seed(&seed).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(keypair.private().secret); - block_on(eth_coin_from_conf_and_request( - &MM_CTX, - "ETH", - ð_testnet_conf(), - &req, - CoinProtocol::ETH, - priv_key_policy, - )) - .unwrap() -} - -pub fn jst_distributor() -> EthCoin { - let req = json!({ - "method": "enable", - "coin": "ETH", - "urls": ETH_DEV_NODES, - "swap_contract_address": ETH_DEV_SWAP_CONTRACT, - }); - let seed = get_passphrase!(".env.seed", "BOB_PASSPHRASE").unwrap(); - let keypair = key_pair_from_seed(&seed).unwrap(); - let priv_key_policy = PrivKeyBuildPolicy::IguanaPrivKey(keypair.private().secret); - block_on(eth_coin_from_conf_and_request( - &MM_CTX, - "ETH", - ð_testnet_conf(), - &req, - CoinProtocol::ERC20 { - platform: "ETH".to_string(), - contract_address: ETH_DEV_TOKEN_CONTRACT.to_string(), - }, - priv_key_policy, - )) - .unwrap() -} - -fn eth_coin_for_test( - coin_type: EthCoinType, - urls: &[&str], - fallback_swap_contract: Option
, -) -> (MmArc, EthCoin) { - let key_pair = KeyPair::from_secret_slice( - &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), - ) - .unwrap(); - eth_coin_from_keypair(coin_type, urls, fallback_swap_contract, key_pair) -} - -fn random_eth_coin_for_test( - coin_type: EthCoinType, - urls: &[&str], - fallback_swap_contract: Option
, -) -> (MmArc, EthCoin) { - let key_pair = Random.generate().unwrap(); - fill_eth(key_pair.address(), 0.001); - eth_coin_from_keypair(coin_type, urls, fallback_swap_contract, key_pair) -} - -fn eth_coin_from_keypair( - coin_type: EthCoinType, - urls: &[&str], - fallback_swap_contract: Option
, - key_pair: KeyPair, -) -> (MmArc, EthCoin) { - let mut nodes = vec![]; - for url in urls.iter() { - nodes.push(HttpTransportNode { - uri: url.parse().unwrap(), - gui_auth: false, - }); - } - drop_mutability!(nodes); - - let transport = Web3Transport::with_nodes(nodes); - let web3 = Web3::new(transport); - let conf = json!({ - "coins":[ - eth_testnet_conf(), - eth_jst_testnet_conf() - ] - }); - let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); - let ticker = match coin_type { - EthCoinType::Eth => "ETH".to_string(), - EthCoinType::Erc20 { .. } => "JST".to_string(), - }; - - let eth_coin = EthCoin(Arc::new(EthCoinImpl { - coin_type, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - fallback_swap_contract, - contract_supports_watchers: false, - ticker, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); - (ctx, eth_coin) -} - -pub fn fill_eth(to_addr: Address, amount: f64) { - let wei_per_eth: u64 = 1_000_000_000_000_000_000; - let amount_in_wei = (amount * wei_per_eth as f64) as u64; - ETH_DISTRIBUTOR - .send_to_address(to_addr, amount_in_wei.into()) - .wait() - .unwrap(); -} - -pub fn fill_jst(to_addr: Address, amount: f64) { - let wei_per_jst: u64 = 1_000_000_000_000_000_000; - let amount_in_wei = (amount * wei_per_jst as f64) as u64; - JST_DISTRIBUTOR - .send_to_address(to_addr, amount_in_wei.into()) - .wait() - .unwrap(); -} - #[test] /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md#test-cases fn test_check_sum_address() { @@ -303,293 +154,20 @@ fn test_wei_from_big_decimal() { assert_eq!(expected_wei, wei); } -#[test] -fn send_and_refund_erc20_payment() { - let key_pair = Random.generate().unwrap(); - fill_eth(key_pair.address(), 0.001); - fill_jst(key_pair.address(), 0.0001); - - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); - let web3 = Web3::new(transport); - let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = EthCoin(Arc::new(EthCoinImpl { - ticker: "ETH".into(), - coin_type: EthCoinType::Erc20 { - platform: "ETH".to_string(), - token_addr: Address::from_str(ETH_DEV_TOKEN_CONTRACT).unwrap(), - }, - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - fallback_swap_contract: None, - contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotStarted), - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); - - let time_lock = now_sec() - 200; - let secret_hash = &[1; 20]; - let maker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock, - other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, - secret_hash, - amount: "0.0001".parse().unwrap(), - swap_contract_address: &coin.swap_contract_address(), - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: wait_until_sec(15), - }; - let payment = coin.send_maker_payment(maker_payment_args).wait().unwrap(); - log!("{:?}", payment); - - let swap_id = coin.etomic_swap_id(time_lock.try_into().unwrap(), secret_hash); - let status = block_on( - coin.payment_status( - Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - Token::FixedBytes(swap_id.clone()), - ) - .compat(), - ) - .unwrap(); - assert_eq!(status, U256::from(PaymentState::Sent as u8)); - - let maker_refunds_payment_args = RefundPaymentArgs { - payment_tx: &payment.tx_hex(), - time_lock, - other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, - secret_hash, - swap_contract_address: &coin.swap_contract_address(), - swap_unique_data: &[], - watcher_reward: false, - }; - let refund = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); - log!("{:?}", refund); - - let status = block_on( - coin.payment_status( - Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - Token::FixedBytes(swap_id), - ) - .compat(), - ) - .unwrap(); - assert_eq!(status, U256::from(PaymentState::Refunded as u8)); -} - -#[test] -fn send_and_refund_eth_payment() { - let key_pair = Random.generate().unwrap(); - fill_eth(key_pair.address(), 0.001); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); - let web3 = Web3::new(transport); - let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = EthCoin(Arc::new(EthCoinImpl { - ticker: "ETH".into(), - coin_type: EthCoinType::Eth, - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - fallback_swap_contract: None, - contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotStarted), - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); - - let time_lock = now_sec() - 200; - let secret_hash = &[1; 20]; - let send_maker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock, - other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, - secret_hash, - amount: "0.0001".parse().unwrap(), - swap_contract_address: &coin.swap_contract_address(), - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let payment = coin.send_maker_payment(send_maker_payment_args).wait().unwrap(); - - log!("{:?}", payment); - - let swap_id = coin.etomic_swap_id(time_lock.try_into().unwrap(), secret_hash); - let status = block_on( - coin.payment_status( - Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - Token::FixedBytes(swap_id.clone()), - ) - .compat(), - ) - .unwrap(); - assert_eq!(status, U256::from(PaymentState::Sent as u8)); - - let maker_refunds_payment_args = RefundPaymentArgs { - payment_tx: &payment.tx_hex(), - time_lock, - other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, - secret_hash, - swap_contract_address: &coin.swap_contract_address(), - swap_unique_data: &[], - watcher_reward: false, - }; - let refund = block_on(coin.send_maker_refunds_payment(maker_refunds_payment_args)).unwrap(); - - log!("{:?}", refund); - - let status = block_on( - coin.payment_status( - Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - Token::FixedBytes(swap_id), - ) - .compat(), - ) - .unwrap(); - assert_eq!(status, U256::from(PaymentState::Refunded as u8)); -} - -#[test] -fn test_nonce_several_urls() { - let key_pair = KeyPair::from_secret_slice( - &hex::decode("0dbc09312ec67cf775c00e72dd88c9a7c4b7452d4ee84ee7ca0bb55c4be35446").unwrap(), - ) - .unwrap(); - - let devnet_transport = Web3Transport::single_node(ETH_DEV_NODE, false); - let sepolia_transport = Web3Transport::single_node("https://rpc2.sepolia.org", false); - // get nonce must succeed if some nodes are down at the moment for some reason - let failing_transport = Web3Transport::single_node("http://195.201.0.6:8989", false); - - let web3_devnet = Web3::new(devnet_transport); - let web3_sepolia = Web3::new(sepolia_transport); - let web3_failing = Web3::new(failing_transport); - - let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = EthCoin(Arc::new(EthCoinImpl { - ticker: "ETH".into(), - coin_type: EthCoinType::Eth, - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - fallback_swap_contract: None, - contract_supports_watchers: false, - web3_instances: vec![ - Web3Instance { - web3: web3_devnet.clone(), - is_parity: false, - }, - Web3Instance { - web3: web3_sepolia, - is_parity: false, - }, - Web3Instance { - web3: web3_failing, - is_parity: false, - }, - ], - web3: web3_devnet, - decimals: 18, - gas_station_url: Some("https://ethgasstation.info/json/ethgasAPI.json".into()), - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotStarted), - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); - - log!("My address {:?}", coin.my_address); - log!("before payment"); - let payment = coin.send_to_address(coin.my_address, 200000000.into()).wait().unwrap(); - - log!("{:?}", payment); - let new_nonce = get_addr_nonce(coin.my_address, coin.web3_instances.clone()) - .wait() - .unwrap(); - log!("{:?}", new_nonce); -} - #[test] fn test_wait_for_payment_spend_timeout() { EthCoin::spend_events.mock_safe(|_, _, _, _| MockResult::Return(Box::new(futures01::future::ok(vec![])))); EthCoin::current_block.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(900)))); - let key_pair = KeyPair::from_secret_slice( - &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), - ) - .unwrap(); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); - let web3 = Web3::new(transport); - let ctx = MmCtxBuilder::new().into_mm_arc(); - - let coin = EthCoinImpl { - coin_type: EthCoinType::Eth, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - fallback_swap_contract: None, - contract_supports_watchers: false, - ticker: "ETH".into(), - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - }; + let key_pair = Random.generate().unwrap(); + let (_ctx, coin) = eth_coin_from_keypair( + EthCoinType::Eth, + ETH_SEPOLIA_NODES, + None, + key_pair, + ETH_SEPOLIA_CHAIN_ID, + ); - let coin = EthCoin(Arc::new(coin)); let wait_until = now_sec() - 1; let from_block = 1; // raw transaction bytes of https://etherscan.io/tx/0x0869be3e5d4456a29d488a533ad6c118620fef450f36778aecf31d356ff8b41f @@ -620,204 +198,16 @@ fn test_wait_for_payment_spend_timeout() { .is_err()); } -#[test] -fn test_search_for_swap_tx_spend_was_spent() { - let key_pair = KeyPair::from_secret_slice( - &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), - ) - .unwrap(); - let transport = Web3Transport::single_node(ETH_MAINNET_NODE, false); - let web3 = Web3::new(transport); - let ctx = MmCtxBuilder::new().into_mm_arc(); - - let swap_contract_address = Address::from_str(ETH_MAINNET_SWAP_CONTRACT).unwrap(); - let coin = EthCoin(Arc::new(EthCoinImpl { - coin_type: EthCoinType::Eth, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address, - fallback_swap_contract: None, - contract_supports_watchers: false, - ticker: "ETH".into(), - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); - - // raw transaction bytes of https://etherscan.io/tx/0x2814718945e90fe4301e2a74eaaa46b4fdbdba1536e1d94e3b0bd665b2dd091d - let payment_tx = [ - 248, 241, 1, 133, 8, 158, 68, 19, 192, 131, 2, 73, 240, 148, 36, 171, 228, 199, 31, 198, 88, 201, 19, 19, 182, - 85, 44, 212, 12, 216, 8, 179, 234, 128, 135, 29, 133, 195, 185, 99, 4, 0, 184, 132, 21, 44, 243, 175, 130, 126, - 209, 71, 198, 107, 13, 87, 207, 36, 150, 22, 77, 57, 198, 35, 248, 38, 203, 5, 242, 55, 219, 79, 252, 124, 162, - 67, 251, 160, 210, 247, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 229, 230, 210, 113, 0, 71, 77, 52, 204, 15, 135, - 238, 56, 119, 86, 57, 80, 25, 1, 156, 70, 83, 37, 132, 127, 196, 109, 164, 129, 132, 149, 187, 70, 120, 38, 83, - 173, 7, 235, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 99, 54, 210, 77, 38, 160, 254, 78, 202, 143, 121, 136, 202, 110, 251, 121, 110, 25, - 124, 62, 205, 40, 168, 154, 212, 180, 118, 59, 28, 135, 255, 44, 20, 62, 49, 109, 170, 215, 160, 72, 251, 237, - 69, 215, 60, 8, 59, 204, 150, 18, 163, 242, 159, 79, 115, 146, 19, 78, 61, 142, 91, 221, 195, 178, 80, 197, - 162, 242, 179, 182, 235, - ]; - - // raw transaction bytes of https://etherscan.io/tx/0xe9c2c8126e8b947eb3bbc6008ef9e3880e7c54f5bc5ccdc34ad412c4d271c76b - let spend_tx = [ - 249, 1, 10, 4, 133, 8, 154, 252, 216, 0, 131, 2, 73, 240, 148, 36, 171, 228, 199, 31, 198, 88, 201, 19, 19, - 182, 85, 44, 212, 12, 216, 8, 179, 234, 128, 128, 184, 164, 2, 237, 41, 43, 130, 126, 209, 71, 198, 107, 13, - 87, 207, 36, 150, 22, 77, 57, 198, 35, 248, 38, 203, 5, 242, 55, 219, 79, 252, 124, 162, 67, 251, 160, 210, - 247, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 133, 195, 185, 99, 4, 0, - 50, 250, 104, 200, 70, 202, 119, 58, 239, 14, 250, 118, 21, 252, 240, 40, 50, 95, 151, 187, 141, 226, 240, 198, - 32, 99, 37, 100, 241, 251, 122, 89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 82, 6, 91, 85, 191, 21, 5, 181, 176, 40, 104, 25, - 86, 135, 213, 121, 230, 186, 218, 38, 160, 19, 239, 26, 4, 109, 84, 68, 160, 43, 178, 4, 249, 52, 209, 146, 13, - 53, 179, 63, 117, 17, 184, 115, 83, 75, 59, 89, 18, 198, 47, 37, 101, 160, 85, 163, 23, 247, 219, 101, 69, 138, - 8, 152, 81, 205, 76, 253, 225, 123, 167, 12, 147, 151, 215, 248, 198, 91, 254, 47, 99, 203, 102, 5, 212, 217, - ]; - let spend_tx = FoundSwapTxSpend::Spent(signed_eth_tx_from_bytes(&spend_tx).unwrap().into()); - - let found_tx = - block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, &[0; 20], 15643279, false)) - .unwrap() - .unwrap(); - assert_eq!(spend_tx, found_tx); -} - -#[test] -fn test_gas_station() { - make_gas_station_request.mock_safe(|_| { - let data = GasStationData { - average: 500.into(), - fast: 1000.into(), - }; - MockResult::Return(Box::pin(async move { Ok(data) })) - }); - let res_eth = GasStationData::get_gas_price( - "https://ethgasstation.info/api/ethgasAPI.json", - 8, - GasStationPricePolicy::MeanAverageFast, - ) - .wait() - .unwrap(); - let one_gwei = U256::from(10u64.pow(9)); - - let expected_eth_wei = U256::from(75) * one_gwei; - assert_eq!(expected_eth_wei, res_eth); - - let res_polygon = GasStationData::get_gas_price( - "https://gasstation-mainnet.matic.network/", - 9, - GasStationPricePolicy::Average, - ) - .wait() - .unwrap(); - - let expected_eth_polygon = U256::from(500) * one_gwei; - assert_eq!(expected_eth_polygon, res_polygon); -} - -#[test] -fn test_search_for_swap_tx_spend_was_refunded() { - let key_pair = KeyPair::from_secret_slice( - &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), - ) - .unwrap(); - let transport = Web3Transport::single_node(ETH_MAINNET_NODE, false); - let web3 = Web3::new(transport); - let ctx = MmCtxBuilder::new().into_mm_arc(); - - let swap_contract_address = Address::from_str(ETH_MAINNET_SWAP_CONTRACT).unwrap(); - let coin = EthCoin(Arc::new(EthCoinImpl { - coin_type: EthCoinType::Erc20 { - platform: "ETH".to_string(), - token_addr: Address::from_str("0x0D8775F648430679A709E98d2b0Cb6250d2887EF").unwrap(), - }, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address, - fallback_swap_contract: None, - contract_supports_watchers: false, - ticker: "BAT".into(), - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); - - // raw transaction bytes of https://etherscan.io/tx/0x02c261dcb1c8615c029b9abc712712b80ef8c1ef20d2cbcdd9bde859e7913476 - let payment_tx = [ - 249, 1, 42, 25, 133, 26, 13, 225, 144, 65, 131, 2, 73, 240, 148, 36, 171, 228, 199, 31, 198, 88, 201, 19, 19, - 182, 85, 44, 212, 12, 216, 8, 179, 234, 128, 128, 184, 196, 155, 65, 91, 42, 22, 125, 52, 19, 176, 17, 106, - 187, 142, 153, 244, 194, 212, 205, 57, 166, 77, 249, 188, 153, 80, 0, 108, 74, 232, 132, 82, 114, 88, 36, 125, - 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 240, 91, 89, 211, 178, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 135, 117, 246, 72, 67, 6, 121, 167, 9, 233, 141, 43, 12, 182, 37, 13, 40, - 135, 239, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 18, 103, 159, 197, 230, 51, 138, 82, 9, 138, 176, 149, 190, - 225, 233, 161, 91, 198, 48, 186, 149, 40, 18, 123, 207, 245, 36, 103, 114, 54, 243, 115, 156, 239, 1, 51, 17, - 244, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 97, 150, 38, 250, 37, 160, 177, 67, 137, 53, 80, 200, 208, 22, 66, 120, 249, 77, 95, 165, 27, - 167, 30, 61, 254, 250, 17, 46, 111, 83, 165, 117, 188, 180, 148, 99, 58, 7, 160, 12, 198, 11, 101, 228, 74, - 229, 5, 50, 87, 185, 28, 16, 35, 182, 55, 163, 141, 135, 255, 195, 44, 130, 37, 145, 39, 90, 98, 131, 205, 110, - 197, - ]; - - // raw transaction bytes of https://etherscan.io/tx/0x3ce6a40d7ad41bd24055cf4cdd564d42d2f36095ec8b6180717b4f0a922a97f4 - let refund_tx = [ - 249, 1, 10, 26, 133, 25, 252, 245, 23, 130, 131, 2, 73, 240, 148, 36, 171, 228, 199, 31, 198, 88, 201, 19, 19, - 182, 85, 44, 212, 12, 216, 8, 179, 234, 128, 128, 184, 164, 70, 252, 2, 148, 22, 125, 52, 19, 176, 17, 106, - 187, 142, 153, 244, 194, 212, 205, 57, 166, 77, 249, 188, 153, 80, 0, 108, 74, 232, 132, 82, 114, 88, 36, 125, - 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 240, 91, 89, 211, 178, 0, 0, - 186, 149, 40, 18, 123, 207, 245, 36, 103, 114, 54, 243, 115, 156, 239, 1, 51, 17, 244, 32, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 135, 117, 246, 72, 67, 6, 121, 167, 9, 233, 141, 43, 12, - 182, 37, 13, 40, 135, 239, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 18, 103, 159, 197, 230, 51, 138, 82, 9, 138, - 176, 149, 190, 225, 233, 161, 91, 198, 48, 37, 160, 175, 56, 178, 83, 9, 93, 241, 61, 203, 189, 163, 249, 203, - 143, 126, 176, 116, 113, 203, 21, 88, 19, 135, 218, 207, 185, 178, 234, 185, 244, 250, 183, 160, 17, 135, 205, - 189, 131, 59, 111, 198, 16, 171, 98, 33, 59, 51, 31, 161, 162, 89, 71, 50, 160, 165, 114, 149, 47, 219, 82, 29, - 183, 80, 80, 157, - ]; - let refund_tx = FoundSwapTxSpend::Refunded(signed_eth_tx_from_bytes(&refund_tx).unwrap().into()); - - let found_tx = - block_on(coin.search_for_swap_tx_spend(&payment_tx, swap_contract_address, &[0; 20], 13638713, false)) - .unwrap() - .unwrap(); - assert_eq!(refund_tx, found_tx); -} - +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_manual_fee() { - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); - EthCoin::my_balance.mock_safe(|_| { + EthCoin::address_balance.mock_safe(|_, _| { let balance = wei_from_big_decimal(&1000000000.into(), 18).unwrap(); MockResult::Return(Box::new(futures01::future::ok(balance))) }); - get_addr_nonce.mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok((0.into(), vec![]))))); + EthCoin::get_addr_nonce.mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok((0.into(), vec![]))))); let withdraw_req = WithdrawRequest { amount: 1.into(), @@ -826,42 +216,47 @@ fn test_withdraw_impl_manual_fee() { coin: "ETH".to_string(), max: false, fee: Some(WithdrawFee::EthGas { - gas: ETH_GAS, + gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), }), memo: None, + ibc_source_channel: None, }; - coin.my_balance().wait().unwrap(); + coin.get_balance().wait().unwrap(); let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( EthTxFeeDetails { coin: "ETH".into(), gas_price: "0.000000001".parse().unwrap(), - gas: ETH_GAS, + gas: gas_limit::ETH_MAX_TRADE_GAS, total_fee: "0.00015".parse().unwrap(), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, } .into(), ); assert_eq!(expected, tx_details.fee_details); } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_fee_details() { let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { platform: "ETH".to_string(), - token_addr: Address::from_str(ETH_DEV_TOKEN_CONTRACT).unwrap(), + token_addr: Address::from_str(ETH_SEPOLIA_TOKEN_CONTRACT).unwrap(), }, &["http://dummy.dummy"], None, + ETH_SEPOLIA_CHAIN_ID, ); - EthCoin::my_balance.mock_safe(|_| { + EthCoin::address_balance.mock_safe(|_, _| { let balance = wei_from_big_decimal(&1000000000.into(), 18).unwrap(); MockResult::Return(Box::new(futures01::future::ok(balance))) }); - get_addr_nonce.mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok((0.into(), vec![]))))); + EthCoin::get_addr_nonce.mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok((0.into(), vec![]))))); let withdraw_req = WithdrawRequest { amount: 1.into(), @@ -870,57 +265,29 @@ fn test_withdraw_impl_fee_details() { coin: "JST".to_string(), max: false, fee: Some(WithdrawFee::EthGas { - gas: ETH_GAS, + gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), }), memo: None, + ibc_source_channel: None, }; - coin.my_balance().wait().unwrap(); + coin.get_balance().wait().unwrap(); let tx_details = block_on(withdraw_impl(coin, withdraw_req)).unwrap(); let expected = Some( EthTxFeeDetails { coin: "ETH".into(), gas_price: "0.000000001".parse().unwrap(), - gas: ETH_GAS, + gas: gas_limit::ETH_MAX_TRADE_GAS, total_fee: "0.00015".parse().unwrap(), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, } .into(), ); assert_eq!(expected, tx_details.fee_details); } -#[test] -#[cfg(not(target_arch = "wasm32"))] -fn test_nonce_lock() { - use futures::future::join_all; - use mm2_test_helpers::for_tests::{wait_for_log, ETH_DEV_NODES}; - - // send several transactions concurrently to check that they are not using same nonce - // using real ETH dev node - let (ctx, coin) = random_eth_coin_for_test(EthCoinType::Eth, ETH_DEV_NODES, None); - let mut futures = vec![]; - for _ in 0..5 { - futures.push( - coin.sign_and_send_transaction( - 1000000000000u64.into(), - Action::Call(coin.my_address), - vec![], - 21000.into(), - ) - .compat(), - ); - } - let results = block_on(join_all(futures)); - for result in results { - result.unwrap(); - } - // Waiting for NONCE_LOCK… might not appear at all if waiting takes less than 0.5 seconds - // but all transactions are sent successfully still - // wait_for_log(&ctx.log, 1.1, &|line| line.contains("Waiting for NONCE_LOCK…"))); - block_on(wait_for_log(&ctx, 1.1, |line| line.contains("get_addr_nonce…"))).unwrap(); -} - #[test] fn test_add_ten_pct_one_gwei() { let num = wei_from_big_decimal(&"0.1".parse().unwrap(), 9).unwrap(); @@ -942,8 +309,8 @@ fn test_add_ten_pct_one_gwei() { #[test] fn get_sender_trade_preimage() { /// Trade fee for the ETH coin is `2 * 150_000 * gas_price` always. - fn expected_fee(gas_price: u64) -> TradeFee { - let amount = u256_to_big_decimal((2 * ETH_GAS * gas_price).into(), 18).expect("!u256_to_big_decimal"); + fn expected_fee(gas_price: u64, gas_limit: u64) -> TradeFee { + let amount = u256_to_big_decimal((gas_limit * gas_price).into(), 18).expect("!u256_to_big_decimal"); TradeFee { coin: "ETH".to_owned(), amount: amount.into(), @@ -951,34 +318,46 @@ fn get_sender_trade_preimage() { } } - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(GAS_PRICE.into())))); - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); let actual = block_on(coin.get_sender_trade_fee( TradePreimageValue::UpperBound(150.into()), FeeApproxStage::WithoutApprox, + true, )) .expect("!get_sender_trade_fee"); - let expected = expected_fee(GAS_PRICE); + let expected = expected_fee(GAS_PRICE, gas_limit::ETH_PAYMENT + gas_limit::ETH_SENDER_REFUND); assert_eq!(actual, expected); let value = u256_to_big_decimal(100.into(), 18).expect("!u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::OrderIssue)) - .expect("!get_sender_trade_fee"); - let expected = expected_fee(GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::OrderIssue, true)) + .expect("!get_sender_trade_fee"); + let expected = expected_fee( + GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE, + gas_limit::ETH_PAYMENT + gas_limit::ETH_SENDER_REFUND, + ); assert_eq!(actual, expected); let value = u256_to_big_decimal(1.into(), 18).expect("!u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::StartSwap)) + let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::StartSwap, true)) .expect("!get_sender_trade_fee"); - let expected = expected_fee(GAS_PRICE_APPROXIMATION_ON_START_SWAP); + let expected = expected_fee( + GAS_PRICE_APPROXIMATION_ON_START_SWAP, + gas_limit::ETH_PAYMENT + gas_limit::ETH_SENDER_REFUND, + ); assert_eq!(actual, expected); let value = u256_to_big_decimal(10000000000u64.into(), 18).expect("!u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::TradePreimage)) - .expect("!get_sender_trade_fee"); - let expected = expected_fee(GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::TradePreimage, true)) + .expect("!get_sender_trade_fee"); + let expected = expected_fee( + GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE, + gas_limit::ETH_PAYMENT + gas_limit::ETH_SENDER_REFUND, + ); assert_eq!(actual, expected); } @@ -991,8 +370,8 @@ fn get_erc20_sender_trade_preimage() { EthCoin::allowance .mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok(unsafe { ALLOWANCE.into() })))); - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); - EthCoin::estimate_gas.mock_safe(|_, _| { + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(GAS_PRICE.into())))); + EthCoin::estimate_gas_wrapper.mock_safe(|_, _| { unsafe { ESTIMATE_GAS_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(APPROVE_GAS_LIMIT.into()))) }); @@ -1013,64 +392,84 @@ fn get_erc20_sender_trade_preimage() { }, &["http://dummy.dummy"], None, + ETH_SEPOLIA_CHAIN_ID, ); // value is allowed unsafe { ALLOWANCE = 1000 }; let value = u256_to_big_decimal(1000.into(), 18).expect("u256_to_big_decimal"); - let actual = - block_on(coin.get_sender_trade_fee(TradePreimageValue::UpperBound(value), FeeApproxStage::WithoutApprox)) - .expect("!get_sender_trade_fee"); + let actual = block_on(coin.get_sender_trade_fee( + TradePreimageValue::UpperBound(value), + FeeApproxStage::WithoutApprox, + true, + )) + .expect("!get_sender_trade_fee"); log!("{:?}", actual.amount.to_decimal()); unsafe { assert!(!ESTIMATE_GAS_CALLED) } - assert_eq!(actual, expected_trade_fee(300_000, GAS_PRICE)); + assert_eq!( + actual, + expected_trade_fee(gas_limit::ERC20_PAYMENT + gas_limit::ERC20_SENDER_REFUND, GAS_PRICE) + ); // value is greater than allowance unsafe { ALLOWANCE = 999 }; let value = u256_to_big_decimal(1000.into(), 18).expect("u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::UpperBound(value), FeeApproxStage::StartSwap)) - .expect("!get_sender_trade_fee"); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::UpperBound(value), FeeApproxStage::StartSwap, true)) + .expect("!get_sender_trade_fee"); unsafe { assert!(ESTIMATE_GAS_CALLED); ESTIMATE_GAS_CALLED = false; } assert_eq!( actual, - expected_trade_fee(360_000, GAS_PRICE_APPROXIMATION_ON_START_SWAP) + expected_trade_fee( + gas_limit::ERC20_PAYMENT + gas_limit::ERC20_SENDER_REFUND + APPROVE_GAS_LIMIT, + GAS_PRICE_APPROXIMATION_ON_START_SWAP + ) ); // value is allowed unsafe { ALLOWANCE = 1000 }; let value = u256_to_big_decimal(999.into(), 18).expect("u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::OrderIssue)) - .expect("!get_sender_trade_fee"); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::OrderIssue, true)) + .expect("!get_sender_trade_fee"); unsafe { assert!(!ESTIMATE_GAS_CALLED) } assert_eq!( actual, - expected_trade_fee(300_000, GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE) + expected_trade_fee( + gas_limit::ERC20_PAYMENT + gas_limit::ERC20_SENDER_REFUND, + GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE + ) ); // value is greater than allowance unsafe { ALLOWANCE = 1000 }; let value = u256_to_big_decimal(1500.into(), 18).expect("u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::TradePreimage)) - .expect("!get_sender_trade_fee"); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::TradePreimage, true)) + .expect("!get_sender_trade_fee"); unsafe { assert!(ESTIMATE_GAS_CALLED); ESTIMATE_GAS_CALLED = false; } assert_eq!( actual, - expected_trade_fee(360_000, GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE) + expected_trade_fee( + gas_limit::ERC20_PAYMENT + gas_limit::ERC20_SENDER_REFUND + APPROVE_GAS_LIMIT, + GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE + ) ); } #[test] fn get_receiver_trade_preimage() { - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(GAS_PRICE.into())))); - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None); - let amount = u256_to_big_decimal((ETH_GAS * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); + let amount = + u256_to_big_decimal((gas_limit::ETH_RECEIVER_SPEND * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); let expected_fee = TradeFee { coin: "ETH".to_owned(), amount: amount.into(), @@ -1089,8 +488,8 @@ fn test_get_fee_to_send_taker_fee() { const DEX_FEE_AMOUNT: u64 = 100_000; const TRANSFER_GAS_LIMIT: u64 = 40_000; - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); - EthCoin::estimate_gas + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(GAS_PRICE.into())))); + EthCoin::estimate_gas_wrapper .mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok(TRANSFER_GAS_LIMIT.into())))); // fee to send taker fee is `TRANSFER_GAS_LIMIT * gas_price` always. @@ -1103,7 +502,7 @@ fn test_get_fee_to_send_taker_fee() { let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); let actual = block_on(coin.get_fee_to_send_taker_fee( DexFee::Standard(MmNumber::from(dex_fee_amount.clone())), FeeApproxStage::WithoutApprox, @@ -1118,6 +517,7 @@ fn test_get_fee_to_send_taker_fee() { }, &["http://dummy.dummy"], None, + ETH_SEPOLIA_CHAIN_ID, ); let actual = block_on(coin.get_fee_to_send_taker_fee( DexFee::Standard(MmNumber::from(dex_fee_amount)), @@ -1138,7 +538,7 @@ fn test_get_fee_to_send_taker_fee() { fn test_get_fee_to_send_taker_fee_insufficient_balance() { const DEX_FEE_AMOUNT: u64 = 100_000_000_000; - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(40.into())))); + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(40.into())))); let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { platform: "ETH".to_string(), @@ -1146,6 +546,7 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { }, &[ETH_MAINNET_NODE], None, + ETH_MAINNET_CHAIN_ID, ); let dex_fee_amount = u256_to_big_decimal(DEX_FEE_AMOUNT.into(), 18).expect("!u256_to_big_decimal"); @@ -1163,10 +564,10 @@ fn test_get_fee_to_send_taker_fee_insufficient_balance() { #[test] fn validate_dex_fee_invalid_sender_eth() { - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None, ETH_MAINNET_CHAIN_ID); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f - let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( H256::from_str("0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f").unwrap(), ))) .unwrap() @@ -1197,10 +598,11 @@ fn validate_dex_fee_invalid_sender_erc() { }, &[ETH_MAINNET_NODE], None, + ETH_MAINNET_CHAIN_ID, ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 - let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( H256::from_str("0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600").unwrap(), ))) .unwrap() @@ -1233,10 +635,10 @@ fn sender_compressed_pub(tx: &SignedEthTx) -> [u8; 33] { #[test] fn validate_dex_fee_eth_confirmed_before_min_block() { - let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None, ETH_MAINNET_CHAIN_ID); // the real dex fee sent on mainnet // https://etherscan.io/tx/0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f - let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( H256::from_str("0x7e9ca16c85efd04ee5e31f2c1914b48f5606d6f9ce96ecce8c96d47d6857278f").unwrap(), ))) .unwrap() @@ -1269,10 +671,11 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { }, &[ETH_MAINNET_NODE], None, + ETH_MAINNET_CHAIN_ID, ); // the real dex fee sent on mainnet // https://etherscan.io/tx/0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600 - let tx = block_on(coin.web3.eth().transaction(TransactionId::Hash( + let tx = block_on(block_on(coin.web3()).unwrap().eth().transaction(TransactionId::Hash( H256::from_str("0xd6403b41c79f9c9e9c83c03d920ee1735e7854d85d94cef48d95dfeca95cd600").unwrap(), ))) .unwrap() @@ -1299,7 +702,7 @@ fn validate_dex_fee_erc_confirmed_before_min_block() { #[test] fn test_negotiate_swap_contract_addr_no_fallback() { - let (_, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None); + let (_, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], None, ETH_MAINNET_CHAIN_ID); let input = None; let error = coin.negotiate_swap_contract_addr(input).unwrap_err().into_inner(); @@ -1328,7 +731,12 @@ fn test_negotiate_swap_contract_addr_no_fallback() { fn test_negotiate_swap_contract_addr_has_fallback() { let fallback = Address::from_str("0x8500AFc0bc5214728082163326C2FF0C73f4a871").unwrap(); - let (_, coin) = eth_coin_for_test(EthCoinType::Eth, &[ETH_MAINNET_NODE], Some(fallback)); + let (_, coin) = eth_coin_for_test( + EthCoinType::Eth, + &[ETH_MAINNET_NODE], + Some(fallback), + ETH_MAINNET_CHAIN_ID, + ); let input = None; let result = coin.negotiate_swap_contract_addr(input).unwrap(); @@ -1393,7 +801,7 @@ fn polygon_check_if_my_payment_sent() { )) .unwrap(); - println!("{:02x}", coin.my_address); + log!("{}", coin.my_address().unwrap()); let secret_hash = hex::decode("fc33114b389f0ee1212abf2867e99e89126f4860").unwrap(); let swap_contract_address = "9130b257d37a52e52f21054c4da3450c72f595ce".into(); @@ -1413,45 +821,19 @@ fn polygon_check_if_my_payment_sent() { .unwrap() .unwrap(); let expected_hash = BytesJson::from("69a20008cea0c15ee483b5bbdff942752634aa072dfd2ff715fe87eec302de11"); - assert_eq!(expected_hash, my_payment.tx_hash()); + assert_eq!(expected_hash, my_payment.tx_hash_as_bytes()); } #[test] fn test_message_hash() { - let key_pair = KeyPair::from_secret_slice( - &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), - ) - .unwrap(); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); - let web3 = Web3::new(transport); - let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = EthCoin(Arc::new(EthCoinImpl { - ticker: "ETH".into(), - coin_type: EthCoinType::Eth, - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - fallback_swap_contract: None, - contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotStarted), - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); + let key_pair = Random.generate().unwrap(); + let (_ctx, coin) = eth_coin_from_keypair( + EthCoinType::Eth, + ETH_SEPOLIA_NODES, + None, + key_pair, + ETH_SEPOLIA_CHAIN_ID, + ); let message_hash = coin.sign_message_hash("test").unwrap(); assert_eq!( @@ -1466,37 +848,13 @@ fn test_sign_verify_message() { &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), ) .unwrap(); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); - - let web3 = Web3::new(transport); - let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = EthCoin(Arc::new(EthCoinImpl { - ticker: "ETH".into(), - coin_type: EthCoinType::Eth, - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - fallback_swap_contract: None, - contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotStarted), - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); + let (_ctx, coin) = eth_coin_from_keypair( + EthCoinType::Eth, + ETH_SEPOLIA_NODES, + None, + key_pair, + ETH_SEPOLIA_CHAIN_ID, + ); let message = "test"; let signature = coin.sign_message(message).unwrap(); @@ -1510,45 +868,12 @@ fn test_sign_verify_message() { #[test] fn test_eth_extract_secret() { - let key_pair = KeyPair::from_secret_slice( - &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), - ) - .unwrap(); - let transport = Web3Transport::single_node("https://ropsten.infura.io/v3/c01c1b4cf66642528547624e1d6d9d6b", false); - let web3 = Web3::new(transport); - let ctx = MmCtxBuilder::new().into_mm_arc(); - - let swap_contract_address = Address::from_str("0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94").unwrap(); - let coin = EthCoin(Arc::new(EthCoinImpl { - coin_type: EthCoinType::Erc20 { - platform: "ETH".to_string(), - token_addr: Address::from_str("0xc0eb7aed740e1796992a08962c15661bdeb58003").unwrap(), - }, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address, - fallback_swap_contract: None, - contract_supports_watchers: false, - ticker: "ETH".into(), - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: true, - }], - web3, - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); + let key_pair = Random.generate().unwrap(); + let coin_type = EthCoinType::Erc20 { + platform: "ETH".to_string(), + token_addr: Address::from_str("0xc0eb7aed740e1796992a08962c15661bdeb58003").unwrap(), + }; + let (_ctx, coin) = eth_coin_from_keypair(coin_type, &["http://dummy.dummy"], None, key_pair, ETH_SEPOLIA_CHAIN_ID); // raw transaction bytes of https://ropsten.etherscan.io/tx/0xcb7c14d3ff309996d582400369393b6fa42314c52245115d4a3f77f072c36da9 let tx_bytes = &[ @@ -1642,3 +967,60 @@ fn test_eth_validate_valid_and_invalid_pubkey() { assert!(coin.validate_other_pubkey(&[1u8; 20]).is_err()); assert!(coin.validate_other_pubkey(&[1u8; 8]).is_err()); } + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_fee_history() { + use mm2_test_helpers::for_tests::ETH_SEPOLIA_NODES; + + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, ETH_SEPOLIA_NODES, None, ETH_SEPOLIA_CHAIN_ID); + // check fee history without percentiles decoded okay + let res = block_on(coin.eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[])); + assert!(res.is_ok()); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_gas_limit_conf() { + use mm2_test_helpers::for_tests::ETH_SEPOLIA_SWAP_CONTRACT; + + let conf = json!({ + "coins": [{ + "coin": "ETH", + "name": "ethereum", + "fname": "Ethereum", + "chain_id": 1337, + "protocol":{ + "type": "ETH" + }, + "chain_id": 1, + "rpcport": 80, + "mm2": 1, + "gas_limit": { + "erc20_payment": 120000, + "erc20_receiver_spend": 130000, + "erc20_sender_refund": 110000 + } + }] + }); + + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123456").unwrap(); + + let req = json!({ + "urls":ETH_SEPOLIA_NODES, + "swap_contract_address":ETH_SEPOLIA_SWAP_CONTRACT + }); + let coin = block_on(lp_coininit(&ctx, "ETH", &req)).unwrap(); + let eth_coin = match coin { + MmCoinEnum::EthCoin(eth_coin) => eth_coin, + _ => panic!("not eth coin"), + }; + assert!( + eth_coin.gas_limit.eth_send_coins == 21_000 + && eth_coin.gas_limit.erc20_payment == 120000 + && eth_coin.gas_limit.erc20_receiver_spend == 130000 + && eth_coin.gas_limit.erc20_sender_refund == 110000 + && eth_coin.gas_limit.eth_max_trade_gas == 150_000 + ); +} diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index cc0e7de4ec..a36e0ac45a 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -1,10 +1,8 @@ use super::*; use crate::lp_coininit; -use crypto::privkey::key_pair_from_seed; use crypto::CryptoCtx; use mm2_core::mm_ctx::MmCtxBuilder; -use mm2_test_helpers::for_tests::{ETH_DEV_NODE, ETH_DEV_SWAP_CONTRACT}; -use mm2_test_helpers::get_passphrase; +use mm2_test_helpers::for_tests::{ETH_SEPOLIA_NODES, ETH_SEPOLIA_SWAP_CONTRACT}; use wasm_bindgen_test::*; use web_sys::console; @@ -16,72 +14,20 @@ fn pass() { let _coins_context = CoinsContext::from_ctx(&ctx).unwrap(); } -#[wasm_bindgen_test] -async fn test_send() { - let seed = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); - let keypair = key_pair_from_seed(&seed).unwrap(); - let key_pair = KeyPair::from_secret_slice(keypair.private_ref()).unwrap(); - let transport = Web3Transport::single_node(ETH_DEV_NODE, false); - let web3 = Web3::new(transport); - let ctx = MmCtxBuilder::new().into_mm_arc(); - let coin = EthCoin(Arc::new(EthCoinImpl { - ticker: "ETH".into(), - coin_type: EthCoinType::Eth, - my_address: key_pair.address(), - sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), - priv_key_policy: key_pair.into(), - swap_contract_address: Address::from_str(ETH_DEV_SWAP_CONTRACT).unwrap(), - fallback_swap_contract: None, - contract_supports_watchers: false, - web3_instances: vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }], - web3, - decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, - gas_station_policy: GasStationPricePolicy::MeanAverageFast, - history_sync_state: Mutex::new(HistorySyncState::NotStarted), - ctx: ctx.weak(), - required_confirmations: 1.into(), - chain_id: None, - logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, - nonce_lock: new_nonce_lock(), - erc20_tokens_infos: Default::default(), - abortable_system: AbortableQueue::default(), - })); - let maker_payment_args = SendPaymentArgs { - time_lock_duration: 0, - time_lock: 1000, - other_pubkey: &DEX_FEE_ADDR_RAW_PUBKEY, - secret_hash: &[1; 20], - amount: "0.001".parse().unwrap(), - swap_contract_address: &coin.swap_contract_address(), - swap_unique_data: &[], - payment_instructions: &None, - watcher_reward: None, - wait_for_confirmation_until: 0, - }; - let tx = coin.send_maker_payment(maker_payment_args).compat().await.unwrap(); - console::log_1(&format!("{:?}", tx).into()); - - let block = coin.current_block().compat().await.unwrap(); - console::log_1(&format!("{:?}", block).into()); -} - -#[wasm_bindgen_test] -async fn test_init_eth_coin() { +async fn init_eth_coin_helper() -> Result<(MmArc, MmCoinEnum), String> { let conf = json!({ "coins": [{ "coin": "ETH", "name": "ethereum", "fname": "Ethereum", + "chain_id": 1337, "protocol":{ "type": "ETH" }, + "chain_id": 1, "rpcport": 80, - "mm2": 1 + "mm2": 1, + "max_eth_tx_type": 2 }] }); @@ -93,8 +39,58 @@ async fn test_init_eth_coin() { .unwrap(); let req = json!({ - "urls":[ETH_DEV_NODE], - "swap_contract_address":ETH_DEV_SWAP_CONTRACT + "urls":ETH_SEPOLIA_NODES, + "swap_contract_address":ETH_SEPOLIA_SWAP_CONTRACT }); - let _coin = lp_coininit(&ctx, "ETH", &req).await.unwrap(); + Ok((ctx.clone(), lp_coininit(&ctx, "ETH", &req).await?)) +} + +#[wasm_bindgen_test] +async fn test_init_eth_coin() { let (_ctx, _coin) = init_eth_coin_helper().await.unwrap(); } + +#[wasm_bindgen_test] +async fn wasm_test_sign_eth_tx() { + // we need to hold ref to _ctx until the end of the test (because of the weak ref to MmCtx in EthCoinImpl) + let (_ctx, coin) = init_eth_coin_helper().await.unwrap(); + let sign_req = json::from_value(json!({ + "coin": "ETH", + "type": "ETH", + "tx": { + "to": "0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94".to_string(), + "value": "1.234", + "gas_limit": "21000" + } + })) + .unwrap(); + let res = coin.sign_raw_tx(&sign_req).await; + console::log_1(&format!("res={:?}", res).into()); + assert!(res.is_ok()); +} + +#[wasm_bindgen_test] +async fn wasm_test_sign_eth_tx_with_priority_fee() { + // we need to hold ref to _ctx until the end of the test (because of the weak ref to MmCtx in EthCoinImpl) + let (_ctx, coin) = init_eth_coin_helper().await.unwrap(); + let sign_req = json::from_value(json!({ + "coin": "ETH", + "type": "ETH", + "tx": { + "to": "0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94".to_string(), + "value": "1.234", + "gas_limit": "21000", + "pay_for_gas": { + "tx_type": "Eip1559", + "max_fee_per_gas": "1234.567", + "max_priority_fee_per_gas": "1.2", + } + } + })) + .unwrap(); + let res = coin.sign_raw_tx(&sign_req).await; + console::log_1(&format!("res={:?}", res).into()); + assert!(res.is_ok()); + let tx: UnverifiedTransactionWrapper = rlp::decode(&res.unwrap().tx_hex).expect("decoding signed tx okay"); + if !matches!(tx, UnverifiedTransactionWrapper::Eip1559(..)) { + panic!("expected eip1559 tx"); + } } diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs new file mode 100644 index 0000000000..b3de177a89 --- /dev/null +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -0,0 +1,487 @@ +use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, EthCoinType, EthDerivationMethod, + EthPrivKeyPolicy, Public, WithdrawError, WithdrawRequest, WithdrawResult, ERC20_CONTRACT, H160, H256}; +use crate::eth::{calc_total_fee, get_eth_gas_details_from_withdraw_fee, tx_builder_with_pay_for_gas_option, + tx_type_from_pay_for_gas_option, Action, Address, EthTxFeeDetails, KeyPair, PayForGasOption, + SignedEthTx, TransactionWrapper, UnSignedEthTxBuilder}; +use crate::hd_wallet::{HDCoinWithdrawOps, HDWalletOps, WithdrawFrom, WithdrawSenderAddress}; +use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; +use crate::{BytesJson, CoinWithDerivationMethod, EthCoin, GetWithdrawSenderAddress, PrivKeyPolicy, TransactionData, + TransactionDetails}; +use async_trait::async_trait; +use bip32::DerivationPath; +use common::custom_futures::timeout::FutureTimerExt; +use common::now_sec; +use crypto::hw_rpc_task::HwRpcTaskAwaitingStatus; +use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProcessor}; +use crypto::{CryptoCtx, HwRpcError}; +use ethabi::Token; +use futures::compat::Future01CompatExt; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::map_mm_error::MapMmError; +use mm2_err_handle::mm_error::MmResult; +use mm2_err_handle::prelude::{MapToMmResult, MmError, OrMmError}; +use std::ops::Deref; +use std::sync::Arc; +#[cfg(target_arch = "wasm32")] +use web3::types::TransactionRequest; + +/// `EthWithdraw` trait provides methods for withdrawing Ethereum and ERC20 tokens. +/// This allows different implementations of withdrawal logic for different types of wallets. +#[async_trait] +pub trait EthWithdraw +where + Self: Sized + Sync, +{ + /// A getter for the coin that implements this trait. + fn coin(&self) -> &EthCoin; + + /// A getter for the withdrawal request. + fn request(&self) -> &WithdrawRequest; + + /// Executes the logic that should be performed just before generating a transaction. + #[allow(clippy::result_large_err)] + fn on_generating_transaction(&self) -> Result<(), MmError>; + + /// Executes the logic that should be performed just before finishing the withdrawal. + #[allow(clippy::result_large_err)] + fn on_finishing(&self) -> Result<(), MmError>; + + /// Signs the transaction with a Trezor hardware wallet. + async fn sign_tx_with_trezor( + &self, + derivation_path: &DerivationPath, + unsigned_tx: &TransactionWrapper, + ) -> Result>; + + /// Transforms the `from` parameter of the withdrawal request into an address. + async fn get_from_address(&self, req: &WithdrawRequest) -> Result> { + let coin = self.coin(); + match req.from { + Some(_) => Ok(coin.get_withdraw_sender_address(req).await?.address), + None => Ok(coin.derivation_method.single_addr_or_err().await?), + } + } + + /// Gets the key pair for the address from which the withdrawal is made. + #[allow(clippy::result_large_err)] + fn get_key_pair(&self, req: &WithdrawRequest) -> Result> { + let coin = self.coin(); + if coin.priv_key_policy.is_trezor() { + return MmError::err(WithdrawError::InternalError("no keypair for hw wallet".to_owned())); + } + + match req.from { + Some(ref from) => { + let derivation_path = self.get_from_derivation_path(from)?; + let raw_priv_key = coin + .priv_key_policy + .hd_wallet_derived_priv_key_or_err(&derivation_path)?; + KeyPair::from_secret_slice(raw_priv_key.as_slice()) + .map_to_mm(|e| WithdrawError::InternalError(e.to_string())) + }, + None => coin + .priv_key_policy + .activated_key_or_err() + .mm_err(|e| WithdrawError::InternalError(e.to_string())) + .cloned(), + } + } + + /// Gets the derivation path for the address from which the withdrawal is made using the `from` parameter. + #[allow(clippy::result_large_err)] + fn get_from_derivation_path(&self, from: &WithdrawFrom) -> Result> { + let coin = self.coin(); + let path_to_coin = &coin.deref().derivation_method.hd_wallet_or_err()?.derivation_path; + let path_to_address = from.to_address_path(path_to_coin.coin_type())?; + let derivation_path = path_to_address.to_derivation_path(path_to_coin)?; + Ok(derivation_path) + } + + /// Gets the derivation path for the address from which the withdrawal is made using the withdrawal request. + async fn get_withdraw_derivation_path( + &self, + req: &WithdrawRequest, + ) -> Result> { + let coin = self.coin(); + match req.from { + Some(ref from) => self.get_from_derivation_path(from), + None => { + let default_hd_address = &coin + .deref() + .derivation_method + .hd_wallet_or_err()? + .get_enabled_address() + .await + .ok_or_else(|| WithdrawError::InternalError("no enabled address".to_owned()))?; + Ok(default_hd_address.derivation_path.clone()) + }, + } + } + + /// Signs the transaction and returns the transaction hash and the signed transaction. + async fn sign_withdraw_tx( + &self, + req: &WithdrawRequest, + unsigned_tx: TransactionWrapper, + ) -> Result<(H256, BytesJson), MmError> { + let coin = self.coin(); + match coin.priv_key_policy { + EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { + let key_pair = self.get_key_pair(req)?; + let signed = unsigned_tx.sign(key_pair.secret(), Some(coin.chain_id))?; + let bytes = rlp::encode(&signed); + + Ok((signed.tx_hash(), BytesJson::from(bytes.to_vec()))) + }, + EthPrivKeyPolicy::Trezor => { + let derivation_path = self.get_withdraw_derivation_path(req).await?; + let signed = self.sign_tx_with_trezor(&derivation_path, &unsigned_tx).await?; + let bytes = rlp::encode(&signed); + Ok((signed.tx_hash(), BytesJson::from(bytes.to_vec()))) + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => MmError::err(WithdrawError::InternalError("invalid policy".to_owned())), + } + } + + /// Sends the transaction and returns the transaction hash and the signed transaction. + /// This method should only be used when withdrawing using an external wallet like MetaMask. + #[cfg(target_arch = "wasm32")] + async fn send_withdraw_tx( + &self, + req: &WithdrawRequest, + tx_to_send: TransactionRequest, + ) -> Result<(H256, BytesJson), MmError> { + let coin = self.coin(); + match coin.priv_key_policy { + EthPrivKeyPolicy::Metamask(_) => { + if !req.broadcast { + let error = + "Set 'broadcast' to generate, sign and broadcast a transaction with MetaMask".to_string(); + return MmError::err(WithdrawError::BroadcastExpected(error)); + } + + // Wait for 10 seconds for the transaction to appear on the RPC node. + let wait_rpc_timeout = 10_000; + let check_every = 1.; + + // Please note that this method may take a long time + // due to `wallet_switchEthereumChain` and `eth_sendTransaction` requests. + let tx_hash = coin.send_transaction(tx_to_send).await?; + + let signed_tx = coin + .wait_for_tx_appears_on_rpc(tx_hash, wait_rpc_timeout, check_every) + .await?; + let tx_hex = signed_tx + .map(|signed_tx| BytesJson::from(rlp::encode(&signed_tx).to_vec())) + // Return an empty `tx_hex` if the transaction is still not appeared on the RPC node. + .unwrap_or_default(); + Ok((tx_hash, tx_hex)) + }, + EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } | EthPrivKeyPolicy::Trezor => { + MmError::err(WithdrawError::InternalError("invalid policy".to_owned())) + }, + } + } + + /// Builds the withdrawal transaction and returns the transaction details. + async fn build(self) -> WithdrawResult { + let coin = self.coin(); + let ticker = coin.deref().ticker.clone(); + let req = self.request().clone(); + + let to_addr = coin + .address_from_str(&req.to) + .map_to_mm(WithdrawError::InvalidAddress)?; + let my_address = self.get_from_address(&req).await?; + + self.on_generating_transaction()?; + + let my_balance = coin.address_balance(my_address).compat().await?; + let my_balance_dec = u256_to_big_decimal(my_balance, coin.decimals)?; + + let (mut wei_amount, dec_amount) = if req.max { + (my_balance, my_balance_dec.clone()) + } else { + let wei_amount = wei_from_big_decimal(&req.amount, coin.decimals)?; + (wei_amount, req.amount.clone()) + }; + if wei_amount > my_balance { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: coin.ticker.clone(), + available: my_balance_dec.clone(), + required: dec_amount, + }); + }; + let (mut eth_value, data, call_addr, fee_coin) = match &coin.coin_type { + EthCoinType::Eth => (wei_amount, vec![], to_addr, ticker.as_str()), + EthCoinType::Erc20 { platform, token_addr } => { + let function = ERC20_CONTRACT.function("transfer")?; + let data = function.encode_input(&[Token::Address(to_addr), Token::Uint(wei_amount)])?; + (0.into(), data, *token_addr, platform.as_str()) + }, + EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), + }; + let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; + + let (gas, pay_for_gas_option) = get_eth_gas_details_from_withdraw_fee( + coin, + req.fee.clone(), + eth_value, + data.clone().into(), + my_address, + call_addr, + false, + ) + .await?; + let total_fee = calc_total_fee(gas, &pay_for_gas_option)?; + let total_fee_dec = u256_to_big_decimal(total_fee, coin.decimals)?; + + if req.max && coin.coin_type == EthCoinType::Eth { + if eth_value < total_fee || wei_amount < total_fee { + return MmError::err(WithdrawError::AmountTooLow { + amount: eth_value_dec, + threshold: total_fee_dec, + }); + } + eth_value -= total_fee; + wei_amount -= total_fee; + }; + drop_mutability!(eth_value); + drop_mutability!(wei_amount); + + let (tx_hash, tx_hex) = match coin.priv_key_policy { + EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } | EthPrivKeyPolicy::Trezor => { + let address_lock = coin.get_address_lock(my_address.to_string()).await; + let _nonce_lock = address_lock.lock().await; + let (nonce, _) = coin + .clone() + .get_addr_nonce(my_address) + .compat() + .timeout_secs(30.) + .await? + .map_to_mm(WithdrawError::Transport)?; + + let tx_type = tx_type_from_pay_for_gas_option!(pay_for_gas_option); + if !coin.is_tx_type_supported(&tx_type) { + return MmError::err(WithdrawError::TxTypeNotSupported); + } + let tx_builder = + UnSignedEthTxBuilder::new(tx_type, nonce, gas, Action::Call(call_addr), eth_value, data); + let tx_builder = tx_builder_with_pay_for_gas_option(coin, tx_builder, &pay_for_gas_option)?; + let unsigned_tx = tx_builder + .build() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + self.sign_withdraw_tx(&req, unsigned_tx).await? + }, + #[cfg(target_arch = "wasm32")] + EthPrivKeyPolicy::Metamask(_) => { + let gas_price = pay_for_gas_option.get_gas_price(); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); + let tx_to_send = TransactionRequest { + from: my_address, + to: Some(to_addr), + gas: Some(gas), + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + value: Some(eth_value), + data: Some(data.into()), + nonce: None, + ..TransactionRequest::default() + }; + self.send_withdraw_tx(&req, tx_to_send).await? + }, + }; + + self.on_finishing()?; + let tx_hash_bytes = BytesJson::from(tx_hash.0.to_vec()); + let tx_hash_str = format!("{:02x}", tx_hash_bytes); + + let amount_decimal = u256_to_big_decimal(wei_amount, coin.decimals)?; + let mut spent_by_me = amount_decimal.clone(); + let received_by_me = if to_addr == my_address { + amount_decimal.clone() + } else { + 0.into() + }; + let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; + if coin.coin_type == EthCoinType::Eth { + spent_by_me += &fee_details.total_fee; + } + Ok(TransactionDetails { + to: vec![checksum_address(&format!("{:#02x}", to_addr))], + from: vec![checksum_address(&format!("{:#02x}", my_address))], + total_amount: amount_decimal, + my_balance_change: &received_by_me - &spent_by_me, + spent_by_me, + received_by_me, + tx: TransactionData::new_signed(tx_hex, tx_hash_str), + block_height: 0, + fee_details: Some(fee_details.into()), + coin: coin.ticker.clone(), + internal_id: vec![].into(), + timestamp: now_sec(), + kmd_rewards: None, + transaction_type: Default::default(), + memo: None, + }) + } +} + +/// Eth withdraw version with user interaction support +pub struct InitEthWithdraw { + ctx: MmArc, + coin: EthCoin, + task_handle: WithdrawTaskHandleShared, + req: WithdrawRequest, +} + +#[async_trait] +impl EthWithdraw for InitEthWithdraw { + fn coin(&self) -> &EthCoin { &self.coin } + + fn request(&self) -> &WithdrawRequest { &self.req } + + fn on_generating_transaction(&self) -> Result<(), MmError> { + Ok(self + .task_handle + .update_in_progress_status(WithdrawInProgressStatus::GeneratingTransaction)?) + } + + fn on_finishing(&self) -> Result<(), MmError> { + Ok(self + .task_handle + .update_in_progress_status(WithdrawInProgressStatus::Finishing)?) + } + + async fn sign_tx_with_trezor( + &self, + derivation_path: &DerivationPath, + unsigned_tx: &TransactionWrapper, + ) -> Result> { + let coin = self.coin(); + let crypto_ctx = CryptoCtx::from_ctx(&self.ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| WithdrawError::HwError(HwRpcError::NoTrezorDeviceAvailable))?; + let trezor_statuses = TrezorRequestStatuses { + on_button_request: WithdrawInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: HwRpcTaskAwaitingStatus::EnterTrezorPin, + on_passphrase_request: HwRpcTaskAwaitingStatus::EnterTrezorPassphrase, + on_ready: WithdrawInProgressStatus::FollowHwDeviceInstructions, + }; + let sign_processor = TrezorRpcTaskProcessor::new(self.task_handle.clone(), trezor_statuses); + let sign_processor = Arc::new(sign_processor); + let mut trezor_session = hw_ctx.trezor(sign_processor).await?; + let unverified_tx = trezor_session + .sign_eth_tx(derivation_path, unsigned_tx, coin.chain_id) + .await?; + Ok(SignedEthTx::new(unverified_tx).map_to_mm(|err| WithdrawError::InternalError(err.to_string()))?) + } +} + +#[allow(clippy::result_large_err)] +impl InitEthWithdraw { + pub fn new( + ctx: MmArc, + coin: EthCoin, + req: WithdrawRequest, + task_handle: WithdrawTaskHandleShared, + ) -> Result> { + Ok(InitEthWithdraw { + ctx, + coin, + task_handle, + req, + }) + } +} + +/// Simple eth withdraw version without user interaction support +pub struct StandardEthWithdraw { + coin: EthCoin, + req: WithdrawRequest, +} + +#[async_trait] +impl EthWithdraw for StandardEthWithdraw { + fn coin(&self) -> &EthCoin { &self.coin } + + fn request(&self) -> &WithdrawRequest { &self.req } + + fn on_generating_transaction(&self) -> Result<(), MmError> { Ok(()) } + + fn on_finishing(&self) -> Result<(), MmError> { Ok(()) } + + async fn sign_tx_with_trezor( + &self, + _derivation_path: &DerivationPath, + _unsigned_tx: &TransactionWrapper, + ) -> Result> { + async { + Err(MmError::new(WithdrawError::UnsupportedError(String::from( + "Trezor not supported for legacy RPC", + )))) + } + .await + } +} + +#[allow(clippy::result_large_err)] +impl StandardEthWithdraw { + pub fn new(coin: EthCoin, req: WithdrawRequest) -> Result> { + Ok(StandardEthWithdraw { coin, req }) + } +} + +#[async_trait] +impl GetWithdrawSenderAddress for EthCoin { + type Address = Address; + type Pubkey = Public; + + async fn get_withdraw_sender_address( + &self, + req: &WithdrawRequest, + ) -> MmResult, WithdrawError> { + eth_get_withdraw_from_address(self, req).await + } +} + +async fn eth_get_withdraw_from_address( + coin: &EthCoin, + req: &WithdrawRequest, +) -> MmResult, WithdrawError> { + match coin.derivation_method() { + EthDerivationMethod::SingleAddress(my_address) => eth_get_withdraw_iguana_sender(coin, req, my_address), + EthDerivationMethod::HDWallet(hd_wallet) => { + let from = req.from.clone().or_mm_err(|| WithdrawError::FromAddressNotFound)?; + coin.get_withdraw_hd_sender(hd_wallet, &from) + .await + .mm_err(WithdrawError::from) + }, + } +} + +#[allow(clippy::result_large_err)] +fn eth_get_withdraw_iguana_sender( + coin: &EthCoin, + req: &WithdrawRequest, + my_address: &Address, +) -> MmResult, WithdrawError> { + if req.from.is_some() { + let error = "'from' is not supported if the coin is initialized with an Iguana private key"; + return MmError::err(WithdrawError::UnexpectedFromAddress(error.to_owned())); + } + + let pubkey = match coin.priv_key_policy { + PrivKeyPolicy::Iguana(ref key_pair) => key_pair.public(), + _ => return MmError::err(WithdrawError::InternalError("not iguana private key policy".to_owned())), + }; + + Ok(WithdrawSenderAddress { + address: *my_address, + pubkey: *pubkey, + derivation_path: None, + }) +} diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs new file mode 100644 index 0000000000..72f3d080ad --- /dev/null +++ b/mm2src/coins/eth/for_tests.rs @@ -0,0 +1,82 @@ +#[cfg(not(target_arch = "wasm32"))] use super::*; +use mm2_core::mm_ctx::{MmArc, MmCtxBuilder}; +#[cfg(not(target_arch = "wasm32"))] +use mm2_test_helpers::for_tests::{eth_sepolia_conf, ETH_SEPOLIA_SWAP_CONTRACT}; + +lazy_static! { + static ref MM_CTX: MmArc = MmCtxBuilder::new().into_mm_arc(); +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn eth_coin_for_test( + coin_type: EthCoinType, + urls: &[&str], + fallback_swap_contract: Option
, + chain_id: u64, +) -> (MmArc, EthCoin) { + let key_pair = KeyPair::from_secret_slice( + &hex::decode("809465b17d0a4ddb3e4c69e8f23c2cabad868f51f8bed5c765ad1d6516c3306f").unwrap(), + ) + .unwrap(); + eth_coin_from_keypair(coin_type, urls, fallback_swap_contract, key_pair, chain_id) +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn eth_coin_from_keypair( + coin_type: EthCoinType, + urls: &[&str], + fallback_swap_contract: Option
, + key_pair: KeyPair, + chain_id: u64, +) -> (MmArc, EthCoin) { + let mut web3_instances = vec![]; + for url in urls.iter() { + let node = HttpTransportNode { + uri: url.parse().unwrap(), + gui_auth: false, + }; + let transport = Web3Transport::new_http(node); + let web3 = Web3::new(transport); + web3_instances.push(Web3Instance { web3, is_parity: false }); + } + drop_mutability!(web3_instances); + + let conf = json!({ "coins": [eth_sepolia_conf()] }); + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + let ticker = match coin_type { + EthCoinType::Eth => "ETH".to_string(), + EthCoinType::Erc20 { .. } => "JST".to_string(), + EthCoinType::Nft { ref platform } => platform.to_string(), + }; + let my_address = key_pair.address(); + let coin_conf = coin_conf(&ctx, &ticker); + let gas_limit = extract_gas_limit_from_conf(&coin_conf).expect("expected valid gas_limit config"); + + let eth_coin = EthCoin(Arc::new(EthCoinImpl { + coin_type, + decimals: 18, + history_sync_state: Mutex::new(HistorySyncState::NotEnabled), + sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), + priv_key_policy: key_pair.into(), + derivation_method: Arc::new(DerivationMethod::SingleAddress(my_address)), + swap_contract_address: Address::from_str(ETH_SEPOLIA_SWAP_CONTRACT).unwrap(), + fallback_swap_contract, + contract_supports_watchers: false, + ticker, + web3_instances: AsyncMutex::new(web3_instances), + ctx: ctx.weak(), + required_confirmations: 1.into(), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), + chain_id, + trezor_coin: None, + logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, + address_nonce_locks: Arc::new(AsyncMutex::new(new_nonce_lock())), + max_eth_tx_type: None, + erc20_tokens_infos: Default::default(), + nfts_infos: Arc::new(Default::default()), + platform_fee_estimator_state: Arc::new(FeeEstimatorState::CoinNotSupported), + gas_limit, + abortable_system: AbortableQueue::default(), + })); + (ctx, eth_coin) +} diff --git a/mm2src/coins/eth/nft_maker_swap_v2_abi.json b/mm2src/coins/eth/nft_maker_swap_v2_abi.json new file mode 100644 index 0000000000..95def23766 --- /dev/null +++ b/mm2src/coins/eth/nft_maker_swap_v2_abi.json @@ -0,0 +1,462 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "MakerPaymentRefundedSecret", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "MakerPaymentRefundedTimelock", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "MakerPaymentSent", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "MakerPaymentSpent", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "makerPayments", + "outputs": [ + { + "internalType": "bytes20", + "name": "paymentHash", + "type": "bytes20" + }, + { + "internalType": "uint32", + "name": "paymentLockTime", + "type": "uint32" + }, + { + "internalType": "enum EtomicSwapMakerNftV2.MakerPaymentState", + "name": "state", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onERC1155BatchReceived", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "onERC1155Received", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "onERC721Received", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecret", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "refundErc1155MakerPaymentSecret", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "refundErc1155MakerPaymentTimelock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecret", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "refundErc721MakerPaymentSecret", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "refundErc721MakerPaymentTimelock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecret", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "spendErc1155MakerPayment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecret", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "spendErc721MakerPayment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/mm2src/coins/eth/nft_swap_contract_abi.json b/mm2src/coins/eth/nft_swap_contract_abi.json new file mode 100644 index 0000000000..fd17b4cd8c --- /dev/null +++ b/mm2src/coins/eth/nft_swap_contract_abi.json @@ -0,0 +1,898 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "feeAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "MakerPaymentRefundedSecret", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "MakerPaymentRefundedTimelock", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "MakerPaymentSent", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "MakerPaymentSpent", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "TakerPaymentApproved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "secret", + "type": "bytes32" + } + ], + "name": "TakerPaymentRefundedSecret", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "TakerPaymentRefundedTimelock", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + } + ], + "name": "TakerPaymentSent", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "secret", + "type": "bytes32" + } + ], + "name": "TakerPaymentSpent", + "type": "event" + }, + { + "inputs": [], + "name": "dexFeeAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dexFee", + "type": "uint256" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "uint32", + "name": "preApproveLockTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "paymentLockTime", + "type": "uint32" + } + ], + "name": "erc20TakerPayment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "dexFee", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "uint32", + "name": "preApproveLockTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "paymentLockTime", + "type": "uint32" + } + ], + "name": "ethTakerPayment", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "makerPayments", + "outputs": [ + { + "internalType": "bytes20", + "name": "paymentHash", + "type": "bytes20" + }, + { + "internalType": "uint32", + "name": "paymentLockTime", + "type": "uint32" + }, + { + "internalType": "enum EtomicSwapNft.MakerPaymentState", + "name": "state", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onERC1155BatchReceived", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "onERC1155Received", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "onERC721Received", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecret", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "refundErc1155MakerPaymentSecret", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "refundErc1155MakerPaymentTimelock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecret", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "refundErc721MakerPaymentSecret", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "refundErc721MakerPaymentTimelock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dexFee", + "type": "uint256" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecret", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "name": "refundTakerPaymentSecret", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dexFee", + "type": "uint256" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "name": "refundTakerPaymentTimelock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecret", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "spendErc1155MakerPayment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecret", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "spendErc721MakerPayment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dexFee", + "type": "uint256" + }, + { + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecret", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "name": "spendTakerPayment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "id", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dexFee", + "type": "uint256" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "takerSecretHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "makerSecretHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + } + ], + "name": "takerPaymentApprove", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "takerPayments", + "outputs": [ + { + "internalType": "bytes20", + "name": "paymentHash", + "type": "bytes20" + }, + { + "internalType": "uint32", + "name": "preApproveLockTime", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "paymentLockTime", + "type": "uint32" + }, + { + "internalType": "enum EtomicSwapNft.TakerPaymentState", + "name": "state", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/mm2src/coins/eth/nft_swap_v2/errors.rs b/mm2src/coins/eth/nft_swap_v2/errors.rs new file mode 100644 index 0000000000..e66cd437d0 --- /dev/null +++ b/mm2src/coins/eth/nft_swap_v2/errors.rs @@ -0,0 +1,41 @@ +use enum_derives::EnumFromStringify; + +#[derive(Debug, Display)] +pub(crate) enum Erc721FunctionError { + AbiError(String), + FunctionNotFound(String), +} + +#[derive(Debug, Display)] +pub(crate) enum HtlcParamsError { + WrongPaymentTx(String), + TxDeserializationError(String), +} + +#[derive(Debug, Display, EnumFromStringify)] +pub(crate) enum PaymentStatusErr { + #[from_stringify("ethabi::Error")] + #[display(fmt = "Abi error: {}", _0)] + AbiError(String), + #[from_stringify("web3::Error")] + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[display(fmt = "Tx deserialization error: {}", _0)] + TxDeserializationError(String), +} + +#[derive(Debug, Display, EnumFromStringify)] +pub(crate) enum PrepareTxDataError { + #[from_stringify("ethabi::Error")] + #[display(fmt = "Abi error: {}", _0)] + AbiError(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + Erc721FunctionError(Erc721FunctionError), +} + +impl From for PrepareTxDataError { + fn from(e: Erc721FunctionError) -> Self { Self::Erc721FunctionError(e) } +} diff --git a/mm2src/coins/eth/nft_swap_v2/mod.rs b/mm2src/coins/eth/nft_swap_v2/mod.rs new file mode 100644 index 0000000000..19f5caca7f --- /dev/null +++ b/mm2src/coins/eth/nft_swap_v2/mod.rs @@ -0,0 +1,541 @@ +use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; +use ethabi::{Contract, Token}; +use ethcore_transaction::{Action, UnverifiedTransactionWrapper}; +use ethereum_types::{Address, U256}; +use futures::compat::Future01CompatExt; +use mm2_err_handle::prelude::{MapToMmResult, MmError, MmResult}; +use mm2_number::BigDecimal; +use std::convert::TryInto; +use web3::types::{Transaction as Web3Tx, TransactionId}; + +pub(crate) mod errors; +use errors::{Erc721FunctionError, HtlcParamsError, PaymentStatusErr, PrepareTxDataError}; +mod structs; +use structs::{ExpectedHtlcParams, PaymentType, ValidationParams}; + +use super::ContractType; +use crate::eth::{addr_from_raw_pubkey, decode_contract_call, EthCoin, EthCoinType, MakerPaymentStateV2, SignedEthTx, + TryToAddress, ERC1155_CONTRACT, ERC721_CONTRACT, NFT_MAKER_SWAP_V2}; +use crate::{ParseCoinAssocTypes, RefundPaymentArgs, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, TransactionErr, + ValidateNftMakerPaymentArgs}; + +impl EthCoin { + pub(crate) async fn send_nft_maker_payment_v2_impl( + &self, + args: SendNftMakerPaymentArgs<'_, Self>, + ) -> Result { + try_tx_s!(validate_payment_args( + args.taker_secret_hash, + args.maker_secret_hash, + &args.amount, + args.nft_swap_info.contract_type + )); + let htlc_data = try_tx_s!(self.prepare_htlc_data(&args)); + + match &self.coin_type { + EthCoinType::Nft { .. } => { + let data = try_tx_s!(self.prepare_nft_maker_payment_v2_data(&args, htlc_data).await); + self.sign_and_send_transaction( + 0.into(), + Action::Call(*args.nft_swap_info.token_address), + data, + U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + ) + .compat() + .await + }, + EthCoinType::Eth | EthCoinType::Erc20 { .. } => Err(TransactionErr::ProtocolNotSupported( + "ETH and ERC20 Protocols are not supported for NFT Swaps".to_string(), + )), + } + } + + pub(crate) async fn validate_nft_maker_payment_v2_impl( + &self, + args: ValidateNftMakerPaymentArgs<'_, Self>, + ) -> ValidatePaymentResult<()> { + let contract_type = args.nft_swap_info.contract_type; + validate_payment_args( + args.taker_secret_hash, + args.maker_secret_hash, + &args.amount, + contract_type, + ) + .map_err(ValidatePaymentError::InternalError)?; + let etomic_swap_contract = args.nft_swap_info.swap_contract_address; + let token_address = args.nft_swap_info.token_address; + let maker_address = addr_from_raw_pubkey(args.maker_pub).map_to_mm(ValidatePaymentError::InternalError)?; + let time_lock_u32 = args + .time_lock + .try_into() + .map_err(ValidatePaymentError::TimelockOverflow)?; + let swap_id = self.etomic_swap_id(time_lock_u32, args.maker_secret_hash); + let maker_status = self + .payment_status_v2( + *etomic_swap_contract, + Token::FixedBytes(swap_id.clone()), + &NFT_MAKER_SWAP_V2, + PaymentType::MakerPayments, + ) + .await?; + let tx_from_rpc = self + .transaction(TransactionId::Hash(args.maker_payment_tx.tx_hash())) + .await?; + let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { + ValidatePaymentError::TxDoesNotExist(format!( + "Didn't find provided tx {:?} on ETH node", + args.maker_payment_tx.tx_hash() + )) + })?; + validate_from_to_and_maker_status(tx_from_rpc, maker_address, *token_address, maker_status).await?; + match self.coin_type { + EthCoinType::Nft { .. } => { + let (decoded, index_bytes) = get_decoded_tx_data_and_index_bytes(contract_type, &tx_from_rpc.input.0)?; + + let amount = if matches!(contract_type, &ContractType::Erc1155) { + Some(args.amount.to_string()) + } else { + None + }; + + let validation_params = ValidationParams { + maker_address, + etomic_swap_contract: *etomic_swap_contract, + token_id: args.nft_swap_info.token_id, + amount, + }; + validate_decoded_data(&decoded, &validation_params)?; + + let taker_address = + addr_from_raw_pubkey(args.taker_pub).map_to_mm(ValidatePaymentError::InternalError)?; + let htlc_params = ExpectedHtlcParams { + swap_id, + taker_address, + token_address: *token_address, + taker_secret_hash: args.taker_secret_hash.to_vec(), + maker_secret_hash: args.maker_secret_hash.to_vec(), + time_lock: U256::from(args.time_lock), + }; + decode_and_validate_htlc_params(decoded, index_bytes, htlc_params)?; + }, + EthCoinType::Eth | EthCoinType::Erc20 { .. } => { + return MmError::err(ValidatePaymentError::InternalError( + "EthCoinType must be Nft".to_string(), + )) + }, + } + Ok(()) + } + + pub(crate) async fn spend_nft_maker_payment_v2_impl( + &self, + args: SpendNftMakerPaymentArgs<'_, Self>, + ) -> Result { + let etomic_swap_contract = args.swap_contract_address; + if args.maker_secret.len() != 32 { + return Err(TransactionErr::Plain(ERRL!("maker_secret must be 32 bytes"))); + } + let contract_type = args.contract_type; + let (decoded, index_bytes) = try_tx_s!(get_decoded_tx_data_and_index_bytes( + contract_type, + args.maker_payment_tx.unsigned().data() + )); + + let (state, htlc_params) = try_tx_s!( + self.status_and_htlc_params_from_tx_data( + *etomic_swap_contract, + &NFT_MAKER_SWAP_V2, + &decoded, + index_bytes, + PaymentType::MakerPayments, + ) + .await + ); + match self.coin_type { + EthCoinType::Nft { .. } => { + let data = try_tx_s!(self.prepare_spend_nft_maker_v2_data(&args, decoded, htlc_params, state)); + self.sign_and_send_transaction( + 0.into(), + Action::Call(*etomic_swap_contract), + data, + U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + ) + .compat() + .await + }, + EthCoinType::Eth | EthCoinType::Erc20 { .. } => Err(TransactionErr::ProtocolNotSupported( + "ETH and ERC20 Protocols are not supported for NFT Swaps".to_string(), + )), + } + } + + pub(crate) async fn refund_nft_maker_payment_v2_timelock_impl( + &self, + args: RefundPaymentArgs<'_>, + ) -> Result { + let _etomic_swap_contract = try_tx_s!(args.swap_contract_address.try_to_address()); + let tx: UnverifiedTransactionWrapper = try_tx_s!(rlp::decode(args.payment_tx)); + let _payment = try_tx_s!(SignedEthTx::new(tx)); + todo!() + } + + async fn prepare_nft_maker_payment_v2_data( + &self, + args: &SendNftMakerPaymentArgs<'_, Self>, + htlc_data: Vec, + ) -> Result, PrepareTxDataError> { + match args.nft_swap_info.contract_type { + ContractType::Erc1155 => { + let function = ERC1155_CONTRACT.function("safeTransferFrom")?; + let amount_u256 = U256::from_dec_str(&args.amount.to_string()) + .map_err(|e| PrepareTxDataError::Internal(e.to_string()))?; + let data = function.encode_input(&[ + Token::Address(self.my_addr().await), + Token::Address(*args.nft_swap_info.swap_contract_address), + Token::Uint(U256::from(args.nft_swap_info.token_id)), + Token::Uint(amount_u256), + Token::Bytes(htlc_data), + ])?; + Ok(data) + }, + ContractType::Erc721 => { + let function = erc721_transfer_with_data()?; + let data = function.encode_input(&[ + Token::Address(self.my_addr().await), + Token::Address(*args.nft_swap_info.swap_contract_address), + Token::Uint(U256::from(args.nft_swap_info.token_id)), + Token::Bytes(htlc_data), + ])?; + Ok(data) + }, + } + } + + fn prepare_htlc_data(&self, args: &SendNftMakerPaymentArgs<'_, Self>) -> Result, PrepareTxDataError> { + let taker_address = + addr_from_raw_pubkey(args.taker_pub).map_err(|e| PrepareTxDataError::Internal(ERRL!("{}", e)))?; + let time_lock_u32 = args + .time_lock + .try_into() + .map_err(|e| PrepareTxDataError::Internal(ERRL!("{}", e)))?; + let id = self.etomic_swap_id(time_lock_u32, args.maker_secret_hash); + let encoded = ethabi::encode(&[ + Token::FixedBytes(id), + Token::Address(taker_address), + Token::Address(*args.nft_swap_info.token_address), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::Uint(U256::from(time_lock_u32)), + ]); + Ok(encoded) + } + + /// Retrieves the payment status from a given smart contract address based on the swap ID and state type. + async fn payment_status_v2( + &self, + swap_address: Address, + swap_id: Token, + contract_abi: &Contract, + state_type: PaymentType, + ) -> Result { + let function_name = state_type.as_str(); + let function = contract_abi.function(function_name)?; + let data = function.encode_input(&[swap_id])?; + let bytes = self + .call_request(self.my_addr().await, swap_address, None, Some(data.into())) + .await?; + let decoded_tokens = function.decode_output(&bytes.0)?; + let state = decoded_tokens + .get(2) + .ok_or_else(|| PaymentStatusErr::Internal(ERRL!("Payment status must contain 'state' as the 2nd token")))?; + match state { + Token::Uint(state) => Ok(*state), + _ => Err(PaymentStatusErr::Internal(ERRL!( + "Payment status must be Uint, got {:?}", + state + ))), + } + } + + /// Prepares the encoded transaction data for spending a maker's NFT payment on the blockchain. + /// + /// This function selects the appropriate contract function based on the NFT's contract type (ERC1155 or ERC721) + /// and encodes the input parameters required for the blockchain transaction. + fn prepare_spend_nft_maker_v2_data( + &self, + args: &SpendNftMakerPaymentArgs<'_, Self>, + decoded: Vec, + htlc_params: Vec, + state: U256, + ) -> Result, PrepareTxDataError> { + let spend_func = match args.contract_type { + ContractType::Erc1155 => NFT_MAKER_SWAP_V2.function("spendErc1155MakerPayment")?, + ContractType::Erc721 => NFT_MAKER_SWAP_V2.function("spendErc721MakerPayment")?, + }; + + if state != U256::from(MakerPaymentStateV2::PaymentSent as u8) { + return Err(PrepareTxDataError::Internal(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + args.maker_payment_tx, + state + ))); + } + + let input_tokens = match args.contract_type { + ContractType::Erc1155 => vec![ + htlc_params[0].clone(), // swap_id + Token::Address(args.maker_payment_tx.sender()), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret.to_vec()), + htlc_params[2].clone(), // tokenAddress + decoded[2].clone(), // tokenId + decoded[3].clone(), // amount + ], + ContractType::Erc721 => vec![ + htlc_params[0].clone(), // swap_id + Token::Address(args.maker_payment_tx.sender()), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret.to_vec()), + htlc_params[2].clone(), // tokenAddress + decoded[2].clone(), // tokenId + ], + }; + + let data = spend_func.encode_input(&input_tokens)?; + Ok(data) + } + + async fn status_and_htlc_params_from_tx_data( + &self, + swap_address: Address, + contract_abi: &Contract, + decoded_data: &[Token], + index: usize, + state_type: PaymentType, + ) -> Result<(U256, Vec), PaymentStatusErr> { + let data_bytes = match decoded_data.get(index) { + Some(Token::Bytes(data_bytes)) => data_bytes, + _ => { + return Err(PaymentStatusErr::TxDeserializationError(ERRL!( + "Failed to decode HTLCParams from data_bytes" + ))) + }, + }; + + let htlc_params = match ethabi::decode(htlc_params(), data_bytes) { + Ok(htlc_params) => htlc_params, + Err(_) => { + return Err(PaymentStatusErr::TxDeserializationError(ERRL!( + "Failed to decode HTLCParams from data_bytes" + ))) + }, + }; + + let state = self + .payment_status_v2(swap_address, htlc_params[0].clone(), contract_abi, state_type) + .await?; + + Ok((state, htlc_params)) + } +} + +/// Validates decoded data from tx input, related to `safeTransferFrom` contract call +fn validate_decoded_data(decoded: &[Token], params: &ValidationParams) -> Result<(), MmError> { + let checks = vec![ + (0, Token::Address(params.maker_address), "maker_address"), + (1, Token::Address(params.etomic_swap_contract), "etomic_swap_contract"), + (2, Token::Uint(U256::from(params.token_id)), "token_id"), + ]; + + for (index, expected_token, field_name) in checks { + if decoded.get(index) != Some(&expected_token) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "NFT Maker Payment `{}` {:?} is invalid, expected {:?}", + field_name, + decoded.get(index), + expected_token + ))); + } + } + if let Some(amount) = ¶ms.amount { + let value = U256::from_dec_str(amount).map_to_mm(|e| ValidatePaymentError::InternalError(e.to_string()))?; + if decoded[3] != Token::Uint(value) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "NFT Maker Payment `amount` {:?} is invalid, expected {:?}", + decoded[3], + Token::Uint(value) + ))); + } + } + Ok(()) +} + +fn decode_and_validate_htlc_params( + decoded: Vec, + index: usize, + expected_params: ExpectedHtlcParams, +) -> MmResult<(), HtlcParamsError> { + let data_bytes = match decoded.get(index) { + Some(Token::Bytes(bytes)) => bytes, + _ => { + return MmError::err(HtlcParamsError::TxDeserializationError( + "Expected Bytes for HTLCParams data".to_string(), + )) + }, + }; + + let decoded_params = match ethabi::decode(htlc_params(), data_bytes) { + Ok(params) => params, + Err(_) => { + return MmError::err(HtlcParamsError::TxDeserializationError( + "Failed to decode HTLCParams from data_bytes".to_string(), + )) + }, + }; + + let expected_taker_secret_hash = Token::FixedBytes(expected_params.taker_secret_hash.clone()); + let expected_maker_secret_hash = Token::FixedBytes(expected_params.maker_secret_hash.clone()); + + let checks = vec![ + (0, Token::FixedBytes(expected_params.swap_id.clone()), "swap_id"), + (1, Token::Address(expected_params.taker_address), "taker_address"), + (2, Token::Address(expected_params.token_address), "token_address"), + (3, expected_taker_secret_hash, "taker_secret_hash"), + (4, expected_maker_secret_hash, "maker_secret_hash"), + (5, Token::Uint(expected_params.time_lock), "time_lock"), + ]; + + for (index, expected_token, param_name) in checks.into_iter() { + if decoded_params[index] != expected_token { + return MmError::err(HtlcParamsError::WrongPaymentTx(format!( + "Invalid '{}' {:?}, expected {:?}", + param_name, decoded_params[index], expected_token + ))); + } + } + + Ok(()) +} + +/// Representation of the Solidity HTLCParams struct. +/// +/// struct HTLCParams { +/// bytes32 id; +/// address taker; +/// address tokenAddress; +/// bytes32 takerSecretHash; +/// bytes32 makerSecretHash; +/// uint32 paymentLockTime; +/// } +fn htlc_params() -> &'static [ethabi::ParamType] { + &[ + ethabi::ParamType::FixedBytes(32), + ethabi::ParamType::Address, + ethabi::ParamType::Address, + ethabi::ParamType::FixedBytes(32), + ethabi::ParamType::FixedBytes(32), + ethabi::ParamType::Uint(256), + ] +} + +/// function to check if BigDecimal is a positive integer +#[inline(always)] +fn is_positive_integer(amount: &BigDecimal) -> bool { amount == &amount.with_scale(0) && amount > &BigDecimal::from(0) } + +fn validate_payment_args<'a>( + taker_secret_hash: &'a [u8], + maker_secret_hash: &'a [u8], + amount: &BigDecimal, + contract_type: &ContractType, +) -> Result<(), String> { + match contract_type { + ContractType::Erc1155 => { + if !is_positive_integer(amount) { + return Err("ERC-1155 amount must be a positive integer".to_string()); + } + }, + ContractType::Erc721 => { + if amount != &BigDecimal::from(1) { + return Err("ERC-721 amount must be 1".to_string()); + } + }, + } + if taker_secret_hash.len() != 32 { + return Err("taker_secret_hash must be 32 bytes".to_string()); + } + if maker_secret_hash.len() != 32 { + return Err("maker_secret_hash must be 32 bytes".to_string()); + } + + Ok(()) +} + +async fn validate_from_to_and_maker_status( + tx_from_rpc: &Web3Tx, + expected_from: Address, + expected_to: Address, + maker_status: U256, +) -> ValidatePaymentResult<()> { + if maker_status != U256::from(MakerPaymentStateV2::PaymentSent as u8) { + return MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( + "NFT Maker Payment state is not PAYMENT_STATE_SENT, got {}", + maker_status + ))); + } + if tx_from_rpc.from != Some(expected_from) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "NFT Maker Payment tx {:?} was sent from wrong address, expected {:?}", + tx_from_rpc, expected_from + ))); + } + // As NFT owner calls "safeTransferFrom" directly, then in Transaction 'to' field we expect token_address + if tx_from_rpc.to != Some(expected_to) { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "NFT Maker Payment tx {:?} was sent to wrong address, expected {:?}", + tx_from_rpc, expected_to, + ))); + } + Ok(()) +} + +/// Identifies the correct "safeTransferFrom" function based on the contract type (either ERC1155 or ERC721) +/// and decodes the provided contract call bytes using the ABI of the identified function. Additionally, it returns +/// the index position of the "bytes" field within the function's parameters. +pub(crate) fn get_decoded_tx_data_and_index_bytes( + contract_type: &ContractType, + contract_call_bytes: &[u8], +) -> Result<(Vec, usize), PrepareTxDataError> { + let (send_func, index_bytes) = match contract_type { + ContractType::Erc1155 => (ERC1155_CONTRACT.function("safeTransferFrom")?, 4), + ContractType::Erc721 => (erc721_transfer_with_data()?, 3), + }; + let decoded = decode_contract_call(send_func, contract_call_bytes)?; + Ok((decoded, index_bytes)) +} + +/// ERC721 contract has overloaded versions of the `safeTransferFrom` function, +/// but `Contract::function` method returns only the first if there are overloaded versions of the same function. +/// Provided function retrieves the `safeTransferFrom` variant that includes a `bytes` parameter. +/// This variant is specifically used for transferring ERC721 tokens with additional data. +fn erc721_transfer_with_data<'a>() -> Result<&'a ethabi::Function, Erc721FunctionError> { + let functions = ERC721_CONTRACT + .functions_by_name("safeTransferFrom") + .map_err(|e| Erc721FunctionError::AbiError(ERRL!("{}", e)))?; + + // Find the correct function variant by inspecting the input parameters. + let function = functions + .iter() + .find(|f| { + f.inputs.len() == 4 + && matches!( + f.inputs.last().map(|input| &input.kind), + Some(ðabi::ParamType::Bytes) + ) + }) + .ok_or_else(|| { + Erc721FunctionError::FunctionNotFound( + "Failed to find the correct safeTransferFrom function variant".to_string(), + ) + })?; + Ok(function) +} diff --git a/mm2src/coins/eth/nft_swap_v2/structs.rs b/mm2src/coins/eth/nft_swap_v2/structs.rs new file mode 100644 index 0000000000..a0c129bf4b --- /dev/null +++ b/mm2src/coins/eth/nft_swap_v2/structs.rs @@ -0,0 +1,33 @@ +use ethereum_types::{Address, U256}; + +pub(crate) struct ExpectedHtlcParams { + pub(crate) swap_id: Vec, + pub(crate) taker_address: Address, + pub(crate) token_address: Address, + pub(crate) taker_secret_hash: Vec, + pub(crate) maker_secret_hash: Vec, + pub(crate) time_lock: U256, +} + +pub(crate) struct ValidationParams<'a> { + pub(crate) maker_address: Address, + pub(crate) etomic_swap_contract: Address, + pub(crate) token_id: &'a [u8], + // Optional, as it's not needed for ERC721 + pub(crate) amount: Option, +} + +#[allow(dead_code)] +pub(crate) enum PaymentType { + MakerPayments, + TakerPayments, +} + +impl PaymentType { + pub(crate) fn as_str(&self) -> &'static str { + match self { + PaymentType::MakerPayments => "makerPayments", + PaymentType::TakerPayments => "takerPayments", + } + } +} diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index fddf8da03f..e8bb6cebeb 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -1,11 +1,21 @@ use super::*; +use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDPathAccountToAddressId, HDWalletCoinStorage, + HDWalletStorageError, DEFAULT_GAP_LIMIT}; +use crate::nft::get_nfts_for_activation; +use crate::nft::nft_errors::{GetNftInfoError, ParseChainTypeError}; +use crate::nft::nft_structs::Chain; #[cfg(target_arch = "wasm32")] use crate::EthMetamaskPolicy; use common::executor::AbortedError; -use crypto::{CryptoCtxError, StandardHDCoinAddress}; -use enum_from::EnumFromTrait; +use crypto::{trezor::TrezorError, Bip32Error, CryptoCtxError, HwError}; +use enum_derives::EnumFromTrait; +use instant::Instant; use mm2_err_handle::common_errors::WithInternal; #[cfg(target_arch = "wasm32")] use mm2_metamask::{from_metamask_error, MetamaskError, MetamaskRpcError, WithMetamaskRpcError}; +use rpc_task::RpcTaskError; +use std::sync::atomic::Ordering; +use url::Url; +use web3_transport::websocket_transport::WebsocketTransport; #[derive(Clone, Debug, Deserialize, Display, EnumFromTrait, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -13,9 +23,9 @@ pub enum EthActivationV2Error { InvalidPayload(String), InvalidSwapContractAddr(String), InvalidFallbackSwapContract(String), - #[display(fmt = "Expected either 'chain_id' or 'rpc_chain_id' to be set")] - #[cfg(target_arch = "wasm32")] - ExpectedRpcChainId, + InvalidPathToAddress(String), + #[display(fmt = "`chain_id` should be set for evm coins or tokens")] + ChainIdNotSet, #[display(fmt = "Platform coin {} activation failed. {}", ticker, error)] ActivationFailed { ticker: String, @@ -28,6 +38,9 @@ pub enum EthActivationV2Error { #[display(fmt = "Error deserializing 'derivation_path': {}", _0)] ErrorDeserializingDerivationPath(String), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + #[display(fmt = "Failed spawning balance events. Error: {_0}")] + FailedSpawningBalanceEvents(String), + HDWalletStorageError(String), #[cfg(target_arch = "wasm32")] #[from_trait(WithMetamaskRpcError::metamask_rpc_error)] #[display(fmt = "{}", _0)] @@ -35,6 +48,17 @@ pub enum EthActivationV2Error { #[from_trait(WithInternal::internal)] #[display(fmt = "Internal: {}", _0)] InternalError(String), + Transport(String), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), + CoinDoesntSupportTrezor, + HwContextNotInitialized, + #[display(fmt = "Initialization task has timed out {:?}", duration)] + TaskTimedOut { + duration: Duration, + }, + HwError(HwRpcError), + #[display(fmt = "Hardware wallet must be called within rpc task framework")] + InvalidHardwareWalletCall, } impl From for EthActivationV2Error { @@ -53,34 +77,97 @@ impl From for EthActivationV2Error { fn from(e: UnexpectedDerivationMethod) -> Self { EthActivationV2Error::InternalError(e.to_string()) } } +impl From for EthActivationV2Error { + fn from(e: EthTokenActivationError) -> Self { + match e { + EthTokenActivationError::InternalError(err) => EthActivationV2Error::InternalError(err), + EthTokenActivationError::CouldNotFetchBalance(err) => EthActivationV2Error::CouldNotFetchBalance(err), + EthTokenActivationError::InvalidPayload(err) => EthActivationV2Error::InvalidPayload(err), + EthTokenActivationError::Transport(err) | EthTokenActivationError::ClientConnectionFailed(err) => { + EthActivationV2Error::Transport(err) + }, + EthTokenActivationError::UnexpectedDerivationMethod(err) => { + EthActivationV2Error::UnexpectedDerivationMethod(err) + }, + EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => EthActivationV2Error::PrivKeyPolicyNotAllowed(e), + } + } +} + +impl From for EthActivationV2Error { + fn from(e: HDWalletStorageError) -> Self { EthActivationV2Error::HDWalletStorageError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: HwError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: Bip32Error) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: TrezorError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(rpc_err: RpcTaskError) -> Self { + match rpc_err { + RpcTaskError::Timeout(duration) => EthActivationV2Error::TaskTimedOut { duration }, + internal_error => EthActivationV2Error::InternalError(internal_error.to_string()), + } + } +} + #[cfg(target_arch = "wasm32")] impl From for EthActivationV2Error { fn from(e: MetamaskError) -> Self { from_metamask_error(e) } } +impl From for EthActivationV2Error { + fn from(e: ParseChainTypeError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } +} + +impl From for EthActivationV2Error { + fn from(e: String) -> Self { EthActivationV2Error::InternalError(e) } +} + +impl From for EthActivationV2Error { + fn from(e: EnableCoinBalanceError) -> Self { + match e { + EnableCoinBalanceError::NewAddressDerivingError(err) => { + EthActivationV2Error::InternalError(err.to_string()) + }, + EnableCoinBalanceError::NewAccountCreationError(err) => { + EthActivationV2Error::InternalError(err.to_string()) + }, + EnableCoinBalanceError::BalanceError(err) => EthActivationV2Error::CouldNotFetchBalance(err.to_string()), + } + } +} + /// An alternative to `crate::PrivKeyActivationPolicy`, typical only for ETH coin. -#[derive(Clone, Deserialize)] +#[derive(Clone, Deserialize, Default)] pub enum EthPrivKeyActivationPolicy { + #[default] ContextPrivKey, + Trezor, #[cfg(target_arch = "wasm32")] Metamask, } -impl Default for EthPrivKeyActivationPolicy { - fn default() -> Self { EthPrivKeyActivationPolicy::ContextPrivKey } +impl EthPrivKeyActivationPolicy { + pub fn is_hw_policy(&self) -> bool { matches!(self, EthPrivKeyActivationPolicy::Trezor) } } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Default)] pub enum EthRpcMode { - Http, + #[default] + Default, #[cfg(target_arch = "wasm32")] Metamask, } -impl Default for EthRpcMode { - fn default() -> Self { EthRpcMode::Http } -} - #[derive(Clone, Deserialize)] pub struct EthActivationV2Request { #[serde(default)] @@ -91,16 +178,15 @@ pub struct EthActivationV2Request { pub fallback_swap_contract: Option
, #[serde(default)] pub contract_supports_watchers: bool, - pub gas_station_url: Option, - pub gas_station_decimals: Option, - #[serde(default)] - pub gas_station_policy: GasStationPricePolicy, pub mm2: Option, pub required_confirmations: Option, #[serde(default)] pub priv_key_policy: EthPrivKeyActivationPolicy, + #[serde(flatten)] + pub enable_params: EnabledCoinBalanceParams, #[serde(default)] - pub path_to_address: StandardHDCoinAddress, + pub path_to_address: HDPathAccountToAddressId, + pub gap_limit: Option, } #[derive(Clone, Deserialize)] @@ -110,31 +196,177 @@ pub struct EthNode { pub gui_auth: bool, } -#[derive(Serialize, SerializeErrorType)] +#[derive(Display, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] -pub enum Erc20TokenActivationError { +pub enum EthTokenActivationError { InternalError(String), + ClientConnectionFailed(String), CouldNotFetchBalance(String), + InvalidPayload(String), + Transport(String), + UnexpectedDerivationMethod(UnexpectedDerivationMethod), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), } -impl From for Erc20TokenActivationError { - fn from(e: AbortedError) -> Self { Erc20TokenActivationError::InternalError(e.to_string()) } +impl From for EthTokenActivationError { + fn from(e: AbortedError) -> Self { EthTokenActivationError::InternalError(e.to_string()) } } -impl From for Erc20TokenActivationError { +impl From for EthTokenActivationError { fn from(err: MyAddressError) -> Self { Self::InternalError(err.to_string()) } } +impl From for EthTokenActivationError { + fn from(e: UnexpectedDerivationMethod) -> Self { EthTokenActivationError::UnexpectedDerivationMethod(e) } +} + +impl From for EthTokenActivationError { + fn from(e: GetNftInfoError) -> Self { + match e { + GetNftInfoError::InvalidRequest(err) => EthTokenActivationError::InvalidPayload(err), + GetNftInfoError::ContractTypeIsNull => EthTokenActivationError::InvalidPayload( + "The contract type is required and should not be null.".to_string(), + ), + GetNftInfoError::Transport(err) | GetNftInfoError::InvalidResponse(err) => { + EthTokenActivationError::Transport(err) + }, + GetNftInfoError::Internal(err) | GetNftInfoError::DbError(err) | GetNftInfoError::NumConversError(err) => { + EthTokenActivationError::InternalError(err) + }, + GetNftInfoError::GetEthAddressError(err) => EthTokenActivationError::InternalError(err.to_string()), + GetNftInfoError::ParseRfc3339Err(err) => EthTokenActivationError::InternalError(err.to_string()), + GetNftInfoError::ProtectFromSpamError(err) => EthTokenActivationError::InternalError(err.to_string()), + GetNftInfoError::TransferConfirmationsError(err) => EthTokenActivationError::InternalError(err.to_string()), + GetNftInfoError::TokenNotFoundInWallet { + token_address, + token_id, + } => EthTokenActivationError::InternalError(format!( + "Token not found in wallet: {}, {}", + token_address, token_id + )), + } + } +} + +impl From for EthTokenActivationError { + fn from(e: ParseChainTypeError) -> Self { EthTokenActivationError::InternalError(e.to_string()) } +} + +impl From for EthTokenActivationError { + fn from(e: String) -> Self { EthTokenActivationError::InternalError(e) } +} + +impl From for EthTokenActivationError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { EthTokenActivationError::PrivKeyPolicyNotAllowed(e) } +} + +impl From for EthTokenActivationError { + fn from(e: GenerateSignedMessageError) -> Self { + match e { + GenerateSignedMessageError::InternalError(e) => EthTokenActivationError::InternalError(e), + GenerateSignedMessageError::PrivKeyPolicyNotAllowed(e) => { + EthTokenActivationError::PrivKeyPolicyNotAllowed(e) + }, + } + } +} + +#[derive(Display, Serialize)] +pub enum GenerateSignedMessageError { + #[display(fmt = "Internal: {}", _0)] + InternalError(String), + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), +} + +impl From for GenerateSignedMessageError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { GenerateSignedMessageError::PrivKeyPolicyNotAllowed(e) } +} + +impl From for GenerateSignedMessageError { + fn from(e: SignatureError) -> Self { GenerateSignedMessageError::InternalError(e.to_string()) } +} + +/// Represents the parameters required for activating either an ERC-20 token or an NFT on the Ethereum platform. +#[derive(Clone, Deserialize)] +#[serde(untagged)] +pub enum EthTokenActivationParams { + Nft(NftActivationRequest), + Erc20(Erc20TokenActivationRequest), +} + +/// Holds ERC-20 token-specific activation parameters, including optional confirmation requirements. #[derive(Clone, Deserialize)] pub struct Erc20TokenActivationRequest { pub required_confirmations: Option, } +/// Holds ERC-20 token-specific activation parameters when using the task manager for activation. +#[derive(Clone, Deserialize)] +pub struct InitErc20TokenActivationRequest { + /// The number of confirmations required for swap transactions. + pub required_confirmations: Option, + /// Parameters for HD wallet account and addresses initialization. + #[serde(flatten)] + pub enable_params: EnabledCoinBalanceParams, + /// This determines which Address of the HD account to be used for swaps for this Token. + /// If not specified, the first non-change address for the first account is used. + #[serde(default)] + pub path_to_address: HDPathAccountToAddressId, +} + +impl From for Erc20TokenActivationRequest { + fn from(req: InitErc20TokenActivationRequest) -> Self { + Erc20TokenActivationRequest { + required_confirmations: req.required_confirmations, + } + } +} + +/// Encapsulates the request parameters for NFT activation, specifying the provider to be used. +#[derive(Clone, Deserialize)] +pub struct NftActivationRequest { + pub provider: NftProviderEnum, +} + +/// Defines available NFT providers and their configuration. +#[derive(Clone, Deserialize)] +#[serde(tag = "type", content = "info")] +pub enum NftProviderEnum { + Moralis { + url: Url, + #[serde(default)] + proxy_auth: bool, + }, +} + +/// Represents the protocol type for an Ethereum-based token, distinguishing between ERC-20 tokens and NFTs. +pub enum EthTokenProtocol { + Erc20(Erc20Protocol), + Nft(NftProtocol), +} + +/// Details for an ERC-20 token protocol. +#[derive(Clone)] pub struct Erc20Protocol { pub platform: String, pub token_addr: Address, } +impl From for CoinProtocol { + fn from(erc20_protocol: Erc20Protocol) -> Self { + CoinProtocol::ERC20 { + platform: erc20_protocol.platform, + contract_address: erc20_protocol.token_addr.to_string(), + } + } +} + +/// Details for an NFT protocol. +#[derive(Debug)] +pub struct NftProtocol { + pub platform: String, +} + #[cfg_attr(test, mockable)] impl EthCoin { pub async fn initialize_erc20_token( @@ -142,29 +374,37 @@ impl EthCoin { activation_params: Erc20TokenActivationRequest, protocol: Erc20Protocol, ticker: String, - ) -> MmResult { + ) -> MmResult { // TODO // Check if ctx is required. // Remove it to avoid circular references if possible let ctx = MmArc::from_weak(&self.ctx) .ok_or_else(|| String::from("No context")) - .map_err(Erc20TokenActivationError::InternalError)?; + .map_err(EthTokenActivationError::InternalError)?; let conf = coin_conf(&ctx, &ticker); let decimals = match conf["decimals"].as_u64() { - None | Some(0) => get_token_decimals(&self.web3, protocol.token_addr) - .await - .map_err(Erc20TokenActivationError::InternalError)?, + None | Some(0) => get_token_decimals( + &self + .web3() + .await + .map_err(|e| EthTokenActivationError::ClientConnectionFailed(e.to_string()))?, + protocol.token_addr, + ) + .await + .map_err(EthTokenActivationError::InternalError)?, Some(d) => d as u8, }; let web3_instances: Vec = self .web3_instances + .lock() + .await .iter() .map(|node| { let mut transport = node.web3.transport().clone(); - if let Some(auth) = transport.gui_auth_validation_generator_as_mut() { + if let Some(auth) = transport.proxy_auth_validation_generator_as_mut() { auth.coin_ticker = ticker.clone(); } let web3 = Web3::new(transport); @@ -175,12 +415,6 @@ impl EthCoin { }) .collect(); - let mut transport = self.web3.transport().clone(); - if let Some(auth) = transport.gui_auth_validation_generator_as_mut() { - auth.coin_ticker = ticker.clone(); - } - let web3 = Web3::new(transport); - let required_confirmations = activation_params .required_confirmations .unwrap_or_else(|| conf["required_confirmations"].as_u64().unwrap_or(1)) @@ -190,47 +424,151 @@ impl EthCoin { // all spawned futures related to `ERC20` coin will be aborted as well. let abortable_system = ctx.abortable_system.create_subsystem()?; + let coin_type = EthCoinType::Erc20 { + platform: protocol.platform, + token_addr: protocol.token_addr, + }; + let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; + let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; + let gas_limit = extract_gas_limit_from_conf(&conf) + .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; + let token = EthCoinImpl { priv_key_policy: self.priv_key_policy.clone(), - my_address: self.my_address, - coin_type: EthCoinType::Erc20 { - platform: protocol.platform, - token_addr: protocol.token_addr, - }, + // We inherit the derivation method from the parent/platform coin + // If we want a new wallet for each token we can add this as an option in the future + // storage ticker will be the platform coin ticker + derivation_method: self.derivation_method.clone(), + coin_type, sign_message_prefix: self.sign_message_prefix.clone(), swap_contract_address: self.swap_contract_address, fallback_swap_contract: self.fallback_swap_contract, contract_supports_watchers: self.contract_supports_watchers, decimals, ticker, - gas_station_url: self.gas_station_url.clone(), - gas_station_decimals: self.gas_station_decimals, - gas_station_policy: self.gas_station_policy.clone(), - web3, - web3_instances, + web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), + max_eth_tx_type, ctx: self.ctx.clone(), required_confirmations, chain_id: self.chain_id, + trezor_coin: self.trezor_coin.clone(), logs_block_range: self.logs_block_range, - nonce_lock: self.nonce_lock.clone(), + address_nonce_locks: self.address_nonce_locks.clone(), erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), + platform_fee_estimator_state, + gas_limit, abortable_system, }; Ok(EthCoin(Arc::new(token))) } + + /// Initializes a Global NFT instance for a specific blockchain platform (e.g., Ethereum, Polygon). + /// + /// A "Global NFT" consolidates information about all NFTs owned by a user into a single `EthCoin` instance, + /// avoiding the need for separate instances for each NFT. + /// The function configures the necessary settings for the Global NFT, including web3 connections and confirmation requirements. + /// It fetches NFT details from a given URL to populate the `nfts_infos` field, which stores information about the user's NFTs. + /// + /// This setup allows the Global NFT to function like a coin, supporting swap operations and providing easy access to NFT details via `nfts_infos`. + pub async fn global_nft_from_platform_coin( + &self, + original_url: &Url, + proxy_auth: &bool, + ) -> MmResult { + let chain = Chain::from_ticker(self.ticker())?; + let ticker = chain.to_nft_ticker().to_string(); + + let ctx = MmArc::from_weak(&self.ctx) + .ok_or_else(|| String::from("No context")) + .map_err(EthTokenActivationError::InternalError)?; + + let conf = coin_conf(&ctx, &ticker); + + // Create an abortable system linked to the `platform_coin` (which is self) so if the platform coin is disabled, + // all spawned futures related to global Non-Fungible Token will be aborted as well. + let abortable_system = self.abortable_system.create_subsystem()?; + + // Todo: support HD wallet for NFTs, currently we get nfts for enabled address only and there might be some issues when activating NFTs while ETH is activated with HD wallet + let my_address = self.derivation_method.single_addr_or_err().await?; + + let my_address_str = display_eth_address(&my_address); + let signed_message = + generate_signed_message(*proxy_auth, &chain, my_address_str, self.priv_key_policy()).await?; + + let nft_infos = get_nfts_for_activation(&chain, &my_address, original_url, signed_message.as_ref()).await?; + let coin_type = EthCoinType::Nft { + platform: self.ticker.clone(), + }; + let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; + let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; + let gas_limit = extract_gas_limit_from_conf(&conf) + .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; + + let global_nft = EthCoinImpl { + ticker, + coin_type, + priv_key_policy: self.priv_key_policy.clone(), + derivation_method: self.derivation_method.clone(), + sign_message_prefix: self.sign_message_prefix.clone(), + swap_contract_address: self.swap_contract_address, + fallback_swap_contract: self.fallback_swap_contract, + contract_supports_watchers: self.contract_supports_watchers, + web3_instances: self.web3_instances.lock().await.clone().into(), + decimals: self.decimals, + history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), + max_eth_tx_type, + required_confirmations: AtomicU64::new(self.required_confirmations.load(Ordering::Relaxed)), + ctx: self.ctx.clone(), + chain_id: self.chain_id, + trezor_coin: self.trezor_coin.clone(), + logs_block_range: self.logs_block_range, + address_nonce_locks: self.address_nonce_locks.clone(), + erc20_tokens_infos: Default::default(), + nfts_infos: Arc::new(AsyncMutex::new(nft_infos)), + platform_fee_estimator_state, + gas_limit, + abortable_system, + }; + Ok(EthCoin(Arc::new(global_nft))) + } } +pub(crate) async fn generate_signed_message( + proxy_auth: bool, + chain: &Chain, + my_address: String, + priv_key_policy: &EthPrivKeyPolicy, +) -> MmResult, GenerateSignedMessageError> { + if !proxy_auth { + return Ok(None); + } + + let secret = priv_key_policy.activated_key_or_err()?.secret().clone(); + let validation_generator = ProxyAuthValidationGenerator { + coin_ticker: chain.to_nft_ticker().to_string(), + secret, + address: my_address, + }; + + let signed_message = EthCoin::generate_proxy_auth_signed_validation(validation_generator)?; + + Ok(Some(signed_message)) +} + +/// Activate eth coin from coin config and private key build policy, +/// version 2 of the activation function, with no intrinsic tokens creation pub async fn eth_coin_from_conf_and_request_v2( ctx: &MmArc, ticker: &str, conf: &Json, req: EthActivationV2Request, - priv_key_policy: EthPrivKeyBuildPolicy, + priv_key_build_policy: EthPrivKeyBuildPolicy, ) -> MmResult { - let ticker = ticker.to_string(); - if req.swap_contract_address == Address::default() { return Err(EthActivationV2Error::InvalidSwapContractAddr( "swap_contract_address can't be zero address".to_string(), @@ -247,37 +585,57 @@ pub async fn eth_coin_from_conf_and_request_v2( } } - let (my_address, priv_key_policy) = - build_address_and_priv_key_policy(conf, priv_key_policy, &req.path_to_address).await?; - let my_address_str = checksum_address(&format!("{:02x}", my_address)); - - let chain_id = conf["chain_id"].as_u64(); - - let (web3, web3_instances) = match (req.rpc_mode, &priv_key_policy) { + let (priv_key_policy, derivation_method) = build_address_and_priv_key_policy( + ctx, + ticker, + conf, + priv_key_build_policy, + &req.path_to_address, + req.gap_limit, + ) + .await?; + + let chain_id = conf["chain_id"].as_u64().ok_or(EthActivationV2Error::ChainIdNotSet)?; + let web3_instances = match (req.rpc_mode, &priv_key_policy) { ( - EthRpcMode::Http, + EthRpcMode::Default, EthPrivKeyPolicy::Iguana(key_pair) | EthPrivKeyPolicy::HDWallet { activated_key: key_pair, .. }, - ) => build_http_transport(ctx, ticker.clone(), my_address_str, key_pair, &req.nodes).await?, - (EthRpcMode::Http, EthPrivKeyPolicy::Trezor) => { - return MmError::err(EthActivationV2Error::PrivKeyPolicyNotAllowed( - PrivKeyPolicyNotAllowed::HardwareWalletNotSupported, - )); + ) => { + let auth_address = key_pair.address(); + let auth_address_str = display_eth_address(&auth_address); + build_web3_instances(ctx, ticker.to_string(), auth_address_str, key_pair, req.nodes.clone()).await? + }, + (EthRpcMode::Default, EthPrivKeyPolicy::Trezor) => { + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let secp256k1_key_pair = crypto_ctx.mm2_internal_key_pair(); + let auth_key_pair = KeyPair::from_secret_slice(secp256k1_key_pair.private_ref()) + .map_to_mm(|_| EthActivationV2Error::InternalError("could not get internal keypair".to_string()))?; + let auth_address = auth_key_pair.address(); + let auth_address_str = display_eth_address(&auth_address); + build_web3_instances( + ctx, + ticker.to_string(), + auth_address_str, + &auth_key_pair, + req.nodes.clone(), + ) + .await? }, #[cfg(target_arch = "wasm32")] (EthRpcMode::Metamask, EthPrivKeyPolicy::Metamask(_)) => { - let chain_id = chain_id - .or_else(|| conf["rpc_chain_id"].as_u64()) - .or_mm_err(|| EthActivationV2Error::ExpectedRpcChainId)?; - build_metamask_transport(ctx, ticker.clone(), chain_id).await? + build_metamask_transport(ctx, ticker.to_string(), chain_id).await? }, #[cfg(target_arch = "wasm32")] - (EthRpcMode::Http, EthPrivKeyPolicy::Metamask(_)) | (EthRpcMode::Metamask, _) => { + (EthRpcMode::Default, EthPrivKeyPolicy::Metamask(_)) | (EthRpcMode::Metamask, _) => { let error = r#"priv_key_policy="Metamask" and rpc_mode="Metamask" should be used both"#.to_string(); - return MmError::err(EthActivationV2Error::ActivationFailed { ticker, error }); + return MmError::err(EthActivationV2Error::ActivationFailed { + ticker: ticker.to_string(), + error, + }); }, }; @@ -293,70 +651,145 @@ pub async fn eth_coin_from_conf_and_request_v2( let sign_message_prefix: Option = json::from_value(conf["sign_message_prefix"].clone()).ok(); - let mut map = NONCE_LOCK.lock().unwrap(); - let nonce_lock = map.entry(ticker.clone()).or_insert_with(new_nonce_lock).clone(); + let trezor_coin: Option = json::from_value(conf["trezor_coin"].clone()).ok(); + + let address_nonce_locks = { + let mut map = NONCE_LOCK.lock().unwrap(); + Arc::new(AsyncMutex::new( + map.entry(ticker.to_string()).or_insert_with(new_nonce_lock).clone(), + )) + }; // Create an abortable system linked to the `MmCtx` so if the app is stopped on `MmArc::stop`, // all spawned futures related to `ETH` coin will be aborted as well. let abortable_system = ctx.abortable_system.create_subsystem()?; + let coin_type = EthCoinType::Eth; + let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; + let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; + let gas_limit = extract_gas_limit_from_conf(conf) + .map_to_mm(|e| EthActivationV2Error::InternalError(format!("invalid gas_limit config {}", e)))?; let coin = EthCoinImpl { priv_key_policy, - my_address, - coin_type: EthCoinType::Eth, + derivation_method: Arc::new(derivation_method), + coin_type, sign_message_prefix, swap_contract_address: req.swap_contract_address, fallback_swap_contract: req.fallback_swap_contract, contract_supports_watchers: req.contract_supports_watchers, decimals: ETH_DECIMALS, - ticker, - gas_station_url: req.gas_station_url, - gas_station_decimals: req.gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), - gas_station_policy: req.gas_station_policy, - web3, - web3_instances, + ticker: ticker.to_string(), + web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(HistorySyncState::NotEnabled), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), + max_eth_tx_type, ctx: ctx.weak(), required_confirmations, chain_id, + trezor_coin, logs_block_range: conf["logs_block_range"].as_u64().unwrap_or(DEFAULT_LOGS_BLOCK_RANGE), - nonce_lock, + address_nonce_locks, erc20_tokens_infos: Default::default(), + nfts_infos: Default::default(), + platform_fee_estimator_state, + gas_limit, abortable_system, }; - Ok(EthCoin(Arc::new(coin))) + let coin = EthCoin(Arc::new(coin)); + coin.spawn_balance_stream_if_enabled(ctx) + .await + .map_err(EthActivationV2Error::FailedSpawningBalanceEvents)?; + + Ok(coin) } /// Processes the given `priv_key_policy` and generates corresponding `KeyPair`. /// This function expects either [`PrivKeyBuildPolicy::IguanaPrivKey`] /// or [`PrivKeyBuildPolicy::GlobalHDAccount`], otherwise returns `PrivKeyPolicyNotAllowed` error. pub(crate) async fn build_address_and_priv_key_policy( + ctx: &MmArc, + ticker: &str, conf: &Json, - priv_key_policy: EthPrivKeyBuildPolicy, - path_to_address: &StandardHDCoinAddress, -) -> MmResult<(Address, EthPrivKeyPolicy), EthActivationV2Error> { - match priv_key_policy { + priv_key_build_policy: EthPrivKeyBuildPolicy, + path_to_address: &HDPathAccountToAddressId, + gap_limit: Option, +) -> MmResult<(EthPrivKeyPolicy, EthDerivationMethod), EthActivationV2Error> { + match priv_key_build_policy { EthPrivKeyBuildPolicy::IguanaPrivKey(iguana) => { let key_pair = KeyPair::from_secret_slice(iguana.as_slice()) .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; - Ok((key_pair.address(), EthPrivKeyPolicy::Iguana(key_pair))) + let address = key_pair.address(); + let derivation_method = DerivationMethod::SingleAddress(address); + Ok((EthPrivKeyPolicy::Iguana(key_pair), derivation_method)) }, EthPrivKeyBuildPolicy::GlobalHDAccount(global_hd_ctx) => { // Consider storing `derivation_path` at `EthCoinImpl`. - let derivation_path = json::from_value(conf["derivation_path"].clone()) + let path_to_coin = json::from_value(conf["derivation_path"].clone()) .map_to_mm(|e| EthActivationV2Error::ErrorDeserializingDerivationPath(e.to_string()))?; let raw_priv_key = global_hd_ctx - .derive_secp256k1_secret(&derivation_path, path_to_address) + .derive_secp256k1_secret( + &path_to_address + .to_derivation_path(&path_to_coin) + .mm_err(|e| EthActivationV2Error::InvalidPathToAddress(e.to_string()))?, + ) .mm_err(|e| EthActivationV2Error::InternalError(e.to_string()))?; - let activated_key_pair = KeyPair::from_secret_slice(raw_priv_key.as_slice()) + let activated_key = KeyPair::from_secret_slice(raw_priv_key.as_slice()) .map_to_mm(|e| EthActivationV2Error::InternalError(e.to_string()))?; let bip39_secp_priv_key = global_hd_ctx.root_priv_key().clone(); - Ok((activated_key_pair.address(), EthPrivKeyPolicy::HDWallet { - derivation_path, - activated_key: activated_key_pair, - bip39_secp_priv_key, - })) + + let hd_wallet_rmd160 = *ctx.rmd160(); + let hd_wallet_storage = HDWalletCoinStorage::init_with_rmd160(ctx, ticker.to_string(), hd_wallet_rmd160) + .await + .mm_err(EthActivationV2Error::from)?; + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, &path_to_coin).await?; + let gap_limit = gap_limit.unwrap_or(DEFAULT_GAP_LIMIT); + let hd_wallet = EthHDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin.clone(), + accounts: HDAccountsMutex::new(accounts), + enabled_address: *path_to_address, + gap_limit, + }; + let derivation_method = DerivationMethod::HDWallet(hd_wallet); + Ok(( + EthPrivKeyPolicy::HDWallet { + path_to_coin, + activated_key, + bip39_secp_priv_key, + }, + derivation_method, + )) + }, + EthPrivKeyBuildPolicy::Trezor => { + let path_to_coin = json::from_value(conf["derivation_path"].clone()) + .map_to_mm(|e| EthActivationV2Error::ErrorDeserializingDerivationPath(e.to_string()))?; + + let trezor_coin: Option = json::from_value(conf["trezor_coin"].clone()).ok(); + if trezor_coin.is_none() { + return MmError::err(EthActivationV2Error::CoinDoesntSupportTrezor); + } + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| EthActivationV2Error::HwContextNotInitialized)?; + let hd_wallet_rmd160 = hw_ctx.rmd160(); + let hd_wallet_storage = HDWalletCoinStorage::init_with_rmd160(ctx, ticker.to_string(), hd_wallet_rmd160) + .await + .mm_err(EthActivationV2Error::from)?; + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, &path_to_coin).await?; + let gap_limit = gap_limit.unwrap_or(DEFAULT_GAP_LIMIT); + let hd_wallet = EthHDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin.clone(), + accounts: HDAccountsMutex::new(accounts), + enabled_address: *path_to_address, + gap_limit, + }; + let derivation_method = DerivationMethod::HDWallet(hd_wallet); + Ok((EthPrivKeyPolicy::Trezor, derivation_method)) }, #[cfg(target_arch = "wasm32")] EthPrivKeyBuildPolicy::Metamask(metamask_ctx) => { @@ -364,68 +797,104 @@ pub(crate) async fn build_address_and_priv_key_policy( let public_key_uncompressed = metamask_ctx.eth_account_pubkey_uncompressed(); let public_key = compress_public_key(public_key_uncompressed)?; Ok(( - address, EthPrivKeyPolicy::Metamask(EthMetamaskPolicy { public_key, public_key_uncompressed, }), + DerivationMethod::SingleAddress(address), )) }, } } -async fn build_http_transport( +async fn build_web3_instances( ctx: &MmArc, coin_ticker: String, address: String, key_pair: &KeyPair, - eth_nodes: &[EthNode], -) -> MmResult<(Web3, Vec), EthActivationV2Error> { + mut eth_nodes: Vec, +) -> MmResult, EthActivationV2Error> { if eth_nodes.is_empty() { return MmError::err(EthActivationV2Error::AtLeastOneNodeRequired); } - let mut http_nodes = vec![]; - for node in eth_nodes { - let uri = node + let mut rng = small_rng(); + eth_nodes.as_mut_slice().shuffle(&mut rng); + drop_mutability!(eth_nodes); + + let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); + + let mut web3_instances = Vec::with_capacity(eth_nodes.len()); + for eth_node in eth_nodes { + let uri: Uri = eth_node .url .parse() - .map_err(|_| EthActivationV2Error::InvalidPayload(format!("{} could not be parsed.", node.url)))?; + .map_err(|_| EthActivationV2Error::InvalidPayload(format!("{} could not be parsed.", eth_node.url)))?; - http_nodes.push(HttpTransportNode { - uri, - gui_auth: node.gui_auth, - }); - } + let transport = match uri.scheme_str() { + Some("ws") | Some("wss") => { + const TMP_SOCKET_CONNECTION: Duration = Duration::from_secs(20); - let mut rng = small_rng(); - http_nodes.as_mut_slice().shuffle(&mut rng); + let node = WebsocketTransportNode { + uri: uri.clone(), + gui_auth: eth_node.gui_auth, + }; - drop_mutability!(http_nodes); + let mut websocket_transport = WebsocketTransport::with_event_handlers(node, event_handlers.clone()); - let mut web3_instances = Vec::with_capacity(http_nodes.len()); - let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); - for node in http_nodes.iter() { - let transport = build_single_http_transport( - coin_ticker.clone(), - address.clone(), - key_pair, - vec![node.clone()], - event_handlers.clone(), - ); + if eth_node.gui_auth { + websocket_transport.proxy_auth_validation_generator = Some(ProxyAuthValidationGenerator { + coin_ticker: coin_ticker.clone(), + secret: key_pair.secret().clone(), + address: address.clone(), + }); + } + + // Temporarily start the connection loop (we close the connection once we have the client version below). + // Ideally, it would be much better to not do this workaround, which requires a lot of refactoring or + // dropping websocket support on parity nodes. + let fut = websocket_transport + .clone() + .start_connection_loop(Some(Instant::now() + TMP_SOCKET_CONNECTION)); + let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", uri)); + ctx.spawner().spawn_with_settings(fut, settings); + + Web3Transport::Websocket(websocket_transport) + }, + Some("http") | Some("https") => { + let node = HttpTransportNode { + uri, + gui_auth: eth_node.gui_auth, + }; + + build_http_transport( + coin_ticker.clone(), + address.clone(), + key_pair, + node, + event_handlers.clone(), + ) + }, + _ => { + return MmError::err(EthActivationV2Error::InvalidPayload(format!( + "Invalid node address '{uri}'. Only http(s) and ws(s) nodes are supported" + ))); + }, + }; let web3 = Web3::new(transport); let version = match web3.web3().client_version().await { Ok(v) => v, Err(e) => { - error!("Couldn't get client version for url {}: {}", node.uri, e); + error!("Couldn't get client version for url {}: {}", eth_node.url, e); continue; }, }; + web3_instances.push(Web3Instance { web3, is_parity: version.contains("Parity") || version.contains("parity"), - }) + }); } if web3_instances.is_empty() { @@ -434,27 +903,28 @@ async fn build_http_transport( ); } - let transport = build_single_http_transport(coin_ticker, address, key_pair, http_nodes, event_handlers); - let web3 = Web3::new(transport); - - Ok((web3, web3_instances)) + Ok(web3_instances) } -fn build_single_http_transport( +fn build_http_transport( coin_ticker: String, address: String, key_pair: &KeyPair, - nodes: Vec, + node: HttpTransportNode, event_handlers: Vec, ) -> Web3Transport { use crate::eth::web3_transport::http_transport::HttpTransport; - let mut http_transport = HttpTransport::with_event_handlers(nodes, event_handlers); - http_transport.gui_auth_validation_generator = Some(GuiAuthValidationGenerator { - coin_ticker, - secret: key_pair.secret().clone(), - address, - }); + let gui_auth = node.gui_auth; + let mut http_transport = HttpTransport::with_event_handlers(node, event_handlers); + + if gui_auth { + http_transport.proxy_auth_validation_generator = Some(ProxyAuthValidationGenerator { + coin_ticker, + secret: key_pair.secret().clone(), + address, + }); + } Web3Transport::from(http_transport) } @@ -463,11 +933,14 @@ async fn build_metamask_transport( ctx: &MmArc, coin_ticker: String, chain_id: u64, -) -> MmResult<(Web3, Vec), EthActivationV2Error> { +) -> MmResult, EthActivationV2Error> { let event_handlers = rpc_event_handlers_for_eth_transport(ctx, coin_ticker.clone()); let eth_config = web3_transport::metamask_transport::MetamaskEthConfig { chain_id }; - let web3 = Web3::new(Web3Transport::new_metamask(eth_config, event_handlers)?); + let web3 = Web3::new(Web3Transport::new_metamask_with_event_handlers( + eth_config, + event_handlers, + )?); // Check if MetaMask supports the given `chain_id`. // Please note that this request may take a long time. @@ -475,12 +948,9 @@ async fn build_metamask_transport( // MetaMask doesn't use Parity nodes. So `MetamaskTransport` doesn't support `parity_nextNonce` RPC. // An example of the `web3_clientVersion` RPC - `MetaMask/v10.22.1`. - let web3_instances = vec![Web3Instance { - web3: web3.clone(), - is_parity: false, - }]; + let web3_instances = vec![Web3Instance { web3, is_parity: false }]; - Ok((web3, web3_instances)) + Ok(web3_instances) } /// This method is based on the fact that `MetamaskTransport` tries to switch the `ChainId` @@ -499,7 +969,7 @@ async fn check_metamask_supports_chain_id( match web3.eth().chain_id().await { Ok(chain_id) if chain_id == U256::from(expected_chain_id) => Ok(()), - // The RPC client should have returned ChainId with which it has been created on [`Web3Transport::new_metamask`]. + // The RPC client should have returned ChainId with which it has been created on [`Web3Transport::new_metamask_with_event_handlers`]. Ok(unexpected_chain_id) => { let error = format!("Expected '{expected_chain_id}' ChainId, found '{unexpected_chain_id}'"); MmError::err(EthActivationV2Error::InternalError(error)) diff --git a/mm2src/coins/eth/web3_transport/http_transport.rs b/mm2src/coins/eth/web3_transport/http_transport.rs index 997ff034d7..463e2455fa 100644 --- a/mm2src/coins/eth/web3_transport/http_transport.rs +++ b/mm2src/coins/eth/web3_transport/http_transport.rs @@ -1,29 +1,27 @@ -use crate::eth::{web3_transport::Web3SendOut, EthCoin, GuiAuthMessages, RpcTransportEventHandler, - RpcTransportEventHandlerShared, Web3RpcError}; +use crate::eth::web3_transport::handle_quicknode_payload; +use crate::eth::{web3_transport::Web3SendOut, RpcTransportEventHandler, RpcTransportEventHandlerShared, Web3RpcError}; use common::APPLICATION_JSON; -use futures::lock::Mutex as AsyncMutex; use http::header::CONTENT_TYPE; use jsonrpc_core::{Call, Response}; -use mm2_net::transport::{GuiAuthValidation, GuiAuthValidationGenerator}; +use mm2_net::transport::{KomodefiProxyAuthValidation, ProxyAuthValidationGenerator}; use serde_json::Value as Json; -#[cfg(not(target_arch = "wasm32"))] use std::ops::Deref; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use web3::error::{Error, TransportError}; use web3::helpers::{build_request, to_result_from_output, to_string}; use web3::{RequestId, Transport}; -#[derive(Serialize, Clone)] -pub struct AuthPayload<'a> { +#[derive(Clone, Serialize)] +pub struct QuicknodePayload<'a> { #[serde(flatten)] pub request: &'a Call, - pub signed_message: GuiAuthValidation, + pub signed_message: KomodefiProxyAuthValidation, } -/// Parse bytes RPC response into `Result`. +/// Deserialize bytes RPC response into `Result`. /// Implementation copied from Web3 HTTP transport -#[cfg(not(target_arch = "wasm32"))] -fn single_response(response: T, rpc_url: &str) -> Result +pub(crate) fn de_rpc_response(response: T, rpc_url: &str) -> Result where T: Deref + std::fmt::Debug, { @@ -42,20 +40,13 @@ where } } -#[derive(Debug)] -struct HttpTransportRpcClient(AsyncMutex); - -#[derive(Debug)] -struct HttpTransportRpcClientImpl { - nodes: Vec, -} - #[derive(Clone, Debug)] pub struct HttpTransport { id: Arc, - client: Arc, + pub(crate) last_request_failed: Arc, + node: HttpTransportNode, event_handlers: Vec, - pub(crate) gui_auth_validation_generator: Option, + pub(crate) proxy_auth_validation_generator: Option, } #[derive(Clone, Debug)] @@ -65,45 +56,26 @@ pub struct HttpTransportNode { } impl HttpTransport { - #[cfg(test)] #[inline] - pub fn new(nodes: Vec) -> Self { - let client_impl = HttpTransportRpcClientImpl { nodes }; + #[cfg(all(test, not(target_arch = "wasm32")))] + pub fn new(node: HttpTransportNode) -> Self { HttpTransport { id: Arc::new(AtomicUsize::new(0)), - client: Arc::new(HttpTransportRpcClient(AsyncMutex::new(client_impl))), + node, event_handlers: Default::default(), - gui_auth_validation_generator: None, + proxy_auth_validation_generator: None, + last_request_failed: Arc::new(AtomicBool::new(false)), } } #[inline] - pub fn with_event_handlers( - nodes: Vec, - event_handlers: Vec, - ) -> Self { - let client_impl = HttpTransportRpcClientImpl { nodes }; + pub fn with_event_handlers(node: HttpTransportNode, event_handlers: Vec) -> Self { HttpTransport { id: Arc::new(AtomicUsize::new(0)), - client: Arc::new(HttpTransportRpcClient(AsyncMutex::new(client_impl))), + node, event_handlers, - gui_auth_validation_generator: None, - } - } - - #[allow(dead_code)] - pub fn single_node(url: &'static str, gui_auth: bool) -> Self { - let nodes = vec![HttpTransportNode { - uri: url.parse().unwrap(), - gui_auth, - }]; - let client_impl = HttpTransportRpcClientImpl { nodes }; - - HttpTransport { - id: Arc::new(AtomicUsize::new(0)), - client: Arc::new(HttpTransportRpcClient(AsyncMutex::new(client_impl))), - event_handlers: Default::default(), - gui_auth_validation_generator: None, + proxy_auth_validation_generator: None, + last_request_failed: Arc::new(AtomicBool::new(false)), } } } @@ -119,72 +91,14 @@ impl Transport for HttpTransport { } #[cfg(not(target_arch = "wasm32"))] - fn send(&self, _id: RequestId, request: Call) -> Self::Out { - Box::pin(send_request( - request, - self.client.clone(), - self.event_handlers.clone(), - self.gui_auth_validation_generator.clone(), - )) - } + fn send(&self, _id: RequestId, request: Call) -> Self::Out { Box::pin(send_request(request, self.clone())) } #[cfg(target_arch = "wasm32")] - fn send(&self, _id: RequestId, request: Call) -> Self::Out { - Box::pin(send_request( - request, - self.client.clone(), - self.event_handlers.clone(), - self.gui_auth_validation_generator.clone(), - )) - } -} - -/// Generates a signed message and inserts it into request -/// payload if gui_auth is activated. Returns false on errors. -fn handle_gui_auth_payload_if_activated( - gui_auth_validation_generator: &Option, - node: &HttpTransportNode, - request: &Call, -) -> Result, Web3RpcError> { - if !node.gui_auth { - return Ok(None); - } - - let generator = match gui_auth_validation_generator.clone() { - Some(gen) => gen, - None => { - return Err(Web3RpcError::Internal(format!( - "GuiAuthValidationGenerator is not provided for {:?} node", - node - ))); - }, - }; - - let signed_message = match EthCoin::generate_gui_auth_signed_validation(generator) { - Ok(t) => t, - Err(e) => { - return Err(Web3RpcError::Internal(format!( - "GuiAuth signed message generation failed for {:?} node, error: {:?}", - node, e - ))); - }, - }; - - let auth_request = AuthPayload { - request, - signed_message, - }; - - Ok(Some(to_string(&auth_request))) + fn send(&self, _id: RequestId, request: Call) -> Self::Out { Box::pin(send_request(request, self.clone())) } } #[cfg(not(target_arch = "wasm32"))] -async fn send_request( - request: Call, - client: Arc, - event_handlers: Vec, - gui_auth_validation_generator: Option, -) -> Result { +async fn send_request(request: Call, transport: HttpTransport) -> Result { use common::executor::Timer; use common::log::warn; use futures::future::{select, Either}; @@ -194,129 +108,107 @@ async fn send_request( const REQUEST_TIMEOUT_S: f64 = 20.; - let mut errors = Vec::new(); - - let serialized_request = to_string(&request); + let mut serialized_request = to_string(&request); - let mut client_impl = client.0.lock().await; - - for (i, node) in client_impl.nodes.clone().iter().enumerate() { - let serialized_request = - match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, node, &request) { - Ok(Some(r)) => r, - Ok(None) => serialized_request.clone(), - Err(e) => { - errors.push(e); - continue; - }, - }; - - event_handlers.on_outgoing_request(serialized_request.as_bytes()); - - let mut req = http::Request::new(serialized_request.into_bytes()); - *req.method_mut() = http::Method::POST; - *req.uri_mut() = node.uri.clone(); - req.headers_mut() - .insert(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON)); - let timeout = Timer::sleep(REQUEST_TIMEOUT_S); - let req = Box::pin(slurp_req(req)); - let rc = select(req, timeout).await; - let res = match rc { - Either::Left((r, _t)) => r, - Either::Right((_t, _r)) => { - let (method, id) = match &request { - Call::MethodCall(m) => (m.method.clone(), m.id.clone()), - Call::Notification(n) => (n.method.clone(), jsonrpc_core::Id::Null), - Call::Invalid { id } => ("Invalid call".to_string(), id.clone()), - }; - let error = format!( - "Error requesting '{}': {}s timeout expired, method: '{}', id: {:?}", - node.uri, REQUEST_TIMEOUT_S, method, id - ); - warn!("{}", error); - errors.push(Web3RpcError::Transport(error)); - continue; + if transport.node.gui_auth { + match handle_quicknode_payload(&transport.proxy_auth_validation_generator, &request) { + Ok(r) => serialized_request = r, + Err(e) => { + return Err(request_failed_error(request, e)); }, }; + } - let (status, _headers, body) = match res { - Ok(r) => r, - Err(err) => { - errors.push(Web3RpcError::Transport(err.to_string())); - continue; - }, - }; + transport + .event_handlers + .on_outgoing_request(serialized_request.as_bytes()); + + let mut req = http::Request::new(serialized_request.into_bytes()); + *req.method_mut() = http::Method::POST; + *req.uri_mut() = transport.node.uri.clone(); + req.headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON)); + let timeout = Timer::sleep(REQUEST_TIMEOUT_S); + let req = Box::pin(slurp_req(req)); + let rc = select(req, timeout).await; + let res = match rc { + Either::Left((r, _t)) => r, + Either::Right((_t, _r)) => { + let (method, id) = match &request { + Call::MethodCall(m) => (m.method.clone(), m.id.clone()), + Call::Notification(n) => (n.method.clone(), jsonrpc_core::Id::Null), + Call::Invalid { id } => ("Invalid call".to_string(), id.clone()), + }; + let error = format!( + "Error requesting '{}': {}s timeout expired, method: '{}', id: {:?}", + transport.node.uri, REQUEST_TIMEOUT_S, method, id + ); + warn!("{}", error); + return Err(request_failed_error(request, Web3RpcError::Transport(error))); + }, + }; + + let (status, _headers, body) = match res { + Ok(r) => r, + Err(err) => { + return Err(request_failed_error(request, Web3RpcError::Transport(err.to_string()))); + }, + }; - event_handlers.on_incoming_response(&body); + transport.event_handlers.on_incoming_response(&body); - if !status.is_success() { - errors.push(Web3RpcError::Transport(format!( + if !status.is_success() { + return Err(request_failed_error( + request, + Web3RpcError::Transport(format!( "Server: '{}', response !200: {}, {}", - node.uri, + transport.node.uri, status, binprint(&body, b'.') - ))); - continue; - } - - let res = match single_response(body, &node.uri.to_string()) { - Ok(r) => r, - Err(err) => { - errors.push(Web3RpcError::InvalidResponse(format!( - "Server: '{}', error: {}", - node.uri, err - ))); - continue; - }, - }; - - client_impl.nodes.rotate_left(i); - - return Ok(res); + )), + )); } - Err(request_failed_error(&request, &errors)) + let res = match de_rpc_response(body, &transport.node.uri.to_string()) { + Ok(r) => r, + Err(err) => { + return Err(request_failed_error( + request, + Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", transport.node.uri, err)), + )); + }, + }; + + Ok(res) } #[cfg(target_arch = "wasm32")] -async fn send_request( - request: Call, - client: Arc, - event_handlers: Vec, - gui_auth_validation_generator: Option, -) -> Result { - let serialized_request = to_string(&request); - - let mut errors = Vec::new(); - let mut client_impl = client.0.lock().await; - - for (i, node) in client_impl.nodes.clone().iter().enumerate() { - let serialized_request = - match handle_gui_auth_payload_if_activated(&gui_auth_validation_generator, node, &request) { - Ok(Some(r)) => r, - Ok(None) => serialized_request.clone(), - Err(e) => { - errors.push(e); - continue; - }, - }; - - match send_request_once(serialized_request, &node.uri, &event_handlers).await { - Ok(response_json) => { - client_impl.nodes.rotate_left(i); - return Ok(response_json); +async fn send_request(request: Call, transport: HttpTransport) -> Result { + let mut serialized_request = to_string(&request); + + if transport.node.gui_auth { + match handle_quicknode_payload(&transport.proxy_auth_validation_generator, &request) { + Ok(r) => serialized_request = r, + Err(e) => { + return Err(request_failed_error( + request, + Web3RpcError::Transport(format!("Server: '{}', error: {}", transport.node.uri, e)), + )); }, - Err(Error::Transport(e)) => { - errors.push(Web3RpcError::Transport(format!("Server: '{}', error: {}", node.uri, e))) - }, - Err(e) => errors.push(Web3RpcError::InvalidResponse(format!( - "Server: '{}', error: {}", - node.uri, e - ))), - } + }; } - Err(request_failed_error(&request, &errors)) + match send_request_once(serialized_request, &transport.node.uri, &transport.event_handlers).await { + Ok(response_json) => Ok(response_json), + Err(Error::Transport(e)) => Err(request_failed_error( + request, + Web3RpcError::Transport(format!("Server: '{}', error: {}", transport.node.uri, e)), + )), + Err(e) => Err(request_failed_error( + request, + Web3RpcError::InvalidResponse(format!("Server: '{}', error: {}", transport.node.uri, e)), + )), + } } #[cfg(target_arch = "wasm32")] @@ -326,7 +218,7 @@ async fn send_request_once( event_handlers: &Vec, ) -> Result { use http::header::ACCEPT; - use mm2_net::wasm_http::FetchRequest; + use mm2_net::wasm::http::FetchRequest; // account for outgoing traffic event_handlers.on_outgoing_request(request_payload.as_bytes()); @@ -360,8 +252,7 @@ async fn send_request_once( } } -fn request_failed_error(request: &Call, errors: &[Web3RpcError]) -> Error { - let errors: String = errors.iter().map(|e| format!("{:?}; ", e)).collect(); - let error = format!("request {:?} failed: {}", request, errors); +fn request_failed_error(request: Call, error: Web3RpcError) -> Error { + let error = format!("request {:?} failed: {}", request, error); Error::Transport(TransportError::Message(error)) } diff --git a/mm2src/coins/eth/web3_transport/metamask_transport.rs b/mm2src/coins/eth/web3_transport/metamask_transport.rs index 5fe71d8dc2..45f8bca74d 100644 --- a/mm2src/coins/eth/web3_transport/metamask_transport.rs +++ b/mm2src/coins/eth/web3_transport/metamask_transport.rs @@ -4,17 +4,21 @@ use jsonrpc_core::Call; use mm2_metamask::{detect_metamask_provider, Eip1193Provider, MetamaskResult, MetamaskSession}; use serde_json::Value as Json; use std::fmt; +use std::sync::atomic::AtomicBool; use std::sync::Arc; use web3::{RequestId, Transport}; -pub(crate) struct MetamaskEthConfig { +/// Configuration for working with the MetaMask wallet. +pub struct MetamaskEthConfig { /// The `ChainId` that the MetaMask wallet should be targeted on each RPC. pub chain_id: u64, } +/// Transport layer for interacting with the MetaMask wallet. #[derive(Clone)] -pub(crate) struct MetamaskTransport { +pub struct MetamaskTransport { inner: Arc, + pub(crate) last_request_failed: Arc, } struct MetamaskTransportInner { @@ -35,7 +39,10 @@ impl MetamaskTransport { eip1193, _event_handlers: event_handlers, }; - Ok(MetamaskTransport { inner: Arc::new(inner) }) + Ok(MetamaskTransport { + inner: Arc::new(inner), + last_request_failed: Arc::new(AtomicBool::new(false)), + }) } } diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index f8b7d62fbd..421c9349a8 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -1,57 +1,76 @@ -use crate::RpcTransportEventHandlerShared; use ethereum_types::U256; use futures::future::BoxFuture; use jsonrpc_core::Call; #[cfg(target_arch = "wasm32")] use mm2_metamask::MetamaskResult; -use mm2_net::transport::GuiAuthValidationGenerator; +use mm2_net::transport::ProxyAuthValidationGenerator; use serde_json::Value as Json; use serde_json::Value; -use web3::api::Namespace; -use web3::helpers::{self, CallFuture}; -use web3::types::BlockNumber; +use std::sync::atomic::Ordering; +use web3::helpers::to_string; use web3::{Error, RequestId, Transport}; +use self::http_transport::QuicknodePayload; +use super::{EthCoin, KomodoDefiAuthMessages, Web3RpcError}; +use crate::RpcTransportEventHandlerShared; + pub(crate) mod http_transport; #[cfg(target_arch = "wasm32")] pub(crate) mod metamask_transport; +pub(crate) mod websocket_transport; -type Web3SendOut = BoxFuture<'static, Result>; +pub(crate) type Web3SendOut = BoxFuture<'static, Result>; +/// The transport layer for interacting with a Web3 provider. #[derive(Clone, Debug)] -pub(crate) enum Web3Transport { +pub enum Web3Transport { Http(http_transport::HttpTransport), + Websocket(websocket_transport::WebsocketTransport), #[cfg(target_arch = "wasm32")] Metamask(metamask_transport::MetamaskTransport), } impl Web3Transport { - pub fn new_http( - nodes: Vec, + pub fn new_http_with_event_handlers( + node: http_transport::HttpTransportNode, event_handlers: Vec, ) -> Web3Transport { - http_transport::HttpTransport::with_event_handlers(nodes, event_handlers).into() + http_transport::HttpTransport::with_event_handlers(node, event_handlers).into() } #[cfg(target_arch = "wasm32")] - pub(crate) fn new_metamask( + pub(crate) fn new_metamask_with_event_handlers( eth_config: metamask_transport::MetamaskEthConfig, event_handlers: Vec, ) -> MetamaskResult { Ok(metamask_transport::MetamaskTransport::detect(eth_config, event_handlers)?.into()) } - #[cfg(test)] - pub fn with_nodes(nodes: Vec) -> Web3Transport { - http_transport::HttpTransport::new(nodes).into() + pub fn is_last_request_failed(&self) -> bool { + match self { + Web3Transport::Http(http) => http.last_request_failed.load(Ordering::SeqCst), + Web3Transport::Websocket(websocket) => websocket.last_request_failed.load(Ordering::SeqCst), + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.last_request_failed.load(Ordering::SeqCst), + } + } + + fn set_last_request_failed(&self, val: bool) { + match self { + Web3Transport::Http(http) => http.last_request_failed.store(val, Ordering::SeqCst), + Web3Transport::Websocket(websocket) => websocket.last_request_failed.store(val, Ordering::SeqCst), + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.last_request_failed.store(val, Ordering::SeqCst), + } } - #[allow(dead_code)] - pub fn single_node(url: &'static str, gui_auth: bool) -> Self { - http_transport::HttpTransport::single_node(url, gui_auth).into() + #[cfg(all(test, not(target_arch = "wasm32")))] + pub fn new_http(node: http_transport::HttpTransportNode) -> Web3Transport { + http_transport::HttpTransport::new(node).into() } - pub fn gui_auth_validation_generator_as_mut(&mut self) -> Option<&mut GuiAuthValidationGenerator> { + pub fn proxy_auth_validation_generator_as_mut(&mut self) -> Option<&mut ProxyAuthValidationGenerator> { match self { - Web3Transport::Http(http) => http.gui_auth_validation_generator.as_mut(), + Web3Transport::Http(http) => http.proxy_auth_validation_generator.as_mut(), + Web3Transport::Websocket(websocket) => websocket.proxy_auth_validation_generator.as_mut(), #[cfg(target_arch = "wasm32")] Web3Transport::Metamask(_) => None, } @@ -64,17 +83,29 @@ impl Transport for Web3Transport { fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { match self { Web3Transport::Http(http) => http.prepare(method, params), + Web3Transport::Websocket(websocket) => websocket.prepare(method, params), #[cfg(target_arch = "wasm32")] Web3Transport::Metamask(metamask) => metamask.prepare(method, params), } } fn send(&self, id: RequestId, request: Call) -> Self::Out { - match self { - Web3Transport::Http(http) => http.send(id, request), - #[cfg(target_arch = "wasm32")] - Web3Transport::Metamask(metamask) => metamask.send(id, request), - } + let selfi = self.clone(); + let fut = async move { + let result = match &selfi { + Web3Transport::Http(http) => http.send(id, request), + Web3Transport::Websocket(websocket) => websocket.send(id, request), + #[cfg(target_arch = "wasm32")] + Web3Transport::Metamask(metamask) => metamask.send(id, request), + } + .await; + + selfi.set_last_request_failed(result.is_err()); + + result + }; + + Box::pin(fut) } } @@ -82,48 +113,47 @@ impl From for Web3Transport { fn from(http: http_transport::HttpTransport) -> Self { Web3Transport::Http(http) } } +impl From for Web3Transport { + fn from(websocket: websocket_transport::WebsocketTransport) -> Self { Web3Transport::Websocket(websocket) } +} + #[cfg(target_arch = "wasm32")] impl From for Web3Transport { fn from(metamask: metamask_transport::MetamaskTransport) -> Self { Web3Transport::Metamask(metamask) } } -/// eth_feeHistory support is missing even in the latest rust-web3 -/// It's the custom namespace implementing it -#[derive(Debug, Clone)] -pub struct EthFeeHistoryNamespace { - transport: T, -} - -impl Namespace for EthFeeHistoryNamespace { - fn new(transport: T) -> Self - where - Self: Sized, - { - Self { transport } - } - - fn transport(&self) -> &T { &self.transport } -} - #[derive(Debug, Deserialize)] pub struct FeeHistoryResult { #[serde(rename = "oldestBlock")] pub oldest_block: U256, #[serde(rename = "baseFeePerGas")] pub base_fee_per_gas: Vec, + #[serde(rename = "gasUsedRatio")] + pub gas_used_ratio: Vec, + #[serde(rename = "reward")] + pub priority_rewards: Option>>, } -impl EthFeeHistoryNamespace { - pub fn eth_fee_history( - &self, - count: U256, - block: BlockNumber, - reward_percentiles: &[f64], - ) -> CallFuture { - let count = helpers::serialize(&count); - let block = helpers::serialize(&block); - let reward_percentiles = helpers::serialize(&reward_percentiles); - let params = vec![count, block, reward_percentiles]; - CallFuture::new(self.transport.execute("eth_feeHistory", params)) - } +/// Generates a signed message and inserts it into the request payload. +pub(super) fn handle_quicknode_payload( + proxy_auth_validation_generator: &Option, + request: &Call, +) -> Result { + let generator = proxy_auth_validation_generator + .clone() + .ok_or_else(|| Web3RpcError::Internal("ProxyAuthValidationGenerator is not provided for".to_string()))?; + + let signed_message = EthCoin::generate_proxy_auth_signed_validation(generator).map_err(|e| { + Web3RpcError::Internal(format!( + "KomodefiProxyAuthValidation signed message generation failed. Error: {:?}", + e + )) + })?; + + let auth_request = QuicknodePayload { + request, + signed_message, + }; + + Ok(to_string(&auth_request)) } diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs new file mode 100644 index 0000000000..951ed4d2c6 --- /dev/null +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -0,0 +1,395 @@ +//! This module offers a transport layer for managing request-response style communication with Ethereum +//! nodes using websockets that can work concurrently. +//! +//! In comparison to HTTP transport, this approach proves to be much quicker (low-latency) and consumes +//! less bandwidth. This efficiency is achieved by avoiding the handling of TCP handshakes (connection reusability) +//! for each request. + +use super::handle_quicknode_payload; +use super::http_transport::de_rpc_response; +use crate::eth::eth_rpc::ETH_RPC_REQUEST_TIMEOUT; +use crate::eth::web3_transport::Web3SendOut; +use crate::eth::{EthCoin, RpcTransportEventHandlerShared}; +use crate::{MmCoin, RpcTransportEventHandler}; +use common::executor::{AbortSettings, SpawnAbortable, Timer}; +use common::expirable_map::ExpirableMap; +use common::log; +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures::channel::oneshot; +use futures::lock::Mutex as AsyncMutex; +use futures_ticker::Ticker; +use futures_util::{FutureExt, SinkExt, StreamExt}; +use instant::{Duration, Instant}; +use jsonrpc_core::Call; +use mm2_net::transport::ProxyAuthValidationGenerator; +use std::sync::atomic::AtomicBool; +use std::sync::{atomic::{AtomicUsize, Ordering}, + Arc}; +use tokio_tungstenite_wasm::WebSocketStream; +use web3::error::{Error, TransportError}; +use web3::helpers::to_string; +use web3::{helpers::build_request, RequestId, Transport}; + +const MAX_ATTEMPTS: u32 = 3; +const SLEEP_DURATION: f64 = 1.; +const KEEPALIVE_DURATION: Duration = Duration::from_secs(10); + +#[derive(Clone, Debug)] +pub(crate) struct WebsocketTransportNode { + pub(crate) uri: http::Uri, + pub(crate) gui_auth: bool, +} + +#[derive(Clone, Debug)] +pub struct WebsocketTransport { + request_id: Arc, + pub(crate) last_request_failed: Arc, + node: WebsocketTransportNode, + event_handlers: Vec, + pub(crate) proxy_auth_validation_generator: Option, + controller_channel: Arc, + connection_guard: Arc>, +} + +#[derive(Debug)] +struct ControllerChannel { + tx: Arc>>, + rx: Arc>>, +} + +enum ControllerMessage { + Request(WsRequest), + Close, +} + +#[derive(Debug)] +struct WsRequest { + serialized_request: String, + request_id: RequestId, + response_notifier: oneshot::Sender>, +} + +enum OuterAction { + None, + Continue, + Break, + Return, +} + +impl WebsocketTransport { + pub(crate) fn with_event_handlers( + node: WebsocketTransportNode, + event_handlers: Vec, + ) -> Self { + let (req_tx, req_rx) = futures::channel::mpsc::unbounded(); + + WebsocketTransport { + node, + event_handlers, + request_id: Arc::new(AtomicUsize::new(1)), + controller_channel: ControllerChannel { + tx: Arc::new(AsyncMutex::new(req_tx)), + rx: Arc::new(AsyncMutex::new(req_rx)), + } + .into(), + connection_guard: Arc::new(AsyncMutex::new(())), + proxy_auth_validation_generator: None, + last_request_failed: Arc::new(AtomicBool::new(false)), + } + } + + async fn handle_keepalive( + &self, + wsocket: &mut WebSocketStream, + response_notifiers: &mut ExpirableMap>>, + expires_at: Option, + ) -> OuterAction { + const SIMPLE_REQUEST: &str = r#"{"jsonrpc":"2.0","method":"net_version","params":[],"id": 0 }"#; + + if let Some(expires_at) = expires_at { + if Instant::now() >= expires_at { + log::debug!("Dropping temporary connection for {:?}", self.node.uri.to_string()); + return OuterAction::Break; + } + } + + // Drop expired response notifier channels + response_notifiers.clear_expired_entries(); + + let mut should_continue = Default::default(); + for _ in 0..MAX_ATTEMPTS { + match wsocket + .send(tokio_tungstenite_wasm::Message::Text(SIMPLE_REQUEST.to_string())) + .await + { + Ok(_) => { + should_continue = false; + break; + }, + Err(e) => { + log::error!("{e}"); + should_continue = true; + }, + }; + + Timer::sleep(SLEEP_DURATION).await; + } + + if should_continue { + return OuterAction::Continue; + } + + OuterAction::None + } + + async fn handle_send_request( + &self, + request: Option, + wsocket: &mut WebSocketStream, + response_notifiers: &mut ExpirableMap>>, + ) -> OuterAction { + match request { + Some(ControllerMessage::Request(WsRequest { + request_id, + serialized_request, + response_notifier, + })) => { + response_notifiers.insert( + request_id, + response_notifier, + // Since request will be cancelled when timeout occurs, we are free to drop its state. + ETH_RPC_REQUEST_TIMEOUT, + ); + + let mut should_continue = Default::default(); + for _ in 0..MAX_ATTEMPTS { + match wsocket + .send(tokio_tungstenite_wasm::Message::Text(serialized_request.clone())) + .await + { + Ok(_) => { + should_continue = false; + break; + }, + Err(e) => { + log::error!("{e}"); + should_continue = true; + }, + } + + Timer::sleep(SLEEP_DURATION).await; + } + + if should_continue { + let _ = response_notifiers.remove(&request_id); + return OuterAction::Continue; + } + }, + Some(ControllerMessage::Close) => { + return OuterAction::Break; + }, + _ => {}, + } + + OuterAction::None + } + + async fn handle_response( + &self, + message: Option>, + response_notifiers: &mut ExpirableMap>>, + ) -> OuterAction { + match message { + Some(Ok(tokio_tungstenite_wasm::Message::Text(inc_event))) => { + if let Ok(inc_event) = serde_json::from_str::(&inc_event) { + if !inc_event.is_object() { + return OuterAction::Continue; + } + + if let Some(id) = inc_event.get("id") { + // just to ensure we don't have outdated entries + response_notifiers.clear_expired_entries(); + + let request_id = id.as_u64().unwrap_or_default() as usize; + + if let Some(notifier) = response_notifiers.remove(&request_id) { + let mut res_bytes: Vec = Vec::new(); + if serde_json::to_writer(&mut res_bytes, &inc_event).is_ok() { + notifier.send(res_bytes).expect("receiver channel must be alive"); + } + } + } + } + }, + Some(Ok(tokio_tungstenite_wasm::Message::Binary(_))) => return OuterAction::Continue, + Some(Ok(tokio_tungstenite_wasm::Message::Close(_))) => return OuterAction::Break, + Some(Err(e)) => { + log::error!("{e}"); + return OuterAction::Return; + }, + None => return OuterAction::Continue, + }; + + OuterAction::None + } + + async fn attempt_to_establish_socket_connection( + &self, + max_attempts: u32, + mut sleep_duration_on_failure: f64, + ) -> tokio_tungstenite_wasm::Result { + const MAX_SLEEP_DURATION: f64 = 32.0; + let mut attempts = 0; + + loop { + match tokio_tungstenite_wasm::connect(self.node.uri.to_string()).await { + Ok(ws) => return Ok(ws), + Err(e) => { + attempts += 1; + if attempts > max_attempts { + return Err(e); + } + + Timer::sleep(sleep_duration_on_failure).await; + sleep_duration_on_failure = (sleep_duration_on_failure * 2.0).min(MAX_SLEEP_DURATION); + }, + }; + } + } + + pub(crate) async fn start_connection_loop(self, expires_at: Option) { + let _guard = self.connection_guard.lock().await; + + // List of awaiting requests + let mut response_notifiers: ExpirableMap>> = ExpirableMap::default(); + + let mut wsocket = match self + .attempt_to_establish_socket_connection(MAX_ATTEMPTS, SLEEP_DURATION) + .await + { + Ok(ws) => ws, + Err(e) => { + log::error!("Connection could not established for {}. Error {e}", self.node.uri); + return; + }, + }; + + let mut keepalive_interval = Ticker::new(KEEPALIVE_DURATION); + let mut req_rx = self.controller_channel.rx.lock().await; + + loop { + futures_util::select! { + _ = keepalive_interval.next().fuse() => { + match self.handle_keepalive(&mut wsocket, &mut response_notifiers, expires_at).await { + OuterAction::None => {}, + OuterAction::Continue => continue, + OuterAction::Break => break, + OuterAction::Return => return, + } + } + + request = req_rx.next().fuse() => { + match self.handle_send_request(request, &mut wsocket, &mut response_notifiers).await { + OuterAction::None => {}, + OuterAction::Continue => continue, + OuterAction::Break => break, + OuterAction::Return => return, + } + } + + message = wsocket.next().fuse() => { + match self.handle_response(message, &mut response_notifiers).await { + OuterAction::None => {}, + OuterAction::Continue => continue, + OuterAction::Break => break, + OuterAction::Return => return, + } + } + } + } + } + + pub(crate) async fn stop_connection_loop(&self) { + let mut tx = self.controller_channel.tx.lock().await; + tx.send(ControllerMessage::Close) + .await + .expect("receiver channel must be alive"); + } + + pub(crate) fn maybe_spawn_connection_loop(&self, coin: EthCoin) { + self.maybe_spawn_connection_loop_inner(coin, None) + } + + pub(crate) fn maybe_spawn_temporary_connection_loop(&self, coin: EthCoin, expires_at: Instant) { + self.maybe_spawn_connection_loop_inner(coin, Some(expires_at)) + } + + fn maybe_spawn_connection_loop_inner(&self, coin: EthCoin, expires_at: Option) { + // if we can acquire the lock here, it means connection loop is not alive + if self.connection_guard.try_lock().is_some() { + let fut = self.clone().start_connection_loop(expires_at); + let settings = AbortSettings::info_on_abort(format!("connection loop stopped for {:?}", self.node.uri)); + coin.spawner().spawn_with_settings(fut, settings); + } + } +} + +async fn send_request( + transport: WebsocketTransport, + request: Call, + request_id: RequestId, + event_handlers: Vec, +) -> Result { + let mut serialized_request = to_string(&request); + + if transport.node.gui_auth { + match handle_quicknode_payload(&transport.proxy_auth_validation_generator, &request) { + Ok(r) => serialized_request = r, + Err(e) => { + return Err(Error::Transport(TransportError::Message(format!( + "Couldn't generate signed message payload for {:?}. Error: {e}", + request + )))); + }, + }; + } + + let mut tx = transport.controller_channel.tx.lock().await; + + let (notification_sender, notification_receiver) = futures::channel::oneshot::channel::>(); + + event_handlers.on_outgoing_request(serialized_request.as_bytes()); + + tx.send(ControllerMessage::Request(WsRequest { + request_id, + serialized_request, + response_notifier: notification_sender, + })) + .await + .map_err(|e| Error::Transport(TransportError::Message(e.to_string())))?; + + if let Ok(response) = notification_receiver.await { + event_handlers.on_incoming_response(&response); + return de_rpc_response(response, &transport.node.uri.to_string()); + }; + + Err(Error::Transport(TransportError::Message(format!( + "Sending {:?} failed.", + request + )))) +} + +impl Transport for WebsocketTransport { + type Out = Web3SendOut; + + fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { + let request_id = self.request_id.fetch_add(1, Ordering::SeqCst); + let request = build_request(request_id, method, params); + + (request_id, request) + } + + fn send(&self, id: RequestId, request: Call) -> Self::Out { + Box::pin(send_request(self.clone(), request, id, self.event_handlers.clone())) + } +} diff --git a/mm2src/coins/for_tests/DOC_HD_tx_history_fixtures.json b/mm2src/coins/for_tests/DOC_HD_tx_history_fixtures.json new file mode 100644 index 0000000000..947e9fc877 --- /dev/null +++ b/mm2src/coins/for_tests/DOC_HD_tx_history_fixtures.json @@ -0,0 +1,252 @@ +[ + { + "tx_hex": "0400008085202f89019544a044ee7940c9030e202bdf2556cc67ebf7a679018f9ffcad48997e996c25630000006b4830450221008f1b58e12b8f05dd8a3542edc6c1f512414a18e947b75be95972b23cfb1053aa02201b10d158d7583156839da4f6da9a05df4a0f261be16c2a67a661dd8ac303818c012102827cbda06abe83afc9957f43879c5b45bbc596c2378080cde44710feddf6b599feffffff65eb5b9c51000000001976a91400119164645e3c8734a85d0985775043206c5d5988ac884c9351000000001976a914dbdf6bd26c1cdbf15558a875fe1c71fde39806e588acc27b8851000000001976a91457dbcf6e34fba4fc9bb6534b077bc05910ccdef388ac20d36051000000001976a9140e0b93457d8c9a4a4eae1b8957d6fe3a6096271c88aca9cb5951000000001976a9145d81fec7689289850da3aafac308dea25f62abb088ac194e4251000000001976a9142c7e25e691858a372cadb6c423aa600891294adf88ac81443351000000001976a91464856896387d11dba6ac54725afc2086d9702cfe88acfdad1d51000000001976a9149b8fc09bcef640a67b4e5692d27f31628f9d5e4f88ac920c0f51000000001976a9143f75eac3a861cebfdfd118303b4db4e4183d045e88acfea70051000000001976a914907a0e46696113adc87cd2ef7e0ab628c7bce67388ac309cc950000000001976a914e5d8ab6e482ea5eabbc79d56534ccb19febc4bb288acbdb6c550000000001976a914fe0b2fdacfd0842be4247db13e98377742702d9488acdc69c350000000001976a914be1bba65a6cfac4c2ad8a1a374ee81dec4fc26e988ac7481c150000000001976a914ee6ef0cfe16cf01ec66b712120c1bafcf1b6225588acd706bf50000000001976a91454937fb957cd6b2cf24b05c28b999cec0306e79588ac981fbb50000000001976a914d10203dbe5f8576a0c10e338a17858cf7fc38a9d88ac981fbb50000000001976a9145a772341158f294cb5a86aab97bee854d784377c88ac981fbb50000000001976a9141e1c94fd8908b33971e1b650c91b0159a7991c4388ac6349b750000000001976a914e5663d584a31f1614eb1a4d5201f5e1e9b015fe288ac9862b450000000001976a914ea7b8f508244558211c7efb037b3fe00bf56ce6888ac3871b050000000001976a914d059cb52fa86410600eaafbac5e6fbf570a30d2b88ac3871b050000000001976a91489b78630b77beae9f50572520e0673d9f9efecf388ac3871b050000000001976a91439e6330611604be92d4c9fa5628a95dc4fcd880288ac3871b050000000001976a91436a79125a7b11ccacdcb644d3c8b2bd8fb0c203888acbeebad50000000001976a914393fc5c7cc14e3fe0b02f1725c9f5c2fe591eeca88ac512ca850000000001976a9148af0e6e3ecf9ec8e35f6d923876129392d06171a88ac91449950000000001976a91476c090276beb7bc604f76f5a91475d22d11852fe88ac9c449250000000001976a914e1e1e90f5e48d15de5d9e695a946320fc9d81ca088ac8d148950000000001976a914014d22617e75f0a6927fbbbb34a50e02b883f57188acf0577c50000000001976a914deb6190cadd4d9b38eddd81a5fc3df21733721a288ac0c607350000000001976a91422babf438f024155ba2ce056a831b10b530da8c088ac70d46a500000000017a9143006759c25a03f5289be9f05ee1b858283c0d5e28725635550000000001976a914bb35d537f7aff439e6f606451681cda7a9e1f05388acc2d74c50000000001976a91486b2dc76a4aab1c7d5a3d1e3b06cd55c8600ef7288acbb7a3f50000000001976a914bdf0b7ba28a0285b132f9f8af518e0cc4e6928cb88ace09a2c50000000001976a91464452bd328bccc1a29383011cf611113dc4afb7888ac4d3d2750000000001976a91426c20b0e51a1bda5fe7d67cb092a5c71a3ce1a3088ac70d21b50000000001976a9145187e26160bfe02b839457597504677c012acb1188aca0621750000000001976a91422024ba1cb2b53d25e37019a691671041db5fc1988ac7d0a1250000000001976a9146db8018ac41b54f7cba8740dfb69720db7736ae788accfcbf84f000000001976a9145c811518f65d70c24bd421939470740759800ddd88acb4cceb4f000000001976a914febe4190846d471a1584a05c53ee6092a92f0a2288ac11a0d54f000000001976a914f30b0323ea885fabe035d290dae99314d8d8c9ed88ac67afd34f000000001976a914d9b2a61924d3b206b939795ab05a4dfc0175597088acb751b94f000000001976a914e021263e72abff4c3ca4325075f814af43e568f688ac2917af4f000000001976a914ff661f373098f0212538f99c8f9d857ffde0f7e488ac5c817c4f000000001976a91482d277da3a1804ebf31911a7b3c010640dbb184288ac7f13794f000000001976a9149308e5abc49d9fc6d93373dddc483d8b4597048d88ac8ec0744f000000001976a9145b581ed40f9fc5a1561cfdfcfff7168a221b5aeb88ac74916b4f000000001976a914cddcf36ac3d33cbecbf4e72c5ca0bb449fb5eeb188ac00b5644f000000001976a914fb6310bcad73bff01a6e0d0ae55b73d4d8f8048d88ac00b5644f000000001976a914976e7ebf2a898135e39ac7d57531b07e65c63a1f88acc8bf414f000000001976a914e51f303a29a1afb69e09fed704335bf759b5978388ac7c82274f000000001976a914ade5cec3cda719f4eaeeaef4d212e9d848e33cf788ac1028254f000000001976a91436864142312af521f7e017ae55bd3a8dffd4ddfe88ac71b50d4f000000001976a91463accd7816c7a823ee7e9b60381c15077684a6b688acdad70b4f000000001976a914bedbe64661aa61e94c1dfb99851a696475e2f1f988acaa5cf54e000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88ac294ee34e000000001976a914dd6c703e0ac56efaa507624dc5e30badbe8ba3b988acf089be4e000000001976a9143c7719101e620e11a49b1c9de36a43027069fb3488ac58048c4e000000001976a914a7d117393f030892da9089a1c074986046d4d1ca88ac7473884e000000001976a914c2e49d57412294a072b09c2277c7dac8a5532e5f88ac5a067e4e000000001976a914ac8420d6a684d6ad984e1a3630429f7530224e8c88ac7061674e000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988acd2a32a4e000000001976a914dfa72fa73b20c58342354e80904c44eac968473b88ac88fa064e000000001976a914c5f8f0dae60963b9868c4160bde949a7b59b460688acee4b014e000000001976a9147a02ef277409cd06c3001e176ad63b3dd764ddd688ac7ad8c84d000000001976a91421bcabed00babf83fa70dc58bded5fc72c0dc74588acdf2aa64d000000001976a914f688374529c0e35baa77394d73b7d89cbceab7ee88ac18697c4d000000001976a91490af39fbfb266c596ea7b5327a103374da5c6aec88ac3198774d000000001976a914f31b7d0f1a37b43f0d8708ab56db0e34158872dc88ac8df6744d000000001976a91420ecf5562ed504e3330984875fc8b4a85bba2ab888ac2f6a604d000000001976a9141e89f0ffa2065b88510b59bcfda491c590e9938388ac92345c4d000000001976a91404cd77faac11955a6c8d57ec320bf7c6949dc41488ac7756f54c000000001976a91421e20f0a21a6ea74504ec7b03560de471086baba88acbc76f34c000000001976a914f6b9b015ec458ebdcfcc215d664210ce1a721d4188ac16eaec4c000000001976a9149ca90cdc0907c4797db8d119fa66d10f32cf9eee88ac4536e54c000000001976a914f39faffa0abce060653476678cea8e2e5eb7ba2c88ac9da2ca4c0000000017a914b9053e5c14d319a16c653b604f3c3d708cc6445387b26aa24c000000001976a914ccf41559bd3583ebbf082f3434dc1ddd6737e6b488acd6de864c000000001976a914355e9ea170105bba94295cf6a4965e8a5b49cb9688aceffa664c000000001976a9142b5adc4ce302ad3ba167de7045a4180019a1aad388ac74904d4c000000001976a914ab164a6f7365b7afc9120fc8b8efc4739e6ced8e88ac8ab02a4c000000001976a914fe3e90764770e0fbee333e1efbdd3b4c9adfbd4788acb823284c000000001976a914e754d96556bd00b988a2aabb375e39bc60991ee588ac70711d4c000000001976a914d6a5d7032a3334292e4f79b0a3f80f86857d799488acd4d51c4c000000001976a91405152a18db02dc9e4f9f729533e8cd70c723da6388acba27174c000000001976a91430ba6860b2479c9500bdcc195d00512182071a0088ac09b8f84b000000001976a914004958b597e6120c23de7c0677cb295c9f547e6788accaedec4b000000001976a914c4161a53a1f3b9049207cce3b0bd2a3d08ac679a88acf498eb4b000000001976a914f1fedd086e8063df0afcfc02653d94daa9b9eed188ac0469e94b000000001976a914bc2b1177d0c011f17d36de1587ee177dac1aa0ba88ac4d96dc4b000000001976a9146ddec0405363b70d487e14d43095f8b6bd3688f688ace592ba4b000000001976a9143b4664f6c0b930e3932aeb18de34062ca266ee5f88acee26a74b000000001976a914dcb2819a9710b1309728ff63387f5ec82a8d75a888acffa4924b000000001976a9149d107fb12cda05bbe956f03a0bf6bec737e02cc788ac7dd8894b000000001976a914d1632a04ae05bed90ffab24b190b3be71b87339388acf5617a4b000000001976a9140a1f6f1481780f44cd28226b9c84269df4650ca188acc894754b000000001976a91481add3e4bf9b3d06c2af67eb2032a5940d7bd06388ac14b27b7087d262011976a91417de9c09609a231f34f99f2a1a56ea1ba93409c188ac24fd224b000000001976a914efabaa3e40a7e6eaa1c47488417eb0516ec534a788ac37683864590100000000000000000000000000", + "tx_hash": "071200b4b2967cfe3522b8a6713b8bdcd09f74a17d575fad87b4e97bc442f404", + "from": [ + "RAUNnd2eu1aEHBk7t48nvm1ZuB6dV35xSj" + ], + "to": [ + "R9HZ8A76dc8AVY2prhKdTJhGUJ5nsEeu1w", + "R9JhwioyQefWNB41NkLYafZBmcA9zP1gcK", + "R9Q5A6MBWvr9fWapmC8ZqsWjRTprwt8mQ4", + "R9iavVmNLKSa3TfPo2jgFToxx1Gdfcv4cN", + "R9k4p1Kp1E4vNdRy1yZgQbfZ8DaMJn3zyv", + "RACiVVyu7roWL3VBs8cDeW75ksbd3ggreq", + "RAZTQTzUn4NxYj46ArudGWLvK1Cih5CS4p", + "RBTQJ7FhwhR8or1xpWYShnz4j7EAU7tDHU", + "RC2QbBYtGZL7pZxCdKvXMEuCR7SnuqH2JB", + "RC4fbcBwSm6qc27H5ociebYsP9cX68WJR9", + "RCHHZotJv1Yyjd3RspV6m4daf7FR2HAsUE", + "RCMaPygJwofpCwYRbtMk6bYrguqwjbGW1x", + "RCNMBgc61JHiQ1YhN9Vs5y7EK251ZQBTNE", + "RCP1oXaki4UXrdc6y5CjRvyWVfrkBCBtkm", + "RCSpmTQCaonqcGDLn6fyf1KGgLCP6rBwtG", + "RCp8DAPLZQ359wPZG6NM3eerDMKLDfxi6t", + "RDERysCc4tPBEajUF87byr1vPWgJ6kmN5g", + "RDLSvsobHguvPLp1wiHBPGUg9dfrfJLtSQ", + "RDiqpZRFQbn4NdDpkhZZUesk4YRwTppaGB", + "RE9PEyJxBHQZfVYR9HfVD3nW5YdpTnSxNF", + "REFVQ51obEg4ZihgEdd7dftkeueQdFdJhh", + "REGBJdf93AVUeRCLVA48mevdJiqRccGnye", + "REVtzSsiu8NMimdXDwJEHRNkBiqX5AzcVA", + "REZLMyjSjDBR62KRAMzNiYbwz3ZT41XmVg", + "REgcGvRxzs9zo7JwFcovK6bVpiQpcfao3V", + "REnuJ6MntvU1Pkc5mWPMPDMh8XqA3m5KcK", + "RF4jud119JAcDFM5ZZk3yzsD7MNH3N7KPw", + "RGiHbTghzBj1GupWZzVpDvTxdcGzPhKKLJ", + "RGzPY2xZe6u98J8mm6rp71imgNNsRHAwMR", + "RHHkBtdYVqRe7sqhJKHqEQTeEWbBH29paA", + "RHXXccaDjrCSkrFu59VLrwdDGqgvrFtQf2", + "RHcB8goFaLNwBh91qsCoT1zbj9zPA54z88", + "RHiJsy5Zg2HcLUY6eSfWWfacJS5WgLoK7P", + "RHoceanFVZTqKmzfNG1vYAbdPfdT5ZN8dX", + "RJNDz3U7gDAQNYV3UxoZ7V1S1XJHCyYH8U", + "RJRNWoDho6X23Km58Gppahk8ZGk4B1W7Vq", + "RJShU61KY2tDGFjpETxfcynNTFWpJB62j5", + "RKHLAPUHKFVMhgVFTSfp2CE5GzKnWbAxKg", + "RKJ8aSBwtZFSKkRQVgPDSDhvvDWdvyd17A", + "RL76VfsBB9XoBzwatQSpEMzh3RrgdwXMpJ", + "RLQL2npjgCatuTvbJfcX9tRJqemzb8jMT6", + "RM6sV2J9uqAkiAVrK1erPEVNYzFvSb7qtY", + "RMCv43DkS7Cq7Kv4UnxLNFNSfTpQp6LiFw", + "RMZQtiPNuoUatUWLag3g1g57EowHyAUGRT", + "RMqNWHDjccyPKDkQFgX7Fi9t1kiN2cvbuW", + "RMwqv9VNBu7wjrEvQtMrYX7c6ddogyStBG", + "RNT7bisG6k2uYeUmLbRDvxnN7FnyKtTtLE", + "RNUDJ6CJyefAuPcXrFMfdXkBb7wCjpqFrj", + "RNge4uADWDtA5xLeBXx6pfBbC8rQF6apiw", + "RP5tUpLKvXtxPdDarmUNPw2kz7vu6fusxo", + "RPTj2LgEVwbMprHR261e1ej63YtApX6sn6", + "RPZY1Fu5hjn3SiWvdpkga4eDfNTZo5SZ8T", + "RPbfvzNW2LzLu2qu2MJofWnAERTiA8uhxa", + "RPggeZYc1tN2u95vHy93adqFZoXrzqqtBH", + "RQaXQRrz3sNFbYPo4QVFVxW96TaNibvZoS", + "RQspL7F2wMP9LNFRrxV87zsLPrGycGSCwx", + "RR1NawjEhvsVdpuSHtuBnFq5S3WHHsU9YJ", + "RR8gH27HJ44xgLy7mMaUGMCT9kmyK3Z8VD", + "RSM4vmCHJqd1b8DcToDWN8zutEVsrt1JGx", + "RSS8i1jvAg8Fvx8X6sVEbnNTuZb1U73tkC", + "RSbWA8VM2gzLZN2ZR4i8NzPujQqFc5hfeN", + "RScPgYgBwZPfLJDkVaSFK4xRULVRmWM3AH", + "RSgMtrEpm23t1SYX2ijscKQ3TgSdEM6ixi", + "RT3h39pgmsSJDVnSokT7VqZnvUp3TQB6WU", + "RT9zzr5UpdzZizC9UCe8HBmW4QTXrAoQ7q", + "RTKyR25iHoTgZ6N8XSVuybejGkUA9wv3io", + "RTxtKrvBd1h9yMHGwZNbqvmqyUXaZqVvGJ", + "RU3hHmvWhuBEJQ1BGhvFeHPQLWfmTekLzE", + "RUGrCRsTJu5x9AEovMJvZwdDQSxuhywPZb", + "RULKicpdqc77VLtVDK6eKFoFtLfJaLJMcQ", + "RUNL6erQZtxDYcz67eJxHaf1SrT1zYZajQ", + "RUr9MKqASwh7N5HJp8uLuWpadrZU27BKEM", + "RV8Git3q3TEBqTEVNvXJ6nWujweKNt86mb", + "RVKmi6uV4MCC7oJmmqmBGNv7i3BS6xvufG", + "RVQ8aYwU5fckN2mzfnmf3YKeEUjjn7JPbX", + "RVTyKM22LFEXixFCCe9JyR9kPKX8BfLRN4", + "RVanEUxUaumVoFvUcwkdyWjRUvaGW71hVT", + "RVfm3bFKsf42kakSgbkKmysXEdztWosp1X", + "RViH9kE8USTdnNWZMvom3k1v6fnQKSFfjH", + "RVsYkCxid9XggtkMhW1QJYuPWQ4Vb92Bce", + "RWAgBTC4hzkda6zB38tvPJpyPNApg2zkND", + "RWC9JBLutyP4zM9F7Rz2Uo5DAEtvxNnbdQ", + "RWEWNv7VKq2NSdNPmf4JzpLYwdsXq3WSzG", + "RWNMpFjXMRAERSQwwnciH4fJWKEV9jZtLT", + "RWf2DVT6yrh9AHeU9R6CB4qpKxssj5y2n4", + "RX1uoTubn4iLvtMveUxFr8HAppKzTdNCiF", + "RX8TDrhYePANad5HRt8b7YzUT58hpF72pJ", + "RXLkEy4T96TUyzSwVifhom3v1sDhFLqi26", + "RXSHUGzciEyCGmKpUk2dFaySNCiGSBVBvL", + "RXSdD4niTG7bMau3Mk4YdLLDLdeaPfeg4c", + "RXVMaQXtcZorqMApDC8TmvkwtKkibZeeCV", + "RXkjV9pjc7AicF363J6mhNLjxHvqSowxwG", + "RXmkkXJYGeRQnfKJHCxMFg4L4ons7ckaBJ", + "RYCQMrBHFZDXgAzE9aQocDLdrFHVzUijqe", + "RYM6yDMn8vdqtkYKLzY5dNe7p3T6YmMWvq", + "RYST7XGacQawwx6XanJnL1FikZvunj1i5K", + "RYTWfHVK5p8jeXye4rxcPB3W8Y9T5AV2U6", + "RYW9dU4SqsV9v9AkK319zCWhDYRLNdEiai", + "RYZcizLKQp7rfp7g48s1XFf2Ft13sMY5QP", + "bH7CsFFeouuXvSyRRprMkW9gq8ca5VoUsG", + "bVbZz3tP5NKptwD57iXuJoxzFeSA7HUA1S" + ], + "total_amount": "998737532.25880975", + "spent_by_me": "0", + "received_by_me": "13.15398", + "my_balance_change": "13.15398", + "block_height": 146, + "timestamp": 1681418505, + "fee_details": { + "type": "Utxo", + "coin": "DOC", + "amount": "0.00005" + }, + "coin": "DOC", + "internal_id": "071200b4b2967cfe3522b8a6713b8bdcd09f74a17d575fad87b4e97bc442f404", + "transaction_type": "StandardTransfer", + "memo": null + }, + { + "tx_hex": "0400008085202f8901370e8e48eb9658409715b42d78d257c6c8ccc62b9a44a88e1bc9f76693bc5263630000006b483045022100c68668ce5154ab738678d8c24921dc5cebdc9b1ab1e08af65e326c951d9a2e68022011b91ed495473c9ced1bd874d146ede660aff61b669811e99d6b58835cd97852012103e108fb04390c946355a4f8e3973a7242ea37399e48af6f877cd15ed80260be85feffffff658ba04a30000000001976a914e4390632464ceed0ab4270fe79ab8da172f4d13b88ac904b4530000000001976a91450641ab9097851085c1ade02887b0850770e83c888acd0474430000000001976a914a14171395bd978de6311d289bbc4699e8ca731f088ac087d3b30000000001976a914a27e6aacd2c5943ed1b32c35466a31b9f7c29b6488ac105f2430000000001976a9145dd5fd4a3da4770e6ec51f497177fa4beb84e3b588ac32f81e30000000001976a914c90f20d617c9c9b54c0097ad89295201332b166a88acc5240e300000000017a914cc9b96a7473df8a68753438bbaf3babcec996a9a877aac0330000000001976a9144a13f6e4d4ed52a86e8520f9ae40b8e12eaa58c888accc57ff2f000000001976a914ca3c9bb3248ac7feedffed6d1e323a2003e88ba388ac8293fa2f000000001976a914623153b584d7df10d95912c9b35385c8fbde493788ac1488e32f000000001976a91447a79d95b9dc7328c188ebc3031b8d785a68399788acf836d82f000000001976a9143b81c728b12261cf73103d53aa2980d53143b72d88ac78fed62f000000001976a914746fecca628da928b41ad95b463f335ce2930c4288ac28b1d32f000000001976a9148de304bcf0997b4dedf0940da03c81f62127c01e88acf650c22f000000001976a9146c4a8b69875b68825398b4bec2fc0d75a239728988ac2cc7b82f000000001976a914cf043edccbde2292701ddf6c53809a23cbdd10b788acc0e8ae2f000000001976a914653e70666cab9690909b5a1fcafff1df694de07888acb812ab2f000000001976a914472ca68e7acd52b99c30937294795eb6add0261088acf550a92f000000001976a914b16ab3a47859a5bec9ed917149cc07bfb8c12d3288ac78d09b2f000000001976a9140edecf9e25156deaacc9a3ecfb251c30b3d21b9188accf83862f000000001976a914bb3cef9a0d7e8619d12a20547714d5a818cb4fce88acd089712f000000001976a91447a0b3ac771e366528e9ae022ec624ea3c8faf6a88acfadd5a2f000000001976a91424a2c58d6b63d26e6f9448da4134d3b002d3608d88acfb0a592f000000001976a914a4431e15bdc550a437fa95ef847b51480987b5fd88ac181a582f000000001976a91489028f405d981928f7e55da026208374cb488c7a88acd7c1412f000000001976a91498bc706d35aad1fbadda610739c204e80d686f5d88acfcc12b2f000000001976a9143b06a7f59795e67ca70b00d6bfa0b05f6b45dd3788ac6cb5192f000000001976a9140312ac8ab576d089f8591b8a6d9d4259b5175f6b88ac7843102f000000001976a91446c8ecbf5e2f593c09105637d8ee4ad8f481cb8788ac823c022f000000001976a91480ff6014931e84f8ac606a5cb69b2213935d12d088acd249f82e000000001976a914cdee94238515d68e41a5331b7fd2e42373aeb6e088ac00edf72e0000000017a9141e20a2d463b2c56358fa312d3089e684f92fffc3873855f32e000000001976a9142b4d71e9cb0436be75c30cf80319153288915a1288ac8049f32e000000001976a914b591d089ee36906f96172761c78556f2f75953aa88ac1032f32e000000001976a914ac80d24af5e48e1276faee61bd954e6e09846fb688ac00b8eb2e000000001976a9140c9864e45ab6ae934ce8d228cd09b027e9d661ca88acde2ae82e000000001976a9148199b734d917d6e024bc2bd78d271e885c67500888ac251de72e000000001976a91419db924120ac36b1efcd6e329372d4907d678d0f88ac1d78c62e000000001976a914df5ecba83a17a58b94c8c82690f1a13b059f5b6788ac1d78c62e000000001976a914a0be1efd4046a1d06e277f650aab85bb50a2f78888ac0437c52e000000001976a91454a048eafd03c00358e3a1f83f83474f755c25c588ac1ef9c32e000000001976a91430b225b9c1f53bc0d9f073b0cfccaf2ce1daed4388ace4e3c22e000000001976a914f96097e763b75822bd7aecae76dca69ba1b1f13088acad79b42e000000001976a9144066b64de71b290df43e6b10b9b9cffee2cccdea88acfa3eb42e000000001976a91428e1603e94800bb2461c3fe20925e9af313f774788ac8915af2e000000001976a914ca158ce05bf3e70ad169902ca923c7fcfee15b8288acd15cab2e000000001976a91402bb11742965626d66839e3dbc00bdfe2ab3f2a988ac4299a92e000000001976a9142c4eea2e017267a078a7877c6e38d6749195051c88ac9ae1a52e000000001976a914089d7913c958614094395ea6cca56bc56105619188acd8fd9b2e000000001976a914d175aac3aa0e598fe3a15b7c73fbd318c42eb73b88ac7885882e000000001976a914d010fc4cb5f06586d4e35149ac1e240cad00769e88ac9ab9872e000000001976a914aff4f137072f70fdf12ebf8e1da4efe45719f26388ac1af37c2e000000001976a914229ed25184c0d921bef89c9268d65889b7c735b688ac1f707c2e000000001976a914ec13b8fec13e38673086a24c3ec8bc880c39899988ac89ff7b2e000000001976a91408296baf57f549e17b986c803fc59d7caa7e2a8988ac5b00762e000000001976a914af76fc53bbb41b0ee480fc32242e227fc24d281b88ac7dbc752e000000001976a9147e38d6576b2e02563752cc30a8dc778d0f2b87f388acf2bf702e000000001976a9143d1503c117270d3be0d7ae11481baea95b427e6388acd0dd692e000000001976a914c5403dfe7381514b7eda075d840d69fc5e2bfbb488acd0dd692e000000001976a91455b10311ea020628419969df2fdfd6de5e0befb088ac0c70692e000000001976a91478fb51fc11af250f5327891e161d3d09ca74de1c88ac1009682e000000001976a914a5ab2ba1d037b009f140840c9f57653a0a2d51ec88acf0ba672e000000001976a914e35b469abbb418cd117f68f0b07cddf12346f54c88acc7af672e000000001976a914c44d4de6cd137ba71b2f30be1d05e3427e8f232388acc4f5662e000000001976a914bf9286625d407d84050633abffb84c8bc8aaf60288ac2a50662e000000001976a914818b6e55ae9fa6f40ee766121be5a9ea0e5c3f3688acc0d4642e000000001976a9149e22a469bb009301ddb971b0889794c2f255da6788ac7b445f2e000000001976a914d958f4aea5d54f8654e9398a39935b8d4d1f61b188acf4505c2e000000001976a91494a2e92b1fda3d6e79e32462022c96070ceefff588aced4d5c2e000000001976a9146b1cced256575e3d29c388aead95315dbe19ad5288acf9955b2e000000001976a9140fd6a2abdc4b54cc1e5f92be5f0688e912f8853c88acd8d75a2e000000001976a914dfb2869d0b6d85e46d20ceec3150258d9872c88388ac03c85a2e000000001976a914e3afcafa124461134f5c50cab65d8dcf2427ffac88acc4c75a2e000000001976a9142e62a63c5b96c9126d02ae300a671397baf4f32088aca0c25a2e0000000017a91483dbed2774b9729398823cd2efd723edc558746687a0c25a2e0000000017a9147db51d4816ae870121c0b0f67b310532d06b155587a0c25a2e0000000017a91467aeaf03a84773c5903f8c358084dea483c4107087a0c25a2e0000000017a9144e5bf746ed968cd34d4fb0a7043e15e8183fe2f587a0c25a2e000000001976a914ffc5a9dca4401dbfd57f49ce94896ab24ccd2b3688aca0c25a2e000000001976a914feb0bab685bb1e29f0f41c490d94ca952547a11988aca0c25a2e000000001976a914fea18ff597dbfa935645d9f1ef5041737eee376c88aca0c25a2e000000001976a914fe929475335cd084032528eaae191058b0cf9e7a88aca0c25a2e000000001976a914fd89abeb94f760771505c04355b4b7e28b1a792988aca0c25a2e000000001976a914fd63fb17238dfccc1da898360ae3123b182f40c088aca0c25a2e000000001976a914fd59c9bfca724198798e3bf416d969c27c08261b88aca0c25a2e000000001976a914fd2609deb042a205dc3083b47f3ce1d92d39482988aca0c25a2e000000001976a914fcf6ecce595cdb0e8fc4bfe0ba7f7bef3087282188aca0c25a2e000000001976a914fbc86ce535f26a90f5bcdf8c98cb12e2a91e1c5c88aca0c25a2e000000001976a914fb774e8b36f0e7b74aea00175f85e1999ab9e3ea88aca0c25a2e000000001976a914fb574678a16f5be04c46c95ee0c65369cf71a34788aca0c25a2e000000001976a914fb0a0baa37b54e4fa1effffb346beaf424cc8cce88aca0c25a2e000000001976a914fa891ffcb0a11c76aa4f3d71facf78a590ae2b8288aca0c25a2e000000001976a914f9feb4a3aa9e50161d52c54bd55eb12210f09b8588aca0c25a2e000000001976a914f939ac0b52a54b498c91a3f7ba5eec80cf8d688488aca0c25a2e000000001976a914f8bdc2447132283a3507b37060ad1ae1156e1f2d88aca0c25a2e000000001976a914f81178ffaba06160ff713cb1f57ace80b2666c1088aca0c25a2e000000001976a914f68ba1678cbc97e22eb1a9edd9a234a03960eebb88aca0c25a2e000000001976a914f642c585d1d52614e873bed2af5a532971dd815588aca0c25a2e000000001976a914f606405494f7f15dd7f8d07d5fe9e5fe9e5ba97f88acc2b000c96cd162011976a914842a9258bacf0491f798d13a76afae991ca1351588aca0c25a2e000000001976a914f601c7ec754d89fdea640fd7d74d7085ece63c9d88ac37683864590100000000000000000000000000", + "tx_hash": "2f8b4178b56d0a9f0ad31afcbef6ff267a0bf655dcff72de530107a4c93407b6", + "from": [ + "R9tpBXKZa5cM3UAQsQjWp5j4Q1UkgzR4gZ" + ], + "to": [ + "R9XdXdBZpURHcXiDaT6GHgnhepD5hekMtz", + "R9ZSUanF8P1ucWo6ajpryeDhAJHL9jsNRx", + "RA2M71PZj82h2Q4QM5oKX7ow94MBoS2iW5", + "RA4k8TqzKB3m7aFytQAkaJTo46K8Xf7K7y", + "RARnkM3UGcVWqapKCnJRAQBTB9TRrAHci1", + "RAdpTP6nf3vk2aakdgA6RG2cqqBTjLr56o", + "RAiwLXQHNG7LGxwwJBBLa2LrJfFKDTm2Xz", + "RBdv1PnTRpsdJKVWbNXm7L8u1Xm7FBXJxF", + "RCSFK9rbnwmWZaDb4nHNGioXBaMoTiKmhT", + "RCcuPznoCewfUwaVL3HVTyRYB1UvD7TpPE", + "RD1M6ati1cawYu3X8KgqV2LAfbmF4mNTcz", + "RDE9ujdjYQ5EimxMxwpJd4t7CdwAqCVbTm", + "RDKUM3tM328biZC75FcuVz2Czu3APL9oMQ", + "RDWTLcni9dMz16KSxeFFwLBZ4RWR5m6iaj", + "RDifvcuZXNvvPGh6gK3EpSGYz3T7hfGMAr", + "REfHvJnHHWfRbJzu6BPPPT6wv9ZoNNkswg", + "REhqQxvveAARk8qEvfDyCGM8zpqCgtN11t", + "RErAULoZnBgT3XEAkMa7qWrm9tBP4YNF2K", + "RF9iNMehNgwjPfSxdUBhUeToDhVCr9ymNH", + "RFjU5SgqvA2LDZUkM9MTS7uaSRbxTPCV72", + "RFmXYWpPFC24EpnejKQwpU7bG8wesRodPJ", + "RFovZtwXCjAE8fAtB6xULQype7BVCQsxd1", + "RFp4rGbZPh6ZYc6aBTNMteCVEL91KsT5dX", + "RG2szs614CGnvSK4x1HsHxGsdrWampTFdm", + "RGcG4Ei5mPCHaGYvHfmqXLg9wBk7PFb8Co", + "RGzerR3niEJ2EUcfiMFZ2GCK51XzwmdzBF", + "RH6HZq7yHvLQES1xG7CcMMR6eYKpS4ct1t", + "RHqMGbqVdk8txXqne7wUwS6zduqxf5uQs8", + "RJEPPcc4yRzuhxSPHFpihEd4Y9A1dLh2P5", + "RJWX8EULEwMEaak9K2fmvcDpr9hRuWBUPj", + "RK3Ytedp7eAt3JxbDThpQ2EAD49X5eCs1i", + "RK9nMgWA5jwEEWss4pr9LeKopDy51q629G", + "RKtrYT7yN2ZgV8wqEj5c2EoZPMtB1gdjDR", + "RLJtEaBnSRBQDDi68zM4ZMhBpi5s4aY7Dz", + "RLnbJouZbzjNQJSQnRfqMrQ3sVaChJ49L6", + "RM3GVrjNGDVBaqJSk4WLCTAMcvuny8xvLG", + "RM6AH6cR9Ti5ogHSX7R5Ri7zfWXBZfpCKb", + "RM6TPcVhrDtRHk1NiMCwwbvH1mLCdnpcdm", + "RML2GmYtLpbcHxH234tZ9JGqJFdftD3aJ1", + "RMmdie2nx2fvSF2q5B6SQrbGzuwp7sNWW2", + "RNDRK5ydL7c4ca4s5oFjPrgzgbL1UMDzVU", + "RNq7F9akQsFw1nq6rRVCy4f24fKDFesh9B", + "RPCnXf4kRJcHAH8iEPYpmUUTG6BGKDUtFp", + "RPhLLowiL9qgVRHE85rtrziF2xvsWfWrju", + "RPw7x3vU1SmZoPcHpP4Ha8BupRExpaSXae", + "RPyqGQk8t9sVhFaSi9oCdcqWu77aWTXy7c", + "RQ6NzBKx3AevJauCfMrTSCHMcEkW5jfiwQ", + "RQFjJPBV2XbE2unBxs7wRX7p8jdcBr5ovc", + "RQPAdJGkY8eXpweKLhtbA3jxj23Ys9EALh", + "RR1JdBYW2dnHMgc1Gx27LfFiE1sMXRzW1S", + "RRGxsMWgcnsiiA9jrL7sXVwg2DQkx3rZWZ", + "RRKZkyL37MEDMcL7bkt59FC9brsFg3yn33", + "RRTHWEeWeqyzAGn3yNMmsr7jB4LFcEZ3Lu", + "RRqF4cYniMwYs66S4QDUUZ4GJQFQF69rBE", + "RSMDSHtX6f26fsi9dPY4WdCoF9zJygYLoE", + "RSk8ftPhRWN8n7sPiLq8vpz5NKUwNf2gcY", + "RTB98K3goXvaWqCY7roa8M7uZLU9UbgcX6", + "RTGA9vvJCuR8tpLntvNv4PEqnyuk3kL2HZ", + "RTcJ2ConuuyseVCSifjSLeXJ3JYKZuMchN", + "RThiPc9WM1CpREL9iKoW5PMHjXtau9WaDh", + "RTiXBQ1Xfad554je7XPKErVbRAX23SRrV4", + "RU44QaTc1CytYQGL9GGRs7CDHmzudZn6J7", + "RU9o3DDQkW52uSuYsDS7B5pT9yA2cpMvVk", + "RUFLybMiWpuv8kiyWMC2YViBnt8ZHWtZhH", + "RUNiGFwgVso4vicyuJyKAr67dmGd26uFa1", + "RV6RGu9XS24RE3dyfPYkvzTXo3DUMTn1uq", + "RVeGKoZLwVoYmMKSZZR2VZAZ1nuPch6L7e", + "RVfzdVBJo66qExMWcvWSV9uY6A7VASsGp5", + "RW1Lp1qVdJS3Yy1hr4BaULXVoHtvNdwAik", + "RW364N7iWW9GhBp88n3BXaWMaj2jZiYaNo", + "RW5vTN6kjSTUgXfR1CtwXZY77jjZu1dgEL", + "RWoTB98FD4bsCk9gt1XSjxj7ZRzCrUM9cg", + "RXhxSSzr8A4zwsuFJT6vCWxZFvwQEACLi7", + "RXi3o44fx45vSBbYmmHb1aPJd6cNSrbxzH", + "RXjJJ5PszpgHUAsc3PtCQfxeAwjGWVpiRW", + "RXkoaQJekHRTVdrauZBgvwuXHLapGZGRHR", + "RXtrb7Pyu4vv6o48FhvsTNnsBxsRsEKxvw", + "RXxQymQqTZ71aPGWDHTsi2zq5VtUrBcfUB", + "RXzyRQVvRTuK5UEUXTEjPzT8gHK3keE8WS", + "RY1n3hy7y2yaA5DEE2m3ngeuFMQn2aKfFz", + "RY53TYCYedZqw4hCkZyDJGsoQfGrvp2dsH", + "RY7uH5ijYuKoMY3T4z2PigQMEv6V5LGQxB", + "RYAZieHYFxJ8pxMWmikZons37XYtkWScK6", + "RYCAEf8DYzxiFv64YomAmVeaiFARCRDhnr", + "RYCpcFLshqKDornm9hyxKFy9btvWiwtPtx", + "RYEVnTtsjAAaBNo3MD7fTH78NPVK7XLck6", + "RYLkAWTgEHuq2G1brGR5FxVVbm62DPwt1E", + "RYMic1n9sDwwvMwRn3s6UV9rwBMMiLg9g5", + "RYNnbdkmc9fKMV9MbnG9PWCsf7oGmuT8F8", + "RYNzoqan9CQJJjMGmmkYAZuUbw4rtnVe88", + "RYPmxduQ7iozBtfd63cfDj1BRuv9qwgZdM", + "RYVFJntFkjgPGZ28eF41DEyQTBFSCQtymM", + "RYVZFnjJMwCursrayop7K793ienxXswLpz", + "RYVsRcQmTQiMP7kbZPmctDdBCJ2MjY3BJ3", + "RYbbBLPkktahqmWJNRFPZbRNHa88sPYxJ3", + "bFUa4aVz1QqDHMMqCzSHjFJCeSFELgYnqP", + "bKsba6eCwJifztKPjpFzLt9geve34ySY2Z", + "bNBVZDqAqFZsrjKkTZUDHXqPLjym66G1ZU", + "bQBx8Ft3yiZsvdZb42oavZfzKPAjZmyJt6", + "bQkUg7YBYVQkhBHHH4c58erctY5NnicbGM", + "bXP8vqnbWNcAZcLEv9VXTH6AJuc7BLRaPp" + ], + "total_amount": "998724855.46075177", + "spent_by_me": "0", + "received_by_me": "7.87696", + "my_balance_change": "7.87696", + "block_height": 146, + "timestamp": 1681418505, + "fee_details": { + "type": "Utxo", + "coin": "DOC", + "amount": "0.00005" + }, + "coin": "DOC", + "internal_id": "2f8b4178b56d0a9f0ad31afcbef6ff267a0bf655dcff72de530107a4c93407b6", + "transaction_type": "StandardTransfer", + "memo": null + } +] \ No newline at end of file diff --git a/mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json b/mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json deleted file mode 100644 index 1d35ebcfaa..0000000000 --- a/mm2src/coins/for_tests/MORTY_HD_tx_history_fixtures.json +++ /dev/null @@ -1,131 +0,0 @@ -[ - { - "tx_hex": "0400008085202f890189cb3d4e9d36a7c2c6bb1b76dcfc1ee7c9d3d83b364c50a3f3a791c5efb6f392030000006b483045022100a0b910ecbf5ed1c473507c3e1a5a06ad612c982d3204d2dc066bfbbeb39c5e400220559a82d151cbcdcb097786127f499863e0864662519a9caf758c929f1659bc34012102d09f2cb1693be9c0ea73bb48d45ce61805edd1c43590681b02f877206078a5b3ffffffff0400e1f505000000001976a914ab19b1f2bd2337a58c1b5d198468951ac42d796788ac00c2eb0b000000001976a914ab19b1f2bd2337a58c1b5d198468951ac42d796788aca01f791c000000001976a914ab19b1f2bd2337a58c1b5d198468951ac42d796788ac3091cce2e80000001976a91490a0d8ba62c339ade97a14e81b6f531de03fdbb288ac00000000000000000000000000000000000000", - "tx_hash": "6ca27dd058b939c98a33625b9f68eaeebca5a3058aec062647ca6fd7634bb339", - "from": [ - "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB" - ], - "to": [ - "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB", - "RQstQeTUEZLh6c3YWJDkeVTTQoZUsfvNCr" - ], - "total_amount": "10010.1518", - "spent_by_me": "0", - "received_by_me": "7.777", - "my_balance_change": "7.777", - "block_height": 1629306, - "timestamp": 1663619097, - "fee_details": { - "type": "Utxo", - "coin": "MORTY", - "amount": "0.0001" - }, - "coin": "MORTY", - "internal_id": "6ca27dd058b939c98a33625b9f68eaeebca5a3058aec062647ca6fd7634bb339", - "transaction_type": "StandardTransfer", - "confirmations": 4 - }, - { - "tx_hex": "0400008085202f89011b8746195a7e80172d948e1eb7f2d6710bd2ecbe7750e653bb8d345b940da55b030000006a4730440220482c7a7762977ed3a8afcf751b7413d5ef978f604e550eb4fd35199e1f4d52400220486fb4f3831a7f512dba1271a4f793e7050ea123bcb4922e3848bb18c7cac4c4012102d09f2cb1693be9c0ea73bb48d45ce61805edd1c43590681b02f877206078a5b3ffffffff0400e1f505000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88ac00c2eb0b000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88aca01f791c000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88acf037389ce90000001976a91490a0d8ba62c339ade97a14e81b6f531de03fdbb288ac00000000000000000000000000000000000000", - "tx_hash": "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", - "from": [ - "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB" - ], - "to": [ - "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB", - "RSYdSLRYWuzBson2GDbWBa632q2PmFnCaH" - ], - "total_amount": "10041.2602", - "spent_by_me": "0", - "received_by_me": "7.777", - "my_balance_change": "7.777", - "block_height": 1617702, - "timestamp": 1662914954, - "fee_details": { - "type": "Utxo", - "coin": "MORTY", - "amount": "0.0001" - }, - "coin": "MORTY", - "internal_id": "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", - "transaction_type": "StandardTransfer", - "confirmations": 11603 - }, - { - "tx_hex": "0400008085202f890175fb4250e95dc0c89cd89a9487324aa8dd7a2f8fd8581fc90881567ca6be02bf000000006b483045022100be3e58c5d4dbe5ea35ab831d610b42bb1ae01fc0df1786f11cbe6969d8d45c6302207b8747b4012a6c4aefecf670eaf8d787445909ebd7775b85c77d723e9d6445a8012103f7f831c6fbe62b987e4b2f455e5b7f27375cf59b57eb3ffa9e122b2c9b395f6bffffffff0200e1f505000000001976a914c23136f831b15dd4522eb1c6eb4c5cd3abbfbe3b88aca8b66428000000001976a914bd658bdd369c8bb98ac837071639819e5f8dd3cb88ac00000000000000000000000000000000000000", - "tx_hash": "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", - "from": [ - "RPj9JXUVnewWwVpxZDeqGB25qVqz5qJzwP" - ], - "to": [ - "RSYdSLRYWuzBson2GDbWBa632q2PmFnCaH", - "RSyz8EJaTzhkT6uZinAtHhFu5bfsvqcLqg" - ], - "total_amount": "7.77699", - "spent_by_me": "7.77699", - "received_by_me": "6.77689", - "my_balance_change": "-1.00010", - "block_height": 1499070, - "timestamp": 1655738171, - "fee_details": { - "type": "Utxo", - "coin": "MORTY", - "amount": "0.0001" - }, - "coin": "MORTY", - "internal_id": "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", - "transaction_type": "StandardTransfer", - "confirmations": 130235 - }, - { - "tx_hex": "0400008085202f89038dea273bcc9194a80b19ea1a8ca076d3d00c590b4571808f10f3eee5aa38c07d000000006b483045022100ff31b7c36145dc9fa06346fe38b75b81273b41b7d236b900322154ba136799bb02204ddd253eb93e95c9d24f8e36a8a5a42022c63d6edd81c453f24cbb5db785037d012102d6a78b71b10459bd0757a614ca1eef62f4a65514225d10e95df31ee9cb23ffd5ffffffff8dea273bcc9194a80b19ea1a8ca076d3d00c590b4571808f10f3eee5aa38c07d010000006a47304402202d3e0fd3ce7b4753725adc0175a050c919f93b564682ffd8d65974c15996b3560220211b8120acb3ad80dbdd9160f639884f0e15c0c5703c86c069b3abe85cb6c4fd012102d6a78b71b10459bd0757a614ca1eef62f4a65514225d10e95df31ee9cb23ffd5ffffffff8dea273bcc9194a80b19ea1a8ca076d3d00c590b4571808f10f3eee5aa38c07d020000006b483045022100c788b064db34e961479393bad19d8c5f7d437b02c64c3172381fbe2b7d104efd022027167b2e5a6f29a7567e43b39a3d5b8f50c20767162ec29328067a6fcbd87409012102d6a78b71b10459bd0757a614ca1eef62f4a65514225d10e95df31ee9cb23ffd5ffffffff01b8be5a2e000000001976a9149e7a424abb2f341d655ce6af1143409edef4d55588acbdb1d661000000000000000000000000000000", - "tx_hash": "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", - "from": [ - "RYM6yDMn8vdqtkYKLzY5dNe7p3T6YmMWvq" - ], - "to": [ - "RPj9JXUVnewWwVpxZDeqGB25qVqz5qJzwP" - ], - "total_amount": "7.777", - "spent_by_me": "7.777", - "received_by_me": "7.77699", - "my_balance_change": "-0.00001", - "block_height": 1263905, - "timestamp": 1641460263, - "fee_details": { - "type": "Utxo", - "coin": "MORTY", - "amount": "0.00001" - }, - "coin": "MORTY", - "internal_id": "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", - "transaction_type": "StandardTransfer", - "confirmations": 365400 - }, - { - "tx_hex": "0400008085202f8901fe0ccc272929c811b20ab910f5478a99229a5304480b5897cf9507149a044c63030000006b483045022100c5b24fbd1ce11736760ebc0edccad6bde2dcbbff090528db4602a485a5ec645f02201340e5f818b9d7ab75e39abaacaa5d94c03fdb8e41698182eeff389a45c5ad15012102d09f2cb1693be9c0ea73bb48d45ce61805edd1c43590681b02f877206078a5b3ffffffff0400e1f505000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988ac00c2eb0b000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988aca01f791c000000001976a914fd084ad97ae0313bba5717acedfba629b8bd426988ac40053045260100001976a91490a0d8ba62c339ade97a14e81b6f531de03fdbb288ac00000000000000000000000000000000000000", - "tx_hash": "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", - "from": [ - "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB" - ], - "to": [ - "RNTv4xTLLm26p3SvsQCBy9qNK7s1RgGYSB", - "RYM6yDMn8vdqtkYKLzY5dNe7p3T6YmMWvq" - ], - "total_amount": "12646.5887", - "spent_by_me": "0", - "received_by_me": "7.777", - "my_balance_change": "7.777", - "block_height": 1263875, - "timestamp": 1641458818, - "fee_details": { - "type": "Utxo", - "coin": "MORTY", - "amount": "0.0001" - }, - "coin": "MORTY", - "internal_id": "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", - "transaction_type": "StandardTransfer", - "confirmations": 365430 - } -] \ No newline at end of file diff --git a/mm2src/coins/hd_pubkey.rs b/mm2src/coins/hd_pubkey.rs deleted file mode 100644 index 9bb122bee1..0000000000 --- a/mm2src/coins/hd_pubkey.rs +++ /dev/null @@ -1,208 +0,0 @@ -use crate::hd_wallet::NewAccountCreatingError; -use async_trait::async_trait; -use crypto::hw_rpc_task::HwConnectStatuses; -use crypto::trezor::trezor_rpc_task::{TrezorRpcTaskProcessor, TryIntoUserAction}; -use crypto::trezor::utxo::IGNORE_XPUB_MAGIC; -use crypto::trezor::{ProcessTrezorResponse, TrezorError, TrezorProcessingError}; -use crypto::{CryptoCtx, CryptoCtxError, DerivationPath, EcdsaCurve, HardwareWalletArc, HwError, HwProcessingError, - XPub, XPubConverter, XpubError}; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle}; - -const SHOW_PUBKEY_ON_DISPLAY: bool = false; - -#[derive(Clone)] -pub enum HDExtractPubkeyError { - HwContextNotInitialized, - CoinDoesntSupportTrezor, - RpcTaskError(RpcTaskError), - HardwareWalletError(HwError), - InvalidXpub(String), - Internal(String), -} - -impl From for HDExtractPubkeyError { - fn from(e: CryptoCtxError) -> Self { HDExtractPubkeyError::Internal(e.to_string()) } -} - -impl From for HDExtractPubkeyError { - fn from(e: TrezorError) -> Self { HDExtractPubkeyError::HardwareWalletError(HwError::from(e)) } -} - -impl From for HDExtractPubkeyError { - fn from(e: HwError) -> Self { HDExtractPubkeyError::HardwareWalletError(e) } -} - -impl From> for HDExtractPubkeyError { - fn from(e: TrezorProcessingError) -> Self { - match e { - TrezorProcessingError::TrezorError(trezor) => HDExtractPubkeyError::from(HwError::from(trezor)), - TrezorProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), - } - } -} - -impl From> for HDExtractPubkeyError { - fn from(e: HwProcessingError) -> Self { - match e { - HwProcessingError::HwError(hw) => HDExtractPubkeyError::from(hw), - HwProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), - } - } -} - -impl From for HDExtractPubkeyError { - fn from(e: XpubError) -> Self { HDExtractPubkeyError::InvalidXpub(e.to_string()) } -} - -impl From for NewAccountCreatingError { - fn from(e: HDExtractPubkeyError) -> Self { - match e { - HDExtractPubkeyError::HwContextNotInitialized => NewAccountCreatingError::HwContextNotInitialized, - HDExtractPubkeyError::CoinDoesntSupportTrezor => NewAccountCreatingError::CoinDoesntSupportTrezor, - HDExtractPubkeyError::RpcTaskError(rpc) => NewAccountCreatingError::RpcTaskError(rpc), - HDExtractPubkeyError::HardwareWalletError(hw) => NewAccountCreatingError::HardwareWalletError(hw), - HDExtractPubkeyError::InvalidXpub(xpub) => { - NewAccountCreatingError::HardwareWalletError(HwError::InvalidXpub(xpub)) - }, - HDExtractPubkeyError::Internal(internal) => NewAccountCreatingError::Internal(internal), - } - } -} - -#[async_trait] -pub trait ExtractExtendedPubkey { - type ExtendedPublicKey; - - async fn extract_extended_pubkey( - &self, - xpub_extractor: &XPubExtractor, - derivation_path: DerivationPath, - ) -> MmResult - where - XPubExtractor: HDXPubExtractor; -} - -#[async_trait] -pub trait HDXPubExtractor: Sync { - async fn extract_utxo_xpub( - &self, - trezor_utxo_coin: String, - derivation_path: DerivationPath, - ) -> MmResult; -} - -pub enum RpcTaskXPubExtractor<'task, Task: RpcTask> { - Trezor { - hw_ctx: HardwareWalletArc, - task_handle: &'task RpcTaskHandle, - statuses: HwConnectStatuses, - }, -} - -#[async_trait] -impl<'task, Task> HDXPubExtractor for RpcTaskXPubExtractor<'task, Task> -where - Task: RpcTask, - Task::UserAction: TryIntoUserAction + Send, -{ - async fn extract_utxo_xpub( - &self, - trezor_utxo_coin: String, - derivation_path: DerivationPath, - ) -> MmResult { - match self { - RpcTaskXPubExtractor::Trezor { - hw_ctx, - task_handle, - statuses, - } => { - Self::extract_utxo_xpub_from_trezor(hw_ctx, task_handle, statuses, trezor_utxo_coin, derivation_path) - .await - }, - } - } -} - -impl<'task, Task> RpcTaskXPubExtractor<'task, Task> -where - Task: RpcTask, - Task::UserAction: TryIntoUserAction + Send, -{ - pub fn new( - ctx: &MmArc, - task_handle: &'task RpcTaskHandle, - statuses: HwConnectStatuses, - ) -> MmResult, HDExtractPubkeyError> { - let crypto_ctx = CryptoCtx::from_ctx(ctx)?; - let hw_ctx = crypto_ctx - .hw_ctx() - .or_mm_err(|| HDExtractPubkeyError::HwContextNotInitialized)?; - Ok(RpcTaskXPubExtractor::Trezor { - hw_ctx, - task_handle, - statuses, - }) - } - - /// Constructs an Xpub extractor without checking if the MarketMaker is initialized with a hardware wallet. - pub fn new_unchecked( - ctx: &MmArc, - task_handle: &'task RpcTaskHandle, - statuses: HwConnectStatuses, - ) -> XPubExtractorUnchecked> { - XPubExtractorUnchecked(Self::new(ctx, task_handle, statuses)) - } - - async fn extract_utxo_xpub_from_trezor( - hw_ctx: &HardwareWalletArc, - task_handle: &RpcTaskHandle, - statuses: &HwConnectStatuses, - trezor_coin: String, - derivation_path: DerivationPath, - ) -> MmResult { - let mut trezor_session = hw_ctx.trezor().await?; - - let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, statuses.to_trezor_request_statuses()); - let xpub = trezor_session - .get_public_key( - derivation_path, - trezor_coin, - EcdsaCurve::Secp256k1, - SHOW_PUBKEY_ON_DISPLAY, - IGNORE_XPUB_MAGIC, - ) - .await? - .process(&pubkey_processor) - .await?; - - // Despite we pass `IGNORE_XPUB_MAGIC` to the [`TrezorSession::get_public_key`] method, - // Trezor sometimes returns pubkeys with magic prefixes like `dgub` prefix for DOGE coin. - // So we need to replace the magic prefix manually. - XPubConverter::replace_magic_prefix(xpub).mm_err(HDExtractPubkeyError::from) - } -} - -/// This is a wrapper over `XPubExtractor`. The main goal of this structure is to allow construction of an Xpub extractor -/// even if HD wallet is not supported. But if someone tries to extract an Xpub despite HD wallet is not supported, -/// it fails with an inner `HDExtractPubkeyError` error. -pub struct XPubExtractorUnchecked(MmResult); - -#[async_trait] -impl HDXPubExtractor for XPubExtractorUnchecked -where - XPubExtractor: HDXPubExtractor + Send + Sync, -{ - async fn extract_utxo_xpub( - &self, - trezor_utxo_coin: String, - derivation_path: DerivationPath, - ) -> MmResult { - self.0 - .as_ref() - .map_err(Clone::clone)? - .extract_utxo_xpub(trezor_utxo_coin, derivation_path) - .await - } -} diff --git a/mm2src/coins/hd_wallet.rs b/mm2src/coins/hd_wallet.rs deleted file mode 100644 index 6124393ef1..0000000000 --- a/mm2src/coins/hd_wallet.rs +++ /dev/null @@ -1,435 +0,0 @@ -use crate::hd_confirm_address::{HDConfirmAddress, HDConfirmAddressError}; -use crate::hd_pubkey::HDXPubExtractor; -use crate::hd_wallet_storage::HDWalletStorageError; -use crate::{BalanceError, WithdrawError}; -use async_trait::async_trait; -use crypto::{Bip32DerPathError, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, HwError, StandardHDPath, - StandardHDPathError}; -use derive_more::Display; -use itertools::Itertools; -use mm2_err_handle::prelude::*; -use rpc_task::RpcTaskError; -use serde::Serialize; -use std::collections::BTreeMap; - -pub use futures::lock::{MappedMutexGuard as AsyncMappedMutexGuard, Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; - -pub type HDAccountsMap = BTreeMap; -pub type HDAccountsMutex = AsyncMutex>; -pub type HDAccountsMut<'a, HDAccount> = AsyncMutexGuard<'a, HDAccountsMap>; -pub type HDAccountMut<'a, HDAccount> = AsyncMappedMutexGuard<'a, HDAccountsMap, HDAccount>; - -pub type AddressDerivingResult = MmResult; - -const DEFAULT_ADDRESS_LIMIT: u32 = ChildNumber::HARDENED_FLAG; -const DEFAULT_ACCOUNT_LIMIT: u32 = ChildNumber::HARDENED_FLAG; -const DEFAULT_RECEIVER_CHAIN: Bip44Chain = Bip44Chain::External; - -#[derive(Debug, Display)] -pub enum AddressDerivingError { - #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] - InvalidBip44Chain { - chain: Bip44Chain, - }, - #[display(fmt = "BIP32 address deriving error: {}", _0)] - Bip32Error(Bip32Error), - Internal(String), -} - -impl From for AddressDerivingError { - fn from(e: InvalidBip44ChainError) -> Self { AddressDerivingError::InvalidBip44Chain { chain: e.chain } } -} - -impl From for AddressDerivingError { - fn from(e: Bip32Error) -> Self { AddressDerivingError::Bip32Error(e) } -} - -impl From for BalanceError { - fn from(e: AddressDerivingError) -> Self { BalanceError::Internal(e.to_string()) } -} - -impl From for WithdrawError { - fn from(e: AddressDerivingError) -> Self { - match e { - AddressDerivingError::InvalidBip44Chain { .. } | AddressDerivingError::Bip32Error(_) => { - WithdrawError::UnexpectedFromAddress(e.to_string()) - }, - AddressDerivingError::Internal(internal) => WithdrawError::InternalError(internal), - } - } -} - -#[derive(Display)] -pub enum NewAddressDerivingError { - #[display(fmt = "Addresses limit reached. Max number of addresses: {}", max_addresses_number)] - AddressLimitReached { max_addresses_number: u32 }, - #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] - InvalidBip44Chain { chain: Bip44Chain }, - #[display(fmt = "BIP32 address deriving error: {}", _0)] - Bip32Error(Bip32Error), - #[display(fmt = "Wallet storage error: {}", _0)] - WalletStorageError(HDWalletStorageError), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), -} - -impl From for NewAddressDerivingError { - fn from(e: Bip32Error) -> Self { NewAddressDerivingError::Bip32Error(e) } -} - -impl From for NewAddressDerivingError { - fn from(e: AddressDerivingError) -> Self { - match e { - AddressDerivingError::InvalidBip44Chain { chain } => NewAddressDerivingError::InvalidBip44Chain { chain }, - AddressDerivingError::Bip32Error(bip32) => NewAddressDerivingError::Bip32Error(bip32), - AddressDerivingError::Internal(internal) => NewAddressDerivingError::Internal(internal), - } - } -} - -impl From for NewAddressDerivingError { - fn from(e: InvalidBip44ChainError) -> Self { NewAddressDerivingError::InvalidBip44Chain { chain: e.chain } } -} - -impl From for NewAddressDerivingError { - fn from(e: AccountUpdatingError) -> Self { - match e { - AccountUpdatingError::AddressLimitReached { max_addresses_number } => { - NewAddressDerivingError::AddressLimitReached { max_addresses_number } - }, - AccountUpdatingError::InvalidBip44Chain(e) => NewAddressDerivingError::from(e), - AccountUpdatingError::WalletStorageError(storage) => NewAddressDerivingError::WalletStorageError(storage), - } - } -} - -pub enum NewAddressDeriveConfirmError { - DeriveError(NewAddressDerivingError), - ConfirmError(HDConfirmAddressError), -} - -impl From for NewAddressDeriveConfirmError { - fn from(e: HDConfirmAddressError) -> Self { NewAddressDeriveConfirmError::ConfirmError(e) } -} - -impl From for NewAddressDeriveConfirmError { - fn from(e: NewAddressDerivingError) -> Self { NewAddressDeriveConfirmError::DeriveError(e) } -} - -impl From for NewAddressDeriveConfirmError { - fn from(e: AccountUpdatingError) -> Self { - NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) - } -} - -impl From for NewAddressDeriveConfirmError { - fn from(e: InvalidBip44ChainError) -> Self { - NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) - } -} - -#[derive(Display)] -pub enum NewAccountCreatingError { - #[display(fmt = "Hardware Wallet context is not initialized")] - HwContextNotInitialized, - #[display(fmt = "HD wallet is unavailable")] - HDWalletUnavailable, - #[display( - fmt = "Coin doesn't support Trezor hardware wallet. Please consider adding the 'trezor_coin' field to the coins config" - )] - CoinDoesntSupportTrezor, - RpcTaskError(RpcTaskError), - HardwareWalletError(HwError), - #[display(fmt = "Accounts limit reached. Max number of accounts: {}", max_accounts_number)] - AccountLimitReached { - max_accounts_number: u32, - }, - #[display(fmt = "Error saving HD account to storage: {}", _0)] - ErrorSavingAccountToStorage(String), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), -} - -impl From for NewAccountCreatingError { - fn from(e: Bip32DerPathError) -> Self { - NewAccountCreatingError::Internal(StandardHDPathError::from(e).to_string()) - } -} - -impl From for NewAccountCreatingError { - fn from(e: HDWalletStorageError) -> Self { - match e { - HDWalletStorageError::ErrorSaving(e) | HDWalletStorageError::ErrorSerializing(e) => { - NewAccountCreatingError::ErrorSavingAccountToStorage(e) - }, - HDWalletStorageError::HDWalletUnavailable => NewAccountCreatingError::HDWalletUnavailable, - HDWalletStorageError::Internal(internal) => NewAccountCreatingError::Internal(internal), - other => NewAccountCreatingError::Internal(other.to_string()), - } - } -} - -/// Currently, we suppose that ETH/ERC20/QRC20 don't have [`Bip44Chain::Internal`] addresses. -#[derive(Display)] -#[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] -pub struct InvalidBip44ChainError { - pub chain: Bip44Chain, -} - -#[derive(Display)] -pub enum AccountUpdatingError { - AddressLimitReached { max_addresses_number: u32 }, - InvalidBip44Chain(InvalidBip44ChainError), - WalletStorageError(HDWalletStorageError), -} - -impl From for AccountUpdatingError { - fn from(e: InvalidBip44ChainError) -> Self { AccountUpdatingError::InvalidBip44Chain(e) } -} - -impl From for AccountUpdatingError { - fn from(e: HDWalletStorageError) -> Self { AccountUpdatingError::WalletStorageError(e) } -} - -impl From for BalanceError { - fn from(e: AccountUpdatingError) -> Self { - let error = e.to_string(); - match e { - AccountUpdatingError::AddressLimitReached { .. } | AccountUpdatingError::InvalidBip44Chain(_) => { - // Account updating is expected to be called after `address_id` and `chain` validation. - BalanceError::Internal(format!("Unexpected internal error: {}", error)) - }, - AccountUpdatingError::WalletStorageError(_) => BalanceError::WalletStorageError(error), - } - } -} - -#[derive(Clone)] -pub struct HDAddress { - pub address: Address, - pub pubkey: Pubkey, - pub derivation_path: DerivationPath, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct HDAccountAddressId { - pub account_id: u32, - pub chain: Bip44Chain, - pub address_id: u32, -} - -impl From for HDAccountAddressId { - fn from(der_path: StandardHDPath) -> Self { - HDAccountAddressId { - account_id: der_path.account_id(), - chain: der_path.chain(), - address_id: der_path.address_id(), - } - } -} - -#[derive(Clone, Eq, Hash, PartialEq)] -pub struct HDAddressId { - pub chain: Bip44Chain, - pub address_id: u32, -} - -#[async_trait] -pub trait HDWalletCoinOps { - type Address: Send + Sync; - type Pubkey: Send; - type HDWallet: HDWalletOps; - type HDAccount: HDAccountOps; - - /// Derives an address from the given info. - async fn derive_address( - &self, - hd_account: &Self::HDAccount, - chain: Bip44Chain, - address_id: u32, - ) -> AddressDerivingResult> { - self.derive_addresses(hd_account, std::iter::once(HDAddressId { chain, address_id })) - .await? - .into_iter() - .exactly_one() - // Unfortunately, we can't use [`MapToMmResult::map_to_mm`] due to unsatisfied trait bounds, - // and it's easier to use [`Result::map_err`] instead of adding more trait bounds to this method. - .map_err(|e| MmError::new(AddressDerivingError::Internal(e.to_string()))) - } - - /// Derives HD addresses from the given info. - async fn derive_addresses( - &self, - hd_account: &Self::HDAccount, - address_ids: Ids, - ) -> AddressDerivingResult>> - where - Ids: Iterator + Send; - - async fn derive_known_addresses( - &self, - hd_account: &Self::HDAccount, - chain: Bip44Chain, - ) -> AddressDerivingResult>> { - let known_addresses_number = hd_account.known_addresses_number(chain)?; - let address_ids = (0..known_addresses_number) - .into_iter() - .map(|address_id| HDAddressId { chain, address_id }); - self.derive_addresses(hd_account, address_ids).await - } - - /// Generates a new address and updates the corresponding number of used `hd_account` addresses. - async fn generate_new_address( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - ) -> MmResult, NewAddressDerivingError> { - let inner_impl::NewAddress { - address, - new_known_addresses_number, - } = inner_impl::generate_new_address_immutable(self, hd_wallet, hd_account, chain).await?; - - self.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) - .await?; - Ok(address) - } - - /// Generates a new address, requests the user to confirm if it's the same as on the HW device, - /// and then updates the corresponding number of used `hd_account` addresses. - async fn generate_and_confirm_new_address( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - confirm_address: &ConfirmAddress, - ) -> MmResult, NewAddressDeriveConfirmError> - where - ConfirmAddress: HDConfirmAddress; - - /// Creates a new HD account, registers it within the given `hd_wallet` - /// and returns a mutable reference to the registered account. - async fn create_new_account<'a, XPubExtractor>( - &self, - hd_wallet: &'a Self::HDWallet, - xpub_extractor: &XPubExtractor, - ) -> MmResult, NewAccountCreatingError> - where - XPubExtractor: HDXPubExtractor; - - async fn set_known_addresses_number( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - new_known_addresses_number: u32, - ) -> MmResult<(), AccountUpdatingError>; -} - -#[async_trait] -pub trait HDWalletOps: Send + Sync { - type HDAccount: HDAccountOps + Clone + Send; - - fn coin_type(&self) -> u32; - - fn gap_limit(&self) -> u32; - - /// Returns limit on the number of addresses. - fn address_limit(&self) -> u32 { DEFAULT_ADDRESS_LIMIT } - - /// Returns limit on the number of accounts. - fn account_limit(&self) -> u32 { DEFAULT_ACCOUNT_LIMIT } - - /// Returns a BIP44 chain that is considered as default for receiver addresses. - fn default_receiver_chain(&self) -> Bip44Chain { DEFAULT_RECEIVER_CHAIN } - - fn get_accounts_mutex(&self) -> &HDAccountsMutex; - - /// Returns a copy of an account by the given `account_id` if it's activated. - async fn get_account(&self, account_id: u32) -> Option { - let accounts = self.get_accounts_mutex().lock().await; - accounts.get(&account_id).cloned() - } - - /// Returns a mutable reference to an account by the given `account_id` if it's activated. - async fn get_account_mut(&self, account_id: u32) -> Option> { - let accounts = self.get_accounts_mutex().lock().await; - if !accounts.contains_key(&account_id) { - return None; - } - - Some(AsyncMutexGuard::map(accounts, |accounts| { - accounts - .get_mut(&account_id) - .expect("getting an element should never fail due to the checks above") - })) - } - - /// Returns copies of all activated accounts. - async fn get_accounts(&self) -> HDAccountsMap { self.get_accounts_mutex().lock().await.clone() } - - /// Returns a mutable reference to all activated accounts. - async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount> { self.get_accounts_mutex().lock().await } - - async fn remove_account_if_last(&self, account_id: u32) -> Option { - let mut x = self.get_accounts_mutex().lock().await; - // `BTreeMap::last_entry` is still unstable. - let (last_account_id, _) = x.iter().last()?; - if *last_account_id == account_id { - x.remove(&account_id) - } else { - None - } - } -} - -pub trait HDAccountOps: Send + Sync { - /// Returns a number of used addresses of this account - /// or an `InvalidBip44ChainError` error if the coin doesn't support the given `chain`. - fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult; - - /// Returns a derivation path of this account. - fn account_derivation_path(&self) -> DerivationPath; - - /// Returns an index of this account. - fn account_id(&self) -> u32; - - /// Returns true if the given address is known at this time. - fn is_address_activated(&self, chain: Bip44Chain, address_id: u32) -> MmResult { - let is_activated = address_id < self.known_addresses_number(chain)?; - Ok(is_activated) - } -} - -pub(crate) mod inner_impl { - use super::*; - - pub struct NewAddress { - pub address: HDAddress, - pub new_known_addresses_number: u32, - } - - /// Generates a new address without updating a corresponding number of used `hd_account` addresses. - pub async fn generate_new_address_immutable( - coin: &Coin, - hd_wallet: &Coin::HDWallet, - hd_account: &Coin::HDAccount, - chain: Bip44Chain, - ) -> MmResult, NewAddressDerivingError> - where - Coin: HDWalletCoinOps + ?Sized + Sync, - { - let known_addresses_number = hd_account.known_addresses_number(chain)?; - // Address IDs start from 0, so the `known_addresses_number = last_known_address_id + 1`. - let new_address_id = known_addresses_number; - let max_addresses_number = hd_wallet.address_limit(); - if new_address_id >= max_addresses_number { - return MmError::err(NewAddressDerivingError::AddressLimitReached { max_addresses_number }); - } - let address = coin.derive_address(hd_account, chain, new_address_id).await?; - Ok(NewAddress { - address, - new_known_addresses_number: known_addresses_number + 1, - }) - } -} diff --git a/mm2src/coins/hd_wallet/account_ops.rs b/mm2src/coins/hd_wallet/account_ops.rs new file mode 100644 index 0000000000..eeba0c15f2 --- /dev/null +++ b/mm2src/coins/hd_wallet/account_ops.rs @@ -0,0 +1,54 @@ +use super::{ExtendedPublicKeyOps, HDAddressOps, HDAddressesCache, InvalidBip44ChainError}; +use crypto::{Bip44Chain, DerivationPath, HDPathToAccount}; +use mm2_err_handle::prelude::*; + +/// `HDAccountOps` Trait +/// +/// Defines operations associated with an HD (Hierarchical Deterministic) account. +/// In the context of BIP-44 derivation paths, an HD account corresponds to the third level (`account'`) +/// in the structure `m / purpose' / coin_type' / account' / chain (or change) / address_index`. +/// This allows for segregating funds into different accounts under the same seed, +/// with each account having multiple chains (often representing internal and external addresses). +/// +/// Implementors of this trait should provide details about such HD account like its specific derivation path, known addresses, and its index. +pub trait HDAccountOps { + type HDAddress: HDAddressOps + Clone + Send; + /// Any type that represents an extended public key, whether it's secp256k1, ed25519, Schnorr, etc. + /// This type should implement the `ExtendedPublicKeyOps` trait. + type ExtendedPublicKey: ExtendedPublicKeyOps; + + /// A constructor for any type that implements `HDAccountOps`. + fn new( + account_id: u32, + account_extended_pubkey: Self::ExtendedPublicKey, + account_derivation_path: HDPathToAccount, + ) -> Self; + + /// Returns the limit on the number of addresses that can be added to an account. + fn address_limit(&self) -> u32; + + /// Returns the number of known addresses for this account for a specific chain/change + /// (internal/external) path. + fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult; + + /// Sets the number of known addresses for this account for a specific chain/change + /// (internal/external) path. + fn set_known_addresses_number(&mut self, chain: Bip44Chain, new_known_addresses_number: u32); + + /// Returns the derivation path associated with this account. + fn account_derivation_path(&self) -> DerivationPath; + + /// Returns the index of this account. + /// The account index is used as part of the derivation path, + /// following the pattern `m/purpose'/coin'/account'`. + fn account_id(&self) -> u32; + + /// Checks if a specific address is activated (known) for this account at the present time. + fn is_address_activated(&self, chain: Bip44Chain, address_id: u32) -> MmResult; + + /// Fetches the derived/cached addresses. + fn derived_addresses(&self) -> &HDAddressesCache; + + /// Fetches the extended public key associated with this account. + fn extended_pubkey(&self) -> &Self::ExtendedPublicKey; +} diff --git a/mm2src/coins/hd_wallet/address_ops.rs b/mm2src/coins/hd_wallet/address_ops.rs new file mode 100644 index 0000000000..45c38e717c --- /dev/null +++ b/mm2src/coins/hd_wallet/address_ops.rs @@ -0,0 +1,18 @@ +use bip32::DerivationPath; +use std::fmt::Display; +use std::hash::Hash; + +/// `HDAddressOps` Trait +/// +/// Defines operations associated with an HD (Hierarchical Deterministic) address. +/// In the context of BIP-44 derivation paths, an HD address corresponds to the fifth level (`address_index`) +/// in the structure `m / purpose' / coin_type' / account' / chain (or change) / address_index`. +/// This allows for managing individual addresses within a specific account and chain. +pub trait HDAddressOps { + type Address: Clone + Display + Eq + Hash + Send + Sync; + type Pubkey: Clone; + + fn address(&self) -> Self::Address; + fn pubkey(&self) -> Self::Pubkey; + fn derivation_path(&self) -> &DerivationPath; +} diff --git a/mm2src/coins/hd_wallet/coin_ops.rs b/mm2src/coins/hd_wallet/coin_ops.rs new file mode 100644 index 0000000000..6a70b8c037 --- /dev/null +++ b/mm2src/coins/hd_wallet/coin_ops.rs @@ -0,0 +1,241 @@ +use super::{inner_impl, AccountUpdatingError, AddressDerivingError, ExtendedPublicKeyOps, HDAccountOps, HDCoinAddress, + HDCoinExtendedPubkey, HDCoinHDAccount, HDCoinHDAddress, HDConfirmAddress, HDWalletOps, + NewAddressDeriveConfirmError, NewAddressDerivingError}; +use crate::hd_wallet::{HDAddressOps, HDWalletStorageOps, TrezorCoinError}; +use async_trait::async_trait; +use bip32::{ChildNumber, DerivationPath}; +use crypto::Bip44Chain; +use itertools::Itertools; +use mm2_err_handle::mm_error::{MmError, MmResult}; +use std::collections::HashMap; + +type AddressDerivingResult = MmResult; + +/// Unique identifier for an HD address within an account. +#[derive(Clone, Eq, Hash, PartialEq)] +pub struct HDAddressId { + pub chain: Bip44Chain, + pub address_id: u32, +} + +/// `HDWalletCoinOps` defines operations that coins should support to have HD wallet functionalities. +/// This trait outlines fundamental operations like address derivation, account creation, and more. +#[async_trait] +pub trait HDWalletCoinOps { + /// Any type that represents a Hierarchical Deterministic (HD) wallet. + type HDWallet: HDWalletOps + HDWalletStorageOps + Send + Sync; + + /// Returns a formatter function for address representation. + /// Useful when an address has multiple display formats. + /// For example, Ethereum addresses can be fully displayed or truncated. + /// By default, the formatter uses the Display trait of the address type, which truncates Ethereum addresses. + /// Implement this function if a different display format is required. + fn address_formatter(&self) -> fn(&HDCoinAddress) -> String { |address| address.to_string() } + + /// Derives an address for the coin that implements this trait from an extended public key and a derivation path. + fn address_from_extended_pubkey( + &self, + extended_pubkey: &HDCoinExtendedPubkey, + derivation_path: DerivationPath, + ) -> HDCoinHDAddress; + + /// Retrieves an HD address from the cache or derives it if it hasn't been derived yet. + fn derive_address_with_cache( + &self, + hd_account: &HDCoinHDAccount, + hd_addresses_cache: &mut HashMap>, + hd_address_id: HDAddressId, + ) -> AddressDerivingResult> { + // Check if the given HD address has been derived already. + if let Some(hd_address) = hd_addresses_cache.get(&hd_address_id) { + return Ok(hd_address.clone()); + } + + let change_child = hd_address_id.chain.to_child_number(); + let address_id_child = ChildNumber::from(hd_address_id.address_id); + + let derived_pubkey = hd_account + .extended_pubkey() + .derive_child(change_child)? + .derive_child(address_id_child)?; + + let mut derivation_path = hd_account.account_derivation_path(); + derivation_path.push(change_child); + derivation_path.push(address_id_child); + drop_mutability!(derivation_path); + let hd_address = self.address_from_extended_pubkey(&derived_pubkey, derivation_path); + + // Cache the derived `hd_address`. + hd_addresses_cache.insert(hd_address_id, hd_address.clone()); + Ok(hd_address) + } + + /// Derives a single HD address for a given account, chain, and address identifier. + async fn derive_address( + &self, + hd_account: &HDCoinHDAccount, + chain: Bip44Chain, + address_id: u32, + ) -> AddressDerivingResult> { + self.derive_addresses(hd_account, std::iter::once(HDAddressId { chain, address_id })) + .await? + .into_iter() + .exactly_one() + // Unfortunately, we can't use [`MapToMmResult::map_to_mm`] due to unsatisfied trait bounds, + // and it's easier to use [`Result::map_err`] instead of adding more trait bounds to this method. + .map_err(|e| MmError::new(AddressDerivingError::Internal(e.to_string()))) + } + + /// Derives a set of HD addresses for a coin using the specified HD account and address identifiers. + #[cfg(not(target_arch = "wasm32"))] + async fn derive_addresses( + &self, + hd_account: &HDCoinHDAccount, + address_ids: Ids, + ) -> AddressDerivingResult>> + where + Ids: Iterator + Send, + { + let mut hd_addresses_cache_guard = hd_account.derived_addresses().lock().await; + let hd_addresses_cache = &mut *hd_addresses_cache_guard; + address_ids + .map(|hd_address_id| self.derive_address_with_cache(hd_account, hd_addresses_cache, hd_address_id)) + .collect() + } + + // Todo: combine both implementations once worker threads are supported in WASM + /// [`HDWalletCoinOps::derive_addresses`] WASM implementation. + /// + /// # Important + /// + /// This function locks [`HDAddressesCache::cache`] mutex at each iteration. + /// + /// # Performance + /// + /// Locking the [`HDAddressesCache::cache`] mutex at each iteration may significantly degrade performance. + /// But this is required at least for now due the facts that: + /// 1) mm2 runs in the same thread as `KomodoPlatform/air_dex` runs; + /// 2) [`ExtendedPublicKey::derive_child`] is a synchronous operation, and it takes a long time. + /// So we need to periodically invoke Javascript runtime to handle UI events and other asynchronous tasks. + #[cfg(target_arch = "wasm32")] + async fn derive_addresses( + &self, + hd_account: &HDCoinHDAccount, + address_ids: Ids, + ) -> AddressDerivingResult>> + where + Ids: Iterator + Send, + { + let mut result = Vec::new(); + for hd_address_id in address_ids { + let mut hd_addresses_cache = hd_account.derived_addresses().lock().await; + + let hd_address = self.derive_address_with_cache(hd_account, &mut hd_addresses_cache, hd_address_id)?; + result.push(hd_address); + } + + Ok(result) + } + + /// Retrieves or derives known HD addresses for a specific account and chain. + /// Essentially, this retrieves addresses that have been interacted with in the past. + async fn derive_known_addresses( + &self, + hd_account: &HDCoinHDAccount, + chain: Bip44Chain, + ) -> AddressDerivingResult>> { + let known_addresses_number = hd_account.known_addresses_number(chain)?; + let address_ids = (0..known_addresses_number).map(|address_id| HDAddressId { chain, address_id }); + self.derive_addresses(hd_account, address_ids).await + } + + /// Generates a new address for a coin and updates the corresponding number of used `hd_account` addresses. + async fn generate_new_address( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, + ) -> MmResult, NewAddressDerivingError> { + let inner_impl::NewAddress { + hd_address: address, + new_known_addresses_number, + } = inner_impl::generate_new_address_immutable(self, hd_account, chain).await?; + + self.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) + .await?; + Ok(address) + } + + /// Generates a new address with an added confirmation step. + /// This method prompts the user to verify if the derived address matches + /// the hardware wallet display, ensuring security and accuracy when + /// dealing with hardware wallets. + async fn generate_and_confirm_new_address( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, + confirm_address: &ConfirmAddress, + ) -> MmResult, NewAddressDeriveConfirmError> + where + ConfirmAddress: HDConfirmAddress, + { + use super::inner_impl; + + let inner_impl::NewAddress { + hd_address, + new_known_addresses_number, + } = inner_impl::generate_new_address_immutable(self, hd_account, chain).await?; + + let trezor_coin = self.trezor_coin()?; + let derivation_path = hd_address.derivation_path().clone(); + let expected_address = hd_address.address().to_string(); + // Ask the user to confirm if the given `expected_address` is the same as on the HW display. + confirm_address + .confirm_address(trezor_coin, derivation_path, expected_address) + .await?; + + let actual_known_addresses_number = hd_account.known_addresses_number(chain)?; + // Check if the actual `known_addresses_number` hasn't been changed while we waited for the user confirmation. + // If the actual value is greater than the new one, we don't need to update. + if actual_known_addresses_number < new_known_addresses_number { + self.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) + .await?; + } + + Ok(hd_address) + } + + /// Updates the count of known addresses for a specified HD account and chain/change path. + /// This is useful for tracking the number of created addresses. + async fn set_known_addresses_number( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut HDCoinHDAccount, + chain: Bip44Chain, + new_known_addresses_number: u32, + ) -> MmResult<(), AccountUpdatingError> { + let max_addresses_number = hd_account.address_limit(); + if new_known_addresses_number >= max_addresses_number { + return MmError::err(AccountUpdatingError::AddressLimitReached { max_addresses_number }); + } + match chain { + Bip44Chain::External => { + hd_wallet + .update_external_addresses_number(hd_account.account_id(), new_known_addresses_number) + .await? + }, + Bip44Chain::Internal => { + hd_wallet + .update_internal_addresses_number(hd_account.account_id(), new_known_addresses_number) + .await? + }, + } + hd_account.set_known_addresses_number(chain, new_known_addresses_number); + + Ok(()) + } + + /// Returns the Trezor coin name for this coin. + fn trezor_coin(&self) -> MmResult; +} diff --git a/mm2src/coins/hd_confirm_address.rs b/mm2src/coins/hd_wallet/confirm_address.rs similarity index 69% rename from mm2src/coins/hd_confirm_address.rs rename to mm2src/coins/hd_wallet/confirm_address.rs index d6ee019855..39bb286322 100644 --- a/mm2src/coins/hd_confirm_address.rs +++ b/mm2src/coins/hd_wallet/confirm_address.rs @@ -2,12 +2,13 @@ use async_trait::async_trait; use bip32::DerivationPath; use crypto::hw_rpc_task::HwConnectStatuses; use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProcessor, TryIntoUserAction}; -use crypto::trezor::{ProcessTrezorResponse, TrezorError, TrezorProcessingError}; +use crypto::trezor::{ProcessTrezorResponse, TrezorError, TrezorMessageType, TrezorProcessingError}; use crypto::{CryptoCtx, CryptoCtxError, HardwareWalletArc, HwError, HwProcessingError}; -use enum_from::{EnumFromInner, EnumFromStringify}; +use enum_derives::{EnumFromInner, EnumFromStringify}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle}; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared}; +use std::sync::Arc; const SHOW_ADDRESS_ON_DISPLAY: bool = true; @@ -21,6 +22,7 @@ pub enum HDConfirmAddressError { expected: String, found: String, }, + NoAddressReceived, #[from_stringify("CryptoCtxError")] Internal(String), } @@ -43,12 +45,13 @@ impl From> for HDConfirmAddressError { match e { HwProcessingError::HwError(hw) => HDConfirmAddressError::from(hw), HwProcessingError::ProcessorError(rpc) => HDConfirmAddressError::RpcTaskError(rpc), + HwProcessingError::InternalError(err) => HDConfirmAddressError::Internal(err), } } } /// An `InProgress` status constructor. -pub trait ConfirmAddressStatus: Sized { +pub(crate) trait ConfirmAddressStatus: Sized { /// Returns an `InProgress` RPC status that will be used to ask the user /// to confirm an `address` on his HW device. fn confirm_addr_status(address: String) -> Self; @@ -58,30 +61,31 @@ pub trait ConfirmAddressStatus: Sized { #[async_trait] pub trait HDConfirmAddress: Sync { /// Asks the user to confirm if the given `expected_address` is the same as on the HW display. - async fn confirm_utxo_address( + async fn confirm_address( &self, - trezor_utxo_coin: String, + trezor_coin: String, derivation_path: DerivationPath, expected_address: String, ) -> MmResult<(), HDConfirmAddressError>; } -pub enum RpcTaskConfirmAddress<'task, Task: RpcTask> { +pub(crate) enum RpcTaskConfirmAddress { Trezor { hw_ctx: HardwareWalletArc, - task_handle: &'task RpcTaskHandle, + task_handle: RpcTaskHandleShared, statuses: HwConnectStatuses, + trezor_message_type: TrezorMessageType, }, } #[async_trait] -impl<'task, Task> HDConfirmAddress for RpcTaskConfirmAddress<'task, Task> +impl HDConfirmAddress for RpcTaskConfirmAddress where Task: RpcTask, Task::InProgressStatus: ConfirmAddressStatus, Task::UserAction: TryIntoUserAction + Send, { - async fn confirm_utxo_address( + async fn confirm_address( &self, trezor_utxo_coin: String, derivation_path: DerivationPath, @@ -92,14 +96,16 @@ where hw_ctx, task_handle, statuses, + trezor_message_type, } => { - Self::confirm_utxo_address_with_trezor( + Self::confirm_address_with_trezor( hw_ctx, - task_handle, + task_handle.clone(), statuses, trezor_utxo_coin, derivation_path, expected_address, + trezor_message_type, ) .await }, @@ -107,7 +113,7 @@ where } } -impl<'task, Task> RpcTaskConfirmAddress<'task, Task> +impl RpcTaskConfirmAddress where Task: RpcTask, Task::InProgressStatus: ConfirmAddressStatus, @@ -115,9 +121,10 @@ where { pub fn new( ctx: &MmArc, - task_handle: &'task RpcTaskHandle, + task_handle: RpcTaskHandleShared, statuses: HwConnectStatuses, - ) -> MmResult, HDConfirmAddressError> { + trezor_message_type: TrezorMessageType, + ) -> MmResult, HDConfirmAddressError> { let crypto_ctx = CryptoCtx::from_ctx(ctx)?; let hw_ctx = crypto_ctx .hw_ctx() @@ -126,30 +133,42 @@ where hw_ctx, task_handle, statuses, + trezor_message_type, }) } - async fn confirm_utxo_address_with_trezor( + async fn confirm_address_with_trezor( hw_ctx: &HardwareWalletArc, - task_handle: &RpcTaskHandle, + task_handle: RpcTaskHandleShared, connect_statuses: &HwConnectStatuses, trezor_coin: String, derivation_path: DerivationPath, expected_address: String, + trezor_message_type: &TrezorMessageType, ) -> MmResult<(), HDConfirmAddressError> { - let mut trezor_session = hw_ctx.trezor().await?; - let confirm_statuses = TrezorRequestStatuses { on_button_request: Task::InProgressStatus::confirm_addr_status(expected_address.clone()), ..connect_statuses.to_trezor_request_statuses() }; let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, confirm_statuses); - let address = trezor_session - .get_utxo_address(derivation_path, trezor_coin, SHOW_ADDRESS_ON_DISPLAY) - .await? - .process(&pubkey_processor) - .await?; + let pubkey_processor = Arc::new(pubkey_processor); + let mut trezor_session = hw_ctx.trezor(pubkey_processor.clone()).await?; + let address = match trezor_message_type { + TrezorMessageType::Bitcoin => { + trezor_session + .get_utxo_address(derivation_path, trezor_coin, SHOW_ADDRESS_ON_DISPLAY) + .await? + .process(pubkey_processor.clone()) + .await? + }, + TrezorMessageType::Ethereum => trezor_session + .get_eth_address(derivation_path, SHOW_ADDRESS_ON_DISPLAY) + .await? + .process(pubkey_processor.clone()) + .await? + .or_mm_err(|| HDConfirmAddressError::NoAddressReceived)?, + }; if address != expected_address { return MmError::err(HDConfirmAddressError::InvalidAddress { @@ -167,12 +186,12 @@ pub(crate) mod for_tests { use mocktopus::macros::mockable; #[derive(Default)] - pub struct MockableConfirmAddress; + pub(crate) struct MockableConfirmAddress; #[async_trait] #[mockable] impl HDConfirmAddress for MockableConfirmAddress { - async fn confirm_utxo_address( + async fn confirm_address( &self, _trezor_utxo_coin: String, _derivation_path: DerivationPath, diff --git a/mm2src/coins/hd_wallet/errors.rs b/mm2src/coins/hd_wallet/errors.rs new file mode 100644 index 0000000000..8b517bc609 --- /dev/null +++ b/mm2src/coins/hd_wallet/errors.rs @@ -0,0 +1,244 @@ +use super::{HDConfirmAddressError, HDWalletStorageError}; +use bip32::Error as Bip32Error; +use crypto::trezor::{TrezorError, TrezorProcessingError}; +use crypto::{Bip32DerPathError, Bip44Chain, CryptoCtxError, HwError, HwProcessingError, StandardHDPathError, XpubError}; +use rpc_task::RpcTaskError; + +#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum AddressDerivingError { + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { + chain: Bip44Chain, + }, + #[display(fmt = "BIP32 address deriving error: {}", _0)] + Bip32Error(String), + Internal(String), +} + +impl From for AddressDerivingError { + fn from(e: InvalidBip44ChainError) -> Self { AddressDerivingError::InvalidBip44Chain { chain: e.chain } } +} + +impl From for AddressDerivingError { + fn from(e: Bip32Error) -> Self { AddressDerivingError::Bip32Error(e.to_string()) } +} + +#[derive(Display)] +pub enum NewAddressDerivingError { + #[display(fmt = "Addresses limit reached. Max number of addresses: {}", max_addresses_number)] + AddressLimitReached { max_addresses_number: u32 }, + #[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] + InvalidBip44Chain { chain: Bip44Chain }, + #[display(fmt = "BIP32 address deriving error: {}", _0)] + Bip32Error(String), + #[display(fmt = "Wallet storage error: {}", _0)] + WalletStorageError(HDWalletStorageError), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for NewAddressDerivingError { + fn from(e: Bip32Error) -> Self { NewAddressDerivingError::Bip32Error(e.to_string()) } +} + +impl From for NewAddressDerivingError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::InvalidBip44Chain { chain } => NewAddressDerivingError::InvalidBip44Chain { chain }, + AddressDerivingError::Bip32Error(bip32) => NewAddressDerivingError::Bip32Error(bip32), + AddressDerivingError::Internal(internal) => NewAddressDerivingError::Internal(internal), + } + } +} + +impl From for NewAddressDerivingError { + fn from(e: InvalidBip44ChainError) -> Self { NewAddressDerivingError::InvalidBip44Chain { chain: e.chain } } +} + +impl From for NewAddressDerivingError { + fn from(e: AccountUpdatingError) -> Self { + match e { + AccountUpdatingError::AddressLimitReached { max_addresses_number } => { + NewAddressDerivingError::AddressLimitReached { max_addresses_number } + }, + AccountUpdatingError::InvalidBip44Chain(e) => NewAddressDerivingError::from(e), + AccountUpdatingError::WalletStorageError(storage) => NewAddressDerivingError::WalletStorageError(storage), + } + } +} + +pub enum NewAddressDeriveConfirmError { + DeriveError(NewAddressDerivingError), + ConfirmError(HDConfirmAddressError), +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: HDConfirmAddressError) -> Self { NewAddressDeriveConfirmError::ConfirmError(e) } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: NewAddressDerivingError) -> Self { NewAddressDeriveConfirmError::DeriveError(e) } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: AccountUpdatingError) -> Self { + NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) + } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: InvalidBip44ChainError) -> Self { + NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::from(e)) + } +} + +#[derive(Display)] +pub enum NewAccountCreationError { + #[display(fmt = "Hardware Wallet context is not initialized")] + HwContextNotInitialized, + #[display(fmt = "HD wallet is unavailable")] + HDWalletUnavailable, + #[display( + fmt = "Coin doesn't support Trezor hardware wallet. Please consider adding the 'trezor_coin' field to the coins config" + )] + CoinDoesntSupportTrezor, + RpcTaskError(RpcTaskError), + HardwareWalletError(HwError), + #[display(fmt = "Accounts limit reached. Max number of accounts: {}", max_accounts_number)] + AccountLimitReached { + max_accounts_number: u32, + }, + #[display(fmt = "Error saving HD account to storage: {}", _0)] + ErrorSavingAccountToStorage(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl From for NewAccountCreationError { + fn from(e: Bip32DerPathError) -> Self { + NewAccountCreationError::Internal(StandardHDPathError::from(e).to_string()) + } +} + +impl From for NewAccountCreationError { + fn from(e: HDWalletStorageError) -> Self { + match e { + HDWalletStorageError::ErrorSaving(e) | HDWalletStorageError::ErrorSerializing(e) => { + NewAccountCreationError::ErrorSavingAccountToStorage(e) + }, + HDWalletStorageError::HDWalletUnavailable => NewAccountCreationError::HDWalletUnavailable, + HDWalletStorageError::Internal(internal) => NewAccountCreationError::Internal(internal), + other => NewAccountCreationError::Internal(other.to_string()), + } + } +} + +/// Currently, we suppose that ETH/ERC20/QRC20 don't have [`Bip44Chain::Internal`] addresses. +#[derive(Display)] +#[display(fmt = "Coin doesn't support the given BIP44 chain: {:?}", chain)] +pub struct InvalidBip44ChainError { + pub chain: Bip44Chain, +} + +#[derive(Display)] +pub enum AccountUpdatingError { + AddressLimitReached { max_addresses_number: u32 }, + InvalidBip44Chain(InvalidBip44ChainError), + WalletStorageError(HDWalletStorageError), +} + +impl From for AccountUpdatingError { + fn from(e: InvalidBip44ChainError) -> Self { AccountUpdatingError::InvalidBip44Chain(e) } +} + +impl From for AccountUpdatingError { + fn from(e: HDWalletStorageError) -> Self { AccountUpdatingError::WalletStorageError(e) } +} + +#[derive(Display)] +pub enum HDWithdrawError { + UnexpectedFromAddress(String), + UnknownAccount { account_id: u32 }, + AddressDerivingError(AddressDerivingError), + InternalError(String), +} + +impl From for HDWithdrawError { + fn from(e: AddressDerivingError) -> Self { HDWithdrawError::AddressDerivingError(e) } +} + +#[derive(Clone)] +pub enum HDExtractPubkeyError { + HwContextNotInitialized, + CoinDoesntSupportTrezor, + RpcTaskError(RpcTaskError), + HardwareWalletError(HwError), + InvalidXpub(String), + Internal(String), +} + +impl From for HDExtractPubkeyError { + fn from(e: CryptoCtxError) -> Self { HDExtractPubkeyError::Internal(e.to_string()) } +} + +impl From for HDExtractPubkeyError { + fn from(e: TrezorError) -> Self { HDExtractPubkeyError::HardwareWalletError(HwError::from(e)) } +} + +impl From for HDExtractPubkeyError { + fn from(e: HwError) -> Self { HDExtractPubkeyError::HardwareWalletError(e) } +} + +impl From> for HDExtractPubkeyError { + fn from(e: TrezorProcessingError) -> Self { + match e { + TrezorProcessingError::TrezorError(trezor) => HDExtractPubkeyError::from(HwError::from(trezor)), + TrezorProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), + } + } +} + +impl From> for HDExtractPubkeyError { + fn from(e: HwProcessingError) -> Self { + match e { + HwProcessingError::HwError(hw) => HDExtractPubkeyError::from(hw), + HwProcessingError::ProcessorError(rpc) => HDExtractPubkeyError::RpcTaskError(rpc), + HwProcessingError::InternalError(internal) => HDExtractPubkeyError::Internal(internal), + } + } +} + +impl From for HDExtractPubkeyError { + fn from(e: XpubError) -> Self { HDExtractPubkeyError::InvalidXpub(e.to_string()) } +} + +impl From for NewAccountCreationError { + fn from(e: HDExtractPubkeyError) -> Self { + match e { + HDExtractPubkeyError::HwContextNotInitialized => NewAccountCreationError::HwContextNotInitialized, + HDExtractPubkeyError::CoinDoesntSupportTrezor => NewAccountCreationError::CoinDoesntSupportTrezor, + HDExtractPubkeyError::RpcTaskError(rpc) => NewAccountCreationError::RpcTaskError(rpc), + HDExtractPubkeyError::HardwareWalletError(hw) => NewAccountCreationError::HardwareWalletError(hw), + HDExtractPubkeyError::InvalidXpub(xpub) => { + NewAccountCreationError::HardwareWalletError(HwError::InvalidXpub(xpub)) + }, + HDExtractPubkeyError::Internal(internal) => NewAccountCreationError::Internal(internal), + } + } +} + +#[derive(Display)] +pub enum TrezorCoinError { + Internal(String), +} + +impl From for HDExtractPubkeyError { + fn from(e: TrezorCoinError) -> Self { HDExtractPubkeyError::Internal(e.to_string()) } +} + +impl From for NewAddressDeriveConfirmError { + fn from(e: TrezorCoinError) -> Self { + NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::Internal(e.to_string())) + } +} diff --git a/mm2src/coins/hd_wallet/mod.rs b/mm2src/coins/hd_wallet/mod.rs new file mode 100644 index 0000000000..1d77db3543 --- /dev/null +++ b/mm2src/coins/hd_wallet/mod.rs @@ -0,0 +1,523 @@ +use async_trait::async_trait; +use common::log::warn; +use crypto::{Bip32DerPathOps, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, HDPathToAccount, HDPathToCoin, + Secp256k1ExtendedPublicKey, StandardHDPath, StandardHDPathError}; +use futures::lock::{MappedMutexGuard as AsyncMappedMutexGuard, Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; +use mm2_err_handle::prelude::*; +use primitives::hash::H160; +use serde::Serialize; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::Display; +use std::hash::Hash; +use std::str::FromStr; +use std::sync::Arc; + +mod account_ops; +pub use account_ops::HDAccountOps; + +mod address_ops; +pub use address_ops::HDAddressOps; + +mod coin_ops; +pub use coin_ops::{HDAddressId, HDWalletCoinOps}; + +mod confirm_address; +#[cfg(test)] +pub(crate) use confirm_address::for_tests::MockableConfirmAddress; +pub(crate) use confirm_address::{ConfirmAddressStatus, RpcTaskConfirmAddress}; +pub use confirm_address::{HDConfirmAddress, HDConfirmAddressError}; + +mod errors; +pub use errors::{AccountUpdatingError, AddressDerivingError, HDExtractPubkeyError, HDWithdrawError, + InvalidBip44ChainError, NewAccountCreationError, NewAddressDeriveConfirmError, + NewAddressDerivingError, TrezorCoinError}; + +mod pubkey; +pub use pubkey::{ExtendedPublicKeyOps, ExtractExtendedPubkey, HDXPubExtractor, RpcTaskXPubExtractor}; + +mod storage; +#[cfg(target_arch = "wasm32")] +pub(crate) use storage::HDWalletDb; +#[cfg(test)] pub(crate) use storage::HDWalletMockStorage; +pub use storage::{HDAccountStorageItem, HDAccountStorageOps, HDWalletCoinStorage, HDWalletId, HDWalletStorageError, + HDWalletStorageOps}; +pub(crate) use storage::{HDWalletStorageInternalOps, HDWalletStorageResult}; + +mod wallet_ops; +pub use wallet_ops::HDWalletOps; + +mod withdraw_ops; +pub use withdraw_ops::{HDCoinWithdrawOps, WithdrawFrom, WithdrawSenderAddress}; + +pub(crate) type HDAccountsMap = BTreeMap; +pub(crate) type HDAccountsMutex = AsyncMutex>; +pub(crate) type HDAccountsMut<'a, HDAccount> = AsyncMutexGuard<'a, HDAccountsMap>; +pub(crate) type HDAccountMut<'a, HDAccount> = AsyncMappedMutexGuard<'a, HDAccountsMap, HDAccount>; +type HDWalletHDAddress = <::HDAccount as HDAccountOps>::HDAddress; +type HDCoinHDAddress = HDWalletHDAddress<::HDWallet>; +pub(crate) type HDWalletAddress = + <<::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Address; +pub(crate) type HDCoinAddress = HDWalletAddress<::HDWallet>; +type HDWalletExtendedPubkey = <::HDAccount as HDAccountOps>::ExtendedPublicKey; +pub(crate) type HDCoinExtendedPubkey = HDWalletExtendedPubkey<::HDWallet>; +pub(crate) type HDCoinHDAccount = HDWalletHDAccount<::HDWallet>; +type HDWalletHDAccount = ::HDAccount; + +pub(crate) const DEFAULT_GAP_LIMIT: u32 = 20; +const DEFAULT_ACCOUNT_LIMIT: u32 = ChildNumber::HARDENED_FLAG; +const DEFAULT_ADDRESS_LIMIT: u32 = ChildNumber::HARDENED_FLAG; +const DEFAULT_RECEIVER_CHAIN: Bip44Chain = Bip44Chain::External; + +/// A generic HD address that can be used with any HD wallet. +#[derive(Clone)] +pub struct HDAddress { + pub address: Address, + pub pubkey: Pubkey, + pub derivation_path: DerivationPath, +} + +impl HDAddressOps for HDAddress +where + Address: Clone + Display + Eq + Hash + Send + Sync, + Pubkey: Clone, +{ + type Address = Address; + type Pubkey = Pubkey; + + fn address(&self) -> Self::Address { self.address.clone() } + + fn pubkey(&self) -> Self::Pubkey { self.pubkey.clone() } + + fn derivation_path(&self) -> &DerivationPath { &self.derivation_path } +} + +/// A generic HD address that can be used with any HD wallet. +#[derive(Clone, Debug)] +pub struct HDAddressesCache { + cache: Arc>>, +} + +impl Default for HDAddressesCache { + fn default() -> Self { + HDAddressesCache { + cache: Arc::new(AsyncMutex::new(HashMap::new())), + } + } +} + +impl HDAddressesCache { + pub fn with_capacity(capacity: usize) -> Self { + HDAddressesCache { + cache: Arc::new(AsyncMutex::new(HashMap::with_capacity(capacity))), + } + } + + pub async fn lock(&self) -> AsyncMutexGuard<'_, HashMap> { self.cache.lock().await } +} + +/// A generic HD account that can be used with any HD wallet. +#[derive(Clone, Debug)] +pub struct HDAccount +where + HDAddress: HDAddressOps + Send, + ExtendedPublicKey: ExtendedPublicKeyOps, +{ + pub account_id: u32, + /// [Extended public key](https://learnmeabitcoin.com/technical/extended-keys) that corresponds to the derivation path: + /// `m/purpose'/coin_type'/account'`. + pub extended_pubkey: ExtendedPublicKey, + /// [`HDWallet::derivation_path`] derived by [`HDAccount::account_id`]. + pub account_derivation_path: HDPathToAccount, + /// The number of addresses that we know have been used by the user. + /// This is used in order not to check the transaction history for each address, + /// but to request the balance of addresses whose index is less than `address_number`. + pub external_addresses_number: u32, + /// The number of internal addresses that we know have been used by the user. + /// This is used in order not to check the transaction history for each address, + /// but to request the balance of addresses whose index is less than `address_number`. + pub internal_addresses_number: u32, + /// The cache of derived addresses. + /// This is used at [`HDWalletCoinOps::derive_address`]. + pub derived_addresses: HDAddressesCache, +} + +impl HDAccountOps for HDAccount +where + HDAddress: HDAddressOps + Clone + Send, + ExtendedPublicKey: ExtendedPublicKeyOps, +{ + type HDAddress = HDAddress; + type ExtendedPublicKey = ExtendedPublicKey; + + fn new( + account_id: u32, + extended_pubkey: Self::ExtendedPublicKey, + account_derivation_path: HDPathToAccount, + ) -> Self { + HDAccount { + account_id, + extended_pubkey, + account_derivation_path, + external_addresses_number: 0, + internal_addresses_number: 0, + derived_addresses: HDAddressesCache::default(), + } + } + + fn address_limit(&self) -> u32 { DEFAULT_ADDRESS_LIMIT } + + fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult { + match chain { + Bip44Chain::External => Ok(self.external_addresses_number), + Bip44Chain::Internal => Ok(self.internal_addresses_number), + } + } + + fn set_known_addresses_number(&mut self, chain: Bip44Chain, num: u32) { + match chain { + Bip44Chain::External => { + self.external_addresses_number = num; + }, + Bip44Chain::Internal => { + self.internal_addresses_number = num; + }, + } + } + + fn account_derivation_path(&self) -> DerivationPath { self.account_derivation_path.to_derivation_path() } + + fn account_id(&self) -> u32 { self.account_id } + + fn is_address_activated(&self, chain: Bip44Chain, address_id: u32) -> MmResult { + let is_activated = address_id < self.known_addresses_number(chain)?; + Ok(is_activated) + } + + fn derived_addresses(&self) -> &HDAddressesCache { &self.derived_addresses } + + fn extended_pubkey(&self) -> &Self::ExtendedPublicKey { &self.extended_pubkey } +} + +impl HDAccountStorageOps for HDAccount +where + HDAddress: HDAddressOps + Send, + ExtendedPublicKey: ExtendedPublicKeyOps, + ::Err: Display, +{ + fn try_from_storage_item( + wallet_der_path: &HDPathToCoin, + account_info: &HDAccountStorageItem, + ) -> HDWalletStorageResult + where + Self: Sized, + { + const ACCOUNT_CHILD_HARDENED: bool = true; + + let account_child = ChildNumber::new(account_info.account_id, ACCOUNT_CHILD_HARDENED)?; + let account_derivation_path = wallet_der_path + .derive(account_child) + .map_to_mm(StandardHDPathError::from)?; + let extended_pubkey = ExtendedPublicKey::from_str(&account_info.account_xpub) + .map_err(|e| HDWalletStorageError::ErrorDeserializing(e.to_string()))?; + let capacity = + account_info.external_addresses_number + account_info.internal_addresses_number + DEFAULT_GAP_LIMIT; + Ok(HDAccount { + account_id: account_info.account_id, + extended_pubkey, + account_derivation_path, + external_addresses_number: account_info.external_addresses_number, + internal_addresses_number: account_info.internal_addresses_number, + derived_addresses: HDAddressesCache::with_capacity(capacity as usize), + }) + } + + fn to_storage_item(&self) -> HDAccountStorageItem { + HDAccountStorageItem { + account_id: self.account_id, + account_xpub: self.extended_pubkey.to_string(bip32::Prefix::XPUB), + external_addresses_number: self.external_addresses_number, + internal_addresses_number: self.internal_addresses_number, + } + } +} + +pub async fn load_hd_accounts_from_storage( + hd_wallet_storage: &HDWalletCoinStorage, + derivation_path: &HDPathToCoin, +) -> HDWalletStorageResult>> +where + HDAddress: HDAddressOps + Send, + ExtendedPublicKey: ExtendedPublicKeyOps, + ::Err: Display, +{ + let accounts = hd_wallet_storage.load_all_accounts().await?; + let res: HDWalletStorageResult>> = accounts + .iter() + .map(|account_info| { + let account = HDAccount::try_from_storage_item(derivation_path, account_info)?; + Ok((account.account_id, account)) + }) + .collect(); + match res { + Ok(accounts) => Ok(accounts), + Err(e) if e.get_inner().is_deserializing_err() => { + warn!("Error loading HD accounts from the storage: '{}'. Clear accounts", e); + hd_wallet_storage.clear_accounts().await?; + Ok(HDAccountsMap::new()) + }, + Err(e) => Err(e), + } +} + +/// Represents a Hierarchical Deterministic (HD) wallet for UTXO coins. +/// This struct encapsulates all the necessary data for HD wallet operations +/// and is initialized whenever a utxo coin is activated in HD wallet mode. +#[derive(Debug)] +pub struct HDWallet +where + HDAccount: HDAccountOps + Clone + Send + Sync, +{ + /// A unique identifier for the HD wallet derived from the master public key. + /// Specifically, it's the RIPEMD160 hash of the SHA256 hash of the master pubkey. + /// This property aids in storing database items uniquely for each HD wallet. + pub hd_wallet_rmd160: H160, + /// Provides a means to access database operations for a specific user, HD wallet, and coin. + /// The storage wrapper associates with the `coin` and `hd_wallet_rmd160` to provide unique storage access. + pub hd_wallet_storage: HDWalletCoinStorage, + /// Derivation path of the coin. + /// This derivation path consists of `purpose` and `coin_type` only + /// where the full `BIP44` address has the following structure: + /// `m/purpose'/coin_type'/account'/change/address_index`. + pub derivation_path: HDPathToCoin, + /// Contains information about the accounts enabled for this HD wallet. + pub accounts: HDAccountsMutex, + // Todo: This should be removed in the future to enable simultaneous swaps from multiple addresses + /// The address that's specifically enabled for certain operations, e.g. swaps. + pub enabled_address: HDPathAccountToAddressId, + /// Defines the maximum number of consecutive addresses that can be generated + /// without any associated transactions. If an address outside this limit + /// receives transactions, they won't be identified. + pub gap_limit: u32, +} + +#[async_trait] +impl HDWalletOps for HDWallet +where + HDAccount: HDAccountOps + Clone + Send + Sync, +{ + type HDAccount = HDAccount; + + fn coin_type(&self) -> u32 { self.derivation_path.coin_type() } + + fn derivation_path(&self) -> &HDPathToCoin { &self.derivation_path } + + fn gap_limit(&self) -> u32 { self.gap_limit } + + fn account_limit(&self) -> u32 { DEFAULT_ACCOUNT_LIMIT } + + fn default_receiver_chain(&self) -> Bip44Chain { DEFAULT_RECEIVER_CHAIN } + + fn get_accounts_mutex(&self) -> &HDAccountsMutex { &self.accounts } + + async fn get_account(&self, account_id: u32) -> Option { + let accounts = self.get_accounts_mutex().lock().await; + accounts.get(&account_id).cloned() + } + + async fn get_account_mut(&self, account_id: u32) -> Option> { + let accounts = self.get_accounts_mutex().lock().await; + if !accounts.contains_key(&account_id) { + return None; + } + + Some(AsyncMutexGuard::map(accounts, |accounts| { + accounts + .get_mut(&account_id) + .expect("getting an element should never fail due to the checks above") + })) + } + + async fn get_accounts(&self) -> HDAccountsMap { self.get_accounts_mutex().lock().await.clone() } + + async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount> { self.get_accounts_mutex().lock().await } + + async fn remove_account_if_last(&self, account_id: u32) -> Option { + let mut x = self.get_accounts_mutex().lock().await; + // `BTreeMap::last_entry` is still unstable. + let (last_account_id, _) = x.iter().last()?; + if *last_account_id == account_id { + x.remove(&account_id) + } else { + None + } + } + + async fn get_enabled_address(&self) -> Option<::HDAddress> { + let enabled_address = self.enabled_address; + let account = self.get_account(enabled_address.account_id).await?; + let hd_address_id = HDAddressId { + chain: enabled_address.chain, + address_id: enabled_address.address_id, + }; + let derived = account.derived_addresses().lock().await; + + let address = derived.get(&hd_address_id); + address.cloned() + } +} + +/// Creates and registers a new HD account for a HDWallet. +/// +/// # Parameters +/// - `coin`: A coin that implements [`ExtractExtendedPubkey`]. +/// - `hd_wallet`: The specified HD wallet. +/// - `xpub_extractor`: Optional method for extracting the extended public key. +/// This is especially useful when dealing with hardware wallets. It can +/// allow for the extraction of the extended public key directly from the +/// wallet when needed. +/// - `account_id`: Optional account identifier. +/// +/// # Returns +/// A result containing a mutable reference to the created `HDAccount` if successful. +pub async fn create_new_account<'a, Coin, XPubExtractor, HDWallet, HDAccount>( + coin: &Coin, + hd_wallet: &'a HDWallet, + xpub_extractor: Option, + account_id: Option, +) -> MmResult>, NewAccountCreationError> +where + Coin: ExtractExtendedPubkey> + Sync, + HDWallet: HDWalletOps + HDWalletStorageOps + Sync, + XPubExtractor: HDXPubExtractor + Send, + HDAccount: 'a + HDAccountOps + HDAccountStorageOps, +{ + const INIT_ACCOUNT_ID: u32 = 0; + let new_account_id = match account_id { + Some(account_id) => account_id, + None => { + let accounts = hd_wallet.get_accounts_mut().await; + let last_account_id = accounts.iter().last().map(|(account_id, _account)| *account_id); + last_account_id.map_or(INIT_ACCOUNT_ID, |last_id| { + (INIT_ACCOUNT_ID..=last_id) + .find(|id| !accounts.contains_key(id)) + .unwrap_or(last_id + 1) + }) + }, + }; + let max_accounts_number = hd_wallet.account_limit(); + if new_account_id >= max_accounts_number { + return MmError::err(NewAccountCreationError::AccountLimitReached { max_accounts_number }); + } + + let account_child_hardened = true; + let account_child = ChildNumber::new(new_account_id, account_child_hardened) + .map_to_mm(|e| NewAccountCreationError::Internal(e.to_string()))?; + + let account_derivation_path: HDPathToAccount = hd_wallet.derivation_path().derive(account_child)?; + let account_pubkey = coin + .extract_extended_pubkey(xpub_extractor, account_derivation_path.to_derivation_path()) + .await?; + + let new_account = HDAccount::new(new_account_id, account_pubkey, account_derivation_path); + + let accounts = hd_wallet.get_accounts_mut().await; + if accounts.contains_key(&new_account_id) { + let error = format!( + "Account '{}' has been activated while we proceed the 'create_new_account' function", + new_account_id + ); + return MmError::err(NewAccountCreationError::Internal(error)); + } + + hd_wallet.upload_new_account(new_account.to_storage_item()).await?; + + Ok(AsyncMutexGuard::map(accounts, |accounts| { + accounts + .entry(new_account_id) + // the `entry` method should return [`Entry::Vacant`] due to the checks above + .or_insert(new_account) + })) +} + +#[async_trait] +impl HDWalletStorageOps for HDWallet +where + HDAccount: HDAccountOps + HDAccountStorageOps + Clone + Send + Sync, +{ + fn hd_wallet_storage(&self) -> &HDWalletCoinStorage { &self.hd_wallet_storage } +} + +/// Unique identifier for an HD wallet address within the whole wallet context. +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +pub struct HDPathAccountToAddressId { + pub account_id: u32, + pub chain: Bip44Chain, + pub address_id: u32, +} + +impl Default for HDPathAccountToAddressId { + fn default() -> Self { + HDPathAccountToAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 0, + } + } +} + +impl From for HDPathAccountToAddressId { + fn from(der_path: StandardHDPath) -> Self { + HDPathAccountToAddressId { + account_id: der_path.account_id(), + chain: der_path.chain(), + address_id: der_path.address_id(), + } + } +} + +impl HDPathAccountToAddressId { + pub fn to_derivation_path(&self, path_to_coin: &HDPathToCoin) -> Result> { + let mut account_der_path = path_to_coin.to_derivation_path(); + account_der_path.push(ChildNumber::new(self.account_id, true)?); + account_der_path.push(self.chain.to_child_number()); + account_der_path.push(ChildNumber::new(self.address_id, false)?); + + Ok(account_der_path) + } +} + +pub(crate) mod inner_impl { + use super::*; + use coin_ops::HDWalletCoinOps; + + pub struct NewAddress + where + HDAddress: HDAddressOps, + { + pub hd_address: HDAddress, + pub new_known_addresses_number: u32, + } + + /// Generates a new address without updating a corresponding number of used `hd_account` addresses. + pub async fn generate_new_address_immutable( + coin: &Coin, + hd_account: &HDCoinHDAccount, + chain: Bip44Chain, + ) -> MmResult>, NewAddressDerivingError> + where + Coin: HDWalletCoinOps + ?Sized + Sync, + { + let known_addresses_number = hd_account.known_addresses_number(chain)?; + // Address IDs start from 0, so the `known_addresses_number = last_known_address_id + 1`. + let new_address_id = known_addresses_number; + let max_addresses_number = hd_account.address_limit(); + if new_address_id >= max_addresses_number { + return MmError::err(NewAddressDerivingError::AddressLimitReached { max_addresses_number }); + } + let address = coin.derive_address(hd_account, chain, new_address_id).await?; + Ok(NewAddress { + hd_address: address, + new_known_addresses_number: known_addresses_number + 1, + }) + } +} diff --git a/mm2src/coins/hd_wallet/pubkey.rs b/mm2src/coins/hd_wallet/pubkey.rs new file mode 100644 index 0000000000..7732819295 --- /dev/null +++ b/mm2src/coins/hd_wallet/pubkey.rs @@ -0,0 +1,201 @@ +use crate::CoinProtocol; + +use super::*; +use async_trait::async_trait; +use bip32::Prefix; +use crypto::hw_rpc_task::HwConnectStatuses; +use crypto::trezor::trezor_rpc_task::{TrezorRpcTaskProcessor, TryIntoUserAction}; +use crypto::trezor::utxo::IGNORE_XPUB_MAGIC; +use crypto::trezor::ProcessTrezorResponse; +use crypto::trezor::TrezorMessageType; +use crypto::{CryptoCtx, DerivationPath, EcdsaCurve, HardwareWalletArc, XPub, XPubConverter}; +use mm2_core::mm_ctx::MmArc; +use rpc_task::{RpcTask, RpcTaskHandleShared}; +use std::sync::Arc; + +const SHOW_PUBKEY_ON_DISPLAY: bool = false; + +/// A trait that should be implemented by any extended public key type +/// to allow it to work with the HD wallet traits. +pub trait ExtendedPublicKeyOps: FromStr + Sized { + /// Derives a child extended public key from the current one. + fn derive_child(&self, child_number: ChildNumber) -> Result; + /// Converts the extended public key to a string. + fn to_string(&self, prefix: Prefix) -> String; +} + +impl ExtendedPublicKeyOps for Secp256k1ExtendedPublicKey { + fn derive_child(&self, child_number: ChildNumber) -> Result { self.derive_child(child_number) } + + fn to_string(&self, prefix: Prefix) -> String { self.to_string(prefix) } +} + +/// This trait should be implemented for coins +/// to support extracting extended public keys from any depth. +/// The extraction can be from either an internal or external wallet. +#[async_trait] +pub trait ExtractExtendedPubkey { + type ExtendedPublicKey; + + async fn extract_extended_pubkey( + &self, + xpub_extractor: Option, + derivation_path: DerivationPath, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Send; +} + +/// A trait for extracting an extended public key from an external source. +#[async_trait] +pub trait HDXPubExtractor: Sync { + async fn extract_xpub( + &self, + trezor_coin: String, + derivation_path: DerivationPath, + ) -> MmResult; +} + +/// The task for extracting an extended public key from an external source. +pub enum RpcTaskXPubExtractor { + Trezor { + hw_ctx: HardwareWalletArc, + task_handle: RpcTaskHandleShared, + statuses: HwConnectStatuses, + trezor_message_type: TrezorMessageType, + }, +} + +#[async_trait] +impl HDXPubExtractor for RpcTaskXPubExtractor +where + Task: RpcTask, + Task::UserAction: TryIntoUserAction + Send, +{ + async fn extract_xpub( + &self, + trezor_coin: String, + derivation_path: DerivationPath, + ) -> MmResult { + match self { + RpcTaskXPubExtractor::Trezor { + hw_ctx, + task_handle, + statuses, + trezor_message_type, + } => match trezor_message_type { + TrezorMessageType::Bitcoin => { + Self::extract_utxo_xpub_from_trezor( + hw_ctx, + task_handle.clone(), + statuses, + trezor_coin, + derivation_path, + ) + .await + }, + TrezorMessageType::Ethereum => { + Self::extract_eth_xpub_from_trezor(hw_ctx, task_handle.clone(), statuses, derivation_path).await + }, + }, + } + } +} + +impl RpcTaskXPubExtractor +where + Task: RpcTask, + Task::UserAction: TryIntoUserAction + Send, +{ + pub fn new_trezor_extractor( + ctx: &MmArc, + task_handle: RpcTaskHandleShared, + statuses: HwConnectStatuses, + coin_protocol: CoinProtocol, + ) -> MmResult, HDExtractPubkeyError> { + let crypto_ctx = CryptoCtx::from_ctx(ctx)?; + let hw_ctx = crypto_ctx + .hw_ctx() + .or_mm_err(|| HDExtractPubkeyError::HwContextNotInitialized)?; + + let trezor_message_type = match coin_protocol { + CoinProtocol::UTXO => TrezorMessageType::Bitcoin, + CoinProtocol::QTUM => TrezorMessageType::Bitcoin, + CoinProtocol::ETH | CoinProtocol::ERC20 { .. } => TrezorMessageType::Ethereum, + _ => return Err(MmError::new(HDExtractPubkeyError::CoinDoesntSupportTrezor)), + }; + Ok(RpcTaskXPubExtractor::Trezor { + hw_ctx, + task_handle, + statuses, + trezor_message_type, + }) + } + + async fn extract_utxo_xpub_from_trezor( + hw_ctx: &HardwareWalletArc, + task_handle: RpcTaskHandleShared, + statuses: &HwConnectStatuses, + trezor_coin: String, + derivation_path: DerivationPath, + ) -> MmResult { + let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, statuses.to_trezor_request_statuses()); + let pubkey_processor = Arc::new(pubkey_processor); + let mut trezor_session = hw_ctx.trezor(pubkey_processor.clone()).await?; + let xpub = trezor_session + .get_public_key( + derivation_path, + trezor_coin, + EcdsaCurve::Secp256k1, + SHOW_PUBKEY_ON_DISPLAY, + IGNORE_XPUB_MAGIC, + ) + .await? + .process(pubkey_processor.clone()) + .await?; + // Despite we pass `IGNORE_XPUB_MAGIC` to the [`TrezorSession::get_public_key`] method, + // Trezor sometimes returns pubkeys with magic prefixes like `dgub` prefix for DOGE coin. + // So we need to replace the magic prefix manually. + XPubConverter::replace_magic_prefix(xpub).mm_err(HDExtractPubkeyError::from) + } + + async fn extract_eth_xpub_from_trezor( + hw_ctx: &HardwareWalletArc, + task_handle: RpcTaskHandleShared, + statuses: &HwConnectStatuses, + derivation_path: DerivationPath, + ) -> MmResult { + let pubkey_processor = TrezorRpcTaskProcessor::new(task_handle, statuses.to_trezor_request_statuses()); + let pubkey_processor = Arc::new(pubkey_processor); + let mut trezor_session = hw_ctx.trezor(pubkey_processor.clone()).await?; + trezor_session + .get_eth_public_key(&derivation_path, SHOW_PUBKEY_ON_DISPLAY) + .await? + .process(pubkey_processor) + .await + .mm_err(HDExtractPubkeyError::from) + } +} + +/// This is a wrapper over `XPubExtractor`. The main goal of this structure is to allow construction of an Xpub extractor +/// even if HD wallet is not supported. But if someone tries to extract an Xpub despite HD wallet is not supported, +/// it fails with an inner `HDExtractPubkeyError` error. +pub struct XPubExtractorUnchecked(MmResult); + +#[async_trait] +impl HDXPubExtractor for XPubExtractorUnchecked +where + XPubExtractor: HDXPubExtractor + Send + Sync, +{ + async fn extract_xpub( + &self, + trezor_coin: String, + derivation_path: DerivationPath, + ) -> MmResult { + self.0 + .as_ref() + .map_err(Clone::clone)? + .extract_xpub(trezor_coin, derivation_path) + .await + } +} diff --git a/mm2src/coins/hd_wallet_storage/mock_storage.rs b/mm2src/coins/hd_wallet/storage/mock_storage.rs similarity index 90% rename from mm2src/coins/hd_wallet_storage/mock_storage.rs rename to mm2src/coins/hd_wallet/storage/mock_storage.rs index 2fbbc19f4c..8086e58be8 100644 --- a/mm2src/coins/hd_wallet_storage/mock_storage.rs +++ b/mm2src/coins/hd_wallet/storage/mock_storage.rs @@ -1,9 +1,9 @@ -use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageInternalOps, HDWalletStorageResult}; +use crate::hd_wallet::{HDAccountStorageItem, HDWalletId, HDWalletStorageInternalOps, HDWalletStorageResult}; use async_trait::async_trait; use mm2_core::mm_ctx::MmArc; #[cfg(test)] use mocktopus::macros::*; -pub struct HDWalletMockStorage; +pub(crate) struct HDWalletMockStorage; #[async_trait] #[cfg_attr(test, mockable)] diff --git a/mm2src/coins/hd_wallet_storage/mod.rs b/mm2src/coins/hd_wallet/storage/mod.rs similarity index 87% rename from mm2src/coins/hd_wallet_storage/mod.rs rename to mm2src/coins/hd_wallet/storage/mod.rs index 2c52cf3895..cb8f11b01d 100644 --- a/mm2src/coins/hd_wallet_storage/mod.rs +++ b/mm2src/coins/hd_wallet/storage/mod.rs @@ -1,6 +1,5 @@ -use crate::hd_wallet::HDWalletCoinOps; use async_trait::async_trait; -use crypto::{CryptoCtx, CryptoCtxError, XPub}; +use crypto::{Bip32Error, CryptoCtx, CryptoCtxError, HDPathToCoin, StandardHDPathError, XPub}; use derive_more::Display; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; @@ -16,19 +15,18 @@ use std::ops::Deref; #[cfg(any(test, target_arch = "wasm32"))] mod mock_storage; #[cfg(any(test, target_arch = "wasm32"))] -pub use mock_storage::HDWalletMockStorage; +pub(crate) use mock_storage::HDWalletMockStorage; cfg_wasm32! { use wasm_storage::HDWalletIndexedDbStorage as HDWalletStorageInstance; - - pub use wasm_storage::{HDWalletDb, HDWalletDbLocked}; + pub(crate) use wasm_storage::HDWalletDb; } cfg_native! { use sqlite_storage::HDWalletSqliteStorage as HDWalletStorageInstance; } -pub type HDWalletStorageResult = MmResult; +pub(crate) type HDWalletStorageResult = MmResult; type HDWalletStorageBoxed = Box; #[derive(Debug, Display)] @@ -49,10 +47,18 @@ pub enum HDWalletStorageError { Internal(String), } +impl From for HDWalletStorageError { + fn from(e: Bip32Error) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) } +} + impl From for HDWalletStorageError { fn from(e: CryptoCtxError) -> Self { HDWalletStorageError::Internal(e.to_string()) } } +impl From for HDWalletStorageError { + fn from(e: StandardHDPathError) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) } +} + impl HDWalletStorageError { pub fn is_deserializing_err(&self) -> bool { matches!(self, HDWalletStorageError::ErrorDeserializing(_)) } } @@ -85,7 +91,7 @@ pub struct HDAccountStorageItem { #[async_trait] #[cfg_attr(test, mockable)] -pub trait HDWalletStorageInternalOps { +pub(crate) trait HDWalletStorageInternalOps { async fn init(ctx: &MmArc) -> HDWalletStorageResult where Self: Sized; @@ -121,69 +127,81 @@ pub trait HDWalletStorageInternalOps { async fn clear_accounts(&self, wallet_id: HDWalletId) -> HDWalletStorageResult<()>; } +/// `HDWalletStorageOps` is a trait that allows us to interact with the storage implementation of the HD wallet. #[async_trait] -pub trait HDWalletCoinWithStorageOps: HDWalletCoinOps { - fn hd_wallet_storage<'a>(&self, hd_wallet: &'a Self::HDWallet) -> &'a HDWalletCoinStorage; +pub trait HDWalletStorageOps { + /// Getter for the HD wallet storage. + fn hd_wallet_storage(&self) -> &HDWalletCoinStorage; - async fn load_all_accounts(&self, hd_wallet: &Self::HDWallet) -> HDWalletStorageResult> { - let storage = self.hd_wallet_storage(hd_wallet); + /// Loads all accounts from the HD wallet storage. + async fn load_all_accounts(&self) -> HDWalletStorageResult> { + let storage = self.hd_wallet_storage(); storage.load_all_accounts().await } - async fn load_account( - &self, - hd_wallet: &Self::HDWallet, - account_id: u32, - ) -> HDWalletStorageResult> { - let storage = self.hd_wallet_storage(hd_wallet); + /// Loads a specific account from the HD wallet storage. + async fn load_account(&self, account_id: u32) -> HDWalletStorageResult> { + let storage = self.hd_wallet_storage(); storage.load_account(account_id).await } + /// Updates the number of external addresses for a specific account. async fn update_external_addresses_number( &self, - hd_wallet: &Self::HDWallet, account_id: u32, new_external_addresses_number: u32, ) -> HDWalletStorageResult<()> { - let storage = self.hd_wallet_storage(hd_wallet); + let storage = self.hd_wallet_storage(); storage .update_external_addresses_number(account_id, new_external_addresses_number) .await } + /// Updates the number of internal addresses for a specific account. async fn update_internal_addresses_number( &self, - hd_wallet: &Self::HDWallet, account_id: u32, new_internal_addresses_number: u32, ) -> HDWalletStorageResult<()> { - let storage = self.hd_wallet_storage(hd_wallet); + let storage = self.hd_wallet_storage(); storage .update_internal_addresses_number(account_id, new_internal_addresses_number) .await } - async fn upload_new_account( - &self, - hd_wallet: &Self::HDWallet, - account_info: HDAccountStorageItem, - ) -> HDWalletStorageResult<()> { - let storage = self.hd_wallet_storage(hd_wallet); + /// Saves new account details to the HD wallet storage. + async fn upload_new_account(&self, account_info: HDAccountStorageItem) -> HDWalletStorageResult<()> { + let storage = self.hd_wallet_storage(); storage.upload_new_account(account_info).await } - async fn clear_accounts(&self, hd_wallet: &Self::HDWallet) -> HDWalletStorageResult<()> { - let storage = self.hd_wallet_storage(hd_wallet); + /// Deletes all accounts from the HD wallet storage. + async fn clear_accounts(&self) -> HDWalletStorageResult<()> { + let storage = self.hd_wallet_storage(); storage.clear_accounts().await } } +/// `HDAccountStorageOps` is a trait that allows us to convert `HDAccountStorageItem` to whatever implements this trait and vice versa. +pub trait HDAccountStorageOps { + /// Converts `HDAccountStorageItem` to whatever implements this trait. + fn try_from_storage_item( + wallet_der_path: &HDPathToCoin, + account_info: &HDAccountStorageItem, + ) -> HDWalletStorageResult + where + Self: Sized; + + /// Converts whatever implements this trait to `HDAccountStorageItem`. + fn to_storage_item(&self) -> HDAccountStorageItem; +} + /// The wrapper over the [`HDWalletStorage::inner`] database implementation. /// It's associated with a specific mm2 user, HD wallet and coin. pub struct HDWalletCoinStorage { coin: String, /// RIPEMD160(SHA256(x)) where x is a pubkey extracted from a Hardware Wallet device or passphrase. - /// This property allows us to store DB items that are unique to each Hardware Wallet device. + /// This property allows us to store DB items that are unique to each Hardware Wallet device or HD wallet. hd_wallet_rmd160: H160, inner: HDWalletStorageBoxed, } @@ -222,7 +240,6 @@ impl HDWalletCoinStorage { }) } - #[cfg(any(test, target_arch = "wasm32"))] pub async fn init_with_rmd160( ctx: &MmArc, coin: String, @@ -291,14 +308,14 @@ mod tests { use primitives::hash::H160; cfg_wasm32! { - use crate::hd_wallet_storage::wasm_storage::get_all_storage_items; + use wasm_storage::get_all_storage_items; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); } cfg_native! { - use crate::hd_wallet_storage::sqlite_storage::get_all_storage_items; + use sqlite_storage::get_all_storage_items; use common::block_on; } diff --git a/mm2src/coins/hd_wallet_storage/sqlite_storage.rs b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs similarity index 97% rename from mm2src/coins/hd_wallet_storage/sqlite_storage.rs rename to mm2src/coins/hd_wallet/storage/sqlite_storage.rs index 7d015b3d29..898f4c8823 100644 --- a/mm2src/coins/hd_wallet_storage/sqlite_storage.rs +++ b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs @@ -1,7 +1,7 @@ #![allow(deprecated)] // TODO: remove this once rusqlite is >= 0.29 -use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, - HDWalletStorageResult}; +use crate::hd_wallet::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, + HDWalletStorageResult}; use async_trait::async_trait; use common::async_blocking; use db_common::owned_named_params; @@ -91,7 +91,7 @@ impl HDWalletId { } #[derive(Clone)] -pub struct HDWalletSqliteStorage { +pub(super) struct HDWalletSqliteStorage { conn: SqliteConnWeak, } @@ -275,7 +275,7 @@ enum UpdatingProperty { /// This function is used in `hd_wallet_storage::tests`. #[cfg(test)] -pub(super) async fn get_all_storage_items(ctx: &MmArc) -> Vec { +pub(crate) async fn get_all_storage_items(ctx: &MmArc) -> Vec { const SELECT_ALL_ACCOUNTS: &str = "SELECT account_id, account_xpub, external_addresses_number, internal_addresses_number FROM hd_account"; diff --git a/mm2src/coins/hd_wallet_storage/wasm_storage.rs b/mm2src/coins/hd_wallet/storage/wasm_storage.rs similarity index 96% rename from mm2src/coins/hd_wallet_storage/wasm_storage.rs rename to mm2src/coins/hd_wallet/storage/wasm_storage.rs index 76ad67494f..4654474236 100644 --- a/mm2src/coins/hd_wallet_storage/wasm_storage.rs +++ b/mm2src/coins/hd_wallet/storage/wasm_storage.rs @@ -1,5 +1,5 @@ -use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, - HDWalletStorageResult}; +use crate::hd_wallet::{HDAccountStorageItem, HDWalletId, HDWalletStorageError, HDWalletStorageInternalOps, + HDWalletStorageResult}; use crate::CoinsContext; use async_trait::async_trait; use crypto::XPub; @@ -21,7 +21,7 @@ const WALLET_ID_INDEX: &str = "wallet_id"; /// * account_id - HD account id const WALLET_ACCOUNT_ID_INDEX: &str = "wallet_account_id"; -pub type HDWalletDbLocked<'a> = DbLocked<'a, HDWalletDb>; +type HDWalletDbLocked<'a> = DbLocked<'a, HDWalletDb>; impl From for HDWalletStorageError { fn from(e: DbTransactionError) -> Self { @@ -77,7 +77,7 @@ impl From for HDWalletStorageError { /// and one unique multi-index `wallet_account_id` that consists of these four indexes in a row. /// See [`HDAccountTable::on_update_needed`]. #[derive(Deserialize, Serialize)] -pub struct HDAccountTable { +struct HDAccountTable { /// [`HDWalletId::coin`]. /// Non-unique index that is used to fetch/remove items from the storage. coin: String, @@ -94,11 +94,11 @@ pub struct HDAccountTable { } impl TableSignature for HDAccountTable { - fn table_name() -> &'static str { "hd_account" } + const TABLE_NAME: &'static str = "hd_account"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if let (0, 1) = (old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(WALLET_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; table.create_multi_index( WALLET_ACCOUNT_ID_INDEX, @@ -135,7 +135,7 @@ impl From for HDAccountStorageItem { } } -pub struct HDWalletDb { +pub(crate) struct HDWalletDb { pub(crate) inner: IndexedDb, } @@ -154,7 +154,7 @@ impl DbInstance for HDWalletDb { } /// The wrapper over the [`CoinsContext::hd_wallet_db`] weak pointer. -pub struct HDWalletIndexedDbStorage { +pub(super) struct HDWalletIndexedDbStorage { db: WeakDb, } diff --git a/mm2src/coins/hd_wallet/wallet_ops.rs b/mm2src/coins/hd_wallet/wallet_ops.rs new file mode 100644 index 0000000000..65b1de223e --- /dev/null +++ b/mm2src/coins/hd_wallet/wallet_ops.rs @@ -0,0 +1,53 @@ +use super::{HDAccountMut, HDAccountOps, HDAccountsMap, HDAccountsMut, HDAccountsMutex}; +use async_trait::async_trait; +use crypto::{Bip44Chain, HDPathToCoin}; + +/// `HDWalletOps`: Operations that should be implemented for Structs or any type that represents HD wallets. +#[async_trait] +pub trait HDWalletOps { + /// Any type that represents a Hierarchical Deterministic (HD) wallet account. + type HDAccount: HDAccountOps + Clone + Send + Sync; + + /// Returns the coin type associated with this HD Wallet. + /// + /// This method should be implemented to fetch the coin type as specified in the wallet's BIP44 derivation path. + /// For example, in the derivation path `m/44'/0'/0'/0`, the coin type would be the third level `0'` + /// (representing Bitcoin). + fn coin_type(&self) -> u32; + + /// Returns the derivation path associated with this HD Wallet. This is the path used to derive the accounts. + fn derivation_path(&self) -> &HDPathToCoin; + + /// Fetches the gap limit associated with this HD Wallet. + /// Gap limit is the maximum number of consecutive unused addresses in an account + /// that should be checked before considering the wallet as having no more funds. + fn gap_limit(&self) -> u32; + + /// Returns the limit on the number of accounts that can be added to the wallet. + fn account_limit(&self) -> u32; + + /// Returns the default BIP44 chain for receiver addresses. + fn default_receiver_chain(&self) -> Bip44Chain; + + /// Returns a mutex that can be used to access the accounts. + fn get_accounts_mutex(&self) -> &HDAccountsMutex; + + /// Fetches an account based on its ID. This method will return `None` if the account is not activated. + async fn get_account(&self, account_id: u32) -> Option; + + /// Similar to `get_account`, but provides a mutable reference. + async fn get_account_mut(&self, account_id: u32) -> Option>; + + /// Fetches all accounts in the wallet. + async fn get_accounts(&self) -> HDAccountsMap; + + /// Similar to `get_accounts`, but provides a mutable reference to the accounts. + async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount>; + + /// Attempts to remove an account only if it's the last in the set. + /// This method will return the removed account if successful or `None` otherwise. + async fn remove_account_if_last(&self, account_id: u32) -> Option; + + /// Returns an address that's currently enabled for single-address operations, such as swaps. + async fn get_enabled_address(&self) -> Option<::HDAddress>; +} diff --git a/mm2src/coins/hd_wallet/withdraw_ops.rs b/mm2src/coins/hd_wallet/withdraw_ops.rs new file mode 100644 index 0000000000..7f1aa8b19c --- /dev/null +++ b/mm2src/coins/hd_wallet/withdraw_ops.rs @@ -0,0 +1,95 @@ +use super::{HDPathAccountToAddressId, HDWalletOps, HDWithdrawError}; +use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDWalletCoinOps}; +use async_trait::async_trait; +use bip32::DerivationPath; +use crypto::{StandardHDPath, StandardHDPathError}; +use mm2_err_handle::prelude::*; +use std::str::FromStr; + +type HDCoinPubKey = + <<<::HDWallet as HDWalletOps>::HDAccount as HDAccountOps>::HDAddress as HDAddressOps>::Pubkey; + +/// Represents the source of the funds for a withdrawal operation. +#[derive(Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum WithdrawFrom { + /// The address id of the sender address which is specified by the account id, chain, and address id. + AddressId(HDPathAccountToAddressId), + /// The derivation path of the sender address in the BIP-44 format. + /// + /// IMPORTANT: Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path, + /// `serde::Deserialize` returns "data did not match any variant of untagged enum WithdrawFrom". + /// It's better to show the user an informative error. + DerivationPath { derivation_path: String }, +} + +impl WithdrawFrom { + #[allow(clippy::result_large_err)] + pub fn to_address_path(&self, expected_coin_type: u32) -> MmResult { + match self { + WithdrawFrom::AddressId(address_id) => Ok(*address_id), + WithdrawFrom::DerivationPath { derivation_path } => { + let derivation_path = StandardHDPath::from_str(derivation_path) + .map_to_mm(StandardHDPathError::from) + .mm_err(|e| HDWithdrawError::UnexpectedFromAddress(e.to_string()))?; + let coin_type = derivation_path.coin_type(); + if coin_type != expected_coin_type { + let error = format!( + "Derivation path '{}' must have '{}' coin type", + derivation_path, expected_coin_type + ); + return MmError::err(HDWithdrawError::UnexpectedFromAddress(error)); + } + Ok(HDPathAccountToAddressId::from(derivation_path)) + }, + } + } +} + +/// Contains the details of the sender address for a withdraw operation. +pub struct WithdrawSenderAddress { + pub(crate) address: Address, + pub(crate) pubkey: Pubkey, + pub(crate) derivation_path: Option, +} + +/// `HDCoinWithdrawOps`: Operations that should be implemented for coins to support withdraw from HD wallets. +#[async_trait] +pub trait HDCoinWithdrawOps: HDWalletCoinOps { + /// Fetches the sender address for a withdraw operation. + /// This is the address from which the funds will be withdrawn. + async fn get_withdraw_hd_sender( + &self, + hd_wallet: &Self::HDWallet, + from: &WithdrawFrom, + ) -> MmResult, HDCoinPubKey>, HDWithdrawError> { + let HDPathAccountToAddressId { + account_id, + chain, + address_id, + } = from.to_address_path(hd_wallet.coin_type())?; + + let hd_account = hd_wallet + .get_account(account_id) + .await + .or_mm_err(|| HDWithdrawError::UnknownAccount { account_id })?; + + let is_address_activated = hd_account + .is_address_activated(chain, address_id) + // If [`HDWalletCoinOps::derive_address`] succeeds, [`HDAccountOps::is_address_activated`] shouldn't fails with an `InvalidBip44ChainError`. + .mm_err(|e| HDWithdrawError::InternalError(e.to_string()))?; + + let hd_address = self.derive_address(&hd_account, chain, address_id).await?; + let address = hd_address.address(); + if !is_address_activated { + let error = format!("'{}' address is not activated", address); + return MmError::err(HDWithdrawError::UnexpectedFromAddress(error)); + } + + Ok(WithdrawSenderAddress { + address, + pubkey: hd_address.pubkey(), + derivation_path: Some(hd_address.derivation_path().clone()), + }) + } +} diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 447990e2b1..2feb7ef014 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -10,7 +10,7 @@ mod ln_sql; pub mod ln_storage; pub mod ln_utils; -use crate::coin_errors::MyAddressError; +use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; use crate::lightning::ln_utils::{filter_channels, pay_invoice_with_max_total_cltv_expiry_delta, PaymentError}; use crate::utxo::rpc_clients::UtxoRpcClientEnum; use crate::utxo::utxo_common::{big_decimal_from_sat, big_decimal_from_sat_unsigned}; @@ -18,16 +18,16 @@ use crate::utxo::{sat_from_big_decimal, utxo_common, BlockchainNetwork}; use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - RawTransactionError, RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, - RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, TransactionEnum, TransactionErr, - TransactionFut, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, UtxoStandardCoin, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, - ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, - VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, - WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; + RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, + RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, + SendPaymentArgs, SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, + TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, + TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TxMarshalingErr, + UnexpectedDerivationMethod, UtxoStandardCoin, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; use bitcoin::bech32::ToBase32; use bitcoin::hashes::Hash; @@ -37,7 +37,7 @@ use bitcrypto::{dhash256, ripemd160}; use common::custom_futures::repeatable::{Ready, Retry}; use common::executor::{AbortableSystem, AbortedError, Timer}; use common::log::{error, info, LogOnError, LogState}; -use common::{async_blocking, get_local_duration_since_epoch, log, now_sec, PagingOptionsEnum}; +use common::{async_blocking, get_local_duration_since_epoch, log, now_sec, Future01CompatExt, PagingOptionsEnum}; use db_common::sqlite::rusqlite::Error as SqlError; use futures::{FutureExt, TryFutureExt}; use futures01::Future; @@ -152,7 +152,7 @@ pub(crate) struct GetOpenChannelsResult { impl Transaction for PaymentHash { fn tx_hex(&self) -> Vec { self.0.to_vec() } - fn tx_hash(&self) -> BytesJson { self.0.to_vec().into() } + fn tx_hash_as_bytes(&self) -> BytesJson { self.0.to_vec().into() } } impl LightningCoin { @@ -539,26 +539,24 @@ impl LightningCoin { Ok(PaymentInstructions::Lightning(invoice)) } - fn spend_swap_payment(&self, spend_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { - let payment_hash = try_tx_fus!(payment_hash_from_slice(spend_payment_args.other_payment_tx)); + async fn spend_swap_payment(&self, spend_payment_args: SpendPaymentArgs<'_>) -> TransactionResult { + let payment_hash = try_tx_s!(payment_hash_from_slice(spend_payment_args.other_payment_tx)); + let mut preimage = [b' '; 32]; preimage.copy_from_slice(spend_payment_args.secret); + drop_mutability!(preimage); - let coin = self.clone(); - let fut = async move { - let payment_preimage = PaymentPreimage(preimage); - coin.channel_manager.claim_funds(payment_preimage); - coin.db - .update_payment_preimage_in_db(payment_hash, payment_preimage) - .await - .error_log_with_msg(&format!( - "Unable to update payment {} information in DB with preimage: {}!", - hex::encode(payment_hash.0), - hex::encode(preimage) - )); - Ok(TransactionEnum::LightningPayment(payment_hash)) - }; - Box::new(fut.boxed().compat()) + let payment_preimage = PaymentPreimage(preimage); + self.channel_manager.claim_funds(payment_preimage); + self.db + .update_payment_preimage_in_db(payment_hash, payment_preimage) + .await + .error_log_with_msg(&format!( + "Unable to update payment {} information in DB with preimage: {}!", + hex::encode(payment_hash.0), + hex::encode(preimage) + )); + Ok(TransactionEnum::LightningPayment(payment_hash)) } fn validate_swap_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { @@ -612,7 +610,7 @@ impl LightningCoin { #[async_trait] impl SwapOps for LightningCoin { // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning - fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let fut = async move { Ok(TransactionEnum::LightningPayment(PaymentHash([1; 32]))) }; Box::new(fut.boxed().compat()) } @@ -652,13 +650,19 @@ impl SwapOps for LightningCoin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { - self.spend_swap_payment(maker_spends_payment_args) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_swap_payment(maker_spends_payment_args).await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut { - self.spend_swap_payment(taker_spends_payment_args) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + self.spend_swap_payment(taker_spends_payment_args).await } async fn send_taker_refunds_payment( @@ -685,13 +689,13 @@ impl SwapOps for LightningCoin { } #[inline] - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - self.validate_swap_payment(input) + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + self.validate_swap_payment(input).compat().await } #[inline] - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - self.validate_swap_payment(input) + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + self.validate_swap_payment(input).compat().await } fn check_if_my_payment_sent( @@ -1028,12 +1032,13 @@ impl WatcherOps for LightningCoin { } } +#[async_trait] impl MarketCoinOps for LightningCoin { fn ticker(&self) -> &str { &self.conf.ticker } fn my_address(&self) -> MmResult { Ok(self.my_node_id()) } - fn get_public_key(&self) -> Result> { Ok(self.my_node_id()) } + async fn get_public_key(&self) -> Result> { Ok(self.my_node_id()) } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { let mut _message_prefix = self.conf.sign_message_prefix.clone()?; @@ -1106,6 +1111,13 @@ impl MarketCoinOps for LightningCoin { )) } + #[inline(always)] + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { + MmError::err(RawTransactionError::NotImplemented { + coin: self.ticker().to_string(), + }) + } + // Todo: Add waiting for confirmations logic for the case of if the channel is closed and the htlc can be claimed on-chain // Todo: The above is postponed and might not be needed after this issue is resolved https://github.com/lightningdevkit/rust-lightning/issues/2017 fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { @@ -1248,6 +1260,8 @@ impl MarketCoinOps for LightningCoin { // Todo: Equals to min_tx_amount for now (1 satoshi), should change this later // Todo: doesn't take routing fees into account too, There is no way to know the route to the other side of the swap when placing the order, need to find a workaround for this fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + + fn is_trezor(&self) -> bool { self.platform.coin.is_trezor() } } #[derive(Deserialize, Serialize)] @@ -1322,6 +1336,7 @@ impl MmCoin for LightningCoin { &self, _value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { Ok(TradeFee { coin: self.ticker().to_owned(), diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index a90b13be82..d6c78f9ad9 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -188,9 +188,9 @@ pub enum SignFundingTransactionError { } // Generates the raw funding transaction with one output equal to the channel value. -fn sign_funding_transaction( +async fn sign_funding_transaction( uuid: Uuid, - output_script: &Script, + output_script_pubkey: &Script, platform: Arc, ) -> Result { let coin = &platform.coin; @@ -206,24 +206,17 @@ fn sign_funding_transaction( })? .clone() }; - unsigned.outputs[0].script_pubkey = output_script.to_bytes().into(); + unsigned.outputs[0].script_pubkey = output_script_pubkey.to_bytes().into(); - let my_address = coin - .as_ref() - .derivation_method - .single_addr_or_err() - .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let key_pair = coin .as_ref() .priv_key_policy .activated_key_or_err() .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; - let prev_script = Builder::build_p2pkh(&my_address.hash); let signed = sign_tx( unsigned, key_pair, - prev_script, SignatureVersion::WitnessV0, coin.as_ref().conf.fork_id, ) @@ -297,30 +290,32 @@ impl LightningEventHandler { "Handling FundingGenerationReady event for channel with uuid: {} with: {}", uuid, counterparty_node_id ); - let funding_tx = match sign_funding_transaction(uuid, &output_script, self.platform.clone()) { - Ok(tx) => tx, - Err(e) => { - error!( - "Error generating funding transaction for channel with uuid {}: {}", - uuid, - e.to_string() - ); - return; - }, - }; - let funding_txid = funding_tx.txid(); - // Give the funding transaction back to LDK for opening the channel. - if let Err(e) = - self.channel_manager - .funding_transaction_generated(&temporary_channel_id, &counterparty_node_id, funding_tx) - { - error!("{:?}", e); - return; - } + + let channel_manager = self.channel_manager.clone(); let platform = self.platform.clone(); let db = self.db.clone(); let fut = async move { + let funding_tx = match sign_funding_transaction(uuid, &output_script, platform.clone()).await { + Ok(tx) => tx, + Err(e) => { + error!( + "Error generating funding transaction for channel with uuid {}: {}", + uuid, + e.to_string() + ); + return; + }, + }; + let funding_txid = funding_tx.txid(); + // Give the funding transaction back to LDK for opening the channel. + if let Err(e) = + channel_manager.funding_transaction_generated(&temporary_channel_id, &counterparty_node_id, funding_tx) + { + error!("{:?}", e); + return; + } + let best_block_height = platform.best_block_height(); db.add_funding_tx_to_db( uuid, @@ -515,21 +510,30 @@ impl LightningEventHandler { return; } - // Todo: add support for Hardware wallets for funding transactions and spending spendable outputs (channel closing transactions) - let my_address = match self.platform.coin.as_ref().derivation_method.single_addr_or_err() { - Ok(addr) => addr.clone(), - Err(e) => { - error!("{}", e); - return; - }, - }; - let platform = self.platform.clone(); let db = self.db.clone(); let keys_manager = self.keys_manager.clone(); let fut = async move { - let change_destination_script = Builder::build_witness_script(&my_address.hash).to_bytes().take().into(); + // Todo: add support for HD and Hardware wallets for funding transactions and spending spendable outputs (channel closing transactions) + let my_address = match platform.coin.as_ref().derivation_method.single_addr_or_err().await { + Ok(addr) => addr.clone(), + Err(e) => { + error!("{}", e); + return; + }, + }; + let change_destination_script = match Builder::build_p2wpkh(my_address.hash()) { + Ok(script) => script.to_bytes().take().into(), + Err(err) => { + error!( + "Could not create witness script for change output {}: {}", + my_address.to_string(), + err.to_string() + ); + return; + }, + }; let feerate_sat_per_1000_weight = platform.get_est_sat_per_1000_weight(ConfirmationTarget::Normal); let output_descriptors = outputs.iter().collect::>(); let claiming_tx = match keys_manager.spend_spendable_outputs( diff --git a/mm2src/coins/lightning/ln_p2p.rs b/mm2src/coins/lightning/ln_p2p.rs index 7db3a0b4e2..64c7c940aa 100644 --- a/mm2src/coins/lightning/ln_p2p.rs +++ b/mm2src/coins/lightning/ln_p2p.rs @@ -110,7 +110,6 @@ fn netaddress_from_ipaddr(addr: IpAddr, port: u16) -> Vec { if addr == Ipv4Addr::new(0, 0, 0, 0) || addr == Ipv4Addr::new(127, 0, 0, 1) { return Vec::new(); } - let mut addresses = Vec::new(); let address = match addr { IpAddr::V4(addr) => NetAddress::IPv4 { addr: u32::from(addr).to_be_bytes(), @@ -121,8 +120,7 @@ fn netaddress_from_ipaddr(addr: IpAddr, port: u16) -> Vec { port, }, }; - addresses.push(address); - addresses + vec![address] } pub async fn ln_node_announcement_loop( diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index c4c2f0e655..4d0573af95 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -399,6 +399,7 @@ impl Platform { output.script_pubkey.as_ref(), output.outpoint.index.into(), BlockHashOrHeight::Hash(Default::default()), + self.coin.as_ref().tx_hash_algo, ) .compat() .await @@ -545,7 +546,7 @@ impl Platform { .await .map_to_mm(|e| SaveChannelClosingError::WaitForFundingTxSpendError(e.get_plain_text_format()))?; - let closing_tx_hash = format!("{:02x}", closing_tx.tx_hash()); + let closing_tx_hash = format!("{:02x}", closing_tx.tx_hash_as_bytes()); Ok(closing_tx_hash) } diff --git a/mm2src/coins/lightning/ln_sql.rs b/mm2src/coins/lightning/ln_sql.rs index 53eab1ac78..0062479ebf 100644 --- a/mm2src/coins/lightning/ln_sql.rs +++ b/mm2src/coins/lightning/ln_sql.rs @@ -609,11 +609,14 @@ pub struct SqliteLightningDB { } impl SqliteLightningDB { - pub fn new(ticker: String, sqlite_connection: SqliteConnShared) -> Self { - Self { - db_ticker: ticker.replace('-', "_"), + pub fn new(ticker: String, sqlite_connection: SqliteConnShared) -> Result { + let db_ticker = ticker.replace('-', "_"); + validate_table_name(&db_ticker)?; + + Ok(Self { + db_ticker, sqlite_connection, - } + }) } } @@ -1047,7 +1050,7 @@ mod tests { use super::*; use crate::lightning::ln_db::DBChannelDetails; use common::{block_on, new_uuid}; - use db_common::sqlite::rusqlite::Connection; + use db_common::sqlite::rusqlite::{self, Connection}; use rand::distributions::Alphanumeric; use rand::{Rng, RngCore}; use secp256k1v24::{Secp256k1, SecretKey}; @@ -1056,7 +1059,7 @@ mod tests { fn generate_random_channels(num: u64) -> Vec { let mut rng = rand::thread_rng(); - let mut channels = vec![]; + let mut channels = Vec::with_capacity(num.try_into().expect("Shouldn't overflow.")); let s = Secp256k1::new(); let mut bytes = [0; 32]; for _i in 0..num { @@ -1108,7 +1111,7 @@ mod tests { fn generate_random_payments(num: u64) -> Vec { let mut rng = rand::thread_rng(); - let mut payments = vec![]; + let mut payments = Vec::with_capacity(num.try_into().expect("Shouldn't overflow.")); let s = Secp256k1::new(); let mut bytes = [0; 32]; for _ in 0..num { @@ -1157,7 +1160,8 @@ mod tests { let db = SqliteLightningDB::new( "init_sql_collection".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); let initialized = block_on(db.is_db_initialized()).unwrap(); assert!(!initialized); @@ -1174,7 +1178,8 @@ mod tests { let db = SqliteLightningDB::new( "add_get_channel".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); block_on(db.init_db()).unwrap(); @@ -1282,7 +1287,8 @@ mod tests { let db = SqliteLightningDB::new( "add_get_payment".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); block_on(db.init_db()).unwrap(); @@ -1371,7 +1377,8 @@ mod tests { let db = SqliteLightningDB::new( "test_get_payments_by_filter".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); block_on(db.init_db()).unwrap(); @@ -1441,6 +1448,7 @@ mod tests { let expected_payments = if expected_payments_vec.len() > 10 { expected_payments_vec[..10].to_vec() } else { + #[allow(clippy::redundant_clone)] // This is a false-possitive bug from clippy expected_payments_vec.clone() }; let actual_payments = result.payments; @@ -1485,12 +1493,48 @@ mod tests { assert_eq!(expected_payments, actual_payments); } + #[test] + fn test_invalid_lightning_db_name() { + let db = SqliteLightningDB::new("123".into(), Mutex::new(Connection::open_in_memory().unwrap()).into()); + + let expected = || { + SqlError::SqliteFailure( + rusqlite::ffi::Error { + code: rusqlite::ErrorCode::ApiMisuse, + extended_code: rusqlite::ffi::SQLITE_MISUSE, + }, + None, + ) + }; + + assert_eq!(db.err(), Some(expected())); + + let db = SqliteLightningDB::new( + "t".repeat(u8::MAX as usize + 1), + Mutex::new(Connection::open_in_memory().unwrap()).into(), + ); + + assert_eq!(db.err(), Some(expected())); + + let db = SqliteLightningDB::new( + "PROCEDURE".to_owned(), + Mutex::new(Connection::open_in_memory().unwrap()).into(), + ); + + assert_eq!(db.err(), Some(expected())); + + let db = SqliteLightningDB::new(String::new(), Mutex::new(Connection::open_in_memory().unwrap()).into()); + + assert_eq!(db.err(), Some(expected())); + } + #[test] fn test_get_channels_by_filter() { let db = SqliteLightningDB::new( "test_get_channels_by_filter".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); block_on(db.init_db()).unwrap(); @@ -1568,6 +1612,7 @@ mod tests { let expected_channels = if expected_channels_vec.len() > 10 { expected_channels_vec[..10].to_vec() } else { + #[allow(clippy::redundant_clone)] // This is a false-possitive bug from clippy expected_channels_vec.clone() }; let actual_channels = result.channels; diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 88af1d68cc..693b7c3a4f 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -76,7 +76,7 @@ pub async fn init_db(ctx: &MmArc, ticker: String) -> EnableLightningResult = DbLocked<'a, TxHistoryDb>; @@ -123,6 +124,29 @@ macro_rules! try_f { }; } +#[cfg(feature = "enable-solana")] +macro_rules! try_tx_fus_err { + ($err: expr) => { + return Box::new(futures01::future::err(crate::TransactionErr::Plain(ERRL!( + "{:?}", $err + )))) + }; +} + +#[cfg(feature = "enable-solana")] +macro_rules! try_tx_fus_opt { + ($e: expr, $err: expr) => { + match $e { + Some(ok) => ok, + None => { + return Box::new(futures01::future::err(crate::TransactionErr::Plain(ERRL!( + "{:?}", $err + )))) + }, + } + }; +} + /// `TransactionErr` compatible `try_fus` macro. macro_rules! try_tx_fus { ($e: expr) => { @@ -203,11 +227,13 @@ macro_rules! ok_or_continue_after_sleep { } pub mod coin_balance; +use coin_balance::{AddressBalanceStatus, HDAddressBalance, HDWalletBalanceOps}; + pub mod lp_price; pub mod watcher_common; pub mod coin_errors; -use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut}; +use coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentResult}; #[doc(hidden)] #[cfg(test)] @@ -217,14 +243,13 @@ pub mod eth; use eth::GetValidEthWithdrawAddError; use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetailsErr, EthTxFeeDetails, GetEthAddressError, SignedEthTx}; - -pub mod hd_confirm_address; -pub mod hd_pubkey; +use ethereum_types::U256; pub mod hd_wallet; -use hd_wallet::{HDAccountAddressId, HDAddress}; +use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, + HDCoinHDAccount, HDExtractPubkeyError, HDPathAccountToAddressId, HDWalletAddress, HDWalletCoinOps, + HDWalletOps, HDWithdrawError, HDXPubExtractor, WithdrawFrom, WithdrawSenderAddress}; -pub mod hd_wallet_storage; #[cfg(not(target_arch = "wasm32"))] pub mod lightning; #[cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))] pub mod my_tx_history_v2; @@ -240,8 +265,9 @@ use rpc_command::{get_new_address::{GetNewAddressTaskManager, GetNewAddressTaskM init_withdraw::{WithdrawTaskManager, WithdrawTaskManagerShared}}; pub mod tendermint; -use tendermint::{CosmosTransaction, CustomTendermintMsgType, TendermintCoin, TendermintFeeDetails, - TendermintProtocolInfo, TendermintToken, TendermintTokenProtocolInfo}; +use tendermint::htlc::CustomTendermintMsgType; +use tendermint::{CosmosTransaction, TendermintCoin, TendermintFeeDetails, TendermintProtocolInfo, TendermintToken, + TendermintTokenProtocolInfo}; #[doc(hidden)] #[allow(unused_variables)] @@ -272,7 +298,7 @@ pub use solana::spl::SplToken; not(target_os = "android"), not(target_arch = "wasm32") ))] -pub use solana::{SolanaActivationParams, SolanaCoin, SolanaFeeDetails}; +pub use solana::{SolTransaction, SolanaActivationParams, SolanaCoin, SolanaFeeDetails}; pub mod utxo; use utxo::bch::{bch_coin_with_policy, BchActivationRequest, BchCoin}; @@ -281,16 +307,19 @@ use utxo::qtum::{self, qtum_coin_with_policy, Qrc20AddressError, QtumCoin, QtumD use utxo::rpc_clients::UtxoRpcError; use utxo::slp::SlpToken; use utxo::slp::{slp_addr_from_pubkey_str, SlpFeeDetails}; -use utxo::utxo_common::big_decimal_from_sat_unsigned; +use utxo::utxo_common::{big_decimal_from_sat_unsigned, payment_script, WaitForOutputSpendErr}; use utxo::utxo_standard::{utxo_standard_coin_with_policy, UtxoStandardCoin}; -use utxo::UtxoActivationParams; -use utxo::{BlockchainNetwork, GenerateTxError, UtxoFeeDetails, UtxoTx}; +use utxo::{swap_proto_v2_scripts, BlockchainNetwork, GenerateTxError, UtxoActivationParams, UtxoFeeDetails, UtxoTx}; pub mod nft; use nft::nft_errors::GetNftInfoError; +use script::Script; pub mod z_coin; +use crate::coin_balance::{BalanceObjectOps, HDWalletBalanceObject}; use z_coin::{ZCoin, ZcoinProtocolInfo}; +#[cfg(feature = "enable-sia")] pub mod sia; +#[cfg(feature = "enable-sia")] use sia::SiaCoin; pub type TransactionFut = Box + Send>; pub type TransactionResult = Result; @@ -315,8 +344,8 @@ pub type RawTransactionFut<'a> = pub type RefundResult = Result>; /// Helper type used for swap transactions' spend preimage generation result pub type GenPreimageResult = MmResult, TxGenError>; -/// Helper type used for taker funding's validation result -pub type ValidateTakerFundingResult = MmResult<(), ValidateTakerFundingError>; +/// Helper type used for swap v2 tx validation result +pub type ValidateSwapV2TxResult = MmResult<(), ValidateSwapV2TxError>; /// Helper type used for taker funding's spend preimage validation result pub type ValidateTakerFundingSpendPreimageResult = MmResult<(), ValidateTakerFundingSpendPreimageError>; /// Helper type used for taker payment's spend preimage validation result @@ -335,30 +364,50 @@ pub const INVALID_SWAP_ID_ERR_LOG: &str = "Invalid swap id"; pub const INVALID_SCRIPT_ERR_LOG: &str = "Invalid script"; pub const INVALID_REFUND_TX_ERR_LOG: &str = "Invalid refund transaction"; -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum RawTransactionError { #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, #[display(fmt = "Invalid hash: {}", _0)] InvalidHashError(String), + #[from_stringify("web3::Error")] #[display(fmt = "Transport error: {}", _0)] Transport(String), #[display(fmt = "Hash does not exist: {}", _0)] HashNotExist(String), #[display(fmt = "Internal error: {}", _0)] InternalError(String), + #[display(fmt = "Transaction decode error: {}", _0)] + DecodeError(String), + #[from_stringify("NumConversError", "FromHexError")] + #[display(fmt = "Invalid param: {}", _0)] + InvalidParam(String), + #[display(fmt = "Non-existent previous output: {}", _0)] + NonExistentPrevOutputError(String), + #[display(fmt = "Signing error: {}", _0)] + SigningError(String), + #[display(fmt = "Not implemented for this coin {}", coin)] + NotImplemented { coin: String }, + #[display(fmt = "Transaction error {}", _0)] + TransactionError(String), } impl HttpStatusCode for RawTransactionError { fn status_code(&self) -> StatusCode { match self { - RawTransactionError::NoSuchCoin { .. } - | RawTransactionError::InvalidHashError(_) - | RawTransactionError::HashNotExist(_) => StatusCode::BAD_REQUEST, - RawTransactionError::Transport(_) | RawTransactionError::InternalError(_) => { + RawTransactionError::InternalError(_) | RawTransactionError::SigningError(_) => { StatusCode::INTERNAL_SERVER_ERROR }, + RawTransactionError::NoSuchCoin { .. } + | RawTransactionError::InvalidHashError(_) + | RawTransactionError::HashNotExist(_) + | RawTransactionError::DecodeError(_) + | RawTransactionError::InvalidParam(_) + | RawTransactionError::NonExistentPrevOutputError(_) + | RawTransactionError::TransactionError(_) => StatusCode::BAD_REQUEST, + RawTransactionError::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED, + RawTransactionError::Transport(_) => StatusCode::BAD_GATEWAY, } } } @@ -415,11 +464,90 @@ pub struct RawTransactionRes { pub tx_hex: BytesJson, } +/// Previous utxo transaction data for signing +#[derive(Clone, Debug, Deserialize)] +pub struct PrevTxns { + /// transaction hash + tx_hash: String, + /// transaction output index + index: u32, + /// transaction output script pub key + script_pub_key: String, + // TODO: implement if needed: + // redeem script for P2SH script pubkey + // pub redeem_script: Option, + /// transaction output amount + amount: BigDecimal, +} + +/// sign_raw_transaction RPC request's params for signing raw utxo transactions +#[derive(Clone, Debug, Deserialize)] +pub struct SignUtxoTransactionParams { + /// unsigned utxo transaction in hex + tx_hex: String, + /// optional data of previous transactions referred by unsigned transaction inputs + prev_txns: Option>, + // TODO: add if needed for utxo: + // pub sighash_type: Option, optional signature hash type, one of values: NONE, SINGLE, ALL, NONE|ANYONECANPAY, SINGLE|ANYONECANPAY, ALL|ANYONECANPAY (if not set 'ALL' is used) + // pub branch_id: Option, zcash or komodo optional consensus branch id, used for signing transactions ahead of current height +} + +#[derive(Clone, Debug, Deserialize)] +pub struct LegacyGasPrice { + /// Gas price in decimal gwei + pub gas_price: BigDecimal, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Eip1559FeePerGas { + /// Max fee per gas in decimal gwei + pub max_fee_per_gas: BigDecimal, + /// Max priority fee per gas in decimal gwei + pub max_priority_fee_per_gas: BigDecimal, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "tx_type")] +pub enum PayForGasParams { + Legacy(LegacyGasPrice), + Eip1559(Eip1559FeePerGas), +} + +/// sign_raw_transaction RPC request's params for signing raw eth transactions +#[derive(Clone, Debug, Deserialize)] +pub struct SignEthTransactionParams { + /// Eth transfer value + value: Option, + /// Eth to address + to: Option, + /// Eth contract data + data: Option, + /// Eth gas use limit + gas_limit: U256, + /// Optional gas price or fee per gas params + pay_for_gas: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "type", content = "tx")] +pub enum SignRawTransactionEnum { + UTXO(SignUtxoTransactionParams), + ETH(SignEthTransactionParams), +} + +/// sign_raw_transaction RPC request +#[derive(Clone, Debug, Deserialize)] +pub struct SignRawTransactionRequest { + coin: String, + #[serde(flatten)] + tx: SignRawTransactionEnum, +} + #[derive(Debug, Deserialize)] pub struct MyAddressReq { coin: String, #[serde(default)] - path_to_address: StandardHDCoinAddress, + path_to_address: HDPathAccountToAddressId, } #[derive(Debug, Serialize)] @@ -431,7 +559,7 @@ pub struct MyWalletAddress { pub type SignatureResult = Result>; pub type VerificationResult = Result>; -#[derive(Debug, Display)] +#[derive(Debug, Display, EnumFromStringify)] pub enum TxHistoryError { ErrorSerializing(String), ErrorDeserializing(String), @@ -443,6 +571,7 @@ pub enum TxHistoryError { internal_id: BytesJson, }, NotSupported(String), + #[from_stringify("MyAddressError")] InternalError(String), } @@ -465,7 +594,7 @@ impl Serialize for PrivKeyPolicyNotAllowed { } } -#[derive(Clone, Debug, Display, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] pub enum UnexpectedDerivationMethod { #[display(fmt = "Expected 'SingleAddress' derivation method")] ExpectedSingleAddress, @@ -493,13 +622,15 @@ pub trait Transaction: fmt::Debug + 'static { /// Raw transaction bytes of the transaction fn tx_hex(&self) -> Vec; /// Serializable representation of tx hash for displaying purpose - fn tx_hash(&self) -> BytesJson; + fn tx_hash_as_bytes(&self) -> BytesJson; } #[derive(Clone, Debug, PartialEq)] pub enum TransactionEnum { UtxoTx(UtxoTx), SignedEthTx(SignedEthTx), + #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] + SolTransaction(SolTransaction), ZTransaction(ZTransaction), CosmosTransaction(CosmosTransaction), #[cfg(not(target_arch = "wasm32"))] @@ -508,6 +639,8 @@ pub enum TransactionEnum { ifrom!(TransactionEnum, UtxoTx); ifrom!(TransactionEnum, SignedEthTx); +#[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] +ifrom!(TransactionEnum, SolTransaction); ifrom!(TransactionEnum, ZTransaction); #[cfg(not(target_arch = "wasm32"))] ifrom!(TransactionEnum, LightningPayment); @@ -531,6 +664,8 @@ impl Deref for TransactionEnum { TransactionEnum::CosmosTransaction(ref t) => t, #[cfg(not(target_arch = "wasm32"))] TransactionEnum::LightningPayment(ref p) => p, + #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] + TransactionEnum::SolTransaction(ref s) => s, } } } @@ -545,13 +680,15 @@ pub enum TxMarshalingErr { Internal(String), } -#[derive(Debug, Clone)] +#[derive(Clone, Debug, EnumFromStringify)] #[allow(clippy::large_enum_variant)] pub enum TransactionErr { /// Keeps transactions while throwing errors. TxRecoverable(TransactionEnum, String), /// Simply for plain error messages. + #[from_stringify("keys::Error")] Plain(String), + ProtocolNotSupported(String), } impl TransactionErr { @@ -569,7 +706,7 @@ impl TransactionErr { pub fn get_plain_text_format(&self) -> String { match self { TransactionErr::TxRecoverable(_, err) => err.to_string(), - TransactionErr::Plain(err) => err.to_string(), + TransactionErr::Plain(err) | TransactionErr::ProtocolNotSupported(err) => err.to_string(), } } } @@ -735,6 +872,60 @@ pub struct WatcherReward { pub send_contract_reward_on_spend: bool, } +/// Enum representing possible variants of swap transaction including secret hash(es) +#[derive(Debug)] +pub enum SwapTxTypeWithSecretHash<'a> { + /// Legacy protocol transaction + TakerOrMakerPayment { maker_secret_hash: &'a [u8] }, + /// Taker funding transaction + TakerFunding { taker_secret_hash: &'a [u8] }, + /// Maker payment v2 (with immediate refund path) + MakerPaymentV2 { + maker_secret_hash: &'a [u8], + taker_secret_hash: &'a [u8], + }, + /// Taker payment v2 + TakerPaymentV2 { maker_secret_hash: &'a [u8] }, +} + +impl<'a> SwapTxTypeWithSecretHash<'a> { + pub fn redeem_script(&self, time_lock: u32, my_public: &Public, other_public: &Public) -> Script { + match self { + SwapTxTypeWithSecretHash::TakerOrMakerPayment { maker_secret_hash } => { + payment_script(time_lock, maker_secret_hash, my_public, other_public) + }, + SwapTxTypeWithSecretHash::TakerFunding { taker_secret_hash } => { + swap_proto_v2_scripts::taker_funding_script(time_lock, taker_secret_hash, my_public, other_public) + }, + SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash, + taker_secret_hash, + } => swap_proto_v2_scripts::maker_payment_script( + time_lock, + maker_secret_hash, + taker_secret_hash, + my_public, + other_public, + ), + SwapTxTypeWithSecretHash::TakerPaymentV2 { maker_secret_hash } => { + swap_proto_v2_scripts::taker_payment_script(time_lock, maker_secret_hash, my_public, other_public) + }, + } + } + + pub fn op_return_data(&self) -> Vec { + match self { + SwapTxTypeWithSecretHash::TakerOrMakerPayment { maker_secret_hash } => maker_secret_hash.to_vec(), + SwapTxTypeWithSecretHash::TakerFunding { taker_secret_hash } => taker_secret_hash.to_vec(), + SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash, + taker_secret_hash, + } => [*maker_secret_hash, *taker_secret_hash].concat(), + SwapTxTypeWithSecretHash::TakerPaymentV2 { maker_secret_hash } => maker_secret_hash.to_vec(), + } + } +} + /// Helper struct wrapping arguments for [SwapOps::send_taker_payment] and [SwapOps::send_maker_payment]. #[derive(Clone, Debug)] pub struct SendPaymentArgs<'a> { @@ -780,7 +971,7 @@ pub struct SpendPaymentArgs<'a> { pub watcher_reward: bool, } -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct RefundPaymentArgs<'a> { pub payment_tx: &'a [u8], pub time_lock: u64, @@ -788,7 +979,7 @@ pub struct RefundPaymentArgs<'a> { /// * Taker's pubkey if this structure is used in [`SwapOps::send_maker_refunds_payment`]. /// * Maker's pubkey if this structure is used in [`SwapOps::send_taker_refunds_payment`]. pub other_pubkey: &'a [u8], - pub secret_hash: &'a [u8], + pub tx_type_with_secret_hash: SwapTxTypeWithSecretHash<'a>, pub swap_contract_address: &'a Option, pub swap_unique_data: &'a [u8], pub watcher_reward: bool, @@ -862,17 +1053,14 @@ pub struct PaymentInstructionArgs<'a> { pub wait_until: u64, } -#[derive(Display)] +#[derive(Display, EnumFromStringify)] pub enum PaymentInstructionsErr { LightningInvoiceErr(String), WatcherRewardErr(String), + #[from_stringify("NumConversError")] InternalError(String), } -impl From for PaymentInstructionsErr { - fn from(e: NumConversError) -> Self { PaymentInstructionsErr::InternalError(e.to_string()) } -} - #[derive(Display)] pub enum ValidateInstructionsErr { ValidateLightningInvoiceErr(String), @@ -903,15 +1091,21 @@ pub enum WatcherRewardError { /// Swap operations (mostly based on the Hash/Time locked transactions implemented by coin wallets). #[async_trait] pub trait SwapOps { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut; + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionFut; fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionFut; fn send_taker_payment(&self, taker_payment_args: SendPaymentArgs<'_>) -> TransactionFut; - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut; + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult; - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs<'_>) -> TransactionFut; + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult; async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult; @@ -919,9 +1113,9 @@ pub trait SwapOps { fn validate_fee(&self, validate_fee_args: ValidateFeeArgs<'_>) -> ValidatePaymentFut<()>; - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()>; + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()>; - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()>; + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()>; fn check_if_my_payment_sent( &self, @@ -1083,7 +1277,7 @@ pub trait WatcherOps { ) -> Result, MmError>; } -/// Helper struct wrapping arguments for [SwapOpsV2::send_taker_funding] +/// Helper struct wrapping arguments for [TakerCoinSwapOpsV2::send_taker_funding] pub struct SendTakerFundingArgs<'a> { /// Taker will be able to refund the payment after this timestamp pub time_lock: u64, @@ -1091,8 +1285,8 @@ pub struct SendTakerFundingArgs<'a> { pub taker_secret_hash: &'a [u8], /// Maker's pubkey pub maker_pub: &'a [u8], - /// DEX fee amount - pub dex_fee_amount: BigDecimal, + /// DEX fee + pub dex_fee: &'a DexFee, /// Additional reward for maker (premium) pub premium_amount: BigDecimal, /// Actual volume of taker's payment @@ -1101,8 +1295,8 @@ pub struct SendTakerFundingArgs<'a> { pub swap_unique_data: &'a [u8], } -/// Helper struct wrapping arguments for [SwapOpsV2::refund_taker_funding_secret] -pub struct RefundFundingSecretArgs<'a, Coin: CoinAssocTypes + ?Sized> { +/// Helper struct wrapping arguments for [TakerCoinSwapOpsV2::refund_taker_funding_secret] +pub struct RefundFundingSecretArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { pub funding_tx: &'a Coin::Tx, pub time_lock: u64, pub maker_pubkey: &'a Coin::Pubkey, @@ -1113,8 +1307,8 @@ pub struct RefundFundingSecretArgs<'a, Coin: CoinAssocTypes + ?Sized> { pub watcher_reward: bool, } -/// Helper struct wrapping arguments for [SwapOpsV2::gen_taker_funding_spend_preimage] -pub struct GenTakerFundingSpendArgs<'a, Coin: CoinAssocTypes + ?Sized> { +/// Helper struct wrapping arguments for [TakerCoinSwapOpsV2::gen_taker_funding_spend_preimage] +pub struct GenTakerFundingSpendArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { /// Taker payment transaction serialized to raw bytes pub funding_tx: &'a Coin::Tx, /// Maker's pubkey @@ -1131,8 +1325,8 @@ pub struct GenTakerFundingSpendArgs<'a, Coin: CoinAssocTypes + ?Sized> { pub maker_secret_hash: &'a [u8], } -/// Helper struct wrapping arguments for [SwapOpsV2::validate_taker_funding] -pub struct ValidateTakerFundingArgs<'a, Coin: CoinAssocTypes + ?Sized> { +/// Helper struct wrapping arguments for [TakerCoinSwapOpsV2::validate_taker_funding] +pub struct ValidateTakerFundingArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { /// Taker funding transaction pub funding_tx: &'a Coin::Tx, /// Taker will be able to refund the payment after this timestamp @@ -1142,7 +1336,7 @@ pub struct ValidateTakerFundingArgs<'a, Coin: CoinAssocTypes + ?Sized> { /// Taker's pubkey pub other_pub: &'a Coin::Pubkey, /// DEX fee amount - pub dex_fee_amount: BigDecimal, + pub dex_fee: &'a DexFee, /// Additional reward for maker (premium) pub premium_amount: BigDecimal, /// Actual volume of taker's payment @@ -1152,23 +1346,25 @@ pub struct ValidateTakerFundingArgs<'a, Coin: CoinAssocTypes + ?Sized> { } /// Helper struct wrapping arguments for taker payment's spend generation, used in -/// [SwapOpsV2::gen_taker_payment_spend_preimage], [SwapOpsV2::validate_taker_payment_spend_preimage] and -/// [SwapOpsV2::sign_and_broadcast_taker_payment_spend] -pub struct GenTakerPaymentSpendArgs<'a, Coin: CoinAssocTypes + ?Sized> { +/// [TakerCoinSwapOpsV2::gen_taker_payment_spend_preimage], [TakerCoinSwapOpsV2::validate_taker_payment_spend_preimage] and +/// [TakerCoinSwapOpsV2::sign_and_broadcast_taker_payment_spend] +pub struct GenTakerPaymentSpendArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { /// Taker payment transaction serialized to raw bytes pub taker_tx: &'a Coin::Tx, /// Taker will be able to refund the payment after this timestamp pub time_lock: u64, /// The hash of the secret generated by maker - pub secret_hash: &'a [u8], + pub maker_secret_hash: &'a [u8], /// Maker's pubkey pub maker_pub: &'a Coin::Pubkey, + /// Maker's address + pub maker_address: &'a Coin::Address, /// Taker's pubkey pub taker_pub: &'a Coin::Pubkey, /// Pubkey of address, receiving DEX fees pub dex_fee_pub: &'a [u8], - /// DEX fee amount - pub dex_fee_amount: BigDecimal, + /// DEX fee + pub dex_fee: &'a DexFee, /// Additional reward for maker (premium) pub premium_amount: BigDecimal, /// Actual volume of taker's payment @@ -1176,7 +1372,7 @@ pub struct GenTakerPaymentSpendArgs<'a, Coin: CoinAssocTypes + ?Sized> { } /// Taker payment spend preimage with taker's signature -pub struct TxPreimageWithSig { +pub struct TxPreimageWithSig { /// The preimage, might be () for certain coin types (only signature might be used) pub preimage: Coin::Preimage, /// Taker's signature @@ -1202,6 +1398,8 @@ pub enum TxGenError { TxFeeTooHigh(String), /// Previous tx is not valid PrevTxIsNotValid(String), + /// Other errors, can be used to return an error that can happen only in specific coin protocol implementation + Other(String), } impl From for TxGenError { @@ -1216,9 +1414,9 @@ impl From for TxGenError { fn from(err: UtxoSignWithKeyPairError) -> Self { TxGenError::Signing(err.to_string()) } } -/// Enum covering error cases that can happen during taker funding validation. +/// Enum covering error cases that can happen during swap v2 transaction validation. #[derive(Debug, Display)] -pub enum ValidateTakerFundingError { +pub enum ValidateSwapV2TxError { /// Payment sent to wrong address or has invalid amount. InvalidDestinationOrAmount(String), /// Error during conversion of BigDecimal amount to coin's specific monetary units (satoshis, wei, etc.). @@ -1232,18 +1430,20 @@ pub enum ValidateTakerFundingError { TxLacksOfOutputs, /// Input payment timelock overflows the type used by specific coin. LocktimeOverflow(String), + /// Internal error + Internal(String), } -impl From for ValidateTakerFundingError { - fn from(err: NumConversError) -> Self { ValidateTakerFundingError::NumConversion(err.to_string()) } +impl From for ValidateSwapV2TxError { + fn from(err: NumConversError) -> Self { ValidateSwapV2TxError::NumConversion(err.to_string()) } } -impl From for ValidateTakerFundingError { - fn from(err: UtxoRpcError) -> Self { ValidateTakerFundingError::Rpc(err.to_string()) } +impl From for ValidateSwapV2TxError { + fn from(err: UtxoRpcError) -> Self { ValidateSwapV2TxError::Rpc(err.to_string()) } } /// Enum covering error cases that can happen during taker funding spend preimage validation. -#[derive(Debug, Display)] +#[derive(Debug, Display, EnumFromStringify)] pub enum ValidateTakerFundingSpendPreimageError { /// Funding tx has no outputs FundingTxNoOutputs, @@ -1254,37 +1454,30 @@ pub enum ValidateTakerFundingSpendPreimageError { /// Error during preimage comparison to an expected one. InvalidPreimage(String), /// Error during taker's signature check. + #[from_stringify("UtxoSignWithKeyPairError")] SignatureVerificationFailure(String), /// Error during generation of an expected preimage. TxGenError(String), /// Input payment timelock overflows the type used by specific coin. LocktimeOverflow(String), /// Coin's RPC error + #[from_stringify("UtxoRpcError")] Rpc(String), } -impl From for ValidateTakerFundingSpendPreimageError { - fn from(err: UtxoSignWithKeyPairError) -> Self { - ValidateTakerFundingSpendPreimageError::SignatureVerificationFailure(err.to_string()) - } -} - impl From for ValidateTakerFundingSpendPreimageError { fn from(err: TxGenError) -> Self { ValidateTakerFundingSpendPreimageError::TxGenError(format!("{:?}", err)) } } -impl From for ValidateTakerFundingSpendPreimageError { - fn from(err: UtxoRpcError) -> Self { ValidateTakerFundingSpendPreimageError::Rpc(err.to_string()) } -} - /// Enum covering error cases that can happen during taker payment spend preimage validation. -#[derive(Debug, Display)] +#[derive(Debug, Display, EnumFromStringify)] pub enum ValidateTakerPaymentSpendPreimageError { /// Error during signature deserialization. InvalidTakerSignature, /// Error during preimage comparison to an expected one. InvalidPreimage(String), /// Error during taker's signature check. + #[from_stringify("UtxoSignWithKeyPairError")] SignatureVerificationFailure(String), /// Error during generation of an expected preimage. TxGenError(String), @@ -1292,12 +1485,6 @@ pub enum ValidateTakerPaymentSpendPreimageError { LocktimeOverflow(String), } -impl From for ValidateTakerPaymentSpendPreimageError { - fn from(err: UtxoSignWithKeyPairError) -> Self { - ValidateTakerPaymentSpendPreimageError::SignatureVerificationFailure(err.to_string()) - } -} - impl From for ValidateTakerPaymentSpendPreimageError { fn from(err: TxGenError) -> Self { ValidateTakerPaymentSpendPreimageError::TxGenError(format!("{:?}", err)) } } @@ -1308,15 +1495,22 @@ pub trait ToBytes { } /// Defines associated types specific to each coin (Pubkey, Address, etc.) -pub trait CoinAssocTypes { +#[async_trait] +pub trait ParseCoinAssocTypes { + type Address: Send + Sync + fmt::Display; + type AddressParseError: fmt::Debug + Send + fmt::Display; type Pubkey: ToBytes + Send + Sync; - type PubkeyParseError: Send + std::fmt::Display; + type PubkeyParseError: fmt::Debug + Send + fmt::Display; type Tx: Transaction + Send + Sync; - type TxParseError: Send + std::fmt::Display; + type TxParseError: fmt::Debug + Send + fmt::Display; type Preimage: ToBytes + Send + Sync; - type PreimageParseError: Send + std::fmt::Display; + type PreimageParseError: fmt::Debug + Send + fmt::Display; type Sig: ToBytes + Send + Sync; - type SigParseError: Send + std::fmt::Display; + type SigParseError: fmt::Debug + Send + fmt::Display; + + async fn my_addr(&self) -> Self::Address; + + fn parse_address(&self, address: &str) -> Result; fn parse_pubkey(&self, pubkey: &[u8]) -> Result; @@ -1327,18 +1521,306 @@ pub trait CoinAssocTypes { fn parse_signature(&self, sig: &[u8]) -> Result; } -/// Operations specific to the [Trading Protocol Upgrade implementation](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) +/// Defines associated types specific to Non-Fungible Tokens (Token Address, Token Id, etc.) +pub trait ParseNftAssocTypes { + type ContractAddress: Send + Sync + fmt::Display; + type TokenId: ToBytes + Send + Sync; + type ContractType: ToBytes + Send + Sync; + type NftAssocTypesError: fmt::Debug + Send + fmt::Display; + + fn parse_contract_address( + &self, + contract_address: &[u8], + ) -> Result; + + fn parse_token_id(&self, token_id: &[u8]) -> Result; + + fn parse_contract_type(&self, contract_type: &[u8]) -> Result; +} + +pub struct SendMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { + /// Maker will be able to refund the payment after this timestamp + pub time_lock: u64, + /// The hash of the secret generated by taker, this is used for immediate refund + pub taker_secret_hash: &'a [u8], + /// The hash of the secret generated by maker, taker needs it to spend the payment + pub maker_secret_hash: &'a [u8], + /// Payment amount + pub amount: BigDecimal, + /// Taker's HTLC pubkey + pub taker_pub: &'a Coin::Pubkey, + /// Unique data of specific swap + pub swap_unique_data: &'a [u8], +} + +/// Structure representing necessary NFT info for Swap +pub struct NftSwapInfo<'a, Coin: ParseNftAssocTypes + ?Sized> { + /// The address of the NFT token + pub token_address: &'a Coin::ContractAddress, + /// The ID of the NFT token. + pub token_id: &'a [u8], + /// The type of smart contract that governs this NFT + pub contract_type: &'a Coin::ContractType, + /// Etomic swap contract address + pub swap_contract_address: &'a Coin::ContractAddress, +} + +pub struct SendNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAssocTypes + ?Sized> { + /// Maker will be able to refund the payment after this timestamp + pub time_lock: u64, + /// The hash of the secret generated by taker, this is used for immediate refund + pub taker_secret_hash: &'a [u8], + /// The hash of the secret generated by maker, taker needs it to spend the payment + pub maker_secret_hash: &'a [u8], + /// Payment amount + pub amount: BigDecimal, + /// Taker's HTLC pubkey + pub taker_pub: &'a Coin::Pubkey, + /// Unique data of specific swap + pub swap_unique_data: &'a [u8], + /// Structure representing necessary NFT info for Swap + pub nft_swap_info: &'a NftSwapInfo<'a, Coin>, +} + +pub struct ValidateMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { + /// Maker payment tx + pub maker_payment_tx: &'a Coin::Tx, + /// Maker will be able to refund the payment after this timestamp + pub time_lock: u64, + /// The hash of the secret generated by taker, this is used for immediate refund + pub taker_secret_hash: &'a [u8], + /// The hash of the secret generated by maker, taker needs it to spend the payment + pub maker_secret_hash: &'a [u8], + /// Payment amount + pub amount: BigDecimal, + /// Maker's HTLC pubkey + pub maker_pub: &'a Coin::Pubkey, + /// Unique data of specific swap + pub swap_unique_data: &'a [u8], +} + +pub struct ValidateNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAssocTypes + ?Sized> { + /// Maker payment tx + pub maker_payment_tx: &'a Coin::Tx, + /// Maker will be able to refund the payment after this timestamp + pub time_lock: u64, + /// The hash of the secret generated by taker, this is used for immediate refund + pub taker_secret_hash: &'a [u8], + /// The hash of the secret generated by maker, taker needs it to spend the payment + pub maker_secret_hash: &'a [u8], + /// Payment amount + pub amount: BigDecimal, + /// Taker's HTLC pubkey + pub taker_pub: &'a Coin::Pubkey, + /// Maker's HTLC pubkey + pub maker_pub: &'a Coin::Pubkey, + /// Unique data of specific swap + pub swap_unique_data: &'a [u8], + /// Structure representing necessary NFT info for Swap + pub nft_swap_info: &'a NftSwapInfo<'a, Coin>, +} + +pub struct RefundMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { + /// Maker payment tx + pub maker_payment_tx: &'a Coin::Tx, + /// Maker will be able to refund the payment after this timestamp + pub time_lock: u64, + /// The hash of the secret generated by taker, this is used for immediate refund + pub taker_secret_hash: &'a [u8], + /// The hash of the secret generated by maker, taker needs it to spend the payment + pub maker_secret_hash: &'a [u8], + /// Taker's secret + pub taker_secret: &'a [u8], + /// Taker's HTLC pubkey + pub taker_pub: &'a Coin::Pubkey, + /// Unique data of specific swap + pub swap_unique_data: &'a [u8], +} + +pub struct SpendMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { + /// Maker payment tx + pub maker_payment_tx: &'a Coin::Tx, + /// Maker will be able to refund the payment after this timestamp + pub time_lock: u64, + /// The hash of the secret generated by taker, this is used for immediate refund + pub taker_secret_hash: &'a [u8], + /// The hash of the secret generated by maker, taker needs it to spend the payment + pub maker_secret_hash: &'a [u8], + /// The secret generated by maker, revealed when maker spends taker's payment + pub maker_secret: &'a [u8], + /// Maker's HTLC pubkey + pub maker_pub: &'a Coin::Pubkey, + /// Unique data of specific swap + pub swap_unique_data: &'a [u8], +} + +pub struct SpendNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAssocTypes + ?Sized> { + /// Maker payment tx + pub maker_payment_tx: &'a Coin::Tx, + /// Maker will be able to refund the payment after this timestamp + pub time_lock: u64, + /// The hash of the secret generated by taker, this is used for immediate refund + pub taker_secret_hash: &'a [u8], + /// The hash of the secret generated by maker, taker needs it to spend the payment + pub maker_secret_hash: &'a [u8], + /// The secret generated by maker, revealed when maker spends taker's payment + pub maker_secret: &'a [u8], + /// Maker's HTLC pubkey + pub maker_pub: &'a Coin::Pubkey, + /// Unique data of specific swap + pub swap_unique_data: &'a [u8], + /// The type of smart contract that governs this NFT + pub contract_type: &'a Coin::ContractType, + /// Etomic swap contract address + pub swap_contract_address: &'a Coin::ContractAddress, +} + +/// Operations specific to maker coin in [Trading Protocol Upgrade implementation](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) +#[async_trait] +pub trait MakerCoinSwapOpsV2: ParseCoinAssocTypes + Send + Sync + 'static { + /// Generate and broadcast maker payment transaction + async fn send_maker_payment_v2(&self, args: SendMakerPaymentArgs<'_, Self>) -> Result; + + /// Validate maker payment transaction + async fn validate_maker_payment_v2(&self, args: ValidateMakerPaymentArgs<'_, Self>) -> ValidatePaymentResult<()>; + + /// Refund maker payment transaction using timelock path + async fn refund_maker_payment_v2_timelock(&self, args: RefundPaymentArgs<'_>) -> Result; + + /// Refund maker payment transaction using immediate refund path + async fn refund_maker_payment_v2_secret( + &self, + args: RefundMakerPaymentArgs<'_, Self>, + ) -> Result; + + /// Spend maker payment transaction + async fn spend_maker_payment_v2(&self, args: SpendMakerPaymentArgs<'_, Self>) -> Result; +} + #[async_trait] -pub trait SwapOpsV2: CoinAssocTypes + Send + Sync + 'static { +pub trait MakerNftSwapOpsV2: ParseCoinAssocTypes + ParseNftAssocTypes + Send + Sync + 'static { + async fn send_nft_maker_payment_v2( + &self, + args: SendNftMakerPaymentArgs<'_, Self>, + ) -> Result; + + /// Validate NFT maker payment transaction + async fn validate_nft_maker_payment_v2( + &self, + args: ValidateNftMakerPaymentArgs<'_, Self>, + ) -> ValidatePaymentResult<()>; + + /// Spend NFT maker payment transaction + async fn spend_nft_maker_payment_v2( + &self, + args: SpendNftMakerPaymentArgs<'_, Self>, + ) -> Result; + + /// Refund NFT maker payment transaction using timelock path + async fn refund_nft_maker_payment_v2_timelock( + &self, + args: RefundPaymentArgs<'_>, + ) -> Result; + + /// Refund NFT maker payment transaction using immediate refund path + async fn refund_nft_maker_payment_v2_secret( + &self, + args: RefundMakerPaymentArgs<'_, Self>, + ) -> Result; +} + +/// Enum representing errors that can occur while waiting for taker payment spend. +#[derive(Display)] +pub enum WaitForTakerPaymentSpendError { + /// Timeout error variant, indicating that the wait for taker payment spend has timed out. + #[display( + fmt = "Timed out waiting for taker payment spend, wait_until {}, now {}", + wait_until, + now + )] + Timeout { + /// The timestamp until which the wait was expected to complete. + wait_until: u64, + /// The current timestamp when the timeout occurred. + now: u64, + }, + + /// Invalid input transaction error variant, containing additional information about the error. + InvalidInputTx(String), +} + +impl From for WaitForTakerPaymentSpendError { + fn from(err: WaitForOutputSpendErr) -> Self { + match err { + WaitForOutputSpendErr::Timeout { wait_until, now } => { + WaitForTakerPaymentSpendError::Timeout { wait_until, now } + }, + WaitForOutputSpendErr::NoOutputWithIndex(index) => { + WaitForTakerPaymentSpendError::InvalidInputTx(format!("Tx doesn't have output with index {}", index)) + }, + } + } +} + +/// Enum representing different ways a funding transaction can be spent. +/// +/// This enum is generic over types that implement the `ParseCoinAssocTypes` trait. +pub enum FundingTxSpend { + /// Variant indicating that the funding transaction has been spent through a timelock path. + RefundedTimelock(T::Tx), + /// Variant indicating that the funding transaction has been spent by revealing a taker's secret (immediate refund path). + RefundedSecret { + /// The spending transaction. + tx: T::Tx, + /// The taker's secret value revealed in the spending transaction. + secret: [u8; 32], + }, + /// Variant indicating that the funds from the funding transaction have been transferred + /// to the taker's payment transaction. + TransferredToTakerPayment(T::Tx), +} + +impl fmt::Debug for FundingTxSpend { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FundingTxSpend::RefundedTimelock(tx) => { + write!(f, "RefundedTimelock({:?})", tx) + }, + FundingTxSpend::RefundedSecret { tx, secret: _ } => { + write!(f, "RefundedSecret {{ tx: {:?} }}", tx) + }, + FundingTxSpend::TransferredToTakerPayment(tx) => { + write!(f, "TransferredToTakerPayment({:?})", tx) + }, + } + } +} + +/// Enum representing errors that can occur during the search for funding spend. +#[derive(Debug)] +pub enum SearchForFundingSpendErr { + /// Variant indicating an invalid input transaction error with additional information. + InvalidInputTx(String), + /// Variant indicating a failure to process the spending transaction with additional details. + FailedToProcessSpendTx(String), + /// Variant indicating a coin's RPC error with additional information. + Rpc(String), + /// Variant indicating an error during conversion of the `from_block` argument with associated `TryFromIntError`. + FromBlockConversionErr(TryFromIntError), +} + +/// Operations specific to taker coin in [Trading Protocol Upgrade implementation](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) +#[async_trait] +pub trait TakerCoinSwapOpsV2: ParseCoinAssocTypes + Send + Sync + 'static { /// Generate and broadcast taker funding transaction that includes dex fee, maker premium and actual trading volume. /// Funding tx can be reclaimed immediately if maker back-outs (doesn't send maker payment) async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result; /// Validates taker funding transaction. - async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateTakerFundingResult; + async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateSwapV2TxResult; /// Refunds taker funding transaction using time-locked path without secret reveal. - async fn refund_taker_funding_timelock(&self, args: RefundPaymentArgs<'_>) -> TransactionResult; + async fn refund_taker_funding_timelock(&self, args: RefundPaymentArgs<'_>) -> Result; /// Reclaims taker funding transaction using immediate refund path with secret reveal. async fn refund_taker_funding_secret( @@ -1346,6 +1828,14 @@ pub trait SwapOpsV2: CoinAssocTypes + Send + Sync + 'static { args: RefundFundingSecretArgs<'_, Self>, ) -> Result; + /// Looks for taker funding transaction spend and detects path used + async fn search_for_taker_funding_spend( + &self, + tx: &Self::Tx, + from_block: u64, + secret_hash: &[u8], + ) -> Result>, SearchForFundingSpendErr>; + /// Generates and signs a preimage spending funding tx to the combined taker payment async fn gen_taker_funding_spend_preimage( &self, @@ -1369,7 +1859,7 @@ pub trait SwapOpsV2: CoinAssocTypes + Send + Sync + 'static { ) -> Result; /// Refunds taker payment transaction. - async fn refund_combined_taker_payment(&self, args: RefundPaymentArgs<'_>) -> TransactionResult; + async fn refund_combined_taker_payment(&self, args: RefundPaymentArgs<'_>) -> Result; /// Generates and signs taker payment spend preimage. The preimage and signature should be /// shared with maker to proceed with protocol execution. @@ -1393,7 +1883,15 @@ pub trait SwapOpsV2: CoinAssocTypes + Send + Sync + 'static { gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], swap_unique_data: &[u8], - ) -> TransactionResult; + ) -> Result; + + /// Wait until taker payment spend is found on-chain + async fn wait_for_taker_payment_spend( + &self, + taker_payment: &Self::Tx, + from_block: u64, + wait_until: u64, + ) -> MmResult; /// Derives an HTLC key-pair and returns a public key corresponding to that key. fn derive_htlc_pubkey_v2(&self, swap_unique_data: &[u8]) -> Self::Pubkey; @@ -1401,12 +1899,13 @@ pub trait SwapOpsV2: CoinAssocTypes + Send + Sync + 'static { /// Operations that coins have independently from the MarketMaker. /// That is, things implemented by the coin wallets or public coin services. +#[async_trait] pub trait MarketCoinOps { fn ticker(&self) -> &str; fn my_address(&self) -> MmResult; - fn get_public_key(&self) -> Result>; + async fn get_public_key(&self) -> Result>; fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]>; @@ -1441,6 +1940,9 @@ pub trait MarketCoinOps { /// Receives raw transaction bytes as input and returns tx hash in hexadecimal format fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send>; + /// Signs raw utxo transaction in hexadecimal format as input and returns signed transaction in hexadecimal format + async fn sign_raw_tx(&self, args: &SignRawTransactionRequest) -> RawTransactionResult; + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send>; fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut; @@ -1458,6 +1960,17 @@ pub trait MarketCoinOps { fn min_trading_vol(&self) -> MmNumber; fn is_privacy(&self) -> bool { false } + + fn is_trezor(&self) -> bool; +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum EthGasLimitOption { + /// Use this value as gas limit + Set(u64), + /// Make MM2 calculate gas limit + Calc, } #[derive(Clone, Debug, Deserialize, PartialEq)] @@ -1474,6 +1987,12 @@ pub enum WithdrawFee { gas_price: BigDecimal, gas: u64, }, + EthGasEip1559 { + /// in gwei + max_priority_fee_per_gas: BigDecimal, + max_fee_per_gas: BigDecimal, + gas_option: EthGasLimitOption, + }, Qrc20Gas { /// in satoshi gas_limit: u64, @@ -1485,22 +2004,6 @@ pub enum WithdrawFee { }, } -pub struct WithdrawSenderAddress { - address: Address, - pubkey: Pubkey, - derivation_path: Option, -} - -impl From> for WithdrawSenderAddress { - fn from(addr: HDAddress) -> Self { - WithdrawSenderAddress { - address: addr.address, - pubkey: addr.pubkey, - derivation_path: Some(addr.derivation_path), - } - } -} - /// Rename to `GetWithdrawSenderAddresses` when withdraw supports multiple `from` addresses. #[async_trait] pub trait GetWithdrawSenderAddress { @@ -1513,19 +2016,10 @@ pub trait GetWithdrawSenderAddress { ) -> MmResult, WithdrawError>; } -#[derive(Clone, Deserialize, Serialize)] -#[serde(untagged)] -pub enum WithdrawFrom { - AddressId(HDAccountAddressId), - /// Don't use `Bip44DerivationPath` or `RpcDerivationPath` because if there is an error in the path, - /// `serde::Deserialize` returns "data did not match any variant of untagged enum WithdrawFrom". - /// It's better to show the user an informative error. - DerivationPath { - derivation_path: String, - }, - HDWalletAddress(StandardHDCoinAddress), -} - +/// TODO: Avoid using a single request structure on every platform. +/// Instead, accept a generic type from withdraw implementations. +/// This way we won't have to update the payload for every platform when +/// one of them requires specific addition. #[derive(Clone, Deserialize)] pub struct WithdrawRequest { coin: String, @@ -1537,6 +2031,8 @@ pub struct WithdrawRequest { max: bool, fee: Option, memo: Option, + /// Tendermint specific field used for manually providing the IBC channel IDs. + ibc_source_channel: Option, /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask **only**. #[cfg(target_arch = "wasm32")] #[serde(default)] @@ -1591,6 +2087,7 @@ impl WithdrawRequest { max: true, fee: None, memo: None, + ibc_source_channel: None, #[cfg(target_arch = "wasm32")] broadcast: false, } @@ -1733,15 +2230,14 @@ pub enum TransactionType { token_id: Option, }, NftTransfer, + TendermintIBCTransfer, } /// Transaction details #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TransactionDetails { - /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction - pub tx_hex: BytesJson, - /// Transaction hash in hexadecimal format - tx_hash: String, + #[serde(flatten)] + pub tx: TransactionData, /// Coins are sent from these addresses from: Vec, /// Coins are sent to these addresses @@ -1775,6 +2271,40 @@ pub struct TransactionDetails { memo: Option, } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum TransactionData { + Signed { + /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction + tx_hex: BytesJson, + /// Transaction hash in hexadecimal format + tx_hash: String, + }, + /// This can contain entirely different data depending on the platform. + /// TODO: Perhaps using generics would be more suitable here? + Unsigned(Json), +} + +impl TransactionData { + pub fn new_signed(tx_hex: BytesJson, tx_hash: String) -> Self { Self::Signed { tx_hex, tx_hash } } + + pub fn new_unsigned(unsigned_tx_data: Json) -> Self { Self::Unsigned(unsigned_tx_data) } + + pub fn tx_hex(&self) -> Option<&BytesJson> { + match self { + TransactionData::Signed { tx_hex, .. } => Some(tx_hex), + TransactionData::Unsigned(_) => None, + } + } + + pub fn tx_hash(&self) -> Option<&str> { + match self { + TransactionData::Signed { tx_hash, .. } => Some(tx_hash), + TransactionData::Unsigned(_) => None, + } + } +} + #[derive(Clone, Copy, Debug)] pub struct BlockHeightAndTime { height: u64, @@ -1820,12 +2350,38 @@ pub struct TradeFee { pub paid_from_trading_vol: bool, } +/// A type alias for a HashMap where the key is a String representing the coin/token ticker, +/// and the value is a `CoinBalance` struct representing the balance of that coin/token. +/// This is used to represent the balance of a wallet or account for multiple coins/tokens. +pub type CoinBalanceMap = HashMap; + +impl BalanceObjectOps for CoinBalanceMap { + fn new() -> Self { HashMap::new() } + + fn add(&mut self, other: Self) { + for (ticker, balance) in other { + let total_balance = self.entry(ticker).or_insert_with(CoinBalance::default); + *total_balance += balance; + } + } + + fn get_total_for_ticker(&self, ticker: &str) -> Option { self.get(ticker).map(|b| b.get_total()) } +} + #[derive(Clone, Debug, Default, PartialEq, PartialOrd, Serialize)] pub struct CoinBalance { pub spendable: BigDecimal, pub unspendable: BigDecimal, } +impl BalanceObjectOps for CoinBalance { + fn new() -> Self { CoinBalance::default() } + + fn add(&mut self, other: Self) { *self += other; } + + fn get_total_for_ticker(&self, _ticker: &str) -> Option { Some(self.get_total()) } +} + impl CoinBalance { pub fn new(spendable: BigDecimal) -> CoinBalance { CoinBalance { @@ -1850,8 +2406,15 @@ impl Add for CoinBalance { } } +impl AddAssign for CoinBalance { + fn add_assign(&mut self, rhs: Self) { + self.spendable += rhs.spendable; + self.unspendable += rhs.unspendable; + } +} + /// The approximation is needed to cover the dynamic miner fee changing during a swap. -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum FeeApproxStage { /// Do not increase the trade fee. WithoutApprox, @@ -1871,7 +2434,43 @@ pub enum TradePreimageValue { UpperBound(BigDecimal), } -#[derive(Debug, Display, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub enum SwapTxFeePolicy { + #[default] + Unsupported, + Internal, + Low, + Medium, + High, +} + +#[derive(Debug, Deserialize)] +pub struct SwapTxFeePolicyRequest { + coin: String, + #[serde(default)] + swap_tx_fee_policy: SwapTxFeePolicy, +} + +#[derive(Debug, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum SwapTxFeePolicyError { + #[from_stringify("CoinFindError")] + NoSuchCoin(String), + #[display(fmt = "eip-1559 policy is not supported for coin {}", _0)] + NotSupported(String), +} + +impl HttpStatusCode for SwapTxFeePolicyError { + fn status_code(&self) -> StatusCode { + match self { + SwapTxFeePolicyError::NoSuchCoin(_) | SwapTxFeePolicyError::NotSupported(_) => StatusCode::BAD_REQUEST, + } + } +} + +pub type SwapTxFeePolicyResult = Result>; + +#[derive(Debug, Display, EnumFromStringify, PartialEq)] pub enum TradePreimageError { #[display( fmt = "Not enough {} to preimage the trade: available {}, required at least {}", @@ -1888,16 +2487,11 @@ pub enum TradePreimageError { AmountIsTooSmall { amount: BigDecimal, threshold: BigDecimal }, #[display(fmt = "Transport error: {}", _0)] Transport(String), + #[from_stringify("NumConversError", "UnexpectedDerivationMethod")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), -} - -impl From for TradePreimageError { - fn from(e: NumConversError) -> Self { TradePreimageError::InternalError(e.to_string()) } -} - -impl From for TradePreimageError { - fn from(e: UnexpectedDerivationMethod) -> Self { TradePreimageError::InternalError(e.to_string()) } + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, } impl TradePreimageError { @@ -1968,7 +2562,7 @@ impl TradePreimageError { } /// The reason of unsuccessful conversion of two internal numbers, e.g. `u64` from `BigNumber`. -#[derive(Debug, Display)] +#[derive(Clone, Debug, Display)] pub struct NumConversError(String); impl From for NumConversError { @@ -1981,7 +2575,8 @@ impl NumConversError { pub fn description(&self) -> &str { &self.0 } } -#[derive(Clone, Debug, Display, PartialEq, Serialize)] +#[derive(Clone, Debug, Display, EnumFromStringify, PartialEq, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] pub enum BalanceError { #[display(fmt = "Transport: {}", _0)] Transport(String), @@ -1990,6 +2585,7 @@ pub enum BalanceError { UnexpectedDerivationMethod(UnexpectedDerivationMethod), #[display(fmt = "Wallet storage error: {}", _0)] WalletStorageError(String), + #[from_stringify("Bip32Error", "NumConversError")] #[display(fmt = "Internal: {}", _0)] Internal(String), } @@ -2002,29 +2598,39 @@ pub enum GetNonZeroBalance { BalanceIsZero, } -impl From for GetNonZeroBalance { - fn from(e: BalanceError) -> Self { GetNonZeroBalance::MyBalanceError(e) } +impl From for BalanceError { + fn from(e: AddressDerivingError) -> Self { BalanceError::Internal(e.to_string()) } +} + +impl From for BalanceError { + fn from(e: AccountUpdatingError) -> Self { + let error = e.to_string(); + match e { + AccountUpdatingError::AddressLimitReached { .. } | AccountUpdatingError::InvalidBip44Chain(_) => { + // Account updating is expected to be called after `address_id` and `chain` validation. + BalanceError::Internal(format!("Unexpected internal error: {}", error)) + }, + AccountUpdatingError::WalletStorageError(_) => BalanceError::WalletStorageError(error), + } + } } -impl From for BalanceError { - fn from(e: NumConversError) -> Self { BalanceError::Internal(e.to_string()) } +impl From for GetNonZeroBalance { + fn from(e: BalanceError) -> Self { GetNonZeroBalance::MyBalanceError(e) } } impl From for BalanceError { fn from(e: UnexpectedDerivationMethod) -> Self { BalanceError::UnexpectedDerivationMethod(e) } } -impl From for BalanceError { - fn from(e: Bip32Error) -> Self { BalanceError::Internal(e.to_string()) } -} - -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum StakingInfosError { #[display(fmt = "Staking infos not available for: {}", coin)] CoinDoesntSupportStakingInfos { coin: String }, #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, + #[from_stringify("UnexpectedDerivationMethod")] #[display(fmt = "Derivation method is not supported: {}", _0)] UnexpectedDerivationMethod(String), #[display(fmt = "Transport error: {}", _0)] @@ -2045,10 +2651,6 @@ impl From for StakingInfosError { } } -impl From for StakingInfosError { - fn from(e: UnexpectedDerivationMethod) -> Self { StakingInfosError::UnexpectedDerivationMethod(e.to_string()) } -} - impl From for StakingInfosError { fn from(e: Qrc20AddressError) -> Self { match e { @@ -2066,7 +2668,8 @@ impl HttpStatusCode for StakingInfosError { StakingInfosError::NoSuchCoin { .. } | StakingInfosError::CoinDoesntSupportStakingInfos { .. } | StakingInfosError::UnexpectedDerivationMethod(_) => StatusCode::BAD_REQUEST, - StakingInfosError::Transport(_) | StakingInfosError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + StakingInfosError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + StakingInfosError::Transport(_) => StatusCode::BAD_GATEWAY, } } } @@ -2079,7 +2682,7 @@ impl From for StakingInfosError { } } -#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum DelegationError { #[display( @@ -2101,6 +2704,7 @@ pub enum DelegationError { NoSuchCoin { coin: String }, #[display(fmt = "{}", _0)] CannotInteractWithSmartContract(String), + #[from_stringify("ScriptHashTypeNotSupported")] #[display(fmt = "{}", _0)] AddressError(String), #[display(fmt = "Already delegating to: {}", _0)] @@ -2109,6 +2713,7 @@ pub enum DelegationError { DelegationOpsNotSupported { reason: String }, #[display(fmt = "Transport error: {}", _0)] Transport(String), + #[from_stringify("MyAddressError")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), } @@ -2179,14 +2784,11 @@ impl From for DelegationError { } } -impl From for DelegationError { - fn from(e: ScriptHashTypeNotSupported) -> Self { DelegationError::AddressError(e.to_string()) } -} - impl HttpStatusCode for DelegationError { fn status_code(&self) -> StatusCode { match self { - DelegationError::Transport(_) | DelegationError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + DelegationError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + DelegationError::Transport(_) => StatusCode::BAD_GATEWAY, _ => StatusCode::BAD_REQUEST, } } @@ -2306,7 +2908,12 @@ pub enum WithdrawError { #[display(fmt = "Transport error: {}", _0)] Transport(String), #[from_trait(WithInternal::internal)] - #[from_stringify("NumConversError", "UnexpectedDerivationMethod", "PrivKeyPolicyNotAllowed")] + #[from_stringify( + "MyAddressError", + "NumConversError", + "UnexpectedDerivationMethod", + "PrivKeyPolicyNotAllowed" + )] #[display(fmt = "Internal error: {}", _0)] InternalError(String), #[display(fmt = "Unsupported error: {}", _0)] @@ -2315,11 +2922,6 @@ pub enum WithdrawError { CoinDoesntSupportNftWithdraw { coin: String, }, - #[display(fmt = "My address {} and from address {} mismatch", my_address, from)] - AddressMismatchError { - my_address: String, - from: String, - }, #[display(fmt = "Contract type {} doesnt support 'withdraw_nft' yet", _0)] ContractTypeDoesntSupportNftWithdrawing(String), #[display(fmt = "Action not allowed for coin: {}", _0)] @@ -2340,6 +2942,28 @@ pub enum WithdrawError { }, #[display(fmt = "DB error {}", _0)] DbError(String), + #[display(fmt = "My address is {}, while current Nft owner is {}", my_address, token_owner)] + MyAddressNotNftOwner { + my_address: String, + token_owner: String, + }, + #[display(fmt = "Nft Protocol is not supported yet!")] + NftProtocolNotSupported, + #[display(fmt = "Chain id must be set for typed transaction for coin {}", coin)] + NoChainIdSet { + coin: String, + }, + #[display(fmt = "Signing error {}", _0)] + SigningError(String), + #[display(fmt = "Eth transaction type not supported")] + TxTypeNotSupported, + #[display(fmt = "'chain_registry_name' was not found in coins configuration for '{}'", _0)] + RegistryNameIsMissing(String), + #[display( + fmt = "IBC channel could not found for '{}' address. Consider providing it manually with 'ibc_source_channel' in the request.", + _0 + )] + IBCChannelCouldNotFound(String), } impl HttpStatusCode for WithdrawError { @@ -2362,16 +2986,33 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::UnsupportedError(_) | WithdrawError::ActionNotAllowed(_) | WithdrawError::GetNftInfoError(_) - | WithdrawError::AddressMismatchError { .. } | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) | WithdrawError::CoinDoesntSupportNftWithdraw { .. } - | WithdrawError::NotEnoughNftsAmount { .. } => StatusCode::BAD_REQUEST, + | WithdrawError::NotEnoughNftsAmount { .. } + | WithdrawError::NoChainIdSet { .. } + | WithdrawError::TxTypeNotSupported + | WithdrawError::SigningError(_) + | WithdrawError::RegistryNameIsMissing(_) + | WithdrawError::IBCChannelCouldNotFound(_) + | WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] WithdrawError::BroadcastExpected(_) => StatusCode::BAD_REQUEST, - WithdrawError::Transport(_) | WithdrawError::InternalError(_) | WithdrawError::DbError(_) => { + WithdrawError::InternalError(_) | WithdrawError::DbError(_) | WithdrawError::NftProtocolNotSupported => { StatusCode::INTERNAL_SERVER_ERROR }, + WithdrawError::Transport(_) => StatusCode::BAD_GATEWAY, + } + } +} + +impl From for WithdrawError { + fn from(e: AddressDerivingError) -> Self { + match e { + AddressDerivingError::InvalidBip44Chain { .. } | AddressDerivingError::Bip32Error(_) => { + WithdrawError::UnexpectedFromAddress(e.to_string()) + }, + AddressDerivingError::Internal(internal) => WithdrawError::InternalError(internal), } } } @@ -2395,6 +3036,17 @@ impl From for WithdrawError { } } +impl From for WithdrawError { + fn from(e: HDWithdrawError) -> Self { + match e { + HDWithdrawError::UnexpectedFromAddress(e) => WithdrawError::UnexpectedFromAddress(e), + HDWithdrawError::UnknownAccount { account_id } => WithdrawError::UnknownAccount { account_id }, + HDWithdrawError::AddressDerivingError(e) => e.into(), + HDWithdrawError::InternalError(e) => WithdrawError::InternalError(e), + } + } +} + impl From for WithdrawError { fn from(e: UtxoSignWithKeyPairError) -> Self { let error = format!("Error signing: {}", e); @@ -2409,9 +3061,6 @@ impl From for WithdrawError { impl From for WithdrawError { fn from(e: GetValidEthWithdrawAddError) -> Self { match e { - GetValidEthWithdrawAddError::AddressMismatchError { my_address, from } => { - WithdrawError::AddressMismatchError { my_address, from } - }, GetValidEthWithdrawAddError::CoinDoesntSupportNftWithdraw { coin } => { WithdrawError::CoinDoesntSupportNftWithdraw { coin } }, @@ -2426,6 +3075,7 @@ impl From for WithdrawError { EthGasDetailsErr::InvalidFeePolicy(e) => WithdrawError::InvalidFeePolicy(e), EthGasDetailsErr::Internal(e) => WithdrawError::InternalError(e), EthGasDetailsErr::Transport(e) => WithdrawError::Transport(e), + EthGasDetailsErr::NftProtocolNotSupported => WithdrawError::NftProtocolNotSupported, } } } @@ -2433,7 +3083,7 @@ impl From for WithdrawError { impl From for WithdrawError { fn from(e: Bip32Error) -> Self { let error = format!("Error deriving key: {}", e); - WithdrawError::InternalError(error) + WithdrawError::UnexpectedFromAddress(error) } } @@ -2506,17 +3156,21 @@ impl HttpStatusCode for SignatureError { } } -#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Display, EnumFromStringify, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum VerificationError { #[display(fmt = "Invalid request: {}", _0)] InvalidRequest(String), + #[from_stringify("ethkey::Error", "keys::Error")] #[display(fmt = "Internal error: {}", _0)] InternalError(String), + #[from_stringify("base64::DecodeError")] #[display(fmt = "Signature decoding error: {}", _0)] SignatureDecodingError(String), + #[from_stringify("hex::FromHexError")] #[display(fmt = "Address decoding error: {}", _0)] AddressDecodingError(String), + #[from_stringify("CoinFindError")] #[display(fmt = "Coin is not found: {}", _0)] CoinIsNotFound(String), #[display(fmt = "sign_message_prefix is not set in coin config")] @@ -2536,14 +3190,6 @@ impl HttpStatusCode for VerificationError { } } -impl From for VerificationError { - fn from(e: base64::DecodeError) -> Self { VerificationError::SignatureDecodingError(e.to_string()) } -} - -impl From for VerificationError { - fn from(e: hex::FromHexError) -> Self { VerificationError::AddressDecodingError(e.to_string()) } -} - impl From for VerificationError { fn from(e: FromBase58Error) -> Self { match e { @@ -2557,18 +3203,6 @@ impl From for VerificationError { } } -impl From for VerificationError { - fn from(e: keys::Error) -> Self { VerificationError::InternalError(e.to_string()) } -} - -impl From for VerificationError { - fn from(e: ethkey::Error) -> Self { VerificationError::InternalError(e.to_string()) } -} - -impl From for VerificationError { - fn from(e: CoinFindError) -> Self { VerificationError::CoinIsNotFound(e.to_string()) } -} - /// NB: Implementations are expected to follow the pImpl idiom, providing cheap reference-counted cloning and garbage collection. #[async_trait] pub trait MmCoin: @@ -2660,11 +3294,12 @@ pub trait MmCoin: /// Get fee to be paid per 1 swap transaction fn get_trade_fee(&self) -> Box + Send>; - /// Get fee to be paid by sender per whole swap using the sending value and check if the wallet has sufficient balance to pay the fee. + /// Get fee to be paid by sender per whole swap (including possible refund) using the sending value and check if the wallet has sufficient balance to pay the fee. async fn get_sender_trade_fee( &self, value: TradePreimageValue, stage: FeeApproxStage, + include_refund_fee: bool, ) -> TradePreimageResult; /// Get fee to be paid by receiver per whole swap and check if the wallet has sufficient balance to pay the fee. @@ -2783,6 +3418,8 @@ pub enum MmCoinEnum { SplToken(SplToken), #[cfg(not(target_arch = "wasm32"))] LightningCoin(LightningCoin), + #[cfg(feature = "enable-sia")] + SiaCoin(SiaCoin), Test(TestCoin), } @@ -2851,6 +3488,11 @@ impl From for MmCoinEnum { fn from(c: ZCoin) -> MmCoinEnum { MmCoinEnum::ZCoin(c) } } +#[cfg(feature = "enable-sia")] +impl From for MmCoinEnum { + fn from(c: SiaCoin) -> MmCoinEnum { MmCoinEnum::SiaCoin(c) } +} + // NB: When stable and groked by IDEs, `enum_dispatch` can be used instead of `Deref` to speed things up. impl Deref for MmCoinEnum { type Target = dyn MmCoin; @@ -2867,6 +3509,8 @@ impl Deref for MmCoinEnum { #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::LightningCoin(ref c) => c, MmCoinEnum::ZCoin(ref c) => c, + #[cfg(feature = "enable-sia")] + MmCoinEnum::SiaCoin(ref c) => c, MmCoinEnum::Test(ref c) => c, #[cfg(all( feature = "enable-solana", @@ -2917,7 +3561,7 @@ pub struct MmCoinStruct { } impl MmCoinStruct { - fn new(coin: MmCoinEnum) -> Self { + pub fn new(coin: MmCoinEnum) -> Self { Self { inner: coin, is_available: AtomicBool::new(true).into(), @@ -3087,10 +3731,17 @@ impl CoinsContext { self.add_token(coin).await } + /// Adds a platform coin and its associated tokens to the CoinsContext. + /// + /// Registers a platform coin alongside its associated ERC-20 tokens and optionally a global NFT. + /// Regular tokens are added to the context without overwriting existing entries, preserving any previously activated tokens. + /// In contrast, the global NFT, if provided, replaces any previously stored NFT data for the platform, ensuring the NFT info is up-to-date. + /// An error is returned if the platform coin is already activated within the context, enforcing a single active instance for each platform. pub async fn add_platform_with_tokens( &self, platform: MmCoinEnum, tokens: Vec, + global_nft: Option, ) -> Result<(), MmError> { let mut coins = self.coins.lock().await; let mut platform_coin_tokens = self.platform_coin_tokens.lock(); @@ -3121,6 +3772,11 @@ impl CoinsContext { .entry(token.ticker().into()) .or_insert_with(|| MmCoinStruct::new(token)); } + if let Some(nft) = global_nft { + token_tickers.insert(nft.ticker().to_string()); + // For NFT overwrite existing data + coins.insert(nft.ticker().into(), MmCoinStruct::new(nft)); + } platform_coin_tokens .entry(platform_ticker) @@ -3178,32 +3834,62 @@ impl CoinsContext { async fn tx_history_db(&self) -> TxHistoryResult> { Ok(self.tx_history_db.get_or_initialize().await?) } + + #[inline(always)] + pub async fn lock_coins(&self) -> AsyncMutexGuard> { self.coins.lock().await } } /// This enum is used in coin activation requests. -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Deserialize, Serialize, Default)] pub enum PrivKeyActivationPolicy { + #[default] ContextPrivKey, Trezor, } -impl Default for PrivKeyActivationPolicy { - fn default() -> Self { PrivKeyActivationPolicy::ContextPrivKey } +impl PrivKeyActivationPolicy { + pub fn is_hw_policy(&self) -> bool { matches!(self, PrivKeyActivationPolicy::Trezor) } } +/// Enum representing various private key management policies. +/// +/// This enum defines the various ways in which private keys can be managed +/// or sourced within the system, whether it's from a local software-based HD Wallet, +/// a hardware device like Trezor, or even external sources like Metamask. #[derive(Clone, Debug)] pub enum PrivKeyPolicy { + /// The legacy private key policy. + /// + /// This policy corresponds to a one-to-one mapping of private keys to addresses. + /// In this scheme, only a single key and corresponding address is activated per coin, + /// without any hierarchical deterministic derivation. Iguana(T), + /// The HD Wallet private key policy. + /// + /// This variant uses a BIP44 derivation path up to the coin level + /// and contains the necessary information to manage and derive + /// keys using an HD Wallet scheme. HDWallet { - /// Derivation path of the coin. - /// This derivation path consists of `purpose` and `coin_type` only - /// where the full `BIP44` address has the following structure: + /// Derivation path up to coin. + /// + /// Represents the first two segments of the BIP44 derivation path: `purpose` and `coin_type`. + /// A full BIP44 address is structured as: /// `m/purpose'/coin_type'/account'/change/address_index`. - derivation_path: StandardHDPathToCoin, + path_to_coin: HDPathToCoin, + /// The key that's currently activated and in use for this HD Wallet policy. activated_key: T, + /// Extended private key based on the secp256k1 elliptic curve cryptography scheme. bip39_secp_priv_key: ExtendedPrivateKey, }, + /// The Trezor hardware wallet private key policy. + /// + /// Details about how the keys are managed with the Trezor device + /// are abstracted away and are not directly managed by this policy. Trezor, + /// The Metamask private key policy, specific to the WASM target architecture. + /// + /// This variant encapsulates details about how keys are managed when interfacing + /// with the Metamask extension, especially within web-based contexts. #[cfg(target_arch = "wasm32")] Metamask(EthMetamaskPolicy), } @@ -3263,17 +3949,22 @@ impl PrivKeyPolicy { }) } - fn derivation_path(&self) -> Option<&StandardHDPathToCoin> { + fn path_to_coin(&self) -> Option<&HDPathToCoin> { match self { - PrivKeyPolicy::HDWallet { derivation_path, .. } => Some(derivation_path), - PrivKeyPolicy::Iguana(_) | PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::HDWallet { + path_to_coin: derivation_path, + .. + } => Some(derivation_path), + PrivKeyPolicy::Trezor => None, + PrivKeyPolicy::Iguana(_) => None, #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => None, } } - fn derivation_path_or_err(&self) -> Result<&StandardHDPathToCoin, MmError> { - self.derivation_path().or_mm_err(|| { + // Todo: this can be removed after the HDWallet is fully implemented for all protocols + fn path_to_coin_or_err(&self) -> Result<&HDPathToCoin, MmError> { + self.path_to_coin().or_mm_err(|| { PrivKeyPolicyNotAllowed::UnsupportedMethod( "`derivation_path_or_err` is supported only for `PrivKeyPolicy::HDWallet`".to_string(), ) @@ -3282,13 +3973,56 @@ impl PrivKeyPolicy { fn hd_wallet_derived_priv_key_or_err( &self, - path_to_address: &StandardHDCoinAddress, + derivation_path: &DerivationPath, ) -> Result> { let bip39_secp_priv_key = self.bip39_secp_priv_key_or_err()?; - let derivation_path = self.derivation_path_or_err()?; - derive_secp256k1_secret(bip39_secp_priv_key.clone(), derivation_path, path_to_address) + derive_secp256k1_secret(bip39_secp_priv_key.clone(), derivation_path) .mm_err(|e| PrivKeyPolicyNotAllowed::InternalError(e.to_string())) } + + fn is_trezor(&self) -> bool { matches!(self, PrivKeyPolicy::Trezor) } +} + +/// 'CoinWithPrivKeyPolicy' trait is used to get the private key policy of a coin. +pub trait CoinWithPrivKeyPolicy { + /// The type of the key pair used by the coin. + type KeyPair; + + /// Returns the private key policy of the coin. + fn priv_key_policy(&self) -> &PrivKeyPolicy; +} + +/// A common function to get the extended public key for a certain coin and derivation path. +pub async fn extract_extended_pubkey_impl( + coin: &Coin, + xpub_extractor: Option, + derivation_path: DerivationPath, +) -> MmResult +where + XPubExtractor: HDXPubExtractor + Send, + Coin: HDWalletCoinOps + CoinWithPrivKeyPolicy, +{ + match xpub_extractor { + Some(xpub_extractor) => { + let trezor_coin = coin.trezor_coin()?; + let xpub = xpub_extractor.extract_xpub(trezor_coin, derivation_path).await?; + Secp256k1ExtendedPublicKey::from_str(&xpub).map_to_mm(|e| HDExtractPubkeyError::InvalidXpub(e.to_string())) + }, + None => { + let mut priv_key = coin + .priv_key_policy() + .bip39_secp_priv_key_or_err() + .mm_err(|e| HDExtractPubkeyError::Internal(e.to_string()))? + .clone(); + for child in derivation_path { + priv_key = priv_key + .derive_child(child) + .map_to_mm(|e| HDExtractPubkeyError::Internal(e.to_string()))?; + } + drop_mutability!(priv_key); + Ok(priv_key.public_key()) + }, + } } #[derive(Clone)] @@ -3313,22 +4047,55 @@ impl PrivKeyBuildPolicy { } } +/// Serializable struct for compatibility with the discontinued DerivationMethod struct +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type", content = "data")] +pub enum DerivationMethodResponse { + /// Legacy iguana's privkey derivation, used by default + Iguana, + /// HD wallet derivation path, String is temporary here + HDWallet(String), +} + +/// Enum representing methods for deriving cryptographic addresses. +/// +/// This enum distinguishes between two primary strategies for address generation: +/// 1. A static, single address approach. +/// 2. A hierarchical deterministic (HD) wallet that can derive multiple addresses. #[derive(Debug)] -pub enum DerivationMethod { +pub enum DerivationMethod +where + HDWallet: HDWalletOps, + HDWalletAddress: Into
, +{ + /// Represents the use of a single, static address for transactions and operations. SingleAddress(Address), + /// Represents the use of an HD wallet for deriving multiple addresses. + /// + /// The encapsulated HD wallet should be capable of operations like + /// getting the globally enabled address, and more, as defined by the + /// [`HDWalletOps`] trait. HDWallet(HDWallet), } -impl DerivationMethod { - pub fn single_addr(&self) -> Option<&Address> { +impl DerivationMethod +where + Address: Clone, + HDWallet: HDWalletOps, + HDWalletAddress: Into
, +{ + pub async fn single_addr(&self) -> Option
{ match self { - DerivationMethod::SingleAddress(my_address) => Some(my_address), - DerivationMethod::HDWallet(_) => None, + DerivationMethod::SingleAddress(my_address) => Some(my_address.clone()), + DerivationMethod::HDWallet(hd_wallet) => { + hd_wallet.get_enabled_address().await.map(|addr| addr.address().into()) + }, } } - pub fn single_addr_or_err(&self) -> MmResult<&Address, UnexpectedDerivationMethod> { + pub async fn single_addr_or_err(&self) -> MmResult { self.single_addr() + .await .or_mm_err(|| UnexpectedDerivationMethod::ExpectedSingleAddress) } @@ -3347,19 +4114,93 @@ impl DerivationMethod { /// # Panic /// /// Panic if the address mode is [`DerivationMethod::HDWallet`]. - pub fn unwrap_single_addr(&self) -> &Address { self.single_addr_or_err().unwrap() } + pub async fn unwrap_single_addr(&self) -> Address { self.single_addr_or_err().await.unwrap() } + + pub async fn to_response(&self) -> MmResult { + match self { + DerivationMethod::SingleAddress(_) => Ok(DerivationMethodResponse::Iguana), + DerivationMethod::HDWallet(hd_wallet) => { + let enabled_address = hd_wallet + .get_enabled_address() + .await + .or_mm_err(|| UnexpectedDerivationMethod::ExpectedHDWallet)?; + Ok(DerivationMethodResponse::HDWallet( + enabled_address.derivation_path().to_string(), + )) + }, + } + } } +/// A trait representing coins with specific address derivation methods. +/// +/// This trait is designed for coins that have a defined mechanism for address derivation, +/// be it a single address approach or a hierarchical deterministic (HD) wallet strategy. +/// Coins implementing this trait should be clear about their chosen derivation method and +/// offer utility functions to interact with that method. +/// +/// Implementors of this trait will typically be coins or tokens that are either used within +/// a traditional single address scheme or leverage the power and flexibility of HD wallets. #[async_trait] -pub trait CoinWithDerivationMethod { - type Address; - type HDWallet; - - fn derivation_method(&self) -> &DerivationMethod; +pub trait CoinWithDerivationMethod: HDWalletCoinOps { + /// Returns the address derivation method associated with the coin. + /// + /// Implementors should return the specific `DerivationMethod` that the coin utilizes, + /// either `SingleAddress` for a static address approach or `HDWallet` for an HD wallet strategy. + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet>; + /// Checks if the coin uses the HD wallet strategy for address derivation. + /// + /// This is a utility function that returns `true` if the coin's derivation method is `HDWallet` and + /// `false` otherwise. + /// + /// # Returns + /// + /// - `true` if the coin uses an HD wallet for address derivation. + /// - `false` if it uses any other method. fn has_hd_wallet_derivation_method(&self) -> bool { matches!(self.derivation_method(), DerivationMethod::HDWallet(_)) } + + /// Retrieves all addresses associated with the coin. + async fn all_addresses(&self) -> MmResult>, AddressDerivingError> { + const ADDRESSES_CAPACITY: usize = 60; + + match self.derivation_method() { + DerivationMethod::SingleAddress(ref my_address) => Ok(iter::once(my_address.clone()).collect()), + DerivationMethod::HDWallet(ref hd_wallet) => { + let hd_accounts = hd_wallet.get_accounts().await; + + // We pre-allocate a suitable capacity for the HashSet to try to avoid re-allocations. + // If the capacity is exceeded, the HashSet will automatically resize itself by re-allocating, + // but this will not happen in most use cases where addresses will be below the capacity. + let mut all_addresses = HashSet::with_capacity(ADDRESSES_CAPACITY); + for (_, hd_account) in hd_accounts { + let external_addresses = self.derive_known_addresses(&hd_account, Bip44Chain::External).await?; + let internal_addresses = self.derive_known_addresses(&hd_account, Bip44Chain::Internal).await?; + + let addresses_it = external_addresses + .into_iter() + .chain(internal_addresses) + .map(|hd_address| hd_address.address()); + all_addresses.extend(addresses_it); + } + + Ok(all_addresses) + }, + } + } +} + +/// The `IguanaBalanceOps` trait provides an interface for fetching the balance of a coin and its tokens. +/// This trait should be implemented by coins that use the iguana derivation method. +#[async_trait] +pub trait IguanaBalanceOps { + /// The object that holds the balance/s of the coin. + type BalanceObject: BalanceObjectOps; + + /// Fetches the balance of the coin and its tokens if the coin uses an iguana derivation method. + async fn iguana_balances(&self) -> BalanceResult; } #[allow(clippy::upper_case_acronyms)] @@ -3403,6 +4244,11 @@ pub enum CoinProtocol { decimals: u8, }, ZHTLC(ZcoinProtocolInfo), + #[cfg(feature = "enable-sia")] + SIA, + NFT { + platform: String, + }, } pub type RpcTransportEventHandlerShared = Arc; @@ -3655,6 +4501,7 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result return ERR!("TENDERMINT protocol is not supported by lp_coininit"), CoinProtocol::TENDERMINTTOKEN(_) => return ERR!("TENDERMINTTOKEN protocol is not supported by lp_coininit"), CoinProtocol::ZHTLC { .. } => return ERR!("ZHTLC protocol is not supported by lp_coininit"), + CoinProtocol::NFT { .. } => return ERR!("NFT protocol is not supported by lp_coininit"), #[cfg(not(target_arch = "wasm32"))] CoinProtocol::LIGHTNING { .. } => return ERR!("Lightning protocol is not supported by lp_coininit"), #[cfg(all(feature = "enable-solana", not(target_arch = "wasm32")))] @@ -3665,6 +4512,10 @@ pub async fn lp_coininit(ctx: &MmArc, ticker: &str, req: &Json) -> Result { return ERR!("SplToken protocol is not supported by lp_coininit - use enable_spl instead") }, + #[cfg(feature = "enable-sia")] + CoinProtocol::SIA { .. } => { + return ERR!("SIA protocol is not supported by lp_coininit. Use task::enable_sia::init"); + }, }; let register_params = RegisterCoinParams { @@ -3874,6 +4725,11 @@ pub async fn verify_message(ctx: MmArc, req: VerificationRequest) -> Verificatio Ok(VerificationResponse { is_valid }) } +pub async fn sign_raw_transaction(ctx: MmArc, req: SignRawTransactionRequest) -> RawTransactionResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + coin.sign_raw_tx(&req).await +} + pub async fn remove_delegation(ctx: MmArc, req: RemoveDelegateRequest) -> DelegationResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; match coin { @@ -4011,6 +4867,12 @@ pub async fn my_tx_history(ctx: MmArc, req: Json) -> Result>, S Ok(try_s!(Response::builder().body(body))) } +/// `get_trade_fee` rpc implementation. +/// There is some consideration about this rpc: +/// for eth coin this rpc returns max possible trade fee (estimated for maximum possible gas limit for any kind of swap). +/// However for eth coin, as part of fixing this issue https://github.com/KomodoPlatform/komodo-defi-framework/issues/1848, +/// `max_taker_vol' and `trade_preimage` rpc now return more accurate required gas calculations. +/// So maybe it would be better to deprecate this `get_trade_fee` rpc pub async fn get_trade_fee(ctx: MmArc, req: Json) -> Result>, String> { let ticker = try_s!(req["coin"].as_str().ok_or("No 'coin' field")).to_owned(); let coin = match lp_coinfind(&ctx, &ticker).await { @@ -4166,7 +5028,7 @@ struct ConvertUtxoAddressReq { pub async fn convert_utxo_address(ctx: MmArc, req: Json) -> Result>, String> { let req: ConvertUtxoAddressReq = try_s!(json::from_value(req)); - let mut addr: utxo::Address = try_s!(req.address.parse()); + let mut addr: utxo::LegacyAddress = try_s!(req.address.parse()); // Only legacy addresses supported as source let coin = match lp_coinfind(&ctx, &req.to_coin).await { Ok(Some(c)) => c, _ => return ERR!("Coin {} is not activated", req.to_coin), @@ -4175,8 +5037,7 @@ pub async fn convert_utxo_address(ctx: MmArc, req: Json) -> Result utxo, _ => return ERR!("Coin {} is not utxo", req.to_coin), }; - addr.prefix = coin.as_ref().conf.pub_addr_prefix; - addr.t_addr_prefix = coin.as_ref().conf.pub_t_addr_prefix; + addr.prefix = coin.as_ref().conf.address_prefixes.p2pkh.clone(); addr.checksum_type = coin.as_ref().conf.checksum_type; let response = try_s!(json::to_vec(&json!({ @@ -4194,7 +5055,7 @@ pub fn address_by_coin_conf_and_pubkey_str( ) -> Result { let protocol: CoinProtocol = try_s!(json::from_value(conf["protocol"].clone())); match protocol { - CoinProtocol::ERC20 { .. } | CoinProtocol::ETH => eth::addr_from_pubkey_str(pubkey), + CoinProtocol::ERC20 { .. } | CoinProtocol::ETH | CoinProtocol::NFT { .. } => eth::addr_from_pubkey_str(pubkey), CoinProtocol::UTXO | CoinProtocol::QTUM | CoinProtocol::QRC20 { .. } | CoinProtocol::BCH { .. } => { utxo::address_by_conf_and_pubkey_str(coin, conf, pubkey, addr_format) }, @@ -4240,6 +5101,8 @@ pub fn address_by_coin_conf_and_pubkey_str( ERR!("Solana pubkey is the public address - you do not need to use this rpc call.") }, CoinProtocol::ZHTLC { .. } => ERR!("address_by_coin_conf_and_pubkey_str is not supported for ZHTLC protocol!"), + #[cfg(feature = "enable-sia")] + CoinProtocol::SIA { .. } => ERR!("address_by_coin_conf_and_pubkey_str is not supported for SIA protocol!"), // TODO Alright } } @@ -4520,6 +5383,117 @@ fn coins_conf_check(ctx: &MmArc, coins_en: &Json, ticker: &str, req: Option<&Jso Ok(()) } +#[async_trait] +pub trait Eip1559Ops { + /// Return swap transaction fee policy + fn get_swap_transaction_fee_policy(&self) -> SwapTxFeePolicy; + + /// set swap transaction fee policy + fn set_swap_transaction_fee_policy(&self, swap_txfee_policy: SwapTxFeePolicy); +} + +/// Get eip 1559 transaction fee per gas policy (low, medium, high) set for the coin +pub async fn get_swap_transaction_fee_policy(ctx: MmArc, req: SwapTxFeePolicyRequest) -> SwapTxFeePolicyResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::EthCoin(eth_coin) => Ok(eth_coin.get_swap_transaction_fee_policy()), + MmCoinEnum::Qrc20Coin(qrc20_coin) => Ok(qrc20_coin.get_swap_transaction_fee_policy()), + _ => MmError::err(SwapTxFeePolicyError::NotSupported(req.coin)), + } +} + +/// Set eip 1559 transaction fee per gas policy (low, medium, high) +pub async fn set_swap_transaction_fee_policy(ctx: MmArc, req: SwapTxFeePolicyRequest) -> SwapTxFeePolicyResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::EthCoin(eth_coin) => { + eth_coin.set_swap_transaction_fee_policy(req.swap_tx_fee_policy); + Ok(eth_coin.get_swap_transaction_fee_policy()) + }, + MmCoinEnum::Qrc20Coin(qrc20_coin) => { + qrc20_coin.set_swap_transaction_fee_policy(req.swap_tx_fee_policy); + Ok(qrc20_coin.get_swap_transaction_fee_policy()) + }, + _ => MmError::err(SwapTxFeePolicyError::NotSupported(req.coin)), + } +} + +/// Checks addresses that either had empty transaction history last time we checked or has not been checked before. +/// The checking stops at the moment when we find `gap_limit` consecutive empty addresses. +pub async fn scan_for_new_addresses_impl( + coin: &T, + hd_wallet: &T::HDWallet, + hd_account: &mut HDCoinHDAccount, + address_scanner: &T::HDAddressScanner, + chain: Bip44Chain, + gap_limit: u32, +) -> BalanceResult>>> +where + T: HDWalletBalanceOps + Sync, +{ + let mut balances = Vec::with_capacity(gap_limit as usize); + + // Get the first unknown address id. + let mut checking_address_id = hd_account + .known_addresses_number(chain) + // A UTXO coin should support both [`Bip44Chain::External`] and [`Bip44Chain::Internal`]. + .mm_err(|e| BalanceError::Internal(e.to_string()))?; + + let mut unused_addresses_counter = 0; + let max_addresses_number = hd_account.address_limit(); + while checking_address_id < max_addresses_number && unused_addresses_counter <= gap_limit { + let hd_address = coin.derive_address(hd_account, chain, checking_address_id).await?; + let checking_address = hd_address.address(); + let checking_address_der_path = hd_address.derivation_path(); + + match coin.is_address_used(&checking_address, address_scanner).await? { + // We found a non-empty address, so we have to fill up the balance list + // with zeros starting from `last_non_empty_address_id = checking_address_id - unused_addresses_counter`. + AddressBalanceStatus::Used(non_empty_balance) => { + let last_non_empty_address_id = checking_address_id - unused_addresses_counter; + + // First, derive all empty addresses and put it into `balances` with default balance. + let address_ids = (last_non_empty_address_id..checking_address_id) + .map(|address_id| HDAddressId { chain, address_id }); + let empty_addresses = + coin.derive_addresses(hd_account, address_ids) + .await? + .into_iter() + .map(|empty_address| HDAddressBalance { + address: coin.address_formatter()(&empty_address.address()), + derivation_path: RpcDerivationPath(empty_address.derivation_path().clone()), + chain, + balance: HDWalletBalanceObject::::new(), + }); + balances.extend(empty_addresses); + + // Then push this non-empty address. + balances.push(HDAddressBalance { + address: coin.address_formatter()(&checking_address), + derivation_path: RpcDerivationPath(checking_address_der_path.clone()), + chain, + balance: non_empty_balance, + }); + // Reset the counter of unused addresses to zero since we found a non-empty address. + unused_addresses_counter = 0; + }, + AddressBalanceStatus::NotUsed => unused_addresses_counter += 1, + } + + checking_address_id += 1; + } + + coin.set_known_addresses_number( + hd_wallet, + hd_account, + chain, + checking_address_id - unused_addresses_counter, + ) + .await?; + + Ok(balances) +} + #[cfg(test)] mod tests { use super::*; @@ -4534,7 +5508,7 @@ mod tests { let coin = MmCoinEnum::Test(TestCoin::new(RICK)); // Add test coin to coins context - common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![])).unwrap(); + common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![], None)).unwrap(); // Try to find RICK from coins context that was added above let _found = common::block_on(lp_coinfind(&ctx, RICK)).unwrap(); @@ -4559,7 +5533,7 @@ mod tests { let coin = MmCoinEnum::Test(TestCoin::new(RICK)); // Add test coin to coins context - common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![])).unwrap(); + common::block_on(coins_ctx.add_platform_with_tokens(coin.clone(), vec![], None)).unwrap(); // Try to find RICK from coins context that was added above let _found = common::block_on(lp_coinfind_any(&ctx, RICK)).unwrap(); @@ -4577,3 +5551,61 @@ mod tests { assert!(matches!(Some(coin), _found)); } } + +#[cfg(all(feature = "for-tests", not(target_arch = "wasm32")))] +pub mod for_tests { + use crate::rpc_command::init_withdraw::WithdrawStatusRequest; + use crate::rpc_command::init_withdraw::{init_withdraw, withdraw_status}; + use crate::{TransactionDetails, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawRequest}; + use common::executor::Timer; + use common::{now_ms, wait_until_ms}; + use mm2_core::mm_ctx::MmArc; + use mm2_err_handle::prelude::MmResult; + use mm2_number::BigDecimal; + use rpc_task::RpcTaskStatus; + use std::str::FromStr; + + /// Helper to call init_withdraw and wait for completion + pub async fn test_withdraw_init_loop( + ctx: MmArc, + ticker: &str, + to: &str, + amount: &str, + from_derivation_path: Option<&str>, + fee: Option, + ) -> MmResult { + let withdraw_req = WithdrawRequest { + amount: BigDecimal::from_str(amount).unwrap(), + from: from_derivation_path.map(|from_derivation_path| WithdrawFrom::DerivationPath { + derivation_path: from_derivation_path.to_owned(), + }), + to: to.to_owned(), + coin: ticker.to_owned(), + max: false, + fee, + memo: None, + ibc_source_channel: None, + }; + let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); + let timeout = wait_until_ms(150000); + loop { + if now_ms() > timeout { + panic!("{} init_withdraw timed out", ticker); + } + let status = withdraw_status(ctx.clone(), WithdrawStatusRequest { + task_id: init.task_id, + forget_if_finished: true, + }) + .await; + if let Ok(status) = status { + match status { + RpcTaskStatus::Ok(tx_details) => break Ok(tx_details), + RpcTaskStatus::Error(e) => break Err(e), + _ => Timer::sleep(1.).await, + } + } else { + panic!("{} could not get withdraw_status", ticker) + } + } + } +} diff --git a/mm2src/coins/lp_price.rs b/mm2src/coins/lp_price.rs index d67721917c..fb339d3752 100644 --- a/mm2src/coins/lp_price.rs +++ b/mm2src/coins/lp_price.rs @@ -1,4 +1,4 @@ -use common::log::{debug, error}; +use common::log::{debug, error, info}; use common::StatusCode; use mm2_err_handle::prelude::{MmError, OrMmError}; use mm2_net::transport::SlurpError; @@ -10,10 +10,10 @@ use std::collections::HashMap; #[cfg(feature = "run-docker-tests")] use std::str::FromStr; use std::str::Utf8Error; -const PRICE_ENDPOINTS: [&str; 3] = [ +pub const PRICE_ENDPOINTS: [&str; 3] = [ "https://prices.komodian.info/api/v2/tickers", "https://prices.cipig.net:1717/api/v2/tickers", - "https://defi-stats.komodo.earth/api/v3/prices/tickers_v2", + "https://cache.defi-stats.komodo.earth/api/v3/prices/tickers_v2.json", ]; #[derive(Debug)] @@ -70,7 +70,7 @@ struct TickerInfos { change_24_h_provider: Provider, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] pub enum Provider { #[serde(rename = "binance")] Binance, @@ -82,14 +82,13 @@ pub enum Provider { Forex, #[serde(rename = "nomics")] Nomics, + #[serde(rename = "testcoin")] + TestCoin, #[serde(rename = "unknown", other)] + #[default] Unknown, } -impl Default for Provider { - fn default() -> Self { Provider::Unknown } -} - #[derive(Default, Clone, Debug)] pub struct RateInfos { #[allow(dead_code)] @@ -200,7 +199,7 @@ async fn process_price_request(price_url: &str) -> Result Result> { debug!("Fetching price from: {}", price_url); - let (status, headers, body) = mm2_net::wasm_http::slurp_url(price_url).await?; + let (status, headers, body) = mm2_net::wasm::http::slurp_url(price_url).await?; let (status_code, body, _) = (status, std::str::from_utf8(&body)?.trim().into(), headers); if status_code != StatusCode::OK { return MmError::err(PriceServiceRequestError::HttpProcessError(body)); @@ -209,10 +208,26 @@ async fn process_price_request(price_url: &str) -> Result Result> { - let model = process_price_request(price_url).await?; - debug!("price registry size: {}", model.0.len()); - Ok(model) +pub async fn fetch_price_tickers( + price_urls: &mut [String], +) -> Result> { + for (i, url) in price_urls.to_owned().iter().enumerate() { + let model = match process_price_request(url).await { + Ok(model) => model, + Err(err) => { + error!("Error fetching price from: {}, error: {:?}", url, err); + continue; + }, + }; + price_urls.rotate_left(i); + debug!("price registry size: {}", model.0.len()); + info!("price successfully fetched from {url}"); + return Ok(model); + } + + MmError::err(PriceServiceRequestError::HttpProcessError( + "couldn't fetch price".to_string(), + )) } /// CEXRates, structure for storing `base` coin and `rel` coin USD price diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index 97c5a5ca8f..f7348a5e78 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -3,13 +3,16 @@ use crate::tendermint::{TENDERMINT_ASSET_PROTOCOL_TYPE, TENDERMINT_COIN_PROTOCOL use crate::tx_history_storage::{CreateTxHistoryStorageError, FilteringAddresses, GetTxHistoryFilters, TxHistoryStorageBuilder, WalletId}; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; -use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDAccountAddressId, HistorySyncState, - MmCoin, MmCoinEnum, Transaction, TransactionDetails, TransactionType, TxFeeDetails, UtxoRpcError}; +use crate::MyAddressError; +use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDPathAccountToAddressId, + HistorySyncState, MmCoin, MmCoinEnum, Transaction, TransactionData, TransactionDetails, TransactionType, + TxFeeDetails, UtxoRpcError}; use async_trait::async_trait; use bitcrypto::sha256; use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum, StatusCode}; use crypto::StandardHDPath; use derive_more::Display; +use enum_derives::EnumFromStringify; use futures::compat::Future01CompatExt; use keys::{Address, CashAddress}; use mm2_core::mm_ctx::MmArc; @@ -214,7 +217,7 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T let mut to: Vec<_> = self.to_addresses.iter().map(DisplayAddress::display_address).collect(); to.sort(); - let tx_hash = self.tx.tx_hash(); + let tx_hash = self.tx.tx_hash_as_bytes(); let internal_id = match &self.transaction_type { TransactionType::TokenTransfer(token_id) => { let mut bytes_for_hash = tx_hash.0.clone(); @@ -234,13 +237,13 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T | TransactionType::RemoveDelegation | TransactionType::FeeForTokenTx | TransactionType::StandardTransfer - | TransactionType::NftTransfer => tx_hash.clone(), + | TransactionType::NftTransfer + | TransactionType::TendermintIBCTransfer => tx_hash.clone(), }; TransactionDetails { coin: self.coin, - tx_hex: self.tx.tx_hex().into(), - tx_hash: tx_hash.to_tx_hash(), + tx: TransactionData::new_signed(self.tx.tx_hex().into(), tx_hash.to_tx_hash()), from, to, total_amount: self.total_amount, @@ -261,17 +264,17 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] +#[derive(Default)] pub enum MyTxHistoryTarget { + #[default] Iguana, - AccountId { account_id: u32 }, - AddressId(HDAccountAddressId), + AccountId { + account_id: u32, + }, + AddressId(HDPathAccountToAddressId), AddressDerivationPath(StandardHDPath), } -impl Default for MyTxHistoryTarget { - fn default() -> Self { MyTxHistoryTarget::Iguana } -} - #[derive(Clone, Deserialize)] pub struct MyTxHistoryRequestV2 { pub(crate) coin: String, @@ -304,15 +307,19 @@ pub struct MyTxHistoryResponseV2 { pub(crate) paging_options: PagingOptionsEnum, } -#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Display, EnumFromStringify, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum MyTxHistoryErrorV2 { CoinIsNotActive(String), + #[from_stringify("InvalidBip44ChainError")] InvalidTarget(String), StorageIsNotInitialized(String), + #[from_stringify("CreateTxHistoryStorageError")] StorageError(String), + #[from_stringify("UtxoRpcError")] RpcError(String), NotSupportedFor(String), + #[from_stringify("MyAddressError")] Internal(String), } @@ -350,14 +357,6 @@ impl From for MyTxHistoryErrorV2 { } } -impl From for MyTxHistoryErrorV2 { - fn from(e: CreateTxHistoryStorageError) -> Self { MyTxHistoryErrorV2::StorageError(e.to_string()) } -} - -impl From for MyTxHistoryErrorV2 { - fn from(err: UtxoRpcError) -> Self { MyTxHistoryErrorV2::RpcError(err.to_string()) } -} - impl From for MyTxHistoryErrorV2 { fn from(e: AddressDerivingError) -> Self { match e { @@ -368,10 +367,6 @@ impl From for MyTxHistoryErrorV2 { } } -impl From for MyTxHistoryErrorV2 { - fn from(e: InvalidBip44ChainError) -> Self { MyTxHistoryErrorV2::InvalidTarget(e.to_string()) } -} - #[async_trait] pub trait CoinWithTxHistoryV2 { fn history_wallet_id(&self) -> WalletId; @@ -514,7 +509,6 @@ where }) } -#[cfg(not(target_arch = "wasm32"))] pub async fn z_coin_tx_history_rpc( ctx: MmArc, request: MyTxHistoryRequestV2, diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 40736a6414..4d432235a9 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -3,34 +3,36 @@ use mm2_err_handle::prelude::{MmError, MmResult}; use url::Url; pub(crate) mod nft_errors; -pub(crate) mod nft_structs; +pub mod nft_structs; pub(crate) mod storage; #[cfg(any(test, target_arch = "wasm32"))] mod nft_tests; -use crate::{coin_conf, get_my_address, lp_coinfind_or_err, MarketCoinOps, MmCoinEnum, MyAddressReq, WithdrawError}; +use crate::{coin_conf, get_my_address, lp_coinfind_or_err, CoinsContext, HDPathAccountToAddressId, MarketCoinOps, + MmCoinEnum, MmCoinStruct, MyAddressReq, WithdrawError}; use nft_errors::{GetNftInfoError, UpdateNftError}; use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftList, NftListReq, NftMetadataReq, NftTransferHistory, NftTransferHistoryFromMoralis, NftTransfersReq, NftsTransferHistoryList, TransactionNftDetails, UpdateNftReq, WithdrawNftReq}; use crate::eth::{eth_addr_to_hex, get_eth_address, withdraw_erc1155, withdraw_erc721, EthCoin, EthCoinType, - EthTxFeeDetails}; -use crate::nft::nft_errors::{MetaFromUrlError, ProtectFromSpamError, TransferConfirmationsError, + EthTxFeeDetails, LegacyGasPrice, PayForGasOption}; +use crate::nft::nft_errors::{ClearNftDbError, MetaFromUrlError, ProtectFromSpamError, TransferConfirmationsError, UpdateSpamPhishingError}; -use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, NftCommon, NftCtx, NftTransferCommon, - PhishingDomainReq, PhishingDomainRes, RefreshMetadataReq, SpamContractReq, - SpamContractRes, TransferMeta, TransferStatus, UriMeta}; +use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, ClearNftDbReq, NftCommon, NftCtx, NftInfo, + NftTransferCommon, PhishingDomainReq, PhishingDomainRes, RefreshMetadataReq, + SpamContractReq, SpamContractRes, TransferMeta, TransferStatus, UriMeta}; use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps}; +use common::log::error; use common::parse_rfc3339_to_timestamp; -use crypto::StandardHDCoinAddress; use ethereum_types::{Address, H256}; use futures::compat::Future01CompatExt; use futures::future::try_join_all; use mm2_err_handle::map_to_mm::MapToMmResult; -use mm2_net::transport::send_post_request_to_uri; -use mm2_number::{BigDecimal, BigUint}; +use mm2_net::transport::{send_post_request_to_uri, KomodefiProxyAuthValidation}; +use mm2_number::BigUint; use regex::Regex; +use serde::Deserialize; use serde_json::Value as Json; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; @@ -40,10 +42,12 @@ use web3::types::TransactionId; #[cfg(not(target_arch = "wasm32"))] use mm2_net::native_http::send_request_to_uri; +use crate::eth::v2_activation::generate_signed_message; #[cfg(target_arch = "wasm32")] -use mm2_net::wasm_http::send_request_to_uri; +use mm2_net::wasm::http::send_request_to_uri; -const MORALIS_API_ENDPOINT: &str = "api/v2"; +const MORALIS_API: &str = "api"; +const MORALIS_ENDPOINT_V: &str = "v2"; /// query parameters for moralis request: The format of the token ID const MORALIS_FORMAT_QUERY_NAME: &str = "format"; const MORALIS_FORMAT_QUERY_VALUE: &str = "decimal"; @@ -94,7 +98,6 @@ pub async fn get_nft_list(ctx: MmArc, req: NftListReq) -> MmResult MmResult MmResult { let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; @@ -133,7 +123,6 @@ pub async fn get_nft_metadata(ctx: MmArc, req: NftMetadataReq) -> MmResult MmResult`: A result indicating success or an error. pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNftError> { let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; @@ -248,6 +227,7 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft NftTransferHistoryStorageOps::init(&storage, chain).await?; None }; + // TODO activate and use global NFT instead of ETH coin after adding enable nft using coin conf support let coin_enum = lp_coinfind_or_err(&ctx, chain.to_ticker()).await?; let eth_coin = match coin_enum { MmCoinEnum::EthCoin(eth_coin) => eth_coin, @@ -257,26 +237,36 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft }) }, }; - let nft_transfers = get_moralis_nft_transfers(&ctx, chain, from_block, &req.url, eth_coin).await?; + let my_address = eth_coin.my_address()?; + let signed_message = + generate_signed_message(req.proxy_auth, chain, my_address, ð_coin.priv_key_policy).await?; + let wrapper = UrlSignWrapper { + chain, + orig_url: &req.url, + url_antispam: &req.url_antispam, + signed_message: signed_message.as_ref(), + }; + + let nft_transfers = get_moralis_nft_transfers(&ctx, from_block, eth_coin, &wrapper).await?; storage.add_transfers_to_history(*chain, nft_transfers).await?; let nft_block = match NftListStorageOps::get_last_block_number(&storage, chain).await { Ok(Some(block)) => block, Ok(None) => { // if there are no rows in NFT LIST table we can try to get nft list from moralis. - let nft_list = cache_nfts_from_moralis(&ctx, &storage, chain, &req.url, &req.url_antispam).await?; + let nft_list = cache_nfts_from_moralis(&ctx, &storage, &wrapper).await?; update_meta_in_transfers(&storage, chain, nft_list).await?; - update_transfers_with_empty_meta(&storage, chain, &req.url, &req.url_antispam).await?; + update_transfers_with_empty_meta(&storage, &wrapper).await?; update_spam(&storage, *chain, &req.url_antispam).await?; update_phishing(&storage, chain, &req.url_antispam).await?; continue; }, Err(_) => { - // if there is an error, then NFT LIST table doesnt exist, so we need to cache nft list from moralis. + // if there is an error, then NFT LIST table doesn't exist, so we need to cache nft list from moralis. NftListStorageOps::init(&storage, chain).await?; - let nft_list = cache_nfts_from_moralis(&ctx, &storage, chain, &req.url, &req.url_antispam).await?; + let nft_list = cache_nfts_from_moralis(&ctx, &storage, &wrapper).await?; update_meta_in_transfers(&storage, chain, nft_list).await?; - update_transfers_with_empty_meta(&storage, chain, &req.url, &req.url_antispam).await?; + update_transfers_with_empty_meta(&storage, &wrapper).await?; update_spam(&storage, *chain, &req.url_antispam).await?; update_phishing(&storage, chain, &req.url_antispam).await?; continue; @@ -297,22 +287,72 @@ pub async fn update_nft(ctx: MmArc, req: UpdateNftReq) -> MmResult<(), UpdateNft last_nft_block: nft_block.to_string(), }); } - update_nft_list( - ctx.clone(), - &storage, - chain, - scanned_block + 1, - &req.url, - &req.url_antispam, - ) - .await?; - update_transfers_with_empty_meta(&storage, chain, &req.url, &req.url_antispam).await?; + update_nft_list(ctx.clone(), &storage, scanned_block + 1, &wrapper).await?; + update_nft_global_in_coins_ctx(&ctx, &storage, *chain).await?; + update_transfers_with_empty_meta(&storage, &wrapper).await?; update_spam(&storage, *chain, &req.url_antispam).await?; update_phishing(&storage, chain, &req.url_antispam).await?; } Ok(()) } +/// Updates the global NFT information in the coins context. +/// +/// This function uses the up-to-date NFT list for a given chain and updates the +/// corresponding global NFT information in the coins context. +async fn update_nft_global_in_coins_ctx(ctx: &MmArc, storage: &T, chain: Chain) -> MmResult<(), UpdateNftError> +where + T: NftListStorageOps + NftTransferHistoryStorageOps, +{ + let coins_ctx = CoinsContext::from_ctx(ctx).map_to_mm(UpdateNftError::Internal)?; + let mut coins = coins_ctx.lock_coins().await; + let ticker = chain.to_nft_ticker(); + + if let Some(MmCoinStruct { + inner: MmCoinEnum::EthCoin(nft_global), + .. + }) = coins.get_mut(ticker) + { + let nft_list = storage.get_nft_list(vec![chain], true, 1, None, None).await?; + update_nft_infos(nft_global, nft_list.nfts).await; + return Ok(()); + } + // if global NFT is None in CoinsContext, then it's just not activated + Ok(()) +} + +/// Updates the global NFT information with the latest NFT list. +/// +/// This function replaces the existing NFT information (`nfts_infos`) in the global NFT with the new data provided by `nft_list`. +/// The `nft_list` must be current, accurately reflecting the NFTs presently owned by the user. +/// This includes accounting for any changes such as NFTs that have been transferred away, so user is not owner anymore, +/// or changes in the amounts of ERC1155 tokens. +/// Ensuring the data's accuracy is vital for maintaining a correct representation of ownership in the global NFT. +/// +/// # Warning +/// Using an outdated `nft_list` for this operation may result in incorrect NFT information persisting in the global NFT, +/// potentially leading to inconsistencies with the actual state of NFT ownership. +async fn update_nft_infos(nft_global: &mut EthCoin, nft_list: Vec) { + let new_nft_infos: HashMap = nft_list + .into_iter() + .map(|nft| { + let key = format!("{},{}", nft.common.token_address, nft.token_id); + let nft_info = NftInfo { + token_address: nft.common.token_address, + token_id: nft.token_id, + chain: nft.chain, + contract_type: nft.contract_type, + amount: nft.common.amount, + }; + (key, nft_info) + }) + .collect(); + + let mut global_nft_infos = nft_global.nfts_infos.lock().await; + // we can do this as some `global_nft_infos` keys may not present in `new_nft_infos`, so we will have to remove them anyway + *global_nft_infos = new_nft_infos; +} + /// `update_spam` function updates spam contracts info in NFT list and NFT transfers. async fn update_spam(storage: &T, chain: Chain, url_antispam: &Url) -> MmResult<(), UpdateSpamPhishingError> where @@ -418,29 +458,33 @@ fn prepare_uri_for_blocklist_endpoint( /// phishing domains using the provided `url_antispam`. If the fetched metadata or its domain /// is identified as spam or matches with any phishing domains, the NFT's `possible_spam` and/or /// `possible_phishing` flags are set to true. -/// -/// # Arguments -/// -/// * `ctx`: Context required for handling internal operations. -/// * `req`: A request containing details about the NFT whose metadata needs to be refreshed. -/// -/// # Returns -/// -/// * `MmResult<(), UpdateNftError>`: A result indicating success or an error. pub async fn refresh_nft_metadata(ctx: MmArc, req: RefreshMetadataReq) -> MmResult<(), UpdateNftError> { let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(GetNftInfoError::Internal)?; let storage = nft_ctx.lock_db().await?; + + // TODO activate and use global NFT instead of ETH coin after adding enable nft using coin conf support + let coin_enum = lp_coinfind_or_err(&ctx, req.chain.to_ticker()).await?; + let eth_coin = match coin_enum { + MmCoinEnum::EthCoin(eth_coin) => eth_coin, + _ => { + return MmError::err(UpdateNftError::CoinDoesntSupportNft { + coin: coin_enum.ticker().to_owned(), + }) + }, + }; + let my_address = eth_coin.my_address()?; + let signed_message = + generate_signed_message(req.proxy_auth, &req.chain, my_address, ð_coin.priv_key_policy).await?; + let wrapper = UrlSignWrapper { + chain: &req.chain, + orig_url: &req.url, + url_antispam: &req.url_antispam, + signed_message: signed_message.as_ref(), + }; + let token_address_str = eth_addr_to_hex(&req.token_address); - let moralis_meta = match get_moralis_metadata( - token_address_str.clone(), - req.token_id.clone(), - &req.chain, - &req.url, - &req.url_antispam, - ) - .await - { + let mut moralis_meta = match get_moralis_metadata(token_address_str.clone(), req.token_id.clone(), &wrapper).await { Ok(moralis_meta) => moralis_meta, Err(_) => { storage @@ -461,10 +505,14 @@ pub async fn refresh_nft_metadata(ctx: MmArc, req: RefreshMetadataReq) -> MmResu })?; let token_uri = check_moralis_ipfs_bafy(moralis_meta.common.token_uri.as_deref()); let token_domain = get_domain_from_url(token_uri.as_deref()); + check_token_uri(&mut moralis_meta.common.possible_spam, token_uri.as_deref())?; + drop_mutability!(moralis_meta); let uri_meta = get_uri_meta( token_uri.as_deref(), moralis_meta.common.metadata.as_deref(), &req.url_antispam, + moralis_meta.common.possible_spam, + nft_db.possible_phishing, ) .await; // Gather domains for phishing checks @@ -575,43 +623,28 @@ where Ok(()) } -async fn get_moralis_nft_list( - ctx: &MmArc, - chain: &Chain, - url: &Url, - url_antispam: &Url, -) -> MmResult, GetNftInfoError> { +async fn get_moralis_nft_list(ctx: &MmArc, wrapper: &UrlSignWrapper<'_>) -> MmResult, GetNftInfoError> { let mut res_list = Vec::new(); + let chain = wrapper.chain; let ticker = chain.to_ticker(); let conf = coin_conf(ctx, ticker); - let my_address = get_eth_address(ctx, &conf, ticker, &StandardHDCoinAddress::default()).await?; - - let mut uri_without_cursor = url.clone(); - uri_without_cursor.set_path(MORALIS_API_ENDPOINT); - uri_without_cursor - .path_segments_mut() - .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? - .push(&my_address.wallet_address) - .push("nft"); - uri_without_cursor - .query_pairs_mut() - .append_pair("chain", &chain.to_string()) - .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE); - drop_mutability!(uri_without_cursor); + let my_address = get_eth_address(ctx, &conf, ticker, &HDPathAccountToAddressId::default()).await?; + let uri_without_cursor = construct_moralis_uri_for_nft(wrapper.orig_url, &my_address.wallet_address, chain)?; // The cursor returned in the previous response (used for getting the next page). let mut cursor = String::new(); loop { + // Create a new URL instance from uri_without_cursor and modify its query to include the cursor if present let uri = format!("{}{}", uri_without_cursor, cursor); - let response = send_request_to_uri(uri.as_str()).await?; + let response = build_and_send_request(uri.as_str(), wrapper.signed_message).await?; if let Some(nfts_list) = response["result"].as_array() { for nft_json in nfts_list { - let nft_moralis: NftFromMoralis = serde_json::from_str(&nft_json.to_string())?; + let nft_moralis = NftFromMoralis::deserialize(nft_json)?; let contract_type = match nft_moralis.contract_type { Some(contract_type) => contract_type, None => continue, }; - let mut nft = build_nft_from_moralis(*chain, nft_moralis, contract_type, url_antispam).await; + let mut nft = build_nft_from_moralis(*chain, nft_moralis, contract_type, wrapper.url_antispam).await; protect_from_nft_spam_links(&mut nft, false)?; // collect NFTs from the page res_list.push(nft); @@ -619,34 +652,93 @@ async fn get_moralis_nft_list( // if cursor is not null, there are other NFTs on next page, // and we need to send new request with cursor to get info from the next page. if let Some(cursor_res) = response["cursor"].as_str() { - cursor = format!("{}{}", "&cursor=", cursor_res); + cursor = format!("&cursor={}", cursor_res); continue; } else { break; } + } else { + break; } } - drop_mutability!(res_list); Ok(res_list) } +pub(crate) async fn get_nfts_for_activation( + chain: &Chain, + my_address: &Address, + orig_url: &Url, + signed_message: Option<&KomodefiProxyAuthValidation>, +) -> MmResult, GetNftInfoError> { + let mut nfts_map = HashMap::new(); + let uri_without_cursor = construct_moralis_uri_for_nft(orig_url, ð_addr_to_hex(my_address), chain)?; + + // The cursor returned in the previous response (used for getting the next page). + let mut cursor = String::new(); + loop { + // Create a new URL instance from uri_without_cursor and modify its query to include the cursor if present + let uri = format!("{}{}", uri_without_cursor, cursor); + let response = build_and_send_request(uri.as_str(), signed_message).await?; + if let Some(nfts_list) = response["result"].as_array() { + process_nft_list_for_activation(nfts_list, chain, &mut nfts_map)?; + // if cursor is not null, there are other NFTs on next page, + // and we need to send new request with cursor to get info from the next page. + if let Some(cursor_res) = response["cursor"].as_str() { + cursor = format!("&cursor={}", cursor_res); + continue; + } else { + break; + } + } else { + break; + } + } + Ok(nfts_map) +} + +fn process_nft_list_for_activation( + nfts_list: &[Json], + chain: &Chain, + nfts_map: &mut HashMap, +) -> MmResult<(), GetNftInfoError> { + for nft_json in nfts_list { + let nft_moralis = NftFromMoralis::deserialize(nft_json)?; + let contract_type = match nft_moralis.contract_type { + Some(contract_type) => contract_type, + None => continue, + }; + let token_address_str = eth_addr_to_hex(&nft_moralis.common.token_address); + let nft_info = NftInfo { + token_address: nft_moralis.common.token_address, + token_id: nft_moralis.token_id.0.clone(), + chain: *chain, + contract_type, + amount: nft_moralis.common.amount, + }; + let key = format!("{},{}", token_address_str, nft_moralis.token_id.0); + nfts_map.insert(key, nft_info); + } + Ok(()) +} + async fn get_moralis_nft_transfers( ctx: &MmArc, - chain: &Chain, from_block: Option, - url: &Url, eth_coin: EthCoin, + wrapper: &UrlSignWrapper<'_>, ) -> MmResult, GetNftInfoError> { + let chain = wrapper.chain; let mut res_list = Vec::new(); let ticker = chain.to_ticker(); let conf = coin_conf(ctx, ticker); - let my_address = get_eth_address(ctx, &conf, ticker, &StandardHDCoinAddress::default()).await?; + let my_address = get_eth_address(ctx, &conf, ticker, &HDPathAccountToAddressId::default()).await?; - let mut uri_without_cursor = url.clone(); - uri_without_cursor.set_path(MORALIS_API_ENDPOINT); + let mut uri_without_cursor = wrapper.orig_url.clone(); uri_without_cursor .path_segments_mut() .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? + .push(MORALIS_API) + .push(MORALIS_ENDPOINT_V) .push(&my_address.wallet_address) .push("nft") .push("transfers"); @@ -665,90 +757,115 @@ async fn get_moralis_nft_transfers( let mut cursor = String::new(); let wallet_address = my_address.wallet_address; loop { + // Create a new URL instance from uri_without_cursor and modify its query to include the cursor if present let uri = format!("{}{}", uri_without_cursor, cursor); - let response = send_request_to_uri(uri.as_str()).await?; + let response = build_and_send_request(uri.as_str(), wrapper.signed_message).await?; if let Some(transfer_list) = response["result"].as_array() { - for transfer in transfer_list { - let transfer_moralis: NftTransferHistoryFromMoralis = serde_json::from_str(&transfer.to_string())?; - let contract_type = match transfer_moralis.contract_type { - Some(contract_type) => contract_type, - None => continue, - }; - let status = - get_transfer_status(&wallet_address, ð_addr_to_hex(&transfer_moralis.common.to_address)); - let block_timestamp = parse_rfc3339_to_timestamp(&transfer_moralis.block_timestamp)?; - let fee_details = get_fee_details(ð_coin, &transfer_moralis.common.transaction_hash).await; - let transfer_history = NftTransferHistory { - common: NftTransferCommon { - block_hash: transfer_moralis.common.block_hash, - transaction_hash: transfer_moralis.common.transaction_hash, - transaction_index: transfer_moralis.common.transaction_index, - log_index: transfer_moralis.common.log_index, - value: transfer_moralis.common.value, - transaction_type: transfer_moralis.common.transaction_type, - token_address: transfer_moralis.common.token_address, - from_address: transfer_moralis.common.from_address, - to_address: transfer_moralis.common.to_address, - amount: transfer_moralis.common.amount, - verified: transfer_moralis.common.verified, - operator: transfer_moralis.common.operator, - possible_spam: transfer_moralis.common.possible_spam, - }, - chain: *chain, - token_id: transfer_moralis.token_id.0, - block_number: *transfer_moralis.block_number, - block_timestamp, - contract_type, - token_uri: None, - token_domain: None, - collection_name: None, - image_url: None, - image_domain: None, - token_name: None, - status, - possible_phishing: false, - fee_details, - confirmations: 0, - }; - // collect NFTs transfers from the page - res_list.push(transfer_history); - } + process_transfer_list(transfer_list, chain, wallet_address.as_str(), ð_coin, &mut res_list).await?; // if the cursor is not null, there are other NFTs transfers on next page, // and we need to send new request with cursor to get info from the next page. if let Some(cursor_res) = response["cursor"].as_str() { - cursor = format!("{}{}", "&cursor=", cursor_res); + cursor = format!("&cursor={}", cursor_res); continue; } else { break; } + } else { + break; } } - drop_mutability!(res_list); Ok(res_list) } +async fn process_transfer_list( + transfer_list: &[Json], + chain: &Chain, + wallet_address: &str, + eth_coin: &EthCoin, + res_list: &mut Vec, +) -> MmResult<(), GetNftInfoError> { + for transfer in transfer_list { + let transfer_moralis = NftTransferHistoryFromMoralis::deserialize(transfer)?; + let contract_type = match transfer_moralis.contract_type { + Some(contract_type) => contract_type, + None => continue, + }; + let status = get_transfer_status(wallet_address, ð_addr_to_hex(&transfer_moralis.common.to_address)); + let block_timestamp = parse_rfc3339_to_timestamp(&transfer_moralis.block_timestamp)?; + let fee_details = get_fee_details(eth_coin, &transfer_moralis.common.transaction_hash).await; + let transfer_history = NftTransferHistory { + common: NftTransferCommon { + block_hash: transfer_moralis.common.block_hash, + transaction_hash: transfer_moralis.common.transaction_hash, + transaction_index: transfer_moralis.common.transaction_index, + log_index: transfer_moralis.common.log_index, + value: transfer_moralis.common.value, + transaction_type: transfer_moralis.common.transaction_type, + token_address: transfer_moralis.common.token_address, + from_address: transfer_moralis.common.from_address, + to_address: transfer_moralis.common.to_address, + amount: transfer_moralis.common.amount, + verified: transfer_moralis.common.verified, + operator: transfer_moralis.common.operator, + possible_spam: transfer_moralis.common.possible_spam, + }, + chain: *chain, + token_id: transfer_moralis.token_id.0, + block_number: *transfer_moralis.block_number, + block_timestamp, + contract_type, + token_uri: None, + token_domain: None, + collection_name: None, + image_url: None, + image_domain: None, + token_name: None, + status, + possible_phishing: false, + fee_details, + confirmations: 0, + }; + // collect NFTs transfers from the page + res_list.push(transfer_history); + } + Ok(()) +} + +// TODO: get fee details from non fungible token instead of eth coin? async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option { let hash = H256::from_str(transaction_hash).ok()?; - let receipt = eth_coin.web3.eth().transaction_receipt(hash).await.ok()?; + let receipt = eth_coin.web3().await.ok()?.eth().transaction_receipt(hash).await.ok()?; let fee_coin = match eth_coin.coin_type { EthCoinType::Eth => eth_coin.ticker(), - EthCoinType::Erc20 { .. } => return None, + EthCoinType::Erc20 { .. } | EthCoinType::Nft { .. } => return None, }; match receipt { Some(r) => { let gas_used = r.gas_used.unwrap_or_default(); match r.effective_gas_price { - Some(gas_price) => EthTxFeeDetails::new(gas_used, gas_price, fee_coin).ok(), + Some(gas_price) => EthTxFeeDetails::new( + gas_used, + PayForGasOption::Legacy(LegacyGasPrice { gas_price }), + fee_coin, + ) + .ok(), None => { let web3_tx = eth_coin - .web3 + .web3() + .await + .ok()? .eth() .transaction(TransactionId::Hash(hash)) .await .ok()??; let gas_price = web3_tx.gas_price.unwrap_or_default(); - EthTxFeeDetails::new(gas_used, gas_price, fee_coin).ok() + EthTxFeeDetails::new( + gas_used, + PayForGasOption::Legacy(LegacyGasPrice { gas_price }), + fee_coin, + ) + .ok() }, } }, @@ -764,18 +881,18 @@ async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option, ) -> MmResult { - let mut uri = url.clone(); - uri.set_path(MORALIS_API_ENDPOINT); + let mut uri = wrapper.orig_url.clone(); + let chain = wrapper.chain; uri.path_segments_mut() .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? + .push(MORALIS_API) + .push(MORALIS_ENDPOINT_V) .push("nft") .push(&token_address) .push(&token_id.to_string()); @@ -784,13 +901,13 @@ async fn get_moralis_metadata( .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE); drop_mutability!(uri); - let response = send_request_to_uri(uri.as_str()).await?; + let response = build_and_send_request(uri.as_str(), wrapper.signed_message).await?; let nft_moralis: NftFromMoralis = serde_json::from_str(&response.to_string())?; let contract_type = match nft_moralis.contract_type { Some(contract_type) => contract_type, None => return MmError::err(GetNftInfoError::ContractTypeIsNull), }; - let mut nft_metadata = build_nft_from_moralis(*chain, nft_moralis, contract_type, url_antispam).await; + let mut nft_metadata = build_nft_from_moralis(*chain, nft_moralis, contract_type, wrapper.url_antispam).await; protect_from_nft_spam_links(&mut nft_metadata, false)?; Ok(nft_metadata) } @@ -827,14 +944,23 @@ fn check_moralis_ipfs_bafy(token_uri: Option<&str>) -> Option { }) } -async fn get_uri_meta(token_uri: Option<&str>, metadata: Option<&str>, url_antispam: &Url) -> UriMeta { +async fn get_uri_meta( + token_uri: Option<&str>, + metadata: Option<&str>, + url_antispam: &Url, + possible_spam: bool, + possible_phishing: bool, +) -> UriMeta { let mut uri_meta = UriMeta::default(); - // Fetching data from the URL if token_uri is provided - if let Some(token_uri) = token_uri { - if let Some(url) = construct_camo_url_with_token(token_uri, url_antispam) { - uri_meta = fetch_meta_from_url(url).await.unwrap_or_default(); + if !possible_spam && !possible_phishing { + // Fetching data from the URL if token_uri is provided + if let Some(token_uri) = token_uri { + if let Some(url) = construct_camo_url_with_token(token_uri, url_antispam) { + uri_meta = fetch_meta_from_url(url).await.unwrap_or_default(); + } } } + // Filling fields from metadata if provided if let Some(metadata) = metadata { if let Ok(meta_from_meta) = serde_json::from_str::(metadata) { @@ -842,7 +968,6 @@ async fn get_uri_meta(token_uri: Option<&str>, metadata: Option<&str>, url_antis } } update_uri_moralis_ipfs_fields(&mut uri_meta); - drop_mutability!(uri_meta); uri_meta } @@ -854,7 +979,7 @@ fn construct_camo_url_with_token(token_uri: &str, url_antispam: &Url) -> Option< } async fn fetch_meta_from_url(url: Url) -> MmResult { - let response_meta = send_request_to_uri(url.as_str()).await?; + let response_meta = send_request_to_uri(url.as_str(), None).await?; serde_json::from_value(response_meta).map_err(|e| e.into()) } @@ -881,39 +1006,37 @@ fn get_transfer_status(my_wallet: &str, to_address: &str) -> TransferStatus { async fn update_nft_list( ctx: MmArc, storage: &T, - chain: &Chain, scan_from_block: u64, - url: &Url, - url_antispam: &Url, + wrapper: &UrlSignWrapper<'_>, ) -> MmResult<(), UpdateNftError> { + let chain = wrapper.chain; let transfers = storage.get_transfers_from_block(*chain, scan_from_block).await?; let req = MyAddressReq { coin: chain.to_ticker().to_string(), - path_to_address: StandardHDCoinAddress::default(), + path_to_address: HDPathAccountToAddressId::default(), }; let my_address = get_my_address(ctx.clone(), req).await?.wallet_address.to_lowercase(); for transfer in transfers.into_iter() { - handle_nft_transfer(storage, chain, url, url_antispam, transfer, &my_address).await?; + handle_nft_transfer(storage, wrapper, transfer, &my_address).await?; } Ok(()) } async fn handle_nft_transfer( storage: &T, - chain: &Chain, - url: &Url, - url_antispam: &Url, + wrapper: &UrlSignWrapper<'_>, transfer: NftTransferHistory, my_address: &str, ) -> MmResult<(), UpdateNftError> { + let chain = wrapper.chain; match (transfer.status, transfer.contract_type) { (TransferStatus::Send, ContractType::Erc721) => handle_send_erc721(storage, chain, transfer).await, (TransferStatus::Receive, ContractType::Erc721) => { - handle_receive_erc721(storage, chain, transfer, url, url_antispam, my_address).await + handle_receive_erc721(storage, transfer, wrapper, my_address).await }, (TransferStatus::Send, ContractType::Erc1155) => handle_send_erc1155(storage, chain, transfer).await, (TransferStatus::Receive, ContractType::Erc1155) => { - handle_receive_erc1155(storage, chain, transfer, url, url_antispam, my_address).await + handle_receive_erc1155(storage, transfer, wrapper, my_address).await }, } } @@ -947,12 +1070,11 @@ async fn handle_send_erc721 async fn handle_receive_erc721( storage: &T, - chain: &Chain, transfer: NftTransferHistory, - url: &Url, - url_antispam: &Url, + wrapper: &UrlSignWrapper<'_>, my_address: &str, ) -> MmResult<(), UpdateNftError> { + let chain = wrapper.chain; let token_address_str = eth_addr_to_hex(&transfer.common.token_address); match storage .get_nft(chain, token_address_str.clone(), transfer.token_id.clone()) @@ -973,14 +1095,8 @@ async fn handle_receive_erc721 { - let mut nft = match get_moralis_metadata( - token_address_str.clone(), - transfer.token_id.clone(), - chain, - url, - url_antispam, - ) - .await + let mut nft = match get_moralis_metadata(token_address_str.clone(), transfer.token_id.clone(), wrapper) + .await { Ok(mut moralis_meta) => { // sometimes moralis updates Get All NFTs (which also affects Get Metadata) later @@ -1040,12 +1156,11 @@ async fn handle_send_erc1155( storage: &T, - chain: &Chain, transfer: NftTransferHistory, - url: &Url, - url_antispam: &Url, + wrapper: &UrlSignWrapper<'_>, my_address: &str, ) -> MmResult<(), UpdateNftError> { + let chain = wrapper.chain; let token_address_str = eth_addr_to_hex(&transfer.common.token_address); let mut nft = match storage .get_nft(chain, token_address_str.clone(), transfer.token_id.clone()) @@ -1066,17 +1181,10 @@ async fn handle_receive_erc1155 { - let nft = match get_moralis_metadata( - token_address_str.clone(), - transfer.token_id.clone(), - chain, - url, - url_antispam, - ) - .await - { + let nft = match get_moralis_metadata(token_address_str.clone(), transfer.token_id.clone(), wrapper).await { Ok(moralis_meta) => { - create_nft_from_moralis_metadata(moralis_meta, &transfer, my_address, chain, url_antispam).await? + create_nft_from_moralis_metadata(moralis_meta, &transfer, my_address, chain, wrapper.url_antispam) + .await? }, Err(_) => { mark_as_spam_and_build_empty_meta(storage, chain, token_address_str, &transfer, my_address).await? @@ -1092,8 +1200,18 @@ async fn handle_receive_erc1155) -> MmResult<(), regex::Error> { + if let Some(uri) = token_uri { + if is_malicious(uri)? { + *possible_spam = true; + } + } + Ok(()) +} + async fn create_nft_from_moralis_metadata( - moralis_meta: Nft, + mut moralis_meta: Nft, transfer: &NftTransferHistory, my_address: &str, chain: &Chain, @@ -1101,10 +1219,13 @@ async fn create_nft_from_moralis_metadata( ) -> MmResult { let token_uri = check_moralis_ipfs_bafy(moralis_meta.common.token_uri.as_deref()); let token_domain = get_domain_from_url(token_uri.as_deref()); + check_token_uri(&mut moralis_meta.common.possible_spam, token_uri.as_deref())?; let uri_meta = get_uri_meta( token_uri.as_deref(), moralis_meta.common.metadata.as_deref(), url_antispam, + moralis_meta.common.possible_spam, + moralis_meta.possible_phishing, ) .await; let nft = Nft { @@ -1160,43 +1281,17 @@ async fn mark_as_spam_and_build_empty_meta MmResult { - let nft_ctx = NftCtx::from_ctx(ctx).map_to_mm(GetNftInfoError::Internal)?; - - let storage = nft_ctx.lock_db().await?; - if !NftListStorageOps::is_initialized(&storage, chain).await? { - NftListStorageOps::init(&storage, chain).await?; - } - let nft_meta = storage - .get_nft(chain, token_address.to_lowercase(), token_id.clone()) - .await? - .ok_or_else(|| GetNftInfoError::TokenNotFoundInWallet { - token_address, - token_id: token_id.to_string(), - })?; - Ok(nft_meta.common.amount) -} - async fn cache_nfts_from_moralis( ctx: &MmArc, storage: &T, - chain: &Chain, - url: &Url, - url_antispam: &Url, + wrapper: &UrlSignWrapper<'_>, ) -> MmResult, UpdateNftError> { - let nft_list = get_moralis_nft_list(ctx, chain, url, url_antispam).await?; - let last_scanned_block = NftTransferHistoryStorageOps::get_last_block_number(storage, chain) + let nft_list = get_moralis_nft_list(ctx, wrapper).await?; + let last_scanned_block = NftTransferHistoryStorageOps::get_last_block_number(storage, wrapper.chain) .await? .unwrap_or(0); storage - .add_nfts_to_list(*chain, nft_list.clone(), last_scanned_block) + .add_nfts_to_list(*wrapper.chain, nft_list.clone(), last_scanned_block) .await?; Ok(nft_list) } @@ -1213,42 +1308,43 @@ where } /// `update_transfers_with_empty_meta` function updates empty metadata in transfers. -async fn update_transfers_with_empty_meta( - storage: &T, - chain: &Chain, - url: &Url, - url_antispam: &Url, -) -> MmResult<(), UpdateNftError> +async fn update_transfers_with_empty_meta(storage: &T, wrapper: &UrlSignWrapper<'_>) -> MmResult<(), UpdateNftError> where T: NftListStorageOps + NftTransferHistoryStorageOps, { - let nft_token_addr_id = storage.get_transfers_with_empty_meta(*chain).await?; - for addr_id_pair in nft_token_addr_id.into_iter() { - let mut nft_meta = match get_moralis_metadata( - addr_id_pair.token_address.clone(), - addr_id_pair.token_id, - chain, - url, - url_antispam, - ) - .await - { - Ok(nft_meta) => nft_meta, - Err(_) => { - storage - .update_nft_spam_by_token_address(chain, addr_id_pair.token_address.clone(), true) - .await?; - storage - .update_transfer_spam_by_token_address(chain, addr_id_pair.token_address, true) - .await?; - continue; - }, - }; + let chain = wrapper.chain; + let token_addr_id = storage.get_transfers_with_empty_meta(*chain).await?; + for addr_id_pair in token_addr_id.into_iter() { + let mut nft_meta = + match get_moralis_metadata(addr_id_pair.token_address.clone(), addr_id_pair.token_id, wrapper).await { + Ok(nft_meta) => nft_meta, + Err(_) => { + storage + .update_nft_spam_by_token_address(chain, addr_id_pair.token_address.clone(), true) + .await?; + storage + .update_transfer_spam_by_token_address(chain, addr_id_pair.token_address, true) + .await?; + continue; + }, + }; update_transfer_meta_using_nft(storage, chain, &mut nft_meta).await?; } Ok(()) } +/// Checks if the given URL is potentially malicious based on certain patterns. +fn is_malicious(token_uri: &str) -> MmResult { + let patterns = vec![r"\.(xyz|gq|top)(/|$)", r"\.(json|xml|jpg|png)[%?]"]; + for pattern in patterns { + let regex = Regex::new(pattern)?; + if regex.is_match(token_uri) { + return Ok(true); + } + } + Ok(false) +} + /// `contains_disallowed_scheme` function checks if the text contains some link. fn contains_disallowed_url(text: &str) -> Result { let url_regex = Regex::new( @@ -1353,15 +1449,20 @@ fn process_metadata_field( async fn build_nft_from_moralis( chain: Chain, - nft_moralis: NftFromMoralis, + mut nft_moralis: NftFromMoralis, contract_type: ContractType, url_antispam: &Url, ) -> Nft { let token_uri = check_moralis_ipfs_bafy(nft_moralis.common.token_uri.as_deref()); + if let Err(e) = check_token_uri(&mut nft_moralis.common.possible_spam, token_uri.as_deref()) { + error!("Error checking token URI: {}", e); + } let uri_meta = get_uri_meta( token_uri.as_deref(), nft_moralis.common.metadata.as_deref(), url_antispam, + nft_moralis.common.possible_spam, + false, ) .await; let token_domain = get_domain_from_url(token_uri.as_deref()); @@ -1396,3 +1497,82 @@ pub(crate) fn get_domain_from_url(url: Option<&str>) -> Option { url.and_then(|uri| Url::parse(uri).ok()) .and_then(|url| url.domain().map(String::from)) } + +/// Clears NFT data from the database for specified chains. +pub async fn clear_nft_db(ctx: MmArc, req: ClearNftDbReq) -> MmResult<(), ClearNftDbError> { + if req.clear_all { + let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(ClearNftDbError::Internal)?; + let storage = nft_ctx.lock_db().await?; + storage.clear_all_nft_data().await?; + storage.clear_all_history_data().await?; + return Ok(()); + } + + if req.chains.is_empty() { + return MmError::err(ClearNftDbError::InvalidRequest( + "Nothing to clear was specified".to_string(), + )); + } + + let nft_ctx = NftCtx::from_ctx(&ctx).map_to_mm(ClearNftDbError::Internal)?; + let storage = nft_ctx.lock_db().await?; + let mut errors = Vec::new(); + for chain in req.chains.iter() { + if let Err(e) = clear_data_for_chain(&storage, chain).await { + errors.push(e); + } + } + if !errors.is_empty() { + return MmError::err(ClearNftDbError::DbError(format!("{:?}", errors))); + } + + Ok(()) +} + +async fn clear_data_for_chain(storage: &T, chain: &Chain) -> MmResult<(), ClearNftDbError> +where + T: NftListStorageOps + NftTransferHistoryStorageOps, +{ + let (is_nft_list_init, is_history_init) = ( + NftListStorageOps::is_initialized(storage, chain).await?, + NftTransferHistoryStorageOps::is_initialized(storage, chain).await?, + ); + if is_nft_list_init { + storage.clear_nft_data(chain).await?; + } + if is_history_init { + storage.clear_history_data(chain).await?; + } + Ok(()) +} + +fn construct_moralis_uri_for_nft(orig_url: &Url, address: &str, chain: &Chain) -> MmResult { + let mut uri = orig_url.clone(); + uri.path_segments_mut() + .map_to_mm(|_| GetNftInfoError::Internal("Invalid URI".to_string()))? + .push(MORALIS_API) + .push(MORALIS_ENDPOINT_V) + .push(address) + .push("nft"); + uri.query_pairs_mut() + .append_pair("chain", &chain.to_string()) + .append_pair(MORALIS_FORMAT_QUERY_NAME, MORALIS_FORMAT_QUERY_VALUE); + Ok(uri) +} + +/// A wrapper struct for holding the chain identifier, original URL field from RPC, anti-spam URL and signed message. +struct UrlSignWrapper<'a> { + chain: &'a Chain, + orig_url: &'a Url, + url_antispam: &'a Url, + signed_message: Option<&'a KomodefiProxyAuthValidation>, +} + +async fn build_and_send_request( + uri: &str, + signed_message: Option<&KomodefiProxyAuthValidation>, +) -> MmResult { + let payload = signed_message.map(|msg| serde_json::to_string(&msg)).transpose()?; + let response = send_request_to_uri(uri, payload.as_deref()).await?; + Ok(response) +} diff --git a/mm2src/coins/nft/nft_errors.rs b/mm2src/coins/nft/nft_errors.rs index f5dd5adaba..12e8d326a0 100644 --- a/mm2src/coins/nft/nft_errors.rs +++ b/mm2src/coins/nft/nft_errors.rs @@ -1,13 +1,15 @@ +use crate::eth::v2_activation::GenerateSignedMessageError; use crate::eth::GetEthAddressError; #[cfg(target_arch = "wasm32")] use crate::nft::storage::wasm::WasmNftCacheError; use crate::nft::storage::NftStorageError; -use crate::{CoinFindError, GetMyAddressError, WithdrawError}; +use crate::{CoinFindError, GetMyAddressError, MyAddressError, NumConversError, PrivKeyPolicyNotAllowed, + UnexpectedDerivationMethod, WithdrawError}; use common::{HttpStatusCode, ParseRfc3339Err}; #[cfg(not(target_arch = "wasm32"))] use db_common::sqlite::rusqlite::Error as SqlError; use derive_more::Display; -use enum_from::EnumFromStringify; +use enum_derives::EnumFromStringify; use http::StatusCode; use mm2_net::transport::{GetInfoFromUriError, SlurpError}; use serde::{Deserialize, Serialize}; @@ -36,6 +38,7 @@ pub enum GetNftInfoError { token_address: String, token_id: String, }, + #[from_stringify("LockDBError")] #[display(fmt = "DB error {}", _0)] DbError(String), ParseRfc3339Err(ParseRfc3339Err), @@ -43,12 +46,18 @@ pub enum GetNftInfoError { ContractTypeIsNull, ProtectFromSpamError(ProtectFromSpamError), TransferConfirmationsError(TransferConfirmationsError), + #[from_stringify("NumConversError")] + NumConversError(String), } impl From for WithdrawError { fn from(e: GetNftInfoError) -> Self { WithdrawError::GetNftInfoError(e) } } +impl From for GetNftInfoError { + fn from(e: UnexpectedDerivationMethod) -> Self { GetNftInfoError::Internal(e.to_string()) } +} + impl From for GetNftInfoError { fn from(e: SlurpError) -> Self { let error_str = e.to_string(); @@ -101,18 +110,24 @@ impl From for GetNftInfoError { fn from(e: ProtectFromSpamError) -> Self { GetNftInfoError::ProtectFromSpamError(e) } } -impl From for GetNftInfoError { - fn from(e: LockDBError) -> Self { GetNftInfoError::DbError(e.to_string()) } -} - impl From for GetNftInfoError { fn from(e: TransferConfirmationsError) -> Self { GetNftInfoError::TransferConfirmationsError(e) } } +impl From for GetNftInfoError { + fn from(e: ethabi::Error) -> Self { + // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. + // It's an internal error if there are any issues during working with a smart contract ABI. + GetNftInfoError::Internal(e.to_string()) + } +} + impl HttpStatusCode for GetNftInfoError { fn status_code(&self) -> StatusCode { match self { - GetNftInfoError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + GetNftInfoError::InvalidRequest(_) | GetNftInfoError::TransferConfirmationsError(_) => { + StatusCode::BAD_REQUEST + }, GetNftInfoError::InvalidResponse(_) | GetNftInfoError::ParseRfc3339Err(_) => StatusCode::FAILED_DEPENDENCY, GetNftInfoError::ContractTypeIsNull => StatusCode::NOT_FOUND, GetNftInfoError::Transport(_) @@ -121,7 +136,7 @@ impl HttpStatusCode for GetNftInfoError { | GetNftInfoError::TokenNotFoundInWallet { .. } | GetNftInfoError::DbError(_) | GetNftInfoError::ProtectFromSpamError(_) - | GetNftInfoError::TransferConfirmationsError(_) => StatusCode::INTERNAL_SERVER_ERROR, + | GetNftInfoError::NumConversError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -139,8 +154,10 @@ impl HttpStatusCode for GetNftInfoError { #[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum UpdateNftError { + #[from_stringify("LockDBError")] #[display(fmt = "DB error {}", _0)] DbError(String), + #[from_stringify("regex::Error", "MyAddressError")] #[display(fmt = "Internal: {}", _0)] Internal(String), GetNftInfoError(GetNftInfoError), @@ -198,6 +215,8 @@ pub enum UpdateNftError { CoinDoesntSupportNft { coin: String, }, + #[display(fmt = "Private key policy is not allowed: {}", _0)] + PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), } impl From for UpdateNftError { @@ -224,10 +243,6 @@ impl From for UpdateNftError { fn from(e: ProtectFromSpamError) -> Self { UpdateNftError::ProtectFromSpamError(e) } } -impl From for UpdateNftError { - fn from(e: LockDBError) -> Self { UpdateNftError::DbError(e.to_string()) } -} - impl From for UpdateNftError { fn from(e: CoinFindError) -> Self { match e { @@ -236,6 +251,19 @@ impl From for UpdateNftError { } } +impl From for UpdateNftError { + fn from(e: PrivKeyPolicyNotAllowed) -> Self { Self::PrivKeyPolicyNotAllowed(e) } +} + +impl From for UpdateNftError { + fn from(e: GenerateSignedMessageError) -> Self { + match e { + GenerateSignedMessageError::InternalError(e) => UpdateNftError::Internal(e), + GenerateSignedMessageError::PrivKeyPolicyNotAllowed(e) => UpdateNftError::PrivKeyPolicyNotAllowed(e), + } + } +} + impl HttpStatusCode for UpdateNftError { fn status_code(&self) -> StatusCode { match self { @@ -254,19 +282,19 @@ impl HttpStatusCode for UpdateNftError { | UpdateNftError::SerdeError(_) | UpdateNftError::ProtectFromSpamError(_) | UpdateNftError::NoSuchCoin { .. } - | UpdateNftError::CoinDoesntSupportNft { .. } => StatusCode::INTERNAL_SERVER_ERROR, + | UpdateNftError::CoinDoesntSupportNft { .. } + | UpdateNftError::PrivKeyPolicyNotAllowed(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } /// Enumerates the errors that can occur during spam protection operations. -/// -/// This includes issues such as regex failures during text validation and -/// serialization/deserialization problems. #[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize)] pub enum ProtectFromSpamError { + /// Error related to regular expression operations. #[from_stringify("regex::Error")] RegexError(String), + /// Error related to serialization or deserialization with serde_json. #[from_stringify("serde_json::Error")] SerdeError(String), } @@ -315,7 +343,7 @@ impl From for UpdateSpamPhishingError { /// Errors encountered when parsing a `Chain` from a string. #[derive(Debug, Display)] pub enum ParseChainTypeError { - /// The provided string does not correspond to any of the supported blockchain types. + #[display(fmt = "The provided string does not correspond to any of the supported blockchain types.")] UnsupportedChainType, } @@ -331,10 +359,13 @@ impl From for MetaFromUrlError { fn from(e: GetInfoFromUriError) -> Self { MetaFromUrlError::GetInfoFromUriError(e) } } +/// Represents errors that can occur while locking the NFT database. #[derive(Debug, Display)] pub enum LockDBError { + /// Errors specific to the WebAssembly (WASM) environment's NFT cache. #[cfg(target_arch = "wasm32")] WasmNftCacheError(WasmNftCacheError), + /// Errors related to SQL operations in non-WASM environments. #[cfg(not(target_arch = "wasm32"))] SqlError(SqlError), } @@ -349,12 +380,16 @@ impl From for LockDBError { fn from(e: WasmNftCacheError) -> Self { LockDBError::WasmNftCacheError(e) } } +/// Errors related to calculating transfer confirmations for NFTs. #[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] pub enum TransferConfirmationsError { + /// Occurs when the specified coin does not exist. #[display(fmt = "No such coin {}", coin)] NoSuchCoin { coin: String }, + /// Triggered when the specified coin does not support NFT operations. #[display(fmt = "{} coin doesn't support NFT", coin)] CoinDoesntSupportNft { coin: String }, + /// Represents errors encountered while retrieving the current block number. #[display(fmt = "Get current block error: {}", _0)] GetCurrentBlockErr(String), } @@ -366,3 +401,39 @@ impl From for TransferConfirmationsError { } } } + +/// Enumerates errors that can occur while clearing NFT data from the database. +#[derive(Clone, Debug, Deserialize, Display, EnumFromStringify, PartialEq, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ClearNftDbError { + /// Represents errors related to database operations. + #[from_stringify("LockDBError")] + #[display(fmt = "DB error {}", _0)] + DbError(String), + /// Indicates internal errors not directly associated with database operations. + #[display(fmt = "Internal: {}", _0)] + Internal(String), + /// Used for various types of invalid requests, such as missing or contradictory parameters. + #[display(fmt = "Invalid request: {}", _0)] + InvalidRequest(String), +} + +impl From for ClearNftDbError { + fn from(err: T) -> Self { ClearNftDbError::DbError(format!("{:?}", err)) } +} + +impl HttpStatusCode for ClearNftDbError { + fn status_code(&self) -> StatusCode { + match self { + ClearNftDbError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + ClearNftDbError::DbError(_) | ClearNftDbError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +/// An error type for issues encountered while parsing contract type. +#[derive(Debug, Display)] +pub enum ParseContractTypeError { + /// Indicates that the contract type being parsed is not supported or recognized. + UnsupportedContractType, +} diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index 9173f3bd5b..92e9c62d30 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -1,4 +1,5 @@ use common::ten; +use enum_derives::EnumVariantList; use ethereum_types::Address; use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_err_handle::prelude::*; @@ -16,7 +17,7 @@ use url::Url; use crate::eth::EthTxFeeDetails; use crate::nft::eth_addr_to_hex; -use crate::nft::nft_errors::{LockDBError, ParseChainTypeError}; +use crate::nft::nft_errors::{LockDBError, ParseChainTypeError, ParseContractTypeError}; use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps}; use crate::{TransactionType, TxFeeDetails, WithdrawFee}; @@ -67,43 +68,42 @@ pub struct NftListFilters { } /// Contains parameters required to fetch metadata for a specified NFT. -/// # Fields -/// * `token_address`: The address of the NFT token. -/// * `token_id`: The ID of the NFT token. -/// * `chain`: The blockchain where the NFT exists. -/// * `protect_from_spam`: Indicates whether to check and redact potential spam. If set to true, -/// the internal function `protect_from_nft_spam` is utilized. #[derive(Debug, Deserialize)] pub struct NftMetadataReq { + /// The address of the NFT token. pub(crate) token_address: Address, + /// The ID of the NFT token. #[serde(deserialize_with = "deserialize_token_id")] pub(crate) token_id: BigUint, + /// The blockchain where the NFT exists. pub(crate) chain: Chain, + /// Indicates whether to check and redact potential spam. If set to true, + /// the internal function `protect_from_nft_spam` is utilized. #[serde(default)] pub(crate) protect_from_spam: bool, } /// Contains parameters required to refresh metadata for a specified NFT. -/// # Fields -/// * `token_address`: The address of the NFT token whose metadata needs to be refreshed. -/// * `token_id`: The ID of the NFT token. -/// * `chain`: The blockchain where the NFT exists. -/// * `url`: URL to fetch the metadata. -/// * `url_antispam`: URL used to validate if the fetched contract addresses are associated -/// with spam contracts or if domain fields in the fetched metadata match known phishing domains. #[derive(Debug, Deserialize)] pub struct RefreshMetadataReq { + /// The address of the NFT token whose metadata needs to be refreshed. pub(crate) token_address: Address, + /// The ID of the NFT token. #[serde(deserialize_with = "deserialize_token_id")] pub(crate) token_id: BigUint, + /// The blockchain where the NFT exists. pub(crate) chain: Chain, + /// URL to fetch the metadata. pub(crate) url: Url, + /// URL used to validate if the fetched contract addresses are associated + /// with spam contracts or if domain fields in the fetched metadata match known phishing domains. pub(crate) url_antispam: Url, + pub(crate) proxy_auth: bool, } /// Represents blockchains which are supported by NFT feature. /// Currently there are only EVM based chains. -#[derive(Clone, Copy, Debug, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, EnumVariantList, PartialEq, Serialize)] #[serde(rename_all = "UPPERCASE")] pub enum Chain { Avalanche, @@ -113,11 +113,15 @@ pub enum Chain { Polygon, } -pub(crate) trait ConvertChain { +pub trait ConvertChain { fn to_ticker(&self) -> &'static str; + fn from_ticker(s: &str) -> Result; + fn to_nft_ticker(&self) -> &'static str; + fn from_nft_ticker(s: &str) -> Result; } impl ConvertChain for Chain { + #[inline(always)] fn to_ticker(&self) -> &'static str { match self { Chain::Avalanche => "AVAX", @@ -127,6 +131,43 @@ impl ConvertChain for Chain { Chain::Polygon => "MATIC", } } + + /// Converts a coin ticker string to a `Chain` enum. + #[inline(always)] + fn from_ticker(s: &str) -> Result { + match s { + "AVAX" | "avax" => Ok(Chain::Avalanche), + "BNB" | "bnb" => Ok(Chain::Bsc), + "ETH" | "eth" => Ok(Chain::Eth), + "FTM" | "ftm" => Ok(Chain::Fantom), + "MATIC" | "matic" => Ok(Chain::Polygon), + _ => Err(ParseChainTypeError::UnsupportedChainType), + } + } + + #[inline(always)] + fn to_nft_ticker(&self) -> &'static str { + match self { + Chain::Avalanche => "NFT_AVAX", + Chain::Bsc => "NFT_BNB", + Chain::Eth => "NFT_ETH", + Chain::Fantom => "NFT_FTM", + Chain::Polygon => "NFT_MATIC", + } + } + + /// Converts a NFT ticker string to a `Chain` enum. + #[inline(always)] + fn from_nft_ticker(s: &str) -> Result { + match s.to_uppercase().as_str() { + "NFT_AVAX" => Ok(Chain::Avalanche), + "NFT_BNB" => Ok(Chain::Bsc), + "NFT_ETH" => Ok(Chain::Eth), + "NFT_FTM" => Ok(Chain::Fantom), + "NFT_MATIC" => Ok(Chain::Polygon), + _ => Err(ParseChainTypeError::UnsupportedChainType), + } + } } impl fmt::Display for Chain { @@ -144,19 +185,16 @@ impl fmt::Display for Chain { impl FromStr for Chain { type Err = ParseChainTypeError; - #[inline] + /// Converts a string slice to a `Chain` enum. + /// This implementation is primarily used in the context of deserialization with Serde. + #[inline(always)] fn from_str(s: &str) -> Result { match s { - "AVALANCHE" => Ok(Chain::Avalanche), - "avalanche" => Ok(Chain::Avalanche), - "BSC" => Ok(Chain::Bsc), - "bsc" => Ok(Chain::Bsc), - "ETH" => Ok(Chain::Eth), - "eth" => Ok(Chain::Eth), - "FANTOM" => Ok(Chain::Fantom), - "fantom" => Ok(Chain::Fantom), - "POLYGON" => Ok(Chain::Polygon), - "polygon" => Ok(Chain::Polygon), + "AVALANCHE" | "avalanche" => Ok(Chain::Avalanche), + "BSC" | "bsc" => Ok(Chain::Bsc), + "ETH" | "eth" => Ok(Chain::Eth), + "FANTOM" | "fantom" => Ok(Chain::Fantom), + "POLYGON" | "polygon" => Ok(Chain::Polygon), _ => Err(ParseChainTypeError::UnsupportedChainType), } } @@ -173,15 +211,16 @@ impl<'de> Deserialize<'de> for Chain { } } -#[derive(Debug, Display)] -pub(crate) enum ParseContractTypeError { - UnsupportedContractType, -} - +/// Represents the type of smart contract used for NFTs. #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "UPPERCASE")] -pub(crate) enum ContractType { +pub enum ContractType { + /// Represents an ERC-1155 contract, which allows a single contract to manage + /// multiple types of NFTs. This means ERC-1155 represents both fungible and non-fungible assets within a single contract. + /// ERC-1155 provides a way for each token ID to represent multiple assets. Erc1155, + /// Represents an ERC-721 contract, a standard for non-fungible tokens on EVM based chains, + /// where each token is unique and owned individually. Erc721, } @@ -385,37 +424,57 @@ pub struct NftList { pub(crate) total: usize, } +/// Parameters for withdrawing an ERC-1155 token. #[derive(Clone, Deserialize)] pub struct WithdrawErc1155 { + /// The blockchain network to perform the withdrawal on. pub(crate) chain: Chain, + /// The address to send the NFT to. pub(crate) to: String, + /// The address of the ERC-1155 token contract. pub(crate) token_address: String, + /// The unique identifier of the NFT to withdraw. #[serde(deserialize_with = "deserialize_token_id")] pub(crate) token_id: BigUint, + /// Optional amount of the token to withdraw. Defaults to 1 if not specified. pub(crate) amount: Option, + /// If set to `true`, withdraws the maximum amount available. Overrides the `amount` field. #[serde(default)] pub(crate) max: bool, + /// Optional details for the withdrawal fee. pub(crate) fee: Option, } +/// Parameters for withdrawing an ERC-721 token. #[derive(Clone, Deserialize)] pub struct WithdrawErc721 { + /// The blockchain network to perform the withdrawal on. pub(crate) chain: Chain, + /// The address to send the NFT to. pub(crate) to: String, + /// The address of the ERC-721 token contract. pub(crate) token_address: String, + /// The unique identifier of the NFT to withdraw. #[serde(deserialize_with = "deserialize_token_id")] pub(crate) token_id: BigUint, + /// Optional details for the withdrawal fee. pub(crate) fee: Option, } +/// Represents a request for withdrawing an NFT, supporting different ERC standards. #[derive(Clone, Deserialize)] #[serde(tag = "type", content = "withdraw_data")] #[serde(rename_all = "snake_case")] pub enum WithdrawNftReq { + /// Parameters for withdrawing an ERC-1155 token. WithdrawErc1155(WithdrawErc1155), + /// Parameters for withdrawing an ERC-721 token. WithdrawErc721(WithdrawErc721), } +/// Details of NFT transaction. +/// +/// Includes the raw transaction hex for broadcasting, along with additional information about the transaction. #[derive(Debug, Deserialize, Serialize)] pub struct TransactionNftDetails { /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction` RPC to broadcast the transaction @@ -593,33 +652,45 @@ pub struct NftTransferHistoryFilters { } /// Contains parameters required to update NFT transfer history and NFT list. -/// # Fields -/// * `chains`: A list of blockchains for which the NFTs need to be updated. -/// * `url`: URL to fetch the NFT data. -/// * `url_antispam`: URL used to validate if the fetched contract addresses are associated -/// with spam contracts or if domain fields in the fetched metadata match known phishing domains. #[derive(Debug, Deserialize)] pub struct UpdateNftReq { + /// A list of blockchains for which the NFTs need to be updated. pub(crate) chains: Vec, + /// URL to fetch the NFT data. pub(crate) url: Url, + /// URL used to validate if the fetched contract addresses are associated + /// with spam contracts or if domain fields in the fetched metadata match known phishing domains. pub(crate) url_antispam: Url, + pub(crate) proxy_auth: bool, } +/// Represents a unique identifier for an NFT, consisting of its token address and token ID. #[derive(Debug, Deserialize, Eq, Hash, PartialEq)] pub struct NftTokenAddrId { + /// The address of the NFT token contract. pub(crate) token_address: String, + /// The unique identifier of the NFT within its contract. pub(crate) token_id: BigUint, } +/// Holds metadata information for an NFT transfer. #[derive(Debug)] pub struct TransferMeta { + /// The address of the NFT token contract. pub(crate) token_address: String, + /// The unique identifier of the NFT. pub(crate) token_id: BigUint, + /// Optional URI for the NFT's metadata. pub(crate) token_uri: Option, + /// Optional domain associated with the NFT's metadata. pub(crate) token_domain: Option, + /// Optional name of the NFT's collection. pub(crate) collection_name: Option, + /// Optional URL for the NFT's image. pub(crate) image_url: Option, + /// Optional domain for the NFT's image. pub(crate) image_domain: Option, + /// Optional name of the NFT. pub(crate) token_name: Option, } @@ -732,3 +803,32 @@ where let s = String::deserialize(deserializer)?; BigUint::from_str(&s).map_err(serde::de::Error::custom) } + +/// Request parameters for clearing NFT data from the database. +#[derive(Debug, Deserialize)] +pub struct ClearNftDbReq { + /// Specifies the blockchain networks (e.g., Ethereum, BSC) to clear NFT data. + #[serde(default)] + pub(crate) chains: Vec, + /// If `true`, clears NFT data for all chains, ignoring the `chains` field. Defaults to `false`. + #[serde(default)] + pub(crate) clear_all: bool, +} + +/// Represents detailed information about a Non-Fungible Token (NFT). +/// This struct is used to keep info about NFTs owned by user in global Non-Fungible Token. +#[derive(Clone, Debug, Serialize)] +pub struct NftInfo { + /// The address of the NFT token. + pub token_address: Address, + /// The ID of the NFT token. + #[serde(serialize_with = "serialize_token_id")] + pub token_id: BigUint, + /// The blockchain where the NFT exists. + pub chain: Chain, + /// The type of smart contract that governs this NFT. + pub contract_type: ContractType, + /// The quantity of this type of NFT owned. Particularly relevant for ERC-1155 tokens, + /// where a single token ID can represent multiple assets. + pub amount: BigDecimal, +} diff --git a/mm2src/coins/nft/nft_tests.rs b/mm2src/coins/nft/nft_tests.rs index 99ec3925de..05f732a9ee 100644 --- a/mm2src/coins/nft/nft_tests.rs +++ b/mm2src/coins/nft/nft_tests.rs @@ -4,7 +4,7 @@ use crate::nft::nft_structs::{Chain, NftFromMoralis, NftListFilters, NftTransfer SpamContractRes, TransferMeta, UriMeta}; use crate::nft::storage::db_test_helpers::{get_nft_ctx, nft, nft_list, nft_transfer_history}; use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps, RemoveNftResult}; -use crate::nft::{check_moralis_ipfs_bafy, get_domain_from_url, process_metadata_for_spam_link, +use crate::nft::{check_moralis_ipfs_bafy, get_domain_from_url, is_malicious, process_metadata_for_spam_link, process_text_for_spam_link}; use common::cross_test; use ethereum_types::Address; @@ -27,9 +27,17 @@ use mm2_net::native_http::send_request_to_uri; common::cfg_wasm32! { use wasm_bindgen_test::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - use mm2_net::wasm_http::send_request_to_uri; + use mm2_net::wasm::http::send_request_to_uri; } +cross_test!(test_is_malicious, { + let token_uri = "https://btrgtrhbyjuyj.xyz/BABYDOGE.json"; + assert!(is_malicious(token_uri).unwrap()); + + let token_uri1 = "https://btrgtrhbyjuyj.com/BABYDOGE.json%00"; + assert!(is_malicious(token_uri1).unwrap()); +}); + cross_test!(test_moralis_ipfs_bafy, { let uri = "https://ipfs.moralis.io:2053/ipfs/bafybeifnek24coy5xj5qabdwh24dlp5omq34nzgvazkfyxgnqms4eidsiq/1.json"; let res_uri = check_moralis_ipfs_bafy(Some(uri)); @@ -86,7 +94,7 @@ cross_test!(test_moralis_requests, { "{}/{}/nft?chain=POLYGON&format=decimal", MORALIS_API_ENDPOINT_TEST, TEST_WALLET_ADDR_EVM ); - let response_nft_list = send_request_to_uri(uri_nft_list.as_str()).await.unwrap(); + let response_nft_list = send_request_to_uri(uri_nft_list.as_str(), None).await.unwrap(); let nfts_list = response_nft_list["result"].as_array().unwrap(); for nft_json in nfts_list { let nft_moralis: NftFromMoralis = serde_json::from_str(&nft_json.to_string()).unwrap(); @@ -97,7 +105,7 @@ cross_test!(test_moralis_requests, { "{}/{}/nft/transfers?chain=POLYGON&format=decimal", MORALIS_API_ENDPOINT_TEST, TEST_WALLET_ADDR_EVM ); - let response_transfer_history = send_request_to_uri(uri_history.as_str()).await.unwrap(); + let response_transfer_history = send_request_to_uri(uri_history.as_str(), None).await.unwrap(); let mut transfer_list = response_transfer_history["result"].as_array().unwrap().clone(); assert!(!transfer_list.is_empty()); let first_transfer = transfer_list.remove(transfer_list.len() - 1); @@ -111,9 +119,9 @@ cross_test!(test_moralis_requests, { "{}/nft/0xed55e4477b795eaa9bb4bca24df42214e1a05c18/1111777?chain=POLYGON&format=decimal", MORALIS_API_ENDPOINT_TEST ); - let response_meta = send_request_to_uri(uri_meta.as_str()).await.unwrap(); + let response_meta = send_request_to_uri(uri_meta.as_str(), None).await.unwrap(); let nft_moralis: NftFromMoralis = serde_json::from_str(&response_meta.to_string()).unwrap(); - assert_eq!(41237364, *nft_moralis.block_number_minted.unwrap()); + assert_eq!(42563567, nft_moralis.block_number.0); }); cross_test!(test_antispam_scan_endpoints, { @@ -147,7 +155,7 @@ cross_test!(test_antispam_scan_endpoints, { cross_test!(test_camo, { let hex_token_uri = hex::encode("https://tikimetadata.s3.amazonaws.com/tiki_box.json"); let uri_decode = format!("{}/url/decode/{}", BLOCKLIST_API_ENDPOINT, hex_token_uri); - let decode_res = send_request_to_uri(&uri_decode).await.unwrap(); + let decode_res = send_request_to_uri(&uri_decode, None).await.unwrap(); let uri_meta: UriMeta = serde_json::from_value(decode_res).unwrap(); assert_eq!( uri_meta.raw_image_url.unwrap(), @@ -395,6 +403,48 @@ cross_test!(test_exclude_nft_phishing_spam, { assert_eq!(nfts.len(), 2); }); +cross_test!(test_clear_nft, { + let chain = Chain::Bsc; + let nft_ctx = get_nft_ctx(&chain).await; + let storage = nft_ctx.lock_db().await.unwrap(); + NftListStorageOps::init(&storage, &chain).await.unwrap(); + let nft = nft(); + storage.add_nfts_to_list(chain, vec![nft], 28056726).await.unwrap(); + + storage.clear_nft_data(&chain).await.unwrap(); + test_clear_nft_target(&storage, &chain).await; +}); + +cross_test!(test_clear_all_nft, { + let chain = Chain::Bsc; + let nft_ctx = get_nft_ctx(&chain).await; + let storage = nft_ctx.lock_db().await.unwrap(); + NftListStorageOps::init(&storage, &chain).await.unwrap(); + let nft = nft(); + storage.add_nfts_to_list(chain, vec![nft], 28056726).await.unwrap(); + + storage.clear_all_nft_data().await.unwrap(); + test_clear_nft_target(&storage, &chain).await; +}); + +#[cfg(not(target_arch = "wasm32"))] +async fn test_clear_nft_target(storage: &S, chain: &Chain) { + let is_initialized = NftListStorageOps::is_initialized(storage, chain).await.unwrap(); + assert!(!is_initialized); + + let is_err = storage.get_nft_list(vec![*chain], false, 10, None, None).await.is_err(); + assert!(is_err); + + let is_err = storage.get_last_scanned_block(chain).await.is_err(); + assert!(is_err); +} + +#[cfg(target_arch = "wasm32")] +async fn test_clear_nft_target(storage: &S, chain: &Chain) { + let nft_list = storage.get_nft_list(vec![*chain], true, 1, None, None).await.unwrap(); + assert!(nft_list.nfts.is_empty()); +} + cross_test!(test_add_get_transfers, { let chain = Chain::Bsc; let nft_ctx = get_nft_ctx(&chain).await; @@ -527,7 +577,7 @@ cross_test!(test_get_update_transfer_meta, { storage.add_transfers_to_history(chain, transfers).await.unwrap(); let vec_token_add_id = storage.get_transfers_with_empty_meta(chain).await.unwrap(); - assert_eq!(vec_token_add_id.len(), 3); + assert_eq!(vec_token_add_id.len(), 2); let token_add = "0x5c7d6712dfaf0cb079d48981781c8705e8417ca0".to_string(); let transfer_meta = TransferMeta { @@ -693,3 +743,44 @@ cross_test!(test_exclude_transfer_phishing_spam, { .transfer_history; assert_eq!(transfers.len(), 1); }); + +cross_test!(test_clear_history, { + let chain = Chain::Bsc; + let nft_ctx = get_nft_ctx(&chain).await; + let storage = nft_ctx.lock_db().await.unwrap(); + NftTransferHistoryStorageOps::init(&storage, &chain).await.unwrap(); + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + storage.clear_history_data(&chain).await.unwrap(); + test_clear_history_target(&storage, &chain).await; +}); + +cross_test!(test_clear_all_history, { + let chain = Chain::Bsc; + let nft_ctx = get_nft_ctx(&chain).await; + let storage = nft_ctx.lock_db().await.unwrap(); + NftTransferHistoryStorageOps::init(&storage, &chain).await.unwrap(); + let transfers = nft_transfer_history(); + storage.add_transfers_to_history(chain, transfers).await.unwrap(); + + storage.clear_all_history_data().await.unwrap(); + test_clear_history_target(&storage, &chain).await; +}); + +#[cfg(not(target_arch = "wasm32"))] +async fn test_clear_history_target(storage: &S, chain: &Chain) { + let is_init = NftTransferHistoryStorageOps::is_initialized(storage, chain) + .await + .unwrap(); + assert!(!is_init); +} + +#[cfg(target_arch = "wasm32")] +async fn test_clear_history_target(storage: &S, chain: &Chain) { + let transfer_list = storage + .get_transfer_history(vec![*chain], true, 1, None, None) + .await + .unwrap(); + assert!(transfer_list.transfer_history.is_empty()); +} diff --git a/mm2src/coins/nft/storage/mod.rs b/mm2src/coins/nft/storage/mod.rs index c28c33ea54..ad255100c3 100644 --- a/mm2src/coins/nft/storage/mod.rs +++ b/mm2src/coins/nft/storage/mod.rs @@ -1,7 +1,6 @@ use crate::eth::EthTxFeeDetails; use crate::nft::nft_structs::{Chain, Nft, NftList, NftListFilters, NftTokenAddrId, NftTransferHistory, NftTransferHistoryFilters, NftsTransferHistoryList, TransferMeta}; -use crate::WithdrawError; use async_trait::async_trait; use ethereum_types::Address; use mm2_err_handle::mm_error::MmResult; @@ -28,10 +27,6 @@ pub enum RemoveNftResult { /// Defines the standard errors that can occur in NFT storage operations pub trait NftStorageError: std::fmt::Debug + NotMmError + NotEqual + Send {} -impl From for WithdrawError { - fn from(err: T) -> Self { WithdrawError::DbError(format!("{:?}", err)) } -} - /// Provides asynchronous operations for handling and querying NFT listings. #[async_trait] pub trait NftListStorageOps { @@ -112,6 +107,11 @@ pub trait NftListStorageOps { domain: String, possible_phishing: bool, ) -> MmResult<(), Self::Error>; + + async fn clear_nft_data(&self, chain: &Chain) -> MmResult<(), Self::Error>; + + /// Clears all nft list tables related to each chain. + async fn clear_all_nft_data(&self) -> MmResult<(), Self::Error>; } /// Provides asynchronous operations related to the history of NFT transfers. @@ -201,6 +201,11 @@ pub trait NftTransferHistoryStorageOps { domain: String, possible_phishing: bool, ) -> MmResult<(), Self::Error>; + + async fn clear_history_data(&self, chain: &Chain) -> MmResult<(), Self::Error>; + + /// Clears all nft history tables related to each chain. + async fn clear_all_history_data(&self) -> MmResult<(), Self::Error>; } /// `get_offset_limit` function calculates offset and limit for final result if we use pagination. diff --git a/mm2src/coins/nft/storage/sql_storage.rs b/mm2src/coins/nft/storage/sql_storage.rs index 86166a4793..6844b261d9 100644 --- a/mm2src/coins/nft/storage/sql_storage.rs +++ b/mm2src/coins/nft/storage/sql_storage.rs @@ -10,7 +10,7 @@ use db_common::sql_build::{SqlCondition, SqlQuery}; use db_common::sqlite::rusqlite::types::{FromSqlError, Type}; use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Result as SqlResult, Row, Statement}; use db_common::sqlite::sql_builder::SqlBuilder; -use db_common::sqlite::{query_single_row, string_from_row, validate_table_name, CHECK_TABLE_EXISTS_SQL}; +use db_common::sqlite::{query_single_row, string_from_row, SafeTableName, CHECK_TABLE_EXISTS_SQL}; use ethereum_types::Address; use futures::lock::MutexGuard as AsyncMutexGuard; use mm2_err_handle::prelude::*; @@ -23,27 +23,27 @@ use std::num::NonZeroUsize; use std::str::FromStr; impl Chain { - fn nft_list_table_name(&self) -> SqlResult { + fn nft_list_table_name(&self) -> SqlResult { let name = self.to_ticker().to_owned() + "_nft_list"; - validate_table_name(&name)?; - Ok(name) + let safe_name = SafeTableName::new(&name)?; + Ok(safe_name) } - fn transfer_history_table_name(&self) -> SqlResult { + fn transfer_history_table_name(&self) -> SqlResult { let name = self.to_ticker().to_owned() + "_nft_transfer_history"; - validate_table_name(&name)?; - Ok(name) + let safe_name = SafeTableName::new(&name)?; + Ok(safe_name) } } -fn scanned_nft_blocks_table_name() -> SqlResult { +fn scanned_nft_blocks_table_name() -> SqlResult { let name = "scanned_nft_blocks".to_string(); - validate_table_name(&name)?; - Ok(name) + let safe_name = SafeTableName::new(&name)?; + Ok(safe_name) } fn create_nft_list_table_sql(chain: &Chain) -> MmResult { - let table_name = chain.nft_list_table_name()?; + let safe_table_name = chain.nft_list_table_name()?; let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( token_address VARCHAR(256) NOT NULL, @@ -75,13 +75,13 @@ fn create_nft_list_table_sql(chain: &Chain) -> MmResult { details_json TEXT, PRIMARY KEY (token_address, token_id) );", - table_name + safe_table_name.inner() ); Ok(sql) } fn create_transfer_history_table_sql(chain: &Chain) -> Result { - let table_name = chain.transfer_history_table_name()?; + let safe_table_name = chain.transfer_history_table_name()?; let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( transaction_hash VARCHAR(256) NOT NULL, @@ -105,19 +105,19 @@ fn create_transfer_history_table_sql(chain: &Chain) -> Result details_json TEXT, PRIMARY KEY (transaction_hash, log_index) );", - table_name + safe_table_name.inner() ); Ok(sql) } fn create_scanned_nft_blocks_sql() -> Result { - let table_name = scanned_nft_blocks_table_name()?; + let safe_table_name = scanned_nft_blocks_table_name()?; let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( chain TEXT PRIMARY KEY, last_scanned_block INTEGER DEFAULT 0 );", - table_name + safe_table_name.inner() ); Ok(sql) } @@ -129,7 +129,7 @@ fn get_nft_list_builder_preimage(chains: Vec, filters: Option, filters: Option) -> Result { - let mut sql_builder = SqlBuilder::select_from(table_name); +fn nft_list_builder_preimage( + safe_table_name: SafeTableName, + filters: Option, +) -> Result { + let mut sql_builder = SqlBuilder::select_from(safe_table_name.inner()); if let Some(filters) = filters { if filters.exclude_spam { sql_builder.and_where("possible_spam == 0"); @@ -167,7 +170,7 @@ fn get_nft_transfer_builder_preimage( .into_iter() .map(|chain| { let table_name = chain.transfer_history_table_name()?; - let sql_builder = nft_history_table_builder_preimage(table_name.as_str(), filters)?; + let sql_builder = nft_history_table_builder_preimage(table_name, filters)?; let sql_string = sql_builder .sql() .map_err(|e| SqlError::ToSqlConversionFailure(e.into()))? @@ -184,10 +187,10 @@ fn get_nft_transfer_builder_preimage( } fn nft_history_table_builder_preimage( - table_name: &str, + safe_table_name: SafeTableName, filters: Option, ) -> Result { - let mut sql_builder = SqlBuilder::select_from(table_name); + let mut sql_builder = SqlBuilder::select_from(safe_table_name.inner()); if let Some(filters) = filters { if filters.send && !filters.receive { sql_builder.and_where_eq("status", "'Send'"); @@ -388,7 +391,7 @@ fn token_address_id_from_row(row: &Row<'_>) -> Result } fn insert_nft_in_list_sql(chain: &Chain) -> Result { - let table_name = chain.nft_list_table_name()?; + let safe_table_name = chain.nft_list_table_name()?; let sql = format!( "INSERT INTO {} ( token_address, token_id, chain, amount, block_number, contract_type, possible_spam, @@ -400,13 +403,13 @@ fn insert_nft_in_list_sql(chain: &Chain) -> Result { ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27 );", - table_name + safe_table_name.inner() ); Ok(sql) } fn insert_transfer_in_history_sql(chain: &Chain) -> Result { - let table_name = chain.transfer_history_table_name()?; + let safe_table_name = chain.transfer_history_table_name()?; let sql = format!( "INSERT INTO {} ( transaction_hash, log_index, chain, block_number, block_timestamp, contract_type, @@ -415,66 +418,69 @@ fn insert_transfer_in_history_sql(chain: &Chain) -> Result { ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19 );", - table_name + safe_table_name.inner() ); Ok(sql) } fn upsert_last_scanned_block_sql() -> Result { - let table_name = scanned_nft_blocks_table_name()?; + let safe_table_name = scanned_nft_blocks_table_name()?; let sql = format!( "INSERT OR REPLACE INTO {} (chain, last_scanned_block) VALUES (?1, ?2);", - table_name + safe_table_name.inner() ); Ok(sql) } fn refresh_nft_metadata_sql(chain: &Chain) -> Result { - let table_name = chain.nft_list_table_name()?; + let safe_table_name = chain.nft_list_table_name()?; let sql = format!( "UPDATE {} SET possible_spam = ?1, possible_phishing = ?2, collection_name = ?3, symbol = ?4, token_uri = ?5, token_domain = ?6, metadata = ?7, \ last_token_uri_sync = ?8, last_metadata_sync = ?9, raw_image_url = ?10, image_url = ?11, image_domain = ?12, token_name = ?13, description = ?14, \ attributes = ?15, animation_url = ?16, animation_domain = ?17, external_url = ?18, external_domain = ?19, image_details = ?20 WHERE token_address = ?21 AND token_id = ?22;", - table_name + safe_table_name.inner() ); Ok(sql) } fn update_transfers_meta_by_token_addr_id_sql(chain: &Chain) -> Result { - let table_name = chain.transfer_history_table_name()?; + let safe_table_name = chain.transfer_history_table_name()?; let sql = format!( "UPDATE {} SET token_uri = ?1, token_domain = ?2, collection_name = ?3, image_url = ?4, image_domain = ?5, \ token_name = ?6 WHERE token_address = ?7 AND token_id = ?8;", - table_name + safe_table_name.inner() ); Ok(sql) } fn update_transfer_spam_by_token_addr_id(chain: &Chain) -> Result { - let table_name = chain.transfer_history_table_name()?; + let safe_table_name = chain.transfer_history_table_name()?; let sql = format!( "UPDATE {} SET possible_spam = ?1 WHERE token_address = ?2 AND token_id = ?3;", - table_name + safe_table_name.inner() ); Ok(sql) } -fn select_last_block_number_sql(table_name: String) -> Result { +fn select_last_block_number_sql(safe_table_name: SafeTableName) -> Result { let sql = format!( "SELECT block_number FROM {} ORDER BY block_number DESC LIMIT 1", - table_name + safe_table_name.inner() ); Ok(sql) } fn select_last_scanned_block_sql() -> MmResult { let table_name = scanned_nft_blocks_table_name()?; - let sql = format!("SELECT last_scanned_block FROM {} WHERE chain=?1", table_name,); + let sql = format!("SELECT last_scanned_block FROM {} WHERE chain=?1", table_name.inner()); Ok(sql) } -fn delete_nft_sql(table_name: String) -> Result { - let sql = format!("DELETE FROM {} WHERE token_address=?1 AND token_id=?2", table_name); +fn delete_nft_sql(safe_table_name: SafeTableName) -> Result { + let sql = format!( + "DELETE FROM {} WHERE token_address=?1 AND token_id=?2", + safe_table_name.inner() + ); Ok(sql) } @@ -482,38 +488,44 @@ fn block_number_from_row(row: &Row<'_>) -> Result { row.get::<_, fn nft_amount_from_row(row: &Row<'_>) -> Result { row.get(0) } -fn get_nfts_by_token_address_statement(conn: &Connection, table_name: String) -> Result { - let sql_query = format!("SELECT * FROM {} WHERE token_address = ?", table_name); +fn get_nfts_by_token_address_statement( + conn: &Connection, + safe_table_name: SafeTableName, +) -> Result { + let sql_query = format!("SELECT * FROM {} WHERE token_address = ?", safe_table_name.inner()); let stmt = conn.prepare(&sql_query)?; Ok(stmt) } -fn get_token_addresses_statement(conn: &Connection, table_name: String) -> Result { - let sql_query = format!("SELECT DISTINCT token_address FROM {}", table_name); +fn get_token_addresses_statement(conn: &Connection, safe_table_name: SafeTableName) -> Result { + let sql_query = format!("SELECT DISTINCT token_address FROM {}", safe_table_name.inner()); let stmt = conn.prepare(&sql_query)?; Ok(stmt) } fn get_transfers_from_block_statement<'a>(conn: &'a Connection, chain: &'a Chain) -> Result, SqlError> { - let table_name = chain.transfer_history_table_name()?; + let safe_table_name = chain.transfer_history_table_name()?; let sql_query = format!( "SELECT * FROM {} WHERE block_number >= ? ORDER BY block_number ASC", - table_name + safe_table_name.inner() ); let stmt = conn.prepare(&sql_query)?; Ok(stmt) } fn get_transfers_by_token_addr_id_statement(conn: &Connection, chain: Chain) -> Result { - let table_name = chain.transfer_history_table_name()?; - let sql_query = format!("SELECT * FROM {} WHERE token_address = ? AND token_id = ?", table_name); + let safe_table_name = chain.transfer_history_table_name()?; + let sql_query = format!( + "SELECT * FROM {} WHERE token_address = ? AND token_id = ?", + safe_table_name.inner() + ); let stmt = conn.prepare(&sql_query)?; Ok(stmt) } fn get_transfers_with_empty_meta_builder<'a>(conn: &'a Connection, chain: &'a Chain) -> Result, SqlError> { - let table_name = chain.transfer_history_table_name()?; - let mut sql_builder = SqlQuery::select_from(conn, table_name.as_str())?; + let safe_table_name = chain.transfer_history_table_name()?; + let mut sql_builder = SqlQuery::select_from(conn, safe_table_name.inner())?; sql_builder .sql_builder() .distinct() @@ -528,6 +540,12 @@ fn get_transfers_with_empty_meta_builder<'a>(conn: &'a Connection, chain: &'a Ch Ok(sql_builder) } +fn is_table_empty(conn: &Connection, safe_table_name: SafeTableName) -> Result { + let query = format!("SELECT COUNT(*) FROM {}", safe_table_name.inner()); + conn.query_row(&query, [], |row| row.get::<_, i64>(0)) + .map(|count| count == 0) +} + #[async_trait] impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { type Error = AsyncConnError; @@ -546,11 +564,12 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { async fn is_initialized(&self, chain: &Chain) -> MmResult { let table_name = chain.nft_list_table_name()?; self.call(move |conn| { - let nft_list_initialized = query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [table_name], string_from_row)?; + let nft_list_initialized = + query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [table_name.inner()], string_from_row)?; let scanned_nft_blocks_initialized = query_single_row( conn, CHECK_TABLE_EXISTS_SQL, - [scanned_nft_blocks_table_name()?], + [scanned_nft_blocks_table_name()?.inner()], string_from_row, )?; Ok(nft_list_initialized.is_some() && scanned_nft_blocks_initialized.is_some()) @@ -660,7 +679,10 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { ) -> MmResult, Self::Error> { let table_name = chain.nft_list_table_name()?; self.call(move |conn| { - let sql = format!("SELECT * FROM {} WHERE token_address=?1 AND token_id=?2", table_name); + let sql = format!( + "SELECT * FROM {} WHERE token_address=?1 AND token_id=?2", + table_name.inner() + ); let params = [token_address, token_id.to_string()]; let nft = query_single_row(conn, &sql, params, nft_from_row)?; Ok(nft) @@ -706,7 +728,7 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { let table_name = chain.nft_list_table_name()?; let sql = format!( "SELECT amount FROM {} WHERE token_address=?1 AND token_id=?2", - table_name + table_name.inner() ); let params = [token_address, token_id.to_string()]; self.call(move |conn| { @@ -783,7 +805,7 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { let table_name = chain.nft_list_table_name()?; let sql = format!( "UPDATE {} SET amount = ?1 WHERE token_address = ?2 AND token_id = ?3;", - table_name + table_name.inner() ); let scanned_block_params = [chain.to_ticker().to_string(), scanned_block.to_string()]; self.call(move |conn| { @@ -806,7 +828,7 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { let table_name = chain.nft_list_table_name()?; let sql = format!( "UPDATE {} SET amount = ?1, block_number = ?2 WHERE token_address = ?3 AND token_id = ?4;", - table_name + table_name.inner() ); let scanned_block_params = [chain.to_ticker().to_string(), nft.block_number.to_string()]; self.call(move |conn| { @@ -846,7 +868,10 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { possible_spam: bool, ) -> MmResult<(), Self::Error> { let table_name = chain.nft_list_table_name()?; - let sql = format!("UPDATE {} SET possible_spam = ?1 WHERE token_address = ?2;", table_name); + let sql = format!( + "UPDATE {} SET possible_spam = ?1 WHERE token_address = ?2;", + table_name.inner() + ); self.call(move |conn| { let sql_transaction = conn.transaction()?; let params = [Some(i32::from(possible_spam).to_string()), Some(token_address.clone())]; @@ -859,8 +884,9 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { } async fn get_animation_external_domains(&self, chain: &Chain) -> MmResult, Self::Error> { - let table_name = chain.nft_list_table_name()?; + let safe_table_name = chain.nft_list_table_name()?; self.call(move |conn| { + let table_name = safe_table_name.inner(); let sql_query = format!( "SELECT DISTINCT animation_domain FROM {} UNION SELECT DISTINCT external_domain FROM {}", table_name, table_name @@ -886,7 +912,7 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { let sql = format!( "UPDATE {} SET possible_phishing = ?1 WHERE token_domain = ?2 OR image_domain = ?2 OR animation_domain = ?2 OR external_domain = ?2;", - table_name + table_name.inner() ); self.call(move |conn| { let sql_transaction = conn.transaction()?; @@ -898,6 +924,43 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { .await .map_to_mm(AsyncConnError::from) } + + async fn clear_nft_data(&self, chain: &Chain) -> MmResult<(), Self::Error> { + let table_nft_name = chain.nft_list_table_name()?; + let sql_nft = format!("DROP TABLE IF EXISTS {};", table_nft_name.inner()); + let table_scanned_blocks = scanned_nft_blocks_table_name()?; + let sql_scanned_block = format!("DELETE from {} where chain=?1", table_scanned_blocks.inner()); + let scanned_block_param = [chain.to_ticker()]; + self.call(move |conn| { + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&sql_nft, [])?; + sql_transaction.execute(&sql_scanned_block, scanned_block_param)?; + sql_transaction.commit()?; + if is_table_empty(conn, table_scanned_blocks.clone())? { + conn.execute(&format!("DROP TABLE IF EXISTS {};", table_scanned_blocks.inner()), []) + .map(|_| ())?; + } + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn clear_all_nft_data(&self) -> MmResult<(), Self::Error> { + self.call(move |conn| { + let sql_transaction = conn.transaction()?; + for chain in Chain::variant_list().into_iter() { + let table_name = chain.nft_list_table_name()?; + sql_transaction.execute(&format!("DROP TABLE IF EXISTS {};", table_name.inner()), [])?; + } + let table_scanned_blocks = scanned_nft_blocks_table_name()?; + sql_transaction.execute(&format!("DROP TABLE IF EXISTS {};", table_scanned_blocks.inner()), [])?; + sql_transaction.commit()?; + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } } #[async_trait] @@ -917,7 +980,8 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { async fn is_initialized(&self, chain: &Chain) -> MmResult { let table_name = chain.transfer_history_table_name()?; self.call(move |conn| { - let nft_list_initialized = query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [table_name], string_from_row)?; + let nft_list_initialized = + query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [table_name.inner()], string_from_row)?; Ok(nft_list_initialized.is_some()) }) .await @@ -1066,7 +1130,7 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { let table_name = chain.transfer_history_table_name()?; let sql = format!( "SELECT * FROM {} WHERE transaction_hash=?1 AND log_index = ?2", - table_name + table_name.inner() ); self.call(move |conn| { let transfer = query_single_row( @@ -1151,7 +1215,10 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { possible_spam: bool, ) -> MmResult<(), Self::Error> { let table_name = chain.transfer_history_table_name()?; - let sql = format!("UPDATE {} SET possible_spam = ?1 WHERE token_address = ?2;", table_name); + let sql = format!( + "UPDATE {} SET possible_spam = ?1 WHERE token_address = ?2;", + table_name.inner() + ); self.call(move |conn| { let sql_transaction = conn.transaction()?; let params = [Some(i32::from(possible_spam).to_string()), Some(token_address.clone())]; @@ -1177,8 +1244,9 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { } async fn get_domains(&self, chain: &Chain) -> MmResult, Self::Error> { - let table_name = chain.transfer_history_table_name()?; + let safe_table_name = chain.transfer_history_table_name()?; self.call(move |conn| { + let table_name = safe_table_name.inner(); let sql_query = format!( "SELECT DISTINCT token_domain FROM {} UNION SELECT DISTINCT image_domain FROM {}", table_name, table_name @@ -1200,10 +1268,10 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { domain: String, possible_phishing: bool, ) -> MmResult<(), Self::Error> { - let table_name = chain.transfer_history_table_name()?; + let safe_table_name = chain.transfer_history_table_name()?; let sql = format!( "UPDATE {} SET possible_phishing = ?1 WHERE token_domain = ?2 OR image_domain = ?2;", - table_name + safe_table_name.inner() ); self.call(move |conn| { let sql_transaction = conn.transaction()?; @@ -1215,4 +1283,30 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { .await .map_to_mm(AsyncConnError::from) } + + async fn clear_history_data(&self, chain: &Chain) -> MmResult<(), Self::Error> { + let table_name = chain.transfer_history_table_name()?; + self.call(move |conn| { + let sql_transaction = conn.transaction()?; + sql_transaction.execute(&format!("DROP TABLE IF EXISTS {};", table_name.inner()), [])?; + sql_transaction.commit()?; + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } + + async fn clear_all_history_data(&self) -> MmResult<(), Self::Error> { + self.call(move |conn| { + let sql_transaction = conn.transaction()?; + for chain in Chain::variant_list().into_iter() { + let table_name = chain.transfer_history_table_name()?; + sql_transaction.execute(&format!("DROP TABLE IF EXISTS {};", table_name.inner()), [])?; + } + sql_transaction.commit()?; + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } } diff --git a/mm2src/coins/nft/storage/wasm/mod.rs b/mm2src/coins/nft/storage/wasm/mod.rs index ab8f69af68..05c7bac5d9 100644 --- a/mm2src/coins/nft/storage/wasm/mod.rs +++ b/mm2src/coins/nft/storage/wasm/mod.rs @@ -19,6 +19,9 @@ pub enum WasmNftCacheError { NotSupported(String), InternalError(String), GetLastNftBlockError(String), + GetItemError(String), + CursorBuilderError(String), + OpenCursorError(String), } impl From for WasmNftCacheError { diff --git a/mm2src/coins/nft/storage/wasm/wasm_storage.rs b/mm2src/coins/nft/storage/wasm/wasm_storage.rs index faf79f663a..e5ea955918 100644 --- a/mm2src/coins/nft/storage/wasm/wasm_storage.rs +++ b/mm2src/coins/nft/storage/wasm/wasm_storage.rs @@ -44,6 +44,7 @@ where I: Iterator, { let mut filtered_nfts = Vec::new(); + for nft_table in nfts { let nft = nft_details_from_item(nft_table)?; match filters { @@ -82,6 +83,7 @@ where I: Iterator, { let mut filtered_transfers = Vec::new(); + for transfers_table in transfers { let transfer = transfer_details_from_item(transfers_table)?; match filters { @@ -421,6 +423,27 @@ impl NftListStorageOps for NftCacheIDBLocked<'_> { update_nft_phishing_for_index(&table, &chain_str, external_index, &domain, possible_phishing).await?; Ok(()) } + + async fn clear_nft_data(&self, chain: &Chain) -> MmResult<(), Self::Error> { + let db_transaction = self.get_inner().transaction().await?; + let nft_table = db_transaction.table::().await?; + let last_scanned_block_table = db_transaction.table::().await?; + + nft_table.delete_items_by_index("chain", chain.to_string()).await?; + last_scanned_block_table + .delete_item_by_unique_index("chain", chain.to_string()) + .await?; + Ok(()) + } + + async fn clear_all_nft_data(&self) -> MmResult<(), Self::Error> { + let db_transaction = self.get_inner().transaction().await?; + let nft_table = db_transaction.table::().await?; + let last_scanned_block_table = db_transaction.table::().await?; + nft_table.clear().await?; + last_scanned_block_table.clear().await?; + Ok(()) + } } #[async_trait] @@ -481,20 +504,21 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { ) -> MmResult, Self::Error> { let db_transaction = self.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let items = table + let mut cursor_iter = table .cursor_builder() .only("chain", chain.to_string()) - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .map_err(|e| WasmNftCacheError::CursorBuilderError(e.to_string()))? .bound("block_number", BeBigUint::from(from_block), BeBigUint::from(u64::MAX)) .open_cursor(CHAIN_BLOCK_NUMBER_INDEX) .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? - .collect() - .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))?; + .map_err(|e| WasmNftCacheError::OpenCursorError(e.to_string()))?; let mut res = Vec::new(); - for (_item_id, item) in items.into_iter() { + while let Some((_item_id, item)) = cursor_iter + .next() + .await + .map_err(|e| WasmNftCacheError::GetItemError(e.to_string()))? + { let transfer = transfer_details_from_item(item)?; res.push(transfer); } @@ -592,19 +616,20 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { async fn get_transfers_with_empty_meta(&self, chain: Chain) -> MmResult, Self::Error> { let db_transaction = self.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let items = table + let mut cursor_iter = table .cursor_builder() .only("chain", chain.to_string()) - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .map_err(|e| WasmNftCacheError::CursorBuilderError(e.to_string()))? .open_cursor("chain") .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? - .collect() - .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))?; + .map_err(|e| WasmNftCacheError::OpenCursorError(e.to_string()))?; let mut res = HashSet::new(); - for (_item_id, item) in items.into_iter() { + while let Some((_item_id, item)) = cursor_iter + .next() + .await + .map_err(|e| WasmNftCacheError::GetItemError(e.to_string()))? + { if item.token_uri.is_none() && item.collection_name.is_none() && item.image_url.is_none() @@ -682,7 +707,7 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { let table = db_transaction.table::().await?; let items = table.get_items("chain", chain.to_string()).await?; - let mut token_addresses = HashSet::new(); + let mut token_addresses = HashSet::with_capacity(items.len()); for (_item_id, item) in items.into_iter() { let transfer = transfer_details_from_item(item)?; token_addresses.insert(transfer.common.token_address); @@ -722,6 +747,20 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { .await?; Ok(()) } + + async fn clear_history_data(&self, chain: &Chain) -> MmResult<(), Self::Error> { + let db_transaction = self.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + table.delete_items_by_index("chain", chain.to_string()).await?; + Ok(()) + } + + async fn clear_all_history_data(&self) -> MmResult<(), Self::Error> { + let db_transaction = self.get_inner().transaction().await?; + let table = db_transaction.table::().await?; + table.clear().await?; + Ok(()) + } } async fn update_transfer_phishing_for_index( @@ -778,25 +817,26 @@ async fn get_last_block_from_table( table: DbTable<'_, impl TableSignature + BlockNumberTable>, cursor: &str, ) -> MmResult, WasmNftCacheError> { - let items = table + let maybe_item = table .cursor_builder() .only("chain", chain.to_string()) - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? + .map_err(|e| WasmNftCacheError::CursorBuilderError(e.to_string()))? // Sets lower and upper bounds for block_number field .bound("block_number", BeBigUint::from(0u64), BeBigUint::from(u64::MAX)) + // Cursor returns values from the lowest to highest key indexes. + // But we need to get the highest block_number, so reverse the cursor direction. + .reverse() + .where_first() // Opens a cursor by the specified index. // In get_last_block_from_table case it is CHAIN_BLOCK_NUMBER_INDEX, as we need to search block_number for specific chain. - // Cursor returns values from the lowest to highest key indexes. .open_cursor(cursor) .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))? - .collect() + .map_err(|e| WasmNftCacheError::OpenCursorError(e.to_string()))? + .next() .await - .map_err(|e| WasmNftCacheError::GetLastNftBlockError(e.to_string()))?; + .map_err(|e| WasmNftCacheError::GetItemError(e.to_string()))?; - let maybe_item = items - .into_iter() - .last() + let maybe_item = maybe_item .map(|(_item_id, item)| { item.get_block_number() .to_u64() @@ -860,11 +900,11 @@ impl NftListTable { } impl TableSignature for NftListTable { - fn table_name() -> &'static str { "nft_list_cache_table" } + const TABLE_NAME: &'static str = "nft_list_cache_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if is_initial_upgrade(old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index( CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, &["chain", "token_address", "token_id"], @@ -941,11 +981,11 @@ impl NftTransferHistoryTable { } impl TableSignature for NftTransferHistoryTable { - fn table_name() -> &'static str { "nft_transfer_history_cache_table" } + const TABLE_NAME: &'static str = "nft_transfer_history_cache_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if is_initial_upgrade(old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index( CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, &["chain", "token_address", "token_id"], @@ -974,11 +1014,11 @@ pub(crate) struct LastScannedBlockTable { } impl TableSignature for LastScannedBlockTable { - fn table_name() -> &'static str { "last_scanned_block_table" } + const TABLE_NAME: &'static str = "last_scanned_block_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if is_initial_upgrade(old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_index("chain", true)?; } Ok(()) diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 4043d299bb..3ee9e7761b 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1,4 +1,4 @@ -use crate::coin_errors::{MyAddressError, ValidatePaymentError}; +use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::eth::{self, u256_to_big_decimal, wei_from_big_decimal, TryToAddress}; use crate::qrc20::rpc_clients::{LogEntry, Qrc20ElectrumOps, Qrc20NativeOps, Qrc20RpcOps, TopicFilter, TxReceipt, ViewContractCallType}; @@ -13,22 +13,23 @@ use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuildResult, UtxoCoi use crate::utxo::utxo_common::{self, big_decimal_from_sat, check_all_utxo_inputs_signed_by_pub, UtxoTxBuilder}; use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, BroadcastTxErr, FeePolicy, GenerateTxError, GetUtxoListOps, HistoryUtxoTx, HistoryUtxoTxMap, MatureUnspentList, RecentlySpentOutPointsGuard, - UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFromLegacyReqErr, - UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; + UnsupportedAddr, UtxoActivationParams, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, + UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, + UTXO_LOCK}; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, - DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, + DexFee, Eip1559Ops, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, - RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, - SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, - TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, - ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, - WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, - WithdrawRequest, WithdrawResult}; + RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, SwapTxFeePolicy, + TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionData, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, + TransactionResult, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, + ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; @@ -43,7 +44,7 @@ use futures::compat::Future01CompatExt; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use keys::bytes::Bytes as ScriptBytes; -use keys::{Address as UtxoAddress, Address, KeyPair, Public}; +use keys::{Address as UtxoAddress, KeyPair, Public}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; @@ -88,6 +89,7 @@ pub enum Qrc20GenTxError { ErrorSigningTx(UtxoSignWithKeyPairError), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), UnexpectedDerivationMethod(UnexpectedDerivationMethod), + InvalidAddress(String), } impl From for Qrc20GenTxError { @@ -119,6 +121,7 @@ impl Qrc20GenTxError { Qrc20GenTxError::ErrorSigningTx(sign_err) => WithdrawError::InternalError(sign_err.to_string()), Qrc20GenTxError::PrivKeyPolicyNotAllowed(priv_err) => WithdrawError::InternalError(priv_err.to_string()), Qrc20GenTxError::UnexpectedDerivationMethod(addr_err) => WithdrawError::InternalError(addr_err.to_string()), + Qrc20GenTxError::InvalidAddress(addr_err) => WithdrawError::InvalidAddress(addr_err), } } } @@ -518,8 +521,8 @@ impl Qrc20Coin { &self, contract_outputs: Vec, ) -> Result> { - let my_address = self.utxo.derivation_method.single_addr_or_err()?; - let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; + let my_address = self.utxo.derivation_method.single_addr_or_err().await?; + let (unspents, _) = self.get_unspent_ordered_list(&my_address).await?; let mut gas_fee = 0; let mut outputs = Vec::with_capacity(contract_outputs.len()); @@ -529,20 +532,18 @@ impl Qrc20Coin { } let (unsigned, data) = UtxoTxBuilder::new(self) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_gas_fee(gas_fee) .build() .await?; - let my_address = self.utxo.derivation_method.single_addr_or_err()?; let key_pair = self.utxo.priv_key_policy.activated_key_or_err()?; - let prev_script = ScriptBuilder::build_p2pkh(&my_address.hash); let signed = sign_tx( unsigned, key_pair, - prev_script, self.utxo.conf.signature_version, self.utxo.conf.fork_id, )?; @@ -629,21 +630,21 @@ impl UtxoTxGenerationOps for Qrc20Coin { impl GetUtxoListOps for Qrc20Coin { async fn get_unspent_ordered_list( &self, - address: &Address, + address: &UtxoAddress, ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { utxo_common::get_unspent_ordered_list(self, address).await } async fn get_all_unspent_ordered_list( &self, - address: &Address, + address: &UtxoAddress, ) -> UtxoRpcResult<(Vec, RecentlySpentOutPointsGuard<'_>)> { utxo_common::get_all_unspent_ordered_list(self, address).await } async fn get_mature_unspent_ordered_list( &self, - address: &Address, + address: &UtxoAddress, ) -> UtxoRpcResult<(MatureUnspentList, RecentlySpentOutPointsGuard<'_>)> { utxo_common::get_mature_unspent_ordered_list(self, address).await } @@ -670,6 +671,10 @@ impl UtxoCommonOps for Qrc20Coin { utxo_common::checked_address_from_str(self, address) } + fn script_for_address(&self, address: &UtxoAddress) -> MmResult { + utxo_common::output_script_checked(self.as_ref(), address) + } + async fn get_current_mtp(&self) -> UtxoRpcResult { utxo_common::get_current_mtp(&self.utxo, CoinVariant::Qtum).await } @@ -739,12 +744,11 @@ impl UtxoCommonOps for Qrc20Coin { utxo_common::addr_format_for_standard_scripts(self) } - fn address_from_pubkey(&self, pubkey: &Public) -> Address { + fn address_from_pubkey(&self, pubkey: &Public) -> UtxoAddress { let conf = &self.utxo.conf; utxo_common::address_from_pubkey( pubkey, - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + conf.address_prefixes.clone(), conf.checksum_type, conf.bech32_hrp.clone(), self.addr_format().clone(), @@ -754,7 +758,7 @@ impl UtxoCommonOps for Qrc20Coin { #[async_trait] impl SwapOps for Qrc20Coin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let to_address = try_tx_fus!(self.contract_address_from_raw_pubkey(fee_addr)); let amount = try_tx_fus!(wei_from_big_decimal(&dex_fee.fee_amount().into(), self.utxo.decimals)); let transfer_output = @@ -803,35 +807,31 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { let payment_tx: UtxoTx = - try_tx_fus!(deserialize(maker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_tx_fus!(maker_spends_payment_args.swap_contract_address.try_to_address()); + try_tx_s!(deserialize(maker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_s!(maker_spends_payment_args.swap_contract_address.try_to_address()); let secret = maker_spends_payment_args.secret.to_vec(); - let selfi = self.clone(); - let fut = async move { - selfi - .spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) - .await - }; - Box::new(fut.boxed().compat()) + self.spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) + .await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { let payment_tx: UtxoTx = - try_tx_fus!(deserialize(taker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); + try_tx_s!(deserialize(taker_spends_payment_args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); let secret = taker_spends_payment_args.secret.to_vec(); - let swap_contract_address = try_tx_fus!(taker_spends_payment_args.swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_s!(taker_spends_payment_args.swap_contract_address.try_to_address()); - let selfi = self.clone(); - let fut = async move { - selfi - .spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) - .await - }; - Box::new(fut.boxed().compat()) + self.spend_hash_time_locked_payment(payment_tx, swap_contract_address, secret) + .await } #[inline] @@ -891,64 +891,55 @@ impl SwapOps for Qrc20Coin { } #[inline] - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - let payment_tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); - let sender = try_f!(self + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + let payment_tx: UtxoTx = deserialize(input.payment_tx.as_slice())?; + let sender = self .contract_address_from_raw_pubkey(&input.other_pub) - .map_to_mm(ValidatePaymentError::InvalidParameter)); - let swap_contract_address = try_f!(input + .map_to_mm(ValidatePaymentError::InvalidParameter)?; + let swap_contract_address = input .swap_contract_address .try_to_address() - .map_to_mm(ValidatePaymentError::InvalidParameter)); + .map_to_mm(ValidatePaymentError::InvalidParameter)?; - let time_lock = try_f!(input + let time_lock = input .time_lock .try_into() - .map_to_mm(ValidatePaymentError::TimelockOverflow)); - let selfi = self.clone(); - let fut = async move { - selfi - .validate_payment( - payment_tx, - time_lock, - sender, - input.secret_hash, - input.amount, - swap_contract_address, - ) - .await - }; - Box::new(fut.boxed().compat()) + .map_to_mm(ValidatePaymentError::TimelockOverflow)?; + self.validate_payment( + payment_tx, + time_lock, + sender, + input.secret_hash, + input.amount, + swap_contract_address, + ) + .await } #[inline] - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - let swap_contract_address = try_f!(input + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + let swap_contract_address = input .swap_contract_address .try_to_address() - .map_to_mm(ValidatePaymentError::InvalidParameter)); - let payment_tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); - let sender = try_f!(self + .map_to_mm(ValidatePaymentError::InvalidParameter)?; + let payment_tx: UtxoTx = deserialize(input.payment_tx.as_slice())?; + let sender = self .contract_address_from_raw_pubkey(&input.other_pub) - .map_to_mm(ValidatePaymentError::InvalidParameter)); - let time_lock = try_f!(input + .map_to_mm(ValidatePaymentError::InvalidParameter)?; + let time_lock = input .time_lock .try_into() - .map_to_mm(ValidatePaymentError::TimelockOverflow)); - let selfi = self.clone(); - let fut = async move { - selfi - .validate_payment( - payment_tx, - time_lock, - sender, - input.secret_hash, - input.amount, - swap_contract_address, - ) - .await - }; - Box::new(fut.boxed().compat()) + .map_to_mm(ValidatePaymentError::TimelockOverflow)?; + + self.validate_payment( + payment_tx, + time_lock, + sender, + input.secret_hash, + input.amount, + swap_contract_address, + ) + .await } #[inline] @@ -1183,12 +1174,13 @@ impl WatcherOps for Qrc20Coin { } } +#[async_trait] impl MarketCoinOps for Qrc20Coin { fn ticker(&self) -> &str { &self.utxo.conf.ticker } fn my_address(&self) -> MmResult { utxo_common::my_address(self) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.as_ref())?; Ok(pubkey.to_string()) } @@ -1212,6 +1204,7 @@ impl MarketCoinOps for Qrc20Coin { let fut = async move { let my_address = coin .my_addr_as_contract_addr() + .await .mm_err(|e| BalanceError::Internal(e.to_string()))?; let params = [Token::Address(my_address)]; let contract_address = coin.contract_address; @@ -1235,7 +1228,6 @@ impl MarketCoinOps for Qrc20Coin { }; Box::new(fut.boxed().compat()) } - fn base_coin_balance(&self) -> BalanceFut { // use standard UTXO my_balance implementation that returns Qtum balance instead of QRC20 Box::new(utxo_common::my_balance(self.clone()).map(|CoinBalance { spendable, .. }| spendable)) @@ -1253,6 +1245,11 @@ impl MarketCoinOps for Qrc20Coin { utxo_common::send_raw_tx_bytes(&self.utxo, tx) } + #[inline(always)] + async fn sign_raw_tx(&self, args: &SignRawTransactionRequest) -> RawTransactionResult { + utxo_common::sign_raw_tx(self, args).await + } + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { let tx: UtxoTx = try_fus!(deserialize(input.payment_tx.as_slice()).map_err(|e| ERRL!("{:?}", e))); let selfi = self.clone(); @@ -1307,6 +1304,8 @@ impl MarketCoinOps for Qrc20Coin { let pow = self.utxo.decimals as u32; MmNumber::from(1) / MmNumber::from(10u64.pow(pow)) } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -1362,6 +1361,7 @@ impl MmCoin for Qrc20Coin { &self, value: TradePreimageValue, stage: FeeApproxStage, + include_refund_fee: bool, ) -> TradePreimageResult { let decimals = self.utxo.decimals; // pass the dummy params @@ -1393,14 +1393,18 @@ impl MmCoin for Qrc20Coin { .await? }; - let sender_refund_fee = { + // Optionally calculate refund fee. + let sender_refund_fee = if include_refund_fee { let sender_refund_output = self.sender_refund_output(&self.swap_contract_address, swap_id, value, secret_hash, receiver_addr)?; self.preimage_trade_fee_required_to_send_outputs(vec![sender_refund_output], &stage) .await? + } else { + BigDecimal::from(0) // No refund fee if not included. }; let total_fee = erc20_payment_fee + sender_refund_fee; + Ok(TradeFee { coin: self.platform.clone(), amount: total_fee.into(), @@ -1499,8 +1503,10 @@ impl MmCoin for Qrc20Coin { } pub fn qrc20_swap_id(time_lock: u32, secret_hash: &[u8]) -> Vec { - let mut input = vec![]; - input.extend_from_slice(&time_lock.to_le_bytes()); + let timelock_bytes = time_lock.to_le_bytes(); + let mut input = Vec::with_capacity(timelock_bytes.len() + secret_hash.len()); + + input.extend_from_slice(&timelock_bytes); input.extend_from_slice(secret_hash); sha256(&input).to_vec() } @@ -1522,12 +1528,10 @@ pub struct Qrc20FeeDetails { } async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult { - let to_addr = UtxoAddress::from_str(&req.to) - .map_err(|e| e.to_string()) + let to_addr = UtxoAddress::from_legacyaddress(&req.to, &coin.as_ref().conf.address_prefixes) .map_to_mm(WithdrawError::InvalidAddress)?; let conf = &coin.utxo.conf; - let is_p2pkh = to_addr.prefix == conf.pub_addr_prefix && to_addr.t_addr_prefix == conf.pub_t_addr_prefix; - if !is_p2pkh { + if !to_addr.is_pubkey_hash() { let error = "QRC20 can be sent to P2PKH addresses only".to_owned(); return MmError::err(WithdrawError::InvalidAddress(error)); } @@ -1578,8 +1582,8 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult .await .mm_err(|gen_tx_error| gen_tx_error.into_withdraw_error(coin.platform.clone(), coin.utxo.decimals))?; - let my_address = coin.utxo.derivation_method.single_addr_or_err()?; - let received_by_me = if to_addr == *my_address { + let my_address = coin.utxo.derivation_method.single_addr_or_err().await?; + let received_by_me = if to_addr == my_address { qrc20_amount.clone() } else { 0.into() @@ -1605,8 +1609,10 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult spent_by_me: qrc20_amount, received_by_me, my_balance_change, - tx_hash: signed.hash().reversed().to_vec().to_tx_hash(), - tx_hex: serialize(&signed).into(), + tx: TransactionData::new_signed( + serialize(&signed).into(), + signed.hash().reversed().to_vec().to_tx_hash(), + ), fee_details: Some(fee_details.into()), block_height: 0, coin: conf.ticker.clone(), @@ -1673,3 +1679,9 @@ fn transfer_event_from_log(log: &LogEntry) -> Result SwapTxFeePolicy { SwapTxFeePolicy::Unsupported } + + fn set_swap_transaction_fee_policy(&self, _swap_txfee_policy: SwapTxFeePolicy) {} +} diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index b3ae9e7655..af3c41f078 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -194,7 +194,10 @@ impl Qrc20Coin { let mut input_transactions = HistoryUtxoTxMap::new(); let qtum_details = try_s!(utxo_common::tx_details_by_hash(self, &tx_hash.0, &mut input_transactions).await); // Deserialize the UtxoTx to get a script pubkey - let qtum_tx: UtxoTx = try_s!(deserialize(qtum_details.tx_hex.as_slice()).map_err(|e| ERRL!("{:?}", e))); + let qtum_tx: UtxoTx = try_s!(deserialize( + try_s!(qtum_details.tx.tx_hex().ok_or("unexpected tx type")).as_slice() + ) + .map_err(|e| ERRL!("{:?}", e))); let miner_fee = { let total_qtum_fee = match qtum_details.fee_details { @@ -209,23 +212,29 @@ impl Qrc20Coin { let mut details = TxTransferMap::new(); for receipt in receipts { - let log_details = - try_s!(self.transfer_details_from_receipt(&qtum_tx, &qtum_details, receipt, miner_fee.clone())); + let log_details = try_s!( + self.transfer_details_from_receipt(&qtum_tx, &qtum_details, receipt, miner_fee.clone()) + .await + ); details.extend(log_details.into_iter()) } Ok(details) } - fn transfer_details_from_receipt( + async fn transfer_details_from_receipt( &self, qtum_tx: &UtxoTx, qtum_details: &TransactionDetails, receipt: TxReceipt, miner_fee: BigDecimal, ) -> Result { - let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err()); - let tx_hash: H256Json = try_s!(H256Json::from_str(&qtum_details.tx_hash)); + let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err().await); + let tx_hash: H256Json = try_s!(H256Json::from_str(try_s!(qtum_details + .tx + .tx_hash() + .ok_or("unexpected tx type")))); + if qtum_tx.outputs.len() <= (receipt.output_index as usize) { return ERR!( "Length of the transaction {:?} outputs less than output_index {}", @@ -280,17 +289,17 @@ impl Qrc20Coin { }; // https://github.com/qtumproject/qtum-electrum/blob/v4.0.2/electrum/wallet.py#L2102 - if from != *my_address && to != *my_address { + if from != my_address && to != my_address { // address mismatch continue; } - let spent_by_me = if from == *my_address { + let spent_by_me = if from == my_address { total_amount.clone() } else { 0.into() }; - let received_by_me = if to == *my_address { + let received_by_me = if to == my_address { total_amount.clone() } else { 0.into() @@ -604,16 +613,16 @@ impl TransferHistoryBuilder { } pub async fn build(self) -> Result, MmError> { - let params = self.build_params()?; + let params = self.build_params().await?; self.coin.utxo.rpc_client.build(params).await } pub async fn build_tx_idents(self) -> Result, MmError> { - let params = self.build_params()?; + let params = self.build_params().await?; self.coin.utxo.rpc_client.build_tx_idents(params).await } - fn build_params(&self) -> Result> { + async fn build_params(&self) -> Result> { let address = match self.address { Some(addr) => addr, None => { @@ -622,9 +631,9 @@ impl TransferHistoryBuilder { .utxo .derivation_method .single_addr_or_err() + .await .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; - qtum::contract_addr_from_utxo_addr(my_address.clone()) - .mm_err(|e| UtxoRpcError::Internal(e.to_string()))? + qtum::contract_addr_from_utxo_addr(my_address).mm_err(|e| UtxoRpcError::Internal(e.to_string()))? }, }; diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index a837e18364..2caf87c3bf 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -1,17 +1,23 @@ use super::*; -use crate::utxo::rpc_clients::UnspentInfo; use crate::{DexFee, TxFeeDetails, WaitForHTLCTxSpendArgs}; -use chain::OutPoint; use common::{block_on, wait_until_sec, DEX_FEE_ADDR_RAW_PUBKEY}; use crypto::Secp256k1Secret; use itertools::Itertools; +use keys::Address; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_number::bigdecimal::Zero; -use mocktopus::mocking::{MockResult, Mockable}; use rpc::v1::types::ToTxHash; use std::convert::TryFrom; use std::mem::discriminant; +cfg_native!( + use crate::utxo::rpc_clients::UnspentInfo; + + use chain::OutPoint; + use keys::AddressBuilder; + use mocktopus::mocking::{MockResult, Mockable}; +); + const EXPECTED_TX_FEE: i64 = 1000; const CONTRACT_CALL_GAS_FEE: i64 = (QRC20_GAS_LIMIT_DEFAULT * QRC20_GAS_PRICE_DEFAULT) as i64; const SWAP_PAYMENT_GAS_FEE: i64 = (QRC20_PAYMENT_GAS_LIMIT * QRC20_GAS_PRICE_DEFAULT) as i64; @@ -57,6 +63,7 @@ fn check_tx_fee(coin: &Qrc20Coin, expected_tx_fee: ActualTxFee) { assert_eq!(actual_tx_fee, expected_tx_fee); } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_to_p2sh_address_should_fail() { let priv_key = [ @@ -65,14 +72,19 @@ fn test_withdraw_to_p2sh_address_should_fail() { ]; let (_, coin) = qrc20_coin_for_test(priv_key, None); - let p2sh_address = Address { - prefix: coin.as_ref().conf.p2sh_addr_prefix, - hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let p2sh_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh( + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + ) + .build() + .expect("valid address props"); let req = WithdrawRequest { amount: 10.into(), @@ -82,14 +94,23 @@ fn test_withdraw_to_p2sh_address_should_fail() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let err = coin.withdraw(req).wait().unwrap_err().into_inner(); let expect = WithdrawError::InvalidAddress("QRC20 can be sent to P2PKH addresses only".to_owned()); assert_eq!(err, expect); } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_fee_details() { + // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG + let priv_key = [ + 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, + 172, 110, 180, 13, 123, 179, 10, 49, + ]; + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); + Qrc20Coin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { @@ -99,17 +120,13 @@ fn test_withdraw_impl_fee_details() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); - let withdraw_req = WithdrawRequest { amount: 10.into(), from: None, @@ -121,6 +138,7 @@ fn test_withdraw_impl_fee_details() { gas_price: 40, }), memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); @@ -149,8 +167,12 @@ fn test_validate_maker_payment() { let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); assert_eq!( - *coin.utxo.derivation_method.unwrap_single_addr(), - "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into() + block_on(coin.utxo.derivation_method.unwrap_single_addr()), + Address::from_legacyaddress( + "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf", + &coin.as_ref().conf.address_prefixes + ) + .unwrap() ); // tx_hash: 016a59dd2b181b3906b0f0333d5c7561dacb332dc99ac39679a591e523f2c49a @@ -173,12 +195,10 @@ fn test_validate_maker_payment() { watcher_reward: None, }; - coin.validate_maker_payment(input.clone()).wait().unwrap(); + block_on(coin.validate_maker_payment(input.clone())).unwrap(); input.other_pub = hex::decode("022b00078841f37b5d30a6a1defb82b3af4d4e2d24dd4204d41f0c9ce1e875de1a").unwrap(); - let error = coin - .validate_maker_payment(input.clone()) - .wait() + let error = block_on(coin.validate_maker_payment(input.clone())) .unwrap_err() .into_inner(); log!("error: {:?}", error); @@ -189,9 +209,7 @@ fn test_validate_maker_payment() { input.other_pub = correct_maker_pub; input.amount = BigDecimal::from_str("0.3").unwrap(); - let error = coin - .validate_maker_payment(input.clone()) - .wait() + let error = block_on(coin.validate_maker_payment(input.clone())) .unwrap_err() .into_inner(); log!("error: {:?}", error); @@ -207,9 +225,7 @@ fn test_validate_maker_payment() { input.amount = correct_amount; input.secret_hash = vec![2; 20]; - let error = coin - .validate_maker_payment(input.clone()) - .wait() + let error = block_on(coin.validate_maker_payment(input.clone())) .unwrap_err() .into_inner(); log!("error: {:?}", error); @@ -225,7 +241,7 @@ fn test_validate_maker_payment() { input.secret_hash = vec![1; 20]; input.time_lock = 123; - let error = coin.validate_maker_payment(input).wait().unwrap_err().into_inner(); + let error = block_on(coin.validate_maker_payment(input)).unwrap_err().into_inner(); log!("error: {:?}", error); match error { ValidatePaymentError::UnexpectedPaymentState(err) => { @@ -248,8 +264,12 @@ fn test_wait_for_confirmations_excepted() { let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); assert_eq!( - *coin.utxo.derivation_method.unwrap_single_addr(), - "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf".into() + block_on(coin.utxo.derivation_method.unwrap_single_addr()), + Address::from_legacyaddress( + "qUX9FGHubczidVjWPCUWuwCUJWpkAtGCgf", + &coin.as_ref().conf.address_prefixes + ) + .unwrap() ); // tx_hash: 35e03bc529528a853ee75dde28f27eec8ed7b152b6af7ab6dfa5d55ea46f25ac @@ -298,39 +318,6 @@ fn test_wait_for_confirmations_excepted() { assert!(error.contains("Contract call failed with an error: Revert")); } -#[test] -fn test_send_taker_fee() { - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); - - let amount = BigDecimal::from_str("0.01").unwrap(); - let tx = coin - .send_taker_fee(&DEX_FEE_ADDR_RAW_PUBKEY, DexFee::Standard(amount.clone().into()), &[]) - .wait() - .unwrap(); - let tx_hash: H256Json = match tx { - TransactionEnum::UtxoTx(ref tx) => tx.hash().reversed().into(), - _ => panic!("Expected UtxoTx"), - }; - log!("Fee tx {:?}", tx_hash); - - let result = coin - .validate_fee(ValidateFeeArgs { - fee_tx: &tx, - expected_sender: coin.my_public_key().unwrap(), - fee_addr: &DEX_FEE_ADDR_RAW_PUBKEY, - dex_fee: &DexFee::Standard(amount.into()), - min_block_number: 0, - uuid: &[], - }) - .wait(); - assert!(result.is_ok()); -} - #[test] fn test_validate_fee() { // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG @@ -557,7 +544,11 @@ fn test_generate_token_transfer_script_pubkey() { gas_price, }; - let to_addr: UtxoAddress = "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(); + let to_addr: UtxoAddress = UtxoAddress::from_legacyaddress( + "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); let to_addr = qtum::contract_addr_from_utxo_addr(to_addr).unwrap(); let amount: U256 = 1000000000.into(); let actual = coin.transfer_output(to_addr, amount, gas_limit, gas_price).unwrap(); @@ -609,8 +600,7 @@ fn test_transfer_details_by_hash() { // qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8 is UTXO representation of 1549128bbfb33b997949b4105b6a6371c998e212 contract address let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], to: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], total_amount: BigDecimal::from_str("0.003").unwrap(), @@ -634,8 +624,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00295").unwrap(), @@ -659,8 +648,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], to: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], total_amount: BigDecimal::from_str("0.003").unwrap(), @@ -684,8 +672,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00295").unwrap(), @@ -709,8 +696,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex, - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex, tx_hash_bytes.to_tx_hash()), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00005000").unwrap(), @@ -782,7 +768,7 @@ fn test_sender_trade_preimage_zero_allowance() { let sender_refund_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); let actual = - block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(1.into()), FeeApproxStage::WithoutApprox)) + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(1.into()), FeeApproxStage::WithoutApprox, true)) .expect("!get_sender_trade_fee"); // one `approve` contract call should be included into the expected trade fee let expected = TradeFee { @@ -822,6 +808,7 @@ fn test_sender_trade_preimage_with_allowance() { let actual = block_on(coin.get_sender_trade_fee( TradePreimageValue::Exact(BigDecimal::try_from(2.5).unwrap()), FeeApproxStage::WithoutApprox, + true, )) .expect("!get_sender_trade_fee"); // the expected fee should not include any `approve` contract call @@ -835,6 +822,7 @@ fn test_sender_trade_preimage_with_allowance() { let actual = block_on(coin.get_sender_trade_fee( TradePreimageValue::Exact(BigDecimal::try_from(3.5).unwrap()), FeeApproxStage::WithoutApprox, + true, )) .expect("!get_sender_trade_fee"); // two `approve` contract calls should be included into the expected trade fee @@ -890,10 +878,11 @@ fn test_get_sender_trade_fee_preimage_for_correct_ticker() { )) .unwrap(); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(0.into()), FeeApproxStage::OrderIssue)) - .err() - .unwrap() - .into_inner(); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(0.into()), FeeApproxStage::OrderIssue, true)) + .err() + .unwrap() + .into_inner(); // expecting TradePreimageError::NotSufficientBalance let expected = TradePreimageError::NotSufficientBalance { coin: "tQTUM".to_string(), @@ -1076,9 +1065,7 @@ fn test_validate_maker_payment_malicious() { unique_swap_data: Vec::new(), watcher_reward: None, }; - let error = coin - .validate_maker_payment(input) - .wait() + let error = block_on(coin.validate_maker_payment(input)) .expect_err("'erc20Payment' was called from another swap contract, expected an error") .into_inner(); log!("error: {}", error); diff --git a/mm2src/coins/qrc20/script_pubkey.rs b/mm2src/coins/qrc20/script_pubkey.rs index 08abad3024..ec85412d01 100644 --- a/mm2src/coins/qrc20/script_pubkey.rs +++ b/mm2src/coins/qrc20/script_pubkey.rs @@ -25,10 +25,10 @@ pub fn generate_contract_call_script_pubkey( Ok(ScriptBuilder::default() .push_opcode(Opcode::OP_4) - .push_bytes(&gas_limit) - .push_bytes(&gas_price) + .push_data(&gas_limit) + .push_data(&gas_price) .push_data(function_call) - .push_bytes(contract_address) + .push_data(contract_address) .push_opcode(Opcode::OP_CALL) .into_script()) } @@ -193,6 +193,8 @@ fn decode_contract_number(source: &[u8]) -> Result { #[cfg(test)] mod tests { + use keys::prefixes::QRC20_PREFIXES; + use super::*; #[test] @@ -246,7 +248,8 @@ mod tests { fn test_extract_contract_call() { let script: Script = "5403a02526012844a9059cbb0000000000000000000000000240b898276ad2cc0d2fe6f527e8e31104e7fde3000000000000000000000000000000000000000000000000000000003b9aca0014d362e096e873eb7907e205fadc6175c6fec7bc44c2".into(); - let to_addr: UtxoAddress = "qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs".into(); + let to_addr: UtxoAddress = + UtxoAddress::from_legacyaddress("qHmJ3KA6ZAjR9wGjpFASn4gtUSeFAqdZgs", &QRC20_PREFIXES).unwrap(); let to_addr = qtum::contract_addr_from_utxo_addr(to_addr).unwrap(); let amount: U256 = 1000000000.into(); let function = eth::ERC20_CONTRACT.function("transfer").unwrap(); diff --git a/mm2src/coins/qrc20/swap.rs b/mm2src/coins/qrc20/swap.rs index a560d37057..a6162f6421 100644 --- a/mm2src/coins/qrc20/swap.rs +++ b/mm2src/coins/qrc20/swap.rs @@ -136,7 +136,7 @@ impl Qrc20Coin { let expected_call_bytes = { let expected_value = wei_from_big_decimal(&amount, self.utxo.decimals)?; - let my_address = self.utxo.derivation_method.single_addr_or_err()?.clone(); + let my_address = self.utxo.derivation_method.single_addr_or_err().await?; let expected_receiver = qtum::contract_addr_from_utxo_addr(my_address) .mm_err(|err| ValidatePaymentError::InternalError(err.to_string()))?; self.erc20_payment_call_bytes( @@ -264,7 +264,7 @@ impl Qrc20Coin { } // Else try to find a 'senderRefund' contract call. - let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err()).clone(); + let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err().await); let sender = try_s!(qtum::contract_addr_from_utxo_addr(my_address)); let refund_txs = try_s!(self.sender_refund_transactions(sender, search_from_block).await); let found = refund_txs.into_iter().find(|tx| { @@ -288,7 +288,7 @@ impl Qrc20Coin { return Ok(None); }; - let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err()).clone(); + let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err().await); let sender = try_s!(qtum::contract_addr_from_utxo_addr(my_address)); let erc20_payment_txs = try_s!(self.erc20_payment_transactions(sender, search_from_block).await); let found = erc20_payment_txs @@ -461,6 +461,7 @@ impl Qrc20Coin { .utxo .derivation_method .single_addr_or_err() + .await .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; let tokens = self .utxo diff --git a/mm2src/coins/rpc_command/account_balance.rs b/mm2src/coins/rpc_command/account_balance.rs index 4c29383d1e..c0cb1bf09b 100644 --- a/mm2src/coins/rpc_command/account_balance.rs +++ b/mm2src/coins/rpc_command/account_balance.rs @@ -1,13 +1,11 @@ use crate::coin_balance::HDAddressBalance; -use crate::hd_wallet::HDWalletCoinOps; use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; -use crate::{lp_coinfind_or_err, CoinBalance, CoinWithDerivationMethod, MmCoinEnum}; +use crate::{lp_coinfind_or_err, CoinBalance, CoinBalanceMap, CoinWithDerivationMethod, MmCoinEnum}; use async_trait::async_trait; use common::PagingOptionsEnum; use crypto::{Bip44Chain, RpcDerivationPath}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; -use std::fmt; #[derive(Deserialize)] pub struct HDAccountBalanceRequest { @@ -27,11 +25,12 @@ pub struct AccountBalanceParams { } #[derive(Debug, PartialEq, Serialize)] -pub struct HDAccountBalanceResponse { +pub struct HDAccountBalanceResponse { pub account_index: u32, pub derivation_path: RpcDerivationPath, - pub addresses: Vec, - pub page_balance: CoinBalance, + pub addresses: Vec>, + // Todo: Add option to get total balance of all addresses in addition to page_balance + pub page_balance: BalanceObject, pub limit: usize, pub skipped: u32, pub total: u32, @@ -39,38 +38,57 @@ pub struct HDAccountBalanceResponse { pub paging_options: PagingOptionsEnum, } +/// Enum for the response of the `account_balance` RPC command. +#[derive(Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum HDAccountBalanceResponseEnum { + Single(HDAccountBalanceResponse), + Map(HDAccountBalanceResponse), +} + +/// Trait for the `account_balance` RPC command. #[async_trait] pub trait AccountBalanceRpcOps { + type BalanceObject; + async fn account_balance_rpc( &self, params: AccountBalanceParams, - ) -> MmResult; + ) -> MmResult, HDAccountBalanceRpcError>; } +/// `account_balance` RPC command implementation. pub async fn account_balance( ctx: MmArc, req: HDAccountBalanceRequest, -) -> MmResult { +) -> MmResult { match lp_coinfind_or_err(&ctx, &req.coin).await? { - MmCoinEnum::UtxoCoin(utxo) => utxo.account_balance_rpc(req.params).await, - MmCoinEnum::QtumCoin(qtum) => qtum.account_balance_rpc(req.params).await, + MmCoinEnum::UtxoCoin(utxo) => Ok(HDAccountBalanceResponseEnum::Single( + utxo.account_balance_rpc(req.params).await?, + )), + MmCoinEnum::QtumCoin(qtum) => Ok(HDAccountBalanceResponseEnum::Single( + qtum.account_balance_rpc(req.params).await?, + )), + MmCoinEnum::EthCoin(eth) => Ok(HDAccountBalanceResponseEnum::Map( + eth.account_balance_rpc(req.params).await?, + )), _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), } } pub mod common_impl { use super::*; - use crate::coin_balance::HDWalletBalanceOps; + use crate::coin_balance::{BalanceObjectOps, HDWalletBalanceObject, HDWalletBalanceOps}; use crate::hd_wallet::{HDAccountOps, HDWalletOps}; use common::calc_total_pages; + /// Common implementation for the `account_balance` RPC command. pub async fn account_balance_rpc( coin: &Coin, params: AccountBalanceParams, - ) -> MmResult + ) -> MmResult>, HDAccountBalanceRpcError> where - Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, - ::Address: fmt::Display + Clone, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Sync, { let account_id = params.account_index; let hd_account = coin @@ -90,9 +108,13 @@ pub mod common_impl { let addresses = coin .known_addresses_balances_with_ids(&hd_account, params.chain, from_address_id..to_address_id) .await?; - let page_balance = addresses.iter().fold(CoinBalance::default(), |total, addr_balance| { - total + addr_balance.balance.clone() - }); + + let page_balance = addresses + .iter() + .fold(HDWalletBalanceObject::::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total + }); let result = HDAccountBalanceResponse { account_index: account_id, diff --git a/mm2src/coins/rpc_command/get_estimated_fees.rs b/mm2src/coins/rpc_command/get_estimated_fees.rs new file mode 100644 index 0000000000..b62e572756 --- /dev/null +++ b/mm2src/coins/rpc_command/get_estimated_fees.rs @@ -0,0 +1,331 @@ +//! RPCs to start/stop gas fee estimator and get estimated base and priority fee per gas + +use crate::eth::{EthCoin, EthCoinType, FeeEstimatorContext, FeeEstimatorState, FeePerGasEstimated}; +use crate::{lp_coinfind_or_err, wei_to_gwei_decimal, AsyncMutex, CoinFindError, MmCoinEnum, NumConversError}; +use common::executor::{spawn_abortable, Timer}; +use common::log::debug; +use common::{HttpStatusCode, StatusCode}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; +use serde::{Deserialize, Serialize}; +use serde_json::{self as json, Value as Json}; +use std::convert::{TryFrom, TryInto}; +use std::ops::Deref; +use std::sync::Arc; + +const FEE_ESTIMATOR_NAME: &str = "eth_gas_fee_estimator_loop"; + +/// Estimated fee per gas units +#[derive(Clone, Debug, Serialize)] +pub enum EstimationUnits { + Gwei, +} + +impl Default for EstimationUnits { + fn default() -> Self { Self::Gwei } +} + +/// Priority level estimated max fee per gas +#[derive(Clone, Debug, Default, Serialize)] +pub struct FeePerGasLevel { + /// estimated max priority tip fee per gas in gwei + pub max_priority_fee_per_gas: BigDecimal, + /// estimated max fee per gas in gwei + pub max_fee_per_gas: BigDecimal, + /// estimated transaction min wait time in mempool in ms for this priority level + pub min_wait_time: Option, + /// estimated transaction max wait time in mempool in ms for this priority level + pub max_wait_time: Option, +} + +/// External struct for estimated fee per gas for several priority levels, in gwei +/// low/medium/high levels are supported +#[derive(Default, Debug, Clone, Serialize)] +pub struct FeePerGasEstimatedExt { + /// base fee for the next block in gwei + pub base_fee: BigDecimal, + /// estimated low priority fee + pub low: FeePerGasLevel, + /// estimated medium priority fee + pub medium: FeePerGasLevel, + /// estimated high priority fee + pub high: FeePerGasLevel, + /// which estimator used + pub source: String, + /// base trend (up or down) + pub base_fee_trend: String, + /// priority trend (up or down) + pub priority_fee_trend: String, + /// fee units + pub units: EstimationUnits, +} + +impl TryFrom for FeePerGasEstimatedExt { + type Error = MmError; + + fn try_from(fees: FeePerGasEstimated) -> Result { + Ok(Self { + base_fee: wei_to_gwei_decimal!(fees.base_fee)?, + low: FeePerGasLevel { + max_fee_per_gas: wei_to_gwei_decimal!(fees.low.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.low.max_priority_fee_per_gas)?, + min_wait_time: fees.low.min_wait_time, + max_wait_time: fees.low.max_wait_time, + }, + medium: FeePerGasLevel { + max_fee_per_gas: wei_to_gwei_decimal!(fees.medium.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.medium.max_priority_fee_per_gas)?, + min_wait_time: fees.medium.min_wait_time, + max_wait_time: fees.medium.max_wait_time, + }, + high: FeePerGasLevel { + max_fee_per_gas: wei_to_gwei_decimal!(fees.high.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.high.max_priority_fee_per_gas)?, + min_wait_time: fees.high.min_wait_time, + max_wait_time: fees.high.max_wait_time, + }, + source: fees.source.to_string(), + base_fee_trend: fees.base_fee_trend, + priority_fee_trend: fees.priority_fee_trend, + units: EstimationUnits::Gwei, + }) + } +} + +#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum FeeEstimatorError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Gas fee estimation not supported for this coin")] + CoinNotSupported, + #[display(fmt = "Platform coin needs to be enabled for gas fee estimation")] + PlatformCoinRequired, + #[display(fmt = "Gas fee estimator is already started")] + AlreadyStarted, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Gas fee estimator is not running")] + NotRunning, + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for FeeEstimatorError { + fn status_code(&self) -> StatusCode { + match self { + FeeEstimatorError::NoSuchCoin { .. } + | FeeEstimatorError::CoinNotSupported + | FeeEstimatorError::PlatformCoinRequired + | FeeEstimatorError::AlreadyStarted + | FeeEstimatorError::NotRunning => StatusCode::BAD_REQUEST, + FeeEstimatorError::Transport(_) | FeeEstimatorError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for FeeEstimatorError { + fn from(e: NumConversError) -> Self { FeeEstimatorError::InternalError(e.to_string()) } +} + +impl From for FeeEstimatorError { + fn from(e: String) -> Self { FeeEstimatorError::InternalError(e) } +} + +impl From for FeeEstimatorError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => FeeEstimatorError::NoSuchCoin { coin }, + } + } +} + +/// Gas fee estimator configuration +#[derive(Deserialize)] +enum FeeEstimatorConf { + NotConfigured, + #[serde(rename = "simple")] + Simple, + #[serde(rename = "provider")] + Provider, +} + +impl Default for FeeEstimatorConf { + fn default() -> Self { Self::NotConfigured } +} + +impl FeeEstimatorState { + /// Creates gas FeeEstimatorContext if configured for this coin and chain id, otherwise returns None. + /// The created context object (or None) is wrapped into a FeeEstimatorState so a gas fee rpc caller may know the reason why it was not created + pub(crate) async fn init_fee_estimator( + ctx: &MmArc, + conf: &Json, + coin_type: &EthCoinType, + ) -> Result, String> { + let fee_estimator_json = conf["gas_fee_estimator"].clone(); + let fee_estimator_conf: FeeEstimatorConf = if !fee_estimator_json.is_null() { + try_s!(json::from_value(fee_estimator_json)) + } else { + Default::default() + }; + match (fee_estimator_conf, coin_type) { + (FeeEstimatorConf::Simple, EthCoinType::Eth) => { + let fee_estimator_state = FeeEstimatorState::Simple(FeeEstimatorContext::new()); + Ok(Arc::new(fee_estimator_state)) + }, + (FeeEstimatorConf::Provider, EthCoinType::Eth) => { + let fee_estimator_state = FeeEstimatorState::Provider(FeeEstimatorContext::new()); + Ok(Arc::new(fee_estimator_state)) + }, + (_, EthCoinType::Erc20 { platform, .. }) | (_, EthCoinType::Nft { platform, .. }) => { + let platform_coin = lp_coinfind_or_err(ctx, platform).await; + match platform_coin { + Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin.platform_fee_estimator_state.clone()), + _ => Ok(Arc::new(FeeEstimatorState::PlatformCoinRequired)), + } + }, + (FeeEstimatorConf::NotConfigured, _) => Ok(Arc::new(FeeEstimatorState::CoinNotSupported)), + } + } +} + +impl FeeEstimatorContext { + fn new() -> AsyncMutex { + AsyncMutex::new(FeeEstimatorContext { + estimated_fees: Default::default(), + abort_handler: AsyncMutex::new(None), + }) + } + + /// Fee estimation update period in secs, basically equals to eth blocktime + const fn get_refresh_interval() -> f64 { 15.0 } + + fn get_estimator_ctx(coin: &EthCoin) -> Result<&AsyncMutex, MmError> { + match coin.platform_fee_estimator_state.deref() { + FeeEstimatorState::CoinNotSupported => MmError::err(FeeEstimatorError::CoinNotSupported), + FeeEstimatorState::PlatformCoinRequired => MmError::err(FeeEstimatorError::PlatformCoinRequired), + FeeEstimatorState::Simple(fee_estimator_ctx) | FeeEstimatorState::Provider(fee_estimator_ctx) => { + Ok(fee_estimator_ctx) + }, + } + } + + async fn start_if_not_running(coin: &EthCoin) -> Result<(), MmError> { + let estimator_ctx = Self::get_estimator_ctx(coin)?; + let estimator_ctx = estimator_ctx.lock().await; + let mut handler = estimator_ctx.abort_handler.lock().await; + if handler.is_some() { + return MmError::err(FeeEstimatorError::AlreadyStarted); + } + *handler = Some(spawn_abortable(Self::fee_estimator_loop(coin.clone()))); + Ok(()) + } + + async fn request_to_stop(coin: &EthCoin) -> Result<(), MmError> { + let estimator_ctx = Self::get_estimator_ctx(coin)?; + let estimator_ctx = estimator_ctx.lock().await; + let mut handle_guard = estimator_ctx.abort_handler.lock().await; + // Handler will be dropped here, stopping the spawned loop immediately + handle_guard + .take() + .map(|_| ()) + .or_mm_err(|| FeeEstimatorError::NotRunning) + } + + async fn get_estimated_fees(coin: &EthCoin) -> Result> { + let estimator_ctx = Self::get_estimator_ctx(coin)?; + let estimator_ctx = estimator_ctx.lock().await; + let estimated_fees = estimator_ctx.estimated_fees.lock().await; + Ok(estimated_fees.clone()) + } + + async fn check_if_estimator_supported(ctx: &MmArc, ticker: &str) -> Result> { + let eth_coin = match lp_coinfind_or_err(ctx, ticker).await? { + MmCoinEnum::EthCoin(eth) => eth, + _ => return MmError::err(FeeEstimatorError::CoinNotSupported), + }; + let _ = Self::get_estimator_ctx(ð_coin)?; + Ok(eth_coin) + } + + /// Loop polling gas fee estimator + /// + /// This loop periodically calls get_eip1559_gas_fee which fetches fee per gas estimations from a gas api provider or calculates them internally + /// The retrieved data are stored in the fee estimator context + /// To connect to the chain and gas api provider the web3 instances are used from an EthCoin coin passed in the start rpc param, + /// so this coin must be enabled first. + /// Once the loop started any other EthCoin in mainnet may request fee estimations. + /// It is up to GUI to start and stop the loop when it needs it (considering that the data in context may be used + /// for any coin with Eth or Erc20 type from the mainnet). + async fn fee_estimator_loop(coin: EthCoin) { + loop { + let started = common::now_float(); + if let Ok(estimator_ctx) = Self::get_estimator_ctx(&coin) { + let estimated_fees = coin.get_eip1559_gas_fee().await.unwrap_or_default(); + let estimator_ctx = estimator_ctx.lock().await; + *estimator_ctx.estimated_fees.lock().await = estimated_fees; + } + + let elapsed = common::now_float() - started; + debug!("{FEE_ESTIMATOR_NAME} call to provider processed in {} seconds", elapsed); + + let wait_secs = FeeEstimatorContext::get_refresh_interval() - elapsed; + let wait_secs = if wait_secs < 0.0 { 0.0 } else { wait_secs }; + Timer::sleep(wait_secs).await; + } + } +} + +/// Rpc request to start or stop gas fee estimator +#[derive(Deserialize)] +pub struct FeeEstimatorStartStopRequest { + coin: String, +} + +/// Rpc response to request to start or stop gas fee estimator +#[derive(Serialize)] +pub struct FeeEstimatorStartStopResponse { + result: String, +} + +pub type FeeEstimatorStartStopResult = Result>; + +/// Rpc request to get latest estimated fee per gas +#[derive(Deserialize)] +pub struct FeeEstimatorRequest { + /// coin ticker + coin: String, +} + +pub type FeeEstimatorResult = Result>; + +/// Start gas priority fee estimator loop +pub async fn start_eth_fee_estimator(ctx: MmArc, req: FeeEstimatorStartStopRequest) -> FeeEstimatorStartStopResult { + let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; + FeeEstimatorContext::start_if_not_running(&coin).await?; + Ok(FeeEstimatorStartStopResponse { + result: "Success".to_string(), + }) +} + +/// Stop gas priority fee estimator loop +pub async fn stop_eth_fee_estimator(ctx: MmArc, req: FeeEstimatorStartStopRequest) -> FeeEstimatorStartStopResult { + let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; + FeeEstimatorContext::request_to_stop(&coin).await?; + Ok(FeeEstimatorStartStopResponse { + result: "Success".to_string(), + }) +} + +/// Get latest estimated fee per gas for a eth coin +/// +/// Estimation loop for this coin must be stated. +/// Only main chain is supported +/// +/// Returns latest estimated fee per gas for the next block +pub async fn get_eth_estimated_fee_per_gas(ctx: MmArc, req: FeeEstimatorRequest) -> FeeEstimatorResult { + let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; + let estimated_fees = FeeEstimatorContext::get_estimated_fees(&coin).await?; + estimated_fees.try_into().mm_err(Into::into) +} diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs index 54263226e6..9bba74cf75 100644 --- a/mm2src/coins/rpc_command/get_new_address.rs +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -1,29 +1,32 @@ use crate::coin_balance::HDAddressBalance; -use crate::hd_confirm_address::{ConfirmAddressStatus, HDConfirmAddress, HDConfirmAddressError, RpcTaskConfirmAddress}; -use crate::hd_wallet::{AddressDerivingError, InvalidBip44ChainError, NewAddressDeriveConfirmError, - NewAddressDerivingError}; -use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, CoinsContext, MmCoinEnum, UnexpectedDerivationMethod}; +use crate::hd_wallet::{AddressDerivingError, ConfirmAddressStatus, HDConfirmAddress, HDConfirmAddressError, + InvalidBip44ChainError, NewAddressDeriveConfirmError, NewAddressDerivingError, + RpcTaskConfirmAddress}; +use crate::{lp_coinfind_or_err, BalanceError, CoinBalance, CoinBalanceMap, CoinFindError, CoinsContext, MmCoinEnum, + UnexpectedDerivationMethod}; use async_trait::async_trait; use common::{HttpStatusCode, SuccessResponse}; use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction, HwRpcTaskUserActionRequest}; +use crypto::trezor::TrezorMessageType; use crypto::{from_hw_error, Bip44Chain, HwError, HwRpcError, WithHwRpcError}; use derive_more::Display; -use enum_from::EnumFromTrait; +use enum_derives::EnumFromTrait; use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; use std::time::Duration; pub type GetNewAddressUserAction = HwRpcTaskUserAction; pub type GetNewAddressAwaitingStatus = HwRpcTaskAwaitingStatus; pub type GetNewAddressTaskManager = RpcTaskManager; pub type GetNewAddressTaskManagerShared = RpcTaskManagerShared; -pub type GetNewAddressTaskHandle = RpcTaskHandle; +pub type GetNewAddressTaskHandleShared = RpcTaskHandleShared; pub type GetNewAddressRpcTaskStatus = RpcTaskStatus< - GetNewAddressResponse, + GetNewAddressResponseEnum, GetNewAddressRpcError, GetNewAddressInProgressStatus, GetNewAddressAwaitingStatus, @@ -54,6 +57,8 @@ pub enum GetNewAddressRpcError { RpcInvalidResponse(String), #[display(fmt = "HD wallet storage error: {_0}")] WalletStorageError(String), + #[display(fmt = "Failed scripthash subscription. Error: {_0}")] + FailedScripthashSubscription(String), #[from_trait(WithTimeout::timeout)] #[display(fmt = "RPC timed out {_0:?}")] Timeout(Duration), @@ -107,7 +112,7 @@ impl From for GetNewAddressRpcError { GetNewAddressRpcError::AddressLimitReached { max_addresses_number } }, NewAddressDerivingError::InvalidBip44Chain { chain } => GetNewAddressRpcError::InvalidBip44Chain { chain }, - NewAddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::Internal(bip32.to_string()), + NewAddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::Internal(bip32), NewAddressDerivingError::WalletStorageError(storage) => { GetNewAddressRpcError::WalletStorageError(storage.to_string()) }, @@ -120,7 +125,7 @@ impl From for GetNewAddressRpcError { fn from(e: AddressDerivingError) -> Self { match e { AddressDerivingError::InvalidBip44Chain { chain } => GetNewAddressRpcError::InvalidBip44Chain { chain }, - AddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::ErrorDerivingAddress(bip32.to_string()), + AddressDerivingError::Bip32Error(bip32) => GetNewAddressRpcError::ErrorDerivingAddress(bip32), AddressDerivingError::Internal(internal) => GetNewAddressRpcError::Internal(internal), } } @@ -144,6 +149,9 @@ impl From for GetNewAddressRpcError { HDConfirmAddressError::InvalidAddress { expected, found } => GetNewAddressRpcError::Internal(format!( "Confirmation address mismatched: expected '{expected}, found '{found}''" )), + HDConfirmAddressError::NoAddressReceived => { + GetNewAddressRpcError::Internal("No address received".to_string()) + }, HDConfirmAddressError::Internal(internal) => GetNewAddressRpcError::Internal(internal), } } @@ -183,6 +191,7 @@ impl HttpStatusCode for GetNewAddressRpcError { GetNewAddressRpcError::Transport(_) | GetNewAddressRpcError::RpcInvalidResponse(_) | GetNewAddressRpcError::WalletStorageError(_) + | GetNewAddressRpcError::FailedScripthashSubscription(_) | GetNewAddressRpcError::HwError(_) | GetNewAddressRpcError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, GetNewAddressRpcError::Timeout(_) => StatusCode::REQUEST_TIMEOUT, @@ -190,7 +199,7 @@ impl HttpStatusCode for GetNewAddressRpcError { } } -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct GetNewAddressRequest { coin: String, #[serde(flatten)] @@ -207,9 +216,18 @@ pub struct GetNewAddressParams { pub(crate) gap_limit: Option, } +/// Generic response for the `get_new_address` RPC command. +#[derive(Clone, Debug, Serialize)] +pub struct GetNewAddressResponse { + new_address: HDAddressBalance, +} + +/// Enum for the response of the `get_new_address` RPC command. #[derive(Clone, Debug, Serialize)] -pub struct GetNewAddressResponse { - new_address: HDAddressBalance, +#[serde(untagged)] +pub enum GetNewAddressResponseEnum { + Single(GetNewAddressResponse), + Map(GetNewAddressResponse), } #[derive(Clone, Serialize)] @@ -232,25 +250,29 @@ impl ConfirmAddressStatus for GetNewAddressInProgressStatus { } } +/// A trait for the `get_new_address` RPC commands. #[async_trait] pub trait GetNewAddressRpcOps { + type BalanceObject; + /// Generates a new address. /// TODO remove once GUI integrates `task::get_new_address::init`. async fn get_new_address_rpc_without_conf( &self, params: GetNewAddressParams, - ) -> MmResult; + ) -> MmResult, GetNewAddressRpcError>; /// Generates and asks the user to confirm a new address. async fn get_new_address_rpc( &self, params: GetNewAddressParams, confirm_address: &ConfirmAddress, - ) -> MmResult + ) -> MmResult, GetNewAddressRpcError> where ConfirmAddress: HDConfirmAddress; } +#[derive(Clone)] pub struct InitGetNewAddressTask { ctx: MmArc, coin: MmCoinEnum, @@ -258,7 +280,7 @@ pub struct InitGetNewAddressTask { } impl RpcTaskTypes for InitGetNewAddressTask { - type Item = GetNewAddressResponse; + type Item = GetNewAddressResponseEnum; type Error = GetNewAddressRpcError; type InProgressStatus = GetNewAddressInProgressStatus; type AwaitingStatus = GetNewAddressAwaitingStatus; @@ -272,13 +294,14 @@ impl RpcTask for InitGetNewAddressTask { // Do nothing if the task has been cancelled. async fn cancel(self) {} - async fn run(&mut self, task_handle: &RpcTaskHandle) -> Result> { + async fn run(&mut self, task_handle: RpcTaskHandleShared) -> Result> { async fn get_new_address_helper( ctx: &MmArc, coin: &Coin, params: GetNewAddressParams, - task_handle: &GetNewAddressTaskHandle, - ) -> MmResult + task_handle: GetNewAddressTaskHandleShared, + trezor_message_type: TrezorMessageType, + ) -> MmResult::BalanceObject>, GetNewAddressRpcError> where Coin: GetNewAddressRpcOps + Send + Sync, { @@ -291,33 +314,63 @@ impl RpcTask for InitGetNewAddressTask { on_passphrase_request: GetNewAddressAwaitingStatus::EnterTrezorPassphrase, on_ready: GetNewAddressInProgressStatus::RequestingAccountBalance, }; - let confirm_address: RpcTaskConfirmAddress<'_, InitGetNewAddressTask> = - RpcTaskConfirmAddress::new(ctx, task_handle, hw_statuses)?; + let confirm_address: RpcTaskConfirmAddress = + RpcTaskConfirmAddress::new(ctx, task_handle, hw_statuses, trezor_message_type)?; coin.get_new_address_rpc(params, &confirm_address).await } match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => { - get_new_address_helper(&self.ctx, utxo, self.req.params.clone(), task_handle).await - }, - MmCoinEnum::QtumCoin(ref qtum) => { - get_new_address_helper(&self.ctx, qtum, self.req.params.clone(), task_handle).await - }, + MmCoinEnum::UtxoCoin(ref utxo) => Ok(GetNewAddressResponseEnum::Single( + get_new_address_helper( + &self.ctx, + utxo, + self.req.params.clone(), + task_handle, + TrezorMessageType::Bitcoin, + ) + .await?, + )), + MmCoinEnum::QtumCoin(ref qtum) => Ok(GetNewAddressResponseEnum::Single( + get_new_address_helper( + &self.ctx, + qtum, + self.req.params.clone(), + task_handle, + TrezorMessageType::Bitcoin, + ) + .await?, + )), + MmCoinEnum::EthCoin(ref eth) => Ok(GetNewAddressResponseEnum::Map( + get_new_address_helper( + &self.ctx, + eth, + self.req.params.clone(), + task_handle, + TrezorMessageType::Ethereum, + ) + .await?, + )), _ => MmError::err(GetNewAddressRpcError::CoinIsActivatedNotWithHDWallet), } } } /// Generates a new address. -/// TODO remove once GUI integrates `task::get_new_address::init`. pub async fn get_new_address( ctx: MmArc, req: GetNewAddressRequest, -) -> MmResult { +) -> MmResult { let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; match coin { - MmCoinEnum::UtxoCoin(utxo) => utxo.get_new_address_rpc_without_conf(req.params).await, - MmCoinEnum::QtumCoin(qtum) => qtum.get_new_address_rpc_without_conf(req.params).await, + MmCoinEnum::UtxoCoin(utxo) => Ok(GetNewAddressResponseEnum::Single( + utxo.get_new_address_rpc_without_conf(req.params).await?, + )), + MmCoinEnum::QtumCoin(qtum) => Ok(GetNewAddressResponseEnum::Single( + qtum.get_new_address_rpc_without_conf(req.params).await?, + )), + MmCoinEnum::EthCoin(eth) => Ok(GetNewAddressResponseEnum::Map( + eth.get_new_address_rpc_without_conf(req.params).await?, + )), _ => MmError::err(GetNewAddressRpcError::CoinIsActivatedNotWithHDWallet), } } @@ -378,22 +431,24 @@ pub async fn cancel_get_new_address( pub(crate) mod common_impl { use super::*; - use crate::coin_balance::{HDAddressBalanceScanner, HDWalletBalanceOps}; - use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; - use crate::{CoinWithDerivationMethod, HDAddress}; + use crate::coin_balance::{HDAddressBalanceScanner, HDWalletBalanceObject, HDWalletBalanceOps}; + use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDCoinHDAccount, HDWalletOps}; + use crate::CoinWithDerivationMethod; use crypto::RpcDerivationPath; + use std::collections::HashSet; use std::fmt; + use std::fmt::Display; + use std::hash::Hash; use std::ops::DerefMut; /// TODO remove once GUI integrates `task::get_new_address::init`. pub async fn get_new_address_rpc_without_conf( coin: &Coin, params: GetNewAddressParams, - ) -> MmResult + ) -> MmResult>, GetNewAddressRpcError> where - Coin: - HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync + Send, - ::Address: fmt::Display, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Sync + Send, + HDCoinAddress: fmt::Display, { let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; @@ -407,21 +462,18 @@ pub(crate) mod common_impl { let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); // Check if we can generate new address. - check_if_can_get_new_address(coin, hd_wallet, &hd_account, chain, gap_limit).await?; + check_if_can_get_new_address(coin, &hd_account, chain, gap_limit).await?; - let HDAddress { - address, - derivation_path, - .. - } = coin + let hd_address = coin .generate_new_address(hd_wallet, hd_account.deref_mut(), chain) .await?; + let address = hd_address.address(); let balance = coin.known_address_balance(&address).await?; Ok(GetNewAddressResponse { new_address: HDAddressBalance { address: address.to_string(), - derivation_path: RpcDerivationPath(derivation_path), + derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance, }, @@ -432,12 +484,11 @@ pub(crate) mod common_impl { coin: &Coin, params: GetNewAddressParams, confirm_address: &ConfirmAddress, - ) -> MmResult + ) -> MmResult>, GetNewAddressRpcError> where ConfirmAddress: HDConfirmAddress, - Coin: - HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Send + Sync, - ::Address: fmt::Display, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Send + Sync, + HDCoinAddress: Display + Eq + Hash, { let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; @@ -451,21 +502,22 @@ pub(crate) mod common_impl { let gap_limit = params.gap_limit.unwrap_or_else(|| hd_wallet.gap_limit()); // Check if we can generate new address. - check_if_can_get_new_address(coin, hd_wallet, &hd_account, chain, gap_limit).await?; + check_if_can_get_new_address(coin, &hd_account, chain, gap_limit).await?; - let HDAddress { - address, - derivation_path, - .. - } = coin + let hd_address = coin .generate_and_confirm_new_address(hd_wallet, &mut hd_account, chain, confirm_address) .await?; - + let address = hd_address.address(); let balance = coin.known_address_balance(&address).await?; + + coin.prepare_addresses_for_balance_stream_if_enabled(HashSet::from([address.to_string()])) + .await + .map_err(|e| GetNewAddressRpcError::FailedScripthashSubscription(e.to_string()))?; + Ok(GetNewAddressResponse { new_address: HDAddressBalance { address: address.to_string(), - derivation_path: RpcDerivationPath(derivation_path), + derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance, }, @@ -474,21 +526,20 @@ pub(crate) mod common_impl { async fn check_if_can_get_new_address( coin: &Coin, - hd_wallet: &Coin::HDWallet, - hd_account: &Coin::HDAccount, + hd_account: &HDCoinHDAccount, chain: Bip44Chain, gap_limit: u32, ) -> MmResult<(), GetNewAddressRpcError> where Coin: HDWalletBalanceOps + Sync, - ::Address: fmt::Display, + HDCoinAddress: fmt::Display, { let known_addresses_number = hd_account.known_addresses_number(chain)?; if known_addresses_number == 0 || gap_limit > known_addresses_number { return Ok(()); } - let max_addresses_number = hd_wallet.address_limit(); + let max_addresses_number = hd_account.address_limit(); if known_addresses_number >= max_addresses_number { return MmError::err(GetNewAddressRpcError::AddressLimitReached { max_addresses_number }); } @@ -500,7 +551,7 @@ pub(crate) mod common_impl { let last_address_id = known_addresses_number - 1; for address_id in (0..=last_address_id).rev() { - let HDAddress { address, .. } = coin.derive_address(hd_account, chain, address_id).await?; + let address = coin.derive_address(hd_account, chain, address_id).await?.address(); if address_scanner.is_address_used(&address).await? { return Ok(()); } diff --git a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs index e929692285..2e3592cc2c 100644 --- a/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs +++ b/mm2src/coins/rpc_command/hd_account_balance_rpc_error.rs @@ -26,6 +26,8 @@ pub enum HDAccountBalanceRpcError { WalletStorageError(String), #[display(fmt = "Electrum/Native RPC invalid response: {}", _0)] RpcInvalidResponse(String), + #[display(fmt = "Failed scripthash subscription. Error: {_0}")] + FailedScripthashSubscription(String), #[display(fmt = "Transport: {}", _0)] Transport(String), #[display(fmt = "Internal: {}", _0)] @@ -44,6 +46,7 @@ impl HttpStatusCode for HDAccountBalanceRpcError { HDAccountBalanceRpcError::Transport(_) | HDAccountBalanceRpcError::WalletStorageError(_) | HDAccountBalanceRpcError::RpcInvalidResponse(_) + | HDAccountBalanceRpcError::FailedScripthashSubscription(_) | HDAccountBalanceRpcError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -85,9 +88,7 @@ impl From for HDAccountBalanceRpcError { fn from(e: AddressDerivingError) -> Self { match e { AddressDerivingError::InvalidBip44Chain { chain } => HDAccountBalanceRpcError::InvalidBip44Chain { chain }, - AddressDerivingError::Bip32Error(bip32) => { - HDAccountBalanceRpcError::ErrorDerivingAddress(bip32.to_string()) - }, + AddressDerivingError::Bip32Error(bip32) => HDAccountBalanceRpcError::ErrorDerivingAddress(bip32), AddressDerivingError::Internal(internal) => HDAccountBalanceRpcError::Internal(internal), } } diff --git a/mm2src/coins/rpc_command/init_account_balance.rs b/mm2src/coins/rpc_command/init_account_balance.rs index 3317acea67..94745f65e5 100644 --- a/mm2src/coins/rpc_command/init_account_balance.rs +++ b/mm2src/coins/rpc_command/init_account_balance.rs @@ -1,4 +1,4 @@ -use crate::coin_balance::HDAccountBalance; +use crate::coin_balance::{BalanceObjectOps, HDAccountBalance, HDAccountBalanceEnum}; use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; use crate::{lp_coinfind_or_err, CoinsContext, MmCoinEnum}; use async_trait::async_trait; @@ -7,15 +7,15 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest}; -use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; pub type AccountBalanceUserAction = SerdeInfallible; pub type AccountBalanceAwaitingStatus = SerdeInfallible; pub type AccountBalanceTaskManager = RpcTaskManager; pub type AccountBalanceTaskManagerShared = RpcTaskManagerShared; -pub type InitAccountBalanceTaskHandle = RpcTaskHandle; +pub type InitAccountBalanceTaskHandleShared = RpcTaskHandleShared; pub type AccountBalanceRpcTaskStatus = RpcTaskStatus< - HDAccountBalance, + HDAccountBalanceEnum, HDAccountBalanceRpcError, AccountBalanceInProgressStatus, AccountBalanceAwaitingStatus, @@ -40,10 +40,12 @@ pub struct InitAccountBalanceParams { #[async_trait] pub trait InitAccountBalanceRpcOps { + type BalanceObject; + async fn init_account_balance_rpc( &self, params: InitAccountBalanceParams, - ) -> MmResult; + ) -> MmResult, HDAccountBalanceRpcError>; } pub struct InitAccountBalanceTask { @@ -52,7 +54,7 @@ pub struct InitAccountBalanceTask { } impl RpcTaskTypes for InitAccountBalanceTask { - type Item = HDAccountBalance; + type Item = HDAccountBalanceEnum; type Error = HDAccountBalanceRpcError; type InProgressStatus = AccountBalanceInProgressStatus; type AwaitingStatus = AccountBalanceAwaitingStatus; @@ -66,10 +68,20 @@ impl RpcTask for InitAccountBalanceTask { // Do nothing if the task has been cancelled. async fn cancel(self) {} - async fn run(&mut self, _task_handle: &InitAccountBalanceTaskHandle) -> Result> { + async fn run( + &mut self, + _task_handle: InitAccountBalanceTaskHandleShared, + ) -> Result> { match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => utxo.init_account_balance_rpc(self.req.params.clone()).await, - MmCoinEnum::QtumCoin(ref qtum) => qtum.init_account_balance_rpc(self.req.params.clone()).await, + MmCoinEnum::UtxoCoin(ref utxo) => Ok(HDAccountBalanceEnum::Single( + utxo.init_account_balance_rpc(self.req.params.clone()).await?, + )), + MmCoinEnum::QtumCoin(ref qtum) => Ok(HDAccountBalanceEnum::Single( + qtum.init_account_balance_rpc(self.req.params.clone()).await?, + )), + MmCoinEnum::EthCoin(ref eth) => Ok(HDAccountBalanceEnum::Map( + eth.init_account_balance_rpc(self.req.params.clone()).await?, + )), _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), } } @@ -116,19 +128,19 @@ pub async fn cancel_account_balance( pub mod common_impl { use super::*; - use crate::coin_balance::HDWalletBalanceOps; - use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; - use crate::{CoinBalance, CoinWithDerivationMethod}; + use crate::coin_balance::{HDWalletBalanceObject, HDWalletBalanceOps}; + use crate::hd_wallet::{HDAccountOps, HDCoinAddress, HDWalletOps}; + use crate::CoinWithDerivationMethod; use crypto::RpcDerivationPath; use std::fmt; pub async fn init_account_balance_rpc( coin: &Coin, params: InitAccountBalanceParams, - ) -> MmResult + ) -> MmResult>, HDAccountBalanceRpcError> where - Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, - ::Address: fmt::Display + Clone, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Sync, + HDCoinAddress: fmt::Display + Clone, { let account_id = params.account_index; let hd_account = coin @@ -139,10 +151,12 @@ pub mod common_impl { .or_mm_err(|| HDAccountBalanceRpcError::UnknownAccount { account_id })?; let addresses = coin.all_known_addresses_balances(&hd_account).await?; + let total_balance = addresses .iter() - .fold(CoinBalance::default(), |total_balance, address_balance| { - total_balance + address_balance.balance.clone() + .fold(HDWalletBalanceObject::::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total }); Ok(HDAccountBalance { diff --git a/mm2src/coins/rpc_command/init_create_account.rs b/mm2src/coins/rpc_command/init_create_account.rs index 82f99587b6..012009d04b 100644 --- a/mm2src/coins/rpc_command/init_create_account.rs +++ b/mm2src/coins/rpc_command/init_create_account.rs @@ -1,21 +1,21 @@ -use crate::coin_balance::HDAccountBalance; -use crate::hd_pubkey::{HDExtractPubkeyError, HDXPubExtractor, RpcTaskXPubExtractor}; -use crate::hd_wallet::NewAccountCreatingError; -use crate::{lp_coinfind_or_err, BalanceError, CoinBalance, CoinFindError, CoinWithDerivationMethod, CoinsContext, - MmCoinEnum, UnexpectedDerivationMethod}; +use crate::coin_balance::{BalanceObjectOps, HDAccountBalance, HDAccountBalanceEnum}; +use crate::hd_wallet::{HDExtractPubkeyError, HDXPubExtractor, NewAccountCreationError, RpcTaskXPubExtractor}; +use crate::{lp_coinfind_or_err, BalanceError, CoinFindError, CoinProtocol, CoinWithDerivationMethod, CoinsContext, + MarketCoinOps, MmCoinEnum, UnexpectedDerivationMethod}; use async_trait::async_trait; use common::{true_f, HttpStatusCode, SuccessResponse}; use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction, HwRpcTaskUserActionRequest}; use crypto::{from_hw_error, Bip44Chain, HwError, HwRpcError, RpcDerivationPath, WithHwRpcError}; use derive_more::Display; -use enum_from::EnumFromTrait; +use enum_derives::EnumFromTrait; use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use parking_lot::Mutex as PaMutex; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, + RpcTaskTypes}; use std::sync::Arc; use std::time::Duration; @@ -23,11 +23,15 @@ pub type CreateAccountUserAction = HwRpcTaskUserAction; pub type CreateAccountAwaitingStatus = HwRpcTaskAwaitingStatus; pub type CreateAccountTaskManager = RpcTaskManager; pub type CreateAccountTaskManagerShared = RpcTaskManagerShared; -pub type CreateAccountTaskHandle = RpcTaskHandle; -pub type CreateAccountRpcTaskStatus = - RpcTaskStatus; +pub type CreateAccountTaskHandleShared = RpcTaskHandleShared; +pub type CreateAccountRpcTaskStatus = RpcTaskStatus< + HDAccountBalanceEnum, + CreateAccountRpcError, + CreateAccountInProgressStatus, + CreateAccountAwaitingStatus, +>; -type CreateAccountXPubExtractor<'task> = RpcTaskXPubExtractor<'task, InitCreateAccountTask>; +type CreateAccountXPubExtractor = RpcTaskXPubExtractor; #[derive(Clone, Debug, Display, EnumFromTrait, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -78,24 +82,24 @@ impl From for CreateAccountRpcError { } } -impl From for CreateAccountRpcError { - fn from(e: NewAccountCreatingError) -> Self { +impl From for CreateAccountRpcError { + fn from(e: NewAccountCreationError) -> Self { match e { - NewAccountCreatingError::HwContextNotInitialized => CreateAccountRpcError::HwContextNotInitialized, - NewAccountCreatingError::HDWalletUnavailable => CreateAccountRpcError::CoinIsActivatedNotWithHDWallet, - NewAccountCreatingError::CoinDoesntSupportTrezor => { + NewAccountCreationError::HwContextNotInitialized => CreateAccountRpcError::HwContextNotInitialized, + NewAccountCreationError::HDWalletUnavailable => CreateAccountRpcError::CoinIsActivatedNotWithHDWallet, + NewAccountCreationError::CoinDoesntSupportTrezor => { CreateAccountRpcError::Internal("Coin must support Trezor at this point".to_string()) }, - NewAccountCreatingError::RpcTaskError(rpc) => CreateAccountRpcError::from(rpc), - NewAccountCreatingError::HardwareWalletError(hw) => CreateAccountRpcError::from(hw), - NewAccountCreatingError::AccountLimitReached { max_accounts_number } => { + NewAccountCreationError::RpcTaskError(rpc) => CreateAccountRpcError::from(rpc), + NewAccountCreationError::HardwareWalletError(hw) => CreateAccountRpcError::from(hw), + NewAccountCreationError::AccountLimitReached { max_accounts_number } => { CreateAccountRpcError::AccountLimitReached { max_accounts_number } }, - NewAccountCreatingError::ErrorSavingAccountToStorage(e) => { + NewAccountCreationError::ErrorSavingAccountToStorage(e) => { let error = format!("Error uploading HD account info to the storage: {}", e); CreateAccountRpcError::WalletStorageError(error) }, - NewAccountCreatingError::Internal(internal) => CreateAccountRpcError::Internal(internal), + NewAccountCreationError::Internal(internal) => CreateAccountRpcError::Internal(internal), } } } @@ -114,7 +118,7 @@ impl From for CreateAccountRpcError { } impl From for CreateAccountRpcError { - fn from(e: HDExtractPubkeyError) -> Self { CreateAccountRpcError::from(NewAccountCreatingError::from(e)) } + fn from(e: HDExtractPubkeyError) -> Self { CreateAccountRpcError::from(NewAccountCreationError::from(e)) } } impl From for CreateAccountRpcError { @@ -155,7 +159,7 @@ impl HttpStatusCode for CreateAccountRpcError { } } -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct CreateNewAccountRequest { coin: String, #[serde(flatten)] @@ -169,6 +173,7 @@ pub struct CreateNewAccountParams { // The max number of empty addresses in a row. // If transactions were sent to an address outside the `gap_limit`, they will not be identified. gap_limit: Option, + account_id: Option, } #[derive(Clone, Serialize)] @@ -198,18 +203,21 @@ impl CreateAccountState { #[async_trait] pub trait InitCreateAccountRpcOps { + type BalanceObject; + async fn init_create_account_rpc( &self, params: CreateNewAccountParams, state: CreateAccountState, - xpub_extractor: &XPubExtractor, - ) -> MmResult + xpub_extractor: Option, + ) -> MmResult, CreateAccountRpcError> where - XPubExtractor: HDXPubExtractor; + XPubExtractor: HDXPubExtractor + Send; async fn revert_creating_account(&self, account_id: u32); } +#[derive(Clone)] pub struct InitCreateAccountTask { ctx: MmArc, coin: MmCoinEnum, @@ -219,7 +227,7 @@ pub struct InitCreateAccountTask { } impl RpcTaskTypes for InitCreateAccountTask { - type Item = HDAccountBalance; + type Item = HDAccountBalanceEnum; type Error = CreateAccountRpcError; type InProgressStatus = CreateAccountInProgressStatus; type AwaitingStatus = CreateAccountAwaitingStatus; @@ -236,56 +244,84 @@ impl RpcTask for InitCreateAccountTask { match self.coin { MmCoinEnum::UtxoCoin(utxo) => utxo.revert_creating_account(account_id).await, MmCoinEnum::QtumCoin(qtum) => qtum.revert_creating_account(account_id).await, + MmCoinEnum::EthCoin(eth) => eth.revert_creating_account(account_id).await, _ => (), } }; } - async fn run(&mut self, task_handle: &CreateAccountTaskHandle) -> Result> { + async fn run(&mut self, task_handle: CreateAccountTaskHandleShared) -> Result> { async fn create_new_account_helper( ctx: &MmArc, coin: &Coin, params: CreateNewAccountParams, state: CreateAccountState, - task_handle: &CreateAccountTaskHandle, - ) -> MmResult + task_handle: CreateAccountTaskHandleShared, + is_trezor: bool, + coin_protocol: CoinProtocol, + ) -> MmResult::BalanceObject>, CreateAccountRpcError> where Coin: InitCreateAccountRpcOps + Send + Sync, { - let hw_statuses = HwConnectStatuses { - on_connect: CreateAccountInProgressStatus::WaitingForTrezorToConnect, - on_connected: CreateAccountInProgressStatus::Preparing, - on_connection_failed: CreateAccountInProgressStatus::Finishing, - on_button_request: CreateAccountInProgressStatus::FollowHwDeviceInstructions, - on_pin_request: CreateAccountAwaitingStatus::EnterTrezorPin, - on_passphrase_request: CreateAccountAwaitingStatus::EnterTrezorPassphrase, - on_ready: CreateAccountInProgressStatus::RequestingAccountBalance, + let xpub_extractor = if is_trezor { + let hw_statuses = HwConnectStatuses { + on_connect: CreateAccountInProgressStatus::WaitingForTrezorToConnect, + on_connected: CreateAccountInProgressStatus::Preparing, + on_connection_failed: CreateAccountInProgressStatus::Finishing, + on_button_request: CreateAccountInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: CreateAccountAwaitingStatus::EnterTrezorPin, + on_passphrase_request: CreateAccountAwaitingStatus::EnterTrezorPassphrase, + on_ready: CreateAccountInProgressStatus::RequestingAccountBalance, + }; + Some(CreateAccountXPubExtractor::new_trezor_extractor( + ctx, + task_handle, + hw_statuses, + coin_protocol, + )?) + } else { + None }; - let xpub_extractor = CreateAccountXPubExtractor::new(ctx, task_handle, hw_statuses)?; - coin.init_create_account_rpc(params, state, &xpub_extractor).await + coin.init_create_account_rpc(params, state, xpub_extractor).await } match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => { + MmCoinEnum::UtxoCoin(ref utxo) => Ok(HDAccountBalanceEnum::Single( create_new_account_helper( &self.ctx, utxo, self.req.params.clone(), self.task_state.clone(), task_handle, + utxo.is_trezor(), + CoinProtocol::UTXO, ) - .await - }, - MmCoinEnum::QtumCoin(ref qtum) => { + .await?, + )), + MmCoinEnum::QtumCoin(ref qtum) => Ok(HDAccountBalanceEnum::Single( create_new_account_helper( &self.ctx, qtum, self.req.params.clone(), self.task_state.clone(), task_handle, + qtum.is_trezor(), + CoinProtocol::QTUM, ) - .await - }, + .await?, + )), + MmCoinEnum::EthCoin(ref eth) => Ok(HDAccountBalanceEnum::Map( + create_new_account_helper( + &self.ctx, + eth, + self.req.params.clone(), + self.task_state.clone(), + task_handle, + eth.is_trezor(), + CoinProtocol::ETH, + ) + .await?, + )), _ => MmError::err(CreateAccountRpcError::CoinIsActivatedNotWithHDWallet), } } @@ -350,23 +386,28 @@ pub async fn cancel_create_new_account( pub(crate) mod common_impl { use super::*; - use crate::coin_balance::HDWalletBalanceOps; - use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; + use crate::coin_balance::{HDWalletBalanceObject, HDWalletBalanceOps}; + use crate::hd_wallet::{create_new_account, ExtractExtendedPubkey, HDAccountOps, HDAccountStorageOps, + HDCoinExtendedPubkey, HDCoinHDAccount, HDWalletOps}; pub async fn init_create_new_account_rpc<'a, Coin, XPubExtractor>( coin: &Coin, params: CreateNewAccountParams, state: CreateAccountState, - xpub_extractor: &XPubExtractor, - ) -> MmResult + xpub_extractor: Option, + ) -> MmResult>, CreateAccountRpcError> where - Coin: - HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Send + Sync, - XPubExtractor: HDXPubExtractor, + Coin: ExtractExtendedPubkey> + + HDWalletBalanceOps + + CoinWithDerivationMethod + + Send + + Sync, + XPubExtractor: HDXPubExtractor + Send, + HDCoinHDAccount: HDAccountStorageOps, { let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; - let mut new_account = coin.create_new_account(hd_wallet, xpub_extractor).await?; + let mut new_account = create_new_account(coin, hd_wallet, xpub_extractor, params.account_id).await?; let account_index = new_account.account_id(); let account_derivation_path = new_account.account_derivation_path(); @@ -384,8 +425,9 @@ pub(crate) mod common_impl { let total_balance = addresses .iter() - .fold(CoinBalance::default(), |total_balance, address_balance| { - total_balance + address_balance.balance.clone() + .fold(HDWalletBalanceObject::::new(), |mut total, addr_balance| { + total.add(addr_balance.balance.clone()); + total }); Ok(HDAccountBalance { @@ -398,10 +440,59 @@ pub(crate) mod common_impl { pub async fn revert_creating_account(coin: &Coin, account_id: u32) where - Coin: HDWalletBalanceOps + CoinWithDerivationMethod::HDWallet> + Sync, + Coin: HDWalletBalanceOps + CoinWithDerivationMethod + Sync, { if let Some(hd_wallet) = coin.derivation_method().hd_wallet() { hd_wallet.remove_account_if_last(account_id).await; } } } + +#[cfg(all(feature = "for-tests", not(target_arch = "wasm32")))] +pub mod for_tests { + use super::{init_create_new_account, init_create_new_account_status, CreateAccountRpcError}; + use crate::coin_balance::HDAccountBalanceEnum; + use common::executor::Timer; + use common::{now_ms, wait_until_ms}; + use mm2_core::mm_ctx::MmArc; + use mm2_err_handle::prelude::MmResult; + use rpc_task::rpc_common::RpcTaskStatusRequest; + use rpc_task::RpcTaskStatus; + use serde_json::{self, json}; + + /// Helper to call init_create_new_account fn and wait for completion + pub async fn test_create_new_account_init_loop( + ctx: MmArc, + ticker: &str, + account_id: Option, + ) -> MmResult { + let req = serde_json::from_value(json!({ + "coin": ticker, + "params": { + "account_id": account_id + } + })) + .unwrap(); + let init = init_create_new_account(ctx.clone(), req).await.unwrap(); + let timeout = wait_until_ms(150000); + loop { + if now_ms() > timeout { + panic!("{} init_withdraw timed out", ticker); + } + let status = init_create_new_account_status(ctx.clone(), RpcTaskStatusRequest { + task_id: init.task_id, + forget_if_finished: true, + }) + .await; + if let Ok(status) = status { + match status { + RpcTaskStatus::Ok(account_balance) => break Ok(account_balance), + RpcTaskStatus::Error(e) => break Err(e), + _ => Timer::sleep(1.).await, + } + } else { + panic!("{} could not get withdraw_status", ticker) + } + } + } +} diff --git a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs index e90d44eecc..4eabda46e9 100644 --- a/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs +++ b/mm2src/coins/rpc_command/init_scan_for_new_addresses.rs @@ -1,6 +1,6 @@ use crate::coin_balance::HDAddressBalance; use crate::rpc_command::hd_account_balance_rpc_error::HDAccountBalanceRpcError; -use crate::{lp_coinfind_or_err, CoinsContext, MmCoinEnum}; +use crate::{lp_coinfind_or_err, CoinBalance, CoinBalanceMap, CoinsContext, MmCoinEnum}; use async_trait::async_trait; use common::{SerdeInfallible, SuccessResponse}; use crypto::RpcDerivationPath; @@ -8,25 +8,34 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest}; -use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; +use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatus, RpcTaskTypes}; pub type ScanAddressesUserAction = SerdeInfallible; pub type ScanAddressesAwaitingStatus = SerdeInfallible; pub type ScanAddressesTaskManager = RpcTaskManager; pub type ScanAddressesTaskManagerShared = RpcTaskManagerShared; -pub type ScanAddressesTaskHandle = RpcTaskHandle; +pub type ScanAddressesTaskHandleShared = RpcTaskHandleShared; pub type ScanAddressesRpcTaskStatus = RpcTaskStatus< - ScanAddressesResponse, + ScanAddressesResponseEnum, HDAccountBalanceRpcError, ScanAddressesInProgressStatus, ScanAddressesAwaitingStatus, >; +/// Generic response for the `scan_for_new_addresses` RPC commands. #[derive(Clone, Debug, PartialEq, Serialize)] -pub struct ScanAddressesResponse { +pub struct ScanAddressesResponse { pub account_index: u32, pub derivation_path: RpcDerivationPath, - pub new_addresses: Vec, + pub new_addresses: Vec>, +} + +/// Enum for the response of the `scan_for_new_addresses` RPC commands. +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ScanAddressesResponseEnum { + Single(ScanAddressesResponse), + Map(ScanAddressesResponse), } #[derive(Deserialize)] @@ -49,12 +58,15 @@ pub enum ScanAddressesInProgressStatus { InProgress, } +/// Trait for the `scan_for_new_addresses` RPC commands. #[async_trait] pub trait InitScanAddressesRpcOps { + type BalanceObject; + async fn init_scan_for_new_addresses_rpc( &self, params: ScanAddressesParams, - ) -> MmResult; + ) -> MmResult, HDAccountBalanceRpcError>; } pub struct InitScanAddressesTask { @@ -63,7 +75,7 @@ pub struct InitScanAddressesTask { } impl RpcTaskTypes for InitScanAddressesTask { - type Item = ScanAddressesResponse; + type Item = ScanAddressesResponseEnum; type Error = HDAccountBalanceRpcError; type InProgressStatus = ScanAddressesInProgressStatus; type AwaitingStatus = ScanAddressesAwaitingStatus; @@ -78,10 +90,17 @@ impl RpcTask for InitScanAddressesTask { // Do nothing if the task has been cancelled. async fn cancel(self) {} - async fn run(&mut self, _task_handle: &ScanAddressesTaskHandle) -> Result> { + async fn run(&mut self, _task_handle: ScanAddressesTaskHandleShared) -> Result> { match self.coin { - MmCoinEnum::UtxoCoin(ref utxo) => utxo.init_scan_for_new_addresses_rpc(self.req.params.clone()).await, - MmCoinEnum::QtumCoin(ref qtum) => qtum.init_scan_for_new_addresses_rpc(self.req.params.clone()).await, + MmCoinEnum::UtxoCoin(ref utxo) => Ok(ScanAddressesResponseEnum::Single( + utxo.init_scan_for_new_addresses_rpc(self.req.params.clone()).await?, + )), + MmCoinEnum::QtumCoin(ref qtum) => Ok(ScanAddressesResponseEnum::Single( + qtum.init_scan_for_new_addresses_rpc(self.req.params.clone()).await?, + )), + MmCoinEnum::EthCoin(ref eth) => Ok(ScanAddressesResponseEnum::Map( + eth.init_scan_for_new_addresses_rpc(self.req.params.clone()).await?, + )), _ => MmError::err(HDAccountBalanceRpcError::CoinIsActivatedNotWithHDWallet), } } @@ -128,19 +147,18 @@ pub async fn cancel_scan_for_new_addresses( pub mod common_impl { use super::*; - use crate::coin_balance::HDWalletBalanceOps; - use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; + use crate::coin_balance::{HDWalletBalanceObject, HDWalletBalanceOps}; + use crate::hd_wallet::{HDAccountOps, HDWalletOps}; use crate::CoinWithDerivationMethod; - use std::fmt; + use std::collections::HashSet; use std::ops::DerefMut; pub async fn scan_for_new_addresses_rpc( coin: &Coin, params: ScanAddressesParams, - ) -> MmResult + ) -> MmResult>, HDAccountBalanceRpcError> where - Coin: CoinWithDerivationMethod::HDWallet> + HDWalletBalanceOps + Sync, - ::Address: fmt::Display, + Coin: CoinWithDerivationMethod + HDWalletBalanceOps + Sync, { let hd_wallet = coin.derivation_method().hd_wallet_or_err()?; @@ -157,6 +175,15 @@ pub mod common_impl { .scan_for_new_addresses(hd_wallet, hd_account.deref_mut(), &address_scanner, gap_limit) .await?; + let addresses: HashSet<_> = new_addresses + .iter() + .map(|address_balance| address_balance.address.clone()) + .collect(); + + coin.prepare_addresses_for_balance_stream_if_enabled(addresses) + .await + .map_err(|e| HDAccountBalanceRpcError::FailedScripthashSubscription(e.to_string()))?; + Ok(ScanAddressesResponse { account_index: account_id, derivation_path: RpcDerivationPath(account_derivation_path), diff --git a/mm2src/coins/rpc_command/init_withdraw.rs b/mm2src/coins/rpc_command/init_withdraw.rs index c9ba606250..43b86cf19a 100644 --- a/mm2src/coins/rpc_command/init_withdraw.rs +++ b/mm2src/coins/rpc_command/init_withdraw.rs @@ -7,7 +7,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc_task::rpc_common::{CancelRpcTaskError, CancelRpcTaskRequest, InitRpcTaskResponse, RpcTaskStatusError, RpcTaskStatusRequest, RpcTaskUserActionError}; -use rpc_task::{RpcTask, RpcTaskHandle, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatusAlias, RpcTaskTypes}; +use rpc_task::{RpcTask, RpcTaskHandleShared, RpcTaskManager, RpcTaskManagerShared, RpcTaskStatusAlias, RpcTaskTypes}; pub type WithdrawAwaitingStatus = HwRpcTaskAwaitingStatus; pub type WithdrawUserAction = HwRpcTaskUserAction; @@ -18,7 +18,7 @@ pub type WithdrawStatusRequest = RpcTaskStatusRequest; pub type WithdrawUserActionRequest = HwRpcTaskUserActionRequest; pub type WithdrawTaskManager = RpcTaskManager; pub type WithdrawTaskManagerShared = RpcTaskManagerShared; -pub type WithdrawTaskHandle = RpcTaskHandle; +pub type WithdrawTaskHandleShared = RpcTaskHandleShared; pub type WithdrawRpcStatus = RpcTaskStatusAlias; pub type WithdrawInitResult = Result>; @@ -28,7 +28,7 @@ pub trait CoinWithdrawInit { fn init_withdraw( ctx: MmArc, req: WithdrawRequest, - rpc_task_handle: &WithdrawTaskHandle, + rpc_task_handle: WithdrawTaskHandleShared, ) -> WithdrawInitResult; } @@ -101,7 +101,7 @@ pub trait InitWithdrawCoin { &self, ctx: MmArc, req: WithdrawRequest, - task_handle: &WithdrawTaskHandle, + task_handle: WithdrawTaskHandleShared, ) -> Result>; } @@ -126,14 +126,14 @@ impl RpcTask for WithdrawTask { // Do nothing if the task has been cancelled. async fn cancel(self) {} - async fn run(&mut self, task_handle: &WithdrawTaskHandle) -> Result> { + async fn run(&mut self, task_handle: WithdrawTaskHandleShared) -> Result> { let ctx = self.ctx.clone(); let request = self.request.clone(); match self.coin { MmCoinEnum::UtxoCoin(ref standard_utxo) => standard_utxo.init_withdraw(ctx, request, task_handle).await, MmCoinEnum::QtumCoin(ref qtum) => qtum.init_withdraw(ctx, request, task_handle).await, - #[cfg(not(target_arch = "wasm32"))] MmCoinEnum::ZCoin(ref z) => z.init_withdraw(ctx, request, task_handle).await, + MmCoinEnum::EthCoin(ref eth) => eth.init_withdraw(ctx, request, task_handle).await, _ => MmError::err(WithdrawError::CoinDoesntSupportInitWithdraw { coin: self.coin.ticker().to_owned(), }), diff --git a/mm2src/coins/rpc_command/lightning/open_channel.rs b/mm2src/coins/rpc_command/lightning/open_channel.rs index f79ec0b433..fdb5b9caa9 100644 --- a/mm2src/coins/rpc_command/lightning/open_channel.rs +++ b/mm2src/coins/rpc_command/lightning/open_channel.rs @@ -146,8 +146,8 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes let platform_coin = ln_coin.platform_coin().clone(); let decimals = platform_coin.as_ref().decimals; - let my_address = platform_coin.as_ref().derivation_method.single_addr_or_err()?; - let (unspents, _) = platform_coin.get_unspent_ordered_list(my_address).await?; + let my_address = platform_coin.as_ref().derivation_method.single_addr_or_err().await?; + let (unspents, _) = platform_coin.get_unspent_ordered_list(&my_address).await?; let (value, fee_policy) = match req.amount.clone() { ChannelOpenAmount::Max => ( unspents.iter().fold(0, |sum, unspent| sum + unspent.value), @@ -161,11 +161,14 @@ pub async fn open_channel(ctx: MmArc, req: OpenChannelRequest) -> OpenChannelRes // The actual script_pubkey will replace this before signing the transaction after receiving the required // output script from the other node when the channel is accepted - let script_pubkey = - Builder::build_witness_script(&AddressHashEnum::WitnessScriptHash(Default::default())).to_bytes(); + let script_pubkey = match Builder::build_p2wsh(&AddressHashEnum::WitnessScriptHash(Default::default())) { + Ok(script) => script.to_bytes(), + Err(err) => return MmError::err(OpenChannelError::InternalError(err.to_string())), + }; let outputs = vec![TransactionOutput { value, script_pubkey }]; let mut tx_builder = UtxoTxBuilder::new(&platform_coin) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy); diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index c401853b2d..0bec5ef493 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -1,6 +1,7 @@ pub mod account_balance; pub mod get_current_mtp; pub mod get_enabled_coins; +pub mod get_estimated_fees; pub mod get_new_address; pub mod hd_account_balance_rpc_error; pub mod init_account_balance; diff --git a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs index fce69042c6..4edcd0cd55 100644 --- a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs +++ b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs @@ -2,14 +2,14 @@ use common::HttpStatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; -use crate::{lp_coinfind_or_err, MmCoinEnum}; +use crate::{coin_conf, tendermint::get_ibc_transfer_channels}; pub type IBCTransferChannelsResult = Result>; #[derive(Clone, Deserialize)] pub struct IBCTransferChannelsRequest { - pub(crate) coin: String, - pub(crate) destination_chain_registry_name: String, + pub(crate) source_coin: String, + pub(crate) destination_coin: String, } #[derive(Clone, Serialize)] @@ -42,10 +42,17 @@ pub enum IBCTransferChannelsRequestError { _0 )] UnsupportedCoin(String), + #[display( + fmt = "'chain_registry_name' was not found in coins configuration for '{}' prefix. Either update the coins configuration or use 'ibc_source_channel' in the request.", + _0 + )] + RegistryNameIsMissing(String), #[display(fmt = "Could not find '{}' registry source.", _0)] RegistrySourceCouldNotFound(String), #[display(fmt = "Transport error: {}", _0)] Transport(String), + #[display(fmt = "Could not found channel for '{}'.", _0)] + CouldNotFindChannel(String), #[display(fmt = "Internal error: {}", _0)] InternalError(String), } @@ -56,7 +63,9 @@ impl HttpStatusCode for IBCTransferChannelsRequestError { IBCTransferChannelsRequestError::UnsupportedCoin(_) | IBCTransferChannelsRequestError::NoSuchCoin(_) => { common::StatusCode::BAD_REQUEST }, - IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(_) => common::StatusCode::NOT_FOUND, + IBCTransferChannelsRequestError::CouldNotFindChannel(_) + | IBCTransferChannelsRequestError::RegistryNameIsMissing(_) + | IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(_) => common::StatusCode::NOT_FOUND, IBCTransferChannelsRequestError::Transport(_) => common::StatusCode::SERVICE_UNAVAILABLE, IBCTransferChannelsRequestError::InternalError(_) => common::StatusCode::INTERNAL_SERVER_ERROR, } @@ -64,13 +73,33 @@ impl HttpStatusCode for IBCTransferChannelsRequestError { } pub async fn ibc_transfer_channels(ctx: MmArc, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin) - .await - .map_err(|_| IBCTransferChannelsRequestError::NoSuchCoin(req.coin.clone()))?; + let source_coin_conf = coin_conf(&ctx, &req.source_coin); + let source_registry_name = source_coin_conf + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("chain_registry_name") + .map(|t| t.as_str().unwrap_or_default().to_owned()); - match coin { - MmCoinEnum::Tendermint(coin) => coin.get_ibc_transfer_channels(req).await, - MmCoinEnum::TendermintToken(token) => token.platform_coin.get_ibc_transfer_channels(req).await, - _ => MmError::err(IBCTransferChannelsRequestError::UnsupportedCoin(req.coin)), - } + let Some(source_registry_name) = source_registry_name else { + return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing(req.source_coin)); + }; + + let destination_coin_conf = coin_conf(&ctx, &req.destination_coin); + let destination_registry_name = destination_coin_conf + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("chain_registry_name") + .map(|t| t.as_str().unwrap_or_default().to_owned()); + + let Some(destination_registry_name) = destination_registry_name else { + return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing( + req.destination_coin, + )); + }; + + get_ibc_transfer_channels(source_registry_name, destination_registry_name).await } diff --git a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs deleted file mode 100644 index 037823ee66..0000000000 --- a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs +++ /dev/null @@ -1,29 +0,0 @@ -use common::Future01CompatExt; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::MmError; -use mm2_number::BigDecimal; - -use crate::{lp_coinfind_or_err, MmCoinEnum, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawResult}; - -#[derive(Clone, Deserialize)] -pub struct IBCWithdrawRequest { - pub(crate) ibc_source_channel: String, - pub(crate) from: Option, - pub(crate) coin: String, - pub(crate) to: String, - #[serde(default)] - pub(crate) amount: BigDecimal, - #[serde(default)] - pub(crate) max: bool, - pub(crate) memo: Option, - pub(crate) fee: Option, -} - -pub async fn ibc_withdraw(ctx: MmArc, req: IBCWithdrawRequest) -> WithdrawResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - match coin { - MmCoinEnum::Tendermint(coin) => coin.ibc_withdraw(req).compat().await, - MmCoinEnum::TendermintToken(token) => token.ibc_withdraw(req).compat().await, - _ => MmError::err(WithdrawError::ActionNotAllowed(req.coin)), - } -} diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs index d8211abeac..3e2b664aec 100644 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -1,14 +1,12 @@ mod ibc_chains; mod ibc_transfer_channels; -mod ibc_withdraw; pub use ibc_chains::*; pub use ibc_transfer_channels::*; -pub use ibc_withdraw::*; // Global constants for interacting with https://github.com/KomodoPlatform/chain-registry repository // using `mm2_git` crate. pub(crate) const CHAIN_REGISTRY_REPO_OWNER: &str = "KomodoPlatform"; pub(crate) const CHAIN_REGISTRY_REPO_NAME: &str = "chain-registry"; -pub(crate) const CHAIN_REGISTRY_BRANCH: &str = "master"; +pub(crate) const CHAIN_REGISTRY_BRANCH: &str = "nucl"; pub(crate) const CHAIN_REGISTRY_IBC_DIR_NAME: &str = "_IBC"; diff --git a/mm2src/coins/sia.rs b/mm2src/coins/sia.rs new file mode 100644 index 0000000000..446a507070 --- /dev/null +++ b/mm2src/coins/sia.rs @@ -0,0 +1,598 @@ +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, + TradeFee, TransactionEnum, TransactionFut}; +use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, + ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, + NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + PrivKeyBuildPolicy, PrivKeyPolicy, RawTransactionResult, RefundPaymentArgs, RefundResult, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, + SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, ValidatePaymentResult, ValidateWatcherSpendInput, + VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, + WithdrawRequest}; +use async_trait::async_trait; +use common::executor::AbortedError; +use futures::{FutureExt, TryFutureExt}; +use futures01::Future; +use keys::KeyPair; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::{BigDecimal, MmNumber}; +use rpc::v1::types::Bytes as BytesJson; +use serde_json::Value as Json; +use std::ops::Deref; +use std::sync::Arc; +use url::Url; + +pub mod address; +use address::v1_standard_address_from_pubkey; +pub mod blake2b_internal; +pub mod encoding; +pub mod http_client; +use http_client::{SiaApiClient, SiaApiClientError}; +pub mod spend_policy; + +#[derive(Clone)] +pub struct SiaCoin(SiaArc); +#[derive(Clone)] +pub struct SiaArc(Arc); + +#[derive(Debug, Display)] +pub enum SiaConfError { + #[display(fmt = "'foo' field is not found in config")] + Foo, + Bar(String), +} + +pub type SiaConfResult = Result>; + +#[derive(Debug)] +pub struct SiaCoinConf { + ticker: String, + pub foo: u32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SiaHttpConf { + pub url: Url, + pub auth: String, +} + +// TODO see https://github.com/KomodoPlatform/komodo-defi-framework/pull/2086#discussion_r1521660384 +// for additional fields needed +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SiaCoinActivationParams { + #[serde(default)] + pub tx_history: bool, + pub required_confirmations: Option, + pub gap_limit: Option, + pub http_conf: SiaHttpConf, +} + +pub struct SiaConfBuilder<'a> { + #[allow(dead_code)] + conf: &'a Json, + ticker: &'a str, +} + +impl<'a> SiaConfBuilder<'a> { + pub fn new(conf: &'a Json, ticker: &'a str) -> Self { SiaConfBuilder { conf, ticker } } + + pub fn build(&self) -> SiaConfResult { + Ok(SiaCoinConf { + ticker: self.ticker.to_owned(), + foo: 0, + }) + } +} + +// TODO see https://github.com/KomodoPlatform/komodo-defi-framework/pull/2086#discussion_r1521668313 +// for additional fields needed +pub struct SiaCoinFields { + /// SIA coin config + pub conf: SiaCoinConf, + pub priv_key_policy: PrivKeyPolicy, + /// HTTP(s) client + pub http_client: SiaApiClient, +} + +pub async fn sia_coin_from_conf_and_params( + ctx: &MmArc, + ticker: &str, + conf: &Json, + params: &SiaCoinActivationParams, + priv_key_policy: PrivKeyBuildPolicy, +) -> Result> { + let priv_key = match priv_key_policy { + PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => priv_key, + _ => return Err(SiaCoinBuildError::UnsupportedPrivKeyPolicy.into()), + }; + let key_pair = generate_keypair_from_slice(priv_key.as_slice())?; + let builder = SiaCoinBuilder::new(ctx, ticker, conf, key_pair, params); + builder.build().await +} + +pub struct SiaCoinBuilder<'a> { + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + key_pair: ed25519_dalek::Keypair, + params: &'a SiaCoinActivationParams, +} + +impl<'a> SiaCoinBuilder<'a> { + pub fn new( + ctx: &'a MmArc, + ticker: &'a str, + conf: &'a Json, + key_pair: ed25519_dalek::Keypair, + params: &'a SiaCoinActivationParams, + ) -> Self { + SiaCoinBuilder { + ctx, + ticker, + conf, + key_pair, + params, + } + } +} + +fn generate_keypair_from_slice(priv_key: &[u8]) -> Result { + let secret_key = ed25519_dalek::SecretKey::from_bytes(priv_key).map_err(SiaCoinBuildError::EllipticCurveError)?; + let public_key = ed25519_dalek::PublicKey::from(&secret_key); + Ok(ed25519_dalek::Keypair { + secret: secret_key, + public: public_key, + }) +} + +impl From for SiaCoinBuildError { + fn from(e: SiaConfError) -> Self { SiaCoinBuildError::ConfError(e) } +} + +#[derive(Debug, Display)] +pub enum SiaCoinBuildError { + ConfError(SiaConfError), + UnsupportedPrivKeyPolicy, + ClientError(SiaApiClientError), + EllipticCurveError(ed25519_dalek::ed25519::Error), +} + +impl<'a> SiaCoinBuilder<'a> { + #[allow(dead_code)] + fn ctx(&self) -> &MmArc { self.ctx } + + #[allow(dead_code)] + fn conf(&self) -> &Json { self.conf } + + fn ticker(&self) -> &str { self.ticker } + + async fn build(self) -> MmResult { + let conf = SiaConfBuilder::new(self.conf, self.ticker()).build()?; + let sia_fields = SiaCoinFields { + conf, + http_client: SiaApiClient::new(self.ticker(), self.params.http_conf.clone()) + .map_err(SiaCoinBuildError::ClientError)?, + priv_key_policy: PrivKeyPolicy::Iguana(self.key_pair), + }; + let sia_arc = SiaArc::new(sia_fields); + + Ok(SiaCoin::from(sia_arc)) + } +} + +impl Deref for SiaArc { + type Target = SiaCoinFields; + fn deref(&self) -> &SiaCoinFields { &self.0 } +} + +impl From for SiaArc { + fn from(coin: SiaCoinFields) -> SiaArc { SiaArc::new(coin) } +} + +impl From> for SiaArc { + fn from(arc: Arc) -> SiaArc { SiaArc(arc) } +} + +impl From for SiaCoin { + fn from(coin: SiaArc) -> SiaCoin { SiaCoin(coin) } +} + +impl SiaArc { + pub fn new(fields: SiaCoinFields) -> SiaArc { SiaArc(Arc::new(fields)) } + + pub fn with_arc(inner: Arc) -> SiaArc { SiaArc(inner) } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SiaCoinProtocolInfo; + +#[async_trait] +impl MmCoin for SiaCoin { + fn is_asset_chain(&self) -> bool { false } + + fn spawner(&self) -> CoinFutSpawner { unimplemented!() } + + fn get_raw_transaction(&self, _req: RawTransactionRequest) -> RawTransactionFut { unimplemented!() } + + fn get_tx_hex_by_hash(&self, _tx_hash: Vec) -> RawTransactionFut { unimplemented!() } + + fn withdraw(&self, _req: WithdrawRequest) -> WithdrawFut { unimplemented!() } + + fn decimals(&self) -> u8 { unimplemented!() } + + fn convert_to_address(&self, _from: &str, _to_address_format: Json) -> Result { unimplemented!() } + + fn validate_address(&self, _address: &str) -> ValidateAddressResult { unimplemented!() } + + fn process_history_loop(&self, _ctx: MmArc) -> Box + Send> { unimplemented!() } + + fn history_sync_status(&self) -> HistorySyncState { unimplemented!() } + + /// Get fee to be paid per 1 swap transaction + fn get_trade_fee(&self) -> Box + Send> { unimplemented!() } + + async fn get_sender_trade_fee( + &self, + _value: TradePreimageValue, + _stage: FeeApproxStage, + _include_refund_fee: bool, + ) -> TradePreimageResult { + unimplemented!() + } + + fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { unimplemented!() } + + async fn get_fee_to_send_taker_fee( + &self, + _dex_fee_amount: DexFee, + _stage: FeeApproxStage, + ) -> TradePreimageResult { + unimplemented!() + } + + fn required_confirmations(&self) -> u64 { unimplemented!() } + + fn requires_notarization(&self) -> bool { false } + + fn set_required_confirmations(&self, _confirmations: u64) { unimplemented!() } + + fn set_requires_notarization(&self, _requires_nota: bool) { unimplemented!() } + + fn swap_contract_address(&self) -> Option { unimplemented!() } + + fn fallback_swap_contract(&self) -> Option { unimplemented!() } + + fn mature_confirmations(&self) -> Option { unimplemented!() } + + fn coin_protocol_info(&self, _amount_to_receive: Option) -> Vec { Vec::new() } + + fn is_coin_protocol_supported( + &self, + _info: &Option>, + _amount_to_send: Option, + _locktime: u64, + _is_maker: bool, + ) -> bool { + true + } + + fn on_disabled(&self) -> Result<(), AbortedError> { Ok(()) } + + fn on_token_deactivated(&self, _ticker: &str) {} +} + +// TODO Alright - Dummy values for these functions allow minimal functionality to produce signatures +#[async_trait] +impl MarketCoinOps for SiaCoin { + fn ticker(&self) -> &str { &self.0.conf.ticker } + + // needs test coverage FIXME COME BACK + fn my_address(&self) -> MmResult { + let key_pair = match &self.0.priv_key_policy { + PrivKeyPolicy::Iguana(key_pair) => key_pair, + PrivKeyPolicy::Trezor => { + return Err(MyAddressError::UnexpectedDerivationMethod( + "Trezor not yet supported. Must use iguana seed.".to_string(), + ) + .into()); + }, + PrivKeyPolicy::HDWallet { .. } => { + return Err(MyAddressError::UnexpectedDerivationMethod( + "HDWallet not yet supported. Must use iguana seed.".to_string(), + ) + .into()); + }, + }; + + let address = v1_standard_address_from_pubkey(&key_pair.public); + Ok(address.to_string()) + } + + async fn get_public_key(&self) -> Result> { unimplemented!() } + + fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } + + fn sign_message(&self, _message: &str) -> SignatureResult { unimplemented!() } + + fn verify_message(&self, _signature: &str, _message: &str, _address: &str) -> VerificationResult { + unimplemented!() + } + + fn my_balance(&self) -> BalanceFut { + let fut = async move { + Ok(CoinBalance { + spendable: BigDecimal::default(), + unspendable: BigDecimal::default(), + }) + }; + Box::new(fut.boxed().compat()) + } + fn base_coin_balance(&self) -> BalanceFut { unimplemented!() } + + fn platform_ticker(&self) -> &str { "FOO" } // TODO Alright + + /// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format + fn send_raw_tx(&self, _tx: &str) -> Box + Send> { unimplemented!() } + + fn send_raw_tx_bytes(&self, _tx: &[u8]) -> Box + Send> { + unimplemented!() + } + + #[inline(always)] + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { unimplemented!() } + + fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { + unimplemented!() + } + + fn wait_for_htlc_tx_spend(&self, _args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { unimplemented!() } + + fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { + MmError::err(TxMarshalingErr::NotSupported( + "tx_enum_from_bytes is not supported for Sia coin yet.".to_string(), + )) + } + + fn current_block(&self) -> Box + Send> { + let http_client = self.0.http_client.clone(); // Clone the client + + let height_fut = async move { http_client.get_height().await.map_err(|e| e.to_string()) } + .boxed() // Make the future 'static by boxing + .compat(); // Convert to a futures 0.1-compatible future + + Box::new(height_fut) + } + + fn display_priv_key(&self) -> Result { unimplemented!() } + + fn min_tx_amount(&self) -> BigDecimal { unimplemented!() } + + fn min_trading_vol(&self) -> MmNumber { unimplemented!() } + + fn is_trezor(&self) -> bool { self.0.priv_key_policy.is_trezor() } +} + +#[async_trait] +impl SwapOps for SiaCoin { + fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } + + fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } + + fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } + + async fn send_maker_spends_taker_payment( + &self, + _maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + unimplemented!() + } + + async fn send_taker_spends_maker_payment( + &self, + _taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + unimplemented!() + } + + async fn send_taker_refunds_payment( + &self, + _taker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { + unimplemented!() + } + + async fn send_maker_refunds_payment( + &self, + _maker_refunds_payment_args: RefundPaymentArgs<'_>, + ) -> TransactionResult { + unimplemented!() + } + + fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } + + async fn validate_maker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } + + async fn validate_taker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } + + fn check_if_my_payment_sent( + &self, + _if_my_payment_sent_args: CheckIfMyPaymentSentArgs, + ) -> Box, Error = String> + Send> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_my( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + async fn search_for_swap_tx_spend_other( + &self, + _: SearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!() + } + + fn check_tx_signed_by_pub(&self, _tx: &[u8], _expected_pub: &[u8]) -> Result> { + unimplemented!(); + } + + async fn extract_secret( + &self, + _secret_hash: &[u8], + _spend_tx: &[u8], + _watcher_reward: bool, + ) -> Result, String> { + unimplemented!() + } + + fn is_auto_refundable(&self) -> bool { false } + + async fn wait_for_htlc_refund(&self, _tx: &[u8], _locktime: u64) -> RefundResult<()> { unimplemented!() } + + fn negotiate_swap_contract_addr( + &self, + _other_side_address: Option<&[u8]>, + ) -> Result, MmError> { + unimplemented!() + } + + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { unimplemented!() } + + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { unimplemented!() } + + fn can_refund_htlc(&self, _locktime: u64) -> Box + Send + '_> { + unimplemented!() + } + + fn validate_other_pubkey(&self, _raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { unimplemented!() } + + async fn maker_payment_instructions( + &self, + _args: PaymentInstructionArgs<'_>, + ) -> Result>, MmError> { + unimplemented!() + } + + async fn taker_payment_instructions( + &self, + _args: PaymentInstructionArgs<'_>, + ) -> Result>, MmError> { + unimplemented!() + } + + fn validate_maker_payment_instructions( + &self, + _instructions: &[u8], + _args: PaymentInstructionArgs, + ) -> Result> { + unimplemented!() + } + + fn validate_taker_payment_instructions( + &self, + _instructions: &[u8], + _args: PaymentInstructionArgs, + ) -> Result> { + unimplemented!() + } +} + +#[async_trait] +impl TakerSwapMakerCoin for SiaCoin { + async fn on_taker_payment_refund_start(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_taker_payment_refund_success(&self, _maker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl MakerSwapTakerCoin for SiaCoin { + async fn on_maker_payment_refund_start(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } + + async fn on_maker_payment_refund_success(&self, _taker_payment: &[u8]) -> RefundResult<()> { Ok(()) } +} + +#[async_trait] +impl WatcherOps for SiaCoin { + fn send_maker_payment_spend_preimage(&self, _input: SendMakerPaymentSpendPreimageInput) -> TransactionFut { + unimplemented!(); + } + + fn send_taker_payment_refund_preimage(&self, _watcher_refunds_payment_args: RefundPaymentArgs) -> TransactionFut { + unimplemented!(); + } + + fn create_taker_payment_refund_preimage( + &self, + _taker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_contract_address: &Option, + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn create_maker_payment_spend_preimage( + &self, + _maker_payment_tx: &[u8], + _time_lock: u64, + _maker_pub: &[u8], + _secret_hash: &[u8], + _swap_unique_data: &[u8], + ) -> TransactionFut { + unimplemented!(); + } + + fn watcher_validate_taker_fee(&self, _input: WatcherValidateTakerFeeInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn watcher_validate_taker_payment(&self, _input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { + unimplemented!(); + } + + fn taker_validates_payment_spend_or_refund(&self, _input: ValidateWatcherSpendInput) -> ValidatePaymentFut<()> { + unimplemented!() + } + + async fn watcher_search_for_swap_tx_spend( + &self, + _input: WatcherSearchForSwapTxSpendInput<'_>, + ) -> Result, String> { + unimplemented!(); + } + + async fn get_taker_watcher_reward( + &self, + _other_coin: &MmCoinEnum, + _coin_amount: Option, + _other_coin_amount: Option, + _reward_amount: Option, + _wait_until: u64, + ) -> Result> { + unimplemented!() + } + + async fn get_maker_watcher_reward( + &self, + _other_coin: &MmCoinEnum, + _reward_amount: Option, + _wait_until: u64, + ) -> Result, MmError> { + unimplemented!() + } +} diff --git a/mm2src/coins/sia/address.rs b/mm2src/coins/sia/address.rs new file mode 100644 index 0000000000..5218a08bc8 --- /dev/null +++ b/mm2src/coins/sia/address.rs @@ -0,0 +1,167 @@ +use crate::sia::blake2b_internal::standard_unlock_hash; +use blake2b_simd::Params; +use ed25519_dalek::PublicKey; +use hex::FromHexError; +use rpc::v1::types::H256; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use std::fmt; +use std::str::FromStr; + +// TODO this could probably include the checksum within the data type +// generating the checksum on the fly is how Sia Go does this however +#[derive(Debug, Clone, PartialEq)] +pub struct Address(pub H256); + +impl Address { + pub fn str_without_prefix(&self) -> String { + let bytes = self.0 .0.as_ref(); + let checksum = blake2b_checksum(bytes); + format!("{}{}", hex::encode(bytes), hex::encode(checksum)) + } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "addr:{}", self.str_without_prefix()) } +} + +impl fmt::Display for ParseAddressError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Failed to parse Address: {:?}", self) } +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum ParseAddressError { + #[serde(rename = "Address must begin with addr: prefix")] + MissingPrefix, + InvalidHexEncoding(String), + InvalidChecksum, + InvalidLength, + // Add other error kinds as needed +} + +impl From for ParseAddressError { + fn from(e: FromHexError) -> Self { ParseAddressError::InvalidHexEncoding(e.to_string()) } +} + +impl FromStr for Address { + type Err = ParseAddressError; + + fn from_str(s: &str) -> Result { + if !s.starts_with("addr:") { + return Err(ParseAddressError::MissingPrefix); + } + + let without_prefix = &s[5..]; + if without_prefix.len() != (32 + 6) * 2 { + return Err(ParseAddressError::InvalidLength); + } + + let (address_hex, checksum_hex) = without_prefix.split_at(32 * 2); + + let address_bytes: [u8; 32] = hex::decode(address_hex) + .map_err(ParseAddressError::from)? + .try_into() + .expect("length is 32 bytes"); + + let checksum = hex::decode(checksum_hex).map_err(ParseAddressError::from)?; + let checksum_bytes: [u8; 6] = checksum.try_into().expect("length is 6 bytes"); + + if checksum_bytes != blake2b_checksum(&address_bytes) { + return Err(ParseAddressError::InvalidChecksum); + } + + Ok(Address(H256::from(address_bytes))) + } +} + +// Sia uses the first 6 bytes of blake2b(preimage) appended +// to address as checksum +fn blake2b_checksum(preimage: &[u8]) -> [u8; 6] { + let hash = Params::new().hash_length(32).to_state().update(preimage).finalize(); + hash.as_array()[0..6].try_into().expect("array is 64 bytes long") +} + +pub fn v1_standard_address_from_pubkey(pubkey: &PublicKey) -> Address { + let hash = standard_unlock_hash(pubkey); + Address(hash) +} + +#[test] +fn test_v1_standard_address_from_pubkey() { + let pubkey = PublicKey::from_bytes( + &hex::decode("8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c").unwrap(), + ) + .unwrap(); + let address = v1_standard_address_from_pubkey(&pubkey); + assert_eq!( + format!("{}", address), + "addr:c959f9b423b662c36ee58057b8157acedb4095cfeb7926e4ba44cd9ee1f49a5b7803c7501a7b" + ) +} + +#[test] +fn test_blake2b_checksum() { + let checksum = + blake2b_checksum(&hex::decode("591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a884").unwrap()); + let expected: [u8; 6] = hex::decode("0be0653e411f").unwrap().try_into().unwrap(); + assert_eq!(checksum, expected); +} + +#[test] +fn test_address_display() { + let address = Address("591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a884".into()); + let address_str = "addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f"; + assert_eq!(format!("{}", address), address_str); +} + +#[test] +fn test_address_fromstr() { + let address1 = Address("591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a884".into()); + + let address2 = + Address::from_str("addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f").unwrap(); + assert_eq!(address1, address2); +} + +#[test] +fn test_address_fromstr_bad_length() { + let address = Address::from_str("addr:dead"); + assert!(matches!(address, Err(ParseAddressError::InvalidLength))); +} + +#[test] +fn test_address_fromstr_odd_length() { + let address = Address::from_str("addr:f00"); + assert!(matches!(address, Err(ParseAddressError::InvalidLength))); +} + +#[test] +fn test_address_fromstr_invalid_hex() { + let address = + Address::from_str("addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e41gg"); + assert!(matches!(address, Err(ParseAddressError::InvalidHexEncoding(_)))); +} + +#[test] +fn test_address_fromstr_missing_prefix() { + let address = Address::from_str("591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e41gg"); + assert!(matches!(address, Err(ParseAddressError::MissingPrefix))); +} + +#[test] +fn test_address_fromstr_invalid_checksum() { + let address = + Address::from_str("addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a884ffffffffffff"); + assert!(matches!(address, Err(ParseAddressError::InvalidChecksum))); +} + +#[test] +fn test_address_str_without_prefix() { + let address = + Address::from_str("addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f").unwrap(); + + assert_eq!( + address.str_without_prefix(), + "591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f" + ); +} diff --git a/mm2src/coins/sia/blake2b_internal.rs b/mm2src/coins/sia/blake2b_internal.rs new file mode 100644 index 0000000000..39c4f7c82b --- /dev/null +++ b/mm2src/coins/sia/blake2b_internal.rs @@ -0,0 +1,326 @@ +use blake2b_simd::Params; +use ed25519_dalek::PublicKey; +use rpc::v1::types::H256; +use std::default::Default; + +#[cfg(test)] use hex; +#[cfg(test)] use std::convert::TryInto; + +const LEAF_HASH_PREFIX: [u8; 1] = [0u8]; +const NODE_HASH_PREFIX: [u8; 1] = [1u8]; + +pub const ED25519_IDENTIFIER: [u8; 16] = [ + 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, +]; + +// Precomputed hash values used for all standard v1 addresses +// a standard address has 1 ed25519 public key, requires 1 signature and has a timelock of 0 +// https://github.com/SiaFoundation/core/blob/b5b08cde6b7d0f1b3a6f09b8aa9d0b817e769efb/types/hash.go#L94 +const STANDARD_TIMELOCK_BLAKE2B_HASH: [u8; 32] = [ + 0x51, 0x87, 0xb7, 0xa8, 0x02, 0x1b, 0xf4, 0xf2, 0xc0, 0x04, 0xea, 0x3a, 0x54, 0xcf, 0xec, 0xe1, 0x75, 0x4f, 0x11, + 0xc7, 0x62, 0x4d, 0x23, 0x63, 0xc7, 0xf4, 0xcf, 0x4f, 0xdd, 0xd1, 0x44, 0x1e, +]; + +const STANDARD_SIGS_REQUIRED_BLAKE2B_HASH: [u8; 32] = [ + 0xb3, 0x60, 0x10, 0xeb, 0x28, 0x5c, 0x15, 0x4a, 0x8c, 0xd6, 0x30, 0x84, 0xac, 0xbe, 0x7e, 0xac, 0x0c, 0x4d, 0x62, + 0x5a, 0xb4, 0xe1, 0xa7, 0x6e, 0x62, 0x4a, 0x87, 0x98, 0xcb, 0x63, 0x49, 0x7b, +]; + +#[derive(Debug, PartialEq)] +pub struct Accumulator { + trees: [H256; 64], + num_leaves: u64, +} + +impl Default for Accumulator { + fn default() -> Self { + Accumulator { + trees: [H256::default(); 64], // Initialize all bytes to zero + num_leaves: 0, + } + } +} + +impl Accumulator { + // Check if there is a tree at the given height + fn has_tree_at_height(&self, height: u64) -> bool { self.num_leaves & (1 << height) != 0 } + + // Add a leaf to the accumulator + pub fn add_leaf(&mut self, h: H256) { + let mut i = 0; + let mut new_hash = h; + while self.has_tree_at_height(i) { + new_hash = hash_blake2b_pair(&NODE_HASH_PREFIX, &self.trees[i as usize].0, &new_hash.0); + i += 1; + } + self.trees[i as usize] = new_hash; + self.num_leaves += 1; + } + + // Calulate the root hash of the Merkle tree + pub fn root(&self) -> H256 { + // trailing_zeros determines the height Merkle tree accumulator where the current lowest single leaf is located + let i = self.num_leaves.trailing_zeros() as u64; + if i == 64 { + return H256::default(); // Return all zeros if no leaves + } + let mut root = self.trees[i as usize]; + for j in i + 1..64 { + if self.has_tree_at_height(j) { + root = hash_blake2b_pair(&NODE_HASH_PREFIX, &self.trees[j as usize].0, &root.0); + } + } + root + } +} + +pub fn sigs_required_leaf(sigs_required: u64) -> H256 { + let sigs_required_array: [u8; 8] = sigs_required.to_le_bytes(); + let mut combined = Vec::new(); + combined.extend_from_slice(&LEAF_HASH_PREFIX); + combined.extend_from_slice(&sigs_required_array); + + hash_blake2b_single(&combined) +} + +// public key leaf is +// blake2b(leafHashPrefix + 16_byte_ascii_algorithm_identifier + public_key_length_u64 + public_key) +pub fn public_key_leaf(pubkey: &PublicKey) -> H256 { + let mut combined = Vec::new(); + combined.extend_from_slice(&LEAF_HASH_PREFIX); + combined.extend_from_slice(&ED25519_IDENTIFIER); + combined.extend_from_slice(&32u64.to_le_bytes()); + combined.extend_from_slice(pubkey.as_bytes()); + hash_blake2b_single(&combined) +} + +pub fn timelock_leaf(timelock: u64) -> H256 { + let timelock: [u8; 8] = timelock.to_le_bytes(); + let mut combined = Vec::new(); + combined.extend_from_slice(&LEAF_HASH_PREFIX); + combined.extend_from_slice(&timelock); + + hash_blake2b_single(&combined) +} + +// https://github.com/SiaFoundation/core/blob/b5b08cde6b7d0f1b3a6f09b8aa9d0b817e769efb/types/hash.go#L96 +// An UnlockHash is the Merkle root of UnlockConditions. Since the standard +// UnlockConditions use a single public key, the Merkle tree is: +// +// ┌─────────┴──────────┐ +// ┌─────┴─────┐ │ +// timelock pubkey sigsrequired +pub fn standard_unlock_hash(pubkey: &PublicKey) -> H256 { + let pubkey_leaf = public_key_leaf(pubkey); + let timelock_pubkey_node = hash_blake2b_pair(&NODE_HASH_PREFIX, &STANDARD_TIMELOCK_BLAKE2B_HASH, &pubkey_leaf.0); + hash_blake2b_pair( + &NODE_HASH_PREFIX, + &timelock_pubkey_node.0, + &STANDARD_SIGS_REQUIRED_BLAKE2B_HASH, + ) +} + +pub fn hash_blake2b_single(preimage: &[u8]) -> H256 { + let hash = Params::new().hash_length(32).to_state().update(preimage).finalize(); + let ret_array = hash.as_array(); + ret_array[0..32].into() +} + +fn hash_blake2b_pair(prefix: &[u8], leaf1: &[u8], leaf2: &[u8]) -> H256 { + let hash = Params::new() + .hash_length(32) + .to_state() + .update(prefix) + .update(leaf1) + .update(leaf2) + .finalize(); + let ret_array = hash.as_array(); + ret_array[0..32].into() +} + +#[test] +fn test_accumulator_new() { + let default_accumulator = Accumulator::default(); + + let expected = Accumulator { + trees: [H256::from("0000000000000000000000000000000000000000000000000000000000000000"); 64], + num_leaves: 0, + }; + assert_eq!(default_accumulator, expected) +} + +#[test] +fn test_accumulator_root_default() { assert_eq!(Accumulator::default().root(), H256::default()) } + +#[test] +fn test_accumulator_root() { + let mut accumulator = Accumulator::default(); + + let timelock_leaf = timelock_leaf(0u64); + accumulator.add_leaf(timelock_leaf); + + let pubkey = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let pubkey_leaf = public_key_leaf(&pubkey); + accumulator.add_leaf(pubkey_leaf); + + let sigs_required_leaf = sigs_required_leaf(1u64); + accumulator.add_leaf(sigs_required_leaf); + + let expected = H256::from("72b0762b382d4c251af5ae25b6777d908726d75962e5224f98d7f619bb39515d"); + assert_eq!(accumulator.root(), expected); +} + +#[test] +fn test_accumulator_add_leaf_standard_unlock_hash() { + let mut accumulator = Accumulator::default(); + + let pubkey = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + + let pubkey_leaf = public_key_leaf(&pubkey); + let timelock_leaf = timelock_leaf(0u64); + let sigs_required_leaf = sigs_required_leaf(1u64); + + accumulator.add_leaf(timelock_leaf); + accumulator.add_leaf(pubkey_leaf); + accumulator.add_leaf(sigs_required_leaf); + + let root = accumulator.root(); + let expected = H256::from("72b0762b382d4c251af5ae25b6777d908726d75962e5224f98d7f619bb39515d"); + assert_eq!(root, expected) +} + +#[test] +fn test_accumulator_add_leaf_2of2_multisig_unlock_hash() { + let mut accumulator = Accumulator::default(); + + let pubkey1 = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let pubkey2 = PublicKey::from_bytes( + &hex::decode("0101010000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + + let pubkey1_leaf = public_key_leaf(&pubkey1); + let pubkey2_leaf = public_key_leaf(&pubkey2); + + let timelock_leaf = timelock_leaf(0u64); + let sigs_required_leaf = sigs_required_leaf(2u64); + + accumulator.add_leaf(timelock_leaf); + accumulator.add_leaf(pubkey1_leaf); + accumulator.add_leaf(pubkey2_leaf); + accumulator.add_leaf(sigs_required_leaf); + + let root = accumulator.root(); + let expected = H256::from("1e94357817d236167e54970a8c08bbd41b37bfceeeb52f6c1ce6dd01d50ea1e7"); + assert_eq!(root, expected) +} + +#[test] +fn test_accumulator_add_leaf_1of2_multisig_unlock_hash() { + let mut accumulator = Accumulator::default(); + + let pubkey1 = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let pubkey2 = PublicKey::from_bytes( + &hex::decode("0101010000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + + let pubkey1_leaf = public_key_leaf(&pubkey1); + let pubkey2_leaf = public_key_leaf(&pubkey2); + + let timelock_leaf = timelock_leaf(0u64); + let sigs_required_leaf = sigs_required_leaf(1u64); + + accumulator.add_leaf(timelock_leaf); + accumulator.add_leaf(pubkey1_leaf); + accumulator.add_leaf(pubkey2_leaf); + accumulator.add_leaf(sigs_required_leaf); + + let root = accumulator.root(); + let expected = H256::from("d7f84e3423da09d111a17f64290c8d05e1cbe4cab2b6bed49e3a4d2f659f0585"); + assert_eq!(root, expected) +} + +#[test] +fn test_standard_unlock_hash() { + let pubkey = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + + let hash = standard_unlock_hash(&pubkey); + let expected = H256::from("72b0762b382d4c251af5ae25b6777d908726d75962e5224f98d7f619bb39515d"); + assert_eq!(hash, expected) +} + +#[test] +fn test_hash_blake2b_pair() { + let left: [u8; 32] = hex::decode("cdcce3978a58ceb6c8480d218646db4eae85eb9ea9c2f5138fbacb4ce2c701e3") + .unwrap() + .try_into() + .unwrap(); + let right: [u8; 32] = hex::decode("b36010eb285c154a8cd63084acbe7eac0c4d625ab4e1a76e624a8798cb63497b") + .unwrap() + .try_into() + .unwrap(); + + let hash = hash_blake2b_pair(&NODE_HASH_PREFIX, &left, &right); + let expected = H256::from("72b0762b382d4c251af5ae25b6777d908726d75962e5224f98d7f619bb39515d"); + assert_eq!(hash, expected) +} + +#[test] +fn test_create_ed25519_identifier() { + let mut ed25519_identifier: [u8; 16] = [0; 16]; + + let bytes = "ed25519".as_bytes(); + for (i, &byte) in bytes.iter().enumerate() { + ed25519_identifier[i] = byte; + } + assert_eq!(ed25519_identifier, ED25519_IDENTIFIER); +} + +#[test] +fn test_timelock_leaf() { + let hash = timelock_leaf(0); + let expected = H256::from(STANDARD_TIMELOCK_BLAKE2B_HASH); + assert_eq!(hash, expected) +} + +#[test] +fn test_sigs_required_leaf() { + let hash = sigs_required_leaf(1u64); + let expected = H256::from(STANDARD_SIGS_REQUIRED_BLAKE2B_HASH); + assert_eq!(hash, expected) +} + +#[test] +fn test_hash_blake2b_single() { + let hash = hash_blake2b_single(&hex::decode("006564323535313900000000000000000020000000000000000102030000000000000000000000000000000000000000000000000000000000").unwrap()); + let expected = H256::from("21ce940603a2ee3a283685f6bfb4b122254894fd1ed3eb59434aadbf00c75d5b"); + assert_eq!(hash, expected) +} + +#[test] +fn test_public_key_leaf() { + let pubkey = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + + let hash = public_key_leaf(&pubkey); + let expected = H256::from("21ce940603a2ee3a283685f6bfb4b122254894fd1ed3eb59434aadbf00c75d5b"); + assert_eq!(hash, expected) +} diff --git a/mm2src/coins/sia/encoding.rs b/mm2src/coins/sia/encoding.rs new file mode 100644 index 0000000000..5b6516101a --- /dev/null +++ b/mm2src/coins/sia/encoding.rs @@ -0,0 +1,119 @@ +use crate::sia::blake2b_internal::hash_blake2b_single; +use rpc::v1::types::H256; + +// https://github.com/SiaFoundation/core/blob/092850cc52d3d981b19c66cd327b5d945b3c18d3/types/encoding.go#L16 +// TODO go implementation limits this to 1024 bytes, should we? +#[derive(Default)] +pub struct Encoder { + pub buffer: Vec, +} + +impl Encoder { + pub fn reset(&mut self) { self.buffer.clear(); } + + /// writes a length-prefixed []byte to the underlying stream. + pub fn write_len_prefixed_bytes(&mut self, data: &[u8]) { + self.buffer.extend_from_slice(&data.len().to_le_bytes()); + self.buffer.extend_from_slice(data); + } + + pub fn write_slice(&mut self, data: &[u8]) { self.buffer.extend_from_slice(data); } + + pub fn write_u8(&mut self, u: u8) { self.buffer.extend_from_slice(&[u]) } + + pub fn write_u64(&mut self, u: u64) { self.buffer.extend_from_slice(&u.to_le_bytes()); } + + pub fn write_distinguisher(&mut self, p: &str) { self.buffer.extend_from_slice(format!("sia/{}|", p).as_bytes()); } + + pub fn write_bool(&mut self, b: bool) { self.buffer.push(b as u8) } + + pub fn hash(&self) -> H256 { hash_blake2b_single(&self.buffer) } +} + +#[test] +fn test_encoder_default_hash() { + assert_eq!( + Encoder::default().hash(), + H256::from("0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8") + ) +} + +#[test] +fn test_encoder_write_bytes() { + let mut encoder = Encoder::default(); + encoder.write_len_prefixed_bytes(&[1, 2, 3, 4]); + assert_eq!( + encoder.hash(), + H256::from("d4a72b52e2e1f40e20ee40ea6d5080a1b1f76164786defbb7691a4427f3388f5") + ); +} + +#[test] +fn test_encoder_write_u8() { + let mut encoder = Encoder::default(); + encoder.write_u8(1); + assert_eq!( + encoder.hash(), + H256::from("ee155ace9c40292074cb6aff8c9ccdd273c81648ff1149ef36bcea6ebb8a3e25") + ); +} + +#[test] +fn test_encoder_write_u64() { + let mut encoder = Encoder::default(); + encoder.write_u64(1); + assert_eq!( + encoder.hash(), + H256::from("1dbd7d0b561a41d23c2a469ad42fbd70d5438bae826f6fd607413190c37c363b") + ); +} + +#[test] +fn test_encoder_write_distiguisher() { + let mut encoder = Encoder::default(); + encoder.write_distinguisher("test"); + assert_eq!( + encoder.hash(), + H256::from("25fb524721bf98a9a1233a53c40e7e198971b003bf23c24f59d547a1bb837f9c") + ); +} + +#[test] +fn test_encoder_write_bool() { + let mut encoder = Encoder::default(); + encoder.write_bool(true); + assert_eq!( + encoder.hash(), + H256::from("ee155ace9c40292074cb6aff8c9ccdd273c81648ff1149ef36bcea6ebb8a3e25") + ); +} + +#[test] +fn test_encoder_reset() { + let mut encoder = Encoder::default(); + encoder.write_bool(true); + assert_eq!( + encoder.hash(), + H256::from("ee155ace9c40292074cb6aff8c9ccdd273c81648ff1149ef36bcea6ebb8a3e25") + ); + + encoder.reset(); + encoder.write_bool(false); + assert_eq!( + encoder.hash(), + H256::from("03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314") + ); +} + +#[test] +fn test_encoder_complex() { + let mut encoder = Encoder::default(); + encoder.write_distinguisher("test"); + encoder.write_bool(true); + encoder.write_u8(1); + encoder.write_len_prefixed_bytes(&[1, 2, 3, 4]); + assert_eq!( + encoder.hash(), + H256::from("b66d7a9bef9fb303fe0e41f6b5c5af410303e428c4ff9231f6eb381248693221") + ); +} diff --git a/mm2src/coins/sia/http_client.rs b/mm2src/coins/sia/http_client.rs new file mode 100644 index 0000000000..774dcd08e1 --- /dev/null +++ b/mm2src/coins/sia/http_client.rs @@ -0,0 +1,208 @@ +use crate::sia::address::Address; +use crate::sia::SiaHttpConf; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine as _; // required for .encode() method +use core::fmt::Display; +use core::time::Duration; +use mm2_number::MmNumber; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use reqwest::{Client, Error, Url}; +use serde::de::DeserializeOwned; +use std::ops::Deref; +use std::sync::Arc; + +#[cfg(test)] use std::str::FromStr; + +const ENDPOINT_CONSENSUS_TIP: &str = "api/consensus/tip"; + +/// HTTP(s) client for Sia-protocol coins +#[derive(Debug)] +pub struct SiaHttpClientImpl { + /// Name of coin the http client is intended to work with + pub coin_ticker: String, + /// The uri to send requests to + pub uri: String, + /// Value of Authorization header password, e.g. "Basic base64(:password)" + pub auth: String, +} + +#[derive(Clone, Debug)] +pub struct SiaApiClient(pub Arc); + +impl Deref for SiaApiClient { + type Target = SiaApiClientImpl; + fn deref(&self) -> &SiaApiClientImpl { &self.0 } +} + +impl SiaApiClient { + pub fn new(_coin_ticker: &str, http_conf: SiaHttpConf) -> Result { + let new_arc = SiaApiClientImpl::new(http_conf.url, &http_conf.auth)?; + Ok(SiaApiClient(Arc::new(new_arc))) + } +} + +#[derive(Debug)] +pub struct SiaApiClientImpl { + client: Client, + base_url: Url, +} + +// this is neccesary to show the URL in error messages returned to the user +// this can be removed in favor of using ".with_url()" once reqwest is updated to v0.11.23 +#[derive(Debug)] +pub struct ReqwestErrorWithUrl { + error: Error, + url: Url, +} + +impl Display for ReqwestErrorWithUrl { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Error: {}, URL: {}", self.error, self.url) + } +} + +#[derive(Debug, Display)] +pub enum SiaApiClientError { + Timeout(String), + BuildError(String), + ApiUnreachable(String), + ReqwestError(ReqwestErrorWithUrl), + UrlParse(url::ParseError), +} + +impl From for String { + fn from(e: SiaApiClientError) -> Self { format!("{:?}", e) } +} + +async fn fetch_and_parse(client: &Client, url: Url) -> Result { + client + .get(url.clone()) + .send() + .await + .map_err(|e| { + SiaApiClientError::ReqwestError(ReqwestErrorWithUrl { + error: e, + url: url.clone(), + }) + })? + .json::() + .await + .map_err(|e| SiaApiClientError::ReqwestError(ReqwestErrorWithUrl { error: e, url })) +} + +// https://github.com/SiaFoundation/core/blob/4e46803f702891e7a83a415b7fcd7543b13e715e/types/types.go#L181 +#[derive(Deserialize, Serialize, Debug)] +pub struct GetConsensusTipResponse { + pub height: u64, + pub id: String, // TODO this can match "BlockID" type +} + +// https://github.com/SiaFoundation/walletd/blob/9574e69ff0bf84de1235b68e78db2a41d5e27516/api/api.go#L36 +// https://github.com/SiaFoundation/walletd/blob/9574e69ff0bf84de1235b68e78db2a41d5e27516/wallet/wallet.go#L25 +#[derive(Deserialize, Serialize, Debug)] +pub struct GetAddressesBalanceResponse { + pub siacoins: MmNumber, + #[serde(rename = "immatureSiacoins")] + pub immature_siacoins: MmNumber, + pub siafunds: u64, +} + +impl SiaApiClientImpl { + fn new(base_url: Url, password: &str) -> Result { + let mut headers = HeaderMap::new(); + let auth_value = format!("Basic {}", BASE64.encode(format!(":{}", password))); + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&auth_value).map_err(|e| SiaApiClientError::BuildError(e.to_string()))?, + ); + + let client = Client::builder() + .default_headers(headers) + .timeout(Duration::from_secs(10)) // TODO make this configurable + .build() + .map_err(|e| { + SiaApiClientError::ReqwestError(ReqwestErrorWithUrl { + error: e, + url: base_url.clone(), + }) + })?; + Ok(SiaApiClientImpl { client, base_url }) + } + + pub async fn get_consensus_tip(&self) -> Result { + let base_url = self.base_url.clone(); + let endpoint_url = base_url + .join(ENDPOINT_CONSENSUS_TIP) + .map_err(SiaApiClientError::UrlParse)?; + + fetch_and_parse::(&self.client, endpoint_url).await + } + + pub async fn get_addresses_balance( + &self, + address: &Address, + ) -> Result { + self.get_addresses_balance_str(&address.str_without_prefix()).await + } + + // use get_addresses_balance whenever possible to rely on Address deserialization + pub async fn get_addresses_balance_str( + &self, + address: &str, + ) -> Result { + let base_url = self.base_url.clone(); + + let endpoint_path = format!("api/addresses/{}/balance", address); + let endpoint_url = base_url.join(&endpoint_path).map_err(SiaApiClientError::UrlParse)?; + + fetch_and_parse::(&self.client, endpoint_url).await + } + + pub async fn get_height(&self) -> Result { + let resp = self.get_consensus_tip().await?; + Ok(resp.height) + } +} + +#[tokio::test] +#[ignore] +async fn test_api_client_timeout() { + let api_client = SiaApiClientImpl::new(Url::parse("http://foo").unwrap(), "password").unwrap(); + let result = api_client.get_consensus_tip().await; + assert!(matches!(result, Err(SiaApiClientError::Timeout(_)))); +} + +// TODO all of the following must be adapted to use Docker Sia node +#[tokio::test] +#[ignore] +async fn test_api_client_invalid_auth() { + let api_client = SiaApiClientImpl::new(Url::parse("http://127.0.0.1:9980").unwrap(), "password").unwrap(); + let result = api_client.get_consensus_tip().await; + assert!(matches!(result, Err(SiaApiClientError::BuildError(_)))); +} + +// TODO must be adapted to use Docker Sia node +#[tokio::test] +#[ignore] +async fn test_api_client() { + let api_client = SiaApiClientImpl::new(Url::parse("http://127.0.0.1:9980").unwrap(), "password").unwrap(); + let _result = api_client.get_consensus_tip().await.unwrap(); +} + +#[tokio::test] +#[ignore] +async fn test_api_get_addresses_balance() { + let api_client = SiaApiClientImpl::new(Url::parse("http://127.0.0.1:9980").unwrap(), "password").unwrap(); + let address = + Address::from_str("addr:591fcf237f8854b5653d1ac84ae4c107b37f148c3c7b413f292d48db0c25a8840be0653e411f").unwrap(); + let result = api_client.get_addresses_balance(&address).await.unwrap(); + println!("ret {:?}", result); +} + +#[tokio::test] +#[ignore] +async fn test_api_get_addresses_balance_invalid() { + let api_client = SiaApiClientImpl::new(Url::parse("http://127.0.0.1:9980").unwrap(), "password").unwrap(); + let result = api_client.get_addresses_balance_str("what").await.unwrap(); + println!("ret {:?}", result); +} diff --git a/mm2src/coins/sia/spend_policy.rs b/mm2src/coins/sia/spend_policy.rs new file mode 100644 index 0000000000..6c28f7250a --- /dev/null +++ b/mm2src/coins/sia/spend_policy.rs @@ -0,0 +1,322 @@ +use crate::sia::address::Address; +use crate::sia::blake2b_internal::{public_key_leaf, sigs_required_leaf, standard_unlock_hash, timelock_leaf, + Accumulator, ED25519_IDENTIFIER}; +use crate::sia::encoding::Encoder; +use ed25519_dalek::PublicKey; +use rpc::v1::types::H256; + +#[cfg(test)] use std::str::FromStr; + +const POLICY_VERSION: u8 = 1u8; + +#[derive(Debug, Clone)] +pub enum SpendPolicy { + Above(u64), + After(u64), + PublicKey(PublicKey), + Hash(H256), + Threshold(PolicyTypeThreshold), + Opaque(Address), + UnlockConditions(PolicyTypeUnlockConditions), // For v1 compatibility +} + +impl SpendPolicy { + pub fn to_u8(&self) -> u8 { + match self { + SpendPolicy::Above(_) => 1, + SpendPolicy::After(_) => 2, + SpendPolicy::PublicKey(_) => 3, + SpendPolicy::Hash(_) => 4, + SpendPolicy::Threshold(_) => 5, + SpendPolicy::Opaque(_) => 6, + SpendPolicy::UnlockConditions(_) => 7, + } + } + + pub fn encode(&self) -> Encoder { + let mut encoder = Encoder::default(); + encoder.write_u8(POLICY_VERSION); + encoder.write_slice(&self.encode_wo_prefix().buffer); + encoder + } + + pub fn encode_wo_prefix(&self) -> Encoder { + let mut encoder = Encoder::default(); + let opcode = self.to_u8(); + match self { + SpendPolicy::Above(height) => { + encoder.write_u8(opcode); + encoder.write_u64(*height); + }, + SpendPolicy::After(time) => { + encoder.write_u8(opcode); + encoder.write_u64(*time); + }, + SpendPolicy::PublicKey(pubkey) => { + encoder.write_u8(opcode); + encoder.write_slice(&pubkey.to_bytes()); + }, + SpendPolicy::Hash(hash) => { + encoder.write_u8(opcode); + encoder.write_slice(&hash.0); + }, + SpendPolicy::Threshold(PolicyTypeThreshold { n, of }) => { + encoder.write_u8(opcode); + encoder.write_u8(*n); + encoder.write_u8(of.len() as u8); + for policy in of { + encoder.write_slice(&policy.encode_wo_prefix().buffer); + } + }, + SpendPolicy::Opaque(p) => { + encoder.write_u8(opcode); + encoder.write_slice(&p.0 .0); + }, + SpendPolicy::UnlockConditions(PolicyTypeUnlockConditions(unlock_condition)) => { + encoder.write_u8(opcode); + encoder.write_u64(unlock_condition.timelock); + encoder.write_u64(unlock_condition.pubkeys.len() as u64); + for pubkey in &unlock_condition.pubkeys { + encoder.write_slice(&ED25519_IDENTIFIER); + encoder.write_slice(&pubkey.to_bytes()); + } + encoder.write_u64(unlock_condition.sigs_required); + }, + } + encoder + } + + fn address(&self) -> Address { + if let SpendPolicy::UnlockConditions(PolicyTypeUnlockConditions(unlock_condition)) = self { + return unlock_condition.address(); + } + let mut encoder = Encoder::default(); + encoder.write_distinguisher("address"); + + // if self is a threshold policy, we need to convert all of its subpolicies to opaque + let mut new_policy = self.clone(); + if let SpendPolicy::Threshold(ref mut p) = new_policy { + p.of = p.of.iter().map(SpendPolicy::opaque).collect(); + } + + let encoded_policy = new_policy.encode(); + encoder.write_slice(&encoded_policy.buffer); + Address(encoder.hash()) + } + + pub fn above(height: u64) -> Self { SpendPolicy::Above(height) } + + pub fn after(time: u64) -> Self { SpendPolicy::After(time) } + + pub fn public_key(pk: PublicKey) -> Self { SpendPolicy::PublicKey(pk) } + + pub fn hash(h: H256) -> Self { SpendPolicy::Hash(h) } + + pub fn threshold(n: u8, of: Vec) -> Self { SpendPolicy::Threshold(PolicyTypeThreshold { n, of }) } + + pub fn opaque(p: &SpendPolicy) -> Self { SpendPolicy::Opaque(p.address()) } + + pub fn anyone_can_spend() -> Self { SpendPolicy::threshold(0, vec![]) } +} + +#[derive(Debug, Clone)] +pub struct PolicyTypeThreshold { + pub n: u8, + pub of: Vec, +} + +// Compatibility with Sia's "UnlockConditions" +#[derive(Debug, Clone)] +pub struct PolicyTypeUnlockConditions(UnlockCondition); + +#[derive(Debug, Clone)] +pub struct UnlockCondition { + pubkeys: Vec, + timelock: u64, + sigs_required: u64, +} + +impl UnlockCondition { + pub fn new(pubkeys: Vec, timelock: u64, sigs_required: u64) -> Self { + // TODO check go implementation to see if there should be limitations or checks imposed here + UnlockCondition { + pubkeys, + timelock, + sigs_required, + } + } + + pub fn unlock_hash(&self) -> H256 { + // almost all UnlockConditions are standard, so optimize for that case + if self.timelock == 0 && self.pubkeys.len() == 1 && self.sigs_required == 1 { + return standard_unlock_hash(&self.pubkeys[0]); + } + + let mut accumulator = Accumulator::default(); + + accumulator.add_leaf(timelock_leaf(self.timelock)); + + for pubkey in &self.pubkeys { + accumulator.add_leaf(public_key_leaf(pubkey)); + } + + accumulator.add_leaf(sigs_required_leaf(self.sigs_required)); + accumulator.root() + } + + pub fn address(&self) -> Address { Address(self.unlock_hash()) } +} + +#[test] +fn test_unlock_condition_unlock_hash_standard() { + let pubkey = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let unlock_condition = UnlockCondition::new(vec![pubkey], 0, 1); + + let hash = unlock_condition.unlock_hash(); + let expected = H256::from("72b0762b382d4c251af5ae25b6777d908726d75962e5224f98d7f619bb39515d"); + assert_eq!(hash, expected); + + let hash = standard_unlock_hash(&pubkey); + assert_eq!(hash, expected); +} + +#[test] +fn test_unlock_condition_unlock_hash_2of2_multisig() { + let pubkey = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let pubkey2 = PublicKey::from_bytes( + &hex::decode("0101010000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let unlock_condition = UnlockCondition::new(vec![pubkey, pubkey2], 0, 2); + + let hash = unlock_condition.unlock_hash(); + let expected = H256::from("1e94357817d236167e54970a8c08bbd41b37bfceeeb52f6c1ce6dd01d50ea1e7"); + assert_eq!(hash, expected); +} + +#[test] +fn test_unlock_condition_unlock_hash_1of2_multisig() { + let pubkey = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let pubkey2 = PublicKey::from_bytes( + &hex::decode("0101010000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let unlock_condition = UnlockCondition::new(vec![pubkey, pubkey2], 0, 1); + + let hash = unlock_condition.unlock_hash(); + let expected = H256::from("d7f84e3423da09d111a17f64290c8d05e1cbe4cab2b6bed49e3a4d2f659f0585"); + assert_eq!(hash, expected); +} + +#[test] +fn test_spend_policy_encode_above() { + let policy = SpendPolicy::above(1); + + let hash = policy.encode().hash(); + let expected = H256::from("bebf6cbdfb440a92e3e5d832ac30fe5d226ff6b352ed3a9398b7d35f086a8ab6"); + assert_eq!(hash, expected); + + let address = policy.address(); + let expected = + Address::from_str("addr:188b997bb99dee13e95f92c3ea150bd76b3ec72e5ba57b0d57439a1a6e2865e9b25ea5d1825e").unwrap(); + assert_eq!(address, expected); +} + +#[test] +fn test_spend_policy_encode_after() { + let policy = SpendPolicy::after(1); + + let hash = policy.encode().hash(); + let expected = H256::from("07b0f28eafd87a082ad11dc4724e1c491821260821a30bec68254444f97d9311"); + assert_eq!(hash, expected); + + let address = policy.address(); + let expected = + Address::from_str("addr:60c74e0ce5cede0f13f83b0132cb195c995bc7688c9fac34bbf2b14e14394b8bbe2991bc017f").unwrap(); + assert_eq!(address, expected); +} + +#[test] +fn test_spend_policy_encode_pubkey() { + let pubkey = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let policy = SpendPolicy::PublicKey(pubkey); + + let hash = policy.encode().hash(); + let expected = H256::from("4355c8f80f6e5a98b70c9c2f9a22f17747989b4744783c90439b2b034f698bfe"); + assert_eq!(hash, expected); + + let address = policy.address(); + let expected = + Address::from_str("addr:55a7793237722c6df8222fd512063cb74228085ef1805c5184713648c159b919ac792fbad0e1").unwrap(); + assert_eq!(address, expected); +} + +#[test] +fn test_spend_policy_encode_hash() { + let hash = H256::from("0102030000000000000000000000000000000000000000000000000000000000"); + let policy = SpendPolicy::Hash(hash); + + let encoded = policy.encode(); + let hash = encoded.hash(); + let expected = H256::from("9938967aefa6cbecc1f1620d2df5170d6811d4b2f47a879b621c1099a3b0628a"); + assert_eq!(hash, expected); + + let address = policy.address(); + let expected = + Address::from_str("addr:a4d5a06d8d3c2e45aa26627858ce8e881505ae3c9d122a1d282c7824163751936cffb347e435").unwrap(); + assert_eq!(address, expected); +} + +#[test] +fn test_spend_policy_encode_threshold() { + let policy = SpendPolicy::Threshold(PolicyTypeThreshold { + n: 1, + of: vec![SpendPolicy::above(1), SpendPolicy::after(1)], + }); + + let encoded = policy.encode(); + let hash = encoded.hash(); + let expected = H256::from("7d792df6cd0b5e0f795287b3bf4087bbcc4c1bd0c52880a552cdda3e5e33d802"); + assert_eq!(hash, expected); + + let address = policy.address(); + let expected = + Address::from_str("addr:4179b53aba165e46e4c85b3c8766bb758fb6f0bfa5721550b81981a3ec38efc460557dc1ded4").unwrap(); + assert_eq!(address, expected); +} + +#[test] +fn test_spend_policy_encode_unlock_condition() { + let pubkey = PublicKey::from_bytes( + &hex::decode("0102030000000000000000000000000000000000000000000000000000000000").unwrap(), + ) + .unwrap(); + let unlock_condition = UnlockCondition::new(vec![pubkey], 0, 1); + + let sub_policy = SpendPolicy::UnlockConditions(PolicyTypeUnlockConditions(unlock_condition)); + let base_address = sub_policy.address(); + let expected = + Address::from_str("addr:72b0762b382d4c251af5ae25b6777d908726d75962e5224f98d7f619bb39515dd64b9a56043a").unwrap(); + assert_eq!(base_address, expected); + + let policy = SpendPolicy::Threshold(PolicyTypeThreshold { + n: 1, + of: vec![sub_policy], + }); + let address = policy.address(); + let expected = + Address::from_str("addr:1498a58c843ce66740e52421632d67a0f6991ea96db1fc97c29e46f89ae56e3534078876331d").unwrap(); + assert_eq!(address, expected); +} diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index 025f4c6c7d..2b9abef1ca 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -1,47 +1,62 @@ -use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum, WatcherOps}; -use crate::coin_errors::MyAddressError; -use crate::solana::solana_common::{lamports_to_sol, PrepareTransferData, SufficientBalanceError}; -use crate::solana::spl::SplTokenInfo; -use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, DexFee, - FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, - PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, - PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, - RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionDetails, TransactionFut, TransactionResult, TransactionType, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; +use std::{collections::HashMap, + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, + str::FromStr, + sync::{Arc, Mutex}}; + use async_trait::async_trait; use base58::ToBase58; use bincode::{deserialize, serialize}; -use common::executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}; -use common::{async_blocking, now_sec}; -use crypto::{StandardHDCoinAddress, StandardHDPathToCoin}; +use bitcrypto::sha256; +use common::{async_blocking, + executor::{abortable_queue::AbortableQueue, AbortableSystem, AbortedError}, + log::error, + now_sec}; +use crypto::HDPathToCoin; use derive_more::Display; -use futures::{FutureExt, TryFutureExt}; +use futures::{compat::Future01CompatExt, + {FutureExt, TryFutureExt}}; use futures01::Future; use keys::KeyPair; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; +use num_traits::ToPrimitive; use rpc::v1::types::Bytes as BytesJson; +pub use satomic_swap::{instruction::AtomicSwapInstruction, STORAGE_SPACE_ALLOCATED}; use serde_json::{self as json, Value as Json}; -use solana_client::rpc_request::TokenAccountsFilter; use solana_client::{client_error::{ClientError, ClientErrorKind}, - rpc_client::RpcClient}; -use solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel}; -use solana_sdk::program_error::ProgramError; -use solana_sdk::pubkey::ParsePubkeyError; -use solana_sdk::transaction::Transaction; -use solana_sdk::{pubkey::Pubkey, - signature::{Keypair, Signer}}; -use std::collections::HashMap; -use std::str::FromStr; -use std::sync::Mutex; -use std::{convert::TryFrom, fmt::Debug, ops::Deref, sync::Arc}; + rpc_client::RpcClient, + rpc_request::TokenAccountsFilter}; +pub use solana_sdk::transaction::Transaction as SolTransaction; +use solana_sdk::{commitment_config::{CommitmentConfig, CommitmentLevel}, + instruction::{AccountMeta, Instruction}, + native_token::sol_to_lamports, + program_error::ProgramError, + pubkey::{ParsePubkeyError, Pubkey}, + signature::{Keypair as SolKeypair, Signer}}; +use spl_token::solana_program; + +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, Transaction, TransactionEnum, + TransactionErr, WatcherOps}; +use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::HDPathAccountToAddressId; +use crate::solana::{solana_common::{lamports_to_sol, PrepareTransferData, SufficientBalanceError}, + spl::SplTokenInfo}; +use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, DexFee, + FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, + PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, + RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, + SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, + TransactionData, TransactionDetails, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherReward, WatcherRewardError, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; pub mod solana_common; mod solana_decode_tx_helpers; @@ -140,7 +155,7 @@ pub struct SolanaActivationParams { confirmation_commitment: CommitmentLevel, client_url: String, #[serde(default)] - path_to_address: StandardHDCoinAddress, + path_to_address: HDPathAccountToAddressId, } #[derive(Debug, Display)] @@ -162,7 +177,7 @@ impl From for KeyPairCreationError { fn from(e: ed25519_dalek::SignatureError) -> Self { KeyPairCreationError::SignatureError(e) } } -fn generate_keypair_from_slice(priv_key: &[u8]) -> Result> { +fn generate_keypair_from_slice(priv_key: &[u8]) -> Result> { let secret_key = ed25519_dalek::SecretKey::from_bytes(priv_key)?; let public_key = ed25519_dalek::PublicKey::from(&secret_key); let key_pair = ed25519_dalek::Keypair { @@ -188,8 +203,9 @@ pub async fn solana_coin_with_policy( let priv_key = match priv_key_policy { PrivKeyBuildPolicy::IguanaPrivKey(priv_key) => priv_key, PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { - let derivation_path: StandardHDPathToCoin = try_s!(json::from_value(conf["derivation_path"].clone())); - try_s!(global_hd.derive_secp256k1_secret(&derivation_path, ¶ms.path_to_address)) + let path_to_coin: HDPathToCoin = try_s!(json::from_value(conf["derivation_path"].clone())); + let derivation_path = try_s!(params.path_to_address.to_derivation_path(&path_to_coin)); + try_s!(global_hd.derive_secp256k1_secret(&derivation_path)) }, PrivKeyBuildPolicy::Trezor => return ERR!("{}", PrivKeyPolicyNotAllowed::HardwareWalletNotSupported), }; @@ -217,7 +233,7 @@ pub async fn solana_coin_with_policy( /// pImpl idiom. pub struct SolanaCoinImpl { ticker: String, - key_pair: Keypair, + key_pair: SolKeypair, client: RpcClient, decimals: u8, my_address: String, @@ -271,8 +287,7 @@ async fn withdraw_base_coin_impl(coin: SolanaCoin, req: WithdrawRequest) -> With }; let spent_by_me = &total_amount + &res.sol_required; Ok(TransactionDetails { - tx_hex: serialized_tx.into(), - tx_hash: tx.signatures[0].to_string(), + tx: TransactionData::new_signed(serialized_tx.into(), tx.signatures[0].to_string()), from: vec![coin.my_address.clone()], to: vec![req.to], total_amount: spent_by_me.clone(), @@ -305,6 +320,28 @@ async fn withdraw_impl(coin: SolanaCoin, req: WithdrawRequest) -> WithdrawResult withdraw_base_coin_impl(coin, req).await } +type SolTxFut = Box + Send + 'static>; + +impl Transaction for SolTransaction { + fn tx_hex(&self) -> Vec { + serialize(self).unwrap_or_else(|e| { + error!("Error serializing SolTransaction: {}", e); + vec![] + }) + } + + fn tx_hash_as_bytes(&self) -> BytesJson { + let hash = match self.signatures.get(0) { + Some(signature) => signature, + None => { + error!("No signature found in SolTransaction"); + return BytesJson(Vec::new()); + }, + }; + BytesJson(Vec::from(hash.as_ref())) + } +} + impl SolanaCoin { pub async fn estimate_withdraw_fees(&self) -> Result<(solana_sdk::hash::Hash, u64), MmError> { let hash = async_blocking({ @@ -376,14 +413,254 @@ impl SolanaCoin { let guard = self.spl_tokens_infos.lock().unwrap(); (*guard).clone() } + + fn send_hash_time_locked_payment(&self, args: SendPaymentArgs<'_>) -> SolTxFut { + let receiver = Pubkey::new(args.other_pubkey); + let swap_program_id = Pubkey::new(try_tx_fus_opt!( + args.swap_contract_address, + format!( + "Unable to extract Bytes from args.swap_contract_address ( {:?} )", + args.swap_contract_address + ) + )); + let amount = sol_to_lamports(try_tx_fus_opt!( + args.amount.to_f64(), + format!("Unable to extract value from args.amount ( {:?} )", args.amount) + )); + let secret_hash: [u8; 32] = try_tx_fus!(<[u8; 32]>::try_from(args.secret_hash)); + let (vault_pda, vault_pda_data, vault_bump_seed, vault_bump_seed_data, rent_exemption_lamports) = + try_tx_fus!(self.create_vaults(args.time_lock, secret_hash, swap_program_id, STORAGE_SPACE_ALLOCATED)); + let swap_instruction = AtomicSwapInstruction::LamportsPayment { + secret_hash, + lock_time: args.time_lock, + amount, + receiver, + rent_exemption_lamports, + vault_bump_seed, + vault_bump_seed_data, + }; + + let accounts = vec![ + AccountMeta::new(self.key_pair.pubkey(), true), + AccountMeta::new(vault_pda_data, false), + AccountMeta::new(vault_pda, false), + AccountMeta::new(solana_program::system_program::id(), false), + ]; + self.sign_and_send_swap_transaction_fut(swap_program_id, accounts, swap_instruction.pack()) + } + + fn spend_hash_time_locked_payment(&self, args: SpendPaymentArgs) -> SolTxFut { + let sender = Pubkey::new(args.other_pubkey); + let swap_program_id = Pubkey::new(try_tx_fus_opt!( + args.swap_contract_address.as_ref(), + format!( + "Unable to extract Bytes from args.swap_contract_address ( {:?} )", + args.swap_contract_address + ) + )); + let secret: [u8; 32] = try_tx_fus!(<[u8; 32]>::try_from(args.secret)); + let secret_hash = sha256(secret.as_slice()).take(); + let (lock_time, tx_secret_hash, amount, token_program) = + try_tx_fus!(self.get_swap_transaction_details(args.other_payment_tx)); + if secret_hash != tx_secret_hash { + try_tx_fus_err!(format!( + "Provided secret_hash {:?} does not match transaction secret_hash {:?}", + secret_hash, tx_secret_hash + )); + } + let (vault_pda, vault_pda_data, vault_bump_seed, vault_bump_seed_data, _rent_exemption_lamports) = + try_tx_fus!(self.create_vaults(lock_time, secret_hash, swap_program_id, STORAGE_SPACE_ALLOCATED)); + let swap_instruction = AtomicSwapInstruction::ReceiverSpend { + secret, + lock_time, + amount, + sender, + token_program, + vault_bump_seed, + vault_bump_seed_data, + }; + let accounts = vec![ + AccountMeta::new(self.key_pair.pubkey(), true), + AccountMeta::new(vault_pda_data, false), + AccountMeta::new(vault_pda, false), + AccountMeta::new(solana_program::system_program::id(), false), + ]; + self.sign_and_send_swap_transaction_fut(swap_program_id, accounts, swap_instruction.pack()) + } + + fn refund_hash_time_locked_payment(&self, args: RefundPaymentArgs) -> SolTxFut { + let receiver = Pubkey::new(args.other_pubkey); + let swap_program_id = Pubkey::new(try_tx_fus_opt!( + args.swap_contract_address.as_ref(), + format!( + "Unable to extract Bytes from args.swap_contract_address ( {:?} )", + args.swap_contract_address + ) + )); + let (lock_time, secret_hash, amount, token_program) = + try_tx_fus!(self.get_swap_transaction_details(args.payment_tx)); + let (vault_pda, vault_pda_data, vault_bump_seed, vault_bump_seed_data, _rent_exemption_lamports) = + try_tx_fus!(self.create_vaults(lock_time, secret_hash, swap_program_id, STORAGE_SPACE_ALLOCATED)); + let swap_instruction = AtomicSwapInstruction::SenderRefund { + secret_hash, + lock_time, + amount, + receiver, + token_program, + vault_bump_seed, + vault_bump_seed_data, + }; + let accounts = vec![ + AccountMeta::new(self.key_pair.pubkey(), true), // Marked as signer + AccountMeta::new(vault_pda_data, false), // Not a signer + AccountMeta::new(vault_pda, false), // Not a signer + AccountMeta::new(solana_program::system_program::id(), false), //system_program must be included + ]; + self.sign_and_send_swap_transaction_fut(swap_program_id, accounts, swap_instruction.pack()) + } + + fn get_swap_transaction_details(&self, tx_hex: &[u8]) -> Result<(u64, [u8; 32], u64, Pubkey), Box> { + let transaction: SolTransaction = deserialize(tx_hex) + .map_err(|e| Box::new(TransactionErr::Plain(ERRL!("error deserializing tx_hex: {:?}", e))))?; + + let instruction = transaction + .message + .instructions + .get(0) + .ok_or_else(|| Box::new(TransactionErr::Plain(ERRL!("Instruction not found in message"))))?; + + let instruction_data = &instruction.data[..]; + let instruction = AtomicSwapInstruction::unpack(instruction_data[0], instruction_data) + .map_err(|e| Box::new(TransactionErr::Plain(ERRL!("error unpacking tx data: {:?}", e))))?; + + match instruction { + AtomicSwapInstruction::LamportsPayment { + secret_hash, + lock_time, + amount, + .. + } => Ok((lock_time, secret_hash, amount, Pubkey::new_from_array([0; 32]))), + AtomicSwapInstruction::SPLTokenPayment { + secret_hash, + lock_time, + amount, + token_program, + .. + } => Ok((lock_time, secret_hash, amount, token_program)), + AtomicSwapInstruction::ReceiverSpend { + secret, + lock_time, + amount, + token_program, + .. + } => Ok((lock_time, sha256(&secret).take(), amount, token_program)), + AtomicSwapInstruction::SenderRefund { + secret_hash, + lock_time, + amount, + token_program, + .. + } => Ok((lock_time, secret_hash, amount, token_program)), + } + } + + fn sign_and_send_swap_transaction_fut( + &self, + program_id: Pubkey, + accounts: Vec, + data: Vec, + ) -> SolTxFut { + let coin = self.clone(); + Box::new( + async move { coin.sign_and_send_swap_transaction(program_id, accounts, data).await } + .boxed() + .compat(), + ) + } + + pub async fn sign_and_send_swap_transaction( + &self, + program_id: Pubkey, + accounts: Vec, + data: Vec, + ) -> Result { + // Construct the instruction to send to the program + // The parameters here depend on your specific program's requirements + let instruction = Instruction { + program_id, + accounts, // Specify account metas here + data, // Pass data to the program here + }; + + // Create a transaction + let recent_blockhash = self + .client + .get_latest_blockhash() + .map_err(|e| TransactionErr::Plain(format!("Failed to get recent blockhash: {:?}", e)))?; + + let transaction: SolTransaction = SolTransaction::new_signed_with_payer( + &[instruction], + Some(&self.key_pair.pubkey()), //payer pubkey + &[&self.key_pair], //payer + recent_blockhash, + ); + + // Send the transaction + let tx = self + .client + .send_and_confirm_transaction(&transaction) + .map(|_signature| transaction) + .map_err(|e| TransactionErr::Plain(ERRL!("Solana ClientError: {:?}", e)))?; + + Ok(tx) + } + + fn create_vaults( + &self, + lock_time: u64, + secret_hash: [u8; 32], + program_id: Pubkey, + space: u64, + ) -> Result<(Pubkey, Pubkey, u8, u8, u64), Box> { + let seeds: &[&[u8]] = &[b"swap", &lock_time.to_le_bytes()[..], &secret_hash[..]]; + let (vault_pda, bump_seed) = Pubkey::find_program_address(seeds, &program_id); + + let seeds_data: &[&[u8]] = &[b"swap_data", &lock_time.to_le_bytes()[..], &secret_hash[..]]; + let (vault_pda_data, bump_seed_data) = Pubkey::find_program_address(seeds_data, &program_id); + + let rent_exemption_lamports = self + .client + .get_minimum_balance_for_rent_exemption( + space + .try_into() + .map_err(|e| Box::new(TransactionErr::Plain(ERRL!("unable to convert space: {:?}", e))))?, + ) + .map_err(|e| { + Box::new(TransactionErr::Plain(ERRL!( + "error get_minimum_balance_for_rent_exemption: {:?}", + e + ))) + })?; + + Ok(( + vault_pda, + vault_pda_data, + bump_seed, + bump_seed_data, + rent_exemption_lamports, + )) + } } +#[async_trait] impl MarketCoinOps for SolanaCoin { fn ticker(&self) -> &str { &self.ticker } fn my_address(&self) -> MmResult { Ok(self.my_address.clone()) } - fn get_public_key(&self) -> Result> { unimplemented!() } + async fn get_public_key(&self) -> Result> { + Ok(self.key_pair.pubkey().to_string()) + } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } @@ -419,7 +696,7 @@ impl MarketCoinOps for SolanaCoin { let tx = tx.to_owned(); let fut = async_blocking(move || { let bytes = hex::decode(tx).map_to_mm(|e| e).map_err(|e| format!("{:?}", e))?; - let tx: Transaction = deserialize(bytes.as_slice()) + let tx: SolTransaction = deserialize(bytes.as_slice()) .map_to_mm(|e| e) .map_err(|e| format!("{:?}", e))?; // this is blocking IO @@ -441,6 +718,13 @@ impl MarketCoinOps for SolanaCoin { Box::new(fut.boxed().compat()) } + #[inline(always)] + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { + MmError::err(RawTransactionError::NotImplemented { + coin: self.ticker().to_string(), + }) + } + fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { unimplemented!() } @@ -464,43 +748,73 @@ impl MarketCoinOps for SolanaCoin { fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + + fn is_trezor(&self) -> bool { unimplemented!() } } #[async_trait] impl SwapOps for SolanaCoin { - fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { unimplemented!() } - - fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - - fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - - fn send_maker_spends_taker_payment(&self, _maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { unimplemented!() } - fn send_taker_spends_maker_payment(&self, _taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - unimplemented!() + fn send_maker_payment(&self, maker_payment: SendPaymentArgs) -> TransactionFut { + Box::new( + self.send_hash_time_locked_payment(maker_payment) + .map(TransactionEnum::from), + ) + } + + fn send_taker_payment(&self, taker_payment: SendPaymentArgs) -> TransactionFut { + Box::new( + self.send_hash_time_locked_payment(taker_payment) + .map(TransactionEnum::from), + ) } - async fn send_taker_refunds_payment( + async fn send_maker_spends_taker_payment( &self, - _taker_refunds_payment_args: RefundPaymentArgs<'_>, + maker_spends_payment_args: SpendPaymentArgs<'_>, ) -> TransactionResult { - unimplemented!() + self.spend_hash_time_locked_payment(maker_spends_payment_args) + .compat() + .await + .map(TransactionEnum::from) } - async fn send_maker_refunds_payment( + async fn send_taker_spends_maker_payment( &self, - _maker_refunds_payment_args: RefundPaymentArgs<'_>, + taker_spends_payment_args: SpendPaymentArgs<'_>, ) -> TransactionResult { - unimplemented!() + self.spend_hash_time_locked_payment(taker_spends_payment_args) + .compat() + .await + .map(TransactionEnum::from) + } + + async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.refund_hash_time_locked_payment(taker_refunds_payment_args) + .map(TransactionEnum::from) + .compat() + .await + } + + async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { + self.refund_hash_time_locked_payment(maker_refunds_payment_args) + .map(TransactionEnum::from) + .compat() + .await } fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } fn check_if_my_payment_sent( &self, @@ -737,6 +1051,7 @@ impl MmCoin for SolanaCoin { &self, _value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { unimplemented!() } @@ -779,5 +1094,5 @@ impl MmCoin for SolanaCoin { fn on_disabled(&self) -> Result<(), AbortedError> { AbortableSystem::abort_all(&self.abortable_system) } - fn on_token_deactivated(&self, _ticker: &str) {} + fn on_token_deactivated(&self, _ticker: &str) { unimplemented!() } } diff --git a/mm2src/coins/solana/solana_common_tests.rs b/mm2src/coins/solana/solana_common_tests.rs index 32b030b425..6bb5b833d1 100644 --- a/mm2src/coins/solana/solana_common_tests.rs +++ b/mm2src/coins/solana/solana_common_tests.rs @@ -1,7 +1,6 @@ use super::*; use crate::solana::spl::{SplToken, SplTokenFields}; -use bip39::Language; -use crypto::privkey::key_pair_from_seed; +use crypto::privkey::{bip39_seed_from_passphrase, key_pair_from_seed}; use ed25519_dalek_bip32::{DerivationPath, ExtendedSecretKey}; use mm2_core::mm_ctx::MmCtxBuilder; use solana_client::rpc_client::RpcClient; @@ -22,13 +21,11 @@ pub fn solana_net_to_url(net_type: SolanaNet) -> String { } } -pub fn generate_key_pair_from_seed(seed: String) -> Keypair { +pub fn generate_key_pair_from_seed(seed: &str) -> SolKeypair { let derivation_path = DerivationPath::from_str("m/44'/501'/0'").unwrap(); - let mnemonic = bip39::Mnemonic::from_phrase(seed.as_str(), Language::English).unwrap(); - let seed = bip39::Seed::new(&mnemonic, ""); - let seed_bytes: &[u8] = seed.as_bytes(); + let seed = bip39_seed_from_passphrase(seed).unwrap(); - let ext = ExtendedSecretKey::from_seed(seed_bytes) + let ext = ExtendedSecretKey::from_seed(&seed.0) .unwrap() .derive(&derivation_path) .unwrap(); @@ -41,7 +38,7 @@ pub fn generate_key_pair_from_seed(seed: String) -> Keypair { solana_sdk::signature::keypair_from_seed(pair.to_bytes().as_ref()).unwrap() } -pub fn generate_key_pair_from_iguana_seed(seed: String) -> Keypair { +pub fn generate_key_pair_from_iguana_seed(seed: String) -> SolKeypair { let key_pair = key_pair_from_seed(seed.as_str()).unwrap(); let secret_key = ed25519_dalek::SecretKey::from_bytes(key_pair.private().secret.as_slice()).unwrap(); let public_key = ed25519_dalek::PublicKey::from(&secret_key); diff --git a/mm2src/coins/solana/solana_decode_tx_helpers.rs b/mm2src/coins/solana/solana_decode_tx_helpers.rs index 2ac0876809..bd22fc044e 100644 --- a/mm2src/coins/solana/solana_decode_tx_helpers.rs +++ b/mm2src/coins/solana/solana_decode_tx_helpers.rs @@ -1,6 +1,6 @@ extern crate serde_derive; -use crate::{NumConversResult, SolanaCoin, SolanaFeeDetails, TransactionDetails, TransactionType}; +use crate::{NumConversResult, SolanaCoin, SolanaFeeDetails, TransactionData, TransactionDetails, TransactionType}; use mm2_number::BigDecimal; use solana_sdk::native_token::lamports_to_sol; use std::convert::TryFrom; @@ -54,8 +54,7 @@ impl SolanaConfirmedTransaction { }; let fee = BigDecimal::try_from(lamports_to_sol(self.meta.fee))?; let tx = TransactionDetails { - tx_hex: Default::default(), - tx_hash: self.transaction.signatures[0].to_string(), + tx: TransactionData::new_signed(Default::default(), self.transaction.signatures[0].to_string()), from: vec![instruction.parsed.info.source.clone()], to: vec![instruction.parsed.info.destination.clone()], total_amount: amount, diff --git a/mm2src/coins/solana/solana_tests.rs b/mm2src/coins/solana/solana_tests.rs index 05939b68c5..fe2b104293 100644 --- a/mm2src/coins/solana/solana_tests.rs +++ b/mm2src/coins/solana/solana_tests.rs @@ -1,15 +1,17 @@ -use super::*; -use crate::solana::solana_common_tests::{generate_key_pair_from_iguana_seed, generate_key_pair_from_seed, - solana_coin_for_test, SolanaNet}; -use crate::solana::solana_decode_tx_helpers::SolanaConfirmedTransaction; -use crate::MarketCoinOps; use base58::ToBase58; use common::{block_on, Future01CompatExt}; +use rpc::v1::types::Bytes; use solana_client::rpc_request::TokenAccountsFilter; -use solana_sdk::signature::{Signature, Signer}; +use solana_sdk::{bs58, + signature::{Signature, Signer}}; use solana_transaction_status::UiTransactionEncoding; -use std::ops::Neg; -use std::str::FromStr; +use std::{ops::Neg, str::FromStr}; + +use super::solana_common_tests::{generate_key_pair_from_iguana_seed, generate_key_pair_from_seed, + solana_coin_for_test, SolanaNet}; +use super::solana_decode_tx_helpers::SolanaConfirmedTransaction; +use super::*; +use crate::{MarketCoinOps, SwapTxTypeWithSecretHash}; #[test] #[cfg(not(target_arch = "wasm32"))] @@ -35,9 +37,8 @@ fn solana_keypair_from_secp() { fn solana_prerequisites() { // same test as trustwallet { - let fin = generate_key_pair_from_seed( - "hood vacant left trim hard mushroom device flavor ask better arrest again".to_string(), - ); + let fin = + generate_key_pair_from_seed("hood vacant left trim hard mushroom device flavor ask better arrest again"); let public_address = fin.pubkey().to_string(); let priv_key = &fin.secret().to_bytes()[..].to_base58(); assert_eq!(public_address.len(), 44); @@ -64,7 +65,7 @@ fn solana_prerequisites() { let token_accounts = client .get_token_accounts_by_owner(&key_pair.pubkey(), TokenAccountsFilter::ProgramId(spl_token::id())) .expect(""); - println!("{:?}", token_accounts); + log!("{:?}", token_accounts); let actual_token_pubkey = solana_sdk::pubkey::Pubkey::from_str(token_accounts[0].pubkey.as_str()).unwrap(); let amount = client.get_token_account_balance(&actual_token_pubkey).unwrap(); assert_ne!(amount.ui_amount_string.as_str(), "0"); @@ -97,7 +98,7 @@ fn solana_block_height() { let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); let (_, sol_coin) = solana_coin_for_test(passphrase, SolanaNet::Testnet); let res = block_on(sol_coin.current_block().compat()).unwrap(); - println!("block is : {}", res); + log!("block is : {}", res); assert!(res > 0); } @@ -165,6 +166,7 @@ fn solana_transaction_simulations() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) @@ -193,6 +195,7 @@ fn solana_transaction_zero_balance() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ); @@ -222,6 +225,7 @@ fn solana_transaction_simulations_not_enough_for_fees() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ); @@ -256,6 +260,7 @@ fn solana_transaction_simulations_max() { max: true, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) @@ -267,7 +272,7 @@ fn solana_transaction_simulations_max() { assert_eq!(valid_tx_details.total_amount, balance); assert_eq!(valid_tx_details.spent_by_me, balance); assert_eq!(valid_tx_details.received_by_me, &balance - &sol_required); - println!("{:?}", valid_tx_details); + log!("{:?}", valid_tx_details); } #[test] @@ -285,19 +290,25 @@ fn solana_test_transactions() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) .unwrap(); - println!("{:?}", valid_tx_details); + log!("{:?}", valid_tx_details); - let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let tx_str = hex::encode(&*valid_tx_details.tx.tx_hex().unwrap().0); let res = block_on(sol_coin.send_raw_tx(&tx_str).compat()).unwrap(); - let res2 = block_on(sol_coin.send_raw_tx_bytes(&valid_tx_details.tx_hex.0).compat()).unwrap(); + let res2 = block_on( + sol_coin + .send_raw_tx_bytes(&valid_tx_details.tx.tx_hex().unwrap().0) + .compat(), + ) + .unwrap(); assert_eq!(res, res2); - //println!("{:?}", res); + //log!("{:?}", res); } // This test is just a unit test for brainstorming around tx_history for base_coin. @@ -318,11 +329,108 @@ fn solana_test_tx_history() { .client .get_transaction(&signature, UiTransactionEncoding::JsonParsed) .unwrap(); - println!("{}", serde_json::to_string(&res).unwrap()); + log!("{}", serde_json::to_string(&res).unwrap()); let parsed = serde_json::to_value(&res).unwrap(); let tx_infos: SolanaConfirmedTransaction = serde_json::from_value(parsed).unwrap(); let mut txs = tx_infos.extract_solana_transactions(&sol_coin).unwrap(); history.append(&mut txs); } - println!("{}", serde_json::to_string(&history).unwrap()); + log!("{}", serde_json::to_string(&history).unwrap()); +} + +#[test] +fn solana_coin_send_and_refund_maker_payment() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, coin) = solana_coin_for_test(passphrase, SolanaNet::Devnet); + let solana_program_id = "3fystoi7pB1cnDEbRRpSjFJA4fG3W2vQQZ21jSrBc11B"; + let solana_program_id = bs58::decode(solana_program_id).into_vec().unwrap_or_else(|e| { + log!("Failed to decode program ID: {}", e); + Vec::new() + }); + + let pk_data = [1; 32]; + let time_lock = now_sec() - 3600; + let taker_pub = coin.key_pair.pubkey().to_string(); + let taker_pub = Pubkey::from_str(taker_pub.as_str()).unwrap(); + let secret = [0; 32]; + let secret_hash = sha256(&secret); + + let args = SendPaymentArgs { + time_lock_duration: 0, + time_lock, + other_pubkey: taker_pub.as_ref(), + secret_hash: secret_hash.as_slice(), + amount: "0.01".parse().unwrap(), + swap_contract_address: &Some(Bytes::from(solana_program_id.clone())), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + let tx = coin.send_maker_payment(args).wait().unwrap(); + log!("swap tx {:?}", tx); + + let refund_args = RefundPaymentArgs { + payment_tx: &tx.tx_hex(), + time_lock, + other_pubkey: taker_pub.as_ref(), + tx_type_with_secret_hash: SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: secret_hash.as_slice(), + }, + swap_contract_address: &Some(Bytes::from(solana_program_id)), + swap_unique_data: pk_data.as_slice(), + watcher_reward: false, + }; + let refund_tx = block_on(coin.send_maker_refunds_payment(refund_args)).unwrap(); + log!("refund tx {:?}", refund_tx); +} + +#[test] +fn solana_coin_send_and_spend_maker_payment() { + let passphrase = "federal stay trigger hour exist success game vapor become comfort action phone bright ill target wild nasty crumble dune close rare fabric hen iron".to_string(); + let (_, coin) = solana_coin_for_test(passphrase, SolanaNet::Devnet); + let solana_program_id = "3fystoi7pB1cnDEbRRpSjFJA4fG3W2vQQZ21jSrBc11B"; + let solana_program_id = bs58::decode(solana_program_id).into_vec().unwrap_or_else(|e| { + log!("Failed to decode program ID: {}", e); + Vec::new() + }); + + let pk_data = [1; 32]; + let lock_time = now_sec() - 1000; + let taker_pub = coin.key_pair.pubkey().to_string(); + let taker_pub = Pubkey::from_str(taker_pub.as_str()).unwrap(); + let secret = [0; 32]; + let secret_hash = sha256(&secret); + + let maker_payment_args = SendPaymentArgs { + time_lock_duration: 0, + time_lock: lock_time, + other_pubkey: taker_pub.as_ref(), + secret_hash: secret_hash.as_slice(), + amount: "0.01".parse().unwrap(), + swap_contract_address: &Some(Bytes::from(solana_program_id.clone())), + swap_unique_data: &[], + payment_instructions: &None, + watcher_reward: None, + wait_for_confirmation_until: 0, + }; + + let tx = coin.send_maker_payment(maker_payment_args).wait().unwrap(); + log!("swap tx {:?}", tx); + + let maker_pub = taker_pub; + + let spends_payment_args = SpendPaymentArgs { + other_payment_tx: &tx.tx_hex(), + time_lock: lock_time, + other_pubkey: maker_pub.as_ref(), + secret: &secret, + secret_hash: secret_hash.as_slice(), + swap_contract_address: &Some(Bytes::from(solana_program_id)), + swap_unique_data: pk_data.as_slice(), + watcher_reward: false, + }; + + let spend_tx = block_on(coin.send_taker_spends_maker_payment(spends_payment_args)).unwrap(); + log!("spend tx {}", hex::encode(spend_tx.tx_hash_as_bytes().0)); } diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index 0777e278e5..7bc720e5e7 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -1,13 +1,14 @@ use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum, WatcherOps}; -use crate::coin_errors::MyAddressError; +use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; use crate::solana::solana_common::{ui_amount_to_amount, PrepareTransferData, SufficientBalanceError}; use crate::solana::{solana_common, AccountError, SolanaCommonOps, SolanaFeeDetails}; use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, - PaymentInstructions, PaymentInstructionsErr, RawTransactionFut, RawTransactionRequest, RefundError, - RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SignatureResult, SolanaCoin, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionFut, TransactionResult, + PaymentInstructions, PaymentInstructionsErr, RawTransactionError, RawTransactionFut, + RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, + SignatureResult, SolanaCoin, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionData, TransactionDetails, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, @@ -148,8 +149,7 @@ async fn withdraw_spl_token_impl(coin: SplToken, req: WithdrawRequest) -> Withdr 0.into() }; Ok(TransactionDetails { - tx_hex: serialized_tx.into(), - tx_hash: tx.signatures[0].to_string(), + tx: TransactionData::new_signed(serialized_tx.into(), tx.signatures[0].to_string()), from: vec![coin.platform_coin.my_address.clone()], to: vec![req.to], total_amount: res.to_send.clone(), @@ -230,12 +230,13 @@ impl SplToken { } } +#[async_trait] impl MarketCoinOps for SplToken { fn ticker(&self) -> &str { &self.conf.ticker } fn my_address(&self) -> MmResult { Ok(self.platform_coin.my_address.clone()) } - fn get_public_key(&self) -> Result> { unimplemented!() } + async fn get_public_key(&self) -> Result> { unimplemented!() } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } @@ -266,6 +267,13 @@ impl MarketCoinOps for SplToken { self.platform_coin.send_raw_tx_bytes(tx) } + #[inline(always)] + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { + MmError::err(RawTransactionError::NotImplemented { + coin: self.ticker().to_string(), + }) + } + fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { unimplemented!() } @@ -285,21 +293,31 @@ impl MarketCoinOps for SplToken { fn min_tx_amount(&self) -> BigDecimal { BigDecimal::from(0) } fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + + fn is_trezor(&self) -> bool { self.platform_coin.is_trezor() } } #[async_trait] impl SwapOps for SplToken { - fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_spends_taker_payment(&self, _maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + _maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } - fn send_taker_spends_maker_payment(&self, _taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + _taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } @@ -319,9 +337,13 @@ impl SwapOps for SplToken { fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } fn check_if_my_payment_sent( &self, @@ -531,6 +553,7 @@ impl MmCoin for SplToken { &self, _value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { unimplemented!() } diff --git a/mm2src/coins/solana/spl_tests.rs b/mm2src/coins/solana/spl_tests.rs index c405f99b05..10943e6e33 100644 --- a/mm2src/coins/solana/spl_tests.rs +++ b/mm2src/coins/solana/spl_tests.rs @@ -17,7 +17,7 @@ fn spl_coin_creation() { solana_sdk::pubkey::Pubkey::from_str("CpMah17kQEL2wqyMKt3mZBdTnZbkbfx4nqmQMFDP5vwp").unwrap(), ); - println!("address: {}", sol_spl_usdc_coin.my_address().unwrap()); + log!("address: {}", sol_spl_usdc_coin.my_address().unwrap()); assert_eq!( sol_spl_usdc_coin.my_address().unwrap(), "FJktmyjV9aBHEShT4hfnLpr9ELywdwVtEL1w1rSWgbVf" @@ -113,20 +113,26 @@ fn test_spl_transactions() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) .unwrap(); - println!("{:?}", valid_tx_details); + log!("{:?}", valid_tx_details); assert_eq!(valid_tx_details.total_amount, withdraw_amount); assert_eq!(valid_tx_details.my_balance_change, withdraw_amount.neg()); assert_eq!(valid_tx_details.coin, "USDC".to_string()); assert_ne!(valid_tx_details.timestamp, 0); - let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let tx_str = hex::encode(&*valid_tx_details.tx.tx_hex().unwrap().0); let res = block_on(usdc_sol_coin.send_raw_tx(&tx_str).compat()).unwrap(); - println!("{:?}", res); + log!("{:?}", res); - let res2 = block_on(usdc_sol_coin.send_raw_tx_bytes(&valid_tx_details.tx_hex.0).compat()).unwrap(); + let res2 = block_on( + usdc_sol_coin + .send_raw_tx_bytes(&valid_tx_details.tx.tx_hex().unwrap().0) + .compat(), + ) + .unwrap(); assert_eq!(res, res2); } diff --git a/mm2src/coins/tendermint/iris/ethermint_account.rs b/mm2src/coins/tendermint/ethermint_account.rs similarity index 100% rename from mm2src/coins/tendermint/iris/ethermint_account.rs rename to mm2src/coins/tendermint/ethermint_account.rs diff --git a/mm2src/coins/tendermint/htlc/iris/htlc.rs b/mm2src/coins/tendermint/htlc/iris/htlc.rs new file mode 100644 index 0000000000..09561a4048 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/iris/htlc.rs @@ -0,0 +1,146 @@ +use super::htlc_proto::{IrisClaimHtlcProto, IrisCreateHtlcProto}; + +use cosmrs::proto::traits::TypeUrl; +use cosmrs::{tx::Msg, AccountId, Coin, ErrorReport}; +use std::convert::TryFrom; + +pub(crate) const IRIS_CREATE_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgCreateHTLC"; +pub(crate) const IRIS_CLAIM_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgClaimHTLC"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct IrisCreateHtlcMsg { + /// Sender's address. + pub(crate) to: AccountId, + + /// Recipient's address. + pub(crate) sender: AccountId, + + /// The claim receiving address on the other chain. + pub(crate) receiver_on_other_chain: String, + + /// The counterparty creator address on the other chain. + pub(crate) sender_on_other_chain: String, + + /// Amount to send. + pub(crate) amount: Vec, + + /// The sha256 hash generated from secret and timestamp. + pub(crate) hash_lock: String, + + /// The number of blocks to wait before the asset may be returned to. + pub(crate) time_lock: u64, + + /// The timestamp in seconds for generating hash lock if provided. + pub(crate) timestamp: u64, + + /// Whether it is an HTLT transaction. + pub(crate) transfer: bool, +} + +impl Msg for IrisCreateHtlcMsg { + type Proto = IrisCreateHtlcProto; +} + +impl TryFrom for IrisCreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: IrisCreateHtlcProto) -> Result { + IrisCreateHtlcMsg::try_from(&proto) + } +} + +impl TryFrom<&IrisCreateHtlcProto> for IrisCreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: &IrisCreateHtlcProto) -> Result { + Ok(IrisCreateHtlcMsg { + sender: proto.sender.parse()?, + to: proto.to.parse()?, + amount: proto.amount.iter().map(TryFrom::try_from).collect::>()?, + receiver_on_other_chain: proto.receiver_on_other_chain.clone(), + sender_on_other_chain: proto.sender_on_other_chain.clone(), + hash_lock: proto.hash_lock.clone(), + timestamp: proto.timestamp, + time_lock: proto.time_lock, + transfer: proto.transfer, + }) + } +} + +impl From for IrisCreateHtlcProto { + fn from(t: IrisCreateHtlcMsg) -> IrisCreateHtlcProto { IrisCreateHtlcProto::from(&t) } +} + +impl From<&IrisCreateHtlcMsg> for IrisCreateHtlcProto { + fn from(msg: &IrisCreateHtlcMsg) -> IrisCreateHtlcProto { + IrisCreateHtlcProto { + sender: msg.sender.to_string(), + to: msg.to.to_string(), + amount: msg.amount.iter().map(Into::into).collect(), + receiver_on_other_chain: msg.receiver_on_other_chain.clone(), + sender_on_other_chain: msg.sender_on_other_chain.clone(), + hash_lock: msg.hash_lock.clone(), + timestamp: msg.timestamp, + time_lock: msg.time_lock, + transfer: msg.transfer, + } + } +} + +impl TypeUrl for IrisCreateHtlcProto { + const TYPE_URL: &'static str = IRIS_CREATE_HTLC_TYPE_URL; +} + +#[derive(Clone)] +pub(crate) struct IrisClaimHtlcMsg { + /// Sender's address. + pub(crate) sender: AccountId, + + /// Generated HTLC ID + pub(crate) id: String, + + /// Secret that has been used for generating hash_lock + pub(crate) secret: String, +} + +impl Msg for IrisClaimHtlcMsg { + type Proto = IrisClaimHtlcProto; +} + +impl TryFrom for IrisClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: IrisClaimHtlcProto) -> Result { + IrisClaimHtlcMsg::try_from(&proto) + } +} + +impl TryFrom<&IrisClaimHtlcProto> for IrisClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: &IrisClaimHtlcProto) -> Result { + Ok(IrisClaimHtlcMsg { + sender: proto.sender.parse()?, + id: proto.id.clone(), + secret: proto.secret.clone(), + }) + } +} + +impl From for IrisClaimHtlcProto { + fn from(coin: IrisClaimHtlcMsg) -> IrisClaimHtlcProto { IrisClaimHtlcProto::from(&coin) } +} + +impl From<&IrisClaimHtlcMsg> for IrisClaimHtlcProto { + fn from(msg: &IrisClaimHtlcMsg) -> IrisClaimHtlcProto { + IrisClaimHtlcProto { + sender: msg.sender.to_string(), + id: msg.id.clone(), + secret: msg.secret.clone(), + } + } +} + +impl TypeUrl for IrisClaimHtlcProto { + const TYPE_URL: &'static str = IRIS_CLAIM_HTLC_TYPE_URL; +} diff --git a/mm2src/coins/tendermint/iris/htlc_proto.rs b/mm2src/coins/tendermint/htlc/iris/htlc_proto.rs similarity index 83% rename from mm2src/coins/tendermint/iris/htlc_proto.rs rename to mm2src/coins/tendermint/htlc/iris/htlc_proto.rs index 7bf9b5281b..922da3eebc 100644 --- a/mm2src/coins/tendermint/iris/htlc_proto.rs +++ b/mm2src/coins/tendermint/htlc/iris/htlc_proto.rs @@ -1,5 +1,7 @@ +use crate::tendermint::htlc::HtlcState; + #[derive(prost::Message)] -pub(crate) struct CreateHtlcProtoRep { +pub(crate) struct IrisCreateHtlcProto { #[prost(string, tag = "1")] pub(crate) sender: prost::alloc::string::String, #[prost(string, tag = "2")] @@ -21,7 +23,7 @@ pub(crate) struct CreateHtlcProtoRep { } #[derive(prost::Message)] -pub(crate) struct ClaimHtlcProtoRep { +pub(crate) struct IrisClaimHtlcProto { #[prost(string, tag = "1")] pub(crate) sender: prost::alloc::string::String, #[prost(string, tag = "2")] @@ -31,21 +33,7 @@ pub(crate) struct ClaimHtlcProtoRep { } #[derive(prost::Message)] -pub(crate) struct QueryHtlcRequestProto { - #[prost(string, tag = "1")] - pub(crate) id: prost::alloc::string::String, -} - -#[derive(prost::Enumeration, Debug)] -#[repr(i32)] -pub enum HtlcState { - Open = 0, - Completed = 1, - Refunded = 2, -} - -#[derive(prost::Message)] -pub struct HtlcProto { +pub struct IrisHtlcProto { #[prost(string, tag = "1")] pub(crate) id: prost::alloc::string::String, #[prost(string, tag = "2")] @@ -75,7 +63,7 @@ pub struct HtlcProto { } #[derive(prost::Message)] -pub(crate) struct QueryHtlcResponseProto { +pub(crate) struct IrisQueryHtlcResponseProto { #[prost(message, tag = "1")] - pub(crate) htlc: Option, + pub(crate) htlc: Option, } diff --git a/mm2src/coins/tendermint/htlc/iris/mod.rs b/mm2src/coins/tendermint/htlc/iris/mod.rs new file mode 100644 index 0000000000..7e3fe6e382 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/iris/mod.rs @@ -0,0 +1,24 @@ +//! IRIS HTLC implementation in Rust on top of Cosmos SDK(cosmrs) for komodo-defi-framework. +//! +//! This module includes HTLC creating & claiming representation structstures +//! and their trait implementations. +//! +//! ** Acquiring testnet assets ** +//! +//! Since there is no sdk exists for Rust on Iris Network, we should +//! either implement some of the Iris Network funcionality on Rust or +//! simply use their unit tests. +//! +//! Because we had limited time for the HTLC implementation, for now +//! we can use their unit tests in order to acquire IBC assets. +//! For that, clone https://github.com/onur-ozkan/irishub-sdk-js repository and check +//! dummy.test.ts file(change the asset, amount, target address if needed) +//! and then run the following commands: +//! - yarn +//! - npm run test +//! +//! If the sender address doesn't have enough nyan tokens to complete unit tests, +//! check this page https://www.irisnet.org/docs/get-started/testnet.html#faucet + +pub(crate) mod htlc; +pub(crate) mod htlc_proto; diff --git a/mm2src/coins/tendermint/htlc/mod.rs b/mm2src/coins/tendermint/htlc/mod.rs new file mode 100644 index 0000000000..5675c915f2 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/mod.rs @@ -0,0 +1,303 @@ +mod iris; +mod nucleus; + +use std::{convert::TryFrom, str::FromStr}; + +use cosmrs::{tx::Msg, AccountId, Any, Coin, ErrorReport}; +use iris::htlc::{IrisClaimHtlcMsg, IrisCreateHtlcMsg}; +use nucleus::htlc::{NucleusClaimHtlcMsg, NucleusCreateHtlcMsg}; + +use iris::htlc_proto::{IrisClaimHtlcProto, IrisCreateHtlcProto, IrisQueryHtlcResponseProto}; +use nucleus::htlc_proto::{NucleusClaimHtlcProto, NucleusCreateHtlcProto, NucleusQueryHtlcResponseProto}; +use prost::{DecodeError, Message}; +use std::io; + +/// Defines an open state. +pub(crate) const HTLC_STATE_OPEN: i32 = 0; + +/// Defines a completed state. +pub(crate) const HTLC_STATE_COMPLETED: i32 = 1; + +/// Defines a refunded state. +pub(crate) const HTLC_STATE_REFUNDED: i32 = 2; + +/// Indicates whether this is an IRIS or Nucleus HTLC. +#[derive(Copy, Clone)] +pub(crate) enum HtlcType { + Nucleus, + Iris, +} + +impl FromStr for HtlcType { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + match s { + "iaa" => Ok(HtlcType::Iris), + "nuc" => Ok(HtlcType::Nucleus), + unsupported => Err(io::Error::new( + io::ErrorKind::Unsupported, + format!("Account type '{unsupported}' is not supported for HTLCs"), + )), + } + } +} + +impl HtlcType { + /// Returns the ABCI endpoint path for querying HTLCs. + pub(crate) fn get_htlc_abci_query_path(&self) -> String { + const NUCLEUS_PATH: &str = "/nucleus.htlc.Query/HTLC"; + const IRIS_PATH: &str = "/irismod.htlc.Query/HTLC"; + + match self { + Self::Nucleus => NUCLEUS_PATH.to_owned(), + Self::Iris => IRIS_PATH.to_owned(), + } + } +} + +/// Custom Tendermint message types specific to certain Cosmos chains and may not be available on all chains. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum CustomTendermintMsgType { + /// Create HTLC as sender. + SendHtlcAmount, + /// Claim HTLC as reciever. + ClaimHtlcAmount, + /// Claim HTLC for reciever. + SignClaimHtlc, +} + +/// Defines the state of an HTLC. +#[derive(prost::Enumeration, Debug)] +#[repr(i32)] +pub enum HtlcState { + /// Open state. + Open = HTLC_STATE_OPEN, + /// Completed state. + Completed = HTLC_STATE_COMPLETED, + /// Refunded state. + Refunded = HTLC_STATE_REFUNDED, +} + +#[allow(dead_code)] +pub(crate) struct TendermintHtlc { + /// Generated HTLC's ID. + pub(crate) id: String, + + /// Message payload to be sent. + pub(crate) msg_payload: Any, +} + +#[derive(prost::Message)] +pub(crate) struct QueryHtlcRequestProto { + /// HTLC ID to query. + #[prost(string, tag = "1")] + pub(crate) id: prost::alloc::string::String, +} + +/// Generic enum for abstracting multiple types of create HTLC messages. +#[derive(Debug, PartialEq)] +pub(crate) enum CreateHtlcMsg { + Nucleus(NucleusCreateHtlcMsg), + Iris(IrisCreateHtlcMsg), +} + +impl TryFrom for CreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(value: CreateHtlcProto) -> Result { + match value { + CreateHtlcProto::Nucleus(inner) => Ok(CreateHtlcMsg::Nucleus(NucleusCreateHtlcMsg::try_from(inner)?)), + CreateHtlcProto::Iris(inner) => Ok(CreateHtlcMsg::Iris(IrisCreateHtlcMsg::try_from(inner)?)), + } + } +} + +impl CreateHtlcMsg { + pub(crate) fn new( + htlc_type: HtlcType, + sender: AccountId, + to: AccountId, + amount: Vec, + hash_lock: String, + timestamp: u64, + time_lock: u64, + ) -> Self { + match htlc_type { + HtlcType::Iris => CreateHtlcMsg::Iris(IrisCreateHtlcMsg { + to, + sender, + receiver_on_other_chain: String::default(), + sender_on_other_chain: String::default(), + amount, + hash_lock, + time_lock, + timestamp, + transfer: false, + }), + HtlcType::Nucleus => CreateHtlcMsg::Nucleus(NucleusCreateHtlcMsg { + to, + sender, + amount, + hash_lock, + time_lock, + timestamp, + }), + } + } + + /// Returns the inner field `sender`. + pub(crate) fn sender(&self) -> &AccountId { + match self { + Self::Iris(inner) => &inner.sender, + Self::Nucleus(inner) => &inner.sender, + } + } + + /// Returns the inner field `to`. + pub(crate) fn to(&self) -> &AccountId { + match self { + Self::Iris(inner) => &inner.to, + Self::Nucleus(inner) => &inner.to, + } + } + + /// Returns the inner field `amount`. + pub(crate) fn amount(&self) -> &[Coin] { + match self { + Self::Iris(inner) => &inner.amount, + Self::Nucleus(inner) => &inner.amount, + } + } + + /// Generates `Any` from the inner CreateHTLC message. + pub(crate) fn to_any(&self) -> Result { + match self { + Self::Iris(inner) => inner.to_any(), + Self::Nucleus(inner) => inner.to_any(), + } + } +} + +/// Generic enum for abstracting multiple types of claim HTLC messages. +pub(crate) enum ClaimHtlcMsg { + Nucleus(NucleusClaimHtlcMsg), + Iris(IrisClaimHtlcMsg), +} + +impl ClaimHtlcMsg { + pub(crate) fn new(htlc_type: HtlcType, id: String, sender: AccountId, secret: String) -> Self { + match htlc_type { + HtlcType::Iris => ClaimHtlcMsg::Iris(IrisClaimHtlcMsg { sender, id, secret }), + HtlcType::Nucleus => ClaimHtlcMsg::Nucleus(NucleusClaimHtlcMsg { sender, id, secret }), + } + } + + /// Returns the inner field `secret`. + pub(crate) fn secret(&self) -> &str { + match self { + Self::Iris(inner) => &inner.secret, + Self::Nucleus(inner) => &inner.secret, + } + } + + /// Generates `Any` from the inner ClaimHTLC message. + pub(crate) fn to_any(&self) -> Result { + match self { + Self::Iris(inner) => inner.to_any(), + Self::Nucleus(inner) => inner.to_any(), + } + } +} + +impl TryFrom for ClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(value: ClaimHtlcProto) -> Result { + match value { + ClaimHtlcProto::Nucleus(inner) => Ok(ClaimHtlcMsg::Nucleus(NucleusClaimHtlcMsg::try_from(inner)?)), + ClaimHtlcProto::Iris(inner) => Ok(ClaimHtlcMsg::Iris(IrisClaimHtlcMsg::try_from(inner)?)), + } + } +} + +/// Generic enum for abstracting multiple types of create HTLC protos. +pub(crate) enum CreateHtlcProto { + Nucleus(NucleusCreateHtlcProto), + Iris(IrisCreateHtlcProto), +} + +impl CreateHtlcProto { + /// Decodes an instance (depending on the given `htlc_type`) of `CreateHtlcProto` from a buffer. + pub(crate) fn decode(htlc_type: HtlcType, buffer: &[u8]) -> Result { + match htlc_type { + HtlcType::Nucleus => Ok(Self::Nucleus(NucleusCreateHtlcProto::decode(buffer)?)), + HtlcType::Iris => Ok(Self::Iris(IrisCreateHtlcProto::decode(buffer)?)), + } + } + + /// Returns the inner field `hash_lock`. + pub(crate) fn hash_lock(&self) -> &str { + match self { + Self::Iris(inner) => &inner.hash_lock, + Self::Nucleus(inner) => &inner.hash_lock, + } + } +} + +/// Generic enum for abstracting multiple types of claim HTLC protos. +pub(crate) enum ClaimHtlcProto { + Nucleus(NucleusClaimHtlcProto), + Iris(IrisClaimHtlcProto), +} + +impl ClaimHtlcProto { + /// Decodes an instance (depending on the given `htlc_type`) of `ClaimHtlcProto` from a buffer. + pub(crate) fn decode(htlc_type: HtlcType, buffer: &[u8]) -> Result { + match htlc_type { + HtlcType::Nucleus => Ok(Self::Nucleus(NucleusClaimHtlcProto::decode(buffer)?)), + HtlcType::Iris => Ok(Self::Iris(IrisClaimHtlcProto::decode(buffer)?)), + } + } + + /// Returns the inner field `secret`. + #[cfg(test)] + pub(crate) fn secret(&self) -> &str { + match self { + Self::Iris(inner) => &inner.secret, + Self::Nucleus(inner) => &inner.secret, + } + } +} + +/// Generic enum for abstracting multiple types of HTLC responses. +pub(crate) enum QueryHtlcResponse { + Nucleus(NucleusQueryHtlcResponseProto), + Iris(IrisQueryHtlcResponseProto), +} + +impl QueryHtlcResponse { + /// Decodes an instance (depending on the given `htlc_type`) of `QueryHtlcResponse` from a buffer. + pub(crate) fn decode(htlc_type: HtlcType, buffer: &[u8]) -> Result { + match htlc_type { + HtlcType::Nucleus => Ok(Self::Nucleus(NucleusQueryHtlcResponseProto::decode(buffer)?)), + HtlcType::Iris => Ok(Self::Iris(IrisQueryHtlcResponseProto::decode(buffer)?)), + } + } + + /// Returns the inner field `htlc_state`. + pub(crate) fn htlc_state(&self) -> Option { + match self { + Self::Iris(inner) => Some(inner.htlc.as_ref()?.state), + Self::Nucleus(inner) => Some(inner.htlc.as_ref()?.state), + } + } + + /// Returns the inner field `hash_lock`. + pub(crate) fn hash_lock(&self) -> Option<&str> { + match self { + Self::Iris(inner) => Some(&inner.htlc.as_ref()?.hash_lock), + Self::Nucleus(inner) => Some(&inner.htlc.as_ref()?.hash_lock), + } + } +} diff --git a/mm2src/coins/tendermint/htlc/nucleus/htlc.rs b/mm2src/coins/tendermint/htlc/nucleus/htlc.rs new file mode 100644 index 0000000000..a768f7fd4a --- /dev/null +++ b/mm2src/coins/tendermint/htlc/nucleus/htlc.rs @@ -0,0 +1,131 @@ +use super::htlc_proto::{NucleusClaimHtlcProto, NucleusCreateHtlcProto}; + +use cosmrs::proto::traits::TypeUrl; +use cosmrs::{tx::Msg, AccountId, Coin, ErrorReport}; +use std::convert::TryFrom; + +pub(crate) const NUCLEUS_CREATE_HTLC_TYPE_URL: &str = "/nucleus.htlc.MsgCreateHTLC"; +pub(crate) const NUCLEUS_CLAIM_HTLC_TYPE_URL: &str = "/nucleus.htlc.MsgClaimHTLC"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct NucleusCreateHtlcMsg { + /// Sender's address. + pub(crate) to: AccountId, + + /// Recipient's address. + pub(crate) sender: AccountId, + + /// Amount to send. + pub(crate) amount: Vec, + + /// The sha256 hash generated from secret and timestamp. + pub(crate) hash_lock: String, + + /// The number of blocks to wait before the asset may be returned to. + pub(crate) time_lock: u64, + + /// The timestamp in seconds for generating hash lock if provided. + pub(crate) timestamp: u64, +} + +impl Msg for NucleusCreateHtlcMsg { + type Proto = NucleusCreateHtlcProto; +} + +impl TryFrom for NucleusCreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: NucleusCreateHtlcProto) -> Result { + NucleusCreateHtlcMsg::try_from(&proto) + } +} + +impl TryFrom<&NucleusCreateHtlcProto> for NucleusCreateHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: &NucleusCreateHtlcProto) -> Result { + Ok(NucleusCreateHtlcMsg { + sender: proto.sender.parse()?, + to: proto.to.parse()?, + amount: proto.amount.iter().map(TryFrom::try_from).collect::>()?, + hash_lock: proto.hash_lock.clone(), + timestamp: proto.timestamp, + time_lock: proto.time_lock, + }) + } +} + +impl From for NucleusCreateHtlcProto { + fn from(coin: NucleusCreateHtlcMsg) -> NucleusCreateHtlcProto { NucleusCreateHtlcProto::from(&coin) } +} + +impl From<&NucleusCreateHtlcMsg> for NucleusCreateHtlcProto { + fn from(msg: &NucleusCreateHtlcMsg) -> NucleusCreateHtlcProto { + NucleusCreateHtlcProto { + sender: msg.sender.to_string(), + to: msg.to.to_string(), + amount: msg.amount.iter().map(Into::into).collect(), + hash_lock: msg.hash_lock.clone(), + timestamp: msg.timestamp, + time_lock: msg.time_lock, + } + } +} + +impl TypeUrl for NucleusCreateHtlcProto { + const TYPE_URL: &'static str = NUCLEUS_CREATE_HTLC_TYPE_URL; +} + +#[derive(Clone)] +pub(crate) struct NucleusClaimHtlcMsg { + /// Sender's address. + pub(crate) sender: AccountId, + + /// Generated HTLC ID + pub(crate) id: String, + + /// Secret that has been used for generating hash_lock + pub(crate) secret: String, +} + +impl Msg for NucleusClaimHtlcMsg { + type Proto = NucleusClaimHtlcProto; +} + +impl TryFrom for NucleusClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: NucleusClaimHtlcProto) -> Result { + NucleusClaimHtlcMsg::try_from(&proto) + } +} + +impl TryFrom<&NucleusClaimHtlcProto> for NucleusClaimHtlcMsg { + type Error = ErrorReport; + + fn try_from(proto: &NucleusClaimHtlcProto) -> Result { + Ok(NucleusClaimHtlcMsg { + sender: proto.sender.parse()?, + id: proto.id.clone(), + secret: proto.secret.clone(), + }) + } +} + +impl From for NucleusClaimHtlcProto { + fn from(coin: NucleusClaimHtlcMsg) -> NucleusClaimHtlcProto { NucleusClaimHtlcProto::from(&coin) } +} + +impl From<&NucleusClaimHtlcMsg> for NucleusClaimHtlcProto { + fn from(msg: &NucleusClaimHtlcMsg) -> NucleusClaimHtlcProto { + NucleusClaimHtlcProto { + sender: msg.sender.to_string(), + id: msg.id.clone(), + secret: msg.secret.clone(), + } + } +} + +impl TypeUrl for NucleusClaimHtlcProto { + const TYPE_URL: &'static str = NUCLEUS_CLAIM_HTLC_TYPE_URL; +} diff --git a/mm2src/coins/tendermint/htlc/nucleus/htlc_proto.rs b/mm2src/coins/tendermint/htlc/nucleus/htlc_proto.rs new file mode 100644 index 0000000000..3a4c200eb6 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/nucleus/htlc_proto.rs @@ -0,0 +1,57 @@ +use crate::tendermint::htlc::HtlcState; + +#[derive(prost::Message)] +pub(crate) struct NucleusCreateHtlcProto { + #[prost(string, tag = "1")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) to: prost::alloc::string::String, + #[prost(message, repeated, tag = "3")] + pub(crate) amount: prost::alloc::vec::Vec, + #[prost(string, tag = "4")] + pub(crate) hash_lock: prost::alloc::string::String, + #[prost(uint64, tag = "5")] + pub(crate) timestamp: u64, + #[prost(uint64, tag = "6")] + pub(crate) time_lock: u64, +} + +#[derive(prost::Message)] +pub(crate) struct NucleusClaimHtlcProto { + #[prost(string, tag = "1")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) id: prost::alloc::string::String, + #[prost(string, tag = "3")] + pub(crate) secret: prost::alloc::string::String, +} + +#[derive(prost::Message)] +pub struct NucleusHtlcProto { + #[prost(string, tag = "1")] + pub(crate) id: prost::alloc::string::String, + #[prost(string, tag = "2")] + pub(crate) sender: prost::alloc::string::String, + #[prost(string, tag = "3")] + pub(crate) to: prost::alloc::string::String, + #[prost(message, repeated, tag = "4")] + pub(crate) amount: prost::alloc::vec::Vec, + #[prost(string, tag = "5")] + pub(crate) hash_lock: prost::alloc::string::String, + #[prost(string, tag = "6")] + pub(crate) secret: prost::alloc::string::String, + #[prost(uint64, tag = "7")] + pub(crate) timestamp: u64, + #[prost(uint64, tag = "8")] + pub(crate) expiration_height: u64, + #[prost(enumeration = "HtlcState", tag = "9")] + pub(crate) state: i32, + #[prost(uint64, tag = "10")] + pub(crate) closed_block: u64, +} + +#[derive(prost::Message)] +pub(crate) struct NucleusQueryHtlcResponseProto { + #[prost(message, tag = "1")] + pub(crate) htlc: Option, +} diff --git a/mm2src/coins/tendermint/htlc/nucleus/mod.rs b/mm2src/coins/tendermint/htlc/nucleus/mod.rs new file mode 100644 index 0000000000..3b96898679 --- /dev/null +++ b/mm2src/coins/tendermint/htlc/nucleus/mod.rs @@ -0,0 +1,7 @@ +//! Nucleus HTLC implementation in Rust on top of Cosmos SDK(cosmrs) for komodo-defi-framework. +//! +//! This module includes HTLC creating & claiming representation structstures +//! and their trait implementations. + +pub(crate) mod htlc; +pub(crate) mod htlc_proto; diff --git a/mm2src/coins/tendermint/ibc/mod.rs b/mm2src/coins/tendermint/ibc/mod.rs index 9e1c905398..51df375ab1 100644 --- a/mm2src/coins/tendermint/ibc/mod.rs +++ b/mm2src/coins/tendermint/ibc/mod.rs @@ -1,6 +1,7 @@ mod ibc_proto; pub(crate) mod transfer_v1; +pub(crate) const IBC_TRANSFER_TYPE_URL: &str = "/ibc.applications.transfer.v1.MsgTransfer"; pub(crate) const IBC_OUT_SOURCE_PORT: &str = "transfer"; pub(crate) const IBC_OUT_TIMEOUT_IN_NANOS: u64 = 60000000000 * 15; // 15 minutes pub(crate) const IBC_GAS_LIMIT_DEFAULT: u64 = 150_000; diff --git a/mm2src/coins/tendermint/ibc/transfer_v1.rs b/mm2src/coins/tendermint/ibc/transfer_v1.rs index 34f693aaaa..c5780e32b7 100644 --- a/mm2src/coins/tendermint/ibc/transfer_v1.rs +++ b/mm2src/coins/tendermint/ibc/transfer_v1.rs @@ -1,8 +1,7 @@ use super::{ibc_proto::IBCTransferV1Proto, IBC_OUT_SOURCE_PORT, IBC_OUT_TIMEOUT_IN_NANOS}; -use crate::tendermint::type_urls::IBC_TRANSFER_TYPE_URL; -use common::number_type_casting::SafeTypeCastingNumbers; -use cosmrs::{tx::{Msg, MsgProto}, - AccountId, Coin, ErrorReport}; +use crate::tendermint::ibc::IBC_TRANSFER_TYPE_URL; +use cosmrs::proto::traits::TypeUrl; +use cosmrs::{tx::Msg, AccountId, Coin, ErrorReport}; use std::convert::TryFrom; #[derive(Clone, Debug, Eq, PartialEq)] @@ -34,10 +33,7 @@ impl MsgTransfer { receiver: AccountId, token: Coin, ) -> Self { - let timestamp_as_nanos: u64 = common::get_local_duration_since_epoch() - .expect("get_local_duration_since_epoch shouldn't fail") - .as_nanos() - .into_or_max(); + let timestamp_as_nanos = common::get_utc_timestamp_nanos() as u64; Self { source_port: IBC_OUT_SOURCE_PORT.to_owned(), @@ -103,6 +99,6 @@ impl From<&MsgTransfer> for IBCTransferV1Proto { } } -impl MsgProto for IBCTransferV1Proto { +impl TypeUrl for IBCTransferV1Proto { const TYPE_URL: &'static str = IBC_TRANSFER_TYPE_URL; } diff --git a/mm2src/coins/tendermint/iris/htlc.rs b/mm2src/coins/tendermint/iris/htlc.rs deleted file mode 100644 index 4e7b679481..0000000000 --- a/mm2src/coins/tendermint/iris/htlc.rs +++ /dev/null @@ -1,176 +0,0 @@ -// IRIS HTLC implementation in Rust on top of Cosmos SDK(cosmrs) for AtomicDEX. -// -// This module includes HTLC creating & claiming representation structstures -// and their trait implementations. -// -// ** Acquiring testnet assets ** -// -// Since there is no sdk exists for Rust on Iris Network, we should -// either implement some of the Iris Network funcionality on Rust or -// simply use their unit tests. -// -// Because we had limited time for the HTLC implementation, for now -// we can use their unit tests in order to acquire IBC assets. -// For that, clone https://github.com/onur-ozkan/irishub-sdk-js repository and check -// dummy.test.ts file(change the asset, amount, target address if needed) -// and then run the following commands: -// - yarn -// - npm run test -// -// If the sender address doesn't have enough nyan tokens to complete unit tests, -// check this page https://www.irisnet.org/docs/get-started/testnet.html#faucet - -use super::htlc_proto::{ClaimHtlcProtoRep, CreateHtlcProtoRep}; - -use crate::tendermint::type_urls::{CLAIM_HTLC_TYPE_URL, CREATE_HTLC_TYPE_URL}; -use cosmrs::{tx::{Msg, MsgProto}, - AccountId, Coin, ErrorReport}; -use std::convert::TryFrom; - -// https://github.com/irisnet/irismod/blob/043e058cd6e17f4f96d32f17bfd20b67debfab0b/proto/htlc/htlc.proto#L36 -pub const HTLC_STATE_OPEN: i32 = 0; -pub const HTLC_STATE_COMPLETED: i32 = 1; -pub const HTLC_STATE_REFUNDED: i32 = 2; - -#[allow(dead_code)] -pub(crate) struct IrisHtlc { - /// Generated HTLC's ID. - pub(crate) id: String, - - /// Message payload to be sent - pub(crate) msg_payload: cosmrs::Any, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct MsgCreateHtlc { - /// Sender's address. - pub(crate) to: AccountId, - - /// Recipient's address. - pub(crate) sender: AccountId, - - /// The claim receiving address on the other chain. - pub(crate) receiver_on_other_chain: String, - - /// The counterparty creator address on the other chain. - pub(crate) sender_on_other_chain: String, - - /// Amount to send. - pub(crate) amount: Vec, - - /// The sha256 hash generated from secret and timestamp. - pub(crate) hash_lock: String, - - /// The number of blocks to wait before the asset may be returned to. - pub(crate) time_lock: u64, - - /// The timestamp in seconds for generating hash lock if provided. - pub(crate) timestamp: u64, - - /// Whether it is an HTLT transaction. - pub(crate) transfer: bool, -} - -impl Msg for MsgCreateHtlc { - type Proto = CreateHtlcProtoRep; -} - -impl TryFrom for MsgCreateHtlc { - type Error = ErrorReport; - - fn try_from(proto: CreateHtlcProtoRep) -> Result { MsgCreateHtlc::try_from(&proto) } -} - -impl TryFrom<&CreateHtlcProtoRep> for MsgCreateHtlc { - type Error = ErrorReport; - - fn try_from(proto: &CreateHtlcProtoRep) -> Result { - Ok(MsgCreateHtlc { - sender: proto.sender.parse()?, - to: proto.to.parse()?, - amount: proto.amount.iter().map(TryFrom::try_from).collect::>()?, - receiver_on_other_chain: proto.receiver_on_other_chain.clone(), - sender_on_other_chain: proto.sender_on_other_chain.clone(), - hash_lock: proto.hash_lock.clone(), - timestamp: proto.timestamp, - time_lock: proto.time_lock, - transfer: proto.transfer, - }) - } -} - -impl From for CreateHtlcProtoRep { - fn from(coin: MsgCreateHtlc) -> CreateHtlcProtoRep { CreateHtlcProtoRep::from(&coin) } -} - -impl From<&MsgCreateHtlc> for CreateHtlcProtoRep { - fn from(msg: &MsgCreateHtlc) -> CreateHtlcProtoRep { - CreateHtlcProtoRep { - sender: msg.sender.to_string(), - to: msg.to.to_string(), - amount: msg.amount.iter().map(Into::into).collect(), - receiver_on_other_chain: msg.receiver_on_other_chain.clone(), - sender_on_other_chain: msg.sender_on_other_chain.clone(), - hash_lock: msg.hash_lock.clone(), - timestamp: msg.timestamp, - time_lock: msg.time_lock, - transfer: msg.transfer, - } - } -} - -impl MsgProto for CreateHtlcProtoRep { - const TYPE_URL: &'static str = CREATE_HTLC_TYPE_URL; -} - -#[derive(Clone)] -pub(crate) struct MsgClaimHtlc { - /// Sender's address. - pub(crate) sender: AccountId, - - /// Generated HTLC ID - pub(crate) id: String, - - /// Secret that has been used for generating hash_lock - pub(crate) secret: String, -} - -impl Msg for MsgClaimHtlc { - type Proto = ClaimHtlcProtoRep; -} - -impl TryFrom for MsgClaimHtlc { - type Error = ErrorReport; - - fn try_from(proto: ClaimHtlcProtoRep) -> Result { MsgClaimHtlc::try_from(&proto) } -} - -impl TryFrom<&ClaimHtlcProtoRep> for MsgClaimHtlc { - type Error = ErrorReport; - - fn try_from(proto: &ClaimHtlcProtoRep) -> Result { - Ok(MsgClaimHtlc { - sender: proto.sender.parse()?, - id: proto.id.clone(), - secret: proto.secret.clone(), - }) - } -} - -impl From for ClaimHtlcProtoRep { - fn from(coin: MsgClaimHtlc) -> ClaimHtlcProtoRep { ClaimHtlcProtoRep::from(&coin) } -} - -impl From<&MsgClaimHtlc> for ClaimHtlcProtoRep { - fn from(msg: &MsgClaimHtlc) -> ClaimHtlcProtoRep { - ClaimHtlcProtoRep { - sender: msg.sender.to_string(), - id: msg.id.clone(), - secret: msg.secret.clone(), - } - } -} - -impl MsgProto for ClaimHtlcProtoRep { - const TYPE_URL: &'static str = CLAIM_HTLC_TYPE_URL; -} diff --git a/mm2src/coins/tendermint/iris/mod.rs b/mm2src/coins/tendermint/iris/mod.rs deleted file mode 100644 index 03331ac181..0000000000 --- a/mm2src/coins/tendermint/iris/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod ethermint_account; -pub(crate) mod htlc; -pub(crate) mod htlc_proto; diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index 60a4c61ec1..78009b5db8 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -2,33 +2,19 @@ // Useful resources // https://docs.cosmos.network/ +pub(crate) mod ethermint_account; +pub mod htlc; mod ibc; -mod iris; mod rpc; mod tendermint_balance_events; mod tendermint_coin; mod tendermint_token; pub mod tendermint_tx_history_v2; +pub use cosmrs::tendermint::PublicKey as TendermintPublicKey; +pub use cosmrs::AccountId; pub use tendermint_coin::*; pub use tendermint_token::*; -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub enum CustomTendermintMsgType { - /// Create HTLC as sender - SendHtlcAmount, - /// Claim HTLC as reciever - ClaimHtlcAmount, - /// Claim HTLC for reciever - SignClaimHtlc, -} - pub(crate) const TENDERMINT_COIN_PROTOCOL_TYPE: &str = "TENDERMINT"; pub(crate) const TENDERMINT_ASSET_PROTOCOL_TYPE: &str = "TENDERMINTTOKEN"; - -pub(crate) mod type_urls { - pub(crate) const IBC_TRANSFER_TYPE_URL: &str = "/ibc.applications.transfer.v1.MsgTransfer"; - - pub(crate) const CREATE_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgCreateHTLC"; - pub(crate) const CLAIM_HTLC_TYPE_URL: &str = "/irismod.htlc.MsgClaimHTLC"; -} diff --git a/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs index 4904a2ed30..27ae5e7a0e 100644 --- a/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs +++ b/mm2src/coins/tendermint/rpc/tendermint_native_rpc.rs @@ -1,15 +1,13 @@ use async_trait::async_trait; use core::convert::{TryFrom, TryInto}; use core::str::FromStr; -pub use cosmrs::tendermint::abci::Path as AbciPath; -use cosmrs::tendermint::abci::{self, Transaction}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::evidence::Evidence; use cosmrs::tendermint::Genesis; +use cosmrs::tendermint::Hash; use serde::{de::DeserializeOwned, Serialize}; use std::fmt; use std::time::Duration; -use tendermint_config::net; use tendermint_rpc::endpoint::validators::DEFAULT_VALIDATORS_PER_PAGE; use tendermint_rpc::endpoint::*; pub use tendermint_rpc::endpoint::{abci_query::Request as AbciRequest, health::Request as HealthRequest, @@ -28,14 +26,12 @@ use tokio::time; #[async_trait] pub trait Client { /// `/abci_info`: get information about the ABCI application. - async fn abci_info(&self) -> Result { - Ok(self.perform(abci_info::Request).await?.response) - } + async fn abci_info(&self) -> Result { self.perform(abci_info::Request).await } /// `/abci_query`: query the ABCI application async fn abci_query( &self, - path: Option, + path: Option, data: V, height: Option, prove: bool, @@ -99,19 +95,19 @@ pub trait Client { } /// `/broadcast_tx_async`: broadcast a transaction, returning immediately. - async fn broadcast_tx_async(&self, tx: Transaction) -> Result { + async fn broadcast_tx_async(&self, tx: Vec) -> Result { self.perform(broadcast::tx_async::Request::new(tx)).await } /// `/broadcast_tx_sync`: broadcast a transaction, returning the response /// from `CheckTx`. - async fn broadcast_tx_sync(&self, tx: Transaction) -> Result { + async fn broadcast_tx_sync(&self, tx: Vec) -> Result { self.perform(broadcast::tx_sync::Request::new(tx)).await } /// `/broadcast_tx_commit`: broadcast a transaction, returning the response /// from `DeliverTx`. - async fn broadcast_tx_commit(&self, tx: Transaction) -> Result { + async fn broadcast_tx_commit(&self, tx: Vec) -> Result { self.perform(broadcast::tx_commit::Request::new(tx)).await } @@ -217,7 +213,7 @@ pub trait Client { } /// `/tx`: find transaction by hash. - async fn tx(&self, hash: abci::transaction::Hash, prove: bool) -> Result { + async fn tx(&self, hash: Hash, prove: bool) -> Result { self.perform(tx::Request::new(hash, prove)).await } @@ -257,7 +253,7 @@ pub trait Client { } /// Perform a request against the RPC endpoint - async fn perform(&self, request: R) -> Result + async fn perform(&self, request: R) -> Result where R: SimpleRequest; } @@ -316,11 +312,11 @@ impl HttpClient { #[async_trait] impl Client for HttpClient { - async fn perform(&self, request: R) -> Result + async fn perform(&self, request: R) -> Result where R: SimpleRequest, { - self.inner.perform(request).await + self.inner.perform(request).await.map(From::from) } } @@ -356,17 +352,6 @@ impl TryFrom<&str> for HttpClientUrl { fn try_from(value: &str) -> Result { value.parse() } } -impl TryFrom for HttpClientUrl { - type Error = Error; - - fn try_from(value: net::Address) -> Result { - match value { - net::Address::Tcp { peer_id: _, host, port } => format!("http://{}:{}", host, port).parse(), - net::Address::Unix { .. } => Err(Error::invalid_network_address()), - } - } -} - impl From for Url { fn from(url: HttpClientUrl) -> Self { url.0 } } diff --git a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs index bcbc07c874..3909ce2c25 100644 --- a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs +++ b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs @@ -1,13 +1,11 @@ use common::APPLICATION_JSON; -pub use cosmrs::tendermint::abci::Path as AbciPath; -use cosmrs::tendermint::abci::Transaction; use cosmrs::tendermint::block::Height; use derive_more::Display; use http::header::{ACCEPT, CONTENT_TYPE}; use http::uri::InvalidUri; use http::{StatusCode, Uri}; use mm2_net::transport::SlurpError; -use mm2_net::wasm_http::FetchRequest; +use mm2_net::wasm::http::FetchRequest; use std::str::FromStr; use tendermint_rpc::endpoint::{abci_info, broadcast}; pub use tendermint_rpc::endpoint::{abci_query::{AbciQuery, Request as AbciRequest}, @@ -61,7 +59,7 @@ impl HttpClient { #[inline] pub fn uri(&self) -> http::Uri { Uri::from_str(&self.uri).expect("This should never happen.") } - pub(crate) async fn perform(&self, request: R) -> Result + pub(crate) async fn perform(&self, request: R) -> Result where R: SimpleRequest, { @@ -80,18 +78,18 @@ impl HttpClient { response: response_str, }); } - Ok(R::Response::from_string(response_str)?) + Ok(R::Response::from_string(response_str)?.into()) } /// `/abci_info`: get information about the ABCI application. - pub async fn abci_info(&self) -> Result { - Ok(self.perform(abci_info::Request).await?.response) + pub async fn abci_info(&self) -> Result { + self.perform(abci_info::Request).await } /// `/abci_query`: query the ABCI application pub async fn abci_query( &self, - path: Option, + path: Option, data: V, height: Option, prove: bool, @@ -107,7 +105,7 @@ impl HttpClient { /// `/broadcast_tx_commit`: broadcast a transaction, returning the response /// from `DeliverTx`. - pub async fn broadcast_tx_commit(&self, tx: Transaction) -> Result { + pub async fn broadcast_tx_commit(&self, tx: Vec) -> Result { self.perform(broadcast::tx_commit::Request::new(tx)).await } } diff --git a/mm2src/coins/tendermint/tendermint_balance_events.rs b/mm2src/coins/tendermint/tendermint_balance_events.rs index 122262eb51..7b8cd8ca03 100644 --- a/mm2src/coins/tendermint/tendermint_balance_events.rs +++ b/mm2src/coins/tendermint/tendermint_balance_events.rs @@ -7,7 +7,7 @@ use jsonrpc_core::MethodCall; use jsonrpc_core::{Id as RpcId, Params as RpcParams, Value as RpcValue, Version as RpcVersion}; use mm2_core::mm_ctx::MmArc; use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, - Event, EventStreamConfiguration}; + ErrorEventName, Event, EventName, EventStreamConfiguration}; use mm2_number::BigDecimal; use std::collections::{HashMap, HashSet}; @@ -16,7 +16,9 @@ use crate::{tendermint::TendermintCommons, utxo::utxo_common::big_decimal_from_s #[async_trait] impl EventBehaviour for TendermintCoin { - const EVENT_NAME: &'static str = "COIN_BALANCE"; + fn event_name() -> EventName { EventName::CoinBalance } + + fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } async fn handle(self, _interval: f64, tx: oneshot::Sender) { fn generate_subscription_query(query_filter: String) -> String { @@ -66,10 +68,10 @@ impl EventBehaviour for TendermintCoin { let socket_address = format!("{}/{}", http_uri_to_ws_address(node_uri), "websocket"); - let mut wsocket = match tokio_tungstenite_wasm::connect(socket_address).await { + let mut wsocket = match tokio_tungstenite_wasm::connect(&socket_address).await { Ok(ws) => ws, Err(e) => { - log::error!("{e}"); + log::error!("Couldn't connect to '{socket_address}': {e}"); continue; }, }; @@ -114,12 +116,21 @@ impl EventBehaviour for TendermintCoin { }) .collect(); + let mut balance_updates = vec![]; for denom in denoms { if let Some((ticker, decimals)) = self.active_ticker_and_decimals_from_denom(&denom) { let balance_denom = match self.account_balance_for_denom(&self.account_id, denom).await { Ok(balance_denom) => balance_denom, Err(e) => { - log::error!("{e}"); + log::error!("Failed getting balance for '{ticker}'. Error: {e}"); + let e = serde_json::to_value(e).expect("Serialization should't fail."); + ctx.stream_channel_controller + .broadcast(Event::new( + format!("{}:{}", Self::error_event_name(), ticker), + e.to_string(), + )) + .await; + continue; }, }; @@ -139,35 +150,43 @@ impl EventBehaviour for TendermintCoin { } if broadcast { - let payload = json!({ + balance_updates.push(json!({ "ticker": ticker, "balance": { "spendable": balance_decimal, "unspendable": BigDecimal::default() } - }); - - ctx.stream_channel_controller - .broadcast(Event::new(Self::EVENT_NAME.to_string(), payload.to_string())) - .await; + })); } } } + + if !balance_updates.is_empty() { + ctx.stream_channel_controller + .broadcast(Event::new( + Self::event_name().to_string(), + json!(balance_updates).to_string(), + )) + .await; + } } } } } async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { - if let Some(event) = config.get_event(Self::EVENT_NAME) { + if let Some(event) = config.get_event(&Self::event_name()) { log::info!( "{} event is activated for {}. `stream_interval_seconds`({}) has no effect on this.", - Self::EVENT_NAME, + Self::event_name(), self.ticker(), event.stream_interval_seconds ); let (tx, rx): (Sender, Receiver) = oneshot::channel(); let fut = self.clone().handle(event.stream_interval_seconds, tx); - let settings = - AbortSettings::info_on_abort(format!("{} event is stopped for {}.", Self::EVENT_NAME, self.ticker())); + let settings = AbortSettings::info_on_abort(format!( + "{} event is stopped for {}.", + Self::event_name(), + self.ticker() + )); self.spawner().spawn_with_settings(fut, settings); rx.await.unwrap_or_else(|e| { diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 89b79573c3..f3854ba4ee 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -1,15 +1,14 @@ +use super::ethermint_account::EthermintAccount; +use super::htlc::{ClaimHtlcMsg, ClaimHtlcProto, CreateHtlcMsg, CreateHtlcProto, HtlcType, QueryHtlcRequestProto, + QueryHtlcResponse, TendermintHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, HTLC_STATE_REFUNDED}; use super::ibc::transfer_v1::MsgTransfer; use super::ibc::IBC_GAS_LIMIT_DEFAULT; -use super::iris::ethermint_account::EthermintAccount; -use super::iris::htlc::{IrisHtlc, MsgClaimHtlc, MsgCreateHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, - HTLC_STATE_REFUNDED}; -use super::iris::htlc_proto::{CreateHtlcProtoRep, QueryHtlcRequestProto, QueryHtlcResponseProto}; -use super::rpc::*; -use crate::coin_errors::{MyAddressError, ValidatePaymentError}; +use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; +use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; +use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, - IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequest, - IBCTransferChannelsRequestError, IBCTransferChannelsResponse, - IBCTransferChannelsResult, IBCWithdrawRequest, CHAIN_REGISTRY_BRANCH, + IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, + IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, CHAIN_REGISTRY_IBC_DIR_NAME, CHAIN_REGISTRY_REPO_NAME, CHAIN_REGISTRY_REPO_OWNER}; use crate::tendermint::ibc::IBC_OUT_SOURCE_PORT; use crate::utxo::sat_from_big_decimal; @@ -19,18 +18,19 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, - RefundError, RefundPaymentArgs, RefundResult, RpcCommonOps, SearchForSwapTxSpendInput, - SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureError, SignatureResult, SpendPaymentArgs, - SwapOps, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, - TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, - ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFee, WithdrawFrom, WithdrawFut, WithdrawRequest}; + RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, RpcCommonOps, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, + SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, ToBytes, TradeFee, + TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, + TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, + ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; +use bip32::DerivationPath; use bitcrypto::{dhash160, sha256}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::executor::{AbortedError, Timer}; @@ -45,36 +45,39 @@ use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, use cosmrs::proto::cosmos::base::v1beta1::Coin as CoinProto; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; +use cosmrs::proto::prost::{DecodeError, Message}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; use cosmrs::tx::{self, Fee, Msg, Raw, SignDoc, SignerInfo}; use cosmrs::{AccountId, Any, Coin, Denom, ErrorReport}; use crypto::privkey::key_pair_from_secret; -use crypto::{Secp256k1Secret, StandardHDCoinAddress, StandardHDPathToCoin}; +use crypto::{HDPathToCoin, Secp256k1Secret}; use derive_more::Display; use futures::future::try_join_all; use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use hex::FromHexError; +use instant::Duration; use itertools::Itertools; -use keys::KeyPair; +use keys::{KeyPair, Public}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, GITHUB_API_URI}; use mm2_number::MmNumber; use parking_lot::Mutex as PaMutex; use primitives::hash::H256; -use prost::{DecodeError, Message}; +use regex::Regex; use rpc::v1::types::Bytes as BytesJson; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; use std::convert::TryFrom; +use std::io; +use std::num::NonZeroU32; use std::ops::Deref; use std::str::FromStr; use std::sync::{Arc, Mutex}; -use std::time::Duration; use uuid::Uuid; // ABCI Request Paths @@ -84,7 +87,6 @@ const ABCI_SIMULATE_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/Simulate"; const ABCI_QUERY_ACCOUNT_PATH: &str = "/cosmos.auth.v1beta1.Query/Account"; const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance"; const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx"; -const ABCI_QUERY_HTLC_PATH: &str = "/irismod.htlc.Query/HTLC"; const ABCI_GET_TXS_EVENT_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTxsEvent"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -97,15 +99,39 @@ const ABCI_REQUEST_PROVE: bool = false; const DEFAULT_GAS_PRICE: f64 = 0.25; pub(super) const TIMEOUT_HEIGHT_DELTA: u64 = 100; pub const GAS_LIMIT_DEFAULT: u64 = 125_000; +pub const GAS_WANTED_BASE_VALUE: f64 = 50_000.; pub(crate) const TX_DEFAULT_MEMO: &str = ""; // https://github.com/irisnet/irismod/blob/5016c1be6fdbcffc319943f33713f4a057622f0a/modules/htlc/types/validation.go#L19-L22 const MAX_TIME_LOCK: i64 = 34560; const MIN_TIME_LOCK: i64 = 50; -const ACCOUNT_SEQUENCE_ERR: &str = "incorrect account sequence"; +const ACCOUNT_SEQUENCE_ERR: &str = "account sequence mismatch"; -type TendermintPrivKeyPolicy = PrivKeyPolicy; +lazy_static! { + static ref SEQUENCE_PARSER_REGEX: Regex = Regex::new(r"expected (\d+)").unwrap(); +} + +pub struct SerializedUnsignedTx { + tx_json: Json, + body_bytes: Vec, +} + +type TendermintPrivKeyPolicy = PrivKeyPolicy; + +pub struct TendermintKeyPair { + private_key_secret: Secp256k1Secret, + public_key: Public, +} + +impl TendermintKeyPair { + fn new(private_key_secret: Secp256k1Secret, public_key: Public) -> Self { + Self { + private_key_secret, + public_key, + } + } +} #[async_trait] pub trait TendermintCommons { @@ -115,7 +141,7 @@ pub trait TendermintCommons { async fn get_block_timestamp(&self, block: i64) -> MmResult, TendermintCoinRpcError>; - async fn all_balances(&self) -> MmResult; + async fn get_all_balances(&self) -> MmResult; async fn rpc_client(&self) -> MmResult; } @@ -151,7 +177,7 @@ pub struct TendermintConf { /// This derivation path consists of `purpose` and `coin_type` only /// where the full `BIP44` address has the following structure: /// `m/purpose'/coin_type'/account'/change/address_index`. - derivation_path: Option, + derivation_path: Option, } impl TendermintConf { @@ -183,6 +209,98 @@ impl TendermintConf { } } +pub enum TendermintActivationPolicy { + PrivateKey(PrivKeyPolicy), + PublicKey(PublicKey), +} + +impl TendermintActivationPolicy { + pub fn with_private_key_policy(private_key_policy: PrivKeyPolicy) -> Self { + Self::PrivateKey(private_key_policy) + } + + pub fn with_public_key(account_public_key: PublicKey) -> Self { Self::PublicKey(account_public_key) } + + fn generate_account_id(&self, account_prefix: &str) -> Result { + match self { + Self::PrivateKey(priv_key_policy) => { + let pk = priv_key_policy.activated_key().ok_or_else(|| { + ErrorReport::new(io::Error::new(io::ErrorKind::NotFound, "Activated key not found")) + })?; + + Ok( + account_id_from_privkey(pk.private_key_secret.as_slice(), account_prefix) + .map_err(|e| ErrorReport::new(io::Error::new(io::ErrorKind::InvalidData, e.to_string())))?, + ) + }, + + Self::PublicKey(account_public_key) => { + account_id_from_raw_pubkey(account_prefix, &account_public_key.to_bytes()) + }, + } + } + + fn public_key(&self) -> Result { + match self { + Self::PrivateKey(private_key_policy) => match private_key_policy { + PrivKeyPolicy::Iguana(pair) => PublicKey::from_raw_secp256k1(&pair.public_key.to_bytes()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")), + + PrivKeyPolicy::HDWallet { activated_key, .. } => { + PublicKey::from_raw_secp256k1(&activated_key.public_key.to_bytes()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")) + }, + + PrivKeyPolicy::Trezor => Err(io::Error::new( + io::ErrorKind::Unsupported, + "Trezor is not supported yet!", + )), + + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => unreachable!(), + }, + Self::PublicKey(account_public_key) => Ok(*account_public_key), + } + } + + pub(crate) fn activated_key_or_err(&self) -> Result<&Secp256k1Secret, MmError> { + match self { + Self::PrivateKey(private_key) => Ok(private_key.activated_key_or_err()?.private_key_secret.as_ref()), + Self::PublicKey(_) => MmError::err(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`activated_key_or_err` is not supported for pubkey-only activations".to_string(), + )), + } + } + + pub(crate) fn activated_key(&self) -> Option { + match self { + Self::PrivateKey(private_key) => Some(*private_key.activated_key()?.private_key_secret.as_ref()), + Self::PublicKey(_) => None, + } + } + + pub(crate) fn path_to_coin_or_err(&self) -> Result<&HDPathToCoin, MmError> { + match self { + Self::PrivateKey(private_key) => Ok(private_key.path_to_coin_or_err()?), + Self::PublicKey(_) => MmError::err(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`path_to_coin_or_err` is not supported for pubkey-only activations".to_string(), + )), + } + } + + pub(crate) fn hd_wallet_derived_priv_key_or_err( + &self, + path_to_address: &DerivationPath, + ) -> Result> { + match self { + Self::PrivateKey(pair) => pair.hd_wallet_derived_priv_key_or_err(path_to_address), + Self::PublicKey(_) => MmError::err(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`hd_wallet_derived_priv_key_or_err` is not supported for pubkey-only activations".to_string(), + )), + } + } +} + struct TendermintRpcClient(AsyncMutex); struct TendermintRpcClientImpl { @@ -225,7 +343,7 @@ pub struct TendermintCoinImpl { /// My address pub account_id: AccountId, pub(super) account_prefix: String, - pub(super) priv_key_policy: TendermintPrivKeyPolicy, + pub(super) activation_policy: TendermintActivationPolicy, pub(crate) decimals: u8, pub(super) denom: Denom, chain_id: ChainId, @@ -236,8 +354,9 @@ pub struct TendermintCoinImpl { pub(super) abortable_system: AbortableQueue, pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, - chain_registry_name: Option, + pub(crate) chain_registry_name: Option, pub(crate) ctx: MmWeak, + pub(crate) is_keplr_from_ledger: bool, } #[derive(Clone)] @@ -249,13 +368,13 @@ impl Deref for TendermintCoin { fn deref(&self) -> &Self::Target { &self.0 } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct TendermintInitError { pub ticker: String, pub kind: TendermintInitErrorKind, } -#[derive(Display, Debug)] +#[derive(Display, Debug, Clone)] pub enum TendermintInitErrorKind { Internal(String), InvalidPrivKey(String), @@ -264,6 +383,7 @@ pub enum TendermintInitErrorKind { RpcClientInitError(String), InvalidChainId(String), InvalidDenom(String), + InvalidPathToAddress(String), #[display(fmt = "'derivation_path' field is not found in config")] DerivationPathIsNotSet, #[display(fmt = "'account' field is not found in config")] @@ -281,19 +401,26 @@ pub enum TendermintInitErrorKind { #[display(fmt = "avg_blocktime must be in-between '0' and '255'.")] AvgBlockTimeInvalid, BalanceStreamInitError(String), + #[display(fmt = "Watcher features can not be used with pubkey-only activation policy.")] + CantUseWatchersWithPubkeyPolicy, } -#[derive(Display, Debug)] +#[derive(Display, Debug, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] pub enum TendermintCoinRpcError { - Prost(DecodeError), + Prost(String), InvalidResponse(String), PerformError(String), RpcClientError(String), InternalError(String), + #[display(fmt = "Account type '{}' is not supported for HTLCs", prefix)] + UnexpectedAccountType { + prefix: String, + }, } impl From for TendermintCoinRpcError { - fn from(err: DecodeError) -> Self { TendermintCoinRpcError::Prost(err) } + fn from(err: DecodeError) -> Self { TendermintCoinRpcError::Prost(err.to_string()) } } impl From for TendermintCoinRpcError { @@ -308,10 +435,13 @@ impl From for BalanceError { fn from(err: TendermintCoinRpcError) -> Self { match err { TendermintCoinRpcError::InvalidResponse(e) => BalanceError::InvalidResponse(e), - TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e.to_string()), + TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e), TendermintCoinRpcError::PerformError(e) => BalanceError::Transport(e), TendermintCoinRpcError::RpcClientError(e) => BalanceError::Transport(e), TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e), + TendermintCoinRpcError::UnexpectedAccountType { prefix } => { + BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs")) + }, } } } @@ -320,10 +450,13 @@ impl From for ValidatePaymentError { fn from(err: TendermintCoinRpcError) -> Self { match err { TendermintCoinRpcError::InvalidResponse(e) => ValidatePaymentError::InvalidRpcResponse(e), - TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e.to_string()), + TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e), TendermintCoinRpcError::PerformError(e) => ValidatePaymentError::Transport(e), TendermintCoinRpcError::RpcClientError(e) => ValidatePaymentError::Transport(e), TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e), + TendermintCoinRpcError::UnexpectedAccountType { prefix } => { + ValidatePaymentError::InvalidParameter(format!("Account type '{prefix}' is not supported for HTLCs")) + }, } } } @@ -354,7 +487,7 @@ pub struct CosmosTransaction { impl crate::Transaction for CosmosTransaction { fn tx_hex(&self) -> Vec { self.data.encode_to_vec() } - fn tx_hash(&self) -> BytesJson { + fn tx_hash_as_bytes(&self) -> BytesJson { let bytes = self.data.encode_to_vec(); let hash = sha256(&bytes); hash.to_vec().into() @@ -363,7 +496,7 @@ impl crate::Transaction for CosmosTransaction { pub(crate) fn account_id_from_privkey(priv_key: &[u8], prefix: &str) -> MmResult { let signing_key = - SigningKey::from_bytes(priv_key).map_to_mm(|e| TendermintInitErrorKind::InvalidPrivKey(e.to_string()))?; + SigningKey::from_slice(priv_key).map_to_mm(|e| TendermintInitErrorKind::InvalidPrivKey(e.to_string()))?; signing_key .public_key() @@ -385,10 +518,14 @@ impl From for AccountIdFromPubkeyHexErr { fn from(err: ErrorReport) -> Self { AccountIdFromPubkeyHexErr::CouldNotCreateAccountId(err) } } -pub fn account_id_from_pubkey_hex(prefix: &str, pubkey: &str) -> MmResult { +pub fn account_id_from_pubkey_hex(prefix: &str, pubkey: &str) -> Result { let pubkey_bytes = hex::decode(pubkey)?; - let pubkey_hash = dhash160(&pubkey_bytes); - Ok(AccountId::new(prefix, pubkey_hash.as_slice())?) + Ok(account_id_from_raw_pubkey(prefix, &pubkey_bytes)?) +} + +pub fn account_id_from_raw_pubkey(prefix: &str, pubkey: &[u8]) -> Result { + let pubkey_hash = dhash160(pubkey); + AccountId::new(prefix, pubkey_hash.as_slice()) } #[derive(Debug, Clone, PartialEq)] @@ -404,6 +541,10 @@ enum SearchForSwapTxSpendErr { TxMessagesEmpty, ClaimHtlcTxNotFound, UnexpectedHtlcState(i32), + #[display(fmt = "Account type '{}' is not supported for HTLCs", prefix)] + UnexpectedAccountType { + prefix: String, + }, Proto(DecodeError), } @@ -435,14 +576,14 @@ impl TendermintCommons for TendermintCoin { Ok(u64::try_from(timestamp.seconds).ok()) } - async fn all_balances(&self) -> MmResult { + async fn get_all_balances(&self) -> MmResult { let platform_balance_denom = self .account_balance_for_denom(&self.account_id, self.denom.to_string()) .await?; let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.decimals); let ibc_assets_info = self.tokens_info.lock().clone(); - let mut requests = Vec::new(); + let mut requests = Vec::with_capacity(ibc_assets_info.len()); for (denom, info) in ibc_assets_info { let fut = async move { let balance_denom = self @@ -469,6 +610,7 @@ impl TendermintCommons for TendermintCoin { } impl TendermintCoin { + #[allow(clippy::too_many_arguments)] pub async fn init( ctx: &MmArc, ticker: String, @@ -476,7 +618,8 @@ impl TendermintCoin { protocol_info: TendermintProtocolInfo, rpc_urls: Vec, tx_history: bool, - priv_key_policy: TendermintPrivKeyPolicy, + activation_policy: TendermintActivationPolicy, + is_keplr_from_ledger: bool, ) -> MmResult { if rpc_urls.is_empty() { return MmError::err(TendermintInitError { @@ -485,17 +628,11 @@ impl TendermintCoin { }); } - let priv_key = priv_key_policy.activated_key_or_err().mm_err(|e| TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::Internal(e.to_string()), - })?; - - let account_id = - account_id_from_privkey(priv_key.as_slice(), &protocol_info.account_prefix).mm_err(|kind| { - TendermintInitError { - ticker: ticker.clone(), - kind, - } + let account_id = activation_policy + .generate_account_id(&protocol_info.account_prefix) + .map_to_mm(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::CouldNotGenerateAccountId(e.to_string()), })?; let rpc_clients = clients_from_urls(rpc_urls.as_ref()).mm_err(|kind| TendermintInitError { @@ -535,7 +672,7 @@ impl TendermintCoin { ticker, account_id, account_prefix: protocol_info.account_prefix, - priv_key_policy, + activation_policy, decimals: protocol_info.decimals, denom, chain_id, @@ -547,254 +684,35 @@ impl TendermintCoin { client: TendermintRpcClient(AsyncMutex::new(client_impl)), chain_registry_name: protocol_info.chain_registry_name, ctx: ctx.weak(), + is_keplr_from_ledger, }))) } - pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { - let coin = self.clone(); - let fut = async move { - let to_address = - AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - - let (account_id, priv_key) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { - let priv_key = coin - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" - .to_string(), - )) - }, - None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), - }; - - let (balance_denom, balance_dec) = coin - .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) - .await?; - - // << BEGIN TX SIMULATION FOR FEE CALCULATION - let (amount_denom, amount_dec) = if req.max { - let amount_denom = balance_denom; - (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) - } else { - (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) - }; - - if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { - return MmError::err(WithdrawError::AmountTooLow { - amount: amount_dec, - threshold: coin.min_tx_amount(), - }); - } - - let received_by_me = if to_address == account_id { - amount_dec - } else { - BigDecimal::default() - }; - - let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); - - let msg_transfer = MsgTransfer::new_with_default_timeout( - req.ibc_source_channel.clone(), - account_id.clone(), - to_address.clone(), - Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }, - ) - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let current_block = coin - .current_block() - .compat() - .await - .map_to_mm(WithdrawError::Transport)?; - - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - // >> END TX SIMULATION FOR FEE CALCULATION - - let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); - - let fee_amount_u64 = coin - .calculate_account_fee_amount_as_u64( - &account_id, - &priv_key, - msg_transfer.clone(), - timeout_height, - memo.clone(), - req.fee, - ) - .await?; - let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); - - let fee_amount = Coin { - denom: coin.denom.clone(), - amount: fee_amount_u64.into(), - }; - - let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); - - let (amount_denom, total_amount) = if req.max { - if balance_denom < fee_amount_u64 { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: balance_dec, - required: fee_amount_dec, - }); - } - let amount_denom = balance_denom - fee_amount_u64; - (amount_denom, balance_dec) - } else { - let total = &req.amount + &fee_amount_dec; - if balance_dec < total { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: balance_dec, - required: total, - }); - } - - (sat_from_big_decimal(&req.amount, coin.decimals)?, total) - }; - - let msg_transfer = MsgTransfer::new_with_default_timeout( - req.ibc_source_channel.clone(), - account_id.clone(), - to_address.clone(), - Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }, - ) - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let account_info = coin.account_info(&account_id).await?; - let tx_raw = coin - .any_to_signed_raw_tx(&priv_key, account_info, msg_transfer, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let tx_bytes = tx_raw - .to_bytes() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let hash = sha256(&tx_bytes); - Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), - from: vec![account_id.to_string()], - to: vec![req.to], - my_balance_change: &received_by_me - &total_amount, - spent_by_me: total_amount.clone(), - total_amount, - received_by_me, - block_height: 0, - timestamp: 0, - fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { - coin: coin.ticker.clone(), - amount: fee_amount_dec, - uamount: fee_amount_u64, - gas_limit, - })), - coin: coin.ticker.to_string(), - internal_id: hash.to_vec().into(), - kmd_rewards: None, - transaction_type: TransactionType::default(), - memo: Some(memo), - }) - }; - Box::new(fut.boxed().compat()) - } - - pub async fn get_ibc_transfer_channels(&self, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { - #[derive(Deserialize)] - struct ChainRegistry { - channels: Vec, - } - - #[derive(Deserialize)] - struct ChannelInfo { - channel_id: String, - port_id: String, - } - - #[derive(Deserialize)] - struct IbcChannel { - chain_1: ChannelInfo, - #[allow(dead_code)] - chain_2: ChannelInfo, - ordering: String, - version: String, - tags: Option, - } - - let src_chain_registry_name = self.chain_registry_name.as_ref().or_mm_err(|| { - IBCTransferChannelsRequestError::InternalError(format!( - "`chain_registry_name` is not set for '{}'", - self.platform_ticker() - )) - })?; - - let source_filename = format!( - "{}-{}.json", - src_chain_registry_name, req.destination_chain_registry_name - ); - - let git_controller: GitController = GitController::new(GITHUB_API_URI); + /// Extracts corresponding IBC channel ID for `AccountId` from https://github.com/KomodoPlatform/chain-registry/tree/nucl. + pub(crate) async fn detect_channel_id_for_ibc_transfer( + &self, + to_address: &AccountId, + ) -> Result> { + let ctx = MmArc::from_weak(&self.ctx).ok_or_else(|| WithdrawError::InternalError("No context".to_owned()))?; - let metadata_list = git_controller - .client - .get_file_metadata_list( - CHAIN_REGISTRY_REPO_OWNER, - CHAIN_REGISTRY_REPO_NAME, - CHAIN_REGISTRY_BRANCH, - CHAIN_REGISTRY_IBC_DIR_NAME, - ) - .await - .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + let source_registry_name = self + .chain_registry_name + .clone() + .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; - let source_channel_file = metadata_list - .iter() - .find(|metadata| metadata.name == source_filename) - .or_mm_err(|| IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(source_filename))?; + let destination_registry_name = chain_registry_name_from_account_prefix(&ctx, to_address.prefix()) + .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; - let mut registry_object = git_controller - .client - .deserialize_json_source::(source_channel_file.to_owned()) + let channels = get_ibc_transfer_channels(source_registry_name, destination_registry_name) .await - .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; - - registry_object - .channels - .retain(|ch| ch.chain_1.port_id == *IBC_OUT_SOURCE_PORT); - - let result: Vec = registry_object - .channels - .iter() - .map(|ch| IBCTransferChannel { - channel_id: ch.chain_1.channel_id.clone(), - ordering: ch.ordering.clone(), - version: ch.version.clone(), - tags: ch.tags.clone().map(|t| IBCTransferChannelTag { - status: t.status, - preferred: t.preferred, - dex: t.dex, - }), - }) - .collect(); - - Ok(IBCTransferChannelsResponse { - ibc_transfer_channels: result, - }) + .map_err(|_| WithdrawError::IBCChannelCouldNotFound(to_address.to_string()))?; + + Ok(channels + .ibc_transfer_channels + .last() + .ok_or_else(|| WithdrawError::InternalError("channel list can not be empty".to_owned()))? + .channel_id + .clone()) } #[inline(always)] @@ -802,11 +720,9 @@ impl TendermintCoin { #[allow(unused)] async fn get_latest_block(&self) -> MmResult { - let path = AbciPath::from_str(ABCI_GET_LATEST_BLOCK_PATH).expect("valid path"); - let request = GetLatestBlockRequest {}; let request = AbciRequest::new( - Some(path), + Some(ABCI_GET_LATEST_BLOCK_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -819,11 +735,9 @@ impl TendermintCoin { #[allow(unused)] async fn get_block_by_height(&self, height: i64) -> MmResult { - let path = AbciPath::from_str(ABCI_GET_BLOCK_BY_HEIGHT_PATH).expect("valid path"); - let request = GetBlockByHeightRequest { height }; let request = AbciRequest::new( - Some(path), + Some(ABCI_GET_BLOCK_BY_HEIGHT_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -839,7 +753,7 @@ impl TendermintCoin { // Therefore, we can call SimulateRequest or CheckTx(doesn't work with using Abci interface) to get used gas or fee itself. pub(super) fn gen_simulated_tx( &self, - account_info: BaseAccount, + account_info: &BaseAccount, priv_key: &Secp256k1Secret, tx_payload: Any, timeout_height: u64, @@ -852,7 +766,7 @@ impl TendermintCoin { let fee = Fee::from_amount_and_gas(fee_amount, GAS_LIMIT_DEFAULT); - let signkey = SigningKey::from_bytes(priv_key.as_slice())?; + let signkey = SigningKey::from_slice(priv_key.as_slice())?; let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let auth_info = SignerInfo::single_direct(Some(signkey.public_key()), account_info.sequence).auth_info(fee); let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; @@ -867,7 +781,7 @@ impl TendermintCoin { &self, from_address: &AccountId, to_address: &AccountId, - amount: Vec, + amount: &[Coin], secret_hash: &[u8], ) -> String { // Needs to be sorted if contains multiple coins @@ -888,17 +802,49 @@ impl TendermintCoin { sha256(&htlc_id).to_string().to_uppercase() } - pub(super) async fn seq_safe_send_raw_tx_bytes( + async fn common_send_raw_tx_bytes( &self, tx_payload: Any, fee: Fee, timeout_height: u64, memo: String, + timeout: Duration, ) -> Result<(String, Raw), TransactionErr> { + // As there wouldn't be enough time to process the data, to mitigate potential edge problems (such as attempting to send transaction + // bytes half a second before expiration, which may take longer to send and result in the transaction amount being wasted due to a timeout), + // reduce the expiration time by 5 seconds. + let expiration = timeout - Duration::from_secs(5); + + match self.activation_policy { + TendermintActivationPolicy::PrivateKey(_) => { + try_tx_s!( + self.seq_safe_send_raw_tx_bytes(tx_payload, fee, timeout_height, memo) + .timeout(expiration) + .await + ) + }, + TendermintActivationPolicy::PublicKey(_) => { + try_tx_s!( + self.send_unsigned_tx_externally(tx_payload, fee, timeout_height, memo, expiration) + .timeout(expiration) + .await + ) + }, + } + } + + async fn seq_safe_send_raw_tx_bytes( + &self, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> Result<(String, Raw), TransactionErr> { + let mut account_info = try_tx_s!(self.account_info(&self.account_id).await); let (tx_id, tx_raw) = loop { let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( - try_tx_s!(self.priv_key_policy.activated_key_or_err()), - try_tx_s!(self.account_info(&self.account_id).await), + try_tx_s!(self.activation_policy.activated_key_or_err()), + &account_info, tx_payload.clone(), fee.clone(), timeout_height, @@ -909,6 +855,7 @@ impl TendermintCoin { Ok(tx_id) => break (tx_id, tx_raw), Err(e) => { if e.contains(ACCOUNT_SEQUENCE_ERR) { + account_info.sequence = try_tx_s!(parse_expected_sequence_number(&e)); debug!("Got wrong account sequence, trying again."); continue; } @@ -921,6 +868,50 @@ impl TendermintCoin { Ok((tx_id, tx_raw)) } + async fn send_unsigned_tx_externally( + &self, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + timeout: Duration, + ) -> Result<(String, Raw), TransactionErr> { + #[derive(Deserialize)] + struct TxHashData { + hash: String, + } + + let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already"))); + + let account_info = try_tx_s!(self.account_info(&self.account_id).await); + let SerializedUnsignedTx { tx_json, body_bytes } = if self.is_keplr_from_ledger { + try_tx_s!(self.any_to_legacy_amino_json(&account_info, tx_payload, fee, timeout_height, memo)) + } else { + try_tx_s!(self.any_to_serialized_sign_doc(&account_info, tx_payload, fee, timeout_height, memo)) + }; + + let data: TxHashData = try_tx_s!(ctx + .ask_for_data(&format!("TX_HASH:{}", self.ticker()), tx_json, timeout) + .await + .map_err(|e| ERRL!("{}", e))); + + let tx = try_tx_s!(self.request_tx(data.hash.clone()).await.map_err(|e| ERRL!("{}", e))); + + let tx_raw_inner = TxRaw { + body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + signatures: tx.signatures, + }; + + if body_bytes != tx_raw_inner.body_bytes { + return Err(crate::TransactionErr::Plain(ERRL!( + "Unsigned transaction don't match with the externally provided transaction." + ))); + } + + Ok((data.hash, Raw::from(tx_raw_inner))) + } + #[allow(deprecated)] pub(super) async fn calculate_fee( &self, @@ -929,14 +920,23 @@ impl TendermintCoin { memo: String, withdraw_fee: Option, ) -> MmResult { - let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); + let Ok(activated_priv_key) = self.activation_policy.activated_key_or_err() else { + let (gas_price, gas_limit) = self.gas_info_for_withdraw(&withdraw_fee, GAS_LIMIT_DEFAULT); + let amount = ((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil(); + + let fee_amount = Coin { + denom: self.platform_denom().clone(), + amount: (amount as u64).into(), + }; + + return Ok(Fee::from_amount_and_gas(fee_amount, gas_limit)); + }; + let mut account_info = self.account_info(&self.account_id).await?; let (response, raw_response) = loop { - let account_info = self.account_info(&self.account_id).await?; - let activated_priv_key = self.priv_key_policy.activated_key_or_err()?; let tx_bytes = self .gen_simulated_tx( - account_info, + &account_info, activated_priv_key, msg.clone(), timeout_height, @@ -945,7 +945,7 @@ impl TendermintCoin { .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; let request = AbciRequest::new( - Some(path.clone()), + Some(ABCI_SIMULATE_TX_PATH.to_string()), SimulateRequest { tx_bytes, tx: None }.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -953,7 +953,9 @@ impl TendermintCoin { let raw_response = self.rpc_client().await?.perform(request).await?; - if raw_response.response.log.to_string().contains(ACCOUNT_SEQUENCE_ERR) { + let log = raw_response.response.log.to_string(); + if log.contains(ACCOUNT_SEQUENCE_ERR) { + account_info.sequence = parse_expected_sequence_number(&log)?; debug!("Got wrong account sequence, trying again."); continue; } @@ -997,22 +999,25 @@ impl TendermintCoin { pub(super) async fn calculate_account_fee_amount_as_u64( &self, account_id: &AccountId, - priv_key: &Secp256k1Secret, + priv_key: Option, msg: Any, timeout_height: u64, memo: String, withdraw_fee: Option, ) -> MmResult { - let path = AbciPath::from_str(ABCI_SIMULATE_TX_PATH).expect("valid path"); + let Some(priv_key) = priv_key else { + let (gas_price, _) = self.gas_info_for_withdraw(&withdraw_fee, 0); + return Ok(((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil() as u64); + }; + let mut account_info = self.account_info(account_id).await?; let (response, raw_response) = loop { - let account_info = self.account_info(account_id).await?; let tx_bytes = self - .gen_simulated_tx(account_info, priv_key, msg.clone(), timeout_height, memo.clone()) + .gen_simulated_tx(&account_info, &priv_key, msg.clone(), timeout_height, memo.clone()) .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; let request = AbciRequest::new( - Some(path.clone()), + Some(ABCI_SIMULATE_TX_PATH.to_string()), SimulateRequest { tx_bytes, tx: None }.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -1020,7 +1025,9 @@ impl TendermintCoin { let raw_response = self.rpc_client().await?.perform(request).await?; - if raw_response.response.log.to_string().contains(ACCOUNT_SEQUENCE_ERR) { + let log = raw_response.response.log.to_string(); + if log.contains(ACCOUNT_SEQUENCE_ERR) { + account_info.sequence = parse_expected_sequence_number(&log)?; debug!("Got wrong account sequence, trying again."); continue; } @@ -1054,12 +1061,11 @@ impl TendermintCoin { } pub(super) async fn account_info(&self, account_id: &AccountId) -> MmResult { - let path = AbciPath::from_str(ABCI_QUERY_ACCOUNT_PATH).expect("valid path"); let request = QueryAccountRequest { address: account_id.to_string(), }; let request = AbciRequest::new( - Some(path), + Some(ABCI_QUERY_ACCOUNT_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -1078,10 +1084,10 @@ impl TendermintCoin { ethermint_account .base_account - .or_mm_err(|| TendermintCoinRpcError::Prost(err))? + .or_mm_err(|| TendermintCoinRpcError::Prost(err.to_string()))? }, Err(err) => { - return MmError::err(TendermintCoinRpcError::Prost(err)); + return MmError::err(TendermintCoinRpcError::Prost(err.to_string())); }, }; @@ -1093,13 +1099,12 @@ impl TendermintCoin { account_id: &AccountId, denom: String, ) -> MmResult { - let path = AbciPath::from_str(ABCI_QUERY_BALANCE_PATH).expect("valid path"); let request = QueryBalanceRequest { address: account_id.to_string(), denom, }; let request = AbciRequest::new( - Some(path), + Some(ABCI_QUERY_BALANCE_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -1115,31 +1120,108 @@ impl TendermintCoin { .map_to_mm(|e| TendermintCoinRpcError::InvalidResponse(format!("balance is not u64, err {}", e))) } + #[allow(clippy::result_large_err)] + pub(super) fn account_id_and_pk_for_withdraw( + &self, + withdraw_from: Option, + ) -> Result<(AccountId, Option), WithdrawError> { + if let TendermintActivationPolicy::PublicKey(_) = self.activation_policy { + return Ok((self.account_id.clone(), None)); + } + + match withdraw_from { + Some(from) => { + let path_to_coin = self + .activation_policy + .path_to_coin_or_err() + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let path_to_address = from + .to_address_path(path_to_coin.coin_type()) + .map_err(|e| WithdrawError::InternalError(e.to_string()))? + .to_derivation_path(path_to_coin) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let priv_key = self + .activation_policy + .hd_wallet_derived_priv_key_or_err(&path_to_address) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let account_id = account_id_from_privkey(priv_key.as_slice(), &self.account_prefix) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + Ok((account_id, Some(priv_key))) + }, + None => { + let activated_key = self + .activation_policy + .activated_key_or_err() + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + Ok((self.account_id.clone(), Some(*activated_key))) + }, + } + } + + pub(super) fn any_to_transaction_data( + &self, + maybe_pk: Option, + message: Any, + account_info: &BaseAccount, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> Result { + if let Some(priv_key) = maybe_pk { + let tx_raw = self.any_to_signed_raw_tx(&priv_key, account_info, message, fee, timeout_height, memo)?; + let tx_bytes = tx_raw.to_bytes()?; + let hash = sha256(&tx_bytes); + + Ok(TransactionData::new_signed( + tx_bytes.into(), + hex::encode_upper(hash.as_slice()), + )) + } else { + let SerializedUnsignedTx { tx_json, .. } = if self.is_keplr_from_ledger { + self.any_to_legacy_amino_json(account_info, message, fee, timeout_height, memo) + } else { + self.any_to_serialized_sign_doc(account_info, message, fee, timeout_height, memo) + }?; + + Ok(TransactionData::Unsigned(tx_json)) + } + } + fn gen_create_htlc_tx( &self, denom: Denom, to: &AccountId, - amount: cosmrs::Decimal, + amount: cosmrs::Amount, secret_hash: &[u8], time_lock: u64, - ) -> MmResult { + ) -> MmResult { let amount = vec![Coin { denom, amount }]; let timestamp = 0_u64; - let msg_payload = MsgCreateHtlc { - sender: self.account_id.clone(), - to: to.clone(), - receiver_on_other_chain: "".to_string(), - sender_on_other_chain: "".to_string(), - amount: amount.clone(), - hash_lock: hex::encode(secret_hash), + + let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + TxMarshalingErr::NotSupported(format!( + "Account type '{}' is not supported for HTLCs", + self.account_prefix + )) + })?; + + let msg_payload = CreateHtlcMsg::new( + htlc_type, + self.account_id.clone(), + to.clone(), + amount.clone(), + hex::encode(secret_hash), timestamp, time_lock, - transfer: false, - }; + ); - let htlc_id = self.calculate_htlc_id(&self.account_id, to, amount, secret_hash); + let htlc_id = self.calculate_htlc_id(&self.account_id, to, &amount, secret_hash); - Ok(IrisHtlc { + Ok(TendermintHtlc { id: htlc_id, msg_payload: msg_payload .to_any() @@ -1147,14 +1229,17 @@ impl TendermintCoin { }) } - fn gen_claim_htlc_tx(&self, htlc_id: String, secret: &[u8]) -> MmResult { - let msg_payload = MsgClaimHtlc { - id: htlc_id.clone(), - sender: self.account_id.clone(), - secret: hex::encode(secret), - }; + fn gen_claim_htlc_tx(&self, htlc_id: String, secret: &[u8]) -> MmResult { + let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + TxMarshalingErr::NotSupported(format!( + "Account type '{}' is not supported for HTLCs", + self.account_prefix + )) + })?; - Ok(IrisHtlc { + let msg_payload = ClaimHtlcMsg::new(htlc_type, htlc_id.clone(), self.account_id.clone(), hex::encode(secret)); + + Ok(TendermintHtlc { id: htlc_id, msg_payload: msg_payload .to_any() @@ -1165,19 +1250,131 @@ impl TendermintCoin { pub(super) fn any_to_signed_raw_tx( &self, priv_key: &Secp256k1Secret, - account_info: BaseAccount, + account_info: &BaseAccount, tx_payload: Any, fee: Fee, timeout_height: u64, memo: String, ) -> cosmrs::Result { - let signkey = SigningKey::from_bytes(priv_key.as_slice())?; + let signkey = SigningKey::from_slice(priv_key.as_slice())?; let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); let auth_info = SignerInfo::single_direct(Some(signkey.public_key()), account_info.sequence).auth_info(fee); let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; sign_doc.sign(&signkey) } + pub(super) fn any_to_serialized_sign_doc( + &self, + account_info: &BaseAccount, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> cosmrs::Result { + let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); + let pubkey = self.activation_policy.public_key()?.into(); + let auth_info = SignerInfo::single_direct(Some(pubkey), account_info.sequence).auth_info(fee); + let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?; + + let tx_json = json!({ + "sign_doc": { + "body_bytes": sign_doc.body_bytes, + "auth_info_bytes": sign_doc.auth_info_bytes, + "chain_id": sign_doc.chain_id, + "account_number": sign_doc.account_number, + } + }); + + Ok(SerializedUnsignedTx { + tx_json, + body_bytes: sign_doc.body_bytes, + }) + } + + /// This should only be used for Keplr from Ledger! + /// When using Keplr from Ledger, they don't accept `SING_MODE_DIRECT` transactions. + /// + /// Visit https://docs.cosmos.network/main/build/architecture/adr-050-sign-mode-textual#context for more context. + pub(super) fn any_to_legacy_amino_json( + &self, + account_info: &BaseAccount, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> cosmrs::Result { + const MSG_SEND_TYPE_URL: &str = "/cosmos.bank.v1beta1.MsgSend"; + const LEDGER_MSG_SEND_TYPE_URL: &str = "cosmos-sdk/MsgSend"; + + // Ledger's keplr works as wallet-only, so `MsgSend` support is enough for now. + if tx_payload.type_url != MSG_SEND_TYPE_URL { + return Err(ErrorReport::new(io::Error::new( + io::ErrorKind::Unsupported, + format!( + "Signing mode `SIGN_MODE_LEGACY_AMINO_JSON` is not supported for '{}' transaction type.", + tx_payload.type_url + ), + ))); + } + + let msg_send = MsgSend::from_any(&tx_payload)?; + let timeout_height = u32::try_from(timeout_height)?; + let original_tx_type_url = tx_payload.type_url.clone(); + let body_bytes = tx::Body::new(vec![tx_payload], &memo, timeout_height).into_bytes()?; + + let amount: Vec = msg_send + .amount + .into_iter() + .map(|t| { + json!( { + "denom": t.denom, + // Numbers needs to be converted into string type. + // Ref: https://github.com/cosmos/ledger-cosmos/blob/c707129e59f6e0f07ad67161a6b75e8951af063c/docs/TXSPEC.md#json-format + "amount": t.amount.to_string(), + }) + }) + .collect(); + + let msg = json!({ + "type": LEDGER_MSG_SEND_TYPE_URL, + "value": json!({ + "from_address": msg_send.from_address.to_string(), + "to_address": msg_send.to_address.to_string(), + "amount": amount, + }) + }); + + let fee_amount: Vec = fee + .amount + .into_iter() + .map(|t| { + json!( { + "denom": t.denom, + // Numbers needs to be converted into string type. + // Ref: https://github.com/cosmos/ledger-cosmos/blob/c707129e59f6e0f07ad67161a6b75e8951af063c/docs/TXSPEC.md#json-format + "amount": t.amount.to_string(), + }) + }) + .collect(); + + let tx_json = serde_json::json!({ + "legacy_amino_json": { + "account_number": account_info.account_number.to_string(), + "chain_id": self.chain_id.to_string(), + "fee": { + "amount": fee_amount, + "gas": fee.gas_limit.to_string() + }, + "memo": memo, + "msgs": [msg], + "sequence": account_info.sequence.to_string(), + }, + "original_tx_type_url": original_tx_type_url, + }); + + Ok(SerializedUnsignedTx { tx_json, body_bytes }) + } + pub fn add_activated_token_info(&self, ticker: String, decimals: u8, denom: Denom) { self.tokens_info .lock() @@ -1207,17 +1404,17 @@ impl TendermintCoin { let pubkey_hash = dhash160(other_pub); let to_address = try_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); - let htlc_id = self.calculate_htlc_id(&self.account_id, &to_address, amount, secret_hash); + let htlc_id = self.calculate_htlc_id(&self.account_id, &to_address, &amount, secret_hash); let coin = self.clone(); let fut = async move { let htlc_response = try_s!(coin.query_htlc(htlc_id.clone()).await); - let htlc_data = match htlc_response.htlc { - Some(htlc) => htlc, - None => return Ok(None), + + let Some(htlc_state) = htlc_response.htlc_state() else { + return Ok(None); }; - match htlc_data.state { + match htlc_state { HTLC_STATE_OPEN | HTLC_STATE_COMPLETED | HTLC_STATE_REFUNDED => {}, unexpected_state => return Err(format!("Unexpected state for HTLC {}", unexpected_state)), }; @@ -1246,13 +1443,20 @@ impl TendermintCoin { )); } - let deserialized_tx = try_s!(cosmrs::Tx::from_bytes(tx.tx.as_bytes())); + let deserialized_tx = try_s!(cosmrs::Tx::from_bytes(&tx.tx)); let msg = try_s!(deserialized_tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc = try_s!(CreateHtlcProtoRep::decode(msg.value.as_slice())); + let htlc = try_s!(CreateHtlcProto::decode( + try_s!(HtlcType::from_str(&coin.account_prefix)), + msg.value.as_slice() + )); + + let Some(hash_lock) = htlc_response.hash_lock() else { + return Ok(None); + }; - if htlc.hash_lock.to_uppercase() == htlc_data.hash_lock.to_uppercase() { + if htlc.hash_lock().to_uppercase() == hash_lock.to_uppercase() { let htlc = TransactionEnum::CosmosTransaction(CosmosTransaction { - data: try_s!(TxRaw::decode(tx.tx.as_bytes())), + data: try_s!(TxRaw::decode(tx.tx.as_slice())), }); return Ok(Some(htlc)); } @@ -1277,7 +1481,7 @@ impl TendermintCoin { let to = try_tx_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); let amount_as_u64 = try_tx_fus!(sat_from_big_decimal(&amount, decimals)); - let amount = cosmrs::Decimal::from(amount_as_u64); + let amount = cosmrs::Amount::from(amount_as_u64); let secret_hash = secret_hash.to_vec(); let coin = self.clone(); @@ -1300,11 +1504,12 @@ impl TendermintCoin { ); let (_tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes( + coin.common_send_raw_tx_bytes( create_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(time_lock_duration), ) .await ); @@ -1324,6 +1529,7 @@ impl TendermintCoin { denom: Denom, decimals: u8, uuid: &[u8], + expires_at: u64, ) -> TransactionFut { let memo = try_tx_fus!(Uuid::from_slice(uuid)).to_string(); let from_address = self.account_id.clone(); @@ -1331,7 +1537,7 @@ impl TendermintCoin { let to_address = try_tx_fus!(AccountId::new(&self.account_prefix, pubkey_hash.as_slice())); let amount_as_u64 = try_tx_fus!(sat_from_big_decimal(&amount, decimals)); - let amount = cosmrs::Decimal::from(amount_as_u64); + let amount = cosmrs::Amount::from(amount_as_u64); let amount = vec![Coin { denom, amount }]; @@ -1352,9 +1558,16 @@ impl TendermintCoin { .await ); + let timeout = expires_at.checked_sub(now_sec()).unwrap_or_default(); let (_tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes(tx_payload.clone(), fee.clone(), timeout_height, memo.clone()) - .await + coin.common_send_raw_tx_bytes( + tx_payload.clone(), + fee.clone(), + timeout_height, + memo.clone(), + Duration::from_secs(timeout) + ) + .await ); Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { @@ -1470,83 +1683,83 @@ impl TendermintCoin { Box::new(fut.boxed().compat()) } - pub(super) fn validate_payment_for_denom( + pub(super) async fn validate_payment_for_denom( &self, input: ValidatePaymentInput, denom: Denom, decimals: u8, - ) -> ValidatePaymentFut<()> { - let coin = self.clone(); - let fut = async move { - let tx = cosmrs::Tx::from_bytes(&input.payment_tx) - .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; - - if tx.body.messages.len() != 1 { - return MmError::err(ValidatePaymentError::WrongPaymentTx( - "Payment tx must have exactly one message".into(), - )); - } + ) -> ValidatePaymentResult<()> { + let tx = cosmrs::Tx::from_bytes(&input.payment_tx) + .map_to_mm(|e| ValidatePaymentError::TxDeserializationError(e.to_string()))?; - let create_htlc_msg_proto = CreateHtlcProtoRep::decode(tx.body.messages[0].value.as_slice()) - .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; - let create_htlc_msg = MsgCreateHtlc::try_from(create_htlc_msg_proto) - .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; + if tx.body.messages.len() != 1 { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Payment tx must have exactly one message".into(), + )); + } + let htlc_type = HtlcType::from_str(&self.account_prefix).map_err(|_| { + ValidatePaymentError::InvalidParameter(format!( + "Account type '{}' is not supported for HTLCs", + self.account_prefix + )) + })?; - let sender_pubkey_hash = dhash160(&input.other_pub); - let sender = AccountId::new(&coin.account_prefix, sender_pubkey_hash.as_slice()) - .map_to_mm(|e| ValidatePaymentError::InvalidParameter(e.to_string()))?; + let create_htlc_msg_proto = CreateHtlcProto::decode(htlc_type, tx.body.messages[0].value.as_slice()) + .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; + let create_htlc_msg = CreateHtlcMsg::try_from(create_htlc_msg_proto) + .map_to_mm(|e| ValidatePaymentError::WrongPaymentTx(e.to_string()))?; - let amount = sat_from_big_decimal(&input.amount, decimals)?; - let amount = vec![Coin { - denom, - amount: amount.into(), - }]; + let sender_pubkey_hash = dhash160(&input.other_pub); + let sender = AccountId::new(&self.account_prefix, sender_pubkey_hash.as_slice()) + .map_to_mm(|e| ValidatePaymentError::InvalidParameter(e.to_string()))?; - let time_lock = coin.estimate_blocks_from_duration(input.time_lock_duration); + let amount = sat_from_big_decimal(&input.amount, decimals)?; + let amount = vec![Coin { + denom, + amount: amount.into(), + }]; - let expected_msg = MsgCreateHtlc { - sender: sender.clone(), - to: coin.account_id.clone(), - receiver_on_other_chain: "".into(), - sender_on_other_chain: "".into(), - amount: amount.clone(), - hash_lock: hex::encode(&input.secret_hash), - timestamp: 0, - time_lock: time_lock as u64, - transfer: false, - }; + let time_lock = self.estimate_blocks_from_duration(input.time_lock_duration); - if create_htlc_msg != expected_msg { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Incorrect CreateHtlc message {:?}, expected {:?}", - create_htlc_msg, expected_msg - ))); - } + let expected_msg = CreateHtlcMsg::new( + htlc_type, + sender.clone(), + self.account_id.clone(), + amount.clone(), + hex::encode(&input.secret_hash), + 0, + time_lock as u64, + ); - let hash = hex::encode_upper(sha256(&input.payment_tx).as_slice()); - let tx_from_rpc = coin.request_tx(hash).await?; - if input.payment_tx != tx_from_rpc.encode_to_vec() { - return MmError::err(ValidatePaymentError::InvalidRpcResponse( - "Tx from RPC doesn't match the input".into(), - )); - } + if create_htlc_msg != expected_msg { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Incorrect CreateHtlc message {:?}, expected {:?}", + create_htlc_msg, expected_msg + ))); + } - let htlc_id = coin.calculate_htlc_id(&sender, &coin.account_id, amount, &input.secret_hash); + let hash = hex::encode_upper(sha256(&input.payment_tx).as_slice()); + let tx_from_rpc = self.request_tx(hash).await?; + if input.payment_tx != tx_from_rpc.encode_to_vec() { + return MmError::err(ValidatePaymentError::InvalidRpcResponse( + "Tx from RPC doesn't match the input".into(), + )); + } - let htlc_response = coin.query_htlc(htlc_id.clone()).await?; - let htlc_data = htlc_response - .htlc - .or_mm_err(|| ValidatePaymentError::InvalidRpcResponse(format!("No HTLC data for {}", htlc_id)))?; + let htlc_id = self.calculate_htlc_id(&sender, &self.account_id, &amount, &input.secret_hash); - match htlc_data.state { - HTLC_STATE_OPEN => Ok(()), - unexpected_state => MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( - "{}", - unexpected_state - ))), - } - }; - Box::new(fut.boxed().compat()) + let htlc_response = self.query_htlc(htlc_id.clone()).await?; + let htlc_state = htlc_response + .htlc_state() + .or_mm_err(|| ValidatePaymentError::InvalidRpcResponse(format!("No HTLC data for {}", htlc_id)))?; + + match htlc_state { + HTLC_STATE_OPEN => Ok(()), + unexpected_state => MmError::err(ValidatePaymentError::UnexpectedPaymentState(format!( + "{}", + unexpected_state + ))), + } } pub(super) async fn get_sender_trade_fee_for_denom( @@ -1563,7 +1776,7 @@ impl TendermintCoin { drop_mutability!(sec); let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) - .map_err(|e| MmError::new(TradePreimageError::InternalError(e.into_inner().to_string())))?; + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&amount, decimals)?; @@ -1588,9 +1801,7 @@ impl TendermintCoin { let fee_uamount = self .calculate_account_fee_amount_as_u64( &self.account_id, - self.priv_key_policy - .activated_key_or_err() - .mm_err(|e| TradePreimageError::InternalError(e.to_string()))?, + self.activation_policy.activated_key(), create_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), @@ -1615,7 +1826,7 @@ impl TendermintCoin { dex_fee_amount: DexFee, ) -> TradePreimageResult { let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) - .map_err(|e| MmError::new(TradePreimageError::InternalError(e.into_inner().to_string())))?; + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&dex_fee_amount.fee_amount().into(), decimals)?; let current_block = self.current_block().compat().await.map_err(|e| { @@ -1641,9 +1852,7 @@ impl TendermintCoin { let fee_uamount = self .calculate_account_fee_amount_as_u64( &self.account_id, - self.priv_key_policy - .activated_key_or_err() - .mm_err(|e| TradePreimageError::InternalError(e.to_string()))?, + self.activation_policy.activated_key(), msg_send, timeout_height, TX_DEFAULT_MEMO.to_owned(), @@ -1672,13 +1881,12 @@ impl TendermintCoin { } async fn request_tx(&self, hash: String) -> MmResult { - let path = AbciPath::from_str(ABCI_GET_TX_PATH).expect("valid path"); let request = GetTxRequest { hash }; let response = self .rpc_client() .await? .abci_query( - Some(path), + Some(ABCI_GET_TX_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -1697,13 +1905,12 @@ impl TendermintCoin { &self, hash: String, ) -> MmResult, TendermintCoinRpcError> { - let path = AbciPath::from_str(ABCI_GET_TX_PATH).expect("valid path"); let request = GetTxRequest { hash }; let response = self .rpc_client() .await? .abci_query( - Some(path), + Some(ABCI_GET_TX_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -1716,28 +1923,35 @@ impl TendermintCoin { // non-zero values are error. match tx_response.code { TX_SUCCESS_CODE => Ok(Some(cosmrs::tendermint::abci::Code::Ok)), - err_code => Ok(Some(cosmrs::tendermint::abci::Code::Err(err_code))), + err_code => Ok(Some(cosmrs::tendermint::abci::Code::Err( + // This will never panic, as `0` code goes the the success variant above. + NonZeroU32::new(err_code).unwrap(), + ))), } } else { Ok(None) } } - pub(crate) async fn query_htlc(&self, id: String) -> MmResult { - let path = AbciPath::from_str(ABCI_QUERY_HTLC_PATH).expect("valid path"); + pub(crate) async fn query_htlc(&self, id: String) -> MmResult { + let htlc_type = + HtlcType::from_str(&self.account_prefix).map_err(|_| TendermintCoinRpcError::UnexpectedAccountType { + prefix: self.account_prefix.clone(), + })?; + let request = QueryHtlcRequestProto { id }; let response = self .rpc_client() .await? .abci_query( - Some(path), + Some(htlc_type.get_htlc_abci_query_path()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, ) .await?; - Ok(QueryHtlcResponseProto::decode(response.value.as_slice())?) + Ok(QueryHtlcResponse::decode(htlc_type, response.value.as_slice())?) } #[inline] @@ -1756,33 +1970,43 @@ impl TendermintCoin { .messages .first() .or_mm_err(|| SearchForSwapTxSpendErr::TxMessagesEmpty)?; - let htlc_proto = CreateHtlcProtoRep::decode(first_message.value.as_slice())?; - let htlc = MsgCreateHtlc::try_from(htlc_proto)?; - let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, htlc.amount, input.secret_hash); + + let htlc_type = + HtlcType::from_str(&self.account_prefix).map_err(|_| SearchForSwapTxSpendErr::UnexpectedAccountType { + prefix: self.account_prefix.clone(), + })?; + + let htlc_proto = CreateHtlcProto::decode(htlc_type, first_message.value.as_slice())?; + let htlc = CreateHtlcMsg::try_from(htlc_proto)?; + let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), htlc.amount(), input.secret_hash); let htlc_response = self.query_htlc(htlc_id.clone()).await?; - let htlc_data = match htlc_response.htlc { - Some(htlc) => htlc, + + let htlc_state = match htlc_response.htlc_state() { + Some(htlc_state) => htlc_state, None => return Ok(None), }; - match htlc_data.state { + match htlc_state { HTLC_STATE_OPEN => Ok(None), HTLC_STATE_COMPLETED => { let events_string = format!("claim_htlc.id='{}'", htlc_id); + // TODO: Remove deprecated attribute when new version of tendermint-rs is released + #[allow(deprecated)] let request = GetTxsEventRequest { events: vec![events_string], - pagination: None, order_by: TendermintResultOrder::Ascending as i32, + page: 1, + limit: 1, + pagination: None, }; let encoded_request = request.encode_to_vec(); - let path = AbciPath::from_str(ABCI_GET_TXS_EVENT_PATH).expect("valid path"); let response = self .rpc_client() .await? .abci_query( - Some(path), + Some(ABCI_GET_TXS_EVENT_PATH.to_string()), encoded_request.as_slice(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -1908,6 +2132,13 @@ pub async fn get_ibc_chain_list() -> IBCChainRegistriesResult { impl MmCoin for TendermintCoin { fn is_asset_chain(&self) -> bool { false } + fn wallet_only(&self, ctx: &MmArc) -> bool { + let coin_conf = crate::coin_conf(ctx, self.ticker()); + let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); + + wallet_only_conf || self.is_keplr_from_ledger + } + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { @@ -1915,42 +2146,19 @@ impl MmCoin for TendermintCoin { let fut = async move { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - if to_address.prefix() != coin.account_prefix { - return MmError::err(WithdrawError::InvalidAddress(format!( - "expected {} address prefix", - coin.account_prefix - ))); - } - let (account_id, priv_key) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { - let priv_key = coin - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" - .to_string(), - )) - }, - None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), - }; + let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); + + let (account_id, maybe_pk) = coin.account_id_and_pk_for_withdraw(req.from)?; let (balance_denom, balance_dec) = coin .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) .await?; - // << BEGIN TX SIMULATION FOR FEE CALCULATION let (amount_denom, amount_dec) = if req.max { let amount_denom = balance_denom; (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) } else { - let total = req.amount.clone(); - (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) }; @@ -1967,18 +2175,26 @@ impl MmCoin for TendermintCoin { BigDecimal::default() }; - let msg_send = MsgSend { - from_address: account_id.clone(), - to_address: to_address.clone(), - amount: vec![Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }], - } - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let channel_id = if is_ibc_transfer { + match &req.ibc_source_channel { + Some(_) => req.ibc_source_channel, + None => Some(coin.detect_channel_id_for_ibc_transfer(&to_address).await?), + } + } else { + None + }; + + let msg_payload = create_withdraw_msg_as_any( + account_id.clone(), + to_address.clone(), + &coin.denom, + amount_denom, + channel_id.clone(), + ) + .await?; let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + let current_block = coin .current_block() .compat() @@ -1986,20 +2202,35 @@ impl MmCoin for TendermintCoin { .map_to_mm(WithdrawError::Transport)?; let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - // >> END TX SIMULATION FOR FEE CALCULATION - let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + let (_, gas_limit) = if is_ibc_transfer { + coin.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT) + } else { + coin.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT) + }; let fee_amount_u64 = coin .calculate_account_fee_amount_as_u64( &account_id, - &priv_key, - msg_send, + maybe_pk, + msg_payload.clone(), timeout_height, memo.clone(), req.fee, ) .await?; + + let fee_amount_u64 = if coin.is_keplr_from_ledger { + // When using `SIGN_MODE_LEGACY_AMINO_JSON`, Keplr ignores the fee we calculated + // and calculates another one which is usually double what we calculate. + // To make sure the transaction doesn't fail on the Keplr side (because if Keplr + // calculates a higher fee than us, the withdrawal might fail), we use three times + // the actual fee. + fee_amount_u64 * 3 + } else { + fee_amount_u64 + }; + let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); let fee_amount = Coin { @@ -2032,31 +2263,28 @@ impl MmCoin for TendermintCoin { (sat_from_big_decimal(&req.amount, coin.decimals)?, total) }; - let msg_send = MsgSend { - from_address: account_id.clone(), - to_address, - amount: vec![Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }], - } - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let msg_payload = create_withdraw_msg_as_any( + account_id.clone(), + to_address.clone(), + &coin.denom, + amount_denom, + channel_id, + ) + .await?; let account_info = coin.account_info(&account_id).await?; - let tx_raw = coin - .any_to_signed_raw_tx(&priv_key, account_info, msg_send, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let tx_bytes = tx_raw - .to_bytes() + let tx = coin + .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let hash = sha256(&tx_bytes); + let internal_id = { + let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); + sha256(&hex_vec).to_vec().into() + }; Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), + tx, from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, @@ -2072,9 +2300,13 @@ impl MmCoin for TendermintCoin { gas_limit, })), coin: coin.ticker.to_string(), - internal_id: hash.to_vec().into(), + internal_id, kmd_rewards: None, - transaction_type: TransactionType::default(), + transaction_type: if is_ibc_transfer { + TransactionType::TendermintIBCTransfer + } else { + TransactionType::StandardTransfer + }, memo: Some(memo), }) }; @@ -2114,14 +2346,6 @@ impl MmCoin for TendermintCoin { fn validate_address(&self, address: &str) -> ValidateAddressResult { match AccountId::from_str(address) { - Ok(account) if account.prefix() != self.account_prefix => ValidateAddressResult { - is_valid: false, - reason: Some(format!( - "Expected {} account prefix, got {}", - self.account_prefix, - account.prefix() - )), - }, Ok(_) => ValidateAddressResult { is_valid: true, reason: None, @@ -2148,6 +2372,7 @@ impl MmCoin for TendermintCoin { &self, value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { let amount = match value { TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, @@ -2214,13 +2439,14 @@ impl MmCoin for TendermintCoin { fn on_token_deactivated(&self, _ticker: &str) {} } +#[async_trait] impl MarketCoinOps for TendermintCoin { fn ticker(&self) -> &str { &self.ticker } fn my_address(&self) -> MmResult { Ok(self.account_id.to_string()) } - fn get_public_key(&self) -> Result> { - let key = SigningKey::from_bytes(self.priv_key_policy.activated_key_or_err()?.as_slice()) + async fn get_public_key(&self) -> Result> { + let key = SigningKey::from_slice(self.activation_policy.activated_key_or_err()?.as_slice()) .expect("privkey validity is checked on coin creation"); Ok(key.public_key().to_string()) } @@ -2274,14 +2500,10 @@ impl MarketCoinOps for TendermintCoin { let coin = self.clone(); let tx_bytes = tx.to_owned(); let fut = async move { - let broadcast_res = try_s!( - try_s!(coin.rpc_client().await) - .broadcast_tx_commit(tx_bytes.into()) - .await - ); + let broadcast_res = try_s!(try_s!(coin.rpc_client().await).broadcast_tx_commit(tx_bytes).await); - if broadcast_res.check_tx.log.to_string().contains(ACCOUNT_SEQUENCE_ERR) - || broadcast_res.deliver_tx.log.to_string().contains(ACCOUNT_SEQUENCE_ERR) + if broadcast_res.check_tx.log.contains(ACCOUNT_SEQUENCE_ERR) + || broadcast_res.deliver_tx.log.contains(ACCOUNT_SEQUENCE_ERR) { return ERR!( "{}. check_tx log: {}, deliver_tx log: {}", @@ -2303,6 +2525,13 @@ impl MarketCoinOps for TendermintCoin { Box::new(fut.boxed().compat()) } + #[inline(always)] + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { + MmError::err(RawTransactionError::NotImplemented { + coin: self.ticker().to_string(), + }) + } + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { // Sanity check let _: TxRaw = try_fus!(Message::decode(input.payment_tx.as_slice())); @@ -2342,27 +2571,33 @@ impl MarketCoinOps for TendermintCoin { fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { let tx = try_tx_fus!(cosmrs::Tx::from_bytes(args.tx_bytes)); let first_message = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto = try_tx_fus!(CreateHtlcProtoRep::decode(first_message.value.as_slice())); - let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); - let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, htlc.amount, args.secret_hash); + let htlc_proto = try_tx_fus!(CreateHtlcProto::decode( + try_tx_fus!(HtlcType::from_str(&self.account_prefix)), + first_message.value.as_slice() + )); + let htlc = try_tx_fus!(CreateHtlcMsg::try_from(htlc_proto)); + let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), htlc.amount(), args.secret_hash); let events_string = format!("claim_htlc.id='{}'", htlc_id); + // TODO: Remove deprecated attribute when new version of tendermint-rs is released + #[allow(deprecated)] let request = GetTxsEventRequest { events: vec![events_string], - pagination: None, order_by: TendermintResultOrder::Ascending as i32, + page: 1, + limit: 1, + pagination: None, }; let encoded_request = request.encode_to_vec(); let coin = self.clone(); - let path = try_tx_fus!(AbciPath::from_str(ABCI_GET_TXS_EVENT_PATH)); let wait_until = args.wait_until; let fut = async move { loop { let response = try_tx_s!( try_tx_s!(coin.rpc_client().await) .abci_query( - Some(path.clone()), + Some(ABCI_GET_TXS_EVENT_PATH.to_string()), encoded_request.as_slice(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE @@ -2398,14 +2633,14 @@ impl MarketCoinOps for TendermintCoin { let coin = self.clone(); let fut = async move { let info = try_s!(try_s!(coin.rpc_client().await).abci_info().await); - Ok(info.last_block_height.into()) + Ok(info.response.last_block_height.into()) }; Box::new(fut.boxed().compat()) } fn display_priv_key(&self) -> Result { Ok(self - .priv_key_policy + .activation_policy .activated_key_or_err() .map_err(|e| e.to_string())? .to_string()) @@ -2416,18 +2651,26 @@ impl MarketCoinOps for TendermintCoin { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + + fn is_trezor(&self) -> bool { + match &self.activation_policy { + TendermintActivationPolicy::PrivateKey(pk) => pk.is_trezor(), + TendermintActivationPolicy::PublicKey(_) => false, + } + } } #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintCoin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionFut { self.send_taker_fee_for_denom( fee_addr, dex_fee.fee_amount().into(), self.denom.clone(), self.decimals, uuid, + expire_at, ) } @@ -2453,13 +2696,20 @@ impl SwapOps for TendermintCoin { ) } - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - let tx = try_tx_fus!(cosmrs::Tx::from_bytes(maker_spends_payment_args.other_payment_tx)); - let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(prost::Message::decode(msg.value.as_slice())); - let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + let tx = try_tx_s!(cosmrs::Tx::from_bytes(maker_spends_payment_args.other_payment_tx)); + let msg = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); + + let htlc_proto = try_tx_s!(CreateHtlcProto::decode( + try_tx_s!(HtlcType::from_str(&self.account_prefix)), + msg.value.as_slice() + )); + let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); - let mut amount = htlc.amount.clone(); + let mut amount = htlc.amount().to_vec(); amount.sort(); drop_mutability!(amount); @@ -2469,50 +2719,58 @@ impl SwapOps for TendermintCoin { .collect::>() .join(","); - let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, amount, maker_spends_payment_args.secret_hash); + let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, maker_spends_payment_args.secret_hash); - let claim_htlc_tx = try_tx_fus!(self.gen_claim_htlc_tx(htlc_id, maker_spends_payment_args.secret)); + let claim_htlc_tx = try_tx_s!(self.gen_claim_htlc_tx(htlc_id, maker_spends_payment_args.secret)); + let timeout = maker_spends_payment_args + .time_lock + .checked_sub(now_sec()) + .unwrap_or_default(); let coin = self.clone(); - let fut = async move { - let current_block = try_tx_s!(coin.current_block().compat().await); - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - - let fee = try_tx_s!( - coin.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.to_owned(), - None - ) - .await - ); + let current_block = try_tx_s!(self.current_block().compat().await); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let (_tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes( - claim_htlc_tx.msg_payload.clone(), - fee.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - ) - .await - ); + let fee = try_tx_s!( + self.calculate_fee( + claim_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.to_owned(), + None + ) + .await + ); - Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { - data: tx_raw.into(), - })) - }; + let (_tx_id, tx_raw) = try_tx_s!( + coin.common_send_raw_tx_bytes( + claim_htlc_tx.msg_payload.clone(), + fee.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + Duration::from_secs(timeout), + ) + .await + ); - Box::new(fut.boxed().compat()) + Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: tx_raw.into(), + })) } - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - let tx = try_tx_fus!(cosmrs::Tx::from_bytes(taker_spends_payment_args.other_payment_tx)); - let msg = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto: CreateHtlcProtoRep = try_tx_fus!(prost::Message::decode(msg.value.as_slice())); - let htlc = try_tx_fus!(MsgCreateHtlc::try_from(htlc_proto)); + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + let tx = try_tx_s!(cosmrs::Tx::from_bytes(taker_spends_payment_args.other_payment_tx)); + let msg = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); + + let htlc_proto = try_tx_s!(CreateHtlcProto::decode( + try_tx_s!(HtlcType::from_str(&self.account_prefix)), + msg.value.as_slice() + )); + let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); - let mut amount = htlc.amount.clone(); + let mut amount = htlc.amount().to_vec(); amount.sort(); drop_mutability!(amount); @@ -2522,41 +2780,42 @@ impl SwapOps for TendermintCoin { .collect::>() .join(","); - let htlc_id = self.calculate_htlc_id(&htlc.sender, &htlc.to, amount, taker_spends_payment_args.secret_hash); + let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, taker_spends_payment_args.secret_hash); - let claim_htlc_tx = try_tx_fus!(self.gen_claim_htlc_tx(htlc_id, taker_spends_payment_args.secret)); + let timeout = taker_spends_payment_args + .time_lock + .checked_sub(now_sec()) + .unwrap_or_default(); + let claim_htlc_tx = try_tx_s!(self.gen_claim_htlc_tx(htlc_id, taker_spends_payment_args.secret)); let coin = self.clone(); - let fut = async move { - let current_block = try_tx_s!(coin.current_block().compat().await); - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - - let fee = try_tx_s!( - coin.calculate_fee( - claim_htlc_tx.msg_payload.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - None - ) - .await - ); + let current_block = try_tx_s!(self.current_block().compat().await); + let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let (tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes( - claim_htlc_tx.msg_payload.clone(), - fee.clone(), - timeout_height, - TX_DEFAULT_MEMO.into(), - ) - .await - ); + let fee = try_tx_s!( + self.calculate_fee( + claim_htlc_tx.msg_payload.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + None + ) + .await + ); - Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { - data: tx_raw.into(), - })) - }; + let (tx_id, tx_raw) = try_tx_s!( + coin.common_send_raw_tx_bytes( + claim_htlc_tx.msg_payload.clone(), + fee.clone(), + timeout_height, + TX_DEFAULT_MEMO.into(), + Duration::from_secs(timeout), + ) + .await + ); - Box::new(fut.boxed().compat()) + Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: tx_raw.into(), + })) } async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { @@ -2583,12 +2842,14 @@ impl SwapOps for TendermintCoin { ) } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { self.validate_payment_for_denom(input, self.denom.clone(), self.decimals) + .await } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { self.validate_payment_for_denom(input, self.denom.clone(), self.decimals) + .await } fn check_if_my_payment_sent( @@ -2626,11 +2887,14 @@ impl SwapOps for TendermintCoin { ) -> Result, String> { let tx = try_s!(cosmrs::Tx::from_bytes(spend_tx)); let msg = try_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto: super::iris::htlc_proto::ClaimHtlcProtoRep = - try_s!(prost::Message::decode(msg.value.as_slice())); - let htlc = try_s!(MsgClaimHtlc::try_from(htlc_proto)); - Ok(try_s!(hex::decode(htlc.secret))) + let htlc_proto = try_s!(ClaimHtlcProto::decode( + try_s!(HtlcType::from_str(&self.account_prefix)), + msg.value.as_slice() + )); + let htlc = try_s!(ClaimHtlcMsg::try_from(htlc_proto)); + + Ok(try_s!(hex::decode(htlc.secret()))) } fn check_tx_signed_by_pub(&self, tx: &[u8], expected_pub: &[u8]) -> Result> { @@ -2655,9 +2919,9 @@ impl SwapOps for TendermintCoin { } #[inline] - fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { key_pair_from_secret( - self.priv_key_policy + self.activation_policy .activated_key_or_err() .expect("valid priv key") .as_ref(), @@ -2666,8 +2930,8 @@ impl SwapOps for TendermintCoin { } #[inline] - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { - self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { + self.activation_policy.public_key().expect("valid pubkey").to_bytes() } fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { @@ -2801,25 +3065,48 @@ pub fn tendermint_priv_key_policy( conf: &TendermintConf, ticker: &str, priv_key_build_policy: PrivKeyBuildPolicy, - path_to_address: StandardHDCoinAddress, + path_to_address: HDPathAccountToAddressId, ) -> MmResult { match priv_key_build_policy { - PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(TendermintPrivKeyPolicy::Iguana(iguana)), + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => { + let mm2_internal_key_pair = key_pair_from_secret(iguana.as_ref()).mm_err(|e| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + })?; + + let tendermint_pair = TendermintKeyPair::new(iguana, *mm2_internal_key_pair.public()); + + Ok(TendermintPrivKeyPolicy::Iguana(tendermint_pair)) + }, PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { - let derivation_path = conf.derivation_path.as_ref().or_mm_err(|| TendermintInitError { + let path_to_coin = conf.derivation_path.as_ref().or_mm_err(|| TendermintInitError { ticker: ticker.to_string(), kind: TendermintInitErrorKind::DerivationPathIsNotSet, })?; let activated_priv_key = global_hd - .derive_secp256k1_secret(derivation_path, &path_to_address) + .derive_secp256k1_secret(&path_to_address.to_derivation_path(path_to_coin).mm_err(|e| { + TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::InvalidPathToAddress(e.to_string()), + } + })?) .mm_err(|e| TendermintInitError { ticker: ticker.to_string(), kind: TendermintInitErrorKind::InvalidPrivKey(e.to_string()), })?; let bip39_secp_priv_key = global_hd.root_priv_key().clone(); + let pubkey = Public::from_slice(&bip39_secp_priv_key.public_key().to_bytes()).map_to_mm(|e| { + TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + } + })?; + + let tendermint_pair = TendermintKeyPair::new(activated_priv_key, pubkey); + Ok(TendermintPrivKeyPolicy::HDWallet { - derivation_path: derivation_path.clone(), - activated_key: activated_priv_key, + path_to_coin: path_to_coin.clone(), + activated_key: tendermint_pair, bip39_secp_priv_key, }) }, @@ -2834,6 +3121,166 @@ pub fn tendermint_priv_key_policy( } } +pub(crate) fn chain_registry_name_from_account_prefix(ctx: &MmArc, prefix: &str) -> Option { + let Some(coins) = ctx.conf["coins"].as_array() else { + return None; + }; + + for coin in coins { + let protocol = coin + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("type") + .unwrap_or(&serde_json::Value::Null) + .as_str(); + + if protocol != Some(TENDERMINT_COIN_PROTOCOL_TYPE) { + continue; + } + + let coin_account_prefix = coin + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("account_prefix") + .map(|t| t.as_str().unwrap_or_default()); + + if coin_account_prefix == Some(prefix) { + return coin + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("chain_registry_name") + .map(|t| t.as_str().unwrap_or_default().to_owned()); + } + } + + None +} + +pub(crate) async fn create_withdraw_msg_as_any( + sender: AccountId, + receiver: AccountId, + denom: &Denom, + amount: u64, + ibc_source_channel: Option, +) -> Result> { + if let Some(channel_id) = ibc_source_channel { + MsgTransfer::new_with_default_timeout(channel_id, sender, receiver, Coin { + denom: denom.clone(), + amount: amount.into(), + }) + .to_any() + } else { + MsgSend { + from_address: sender, + to_address: receiver, + amount: vec![Coin { + denom: denom.clone(), + amount: amount.into(), + }], + } + .to_any() + } + .map_to_mm(|e| WithdrawError::InternalError(e.to_string())) +} + +pub async fn get_ibc_transfer_channels( + source_registry_name: String, + destination_registry_name: String, +) -> IBCTransferChannelsResult { + #[derive(Deserialize)] + struct ChainRegistry { + channels: Vec, + } + + #[derive(Deserialize)] + struct ChannelInfo { + channel_id: String, + port_id: String, + } + + #[derive(Deserialize)] + struct IbcChannel { + #[allow(dead_code)] + chain_1: ChannelInfo, + chain_2: ChannelInfo, + ordering: String, + version: String, + tags: Option, + } + + let source_filename = format!("{}-{}.json", source_registry_name, destination_registry_name); + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = git_controller + .client + .get_file_metadata_list( + CHAIN_REGISTRY_REPO_OWNER, + CHAIN_REGISTRY_REPO_NAME, + CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, + ) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + let source_channel_file = metadata_list + .iter() + .find(|metadata| metadata.name == source_filename) + .or_mm_err(|| IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(source_filename))?; + + let mut registry_object = git_controller + .client + .deserialize_json_source::(source_channel_file.to_owned()) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + registry_object + .channels + .retain(|ch| ch.chain_2.port_id == *IBC_OUT_SOURCE_PORT); + + let result: Vec = registry_object + .channels + .iter() + .map(|ch| IBCTransferChannel { + channel_id: ch.chain_2.channel_id.clone(), + ordering: ch.ordering.clone(), + version: ch.version.clone(), + tags: ch.tags.clone().map(|t| IBCTransferChannelTag { + status: t.status, + preferred: t.preferred, + dex: t.dex, + }), + }) + .collect(); + + if result.is_empty() { + return MmError::err(IBCTransferChannelsRequestError::CouldNotFindChannel( + destination_registry_name, + )); + } + + Ok(IBCTransferChannelsResponse { + ibc_transfer_channels: result, + }) +} + +fn parse_expected_sequence_number(e: &str) -> MmResult { + if let Some(sequence) = SEQUENCE_PARSER_REGEX.captures(e).and_then(|c| c.get(1)) { + let account_sequence = + u64::from_str(sequence.as_str()).map_to_mm(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; + + return Ok(account_sequence); + } + + MmError::err(TendermintCoinRpcError::InternalError(format!( + "Could not parse the expected sequence number from this error message: '{}'", + e + ))) +} + #[cfg(test)] pub mod tendermint_coin_tests { use super::*; @@ -2844,7 +3291,7 @@ pub mod tendermint_coin_tests { use std::mem::discriminant; pub const IRIS_TESTNET_HTLC_PAIR1_SEED: &str = "iris test seed"; - // pub const IRIS_TESTNET_HTLC_PAIR1_PUB_KEY: &str = &[ + // pub const IRIS_TESTNET_HTLC_PAIR1_PUB_KEY: &[u8] = &[ // 2, 35, 133, 39, 114, 92, 150, 175, 252, 203, 124, 85, 243, 144, 11, 52, 91, 128, 236, 82, 104, 212, 131, 40, // 79, 22, 40, 7, 119, 93, 50, 179, 43, // ]; @@ -2926,7 +3373,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -2935,15 +3384,15 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, + false, )) .unwrap(); // << BEGIN HTLC CREATION let to: AccountId = IRIS_TESTNET_HTLC_PAIR2_ADDRESS.parse().unwrap(); - const UAMOUNT: u64 = 1; - let amount: cosmrs::Decimal = UAMOUNT.into(); - let amount_dec = big_decimal_from_sat_unsigned(UAMOUNT, coin.decimals); + let amount = 1; + let amount_dec = big_decimal_from_sat_unsigned(amount, coin.decimals); let mut sec = [0u8; 32]; common::os_rng(&mut sec).unwrap(); @@ -2952,7 +3401,13 @@ pub mod tendermint_coin_tests { let time_lock = 1000; let create_htlc_tx = coin - .gen_create_htlc_tx(coin.denom.clone(), &to, amount, sha256(&sec).as_slice(), time_lock) + .gen_create_htlc_tx( + coin.denom.clone(), + &to, + amount.into(), + sha256(&sec).as_slice(), + time_lock, + ) .unwrap(); let current_block_fut = coin.current_block().compat(); @@ -2970,11 +3425,12 @@ pub mod tendermint_coin_tests { .unwrap() }); - let send_tx_fut = coin.seq_safe_send_raw_tx_bytes( + let send_tx_fut = coin.common_send_raw_tx_bytes( create_htlc_tx.msg_payload.clone(), fee, timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(10), ); block_on(async { send_tx_fut.await.unwrap(); @@ -3015,8 +3471,13 @@ pub mod tendermint_coin_tests { .unwrap() }); - let send_tx_fut = - coin.seq_safe_send_raw_tx_bytes(claim_htlc_tx.msg_payload, fee, timeout_height, TX_DEFAULT_MEMO.into()); + let send_tx_fut = coin.common_send_raw_tx_bytes( + claim_htlc_tx.msg_payload, + fee, + timeout_height, + TX_DEFAULT_MEMO.into(), + Duration::from_secs(30), + ); let (tx_id, _tx_raw) = block_on(async { send_tx_fut.await.unwrap() }); @@ -3038,7 +3499,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3047,19 +3510,23 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, + false, )) .unwrap(); let events = "claim_htlc.id='2B925FC83A106CC81590B3DB108AC2AE496FFA912F368FE5E29BC1ED2B754F2C'"; + // TODO: Remove deprecated attribute when new version of tendermint-rs is released + #[allow(deprecated)] let request = GetTxsEventRequest { events: vec![events.into()], - pagination: None, order_by: TendermintResultOrder::Ascending as i32, + page: 1, + limit: 1, + pagination: None, }; - let path = AbciPath::from_str(ABCI_GET_TXS_EVENT_PATH).unwrap(); let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( - Some(path), + Some(ABCI_GET_TXS_EVENT_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -3074,10 +3541,9 @@ pub mod tendermint_coin_tests { let first_msg = tx.body.as_ref().unwrap().messages.first().unwrap(); println!("{:?}", first_msg); - let claim_htlc = - crate::tendermint::iris::htlc_proto::ClaimHtlcProtoRep::decode(first_msg.value.as_slice()).unwrap(); + let claim_htlc = ClaimHtlcProto::decode(HtlcType::Iris, first_msg.value.as_slice()).unwrap(); let expected_secret = [1; 32]; - let actual_secret = hex::decode(claim_htlc.secret).unwrap(); + let actual_secret = hex::decode(claim_htlc.secret()).unwrap(); assert_eq!(actual_secret, expected_secret); } @@ -3096,7 +3562,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3105,7 +3573,8 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, + false, )) .unwrap(); @@ -3116,9 +3585,8 @@ pub mod tendermint_coin_tests { hash: create_tx_hash.into(), }; - let path = AbciPath::from_str(ABCI_GET_TX_PATH).unwrap(); let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( - Some(path), + Some(ABCI_GET_TX_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -3150,7 +3618,7 @@ pub mod tendermint_coin_tests { // https://nyancat.iobscan.io/#/tx?txHash=565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137 let expected_spend_hash = "565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137"; - let hash = spend_tx.tx_hash(); + let hash = spend_tx.tx_hash_as_bytes(); assert_eq!(hex::encode_upper(hash.0), expected_spend_hash); } @@ -3168,7 +3636,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3177,7 +3647,8 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, + false, )) .unwrap(); @@ -3362,7 +3833,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3371,7 +3844,8 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, + false, )) .unwrap(); @@ -3395,7 +3869,7 @@ pub mod tendermint_coin_tests { unique_swap_data: Vec::new(), watcher_reward: None, }; - let validate_err = coin.validate_taker_payment(input).wait().unwrap_err(); + let validate_err = block_on(coin.validate_taker_payment(input)).unwrap_err(); match validate_err.into_inner() { ValidatePaymentError::WrongPaymentTx(e) => assert!(e.contains("Incorrect CreateHtlc message")), unexpected => panic!("Unexpected error variant {:?}", unexpected), @@ -3421,11 +3895,7 @@ pub mod tendermint_coin_tests { unique_swap_data: Vec::new(), watcher_reward: None, }; - let validate_err = block_on( - coin.validate_payment_for_denom(input, "nim".parse().unwrap(), 6) - .compat(), - ) - .unwrap_err(); + let validate_err = block_on(coin.validate_payment_for_denom(input, "nim".parse().unwrap(), 6)).unwrap_err(); match validate_err.into_inner() { ValidatePaymentError::UnexpectedPaymentState(_) => (), unexpected => panic!("Unexpected error variant {:?}", unexpected), @@ -3446,7 +3916,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3455,7 +3927,8 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, + false, )) .unwrap(); @@ -3466,9 +3939,8 @@ pub mod tendermint_coin_tests { hash: create_tx_hash.into(), }; - let path = AbciPath::from_str(ABCI_GET_TX_PATH).unwrap(); let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( - Some(path), + Some(ABCI_GET_TX_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -3502,7 +3974,7 @@ pub mod tendermint_coin_tests { // https://nyancat.iobscan.io/#/tx?txHash=565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137 let expected_spend_hash = "565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137"; - let hash = spend_tx.tx_hash(); + let hash = spend_tx.tx_hash_as_bytes(); assert_eq!(hex::encode_upper(hash.0), expected_spend_hash); } @@ -3520,7 +3992,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3529,7 +4003,8 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, + false, )) .unwrap(); @@ -3540,9 +4015,8 @@ pub mod tendermint_coin_tests { hash: create_tx_hash.into(), }; - let path = AbciPath::from_str(ABCI_GET_TX_PATH).unwrap(); let response = block_on(block_on(coin.rpc_client()).unwrap().abci_query( - Some(path), + Some(ABCI_GET_TX_PATH.to_string()), request.encode_to_vec(), ABCI_REQUEST_HEIGHT, ABCI_REQUEST_PROVE, @@ -3590,7 +4064,9 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = common::block_on(TendermintCoin::init( &ctx, @@ -3599,7 +4075,8 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, + false, )) .unwrap(); @@ -3618,7 +4095,7 @@ pub mod tendermint_coin_tests { assert_eq!( discriminant(&status_code), - discriminant(&cosmrs::tendermint::abci::Code::Err(61)) + discriminant(&cosmrs::tendermint::abci::Code::Err(NonZeroU32::new(61).unwrap())) ); } @@ -3642,7 +4119,9 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = common::block_on(TendermintCoin::init( &ctx, @@ -3651,7 +4130,8 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, + false, )) .unwrap(); @@ -3687,4 +4167,42 @@ pub mod tendermint_coin_tests { block_on(coin.wait_for_confirmations(confirm_payment_input).compat()).unwrap_err(); } } + + #[test] + fn test_generate_account_id() { + let key_pair = key_pair_from_seed("best seed").unwrap(); + + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let pb = PublicKey::from_raw_secp256k1(&key_pair.public().to_bytes()).unwrap(); + + let pk_activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + // Derive account id from the private key. + let pk_account_id = pk_activation_policy.generate_account_id("cosmos").unwrap(); + assert_eq!( + pk_account_id.to_string(), + "cosmos1aghdjgt5gzntzqgdxdzhjfry90upmtfsy2wuwp" + ); + + let pb_activation_policy = TendermintActivationPolicy::with_public_key(pb); + // Derive account id from the public key. + let pb_account_id = pb_activation_policy.generate_account_id("cosmos").unwrap(); + // Public and private keys are from the same keypair, account ids must be equal. + assert_eq!(pk_account_id, pb_account_id); + } + + #[test] + fn test_parse_expected_sequence_number() { + assert_eq!( + 13, + parse_expected_sequence_number("check_tx log: account sequence mismatch, expected 13").unwrap() + ); + assert_eq!( + 5, + parse_expected_sequence_number("check_tx log: account sequence mismatch, expected 5, got...").unwrap() + ); + assert_eq!(17, parse_expected_sequence_number("account sequence mismatch, expected. check_tx log: account sequence mismatch, expected 17, got 16: incorrect account sequence, deliver_tx log...").unwrap()); + assert!(parse_expected_sequence_number("").is_err()); + assert!(parse_expected_sequence_number("check_tx log: account sequence mismatch, expected").is_err()); + } } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 93c5eb56f7..3e63589926 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -1,25 +1,23 @@ //! Module containing implementation for Tendermint Tokens. They include native assets + IBC -use super::ibc::transfer_v1::MsgTransfer; use super::ibc::IBC_GAS_LIMIT_DEFAULT; -use super::{TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, - TX_DEFAULT_MEMO}; -use crate::rpc_command::tendermint::IBCWithdrawRequest; -use crate::tendermint::account_id_from_privkey; +use super::{create_withdraw_msg_as_any, TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, + TIMEOUT_HEIGHT_DELTA, TX_DEFAULT_MEMO}; +use crate::coin_errors::ValidatePaymentResult; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MyAddressError, - NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, RawTransactionFut, - RawTransactionRequest, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, - SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOps, - TakerSwapMakerCoin, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, - TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFrom, WithdrawFut, WithdrawRequest}; + NegotiateSwapContractAddrErr, PaymentInstructions, PaymentInstructionsErr, RawTransactionError, + RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, + TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, + TransactionErr, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFut, WithdrawRequest}; use crate::{DexFee, MmCoinEnum, PaymentInstructionArgs, ValidateWatcherSpendInput, WatcherReward, WatcherRewardError}; use async_trait::async_trait; use bitcrypto::sha256; @@ -27,9 +25,7 @@ use common::executor::abortable_queue::AbortableQueue; use common::executor::{AbortableSystem, AbortedError}; use common::log::warn; use common::Future01CompatExt; -use cosmrs::{bank::MsgSend, - tx::{Fee, Msg}, - AccountId, Coin, Denom}; +use cosmrs::{tx::Fee, AccountId, Coin, Denom}; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use keys::KeyPair; @@ -103,178 +99,19 @@ impl TendermintToken { }; Ok(TendermintToken(Arc::new(token_impl))) } - - pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { - let platform = self.platform_coin.clone(); - let token = self.clone(); - let fut = async move { - let to_address = - AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - - let (account_id, priv_key) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { - let priv_key = platform - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" - .to_string(), - )) - }, - None => ( - platform.account_id.clone(), - *platform.priv_key_policy.activated_key_or_err()?, - ), - }; - - let (base_denom_balance, base_denom_balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) - .await?; - - let (balance_denom, balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&account_id, &token.denom, token.decimals()) - .await?; - - let (amount_denom, amount_dec, total_amount) = if req.max { - ( - balance_denom, - big_decimal_from_sat_unsigned(balance_denom, token.decimals), - balance_dec, - ) - } else { - if balance_dec < req.amount { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: token.ticker.clone(), - available: balance_dec, - required: req.amount, - }); - } - - ( - sat_from_big_decimal(&req.amount, token.decimals())?, - req.amount.clone(), - req.amount, - ) - }; - - if !platform.is_tx_amount_enough(token.decimals, &amount_dec) { - return MmError::err(WithdrawError::AmountTooLow { - amount: amount_dec, - threshold: token.min_tx_amount(), - }); - } - - let received_by_me = if to_address == account_id { - amount_dec - } else { - BigDecimal::default() - }; - - let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); - - let msg_transfer = MsgTransfer::new_with_default_timeout( - req.ibc_source_channel.clone(), - account_id.clone(), - to_address.clone(), - Coin { - denom: token.denom.clone(), - amount: amount_denom.into(), - }, - ) - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let current_block = token - .current_block() - .compat() - .await - .map_to_mm(WithdrawError::Transport)?; - - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - - let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); - - let fee_amount_u64 = platform - .calculate_account_fee_amount_as_u64( - &account_id, - &priv_key, - msg_transfer.clone(), - timeout_height, - memo.clone(), - req.fee, - ) - .await?; - - let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); - - if base_denom_balance < fee_amount_u64 { - return MmError::err(WithdrawError::NotSufficientPlatformBalanceForFee { - coin: platform.ticker().to_string(), - available: base_denom_balance_dec, - required: fee_amount_dec, - }); - } - - let fee_amount = Coin { - denom: platform.denom.clone(), - amount: fee_amount_u64.into(), - }; - - let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); - - let account_info = platform.account_info(&account_id).await?; - let tx_raw = platform - .any_to_signed_raw_tx(&priv_key, account_info, msg_transfer, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let tx_bytes = tx_raw - .to_bytes() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let hash = sha256(&tx_bytes); - Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), - from: vec![account_id.to_string()], - to: vec![req.to], - my_balance_change: &received_by_me - &total_amount, - spent_by_me: total_amount.clone(), - total_amount, - received_by_me, - block_height: 0, - timestamp: 0, - fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { - coin: platform.ticker().to_string(), - amount: fee_amount_dec, - uamount: fee_amount_u64, - gas_limit, - })), - coin: token.ticker.clone(), - internal_id: hash.to_vec().into(), - kmd_rewards: None, - transaction_type: TransactionType::default(), - memo: Some(memo), - }) - }; - Box::new(fut.boxed().compat()) - } } #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintToken { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionFut { self.platform_coin.send_taker_fee_for_denom( fee_addr, dex_fee.fee_amount().into(), self.denom.clone(), self.decimals, uuid, + expire_at, ) } @@ -300,14 +137,22 @@ impl SwapOps for TendermintToken { ) } - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { self.platform_coin .send_maker_spends_taker_payment(maker_spends_payment_args) + .await } - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { self.platform_coin .send_taker_spends_maker_payment(taker_spends_payment_args) + .await } async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { @@ -334,14 +179,16 @@ impl SwapOps for TendermintToken { ) } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { self.platform_coin .validate_payment_for_denom(input, self.denom.clone(), self.decimals) + .await } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { self.platform_coin .validate_payment_for_denom(input, self.denom.clone(), self.decimals) + .await } fn check_if_my_payment_sent( @@ -410,7 +257,7 @@ impl SwapOps for TendermintToken { #[inline] fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { - self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + self.platform_coin.derive_htlc_pubkey(swap_unique_data) } fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { @@ -535,13 +382,14 @@ impl WatcherOps for TendermintToken { } } +#[async_trait] impl MarketCoinOps for TendermintToken { fn ticker(&self) -> &str { &self.ticker } fn my_address(&self) -> MmResult { self.platform_coin.my_address() } - fn get_public_key(&self) -> Result> { - self.platform_coin.get_public_key() + async fn get_public_key(&self) -> Result> { + self.platform_coin.get_public_key().await } fn sign_message_hash(&self, message: &str) -> Option<[u8; 32]> { self.platform_coin.sign_message_hash(message) } @@ -579,6 +427,13 @@ impl MarketCoinOps for TendermintToken { self.platform_coin.send_raw_tx_bytes(tx) } + #[inline(always)] + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { + MmError::err(RawTransactionError::NotImplemented { + coin: self.ticker().to_string(), + }) + } + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { self.platform_coin.wait_for_confirmations(input) } @@ -608,6 +463,8 @@ impl MarketCoinOps for TendermintToken { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } + + fn is_trezor(&self) -> bool { self.platform_coin.is_trezor() } } #[async_trait] @@ -615,6 +472,13 @@ impl MarketCoinOps for TendermintToken { impl MmCoin for TendermintToken { fn is_asset_chain(&self) -> bool { false } + fn wallet_only(&self, ctx: &MmArc) -> bool { + let coin_conf = crate::coin_conf(ctx, self.ticker()); + let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); + + wallet_only_conf || self.platform_coin.is_keplr_from_ledger + } + fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { @@ -623,33 +487,10 @@ impl MmCoin for TendermintToken { let fut = async move { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - if to_address.prefix() != platform.account_prefix { - return MmError::err(WithdrawError::InvalidAddress(format!( - "expected {} address prefix", - platform.account_prefix - ))); - } - let (account_id, priv_key) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { - let priv_key = platform - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnexpectedFromAddress( - "Withdraw from 'AddressId' or 'DerivationPath' is not supported yet for Tendermint!" - .to_string(), - )) - }, - None => ( - platform.account_id.clone(), - *platform.priv_key_policy.activated_key_or_err()?, - ), - }; + let is_ibc_transfer = to_address.prefix() != platform.account_prefix || req.ibc_source_channel.is_some(); + + let (account_id, maybe_pk) = platform.account_id_and_pk_for_withdraw(req.from)?; let (base_denom_balance, base_denom_balance_dec) = platform .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) @@ -694,16 +535,23 @@ impl MmCoin for TendermintToken { BigDecimal::default() }; - let msg_send = MsgSend { - from_address: account_id.clone(), - to_address, - amount: vec![Coin { - denom: token.denom.clone(), - amount: amount_denom.into(), - }], - } - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; + let channel_id = if is_ibc_transfer { + match &req.ibc_source_channel { + Some(_) => req.ibc_source_channel, + None => Some(platform.detect_channel_id_for_ibc_transfer(&to_address).await?), + } + } else { + None + }; + + let msg_payload = create_withdraw_msg_as_any( + account_id.clone(), + to_address.clone(), + &token.denom, + amount_denom, + channel_id.clone(), + ) + .await?; let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); let current_block = token @@ -714,13 +562,17 @@ impl MmCoin for TendermintToken { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + let (_, gas_limit) = if is_ibc_transfer { + platform.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT) + } else { + platform.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT) + }; let fee_amount_u64 = platform .calculate_account_fee_amount_as_u64( &account_id, - &priv_key, - msg_send.clone(), + maybe_pk, + msg_payload.clone(), timeout_height, memo.clone(), req.fee, @@ -745,18 +597,18 @@ impl MmCoin for TendermintToken { let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); let account_info = platform.account_info(&account_id).await?; - let tx_raw = platform - .any_to_signed_raw_tx(&priv_key, account_info, msg_send, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let tx_bytes = tx_raw - .to_bytes() + let tx = platform + .any_to_transaction_data(maybe_pk, msg_payload, &account_info, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let hash = sha256(&tx_bytes); + let internal_id = { + let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); + sha256(&hex_vec).to_vec().into() + }; + Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), + tx, from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, @@ -772,9 +624,13 @@ impl MmCoin for TendermintToken { gas_limit, })), coin: token.ticker.clone(), - internal_id: hash.to_vec().into(), + internal_id, kmd_rewards: None, - transaction_type: TransactionType::default(), + transaction_type: if is_ibc_transfer { + TransactionType::TendermintIBCTransfer + } else { + TransactionType::StandardTransfer + }, memo: Some(memo), }) }; @@ -810,6 +666,7 @@ impl MmCoin for TendermintToken { &self, value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { let amount = match value { TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, @@ -820,22 +677,12 @@ impl MmCoin for TendermintToken { .await } - fn get_receiver_trade_fee(&self, _stage: FeeApproxStage) -> TradePreimageFut { - let token = self.clone(); - let fut = async move { - // We can't simulate Claim Htlc without having information about broadcasted htlc tx. - // Since create and claim htlc fees are almost same, we can simply simulate create htlc tx. - token - .platform_coin - .get_sender_trade_fee_for_denom( - token.ticker.clone(), - token.denom.clone(), - token.decimals, - token.min_tx_amount(), - ) - .await - }; - Box::new(fut.boxed().compat()) + fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { + // As makers may not have a balance in the coin they want to swap, we need to + // calculate this fee in platform coin. + // + // p.s.: Same goes for ETH assets: https://github.com/KomodoPlatform/komodo-defi-framework/blob/b0fd99e8406e67ea06435dd028991caa5f522b5c/mm2src/coins/eth.rs#L4892-L4895 + self.platform_coin.get_receiver_trade_fee(stage) } async fn get_fee_to_send_taker_fee( diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index adead7550e..1c8adf8de3 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -1,16 +1,19 @@ use super::{rpc::*, AllBalancesResult, TendermintCoin, TendermintCommons, TendermintToken}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; -use crate::tendermint::{CustomTendermintMsgType, TendermintFeeDetails}; +use crate::tendermint::htlc::CustomTendermintMsgType; +use crate::tendermint::TendermintFeeDetails; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; -use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionDetails, TransactionType, TxFeeDetails}; +use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionData, TransactionDetails, TransactionType, + TxFeeDetails}; use async_trait::async_trait; +use base64::Engine; use bitcrypto::sha256; use common::executor::Timer; use common::log; -use cosmrs::tendermint::abci::Code as TxCode; use cosmrs::tendermint::abci::Event; +use cosmrs::tendermint::abci::{Code as TxCode, EventAttribute}; use cosmrs::tx::Fee; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; @@ -22,6 +25,26 @@ use rpc::v1::types::Bytes as BytesJson; use std::cmp; use std::convert::Infallible; +const TX_PAGE_SIZE: u8 = 50; + +const DEFAULT_TRANSFER_EVENT_COUNT: usize = 1; +const CREATE_HTLC_EVENT: &str = "create_htlc"; +const CLAIM_HTLC_EVENT: &str = "claim_htlc"; +const TRANSFER_EVENT: &str = "transfer"; +const ACCEPTED_EVENTS: &[&str] = &[CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT, TRANSFER_EVENT]; + +const RECEIVER_TAG_KEY: &str = "receiver"; +const RECEIVER_TAG_KEY_BASE64: &str = "cmVjZWl2ZXI="; + +const RECIPIENT_TAG_KEY: &str = "recipient"; +const RECIPIENT_TAG_KEY_BASE64: &str = "cmVjaXBpZW50"; + +const SENDER_TAG_KEY: &str = "sender"; +const SENDER_TAG_KEY_BASE64: &str = "c2VuZGVy"; + +const AMOUNT_TAG_KEY: &str = "amount"; +const AMOUNT_TAG_KEY_BASE64: &str = "YW1vdW50"; + macro_rules! try_or_return_stopped_as_err { ($exp:expr, $reason: expr, $fmt:literal) => { match $exp { @@ -260,7 +283,7 @@ where let ctx_balances = ctx.balances.clone(); - let balances = match ctx.coin.all_balances().await { + let balances = match ctx.coin.get_all_balances().await { Ok(balances) => balances, Err(_) => { return Self::change_state(OnIoErrorCooldown::new(self.address.clone(), self.last_height_state)); @@ -292,18 +315,6 @@ where self: Box, ctx: &mut TendermintTxHistoryStateMachine, ) -> StateResult> { - const TX_PAGE_SIZE: u8 = 50; - - const DEFAULT_TRANSFER_EVENT_COUNT: usize = 1; - const CREATE_HTLC_EVENT: &str = "create_htlc"; - const CLAIM_HTLC_EVENT: &str = "claim_htlc"; - const TRANSFER_EVENT: &str = "transfer"; - const ACCEPTED_EVENTS: &[&str] = &[CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT, TRANSFER_EVENT]; - const RECIPIENT_TAG_KEY: &str = "recipient"; - const SENDER_TAG_KEY: &str = "sender"; - const RECEIVER_TAG_KEY: &str = "receiver"; - const AMOUNT_TAG_KEY: &str = "amount"; - struct TxAmounts { total: BigDecimal, spent_by_me: BigDecimal, @@ -366,7 +377,7 @@ where coin: coin.platform_ticker().to_string(), amount: big_decimal_from_sat_unsigned(fee_uamount, coin.decimals()), uamount: fee_uamount, - gas_limit: fee.gas_limit.value(), + gas_limit: fee.gas_limit, }) } @@ -389,31 +400,28 @@ where // updates sender and receiver addresses if tx is htlc, and if not leaves as it is. fn read_real_htlc_addresses(transfer_details: &mut TransferDetails, msg_event: &&Event) { - match msg_event.type_str.as_str() { + match msg_event.kind.as_str() { CREATE_HTLC_EVENT => { - transfer_details.from = some_or_return!(msg_event - .attributes - .iter() - .find(|tag| tag.key.to_string() == SENDER_TAG_KEY)) - .value - .to_string(); - - transfer_details.to = some_or_return!(msg_event - .attributes - .iter() - .find(|tag| tag.key.to_string() == RECEIVER_TAG_KEY)) - .value - .to_string(); + transfer_details.from = some_or_return!(get_value_from_event_attributes( + &msg_event.attributes, + SENDER_TAG_KEY, + SENDER_TAG_KEY_BASE64 + )); + + transfer_details.to = some_or_return!(get_value_from_event_attributes( + &msg_event.attributes, + RECEIVER_TAG_KEY, + RECEIVER_TAG_KEY_BASE64, + )); transfer_details.transfer_event_type = TransferEventType::CreateHtlc; }, CLAIM_HTLC_EVENT => { - transfer_details.from = some_or_return!(msg_event - .attributes - .iter() - .find(|tag| tag.key.to_string() == SENDER_TAG_KEY)) - .value - .to_string(); + transfer_details.from = some_or_return!(get_value_from_event_attributes( + &msg_event.attributes, + SENDER_TAG_KEY, + SENDER_TAG_KEY_BASE64 + )); transfer_details.transfer_event_type = TransferEventType::ClaimHtlc; }, @@ -425,13 +433,13 @@ where let mut transfer_details_list: Vec = vec![]; for (index, event) in tx_events.iter().enumerate() { - if event.type_str.as_str() == TRANSFER_EVENT { - let amount_with_denoms = some_or_continue!(event - .attributes - .iter() - .find(|tag| tag.key.to_string() == AMOUNT_TAG_KEY)) - .value - .to_string(); + if event.kind.as_str() == TRANSFER_EVENT { + let amount_with_denoms = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + AMOUNT_TAG_KEY, + AMOUNT_TAG_KEY_BASE64 + )); + let amount_with_denoms = amount_with_denoms.split(','); for amount_with_denom in amount_with_denoms { @@ -440,19 +448,17 @@ where let denom = &amount_with_denom[extracted_amount.len()..]; let amount = some_or_continue!(extracted_amount.parse().ok()); - let from = some_or_continue!(event - .attributes - .iter() - .find(|tag| tag.key.to_string() == SENDER_TAG_KEY)) - .value - .to_string(); + let from = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + SENDER_TAG_KEY, + SENDER_TAG_KEY_BASE64 + )); - let to = some_or_continue!(event - .attributes - .iter() - .find(|tag| tag.key.to_string() == RECIPIENT_TAG_KEY)) - .value - .to_string(); + let to = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + RECIPIENT_TAG_KEY, + RECIPIENT_TAG_KEY_BASE64, + )); let mut tx_details = TransferDetails { from, @@ -467,7 +473,7 @@ where // If previous message is htlc related, that means current transfer // addresses will be wrong. if let Some(prev_event) = tx_events.get(index - 1) { - if [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&prev_event.type_str.as_str()) { + if [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&prev_event.kind.as_str()) { read_real_htlc_addresses(&mut tx_details, prev_event); } }; @@ -496,7 +502,7 @@ where // Filter out irrelevant events let mut events: Vec<&Event> = tx_events .iter() - .filter(|event| ACCEPTED_EVENTS.contains(&event.type_str.as_str())) + .filter(|event| ACCEPTED_EVENTS.contains(&event.kind.as_str())) .collect(); events.reverse(); @@ -504,13 +510,9 @@ where if events.len() > DEFAULT_TRANSFER_EVENT_COUNT { // Retain fee related events events.retain(|event| { - if event.type_str == TRANSFER_EVENT { - let amount_with_denom = event - .attributes - .iter() - .find(|tag| tag.key.to_string() == AMOUNT_TAG_KEY) - .map(|t| t.value.to_string()); - + if event.kind == TRANSFER_EVENT { + let amount_with_denom = + get_value_from_event_attributes(&event.attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64); amount_with_denom != Some(fee_amount_with_denom.clone()) } else { true @@ -618,10 +620,8 @@ where highest_height = cmp::max(highest_height, tx.height.into()); - let deserialized_tx = try_or_continue!( - cosmrs::Tx::from_bytes(tx.tx.as_bytes()), - "Could not deserialize transaction" - ); + let deserialized_tx = + try_or_continue!(cosmrs::Tx::from_bytes(&tx.tx), "Could not deserialize transaction"); let msg = try_or_continue!( deserialized_tx.body.messages.first().ok_or("Tx body couldn't be read."), @@ -720,8 +720,7 @@ where received_by_me: tx_amounts.received_by_me, // This can be 0 since it gets remapped in `coins::my_tx_history_v2` my_balance_change: BigDecimal::default(), - tx_hash: tx_hash.to_string(), - tx_hex: msg.into(), + tx: TransactionData::new_signed(msg.into(), tx_hash.to_string()), fee_details: Some(TxFeeDetails::Tendermint(fee_details.clone())), block_height: tx.height.into(), coin: transfer_details.denom.clone(), @@ -894,13 +893,31 @@ where } } +/// Find, decode (if needed) and return the event attribute value. +/// +/// If the attribute doesn't exist, or decoding fails, `None` will be returned. +fn get_value_from_event_attributes(events: &[EventAttribute], tag: &str, base64_encoded_tag: &str) -> Option { + let event_attribute = events + .iter() + .find(|attribute| attribute.key == tag || attribute.key == base64_encoded_tag)?; + + if event_attribute.key == base64_encoded_tag { + let decoded_bytes = base64::engine::general_purpose::STANDARD + .decode(event_attribute.value.clone()) + .ok()?; + String::from_utf8(decoded_bytes).ok() + } else { + Some(event_attribute.value.clone()) + } +} + pub async fn tendermint_history_loop( coin: TendermintCoin, storage: impl TxHistoryStorage, _ctx: MmArc, _current_balance: Option, ) { - let balances = match coin.all_balances().await { + let balances = match coin.get_all_balances().await { Ok(balances) => balances, Err(e) => { log::error!("{}", e); @@ -921,3 +938,101 @@ pub async fn tendermint_history_loop( .await .expect("The error of this machine is Infallible"); } + +#[cfg(any(test, target_arch = "wasm32"))] +mod tests { + use super::*; + use common::cross_test; + + common::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + + cross_test!(test_get_value_from_event_attributes, { + let attributes = vec![ + EventAttribute { + key: "recipient".to_owned(), + value: "nuc1erfnkjsmalkwtvj44qnfr2drfzdt4n9ledw63y".to_owned(), + index: false, + }, + EventAttribute { + key: "sender".to_owned(), + value: "nuc1a7xynj4ceft8kgdjr6kcq0s07y3ccya60rqwwn".to_owned(), + index: false, + }, + EventAttribute { + key: "amount".to_owned(), + value: "8000ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C".to_owned(), + index: false, + }, + ]; + + let value = get_value_from_event_attributes(&attributes, "invalid", ""); + assert_eq!(value, None); + let value = get_value_from_event_attributes(&attributes, RECIPIENT_TAG_KEY, RECIPIENT_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "nuc1erfnkjsmalkwtvj44qnfr2drfzdt4n9ledw63y"); + let value = get_value_from_event_attributes(&attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "nuc1a7xynj4ceft8kgdjr6kcq0s07y3ccya60rqwwn"); + let value = get_value_from_event_attributes(&attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64).unwrap(); + assert_eq!( + value, + "8000ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C" + ); + + let encoded_attributes = vec![ + EventAttribute { + key: "cmVjaXBpZW50".to_owned(), + value: "bnVjMTd4cGZ2YWttMmFtZzk2MnlsczZmODR6M2tlbGw4YzVsM3B6YTJ5".to_owned(), + index: true, + }, + EventAttribute { + key: "c2VuZGVy".to_owned(), + value: "bnVjMWE3eHluajRjZWZ0OGtnZGpyNmtjcTBzMDd5M2NjeWE2MHJxd3du".to_owned(), + index: true, + }, + EventAttribute { + key: "YW1vdW50".to_owned(), + value: "MjcxNjJ1bnVjbA==".to_owned(), + index: true, + }, + ]; + + let value = get_value_from_event_attributes(&encoded_attributes, "invalid", ""); + assert_eq!(value, None); + let value = + get_value_from_event_attributes(&encoded_attributes, RECIPIENT_TAG_KEY, RECIPIENT_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "nuc17xpfvakm2amg962yls6f84z3kell8c5l3pza2y"); + let value = + get_value_from_event_attributes(&encoded_attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "nuc1a7xynj4ceft8kgdjr6kcq0s07y3ccya60rqwwn"); + let value = + get_value_from_event_attributes(&encoded_attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "27162unucl"); + + let invalid_attributes = vec![ + EventAttribute { + key: String::default(), + value: String::default(), + index: true, + }, + EventAttribute { + key: "invalid-key".to_owned(), + value: String::default(), + index: true, + }, + EventAttribute { + key: "dummy-key".to_owned(), + value: String::default(), + index: true, + }, + ]; + + let value = get_value_from_event_attributes(&invalid_attributes, RECIPIENT_TAG_KEY, RECIPIENT_TAG_KEY_BASE64); + assert_eq!(value, None); + let value = get_value_from_event_attributes(&invalid_attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64); + assert_eq!(value, None); + let value = get_value_from_event_attributes(&invalid_attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64); + assert_eq!(value, None); + }); +} diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index d4da2109bd..c077bf60bd 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -1,21 +1,23 @@ #![allow(clippy::all)] -use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, SwapOps, - TradeFee, TransactionEnum, TransactionFut}; -use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinAssocTypes, - CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, - GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerSwapTakerCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - RefundFundingSecretArgs, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, - SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignatureResult, - SpendPaymentArgs, SwapOpsV2, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, - TradePreimageValue, Transaction, TransactionErr, TransactionResult, TxMarshalingErr, TxPreimageWithSig, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, - ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, - ValidateTakerFundingArgs, ValidateTakerFundingResult, ValidateTakerFundingSpendPreimageResult, - ValidateTakerPaymentSpendPreimageResult, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; +use super::{CoinBalance, FundingTxSpend, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, + RawTransactionRequest, SearchForFundingSpendErr, SwapOps, TradeFee, TransactionEnum, TransactionFut, + WaitForTakerPaymentSpendError}; +use crate::coin_errors::ValidatePaymentResult; +use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, + ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, + GenTakerPaymentSpendArgs, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, + ParseCoinAssocTypes, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + RawTransactionResult, RefundFundingSecretArgs, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, + SignatureResult, SpendPaymentArgs, TakerCoinSwapOpsV2, TakerSwapMakerCoin, TradePreimageFut, + TradePreimageResult, TradePreimageValue, Transaction, TransactionErr, TransactionResult, TxMarshalingErr, + TxPreimageWithSig, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, + ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, VerificationResult, + WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawRequest}; use crate::{DexFee, ToBytes, ValidateWatcherSpendInput}; use async_trait::async_trait; use common::executor::AbortedError; @@ -54,13 +56,15 @@ impl TestCoin { pub fn new(ticker: &str) -> TestCoin { TestCoin(Arc::new(TestCoinImpl { ticker: ticker.into() })) } } +#[async_trait] #[mockable] +#[async_trait] impl MarketCoinOps for TestCoin { fn ticker(&self) -> &str { &self.ticker } fn my_address(&self) -> MmResult { unimplemented!() } - fn get_public_key(&self) -> Result> { unimplemented!() } + async fn get_public_key(&self) -> Result> { unimplemented!() } fn sign_message_hash(&self, _message: &str) -> Option<[u8; 32]> { unimplemented!() } @@ -81,6 +85,9 @@ impl MarketCoinOps for TestCoin { fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { unimplemented!() } + #[inline(always)] + async fn sign_raw_tx(&self, _args: &SignRawTransactionRequest) -> RawTransactionResult { unimplemented!() } + fn wait_for_confirmations(&self, _input: ConfirmPaymentInput) -> Box + Send> { unimplemented!() } @@ -100,22 +107,32 @@ impl MarketCoinOps for TestCoin { fn min_tx_amount(&self) -> BigDecimal { Default::default() } fn min_trading_vol(&self) -> MmNumber { MmNumber::from("0.00777") } + + fn is_trezor(&self) -> bool { unimplemented!() } } #[async_trait] #[mockable] impl SwapOps for TestCoin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } fn send_taker_payment(&self, _taker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } - fn send_maker_spends_taker_payment(&self, _maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + _maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } - fn send_taker_spends_maker_payment(&self, _taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + _taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { unimplemented!() } @@ -135,9 +152,13 @@ impl SwapOps for TestCoin { fn validate_fee(&self, _validate_fee_args: ValidateFeeArgs) -> ValidatePaymentFut<()> { unimplemented!() } - fn validate_maker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } + async fn validate_maker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } - fn validate_taker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentFut<()> { unimplemented!() } + async fn validate_taker_payment(&self, _input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + unimplemented!() + } fn check_if_my_payment_sent( &self, @@ -148,14 +169,14 @@ impl SwapOps for TestCoin { async fn search_for_swap_tx_spend_my( &self, - _: SearchForSwapTxSpendInput<'_>, + _input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { unimplemented!() } async fn search_for_swap_tx_spend_other( &self, - _: SearchForSwapTxSpendInput<'_>, + _input: SearchForSwapTxSpendInput<'_>, ) -> Result, String> { unimplemented!() } @@ -343,6 +364,7 @@ impl MmCoin for TestCoin { &self, _value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { unimplemented!() } @@ -400,7 +422,7 @@ pub struct TestTx {} impl Transaction for TestTx { fn tx_hex(&self) -> Vec { todo!() } - fn tx_hash(&self) -> BytesJson { todo!() } + fn tx_hash_as_bytes(&self) -> BytesJson { todo!() } } pub struct TestPreimage {} @@ -415,7 +437,10 @@ impl ToBytes for TestSig { fn to_bytes(&self) -> Vec { vec![] } } -impl CoinAssocTypes for TestCoin { +#[async_trait] +impl ParseCoinAssocTypes for TestCoin { + type Address = String; + type AddressParseError = String; type Pubkey = TestPubkey; type PubkeyParseError = String; type Tx = TestTx; @@ -425,6 +450,10 @@ impl CoinAssocTypes for TestCoin { type Sig = TestSig; type SigParseError = String; + async fn my_addr(&self) -> Self::Address { todo!() } + + fn parse_address(&self, address: &str) -> Result { todo!() } + fn parse_pubkey(&self, pubkey: &[u8]) -> Result { unimplemented!() } fn parse_tx(&self, tx: &[u8]) -> Result { unimplemented!() } @@ -436,14 +465,16 @@ impl CoinAssocTypes for TestCoin { #[async_trait] #[mockable] -impl SwapOpsV2 for TestCoin { +impl TakerCoinSwapOpsV2 for TestCoin { async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result { todo!() } - async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateTakerFundingResult { + async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateSwapV2TxResult { unimplemented!() } - async fn refund_taker_funding_timelock(&self, args: RefundPaymentArgs<'_>) -> TransactionResult { todo!() } + async fn refund_taker_funding_timelock(&self, args: RefundPaymentArgs<'_>) -> Result { + todo!() + } async fn refund_taker_funding_secret( &self, @@ -452,6 +483,15 @@ impl SwapOpsV2 for TestCoin { todo!() } + async fn search_for_taker_funding_spend( + &self, + tx: &Self::Tx, + from_block: u64, + secret_hash: &[u8], + ) -> Result>, SearchForFundingSpendErr> { + todo!() + } + async fn gen_taker_funding_spend_preimage( &self, args: &GenTakerFundingSpendArgs<'_, Self>, @@ -477,7 +517,9 @@ impl SwapOpsV2 for TestCoin { todo!() } - async fn refund_combined_taker_payment(&self, args: RefundPaymentArgs<'_>) -> TransactionResult { unimplemented!() } + async fn refund_combined_taker_payment(&self, args: RefundPaymentArgs<'_>) -> Result { + unimplemented!() + } async fn gen_taker_payment_spend_preimage( &self, @@ -501,7 +543,16 @@ impl SwapOpsV2 for TestCoin { gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], swap_unique_data: &[u8], - ) -> TransactionResult { + ) -> Result { + unimplemented!() + } + + async fn wait_for_taker_payment_spend( + &self, + taker_payment: &Self::Tx, + from_block: u64, + wait_until: u64, + ) -> MmResult { unimplemented!() } diff --git a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs index 49993e4c6a..cf0575f973 100644 --- a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs @@ -447,24 +447,28 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { let sql_transaction = conn.transaction()?; for tx in transactions { - let tx_hash = tx.tx_hash.clone(); + let Some(tx_hash) = tx.tx.tx_hash() else { continue }; + let Some(tx_hex) = tx.tx.tx_hex().cloned() else { + continue; + }; + let tx_hex = format!("{:02x}", tx_hex); + let internal_id = format!("{:02x}", tx.internal_id); let confirmation_status = ConfirmationStatus::from_block_height(tx.block_height); let token_id = token_id_from_tx_type(&tx.transaction_type); let tx_json = json::to_string(&tx).expect("serialization should not fail"); - let tx_hex = format!("{:02x}", tx.tx_hex); - let tx_cache_params = [&tx_hash, &tx_hex]; + let tx_cache_params = [tx_hash, &tx_hex]; sql_transaction.execute(&insert_tx_in_cache_sql(&wallet_id)?, tx_cache_params)?; let params = [ tx_hash, - internal_id.clone(), - tx.block_height.to_string(), - confirmation_status.to_sql_param_str(), - token_id, - tx_json, + &internal_id, + &tx.block_height.to_string(), + &confirmation_status.to_sql_param_str(), + &token_id, + &tx_json, ]; sql_transaction.execute(&insert_tx_in_history_sql(&wallet_id)?, params)?; diff --git a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs index d160b68fa3..2ffcc9760d 100644 --- a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs +++ b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs @@ -134,7 +134,7 @@ async fn test_get_transaction_impl() { .await .unwrap() .unwrap(); - println!("{:?}", tx); + log!("{:?}", tx); storage .remove_tx_from_history( @@ -363,24 +363,24 @@ async fn test_add_and_get_tx_from_cache_impl() { let tx = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); storage - .add_tx_to_cache(&wallet_id_1, &tx.tx_hash, &tx.tx_hex) + .add_tx_to_cache(&wallet_id_1, tx.tx.tx_hash().unwrap(), tx.tx.tx_hex().unwrap()) .await .unwrap(); let tx_hex_from_1 = storage - .tx_bytes_from_cache(&wallet_id_1, &tx.tx_hash) + .tx_bytes_from_cache(&wallet_id_1, tx.tx.tx_hash().unwrap()) .await .unwrap() .unwrap(); - assert_eq!(tx_hex_from_1, tx.tx_hex); + assert_eq!(&tx_hex_from_1, tx.tx.tx_hex().unwrap()); // Since `wallet_id_1` and `wallet_id_2` wallets have the same `ticker`, the wallets must have one transaction cache. let tx_hex_from_2 = storage - .tx_bytes_from_cache(&wallet_id_2, &tx.tx_hash) + .tx_bytes_from_cache(&wallet_id_2, tx.tx.tx_hash().unwrap()) .await .unwrap() .unwrap(); - assert_eq!(tx_hex_from_2, tx.tx_hex); + assert_eq!(&tx_hex_from_2, tx.tx.tx_hex().unwrap()); } async fn test_get_raw_tx_bytes_on_add_transactions_impl() { @@ -401,7 +401,7 @@ async fn test_get_raw_tx_bytes_on_add_transactions_impl() { let mut tx2 = tx1.clone(); tx2.internal_id = BytesJson(vec![1; 32]); - let expected_tx_hex = tx1.tx_hex.clone(); + let expected_tx_hex = tx1.tx.tx_hex().unwrap().clone(); let transactions = [tx1, tx2]; storage diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs index 90ef077cde..c52fefd76d 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v1.rs @@ -73,11 +73,11 @@ pub(crate) struct TxHistoryTableV1 { } impl TableSignature for TxHistoryTableV1 { - fn table_name() -> &'static str { "tx_history" } + const TABLE_NAME: &'static str = "tx_history"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if let (0, 1) = (old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_index("history_id", true)?; } diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs index 1b19565bf7..acc6d338e2 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs @@ -59,13 +59,15 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { let cache_table = db_transaction.table::().await?; for tx in transactions { + let Some(tx_hash) = tx.tx.tx_hash() else { continue }; + let history_item = TxHistoryTableV2::from_tx_details(wallet_id.clone(), &tx)?; history_table.add_item(&history_item).await?; - let cache_item = TxCacheTableV2::from_tx_details(wallet_id.clone(), &tx); + let cache_item = TxCacheTableV2::from_tx_details(wallet_id.clone(), &tx)?; let index_keys = MultiIndex::new(TxCacheTableV2::COIN_TX_HASH_INDEX) .with_value(&wallet_id.ticker)? - .with_value(&tx.tx_hash)?; + .with_value(tx_hash)?; // `TxHistoryTableV2::tx_hash` is not a unique field, but `TxCacheTableV2::tx_hash` is unique. // So we use `DbTable::add_item_or_ignore_by_unique_multi_index` instead of `DbTable::add_item` // since `transactions` may contain txs with same `tx_hash` but different `internal_id`. @@ -396,12 +398,17 @@ impl TxHistoryTableV2 { const WALLET_ID_TOKEN_ID_INDEX: &'static str = "wallet_id_token_id"; fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> WasmTxHistoryResult { + let tx_hash = tx + .tx + .tx_hash() + .ok_or_else(|| WasmTxHistoryError::NotSupported("Unsupported type of TransactionDetails".to_string()))?; + let details_json = json::to_value(tx).map_to_mm(|e| WasmTxHistoryError::ErrorSerializing(e.to_string()))?; let hd_wallet_rmd160 = wallet_id.hd_wallet_rmd160_or_exclude(); Ok(TxHistoryTableV2 { coin: wallet_id.ticker, hd_wallet_rmd160, - tx_hash: tx.tx_hash.clone(), + tx_hash: tx_hash.to_string(), internal_id: tx.internal_id.clone(), block_height: BeBigUint::from(tx.block_height), confirmation_status: ConfirmationStatus::from_block_height(tx.block_height), @@ -414,11 +421,11 @@ impl TxHistoryTableV2 { } impl TableSignature for TxHistoryTableV2 { - fn table_name() -> &'static str { "tx_history_v2" } + const TABLE_NAME: &'static str = "tx_history_v2"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if let (0, 1) = (old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(TxHistoryTableV2::WALLET_ID_INDEX, &["coin", "hd_wallet_rmd160"], false)?; table.create_multi_index( TxHistoryTableV2::WALLET_ID_INTERNAL_ID_INDEX, @@ -458,21 +465,27 @@ impl TxCacheTableV2 { /// * tx_hash - transaction hash const COIN_TX_HASH_INDEX: &'static str = "coin_tx_hash"; - fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> TxCacheTableV2 { - TxCacheTableV2 { - coin: wallet_id.ticker, - tx_hash: tx.tx_hash.clone(), - tx_hex: tx.tx_hex.clone(), + fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> WasmTxHistoryResult { + if let (Some(tx_hash), Some(tx_hex)) = (tx.tx.tx_hash(), tx.tx.tx_hex()) { + return Ok(TxCacheTableV2 { + coin: wallet_id.ticker, + tx_hash: tx_hash.to_string(), + tx_hex: tx_hex.clone(), + }); } + + MmError::err(WasmTxHistoryError::NotSupported( + "Unsupported type of TransactionDetails".to_string(), + )) } } impl TableSignature for TxCacheTableV2 { - fn table_name() -> &'static str { "tx_cache_v2" } + const TABLE_NAME: &'static str = "tx_cache_v2"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if let (0, 1) = (old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(TxCacheTableV2::COIN_TX_HASH_INDEX, &["coin", "tx_hash"], true)?; } Ok(()) diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 622f2eebb5..328863865c 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright © 2023 Pampex LTD and TillyHK LTD * + * Copyright © 2023 Pampex LTD and TillyHK LTD * * * * See the CONTRIBUTOR-LICENSE-AGREEMENT, COPYING, LICENSE-COPYRIGHT-NOTICE * * and DEVELOPER-CERTIFICATE-OF-ORIGIN files in the LEGAL directory in * @@ -32,9 +32,11 @@ pub mod rpc_clients; pub mod slp; pub mod spv; pub mod swap_proto_v2_scripts; +pub mod utxo_balance_events; pub mod utxo_block_header_storage; pub mod utxo_builder; pub mod utxo_common; +pub mod utxo_hd_wallet; pub mod utxo_standard; pub mod utxo_tx_history_v2; pub mod utxo_withdraw; @@ -51,21 +53,21 @@ use common::first_char_to_upper; use common::jsonrpc_client::JsonRpcError; use common::log::LogOnError; use common::{now_sec, now_sec_u32}; -use crypto::{Bip32DerPathOps, Bip32Error, Bip44Chain, ChildNumber, DerivationPath, Secp256k1ExtendedPublicKey, - StandardHDCoinAddress, StandardHDPathError, StandardHDPathToAccount, StandardHDPathToCoin}; +use crypto::{DerivationPath, HDPathToCoin, Secp256k1ExtendedPublicKey}; use derive_more::Display; #[cfg(not(target_arch = "wasm32"))] use dirs::home_dir; -use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender, UnboundedSender}; +use futures::channel::mpsc::{Receiver as AsyncReceiver, Sender as AsyncSender, UnboundedReceiver, UnboundedSender}; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures01::Future; use keys::bytes::Bytes; +use keys::NetworkAddressPrefixes; use keys::Signature; -pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, - Type as ScriptType}; +pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressPrefix, + AddressScriptType, KeyPair, LegacyAddress, Private, Public, Secret}; #[cfg(not(target_arch = "wasm32"))] use lightning_invoice::Currency as LightningCurrency; -use mm2_core::mm_ctx::MmArc; +use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; @@ -89,11 +91,11 @@ use std::num::{NonZeroU64, TryFromIntError}; use std::ops::Deref; #[cfg(not(target_arch = "wasm32"))] use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicU64}; use std::sync::{Arc, Mutex, Weak}; use utxo_builder::UtxoConfBuilder; use utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; +use utxo_hd_wallet::UtxoHDWallet; use utxo_signer::with_key_pair::sign_tx; use utxo_signer::{TxProvider, TxProviderError, UtxoSignTxError, UtxoSignTxResult}; @@ -103,16 +105,14 @@ use self::rpc_clients::{electrum_script_hash, ElectrumClient, ElectrumRpcRequest use super::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BalanceResult, CoinBalance, CoinFutSpawner, CoinsContext, DerivationMethod, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, KmdRewardsDetails, MarketCoinOps, MmCoin, NumConversError, NumConversResult, PrivKeyActivationPolicy, PrivKeyPolicy, - PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, - RpcTransportEventHandler, RpcTransportEventHandlerShared, TradeFee, TradePreimageError, TradePreimageFut, - TradePreimageResult, Transaction, TransactionDetails, TransactionEnum, TransactionErr, - UnexpectedDerivationMethod, VerificationError, WithdrawError, WithdrawRequest}; + PrivKeyPolicyNotAllowed, RawTransactionFut, RpcTransportEventHandler, RpcTransportEventHandlerShared, + TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, Transaction, TransactionDetails, + TransactionEnum, TransactionErr, UnexpectedDerivationMethod, VerificationError, WithdrawError, + WithdrawRequest}; use crate::coin_balance::{EnableCoinScanPolicy, EnabledCoinBalanceParams, HDAddressBalanceScanner}; -use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDAddressId, HDWalletCoinOps, HDWalletOps, - InvalidBip44ChainError}; -use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletCoinStorage, HDWalletStorageError, HDWalletStorageResult}; +use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDPathAccountToAddressId, HDWalletCoinOps, HDWalletOps}; use crate::utxo::tx_cache::UtxoVerboseCacheShared; -use crate::{CoinAssocTypes, ToBytes}; +use crate::{ParseCoinAssocTypes, ToBytes}; pub mod tx_cache; @@ -134,13 +134,20 @@ const UTXO_DUST_AMOUNT: u64 = 1000; /// 11 > 0 const KMD_MTP_BLOCK_COUNT: NonZeroU64 = unsafe { NonZeroU64::new_unchecked(11u64) }; const DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT: f64 = 0.5; -const DEFAULT_GAP_LIMIT: u32 = 20; pub type GenerateTxResult = Result<(TransactionInputSigner, AdditionalTxData), MmError>; pub type HistoryUtxoTxMap = HashMap; pub type MatureUnspentMap = HashMap; pub type RecentlySpentOutPointsGuard<'a> = AsyncMutexGuard<'a, RecentlySpentOutPoints>; -pub type UtxoHDAddress = HDAddress; + +pub enum ScripthashNotification { + Triggered(String), + SubscribeToAddresses(HashSet
), + RefreshSubscriptions, +} + +pub type ScripthashNotificationSender = Option>; +type ScripthashNotificationHandler = Option>>>; #[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] @@ -175,7 +182,7 @@ impl Transaction for UtxoTx { } } - fn tx_hash(&self) -> BytesJson { self.hash().reversed().to_vec().into() } + fn tx_hash_as_bytes(&self) -> BytesJson { self.hash().reversed().to_vec().into() } } impl From for BalanceError { @@ -191,6 +198,10 @@ impl From for BalanceError { } } +impl From for BalanceError { + fn from(e: keys::Error) -> Self { BalanceError::Internal(e.to_string()) } +} + impl From for WithdrawError { fn from(e: UtxoRpcError) -> Self { match e { @@ -231,14 +242,6 @@ impl From for TxProviderError { } } -impl From for HDWalletStorageError { - fn from(e: StandardHDPathError) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) } -} - -impl From for HDWalletStorageError { - fn from(e: Bip32Error) -> Self { HDWalletStorageError::ErrorDeserializing(e.to_string()) } -} - #[async_trait] impl TxProvider for UtxoRpcClientEnum { async fn get_rpc_transaction(&self, tx_hash: &H256Json) -> Result> { @@ -297,21 +300,20 @@ pub struct CachedUnspentInfo { pub value: u64, } -impl From for CachedUnspentInfo { - fn from(unspent: UnspentInfo) -> CachedUnspentInfo { +impl CachedUnspentInfo { + fn from_unspent_info(unspent: &UnspentInfo) -> CachedUnspentInfo { CachedUnspentInfo { outpoint: unspent.outpoint, value: unspent.value, } } -} -impl From for UnspentInfo { - fn from(cached: CachedUnspentInfo) -> UnspentInfo { + fn to_unspent_info(&self, script: Script) -> UnspentInfo { UnspentInfo { - outpoint: cached.outpoint, - value: cached.value, + outpoint: self.outpoint, + value: self.value, height: None, + script, } } } @@ -338,22 +340,17 @@ impl RecentlySpentOutPoints { } pub fn add_spent(&mut self, inputs: Vec, spend_tx_hash: H256, outputs: Vec) { - let inputs: HashSet<_> = inputs.into_iter().map(From::from).collect(); + let inputs: HashSet<_> = inputs.iter().map(CachedUnspentInfo::from_unspent_info).collect(); let to_replace: HashSet<_> = outputs - .iter() + .into_iter() .enumerate() - .filter_map(|(index, output)| { - if output.script_pubkey == self.for_script_pubkey { - Some(CachedUnspentInfo { - outpoint: OutPoint { - hash: spend_tx_hash, - index: index as u32, - }, - value: output.value, - }) - } else { - None - } + .filter(|(_, output)| output.script_pubkey == self.for_script_pubkey) + .map(|(index, output)| CachedUnspentInfo { + outpoint: OutPoint { + hash: spend_tx_hash, + index: index as u32, + }, + value: output.value, }) .collect(); @@ -388,13 +385,14 @@ impl RecentlySpentOutPoints { pub fn replace_spent_outputs_with_cache(&self, mut outputs: HashSet) -> HashSet { let mut replacement_unspents = HashSet::new(); outputs.retain(|unspent| { - let outs = self.input_to_output_map.get(&unspent.clone().into()); + let outs = self + .input_to_output_map + .get(&CachedUnspentInfo::from_unspent_info(unspent)); + match outs { Some(outs) => { - for out in outs.iter() { - if !replacement_unspents.contains(out) { - replacement_unspents.insert(out.clone()); - } + for out in outs { + replacement_unspents.insert(out.clone()); } false }, @@ -404,7 +402,11 @@ impl RecentlySpentOutPoints { if replacement_unspents.is_empty() { return outputs; } - outputs.extend(replacement_unspents.into_iter().map(From::from)); + outputs.extend( + replacement_unspents + .iter() + .map(|cached| cached.to_unspent_info(self.for_script_pubkey.clone().into())), + ); self.replace_spent_outputs_with_cache(outputs) } } @@ -494,11 +496,8 @@ pub struct UtxoCoinConf { pub ticker: String, /// https://en.bitcoin.it/wiki/List_of_address_prefixes /// https://github.com/jl777/coins/blob/master/coins - pub pub_addr_prefix: u8, - pub p2sh_addr_prefix: u8, pub wif_prefix: u8, - pub pub_t_addr_prefix: u8, - pub p2sh_t_addr_prefix: u8, + pub address_prefixes: NetworkAddressPrefixes, pub sign_message_prefix: Option, // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Segwit_address_format pub bech32_hrp: Option, @@ -567,7 +566,7 @@ pub struct UtxoCoinConf { /// This derivation path consists of `purpose` and `coin_type` only /// where the full `BIP44` address has the following structure: /// `m/purpose'/coin_type'/account'/change/address_index`. - pub derivation_path: Option, + pub derivation_path: Option, /// The average time in seconds needed to mine a new block for this coin. pub avg_blocktime: Option, } @@ -588,7 +587,7 @@ pub struct UtxoCoinFields { pub rpc_client: UtxoRpcClientEnum, /// Either ECDSA key pair or a Hardware Wallet info. pub priv_key_policy: PrivKeyPolicy, - /// Either an Iguana address or an info about last derived account/address. + /// Either an Iguana address or a 'UtxoHDWallet' instance. pub derivation_method: DerivationMethod, pub history_sync_state: Mutex, /// The cache of verbose transactions. @@ -610,6 +609,11 @@ pub struct UtxoCoinFields { /// This abortable system is used to spawn coin's related futures that should be aborted on coin deactivation /// and on [`MmArc::stop`]. pub abortable_system: AbortableQueue, + pub(crate) ctx: MmWeak, + /// This is used for balance event streaming implementation for UTXOs. + /// If balance event streaming isn't enabled, this value will always be `None`; otherwise, + /// it will be used for receiving scripthash notifications to re-fetch balances. + scripthash_notification_handler: ScripthashNotificationHandler, } #[derive(Debug, Display)] @@ -631,12 +635,18 @@ pub enum UnsupportedAddr { HrpError { ticker: String, hrp: String }, #[display(fmt = "Segwit not activated in the config for {}", _0)] SegwitNotActivated(String), + #[display(fmt = "Internal error {}", _0)] + InternalError(String), } impl From for WithdrawError { fn from(e: UnsupportedAddr) -> Self { WithdrawError::InvalidAddress(e.to_string()) } } +impl From for UnsupportedAddr { + fn from(e: keys::Error) -> Self { UnsupportedAddr::InternalError(e.to_string()) } +} + #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum GetTxError { @@ -864,7 +874,7 @@ impl HDAddressBalanceScanner for UtxoAddressScanner { let is_used = match self { UtxoAddressScanner::Native { non_empty_addresses } => non_empty_addresses.contains(&address.to_string()), UtxoAddressScanner::Electrum(electrum_client) => { - let script = output_script(address, ScriptType::P2PKH); + let script = output_script(address)?; let script_hash = electrum_script_hash(&script); let electrum_history = electrum_client @@ -962,6 +972,9 @@ pub trait UtxoCommonOps: /// and if it failed inform user that he used a wrong format. fn address_from_str(&self, address: &str) -> MmResult; + /// For an address create corresponding utxo output script + fn script_for_address(&self, address: &Address) -> MmResult; + async fn get_current_mtp(&self) -> UtxoRpcResult; /// Check if the output is spendable (is not coinbase or it has enough confirmations). @@ -1007,11 +1020,6 @@ pub trait UtxoCommonOps: fn addr_format_for_standard_scripts(&self) -> UtxoAddressFormat; fn address_from_pubkey(&self, pubkey: &Public) -> Address; - - fn address_from_extended_pubkey(&self, extended_pubkey: &Secp256k1ExtendedPublicKey) -> Address { - let pubkey = Public::Compressed(H264::from(extended_pubkey.public_key().serialize())); - self.address_from_pubkey(&pubkey) - } } impl ToBytes for UtxoTx { @@ -1022,7 +1030,10 @@ impl ToBytes for Signature { fn to_bytes(&self) -> Vec { self.to_vec() } } -impl CoinAssocTypes for T { +#[async_trait] +impl ParseCoinAssocTypes for T { + type Address = Address; + type AddressParseError = MmError; type Pubkey = Public; type PubkeyParseError = MmError; type Tx = UtxoTx; @@ -1032,9 +1043,25 @@ impl CoinAssocTypes for T { type Sig = Signature; type SigParseError = MmError; + async fn my_addr(&self) -> Self::Address { + match &self.as_ref().derivation_method { + DerivationMethod::SingleAddress(addr) => addr.clone(), + // Todo: Expect should not fail but we need to handle it properly + DerivationMethod::HDWallet(hd_wallet) => hd_wallet + .get_enabled_address() + .await + .expect("Getting enabled address should not fail!") + .address(), + } + } + + fn parse_address(&self, address: &str) -> Result { + self.address_from_str(address) + } + #[inline] fn parse_pubkey(&self, pubkey: &[u8]) -> Result { - Ok(Public::from_slice(pubkey)?) + Public::from_slice(pubkey).map_err(MmError::from) } #[inline] @@ -1247,6 +1274,10 @@ impl From for GenerateTxError { fn from(e: NumConversError) -> Self { GenerateTxError::Internal(e.to_string()) } } +impl From for GenerateTxError { + fn from(e: keys::Error) -> Self { GenerateTxError::Internal(e.to_string()) } +} + pub enum RequestTxHistoryResult { Ok(Vec<(H256Json, u64)>), Retry { error: String }, @@ -1414,7 +1445,7 @@ pub struct UtxoActivationParams { /// This determines which Address of the HD account to be used for swaps for this UTXO coin. /// If not specified, the first non-change address for the first account is used. #[serde(default)] - pub path_to_address: StandardHDCoinAddress, + pub path_to_address: HDPathAccountToAddressId, } #[derive(Debug, Display)] @@ -1469,7 +1500,7 @@ impl UtxoActivationParams { let priv_key_policy = json::from_value::>(req["priv_key_policy"].clone()) .map_to_mm(UtxoFromLegacyReqErr::InvalidPrivKeyPolicy)? .unwrap_or(PrivKeyActivationPolicy::ContextPrivKey); - let path_to_address = json::from_value::>(req["path_to_address"].clone()) + let path_to_address = json::from_value::>(req["path_to_address"].clone()) .map_to_mm(UtxoFromLegacyReqErr::InvalidAddressIndex)? .unwrap_or_default(); @@ -1518,113 +1549,6 @@ impl Default for ElectrumBuilderArgs { } } -#[derive(Debug)] -pub struct UtxoHDWallet { - pub hd_wallet_rmd160: H160, - pub hd_wallet_storage: HDWalletCoinStorage, - pub address_format: UtxoAddressFormat, - /// Derivation path of the coin. - /// This derivation path consists of `purpose` and `coin_type` only - /// where the full `BIP44` address has the following structure: - /// `m/purpose'/coin_type'/account'/change/address_index`. - pub derivation_path: StandardHDPathToCoin, - /// User accounts. - pub accounts: HDAccountsMutex, - // The max number of empty addresses in a row. - // If transactions were sent to an address outside the `gap_limit`, they will not be identified. - pub gap_limit: u32, -} - -impl HDWalletOps for UtxoHDWallet { - type HDAccount = UtxoHDAccount; - - fn coin_type(&self) -> u32 { self.derivation_path.coin_type() } - - fn gap_limit(&self) -> u32 { self.gap_limit } - - fn get_accounts_mutex(&self) -> &HDAccountsMutex { &self.accounts } -} - -#[derive(Clone, Debug, Default)] -pub struct HDAddressesCache { - cache: Arc>>, -} - -impl HDAddressesCache { - pub fn with_capacity(capacity: usize) -> HDAddressesCache { - HDAddressesCache { - cache: Arc::new(AsyncMutex::new(HashMap::with_capacity(capacity))), - } - } - - pub async fn lock(&self) -> AsyncMutexGuard<'_, HashMap> { self.cache.lock().await } -} - -#[derive(Clone, Debug)] -pub struct UtxoHDAccount { - pub account_id: u32, - /// [Extended public key](https://learnmeabitcoin.com/technical/extended-keys) that corresponds to the derivation path: - /// `m/purpose'/coin_type'/account'`. - pub extended_pubkey: Secp256k1ExtendedPublicKey, - /// [`UtxoHDWallet::derivation_path`] derived by [`UtxoHDAccount::account_id`]. - pub account_derivation_path: StandardHDPathToAccount, - /// The number of addresses that we know have been used by the user. - /// This is used in order not to check the transaction history for each address, - /// but to request the balance of addresses whose index is less than `address_number`. - pub external_addresses_number: u32, - pub internal_addresses_number: u32, - /// The cache of derived addresses. - /// This is used at [`HDWalletCoinOps::derive_address`]. - pub derived_addresses: HDAddressesCache, -} - -impl HDAccountOps for UtxoHDAccount { - fn known_addresses_number(&self, chain: Bip44Chain) -> MmResult { - match chain { - Bip44Chain::External => Ok(self.external_addresses_number), - Bip44Chain::Internal => Ok(self.internal_addresses_number), - } - } - - fn account_derivation_path(&self) -> DerivationPath { self.account_derivation_path.to_derivation_path() } - - fn account_id(&self) -> u32 { self.account_id } -} - -impl UtxoHDAccount { - pub fn try_from_storage_item( - wallet_der_path: &StandardHDPathToCoin, - account_info: &HDAccountStorageItem, - ) -> HDWalletStorageResult { - const ACCOUNT_CHILD_HARDENED: bool = true; - - let account_child = ChildNumber::new(account_info.account_id, ACCOUNT_CHILD_HARDENED)?; - let account_derivation_path = wallet_der_path - .derive(account_child) - .map_to_mm(StandardHDPathError::from)?; - let extended_pubkey = Secp256k1ExtendedPublicKey::from_str(&account_info.account_xpub)?; - let capacity = - account_info.external_addresses_number + account_info.internal_addresses_number + DEFAULT_GAP_LIMIT; - Ok(UtxoHDAccount { - account_id: account_info.account_id, - extended_pubkey, - account_derivation_path, - external_addresses_number: account_info.external_addresses_number, - internal_addresses_number: account_info.internal_addresses_number, - derived_addresses: HDAddressesCache::with_capacity(capacity as usize), - }) - } - - pub fn to_storage_item(&self) -> HDAccountStorageItem { - HDAccountStorageItem { - account_id: self.account_id, - account_xpub: self.extended_pubkey.to_string(bip32::Prefix::XPUB), - external_addresses_number: self.external_addresses_number, - internal_addresses_number: self.internal_addresses_number, - } - } -} - /// Function calculating KMD interest /// https://github.com/KomodoPlatform/komodo/blob/master/src/komodo_interest.h fn kmd_interest( @@ -1757,9 +1681,9 @@ pub async fn kmd_rewards_info(coin: &T) -> Result( where T: UtxoCommonOps + GetUtxoListOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); - let (unspents, recently_sent_txs) = try_tx_s!(coin.get_unspent_ordered_list(my_address).await); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); + let (unspents, recently_sent_txs) = try_tx_s!(coin.get_unspent_ordered_list(&my_address).await); generate_and_send_tx(&coin, unspents, None, FeePolicy::SendExact, recently_sent_txs, outputs).await } @@ -1841,10 +1765,11 @@ async fn generate_and_send_tx( where T: AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.activated_key_or_err()); let mut builder = UtxoTxBuilder::new(coin) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy); @@ -1860,19 +1785,18 @@ where outpoint: input.previous_output, value: input.amount, height: None, + script: input.prev_script.clone(), }) .collect(); - let signature_version = match &my_address.addr_format { + let signature_version = match my_address.addr_format() { UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, _ => coin.as_ref().conf.signature_version, }; - let prev_script = Builder::build_p2pkh(&my_address.hash); let signed = try_tx_s!(sign_tx( unsigned, key_pair, - prev_script, signature_version, coin.as_ref().conf.fork_id )); @@ -1884,18 +1808,19 @@ where Ok(signed) } -pub fn output_script(address: &Address, script_type: ScriptType) -> Script { - match address.addr_format { - UtxoAddressFormat::Segwit => Builder::build_witness_script(&address.hash), - _ => match script_type { - ScriptType::P2PKH => Builder::build_p2pkh(&address.hash), - ScriptType::P2SH => Builder::build_p2sh(&address.hash), - ScriptType::P2WPKH => Builder::build_witness_script(&address.hash), - ScriptType::P2WSH => Builder::build_witness_script(&address.hash), - }, +/// Builds transaction output script for an Address struct +pub fn output_script(address: &Address) -> Result { + match address.script_type() { + AddressScriptType::P2PKH => Ok(Builder::build_p2pkh(address.hash())), + AddressScriptType::P2SH => Ok(Builder::build_p2sh(address.hash())), + AddressScriptType::P2WPKH => Builder::build_p2wpkh(address.hash()), + AddressScriptType::P2WSH => Builder::build_p2wsh(address.hash()), } } +/// Builds transaction output script for a legacy P2PK address +pub fn output_script_p2pk(pubkey: &Public) -> Script { Builder::build_p2pk(pubkey) } + pub fn address_by_conf_and_pubkey_str( coin: &str, conf: &Json, @@ -1915,21 +1840,21 @@ pub fn address_by_conf_and_pubkey_str( priv_key_policy: PrivKeyActivationPolicy::ContextPrivKey, check_utxo_maturity: None, // This will not be used since the pubkey from orderbook/etc.. will be used to generate the address - path_to_address: StandardHDCoinAddress::default(), + path_to_address: HDPathAccountToAddressId::default(), }; let conf_builder = UtxoConfBuilder::new(conf, ¶ms, coin); let utxo_conf = try_s!(conf_builder.build()); let pubkey_bytes = try_s!(hex::decode(pubkey)); - let hash = dhash160(&pubkey_bytes); - - let address = Address { - prefix: utxo_conf.pub_addr_prefix, - t_addr_prefix: utxo_conf.pub_t_addr_prefix, - hash: hash.into(), - checksum_type: utxo_conf.checksum_type, - hrp: utxo_conf.bech32_hrp, + let pubkey = try_s!(Public::from_slice(&pubkey_bytes)); + + let address = AddressBuilder::new( addr_format, - }; + utxo_conf.checksum_type, + utxo_conf.address_prefixes, + utxo_conf.bech32_hrp, + ) + .as_pkh_from_pk(pubkey) + .build()?; address.display_address() } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 072719eeee..f2cf7b4251 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1,25 +1,31 @@ use super::*; -use crate::coin_errors::MyAddressError; +use crate::coin_balance::{EnableCoinBalanceError, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; +use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDExtractPubkeyError, HDXPubExtractor, + TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, TxHistoryStorage}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::rpc_clients::UtxoRpcFut; use crate::utxo::slp::{parse_slp_script, SlpGenesisParams, SlpTokenInfo, SlpTransaction, SlpUnspent}; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; -use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; +use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, utxo_prepare_addresses_for_balance_stream_if_enabled}; +use crate::utxo::utxo_hd_wallet::{UtxoHDAccount, UtxoHDAddress}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; -use crate::{BlockHeightAndTime, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinProtocol, - CoinWithDerivationMethod, ConfirmPaymentInput, DexFee, IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, - PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, RefundError, RefundPaymentArgs, +use crate::{coin_balance, BlockHeightAndTime, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinProtocol, + CoinWithDerivationMethod, CoinWithPrivKeyPolicy, ConfirmPaymentInput, DexFee, GetWithdrawSenderAddress, + IguanaBalanceOps, IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, + PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, + RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, - TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, - ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, - ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; + SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, + TradePreimageValue, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, + WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; use common::log::warn; use derive_more::Display; @@ -156,11 +162,7 @@ impl BchCoin { pub fn slp_address(&self, address: &Address) -> Result { let conf = &self.as_ref().conf; - address.to_cashaddress( - &self.slp_prefix().to_string(), - conf.pub_addr_prefix, - conf.p2sh_addr_prefix, - ) + address.to_cashaddress(&self.slp_prefix().to_string(), &conf.address_prefixes) } pub fn bchd_urls(&self) -> &[String] { &self.bchd_urls } @@ -307,8 +309,9 @@ impl BchCoin { .as_ref() .derivation_method .single_addr_or_err() + .await .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; - let (mut bch_unspents, recently_spent) = self.bch_unspents_for_spend(my_address).await?; + let (mut bch_unspents, recently_spent) = self.bch_unspents_for_spend(&my_address).await?; let (mut slp_unspents, standard_utxos) = ( bch_unspents.slp.remove(token_id).unwrap_or_default(), bch_unspents.standard, @@ -326,8 +329,9 @@ impl BchCoin { .as_ref() .derivation_method .single_addr_or_err() + .await .mm_err(|e| UtxoRpcError::Internal(e.to_string()))?; - let mut bch_unspents = self.bch_unspents_for_display(my_address).await?; + let mut bch_unspents = self.bch_unspents_for_display(&my_address).await?; let (mut slp_unspents, standard_utxos) = ( bch_unspents.slp.remove(token_id).unwrap_or_default(), bch_unspents.standard, @@ -345,13 +349,10 @@ impl BchCoin { self.slp_tokens_infos.lock().unwrap() } - pub fn get_my_slp_address(&self) -> Result { - let my_address = try_s!(self.as_ref().derivation_method.single_addr_or_err()); - let slp_address = my_address.to_cashaddress( - &self.slp_prefix().to_string(), - self.as_ref().conf.pub_addr_prefix, - self.as_ref().conf.p2sh_addr_prefix, - )?; + pub async fn get_my_slp_address(&self) -> Result { + let my_address = try_s!(self.as_ref().derivation_method.single_addr_or_err().await); + let slp_address = + my_address.to_cashaddress(&self.slp_prefix().to_string(), &self.as_ref().conf.address_prefixes)?; Ok(slp_address) } @@ -736,6 +737,31 @@ impl GetUtxoListOps for BchCoin { } } +#[async_trait] +#[cfg_attr(test, mockable)] +impl GetUtxoMapOps for BchCoin { + async fn get_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_unspent_ordered_map(self, addresses).await + } + + async fn get_all_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(UnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_all_unspent_ordered_map(self, addresses).await + } + + async fn get_mature_unspent_ordered_map( + &self, + addresses: Vec
, + ) -> UtxoRpcResult<(MatureUnspentMap, RecentlySpentOutPointsGuard<'_>)> { + utxo_common::get_mature_unspent_ordered_map(self, addresses).await + } +} + // if mockable is placed before async_trait there is `munmap_chunk(): invalid pointer` error on async fn mocking attempt #[async_trait] #[cfg_attr(test, mockable)] @@ -758,8 +784,13 @@ impl UtxoCommonOps for BchCoin { utxo_common::checked_address_from_str(self, address) } + fn script_for_address(&self, address: &Address) -> MmResult { + utxo_common::output_script_checked(self.as_ref(), address) + } + async fn get_current_mtp(&self) -> UtxoRpcResult { - utxo_common::get_current_mtp(&self.utxo_arc, CoinVariant::Standard).await + // BCH uses the same coin variant as BTC for block header deserialization + utxo_common::get_current_mtp(&self.utxo_arc, CoinVariant::BTC).await } fn is_unspent_mature(&self, output: &RpcTransaction) -> bool { @@ -828,8 +859,7 @@ impl UtxoCommonOps for BchCoin { let addr_format = self.addr_format().clone(); utxo_common::address_from_pubkey( pubkey, - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + conf.address_prefixes.clone(), conf.checksum_type, conf.bech32_hrp.clone(), addr_format, @@ -840,7 +870,7 @@ impl UtxoCommonOps for BchCoin { #[async_trait] impl SwapOps for BchCoin { #[inline] - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) } @@ -855,13 +885,19 @@ impl SwapOps for BchCoin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args).await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args).await } #[inline] @@ -891,13 +927,13 @@ impl SwapOps for BchCoin { } #[inline] - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - utxo_common::validate_maker_payment(self, input) + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + utxo_common::validate_maker_payment(self, input).await } #[inline] - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - utxo_common::validate_taker_payment(self, input) + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + utxo_common::validate_taker_payment(self, input).await } #[inline] @@ -1137,12 +1173,13 @@ impl WatcherOps for BchCoin { } } +#[async_trait] impl MarketCoinOps for BchCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } fn my_address(&self) -> MmResult { utxo_common::my_address(self) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) } @@ -1162,8 +1199,8 @@ impl MarketCoinOps for BchCoin { fn my_balance(&self) -> BalanceFut { let coin = self.clone(); let fut = async move { - let my_address = coin.as_ref().derivation_method.single_addr_or_err()?; - let bch_unspents = coin.bch_unspents_for_display(my_address).await?; + let my_address = coin.as_ref().derivation_method.single_addr_or_err().await?; + let bch_unspents = coin.bch_unspents_for_display(&my_address).await?; Ok(bch_unspents.platform_balance(coin.as_ref().decimals)) }; Box::new(fut.boxed().compat()) @@ -1183,13 +1220,18 @@ impl MarketCoinOps for BchCoin { utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) } + #[inline(always)] + async fn sign_raw_tx(&self, args: &SignRawTransactionRequest) -> RawTransactionResult { + utxo_common::sign_raw_tx(self, args).await + } + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { utxo_common::wait_for_confirmations(&self.utxo_arc, input) } fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { utxo_common::wait_for_output_spend( - &self.utxo_arc, + self.clone(), args.tx_bytes, utxo_common::DEFAULT_SWAP_VOUT, args.from_block, @@ -1211,6 +1253,8 @@ impl MarketCoinOps for BchCoin { fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -1258,6 +1302,7 @@ impl MmCoin for BchCoin { &self, value: TradePreimageValue, stage: FeeApproxStage, + _include_refund_fee: bool, // refund fee is taken from swap output ) -> TradePreimageResult { utxo_common::get_sender_trade_fee(self, value, stage).await } @@ -1315,15 +1360,129 @@ impl MmCoin for BchCoin { } } -impl CoinWithDerivationMethod for BchCoin { +#[async_trait] +impl GetWithdrawSenderAddress for BchCoin { type Address = Address; - type HDWallet = UtxoHDWallet; + type Pubkey = Public; + + async fn get_withdraw_sender_address( + &self, + req: &WithdrawRequest, + ) -> MmResult, WithdrawError> { + utxo_common::get_withdraw_from_address(self, req).await + } +} + +impl CoinWithPrivKeyPolicy for BchCoin { + type KeyPair = KeyPair; - fn derivation_method(&self) -> &DerivationMethod { + fn priv_key_policy(&self) -> &PrivKeyPolicy { &self.utxo_arc.priv_key_policy } +} + +impl CoinWithDerivationMethod for BchCoin { + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet> { utxo_common::derivation_method(self.as_ref()) } } +#[async_trait] +impl IguanaBalanceOps for BchCoin { + type BalanceObject = CoinBalance; + + async fn iguana_balances(&self) -> BalanceResult { self.my_balance().compat().await } +} + +#[async_trait] +impl ExtractExtendedPubkey for BchCoin { + type ExtendedPublicKey = Secp256k1ExtendedPublicKey; + + async fn extract_extended_pubkey( + &self, + xpub_extractor: Option, + derivation_path: DerivationPath, + ) -> MmResult + where + XPubExtractor: HDXPubExtractor + Send, + { + crate::extract_extended_pubkey_impl(self, xpub_extractor, derivation_path).await + } +} + +#[async_trait] +impl HDWalletCoinOps for BchCoin { + type HDWallet = UtxoHDWallet; + + fn address_from_extended_pubkey( + &self, + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, + ) -> UtxoHDAddress { + utxo_common::address_from_extended_pubkey(self, extended_pubkey, derivation_path) + } + + fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } +} + +impl HDCoinWithdrawOps for BchCoin {} + +#[async_trait] +impl HDWalletBalanceOps for BchCoin { + type HDAddressScanner = UtxoAddressScanner; + type BalanceObject = CoinBalance; + + async fn produce_hd_address_scanner(&self) -> BalanceResult { + utxo_common::produce_hd_address_scanner(self).await + } + + async fn enable_hd_wallet( + &self, + hd_wallet: &Self::HDWallet, + xpub_extractor: Option, + params: EnabledCoinBalanceParams, + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> + where + XPubExtractor: HDXPubExtractor + Send, + { + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params, path_to_address).await + } + + async fn scan_for_new_addresses( + &self, + hd_wallet: &Self::HDWallet, + hd_account: &mut UtxoHDAccount, + address_scanner: &Self::HDAddressScanner, + gap_limit: u32, + ) -> BalanceResult>> { + utxo_common::scan_for_new_addresses(self, hd_wallet, hd_account, address_scanner, gap_limit).await + } + + async fn all_known_addresses_balances( + &self, + hd_account: &UtxoHDAccount, + ) -> BalanceResult>> { + utxo_common::all_known_addresses_balances(self, hd_account).await + } + + async fn known_address_balance(&self, address: &Address) -> BalanceResult { + utxo_common::address_balance(self, address).await + } + + async fn known_addresses_balances( + &self, + addresses: Vec
, + ) -> BalanceResult> { + utxo_common::addresses_balances(self, addresses).await + } + + async fn prepare_addresses_for_balance_stream_if_enabled( + &self, + addresses: HashSet, + ) -> MmResult<(), String> { + utxo_prepare_addresses_for_balance_stream_if_enabled(self, addresses).await + } +} + #[async_trait] impl CoinWithTxHistoryV2 for BchCoin { fn history_wallet_id(&self) -> WalletId { WalletId::new(self.ticker().to_owned()) } @@ -1349,8 +1508,8 @@ impl CoinWithTxHistoryV2 for BchCoin { #[async_trait] impl UtxoTxHistoryOps for BchCoin { async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { - let my_address = self.as_ref().derivation_method.single_addr_or_err()?; - Ok(std::iter::once(my_address.clone()).collect()) + let addresses = self.all_addresses().await?; + Ok(addresses) } async fn tx_details_by_hash( diff --git a/mm2src/coins/utxo/bchd_grpc.rs b/mm2src/coins/utxo/bchd_grpc.rs index 6017f3b9c0..da240508c6 100644 --- a/mm2src/coins/utxo/bchd_grpc.rs +++ b/mm2src/coins/utxo/bchd_grpc.rs @@ -260,6 +260,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 1000, }, @@ -271,6 +272,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, @@ -294,6 +296,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 1000, }, @@ -305,6 +308,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, @@ -316,6 +320,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, @@ -341,6 +346,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 999, }; @@ -353,6 +359,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }]; @@ -386,6 +393,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 1000, }, @@ -397,6 +405,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, diff --git a/mm2src/coins/utxo/pb.rs b/mm2src/coins/utxo/pb.rs index 61fb9a7e42..3ae572bc23 100644 --- a/mm2src/coins/utxo/pb.rs +++ b/mm2src/coins/utxo/pb.rs @@ -1,92 +1,108 @@ -// RPC MESSAGES - +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetMempoolInfoRequest { -} +pub struct GetMempoolInfoRequest {} +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetMempoolInfoResponse { /// The count of transactions in the mempool - #[prost(uint32, tag="1")] + #[prost(uint32, tag = "1")] pub size: u32, /// The size in bytes of all transactions in the mempool - #[prost(uint32, tag="2")] + #[prost(uint32, tag = "2")] pub bytes: u32, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetMempoolRequest { /// When `full_transactions` is true, full transaction data is provided /// instead of just transaction hashes. Default is false. - #[prost(bool, tag="1")] + #[prost(bool, tag = "1")] pub full_transactions: bool, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetMempoolResponse { /// List of unconfirmed transactions. - #[prost(message, repeated, tag="1")] - pub transaction_data: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "1")] + pub transaction_data: ::prost::alloc::vec::Vec< + get_mempool_response::TransactionData, + >, } /// Nested message and enum types in `GetMempoolResponse`. pub mod get_mempool_response { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TransactionData { /// Either one of the two following is provided, depending on the request. - #[prost(oneof="transaction_data::TxidsOrTxs", tags="1, 2")] + #[prost(oneof = "transaction_data::TxidsOrTxs", tags = "1, 2")] pub txids_or_txs: ::core::option::Option, } /// Nested message and enum types in `TransactionData`. pub mod transaction_data { /// Either one of the two following is provided, depending on the request. + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum TxidsOrTxs { /// The transaction hash, little-endian. - #[prost(bytes, tag="1")] + #[prost(bytes, tag = "1")] TransactionHash(::prost::alloc::vec::Vec), /// The transaction data. - #[prost(message, tag="2")] + #[prost(message, tag = "2")] Transaction(super::super::Transaction), } } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetBlockchainInfoRequest { -} +pub struct GetBlockchainInfoRequest {} +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockchainInfoResponse { /// Which network the node is operating on. - #[prost(enumeration="get_blockchain_info_response::BitcoinNet", tag="1")] + #[prost(enumeration = "get_blockchain_info_response::BitcoinNet", tag = "1")] pub bitcoin_net: i32, /// The current number of blocks on the longest chain. - #[prost(int32, tag="2")] + #[prost(int32, tag = "2")] pub best_height: i32, /// The hash of the best (tip) block in the most-work fully-validated chain, little-endian. - #[prost(bytes="vec", tag="3")] + #[prost(bytes = "vec", tag = "3")] pub best_block_hash: ::prost::alloc::vec::Vec, /// Threshold for adding new blocks. - #[prost(double, tag="4")] + #[prost(double, tag = "4")] pub difficulty: f64, /// Median time of the last 11 blocks. - #[prost(int64, tag="5")] + #[prost(int64, tag = "5")] pub median_time: i64, /// When `tx_index` is true, the node has full transaction index enabled. - #[prost(bool, tag="6")] + #[prost(bool, tag = "6")] pub tx_index: bool, /// When `addr_index` is true, the node has address index enabled and may /// be used with call related by address. - #[prost(bool, tag="7")] + #[prost(bool, tag = "7")] pub addr_index: bool, /// When `slp_index` is true, the node has the slp index enabled and may /// be used with slp related rpc methods and also causes slp metadata to be added /// in some of the existing rpc methods. - #[prost(bool, tag="8")] + #[prost(bool, tag = "8")] pub slp_index: bool, /// When `slp_graphsearch` is true, the node is able to handle calls to slp graph search - #[prost(bool, tag="9")] + #[prost(bool, tag = "9")] pub slp_graphsearch: bool, } /// Nested message and enum types in `GetBlockchainInfoResponse`. pub mod get_blockchain_info_response { /// Bitcoin network types - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] #[repr(i32)] pub enum BitcoinNet { /// Live public network with monetary value. @@ -100,364 +116,425 @@ pub mod get_blockchain_info_response { /// where a specified list of nodes is used, rather than node discovery. Simnet = 3, } + impl BitcoinNet { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + BitcoinNet::Mainnet => "MAINNET", + BitcoinNet::Regtest => "REGTEST", + BitcoinNet::Testnet3 => "TESTNET3", + BitcoinNet::Simnet => "SIMNET", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "MAINNET" => Some(Self::Mainnet), + "REGTEST" => Some(Self::Regtest), + "TESTNET3" => Some(Self::Testnet3), + "SIMNET" => Some(Self::Simnet), + _ => None, + } + } + } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockInfoRequest { - #[prost(oneof="get_block_info_request::HashOrHeight", tags="1, 2")] + #[prost(oneof = "get_block_info_request::HashOrHeight", tags = "1, 2")] pub hash_or_height: ::core::option::Option, } /// Nested message and enum types in `GetBlockInfoRequest`. pub mod get_block_info_request { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum HashOrHeight { /// The block hash as a byte array or base64 encoded string, little-endian. - #[prost(bytes, tag="1")] + #[prost(bytes, tag = "1")] Hash(::prost::alloc::vec::Vec), /// The block number. - #[prost(int32, tag="2")] + #[prost(int32, tag = "2")] Height(i32), } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockInfoResponse { /// Marshaled block header data, as well as metadata. - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub info: ::core::option::Option, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockRequest { /// When `full_transactions` is true, full transactions are returned /// instead of just hashes. Default is false. - #[prost(bool, tag="3")] + #[prost(bool, tag = "3")] pub full_transactions: bool, - #[prost(oneof="get_block_request::HashOrHeight", tags="1, 2")] + #[prost(oneof = "get_block_request::HashOrHeight", tags = "1, 2")] pub hash_or_height: ::core::option::Option, } /// Nested message and enum types in `GetBlockRequest`. pub mod get_block_request { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum HashOrHeight { /// The block hash as a byte array or base64 encoded string, little-endian. - #[prost(bytes, tag="1")] + #[prost(bytes, tag = "1")] Hash(::prost::alloc::vec::Vec), /// The block number. - #[prost(int32, tag="2")] + #[prost(int32, tag = "2")] Height(i32), } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockResponse { /// A marshaled block. - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub block: ::core::option::Option, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetRawBlockRequest { - #[prost(oneof="get_raw_block_request::HashOrHeight", tags="1, 2")] + #[prost(oneof = "get_raw_block_request::HashOrHeight", tags = "1, 2")] pub hash_or_height: ::core::option::Option, } /// Nested message and enum types in `GetRawBlockRequest`. pub mod get_raw_block_request { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum HashOrHeight { /// The block hash as a byte array or base64 encoded string, little-endian. - #[prost(bytes, tag="1")] + #[prost(bytes, tag = "1")] Hash(::prost::alloc::vec::Vec), /// The block number. - #[prost(int32, tag="2")] + #[prost(int32, tag = "2")] Height(i32), } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetRawBlockResponse { /// Raw block data (with header) serialized according the the bitcoin block protocol. - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub block: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockFilterRequest { - #[prost(oneof="get_block_filter_request::HashOrHeight", tags="1, 2")] + #[prost(oneof = "get_block_filter_request::HashOrHeight", tags = "1, 2")] pub hash_or_height: ::core::option::Option, } /// Nested message and enum types in `GetBlockFilterRequest`. pub mod get_block_filter_request { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum HashOrHeight { /// The block hash as a byte array or base64 encoded string, little-endian. - #[prost(bytes, tag="1")] + #[prost(bytes, tag = "1")] Hash(::prost::alloc::vec::Vec), /// The block number. - #[prost(int32, tag="2")] + #[prost(int32, tag = "2")] Height(i32), } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetBlockFilterResponse { /// A compact filter matching input outpoints and public key scripts contained /// in a block (encoded according to BIP158). - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub filter: ::prost::alloc::vec::Vec, } /// Request headers using a list of known block hashes. +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetHeadersRequest { /// A list of block hashes known to the client (most recent first) which /// is exponentially sparser toward the genesis block (0), little-endian. /// Common practice is to include all of the last 10 blocks, and then /// 9 blocks for each order of ten thereafter. - #[prost(bytes="vec", repeated, tag="1")] + #[prost(bytes = "vec", repeated, tag = "1")] pub block_locator_hashes: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, /// hash of the latest desired block header, little-endian; only blocks /// occurring before the stop will be returned. - #[prost(bytes="vec", tag="2")] + #[prost(bytes = "vec", tag = "2")] pub stop_hash: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetHeadersResponse { /// List of block headers. - #[prost(message, repeated, tag="1")] + #[prost(message, repeated, tag = "1")] pub headers: ::prost::alloc::vec::Vec, } /// Get a transaction from a transaction hash. +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTransactionRequest { /// A transaction hash, little-endian. - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, - #[prost(bool, tag="2")] + #[prost(bool, tag = "2")] pub include_token_metadata: bool, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetTransactionResponse { /// A marshaled transaction. - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub transaction: ::core::option::Option, - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag = "2")] pub token_metadata: ::core::option::Option, } /// Get an encoded transaction from a transaction hash. +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetRawTransactionRequest { /// A transaction hash, little-endian. - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetRawTransactionResponse { /// Raw transaction in bytes. - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub transaction: ::prost::alloc::vec::Vec, } /// Get marshaled transactions related to a specific address. /// /// RECOMMENDED: /// Parameters have been provided to query without creating -/// performance issues on the node or client. +/// performance issues on the node or client. /// /// - The number of transactions to skip and fetch allow for iterating -/// over a large set of transactions, if necessary. +/// over a large set of transactions, if necessary. /// /// - A starting block parameter (either `hash` or `height`) -/// may then be used to filter results to those occurring -/// after a certain time. +/// may then be used to filter results to those occurring +/// after a certain time. /// /// This approach will reduce network traffic and response processing -/// for the client, as well as reduce workload on the node. +/// for the client, as well as reduce workload on the node. +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAddressTransactionsRequest { /// The address to query transactions, in lowercase cashaddr format. /// The network prefix is optional (i.e. "cashaddress:"). - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub address: ::prost::alloc::string::String, /// The number of confirmed transactions to skip, starting with the oldest first. /// Does not affect results of unconfirmed transactions. - #[prost(uint32, tag="2")] + #[prost(uint32, tag = "2")] pub nb_skip: u32, /// Specify the number of transactions to fetch. - #[prost(uint32, tag="3")] + #[prost(uint32, tag = "3")] pub nb_fetch: u32, - #[prost(oneof="get_address_transactions_request::StartBlock", tags="4, 5")] - pub start_block: ::core::option::Option, + #[prost(oneof = "get_address_transactions_request::StartBlock", tags = "4, 5")] + pub start_block: ::core::option::Option< + get_address_transactions_request::StartBlock, + >, } /// Nested message and enum types in `GetAddressTransactionsRequest`. pub mod get_address_transactions_request { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum StartBlock { /// Recommended. Only get transactions after (or within) a /// starting block identified by hash, little-endian. - #[prost(bytes, tag="4")] + #[prost(bytes, tag = "4")] Hash(::prost::alloc::vec::Vec), /// Recommended. Only get transactions after (or within) a /// starting block identified by block number. - #[prost(int32, tag="5")] + #[prost(int32, tag = "5")] Height(i32), } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAddressTransactionsResponse { /// Transactions that have been included in a block. - #[prost(message, repeated, tag="1")] + #[prost(message, repeated, tag = "1")] pub confirmed_transactions: ::prost::alloc::vec::Vec, /// Transactions in mempool which have not been included in a block. - #[prost(message, repeated, tag="2")] + #[prost(message, repeated, tag = "2")] pub unconfirmed_transactions: ::prost::alloc::vec::Vec, } /// Get encoded transactions related to a specific address. /// /// RECOMMENDED: /// Parameters have been provided to query without creating -/// performance issues on the node or client. +/// performance issues on the node or client. /// /// - The number of transactions to skip and fetch allow for iterating -/// over a large set of transactions, if necessary. +/// over a large set of transactions, if necessary. /// /// - A starting block parameter (either `hash` or `height`) -/// may then be used to filter results to those occurring -/// after a certain time. +/// may then be used to filter results to those occurring +/// after a certain time. /// /// This approach will reduce network traffic and response processing -/// for the client, as well as reduce workload on the node. +/// for the client, as well as reduce workload on the node. +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetRawAddressTransactionsRequest { /// The address to query transactions, in lowercase cashaddr format. /// The network prefix is optional (i.e. "cashaddress:"). - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub address: ::prost::alloc::string::String, /// The number of confirmed transactions to skip, starting with the oldest first. /// Does not affect results of unconfirmed transactions. - #[prost(uint32, tag="2")] + #[prost(uint32, tag = "2")] pub nb_skip: u32, /// Specify the number of transactions to fetch. - #[prost(uint32, tag="3")] + #[prost(uint32, tag = "3")] pub nb_fetch: u32, - #[prost(oneof="get_raw_address_transactions_request::StartBlock", tags="4, 5")] - pub start_block: ::core::option::Option, + #[prost(oneof = "get_raw_address_transactions_request::StartBlock", tags = "4, 5")] + pub start_block: ::core::option::Option< + get_raw_address_transactions_request::StartBlock, + >, } /// Nested message and enum types in `GetRawAddressTransactionsRequest`. pub mod get_raw_address_transactions_request { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum StartBlock { /// Recommended. Only return transactions after some starting block /// identified by hash, little-endian. - #[prost(bytes, tag="4")] + #[prost(bytes, tag = "4")] Hash(::prost::alloc::vec::Vec), /// Recommended. Only return transactions after some starting block /// identified by block number. - #[prost(int32, tag="5")] + #[prost(int32, tag = "5")] Height(i32), } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetRawAddressTransactionsResponse { /// Transactions that have been included in a block. - #[prost(bytes="vec", repeated, tag="1")] + #[prost(bytes = "vec", repeated, tag = "1")] pub confirmed_transactions: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, /// Transactions in mempool which have not been included in a block. - #[prost(bytes="vec", repeated, tag="2")] + #[prost(bytes = "vec", repeated, tag = "2")] pub unconfirmed_transactions: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAddressUnspentOutputsRequest { /// The address to query transactions, in lowercase cashaddr format. /// The network identifier is optional (i.e. "cashaddress:"). - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub address: ::prost::alloc::string::String, /// When `include_mempool` is true, unconfirmed transactions from mempool /// are returned. Default is false. - #[prost(bool, tag="2")] + #[prost(bool, tag = "2")] pub include_mempool: bool, - #[prost(bool, tag="3")] + #[prost(bool, tag = "3")] pub include_token_metadata: bool, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetAddressUnspentOutputsResponse { /// List of unspent outputs. - #[prost(message, repeated, tag="1")] + #[prost(message, repeated, tag = "1")] pub outputs: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="2")] + #[prost(message, repeated, tag = "2")] pub token_metadata: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetUnspentOutputRequest { /// The hash of the transaction, little-endian. - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, /// The number of the output, starting from zero. - #[prost(uint32, tag="2")] + #[prost(uint32, tag = "2")] pub index: u32, /// When include_mempool is true, unconfirmed transactions from mempool /// are returned. Default is false. - #[prost(bool, tag="3")] + #[prost(bool, tag = "3")] pub include_mempool: bool, - #[prost(bool, tag="4")] + #[prost(bool, tag = "4")] pub include_token_metadata: bool, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetUnspentOutputResponse { /// A reference to the related input. - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub outpoint: ::core::option::Option, /// Locking script dictating how funds can be spent in the future - #[prost(bytes="vec", tag="2")] + #[prost(bytes = "vec", tag = "2")] pub pubkey_script: ::prost::alloc::vec::Vec, /// Amount in satoshi. - #[prost(int64, tag="3")] + #[prost(int64, tag = "3")] pub value: i64, /// When is_coinbase is true, the transaction was the first in a block, /// created by a miner, and used to pay the block reward - #[prost(bool, tag="4")] + #[prost(bool, tag = "4")] pub is_coinbase: bool, /// The index number of the block containing the transaction creating the output. - #[prost(int32, tag="5")] + #[prost(int32, tag = "5")] pub block_height: i32, - #[prost(message, optional, tag="6")] + #[prost(message, optional, tag = "6")] pub slp_token: ::core::option::Option, - #[prost(message, optional, tag="7")] + #[prost(message, optional, tag = "7")] pub token_metadata: ::core::option::Option, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetMerkleProofRequest { /// A transaction hash, little-endian. - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub transaction_hash: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetMerkleProofResponse { /// Block header information for the corresponding transaction - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub block: ::core::option::Option, /// A list containing the transaction hash, the adjacent leaf transaction hash /// and the hashes of the highest nodes in the merkle tree not built with the transaction. /// Proof hashes are ordered following transaction order, or left to right on the merkle tree - #[prost(bytes="vec", repeated, tag="2")] + #[prost(bytes = "vec", repeated, tag = "2")] pub hashes: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, /// Binary representing the location of the matching transaction in the full merkle tree, /// starting with the root (`1`) at position/level 0, where `1` corresponds /// to a left branch and `01` is a right branch. - #[prost(bytes="vec", tag="3")] + #[prost(bytes = "vec", tag = "3")] pub flags: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SubmitTransactionRequest { /// The encoded transaction. - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub transaction: ::prost::alloc::vec::Vec, - #[prost(bool, tag="2")] + #[prost(bool, tag = "2")] pub skip_slp_validity_check: bool, - #[prost(message, repeated, tag="3")] + #[prost(message, repeated, tag = "3")] pub required_slp_burns: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SubmitTransactionResponse { /// Transaction hash, little-endian. - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CheckSlpTransactionRequest { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub transaction: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag="2")] + #[prost(message, repeated, tag = "2")] pub required_slp_burns: ::prost::alloc::vec::Vec, /// Using the slp specification as a basis for validity judgement can lead to confusion for new users and /// result in accidental token burns. use_spec_validity_judgement will cause the response's is_valid property @@ -468,241 +545,308 @@ pub struct CheckSlpTransactionRequest { /// /// When use_spec_validity_judgement is true, there are three cases where the is_valid response property /// will be returned as valid, instead of invalid, as per the slp specification. - /// 1) inputs > outputs - /// 2) missing transaction outputs - /// 3) burned inputs from other tokens + /// 1) inputs > outputs + /// 2) missing transaction outputs + /// 3) burned inputs from other tokens /// /// required_slp_burns is not used when use_spec_validity_judgement is set to true. /// - #[prost(bool, tag="3")] + #[prost(bool, tag = "3")] pub use_spec_validity_judgement: bool, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CheckSlpTransactionResponse { - #[prost(bool, tag="1")] + #[prost(bool, tag = "1")] pub is_valid: bool, - #[prost(string, tag="2")] + #[prost(string, tag = "2")] pub invalid_reason: ::prost::alloc::string::String, - #[prost(int32, tag="3")] + #[prost(int32, tag = "3")] pub best_height: i32, } /// Request to subscribe or unsubscribe from a stream of transactions. +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SubscribeTransactionsRequest { /// Subscribe to a filter. add items to a filter - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub subscribe: ::core::option::Option, /// Unsubscribe to a filter, remove items from a filter - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag = "2")] pub unsubscribe: ::core::option::Option, /// When include_mempool is true, new unconfirmed transactions from mempool are /// included apart from the ones confirmed in a block. - #[prost(bool, tag="3")] + #[prost(bool, tag = "3")] pub include_mempool: bool, /// When include_in_block is true, transactions are included when they are confirmed. /// This notification is sent in addition to any requested mempool notifications. - #[prost(bool, tag="4")] + #[prost(bool, tag = "4")] pub include_in_block: bool, /// When serialize_tx is true, transactions are serialized using /// bitcoin protocol encoding. Default is false, transaction will be Marshaled /// (see `Transaction`, `MempoolTransaction` and `TransactionNotification`) - #[prost(bool, tag="5")] + #[prost(bool, tag = "5")] pub serialize_tx: bool, } /// Options to define data structure to be sent by SubscribeBlock stream: /// -/// - BlockInfo (block metadata): `BlockInfo` -/// - SubscribeBlocksRequest {} +/// - BlockInfo (block metadata): `BlockInfo` +/// - SubscribeBlocksRequest {} /// -/// - Marshaled Block (with transaction hashes): `Block` -/// - SubscribeBlocksRequest { -/// full_block = true -/// } -/// - Marshaled Block (with full transaction data): `Block` -/// - SubscribeBlocksRequest { -/// full_block = true -/// full_transactions = true -/// } -/// - Serialized Block acccording to bitcoin protocol encoding: `bytes` -/// - SubscribeBlocksRequest { -/// serialize_block = true -/// } +/// - Marshaled Block (with transaction hashes): `Block` +/// - SubscribeBlocksRequest { +/// full_block = true +/// } +/// - Marshaled Block (with full transaction data): `Block` +/// - SubscribeBlocksRequest { +/// full_block = true +/// full_transactions = true +/// } +/// - Serialized Block acccording to bitcoin protocol encoding: `bytes` +/// - SubscribeBlocksRequest { +/// serialize_block = true +/// } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SubscribeBlocksRequest { /// When full_block is true, a complete marshaled block is sent. See `Block`. /// Default is false, block metadata is sent. See `BlockInfo`. - #[prost(bool, tag="1")] + #[prost(bool, tag = "1")] pub full_block: bool, /// When full_transactions is true, provide full transaction info /// for a marshaled block. /// Default is false, only the transaction hashes are included for /// a marshaled block. See `TransactionData`. - #[prost(bool, tag="2")] + #[prost(bool, tag = "2")] pub full_transactions: bool, /// When serialize_block is true, blocks are serialized using bitcoin protocol encoding. /// Default is false, block will be Marshaled (see `BlockInfo` and `BlockNotification`) - #[prost(bool, tag="3")] + #[prost(bool, tag = "3")] pub serialize_block: bool, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetSlpTokenMetadataRequest { - #[prost(bytes="vec", repeated, tag="1")] + #[prost(bytes = "vec", repeated, tag = "1")] pub token_ids: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetSlpTokenMetadataResponse { - #[prost(message, repeated, tag="1")] + #[prost(message, repeated, tag = "1")] pub token_metadata: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetSlpParsedScriptRequest { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub slp_opreturn_script: ::prost::alloc::vec::Vec, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetSlpParsedScriptResponse { - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub parsing_error: ::prost::alloc::string::String, - #[prost(bytes="vec", tag="2")] + #[prost(bytes = "vec", tag = "2")] pub token_id: ::prost::alloc::vec::Vec, - #[prost(enumeration="SlpAction", tag="3")] + #[prost(enumeration = "SlpAction", tag = "3")] pub slp_action: i32, - #[prost(enumeration="SlpTokenType", tag="4")] + #[prost(enumeration = "SlpTokenType", tag = "4")] pub token_type: i32, - #[prost(oneof="get_slp_parsed_script_response::SlpMetadata", tags="5, 6, 7, 8, 9")] - pub slp_metadata: ::core::option::Option, + #[prost( + oneof = "get_slp_parsed_script_response::SlpMetadata", + tags = "5, 6, 7, 8, 9" + )] + pub slp_metadata: ::core::option::Option< + get_slp_parsed_script_response::SlpMetadata, + >, } /// Nested message and enum types in `GetSlpParsedScriptResponse`. pub mod get_slp_parsed_script_response { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum SlpMetadata { /// NFT1 Group also uses this - #[prost(message, tag="5")] + #[prost(message, tag = "5")] V1Genesis(super::SlpV1GenesisMetadata), /// NFT1 Group also uses this - #[prost(message, tag="6")] + #[prost(message, tag = "6")] V1Mint(super::SlpV1MintMetadata), /// NFT1 Group also uses this - #[prost(message, tag="7")] + #[prost(message, tag = "7")] V1Send(super::SlpV1SendMetadata), - #[prost(message, tag="8")] + #[prost(message, tag = "8")] V1Nft1ChildGenesis(super::SlpV1Nft1ChildGenesisMetadata), - #[prost(message, tag="9")] + #[prost(message, tag = "9")] V1Nft1ChildSend(super::SlpV1Nft1ChildSendMetadata), } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetSlpTrustedValidationRequest { - #[prost(message, repeated, tag="1")] + #[prost(message, repeated, tag = "1")] pub queries: ::prost::alloc::vec::Vec, - #[prost(bool, tag="2")] + #[prost(bool, tag = "2")] pub include_graphsearch_count: bool, } /// Nested message and enum types in `GetSlpTrustedValidationRequest`. pub mod get_slp_trusted_validation_request { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Query { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub prev_out_hash: ::prost::alloc::vec::Vec, - #[prost(uint32, tag="2")] + #[prost(uint32, tag = "2")] pub prev_out_vout: u32, - #[prost(bytes="vec", repeated, tag="3")] - pub graphsearch_valid_hashes: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, + #[prost(bytes = "vec", repeated, tag = "3")] + pub graphsearch_valid_hashes: ::prost::alloc::vec::Vec< + ::prost::alloc::vec::Vec, + >, } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetSlpTrustedValidationResponse { - #[prost(message, repeated, tag="1")] - pub results: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "1")] + pub results: ::prost::alloc::vec::Vec< + get_slp_trusted_validation_response::ValidityResult, + >, } /// Nested message and enum types in `GetSlpTrustedValidationResponse`. pub mod get_slp_trusted_validation_response { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ValidityResult { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub prev_out_hash: ::prost::alloc::vec::Vec, - #[prost(uint32, tag="2")] + #[prost(uint32, tag = "2")] pub prev_out_vout: u32, - #[prost(bytes="vec", tag="3")] + #[prost(bytes = "vec", tag = "3")] pub token_id: ::prost::alloc::vec::Vec, - #[prost(enumeration="super::SlpAction", tag="4")] + #[prost(enumeration = "super::SlpAction", tag = "4")] pub slp_action: i32, - #[prost(enumeration="super::SlpTokenType", tag="5")] + #[prost(enumeration = "super::SlpTokenType", tag = "5")] pub token_type: i32, - #[prost(bytes="vec", tag="8")] + #[prost(bytes = "vec", tag = "8")] pub slp_txn_opreturn: ::prost::alloc::vec::Vec, - #[prost(uint32, tag="9")] + #[prost(uint32, tag = "9")] pub graphsearch_txn_count: u32, - #[prost(oneof="validity_result::ValidityResultType", tags="6, 7")] - pub validity_result_type: ::core::option::Option, + #[prost(oneof = "validity_result::ValidityResultType", tags = "6, 7")] + pub validity_result_type: ::core::option::Option< + validity_result::ValidityResultType, + >, } /// Nested message and enum types in `ValidityResult`. pub mod validity_result { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum ValidityResultType { - #[prost(uint64, tag="6")] + #[prost(uint64, tag = "6")] V1TokenAmount(u64), - #[prost(bool, tag="7")] + #[prost(bool, tag = "7")] V1MintBaton(bool), } } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetSlpGraphSearchRequest { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", repeated, tag="2")] + #[prost(bytes = "vec", repeated, tag = "2")] pub valid_hashes: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetSlpGraphSearchResponse { - #[prost(bytes="vec", repeated, tag="1")] + #[prost(bytes = "vec", repeated, tag = "1")] pub txdata: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } -// NOTIFICATIONS - +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct BlockNotification { /// Whether the block is connected to the chain. - #[prost(enumeration="block_notification::Type", tag="1")] + #[prost(enumeration = "block_notification::Type", tag = "1")] pub r#type: i32, - #[prost(oneof="block_notification::Block", tags="2, 3, 4")] + #[prost(oneof = "block_notification::Block", tags = "2, 3, 4")] pub block: ::core::option::Option, } /// Nested message and enum types in `BlockNotification`. pub mod block_notification { /// State of the block in relation to the chain. - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] #[repr(i32)] pub enum Type { Connected = 0, Disconnected = 1, } + impl Type { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Type::Connected => "CONNECTED", + Type::Disconnected => "DISCONNECTED", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "CONNECTED" => Some(Self::Connected), + "DISCONNECTED" => Some(Self::Disconnected), + _ => None, + } + } + } + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Block { /// Marshaled block header data, as well as metadata stored by the node. - #[prost(message, tag="2")] + #[prost(message, tag = "2")] BlockInfo(super::BlockInfo), /// A Block. - #[prost(message, tag="3")] + #[prost(message, tag = "3")] MarshaledBlock(super::Block), /// Binary block, serialized using bitcoin protocol encoding. - #[prost(bytes, tag="4")] + #[prost(bytes, tag = "4")] SerializedBlock(::prost::alloc::vec::Vec), } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TransactionNotification { /// Whether or not the transaction has been included in a block. - #[prost(enumeration="transaction_notification::Type", tag="1")] + #[prost(enumeration = "transaction_notification::Type", tag = "1")] pub r#type: i32, - #[prost(oneof="transaction_notification::Transaction", tags="2, 3, 4")] + #[prost(oneof = "transaction_notification::Transaction", tags = "2, 3, 4")] pub transaction: ::core::option::Option, } /// Nested message and enum types in `TransactionNotification`. pub mod transaction_notification { /// State of the transaction acceptance. - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] #[repr(i32)] pub enum Type { /// A transaction in mempool. @@ -710,271 +854,295 @@ pub mod transaction_notification { /// A transaction in a block. Confirmed = 1, } + impl Type { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Type::Unconfirmed => "UNCONFIRMED", + Type::Confirmed => "CONFIRMED", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNCONFIRMED" => Some(Self::Unconfirmed), + "CONFIRMED" => Some(Self::Confirmed), + _ => None, + } + } + } + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Transaction { /// A transaction included in a block. - #[prost(message, tag="2")] + #[prost(message, tag = "2")] ConfirmedTransaction(super::Transaction), /// A transaction in mempool. - #[prost(message, tag="3")] + #[prost(message, tag = "3")] UnconfirmedTransaction(super::MempoolTransaction), /// Binary transaction, serialized using bitcoin protocol encoding. - #[prost(bytes, tag="4")] + #[prost(bytes, tag = "4")] SerializedTransaction(::prost::alloc::vec::Vec), } } -// DATA MESSAGES - /// Metadata for identifying and validating a block /// /// Identification. +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct BlockInfo { /// The double sha256 hash of the six header fields in the first 80 bytes /// of the block, when encoded according the bitcoin protocol, little-endian. /// sha256(sha256(encoded_header)) - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, /// The block number, an incremental index for each block mined. - #[prost(int32, tag="2")] + #[prost(int32, tag = "2")] pub height: i32, - // Block header data. - /// A version number to track software/protocol upgrades. - #[prost(int32, tag="3")] + #[prost(int32, tag = "3")] pub version: i32, /// Hash of the previous block, little-endian. - #[prost(bytes="vec", tag="4")] + #[prost(bytes = "vec", tag = "4")] pub previous_block: ::prost::alloc::vec::Vec, /// The root of the Merkle Tree built from all transactions in the block, little-endian. - #[prost(bytes="vec", tag="5")] + #[prost(bytes = "vec", tag = "5")] pub merkle_root: ::prost::alloc::vec::Vec, /// When mining of the block started, expressed in seconds since 1970-01-01. - #[prost(int64, tag="6")] + #[prost(int64, tag = "6")] pub timestamp: i64, /// Difficulty in Compressed Target Format. - #[prost(uint32, tag="7")] + #[prost(uint32, tag = "7")] pub bits: u32, /// A random value that was generated during block mining which happened to /// result in a computed block hash below the difficulty target at the time. - #[prost(uint32, tag="8")] + #[prost(uint32, tag = "8")] pub nonce: u32, - // Metadata. - /// Number of blocks in a chain, including the block itself upon creation. - #[prost(int32, tag="9")] + #[prost(int32, tag = "9")] pub confirmations: i32, /// Difficulty target at time of creation. - #[prost(double, tag="10")] + #[prost(double, tag = "10")] pub difficulty: f64, /// Hash of the next block in this chain, little-endian. - #[prost(bytes="vec", tag="11")] + #[prost(bytes = "vec", tag = "11")] pub next_block_hash: ::prost::alloc::vec::Vec, /// Size of the block in bytes. - #[prost(int32, tag="12")] + #[prost(int32, tag = "12")] pub size: i32, /// The median block time of the latest 11 block timestamps. - #[prost(int64, tag="13")] + #[prost(int64, tag = "13")] pub median_time: i64, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Block { /// Block header data, as well as metadata stored by the node. - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub info: ::core::option::Option, /// List of transactions or transaction hashes. - #[prost(message, repeated, tag="2")] + #[prost(message, repeated, tag = "2")] pub transaction_data: ::prost::alloc::vec::Vec, } /// Nested message and enum types in `Block`. pub mod block { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TransactionData { - #[prost(oneof="transaction_data::TxidsOrTxs", tags="1, 2")] + #[prost(oneof = "transaction_data::TxidsOrTxs", tags = "1, 2")] pub txids_or_txs: ::core::option::Option, } /// Nested message and enum types in `TransactionData`. pub mod transaction_data { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum TxidsOrTxs { /// Just the transaction hash, little-endian. - #[prost(bytes, tag="1")] + #[prost(bytes, tag = "1")] TransactionHash(::prost::alloc::vec::Vec), /// A marshaled transaction. - #[prost(message, tag="2")] + #[prost(message, tag = "2")] Transaction(super::super::Transaction), } } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Transaction { /// The double sha256 hash of the encoded transaction, little-endian. /// sha256(sha256(encoded_transaction)) - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, /// The version of the transaction format. - #[prost(int32, tag="2")] + #[prost(int32, tag = "2")] pub version: i32, /// List of inputs. - #[prost(message, repeated, tag="3")] + #[prost(message, repeated, tag = "3")] pub inputs: ::prost::alloc::vec::Vec, /// List of outputs. - #[prost(message, repeated, tag="4")] + #[prost(message, repeated, tag = "4")] pub outputs: ::prost::alloc::vec::Vec, /// The block height or timestamp after which this transaction is allowed. /// If value is greater than 500 million, it is assumed to be an epoch timestamp, /// otherwise it is treated as a block-height. Default is zero, or lock. - #[prost(uint32, tag="5")] + #[prost(uint32, tag = "5")] pub lock_time: u32, - // Metadata - /// The size of the transaction in bytes. - #[prost(int32, tag="8")] + #[prost(int32, tag = "8")] pub size: i32, /// When the transaction was included in a block, in epoch time. - #[prost(int64, tag="9")] + #[prost(int64, tag = "9")] pub timestamp: i64, /// Number of blocks including proof of the transaction, including /// the block it appeared. - #[prost(int32, tag="10")] + #[prost(int32, tag = "10")] pub confirmations: i32, /// Number of the block containing the transaction. - #[prost(int32, tag="11")] + #[prost(int32, tag = "11")] pub block_height: i32, /// Hash of the block the transaction was recorded in, little-endian. - #[prost(bytes="vec", tag="12")] + #[prost(bytes = "vec", tag = "12")] pub block_hash: ::prost::alloc::vec::Vec, - #[prost(message, optional, tag="13")] + #[prost(message, optional, tag = "13")] pub slp_transaction_info: ::core::option::Option, } /// Nested message and enum types in `Transaction`. pub mod transaction { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Input { /// The number of the input, starting from zero. - #[prost(uint32, tag="1")] + #[prost(uint32, tag = "1")] pub index: u32, /// The related outpoint. - #[prost(message, optional, tag="2")] + #[prost(message, optional, tag = "2")] pub outpoint: ::core::option::Option, /// An unlocking script asserting a transaction is permitted to spend /// the Outpoint (UTXO) - #[prost(bytes="vec", tag="3")] + #[prost(bytes = "vec", tag = "3")] pub signature_script: ::prost::alloc::vec::Vec, /// As of BIP-68, the sequence number is interpreted as a relative /// lock-time for the input. - #[prost(uint32, tag="4")] + #[prost(uint32, tag = "4")] pub sequence: u32, /// Amount in satoshi. - #[prost(int64, tag="5")] + #[prost(int64, tag = "5")] pub value: i64, /// The pubkey_script of the previous output that is being spent. - #[prost(bytes="vec", tag="6")] + #[prost(bytes = "vec", tag = "6")] pub previous_script: ::prost::alloc::vec::Vec, /// The bitcoin addresses associated with this input. - #[prost(string, tag="7")] + #[prost(string, tag = "7")] pub address: ::prost::alloc::string::String, - #[prost(message, optional, tag="8")] + #[prost(message, optional, tag = "8")] pub slp_token: ::core::option::Option, } /// Nested message and enum types in `Input`. pub mod input { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Outpoint { /// The hash of the transaction containing the output to be spent, little-endian - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, /// The index of specific output on the transaction. - #[prost(uint32, tag="2")] + #[prost(uint32, tag = "2")] pub index: u32, } } + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Output { /// The number of the output, starting from zero. - #[prost(uint32, tag="1")] + #[prost(uint32, tag = "1")] pub index: u32, /// The number of satoshis to be transferred. - #[prost(int64, tag="2")] + #[prost(int64, tag = "2")] pub value: i64, /// The public key script used to pay coins. - #[prost(bytes="vec", tag="3")] + #[prost(bytes = "vec", tag = "3")] pub pubkey_script: ::prost::alloc::vec::Vec, /// The bitcoin addresses associated with this output. - #[prost(string, tag="4")] + #[prost(string, tag = "4")] pub address: ::prost::alloc::string::String, /// The type of script. - #[prost(string, tag="5")] + #[prost(string, tag = "5")] pub script_class: ::prost::alloc::string::String, /// The script expressed in Bitcoin Cash Script. - #[prost(string, tag="6")] + #[prost(string, tag = "6")] pub disassembled_script: ::prost::alloc::string::String, - #[prost(message, optional, tag="7")] + #[prost(message, optional, tag = "7")] pub slp_token: ::core::option::Option, } } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct MempoolTransaction { - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub transaction: ::core::option::Option, /// The time when the transaction was added too the pool. - #[prost(int64, tag="2")] + #[prost(int64, tag = "2")] pub added_time: i64, /// The block height when the transaction was added to the pool. - #[prost(int32, tag="3")] + #[prost(int32, tag = "3")] pub added_height: i32, /// The total fee in satoshi the transaction pays. - #[prost(int64, tag="4")] + #[prost(int64, tag = "4")] pub fee: i64, /// The fee in satoshi per kilobyte the transaction pays. - #[prost(int64, tag="5")] + #[prost(int64, tag = "5")] pub fee_per_kb: i64, /// The priority of the transaction when it was added to the pool. - #[prost(double, tag="6")] + #[prost(double, tag = "6")] pub starting_priority: f64, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct UnspentOutput { /// A reference to the output given by transaction hash and index. - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub outpoint: ::core::option::Option, /// The public key script used to pay coins. - #[prost(bytes="vec", tag="2")] + #[prost(bytes = "vec", tag = "2")] pub pubkey_script: ::prost::alloc::vec::Vec, /// The amount in satoshis - #[prost(int64, tag="3")] + #[prost(int64, tag = "3")] pub value: i64, /// When is_coinbase is true, the output is the first in the block, /// a generation transaction, the result of mining. - #[prost(bool, tag="4")] + #[prost(bool, tag = "4")] pub is_coinbase: bool, /// The block number containing the UXTO. - #[prost(int32, tag="5")] + #[prost(int32, tag = "5")] pub block_height: i32, - #[prost(message, optional, tag="6")] + #[prost(message, optional, tag = "6")] pub slp_token: ::core::option::Option, } +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TransactionFilter { /// Filter by address(es) - #[prost(string, repeated, tag="1")] + #[prost(string, repeated, tag = "1")] pub addresses: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, /// Filter by output hash and index. - #[prost(message, repeated, tag="2")] + #[prost(message, repeated, tag = "2")] pub outpoints: ::prost::alloc::vec::Vec, /// Filter by data elements contained in pubkey scripts. - #[prost(bytes="vec", repeated, tag="3")] + #[prost(bytes = "vec", repeated, tag = "3")] pub data_elements: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, /// Subscribed/Unsubscribe to everything. Other filters /// will be ignored. - #[prost(bool, tag="4")] + #[prost(bool, tag = "4")] pub all_transactions: bool, /// Subscribed/Unsubscribe to everything slp. Other filters /// will be ignored, except this filter will be overriden by all_transactions=true - #[prost(bool, tag="5")] + #[prost(bool, tag = "5")] pub all_slp_transactions: bool, /// only transactions associated with the included tokenIds - #[prost(bytes="vec", repeated, tag="6")] + #[prost(bytes = "vec", repeated, tag = "6")] pub slp_token_ids: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec>, } /// SlpToken info used in transaction inputs / outputs @@ -984,48 +1152,90 @@ pub struct TransactionFilter { /// return a string for the amount field instead of casting uint64 to the JS 'number' /// type. Other languages may require similar treatment. /// +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SlpToken { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub token_id: ::prost::alloc::vec::Vec, - #[prost(uint64, tag="2")] + #[prost(uint64, tag = "2")] pub amount: u64, - #[prost(bool, tag="3")] + #[prost(bool, tag = "3")] pub is_mint_baton: bool, - #[prost(string, tag="4")] + #[prost(string, tag = "4")] pub address: ::prost::alloc::string::String, - #[prost(uint32, tag="5")] + #[prost(uint32, tag = "5")] pub decimals: u32, - #[prost(enumeration="SlpAction", tag="6")] + #[prost(enumeration = "SlpAction", tag = "6")] pub slp_action: i32, - #[prost(enumeration="SlpTokenType", tag="7")] + #[prost(enumeration = "SlpTokenType", tag = "7")] pub token_type: i32, } /// SlpTransactionInfo is used inside the Transaction message type. +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SlpTransactionInfo { - #[prost(enumeration="SlpAction", tag="1")] + #[prost(enumeration = "SlpAction", tag = "1")] pub slp_action: i32, - #[prost(enumeration="slp_transaction_info::ValidityJudgement", tag="2")] + #[prost(enumeration = "slp_transaction_info::ValidityJudgement", tag = "2")] pub validity_judgement: i32, - #[prost(string, tag="3")] + #[prost(string, tag = "3")] pub parse_error: ::prost::alloc::string::String, - #[prost(bytes="vec", tag="4")] + #[prost(bytes = "vec", tag = "4")] pub token_id: ::prost::alloc::vec::Vec, - #[prost(enumeration="slp_transaction_info::BurnFlags", repeated, tag="5")] + #[prost(enumeration = "slp_transaction_info::BurnFlags", repeated, tag = "5")] pub burn_flags: ::prost::alloc::vec::Vec, - #[prost(oneof="slp_transaction_info::TxMetadata", tags="6, 7, 8, 9, 10")] + #[prost(oneof = "slp_transaction_info::TxMetadata", tags = "6, 7, 8, 9, 10")] pub tx_metadata: ::core::option::Option, } /// Nested message and enum types in `SlpTransactionInfo`. pub mod slp_transaction_info { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] #[repr(i32)] pub enum ValidityJudgement { UnknownOrInvalid = 0, Valid = 1, } - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + impl ValidityJudgement { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + ValidityJudgement::UnknownOrInvalid => "UNKNOWN_OR_INVALID", + ValidityJudgement::Valid => "VALID", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNKNOWN_OR_INVALID" => Some(Self::UnknownOrInvalid), + "VALID" => Some(Self::Valid), + _ => None, + } + } + } + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] #[repr(i32)] pub enum BurnFlags { BurnedInputsOutputsTooHigh = 0, @@ -1034,168 +1244,217 @@ pub mod slp_transaction_info { BurnedOutputsMissingBchVout = 3, BurnedInputsGreaterThanOutputs = 4, } + impl BurnFlags { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + BurnFlags::BurnedInputsOutputsTooHigh => "BURNED_INPUTS_OUTPUTS_TOO_HIGH", + BurnFlags::BurnedInputsBadOpreturn => "BURNED_INPUTS_BAD_OPRETURN", + BurnFlags::BurnedInputsOtherToken => "BURNED_INPUTS_OTHER_TOKEN", + BurnFlags::BurnedOutputsMissingBchVout => { + "BURNED_OUTPUTS_MISSING_BCH_VOUT" + } + BurnFlags::BurnedInputsGreaterThanOutputs => { + "BURNED_INPUTS_GREATER_THAN_OUTPUTS" + } + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "BURNED_INPUTS_OUTPUTS_TOO_HIGH" => { + Some(Self::BurnedInputsOutputsTooHigh) + } + "BURNED_INPUTS_BAD_OPRETURN" => Some(Self::BurnedInputsBadOpreturn), + "BURNED_INPUTS_OTHER_TOKEN" => Some(Self::BurnedInputsOtherToken), + "BURNED_OUTPUTS_MISSING_BCH_VOUT" => { + Some(Self::BurnedOutputsMissingBchVout) + } + "BURNED_INPUTS_GREATER_THAN_OUTPUTS" => { + Some(Self::BurnedInputsGreaterThanOutputs) + } + _ => None, + } + } + } + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum TxMetadata { /// NFT1 Group also uses this - #[prost(message, tag="6")] + #[prost(message, tag = "6")] V1Genesis(super::SlpV1GenesisMetadata), /// NFT1 Group also uses this - #[prost(message, tag="7")] + #[prost(message, tag = "7")] V1Mint(super::SlpV1MintMetadata), /// NFT1 Group also uses this - #[prost(message, tag="8")] + #[prost(message, tag = "8")] V1Send(super::SlpV1SendMetadata), - #[prost(message, tag="9")] + #[prost(message, tag = "9")] V1Nft1ChildGenesis(super::SlpV1Nft1ChildGenesisMetadata), - #[prost(message, tag="10")] + #[prost(message, tag = "10")] V1Nft1ChildSend(super::SlpV1Nft1ChildSendMetadata), } } /// SlpV1GenesisMetadata is used to marshal type 1 and NFT1 Group GENESIS OP_RETURN scriptPubKey +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SlpV1GenesisMetadata { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub name: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", tag="2")] + #[prost(bytes = "vec", tag = "2")] pub ticker: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", tag="3")] + #[prost(bytes = "vec", tag = "3")] pub document_url: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", tag="4")] + #[prost(bytes = "vec", tag = "4")] pub document_hash: ::prost::alloc::vec::Vec, - #[prost(uint32, tag="5")] + #[prost(uint32, tag = "5")] pub decimals: u32, - #[prost(uint32, tag="6")] + #[prost(uint32, tag = "6")] pub mint_baton_vout: u32, - #[prost(uint64, tag="7")] + #[prost(uint64, tag = "7")] pub mint_amount: u64, } /// SlpV1MintMetadata is used to marshal type 1 MINT OP_RETURN scriptPubKey +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SlpV1MintMetadata { - #[prost(uint32, tag="1")] + #[prost(uint32, tag = "1")] pub mint_baton_vout: u32, - #[prost(uint64, tag="2")] + #[prost(uint64, tag = "2")] pub mint_amount: u64, } /// SlpV1SendMetadata is used to marshal type 1 and NFT1 Group SEND OP_RETURN scriptPubKey +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SlpV1SendMetadata { - #[prost(uint64, repeated, packed="false", tag="1")] + #[prost(uint64, repeated, packed = "false", tag = "1")] pub amounts: ::prost::alloc::vec::Vec, } /// SlpV1Nft1ChildGenesisMetadata is used to marshal NFT1 Child GENESIS OP_RETURN scriptPubKey +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SlpV1Nft1ChildGenesisMetadata { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub name: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", tag="2")] + #[prost(bytes = "vec", tag = "2")] pub ticker: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", tag="3")] + #[prost(bytes = "vec", tag = "3")] pub document_url: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", tag="4")] + #[prost(bytes = "vec", tag = "4")] pub document_hash: ::prost::alloc::vec::Vec, - #[prost(uint32, tag="5")] + #[prost(uint32, tag = "5")] pub decimals: u32, - #[prost(bytes="vec", tag="6")] + #[prost(bytes = "vec", tag = "6")] pub group_token_id: ::prost::alloc::vec::Vec, } /// SlpV1Nft1ChildSendMetadata is used to marshal NFT1 Child SEND OP_RETURN scriptPubKey +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SlpV1Nft1ChildSendMetadata { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub group_token_id: ::prost::alloc::vec::Vec, } /// SlpTokenMetadata is used to marshal metadata about a specific TokenID +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SlpTokenMetadata { - #[prost(bytes="vec", tag="1")] + #[prost(bytes = "vec", tag = "1")] pub token_id: ::prost::alloc::vec::Vec, - #[prost(enumeration="SlpTokenType", tag="2")] + #[prost(enumeration = "SlpTokenType", tag = "2")] pub token_type: i32, - #[prost(oneof="slp_token_metadata::TypeMetadata", tags="3, 4, 5")] + #[prost(oneof = "slp_token_metadata::TypeMetadata", tags = "3, 4, 5")] pub type_metadata: ::core::option::Option, } /// Nested message and enum types in `SlpTokenMetadata`. pub mod slp_token_metadata { /// V1Fungible is used to marshal metadata specific to Type 1 token IDs + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct V1Fungible { - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub token_ticker: ::prost::alloc::string::String, - #[prost(string, tag="2")] + #[prost(string, tag = "2")] pub token_name: ::prost::alloc::string::String, - #[prost(string, tag="3")] + #[prost(string, tag = "3")] pub token_document_url: ::prost::alloc::string::String, - #[prost(bytes="vec", tag="4")] + #[prost(bytes = "vec", tag = "4")] pub token_document_hash: ::prost::alloc::vec::Vec, - #[prost(uint32, tag="5")] + #[prost(uint32, tag = "5")] pub decimals: u32, - #[prost(bytes="vec", tag="6")] + #[prost(bytes = "vec", tag = "6")] pub mint_baton_hash: ::prost::alloc::vec::Vec, - #[prost(uint32, tag="7")] + #[prost(uint32, tag = "7")] pub mint_baton_vout: u32, } /// V1NFT1Group is used to marshal metadata specific to NFT1 Group token IDs + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct V1nft1Group { - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub token_ticker: ::prost::alloc::string::String, - #[prost(string, tag="2")] + #[prost(string, tag = "2")] pub token_name: ::prost::alloc::string::String, - #[prost(string, tag="3")] + #[prost(string, tag = "3")] pub token_document_url: ::prost::alloc::string::String, - #[prost(bytes="vec", tag="4")] + #[prost(bytes = "vec", tag = "4")] pub token_document_hash: ::prost::alloc::vec::Vec, - #[prost(uint32, tag="5")] + #[prost(uint32, tag = "5")] pub decimals: u32, - #[prost(bytes="vec", tag="6")] + #[prost(bytes = "vec", tag = "6")] pub mint_baton_hash: ::prost::alloc::vec::Vec, - #[prost(uint32, tag="7")] + #[prost(uint32, tag = "7")] pub mint_baton_vout: u32, } /// V1NFT1Child is used to marshal metadata specific to NFT1 Child token IDs + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct V1nft1Child { - #[prost(string, tag="1")] + #[prost(string, tag = "1")] pub token_ticker: ::prost::alloc::string::String, - #[prost(string, tag="2")] + #[prost(string, tag = "2")] pub token_name: ::prost::alloc::string::String, - #[prost(string, tag="3")] + #[prost(string, tag = "3")] pub token_document_url: ::prost::alloc::string::String, - #[prost(bytes="vec", tag="4")] + #[prost(bytes = "vec", tag = "4")] pub token_document_hash: ::prost::alloc::vec::Vec, - #[prost(bytes="vec", tag="5")] + #[prost(bytes = "vec", tag = "5")] pub group_id: ::prost::alloc::vec::Vec, } + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum TypeMetadata { - #[prost(message, tag="3")] + #[prost(message, tag = "3")] V1Fungible(V1Fungible), - #[prost(message, tag="4")] + #[prost(message, tag = "4")] V1Nft1Group(V1nft1Group), - #[prost(message, tag="5")] + #[prost(message, tag = "5")] V1Nft1Child(V1nft1Child), } } /// SlpRequiredBurn is used by clients to allow token burning +#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SlpRequiredBurn { - #[prost(message, optional, tag="1")] + #[prost(message, optional, tag = "1")] pub outpoint: ::core::option::Option, - #[prost(bytes="vec", tag="2")] + #[prost(bytes = "vec", tag = "2")] pub token_id: ::prost::alloc::vec::Vec, - #[prost(enumeration="SlpTokenType", tag="3")] + #[prost(enumeration = "SlpTokenType", tag = "3")] pub token_type: i32, - #[prost(oneof="slp_required_burn::BurnIntention", tags="4, 5")] + #[prost(oneof = "slp_required_burn::BurnIntention", tags = "4, 5")] pub burn_intention: ::core::option::Option, } /// Nested message and enum types in `SlpRequiredBurn`. pub mod slp_required_burn { + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum BurnIntention { - #[prost(uint64, tag="4")] + #[prost(uint64, tag = "4")] Amount(u64), - #[prost(uint32, tag="5")] + #[prost(uint32, tag = "5")] MintBatonVout(u32), } } @@ -1207,6 +1466,30 @@ pub enum SlpTokenType { V1Nft1Child = 65, V1Nft1Group = 129, } +impl SlpTokenType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + SlpTokenType::VersionNotSet => "VERSION_NOT_SET", + SlpTokenType::V1Fungible => "V1_FUNGIBLE", + SlpTokenType::V1Nft1Child => "V1_NFT1_CHILD", + SlpTokenType::V1Nft1Group => "V1_NFT1_GROUP", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "VERSION_NOT_SET" => Some(Self::VersionNotSet), + "V1_FUNGIBLE" => Some(Self::V1Fungible), + "V1_NFT1_CHILD" => Some(Self::V1Nft1Child), + "V1_NFT1_GROUP" => Some(Self::V1Nft1Group), + _ => None, + } + } +} /// SlpAction is used to allow clients to identify the type of slp transaction from this single field. /// /// NOTE: All enum types except for "NON_SLP" may be annotated with one or more BurnFlags. @@ -1227,3 +1510,43 @@ pub enum SlpAction { SlpV1Nft1UniqueChildGenesis = 10, SlpV1Nft1UniqueChildSend = 11, } +impl SlpAction { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + SlpAction::NonSlp => "NON_SLP", + SlpAction::NonSlpBurn => "NON_SLP_BURN", + SlpAction::SlpParseError => "SLP_PARSE_ERROR", + SlpAction::SlpUnsupportedVersion => "SLP_UNSUPPORTED_VERSION", + SlpAction::SlpV1Genesis => "SLP_V1_GENESIS", + SlpAction::SlpV1Mint => "SLP_V1_MINT", + SlpAction::SlpV1Send => "SLP_V1_SEND", + SlpAction::SlpV1Nft1GroupGenesis => "SLP_V1_NFT1_GROUP_GENESIS", + SlpAction::SlpV1Nft1GroupMint => "SLP_V1_NFT1_GROUP_MINT", + SlpAction::SlpV1Nft1GroupSend => "SLP_V1_NFT1_GROUP_SEND", + SlpAction::SlpV1Nft1UniqueChildGenesis => "SLP_V1_NFT1_UNIQUE_CHILD_GENESIS", + SlpAction::SlpV1Nft1UniqueChildSend => "SLP_V1_NFT1_UNIQUE_CHILD_SEND", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "NON_SLP" => Some(Self::NonSlp), + "NON_SLP_BURN" => Some(Self::NonSlpBurn), + "SLP_PARSE_ERROR" => Some(Self::SlpParseError), + "SLP_UNSUPPORTED_VERSION" => Some(Self::SlpUnsupportedVersion), + "SLP_V1_GENESIS" => Some(Self::SlpV1Genesis), + "SLP_V1_MINT" => Some(Self::SlpV1Mint), + "SLP_V1_SEND" => Some(Self::SlpV1Send), + "SLP_V1_NFT1_GROUP_GENESIS" => Some(Self::SlpV1Nft1GroupGenesis), + "SLP_V1_NFT1_GROUP_MINT" => Some(Self::SlpV1Nft1GroupMint), + "SLP_V1_NFT1_GROUP_SEND" => Some(Self::SlpV1Nft1GroupSend), + "SLP_V1_NFT1_UNIQUE_CHILD_GENESIS" => Some(Self::SlpV1Nft1UniqueChildGenesis), + "SLP_V1_NFT1_UNIQUE_CHILD_SEND" => Some(Self::SlpV1Nft1UniqueChildSend), + _ => None, + } + } +} diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index a66ae04331..d3442d6690 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -1,12 +1,10 @@ +use super::utxo_common::utxo_prepare_addresses_for_balance_stream_if_enabled; use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; -use crate::coin_errors::MyAddressError; -use crate::hd_confirm_address::HDConfirmAddress; -use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, NewAccountCreatingError, - NewAddressDeriveConfirmError}; -use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, + HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, @@ -17,26 +15,28 @@ use crate::rpc_command::init_create_account::{self, CreateAccountRpcError, Creat InitCreateAccountRpcOps}; use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpcOps, ScanAddressesParams, ScanAddressesResponse}; -use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandleShared}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::utxo_builder::{MergeUtxoArcOps, UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::utxo_hd_wallet::{UtxoHDAccount, UtxoHDAddress}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; -use crate::{eth, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, - DelegationError, DelegationFut, DexFee, GetWithdrawSenderAddress, IguanaPrivKey, MakerSwapTakerCoin, - MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, - PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, RefundPaymentArgs, RefundResult, - SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignatureResult, - SpendPaymentArgs, StakingInfosFut, SwapOps, TakerSwapMakerCoin, TradePreimageValue, TransactionFut, - TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, - ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, - ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, - WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, - WatcherValidateTakerFeeInput, WithdrawFut, WithdrawSenderAddress}; +use crate::{eth, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, + CoinWithPrivKeyPolicy, ConfirmPaymentInput, DelegationError, DelegationFut, DexFee, + GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, + NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, + RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, StakingInfosFut, SwapOps, + TakerSwapMakerCoin, TradePreimageValue, TransactionFut, TransactionResult, TxMarshalingErr, + UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, + WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; -use crypto::Bip44Chain; use ethereum_types::H160; use futures::{FutureExt, TryFutureExt}; use keys::AddressHashEnum; @@ -111,26 +111,19 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { /// Try to parse address from either wallet (UTXO) format or contract format. fn utxo_address_from_any_format(&self, from: &str) -> Result { - let utxo_err = match Address::from_str(from) { + let utxo_err = match Address::from_legacyaddress(from, &self.as_ref().conf.address_prefixes) { Ok(addr) => { - let is_p2pkh = addr.prefix == self.as_ref().conf.pub_addr_prefix - && addr.t_addr_prefix == self.as_ref().conf.pub_t_addr_prefix; - if is_p2pkh { + if addr.is_pubkey_hash() { return Ok(addr); } - "Address has invalid prefixes".to_string() + "Address has invalid prefix".to_string() }, - Err(e) => e.to_string(), + Err(e) => e, }; - let utxo_segwit_err = match Address::from_segwitaddress( - from, - self.as_ref().conf.checksum_type, - self.as_ref().conf.pub_addr_prefix, - self.as_ref().conf.pub_t_addr_prefix, - ) { + let utxo_segwit_err = match Address::from_segwitaddress(from, self.as_ref().conf.checksum_type) { Ok(addr) => { let is_segwit = - addr.hrp.is_some() && addr.hrp == self.as_ref().conf.bech32_hrp && self.as_ref().conf.segwit; + addr.hrp().is_some() && addr.hrp() == &self.as_ref().conf.bech32_hrp && self.as_ref().conf.segwit; if is_segwit { return Ok(addr); } @@ -152,39 +145,27 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { fn utxo_addr_from_contract_addr(&self, address: H160) -> Address { let utxo = self.as_ref(); - Address { - prefix: utxo.conf.pub_addr_prefix, - t_addr_prefix: utxo.conf.pub_t_addr_prefix, - hash: AddressHashEnum::AddressHash(address.0.into()), - checksum_type: utxo.conf.checksum_type, - hrp: utxo.conf.bech32_hrp.clone(), - addr_format: self.addr_format().clone(), - } + AddressBuilder::new( + self.addr_format().clone(), + utxo.conf.checksum_type, + utxo.conf.address_prefixes.clone(), + utxo.conf.bech32_hrp.clone(), + ) + .as_pkh(AddressHashEnum::AddressHash(address.0.into())) + .build() + .expect("valid address props") } - fn my_addr_as_contract_addr(&self) -> MmResult { - let my_address = self.as_ref().derivation_method.single_addr_or_err()?.clone(); + async fn my_addr_as_contract_addr(&self) -> MmResult { + let my_address = self.as_ref().derivation_method.single_addr_or_err().await?; contract_addr_from_utxo_addr(my_address).mm_err(Qrc20AddressError::from) } - fn utxo_address_from_contract_addr(&self, address: H160) -> Address { - let utxo = self.as_ref(); - Address { - prefix: utxo.conf.pub_addr_prefix, - t_addr_prefix: utxo.conf.pub_t_addr_prefix, - hash: AddressHashEnum::AddressHash(address.0.into()), - checksum_type: utxo.conf.checksum_type, - hrp: utxo.conf.bech32_hrp.clone(), - addr_format: self.addr_format().clone(), - } - } - fn contract_address_from_raw_pubkey(&self, pubkey: &[u8]) -> Result { let utxo = self.as_ref(); let qtum_address = try_s!(utxo_common::address_from_raw_pubkey( pubkey, - utxo.conf.pub_addr_prefix, - utxo.conf.pub_t_addr_prefix, + utxo.conf.address_prefixes.clone(), utxo.conf.checksum_type, utxo.conf.bech32_hrp.clone(), self.addr_format().clone() @@ -418,6 +399,10 @@ impl UtxoCommonOps for QtumCoin { utxo_common::checked_address_from_str(self, address) } + fn script_for_address(&self, address: &Address) -> MmResult { + utxo_common::output_script_checked(self.as_ref(), address) + } + async fn get_current_mtp(&self) -> UtxoRpcResult { utxo_common::get_current_mtp(&self.utxo_arc, CoinVariant::Qtum).await } @@ -491,8 +476,7 @@ impl UtxoCommonOps for QtumCoin { let conf = &self.utxo_arc.conf; utxo_common::address_from_pubkey( pubkey, - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + conf.address_prefixes.clone(), conf.checksum_type, conf.bech32_hrp.clone(), self.addr_format().clone(), @@ -526,7 +510,7 @@ impl UtxoStandardOps for QtumCoin { #[async_trait] impl SwapOps for QtumCoin { #[inline] - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) } @@ -541,13 +525,19 @@ impl SwapOps for QtumCoin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args).await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args).await } #[inline] @@ -577,13 +567,13 @@ impl SwapOps for QtumCoin { } #[inline] - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - utxo_common::validate_maker_payment(self, input) + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + utxo_common::validate_maker_payment(self, input).await } #[inline] - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - utxo_common::validate_taker_payment(self, input) + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + utxo_common::validate_taker_payment(self, input).await } #[inline] @@ -811,12 +801,13 @@ impl WatcherOps for QtumCoin { } } +#[async_trait] impl MarketCoinOps for QtumCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } fn my_address(&self) -> MmResult { utxo_common::my_address(self) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) } @@ -849,13 +840,18 @@ impl MarketCoinOps for QtumCoin { utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) } + #[inline(always)] + async fn sign_raw_tx(&self, args: &SignRawTransactionRequest) -> RawTransactionResult { + utxo_common::sign_raw_tx(self, args).await + } + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { utxo_common::wait_for_confirmations(&self.utxo_arc, input) } fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { utxo_common::wait_for_output_spend( - &self.utxo_arc, + self.clone(), args.tx_bytes, utxo_common::DEFAULT_SWAP_VOUT, args.from_block, @@ -877,6 +873,8 @@ impl MarketCoinOps for QtumCoin { fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -929,6 +927,7 @@ impl MmCoin for QtumCoin { &self, value: TradePreimageValue, stage: FeeApproxStage, + _include_refund_fee: bool, // refund fee is taken from swap output ) -> TradePreimageResult { utxo_common::get_sender_trade_fee(self, value, stage).await } @@ -1001,7 +1000,7 @@ impl InitWithdrawCoin for QtumCoin { &self, ctx: MmArc, req: WithdrawRequest, - task_handle: &WithdrawTaskHandle, + task_handle: WithdrawTaskHandleShared, ) -> Result> { utxo_common::init_withdraw(ctx, self.clone(), req, task_handle).await } @@ -1027,87 +1026,62 @@ impl UtxoSignerOps for QtumCoin { fn tx_provider(&self) -> Self::TxGetter { self.utxo_arc.rpc_client.clone() } } -impl CoinWithDerivationMethod for QtumCoin { - type Address = Address; - type HDWallet = UtxoHDWallet; +impl CoinWithPrivKeyPolicy for QtumCoin { + type KeyPair = KeyPair; - fn derivation_method(&self) -> &DerivationMethod { + fn priv_key_policy(&self) -> &PrivKeyPolicy { &self.utxo_arc.priv_key_policy } +} + +impl CoinWithDerivationMethod for QtumCoin { + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet> { utxo_common::derivation_method(self.as_ref()) } } +#[async_trait] +impl IguanaBalanceOps for QtumCoin { + type BalanceObject = CoinBalance; + + async fn iguana_balances(&self) -> BalanceResult { self.my_balance().compat().await } +} + #[async_trait] impl ExtractExtendedPubkey for QtumCoin { type ExtendedPublicKey = Secp256k1ExtendedPublicKey; async fn extract_extended_pubkey( &self, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, derivation_path: DerivationPath, ) -> MmResult where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { - utxo_common::extract_extended_pubkey(&self.utxo_arc.conf, xpub_extractor, derivation_path).await + crate::extract_extended_pubkey_impl(self, xpub_extractor, derivation_path).await } } #[async_trait] impl HDWalletCoinOps for QtumCoin { - type Address = Address; - type Pubkey = Public; type HDWallet = UtxoHDWallet; - type HDAccount = UtxoHDAccount; - - async fn derive_addresses( - &self, - hd_account: &Self::HDAccount, - address_ids: Ids, - ) -> AddressDerivingResult>> - where - Ids: Iterator + Send, - { - utxo_common::derive_addresses(self, hd_account, address_ids).await - } - async fn generate_and_confirm_new_address( + fn address_from_extended_pubkey( &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - confirm_address: &ConfirmAddress, - ) -> MmResult, NewAddressDeriveConfirmError> - where - ConfirmAddress: HDConfirmAddress, - { - utxo_common::generate_and_confirm_new_address(self, hd_wallet, hd_account, chain, confirm_address).await - } - - async fn create_new_account<'a, XPubExtractor>( - &self, - hd_wallet: &'a Self::HDWallet, - xpub_extractor: &XPubExtractor, - ) -> MmResult, NewAccountCreatingError> - where - XPubExtractor: HDXPubExtractor, - { - utxo_common::create_new_account(self, hd_wallet, xpub_extractor).await + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, + ) -> UtxoHDAddress { + utxo_common::address_from_extended_pubkey(self, extended_pubkey, derivation_path) } - async fn set_known_addresses_number( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - new_known_addresses_number: u32, - ) -> MmResult<(), AccountUpdatingError> { - utxo_common::set_known_addresses_number(self, hd_wallet, hd_account, chain, new_known_addresses_number).await - } + fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } } +impl HDCoinWithdrawOps for QtumCoin {} + #[async_trait] impl HDWalletBalanceOps for QtumCoin { type HDAddressScanner = UtxoAddressScanner; + type BalanceObject = CoinBalance; async fn produce_hd_address_scanner(&self) -> BalanceResult { utxo_common::produce_hd_address_scanner(self).await @@ -1116,53 +1090,60 @@ impl HDWalletBalanceOps for QtumCoin { async fn enable_hd_wallet( &self, hd_wallet: &Self::HDWallet, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { - coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params).await + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params, path_to_address).await } async fn scan_for_new_addresses( &self, hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, + hd_account: &mut UtxoHDAccount, address_scanner: &Self::HDAddressScanner, gap_limit: u32, - ) -> BalanceResult> { + ) -> BalanceResult>> { utxo_common::scan_for_new_addresses(self, hd_wallet, hd_account, address_scanner, gap_limit).await } - async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult> { + async fn all_known_addresses_balances( + &self, + hd_account: &UtxoHDAccount, + ) -> BalanceResult>> { utxo_common::all_known_addresses_balances(self, hd_account).await } - async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult { + async fn known_address_balance(&self, address: &Address) -> BalanceResult { utxo_common::address_balance(self, address).await } async fn known_addresses_balances( &self, - addresses: Vec, - ) -> BalanceResult> { + addresses: Vec
, + ) -> BalanceResult> { utxo_common::addresses_balances(self, addresses).await } -} -impl HDWalletCoinWithStorageOps for QtumCoin { - fn hd_wallet_storage<'a>(&self, hd_wallet: &'a Self::HDWallet) -> &'a HDWalletCoinStorage { - &hd_wallet.hd_wallet_storage + async fn prepare_addresses_for_balance_stream_if_enabled( + &self, + addresses: HashSet, + ) -> MmResult<(), String> { + utxo_prepare_addresses_for_balance_stream_if_enabled(self, addresses).await } } #[async_trait] impl GetNewAddressRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn get_new_address_rpc_without_conf( &self, params: GetNewAddressParams, - ) -> MmResult { + ) -> MmResult, GetNewAddressRpcError> { get_new_address::common_impl::get_new_address_rpc_without_conf(self, params).await } @@ -1170,7 +1151,7 @@ impl GetNewAddressRpcOps for QtumCoin { &self, params: GetNewAddressParams, confirm_address: &ConfirmAddress, - ) -> MmResult + ) -> MmResult, GetNewAddressRpcError> where ConfirmAddress: HDConfirmAddress, { @@ -1180,44 +1161,52 @@ impl GetNewAddressRpcOps for QtumCoin { #[async_trait] impl AccountBalanceRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn account_balance_rpc( &self, params: AccountBalanceParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { account_balance::common_impl::account_balance_rpc(self, params).await } } #[async_trait] impl InitAccountBalanceRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn init_account_balance_rpc( &self, params: InitAccountBalanceParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { init_account_balance::common_impl::init_account_balance_rpc(self, params).await } } #[async_trait] impl InitScanAddressesRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn init_scan_for_new_addresses_rpc( &self, params: ScanAddressesParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await } } #[async_trait] impl InitCreateAccountRpcOps for QtumCoin { + type BalanceObject = CoinBalance; + async fn init_create_account_rpc( &self, params: CreateNewAccountParams, state: CreateAccountState, - xpub_extractor: &XPubExtractor, - ) -> MmResult + xpub_extractor: Option, + ) -> MmResult, CreateAccountRpcError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { init_create_account::common_impl::init_create_new_account_rpc(self, params, state, xpub_extractor).await } @@ -1242,7 +1231,8 @@ impl CoinWithTxHistoryV2 for QtumCoin { #[async_trait] impl UtxoTxHistoryOps for QtumCoin { async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { - utxo_common::utxo_tx_history_v2_common::my_addresses(self).await + let addresses = self.all_addresses().await?; + Ok(addresses) } async fn tx_details_by_hash( @@ -1293,7 +1283,7 @@ impl UtxoTxHistoryOps for QtumCoin { pub fn contract_addr_from_str(addr: &str) -> Result { eth::addr_from_str(addr) } pub fn contract_addr_from_utxo_addr(address: Address) -> MmResult { - match address.hash { + match address.hash() { AddressHashEnum::AddressHash(h) => Ok(h.take().into()), AddressHashEnum::WitnessScriptHash(_) => MmError::err(ScriptHashTypeNotSupported { script_hash_type: "Witness".to_owned(), diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index 231d230c51..ad9aec86e1 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -8,7 +8,7 @@ use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, UtxoTxBuilder}; use crate::utxo::{qtum, utxo_common, Address, GetUtxoListOps, UtxoCommonOps}; use crate::utxo::{PrivKeyPolicyNotAllowed, UTXO_LOCK}; use crate::{DelegationError, DelegationFut, DelegationResult, MarketCoinOps, StakingInfos, StakingInfosError, - StakingInfosFut, StakingInfosResult, TransactionDetails, TransactionType}; + StakingInfosFut, StakingInfosResult, TransactionData, TransactionDetails, TransactionType}; use bitcrypto::dhash256; use common::now_sec; use derive_more::Display; @@ -20,7 +20,6 @@ use keys::{AddressHashEnum, Signature}; use mm2_err_handle::prelude::*; use mm2_number::bigdecimal::{BigDecimal, Zero}; use rpc::v1::types::ToTxHash; -use script::Builder as ScriptBuilder; use serialization::serialize; use std::convert::TryInto; use std::str::FromStr; @@ -142,7 +141,7 @@ impl QtumCoin { }, UtxoRpcClientEnum::Electrum(electrum) => electrum, }; - let address = self.my_addr_as_contract_addr()?; + let address = self.my_addr_as_contract_addr().await?; let address_rpc = contract_addr_into_rpc_format(&address); let add_delegation_history = client .blockchain_contract_event_get_history(&address_rpc, &contract_address, QTUM_ADD_DELEGATION_TOPIC) @@ -187,7 +186,7 @@ impl QtumCoin { .map(|padded_staker_address_hex| padded_staker_address_hex.trim_start_matches('0')) }) { let hash = H160::from_str(raw).map_to_mm(|e| StakingInfosError::Internal(e.to_string()))?; - let address = self.utxo_address_from_contract_addr(hash); + let address = self.utxo_addr_from_contract_addr(hash); Ok(Some(address.to_string())) } else { Ok(None) @@ -198,10 +197,10 @@ impl QtumCoin { async fn get_delegation_infos_impl(&self) -> StakingInfosResult { let coin = self.as_ref(); - let my_address = coin.derivation_method.single_addr_or_err()?; + let my_address = coin.derivation_method.single_addr_or_err().await?; let staker = self.am_i_currently_staking().await?; - let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; + let (unspents, _) = self.get_unspent_ordered_list(&my_address).await?; let lower_bound = QTUM_LOWER_BOUND_DELEGATION_AMOUNT .try_into() .expect("Conversion should succeed"); @@ -219,7 +218,7 @@ impl QtumCoin { amount, staker, am_i_staking, - is_staking_supported: !my_address.addr_format.is_segwit(), + is_staking_supported: !my_address.addr_format().is_segwit(), } .into(), }; @@ -235,14 +234,14 @@ impl QtumCoin { if let Some(staking_addr) = self.am_i_currently_staking().await? { return MmError::err(DelegationError::AlreadyDelegating(staking_addr)); } - let to_addr = - Address::from_str(request.address.as_str()).map_to_mm(|e| DelegationError::AddressError(e.to_string()))?; + let to_addr = Address::from_legacyaddress(request.address.as_str(), &self.as_ref().conf.address_prefixes) + .map_to_mm(DelegationError::AddressError)?; let fee = request.fee.unwrap_or(QTUM_DELEGATION_STANDARD_FEE); let _utxo_lock = UTXO_LOCK.lock(); let staker_address_hex = qtum::contract_addr_from_utxo_addr(to_addr.clone())?; let delegation_output = self.add_delegation_output( staker_address_hex, - to_addr.hash, + to_addr.hash().clone(), fee, QRC20_GAS_LIMIT_DELEGATION, QRC20_GAS_PRICE_DEFAULT, @@ -269,9 +268,9 @@ impl QtumCoin { let utxo = self.as_ref(); let key_pair = utxo.priv_key_policy.activated_key_or_err()?; - let my_address = utxo.derivation_method.single_addr_or_err()?; + let my_address = utxo.derivation_method.single_addr_or_err().await?; - let (unspents, _) = self.get_unspent_ordered_list(my_address).await?; + let (unspents, _) = self.get_unspent_ordered_list(&my_address).await?; let mut gas_fee = 0; let mut outputs = Vec::with_capacity(contract_outputs.len()); for output in contract_outputs { @@ -280,6 +279,7 @@ impl QtumCoin { } let (unsigned, data) = UtxoTxBuilder::new(self) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_gas_fee(gas_fee) @@ -290,14 +290,7 @@ impl QtumCoin { DelegationError::from_generate_tx_error(gen_tx_error, self.ticker().to_string(), utxo.decimals) })?; - let prev_script = ScriptBuilder::build_p2pkh(&my_address.hash); - let signed = sign_tx( - unsigned, - key_pair, - prev_script, - utxo.conf.signature_version, - utxo.conf.fork_id, - )?; + let signed = sign_tx(unsigned, key_pair, utxo.conf.signature_version, utxo.conf.fork_id)?; let miner_fee = data.fee_amount + data.unused_change; let generated_tx = GenerateQrc20TxResult { @@ -322,8 +315,10 @@ impl QtumCoin { let my_balance_change = &received_by_me - &spent_by_me; Ok(TransactionDetails { - tx_hex: serialize(&generated_tx.signed).into(), - tx_hash: generated_tx.signed.hash().reversed().to_vec().to_tx_hash(), + tx: TransactionData::new_signed( + serialize(&generated_tx.signed).into(), + generated_tx.signed.hash().reversed().to_vec().to_tx_hash(), + ), from: vec![my_address_string], to: vec![to_address], total_amount: qtum_amount, diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index c4cb848dde..f5150e4a9a 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -2,21 +2,24 @@ #![cfg_attr(target_arch = "wasm32", allow(dead_code))] use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; -use crate::utxo::{output_script, sat_from_big_decimal, GetBlockHeaderError, GetConfirmedTxError, GetTxError, - GetTxHeightError}; -use crate::{big_decimal_from_sat_unsigned, NumConversError, RpcTransportEventHandler, RpcTransportEventHandlerShared}; +use crate::utxo::{output_script, output_script_p2pk, sat_from_big_decimal, GetBlockHeaderError, GetConfirmedTxError, + GetTxError, GetTxHeightError, NumConversResult, ScripthashNotification}; +use crate::{big_decimal_from_sat_unsigned, MyAddressError, NumConversError, RpcTransportEventHandler, + RpcTransportEventHandlerShared}; use async_trait::async_trait; -use chain::{BlockHeader, BlockHeaderBits, BlockHeaderNonce, OutPoint, Transaction as UtxoTx}; +use chain::{BlockHeader, BlockHeaderBits, BlockHeaderNonce, OutPoint, Transaction as UtxoTx, TransactionInput, + TxHashAlgo}; use common::custom_futures::{select_ok_sequential, timeout::FutureTimerExt}; -use common::custom_iter::{CollectInto, TryIntoGroupMap}; +use common::custom_iter::TryIntoGroupMap; use common::executor::{abortable_queue, abortable_queue::AbortableQueue, AbortableSystem, SpawnFuture, Timer}; use common::jsonrpc_client::{JsonRpcBatchClient, JsonRpcBatchResponse, JsonRpcClient, JsonRpcError, JsonRpcErrorType, JsonRpcId, JsonRpcMultiClient, JsonRpcRemoteAddr, JsonRpcRequest, JsonRpcRequestEnum, JsonRpcResponse, JsonRpcResponseEnum, JsonRpcResponseFut, RpcRes}; -use common::log::LogOnError; +use common::log::{debug, LogOnError}; use common::log::{error, info, warn}; use common::{median, now_float, now_ms, now_sec, OrdRange}; use derive_more::Display; +use enum_derives::EnumFromStringify; use futures::channel::oneshot as async_oneshot; use futures::compat::{Future01CompatExt, Stream01CompatExt}; use futures::future::{join_all, FutureExt, TryFutureExt}; @@ -28,12 +31,13 @@ use futures01::{Future, Sink, Stream}; use http::Uri; use itertools::Itertools; use keys::hash::H256; -use keys::{Address, Type as ScriptType}; +use keys::Address; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, BigInt, MmNumber}; use mm2_rpc::data::legacy::ElectrumProtocol; #[cfg(test)] use mocktopus::macros::*; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; +use script::Script; use serde_json::{self as json, Value as Json}; use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, Reader, SERIALIZE_TRANSACTION_WITNESS}; @@ -52,6 +56,8 @@ use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; use std::sync::Arc; use std::time::Duration; +use super::ScripthashNotificationSender; + cfg_native! { use futures::future::Either; use futures::io::Error; @@ -66,7 +72,6 @@ cfg_native! { use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadBuf}; use tokio::net::TcpStream; use tokio_rustls::{client::TlsStream, TlsConnector}; - use tokio_rustls::webpki::DnsNameRef; use webpki_roots::TLS_SERVER_ROOTS; } @@ -113,6 +118,15 @@ pub enum UtxoRpcClientEnum { Electrum(ElectrumClient), } +impl ToString for UtxoRpcClientEnum { + fn to_string(&self) -> String { + match self { + UtxoRpcClientEnum::Native(_) => "native".to_owned(), + UtxoRpcClientEnum::Electrum(_) => "electrum".to_owned(), + } + } +} + impl From for UtxoRpcClientEnum { fn from(client: ElectrumClient) -> UtxoRpcClientEnum { UtxoRpcClientEnum::Electrum(client) } } @@ -243,19 +257,34 @@ pub struct UnspentInfo { /// The block height transaction mined in. /// Note None if the transaction is not mined yet. pub height: Option, + /// The script pubkey of the UTXO + pub script: Script, } -impl From for UnspentInfo { - fn from(electrum: ElectrumUnspent) -> UnspentInfo { +impl UnspentInfo { + fn from_electrum(unspent: ElectrumUnspent, script: Script) -> UnspentInfo { UnspentInfo { outpoint: OutPoint { - hash: electrum.tx_hash.reversed().into(), - index: electrum.tx_pos, + hash: unspent.tx_hash.reversed().into(), + index: unspent.tx_pos, }, - value: electrum.value, - height: electrum.height, + value: unspent.value, + height: unspent.height, + script, } } + + fn from_native(unspent: NativeUnspent, decimals: u8, height: Option) -> NumConversResult { + Ok(UnspentInfo { + outpoint: OutPoint { + hash: unspent.txid.reversed().into(), + index: unspent.vout, + }, + value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, + height, + script: unspent.script_pub_key.0.into(), + }) + } } #[derive(Debug, PartialEq)] @@ -266,23 +295,26 @@ pub enum BlockHashOrHeight { #[derive(Debug, PartialEq)] pub struct SpentOutputInfo { - // The transaction spending the output - pub spending_tx: UtxoTx, - // The input index that spends the output + /// The input that spends the output + pub input: TransactionInput, + /// The index of spending input pub input_index: usize, - // The block hash or height the includes the spending transaction - // For electrum clients the block height will be returned, for native clients the block hash will be returned + /// The transaction spending the output + pub spending_tx: UtxoTx, + /// The block hash or height the includes the spending transaction + /// For electrum clients the block height will be returned, for native clients the block hash will be returned pub spent_in_block: BlockHashOrHeight, } pub type UtxoRpcResult = Result>; pub type UtxoRpcFut = Box> + Send + 'static>; -#[derive(Debug, Display)] +#[derive(Debug, Display, EnumFromStringify)] pub enum UtxoRpcError { Transport(JsonRpcError), ResponseParseError(JsonRpcError), InvalidResponse(String), + #[from_stringify("MyAddressError")] Internal(String), } @@ -306,6 +338,10 @@ impl From for UtxoRpcError { fn from(e: NumConversError) -> Self { UtxoRpcError::Internal(e.to_string()) } } +impl From for UtxoRpcError { + fn from(e: keys::Error) -> Self { UtxoRpcError::Internal(e.to_string()) } +} + impl UtxoRpcError { pub fn is_tx_not_found_error(&self) -> bool { if let UtxoRpcError::ResponseParseError(ref json_err) = self { @@ -345,6 +381,8 @@ pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { /// Submits the raw `tx` transaction (serialized, hex-encoded) to blockchain network. fn send_raw_transaction(&self, tx: BytesJson) -> UtxoRpcFut; + fn blockchain_scripthash_subscribe(&self, scripthash: String) -> UtxoRpcFut; + /// Returns raw transaction (serialized, hex-encoded) by the given `txid`. fn get_transaction_bytes(&self, txid: &H256Json) -> UtxoRpcFut; @@ -383,6 +421,7 @@ pub trait UtxoRpcClientOps: fmt::Debug + Send + Sync + 'static { script_pubkey: &[u8], vout: usize, from_block: BlockHashOrHeight, + tx_hash_algo: TxHashAlgo, ) -> Box, Error = String> + Send>; /// Get median time past for `count` blocks in the past including `starting_block` @@ -701,12 +740,12 @@ impl JsonRpcClient for NativeClientImpl { .body(Vec::from(request_body)) .map_err(|e| JsonRpcErrorType::InvalidRequest(e.to_string()))); - let event_handles = self.event_handlers.clone(); + let event_handlers = self.event_handlers.clone(); Box::new(slurp_req(http_request).boxed().compat().then( move |result| -> Result<(JsonRpcRemoteAddr, JsonRpcResponseEnum), JsonRpcErrorType> { let res = result.map_err(|e| e.into_inner())?; // measure now only body length, because the `hyper` crate doesn't allow to get total HTTP packet length - event_handles.on_incoming_response(&res.2); + event_handlers.on_incoming_response(&res.2); let body = std::str::from_utf8(&res.2).map_err(|e| JsonRpcErrorType::parse_error(&uri, e.to_string()))?; @@ -735,20 +774,10 @@ impl UtxoRpcClientOps for NativeClient { .list_unspent_impl(0, std::i32::MAX, vec![address.to_string()]) .map_to_mm_fut(UtxoRpcError::from) .and_then(move |unspents| { - let unspents: UtxoRpcResult> = unspents - .into_iter() - .map(|unspent| { - Ok(UnspentInfo { - outpoint: OutPoint { - hash: unspent.txid.reversed().into(), - index: unspent.vout, - }, - value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, - height: None, - }) - }) - .collect(); unspents + .into_iter() + .map(|unspent| Ok(UnspentInfo::from_native(unspent, decimals, None)?)) + .collect::>() }); Box::new(fut) } @@ -776,14 +805,7 @@ impl UtxoRpcClientOps for NativeClient { UtxoRpcError::InvalidResponse(format!("Unexpected address '{}'", unspent.address)) })? .clone(); - let unspent_info = UnspentInfo { - outpoint: OutPoint { - hash: unspent.txid.reversed().into(), - index: unspent.vout, - }, - value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, - height: None, - }; + let unspent_info = UnspentInfo::from_native(unspent, decimals, None)?; Ok((orig_address, unspent_info)) }) // Collect `(Address, UnspentInfo)` items into `HashMap>` grouped by the addresses. @@ -806,6 +828,13 @@ impl UtxoRpcClientOps for NativeClient { Box::new(rpc_func!(self, "sendrawtransaction", tx).map_to_mm_fut(UtxoRpcError::from)) } + fn blockchain_scripthash_subscribe(&self, _scripthash: String) -> UtxoRpcFut { + Box::new(futures01::future::err( + UtxoRpcError::Internal("blockchain_scripthash_subscribe` is not supported for Native Clients".to_owned()) + .into(), + )) + } + fn get_transaction_bytes(&self, txid: &H256Json) -> UtxoRpcFut { Box::new(self.get_raw_transaction_bytes(txid).map_to_mm_fut(UtxoRpcError::from)) } @@ -885,6 +914,7 @@ impl UtxoRpcClientOps for NativeClient { _script_pubkey: &[u8], vout: usize, from_block: BlockHashOrHeight, + tx_hash_algo: TxHashAlgo, ) -> Box, Error = String> + Send> { let selfi = self.clone(); let fut = async move { @@ -899,14 +929,17 @@ impl UtxoRpcClientOps for NativeClient { .filter(|tx| !tx.is_conflicting()) { let maybe_spend_tx_bytes = try_s!(selfi.get_raw_transaction_bytes(&transaction.txid).compat().await); - let maybe_spend_tx: UtxoTx = + let mut maybe_spend_tx: UtxoTx = try_s!(deserialize(maybe_spend_tx_bytes.as_slice()).map_err(|e| ERRL!("{:?}", e))); + maybe_spend_tx.tx_hash_algo = tx_hash_algo; + drop_mutability!(maybe_spend_tx); for (index, input) in maybe_spend_tx.inputs.iter().enumerate() { if input.previous_output.hash == tx_hash && input.previous_output.index == vout as u32 { return Ok(Some(SpentOutputInfo { - spending_tx: maybe_spend_tx, + input: input.clone(), input_index: index, + spending_tx: maybe_spend_tx, spent_in_block: BlockHashOrHeight::Hash(transaction.blockhash), })); } @@ -1432,12 +1465,21 @@ fn addr_to_socket_addr(input: &str) -> Result { } } +#[cfg(not(target_arch = "wasm32"))] +fn server_name_from_domain(dns_name: &str) -> Result { + match ServerName::try_from(dns_name) { + Ok(dns_name) if matches!(dns_name, ServerName::DnsName(_)) => Ok(dns_name), + _ => ERR!("Couldn't parse DNS name from '{}'", dns_name), + } +} + /// Attempts to process the request (parse url, etc), build up the config and create new electrum connection /// The function takes `abortable_system` that will be used to spawn Electrum's related futures. #[cfg(not(target_arch = "wasm32"))] pub fn spawn_electrum( req: &ElectrumRpcRequest, event_handlers: Vec, + scripthash_notification_sender: &ScripthashNotificationSender, abortable_system: AbortableQueue, ) -> Result { let config = match req.protocol { @@ -1448,8 +1490,7 @@ pub fn spawn_electrum( .host() .ok_or(ERRL!("Couldn't retrieve host from addr {}", req.url))?; - // check the dns name - try_s!(DnsNameRef::try_from_ascii_str(host)); + try_s!(server_name_from_domain(host)); ElectrumConfig::SSL { dns_name: host.into(), @@ -1465,6 +1506,7 @@ pub fn spawn_electrum( req.url.clone(), config, event_handlers, + scripthash_notification_sender, abortable_system, )) } @@ -1475,6 +1517,7 @@ pub fn spawn_electrum( pub fn spawn_electrum( req: &ElectrumRpcRequest, event_handlers: Vec, + scripthash_notification_sender: &ScripthashNotificationSender, abortable_system: AbortableQueue, ) -> Result { let mut url = req.url.clone(); @@ -1503,7 +1546,13 @@ pub fn spawn_electrum( }, }; - Ok(electrum_connect(url, config, event_handlers, abortable_system)) + Ok(electrum_connect( + url, + config, + event_handlers, + scripthash_notification_sender, + abortable_system, + )) } /// Represents the active Electrum connection to selected address @@ -1611,6 +1660,10 @@ pub struct ElectrumClientImpl { /// Please also note that this abortable system is a subsystem of [`UtxoCoinFields::abortable_system`]. abortable_system: AbortableQueue, negotiate_version: bool, + /// This is used for balance event streaming implementation for UTXOs. + /// If balance event streaming isn't enabled, this value will always be `None`; otherwise, + /// it will be used for sending scripthash messages to trigger re-connections, re-fetching the balances, etc. + pub(crate) scripthash_notification_sender: ScripthashNotificationSender, } async fn electrum_request_multi( @@ -1700,7 +1753,12 @@ impl ElectrumClientImpl { /// Create an Electrum connection and spawn a green thread actor to handle it. pub async fn add_server(&self, req: &ElectrumRpcRequest) -> Result<(), String> { let subsystem = try_s!(self.abortable_system.create_subsystem()); - let connection = try_s!(spawn_electrum(req, self.event_handlers.clone(), subsystem)); + let connection = try_s!(spawn_electrum( + req, + self.event_handlers.clone(), + &self.scripthash_notification_sender, + subsystem, + )); self.connections.lock().await.push(connection); Ok(()) } @@ -1757,6 +1815,13 @@ impl ElectrumClientImpl { .find(|con| con.addr == server_addr) .ok_or(ERRL!("Unknown electrum address {}", server_addr))?; con.set_protocol_version(version).await; + + if let Some(sender) = &self.scripthash_notification_sender { + sender + .unbounded_send(ScripthashNotification::RefreshSubscriptions) + .map_err(|e| ERRL!("Failed sending scripthash message. {}", e))?; + } + Ok(()) } @@ -1787,6 +1852,8 @@ impl Deref for ElectrumClient { const BLOCKCHAIN_HEADERS_SUB_ID: &str = "blockchain.headers.subscribe"; +const BLOCKCHAIN_SCRIPTHASH_SUB_ID: &str = "blockchain.scripthash.subscribe"; + impl UtxoJsonRpcClientInfo for ElectrumClient { fn coin_name(&self) -> &str { self.coin_ticker.as_str() } } @@ -2104,7 +2171,7 @@ impl ElectrumClient { Ok(headers) => headers, Err(e) => return MmError::err(UtxoRpcError::InvalidResponse(format!("{:?}", e))), }; - let mut block_registry: HashMap = HashMap::new(); + let mut block_registry: HashMap = HashMap::with_capacity(block_headers.len()); let mut starting_height = from_height; for block_header in &block_headers { block_registry.insert(starting_height, block_header.clone()); @@ -2154,10 +2221,10 @@ impl ElectrumClient { return Ok((servers_with_max_count, *max_block_count)); } - return Err(MmError::new(UtxoRpcError::Internal(format!( + Err(MmError::new(UtxoRpcError::Internal(format!( "Couldn't get block count from any server for {}, responses: {:?}", &selfi.coin_ticker, responses - )))); + )))) }; Box::new(fut.boxed().compat()) @@ -2169,48 +2236,67 @@ impl ElectrumClient { #[cfg_attr(test, mockable)] impl UtxoRpcClientOps for ElectrumClient { fn list_unspent(&self, address: &Address, _decimals: u8) -> UtxoRpcFut> { - let script = output_script(address, ScriptType::P2PKH); - let script_hash = electrum_script_hash(&script); - Box::new( - self.scripthash_list_unspent(&hex::encode(script_hash)) - .map_to_mm_fut(UtxoRpcError::from) - .map(move |unspents| { + let mut output_scripts = vec![try_f!(output_script(address))]; + + // If the plain pubkey is available, fetch the UTXOs found in P2PK outputs as well (if any). + if let Some(pubkey) = address.pubkey() { + let p2pk_output_script = output_script_p2pk(pubkey); + output_scripts.push(p2pk_output_script); + } + + let this = self.clone(); + let fut = async move { + let hashes = output_scripts + .iter() + .map(|s| hex::encode(electrum_script_hash(s))) + .collect(); + let unspents = this.scripthash_list_unspent_batch(hashes).compat().await?; + + let unspents = unspents + .into_iter() + .zip(output_scripts) + .flat_map(|(unspents, output_script)| { unspents - .iter() - .map(|unspent| UnspentInfo { - outpoint: OutPoint { - hash: unspent.tx_hash.reversed().into(), - index: unspent.tx_pos, - }, - value: unspent.value, - height: unspent.height, - }) - .collect() - }), - ) + .into_iter() + .map(move |unspent| UnspentInfo::from_electrum(unspent, output_script.clone())) + }) + .collect(); + Ok(unspents) + }; + + Box::new(fut.boxed().compat()) } fn list_unspent_group(&self, addresses: Vec
, _decimals: u8) -> UtxoRpcFut { - let script_hashes = addresses + let output_scripts = try_f!(addresses .iter() - .map(|addr| { - let script = output_script(addr, ScriptType::P2PKH); - let script_hash = electrum_script_hash(&script); - hex::encode(script_hash) - }) - .collect(); + .map(output_script) + .collect::, keys::Error>>()); let this = self.clone(); let fut = async move { - let unspents = this.scripthash_list_unspent_batch(script_hashes).compat().await?; + let hashes = output_scripts + .iter() + .map(|s| hex::encode(electrum_script_hash(s))) + .collect(); + let unspents = this.scripthash_list_unspent_batch(hashes).compat().await?; + + let unspents: Vec> = unspents + .into_iter() + .zip(output_scripts) + .map(|(unspents, output_script)| { + unspents + .into_iter() + .map(|unspent| UnspentInfo::from_electrum(unspent, output_script.clone())) + .collect() + }) + .collect(); let unspent_map = addresses .into_iter() // `scripthash_list_unspent_batch` returns `ScriptHashUnspents` elements in the same order in which they were requested. // So we can zip `addresses` and `unspents` into one iterator. .zip(unspents) - // Map `(Address, Vec)` pairs into `(Address, Vec)`. - .map(|(address, electrum_unspents)| (address, electrum_unspents.collect_into())) .collect(); Ok(unspent_map) }; @@ -2236,6 +2322,10 @@ impl UtxoRpcClientOps for ElectrumClient { ) } + fn blockchain_scripthash_subscribe(&self, scripthash: String) -> UtxoRpcFut { + Box::new(rpc_func!(self, BLOCKCHAIN_SCRIPTHASH_SUB_ID, scripthash).map_to_mm_fut(UtxoRpcError::from)) + } + /// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-get /// returns transaction bytes by default fn get_transaction_bytes(&self, txid: &H256Json) -> UtxoRpcFut { @@ -2269,21 +2359,45 @@ impl UtxoRpcClientOps for ElectrumClient { } fn display_balance(&self, address: Address, decimals: u8) -> RpcRes { - let hash = electrum_script_hash(&output_script(&address, ScriptType::P2PKH)); - let hash_str = hex::encode(hash); - Box::new( - self.scripthash_get_balance(&hash_str) - .map(move |electrum_balance| electrum_balance.to_big_decimal(decimals)), - ) + let output_script = try_f!(output_script(&address).map_err(|err| JsonRpcError::new( + UtxoJsonRpcClientInfo::client_info(self), + rpc_req!(self, "blockchain.scripthash.get_balance").into(), + JsonRpcErrorType::Internal(err.to_string()) + ))); + let mut hashes = vec![hex::encode(electrum_script_hash(&output_script))]; + + // If the plain pubkey is available, fetch the balance found in P2PK output as well (if any). + if let Some(pubkey) = address.pubkey() { + let p2pk_output_script = output_script_p2pk(pubkey); + hashes.push(hex::encode(electrum_script_hash(&p2pk_output_script))); + } + + let this = self.clone(); + let fut = async move { + Ok(this + .scripthash_get_balances(hashes) + .compat() + .await? + .into_iter() + .fold(BigDecimal::from(0), |sum, electrum_balance| { + sum + electrum_balance.to_big_decimal(decimals) + })) + }; + Box::new(fut.boxed().compat()) } fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut> { let this = self.clone(); let fut = async move { - let hashes = addresses.iter().map(|address| { - let hash = electrum_script_hash(&output_script(address, ScriptType::P2PKH)); - hex::encode(hash) - }); + let hashes = addresses + .iter() + .map(|address| { + let output_script = output_script(address)?; + let hash = electrum_script_hash(&output_script); + + Ok(hex::encode(hash)) + }) + .collect::, keys::Error>>()?; let electrum_balances = this.scripthash_get_balances(hashes).compat().await?; let balances = electrum_balances @@ -2323,6 +2437,7 @@ impl UtxoRpcClientOps for ElectrumClient { script_pubkey: &[u8], vout: usize, _from_block: BlockHashOrHeight, + tx_hash_algo: TxHashAlgo, ) -> Box, Error = String> + Send> { let selfi = self.clone(); let script_hash = hex::encode(electrum_script_hash(script_pubkey)); @@ -2336,13 +2451,17 @@ impl UtxoRpcClientOps for ElectrumClient { for item in history.iter() { let transaction = try_s!(selfi.get_transaction_bytes(&item.tx_hash).compat().await); - let maybe_spend_tx: UtxoTx = try_s!(deserialize(transaction.as_slice()).map_err(|e| ERRL!("{:?}", e))); + let mut maybe_spend_tx: UtxoTx = + try_s!(deserialize(transaction.as_slice()).map_err(|e| ERRL!("{:?}", e))); + maybe_spend_tx.tx_hash_algo = tx_hash_algo; + drop_mutability!(maybe_spend_tx); for (index, input) in maybe_spend_tx.inputs.iter().enumerate() { if input.previous_output.hash == tx_hash && input.previous_output.index == vout as u32 { return Ok(Some(SpentOutputInfo { - spending_tx: maybe_spend_tx, + input: input.clone(), input_index: index, + spending_tx: maybe_spend_tx, spent_in_block: BlockHashOrHeight::Height(item.height), })); } @@ -2396,6 +2515,7 @@ impl ElectrumClientImpl { block_headers_storage: BlockHeaderStorage, abortable_system: AbortableQueue, negotiate_version: bool, + scripthash_notification_sender: ScripthashNotificationSender, ) -> ElectrumClientImpl { let protocol_version = OrdRange::new(1.2, 1.4).unwrap(); ElectrumClientImpl { @@ -2409,6 +2529,7 @@ impl ElectrumClientImpl { block_headers_storage, abortable_system, negotiate_version, + scripthash_notification_sender, } } @@ -2419,6 +2540,7 @@ impl ElectrumClientImpl { protocol_version: OrdRange, block_headers_storage: BlockHeaderStorage, abortable_system: AbortableQueue, + scripthash_notification_sender: ScripthashNotificationSender, ) -> ElectrumClientImpl { ElectrumClientImpl { protocol_version, @@ -2428,6 +2550,7 @@ impl ElectrumClientImpl { block_headers_storage, abortable_system, false, + scripthash_notification_sender, ) } } @@ -2438,17 +2561,25 @@ fn rx_to_stream(rx: mpsc::Receiver>) -> impl Stream, Erro rx.map_err(|_| panic!("errors not possible on rx")) } -async fn electrum_process_json(raw_json: Json, arc: &JsonRpcPendingRequestsShared) { +async fn electrum_process_json( + raw_json: Json, + arc: &JsonRpcPendingRequestsShared, + scripthash_notification_sender: &ScripthashNotificationSender, +) { // detect if we got standard JSONRPC response or subscription response as JSONRPC request #[derive(Deserialize)] #[serde(untagged)] enum ElectrumRpcResponseEnum { + /// The subscription response as JSONRPC request. + /// + /// NOTE Because JsonRpcResponse uses default values for each of its field, + /// this variant has to stay at top in this enumeration to be properly deserialized + /// from serde. + SubscriptionNotification(JsonRpcRequest), /// The standard JSONRPC single response. SingleResponse(JsonRpcResponse), /// The batch of standard JSONRPC responses. BatchResponses(JsonRpcBatchResponse), - /// The subscription response as JSONRPC request. - SubscriptionNotification(JsonRpcRequest), } let response: ElectrumRpcResponseEnum = match json::from_value(raw_json) { @@ -2465,6 +2596,25 @@ async fn electrum_process_json(raw_json: Json, arc: &JsonRpcPendingRequestsShare ElectrumRpcResponseEnum::SubscriptionNotification(req) => { let id = match req.method.as_ref() { BLOCKCHAIN_HEADERS_SUB_ID => BLOCKCHAIN_HEADERS_SUB_ID, + BLOCKCHAIN_SCRIPTHASH_SUB_ID => { + let scripthash = match req.params.first() { + Some(t) => t.as_str().unwrap_or_default(), + None => { + debug!("Notification must contain the scripthash value."); + return; + }, + }; + + if let Some(sender) = scripthash_notification_sender { + debug!("Sending scripthash message"); + if let Err(e) = sender.unbounded_send(ScripthashNotification::Triggered(scripthash.to_string())) + { + error!("Failed sending scripthash message. {e}"); + return; + }; + }; + BLOCKCHAIN_SCRIPTHASH_SUB_ID + }, _ => { error!("Couldn't get id of request {:?}", req); return; @@ -2487,7 +2637,11 @@ async fn electrum_process_json(raw_json: Json, arc: &JsonRpcPendingRequestsShare } } -async fn electrum_process_chunk(chunk: &[u8], arc: &JsonRpcPendingRequestsShared) { +async fn electrum_process_chunk( + chunk: &[u8], + arc: &JsonRpcPendingRequestsShared, + scripthash_notification_sender: ScripthashNotificationSender, +) { // we should split the received chunk because we can get several responses in 1 chunk. let split = chunk.split(|item| *item == b'\n'); for chunk in split { @@ -2500,7 +2654,7 @@ async fn electrum_process_chunk(chunk: &[u8], arc: &JsonRpcPendingRequestsShared return; }, }; - electrum_process_json(raw_json, arc).await + electrum_process_json(raw_json, arc, &scripthash_notification_sender).await } } } @@ -2596,9 +2750,8 @@ async fn electrum_last_chunk_loop(last_chunk: Arc) { fn rustls_client_config(unsafe_conf: bool) -> Arc { let mut cert_store = RootCertStore::empty(); - cert_store.add_server_trust_anchors( + cert_store.add_trust_anchors( TLS_SERVER_ROOTS - .0 .iter() .map(|ta| OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints)), ); @@ -2629,6 +2782,7 @@ async fn connect_loop( responses: JsonRpcPendingRequestsShared, connection_tx: Arc>>>>, event_handlers: Vec, + scripthash_notification_sender: ScripthashNotificationSender, _spawner: Spawner, ) -> Result<(), ()> { let delay = Arc::new(AtomicU64::new(0)); @@ -2639,7 +2793,9 @@ async fn connect_loop( Timer::sleep(current_delay as f64).await; }; - let socket_addr = try_loop!(addr_to_socket_addr(&addr), addr, delay); + let socket_addr = addr_to_socket_addr(&addr).map_err(|e| { + error!("{:?} error {:?}", addr, e); + })?; let connect_f = match config.clone() { ElectrumConfig::TCP => Either::Left(TcpStream::connect(&socket_addr).map_ok(ElectrumStream::Tcp)), @@ -2652,14 +2808,15 @@ async fn connect_loop( } else { TlsConnector::from(SAFE_TLS_CONFIG.clone()) }; - - Either::Right(TcpStream::connect(&socket_addr).and_then(move |stream| { - // Can use `unwrap` cause `dns_name` is pre-checked. - let dns = ServerName::try_from(dns_name.as_str()) - .map_err(|e| format!("{:?}", e)) - .unwrap(); - tls_connector.connect(dns, stream).map_ok(ElectrumStream::Tls) - })) + // The address should always be correct since we checked it beforehand in initializaiton. + let dns = server_name_from_domain(dns_name.as_str()).map_err(|e| { + error!("{:?} error {:?}", addr, e); + })?; + + Either::Right( + TcpStream::connect(&socket_addr) + .and_then(move |stream| tls_connector.connect(dns, stream).map_ok(ElectrumStream::Tls)), + ) }, }; @@ -2682,6 +2839,7 @@ async fn connect_loop( let delay = delay.clone(); let addr = addr.clone(); let responses = responses.clone(); + let scripthash_notification_sender = scripthash_notification_sender.clone(); let event_handlers = event_handlers.clone(); async move { let mut buffer = String::with_capacity(1024); @@ -2705,7 +2863,7 @@ async fn connect_loop( event_handlers.on_incoming_response(buffer.as_bytes()); last_chunk.store(now_ms(), AtomicOrdering::Relaxed); - electrum_process_chunk(buffer.as_bytes(), &responses).await; + electrum_process_chunk(buffer.as_bytes(), &responses, scripthash_notification_sender.clone()).await; buffer.clear(); } } @@ -2749,6 +2907,7 @@ async fn connect_loop( responses: JsonRpcPendingRequestsShared, connection_tx: Arc>>>>, event_handlers: Vec, + scripthash_notification_sender: ScripthashNotificationSender, spawner: Spawner, ) -> Result<(), ()> { use std::sync::atomic::AtomicUsize; @@ -2757,7 +2916,7 @@ async fn connect_loop( static ref CONN_IDX: Arc = Arc::new(AtomicUsize::new(0)); } - use mm2_net::wasm_ws::ws_transport; + use mm2_net::wasm::wasm_ws::ws_transport; let delay = Arc::new(AtomicU64::new(0)); loop { @@ -2783,6 +2942,7 @@ async fn connect_loop( let delay = delay.clone(); let addr = addr.clone(); let responses = responses.clone(); + let scripthash_notification_sender = scripthash_notification_sender.clone(); let event_handlers = event_handlers.clone(); async move { while let Some(incoming_res) = transport_rx.next().await { @@ -2795,7 +2955,7 @@ async fn connect_loop( let incoming_str = incoming_json.to_string(); event_handlers.on_incoming_response(incoming_str.as_bytes()); - electrum_process_json(incoming_json, &responses).await; + electrum_process_json(incoming_json, &responses, &scripthash_notification_sender).await; }, Err(e) => { error!("{} error: {:?}", addr, e); @@ -2855,6 +3015,7 @@ fn electrum_connect( addr: String, config: ElectrumConfig, event_handlers: Vec, + scripthash_notification_sender: &ScripthashNotificationSender, abortable_system: AbortableQueue, ) -> ElectrumConnection { let responses = Arc::new(AsyncMutex::new(JsonRpcPendingRequests::default())); @@ -2867,6 +3028,7 @@ fn electrum_connect( responses.clone(), tx.clone(), event_handlers, + scripthash_notification_sender.clone(), spawner.clone(), ) .then(|_| futures::future::ready(())); diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index da2a51224b..c559449c22 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -3,7 +3,7 @@ //! Tracking issue: https://github.com/KomodoPlatform/atomicDEX-API/issues/701 //! More info about the protocol and implementation guides can be found at https://slp.dev/ -use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut}; +use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentResult}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::bch::BchCoin; @@ -13,13 +13,14 @@ use crate::utxo::utxo_common::{self, big_decimal_from_sat_unsigned, payment_scri use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, AdditionalTxData, BroadcastTxErr, FeePolicy, GenerateTxError, RecentlySpentOutPointsGuard, UtxoCoinConf, UtxoCoinFields, UtxoCommonOps, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps}; -use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DexFee, - FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, - NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, - PaymentInstructionsErr, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RefundError, - RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, +use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, DerivationMethod, + DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MakerSwapTakerCoin, MarketCoinOps, MmCoin, + MmCoinEnum, NegotiateSwapContractAddrErr, NumConversError, PaymentInstructionArgs, PaymentInstructions, + PaymentInstructionsErr, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, + RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, + SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TakerSwapMakerCoin, TradeFee, TradePreimageError, + TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, @@ -27,6 +28,8 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, C WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_trait::async_trait; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; use bitcrypto::dhash160; use chain::constants::SEQUENCE_FINAL; use chain::{OutPoint, TransactionOutput}; @@ -452,10 +455,11 @@ impl SlpToken { bch_unspent: UnspentInfo { outpoint: OutPoint { hash: tx.hash(), - index: 1, + index: SLP_SWAP_VOUT as u32, }, value: 0, height: None, + script: tx.outputs[SLP_SWAP_VOUT].script_pubkey.clone().into(), }, slp_amount: slp_satoshis, }; @@ -500,20 +504,22 @@ impl SlpToken { .time_lock .try_into() .map_to_mm(ValidatePaymentError::TimelockOverflow)?; - let validate_fut = utxo_common::validate_payment( + utxo_common::validate_payment( self.platform_coin.clone(), - tx, + &tx, SLP_SWAP_VOUT, first_pub, htlc_keypair.public(), - &input.secret_hash, + SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &input.secret_hash, + }, self.platform_dust_dec(), None, time_lock, wait_until_sec(60), input.confirmations, - ); - validate_fut.compat().await + ) + .await } pub async fn refund_htlc( @@ -550,8 +556,9 @@ impl SlpToken { hash: tx.hash(), index: SLP_SWAP_VOUT as u32, }, - value: tx.outputs[1].value, + value: tx.outputs[SLP_SWAP_VOUT].value, height: None, + script: tx.outputs[SLP_SWAP_VOUT].script_pubkey.clone().into(), }, slp_amount, }; @@ -601,8 +608,9 @@ impl SlpToken { hash: tx.hash(), index: SLP_SWAP_VOUT as u32, }, - value: tx.outputs[1].value, + value: tx.outputs[SLP_SWAP_VOUT].value, height: None, + script: tx.outputs[SLP_SWAP_VOUT].script_pubkey.clone().into(), }, slp_amount, }; @@ -641,6 +649,7 @@ impl SlpToken { let (_, bch_inputs, _recently_spent) = self.slp_unspents_for_spend().await?; let (mut unsigned, _) = UtxoTxBuilder::new(&self.platform_coin) + .await .add_required_inputs(std::iter::once(p2sh_utxo.bch_unspent)) .add_available_inputs(bch_inputs) .add_outputs(outputs) @@ -671,7 +680,6 @@ impl SlpToken { &unsigned, i, my_key_pair, - my_script_pubkey.clone(), self.platform_coin.as_ref().conf.signature_version, self.platform_coin.as_ref().conf.fork_id, ) @@ -1084,11 +1092,19 @@ impl UtxoTxGenerationOps for SlpToken { } } +#[async_trait] impl MarketCoinOps for SlpToken { fn ticker(&self) -> &str { &self.conf.ticker } fn my_address(&self) -> MmResult { - let my_address = self.as_ref().derivation_method.single_addr_or_err()?; + let my_address = match self.platform_coin.as_ref().derivation_method { + DerivationMethod::SingleAddress(ref my_address) => my_address, + DerivationMethod::HDWallet(_) => { + return MmError::err(MyAddressError::UnexpectedDerivationMethod( + "'my_address' is deprecated for HD wallets".to_string(), + )) + }, + }; let slp_address = self .platform_coin .slp_address(my_address) @@ -1096,7 +1112,7 @@ impl MarketCoinOps for SlpToken { slp_address.encode().map_to_mm(MyAddressError::InternalError) } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(self.platform_coin.as_ref())?; Ok(pubkey.to_string()) } @@ -1113,7 +1129,7 @@ impl MarketCoinOps for SlpToken { let message_hash = self .sign_message_hash(message) .ok_or(VerificationError::PrefixNotFound)?; - let signature = CompactSignature::from(base64::decode(signature)?); + let signature = CompactSignature::from(STANDARD.decode(signature)?); let pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; let address_from_pubkey = self.platform_coin.address_from_pubkey(&pubkey); let slp_address = self @@ -1163,13 +1179,18 @@ impl MarketCoinOps for SlpToken { Box::new(fut.boxed().compat()) } + #[inline(always)] + async fn sign_raw_tx(&self, args: &SignRawTransactionRequest) -> RawTransactionResult { + utxo_common::sign_raw_tx(self, args).await + } + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { self.platform_coin.wait_for_confirmations(input) } fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { utxo_common::wait_for_output_spend( - self.platform_coin.as_ref(), + self.clone(), args.tx_bytes, SLP_SWAP_VOUT, args.from_block, @@ -1189,11 +1210,13 @@ impl MarketCoinOps for SlpToken { fn min_tx_amount(&self) -> BigDecimal { big_decimal_from_sat_unsigned(1, self.decimals()) } fn min_trading_vol(&self) -> MmNumber { big_decimal_from_sat_unsigned(1, self.decimals()).into() } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] impl SwapOps for SlpToken { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let coin = self.clone(); let fee_pubkey = try_tx_fus!(Public::from_slice(fee_addr)); let script_pubkey = ScriptBuilder::build_p2pkh(&fee_pubkey.address_hash().into()).into(); @@ -1252,48 +1275,47 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { let tx = maker_spends_payment_args.other_payment_tx.to_owned(); - let taker_pub = try_tx_fus!(Public::from_slice(maker_spends_payment_args.other_pubkey)); + let taker_pub = try_tx_s!(Public::from_slice(maker_spends_payment_args.other_pubkey)); let secret = maker_spends_payment_args.secret.to_owned(); let secret_hash = maker_spends_payment_args.secret_hash.to_owned(); let htlc_keypair = self.derive_htlc_key_pair(maker_spends_payment_args.swap_unique_data); - let coin = self.clone(); - let time_lock = try_tx_fus!(maker_spends_payment_args.time_lock.try_into()); - - let fut = async move { - let tx = try_tx_s!( - coin.spend_htlc(&tx, &taker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) - .await - ); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat()) + let time_lock = try_tx_s!(maker_spends_payment_args.time_lock.try_into()); + let tx = try_tx_s!( + self.spend_htlc(&tx, &taker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) + .await + ); + Ok(tx.into()) } - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { let tx = taker_spends_payment_args.other_payment_tx.to_owned(); - let maker_pub = try_tx_fus!(Public::from_slice(taker_spends_payment_args.other_pubkey)); + let maker_pub = try_tx_s!(Public::from_slice(taker_spends_payment_args.other_pubkey)); let secret = taker_spends_payment_args.secret.to_owned(); let secret_hash = taker_spends_payment_args.secret_hash.to_owned(); let htlc_keypair = self.derive_htlc_key_pair(taker_spends_payment_args.swap_unique_data); - let coin = self.clone(); - let time_lock = try_tx_fus!(taker_spends_payment_args.time_lock.try_into()); - - let fut = async move { - let tx = try_tx_s!( - coin.spend_htlc(&tx, &maker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) - .await - ); - Ok(tx.into()) - }; - Box::new(fut.boxed().compat()) + let time_lock = try_tx_s!(taker_spends_payment_args.time_lock.try_into()); + let tx = try_tx_s!( + self.spend_htlc(&tx, &maker_pub, time_lock, &secret, &secret_hash, &htlc_keypair) + .await + ); + Ok(tx.into()) } async fn send_taker_refunds_payment(&self, taker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { let tx = taker_refunds_payment_args.payment_tx.to_owned(); let maker_pub = try_tx_s!(Public::from_slice(taker_refunds_payment_args.other_pubkey)); - let secret_hash = taker_refunds_payment_args.secret_hash.to_owned(); + let secret_hash = match taker_refunds_payment_args.tx_type_with_secret_hash { + SwapTxTypeWithSecretHash::TakerOrMakerPayment { maker_secret_hash } => maker_secret_hash.to_owned(), + unsupported => return Err(TransactionErr::Plain(ERRL!("SLP doesn't support {:?}", unsupported))), + }; let htlc_keypair = self.derive_htlc_key_pair(taker_refunds_payment_args.swap_unique_data); let time_lock = try_tx_s!(taker_refunds_payment_args.time_lock.try_into()); @@ -1307,7 +1329,10 @@ impl SwapOps for SlpToken { async fn send_maker_refunds_payment(&self, maker_refunds_payment_args: RefundPaymentArgs<'_>) -> TransactionResult { let tx = maker_refunds_payment_args.payment_tx.to_owned(); let taker_pub = try_tx_s!(Public::from_slice(maker_refunds_payment_args.other_pubkey)); - let secret_hash = maker_refunds_payment_args.secret_hash.to_owned(); + let secret_hash = match maker_refunds_payment_args.tx_type_with_secret_hash { + SwapTxTypeWithSecretHash::TakerOrMakerPayment { maker_secret_hash } => maker_secret_hash.to_owned(), + unsupported => return Err(TransactionErr::Plain(ERRL!("SLP doesn't support {:?}", unsupported))), + }; let htlc_keypair = self.derive_htlc_key_pair(maker_refunds_payment_args.swap_unique_data); let time_lock = try_tx_s!(maker_refunds_payment_args.time_lock.try_into()); @@ -1338,22 +1363,12 @@ impl SwapOps for SlpToken { Box::new(fut.boxed().compat()) } - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - let coin = self.clone(); - let fut = async move { - coin.validate_htlc(input).await?; - Ok(()) - }; - Box::new(fut.boxed().compat()) + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + self.validate_htlc(input).await } - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - let coin = self.clone(); - let fut = async move { - coin.validate_htlc(input).await?; - Ok(()) - }; - Box::new(fut.boxed().compat()) + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + self.validate_htlc(input).await } #[inline] @@ -1603,7 +1618,12 @@ impl MmCoin for SlpToken { fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { let coin = self.clone(); let fut = async move { - let my_address = coin.platform_coin.as_ref().derivation_method.single_addr_or_err()?; + if req.from.is_some() { + return MmError::err(WithdrawError::UnsupportedError( + "Withdraw from a specific address is not supported for slp yet".to_owned(), + )); + } + let key_pair = coin.platform_coin.as_ref().priv_key_policy.activated_key_or_err()?; let address = CashAddress::decode(&req.to).map_to_mm(WithdrawError::InvalidAddress)?; @@ -1641,6 +1661,7 @@ impl MmCoin for SlpToken { let slp_output = SlpOutput { amount, script_pubkey }; let (slp_preimage, _) = coin.generate_slp_tx_preimage(vec![slp_output]).await?; let mut tx_builder = UtxoTxBuilder::new(&coin.platform_coin) + .await .add_required_inputs(slp_preimage.slp_inputs.into_iter().map(|slp| slp.bch_unspent)) .add_available_inputs(slp_preimage.available_bch_inputs) .add_outputs(slp_preimage.outputs); @@ -1669,11 +1690,9 @@ impl MmCoin for SlpToken { WithdrawError::from_generate_tx_error(gen_tx_error, coin.platform_ticker().into(), platform_decimals) })?; - let prev_script = ScriptBuilder::build_p2pkh(&my_address.hash); let signed = sign_tx( unsigned, key_pair, - prev_script, coin.platform_conf().signature_version, coin.platform_conf().fork_id, )?; @@ -1694,9 +1713,8 @@ impl MmCoin for SlpToken { let tx_hash: BytesJson = signed.hash().reversed().take().to_vec().into(); let details = TransactionDetails { - tx_hex: serialize(&signed).into(), internal_id: tx_hash.clone(), - tx_hash: tx_hash.to_tx_hash(), + tx: TransactionData::new_signed(serialize(&signed).into(), tx_hash.to_tx_hash()), from: vec![my_address_string], to: vec![to_address], total_amount, @@ -1767,6 +1785,7 @@ impl MmCoin for SlpToken { &self, value: TradePreimageValue, stage: FeeApproxStage, + _include_refund_fee: bool, // refund fee is taken from swap output ) -> TradePreimageResult { let slp_amount = match value { TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => { @@ -2134,8 +2153,8 @@ mod slp_tests { let token_id = H256::from("bb309e48930671582bea508f9a1d9b491e49b69be3d6f372dc08da2ac6e90eb7"); let fusd = SlpToken::new(4, "FUSD".into(), token_id, bch.clone(), 0).unwrap(); - let bch_address = bch.as_ref().derivation_method.unwrap_single_addr(); - let (unspents, recently_spent) = block_on(bch.get_unspent_ordered_list(bch_address)).unwrap(); + let bch_address = block_on(bch.as_ref().derivation_method.unwrap_single_addr()); + let (unspents, recently_spent) = block_on(bch.get_unspent_ordered_list(&bch_address)).unwrap(); let secret_hash = hex::decode("5d9e149ad9ccb20e9f931a69b605df2ffde60242").unwrap(); let other_pub = hex::decode("036879df230663db4cd083c8eeb0f293f46abc460ad3c299b0089b72e6d472202c").unwrap(); @@ -2163,7 +2182,7 @@ mod slp_tests { let err = match tx_err.clone() { TransactionErr::TxRecoverable(_tx, err) => err, - TransactionErr::Plain(err) => err, + TransactionErr::Plain(err) | TransactionErr::ProtocolNotSupported(err) => err, }; println!("{:?}", err); @@ -2233,20 +2252,21 @@ mod slp_tests { let my_pub = bch.my_public_key().unwrap(); // standard BCH validation should pass as the output itself is correct - utxo_common::validate_payment( + block_on(utxo_common::validate_payment( bch.clone(), - deserialize(payment_tx.as_slice()).unwrap(), + &deserialize(payment_tx.as_slice()).unwrap(), SLP_SWAP_VOUT, my_pub, &other_pub, - &secret_hash, + SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &secret_hash, + }, fusd.platform_dust_dec(), None, lock_time, wait_until_sec(60), 1, - ) - .wait() + )) .unwrap(); let input = ValidatePaymentInput { diff --git a/mm2src/coins/utxo/swap_proto_v2_scripts.rs b/mm2src/coins/utxo/swap_proto_v2_scripts.rs index f0b5231e04..79d05d3c28 100644 --- a/mm2src/coins/utxo/swap_proto_v2_scripts.rs +++ b/mm2src/coins/utxo/swap_proto_v2_scripts.rs @@ -1,4 +1,4 @@ -/// This module contains functions building Bitcoins scripts for the "Swap protocol upgrade" feature +/// This module contains functions building Bitcoins scripts for the "Trading protocol upgrade" feature /// For more info, see https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895 use bitcrypto::ripemd160; use keys::Public; @@ -13,32 +13,32 @@ pub fn taker_funding_script( ) -> Script { let mut builder = Builder::default() .push_opcode(Opcode::OP_IF) - .push_bytes(&time_lock.to_le_bytes()) + .push_data(&time_lock.to_le_bytes()) .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY) .push_opcode(Opcode::OP_DROP) - .push_bytes(taker_pub) + .push_data(taker_pub) .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ELSE) .push_opcode(Opcode::OP_IF) - .push_bytes(taker_pub) + .push_data(taker_pub) .push_opcode(Opcode::OP_CHECKSIGVERIFY) - .push_bytes(maker_pub) + .push_data(maker_pub) .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ELSE) .push_opcode(Opcode::OP_SIZE) - .push_bytes(&[32]) + .push_data(&[32]) .push_opcode(Opcode::OP_EQUALVERIFY) .push_opcode(Opcode::OP_HASH160); if taker_secret_hash.len() == 32 { - builder = builder.push_bytes(ripemd160(taker_secret_hash).as_slice()); + builder = builder.push_data(ripemd160(taker_secret_hash).as_slice()); } else { - builder = builder.push_bytes(taker_secret_hash); + builder = builder.push_data(taker_secret_hash); } builder .push_opcode(Opcode::OP_EQUALVERIFY) - .push_bytes(taker_pub) + .push_data(taker_pub) .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ENDIF) .push_opcode(Opcode::OP_ENDIF) @@ -54,29 +54,82 @@ pub fn taker_payment_script( ) -> Script { let mut builder = Builder::default() .push_opcode(Opcode::OP_IF) - .push_bytes(&time_lock.to_le_bytes()) + .push_data(&time_lock.to_le_bytes()) .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY) .push_opcode(Opcode::OP_DROP) - .push_bytes(taker_pub) + .push_data(taker_pub) .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ELSE) .push_opcode(Opcode::OP_SIZE) - .push_bytes(&[32]) + .push_data(&[32]) .push_opcode(Opcode::OP_EQUALVERIFY) .push_opcode(Opcode::OP_HASH160); if maker_secret_hash.len() == 32 { - builder = builder.push_bytes(ripemd160(maker_secret_hash).as_slice()); + builder = builder.push_data(ripemd160(maker_secret_hash).as_slice()); } else { - builder = builder.push_bytes(maker_secret_hash); + builder = builder.push_data(maker_secret_hash); } builder .push_opcode(Opcode::OP_EQUALVERIFY) - .push_bytes(taker_pub) + .push_data(taker_pub) .push_opcode(Opcode::OP_CHECKSIGVERIFY) - .push_bytes(maker_pub) + .push_data(maker_pub) .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ENDIF) .into_script() } + +/// Builds a script for maker payment with immediate refund path +pub fn maker_payment_script( + time_lock: u32, + maker_secret_hash: &[u8], + taker_secret_hash: &[u8], + maker_pub: &Public, + taker_pub: &Public, +) -> Script { + let mut builder = Builder::default() + .push_opcode(Opcode::OP_IF) + .push_data(&time_lock.to_le_bytes()) + .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY) + .push_opcode(Opcode::OP_DROP) + .push_data(maker_pub) + .push_opcode(Opcode::OP_CHECKSIG) + .push_opcode(Opcode::OP_ELSE) + .push_opcode(Opcode::OP_IF) + .push_opcode(Opcode::OP_SIZE) + .push_data(&[32]) + .push_opcode(Opcode::OP_EQUALVERIFY) + .push_opcode(Opcode::OP_HASH160); + + if maker_secret_hash.len() == 32 { + builder = builder.push_data(ripemd160(maker_secret_hash).as_slice()); + } else { + builder = builder.push_data(maker_secret_hash); + } + + builder = builder + .push_opcode(Opcode::OP_EQUALVERIFY) + .push_data(taker_pub) + .push_opcode(Opcode::OP_CHECKSIG) + .push_opcode(Opcode::OP_ELSE) + .push_opcode(Opcode::OP_SIZE) + .push_data(&[32]) + .push_opcode(Opcode::OP_EQUALVERIFY) + .push_opcode(Opcode::OP_HASH160); + + if taker_secret_hash.len() == 32 { + builder = builder.push_data(ripemd160(taker_secret_hash).as_slice()); + } else { + builder = builder.push_data(taker_secret_hash); + } + + builder + .push_opcode(Opcode::OP_EQUALVERIFY) + .push_data(maker_pub) + .push_opcode(Opcode::OP_CHECKSIG) + .push_opcode(Opcode::OP_ENDIF) + .push_opcode(Opcode::OP_ENDIF) + .into_script() +} diff --git a/mm2src/coins/utxo/utxo_balance_events.rs b/mm2src/coins/utxo/utxo_balance_events.rs new file mode 100644 index 0000000000..2d97ef5cc9 --- /dev/null +++ b/mm2src/coins/utxo/utxo_balance_events.rs @@ -0,0 +1,233 @@ +use async_trait::async_trait; +use common::{executor::{AbortSettings, SpawnAbortable, Timer}, + log, Future01CompatExt}; +use futures::channel::oneshot::{self, Receiver, Sender}; +use futures_util::StreamExt; +use keys::Address; +use mm2_core::mm_ctx::MmArc; +use mm2_event_stream::{behaviour::{EventBehaviour, EventInitStatus}, + ErrorEventName, Event, EventName, EventStreamConfiguration}; +use std::collections::{BTreeMap, HashSet}; + +use super::utxo_standard::UtxoStandardCoin; +use crate::{utxo::{output_script, + rpc_clients::electrum_script_hash, + utxo_common::{address_balance, address_to_scripthash}, + ScripthashNotification, UtxoCoinFields}, + CoinWithDerivationMethod, MarketCoinOps, MmCoin}; + +macro_rules! try_or_continue { + ($exp:expr) => { + match $exp { + Ok(t) => t, + Err(e) => { + log::error!("{}", e); + continue; + }, + } + }; +} + +#[async_trait] +impl EventBehaviour for UtxoStandardCoin { + fn event_name() -> EventName { EventName::CoinBalance } + + fn error_event_name() -> ErrorEventName { ErrorEventName::CoinBalanceError } + + async fn handle(self, _interval: f64, tx: oneshot::Sender) { + const RECEIVER_DROPPED_MSG: &str = "Receiver is dropped, which should never happen."; + + async fn subscribe_to_addresses( + utxo: &UtxoCoinFields, + addresses: HashSet
, + ) -> Result, String> { + const LOOP_INTERVAL: f64 = 0.5; + + let mut scripthash_to_address_map: BTreeMap = BTreeMap::new(); + for address in addresses { + let scripthash = address_to_scripthash(&address).map_err(|e| e.to_string())?; + + scripthash_to_address_map.insert(scripthash.clone(), address); + + let mut attempt = 0; + while let Err(e) = utxo + .rpc_client + .blockchain_scripthash_subscribe(scripthash.clone()) + .compat() + .await + { + if attempt == 5 { + return Err(e.to_string()); + } + + log::error!( + "Failed to subscribe {} scripthash ({attempt}/5 attempt). Error: {}", + scripthash, + e.to_string() + ); + + attempt += 1; + Timer::sleep(LOOP_INTERVAL).await; + } + } + + Ok(scripthash_to_address_map) + } + + let ctx = match MmArc::from_weak(&self.as_ref().ctx) { + Some(ctx) => ctx, + None => { + let msg = "MM context must have been initialized already."; + tx.send(EventInitStatus::Failed(msg.to_owned())) + .expect(RECEIVER_DROPPED_MSG); + panic!("{}", msg); + }, + }; + + let scripthash_notification_handler = match self.as_ref().scripthash_notification_handler.as_ref() { + Some(t) => t, + None => { + let e = "Scripthash notification receiver can not be empty."; + tx.send(EventInitStatus::Failed(e.to_string())) + .expect(RECEIVER_DROPPED_MSG); + panic!("{}", e); + }, + }; + + tx.send(EventInitStatus::Success).expect(RECEIVER_DROPPED_MSG); + + let mut scripthash_to_address_map = BTreeMap::default(); + while let Some(message) = scripthash_notification_handler.lock().await.next().await { + let notified_scripthash = match message { + ScripthashNotification::Triggered(t) => t, + ScripthashNotification::SubscribeToAddresses(addresses) => { + match subscribe_to_addresses(self.as_ref(), addresses).await { + Ok(map) => scripthash_to_address_map.extend(map), + Err(e) => { + log::error!("{e}"); + + ctx.stream_channel_controller + .broadcast(Event::new( + format!("{}:{}", Self::error_event_name(), self.ticker()), + json!({ "error": e }).to_string(), + )) + .await; + }, + }; + + continue; + }, + ScripthashNotification::RefreshSubscriptions => { + let my_addresses = try_or_continue!(self.all_addresses().await); + match subscribe_to_addresses(self.as_ref(), my_addresses).await { + Ok(map) => scripthash_to_address_map = map, + Err(e) => { + log::error!("{e}"); + + ctx.stream_channel_controller + .broadcast(Event::new( + format!("{}:{}", Self::error_event_name(), self.ticker()), + json!({ "error": e }).to_string(), + )) + .await; + }, + }; + + continue; + }, + }; + + let address = match scripthash_to_address_map.get(¬ified_scripthash) { + Some(t) => Some(t.clone()), + None => try_or_continue!(self.all_addresses().await) + .into_iter() + .find_map(|addr| { + let script = match output_script(&addr) { + Ok(script) => script, + Err(e) => { + log::error!("{e}"); + return None; + }, + }; + let script_hash = electrum_script_hash(&script); + let scripthash = hex::encode(script_hash); + + if notified_scripthash == scripthash { + scripthash_to_address_map.insert(notified_scripthash.clone(), addr.clone()); + Some(addr) + } else { + None + } + }), + }; + + let address = match address { + Some(t) => t, + None => { + log::debug!( + "Couldn't find the relevant address for {} scripthash.", + notified_scripthash + ); + continue; + }, + }; + + let balance = match address_balance(&self, &address).await { + Ok(t) => t, + Err(e) => { + let ticker = self.ticker(); + log::error!("Failed getting balance for '{ticker}'. Error: {e}"); + let e = serde_json::to_value(e).expect("Serialization should't fail."); + + ctx.stream_channel_controller + .broadcast(Event::new( + format!("{}:{}", Self::error_event_name(), ticker), + e.to_string(), + )) + .await; + + continue; + }, + }; + + let payload = json!({ + "ticker": self.ticker(), + "address": address.to_string(), + "balance": { "spendable": balance.spendable, "unspendable": balance.unspendable } + }); + + ctx.stream_channel_controller + .broadcast(Event::new( + Self::event_name().to_string(), + json!(vec![payload]).to_string(), + )) + .await; + } + } + + async fn spawn_if_active(self, config: &EventStreamConfiguration) -> EventInitStatus { + if let Some(event) = config.get_event(&Self::event_name()) { + log::info!( + "{} event is activated for {}. `stream_interval_seconds`({}) has no effect on this.", + Self::event_name(), + self.ticker(), + event.stream_interval_seconds + ); + + let (tx, rx): (Sender, Receiver) = oneshot::channel(); + let fut = self.clone().handle(event.stream_interval_seconds, tx); + let settings = AbortSettings::info_on_abort(format!( + "{} event is stopped for {}.", + Self::event_name(), + self.ticker() + )); + self.spawner().spawn_with_settings(fut, settings); + + rx.await.unwrap_or_else(|e| { + EventInitStatus::Failed(format!("Event initialization status must be received: {}", e)) + }) + } else { + EventInitStatus::Inactive + } + } +} diff --git a/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs b/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs index 315a94cdd8..756e50614f 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/wasm/block_header_table.rs @@ -15,11 +15,11 @@ impl BlockHeaderStorageTable { } impl TableSignature for BlockHeaderStorageTable { - fn table_name() -> &'static str { "block_header_storage_cache_table" } + const TABLE_NAME: &'static str = "block_header_storage_cache_table"; fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { if let (0, 1) = (old_version, new_version) { - let table = upgrader.create_table(Self::table_name())?; + let table = upgrader.create_table(Self::TABLE_NAME)?; table.create_multi_index(Self::TICKER_HEIGHT_INDEX, &["ticker", "height"], true)?; table.create_multi_index(Self::HASH_TICKER_INDEX, &["hash", "ticker"], true)?; table.create_index("ticker", false)?; diff --git a/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs b/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs index e958300e47..08e1a962c8 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/wasm/indexeddb_block_header_storage.rs @@ -3,6 +3,7 @@ use super::BlockHeaderStorageTable; use async_trait::async_trait; use chain::BlockHeader; use mm2_core::mm_ctx::MmArc; +use mm2_db::indexed_db::cursor_prelude::CursorError; use mm2_db::indexed_db::{BeBigUint, ConstructibleDb, DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder, InitDbResult, MultiIndex, SharedDb}; use mm2_err_handle::prelude::*; @@ -71,59 +72,58 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { &self, headers: HashMap, ) -> Result<(), BlockHeaderStorageError> { - let ticker = self.ticker.clone(); + let ticker = &self.ticker; let locked_db = self .lock_db() .await - .map_err(|err| BlockHeaderStorageError::add_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::add_err(ticker, err.to_string()))?; let db_transaction = locked_db .get_inner() .transaction() .await - .map_err(|err| BlockHeaderStorageError::add_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::add_err(ticker, err.to_string()))?; let block_headers_db = db_transaction .table::() .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; for (height, header) in headers { let hash = header.hash().reversed().to_string(); let raw_header = hex::encode(header.raw()); let bits: u32 = header.bits.into(); let headers_to_store = BlockHeaderStorageTable { - ticker: ticker.clone(), + ticker: self.ticker.clone(), height: BeBigUint::from(height), bits, hash, raw_header, }; let index_keys = MultiIndex::new(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) - .with_value(&ticker) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(ticker) + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))? .with_value(BeBigUint::from(height)) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; block_headers_db .replace_item_by_unique_multi_index(index_keys, &headers_to_store) .await - .map_err(|err| BlockHeaderStorageError::add_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::add_err(ticker, err.to_string()))?; } Ok(()) } async fn get_block_header(&self, height: u64) -> Result, BlockHeaderStorageError> { - let ticker = self.ticker.clone(); if let Some(raw_header) = self.get_block_header_raw(height).await? { let serialized = &hex::decode(raw_header).map_err(|e| BlockHeaderStorageError::DecodeError { - coin: ticker.clone(), + coin: self.ticker.clone(), reason: e.to_string(), })?; - let mut reader = Reader::new_with_coin_variant(serialized, ticker.as_str().into()); + let mut reader = Reader::new_with_coin_variant(serialized, self.ticker.as_str().into()); let header: BlockHeader = reader .read() .map_err(|e: serialization::Error| BlockHeaderStorageError::DecodeError { - coin: ticker, + coin: self.ticker.clone(), reason: e.to_string(), })?; @@ -134,53 +134,53 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { } async fn get_block_header_raw(&self, height: u64) -> Result, BlockHeaderStorageError> { - let ticker = self.ticker.clone(); + let ticker = &self.ticker; let locked_db = self .lock_db() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; let db_transaction = locked_db .get_inner() .transaction() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; let block_headers_db = db_transaction .table::() .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; let index_keys = MultiIndex::new(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) - .with_value(&ticker) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(ticker) + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))? .with_value(BeBigUint::from(height)) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; Ok(block_headers_db .get_item_by_unique_multi_index(index_keys) .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))? .map(|raw| raw.1.raw_header)) } async fn get_last_block_height(&self) -> Result, BlockHeaderStorageError> { - let ticker = self.ticker.clone(); + let ticker = &self.ticker; let locked_db = self .lock_db() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; let db_transaction = locked_db .get_inner() .transaction() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; let block_headers_db = db_transaction .table::() .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; let maybe_item = block_headers_db .cursor_builder() - .only("ticker", ticker.clone()) - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .only("ticker", self.ticker.clone()) + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))? // We need to provide any constraint on the `height` property // since `ticker_height` consists of both `ticker` and `height` properties. .bound("height", BeBigUint::from(0u64), BeBigUint::from(u64::MAX)) @@ -189,16 +189,16 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { .reverse() .open_cursor(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))? .next() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; maybe_item .map(|(_, item)| { item.height .to_u64() - .ok_or_else(|| BlockHeaderStorageError::get_err(&ticker, "height is too large".to_string())) + .ok_or_else(|| BlockHeaderStorageError::get_err(ticker, "height is too large".to_string())) }) .transpose() } @@ -207,44 +207,45 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { &self, max_bits: u32, ) -> Result, BlockHeaderStorageError> { - let ticker = self.ticker.clone(); + let ticker = &self.ticker; let locked_db = self .lock_db() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; let db_transaction = locked_db .get_inner() .transaction() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; let block_headers_db = db_transaction .table::() .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; - let mut cursor = block_headers_db + let condition = move |block| { + serde_json::from_value::(block) + .map_to_mm(|err| CursorError::ErrorDeserializingItem(err.to_string())) + .map(|header| header.bits != max_bits) + }; + let maybe_next = block_headers_db .cursor_builder() - .only("ticker", ticker.clone()) - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? + .only("ticker", self.ticker.clone()) + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))? // We need to provide any constraint on the `height` property // since `ticker_height` consists of both `ticker` and `height` properties. .bound("height", BeBigUint::from(0u64), BeBigUint::from(u64::MAX)) // Cursor returns values from the lowest to highest key indexes. // But we need to get the most highest height, so reverse the cursor direction. .reverse() + .where_(condition) .open_cursor(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; - - while let Some((_item_id, header)) = cursor + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))? .next() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))? - { - if header.bits == max_bits { - continue; - } + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; + if let Some((_item_id, header)) = maybe_next { let serialized = &hex::decode(header.raw_header).map_err(|e| BlockHeaderStorageError::DecodeError { coin: ticker.clone(), reason: e.to_string(), @@ -254,7 +255,7 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { reader .read() .map_err(|e: serialization::Error| BlockHeaderStorageError::DecodeError { - coin: ticker, + coin: self.ticker.clone(), reason: e.to_string(), })?; @@ -265,36 +266,36 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { } async fn get_block_height_by_hash(&self, hash: H256) -> Result, BlockHeaderStorageError> { - let ticker = self.ticker.clone(); + let ticker = &self.ticker; let locked_db = self .lock_db() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; let db_transaction = locked_db .get_inner() .transaction() .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; let block_headers_db = db_transaction .table::() .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; let index_keys = MultiIndex::new(BlockHeaderStorageTable::HASH_TICKER_INDEX) .with_value(hash.to_string()) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? - .with_value(&ticker) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))? + .with_value(ticker) + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; let maybe_item = block_headers_db .get_item_by_unique_multi_index(index_keys) .await - .map_err(|err| BlockHeaderStorageError::get_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::get_err(ticker, err.to_string()))?; maybe_item .map(|(_, item)| { item.height .to_i64() - .ok_or_else(|| BlockHeaderStorageError::get_err(&ticker, "height is too large".to_string())) + .ok_or_else(|| BlockHeaderStorageError::get_err(ticker, "height is too large".to_string())) }) .transpose() } @@ -304,61 +305,61 @@ impl BlockHeaderStorageOps for IDBBlockHeadersStorage { from_height: u64, to_height: u64, ) -> Result<(), BlockHeaderStorageError> { - let ticker = self.ticker.clone(); + let ticker = &self.ticker; let locked_db = self .lock_db() .await - .map_err(|err| BlockHeaderStorageError::delete_err(&ticker, err.to_string(), from_height, to_height))?; + .map_err(|err| BlockHeaderStorageError::delete_err(ticker, err.to_string(), from_height, to_height))?; let db_transaction = locked_db .get_inner() .transaction() .await - .map_err(|err| BlockHeaderStorageError::delete_err(&ticker, err.to_string(), from_height, to_height))?; + .map_err(|err| BlockHeaderStorageError::delete_err(ticker, err.to_string(), from_height, to_height))?; let block_headers_db = db_transaction .table::() .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; for height in from_height..=to_height { let index_keys = MultiIndex::new(BlockHeaderStorageTable::TICKER_HEIGHT_INDEX) - .with_value(&ticker) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))? + .with_value(ticker) + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))? .with_value(BeBigUint::from(height)) - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; block_headers_db .delete_item_by_unique_multi_index(index_keys) .await - .map_err(|err| BlockHeaderStorageError::delete_err(&ticker, err.to_string(), from_height, to_height))?; + .map_err(|err| BlockHeaderStorageError::delete_err(ticker, err.to_string(), from_height, to_height))?; } Ok(()) } async fn is_table_empty(&self) -> Result<(), BlockHeaderStorageError> { - let ticker = self.ticker.clone(); + let ticker = &self.ticker; let locked_db = self .lock_db() .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; let db_transaction = locked_db .get_inner() .transaction() .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; let block_headers_db = db_transaction .table::() .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; let items = block_headers_db .get_items("ticker", ticker.clone()) .await - .map_err(|err| BlockHeaderStorageError::table_err(&ticker, err.to_string()))?; + .map_err(|err| BlockHeaderStorageError::table_err(ticker, err.to_string()))?; if !items.is_empty() { return Err(BlockHeaderStorageError::table_err( - &ticker, + ticker, "Table is not empty".to_string(), )); }; diff --git a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs index b97c1aa94e..60b4d75ff0 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_arc_builder.rs @@ -3,6 +3,7 @@ use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::{UtxoCoinBuildError, UtxoCoinBuilder, UtxoCoinBuilderCommonOps, UtxoFieldsWithGlobalHDBuilder, UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaSecretBuilder}; +use crate::utxo::utxo_standard::UtxoStandardCoin; use crate::utxo::{generate_and_send_tx, FeePolicy, GetUtxoListOps, UtxoArc, UtxoCommonOps, UtxoSyncStatusLoopHandle, UtxoWeak}; use crate::{DerivationMethod, PrivKeyBuildPolicy, UtxoActivationParams}; @@ -13,6 +14,7 @@ use common::log::{debug, error, info, warn}; use futures::compat::Future01CompatExt; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; +use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; #[cfg(test)] use mocktopus::macros::*; use rand::Rng; use script::Builder; @@ -107,6 +109,7 @@ where let utxo = self.build_utxo_fields().await?; let sync_status_loop_handle = utxo.block_headers_status_notifier.clone(); let spv_conf = utxo.conf.spv_conf.clone(); + let (is_native_mode, mode) = (utxo.rpc_client.is_native(), utxo.rpc_client.to_string()); let utxo_arc = UtxoArc::new(utxo); self.spawn_merge_utxo_loop_if_required(&utxo_arc, self.constructor.clone()); @@ -118,6 +121,18 @@ where spawn_block_header_utxo_loop(self.ticker, &utxo_arc, sync_handle, spv_conf); } + if let Some(stream_config) = &self.ctx().event_stream_configuration { + if is_native_mode { + return MmError::err(UtxoCoinBuildError::UnsupportedModeForBalanceEvents { mode }); + } + + if let EventInitStatus::Failed(err) = + EventBehaviour::spawn_if_active(UtxoStandardCoin::from(utxo_arc), stream_config).await + { + return MmError::err(UtxoCoinBuildError::FailedSpawningBalanceEvents(err)); + } + } + Ok(result_coin) } } @@ -166,7 +181,7 @@ async fn merge_utxo_loop( let unspents: Vec<_> = unspents.into_iter().take(max_merge_at_once).collect(); info!("Trying to merge {} UTXOs of coin {}", unspents.len(), ticker); let value = unspents.iter().fold(0, |sum, unspent| sum + unspent.value); - let script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); + let script_pubkey = Builder::build_p2pkh(my_address.hash()).to_bytes(); let output = TransactionOutput { value, script_pubkey }; let merge_tx_fut = generate_and_send_tx( &coin, diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index 16f778d5ff..446cadf2bb 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -1,13 +1,14 @@ -use crate::hd_wallet::{HDAccountsMap, HDAccountsMutex}; -use crate::hd_wallet_storage::{HDWalletCoinStorage, HDWalletStorageError}; +use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDWallet, HDWalletCoinStorage, + HDWalletStorageError, DEFAULT_GAP_LIMIT}; use crate::utxo::rpc_clients::{ElectrumClient, ElectrumClientImpl, ElectrumRpcRequest, EstimateFeeMethod, UtxoRpcClientEnum}; use crate::utxo::tx_cache::{UtxoVerboseCacheOps, UtxoVerboseCacheShared}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; use crate::utxo::utxo_builder::utxo_conf_builder::{UtxoConfBuilder, UtxoConfError}; -use crate::utxo::{output_script, utxo_common, ElectrumBuilderArgs, ElectrumProtoVerifier, ElectrumProtoVerifierEvent, - RecentlySpentOutPoints, TxFee, UtxoCoinConf, UtxoCoinFields, UtxoHDAccount, UtxoHDWallet, - UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, DEFAULT_GAP_LIMIT, UTXO_DUST_AMOUNT}; +use crate::utxo::{output_script, ElectrumBuilderArgs, ElectrumProtoVerifier, ElectrumProtoVerifierEvent, + RecentlySpentOutPoints, ScripthashNotification, ScripthashNotificationSender, TxFee, UtxoCoinConf, + UtxoCoinFields, UtxoHDWallet, UtxoRpcMode, UtxoSyncStatus, UtxoSyncStatusLoopHandle, + UTXO_DUST_AMOUNT}; use crate::{BlockchainNetwork, CoinTransportMetrics, DerivationMethod, HistorySyncState, IguanaPrivKey, PrivKeyBuildPolicy, PrivKeyPolicy, PrivKeyPolicyNotAllowed, RpcClientType, UtxoActivationParams}; use async_trait::async_trait; @@ -17,16 +18,15 @@ use common::executor::{abortable_queue::AbortableQueue, AbortSettings, Abortable Timer}; use common::log::{error, info, LogOnError}; use common::{now_sec, small_rng}; -use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, StandardHDPathError, - StandardHDPathToCoin}; +use crypto::{Bip32DerPathError, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, HwWalletType, StandardHDPathError}; use derive_more::Display; -use futures::channel::mpsc::{channel, unbounded, Receiver as AsyncReceiver, UnboundedReceiver}; +use futures::channel::mpsc::{channel, unbounded, Receiver as AsyncReceiver, UnboundedReceiver, UnboundedSender}; use futures::compat::Future01CompatExt; use futures::lock::Mutex as AsyncMutex; use futures::StreamExt; use keys::bytes::Bytes; -pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, - Type as ScriptType}; +pub use keys::{Address, AddressBuilder, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressScriptType, + KeyPair, Private, Public, Secret}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use primitives::hash::H160; @@ -89,6 +89,13 @@ pub enum UtxoCoinBuildError { #[display(fmt = "SPV params verificaiton failed. Error: {_0}")] SPVError(SPVError), ErrorCalculatingStartingHeight(String), + #[display(fmt = "Failed spawning balance events. Error: {_0}")] + FailedSpawningBalanceEvents(String), + #[display(fmt = "Can not enable balance events for {} mode.", mode)] + UnsupportedModeForBalanceEvents { + mode: String, + }, + InvalidPathToAddress(String), } impl From for UtxoCoinBuildError { @@ -120,6 +127,10 @@ impl From for UtxoCoinBuildError { fn from(e: PrivKeyPolicyNotAllowed) -> Self { UtxoCoinBuildError::PrivKeyPolicyNotAllowed(e) } } +impl From for UtxoCoinBuildError { + fn from(e: keys::Error) -> Self { UtxoCoinBuildError::Internal(e.to_string()) } +} + #[async_trait] pub trait UtxoCoinBuilder: UtxoFieldsWithIguanaSecretBuilder + UtxoFieldsWithGlobalHDBuilder + UtxoFieldsWithHardwareWalletBuilder @@ -157,7 +168,18 @@ pub trait UtxoFieldsWithIguanaSecretBuilder: UtxoCoinBuilderCommonOps { }; let key_pair = KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; let priv_key_policy = PrivKeyPolicy::Iguana(key_pair); - build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy).await + let addr_format = self.address_format()?; + let my_address = AddressBuilder::new( + addr_format, + conf.checksum_type, + conf.address_prefixes.clone(), + conf.bech32_hrp.clone(), + ) + .as_pkh_from_pk(*key_pair.public()) + .build() + .map_to_mm(UtxoCoinBuildError::Internal)?; + let derivation_method = DerivationMethod::SingleAddress(my_address); + build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy, derivation_method).await } } @@ -169,12 +191,17 @@ pub trait UtxoFieldsWithGlobalHDBuilder: UtxoCoinBuilderCommonOps { ) -> UtxoCoinBuildResult { let conf = UtxoConfBuilder::new(self.conf(), self.activation_params(), self.ticker()).build()?; - let derivation_path = conf + let path_to_address = self.activation_params().path_to_address; + let path_to_coin = conf .derivation_path .as_ref() .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet)?; let secret = global_hd_ctx - .derive_secp256k1_secret(derivation_path, &self.activation_params().path_to_address) + .derive_secp256k1_secret( + &path_to_address + .to_derivation_path(path_to_coin) + .mm_err(|e| UtxoCoinBuildError::InvalidPathToAddress(e.to_string()))?, + ) .mm_err(|e| UtxoCoinBuildError::Internal(e.to_string()))?; let private = Private { prefix: conf.wif_prefix, @@ -185,11 +212,53 @@ pub trait UtxoFieldsWithGlobalHDBuilder: UtxoCoinBuilderCommonOps { let activated_key_pair = KeyPair::from_private(private).map_to_mm(|e| UtxoCoinBuildError::Internal(e.to_string()))?; let priv_key_policy = PrivKeyPolicy::HDWallet { - derivation_path: derivation_path.clone(), + path_to_coin: path_to_coin.clone(), activated_key: activated_key_pair, bip39_secp_priv_key: global_hd_ctx.root_priv_key().clone(), }; - build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy).await + + let address_format = self.address_format()?; + let hd_wallet_rmd160 = *self.ctx().rmd160(); + let hd_wallet_storage = + HDWalletCoinStorage::init_with_rmd160(self.ctx(), self.ticker().to_owned(), hd_wallet_rmd160).await?; + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, path_to_coin) + .await + .mm_err(UtxoCoinBuildError::from)?; + let gap_limit = self.gap_limit(); + let hd_wallet = UtxoHDWallet { + inner: HDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin.clone(), + accounts: HDAccountsMutex::new(accounts), + enabled_address: path_to_address, + gap_limit, + }, + address_format, + }; + let derivation_method = DerivationMethod::HDWallet(hd_wallet); + build_utxo_coin_fields_with_conf_and_policy(self, conf, priv_key_policy, derivation_method).await + } + + fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } +} + +// The return type is one-time used only. No need to create a type for it. +#[allow(clippy::type_complexity)] +fn get_scripthash_notification_handlers( + ctx: &MmArc, +) -> Option<( + UnboundedSender, + Arc>>, +)> { + if ctx.event_stream_configuration.is_some() { + let (sender, receiver): ( + UnboundedSender, + UnboundedReceiver, + ) = futures::channel::mpsc::unbounded(); + Some((sender, Arc::new(AsyncMutex::new(receiver)))) + } else { + None } } @@ -197,29 +266,38 @@ async fn build_utxo_coin_fields_with_conf_and_policy( builder: &Builder, conf: UtxoCoinConf, priv_key_policy: PrivKeyPolicy, + derivation_method: DerivationMethod, ) -> UtxoCoinBuildResult where Builder: UtxoCoinBuilderCommonOps + Sync + ?Sized, { let key_pair = priv_key_policy.activated_key_or_err()?; let addr_format = builder.address_format()?; - let my_address = Address { - prefix: conf.pub_addr_prefix, - t_addr_prefix: conf.pub_t_addr_prefix, - hash: AddressHashEnum::AddressHash(key_pair.public().address_hash()), - checksum_type: conf.checksum_type, - hrp: conf.bech32_hrp.clone(), + let my_address = AddressBuilder::new( addr_format, - }; - - let my_script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); - let derivation_method = DerivationMethod::SingleAddress(my_address); + conf.checksum_type, + conf.address_prefixes.clone(), + conf.bech32_hrp.clone(), + ) + .as_pkh_from_pk(*key_pair.public()) + .build() + .map_to_mm(UtxoCoinBuildError::Internal)?; + + let my_script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; + + let (scripthash_notification_sender, scripthash_notification_handler) = + match get_scripthash_notification_handlers(builder.ctx()) { + Some((sender, receiver)) => (Some(sender), Some(receiver)), + None => (None, None), + }; // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to this `UTXO` coin will be aborted as well. let abortable_system: AbortableQueue = builder.ctx().abortable_system.create_subsystem()?; - let rpc_client = builder.rpc_client(abortable_system.create_subsystem()?).await?; + let rpc_client = builder + .rpc_client(scripthash_notification_sender, abortable_system.create_subsystem()?) + .await?; let tx_fee = builder.tx_fee(&rpc_client).await?; let decimals = builder.decimals(&rpc_client).await?; let dust_amount = builder.dust_amount(); @@ -247,7 +325,10 @@ where block_headers_status_notifier, block_headers_status_watcher, abortable_system, + scripthash_notification_handler, + ctx: builder.ctx().weak(), }; + Ok(coin) } @@ -268,31 +349,42 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { let recently_spent_outpoints = AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)); let address_format = self.address_format()?; - let derivation_path = conf + let path_to_coin = conf .derivation_path .clone() .or_mm_err(|| UtxoConfError::DerivationPathIsNotSet)?; let hd_wallet_storage = HDWalletCoinStorage::init(self.ctx(), ticker).await?; - let accounts = self - .load_hd_wallet_accounts(&hd_wallet_storage, &derivation_path) - .await?; + let accounts = load_hd_accounts_from_storage(&hd_wallet_storage, &path_to_coin) + .await + .mm_err(UtxoCoinBuildError::from)?; let gap_limit = self.gap_limit(); let hd_wallet = UtxoHDWallet { - hd_wallet_rmd160, - hd_wallet_storage, + inner: HDWallet { + hd_wallet_rmd160, + hd_wallet_storage, + derivation_path: path_to_coin, + accounts: HDAccountsMutex::new(accounts), + enabled_address: self.activation_params().path_to_address, + gap_limit, + }, address_format, - derivation_path, - accounts: HDAccountsMutex::new(accounts), - gap_limit, }; + let (scripthash_notification_sender, scripthash_notification_handler) = + match get_scripthash_notification_handlers(self.ctx()) { + Some((sender, receiver)) => (Some(sender), Some(receiver)), + None => (None, None), + }; + // Create an abortable system linked to the `MmCtx` so if the context is stopped via `MmArc::stop`, // all spawned futures related to this `UTXO` coin will be aborted as well. let abortable_system: AbortableQueue = self.ctx().abortable_system.create_subsystem()?; - let rpc_client = self.rpc_client(abortable_system.create_subsystem()?).await?; + let rpc_client = self + .rpc_client(scripthash_notification_sender, abortable_system.create_subsystem()?) + .await?; let tx_fee = self.tx_fee(&rpc_client).await?; let decimals = self.decimals(&rpc_client).await?; let dust_amount = self.dust_amount(); @@ -320,20 +412,12 @@ pub trait UtxoFieldsWithHardwareWalletBuilder: UtxoCoinBuilderCommonOps { block_headers_status_notifier, block_headers_status_watcher, abortable_system, + scripthash_notification_handler, + ctx: self.ctx().weak(), }; Ok(coin) } - async fn load_hd_wallet_accounts( - &self, - hd_wallet_storage: &HDWalletCoinStorage, - derivation_path: &StandardHDPathToCoin, - ) -> UtxoCoinBuildResult> { - utxo_common::load_hd_accounts_from_storage(hd_wallet_storage, derivation_path) - .await - .mm_err(UtxoCoinBuildError::from) - } - fn gap_limit(&self) -> u32 { self.activation_params().gap_limit.unwrap_or(DEFAULT_GAP_LIMIT) } fn supports_trezor(&self, conf: &UtxoCoinConf) -> bool { conf.trezor_coin.is_some() } @@ -464,7 +548,11 @@ pub trait UtxoCoinBuilderCommonOps { } } - async fn rpc_client(&self, abortable_system: AbortableQueue) -> UtxoCoinBuildResult { + async fn rpc_client( + &self, + scripthash_notification_sender: ScripthashNotificationSender, + abortable_system: AbortableQueue, + ) -> UtxoCoinBuildResult { match self.activation_params().mode.clone() { UtxoRpcMode::Native => { #[cfg(target_arch = "wasm32")] @@ -479,7 +567,12 @@ pub trait UtxoCoinBuilderCommonOps { }, UtxoRpcMode::Electrum { servers } => { let electrum = self - .electrum_client(abortable_system, ElectrumBuilderArgs::default(), servers) + .electrum_client( + abortable_system, + ElectrumBuilderArgs::default(), + servers, + scripthash_notification_sender, + ) .await?; Ok(UtxoRpcClientEnum::Electrum(electrum)) }, @@ -493,6 +586,7 @@ pub trait UtxoCoinBuilderCommonOps { abortable_system: AbortableQueue, args: ElectrumBuilderArgs, mut servers: Vec, + scripthash_notification_sender: ScripthashNotificationSender, ) -> UtxoCoinBuildResult { let (on_event_tx, on_event_rx) = unbounded(); let ticker = self.ticker().to_owned(); @@ -524,6 +618,7 @@ pub trait UtxoCoinBuilderCommonOps { block_headers_storage, abortable_system, args.negotiate_version, + scripthash_notification_sender, ); for server in servers.iter() { match client.add_server(server).await { @@ -568,7 +663,8 @@ pub trait UtxoCoinBuilderCommonOps { #[cfg(not(target_arch = "wasm32"))] fn native_client(&self) -> UtxoCoinBuildResult { - use base64::{encode_config as base64_encode, URL_SAFE}; + use base64::engine::general_purpose::URL_SAFE; + use base64::Engine; let native_conf_path = self.confpath()?; let network = self.network()?; @@ -591,7 +687,7 @@ pub trait UtxoCoinBuilderCommonOps { let client = Arc::new(NativeClientImpl { coin_ticker, uri: format!("http://127.0.0.1:{}", rpc_port), - auth: format!("Basic {}", base64_encode(&auth_str, URL_SAFE)), + auth: format!("Basic {}", URL_SAFE.encode(auth_str)), event_handlers, request_id: 0u64.into(), list_unspent_concurrent_map: ConcurrentRequestMap::new(), @@ -660,6 +756,7 @@ pub trait UtxoCoinBuilderCommonOps { #[cfg(target_arch = "wasm32")] fn tx_cache(&self) -> UtxoVerboseCacheShared { + #[allow(clippy::default_constructed_unit_structs)] // This is a false-possitive bug from clippy crate::utxo::tx_cache::wasm_tx_cache::WasmVerboseCache::default().into_shared() } @@ -706,16 +803,16 @@ pub trait UtxoCoinBuilderCommonOps { .ok_or_else(|| format!("avg_blocktime not specified in {} coin config", self.ticker())) .map_to_mm(UtxoCoinBuildError::ErrorCalculatingStartingHeight)?; let blocks_per_day = DAY_IN_SECONDS / avg_blocktime; - let current_time_s = now_sec(); + let current_time_sec = now_sec(); - if current_time_s < date_s { + if current_time_sec < date_s { return MmError::err(UtxoCoinBuildError::ErrorCalculatingStartingHeight(format!( "{} sync date must be earlier then current date", self.ticker() ))); }; - let secs_since_date = current_time_s - date_s; + let secs_since_date = current_time_sec - date_s; let days_since_date = (secs_since_date / DAY_IN_SECONDS) - 1; let blocks_to_sync = (days_since_date * blocks_per_day) + blocks_per_day; diff --git a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs index a950b67cb4..5ae7fcb405 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_conf_builder.rs @@ -3,14 +3,16 @@ use crate::utxo::{parse_hex_encoded_u32, UtxoCoinConf, DEFAULT_DYNAMIC_FEE_VOLAT MATURE_CONFIRMATIONS_DEFAULT}; use crate::UtxoActivationParams; use bitcrypto::ChecksumType; -use crypto::{Bip32Error, StandardHDPathToCoin}; +use crypto::{Bip32Error, HDPathToCoin}; use derive_more::Display; -pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, KeyPair, Private, Public, Secret, - Type as ScriptType}; +use keys::NetworkAddressPrefixes; +pub use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, AddressScriptType, KeyPair, Private, + Public, Secret}; use mm2_err_handle::prelude::*; use script::SignatureVersion; use serde_json::{self as json, Value as Json}; use spv_validation::conf::SPVConf; +use std::convert::TryInto; use std::num::NonZeroU64; use std::sync::atomic::AtomicBool; @@ -51,10 +53,29 @@ impl<'a> UtxoConfBuilder<'a> { pub fn build(&self) -> UtxoConfResult { let checksum_type = self.checksum_type(); + let pub_addr_prefix = self.pub_addr_prefix(); - let p2sh_addr_prefix = self.p2sh_address_prefix(); let pub_t_addr_prefix = self.pub_t_address_prefix(); + let mut p2pkh_prefixes = vec![]; + if pub_t_addr_prefix != 0 { + p2pkh_prefixes.push(pub_t_addr_prefix); + } + p2pkh_prefixes.push(pub_addr_prefix); + drop_mutability!(p2pkh_prefixes); + + let p2sh_addr_prefix = self.p2sh_address_prefix(); let p2sh_t_addr_prefix = self.p2sh_t_address_prefix(); + let mut p2sh_prefixes = vec![]; + if p2sh_t_addr_prefix != 0 { + p2sh_prefixes.push(p2sh_t_addr_prefix); + } + p2sh_prefixes.push(p2sh_addr_prefix); + drop_mutability!(p2sh_prefixes); + + let address_prefixes = NetworkAddressPrefixes { + p2pkh: p2pkh_prefixes.as_slice().try_into().expect("prefixes valid"), + p2sh: p2sh_prefixes.as_slice().try_into().expect("prefixes valid"), + }; let sign_message_prefix = self.sign_message_prefix(); let wif_prefix = self.wif_prefix(); @@ -99,10 +120,7 @@ impl<'a> UtxoConfBuilder<'a> { is_posv, requires_notarization, overwintered, - pub_addr_prefix, - p2sh_addr_prefix, - pub_t_addr_prefix, - p2sh_t_addr_prefix, + address_prefixes, sign_message_prefix, bech32_hrp, segwit, @@ -192,10 +210,9 @@ impl<'a> UtxoConfBuilder<'a> { fn overwintered(&self) -> bool { self.conf["overwintered"].as_u64().unwrap_or(0) == 1 } fn tx_fee_volatility_percent(&self) -> f64 { - match self.conf["txfee_volatility_percent"].as_f64() { - Some(volatility) => volatility, - None => DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT, - } + self.conf["txfee_volatility_percent"] + .as_f64() + .unwrap_or(DEFAULT_DYNAMIC_FEE_VOLATILITY_PERCENT) } fn version_group_id(&self, tx_version: i32, overwintered: bool) -> UtxoConfResult { @@ -289,7 +306,7 @@ impl<'a> UtxoConfBuilder<'a> { .map_to_mm(|e| UtxoConfError::ErrorDeserializingSPVConf(e.to_string())) } - fn derivation_path(&self) -> UtxoConfResult> { + fn derivation_path(&self) -> UtxoConfResult> { json::from_value(self.conf["derivation_path"].clone()) .map_to_mm(|e| UtxoConfError::ErrorDeserializingDerivationPath(e.to_string())) } diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 2354d8ba44..be54d53fe1 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -1,50 +1,52 @@ use super::*; -use crate::coin_balance::{AddressBalanceStatus, HDAddressBalance, HDWalletBalanceOps}; -use crate::coin_errors::{MyAddressError, ValidatePaymentError}; +use crate::coin_balance::{HDAddressBalance, HDWalletBalanceObject, HDWalletBalanceOps}; +use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::eth::EthCoinType; -use crate::hd_confirm_address::HDConfirmAddress; -use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, HDAccountsMap, - NewAccountCreatingError, NewAddressDeriveConfirmError, NewAddressDerivingError}; -use crate::hd_wallet_storage::{HDWalletCoinWithStorageOps, HDWalletStorageResult}; +use crate::hd_wallet::{HDCoinAddress, HDCoinHDAccount, HDCoinWithdrawOps, TrezorCoinError}; use crate::lp_price::get_base_price_in_rel; -use crate::rpc_command::init_withdraw::WithdrawTaskHandle; +use crate::rpc_command::init_withdraw::WithdrawTaskHandleShared; use crate::utxo::rpc_clients::{electrum_script_hash, BlockHashOrHeight, UnspentInfo, UnspentMap, UtxoRpcClientEnum, UtxoRpcClientOps, UtxoRpcResult}; use crate::utxo::spv::SimplePaymentVerification; use crate::utxo::tx_cache::TxCacheResult; +use crate::utxo::utxo_hd_wallet::UtxoHDAddress; use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; use crate::watcher_common::validate_watcher_reward; -use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, DexFee, GenPreimageResult, - GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, HDAccountAddressId, - RawTransactionError, RawTransactionRequest, RawTransactionRes, RefundFundingSecretArgs, RefundPaymentArgs, - RewardTarget, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SendTakerFundingArgs, SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TradePreimageValue, - TransactionFut, TransactionResult, TxFeeDetails, TxGenError, TxMarshalingErr, TxPreimageWithSig, - ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, - ValidateTakerFundingArgs, ValidateTakerFundingError, ValidateTakerFundingResult, - ValidateTakerFundingSpendPreimageError, ValidateTakerFundingSpendPreimageResult, - ValidateTakerPaymentSpendPreimageError, ValidateTakerPaymentSpendPreimageResult, - ValidateWatcherSpendInput, VerificationError, VerificationResult, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFrom, WithdrawResult, - WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, - INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; +use crate::{scan_for_new_addresses_impl, CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, + DexFee, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, + RawTransactionError, RawTransactionRequest, RawTransactionRes, RawTransactionResult, + RefundFundingSecretArgs, RefundMakerPaymentArgs, RefundPaymentArgs, RewardTarget, + SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, + SendTakerFundingArgs, SignRawTransactionEnum, SignRawTransactionRequest, SignUtxoTransactionParams, + SignatureError, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, + SwapTxTypeWithSecretHash, TradePreimageValue, TransactionData, TransactionFut, TransactionResult, + TxFeeDetails, TxGenError, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, + ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxError, + ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageError, + ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageError, + ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationError, VerificationResult, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawResult, WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, + INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; use crate::{MmCoinEnum, WatcherReward, WatcherRewardError}; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; pub use bitcrypto::{dhash160, sha256, ChecksumType}; use bitcrypto::{dhash256, ripemd160}; use chain::constants::SEQUENCE_FINAL; -use chain::{OutPoint, TransactionOutput}; +use chain::{OutPoint, TransactionInput, TransactionOutput}; use common::executor::Timer; use common::jsonrpc_client::JsonRpcErrorType; -use common::log::{error, warn}; -use crypto::{Bip32DerPathOps, Bip44Chain, RpcDerivationPath, StandardHDPath, StandardHDPathError}; +use common::log::{debug, error}; +use crypto::Bip44Chain; use futures::compat::Future01CompatExt; use futures::future::{FutureExt, TryFutureExt}; use futures01::future::Either; use itertools::Itertools; use keys::bytes::Bytes; -use keys::{Address, AddressFormat as UtxoAddressFormat, AddressHashEnum, CompactSignature, Public, SegwitAddress, - Type as ScriptType}; +#[cfg(test)] use keys::prefixes::{KMD_PREFIXES, T_QTUM_PREFIXES}; +use keys::{Address, AddressBuilder, AddressBuilderOption, AddressFormat as UtxoAddressFormat, AddressFormat, + AddressHashEnum, AddressScriptType, CompactSignature, Public, SegwitAddress}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::bigdecimal_custom::CheckedDivision; @@ -64,8 +66,6 @@ use utxo_signer::with_key_pair::{calc_and_sign_sighash, p2sh_spend, signature_ha SIGHASH_SINGLE}; use utxo_signer::UtxoSignerOps; -pub use chain::Transaction as UtxoTx; - pub mod utxo_tx_history_v2_common; pub const DEFAULT_FEE_VOUT: usize = 0; @@ -106,234 +106,33 @@ pub async fn get_tx_fee(coin: &UtxoCoinFields) -> UtxoRpcResult { } } -fn derive_address_with_cache( +pub(crate) fn address_from_extended_pubkey( coin: &T, - hd_account: &UtxoHDAccount, - hd_addresses_cache: &mut HashMap, - hd_address_id: HDAddressId, -) -> AddressDerivingResult + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, +) -> UtxoHDAddress where T: UtxoCommonOps, { - // Check if the given HD address has been derived already. - if let Some(hd_address) = hd_addresses_cache.get(&hd_address_id) { - return Ok(hd_address.clone()); - } - - let change_child = hd_address_id.chain.to_child_number(); - let address_id_child = ChildNumber::from(hd_address_id.address_id); + let pubkey = Public::Compressed(H264::from(extended_pubkey.public_key().serialize())); + let address = coin.address_from_pubkey(&pubkey); - let derived_pubkey = hd_account - .extended_pubkey - .derive_child(change_child)? - .derive_child(address_id_child)?; - let address = coin.address_from_extended_pubkey(&derived_pubkey); - let pubkey = Public::Compressed(H264::from(derived_pubkey.public_key().serialize())); - - let mut derivation_path = hd_account.account_derivation_path.to_derivation_path(); - derivation_path.push(change_child); - derivation_path.push(address_id_child); - - let hd_address = HDAddress { + UtxoHDAddress { address, pubkey, derivation_path, - }; - - // Cache the derived `hd_address`. - hd_addresses_cache.insert(hd_address_id, hd_address.clone()); - Ok(hd_address) -} - -/// [`HDWalletCoinOps::derive_addresses`] native implementation. -/// -/// # Important -/// -/// The [`HDAddressesCache::cache`] mutex is locked once for the entire duration of this function. -#[cfg(not(target_arch = "wasm32"))] -pub async fn derive_addresses( - coin: &T, - hd_account: &UtxoHDAccount, - address_ids: Ids, -) -> AddressDerivingResult> -where - T: UtxoCommonOps, - Ids: Iterator, -{ - let mut hd_addresses_cache = hd_account.derived_addresses.lock().await; - address_ids - .map(|hd_address_id| derive_address_with_cache(coin, hd_account, &mut hd_addresses_cache, hd_address_id)) - .collect() -} - -/// [`HDWalletCoinOps::derive_addresses`] WASM implementation. -/// -/// # Important -/// -/// This function locks [`HDAddressesCache::cache`] mutex at each iteration. -/// -/// # Performance -/// -/// Locking the [`HDAddressesCache::cache`] mutex at each iteration may significantly degrade performance. -/// But this is required at least for now due the facts that: -/// 1) mm2 runs in the same thread as `KomodoPlatform/air_dex` runs; -/// 2) [`ExtendedPublicKey::derive_child`] is a synchronous operation, and it takes a long time. -/// So we need to periodically invoke Javascript runtime to handle UI events and other asynchronous tasks. -#[cfg(target_arch = "wasm32")] -pub async fn derive_addresses( - coin: &T, - hd_account: &UtxoHDAccount, - address_ids: Ids, -) -> AddressDerivingResult> -where - T: UtxoCommonOps, - Ids: Iterator, -{ - let mut result = Vec::new(); - for hd_address_id in address_ids { - let mut hd_addresses_cache = hd_account.derived_addresses.lock().await; - - let hd_address = derive_address_with_cache(coin, hd_account, &mut hd_addresses_cache, hd_address_id)?; - result.push(hd_address); } - - Ok(result) } -pub async fn generate_and_confirm_new_address( - coin: &Coin, - hd_wallet: &Coin::HDWallet, - hd_account: &mut Coin::HDAccount, - chain: Bip44Chain, - confirm_address: &ConfirmAddress, -) -> MmResult, NewAddressDeriveConfirmError> +pub(crate) fn trezor_coin(coin: &Coin) -> MmResult where - Coin: HDWalletCoinWithStorageOps
- + AsRef - + Sync, - ConfirmAddress: HDConfirmAddress, + Coin: AsRef, { - use crate::hd_wallet::inner_impl; - - let inner_impl::NewAddress { - address, - new_known_addresses_number, - } = inner_impl::generate_new_address_immutable(coin, hd_wallet, hd_account, chain).await?; - - let trezor_coin = coin.as_ref().conf.trezor_coin.clone().or_mm_err(|| { + coin.as_ref().conf.trezor_coin.clone().or_mm_err(|| { let ticker = &coin.as_ref().conf.ticker; - let error = format!("'{ticker}' coin must contain the 'trezor_coin' field in the coins config"); - NewAddressDeriveConfirmError::DeriveError(NewAddressDerivingError::Internal(error)) - })?; - let expected_address = address.address.to_string(); - // Ask the user to confirm if the given `expected_address` is the same as on the HW display. - confirm_address - .confirm_utxo_address(trezor_coin, address.derivation_path.clone(), expected_address) - .await?; - - let actual_known_addresses_number = hd_account.known_addresses_number(chain)?; - // Check if the actual `known_addresses_number` hasn't been changed while we waited for the user confirmation. - // If the actual value is greater than the new one, we don't need to update. - if actual_known_addresses_number < new_known_addresses_number { - coin.set_known_addresses_number(hd_wallet, hd_account, chain, new_known_addresses_number) - .await?; - } - - Ok(address) -} - -pub async fn create_new_account<'a, Coin, XPubExtractor>( - coin: &Coin, - hd_wallet: &'a UtxoHDWallet, - xpub_extractor: &XPubExtractor, -) -> MmResult, NewAccountCreatingError> -where - Coin: ExtractExtendedPubkey - + HDWalletCoinWithStorageOps - + Sync, - XPubExtractor: HDXPubExtractor, -{ - const INIT_ACCOUNT_ID: u32 = 0; - let new_account_id = hd_wallet - .accounts - .lock() - .await - .iter() - // The last element of the BTreeMap has the max account index. - .last() - .map(|(account_id, _account)| *account_id + 1) - .unwrap_or(INIT_ACCOUNT_ID); - let max_accounts_number = hd_wallet.account_limit(); - if new_account_id >= max_accounts_number { - return MmError::err(NewAccountCreatingError::AccountLimitReached { max_accounts_number }); - } - - let account_child_hardened = true; - let account_child = ChildNumber::new(new_account_id, account_child_hardened) - .map_to_mm(|e| NewAccountCreatingError::Internal(e.to_string()))?; - - let account_derivation_path: StandardHDPathToAccount = hd_wallet.derivation_path.derive(account_child)?; - let account_pubkey = coin - .extract_extended_pubkey(xpub_extractor, account_derivation_path.to_derivation_path()) - .await?; - - let new_account = UtxoHDAccount { - account_id: new_account_id, - extended_pubkey: account_pubkey, - account_derivation_path, - // We don't know how many addresses are used by the user at this moment. - external_addresses_number: 0, - internal_addresses_number: 0, - derived_addresses: HDAddressesCache::default(), - }; - - let accounts = hd_wallet.accounts.lock().await; - if accounts.contains_key(&new_account_id) { - let error = format!( - "Account '{}' has been activated while we proceed the 'create_new_account' function", - new_account_id - ); - return MmError::err(NewAccountCreatingError::Internal(error)); - } - - coin.upload_new_account(hd_wallet, new_account.to_storage_item()) - .await?; - - Ok(AsyncMutexGuard::map(accounts, |accounts| { - accounts - .entry(new_account_id) - // the `entry` method should return [`Entry::Vacant`] due to the checks above - .or_insert(new_account) - })) -} - -pub async fn set_known_addresses_number( - coin: &T, - hd_wallet: &UtxoHDWallet, - hd_account: &mut UtxoHDAccount, - chain: Bip44Chain, - new_known_addresses_number: u32, -) -> MmResult<(), AccountUpdatingError> -where - T: HDWalletCoinWithStorageOps + Sync, -{ - let max_addresses_number = hd_wallet.address_limit(); - if new_known_addresses_number >= max_addresses_number { - return MmError::err(AccountUpdatingError::AddressLimitReached { max_addresses_number }); - } - match chain { - Bip44Chain::External => { - coin.update_external_addresses_number(hd_wallet, hd_account.account_id, new_known_addresses_number) - .await?; - hd_account.external_addresses_number = new_known_addresses_number; - }, - Bip44Chain::Internal => { - coin.update_internal_addresses_number(hd_wallet, hd_account.account_id, new_known_addresses_number) - .await?; - hd_account.internal_addresses_number = new_known_addresses_number; - }, - } - Ok(()) + let error = format!("'{ticker}' coin has 'trezor_coin' field as `None` in the coins config"); + TrezorCoinError::Internal(error) + }) } pub async fn produce_hd_address_scanner(coin: &T) -> BalanceResult @@ -346,13 +145,13 @@ where pub async fn scan_for_new_addresses( coin: &T, hd_wallet: &T::HDWallet, - hd_account: &mut T::HDAccount, + hd_account: &mut HDCoinHDAccount, address_scanner: &T::HDAddressScanner, gap_limit: u32, -) -> BalanceResult> +) -> BalanceResult>>> where T: HDWalletBalanceOps + Sync, - T::Address: std::fmt::Display, + HDCoinAddress: std::fmt::Display, { let mut addresses = scan_for_new_addresses_impl( coin, @@ -378,93 +177,13 @@ where Ok(addresses) } -/// Checks addresses that either had empty transaction history last time we checked or has not been checked before. -/// The checking stops at the moment when we find `gap_limit` consecutive empty addresses. -pub async fn scan_for_new_addresses_impl( - coin: &T, - hd_wallet: &T::HDWallet, - hd_account: &mut T::HDAccount, - address_scanner: &T::HDAddressScanner, - chain: Bip44Chain, - gap_limit: u32, -) -> BalanceResult> -where - T: HDWalletBalanceOps + Sync, - T::Address: std::fmt::Display, -{ - let mut balances = Vec::with_capacity(gap_limit as usize); - - // Get the first unknown address id. - let mut checking_address_id = hd_account - .known_addresses_number(chain) - // A UTXO coin should support both [`Bip44Chain::External`] and [`Bip44Chain::Internal`]. - .mm_err(|e| BalanceError::Internal(e.to_string()))?; - - let mut unused_addresses_counter = 0; - let max_addresses_number = hd_wallet.address_limit(); - while checking_address_id < max_addresses_number && unused_addresses_counter <= gap_limit { - let HDAddress { - address: checking_address, - derivation_path: checking_address_der_path, - .. - } = coin.derive_address(hd_account, chain, checking_address_id).await?; - - match coin.is_address_used(&checking_address, address_scanner).await? { - // We found a non-empty address, so we have to fill up the balance list - // with zeros starting from `last_non_empty_address_id = checking_address_id - unused_addresses_counter`. - AddressBalanceStatus::Used(non_empty_balance) => { - let last_non_empty_address_id = checking_address_id - unused_addresses_counter; - - // First, derive all empty addresses and put it into `balances` with default balance. - let address_ids = (last_non_empty_address_id..checking_address_id) - .into_iter() - .map(|address_id| HDAddressId { chain, address_id }); - let empty_addresses = - coin.derive_addresses(hd_account, address_ids) - .await? - .into_iter() - .map(|empty_address| HDAddressBalance { - address: empty_address.address.to_string(), - derivation_path: RpcDerivationPath(empty_address.derivation_path), - chain, - balance: CoinBalance::default(), - }); - balances.extend(empty_addresses); - - // Then push this non-empty address. - balances.push(HDAddressBalance { - address: checking_address.to_string(), - derivation_path: RpcDerivationPath(checking_address_der_path), - chain, - balance: non_empty_balance, - }); - // Reset the counter of unused addresses to zero since we found a non-empty address. - unused_addresses_counter = 0; - }, - AddressBalanceStatus::NotUsed => unused_addresses_counter += 1, - } - - checking_address_id += 1; - } - - coin.set_known_addresses_number( - hd_wallet, - hd_account, - chain, - checking_address_id - unused_addresses_counter, - ) - .await?; - - Ok(balances) -} - pub async fn all_known_addresses_balances( coin: &T, - hd_account: &T::HDAccount, -) -> BalanceResult> + hd_account: &HDCoinHDAccount, +) -> BalanceResult>>> where T: HDWalletBalanceOps + Sync, - T::Address: std::fmt::Display + Clone, + HDCoinAddress: std::fmt::Display + Clone, { let external_addresses = hd_account .known_addresses_number(Bip44Chain::External) @@ -486,29 +205,6 @@ where Ok(balances) } -pub async fn load_hd_accounts_from_storage( - hd_wallet_storage: &HDWalletCoinStorage, - derivation_path: &StandardHDPathToCoin, -) -> HDWalletStorageResult> { - let accounts = hd_wallet_storage.load_all_accounts().await?; - let res: HDWalletStorageResult> = accounts - .iter() - .map(|account_info| { - let account = UtxoHDAccount::try_from_storage_item(derivation_path, account_info)?; - Ok((account.account_id, account)) - }) - .collect(); - match res { - Ok(accounts) => Ok(accounts), - Err(e) if e.get_inner().is_deserializing_err() => { - warn!("Error loading HD accounts from the storage: '{}'. Clear accounts", e); - hd_wallet_storage.clear_accounts().await?; - Ok(HDAccountsMap::new()) - }, - Err(e) => Err(e), - } -} - /// Requests balance of the given `address`. pub async fn address_balance(coin: &T, address: &Address) -> BalanceResult where @@ -570,22 +266,6 @@ where pub fn derivation_method(coin: &UtxoCoinFields) -> &DerivationMethod { &coin.derivation_method } -pub async fn extract_extended_pubkey( - conf: &UtxoCoinConf, - xpub_extractor: &XPubExtractor, - derivation_path: DerivationPath, -) -> MmResult -where - XPubExtractor: HDXPubExtractor, -{ - let trezor_coin = conf - .trezor_coin - .clone() - .or_mm_err(|| HDExtractPubkeyError::CoinDoesntSupportTrezor)?; - let xpub = xpub_extractor.extract_utxo_xpub(trezor_coin, derivation_path).await?; - Secp256k1ExtendedPublicKey::from_str(&xpub).map_to_mm(|e| HDExtractPubkeyError::InvalidXpub(e.to_string())) -} - /// returns the fee required to be paid for HTLC spend transaction pub async fn get_htlc_spend_fee( coin: &T, @@ -627,29 +307,28 @@ pub fn addresses_from_script(coin: &T, script: &Script) -> Res let addresses = destinations .into_iter() .map(|dst| { - let (prefix, t_addr_prefix, addr_format) = match dst.kind { - ScriptType::P2PKH => ( - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + let (addr_format, build_option) = match dst.kind { + AddressScriptType::P2PKH => ( coin.addr_format_for_standard_scripts(), + AddressBuilderOption::PubkeyHash(dst.hash), ), - ScriptType::P2SH => ( - conf.p2sh_addr_prefix, - conf.p2sh_t_addr_prefix, + AddressScriptType::P2SH => ( coin.addr_format_for_standard_scripts(), + AddressBuilderOption::ScriptHash(dst.hash), ), - ScriptType::P2WPKH => (conf.pub_addr_prefix, conf.pub_t_addr_prefix, UtxoAddressFormat::Segwit), - ScriptType::P2WSH => (conf.pub_addr_prefix, conf.pub_t_addr_prefix, UtxoAddressFormat::Segwit), + AddressScriptType::P2WPKH => (UtxoAddressFormat::Segwit, AddressBuilderOption::PubkeyHash(dst.hash)), + AddressScriptType::P2WSH => (UtxoAddressFormat::Segwit, AddressBuilderOption::ScriptHash(dst.hash)), }; - Address { - hash: dst.hash, - checksum_type: conf.checksum_type, - prefix, - t_addr_prefix, - hrp: conf.bech32_hrp.clone(), + AddressBuilder::new( addr_format, - } + conf.checksum_type, + conf.address_prefixes.clone(), + conf.bech32_hrp.clone(), + ) + .with_build_option(build_option) + .build() + .expect("valid address props") }) .collect(); @@ -670,28 +349,17 @@ where pub fn address_from_str_unchecked(coin: &UtxoCoinFields, address: &str) -> MmResult { let mut errors = Vec::with_capacity(3); - match Address::from_str(address) { + match Address::from_legacyaddress(address, &coin.conf.address_prefixes) { Ok(legacy) => return Ok(legacy), - Err(e) => errors.push(e.to_string()), + Err(e) => errors.push(e), }; - match Address::from_segwitaddress( - address, - coin.conf.checksum_type, - coin.conf.pub_addr_prefix, - coin.conf.pub_t_addr_prefix, - ) { + match Address::from_segwitaddress(address, coin.conf.checksum_type) { Ok(segwit) => return Ok(segwit), Err(e) => errors.push(e), } - match Address::from_cashaddress( - address, - coin.conf.checksum_type, - coin.conf.pub_addr_prefix, - coin.conf.p2sh_addr_prefix, - coin.conf.pub_t_addr_prefix, - ) { + match Address::from_cashaddress(address, coin.conf.checksum_type, &coin.conf.address_prefixes) { Ok(cashaddress) => return Ok(cashaddress), Err(e) => errors.push(e), } @@ -755,6 +423,45 @@ pub fn tx_size_in_v_bytes(from_addr_format: &UtxoAddressFormat, tx: &UtxoTx) -> } } +/// Implements building utxo script pubkey for an address with checking coin conf prefixes +pub fn output_script_checked(coin: &UtxoCoinFields, addr: &Address) -> MmResult { + match addr.addr_format() { + UtxoAddressFormat::Standard => { + if addr.prefix() != &coin.conf.address_prefixes.p2pkh && addr.prefix() != &coin.conf.address_prefixes.p2sh { + return MmError::err(UnsupportedAddr::PrefixError(coin.conf.ticker.clone())); + } + }, + UtxoAddressFormat::Segwit => match (coin.conf.bech32_hrp.as_ref(), addr.hrp().as_ref()) { + (Some(conf_hrp), Some(addr_hrp)) => { + if conf_hrp != addr_hrp { + return MmError::err(UnsupportedAddr::HrpError { + ticker: coin.conf.ticker.clone(), + hrp: addr_hrp.to_string(), + }); + } + }, + (_, _) => { + return MmError::err(UnsupportedAddr::HrpError { + ticker: coin.conf.ticker.clone(), + hrp: addr.hrp().clone().unwrap_or_else(|| "".to_owned()), + }); + }, + }, + UtxoAddressFormat::CashAddress { + network: _, + pub_addr_prefix, + p2sh_addr_prefix, + } => { + if AddressPrefix::from([*pub_addr_prefix]) != coin.conf.address_prefixes.p2pkh + && AddressPrefix::from([*p2sh_addr_prefix]) != coin.conf.address_prefixes.p2sh + { + return MmError::err(UnsupportedAddr::PrefixError(coin.conf.ticker.clone())); + } + }, + } + output_script(addr).map_to_mm(UnsupportedAddr::from) +} + pub struct UtxoTxBuilder<'a, T: AsRef + UtxoTxGenerationOps> { coin: &'a T, from: Option
, @@ -773,11 +480,11 @@ pub struct UtxoTxBuilder<'a, T: AsRef + UtxoTxGenerationOps> { } impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { - pub fn new(coin: &'a T) -> Self { + pub async fn new(coin: &'a T) -> UtxoTxBuilder<'a, T> { UtxoTxBuilder { tx: coin.as_ref().transaction_preimage(), coin, - from: coin.as_ref().derivation_method.single_addr().cloned(), + from: coin.as_ref().derivation_method.single_addr().await, available_inputs: vec![], fee_policy: FeePolicy::SendExact, fee: None, @@ -806,9 +513,9 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { .inputs .extend(inputs.into_iter().map(|input| UnsignedTransactionInput { previous_output: input.outpoint, + prev_script: input.script, sequence: SEQUENCE_FINAL, amount: input.value, - witness: Vec::new(), })); self } @@ -881,9 +588,9 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } if let Some(min_relay) = self.min_relay_fee { if self.tx_fee < min_relay { - outputs_plus_fee -= self.tx_fee; - outputs_plus_fee += min_relay; - self.tx_fee = min_relay; + let fee_diff = min_relay - self.tx_fee; + outputs_plus_fee += fee_diff; + self.tx_fee += fee_diff; } } self.sum_inputs >= outputs_plus_fee @@ -920,7 +627,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. - /// Sends the change (inputs amount - outputs amount) to the [`UtxoTxBuilder::from`] address. + /// sends the change (inputs amount - outputs amount) to the [`UtxoTxBuilder::from`] address. /// Also returns additional transaction data pub async fn build(mut self) -> GenerateTxResult { let coin = self.coin; @@ -929,7 +636,7 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { .from .clone() .or_mm_err(|| GenerateTxError::Internal("'from' address is not specified".to_owned()))?; - let change_script_pubkey = output_script(&from, ScriptType::P2PKH).to_bytes(); + let change_script_pubkey = output_script(&from).map(|script| script.to_bytes())?; let actual_tx_fee = match self.fee { Some(fee) => fee, @@ -972,17 +679,23 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { None }; - for utxo in self.available_inputs.clone() { - self.tx.inputs.push(UnsignedTransactionInput { - previous_output: utxo.outpoint, - sequence: SEQUENCE_FINAL, - amount: utxo.value, - witness: vec![], - }); - self.sum_inputs += utxo.value; + // The function `update_fee_and_check_completeness` checks if the total value of the current inputs + // (added using add_required_inputs or directly) is enough to cover the transaction outputs and fees. + // If it returns `true`, it indicates that no additional inputs are needed from the available inputs, + // and we can skip the loop that adds these additional inputs. + if !self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { + for utxo in self.available_inputs.clone() { + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + prev_script: utxo.script, + sequence: SEQUENCE_FINAL, + amount: utxo.value, + }); + self.sum_inputs += utxo.value; - if self.update_fee_and_check_completeness(&from.addr_format, &actual_tx_fee) { - break; + if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { + break; + } } } @@ -1037,6 +750,48 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { .calc_interest_if_required(self.tx, data, change_script_pubkey, dust) .await?) } + + /// Generates unsigned transaction (TransactionInputSigner) from specified utxos and outputs. + /// Adds or updates inputs with UnspentInfo + /// Does not do any checks or add any outputs + pub async fn build_unchecked(mut self) -> Result> { + for output in self.tx.outputs.iter() { + self.sum_outputs_value += output.value; + } + + true_or!( + !self.available_inputs.is_empty() || !self.tx.inputs.is_empty(), + GenerateTxError::EmptyUtxoSet { + required: self.sum_outputs_value + } + ); + + for utxo in self.available_inputs.clone() { + if let Some(input) = self + .tx + .inputs + .iter_mut() + .find(|input| input.previous_output == utxo.outpoint) + { + input.amount = utxo.value; + input.prev_script = utxo.script; + } else { + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + prev_script: utxo.script, + sequence: SEQUENCE_FINAL, + amount: utxo.value, + }); + } + } + + Ok(self.tx) + } + + pub fn with_transaction_input_signer(mut self, tx_input_signer: TransactionInputSigner) -> Self { + self.tx = tx_input_signer; + self + } } /// Calculates interest if the coin is KMD @@ -1164,8 +919,8 @@ async fn p2sh_spending_tx_preimage( hash: prev_tx.hash(), index: DEFAULT_SWAP_VOUT as u32, }, + prev_script: Vec::new().into(), amount, - witness: Vec::new(), }], outputs, expiry_height: 0, @@ -1461,14 +1216,15 @@ pub async fn sign_and_send_taker_funding_spend( gen_args.taker_pub, gen_args.maker_pub, ); - let payment_address = Address { - checksum_type: coin.as_ref().conf.checksum_type, - hash: AddressHashEnum::AddressHash(dhash160(&payment_redeem_script)), - prefix: coin.as_ref().conf.p2sh_addr_prefix, - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let payment_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh(dhash160(&payment_redeem_script).into()) + .build() + .map_err(TransactionErr::Plain)?; let payment_address_str = payment_address.to_string(); try_tx_s!( client @@ -1487,21 +1243,39 @@ async fn gen_taker_payment_spend_preimage( args: &GenTakerPaymentSpendArgs<'_, T>, n_time: NTimeSetting, ) -> GenPreimageResInner { - let dex_fee_sat = sat_from_big_decimal(&args.dex_fee_amount, coin.as_ref().decimals)?; - let dex_fee_address = address_from_raw_pubkey( args.dex_fee_pub, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), ) .map_to_mm(|e| TxGenError::AddressDerivation(format!("Failed to derive dex_fee_address: {}", e)))?; - let dex_fee_output = TransactionOutput { - value: dex_fee_sat, - script_pubkey: Builder::build_p2pkh(&dex_fee_address.hash).to_bytes(), - }; + + let mut outputs = generate_taker_fee_tx_outputs(coin.as_ref().decimals, dex_fee_address.hash(), args.dex_fee)?; + if let DexFee::WithBurn { .. } = args.dex_fee { + let script = output_script(args.maker_address).map_to_mm(|e| { + TxGenError::Other(format!( + "Couldn't generate output script for maker address {}, error {}", + args.maker_address, e + )) + })?; + let tx_fee = coin + .get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await?; + let maker_value = args + .taker_tx + .first_output() + .map_to_mm(|e| TxGenError::PrevTxIsNotValid(e.to_string()))? + .value + - outputs[0].value + - outputs[1].value + - tx_fee; + outputs.push(TransactionOutput { + value: maker_value, + script_pubkey: script.to_bytes(), + }) + } p2sh_spending_tx_preimage( coin, @@ -1509,7 +1283,7 @@ async fn gen_taker_payment_spend_preimage( LocktimeSetting::UseExact(0), n_time, SEQUENCE_FINAL, - vec![dex_fee_output], + outputs, ) .await .map_to_mm(TxGenError::Legacy) @@ -1528,14 +1302,20 @@ pub async fn gen_and_sign_taker_payment_spend_preimage( let preimage = gen_taker_payment_spend_preimage(coin, args, NTimeSetting::UseNow).await?; let redeem_script = - swap_proto_v2_scripts::taker_payment_script(time_lock, args.secret_hash, args.taker_pub, args.maker_pub); + swap_proto_v2_scripts::taker_payment_script(time_lock, args.maker_secret_hash, args.taker_pub, args.maker_pub); + + let sig_hash_type = match args.dex_fee { + DexFee::Standard(_) => SIGHASH_SINGLE, + DexFee::WithBurn { .. } => SIGHASH_ALL, + }; + let signature = calc_and_sign_sighash( &preimage, DEFAULT_SWAP_VOUT, &redeem_script, htlc_keypair, coin.as_ref().conf.signature_version, - SIGHASH_SINGLE, + sig_hash_type, coin.as_ref().conf.fork_id, )?; Ok(TxPreimageWithSig { @@ -1562,16 +1342,22 @@ pub async fn validate_taker_payment_spend_preimage( .map_to_mm(|e: TryFromIntError| ValidateTakerPaymentSpendPreimageError::LocktimeOverflow(e.to_string()))?; let redeem_script = swap_proto_v2_scripts::taker_payment_script( time_lock, - gen_args.secret_hash, + gen_args.maker_secret_hash, gen_args.taker_pub, gen_args.maker_pub, ); + + let sig_hash_type = match gen_args.dex_fee { + DexFee::Standard(_) => SIGHASH_SINGLE, + DexFee::WithBurn { .. } => SIGHASH_ALL, + }; + let sig_hash = signature_hash_to_sign( &expected_preimage, DEFAULT_SWAP_VOUT, &redeem_script, coin.as_ref().conf.signature_version, - SIGHASH_SINGLE, + sig_hash_type, coin.as_ref().conf.fork_id, )?; @@ -1599,7 +1385,7 @@ pub async fn sign_and_broadcast_taker_payment_spend( gen_args: &GenTakerPaymentSpendArgs<'_, T>, secret: &[u8], htlc_keypair: &KeyPair, -) -> TransactionResult { +) -> Result { let secret_hash = dhash160(secret); let redeem_script = swap_proto_v2_scripts::taker_payment_script( try_tx_s!(gen_args.time_lock.try_into()), @@ -1614,23 +1400,25 @@ pub async fn sign_and_broadcast_taker_payment_spend( payment_input.amount = payment_output.value; signer.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; - let miner_fee = try_tx_s!( - coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await - ); + if let DexFee::Standard(dex_fee) = gen_args.dex_fee { + let dex_fee_sat = try_tx_s!(sat_from_big_decimal(&dex_fee.to_decimal(), coin.as_ref().decimals)); - let maker_amount = &gen_args.trading_amount + &gen_args.premium_amount; - let maker_sat = try_tx_s!(sat_from_big_decimal(&maker_amount, coin.as_ref().decimals)); - if miner_fee + coin.as_ref().dust_amount > maker_sat { - return TX_PLAIN_ERR!("Maker amount is too small to cover miner fee + dust"); - } + let miner_fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); - let maker_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()); - let maker_output = TransactionOutput { - value: maker_sat - miner_fee, - script_pubkey: output_script(maker_address, ScriptType::P2PKH).to_bytes(), - }; - signer.outputs.push(maker_output); + if miner_fee + coin.as_ref().dust_amount + dex_fee_sat > payment_output.value { + return TX_PLAIN_ERR!("Payment amount is too small to cover miner fee + dust + dex_fee_sat"); + } + + let maker_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); + let maker_output = TransactionOutput { + value: payment_output.value - miner_fee - dex_fee_sat, + script_pubkey: try_tx_s!(output_script(&maker_address)).to_bytes(), + }; + signer.outputs.push(maker_output); + } drop_mutability!(signer); let maker_signature = try_tx_s!(calc_and_sign_sighash( @@ -1642,9 +1430,13 @@ pub async fn sign_and_broadcast_taker_payment_spend( SIGHASH_ALL, coin.as_ref().conf.fork_id )); - let sig_hash_single_fork_id = (SIGHASH_SINGLE | coin.as_ref().conf.fork_id) as u8; let mut taker_signature_with_sighash = preimage.signature.to_vec(); - taker_signature_with_sighash.push(sig_hash_single_fork_id); + let taker_sig_hash = match gen_args.dex_fee { + DexFee::Standard(_) => (SIGHASH_SINGLE | coin.as_ref().conf.fork_id) as u8, + DexFee::WithBurn { .. } => (SIGHASH_ALL | coin.as_ref().conf.fork_id) as u8, + }; + + taker_signature_with_sighash.push(taker_sig_hash); drop_mutability!(taker_signature_with_sighash); let sig_hash_all_fork_id = (SIGHASH_ALL | coin.as_ref().conf.fork_id) as u8; @@ -1665,7 +1457,7 @@ pub async fn sign_and_broadcast_taker_payment_spend( drop_mutability!(final_tx); try_tx_s!(coin.broadcast_tx(&final_tx).await, final_tx); - Ok(final_tx.into()) + Ok(final_tx) } pub fn send_taker_fee(coin: T, fee_pub_key: &[u8], dex_fee: DexFee) -> TransactionFut @@ -1674,8 +1466,7 @@ where { let address = try_tx_fus!(address_from_raw_pubkey( fee_pub_key, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), @@ -1683,8 +1474,8 @@ where let outputs = try_tx_fus!(generate_taker_fee_tx_outputs( coin.as_ref().decimals, - &address.hash, - dex_fee, + address.hash(), + &dex_fee, )); send_outputs_from_my_address(coin, outputs) @@ -1693,7 +1484,7 @@ where fn generate_taker_fee_tx_outputs( decimals: u8, address_hash: &AddressHashEnum, - dex_fee: DexFee, + dex_fee: &DexFee, ) -> Result, MmError> { let fee_amount = dex_fee.fee_uamount(decimals)?; @@ -1725,9 +1516,10 @@ where try_tx_fus!(args.time_lock.try_into()), maker_htlc_key_pair.public_slice(), args.other_pubkey, - args.secret_hash, args.amount, - SwapPaymentType::TakerOrMakerPayment, + SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: args.secret_hash + }, )); let send_fut = match &coin.as_ref().rpc_client { UtxoRpcClientEnum::Electrum(_) => Either::A(send_outputs_from_my_address(coin, outputs)), @@ -1762,9 +1554,10 @@ where try_tx_fus!(args.time_lock.try_into()), taker_htlc_key_pair.public_slice(), args.other_pubkey, - args.secret_hash, total_amount, - SwapPaymentType::TakerOrMakerPayment, + SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: args.secret_hash + }, )); let send_fut = match &coin.as_ref().rpc_client { @@ -1782,13 +1575,14 @@ where Box::new(send_fut) } -pub fn send_maker_spends_taker_payment(coin: T, args: SpendPaymentArgs) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); - let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); +pub async fn send_maker_spends_taker_payment( + coin: T, + args: SpendPaymentArgs<'_>, +) -> TransactionResult { + let mut prev_transaction: UtxoTx = try_tx_s!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); - - let payment_value = try_tx_fus!(prev_transaction.first_output()).value; + let payment_value = try_tx_s!(prev_transaction.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); let script_data = Builder::default() @@ -1796,49 +1590,47 @@ pub fn send_maker_spends_taker_payment(coin: T, args .push_opcode(Opcode::OP_0) .into_script(); - let time_lock = try_tx_fus!(args.time_lock.try_into()); + let time_lock = try_tx_s!(args.time_lock.try_into()); let redeem_script = payment_script( time_lock, args.secret_hash, - &try_tx_fus!(Public::from_slice(args.other_pubkey)), + &try_tx_s!(Public::from_slice(args.other_pubkey)), key_pair.public(), ) .into(); - let fut = async move { - let fee = try_tx_s!( - coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= payment_value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + payment_value ); - if fee >= payment_value { - return TX_PLAIN_ERR!( - "HTLC spend fee {} is greater than transaction output {}", - fee, - payment_value - ); - } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); - let output = TransactionOutput { - value: payment_value - fee, - script_pubkey, - }; + } + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; + let output = TransactionOutput { + value: payment_value - fee, + script_pubkey, + }; - let input = P2SHSpendingTxInput { - prev_transaction, - redeem_script, - outputs: vec![output], - script_data, - sequence: SEQUENCE_FINAL, - lock_time: time_lock, - keypair: &key_pair, - }; - let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + let input = P2SHSpendingTxInput { + prev_transaction, + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); - let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_tx_s!(tx_fut.await, transaction); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); - Ok(transaction.into()) - }; - Box::new(fut.boxed().compat()) + Ok(transaction.into()) } pub fn send_maker_payment_spend_preimage( @@ -1892,7 +1684,6 @@ pub fn create_maker_payment_spend_preimage( secret_hash: &[u8], swap_unique_data: &[u8], ) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); @@ -1910,6 +1701,7 @@ pub fn create_maker_payment_spend_preimage( .into(); let coin = coin.clone(); let fut = async move { + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let fee = try_tx_s!( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WatcherPreimage) .await @@ -1922,7 +1714,7 @@ pub fn create_maker_payment_spend_preimage( payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -1953,7 +1745,6 @@ pub fn create_taker_payment_refund_preimage( swap_unique_data: &[u8], ) -> TransactionFut { let coin = coin.clone(); - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; @@ -1970,6 +1761,7 @@ pub fn create_taker_payment_refund_preimage( ) .into(); let fut = async move { + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); let fee = try_tx_s!( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WatcherPreimage) .await @@ -1981,7 +1773,7 @@ pub fn create_taker_payment_refund_preimage( payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -2003,12 +1795,14 @@ pub fn create_taker_payment_refund_preimage( Box::new(fut.boxed().compat()) } -pub fn send_taker_spends_maker_payment(coin: T, args: SpendPaymentArgs) -> TransactionFut { - let my_address = try_tx_fus!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); - let mut prev_transaction: UtxoTx = try_tx_fus!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); +pub async fn send_taker_spends_maker_payment( + coin: T, + args: SpendPaymentArgs<'_>, +) -> TransactionResult { + let mut prev_transaction: UtxoTx = try_tx_s!(deserialize(args.other_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; drop_mutability!(prev_transaction); - let payment_value = try_tx_fus!(prev_transaction.first_output()).value; + let payment_value = try_tx_s!(prev_transaction.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); @@ -2017,58 +1811,55 @@ pub fn send_taker_spends_maker_payment(coin: T, args .push_opcode(Opcode::OP_0) .into_script(); - let time_lock = try_tx_fus!(args.time_lock.try_into()); + let time_lock = try_tx_s!(args.time_lock.try_into()); let redeem_script = payment_script( time_lock, args.secret_hash, - &try_tx_fus!(Public::from_slice(args.other_pubkey)), + &try_tx_s!(Public::from_slice(args.other_pubkey)), key_pair.public(), ) .into(); - let fut = async move { - let fee = try_tx_s!( - coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) - .await + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= payment_value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + payment_value ); - if fee >= payment_value { - return TX_PLAIN_ERR!( - "HTLC spend fee {} is greater than transaction output {}", - fee, - payment_value - ); - } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); - let output = TransactionOutput { - value: payment_value - fee, - script_pubkey, - }; + } + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; + let output = TransactionOutput { + value: payment_value - fee, + script_pubkey, + }; - let input = P2SHSpendingTxInput { - prev_transaction, - redeem_script, - outputs: vec![output], - script_data, - sequence: SEQUENCE_FINAL, - lock_time: time_lock, - keypair: &key_pair, - }; - let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + let input = P2SHSpendingTxInput { + prev_transaction, + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); - let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_tx_s!(tx_fut.await, transaction); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); - Ok(transaction.into()) - }; - Box::new(fut.boxed().compat()) + Ok(transaction.into()) } -async fn refund_htlc_payment( +pub async fn refund_htlc_payment( coin: T, args: RefundPaymentArgs<'_>, - payment_type: SwapPaymentType, -) -> TransactionResult { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); +) -> Result { + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); let mut prev_transaction: UtxoTx = try_tx_s!(deserialize(args.payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); prev_transaction.tx_hash_algo = coin.as_ref().tx_hash_algo; @@ -2080,19 +1871,10 @@ async fn refund_htlc_payment( let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); let time_lock = try_tx_s!(args.time_lock.try_into()); - let redeem_script = match payment_type { - SwapPaymentType::TakerOrMakerPayment => { - payment_script(time_lock, args.secret_hash, key_pair.public(), &other_public).into() - }, - SwapPaymentType::TakerFunding => { - swap_proto_v2_scripts::taker_funding_script(time_lock, args.secret_hash, key_pair.public(), &other_public) - .into() - }, - SwapPaymentType::TakerPaymentV2 => { - swap_proto_v2_scripts::taker_payment_script(time_lock, args.secret_hash, key_pair.public(), &other_public) - .into() - }, - }; + let redeem_script = args + .tx_type_with_secret_hash + .redeem_script(time_lock, key_pair.public(), &other_public) + .into(); let fee = try_tx_s!( coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) .await @@ -2104,7 +1886,7 @@ async fn refund_htlc_payment( payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -2124,7 +1906,7 @@ async fn refund_htlc_payment( let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); try_tx_s!(tx_fut.await, transaction); - Ok(transaction.into()) + Ok(transaction) } #[inline] @@ -2132,7 +1914,7 @@ pub async fn send_taker_refunds_payment( coin: T, args: RefundPaymentArgs<'_>, ) -> TransactionResult { - refund_htlc_payment(coin, args, SwapPaymentType::TakerOrMakerPayment).await + refund_htlc_payment(coin, args).await.map(|tx| tx.into()) } pub fn send_taker_payment_refund_preimage( @@ -2159,7 +1941,7 @@ pub async fn send_maker_refunds_payment( coin: T, args: RefundPaymentArgs<'_>, ) -> TransactionResult { - refund_htlc_payment(coin, args, SwapPaymentType::TakerOrMakerPayment).await + refund_htlc_payment(coin, args).await.map(|tx| tx.into()) } /// Extracts pubkey from script sig @@ -2316,8 +2098,7 @@ pub fn watcher_validate_taker_fee( let address = address_from_raw_pubkey( &fee_addr, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), @@ -2326,7 +2107,7 @@ pub fn watcher_validate_taker_fee( match taker_fee_tx.outputs.get(output_index) { Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(&address.hash).to_bytes(); + let expected_script_pubkey = Builder::build_p2pkh(address.hash()).to_bytes(); if out.script_pubkey != expected_script_pubkey { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", @@ -2359,8 +2140,7 @@ pub fn validate_fee( ) -> ValidatePaymentFut<()> { let address = try_f!(address_from_raw_pubkey( fee_addr, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.pub_t_addr_prefix, + coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), @@ -2409,7 +2189,7 @@ pub fn validate_fee( match tx.outputs.get(output_index) { Some(out) => { - let expected_script_pubkey = Builder::build_p2pkh(&address.hash).to_bytes(); + let expected_script_pubkey = Builder::build_p2pkh(address.hash()).to_bytes(); if out.script_pubkey != expected_script_pubkey { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Provided dex fee tx output script_pubkey doesn't match expected {:?} {:?}", @@ -2464,34 +2244,36 @@ pub fn validate_fee( Box::new(fut.boxed().compat()) } -pub fn validate_maker_payment( +pub async fn validate_maker_payment( coin: &T, input: ValidatePaymentInput, -) -> ValidatePaymentFut<()> { - let mut tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); +) -> ValidatePaymentResult<()> { + let mut tx: UtxoTx = deserialize(input.payment_tx.as_slice())?; tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let htlc_keypair = coin.derive_htlc_key_pair(&input.unique_swap_data); - let other_pub = - &try_f!(Public::from_slice(&input.other_pub) - .map_to_mm(|err| ValidatePaymentError::InvalidParameter(err.to_string()))); - let time_lock = try_f!(input + let other_pub = Public::from_slice(&input.other_pub) + .map_to_mm(|err| ValidatePaymentError::InvalidParameter(err.to_string()))?; + let time_lock = input .time_lock .try_into() - .map_to_mm(ValidatePaymentError::TimelockOverflow)); + .map_to_mm(ValidatePaymentError::TimelockOverflow)?; validate_payment( coin.clone(), - tx, + &tx, DEFAULT_SWAP_VOUT, - other_pub, + &other_pub, htlc_keypair.public(), - &input.secret_hash, + SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &input.secret_hash, + }, input.amount, input.watcher_reward, time_lock, input.try_spv_proof_until, input.confirmations, ) + .await } pub fn watcher_validate_taker_payment( @@ -2571,34 +2353,36 @@ pub fn watcher_validate_taker_payment( Box::new(fut.boxed().compat()) } -pub fn validate_taker_payment( +pub async fn validate_taker_payment( coin: &T, input: ValidatePaymentInput, -) -> ValidatePaymentFut<()> { - let mut tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); +) -> ValidatePaymentResult<()> { + let mut tx: UtxoTx = deserialize(input.payment_tx.as_slice())?; tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let htlc_keypair = coin.derive_htlc_key_pair(&input.unique_swap_data); - let other_pub = - &try_f!(Public::from_slice(&input.other_pub) - .map_to_mm(|err| ValidatePaymentError::InvalidParameter(err.to_string()))); - let time_lock = try_f!(input + let other_pub = Public::from_slice(&input.other_pub) + .map_to_mm(|err| ValidatePaymentError::InvalidParameter(err.to_string()))?; + let time_lock = input .time_lock .try_into() - .map_to_mm(ValidatePaymentError::TimelockOverflow)); + .map_to_mm(ValidatePaymentError::TimelockOverflow)?; validate_payment( coin.clone(), - tx, + &tx, DEFAULT_SWAP_VOUT, - other_pub, + &other_pub, htlc_keypair.public(), - &input.secret_hash, + SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: &input.secret_hash, + }, input.amount, input.watcher_reward, time_lock, input.try_spv_proof_until, input.confirmations, ) + .await } pub fn validate_payment_spend_or_refund( @@ -2608,24 +2392,26 @@ pub fn validate_payment_spend_or_refund( let mut payment_spend_tx: UtxoTx = try_f!(deserialize(input.payment_tx.as_slice())); payment_spend_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; - let my_address = try_f!(coin.as_ref().derivation_method.single_addr_or_err()); - let expected_script_pubkey = &output_script(my_address, ScriptType::P2PKH).to_bytes(); - let output = try_f!(payment_spend_tx - .outputs - .get(DEFAULT_SWAP_VOUT) - .ok_or_else(|| ValidatePaymentError::WrongPaymentTx("Payment tx has no outputs".to_string(),))); + let coin = coin.clone(); + let fut = async move { + let my_address = coin.as_ref().derivation_method.single_addr_or_err().await?; + let expected_script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; + let output = payment_spend_tx + .outputs + .get(DEFAULT_SWAP_VOUT) + .ok_or_else(|| ValidatePaymentError::WrongPaymentTx("Payment tx has no outputs".to_string()))?; - if expected_script_pubkey != &output.script_pubkey { - return Box::new(futures01::future::err( - ValidatePaymentError::WrongPaymentTx(format!( + if expected_script_pubkey != output.script_pubkey { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Provided payment tx script pubkey doesn't match expected {:?} {:?}", output.script_pubkey, expected_script_pubkey - )) - .into(), - )); - } + ))); + } - Box::new(futures01::future::ok(())) + Ok(()) + }; + + Box::new(fut.boxed().compat()) } pub fn check_if_my_payment_sent( @@ -2660,14 +2446,14 @@ pub fn check_if_my_payment_sent( } }, UtxoRpcClientEnum::Native(client) => { - let target_addr = Address { - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - prefix: coin.as_ref().conf.p2sh_addr_prefix, - hash: hash.into(), - checksum_type: coin.as_ref().conf.checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: coin.addr_format().clone(), - }; + let target_addr = AddressBuilder::new( + coin.addr_format_for_standard_scripts(), + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh(hash.into()) + .build()?; let target_addr = target_addr.to_string(); let is_imported = try_s!(client.is_address_imported(&target_addr).await); if !is_imported { @@ -2848,7 +2634,7 @@ pub fn sign_message(coin: &UtxoCoinFields, message: &str) -> SignatureResult( @@ -2858,23 +2644,25 @@ pub fn verify_message( address: &str, ) -> VerificationResult { let message_hash = sign_message_hash(coin.as_ref(), message).ok_or(VerificationError::PrefixNotFound)?; - let signature = CompactSignature::from(base64::decode(signature_base64)?); + let signature = CompactSignature::from(STANDARD.decode(signature_base64)?); let recovered_pubkey = Public::recover_compact(&H256::from(message_hash), &signature)?; let received_address = checked_address_from_str(coin, address)?; - Ok(AddressHashEnum::from(recovered_pubkey.address_hash()) == received_address.hash) + Ok(AddressHashEnum::from(recovered_pubkey.address_hash()) == *received_address.hash()) } pub fn my_balance(coin: T) -> BalanceFut where T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, { - let my_address = try_f!(coin - .as_ref() - .derivation_method - .single_addr_or_err() - .mm_err(BalanceError::from)) - .clone(); - let fut = async move { address_balance(&coin, &my_address).await }; + let fut = async move { + let my_address = coin + .as_ref() + .derivation_method + .single_addr_or_err() + .await + .mm_err(BalanceError::from)?; + address_balance(&coin, &my_address).await + }; Box::new(fut.boxed().compat()) } @@ -2902,6 +2690,157 @@ pub fn send_raw_tx_bytes( ) } +/// Helper to load unspent outputs from cache or rpc +async fn get_unspents_for_inputs( + coin: &UtxoCoinFields, + inputs: &Vec, +) -> Result, RawTransactionError> { + let txids_reversed = inputs + .iter() + .map(|input| input.previous_output.hash.reversed().into()) // reverse hashes to send to electrum + .collect::>(); + + if txids_reversed.is_empty() { + return Ok(vec![]); + } + + let prev_txns_loaded = utxo_common::get_verbose_transactions_from_cache_or_rpc(coin, txids_reversed) + .await + .map_err(|err| RawTransactionError::InvalidParam(err.to_string()))?; + + let mut unspents_loaded = Vec::with_capacity(inputs.len()); + + for input in inputs { + let prev_tx = prev_txns_loaded + .iter() + .find(|prev_tx| (*prev_tx.0).reversed() == input.previous_output.hash.into()) + .ok_or_else(|| { + RawTransactionError::NonExistentPrevOutputError(format!( + "{}/{}", + input.previous_output.hash, input.previous_output.index + )) + }); + let prev_tx = prev_tx?.1.to_inner(); + if (input.previous_output.index as usize) >= prev_tx.vout.len() { + return Err(RawTransactionError::NonExistentPrevOutputError(format!( + "{}/{}", + input.previous_output.hash, input.previous_output.index + ))); + } + let prev_script = Script::from( + prev_tx.vout[input.previous_output.index as usize] + .clone() + .script + .hex + .to_vec(), + ); + let prev_amount = prev_tx.vout[input.previous_output.index as usize] + .value + .ok_or_else(|| { + RawTransactionError::NonExistentPrevOutputError(String::from("No amount in transaction vout")) + })?; + let prev_amount = prev_amount.try_into().expect("Amount conversion must succeed"); + + unspents_loaded.push(UnspentInfo { + outpoint: OutPoint { + hash: input.previous_output.hash, + index: input.previous_output.index, + }, + value: sat_from_big_decimal(&prev_amount, coin.decimals) + .expect("Conversion to satoshi from bigdecimal must be valid"), + height: None, + script: prev_script, + }); + } + Ok(unspents_loaded) +} + +/// Takes args with a raw transaction in hexadecimal format and previous transactions data as input +/// Returns signed tx in hexadecimal format +pub async fn sign_raw_tx + UtxoTxGenerationOps>( + coin: &T, + args: &SignRawTransactionRequest, +) -> RawTransactionResult { + if let SignRawTransactionEnum::UTXO(utxo_args) = &args.tx { + sign_raw_utxo_tx(coin, utxo_args).await + } else { + MmError::err(RawTransactionError::InvalidParam("utxo type expected".to_string())) + } +} + +/// Takes args with a raw transaction in hexadecimal format and previous transactions data as input +/// Returns signed tx in hexadecimal format +async fn sign_raw_utxo_tx + UtxoTxGenerationOps>( + coin: &T, + args: &SignUtxoTransactionParams, +) -> RawTransactionResult { + let tx_bytes = + hex::decode(args.tx_hex.as_bytes()).map_to_mm(|e| RawTransactionError::DecodeError(e.to_string()))?; + let tx: UtxoTx = deserialize(tx_bytes.as_slice()).map_to_mm(|e| RawTransactionError::DecodeError(e.to_string()))?; + + let mut unspents = vec![]; + + if let Some(prev_txns) = &args.prev_txns { + for prev_utxo in prev_txns.iter() { + let prev_script = hex::decode(prev_utxo.clone().script_pub_key) + .map_to_mm(|e| RawTransactionError::DecodeError(e.to_string()))? + .into(); + + let prev_hash = hex::decode(prev_utxo.tx_hash.as_bytes()) + .map_to_mm(|e| RawTransactionError::DecodeError(e.to_string()))?; + + unspents.push(UnspentInfo { + outpoint: OutPoint { + hash: prev_hash.as_slice().into(), + index: prev_utxo.index, + }, + value: sat_from_big_decimal(&prev_utxo.amount, coin.as_ref().decimals) + .expect("conversion satoshi from bigdecimal must be valid"), + height: None, + script: prev_script, + }); + } + } + + let inputs_to_load = tx + .inputs() + .iter() + .filter(|input| !unspents.iter().any(|u| u.outpoint == input.previous_output)) + .cloned() + .collect::>(); + + // If some previous utxos are not provided in the params load them from the chain + if !inputs_to_load.is_empty() { + let loaded_unspents = get_unspents_for_inputs(coin.as_ref(), &inputs_to_load).await?; + unspents.extend(loaded_unspents.into_iter()); + } + + // TODO: use zeroise for privkey + let key_pair = coin.as_ref().priv_key_policy.activated_key_or_err().unwrap(); + + let mut input_signer_incomplete = TransactionInputSigner::from(tx); + input_signer_incomplete.consensus_branch_id = coin.as_ref().conf.consensus_branch_id; + + let builder = UtxoTxBuilder::new(coin) + .await + .with_transaction_input_signer(input_signer_incomplete) + .add_available_inputs(unspents); + let unsigned = builder + .build_unchecked() + .await + .map_err(|e| RawTransactionError::InvalidParam(e.to_string()))?; + debug!("Unsigned tx = {:?} for signing", unsigned); + + let signature_version = coin.as_ref().conf.signature_version; + let tx_signed = sign_tx(unsigned, key_pair, signature_version, coin.as_ref().conf.fork_id) + .map_err(|err| RawTransactionError::SigningError(err.to_string()))?; + + let tx_signed_bytes = serialize_with_flags(&tx_signed, SERIALIZE_TRANSACTION_WITNESS); + Ok(RawTransactionRes { + tx_hex: tx_signed_bytes.into(), + }) +} + pub fn wait_for_confirmations( coin: &UtxoCoinFields, input: ConfirmPaymentInput, @@ -2918,55 +2857,70 @@ pub fn wait_for_confirmations( ) } -pub fn wait_for_output_spend( +#[derive(Debug)] +pub enum WaitForOutputSpendErr { + NoOutputWithIndex(usize), + Timeout { wait_until: u64, now: u64 }, +} + +pub async fn wait_for_output_spend_impl( coin: &UtxoCoinFields, - tx_bytes: &[u8], + tx: &UtxoTx, output_index: usize, from_block: u64, wait_until: u64, check_every: f64, -) -> TransactionFut { - let mut tx: UtxoTx = try_tx_fus!(deserialize(tx_bytes).map_err(|e| ERRL!("{:?}", e))); - tx.tx_hash_algo = coin.tx_hash_algo; - let client = coin.rpc_client.clone(); - let tx_hash_algo = coin.tx_hash_algo; - let fut = async move { - loop { - let script_pubkey = &try_tx_s!(tx - .outputs - .get(output_index) - .ok_or(ERRL!("No output with index {}", output_index))) +) -> MmResult { + loop { + let script_pubkey = &tx + .outputs + .get(output_index) + .or_mm_err(|| WaitForOutputSpendErr::NoOutputWithIndex(output_index))? .script_pubkey; - match client - .find_output_spend( - tx.hash(), - script_pubkey, - output_index, - BlockHashOrHeight::Height(from_block as i64), - ) - .compat() - .await - { - Ok(Some(spent_output_info)) => { - let mut tx = spent_output_info.spending_tx; - tx.tx_hash_algo = tx_hash_algo; - return Ok(tx.into()); - }, - Ok(None) => (), - Err(e) => error!("Error on find_output_spend_of_tx: {}", e), - }; + match coin + .rpc_client + .find_output_spend( + tx.hash(), + script_pubkey, + output_index, + BlockHashOrHeight::Height(from_block as i64), + coin.tx_hash_algo, + ) + .compat() + .await + { + Ok(Some(spent_output_info)) => { + return Ok(spent_output_info.spending_tx); + }, + Ok(None) => (), + Err(e) => error!("Error on find_output_spend_of_tx: {}", e), + }; - if now_sec() > wait_until { - return TX_PLAIN_ERR!( - "Waited too long until {} for transaction {:?} {} to be spent ", - wait_until, - tx, - output_index, - ); - } - Timer::sleep(check_every).await; + let now = now_sec(); + if now > wait_until { + return MmError::err(WaitForOutputSpendErr::Timeout { wait_until, now }); } + Timer::sleep(check_every).await; + } +} + +pub fn wait_for_output_spend + Send + Sync + 'static>( + coin: T, + tx_bytes: &[u8], + output_index: usize, + from_block: u64, + wait_until: u64, + check_every: f64, +) -> TransactionFut { + let mut tx: UtxoTx = try_tx_fus!(deserialize(tx_bytes).map_err(|e| ERRL!("{:?}", e))); + tx.tx_hash_algo = coin.as_ref().tx_hash_algo; + + let fut = async move { + wait_for_output_spend_impl(coin.as_ref(), &tx, output_index, from_block, wait_until, check_every) + .await + .map(|tx| tx.into()) + .map_err(|e| TransactionErr::Plain(format!("{:?}", e))) }; Box::new(fut.boxed().compat()) } @@ -2998,7 +2952,7 @@ pub fn display_priv_key(coin: &UtxoCoinFields) -> Result { activated_key: ref activated_key_pair, .. } => Ok(activated_key_pair.private().to_string()), - PrivKeyPolicy::Trezor => ERR!("'display_priv_key' doesn't support Hardware Wallets"), + PrivKeyPolicy::Trezor => ERR!("'display_priv_key' is not supported for Hardware Wallets"), #[cfg(target_arch = "wasm32")] PrivKeyPolicy::Metamask(_) => ERR!("'display_priv_key' doesn't support Metamask"), } @@ -3041,16 +2995,20 @@ pub async fn get_tx_hex_by_hash(coin: &UtxoCoinFields, tx_hash: Vec) -> RawT pub async fn withdraw(coin: T, req: WithdrawRequest) -> WithdrawResult where - T: UtxoCommonOps + GetUtxoListOps + MarketCoinOps, + T: UtxoCommonOps + + GetUtxoListOps + + MarketCoinOps + + CoinWithDerivationMethod + + GetWithdrawSenderAddress
, { - StandardUtxoWithdraw::new(coin, req)?.build().await + StandardUtxoWithdraw::new(coin, req).await?.build().await } pub async fn init_withdraw( ctx: MmArc, coin: T, req: WithdrawRequest, - task_handle: &WithdrawTaskHandle, + task_handle: WithdrawTaskHandleShared, ) -> WithdrawResult where T: UtxoCommonOps @@ -3067,13 +3025,16 @@ pub async fn get_withdraw_from_address( req: &WithdrawRequest, ) -> MmResult, WithdrawError> where - T: CoinWithDerivationMethod
::HDWallet> - + HDWalletCoinOps
- + UtxoCommonOps, + T: CoinWithDerivationMethod + HDWalletCoinOps + HDCoinWithdrawOps + UtxoCommonOps, { match coin.derivation_method() { DerivationMethod::SingleAddress(my_address) => get_withdraw_iguana_sender(coin, req, my_address), - DerivationMethod::HDWallet(hd_wallet) => get_withdraw_hd_sender(coin, req, hd_wallet).await, + DerivationMethod::HDWallet(hd_wallet) => { + let from = req.from.clone().or_mm_err(|| WithdrawError::FromAddressNotFound)?; + coin.get_withdraw_hd_sender(hd_wallet, &from) + .await + .mm_err(WithdrawError::from) + }, } } @@ -3097,84 +3058,31 @@ pub fn get_withdraw_iguana_sender( }) } -pub async fn get_withdraw_hd_sender( - coin: &T, - req: &WithdrawRequest, - hd_wallet: &T::HDWallet, -) -> MmResult, WithdrawError> -where - T: HDWalletCoinOps
+ Sync, -{ - let HDAccountAddressId { - account_id, - chain, - address_id, - } = match req.from.clone().or_mm_err(|| WithdrawError::FromAddressNotFound)? { - WithdrawFrom::AddressId(id) => id, - WithdrawFrom::DerivationPath { derivation_path } => { - let derivation_path = StandardHDPath::from_str(&derivation_path) - .map_to_mm(StandardHDPathError::from) - .mm_err(|e| WithdrawError::UnexpectedFromAddress(e.to_string()))?; - let coin_type = derivation_path.coin_type(); - let expected_coin_type = hd_wallet.coin_type(); - if coin_type != expected_coin_type { - let error = format!( - "Derivation path '{}' must has '{}' coin type", - derivation_path, expected_coin_type - ); - return MmError::err(WithdrawError::UnexpectedFromAddress(error)); - } - HDAccountAddressId::from(derivation_path) - }, - WithdrawFrom::HDWalletAddress(_) => { - return MmError::err(WithdrawError::UnsupportedError( - "`WithdrawFrom::HDWalletAddress` is not supported for `get_withdraw_hd_sender`".to_string(), - )) - }, - }; - - let hd_account = hd_wallet - .get_account(account_id) - .await - .or_mm_err(|| WithdrawError::UnknownAccount { account_id })?; - let hd_address = coin.derive_address(&hd_account, chain, address_id).await?; - - let is_address_activated = hd_account - .is_address_activated(chain, address_id) - // If [`HDWalletCoinOps::derive_address`] succeeds, [`HDAccountOps::is_address_activated`] shouldn't fails with an `InvalidBip44ChainError`. - .mm_err(|e| WithdrawError::InternalError(e.to_string()))?; - if !is_address_activated { - let error = format!("'{}' address is not activated", hd_address.address); - return MmError::err(WithdrawError::UnexpectedFromAddress(error)); - } - - Ok(WithdrawSenderAddress::from(hd_address)) -} - pub fn decimals(coin: &UtxoCoinFields) -> u8 { coin.decimals } pub fn convert_to_address(coin: &T, from: &str, to_address_format: Json) -> Result { let to_address_format: UtxoAddressFormat = json::from_value(to_address_format).map_err(|e| ERRL!("Error on parse UTXO address format {:?}", e))?; - let mut from_address = try_s!(coin.address_from_str(from)); + let from_address = try_s!(coin.address_from_str(from)); match to_address_format { UtxoAddressFormat::Standard => { - from_address.addr_format = UtxoAddressFormat::Standard; - Ok(from_address.to_string()) + // assuming convertion to p2pkh + Ok(LegacyAddress::new( + from_address.hash(), + coin.as_ref().conf.address_prefixes.p2pkh.clone(), + coin.as_ref().conf.checksum_type, + ) + .to_string()) }, UtxoAddressFormat::Segwit => { let bech32_hrp = &coin.as_ref().conf.bech32_hrp; match bech32_hrp { - Some(hrp) => Ok(SegwitAddress::new(&from_address.hash, hrp.clone()).to_string()), + Some(hrp) => Ok(SegwitAddress::new(from_address.hash(), hrp.clone()).to_string()), None => ERR!("Cannot convert to a segwit address for a coin with no bech32_hrp in config"), } }, UtxoAddressFormat::CashAddress { network, .. } => Ok(try_s!(from_address - .to_cashaddress( - &network, - coin.as_ref().conf.pub_addr_prefix, - coin.as_ref().conf.p2sh_addr_prefix - ) + .to_cashaddress(&network, &coin.as_ref().conf.address_prefixes) .and_then(|cashaddress| cashaddress.encode()))), } } @@ -3191,11 +3099,10 @@ pub fn validate_address(coin: &T, address: &str) -> ValidateAd }, }; - let is_p2pkh = address.prefix == coin.as_ref().conf.pub_addr_prefix - && address.t_addr_prefix == coin.as_ref().conf.pub_t_addr_prefix; - let is_p2sh = address.prefix == coin.as_ref().conf.p2sh_addr_prefix - && address.t_addr_prefix == coin.as_ref().conf.p2sh_t_addr_prefix; - let is_segwit = address.hrp.is_some() && address.hrp == coin.as_ref().conf.bech32_hrp && coin.as_ref().conf.segwit; + let is_p2pkh = address.prefix() == &coin.as_ref().conf.address_prefixes.p2pkh; + let is_p2sh = address.prefix() == &coin.as_ref().conf.address_prefixes.p2sh; + let is_segwit = + address.hrp().is_some() && address.hrp() == &coin.as_ref().conf.bech32_hrp && coin.as_ref().conf.segwit; if is_p2pkh || is_p2sh || is_segwit { ValidateAddressResult { @@ -3205,7 +3112,7 @@ pub fn validate_address(coin: &T, address: &str) -> ValidateAd } else { ValidateAddressResult { is_valid: false, - reason: Some(ERRL!("Address {} has invalid prefixes", address)), + reason: Some(ERRL!("Address {} has invalid prefix", address)), } } } @@ -3307,7 +3214,7 @@ where let mut history_map: HashMap = history .into_iter() .filter_map(|tx| { - let tx_hash = H256Json::from_str(&tx.tx_hash).ok()?; + let tx_hash = H256Json::from_str(tx.tx.tx_hash()?).ok()?; Some((tx_hash, tx)) }) .collect(); @@ -3569,11 +3476,14 @@ where .collect() }, UtxoRpcClientEnum::Electrum(client) => { - let my_address = match coin.as_ref().derivation_method.single_addr_or_err() { + let my_address = match coin.as_ref().derivation_method.single_addr_or_err().await { Ok(my_address) => my_address, Err(e) => return RequestTxHistoryResult::CriticalError(e.to_string()), }; - let script = output_script(my_address, ScriptType::P2PKH); + let script = match output_script(&my_address) { + Ok(script) => script, + Err(err) => return RequestTxHistoryResult::CriticalError(err.to_string()), + }; let script_hash = electrum_script_hash(&script); mm_counter!(metrics, "tx.history.request.count", 1, @@ -3632,7 +3542,7 @@ pub async fn tx_details_by_hash( let verbose_tx = try_s!(coin.as_ref().rpc_client.get_verbose_transaction(&hash).compat().await); let mut tx: UtxoTx = try_s!(deserialize(verbose_tx.hex.as_slice()).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; - let my_address = try_s!(coin.as_ref().derivation_method.single_addr_or_err()); + let my_address = try_s!(coin.as_ref().derivation_method.single_addr_or_err().await); input_transactions.insert(hash, HistoryUtxoTx { tx: tx.clone(), @@ -3671,7 +3581,7 @@ pub async fn tx_details_by_hash( ))?; input_amount += prev_tx_output.value; let from: Vec
= try_s!(coin.addresses_from_script(&prev_tx_output.script_pubkey.clone().into())); - if from.contains(my_address) { + if from.contains(&my_address) { spent_by_me += prev_tx_output.value; } from_addresses.extend(from.into_iter()); @@ -3680,7 +3590,7 @@ pub async fn tx_details_by_hash( for output in tx.outputs.iter() { output_amount += output.value; let to = try_s!(coin.addresses_from_script(&output.script_pubkey.clone().into())); - if to.contains(my_address) { + if to.contains(&my_address) { received_by_me += output.value; } to_addresses.extend(to.into_iter()); @@ -3754,8 +3664,7 @@ pub async fn tx_details_by_hash( spent_by_me: big_decimal_from_sat_unsigned(spent_by_me, coin.as_ref().decimals), my_balance_change: big_decimal_from_sat(received_by_me as i64 - spent_by_me as i64, coin.as_ref().decimals), total_amount: big_decimal_from_sat_unsigned(input_amount, coin.as_ref().decimals), - tx_hash: tx.hash().reversed().to_vec().to_tx_hash(), - tx_hex: verbose_tx.hex, + tx: TransactionData::new_signed(verbose_tx.hex, tx.hash().reversed().to_vec().to_tx_hash()), fee_details: Some(fee_details.into()), block_height: verbose_tx.height.unwrap_or(0), coin: ticker.clone(), @@ -3810,14 +3719,19 @@ pub async fn update_kmd_rewards( where T: UtxoCommonOps + UtxoStandardOps + MarketCoinOps, { + let (Some(tx_hex), Some(tx_hash)) = (tx_details.tx.tx_hex(), tx_details.tx.tx_hash()) else { + return MmError::err(UtxoRpcError::Internal("Invalid TransactionDetails".to_string())); + }; + if !tx_details.should_update_kmd_rewards() { let error = "There is no need to update KMD rewards".to_owned(); return MmError::err(UtxoRpcError::Internal(error)); } - let tx: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).map_to_mm(|e| { + + let tx: UtxoTx = deserialize(tx_hex.as_slice()).map_to_mm(|e| { UtxoRpcError::Internal(format!( "Error deserializing the {:?} transaction hex: {:?}", - tx_details.tx_hash, e + tx_hash, e )) })?; let kmd_rewards = coin.calc_interest_of_tx(&tx, input_transactions).await?; @@ -3831,6 +3745,7 @@ where })); } + // Todo: https://github.com/KomodoPlatform/komodo-defi-framework/issues/1625 let my_address = &coin.my_address()?; let claimed_by_me = tx_details.from.iter().all(|from| from == my_address) && tx_details.to.contains(my_address); @@ -3922,7 +3837,7 @@ where let tx_fee = coin.get_tx_fee().await?; // [`FeePolicy::DeductFromOutput`] is used if the value is [`TradePreimageValue::UpperBound`] only let is_amount_upper_bound = matches!(fee_policy, FeePolicy::DeductFromOutput(_)); - let my_address = coin.as_ref().derivation_method.single_addr_or_err()?; + let my_address = coin.as_ref().derivation_method.single_addr_or_err().await?; match tx_fee { // if it's a dynamic fee, we should generate a swap transaction to get an actual trade fee @@ -3931,11 +3846,12 @@ where let dynamic_fee = coin.increase_dynamic_fee_by_stage(fee, stage); let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(my_address).await?; + let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; let actual_tx_fee = ActualTxFee::Dynamic(dynamic_fee); let mut tx_builder = UtxoTxBuilder::new(coin) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy) @@ -3959,9 +3875,10 @@ where }, ActualTxFee::FixedPerKb(fee) => { let outputs_count = outputs.len(); - let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(my_address).await?; + let (unspents, _recently_sent_txs) = coin.get_unspent_ordered_list(&my_address).await?; let mut tx_builder = UtxoTxBuilder::new(coin) + .await .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(fee_policy) @@ -4021,9 +3938,10 @@ where time_lock, my_pub, other_pub, - secret_hash, amount, - SwapPaymentType::TakerOrMakerPayment, + SwapTxTypeWithSecretHash::TakerOrMakerPayment { + maker_secret_hash: secret_hash, + }, ) .map_to_mm(TradePreimageError::InternalError)?; let gas_fee = None; @@ -4061,7 +3979,7 @@ where { let decimals = coin.as_ref().decimals; - let outputs = generate_taker_fee_tx_outputs(decimals, &AddressHashEnum::default_address_hash(), dex_fee)?; + let outputs = generate_taker_fee_tx_outputs(decimals, &AddressHashEnum::default_address_hash(), &dex_fee)?; let gas_fee = None; let fee_amount = coin @@ -4318,119 +4236,107 @@ pub fn big_decimal_from_sat_unsigned(satoshis: u64, decimals: u8) -> BigDecimal pub fn address_from_raw_pubkey( pub_key: &[u8], - prefix: u8, - t_addr_prefix: u8, + prefixes: NetworkAddressPrefixes, checksum_type: ChecksumType, hrp: Option, addr_format: UtxoAddressFormat, ) -> Result { - Ok(Address { - t_addr_prefix, - prefix, - hash: try_s!(Public::from_slice(pub_key)).address_hash().into(), - checksum_type, - hrp, - addr_format, - }) + AddressBuilder::new(addr_format, checksum_type, prefixes, hrp) + .as_pkh_from_pk(try_s!(Public::from_slice(pub_key))) + .build() } pub fn address_from_pubkey( - pub_key: &Public, - prefix: u8, - t_addr_prefix: u8, + pubkey: &Public, + prefixes: NetworkAddressPrefixes, checksum_type: ChecksumType, hrp: Option, addr_format: UtxoAddressFormat, ) -> Address { - Address { - t_addr_prefix, - prefix, - hash: pub_key.address_hash().into(), - checksum_type, - hrp, - addr_format, - } + AddressBuilder::new(addr_format, checksum_type, prefixes, hrp) + .as_pkh_from_pk(*pubkey) + .build() + .expect("valid address props") } #[allow(clippy::too_many_arguments)] #[cfg_attr(test, mockable)] -pub fn validate_payment( +pub async fn validate_payment<'a, T: UtxoCommonOps>( coin: T, - tx: UtxoTx, + tx: &'a UtxoTx, output_index: usize, - first_pub0: &Public, - second_pub0: &Public, - priv_bn_hash: &[u8], + first_pub0: &'a Public, + second_pub0: &'a Public, + tx_type_with_secret_hash: SwapTxTypeWithSecretHash<'a>, amount: BigDecimal, watcher_reward: Option, time_lock: u32, try_spv_proof_until: u64, confirmations: u64, -) -> ValidatePaymentFut<()> { - let amount = try_f!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); +) -> ValidatePaymentResult<()> { + let amount = sat_from_big_decimal(&amount, coin.as_ref().decimals)?; - let expected_redeem = payment_script(time_lock, priv_bn_hash, first_pub0, second_pub0); - let fut = async move { - let tx_hash = tx.tx_hash(); + let expected_redeem = tx_type_with_secret_hash.redeem_script(time_lock, first_pub0, second_pub0); + let tx_hash = tx.tx_hash_as_bytes(); - let tx_from_rpc = retry_on_err!(coin - .as_ref() + let tx_from_rpc = retry_on_err!(async { + coin.as_ref() .rpc_client .get_transaction_bytes(&tx.hash().reversed().into()) - .compat()) - .repeat_every_secs(10.) - .attempts(4) - .inspect_err(move |e| error!("Error getting tx {tx_hash:?} from rpc: {e:?}")) - .await - .map_err(|repeat_err| repeat_err.into_error().map(ValidatePaymentError::from))?; + .compat() + .await + }) + .repeat_every_secs(10.) + .attempts(4) + .inspect_err(move |e| error!("Error getting tx {tx_hash:?} from rpc: {e:?}")) + .await + .map_err(|repeat_err| repeat_err.into_error().map(ValidatePaymentError::from))?; - if serialize(&tx).take() != tx_from_rpc.0 - && serialize_with_flags(&tx, SERIALIZE_TRANSACTION_WITNESS).take() != tx_from_rpc.0 - { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided payment tx {:?} doesn't match tx data from rpc {:?}", - tx, tx_from_rpc - ))); - } + if serialize(tx).take() != tx_from_rpc.0 + && serialize_with_flags(tx, SERIALIZE_TRANSACTION_WITNESS).take() != tx_from_rpc.0 + { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided payment tx {:?} doesn't match tx data from rpc {:?}", + tx, tx_from_rpc + ))); + } - let expected_script_pubkey: Bytes = Builder::build_p2sh(&dhash160(&expected_redeem).into()).into(); + let expected_script_pubkey: Bytes = Builder::build_p2sh(&dhash160(&expected_redeem).into()).into(); - let actual_output = match tx.outputs.get(output_index) { - Some(output) => output, - None => { - return MmError::err(ValidatePaymentError::WrongPaymentTx( - "Payment tx has no outputs".to_string(), - )) - }, - }; + let actual_output = match tx.outputs.get(output_index) { + Some(output) => output, + None => { + return MmError::err(ValidatePaymentError::WrongPaymentTx( + "Payment tx has no outputs".to_string(), + )) + }, + }; - if expected_script_pubkey != actual_output.script_pubkey { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided payment tx script pubkey doesn't match expected {:?} {:?}", - actual_output.script_pubkey, expected_script_pubkey - ))); - } + if expected_script_pubkey != actual_output.script_pubkey { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided payment tx script pubkey doesn't match expected {:?} {:?}", + actual_output.script_pubkey, expected_script_pubkey + ))); + } - if let Some(watcher_reward) = watcher_reward { - let expected_reward = sat_from_big_decimal(&watcher_reward.amount, coin.as_ref().decimals)?; - let actual_reward = actual_output.value - amount; - validate_watcher_reward(expected_reward, actual_reward, false)?; - } else if actual_output.value != amount { - return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( - "Provided payment tx output value doesn't match expected {:?} {:?}", - actual_output.value, amount - ))); - } + if let Some(watcher_reward) = watcher_reward { + let expected_reward = sat_from_big_decimal(&watcher_reward.amount, coin.as_ref().decimals)?; + let actual_reward = actual_output.value - amount; + validate_watcher_reward(expected_reward, actual_reward, false)?; + } else if actual_output.value != amount { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "Provided payment tx output value doesn't match expected {:?} {:?}", + actual_output.value, amount + ))); + } - if let UtxoRpcClientEnum::Electrum(client) = &coin.as_ref().rpc_client { - if coin.as_ref().conf.spv_conf.is_some() && confirmations != 0 { - client.validate_spv_proof(&tx, try_spv_proof_until).await?; - } + if let UtxoRpcClientEnum::Electrum(client) = &coin.as_ref().rpc_client { + if coin.as_ref().conf.spv_conf.is_some() && confirmations != 0 { + client.validate_spv_proof(tx, try_spv_proof_until).await?; } + } - Ok(()) - }; - Box::new(fut.boxed().compat()) + Ok(()) } #[allow(clippy::too_many_arguments)] @@ -4473,16 +4379,16 @@ async fn search_for_swap_output_spend( tx.hash(), script_pubkey, output_index, - BlockHashOrHeight::Height(search_from_block as i64) + BlockHashOrHeight::Height(search_from_block as i64), + coin.tx_hash_algo, ) .compat() .await ); match spend { Some(spent_output_info) => { - let mut tx = spent_output_info.spending_tx; - tx.tx_hash_algo = coin.tx_hash_algo; - let script: Script = tx.inputs[DEFAULT_SWAP_VIN].script_sig.clone().into(); + let tx = spent_output_info.spending_tx; + let script: Script = spent_output_info.input.script_sig.into(); if let Some(Ok(ref i)) = script.iter().nth(2) { if i.opcode == Opcode::OP_0 { return Ok(Some(FoundSwapTxSpend::Spent(tx.into()))); @@ -4509,35 +4415,20 @@ struct SwapPaymentOutputsResult { outputs: Vec, } -enum SwapPaymentType { - TakerOrMakerPayment, - TakerFunding, - TakerPaymentV2, -} - fn generate_swap_payment_outputs( coin: T, time_lock: u32, my_pub: &[u8], other_pub: &[u8], - secret_hash: &[u8], amount: BigDecimal, - payment_type: SwapPaymentType, + tx_type: SwapTxTypeWithSecretHash<'_>, ) -> Result where T: AsRef, { let my_public = try_s!(Public::from_slice(my_pub)); let other_public = try_s!(Public::from_slice(other_pub)); - let redeem_script = match payment_type { - SwapPaymentType::TakerOrMakerPayment => payment_script(time_lock, secret_hash, &my_public, &other_public), - SwapPaymentType::TakerFunding => { - swap_proto_v2_scripts::taker_funding_script(time_lock, secret_hash, &my_public, &other_public) - }, - SwapPaymentType::TakerPaymentV2 => { - swap_proto_v2_scripts::taker_payment_script(time_lock, secret_hash, &my_public, &other_public) - }, - }; + let redeem_script = tx_type.redeem_script(time_lock, &my_public, &other_public); let redeem_script_hash = dhash160(&redeem_script); let amount = try_s!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); let htlc_out = TransactionOutput { @@ -4552,7 +4443,7 @@ where op_return_builder = if coin.as_ref().conf.ticker == "ARRR" { op_return_builder.push_data(&redeem_script) } else { - op_return_builder.push_bytes(secret_hash) + op_return_builder.push_data(&tx_type.op_return_data()) }; let op_return_script = op_return_builder.into_bytes(); @@ -4562,14 +4453,14 @@ where script_pubkey: op_return_script, }; - let payment_address = Address { - checksum_type: coin.as_ref().conf.checksum_type, - hash: redeem_script_hash.into(), - prefix: coin.as_ref().conf.p2sh_addr_prefix, - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let payment_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh(redeem_script_hash.into()) + .build()?; let result = SwapPaymentOutputsResult { payment_address, outputs: vec![htlc_out, op_return_out], @@ -4580,26 +4471,26 @@ where pub fn payment_script(time_lock: u32, secret_hash: &[u8], pub_0: &Public, pub_1: &Public) -> Script { let mut builder = Builder::default() .push_opcode(Opcode::OP_IF) - .push_bytes(&time_lock.to_le_bytes()) + .push_data(&time_lock.to_le_bytes()) .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY) .push_opcode(Opcode::OP_DROP) - .push_bytes(pub_0) + .push_data(pub_0) .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ELSE) .push_opcode(Opcode::OP_SIZE) - .push_bytes(&[32]) + .push_data(&[32]) .push_opcode(Opcode::OP_EQUALVERIFY) .push_opcode(Opcode::OP_HASH160); if secret_hash.len() == 32 { - builder = builder.push_bytes(ripemd160(secret_hash).as_slice()); + builder = builder.push_data(ripemd160(secret_hash).as_slice()); } else { - builder = builder.push_bytes(secret_hash); + builder = builder.push_data(secret_hash); } builder .push_opcode(Opcode::OP_EQUALVERIFY) - .push_bytes(pub_1) + .push_data(pub_1) .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ENDIF) .into_script() @@ -4608,16 +4499,16 @@ pub fn payment_script(time_lock: u32, secret_hash: &[u8], pub_0: &Public, pub_1: pub fn dex_fee_script(uuid: [u8; 16], time_lock: u32, watcher_pub: &Public, sender_pub: &Public) -> Script { let builder = Builder::default(); builder - .push_bytes(&uuid) + .push_data(&uuid) .push_opcode(Opcode::OP_DROP) .push_opcode(Opcode::OP_IF) - .push_bytes(&time_lock.to_le_bytes()) + .push_data(&time_lock.to_le_bytes()) .push_opcode(Opcode::OP_CHECKLOCKTIMEVERIFY) .push_opcode(Opcode::OP_DROP) - .push_bytes(sender_pub) + .push_data(sender_pub) .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ELSE) - .push_bytes(watcher_pub) + .push_data(watcher_pub) .push_opcode(Opcode::OP_CHECKSIG) .push_opcode(Opcode::OP_ENDIF) .into_script() @@ -4776,7 +4667,7 @@ where pub fn addr_format(coin: &dyn AsRef) -> &UtxoAddressFormat { match coin.as_ref().derivation_method { - DerivationMethod::SingleAddress(ref my_address) => &my_address.addr_format, + DerivationMethod::SingleAddress(ref my_address) => my_address.addr_format(), DerivationMethod::HDWallet(UtxoHDWallet { ref address_format, .. }) => address_format, } } @@ -4794,12 +4685,12 @@ where { let conf = &coin.as_ref().conf; - match addr.addr_format { + match addr.addr_format() { // Considering that legacy is supported with any configured formats // This can be changed depending on the coins implementation UtxoAddressFormat::Standard => { - let is_p2pkh = addr.prefix == conf.pub_addr_prefix && addr.t_addr_prefix == conf.pub_t_addr_prefix; - let is_p2sh = addr.prefix == conf.p2sh_addr_prefix && addr.t_addr_prefix == conf.p2sh_t_addr_prefix; + let is_p2pkh = addr.prefix() == &conf.address_prefixes.p2pkh; + let is_p2sh = addr.prefix() == &conf.address_prefixes.p2sh; if !is_p2pkh && !is_p2sh { MmError::err(UnsupportedAddr::PrefixError(conf.ticker.clone())) } else { @@ -4811,23 +4702,23 @@ where return MmError::err(UnsupportedAddr::SegwitNotActivated(conf.ticker.clone())); } - if addr.hrp != conf.bech32_hrp { + if addr.hrp() != &conf.bech32_hrp { MmError::err(UnsupportedAddr::HrpError { ticker: conf.ticker.clone(), - hrp: addr.hrp.clone().unwrap_or_default(), + hrp: addr.hrp().clone().unwrap_or_default(), }) } else { Ok(()) } }, UtxoAddressFormat::CashAddress { .. } => { - if addr.addr_format == conf.default_address_format || addr.addr_format == *coin.addr_format() { + if addr.addr_format() == &conf.default_address_format || addr.addr_format() == coin.addr_format() { Ok(()) } else { MmError::err(UnsupportedAddr::FormatMismatch { ticker: conf.ticker.clone(), activated_format: coin.addr_format().to_string(), - used_format: addr.addr_format.to_string(), + used_format: addr.addr_format().to_string(), }) } }, @@ -4897,7 +4788,7 @@ where T: UtxoCommonOps + GetUtxoListOps + SwapOps, { let taker_htlc_key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); - let total_amount = &args.dex_fee_amount + &args.premium_amount + &args.trading_amount; + let total_amount = &args.dex_fee.total_spend_amount().to_decimal() + &args.premium_amount + &args.trading_amount; let SwapPaymentOutputsResult { payment_address, @@ -4907,9 +4798,10 @@ where try_tx_s!(args.time_lock.try_into()), taker_htlc_key_pair.public_slice(), args.maker_pub, - args.taker_secret_hash, total_amount, - SwapPaymentType::TakerFunding, + SwapTxTypeWithSecretHash::TakerFunding { + taker_secret_hash: args.taker_secret_hash + }, )); if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { let addr_string = try_tx_s!(payment_address.display_address()); @@ -4922,14 +4814,6 @@ where send_outputs_from_my_address_impl(coin, outputs).await } -/// Common implementation of taker funding reclaim for UTXO coins using time-locked path. -pub async fn refund_taker_funding_timelock(coin: T, args: RefundPaymentArgs<'_>) -> TransactionResult -where - T: UtxoCommonOps + GetUtxoListOps + SwapOps, -{ - refund_htlc_payment(coin, args, SwapPaymentType::TakerFunding).await -} - /// Common implementation of taker funding reclaim for UTXO coins using immediate refund path with secret reveal. pub async fn refund_taker_funding_secret( coin: T, @@ -4938,7 +4822,7 @@ pub async fn refund_taker_funding_secret( where T: UtxoCommonOps + GetUtxoListOps + SwapOps, { - let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err()).clone(); + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); let payment_value = try_tx_s!(args.funding_tx.first_output()).value; let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); @@ -4967,7 +4851,7 @@ where payment_value ); } - let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); + let script_pubkey = output_script(&my_address).map(|script| script.to_bytes())?; let output = TransactionOutput { value: payment_value - fee, script_pubkey, @@ -4991,19 +4875,20 @@ where } /// Common implementation of taker funding validation for UTXO coins. -pub async fn validate_taker_funding(coin: &T, args: ValidateTakerFundingArgs<'_, T>) -> ValidateTakerFundingResult +pub async fn validate_taker_funding(coin: &T, args: ValidateTakerFundingArgs<'_, T>) -> ValidateSwapV2TxResult where T: UtxoCommonOps + SwapOps, { let maker_htlc_key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); - let total_expected_amount = &args.dex_fee_amount + &args.premium_amount + &args.trading_amount; + let total_expected_amount = + &args.dex_fee.total_spend_amount().to_decimal() + &args.premium_amount + &args.trading_amount; let expected_amount_sat = sat_from_big_decimal(&total_expected_amount, coin.as_ref().decimals)?; let time_lock = args .time_lock .try_into() - .map_to_mm(|e: TryFromIntError| ValidateTakerFundingError::LocktimeOverflow(e.to_string()))?; + .map_to_mm(|e: TryFromIntError| ValidateSwapV2TxError::LocktimeOverflow(e.to_string()))?; let redeem_script = swap_proto_v2_scripts::taker_funding_script( time_lock, @@ -5017,7 +4902,7 @@ where }; if args.funding_tx.outputs.get(0) != Some(&expected_output) { - return MmError::err(ValidateTakerFundingError::InvalidDestinationOrAmount(format!( + return MmError::err(ValidateSwapV2TxError::InvalidDestinationOrAmount(format!( "Expected {:?}, got {:?}", expected_output, args.funding_tx.outputs.get(0) @@ -5032,20 +4917,214 @@ where .await?; let actual_tx_bytes = serialize(args.funding_tx).take(); if tx_bytes_from_rpc.0 != actual_tx_bytes { - return MmError::err(ValidateTakerFundingError::TxBytesMismatch { + return MmError::err(ValidateSwapV2TxError::TxBytesMismatch { from_rpc: tx_bytes_from_rpc, actual: actual_tx_bytes.into(), }); } + + // import funding address in native mode to track funding tx spend + let funding_address = AddressBuilder::new( + AddressFormat::Standard, + coin.as_ref().conf.checksum_type, + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh(dhash160(&redeem_script).into()) + .build() + .map_to_mm(ValidateSwapV2TxError::Internal)?; + + if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { + let addr_string = funding_address + .display_address() + .map_to_mm(ValidateSwapV2TxError::Internal)?; + client + .import_address(&addr_string, &addr_string, false) + .compat() + .await + .map_to_mm(|e| ValidateSwapV2TxError::Rpc(e.to_string()))?; + } Ok(()) } -/// Common implementation of combined taker payment refund for UTXO coins. -pub async fn refund_combined_taker_payment(coin: T, args: RefundPaymentArgs<'_>) -> TransactionResult +/// Common implementation of maker payment v2 generation and broadcast for UTXO coins. +pub async fn send_maker_payment_v2(coin: T, args: SendMakerPaymentArgs<'_, T>) -> Result where T: UtxoCommonOps + GetUtxoListOps + SwapOps, { - refund_htlc_payment(coin, args, SwapPaymentType::TakerPaymentV2).await + let maker_htlc_key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); + + let SwapPaymentOutputsResult { + payment_address, + outputs, + } = try_tx_s!(generate_swap_payment_outputs( + &coin, + try_tx_s!(args.time_lock.try_into()), + maker_htlc_key_pair.public_slice(), + args.taker_pub, + args.amount, + SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash: args.maker_secret_hash, + taker_secret_hash: args.taker_secret_hash, + }, + )); + if let UtxoRpcClientEnum::Native(client) = &coin.as_ref().rpc_client { + let addr_string = try_tx_s!(payment_address.display_address()); + client + .import_address(&addr_string, &addr_string, false) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))) + .compat() + .await?; + } + send_outputs_from_my_address_impl(coin, outputs).await +} + +pub fn address_to_scripthash(address: &Address) -> Result { + let script = output_script(address)?; + let script_hash = electrum_script_hash(&script); + Ok(hex::encode(script_hash)) +} + +pub async fn utxo_prepare_addresses_for_balance_stream_if_enabled( + coin: &T, + addresses: HashSet, +) -> MmResult<(), String> +where + T: UtxoCommonOps, +{ + let mut valid_addresses = HashSet::with_capacity(addresses.len()); + for address in addresses { + let valid_address = address_from_str_unchecked(coin.as_ref(), &address).mm_err(|e| e.to_string())?; + valid_addresses.insert(valid_address); + } + if let UtxoRpcClientEnum::Electrum(electrum_client) = &coin.as_ref().rpc_client { + if let Some(sender) = &electrum_client.scripthash_notification_sender { + sender + .unbounded_send(ScripthashNotification::SubscribeToAddresses(valid_addresses)) + .map_err(|e| ERRL!("Failed sending scripthash message. {}", e))?; + } + }; + + Ok(()) +} + +pub async fn spend_maker_payment_v2( + coin: &T, + args: SpendMakerPaymentArgs<'_, T>, +) -> Result { + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); + let payment_value = try_tx_s!(args.maker_payment_tx.first_output()).value; + + let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); + let script_data = Builder::default() + .push_data(args.maker_secret) + .push_opcode(Opcode::OP_1) + .push_opcode(Opcode::OP_0) + .into_script(); + let time_lock = try_tx_s!(args.time_lock.try_into()); + + let redeem_script = swap_proto_v2_scripts::maker_payment_script( + time_lock, + args.maker_secret_hash, + args.taker_secret_hash, + args.maker_pub, + key_pair.public(), + ) + .into(); + + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= payment_value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + payment_value + ); + } + let script_pubkey = try_tx_s!(output_script(&my_address)).to_bytes(); + let output = TransactionOutput { + value: payment_value - fee, + script_pubkey, + }; + + let input = P2SHSpendingTxInput { + prev_transaction: args.maker_payment_tx.clone(), + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); + + Ok(transaction) +} + +/// Common implementation of maker payment v2 reclaim for UTXO coins using immediate refund path with secret reveal. +pub async fn refund_maker_payment_v2_secret( + coin: T, + args: RefundMakerPaymentArgs<'_, T>, +) -> Result +where + T: UtxoCommonOps + SwapOps, +{ + let my_address = try_tx_s!(coin.as_ref().derivation_method.single_addr_or_err().await).clone(); + let payment_value = try_tx_s!(args.maker_payment_tx.first_output()).value; + + let key_pair = coin.derive_htlc_key_pair(args.swap_unique_data); + let script_data = Builder::default() + .push_data(args.taker_secret) + .push_opcode(Opcode::OP_0) + .push_opcode(Opcode::OP_0) + .into_script(); + let time_lock = try_tx_s!(args.time_lock.try_into()); + + let redeem_script = swap_proto_v2_scripts::maker_payment_script( + time_lock, + args.maker_secret_hash, + args.taker_secret_hash, + key_pair.public(), + args.taker_pub, + ) + .into(); + let fee = try_tx_s!( + coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE, &FeeApproxStage::WithoutApprox) + .await + ); + if fee >= payment_value { + return TX_PLAIN_ERR!( + "HTLC spend fee {} is greater than transaction output {}", + fee, + payment_value + ); + } + let script_pubkey = try_tx_s!(output_script(&my_address)).to_bytes(); + let output = TransactionOutput { + value: payment_value - fee, + script_pubkey, + }; + + let input = P2SHSpendingTxInput { + prev_transaction: args.maker_payment_tx.clone(), + redeem_script, + outputs: vec![output], + script_data, + sequence: SEQUENCE_FINAL, + lock_time: time_lock, + keypair: &key_pair, + }; + let transaction = try_tx_s!(coin.p2sh_spending_tx(input).await); + + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); + try_tx_s!(tx_fut.await, transaction); + + Ok(transaction) } #[test] @@ -5098,7 +5177,7 @@ fn test_tx_v_size() { let tx: UtxoTx = "010000000001017996e77b2b1f4e66da606cfc2f16e3f52e1eac4a294168985bd4dbd54442e61f0100000000ffffffff01ab36010000000000220020693090c0e291752d448826a9dc72c9045b34ed4f7bd77e6e8e62645c23d69ac502483045022100d0800719239d646e69171ede7f02af916ac778ffe384fa0a5928645b23826c9f022044072622de2b47cfc81ac5172b646160b0c48d69d881a0ce77be06dbd6f6e5ac0121031ac6d25833a5961e2a8822b2e8b0ac1fd55d90cbbbb18a780552cbd66fc02bb3735a9e61".into(); let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit, &tx); assert_eq!(v_size, 122); - // Multipl segwit inputs with P2PKH output + // Multiple segwit inputs with P2PKH output // https://live.blockcypher.com/btc-testnet/tx/649d514d76702a0925a917d830e407f4f1b52d78832520e486c140ce8d0b879f/ let tx: UtxoTx = "0100000000010250c434acbad252481564d56b41990577c55d247aedf4bb853dca3567c4404c8f0000000000ffffffff55baf016f0628ecf0f0ec228e24d8029879b0491ab18bac61865afaa9d16e8bb0000000000ffffffff01e8030000000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac0247304402202611c05dd0e748f7c9955ed94a172af7ed56a0cdf773e8c919bef6e70b13ec1c02202fd7407891c857d95cdad1038dcc333186815f50da2fc9a334f814dd8d0a2d63012103c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3ed02483045022100bb9d483f6b2b46f8e70d62d65b33b6de056e1878c9c2a1beed69005daef2f89502201690cd44cf6b114fa0d494258f427e1ed11a21d897e407d8a1ff3b7e09b9a426012103c6a78589e18b482aea046975e6d0acbdea7bf7dbf04d9d5bd67fda917815e3ed9cf7bd60".into(); let v_size = tx_size_in_v_bytes(&UtxoAddressFormat::Segwit, &tx); @@ -5118,7 +5197,7 @@ fn test_generate_taker_fee_tx_outputs() { let outputs = generate_taker_fee_tx_outputs( 8, &AddressHashEnum::default_address_hash(), - DexFee::Standard(amount.into()), + &DexFee::Standard(amount.into()), ) .unwrap(); @@ -5138,7 +5217,7 @@ fn test_generate_taker_fee_tx_outputs_with_burn() { let outputs = generate_taker_fee_tx_outputs( 8, &AddressHashEnum::default_address_hash(), - DexFee::with_burn(fee_amount.into(), burn_amount.into()), + &DexFee::with_burn(fee_amount.into(), burn_amount.into()), ) .unwrap(); @@ -5148,3 +5227,21 @@ fn test_generate_taker_fee_tx_outputs_with_burn() { assert_eq!(outputs[1].value, burn_uamount); } + +#[test] +fn test_address_to_scripthash() { + let address = Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(); + let actual = address_to_scripthash(&address).expect("valid script hash to be built"); + let expected = "e850499408c6ebcf6b3340282747e540fb23748429fca5f2b36cdeef54ddf5b1".to_owned(); + assert_eq!(expected, actual); + + let address = Address::from_legacyaddress("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", &KMD_PREFIXES).unwrap(); + let actual = address_to_scripthash(&address).expect("valid script hash to be built"); + let expected = "a70a7a7041ef172ce4b5f8208aabed44c81e2af75493540f50af7bd9afa9955d".to_owned(); + assert_eq!(expected, actual); + + let address = Address::from_legacyaddress("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", &T_QTUM_PREFIXES).unwrap(); + let actual = address_to_scripthash(&address).expect("valid script hash to be built"); + let expected = "c5b5922c86830289231539d1681d8ce621aac8326c96d6ac55400b4d1485f769".to_owned(); + assert_eq!(expected, actual); +} diff --git a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs index 97a637a68c..32577de810 100644 --- a/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs +++ b/mm2src/coins/utxo/utxo_common/utxo_tx_history_v2_common.rs @@ -1,21 +1,20 @@ use crate::coin_balance::CoinBalanceReportOps; -use crate::hd_wallet::{HDAccountOps, HDWalletCoinOps, HDWalletOps}; +use crate::hd_wallet::{HDAccountOps, HDAddressOps, HDCoinAddress, HDWalletCoinOps, HDWalletOps}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, DisplayAddress, MyTxHistoryErrorV2, MyTxHistoryTarget, TxDetailsBuilder, TxHistoryStorage}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::rpc_clients::{electrum_script_hash, ElectrumClient, NativeClient, UtxoRpcClientEnum}; use crate::utxo::utxo_common::{big_decimal_from_sat, HISTORY_TOO_LARGE_ERROR}; -use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, - UtxoTxHistoryOps}; -use crate::utxo::{output_script, RequestTxHistoryResult, UtxoCoinFields, UtxoCommonOps, UtxoHDAccount}; +use crate::utxo::utxo_tx_history_v2::{UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; +use crate::utxo::{output_script, RequestTxHistoryResult, UtxoCoinFields, UtxoCommonOps}; use crate::{big_decimal_from_sat_unsigned, compare_transactions, BalanceResult, CoinWithDerivationMethod, - DerivationMethod, HDAccountAddressId, MarketCoinOps, NumConversError, TransactionDetails, TxFeeDetails, - TxIdHeight, UtxoFeeDetails, UtxoTx}; + DerivationMethod, HDPathAccountToAddressId, MarketCoinOps, NumConversError, TransactionDetails, + TxFeeDetails, TxIdHeight, UtxoFeeDetails, UtxoTx}; use common::jsonrpc_client::JsonRpcErrorType; use crypto::Bip44Chain; use futures::compat::Future01CompatExt; use itertools::Itertools; -use keys::{Address, Type as ScriptType}; +use keys::Address; use mm2_err_handle::prelude::*; use mm2_metrics::MetricsArc; use mm2_number::BigDecimal; @@ -23,7 +22,6 @@ use rpc::v1::types::{TransactionInputEnum, H256 as H256Json}; use serialization::deserialize; use std::collections::{HashMap, HashSet}; use std::convert::{TryFrom, TryInto}; -use std::iter; use std::num::TryFromIntError; /// [`CoinWithTxHistoryV2::history_wallet_id`] implementation. @@ -36,11 +34,8 @@ pub async fn get_tx_history_filters( target: MyTxHistoryTarget, ) -> MmResult where - Coin: CoinWithDerivationMethod::HDWallet> - + HDWalletCoinOps - + MarketCoinOps - + Sync, - ::Address: DisplayAddress, + Coin: CoinWithDerivationMethod + MarketCoinOps + Sync, + HDCoinAddress: DisplayAddress, { match (coin.derivation_method(), target) { (DerivationMethod::SingleAddress(_), MyTxHistoryTarget::Iguana) => { @@ -57,7 +52,7 @@ where get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await }, (DerivationMethod::HDWallet(hd_wallet), MyTxHistoryTarget::AddressDerivationPath(derivation_path)) => { - let hd_address_id = HDAccountAddressId::from(derivation_path); + let hd_address_id = HDPathAccountToAddressId::from(derivation_path); get_tx_history_filters_for_hd_address(coin, hd_wallet, hd_address_id).await }, (DerivationMethod::HDWallet(_), target) => MmError::err(MyTxHistoryErrorV2::with_expected_target( @@ -75,7 +70,7 @@ async fn get_tx_history_filters_for_hd_account( ) -> MmResult where Coin: HDWalletCoinOps + Sync, - Coin::Address: DisplayAddress, + HDCoinAddress: DisplayAddress, { let hd_account = hd_wallet .get_account(account_id) @@ -88,7 +83,7 @@ where let addresses_iter = external_addresses .into_iter() .chain(internal_addresses) - .map(|hd_address| DisplayAddress::display_address(&hd_address.address)); + .map(|hd_address| DisplayAddress::display_address(&hd_address.address())); Ok(GetTxHistoryFilters::for_addresses(addresses_iter)) } @@ -96,11 +91,11 @@ where async fn get_tx_history_filters_for_hd_address( coin: &Coin, hd_wallet: &Coin::HDWallet, - hd_address_id: HDAccountAddressId, + hd_address_id: HDPathAccountToAddressId, ) -> MmResult where Coin: HDWalletCoinOps + Sync, - Coin::Address: DisplayAddress, + HDCoinAddress: DisplayAddress, { let hd_account = hd_wallet .get_account(hd_address_id.account_id) @@ -119,36 +114,7 @@ where let hd_address = coin .derive_address(&hd_account, hd_address_id.chain, hd_address_id.address_id) .await?; - Ok(GetTxHistoryFilters::for_address(hd_address.address.display_address())) -} - -/// [`UtxoTxHistoryOps::my_addresses`] implementation. -pub async fn my_addresses(coin: &Coin) -> MmResult, UtxoMyAddressesHistoryError> -where - Coin: HDWalletCoinOps
+ UtxoCommonOps, -{ - const ADDRESSES_CAPACITY: usize = 60; - - match coin.as_ref().derivation_method { - DerivationMethod::SingleAddress(ref my_address) => Ok(iter::once(my_address.clone()).collect()), - DerivationMethod::HDWallet(ref hd_wallet) => { - let hd_accounts = hd_wallet.get_accounts().await; - - let mut all_addresses = HashSet::with_capacity(ADDRESSES_CAPACITY); - for (_, hd_account) in hd_accounts { - let external_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::External).await?; - let internal_addresses = coin.derive_known_addresses(&hd_account, Bip44Chain::Internal).await?; - - let addresses_it = external_addresses - .into_iter() - .chain(internal_addresses) - .map(|hd_address| hd_address.address); - all_addresses.extend(addresses_it); - } - - Ok(all_addresses) - }, - } + Ok(GetTxHistoryFilters::for_address(hd_address.address().display_address())) } /// [`UtxoTxHistoryOps::tx_details_by_hash`] implementation. @@ -280,10 +246,14 @@ where /// Requests balances of all activated addresses. pub async fn my_addresses_balances(coin: &Coin) -> BalanceResult> where - Coin: CoinBalanceReportOps, + Coin: CoinBalanceReportOps + MarketCoinOps, { let coin_balance = coin.coin_balance_report().await?; - Ok(coin_balance.to_addresses_total_balances()) + let addresses_balances = coin_balance.to_addresses_total_balances(coin.ticker()); + Ok(addresses_balances + .into_iter() + .map(|(addr, balance)| (addr, balance.unwrap_or_default())) + .collect()) } /// [`UtxoTxHistoryOps::request_tx_history`] implementation. @@ -365,14 +335,18 @@ async fn request_tx_history_with_electrum( metrics: MetricsArc, for_addresses: &HashSet
, ) -> RequestTxHistoryResult { - fn addr_to_script_hash(addr: &Address) -> String { - let script = output_script(addr, ScriptType::P2PKH); + fn addr_to_script_hash(addr: &Address) -> Result { + let script = output_script(addr)?; let script_hash = electrum_script_hash(&script); - hex::encode(script_hash) + Ok(hex::encode(script_hash)) } let script_hashes_count = for_addresses.len() as u64; - let script_hashes = for_addresses.iter().map(addr_to_script_hash); + let script_hashes: Result, _> = for_addresses.iter().map(addr_to_script_hash).collect(); + let script_hashes = match script_hashes { + Ok(script_hashes) => script_hashes, + Err(err) => return RequestTxHistoryResult::CriticalError(err.to_string()), + }; mm_counter!(metrics, "tx.history.request.count", script_hashes_count, "coin" => ticker, "client" => "electrum", "method" => "blockchain.scripthash.get_history"); diff --git a/mm2src/coins/utxo/utxo_common_tests.rs b/mm2src/coins/utxo/utxo_common_tests.rs index 00410001e3..03fea53699 100644 --- a/mm2src/coins/utxo/utxo_common_tests.rs +++ b/mm2src/coins/utxo/utxo_common_tests.rs @@ -1,34 +1,38 @@ use super::*; -use crate::hd_wallet::HDAccountsMap; +use crate::hd_wallet::{HDAccountsMap, HDAccountsMutex, HDAddressesCache, HDWallet, HDWalletCoinStorage}; use crate::my_tx_history_v2::{my_tx_history_v2_impl, CoinWithTxHistoryV2, MyTxHistoryDetails, MyTxHistoryRequestV2, MyTxHistoryResponseV2, MyTxHistoryTarget}; use crate::tx_history_storage::TxHistoryStorageBuilder; use crate::utxo::rpc_clients::{ElectrumClient, UtxoRpcClientOps}; use crate::utxo::tx_cache::dummy_tx_cache::DummyVerboseCache; use crate::utxo::tx_cache::UtxoVerboseCacheOps; +use crate::utxo::utxo_hd_wallet::UtxoHDAccount; use crate::utxo::utxo_tx_history_v2::{utxo_history_loop, UtxoTxHistoryOps}; use crate::{compare_transaction_details, UtxoStandardCoin}; use common::custom_futures::repeatable::{Ready, Retry}; use common::executor::{spawn, Timer}; use common::jsonrpc_client::JsonRpcErrorType; +use common::log::info; use common::PagingOptionsEnum; use crypto::privkey::key_pair_from_seed; +use crypto::HDPathToAccount; use itertools::Itertools; +use keys::prefixes::*; use mm2_test_helpers::for_tests::mm_ctx_with_custom_db; +use std::convert::TryFrom; use std::num::NonZeroUsize; +use std::str::FromStr; use std::time::Duration; -pub(super) const TEST_COIN_NAME: &str = "RICK"; -// Made-up hrp for rick to test p2wpkh script -pub(super) const TEST_COIN_HRP: &str = "rck"; +pub(super) const TEST_COIN_NAME: &str = "DOC"; +// Made-up hrp for DOC to test p2wpkh script +pub(super) const TEST_COIN_HRP: &str = "doc"; pub(super) const TEST_COIN_DECIMALS: u8 = 8; -const MORTY_HD_TX_HISTORY_STR: &str = include_str!("../for_tests/MORTY_HD_tx_history_fixtures.json"); +const DOC_HD_TX_HISTORY_STR: &str = include_str!("../for_tests/DOC_HD_tx_history_fixtures.json"); lazy_static! { - static ref MORTY_HD_TX_HISTORY: Vec = parse_tx_history(MORTY_HD_TX_HISTORY_STR); - static ref MORTY_HD_TX_HISTORY_MAP: HashMap = - parse_tx_history_map(MORTY_HD_TX_HISTORY_STR); + static ref DOC_HD_TX_HISTORY_MAP: HashMap = parse_tx_history_map(DOC_HD_TX_HISTORY_STR); } fn parse_tx_history(history_str: &'static str) -> Vec { json::from_str(history_str).unwrap() } @@ -61,23 +65,29 @@ pub(super) fn utxo_coin_fields_for_test( }, }; let key_pair = key_pair_from_seed(&seed).unwrap(); - let my_address = Address { - prefix: 60, - hash: key_pair.public().address_hash().into(), - t_addr_prefix: 0, - checksum_type, - hrp: if is_segwit_coin { - Some(TEST_COIN_HRP.to_string()) - } else { - None - }, - addr_format: if is_segwit_coin { - UtxoAddressFormat::Segwit - } else { - UtxoAddressFormat::Standard - }, + let prefixes = if is_segwit_coin { + NetworkAddressPrefixes::default() + } else { + NetworkAddressPrefixes { + p2pkh: [60].into(), + p2sh: AddressPrefix::default(), + } }; - let my_script_pubkey = Builder::build_p2pkh(&my_address.hash).to_bytes(); + let hrp = if is_segwit_coin { + Some(TEST_COIN_HRP.to_string()) + } else { + None + }; + let addr_format = if is_segwit_coin { + UtxoAddressFormat::Segwit + } else { + UtxoAddressFormat::Standard + }; + let my_address = AddressBuilder::new(addr_format, checksum_type, prefixes, hrp) + .as_pkh_from_pk(*key_pair.public()) + .build() + .expect("valid address props"); + let my_script_pubkey = Builder::build_p2pkh(my_address.hash()).to_bytes(); let priv_key_policy = PrivKeyPolicy::Iguana(key_pair); let derivation_method = DerivationMethod::SingleAddress(my_address); @@ -98,10 +108,10 @@ pub(super) fn utxo_coin_fields_for_test( tx_version: 4, default_address_format: UtxoAddressFormat::Standard, asset_chain: true, - p2sh_addr_prefix: 85, - p2sh_t_addr_prefix: 0, - pub_addr_prefix: 60, - pub_t_addr_prefix: 0, + address_prefixes: NetworkAddressPrefixes { + p2pkh: [60].into(), + p2sh: [85].into(), + }, sign_message_prefix: Some(String::from("Komodo Signed Message:\n")), bech32_hrp, ticker: TEST_COIN_NAME.into(), @@ -131,13 +141,15 @@ pub(super) fn utxo_coin_fields_for_test( priv_key_policy, derivation_method, history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - tx_cache: DummyVerboseCache::default().into_shared(), + tx_cache: DummyVerboseCache.into_shared(), recently_spent_outpoints: AsyncMutex::new(RecentlySpentOutPoints::new(my_script_pubkey)), tx_hash_algo: TxHashAlgo::DSHA256, check_utxo_maturity: false, block_headers_status_notifier: None, block_headers_status_watcher: None, abortable_system: AbortableQueue::default(), + scripthash_notification_handler: None, + ctx: Default::default(), } } @@ -168,6 +180,7 @@ where repeatable!(async { let response = my_tx_history_v2_impl(ctx.clone(), coin, req.clone()).await.unwrap(); + info!("LIMIT: {}", response.transactions.len()); if response.transactions.len() >= expected_txs { return Ready(response); } @@ -179,11 +192,11 @@ where .unwrap() } -pub(super) fn get_morty_hd_transactions_ordered(tx_hashes: &[&str]) -> Vec { +pub(super) fn get_doc_hd_transactions_ordered(tx_hashes: &[&str]) -> Vec { tx_hashes .iter() .map(|tx_hash| { - MORTY_HD_TX_HISTORY_MAP + DOC_HD_TX_HISTORY_MAP .get(*tx_hash) .unwrap_or_else(|| panic!("No such {:?} TX in the file", tx_hash)) .clone() @@ -194,29 +207,29 @@ pub(super) fn get_morty_hd_transactions_ordered(tx_hashes: &[&str]) -> Vec = vec![ ( - "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), - BigDecimal::from_str("5.77699").unwrap(), + Address::from_legacyaddress("RG278CfeNPFtNztFZQir8cgdWexVhViYVy", &KMD_PREFIXES).unwrap(), + BigDecimal::try_from(5.77699).unwrap(), ), ( - "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), - BigDecimal::from_str("3.33").unwrap(), + Address::from_legacyaddress("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN", &KMD_PREFIXES).unwrap(), + BigDecimal::try_from(3.33).unwrap(), ), ( - "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), - BigDecimal::from_str("0.77699").unwrap(), + Address::from_legacyaddress("RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi", &KMD_PREFIXES).unwrap(), + BigDecimal::try_from(0.77699).unwrap(), ), ( - "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), - BigDecimal::from_str("16.55398").unwrap(), + Address::from_legacyaddress("RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF", &KMD_PREFIXES).unwrap(), + BigDecimal::try_from(16.55398).unwrap(), ), ]; assert_eq!(actual, expected); @@ -245,74 +258,76 @@ pub(super) async fn test_electrum_display_balances(rpc_client: &ElectrumClient) /// when [Trezor Daemon Emulator](https://github.com/trezor/trezord-go#emulator-support) is integrated. pub(super) async fn test_hd_utxo_tx_history_impl(rpc_client: ElectrumClient) { let ctx = mm_ctx_with_custom_db(); - let hd_account_for_test = UtxoHDAccount { account_id: 0, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), - external_addresses_number: 11, - internal_addresses_number: 3, + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + external_addresses_number: 10, + internal_addresses_number: 0, derived_addresses: HDAddressesCache::default(), }; + let mut hd_accounts = HDAccountsMap::new(); hd_accounts.insert(0, hd_account_for_test); let mut fields = utxo_coin_fields_for_test(rpc_client.into(), None, false); - fields.conf.ticker = "MORTY".to_string(); + fields.conf.ticker = "DOC".to_string(); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { - hd_wallet_rmd160: "6d9d2b554d768232320587df75c4338ecc8bf37d".into(), - hd_wallet_storage: HDWalletCoinStorage::default(), + inner: HDWallet { + hd_wallet_rmd160: "6d9d2b554d768232320587df75c4338ecc8bf37d".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + derivation_path: HDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + enabled_address: HDPathAccountToAddressId::default(), + gap_limit: 20, + }, address_format: UtxoAddressFormat::Standard, - derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), - accounts: HDAccountsMutex::new(hd_accounts), - gap_limit: 20, }); let coin = utxo_coin_from_fields(fields); - let current_balances = coin.my_addresses_balances().await.unwrap(); - let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); spawn(utxo_history_loop( coin.clone(), storage, ctx.metrics.clone(), - current_balances, + current_balances.clone(), )); let target = MyTxHistoryTarget::AccountId { account_id: 0 }; - let tx_history = wait_for_tx_history_finished(&ctx, &coin, target, 4, 30).await; + let tx_history = wait_for_tx_history_finished(&ctx, &coin, target, 1, 20).await; let actual: Vec<_> = tx_history.transactions.into_iter().map(|tx| tx.details).collect(); - let expected = get_morty_hd_transactions_ordered(&[ - "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", - "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", - "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", - "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", - ]); + let expected = + get_doc_hd_transactions_ordered(&["2f8b4178b56d0a9f0ad31afcbef6ff267a0bf655dcff72de530107a4c93407b6"]); assert_eq!(actual, expected); - // Activate new `RQstQeTUEZLh6c3YWJDkeVTTQoZUsfvNCr` address. + // Activate new `RYM6yDMn8vdqtkYKLzY5dNe7p3T6YmMWvq` address. match coin.as_ref().derivation_method { DerivationMethod::HDWallet(ref hd_wallet) => { - let mut accounts = hd_wallet.accounts.lock().await; - accounts.get_mut(&0).unwrap().internal_addresses_number += 1 + let mut accounts = hd_wallet.inner.accounts.lock().await; + accounts.get_mut(&0).unwrap().external_addresses_number += 1 }, _ => unimplemented!(), } + let storage = TxHistoryStorageBuilder::new(&ctx).build().unwrap(); + spawn(utxo_history_loop( + coin.clone(), + storage, + ctx.metrics.clone(), + current_balances, + )); + // Wait for the TX history loop to fetch Transactions of the activated address. let target = MyTxHistoryTarget::AccountId { account_id: 0 }; - let tx_history = wait_for_tx_history_finished(&ctx, &coin, target, 5, 60).await; + let tx_history = wait_for_tx_history_finished(&ctx, &coin, target, 2, 20).await; let actual: Vec<_> = tx_history.transactions.into_iter().map(|tx| tx.details).collect(); - let expected = get_morty_hd_transactions_ordered(&[ + let expected = get_doc_hd_transactions_ordered(&[ // New transaction: - "6ca27dd058b939c98a33625b9f68eaeebca5a3058aec062647ca6fd7634bb339", - "70c62f42d65f9d71a8fb7f4560057b80dc2ecd9e4990621323faf1de9a53ca97", - "bd031dc681cdc63491fd71902c5960985127b04eb02211a1049bff0d0c8ebce3", - "bf02bea67c568108c91f58d88f2f7adda84a3287949ad89cc8c05de95042fb75", - "7dc038aae5eef3108f8071450b590cd0d376a08c1aea190ba89491cc3b27ea8d", + "2f8b4178b56d0a9f0ad31afcbef6ff267a0bf655dcff72de530107a4c93407b6", + "071200b4b2967cfe3522b8a6713b8bdcd09f74a17d575fad87b4e97bc442f404", ]); assert_eq!(actual, expected); } diff --git a/mm2src/coins/utxo/utxo_hd_wallet.rs b/mm2src/coins/utxo/utxo_hd_wallet.rs new file mode 100644 index 0000000000..5297157e33 --- /dev/null +++ b/mm2src/coins/utxo/utxo_hd_wallet.rs @@ -0,0 +1,58 @@ +use crate::hd_wallet::{HDAccount, HDAccountMut, HDAccountOps, HDAccountsMap, HDAccountsMut, HDAccountsMutex, + HDAddress, HDWallet, HDWalletCoinStorage, HDWalletOps, HDWalletStorageOps, + WithdrawSenderAddress}; +use async_trait::async_trait; +use crypto::{Bip44Chain, HDPathToCoin, Secp256k1ExtendedPublicKey}; +use keys::{Address, AddressFormat as UtxoAddressFormat, Public}; + +pub type UtxoHDAddress = HDAddress; +pub type UtxoHDAccount = HDAccount; +pub type UtxoWithdrawSender = WithdrawSenderAddress; + +/// A struct to encapsulate the types needed for a UTXO HD wallet. +pub struct UtxoHDWallet { + /// The inner HD wallet field that makes use of the generic `HDWallet` struct. + pub inner: HDWallet, + /// Specifies the UTXO address format for all addresses in the wallet. + pub address_format: UtxoAddressFormat, +} + +#[async_trait] +impl HDWalletStorageOps for UtxoHDWallet { + fn hd_wallet_storage(&self) -> &HDWalletCoinStorage { self.inner.hd_wallet_storage() } +} + +#[async_trait] +impl HDWalletOps for UtxoHDWallet { + type HDAccount = UtxoHDAccount; + + fn coin_type(&self) -> u32 { self.inner.coin_type() } + + fn derivation_path(&self) -> &HDPathToCoin { self.inner.derivation_path() } + + fn gap_limit(&self) -> u32 { self.inner.gap_limit() } + + fn account_limit(&self) -> u32 { self.inner.account_limit() } + + fn default_receiver_chain(&self) -> Bip44Chain { self.inner.default_receiver_chain() } + + fn get_accounts_mutex(&self) -> &HDAccountsMutex { self.inner.get_accounts_mutex() } + + async fn get_account(&self, account_id: u32) -> Option { self.inner.get_account(account_id).await } + + async fn get_account_mut(&self, account_id: u32) -> Option> { + self.inner.get_account_mut(account_id).await + } + + async fn get_accounts(&self) -> HDAccountsMap { self.inner.get_accounts().await } + + async fn get_accounts_mut(&self) -> HDAccountsMut<'_, Self::HDAccount> { self.inner.get_accounts_mut().await } + + async fn remove_account_if_last(&self, account_id: u32) -> Option { + self.inner.remove_account_if_last(account_id).await + } + + async fn get_enabled_address(&self) -> Option<::HDAddress> { + self.inner.get_enabled_address().await + } +} diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index ab809ccfc5..75dfb5391a 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -1,12 +1,10 @@ +use super::utxo_common::utxo_prepare_addresses_for_balance_stream_if_enabled; use super::*; use crate::coin_balance::{self, EnableCoinBalanceError, EnabledCoinBalanceParams, HDAccountBalance, HDAddressBalance, HDWalletBalance, HDWalletBalanceOps}; -use crate::coin_errors::MyAddressError; -use crate::hd_confirm_address::HDConfirmAddress; -use crate::hd_pubkey::{ExtractExtendedPubkey, HDExtractPubkeyError, HDXPubExtractor}; -use crate::hd_wallet::{AccountUpdatingError, AddressDerivingResult, HDAccountMut, NewAccountCreatingError, - NewAddressDeriveConfirmError}; -use crate::hd_wallet_storage::HDWalletCoinWithStorageOps; +use crate::coin_errors::{MyAddressError, ValidatePaymentResult}; +use crate::hd_wallet::{ExtractExtendedPubkey, HDCoinAddress, HDCoinWithdrawOps, HDConfirmAddress, + HDExtractPubkeyError, HDXPubExtractor, TrezorCoinError, WithdrawSenderAddress}; use crate::my_tx_history_v2::{CoinWithTxHistoryV2, MyTxHistoryErrorV2, MyTxHistoryTarget, TxHistoryStorage}; use crate::rpc_command::account_balance::{self, AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; use crate::rpc_command::get_new_address::{self, GetNewAddressParams, GetNewAddressResponse, GetNewAddressRpcError, @@ -17,29 +15,35 @@ use crate::rpc_command::init_create_account::{self, CreateAccountRpcError, Creat InitCreateAccountRpcOps}; use crate::rpc_command::init_scan_for_new_addresses::{self, InitScanAddressesRpcOps, ScanAddressesParams, ScanAddressesResponse}; -use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; +use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandleShared}; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; +use crate::utxo::rpc_clients::BlockHashOrHeight; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; +use crate::utxo::utxo_hd_wallet::{UtxoHDAccount, UtxoHDAddress}; use crate::utxo::utxo_tx_history_v2::{UtxoMyAddressesHistoryError, UtxoTxDetailsError, UtxoTxDetailsParams, UtxoTxHistoryOps}; -use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, ConfirmPaymentInput, - DexFee, GenPreimageResult, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, - IguanaPrivKey, MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, - PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, RefundError, RefundFundingSecretArgs, - RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, - SendPaymentArgs, SendTakerFundingArgs, SignatureResult, SpendPaymentArgs, SwapOps, SwapOpsV2, - TakerSwapMakerCoin, ToBytes, TradePreimageValue, TransactionFut, TransactionResult, TxMarshalingErr, - TxPreimageWithSig, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, - ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, - ValidateTakerFundingArgs, ValidateTakerFundingResult, ValidateTakerFundingSpendPreimageResult, - ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationResult, - WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut, WithdrawSenderAddress}; +use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinWithDerivationMethod, CoinWithPrivKeyPolicy, + ConfirmPaymentInput, DexFee, FundingTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, + GenTakerPaymentSpendArgs, GetWithdrawSenderAddress, IguanaBalanceOps, IguanaPrivKey, MakerCoinSwapOpsV2, + MakerSwapTakerCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, + PaymentInstructionsErr, PrivKeyBuildPolicy, RawTransactionRequest, RawTransactionResult, RefundError, + RefundFundingSecretArgs, RefundMakerPaymentArgs, RefundPaymentArgs, RefundResult, + SearchForFundingSpendErr, SearchForSwapTxSpendInput, SendMakerPaymentArgs, + SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionRequest, + SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, + TakerCoinSwapOpsV2, TakerSwapMakerCoin, ToBytes, TradePreimageValue, TransactionFut, TransactionResult, + TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, + ValidateMakerPaymentArgs, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, + ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, + ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WaitForTakerPaymentSpendError, + WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; -use crypto::Bip44Chain; use futures::{FutureExt, TryFutureExt}; use mm2_metrics::MetricsArc; use mm2_number::MmNumber; +use script::Opcode; use utxo_signer::UtxoSignerOps; #[derive(Clone)] @@ -78,6 +82,7 @@ pub async fn utxo_standard_coin_with_policy( .build() .await ); + Ok(coin) } @@ -190,6 +195,10 @@ impl UtxoCommonOps for UtxoStandardCoin { utxo_common::checked_address_from_str(self, address) } + fn script_for_address(&self, address: &Address) -> MmResult { + utxo_common::output_script_checked(self.as_ref(), address) + } + async fn get_current_mtp(&self) -> UtxoRpcResult { utxo_common::get_current_mtp(&self.utxo_arc, self.ticker().into()).await } @@ -259,8 +268,7 @@ impl UtxoCommonOps for UtxoStandardCoin { let conf = &self.utxo_arc.conf; utxo_common::address_from_pubkey( pubkey, - conf.pub_addr_prefix, - conf.pub_t_addr_prefix, + conf.address_prefixes.clone(), conf.checksum_type, conf.bech32_hrp.clone(), self.addr_format().clone(), @@ -294,7 +302,7 @@ impl UtxoStandardOps for UtxoStandardCoin { #[async_trait] impl SwapOps for UtxoStandardCoin { #[inline] - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) } @@ -309,13 +317,19 @@ impl SwapOps for UtxoStandardCoin { } #[inline] - fn send_maker_spends_taker_payment(&self, maker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args) + async fn send_maker_spends_taker_payment( + &self, + maker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_maker_spends_taker_payment(self.clone(), maker_spends_payment_args).await } #[inline] - fn send_taker_spends_maker_payment(&self, taker_spends_payment_args: SpendPaymentArgs) -> TransactionFut { - utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args) + async fn send_taker_spends_maker_payment( + &self, + taker_spends_payment_args: SpendPaymentArgs<'_>, + ) -> TransactionResult { + utxo_common::send_taker_spends_maker_payment(self.clone(), taker_spends_payment_args).await } #[inline] @@ -345,13 +359,13 @@ impl SwapOps for UtxoStandardCoin { } #[inline] - fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - utxo_common::validate_maker_payment(self, input) + async fn validate_maker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + utxo_common::validate_maker_payment(self, input).await } #[inline] - fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentFut<()> { - utxo_common::validate_taker_payment(self, input) + async fn validate_taker_payment(&self, input: ValidatePaymentInput) -> ValidatePaymentResult<()> { + utxo_common::validate_taker_payment(self, input).await } #[inline] @@ -593,17 +607,64 @@ impl ToBytes for Public { } #[async_trait] -impl SwapOpsV2 for UtxoStandardCoin { +impl MakerCoinSwapOpsV2 for UtxoStandardCoin { + async fn send_maker_payment_v2(&self, args: SendMakerPaymentArgs<'_, Self>) -> Result { + utxo_common::send_maker_payment_v2(self.clone(), args).await + } + + async fn validate_maker_payment_v2(&self, args: ValidateMakerPaymentArgs<'_, Self>) -> ValidatePaymentResult<()> { + let taker_pub = self.derive_htlc_pubkey_v2(args.swap_unique_data); + let time_lock = args + .time_lock + .try_into() + .map_to_mm(ValidatePaymentError::TimelockOverflow)?; + utxo_common::validate_payment( + self.clone(), + args.maker_payment_tx, + utxo_common::DEFAULT_SWAP_VOUT, + args.maker_pub, + &taker_pub, + SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash: args.maker_secret_hash, + taker_secret_hash: args.taker_secret_hash, + }, + args.amount, + None, + time_lock, + 0, + 0, + ) + .await + } + + async fn refund_maker_payment_v2_timelock(&self, args: RefundPaymentArgs<'_>) -> Result { + utxo_common::refund_htlc_payment(self.clone(), args).await + } + + async fn refund_maker_payment_v2_secret( + &self, + args: RefundMakerPaymentArgs<'_, Self>, + ) -> Result { + utxo_common::refund_maker_payment_v2_secret(self.clone(), args).await + } + + async fn spend_maker_payment_v2(&self, args: SpendMakerPaymentArgs<'_, Self>) -> Result { + utxo_common::spend_maker_payment_v2(self, args).await + } +} + +#[async_trait] +impl TakerCoinSwapOpsV2 for UtxoStandardCoin { async fn send_taker_funding(&self, args: SendTakerFundingArgs<'_>) -> Result { utxo_common::send_taker_funding(self.clone(), args).await } - async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateTakerFundingResult { + async fn validate_taker_funding(&self, args: ValidateTakerFundingArgs<'_, Self>) -> ValidateSwapV2TxResult { utxo_common::validate_taker_funding(self, args).await } - async fn refund_taker_funding_timelock(&self, args: RefundPaymentArgs<'_>) -> TransactionResult { - utxo_common::refund_taker_funding_timelock(self.clone(), args).await + async fn refund_taker_funding_timelock(&self, args: RefundPaymentArgs<'_>) -> Result { + utxo_common::refund_htlc_payment(self.clone(), args).await } async fn refund_taker_funding_secret( @@ -613,6 +674,80 @@ impl SwapOpsV2 for UtxoStandardCoin { utxo_common::refund_taker_funding_secret(self.clone(), args).await } + async fn search_for_taker_funding_spend( + &self, + tx: &Self::Tx, + from_block: u64, + _secret_hash: &[u8], + ) -> Result>, SearchForFundingSpendErr> { + let script_pubkey = &tx + .first_output() + .map_err(|e| SearchForFundingSpendErr::InvalidInputTx(e.to_string()))? + .script_pubkey; + + let from_block = from_block + .try_into() + .map_err(SearchForFundingSpendErr::FromBlockConversionErr)?; + + let output_spend = self + .as_ref() + .rpc_client + .find_output_spend( + tx.hash(), + script_pubkey, + utxo_common::DEFAULT_SWAP_VOUT, + BlockHashOrHeight::Height(from_block), + self.as_ref().tx_hash_algo, + ) + .compat() + .await + .map_err(SearchForFundingSpendErr::Rpc)?; + match output_spend { + Some(found) => { + let script_sig: Script = found.input.script_sig.into(); + let maybe_first_op_if = script_sig + .get_instruction(1) + .ok_or_else(|| { + SearchForFundingSpendErr::FailedToProcessSpendTx("No instruction at index 1".into()) + })? + .map_err(|e| { + SearchForFundingSpendErr::FailedToProcessSpendTx(format!( + "Couldn't get instruction at index 1: {}", + e + )) + })?; + match maybe_first_op_if.opcode { + Opcode::OP_1 => Ok(Some(FundingTxSpend::RefundedTimelock(found.spending_tx))), + Opcode::OP_PUSHBYTES_32 => Ok(Some(FundingTxSpend::RefundedSecret { + tx: found.spending_tx, + secret: maybe_first_op_if + .data + .ok_or_else(|| { + SearchForFundingSpendErr::FailedToProcessSpendTx( + "No data at instruction with index 1".into(), + ) + })? + .try_into() + .map_err(|e| { + SearchForFundingSpendErr::FailedToProcessSpendTx(format!( + "Failed to parse data at instruction with index 1 as [u8; 32]: {}", + e + )) + })?, + })), + Opcode::OP_PUSHBYTES_70 | Opcode::OP_PUSHBYTES_71 | Opcode::OP_PUSHBYTES_72 => { + Ok(Some(FundingTxSpend::TransferredToTakerPayment(found.spending_tx))) + }, + unexpected => Err(SearchForFundingSpendErr::FailedToProcessSpendTx(format!( + "Got unexpected opcode {:?} at instruction with index 1", + unexpected + ))), + } + }, + None => Ok(None), + } + } + async fn gen_taker_funding_spend_preimage( &self, args: &GenTakerFundingSpendArgs<'_, Self>, @@ -640,8 +775,8 @@ impl SwapOpsV2 for UtxoStandardCoin { utxo_common::sign_and_send_taker_funding_spend(self, preimage, args, &htlc_keypair).await } - async fn refund_combined_taker_payment(&self, args: RefundPaymentArgs<'_>) -> TransactionResult { - utxo_common::refund_combined_taker_payment(self.clone(), args).await + async fn refund_combined_taker_payment(&self, args: RefundPaymentArgs<'_>) -> Result { + utxo_common::refund_htlc_payment(self.clone(), args).await } async fn gen_taker_payment_spend_preimage( @@ -667,20 +802,39 @@ impl SwapOpsV2 for UtxoStandardCoin { gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], swap_unique_data: &[u8], - ) -> TransactionResult { + ) -> Result { let htlc_keypair = self.derive_htlc_key_pair(swap_unique_data); utxo_common::sign_and_broadcast_taker_payment_spend(self, preimage, gen_args, secret, &htlc_keypair).await } + async fn wait_for_taker_payment_spend( + &self, + taker_payment: &Self::Tx, + from_block: u64, + wait_until: u64, + ) -> MmResult { + let res = utxo_common::wait_for_output_spend_impl( + self.as_ref(), + taker_payment, + utxo_common::DEFAULT_SWAP_VOUT, + from_block, + wait_until, + 10., + ) + .await?; + Ok(res) + } + fn derive_htlc_pubkey_v2(&self, swap_unique_data: &[u8]) -> Self::Pubkey { *self.derive_htlc_key_pair(swap_unique_data).public() } } +#[async_trait] impl MarketCoinOps for UtxoStandardCoin { fn ticker(&self) -> &str { &self.utxo_arc.conf.ticker } - fn get_public_key(&self) -> Result> { + async fn get_public_key(&self) -> Result> { let pubkey = utxo_common::my_public_key(&self.utxo_arc)?; Ok(pubkey.to_string()) } @@ -715,13 +869,18 @@ impl MarketCoinOps for UtxoStandardCoin { utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) } + #[inline(always)] + async fn sign_raw_tx(&self, args: &SignRawTransactionRequest) -> RawTransactionResult { + utxo_common::sign_raw_tx(self, args).await + } + fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send> { utxo_common::wait_for_confirmations(&self.utxo_arc, input) } fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { utxo_common::wait_for_output_spend( - &self.utxo_arc, + self.clone(), args.tx_bytes, utxo_common::DEFAULT_SWAP_VOUT, args.from_block, @@ -743,6 +902,8 @@ impl MarketCoinOps for UtxoStandardCoin { fn min_tx_amount(&self) -> BigDecimal { utxo_common::min_tx_amount(self.as_ref()) } fn min_trading_vol(&self) -> MmNumber { utxo_common::min_trading_vol(self.as_ref()) } + + fn is_trezor(&self) -> bool { self.as_ref().priv_key_policy.is_trezor() } } #[async_trait] @@ -794,6 +955,7 @@ impl MmCoin for UtxoStandardCoin { &self, value: TradePreimageValue, stage: FeeApproxStage, + _include_refund_fee: bool, // refund fee is taken from swap output ) -> TradePreimageResult { utxo_common::get_sender_trade_fee(self, value, stage).await } @@ -866,7 +1028,7 @@ impl InitWithdrawCoin for UtxoStandardCoin { &self, ctx: MmArc, req: WithdrawRequest, - task_handle: &WithdrawTaskHandle, + task_handle: WithdrawTaskHandleShared, ) -> Result> { utxo_common::init_withdraw(ctx, self.clone(), req, task_handle).await } @@ -892,90 +1054,66 @@ impl UtxoSignerOps for UtxoStandardCoin { fn tx_provider(&self) -> Self::TxGetter { self.utxo_arc.rpc_client.clone() } } -impl CoinWithDerivationMethod for UtxoStandardCoin { - type Address = Address; - type HDWallet = UtxoHDWallet; +impl CoinWithPrivKeyPolicy for UtxoStandardCoin { + type KeyPair = KeyPair; - fn derivation_method(&self) -> &DerivationMethod { + fn priv_key_policy(&self) -> &PrivKeyPolicy { &self.utxo_arc.priv_key_policy } +} + +impl CoinWithDerivationMethod for UtxoStandardCoin { + fn derivation_method(&self) -> &DerivationMethod, Self::HDWallet> { utxo_common::derivation_method(self.as_ref()) } } +#[async_trait] +impl IguanaBalanceOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + + async fn iguana_balances(&self) -> BalanceResult { self.my_balance().compat().await } +} + #[async_trait] impl ExtractExtendedPubkey for UtxoStandardCoin { type ExtendedPublicKey = Secp256k1ExtendedPublicKey; async fn extract_extended_pubkey( &self, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, derivation_path: DerivationPath, ) -> MmResult where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { - utxo_common::extract_extended_pubkey(&self.utxo_arc.conf, xpub_extractor, derivation_path).await + crate::extract_extended_pubkey_impl(self, xpub_extractor, derivation_path).await } } #[async_trait] impl HDWalletCoinOps for UtxoStandardCoin { - type Address = Address; - type Pubkey = Public; type HDWallet = UtxoHDWallet; - type HDAccount = UtxoHDAccount; - - async fn derive_addresses( - &self, - hd_account: &Self::HDAccount, - address_ids: Ids, - ) -> AddressDerivingResult>> - where - Ids: Iterator + Send, - { - utxo_common::derive_addresses(self, hd_account, address_ids).await - } - async fn generate_and_confirm_new_address( + fn address_from_extended_pubkey( &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - confirm_address: &ConfirmAddress, - ) -> MmResult, NewAddressDeriveConfirmError> - where - ConfirmAddress: HDConfirmAddress, - { - utxo_common::generate_and_confirm_new_address(self, hd_wallet, hd_account, chain, confirm_address).await - } - - async fn create_new_account<'a, XPubExtractor>( - &self, - hd_wallet: &'a Self::HDWallet, - xpub_extractor: &XPubExtractor, - ) -> MmResult, NewAccountCreatingError> - where - XPubExtractor: HDXPubExtractor, - { - utxo_common::create_new_account(self, hd_wallet, xpub_extractor).await + extended_pubkey: &Secp256k1ExtendedPublicKey, + derivation_path: DerivationPath, + ) -> UtxoHDAddress { + utxo_common::address_from_extended_pubkey(self, extended_pubkey, derivation_path) } - async fn set_known_addresses_number( - &self, - hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, - chain: Bip44Chain, - new_known_addresses_number: u32, - ) -> MmResult<(), AccountUpdatingError> { - utxo_common::set_known_addresses_number(self, hd_wallet, hd_account, chain, new_known_addresses_number).await - } + fn trezor_coin(&self) -> MmResult { utxo_common::trezor_coin(self) } } +impl HDCoinWithdrawOps for UtxoStandardCoin {} + #[async_trait] impl GetNewAddressRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn get_new_address_rpc_without_conf( &self, params: GetNewAddressParams, - ) -> MmResult { + ) -> MmResult, GetNewAddressRpcError> { get_new_address::common_impl::get_new_address_rpc_without_conf(self, params).await } @@ -983,7 +1121,7 @@ impl GetNewAddressRpcOps for UtxoStandardCoin { &self, params: GetNewAddressParams, confirm_address: &ConfirmAddress, - ) -> MmResult + ) -> MmResult, GetNewAddressRpcError> where ConfirmAddress: HDConfirmAddress, { @@ -994,6 +1132,7 @@ impl GetNewAddressRpcOps for UtxoStandardCoin { #[async_trait] impl HDWalletBalanceOps for UtxoStandardCoin { type HDAddressScanner = UtxoAddressScanner; + type BalanceObject = CoinBalance; async fn produce_hd_address_scanner(&self) -> BalanceResult { utxo_common::produce_hd_address_scanner(self).await @@ -1002,87 +1141,100 @@ impl HDWalletBalanceOps for UtxoStandardCoin { async fn enable_hd_wallet( &self, hd_wallet: &Self::HDWallet, - xpub_extractor: &XPubExtractor, + xpub_extractor: Option, params: EnabledCoinBalanceParams, - ) -> MmResult + path_to_address: &HDPathAccountToAddressId, + ) -> MmResult, EnableCoinBalanceError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { - coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params).await + coin_balance::common_impl::enable_hd_wallet(self, hd_wallet, xpub_extractor, params, path_to_address).await } async fn scan_for_new_addresses( &self, hd_wallet: &Self::HDWallet, - hd_account: &mut Self::HDAccount, + hd_account: &mut UtxoHDAccount, address_scanner: &Self::HDAddressScanner, gap_limit: u32, - ) -> BalanceResult> { + ) -> BalanceResult>> { utxo_common::scan_for_new_addresses(self, hd_wallet, hd_account, address_scanner, gap_limit).await } - async fn all_known_addresses_balances(&self, hd_account: &Self::HDAccount) -> BalanceResult> { + async fn all_known_addresses_balances( + &self, + hd_account: &UtxoHDAccount, + ) -> BalanceResult>> { utxo_common::all_known_addresses_balances(self, hd_account).await } - async fn known_address_balance(&self, address: &Self::Address) -> BalanceResult { + async fn known_address_balance(&self, address: &Address) -> BalanceResult { utxo_common::address_balance(self, address).await } async fn known_addresses_balances( &self, - addresses: Vec, - ) -> BalanceResult> { + addresses: Vec
, + ) -> BalanceResult> { utxo_common::addresses_balances(self, addresses).await } -} -impl HDWalletCoinWithStorageOps for UtxoStandardCoin { - fn hd_wallet_storage<'a>(&self, hd_wallet: &'a Self::HDWallet) -> &'a HDWalletCoinStorage { - &hd_wallet.hd_wallet_storage + async fn prepare_addresses_for_balance_stream_if_enabled( + &self, + addresses: HashSet, + ) -> MmResult<(), String> { + utxo_prepare_addresses_for_balance_stream_if_enabled(self, addresses).await } } #[async_trait] impl AccountBalanceRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn account_balance_rpc( &self, params: AccountBalanceParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { account_balance::common_impl::account_balance_rpc(self, params).await } } #[async_trait] impl InitAccountBalanceRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn init_account_balance_rpc( &self, params: InitAccountBalanceParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { init_account_balance::common_impl::init_account_balance_rpc(self, params).await } } #[async_trait] impl InitScanAddressesRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn init_scan_for_new_addresses_rpc( &self, params: ScanAddressesParams, - ) -> MmResult { + ) -> MmResult, HDAccountBalanceRpcError> { init_scan_for_new_addresses::common_impl::scan_for_new_addresses_rpc(self, params).await } } #[async_trait] impl InitCreateAccountRpcOps for UtxoStandardCoin { + type BalanceObject = CoinBalance; + async fn init_create_account_rpc( &self, params: CreateNewAccountParams, state: CreateAccountState, - xpub_extractor: &XPubExtractor, - ) -> MmResult + xpub_extractor: Option, + ) -> MmResult, CreateAccountRpcError> where - XPubExtractor: HDXPubExtractor, + XPubExtractor: HDXPubExtractor + Send, { init_create_account::common_impl::init_create_new_account_rpc(self, params, state, xpub_extractor).await } @@ -1107,7 +1259,8 @@ impl CoinWithTxHistoryV2 for UtxoStandardCoin { #[async_trait] impl UtxoTxHistoryOps for UtxoStandardCoin { async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError> { - utxo_common::utxo_tx_history_v2_common::my_addresses(self).await + let addresses = self.all_addresses().await?; + Ok(addresses) } async fn tx_details_by_hash( diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 88d60142bd..868f558f1f 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -1,10 +1,9 @@ use super::*; use crate::coin_balance::HDAddressBalance; use crate::coin_errors::ValidatePaymentError; -use crate::hd_confirm_address::for_tests::MockableConfirmAddress; -use crate::hd_confirm_address::{HDConfirmAddress, HDConfirmAddressError}; -use crate::hd_wallet::HDAccountsMap; -use crate::hd_wallet_storage::{HDWalletMockStorage, HDWalletStorageInternalOps}; +use crate::hd_wallet::{HDAccountsMap, HDAccountsMutex, HDAddressesCache, HDConfirmAddress, HDConfirmAddressError, + HDWallet, HDWalletCoinStorage, HDWalletMockStorage, HDWalletStorageInternalOps, + MockableConfirmAddress}; use crate::my_tx_history_v2::for_tests::init_storage_for; use crate::my_tx_history_v2::CoinWithTxHistoryV2; use crate::rpc_command::account_balance::{AccountBalanceParams, AccountBalanceRpcOps, HDAccountBalanceResponse}; @@ -25,26 +24,29 @@ use crate::utxo::utxo_common::UtxoTxBuilder; #[cfg(not(target_arch = "wasm32"))] use crate::utxo::utxo_common_tests::TEST_COIN_DECIMALS; use crate::utxo::utxo_common_tests::{self, utxo_coin_fields_for_test, utxo_coin_from_fields, TEST_COIN_NAME}; +use crate::utxo::utxo_hd_wallet::UtxoHDAccount; use crate::utxo::utxo_standard::{utxo_standard_coin_with_priv_key, UtxoStandardCoin}; use crate::utxo::utxo_tx_history_v2::{UtxoTxDetailsParams, UtxoTxHistoryOps}; -#[cfg(not(target_arch = "wasm32"))] use crate::WithdrawFee; use crate::{BlockHeightAndTime, CoinBalance, ConfirmPaymentInput, DexFee, IguanaPrivKey, PrivKeyBuildPolicy, SearchForSwapTxSpendInput, SpendPaymentArgs, StakingInfosDetails, SwapOps, TradePreimageValue, - TxFeeDetails, TxMarshalingErr, ValidateFeeArgs, WaitForHTLCTxSpendArgs, INVALID_SENDER_ERR_LOG}; + TxFeeDetails, TxMarshalingErr, ValidateFeeArgs, INVALID_SENDER_ERR_LOG}; +#[cfg(not(target_arch = "wasm32"))] +use crate::{WaitForHTLCTxSpendArgs, WithdrawFee}; use chain::{BlockHeader, BlockHeaderBits, OutPoint}; use common::executor::Timer; use common::{block_on, wait_until_sec, OrdRange, PagingOptionsEnum, DEX_FEE_ADDR_RAW_PUBKEY}; -use crypto::{privkey::key_pair_from_seed, Bip44Chain, RpcDerivationPath, Secp256k1Secret}; +use crypto::{privkey::key_pair_from_seed, Bip44Chain, HDPathToAccount, RpcDerivationPath, Secp256k1Secret}; #[cfg(not(target_arch = "wasm32"))] use db_common::sqlite::rusqlite::Connection; use futures::channel::mpsc::channel; use futures::future::join_all; use futures::TryFutureExt; +use keys::prefixes::*; use mm2_core::mm_ctx::MmCtxBuilder; use mm2_number::bigdecimal::{BigDecimal, Signed}; use mm2_test_helpers::electrums::doc_electrums; use mm2_test_helpers::for_tests::{electrum_servers_rpc, mm_ctx_with_custom_db, DOC_ELECTRUM_ADDRS, - MARTY_ELECTRUM_ADDRS, MORTY_ELECTRUM_ADDRS, RICK_ELECTRUM_ADDRS, T_BCH_ELECTRUMS}; + MARTY_ELECTRUM_ADDRS, T_BCH_ELECTRUMS}; use mocktopus::mocking::*; use rpc::v1::types::H256 as H256Json; use serialization::{deserialize, CoinVariant}; @@ -53,8 +55,8 @@ use spv_validation::storage::BlockHeaderStorageOps; use spv_validation::work::DifficultyAlgorithm; #[cfg(not(target_arch = "wasm32"))] use std::convert::TryFrom; use std::iter; -use std::mem::discriminant; use std::num::NonZeroUsize; +use std::str::FromStr; #[cfg(not(target_arch = "wasm32"))] const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; @@ -84,7 +86,7 @@ pub fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let servers = servers.into_iter().map(|s| json::from_value(s).unwrap()).collect(); let abortable_system = AbortableQueue::default(); - block_on(builder.electrum_client(abortable_system, args, servers)).unwrap() + block_on(builder.electrum_client(abortable_system, args, servers, None)).unwrap() } /// Returned client won't work by default, requires some mocks to be usable @@ -158,7 +160,7 @@ fn test_extract_secret() { #[test] fn test_send_maker_spends_taker_payment_recoverable_tx() { - let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let client = electrum_client_for_test(DOC_ELECTRUM_ADDRS); let coin = utxo_coin_for_test(client.into(), None, false); let tx_hex = hex::decode("0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c").unwrap(); let secret = hex::decode("9da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365").unwrap(); @@ -172,18 +174,11 @@ fn test_send_maker_spends_taker_payment_recoverable_tx() { swap_unique_data: &[], watcher_reward: false, }; - let tx_err = coin - .send_maker_spends_taker_payment(maker_spends_payment_args) - .wait() - .unwrap_err(); + let tx_err = block_on(coin.send_maker_spends_taker_payment(maker_spends_payment_args)) + .expect_err("!send_maker_spends_taker_payment should error missing tx inputs"); - let tx: UtxoTx = deserialize(tx_hex.as_slice()).unwrap(); - - // The error variant should equal to `TxRecoverable` - assert_eq!( - discriminant(&tx_err), - discriminant(&TransactionErr::TxRecoverable(TransactionEnum::from(tx), String::new())) - ); + // The error variant should be `TxRecoverable` + assert!(matches!(tx_err, TransactionErr::TxRecoverable(_, _))); } #[test] @@ -194,6 +189,7 @@ fn test_generate_transaction() { value: 10000000000, outpoint: OutPoint::default(), height: Default::default(), + script: Vec::new().into(), }]; let outputs = vec![TransactionOutput { @@ -201,7 +197,7 @@ fn test_generate_transaction() { value: 999, }]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs); let generated = block_on(builder.build()); @@ -212,6 +208,7 @@ fn test_generate_transaction() { value: 100000, outpoint: OutPoint::default(), height: Default::default(), + script: Vec::new().into(), }]; let outputs = vec![TransactionOutput { @@ -219,7 +216,7 @@ fn test_generate_transaction() { value: 98001, }]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs); let generated = block_on(builder.build()).unwrap(); @@ -236,15 +233,17 @@ fn test_generate_transaction() { value: 100000, outpoint: OutPoint::default(), height: Default::default(), + script: Vec::new().into(), }]; let outputs = vec![TransactionOutput { - script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_single_addr().hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(block_on(coin.as_ref().derivation_method.unwrap_single_addr()).hash()) + .to_bytes(), value: 100000, }]; // test that fee is properly deducted from output amount equal to input amount (max withdraw case) - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(FeePolicy::DeductFromOutput(0)); @@ -262,6 +261,7 @@ fn test_generate_transaction() { value: 100000, outpoint: OutPoint::default(), height: Default::default(), + script: Vec::new().into(), }]; let outputs = vec![TransactionOutput { @@ -270,7 +270,7 @@ fn test_generate_transaction() { }]; // test that generate_transaction returns an error when input amount is not sufficient to cover output + fee - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs); @@ -283,13 +283,21 @@ fn test_addresses_from_script() { let coin = utxo_coin_for_test(client.into(), None, false); // P2PKH let script: Script = "76a91405aab5342166f8594baf17a7d9bef5d56744332788ac".into(); - let expected_addr: Vec
= vec!["R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW".into()]; + let expected_addr: Vec
= vec![Address::from_legacyaddress( + "R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap()]; let actual_addr = coin.addresses_from_script(&script).unwrap(); assert_eq!(expected_addr, actual_addr); // P2SH let script: Script = "a914e71a6120653ebd526e0f9d7a29cde5969db362d487".into(); - let expected_addr: Vec
= vec!["bZoEPR7DjTqSDiQTeRFNDJuQPTRY2335LD".into()]; + let expected_addr: Vec
= vec![Address::from_legacyaddress( + "bZoEPR7DjTqSDiQTeRFNDJuQPTRY2335LD", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap()]; let actual_addr = coin.addresses_from_script(&script).unwrap(); assert_eq!(expected_addr, actual_addr); } @@ -360,7 +368,7 @@ fn test_kmd_interest_kip_0001_reduction() { // Starting from dPoW 7th season, according to KIP0001 AUR should be reduced from 5% to 0.01%, i.e. div by 500 let expected = value / 10512000 * (31 * 24 * 60 - 59) / 500; - println!("expected: {}", expected); + log!("expected: {}", expected); let actual = kmd_interest(height, value, lock_time, current_time).unwrap(); assert_eq!(expected, actual); } @@ -419,7 +427,7 @@ fn test_wait_for_payment_spend_timeout_native() { let client = NativeClientImpl::default(); static mut OUTPUT_SPEND_CALLED: bool = false; - NativeClient::find_output_spend.mock_safe(|_, _, _, _, _| { + NativeClient::find_output_spend.mock_safe(|_, _, _, _, _, _| { unsafe { OUTPUT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(None))) }); @@ -450,7 +458,7 @@ fn test_wait_for_payment_spend_timeout_native() { fn test_wait_for_payment_spend_timeout_electrum() { static mut OUTPUT_SPEND_CALLED: bool = false; - ElectrumClient::find_output_spend.mock_safe(|_, _, _, _, _| { + ElectrumClient::find_output_spend.mock_safe(|_, _, _, _, _, _| { unsafe { OUTPUT_SPEND_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(None))) }); @@ -469,6 +477,7 @@ fn test_wait_for_payment_spend_timeout_electrum() { block_headers_storage, abortable_system, true, + None, ); let client = UtxoRpcClientEnum::Electrum(ElectrumClient(Arc::new(client))); let coin = utxo_coin_for_test(client, None, false); @@ -494,26 +503,26 @@ fn test_wait_for_payment_spend_timeout_electrum() { #[test] fn test_search_for_swap_tx_spend_electrum_was_spent() { - let secret = [0; 32]; - let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let secret = hex::decode("a1c44607b870cd714a75d5243347fa36debcd3a91ff1f50b79f52d83238a0b2d").unwrap(); + let client = electrum_client_for_test(DOC_ELECTRUM_ADDRS); let coin = utxo_coin_for_test( client.into(), Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), false, ); - // raw tx bytes of https://rick.kmd.dev/tx/ba881ecca15b5d4593f14f25debbcdfe25f101fd2e9cf8d0b5d92d19813d4424 - let payment_tx_bytes = hex::decode("0400008085202f8902e115acc1b9e26a82f8403c9f81785445cc1285093b63b6246cf45aabac5e0865000000006b483045022100ca578f2d6bae02f839f71619e2ced54538a18d7aa92bd95dcd86ac26479ec9f802206552b6c33b533dd6fc8985415a501ebec89d1f5c59d0c923d1de5280e9827858012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffffb0721bf69163f7a5033fb3d18ba5768621d8c1347ebaa2fddab0d1f63978ea78020000006b483045022100a3309f99167982e97644dbb5cd7279b86630b35fc34855e843f2c5c0cafdc66d02202a8c3257c44e832476b2e2a723dad1bb4ec1903519502a49b936c155cae382ee012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0300e1f5050000000017a91443fde927a77b3c1d104b78155dc389078c4571b0870000000000000000166a14b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc64b8cd736000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788acba0ce35e000000000000000000000000000000") + // raw tx bytes of https://doc.komodo.earth/tx/2f216f7ddb4350ed52e4c5b3a649da7aa63c932e623a1046066f91bdf00015a0 + let payment_tx_bytes = hex::decode("0400008085202f890129f70bfc256e71600471be0a0e10f31d7025f350301e4c5aab71d4910bc29cb20200000069463043022027283d1a28ce25f3a937376f40873441a7b5f9c4a38c6637642b78845874f12b021f6cf1f4836724e186c5c299507e2c5db40855ed2c0acad73e3446a6f1e00d83012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0320a107000000000017a9148d3a47615c562a08ae3c42c237ca5a6f5f517b7a870000000000000000166a147c0c02ee06e3769376bb2b31e05a9e9965045ffbd2ae2b7b000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788acf318fc65000000000000000000000000000000") .unwrap(); - // raw tx bytes of https://rick.kmd.dev/tx/cea8028f93f7556ce0ef96f14b8b5d88ef2cd29f428df5936e02e71ca5b0c795 - let spend_tx_bytes = hex::decode("0400008085202f890124443d81192dd9b5d0f89c2efd01f125fecdbbde254ff193455d5ba1cc1e88ba00000000d74730440220519d3eed69815a16357ff07bf453b227654dc85b27ffc22a77abe077302833ec02205c27f439ddc542d332504112871ecac310ea710b99e1922f48eb179c045e44ee01200000000000000000000000000000000000000000000000000000000000000000004c6b6304a9e5e25eb1752102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ac6782012088a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc6882102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ac68ffffffff0118ddf505000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788acbffee25e000000000000000000000000000000") + // raw tx bytes of https://doc.komodo.earth/tx/2450b60c8ab0d2498d9cee3cfb67ecbe08e335ec8fa20c7f95c474734d5e007a + let spend_tx_bytes = hex::decode("0400008085202f8901a01500f0bd916f0646103a622e933ca67ada49a6b3c5e452ed5043db7d6f212f00000000d7473044022042dbb34a97d9cdcea2c0db9871f3d1bbeb35ed74d373095eb76286573b121579022010bdee78e995f5b18f50a3ae82fac57db7147d90bedd9b96c42e54a1f65546540120a1c44607b870cd714a75d5243347fa36debcd3a91ff1f50b79f52d83238a0b2d004c6b6304df55fc65b1752102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ac6782012088a9147c0c02ee06e3769376bb2b31e05a9e9965045ffb882102d74dc5ec4c823f40dae5c563d7b22aab52c80f9f18226f47ea6d83107618df62ac68ffffffff01389d0700000000001976a914f26650dc9aa4e4505978ad635cdb15491cee70e188acdf55fc65000000000000000000000000000000") .unwrap(); let spend_tx = TransactionEnum::UtxoTx(deserialize(spend_tx_bytes.as_slice()).unwrap()); let search_input = SearchForSwapTxSpendInput { - time_lock: 1591928233, - other_pub: coin.my_public_key().unwrap(), + time_lock: 1711035871, + other_pub: &hex::decode("02d74dc5ec4c823f40dae5c563d7b22aab52c80f9f18226f47ea6d83107618df62").unwrap(), secret_hash: &*dhash160(&secret), tx: &payment_tx_bytes, search_from_block: 0, @@ -529,26 +538,26 @@ fn test_search_for_swap_tx_spend_electrum_was_spent() { #[test] fn test_search_for_swap_tx_spend_electrum_was_refunded() { - let secret_hash = [0; 20]; - let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let secret_hash = hex::decode("7a752434d4564c11b9333743122dab3a0aa21bd9").unwrap(); + let client = electrum_client_for_test(DOC_ELECTRUM_ADDRS); let coin = utxo_coin_for_test( client.into(), Some("spice describe gravity federal blast come thank unfair canal monkey style afraid"), false, ); - // raw tx bytes of https://rick.kmd.dev/tx/78ea7839f6d1b0dafda2ba7e34c1d8218676a58bd1b33f03a5f76391f61b72b0 - let payment_tx_bytes = hex::decode("0400008085202f8902bf17bf7d1daace52e08f732a6b8771743ca4b1cb765a187e72fd091a0aabfd52000000006a47304402203eaaa3c4da101240f80f9c5e9de716a22b1ec6d66080de6a0cca32011cd77223022040d9082b6242d6acf9a1a8e658779e1c655d708379862f235e8ba7b8ca4e69c6012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffffff023ca13c0e9e085dd13f481f193e8a3e8fd609020936e98b5587342d994f4d020000006b483045022100c0ba56adb8de923975052312467347d83238bd8d480ce66e8b709a7997373994022048507bcac921fdb2302fa5224ce86e41b7efc1a2e20ae63aa738dfa99b7be826012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0300e1f5050000000017a9141ee6d4c38a3c078eab87ad1a5e4b00f21259b10d870000000000000000166a1400000000000000000000000000000000000000001b94d736000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac2d08e35e000000000000000000000000000000") + // raw tx bytes of https://doc.komodo.earth/tx/ba08822b2da3f7120f5ad90cd999d5f0f4be4a63de496f4f76af202c68e4f5eb + let payment_tx_bytes = hex::decode("0400008085202f8901b2781d994b79be8e1f687a7f376109063bbe8b51ab36d04b35d4ff437b21d2a5010000006a47304402201832294ceb2b62a197bc2049218dcee69dcabb414249403efcc35ad65c17e73d0220348fbbb2b40880408bdef604e5a6cba34f514b84d5d6a9a4c4713669cd765fd2012102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ffffffff0320a107000000000017a91489dd2a32ae17a0575759581afb0002176296777b870000000000000000166a147a752434d4564c11b9333743122dab3a0aa21bd94af2de7a000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ac2b0efe65000000000000000000000000000000") .unwrap(); - // raw tx bytes of https://rick.kmd.dev/tx/65085eacab5af46c24b6633b098512cc455478819f3c40f8826ae2b9c1ac15e1 - let refund_tx_bytes = hex::decode("0400008085202f8901b0721bf69163f7a5033fb3d18ba5768621d8c1347ebaa2fddab0d1f63978ea7800000000b6473044022052e06c1abf639148229a3991fdc6da15fe51c97577f4fda351d9c606c7cf53670220780186132d67d354564cae710a77d94b6bb07dcbd7162a13bebee261ffc0963601514c6b63041dfae25eb1752102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ac6782012088a9140000000000000000000000000000000000000000882102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ac68feffffff0118ddf505000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788ace6fae25e000000000000000000000000000000") + // raw tx bytes of https://doc.komodo.earth/tx/1b4e30f0e3101374464ec159c2f35b03412034fadee8c5769e8adcd5c91359bd + let refund_tx_bytes = hex::decode("0400008085202f8901ebf5e4682c20af764f6f49de634abef4f0d599d90cd95a0f12f7a32d2b8208ba00000000b647304402205bd140728b1b6de7b891025873d552a439c488dd7fe4234fd982eaa193e5776602205ad9fea8bc771d94de9d2fdc450186ea21f4fc81f28c2f2f75b5d2ad84891d8f01514c6b63049f0efe65b1752102031d4256c4bc9f99ac88bf3dba21773132281f65f9bf23a59928bce08961e2f3ac6782012088a9147a752434d4564c11b9333743122dab3a0aa21bd9882102d74dc5ec4c823f40dae5c563d7b22aab52c80f9f18226f47ea6d83107618df62ac68feffffff01389d0700000000001976a91405aab5342166f8594baf17a7d9bef5d56744332788acb80efe65000000000000000000000000000000") .unwrap(); let refund_tx = TransactionEnum::UtxoTx(deserialize(refund_tx_bytes.as_slice()).unwrap()); let search_input = SearchForSwapTxSpendInput { - time_lock: 1591933469, - other_pub: coin.as_ref().priv_key_policy.activated_key_or_err().unwrap().public(), + time_lock: 1711148703, + other_pub: &hex::decode("02d74dc5ec4c823f40dae5c563d7b22aab52c80f9f18226f47ea6d83107618df62").unwrap(), secret_hash: &secret_hash, tx: &payment_tx_bytes, search_from_block: 0, @@ -574,6 +583,9 @@ fn test_withdraw_impl_set_fixed_fee() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); @@ -592,6 +604,7 @@ fn test_withdraw_impl_set_fixed_fee() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; let expected = Some( UtxoFeeDetails { @@ -616,6 +629,9 @@ fn test_withdraw_impl_sat_per_kb_fee() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); @@ -634,6 +650,7 @@ fn test_withdraw_impl_sat_per_kb_fee() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; // The resulting transaction size might be 244 or 245 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -661,6 +678,9 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); @@ -679,6 +699,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -708,6 +729,9 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); @@ -726,6 +750,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_equal_to_max_dust_included_to_fee() amount: "0.09999999".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); // The resulting transaction size might be 210 or 211 bytes depending on signature size @@ -755,6 +780,9 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); @@ -773,6 +801,7 @@ fn test_withdraw_impl_sat_per_kb_fee_amount_over_max() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; coin.withdraw(withdraw_req).wait().unwrap_err(); } @@ -789,6 +818,9 @@ fn test_withdraw_impl_sat_per_kb_fee_max() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); @@ -807,6 +839,7 @@ fn test_withdraw_impl_sat_per_kb_fee_max() { amount: "0.1".parse().unwrap(), }), memo: None, + ibc_source_channel: None, }; // The resulting transaction size might be 210 or 211 bytes depending on signature size // MM2 always expects the worst case during fee calculation @@ -832,7 +865,7 @@ fn test_withdraw_kmd_rewards_impl( ) { let verbose: RpcTransaction = json::from_str(verbose_serialized).unwrap(); let unspent_height = verbose.height; - UtxoStandardCoin::get_unspent_ordered_list.mock_safe(move |coin, _| { + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(move |coin: &UtxoStandardCoin, _| { let tx: UtxoTx = tx_hex.into(); let unspents = vec![UnspentInfo { outpoint: OutPoint { @@ -841,6 +874,9 @@ fn test_withdraw_kmd_rewards_impl( }, value: tx.outputs[0].value, height: unspent_height, + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) @@ -867,6 +903,7 @@ fn test_withdraw_kmd_rewards_impl( max: false, fee: None, memo: None, + ibc_source_channel: None, }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some("KMD".into()), @@ -913,6 +950,10 @@ fn test_withdraw_kmd_rewards_zero() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_rick_rewards_none() { + let client = NativeClient(Arc::new(NativeClientImpl::default())); + + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); + // https://rick.explorer.dexstats.info/tx/7181400be323acc6b5f3164240e6c4601ff4c252f40ce7649f87e81634330209 const TX_HEX: &str = "0400008085202f8901df8119c507aa61d32332cd246dbfeb3818a4f96e76492454c1fbba5aa097977e000000004847304402205a7e229ea6929c97fd6dde254c19e4eb890a90353249721701ae7a1c477d99c402206a8b7c5bf42b5095585731d6b4c589ce557f63c20aed69ff242eca22ecfcdc7a01feffffff02d04d1bffbc050000232102afdbba3e3c90db5f0f4064118f79cf308f926c68afd64ea7afc930975663e4c4ac402dd913000000001976a9143e17014eca06281ee600adffa34b4afb0922a22288ac2bdab86035a00e000000000000000000000000"; @@ -925,15 +966,14 @@ fn test_withdraw_rick_rewards_none() { }, value: tx.outputs[0].value, height: Some(1431628), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); - let client = NativeClient(Arc::new(NativeClientImpl::default())); - - let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); - let withdraw_req = WithdrawRequest { amount: BigDecimal::from_str("0.00001").unwrap(), from: None, @@ -942,6 +982,7 @@ fn test_withdraw_rick_rewards_none() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let expected_fee = TxFeeDetails::Utxo(UtxoFeeDetails { coin: Some(TEST_COIN_NAME.into()), @@ -959,7 +1000,8 @@ fn test_utxo_lock() { let coin = utxo_coin_for_test(client.into(), None, false); let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_single_addr().hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(block_on(coin.as_ref().derivation_method.unwrap_single_addr()).hash()) + .to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -1028,7 +1070,7 @@ fn test_electrum_rpc_client_error() { // use the static string instead because the actual error message cannot be obtain // by serde_json serialization - let expected = r#"JsonRpcError { client_info: "coin: RICK", request: JsonRpcRequest { jsonrpc: "2.0", id: "1", method: "blockchain.transaction.get", params: [String("0000000000000000000000000000000000000000000000000000000000000000"), Bool(true)] }, error: Response(electrum1.cipig.net:10060, Object({"code": Number(2), "message": String("daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})")})) }"#; + let expected = r#"JsonRpcError { client_info: "coin: DOC", request: JsonRpcRequest { jsonrpc: "2.0", id: "1", method: "blockchain.transaction.get", params: [String("0000000000000000000000000000000000000000000000000000000000000000"), Bool(true)] }, error: Response(electrum1.cipig.net:10060, Object({"code": Number(2), "message": String("daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})")})) }"#; let actual = format!("{}", err); assert!(actual.contains(expected)); @@ -1137,6 +1179,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower() { value: 1000000000, outpoint: OutPoint::default(), height: Default::default(), + script: Vec::new().into(), }]; let outputs = vec![TransactionOutput { @@ -1144,7 +1187,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower() { value: 900000000, }]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) .with_fee(ActualTxFee::Dynamic(100)); @@ -1179,6 +1222,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower_and_ded value: 1000000000, outpoint: OutPoint::default(), height: Default::default(), + script: Vec::new().into(), }]; let outputs = vec![TransactionOutput { @@ -1186,7 +1230,7 @@ fn test_generate_transaction_relay_fee_is_used_when_dynamic_fee_is_lower_and_ded value: 1000000000, }]; - let tx_builder = UtxoTxBuilder::new(&coin) + let tx_builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) .with_fee_policy(FeePolicy::DeductFromOutput(0)) @@ -1225,6 +1269,7 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { value: 1000000000, outpoint: OutPoint::default(), height: Default::default(), + script: Vec::new().into(), }; 20 ]; @@ -1234,7 +1279,7 @@ fn test_generate_tx_fee_is_correct_when_dynamic_fee_is_larger_than_relay() { value: 19000000000, }]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents) .add_outputs(outputs) .with_fee(ActualTxFee::Dynamic(1000)); @@ -1482,13 +1527,14 @@ fn test_network_info_negative_time_offset() { #[test] fn test_unavailable_electrum_proto_version() { ElectrumClientImpl::new.mock_safe( - |coin_ticker, event_handlers, block_headers_storage, abortable_system, _| { + |coin_ticker, event_handlers, block_headers_storage, abortable_system, _, _| { MockResult::Return(ElectrumClientImpl::with_protocol_version( coin_ticker, event_handlers, OrdRange::new(1.8, 1.9).unwrap(), block_headers_storage, abortable_system, + None, )) }, ); @@ -1534,7 +1580,8 @@ fn test_spam_rick() { let output = TransactionOutput { value: 1000000, - script_pubkey: Builder::build_p2pkh(&coin.as_ref().derivation_method.unwrap_single_addr().hash).to_bytes(), + script_pubkey: Builder::build_p2pkh(block_on(coin.as_ref().derivation_method.unwrap_single_addr()).hash()) + .to_bytes(), }; let mut futures = vec![]; for _ in 0..5 { @@ -1601,8 +1648,12 @@ fn test_qtum_generate_pod() { let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); let coin = block_on(qtum_coin_with_priv_key(&ctx, "tQTUM", &conf, ¶ms, priv_key)).unwrap(); let expected_res = "20086d757b34c01deacfef97a391f8ed2ca761c72a08d5000adc3d187b1007aca86a03bc5131b1f99b66873a12b51f8603213cdc1aa74c05ca5d48fe164b82152b"; - let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); - let res = coin.generate_pod(address.hash).unwrap(); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); + let res = coin.generate_pod(address.hash().clone()).unwrap(); assert_eq!(expected_res, res.to_string()); } @@ -1625,7 +1676,11 @@ fn test_qtum_add_delegation() { keypair.private().secret, )) .unwrap(); - let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); let request = QtumDelegationRequest { address: address.to_string(), fee: Some(10), @@ -1664,7 +1719,11 @@ fn test_qtum_add_delegation_on_already_delegating() { keypair.private().secret, )) .unwrap(); - let address = Address::from_str("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE").unwrap(); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); let request = QtumDelegationRequest { address: address.to_string(), fee: Some(10), @@ -1742,6 +1801,7 @@ fn test_qtum_my_balance() { }, value: 5000000000, height: Default::default(), + script: Vec::new().into(), }, UnspentInfo { outpoint: OutPoint { @@ -1750,6 +1810,7 @@ fn test_qtum_my_balance() { }, value: 1600000000, height: Default::default(), + script: Vec::new().into(), }, ]; // unspendable (2.0) @@ -1760,6 +1821,7 @@ fn test_qtum_my_balance() { }, value: 200000000, height: Default::default(), + script: Vec::new().into(), }]; MockResult::Return(Box::pin(futures::future::ok(( MatureUnspentList { mature, immature }, @@ -1850,6 +1912,7 @@ fn test_get_mature_unspent_ordered_map_from_cache_impl( }, value: 1000000000, height: unspent_height, + script: Vec::new().into(), }]; MockResult::Return(Box::new(futures01::future::ok(unspents))) }); @@ -1871,9 +1934,10 @@ fn test_get_mature_unspent_ordered_map_from_cache_impl( // run test let coin = utxo_coin_for_test(UtxoRpcClientEnum::Electrum(client), None, false); - let (unspents, _) = - block_on(coin.get_mature_unspent_ordered_list(&Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"))) - .expect("Expected an empty unspent list"); + let (unspents, _) = block_on(coin.get_mature_unspent_ordered_list( + &Address::from_legacyaddress("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", &KMD_PREFIXES).unwrap(), + )) + .expect("Expected an empty unspent list"); // unspents should be empty because `is_unspent_mature()` always returns false assert!(unsafe { IS_UNSPENT_MATURE_CALLED }); assert!(unspents.mature.is_empty()); @@ -2012,9 +2076,9 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_tx_in_cache() { let client = native_client_for_test(); let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); - let address: Address = "RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN".into(); - block_on(coin.as_ref().recently_spent_outpoints.lock()).for_script_pubkey = - Builder::build_p2pkh(&address.hash).to_bytes(); + let address: Address = Address::from_legacyaddress("RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN", &KMD_PREFIXES).unwrap(); + let output_script = coin.script_for_address(&address).unwrap(); + block_on(coin.as_ref().recently_spent_outpoints.lock()).for_script_pubkey = output_script.clone().into(); // https://morty.explorer.dexstats.info/tx/31c7aaae89ab1c39febae164a3190a86ed7c6c6f8c9dc98ec28d508b7929d347 let tx: UtxoTx = "0400008085202f89027f57730fcbbc2c72fb18bcc3766a713044831a117bb1cade3ed88644864f7333020000006a47304402206e3737b2fcf078b61b16fa67340cc3e79c5d5e2dc9ffda09608371552a3887450220460a332aa1b8ad8f2de92d319666f70751078b221199951f80265b4f7cef8543012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff42b916a80430b80a77e114445b08cf120735447a524de10742fac8f6a9d4170f000000006a473044022004aa053edafb9d161ea8146e0c21ed1593aa6b9404dd44294bcdf920a1695fd902202365eac15dbcc5e9f83e2eed56a8f2f0e5aded36206f9c3fabc668fd4665fa2d012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff03547b16000000000017a9143e8ad0e2bf573d32cb0b3d3a304d9ebcd0c2023b870000000000000000166a144e2b3c0323ab3c2dc6f86dc5ec0729f11e42f56103970400000000001976a91450f4f098306f988d8843004689fae28c83ef16e888ac89c5925f000000000000000000000000000000".into(); @@ -2023,11 +2087,13 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_tx_in_cache() { outpoint: tx.inputs[0].previous_output, value: 886737, height: Some(642293), + script: output_script.clone(), }, UnspentInfo { outpoint: tx.inputs[1].previous_output, value: 88843, height: Some(642293), + script: output_script, }, ]; @@ -2048,6 +2114,8 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_tx_in_cache() { }, value: tx.outputs[2].value, height: None, + // Should be the same as: Some(output_script.clone()), + script: tx.outputs[2].script_pubkey.clone().into(), }; assert_eq!(vec![expected_unspent], unspents_ordered); } @@ -2056,11 +2124,11 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_tx_in_cache() { #[cfg(not(target_arch = "wasm32"))] fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_txs_in_cache() { let client = native_client_for_test(); - let coin = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); - let address: Address = "RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN".into(); - block_on(coin.recently_spent_outpoints.lock()).for_script_pubkey = Builder::build_p2pkh(&address.hash).to_bytes(); - let coin = utxo_coin_from_fields(coin); + let address: Address = Address::from_legacyaddress("RGfFZaaNV68uVe1uMf6Y37Y8E1i2SyYZBN", &KMD_PREFIXES).unwrap(); + let output_script = coin.script_for_address(&address).unwrap(); + block_on(coin.as_ref().recently_spent_outpoints.lock()).for_script_pubkey = output_script.clone().into(); // https://morty.explorer.dexstats.info/tx/31c7aaae89ab1c39febae164a3190a86ed7c6c6f8c9dc98ec28d508b7929d347 let tx_0: UtxoTx = "0400008085202f89027f57730fcbbc2c72fb18bcc3766a713044831a117bb1cade3ed88644864f7333020000006a47304402206e3737b2fcf078b61b16fa67340cc3e79c5d5e2dc9ffda09608371552a3887450220460a332aa1b8ad8f2de92d319666f70751078b221199951f80265b4f7cef8543012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff42b916a80430b80a77e114445b08cf120735447a524de10742fac8f6a9d4170f000000006a473044022004aa053edafb9d161ea8146e0c21ed1593aa6b9404dd44294bcdf920a1695fd902202365eac15dbcc5e9f83e2eed56a8f2f0e5aded36206f9c3fabc668fd4665fa2d012102d8c948c6af848c588517288168faa397d6ba3ea924596d03d1d84f224b5123c2ffffffff03547b16000000000017a9143e8ad0e2bf573d32cb0b3d3a304d9ebcd0c2023b870000000000000000166a144e2b3c0323ab3c2dc6f86dc5ec0729f11e42f56103970400000000001976a91450f4f098306f988d8843004689fae28c83ef16e888ac89c5925f000000000000000000000000000000".into(); @@ -2069,11 +2137,13 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_tx outpoint: tx_0.inputs[0].previous_output, value: 886737, height: Some(642293), + script: output_script.clone(), }, UnspentInfo { outpoint: tx_0.inputs[1].previous_output, value: 88843, height: Some(642293), + script: output_script.clone(), }, ]; block_on(coin.as_ref().recently_spent_outpoints.lock()).add_spent(spent_by_tx_0.clone(), tx_0.hash(), tx_0.outputs); @@ -2085,16 +2155,19 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_tx outpoint: tx_1.inputs[0].previous_output, value: 300803, height: Some(642293), + script: output_script.clone(), }, UnspentInfo { outpoint: tx_1.inputs[1].previous_output, value: 888544, height: Some(642293), + script: output_script.clone(), }, UnspentInfo { outpoint: tx_1.inputs[2].previous_output, value: 888642, height: Some(642293), + script: output_script.clone(), }, ]; block_on(coin.as_ref().recently_spent_outpoints.lock()).add_spent(spent_by_tx_1.clone(), tx_1.hash(), tx_1.outputs); @@ -2105,11 +2178,13 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_tx outpoint: tx_2.inputs[0].previous_output, value: 832532, height: Some(642293), + script: output_script.clone(), }, UnspentInfo { outpoint: tx_2.inputs[1].previous_output, value: 888823, height: Some(642293), + script: output_script, }, ]; block_on(coin.as_ref().recently_spent_outpoints.lock()).add_spent( @@ -2135,6 +2210,58 @@ fn test_native_client_unspents_filtered_using_tx_cache_single_several_chained_tx }, value: tx_2.outputs[2].value, height: None, + // Should be the same as: Some(output_script.clone()), + script: tx_2.outputs[2].script_pubkey.clone().into(), + }; + assert_eq!(vec![expected_unspent], unspents_ordered); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_native_client_unspents_p2pk_filtered_using_tx_cache_single_tx_in_cache() { + let client = native_client_for_test(); + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); + + let address: Address = Address::from_legacyaddress("RWdLNGQ428ZmhbMs6sVi42KPUbiKYKhiLr", &KMD_PREFIXES).unwrap(); + let output_script = coin.script_for_address(&address).unwrap(); + block_on(coin.as_ref().recently_spent_outpoints.lock()).for_script_pubkey = output_script.into(); + + // https://morty.explorer.dexstats.info/tx/e5b6a8c98bc802cf764430d79f2a8fdb1373ecf8bb0fb07e9ffea559083e9ead + let tx: UtxoTx = "0400008085202f8902c828be61a42ca160a5cbf549b74cb3ac0a8011eb32924b61dd22cb8153dd0c9c000000004948304502210094e99bb9369d2b3c4f13b8ffd7c4abacbdb2b5ee06d9453b13c23d9a924e1b34022005e5939999537f8eb901b6a37fb0776d5f6196fc7a38b1ecc8b043e84ea420bb01ffffffff2ec39160776dc986b7619e07f077d719a3247e1c0ab2148213013a34d79fd56a000000006b483045022100909ec2d09276891d48765f101d6501ef0606af971309f86a814042421f420bc202200bc376f0186aeec215c4ad99d04d8e44354b6838044b26a6a195b36b86de2ed6012102b2e5b95daf6600d4b210ce5e0a9dae507df9c7b89618c4aea05045e5acc1e7eeffffffff01eceadcd4060000001976a914ea29e13cd4446800297a5883a48caddd6d12377688ac00000000becd09000000000000000000000000".into(); + let spent_by_tx = vec![ + UnspentInfo { + outpoint: tx.inputs[0].previous_output, + value: 100139000, + height: Some(642293), + script: "2103c9f7c3b8ff78beb991cf806a5c91561cfe68f530c9df2b1402e57621ecbcd6b0ac".into(), + }, + UnspentInfo { + outpoint: tx.inputs[1].previous_output, + value: 29240917628, + height: Some(642293), + script: "76a914ea29e13cd4446800297a5883a48caddd6d12377688ac".into(), + }, + ]; + + block_on(coin.as_ref().recently_spent_outpoints.lock()).add_spent( + spent_by_tx.clone(), + tx.hash(), + tx.outputs.clone(), + ); + NativeClient::list_unspent + .mock_safe(move |_, _, _| MockResult::Return(Box::new(futures01::future::ok(spent_by_tx.clone())))); + + let (unspents_ordered, _) = block_on(coin.get_unspent_ordered_list(&address)).unwrap(); + // output 0 is spent to self so it must be returned + let expected_unspent = UnspentInfo { + outpoint: OutPoint { + hash: tx.hash(), + index: 0, + }, + value: tx.outputs[0].value, + height: None, + // Should be the same as: Some(output_script.clone()), + script: tx.outputs[0].script_pubkey.clone().into(), }; assert_eq!(vec![expected_unspent], unspents_ordered); } @@ -2396,6 +2523,7 @@ fn test_find_output_spend_skips_conflicting_transactions() { &tx.outputs[vout].script_pubkey, vout, BlockHashOrHeight::Height(from_block), + TxHashAlgo::DSHA256, ) .wait(); assert_eq!(actual, Ok(None)); @@ -2488,6 +2616,7 @@ fn test_get_sender_trade_fee_dynamic_tx_fee() { let fee1 = block_on(coin.get_sender_trade_fee( TradePreimageValue::UpperBound(my_balance.clone()), FeeApproxStage::WithoutApprox, + false, )) .expect("!get_sender_trade_fee"); @@ -2496,15 +2625,19 @@ fn test_get_sender_trade_fee_dynamic_tx_fee() { let fee2 = block_on(coin.get_sender_trade_fee( TradePreimageValue::Exact(value_without_fee), FeeApproxStage::WithoutApprox, + false, )) .expect("!get_sender_trade_fee"); assert_eq!(fee1, fee2); // `2.21934443` value was obtained as a result of executing the `max_taker_vol` RPC call for this wallet let max_taker_vol = BigDecimal::from_str("2.21934443").expect("!BigDecimal::from_str"); - let fee3 = - block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(max_taker_vol), FeeApproxStage::WithoutApprox)) - .expect("!get_sender_trade_fee"); + let fee3 = block_on(coin.get_sender_trade_fee( + TradePreimageValue::Exact(max_taker_vol), + FeeApproxStage::WithoutApprox, + false, + )) + .expect("!get_sender_trade_fee"); assert_eq!(fee1, fee3); } @@ -2707,12 +2840,13 @@ fn test_generate_tx_doge_fee() { outpoint: Default::default(), value: 1000000000000, height: None, + script: Vec::new().into(), }]; let outputs = vec![TransactionOutput { value: 100000000, script_pubkey: vec![0; 26].into(), }]; - let builder = UtxoTxBuilder::new(&doge) + let builder = block_on(UtxoTxBuilder::new(&doge)) .add_available_inputs(unspents) .add_outputs(outputs); let (_, data) = block_on(builder.build()).unwrap(); @@ -2723,6 +2857,7 @@ fn test_generate_tx_doge_fee() { outpoint: Default::default(), value: 1000000000000, height: None, + script: Vec::new().into(), }]; let outputs = vec![ TransactionOutput { @@ -2732,7 +2867,7 @@ fn test_generate_tx_doge_fee() { 40 ]; - let builder = UtxoTxBuilder::new(&doge) + let builder = block_on(UtxoTxBuilder::new(&doge)) .add_available_inputs(unspents) .add_outputs(outputs); let (_, data) = block_on(builder.build()).unwrap(); @@ -2743,6 +2878,7 @@ fn test_generate_tx_doge_fee() { outpoint: Default::default(), value: 1000000000000, height: None, + script: Vec::new().into(), }]; let outputs = vec![ TransactionOutput { @@ -2752,7 +2888,7 @@ fn test_generate_tx_doge_fee() { 60 ]; - let builder = UtxoTxBuilder::new(&doge) + let builder = block_on(UtxoTxBuilder::new(&doge)) .add_available_inputs(unspents) .add_outputs(outputs); let (_, data) = block_on(builder.build()).unwrap(); @@ -2878,7 +3014,9 @@ fn test_tx_details_kmd_rewards() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress( + Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(), + ); let coin = utxo_coin_from_fields(fields); let tx_details = get_tx_details_eq_for_both_versions( @@ -2915,7 +3053,9 @@ fn test_tx_details_kmd_rewards_claimed_by_other() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress( + Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(), + ); let coin = utxo_coin_from_fields(fields); let tx_details = get_tx_details_eq_for_both_versions(&coin, TX_HASH); @@ -2961,7 +3101,9 @@ fn test_update_kmd_rewards() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress( + Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(), + ); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::default(); @@ -2993,7 +3135,9 @@ fn test_update_kmd_rewards_claimed_not_by_me() { ]); let mut fields = utxo_coin_fields_for_test(electrum.into(), None, false); fields.conf.ticker = "KMD".to_owned(); - fields.derivation_method = DerivationMethod::SingleAddress(Address::from("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk")); + fields.derivation_method = DerivationMethod::SingleAddress( + Address::from_legacyaddress("RMGJ9tRST45RnwEKHPGgBLuY3moSYP7Mhk", &KMD_PREFIXES).unwrap(), + ); let coin = utxo_coin_from_fields(fields); let mut input_transactions = HistoryUtxoTxMap::default(); @@ -3033,9 +3177,37 @@ fn tbch_electroncash_verbose_tx_unconfirmed() { let _: RpcTransaction = json::from_str(verbose).expect("!json::from_str"); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_withdraw_to_p2pk_fails() { + let client = NativeClient(Arc::new(NativeClientImpl::default())); + + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); + + let withdraw_req = WithdrawRequest { + amount: 1.into(), + from: None, + to: "03f8f8fa2062590ba9a0a7a86f937de22f540c015864aad35a2a9f6766de906265".to_string(), + coin: TEST_COIN_NAME.into(), + max: false, + fee: None, + memo: None, + ibc_source_channel: None, + }; + + assert!(matches!( + coin.withdraw(withdraw_req).wait().unwrap_err().into_inner(), + WithdrawError::InvalidAddress(..) + )) +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_to_p2pkh() { + let client = NativeClient(Arc::new(NativeClientImpl::default())); + + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { @@ -3045,23 +3217,27 @@ fn test_withdraw_to_p2pkh() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); - let client = NativeClient(Arc::new(NativeClientImpl::default())); - - let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); - // Create a p2pkh address for the test coin - let p2pkh_address = Address { - prefix: coin.as_ref().conf.pub_addr_prefix, - hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), - t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let p2pkh_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_pkh( + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + ) + .build() + .expect("valid address props"); let withdraw_req = WithdrawRequest { amount: 1.into(), @@ -3071,12 +3247,13 @@ fn test_withdraw_to_p2pkh() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); - let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); + let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_p2pkh(&p2pkh_address.hash); + let expected_script = Builder::build_p2pkh(p2pkh_address.hash()); assert_eq!(output_script, expected_script); } @@ -3084,6 +3261,10 @@ fn test_withdraw_to_p2pkh() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_to_p2sh() { + let client = NativeClient(Arc::new(NativeClientImpl::default())); + + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { @@ -3093,23 +3274,27 @@ fn test_withdraw_to_p2sh() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); - let client = NativeClient(Arc::new(NativeClientImpl::default())); - - let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); - // Create a p2sh address for the test coin - let p2sh_address = Address { - prefix: coin.as_ref().conf.p2sh_addr_prefix, - hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), - t_addr_prefix: coin.as_ref().conf.p2sh_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Standard, - }; + let p2sh_address = AddressBuilder::new( + UtxoAddressFormat::Standard, + *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), + coin.as_ref().conf.address_prefixes.clone(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_sh( + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + ) + .build() + .expect("valid address props"); let withdraw_req = WithdrawRequest { amount: 1.into(), @@ -3119,12 +3304,13 @@ fn test_withdraw_to_p2sh() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); - let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); + let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_p2sh(&p2sh_address.hash); + let expected_script = Builder::build_p2sh(p2sh_address.hash()); assert_eq!(output_script, expected_script); } @@ -3132,6 +3318,10 @@ fn test_withdraw_to_p2sh() { #[test] #[cfg(not(target_arch = "wasm32"))] fn test_withdraw_to_p2wpkh() { + let client = NativeClient(Arc::new(NativeClientImpl::default())); + + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, true); + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { @@ -3141,23 +3331,27 @@ fn test_withdraw_to_p2wpkh() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); - let client = NativeClient(Arc::new(NativeClientImpl::default())); - - let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, true); - // Create a p2wpkh address for the test coin - let p2wpkh_address = Address { - prefix: coin.as_ref().conf.pub_addr_prefix, - hash: coin.as_ref().derivation_method.unwrap_single_addr().hash.clone(), - t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - checksum_type: coin.as_ref().derivation_method.unwrap_single_addr().checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format: UtxoAddressFormat::Segwit, - }; + let p2wpkh_address = AddressBuilder::new( + UtxoAddressFormat::Segwit, + *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), + NetworkAddressPrefixes::default(), + coin.as_ref().conf.bech32_hrp.clone(), + ) + .as_pkh( + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + ) + .build() + .expect("valid address props"); let withdraw_req = WithdrawRequest { amount: 1.into(), @@ -3167,14 +3361,66 @@ fn test_withdraw_to_p2wpkh() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); - let transaction: UtxoTx = deserialize(tx_details.tx_hex.as_slice()).unwrap(); + let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); let output_script: Script = transaction.outputs[0].script_pubkey.clone().into(); - let expected_script = Builder::build_witness_script(&p2wpkh_address.hash); + let expected_script = Builder::build_p2wpkh(p2wpkh_address.hash()).expect("valid p2wpkh script"); + + assert_eq!(output_script, expected_script); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_withdraw_p2pk_balance() { + let client = NativeClient(Arc::new(NativeClientImpl::default())); + + let coin = utxo_coin_for_test(UtxoRpcClientEnum::Native(client), None, false); + + UtxoStandardCoin::get_unspent_ordered_list.mock_safe(|coin, _| { + let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); + let unspents = vec![UnspentInfo { + outpoint: OutPoint { + hash: 1.into(), + index: 0, + }, + value: 1000000000, + height: Default::default(), + // Use a p2pk output script for this UTXO + script: output_script_p2pk( + &block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .pubkey() + .unwrap(), + ), + }]; + MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) + }); + + // Create a dummy p2pkh address to withdraw the coins to. + let my_p2pkh_address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); + + let withdraw_req = WithdrawRequest { + amount: 1.into(), + from: None, + to: my_p2pkh_address.to_string(), + coin: TEST_COIN_NAME.into(), + max: false, + fee: None, + memo: None, + ibc_source_channel: None, + }; + let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); + let transaction: UtxoTx = deserialize(tx_details.tx.tx_hex().unwrap().as_slice()).unwrap(); + // The change should be in a p2pkh script. + let output_script: Script = transaction.outputs[1].script_pubkey.clone().into(); + let expected_script = Builder::build_p2pkh(my_p2pkh_address.hash()); assert_eq!(output_script, expected_script); + + // And it should have this value (p2pk balance - amount sent - fees). + assert_eq!(transaction.outputs[1].value, 899999000); } /// `UtxoStandardCoin` has to check UTXO maturity if `check_utxo_maturity` is `true`. @@ -3203,7 +3449,7 @@ fn test_utxo_standard_with_check_utxo_maturity_true() { let priv_key = Secp256k1Secret::from([1; 32]); let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "RICK", &conf, ¶ms, priv_key)).unwrap(); - let address = Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"); + let address = Address::from_legacyaddress("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", &KMD_PREFIXES).unwrap(); // Don't use `block_on` here because it's used within a mock of [`GetUtxoListOps::get_mature_unspent_ordered_list`]. coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); assert!(unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED }); @@ -3239,7 +3485,7 @@ fn test_utxo_standard_without_check_utxo_maturity() { let priv_key = Secp256k1Secret::from([1; 32]); let coin = block_on(utxo_standard_coin_with_priv_key(&ctx, "RICK", &conf, ¶ms, priv_key)).unwrap(); - let address = Address::from("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW"); + let address = Address::from_legacyaddress("R9o9xTocqr6CeEDGDH6mEYpwLoMz6jNjMW", &KMD_PREFIXES).unwrap(); // Don't use `block_on` here because it's used within a mock of [`UtxoStandardCoin::get_all_unspent_ordered_list`]. coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); assert!(unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED }); @@ -3274,7 +3520,11 @@ fn test_qtum_without_check_utxo_maturity() { let priv_key = Secp256k1Secret::from([1; 32]); let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); - let address = Address::from("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); // Don't use `block_on` here because it's used within a mock of [`QtumCoin::get_mature_unspent_ordered_list`]. coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); assert!(unsafe { GET_MATURE_UNSPENT_ORDERED_LIST_CALLED }); @@ -3316,10 +3566,10 @@ fn test_split_qtum() { let ctx = MmCtxBuilder::new().into_mm_arc(); let params = UtxoActivationParams::from_legacy_req(&req).unwrap(); let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); - let p2pkh_address = coin.as_ref().derivation_method.unwrap_single_addr(); - let script: Script = output_script(p2pkh_address, ScriptType::P2PKH); + let p2pkh_address = block_on(coin.as_ref().derivation_method.unwrap_single_addr()); + let script: Script = output_script(&p2pkh_address).expect("valid previous script must be built"); let key_pair = coin.as_ref().priv_key_policy.activated_key_or_err().unwrap(); - let (unspents, _) = block_on(coin.get_mature_unspent_ordered_list(p2pkh_address)).expect("Unspent list is empty"); + let (unspents, _) = block_on(coin.get_mature_unspent_ordered_list(&p2pkh_address)).expect("Unspent list is empty"); log!("Mature unspents vec = {:?}", unspents.mature); let outputs = vec![ TransactionOutput { @@ -3328,26 +3578,18 @@ fn test_split_qtum() { }; 40 ]; - let builder = UtxoTxBuilder::new(&coin) + let builder = block_on(UtxoTxBuilder::new(&coin)) .add_available_inputs(unspents.mature) .add_outputs(outputs); let (unsigned, data) = block_on(builder.build()).unwrap(); // fee_amount must be higher than the minimum fee assert!(data.fee_amount > 400_000); log!("Unsigned tx = {:?}", unsigned); - let signature_version = match p2pkh_address.addr_format { + let signature_version = match p2pkh_address.addr_format() { UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, _ => coin.as_ref().conf.signature_version, }; - let prev_script = Builder::build_p2pkh(&p2pkh_address.hash); - let signed = sign_tx( - unsigned, - key_pair, - prev_script, - signature_version, - coin.as_ref().conf.fork_id, - ) - .unwrap(); + let signed = sign_tx(unsigned, key_pair, signature_version, coin.as_ref().conf.fork_id).unwrap(); log!("Signed tx = {:?}", signed); let res = block_on(coin.broadcast_tx(&signed)).unwrap(); log!("Res = {:?}", res); @@ -3389,7 +3631,11 @@ fn test_qtum_with_check_utxo_maturity_false() { let priv_key = Secp256k1Secret::from([1; 32]); let coin = block_on(qtum_coin_with_priv_key(&ctx, "QTUM", &conf, ¶ms, priv_key)).unwrap(); - let address = Address::from("qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE"); + let address = Address::from_legacyaddress( + "qcyBHeSct7Wr4mAw18iuQ1zW5mMFYmtmBE", + &coin.as_ref().conf.address_prefixes, + ) + .unwrap(); // Don't use `block_on` here because it's used within a mock of [`QtumCoin::get_all_unspent_ordered_list`]. coin.get_unspent_ordered_list(&address).compat().wait().unwrap(); assert!(unsafe { GET_ALL_UNSPENT_ORDERED_LIST_CALLED }); @@ -3398,7 +3644,7 @@ fn test_qtum_with_check_utxo_maturity_false() { #[test] fn test_account_balance_rpc() { let mut addresses_map: HashMap = HashMap::new(); - let mut balances_by_der_path: HashMap = HashMap::new(); + let mut balances_by_der_path: HashMap> = HashMap::new(); macro_rules! known_address { ($der_path:literal, $address:literal, $chain:expr, balance = $balance:literal) => { @@ -3458,7 +3704,7 @@ fn test_account_balance_rpc() { hd_accounts.insert(0, UtxoHDAccount { account_id: 0, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/0'").unwrap(), external_addresses_number: 7, internal_addresses_number: 3, derived_addresses: HDAddressesCache::default(), @@ -3466,18 +3712,21 @@ fn test_account_balance_rpc() { hd_accounts.insert(1, UtxoHDAccount { account_id: 1, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPQq2FdGT6JoieiQZUpTZ3WZn8fcuLJhFVmtCpXbuXxp5aPzaokwcLV2V9LE55Dwt8JYkpuMv7jXKwmyD28WbHYjBH2zhbW2p").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/1'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/1'").unwrap(), external_addresses_number: 0, internal_addresses_number: 1, derived_addresses: HDAddressesCache::default(), }); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { - hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), - hd_wallet_storage: HDWalletCoinStorage::default(), + inner: HDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + derivation_path: HDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + enabled_address: HDPathAccountToAddressId::default(), + gap_limit: 3, + }, address_format: UtxoAddressFormat::Standard, - derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), - accounts: HDAccountsMutex::new(hd_accounts), - gap_limit: 3, }); let coin = utxo_coin_from_fields(fields); @@ -3690,7 +3939,7 @@ fn test_scan_for_new_addresses() { // The list of addresses with a non-empty transaction history. let mut non_empty_addresses: HashSet = HashSet::new(); // The map of results by the addresses. - let mut balances_by_der_path: HashMap = HashMap::new(); + let mut balances_by_der_path: HashMap> = HashMap::new(); macro_rules! new_address { ($der_path:literal, $address:literal, $chain:expr, balance = $balance:expr) => {{ @@ -3781,11 +4030,13 @@ fn test_scan_for_new_addresses() { let client = NativeClient(Arc::new(NativeClientImpl::default())); let mut fields = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); + let ctx = MmCtxBuilder::new().into_mm_arc(); + fields.ctx = ctx.weak(); let mut hd_accounts = HDAccountsMap::new(); hd_accounts.insert(0, UtxoHDAccount { account_id: 0, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/0'").unwrap(), external_addresses_number: 3, internal_addresses_number: 1, derived_addresses: HDAddressesCache::default(), @@ -3793,18 +4044,21 @@ fn test_scan_for_new_addresses() { hd_accounts.insert(1, UtxoHDAccount { account_id: 1, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPQq2FdGT6JoieiQZUpTZ3WZn8fcuLJhFVmtCpXbuXxp5aPzaokwcLV2V9LE55Dwt8JYkpuMv7jXKwmyD28WbHYjBH2zhbW2p").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/1'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/1'").unwrap(), external_addresses_number: 0, internal_addresses_number: 2, derived_addresses: HDAddressesCache::default(), }); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { - hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), - hd_wallet_storage: HDWalletCoinStorage::default(), + inner: HDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + derivation_path: HDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + enabled_address: HDPathAccountToAddressId::default(), + gap_limit: 3, + }, address_format: UtxoAddressFormat::Standard, - derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), - accounts: HDAccountsMutex::new(hd_accounts), - gap_limit: 3, }); let coin = utxo_coin_from_fields(fields); @@ -3860,7 +4114,7 @@ fn test_scan_for_new_addresses() { assert_eq!(actual, expected); let accounts = match coin.as_ref().derivation_method { - DerivationMethod::HDWallet(UtxoHDWallet { ref accounts, .. }) => block_on(accounts.lock()).clone(), + DerivationMethod::HDWallet(UtxoHDWallet { ref inner, .. }) => block_on(inner.accounts.lock()).clone(), _ => unreachable!(), }; assert_eq!(accounts[&0].external_addresses_number, 4); @@ -3911,7 +4165,7 @@ fn test_get_new_address() { } }); - MockableConfirmAddress::confirm_utxo_address + MockableConfirmAddress::confirm_address .mock_safe(move |_, _, _, _| MockResult::Return(Box::pin(futures::future::ok(())))); // This mock is required just not to fail on [`UtxoAddressScanner::init`]. @@ -3920,11 +4174,13 @@ fn test_get_new_address() { let client = NativeClient(Arc::new(NativeClientImpl::default())); let mut fields = utxo_coin_fields_for_test(UtxoRpcClientEnum::Native(client), None, false); + let ctx = MmCtxBuilder::new().into_mm_arc(); + fields.ctx = ctx.weak(); let mut hd_accounts = HDAccountsMap::new(); let hd_account_for_test = UtxoHDAccount { account_id: 0, extended_pubkey: Secp256k1ExtendedPublicKey::from_str("xpub6DEHSksajpRPM59RPw7Eg6PKdU7E2ehxJWtYdrfQ6JFmMGBsrR6jA78ANCLgzKYm4s5UqQ4ydLEYPbh3TRVvn5oAZVtWfi4qJLMntpZ8uGJ").unwrap(), - account_derivation_path: StandardHDPathToAccount::from_str("m/44'/141'/0'").unwrap(), + account_derivation_path: HDPathToAccount::from_str("m/44'/141'/0'").unwrap(), external_addresses_number: 4, internal_addresses_number: 0, derived_addresses: HDAddressesCache::default(), @@ -3936,19 +4192,22 @@ fn test_get_new_address() { hd_accounts.insert(2, hd_account_for_test); fields.derivation_method = DerivationMethod::HDWallet(UtxoHDWallet { - hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), - hd_wallet_storage: HDWalletCoinStorage::default(), + inner: HDWallet { + hd_wallet_rmd160: "21605444b36ec72780bdf52a5ffbc18288893664".into(), + hd_wallet_storage: HDWalletCoinStorage::default(), + derivation_path: HDPathToCoin::from_str("m/44'/141'").unwrap(), + accounts: HDAccountsMutex::new(hd_accounts), + enabled_address: HDPathAccountToAddressId::default(), + gap_limit: 2, + }, address_format: UtxoAddressFormat::Standard, - derivation_path: StandardHDPathToCoin::from_str("m/44'/141'").unwrap(), - accounts: HDAccountsMutex::new(hd_accounts), - gap_limit: 2, }); fields.conf.trezor_coin = Some("Komodo".to_string()); let coin = utxo_coin_from_fields(fields); // ======= - let confirm_address = MockableConfirmAddress::default(); + let confirm_address = MockableConfirmAddress; expected_checked_addresses!["m/44'/141'/0'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1"]; non_empty_addresses!["m/44'/141'/0'/0/3", "RU1gRFXWXNx7uPRAEJ7wdZAW1RZ4TE6Vv1"]; @@ -4047,9 +4306,9 @@ fn test_get_new_address() { block_on(coin.get_new_address_rpc(params, &confirm_address)).unwrap(); unsafe { assert_eq!(CHECKED_ADDRESSES, EXPECTED_CHECKED_ADDRESSES) }; - // Check if `get_new_address_rpc` fails on the `HDAddressConfirm::confirm_utxo_address` error. + // Check if `get_new_address_rpc` fails on the `HDAddressConfirm::confirm_address` error. - MockableConfirmAddress::confirm_utxo_address.mock_safe(move |_, _, _, _| { + MockableConfirmAddress::confirm_address.mock_safe(move |_, _, _, _| { MockResult::Return(Box::pin(futures::future::ready(MmError::err( HDConfirmAddressError::HwContextNotInitialized, )))) @@ -4144,10 +4403,10 @@ fn test_native_display_balances() { let rpc_client = native_client_for_test(); let addresses = vec![ - "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), - "RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), - "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), - "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + Address::from_legacyaddress("RG278CfeNPFtNztFZQir8cgdWexVhViYVy", &KMD_PREFIXES).unwrap(), + Address::from_legacyaddress("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN", &KMD_PREFIXES).unwrap(), + Address::from_legacyaddress("RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi", &KMD_PREFIXES).unwrap(), + Address::from_legacyaddress("RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF", &KMD_PREFIXES).unwrap(), ]; let actual = rpc_client .display_balances(addresses, TEST_COIN_DECIMALS) @@ -4156,16 +4415,19 @@ fn test_native_display_balances() { let expected: Vec<(Address, BigDecimal)> = vec![ ( - "RG278CfeNPFtNztFZQir8cgdWexVhViYVy".into(), + Address::from_legacyaddress("RG278CfeNPFtNztFZQir8cgdWexVhViYVy", &KMD_PREFIXES).unwrap(), BigDecimal::try_from(5.77699).unwrap(), ), - ("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN".into(), BigDecimal::from(0)), ( - "RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi".into(), + Address::from_legacyaddress("RYPz6Lr4muj4gcFzpMdv3ks1NCGn3mkDPN", &KMD_PREFIXES).unwrap(), + BigDecimal::from(0), + ), + ( + Address::from_legacyaddress("RJeDDtDRtKUoL8BCKdH7TNCHqUKr7kQRsi", &KMD_PREFIXES).unwrap(), BigDecimal::try_from(0.77699).unwrap(), ), ( - "RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF".into(), + Address::from_legacyaddress("RQHn9VPHBqNjYwyKfJbZCiaxVrWPKGQjeF", &KMD_PREFIXES).unwrap(), BigDecimal::try_from(0.99998).unwrap(), ), ]; @@ -4224,7 +4486,7 @@ fn test_sign_verify_message_segwit() { ); let is_valid = coin - .verify_message(&signature, message, "rck1qqk4t2dppvmu9jja0z7nan0h464n5gve8h7nhay") + .verify_message(&signature, message, "doc1qqk4t2dppvmu9jja0z7nan0h464n5gve8z592yd") .unwrap(); assert!(is_valid); @@ -4249,15 +4511,12 @@ fn test_tx_enum_from_bytes() { coin.tx_enum_from_bytes(&tx_hex).unwrap(); let err = coin.tx_enum_from_bytes(&vec![0; 1000000]).unwrap_err().into_inner(); - assert_eq!( - discriminant(&err), - discriminant(&TxMarshalingErr::CrossCheckFailed(String::new())) - ); + assert!(matches!(err, TxMarshalingErr::CrossCheckFailed(_))); } #[test] fn test_hd_utxo_tx_history() { - let client = electrum_client_for_test(MORTY_ELECTRUM_ADDRS); + let client = electrum_client_for_test(DOC_ELECTRUM_ADDRS); block_on(utxo_common_tests::test_hd_utxo_tx_history_impl(client)); } @@ -4467,6 +4726,7 @@ fn test_spv_conf_with_verification() { .contains("max_stored_block_headers 2000 must be greater than retargeting interval")); } +#[cfg(not(target_arch = "wasm32"))] fn rick_blocker_5() -> BlockHeader { let header = "0400000028a4f1aa8be606c8bf8195b2e95d478a83314ff9ad7b017457d9e58d00d1710bb43f41db65677e3fdb83ddbd8cfb4a7ad2e110f74bc19726dc949576e003a1ecfbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e381b405d0f0f0f2001003cfb15008ad9f4fab1ff4076f8919f743193f007c0db28f5106e003b0000fd400500acba878991f600ed8c022758be9ff9752ef175e7530324df4d1b87f5a03ca5c2c3fce10b08743bd5ba03912703b8f305f7dd382487d437d9b1823cdc11a00f59a20b235ef57502a0a7ad6fc7d3d242e8f4477a01fb8834ac4dc6e2e40e4909f9edc0db07c0f98df40e5a61327311b005c98a727694ebaabcb366b92dda4af9e3f6e72c5461dd81d6daccbd1fca8ec17597df7585947b54deb83554859776b5bcefadfa566ff12c04ac624f9416e76beccec35694ae0ed11dc17a911f114225be62cf5b971628f364f57d8348d95fdc415b0d2a7a477ea130d3320108739edf761f85f81efd6c0e4eafa8166b05bd74af7928b0786b63ae499dba38065be13e7541b7f4e26727d0fa6887e265e09709b940ca87295ce5984de7d4058b5d340b162935fa46ee20cac955379e3c8fa1ff92fb354bb2a0fedf697b683a5875f4ed2bcef984d296b0c1e07a52920f1dd5a60140c7c1245a52ed196df3292db8bfff52923b0a8615b6a99a5fcf1e5f461f01a04b1c3bb517fe16553e1f8e8aa20bd3cc2cac6d3242a2ce373737b57cec4637907fd236e0d44d91d59533484ec23634b93645c10a858d83805d731f300aa27a162e172216d7fc21170b4d232767e4c66f9a871224f13480e89c2edb0e6e1ef5cf75d9203839cc0282fd7852319232057f30793bb5552d94ebf3ffcc67b73f44e80c3de79b9d8d7f0175939722054bc2ddfb84288dff8c7554f191d6ee1b65c40b75d4435712d4e88c64d6379ab7e578bcd8117501504faa7a3be3a6a2826fd7a3e5e9efb1d3642937f3a35be5793be8e1d4acf9dd2dcd356d6e4c7d0c8b87587b8ad901b9ce71792ae0bdae27811b52300e6809e4691bfc7f738252e7c197e228cce5fda6130f8f518e5059530b731fe8afbf51308aa8da3bd31b1d1eb22cca1a896aed281397925265cd861a7eadb80124363dec8cb508aea7c277f04b9841888dd932471349e651ce2622a59065932f463ffce6b19a975d6914336ab49394afd17dfb9a448157007ea1437b1483587bc7de0dec5103cafad76704e91e9ea2b0b9a8570b935d5c65478e7195b08161be4625b8d5fd3658e6164cf2d6898ecbf1f14945fdd75bb991a3d9ffac713a3a7a81a31a765b9c37a578976aa15e66c97c957f4651dc5fc492c2111d8724d375a8293a36e0ddcf2a01facf30401d8677611522882e1447e4c8be5fa9ad073fb3fdcc6f673981484089090fe4c05bfaae173503e0f99c7407b297852d216463924d365d26b4cd63401a46bd7ed969ddb235044eb2373645144976c7f713720c0238ade9d3aae1d2b153e82d093232d4b12b2108ec564ae0e855e09252f1434c28d90bb298ab6d1750498bf90d93c8797901911548b81af1ba185be52c0dff9c1b11812941d2d527c95c4382879298f364077710b5efd56d1bf39148aedc4fcd9e8bddb4c36a3f901dc11f9493d1fbdfe80c88fa8866c1465c939c0d71cb57e78822b5fc3023578aa2d6b9cd3ebaa54f22876b935f251183d8a68459cab30cd19bcb4e4c1e1a5a83e4687a4795dc23732e81b9f024f70db96e412831d26e61d4fa292a95648e0b614d9a148cd852df1bf26a34ea971e63f8c634133ab7b13ac8045f6d6e20af2313b38d12cb8cee54a7aba7a7cd7e8b1b5e0b0931d4665a0bb36b63f325161b571fdd4f159f470e443e9b0cfb193bf4eea5fa9715dc6132cb8ed97f7f097837471a5147d14f2066cd3dcd50460d70180a7a24e2b5b9ab20caf952d2ea1b51747afec975f76d0313a98e444f20938bf709530960f9fbf5af9857cbe3410d37f3cba10ff57642861586b7c1b1c57019602f1529df9d6e45ca2f7663519c58915e9e299d5beee73cb4553238566844f571374d3f6a247dd8ecbbc893"; @@ -4474,6 +4734,7 @@ fn rick_blocker_5() -> BlockHeader { BlockHeader::try_from_string_with_coin_variant(header.to_string(), "RICK".into()).unwrap() } +#[cfg(not(target_arch = "wasm32"))] #[test] fn test_block_header_utxo_loop_with_reorg() { use crate::utxo::utxo_builder::{block_header_utxo_loop, BlockHeaderUtxoLoopExtraArgs}; diff --git a/mm2src/coins/utxo/utxo_tx_history_v2.rs b/mm2src/coins/utxo/utxo_tx_history_v2.rs index 29861a23dc..698a9bf0b6 100644 --- a/mm2src/coins/utxo/utxo_tx_history_v2.rs +++ b/mm2src/coins/utxo/utxo_tx_history_v2.rs @@ -5,8 +5,9 @@ use crate::tx_history_storage::FilteringAddresses; use crate::utxo::bch::BchCoin; use crate::utxo::slp::ParseSlpScriptError; use crate::utxo::{utxo_common, AddrFromStrError, GetBlockHeaderError}; -use crate::{BalanceError, BalanceResult, BlockHeightAndTime, HistorySyncState, MarketCoinOps, NumConversError, - ParseBigDecimalError, TransactionDetails, UnexpectedDerivationMethod, UtxoRpcError, UtxoTx}; +use crate::{BalanceError, BalanceResult, BlockHeightAndTime, CoinWithDerivationMethod, HistorySyncState, + MarketCoinOps, NumConversError, ParseBigDecimalError, TransactionDetails, UnexpectedDerivationMethod, + UtxoRpcError, UtxoTx}; use async_trait::async_trait; use common::executor::Timer; use common::log::{error, info}; @@ -102,7 +103,9 @@ pub struct UtxoTxDetailsParams<'a, Storage> { } #[async_trait] -pub trait UtxoTxHistoryOps: CoinWithTxHistoryV2 + MarketCoinOps + Send + Sync + 'static { +pub trait UtxoTxHistoryOps: + CoinWithTxHistoryV2 + CoinWithDerivationMethod + MarketCoinOps + Send + Sync + 'static +{ /// Returns addresses for those we need to request Transaction history. async fn my_addresses(&self) -> MmResult, UtxoMyAddressesHistoryError>; @@ -511,7 +514,9 @@ where let txs_with_height: HashMap = self.all_tx_ids_with_height.clone().into_iter().collect(); for mut tx in unconfirmed { - let found = match H256Json::from_str(&tx.tx_hash) { + let Some(tx_hash) = tx.tx.tx_hash() else { continue }; + + let found = match H256Json::from_str(tx_hash) { Ok(unconfirmed_tx_hash) => txs_with_height.get(&unconfirmed_tx_hash), Err(_) => None, }; diff --git a/mm2src/coins/utxo/utxo_wasm_tests.rs b/mm2src/coins/utxo/utxo_wasm_tests.rs index 35902f4ff0..a33e1ba039 100644 --- a/mm2src/coins/utxo/utxo_wasm_tests.rs +++ b/mm2src/coins/utxo/utxo_wasm_tests.rs @@ -11,7 +11,7 @@ use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); -const TEST_COIN_NAME: &str = "RICK"; +const TEST_COIN_NAME: &str = "DOC"; pub async fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let ctx = MmCtxBuilder::default().into_mm_arc(); @@ -41,7 +41,10 @@ pub async fn electrum_client_for_test(servers: &[&str]) -> ElectrumClient { let servers = servers.into_iter().map(|s| json::from_value(s).unwrap()).collect(); let abortable_system = AbortableQueue::default(); - builder.electrum_client(abortable_system, args, servers).await.unwrap() + builder + .electrum_client(abortable_system, args, servers, None) + .await + .unwrap() } #[wasm_bindgen_test] @@ -70,6 +73,6 @@ async fn test_electrum_display_balances() { #[wasm_bindgen_test] async fn test_hd_utxo_tx_history() { - let rpc_client = electrum_client_for_test(&["electrum1.cipig.net:30018", "electrum2.cipig.net:30018"]).await; + let rpc_client = electrum_client_for_test(DOC_ELECTRUM_ADDRS).await; utxo_common_tests::test_hd_utxo_tx_history_impl(rpc_client).await; } diff --git a/mm2src/coins/utxo/utxo_withdraw.rs b/mm2src/coins/utxo/utxo_withdraw.rs index f51726c6c4..8721a6a433 100644 --- a/mm2src/coins/utxo/utxo_withdraw.rs +++ b/mm2src/coins/utxo/utxo_withdraw.rs @@ -1,23 +1,26 @@ -use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandle}; +use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; use crate::utxo::utxo_common::{big_decimal_from_sat, UtxoTxBuilder}; use crate::utxo::{output_script, sat_from_big_decimal, ActualTxFee, Address, FeePolicy, GetUtxoListOps, PrivKeyPolicy, UtxoAddressFormat, UtxoCoinFields, UtxoCommonOps, UtxoFeeDetails, UtxoTx, UTXO_LOCK}; -use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionDetails, WithdrawError, - WithdrawFee, WithdrawFrom, WithdrawRequest, WithdrawResult}; +use crate::{CoinWithDerivationMethod, GetWithdrawSenderAddress, MarketCoinOps, TransactionData, TransactionDetails, + WithdrawError, WithdrawFee, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use chain::TransactionOutput; use common::log::info; use common::now_sec; +use crypto::hw_rpc_task::HwRpcTaskAwaitingStatus; +use crypto::trezor::trezor_rpc_task::{TrezorRequestStatuses, TrezorRpcTaskProcessor}; use crypto::trezor::{TrezorError, TrezorProcessingError}; use crypto::{from_hw_error, CryptoCtx, CryptoCtxError, DerivationPath, HwError, HwProcessingError, HwRpcError}; -use keys::{AddressHashEnum, KeyPair, Private, Public as PublicKey, Type as ScriptType}; +use keys::{AddressFormat, KeyPair, Private, Public as PublicKey}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use rpc::v1::types::ToTxHash; use rpc_task::RpcTaskError; -use script::{Builder, Script, SignatureVersion, TransactionInputSigner}; +use script::{SignatureVersion, TransactionInputSigner}; use serialization::{serialize, serialize_with_flags, SERIALIZE_TRANSACTION_WITNESS}; use std::iter::once; +use std::sync::Arc; use utxo_signer::sign_params::{OutputDestination, SendingOutputInfo, SpendingInputInfo, UtxoSignTxParamsBuilder}; use utxo_signer::{with_key_pair, UtxoSignTxError}; use utxo_signer::{SignPolicy, UtxoSignerOps}; @@ -38,6 +41,7 @@ impl From> for WithdrawError { match e { HwProcessingError::HwError(hw) => WithdrawError::from(hw), HwProcessingError::ProcessorError(rpc_task) => WithdrawError::from(rpc_task), + HwProcessingError::InternalError(err) => WithdrawError::InternalError(err), } } } @@ -83,6 +87,10 @@ impl From for WithdrawError { } } +impl From for WithdrawError { + fn from(e: keys::Error) -> Self { WithdrawError::InternalError(e.to_string()) } +} + #[async_trait] pub trait UtxoWithdraw where @@ -98,14 +106,14 @@ where fn request(&self) -> &WithdrawRequest; fn signature_version(&self) -> SignatureVersion { - match self.sender_address().addr_format { + match self.sender_address().addr_format() { UtxoAddressFormat::Segwit => SignatureVersion::WitnessV0, - _ => self.coin().as_ref().conf.signature_version, + UtxoAddressFormat::Standard | UtxoAddressFormat::CashAddress { .. } => { + self.coin().as_ref().conf.signature_version + }, } } - fn prev_script(&self) -> Script { Builder::build_p2pkh(&self.sender_address().hash) } - #[allow(clippy::result_large_err)] fn on_generating_transaction(&self) -> Result<(), MmError>; @@ -118,26 +126,14 @@ where let coin = self.coin(); let ticker = coin.as_ref().conf.ticker.clone(); let decimals = coin.as_ref().decimals; - let conf = &self.coin().as_ref().conf; let req = self.request(); let to = coin.address_from_str(&req.to)?; - let is_p2pkh = to.prefix == conf.pub_addr_prefix && to.t_addr_prefix == conf.pub_t_addr_prefix; - let is_p2sh = to.prefix == conf.p2sh_addr_prefix && to.t_addr_prefix == conf.p2sh_t_addr_prefix; - - let script_type = if is_p2pkh { - ScriptType::P2PKH - } else if is_p2sh { - ScriptType::P2SH - } else { - return MmError::err(WithdrawError::InvalidAddress("Expected either P2PKH or P2SH".into())); - }; - // Generate unsigned transaction. self.on_generating_transaction()?; - let script_pubkey = output_script(&to, script_type).to_bytes(); + let script_pubkey = output_script(&to).map(|script| script.to_bytes())?; let _utxo_lock = UTXO_LOCK.lock().await; let (unspents, _) = coin.get_unspent_ordered_list(&self.sender_address()).await?; @@ -153,6 +149,7 @@ where let outputs = vec![TransactionOutput { value, script_pubkey }]; let mut tx_builder = UtxoTxBuilder::new(coin) + .await .with_from_address(self.sender_address()) .add_available_inputs(unspents) .add_outputs(outputs) @@ -203,8 +200,7 @@ where spent_by_me: big_decimal_from_sat(data.spent_by_me as i64, decimals), received_by_me: big_decimal_from_sat(data.received_by_me as i64, decimals), my_balance_change: big_decimal_from_sat(data.received_by_me as i64 - data.spent_by_me as i64, decimals), - tx_hash: signed.hash().reversed().to_vec().to_tx_hash(), - tx_hex, + tx: TransactionData::new_signed(tx_hex, signed.hash().reversed().to_vec().to_tx_hash()), fee_details: Some(fee_details.into()), block_height: 0, coin: ticker, @@ -217,10 +213,10 @@ where } } -pub struct InitUtxoWithdraw<'a, Coin> { +pub struct InitUtxoWithdraw { ctx: MmArc, coin: Coin, - task_handle: &'a WithdrawTaskHandle, + task_handle: WithdrawTaskHandleShared, req: WithdrawRequest, from_address: Address, /// Displayed [`InitUtxoWithdraw::from_address`]. @@ -232,7 +228,7 @@ pub struct InitUtxoWithdraw<'a, Coin> { } #[async_trait] -impl<'a, Coin> UtxoWithdraw for InitUtxoWithdraw<'a, Coin> +impl UtxoWithdraw for InitUtxoWithdraw where Coin: UtxoCommonOps + GetUtxoListOps + UtxoSignerOps, { @@ -275,11 +271,21 @@ where let mut sign_params = UtxoSignTxParamsBuilder::new(); // TODO refactor [`UtxoTxBuilder::build`] to return `SpendingInputInfo` and `SendingOutputInfo` within `AdditionalTxData`. - sign_params.add_inputs_infos(unsigned_tx.inputs.iter().map(|_input| SpendingInputInfo::P2PKH { - address_derivation_path: self.from_derivation_path.clone(), - address_pubkey: self.from_pubkey, - })); - + sign_params.add_inputs_infos( + unsigned_tx + .inputs + .iter() + .map(|_input| match self.from_address.addr_format() { + AddressFormat::Segwit => SpendingInputInfo::P2WPKH { + address_derivation_path: self.from_derivation_path.clone(), + address_pubkey: self.from_pubkey, + }, + AddressFormat::Standard | AddressFormat::CashAddress { .. } => SpendingInputInfo::P2PKH { + address_derivation_path: self.from_derivation_path.clone(), + address_pubkey: self.from_pubkey, + }, + }), + ); sign_params.add_outputs_infos(once(SendingOutputInfo { destination_address: OutputDestination::plain(self.req.to.clone()), })); @@ -289,7 +295,10 @@ where // There is a change output. 2 => { sign_params.add_outputs_infos(once(SendingOutputInfo { - destination_address: OutputDestination::change(self.from_derivation_path.clone()), + destination_address: OutputDestination::change( + self.from_derivation_path.clone(), + self.from_address.addr_format().clone(), + ), })); }, unexpected => { @@ -300,8 +309,7 @@ where sign_params .with_signature_version(self.signature_version()) - .with_unsigned_tx(unsigned_tx) - .with_prev_script(Builder::build_p2pkh(&self.from_address.hash)); + .with_unsigned_tx(unsigned_tx); let sign_params = sign_params.build()?; let crypto_ctx = CryptoCtx::from_ctx(&self.ctx)?; @@ -317,7 +325,15 @@ where .. } => SignPolicy::WithKeyPair(activated_key_pair), PrivKeyPolicy::Trezor => { - let trezor_session = hw_ctx.trezor().await?; + let trezor_statuses = TrezorRequestStatuses { + on_button_request: WithdrawInProgressStatus::FollowHwDeviceInstructions, + on_pin_request: HwRpcTaskAwaitingStatus::EnterTrezorPin, + on_passphrase_request: HwRpcTaskAwaitingStatus::EnterTrezorPassphrase, + on_ready: WithdrawInProgressStatus::FollowHwDeviceInstructions, + }; + let sign_processor = TrezorRpcTaskProcessor::new(self.task_handle.clone(), trezor_statuses); + let sign_processor = Arc::new(sign_processor); + let trezor_session = hw_ctx.trezor(sign_processor).await?; SignPolicy::WithTrezor(trezor_session) }, #[cfg(target_arch = "wasm32")] @@ -336,13 +352,13 @@ where } } -impl<'a, Coin> InitUtxoWithdraw<'a, Coin> { +impl InitUtxoWithdraw { pub async fn new( ctx: MmArc, coin: Coin, req: WithdrawRequest, - task_handle: &'a WithdrawTaskHandle, - ) -> Result, MmError> + task_handle: WithdrawTaskHandleShared, + ) -> Result, MmError> where Coin: CoinWithDerivationMethod + GetWithdrawSenderAddress
, { @@ -363,7 +379,7 @@ impl<'a, Coin> InitUtxoWithdraw<'a, Coin> { Ok(InitUtxoWithdraw { ctx, coin, - task_handle, + task_handle: task_handle.clone(), req, from_address: from.address, from_address_string, @@ -377,8 +393,8 @@ pub struct StandardUtxoWithdraw { coin: Coin, req: WithdrawRequest, key_pair: KeyPair, - my_address: Address, - my_address_string: String, + from_address: Address, + from_address_string: String, } #[async_trait] @@ -388,9 +404,9 @@ where { fn coin(&self) -> &Coin { &self.coin } - fn sender_address(&self) -> Address { self.my_address.clone() } + fn sender_address(&self) -> Address { self.from_address.clone() } - fn sender_address_string(&self) -> String { self.my_address_string.clone() } + fn sender_address_string(&self) -> String { self.from_address_string.clone() } fn request(&self) -> &WithdrawRequest { &self.req } @@ -402,7 +418,6 @@ where Ok(with_key_pair::sign_tx( unsigned_tx, &self.key_pair, - self.prev_script(), self.signature_version(), self.coin.as_ref().conf.fork_id, )?) @@ -411,58 +426,44 @@ where impl StandardUtxoWithdraw where - Coin: AsRef + MarketCoinOps, + Coin: AsRef + + MarketCoinOps + + CoinWithDerivationMethod + + GetWithdrawSenderAddress
, { #[allow(clippy::result_large_err)] - pub fn new(coin: Coin, req: WithdrawRequest) -> Result> { - let (key_pair, my_address) = match req.from { - Some(WithdrawFrom::HDWalletAddress(ref path_to_address)) => { + pub async fn new(coin: Coin, req: WithdrawRequest) -> Result> { + let from = coin.get_withdraw_sender_address(&req).await?; + let from_address_string = from.address.display_address().map_to_mm(WithdrawError::InternalError)?; + + let key_pair = match from.derivation_path { + Some(der_path) => { let secret = coin .as_ref() .priv_key_policy - .hd_wallet_derived_priv_key_or_err(path_to_address)?; + .hd_wallet_derived_priv_key_or_err(&der_path)?; let private = Private { prefix: coin.as_ref().conf.wif_prefix, secret, compressed: true, checksum_type: coin.as_ref().conf.checksum_type, }; - let key_pair = - KeyPair::from_private(private).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let addr_format = coin - .as_ref() - .derivation_method - .single_addr_or_err()? - .clone() - .addr_format; - let my_address = Address { - prefix: coin.as_ref().conf.pub_addr_prefix, - t_addr_prefix: coin.as_ref().conf.pub_t_addr_prefix, - hash: AddressHashEnum::AddressHash(key_pair.public().address_hash()), - checksum_type: coin.as_ref().conf.checksum_type, - hrp: coin.as_ref().conf.bech32_hrp.clone(), - addr_format, - }; - (key_pair, my_address) - }, - Some(WithdrawFrom::AddressId(_)) | Some(WithdrawFrom::DerivationPath { .. }) => { - return MmError::err(WithdrawError::UnsupportedError( - "Only `WithdrawFrom::HDWalletAddress` is supported for `StandardUtxoWithdraw`".to_string(), - )) + KeyPair::from_private(private).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))? }, - None => { - let key_pair = coin.as_ref().priv_key_policy.activated_key_or_err()?; - let my_address = coin.as_ref().derivation_method.single_addr_or_err()?.clone(); - (*key_pair, my_address) + // [`WithdrawSenderAddress::derivation_path`] is not set, but the coin is initialized with an HD wallet derivation method. + None if coin.has_hd_wallet_derivation_method() => { + let error = "Cannot determine 'from' address derivation path".to_owned(); + return MmError::err(WithdrawError::UnexpectedFromAddress(error)); }, + None => *coin.as_ref().priv_key_policy.activated_key_or_err()?, }; - let my_address_string = my_address.display_address().map_to_mm(WithdrawError::InternalError)?; + Ok(StandardUtxoWithdraw { coin, req, key_pair, - my_address, - my_address_string, + from_address: from.address, + from_address_string, }) } } diff --git a/mm2src/coins/utxo_signer/src/lib.rs b/mm2src/coins/utxo_signer/src/lib.rs index 9285bf8587..8032e9acf7 100644 --- a/mm2src/coins/utxo_signer/src/lib.rs +++ b/mm2src/coins/utxo_signer/src/lib.rs @@ -55,6 +55,11 @@ pub enum UtxoSignTxError { script: Script, prev_script: Script, }, + #[display( + fmt = "Can't spend the UTXO with script = '{}'. This script format isn't supported", + script + )] + UnspendableUTXO { script: Script }, #[display(fmt = "Transport error: {}", _0)] Transport(String), #[display(fmt = "Internal error: {}", _0)] @@ -82,6 +87,7 @@ impl From for UtxoSignTxError { // that are expected to be checked by [`sign_common::UtxoSignTxParamsBuilder::build`] already. // So if this error happens, it's our internal error. UtxoSignWithKeyPairError::InputIndexOutOfBound { .. } => UtxoSignTxError::Internal(error), + UtxoSignWithKeyPairError::UnspendableUTXO { script } => UtxoSignTxError::UnspendableUTXO { script }, UtxoSignWithKeyPairError::ErrorSigning(sign) => UtxoSignTxError::ErrorSigning(sign), } } @@ -140,13 +146,8 @@ pub trait UtxoSignerOps { signer.sign_tx().await }, SignPolicy::WithKeyPair(key_pair) => { - let signed = with_key_pair::sign_tx( - params.unsigned_tx, - key_pair, - params.prev_script, - params.signature_version, - self.fork_id(), - )?; + let signed = + with_key_pair::sign_tx(params.unsigned_tx, key_pair, params.signature_version, self.fork_id())?; Ok(signed) }, } diff --git a/mm2src/coins/utxo_signer/src/sign_common.rs b/mm2src/coins/utxo_signer/src/sign_common.rs index 85732dd900..1ba966aa6b 100644 --- a/mm2src/coins/utxo_signer/src/sign_common.rs +++ b/mm2src/coins/utxo_signer/src/sign_common.rs @@ -29,6 +29,7 @@ pub(crate) fn complete_tx(unsigned: TransactionInputSigner, signed_inputs: Vec UtxoSignTxError { @@ -21,21 +21,34 @@ pub enum SpendingInputInfo { address_derivation_path: DerivationPath, address_pubkey: PublicKey, }, + P2WPKH { + address_derivation_path: DerivationPath, + address_pubkey: PublicKey, + }, // The fields are used to generate `trezor::proto::messages_bitcoin::MultisigRedeemScriptType` // P2SH {} } /// Either plain destination address or derivation path of a change address. pub enum OutputDestination { - Plain { address: String }, - Change { derivation_path: DerivationPath }, + Plain { + address: String, + }, + Change { + derivation_path: DerivationPath, + addr_format: AddressFormat, + }, } impl OutputDestination { pub fn plain(address: String) -> OutputDestination { OutputDestination::Plain { address } } - pub fn change(derivation_path: DerivationPath) -> OutputDestination { - OutputDestination::Change { derivation_path } + #[inline] + pub fn change(derivation_path: DerivationPath, addr_format: AddressFormat) -> OutputDestination { + OutputDestination::Change { + derivation_path, + addr_format, + } } } @@ -46,7 +59,15 @@ pub struct SendingOutputInfo { impl SendingOutputInfo { /// For now, returns [`TrezorOutputScriptType::PayToAddress`] since we don't support SLP tokens yet. - pub fn trezor_output_script_type(&self) -> TrezorOutputScriptType { TrezorOutputScriptType::PayToAddress } + #[inline] + pub fn trezor_output_script_type(&self) -> TrezorOutputScriptType { + match self.destination_address { + OutputDestination::Change { ref addr_format, .. } if *addr_format == AddressFormat::Segwit => { + TrezorOutputScriptType::PayToWitness + }, + OutputDestination::Change { .. } | OutputDestination::Plain { .. } => TrezorOutputScriptType::PayToAddress, + } + } } pub struct UtxoSignTxParamsBuilder { @@ -56,8 +77,6 @@ pub struct UtxoSignTxParamsBuilder { inputs_infos: Vec, /// The number of elements is expected to be the same as `unsigned_tx.outputs.len()`. outputs_infos: Vec, - /// This is used to check if a built from key pair matches the expected `prev_script`. - prev_script: Option