diff --git a/Makefile b/Makefile index 35c3498..7aab8d7 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,18 @@ VIPER_FILE="${GOPATH}/src/github.com/hyle-team/bridgeless-signer/config.local.ya gen-proto: cd proto && buf generate -run: +build: rm -f $(GOPATH)/bin/signer go build -o $(GOPATH)/bin/signer + +run: KV_VIPER_FILE=$(VIPER_FILE) signer migrate up KV_VIPER_FILE=$(VIPER_FILE) signer run service +build-run: build run + +clear-db: + KV_VIPER_FILE=$(VIPER_FILE) signer migrate down + test: KV_VIPER_FILE=$(VIPER_FILE) go test -count=1 $(TESTING_PACKAGES) diff --git a/README.md b/README.md index af07b32..8ee7756 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,43 @@ bridge deposit actions happening on the source chains and submit a signed transaction to perform corresponding withdraw action on the target chain. Although the service is built using GRPC and Protocol Buffers, it provides a -REST gateway to submit deposit transactions and check the status of the according withdraw. +REST gateway to submit deposit transactions and check the status of the according withdrawal. + +## Architecture + +Service Core logic (`/internal/core`) consists of two parts: JSON API to submit deposits and check the +status of the according withdrawal (`/core/api`), and different handlers to process withdrawals part by part, +where each part is being sent/consumed to/from RabbitMQ queues of messages (`/core/rabbitmq`). + +There are two types of message consuming implemented here: +- Default consuming (`base`) - used to process incoming message immediately after consuming. Is used for +default scenarios where request can be processed independently; +- Batch consuming (`batch`) - used to collect incoming messages and process them after some period. Is used +for requests that should be better grouped and processed together to optimize/enhance processing (f. e. Bitcoin +transactions batching, Core tx submitting batching). + +Multiple request handlers, that implement interface required by either base or +batch consumer, handle specific part of the withdrawal process (`/rabbitmq/consumer/processors`). They use +bridge processor (`/internal/bridge/processor`) to handle the request and then route the next one using +RabbitMQ request producer (`/rabbitmq/producer`). + +In order to avoid unexpected errors each request to process withdrawal can be resent up to +`maxCount` times in case of some system or third party services failure. + +Bridge processor is the core system that interacts with database, Bridge Core module and chains for data +retrieval/parsing/sending etc. It contains a set of proxies - implementations of the generalized method +to work with different chains (`/bridge/proxy`). For now it supports EVM-based chains (`/proxy/evm`) and +Bitcoin (`/proxy/btc`). + +To better understand how withdrawals are processed by the service, lets look on the chain of requests processing: +1. Base/Batch Consumer reads pending request from the RabbitMQ queue; +2. Request is processed by specific consumer implementation; +3. Consumer implementation uses Bridge processor to handle request; +4. Bridge processor uses proxy to extract/parse/transform/send specific chain data; +5. After Bridge processor processed request, Consumer implementation forms and sends request to process the next +part of the withdrawal using Producer. Unexpectedly failed requests can also be resent by Base/Batch Consumer; +6. Returning to step #1 unless all parts of the withdrawal process are successfully finished. + ## Build @@ -46,30 +82,25 @@ rest_gateway: chains: list: ## Chain ID - - id: "80002" + - id: "80002" + ## Chain type + type: evm ## RPC endpoint - rpc: "your_rpc_endpoint_here" + evm_rpc: "your_rpc_endpoint_here" + bitcoin_rpc: + host: "your_rpc_endpoint_here" + user: "your_rpc_endpoint_here" + pass: "your_rpc_endpoint_here" + # bitcoin-specific data + bitcoin_receivers: + - "list_of_addresses" + # bitcoin-specific data + network: testnet ## Bridge contract address bridge_address: "0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627" ## Number of confirmations required for the deposit to be considered final confirmations: 1 -## Tokens configuration -tokens: - ## List of tokens - list: - - - ## Token configuration - token: - ## Chain ID - chain_id: "80002" - ## Token contract address - address: "0xe61174fa1b7e52132d8b365044bf95b0d90f442f" - ## List of available token pairs to bridge - pairs: - - chain_id: "80002" - address: "0xe61174fa1b7e52132d8b365044bf95b0d90f442f" - ## RabbitMQ configuration rabbitmq: ## RabbitMQ connection URL @@ -82,6 +113,12 @@ rabbitmq: max_retry_count: 5 ## delivery resend delays delays: [1000, 2000, 5000, 10000, 20000, 60000] + tx_submitter: + max_size: 5 + period: 5s + bitcoin_submitter: + max_size: 5 + period: 10s ## Service signer private key @@ -153,9 +190,6 @@ services: signer: image: ghcr.io/hyle-team/bridgeless-signer:7108db395fe92c56875657190c4d9305376c4323 - # build: - # context: . - # dockerfile: Dockerfile.vendor hostname: signer container_name: signer restart: unless-stopped diff --git a/go.mod b/go.mod index d198931..6b422f5 100644 --- a/go.mod +++ b/go.mod @@ -20,12 +20,14 @@ require ( require ( github.com/Masterminds/squirrel v1.5.4 + github.com/btcsuite/btcd v0.24.2 + github.com/btcsuite/btcd/btcutil v1.1.5 + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/cosmos/gogoproto v1.5.0 github.com/ethereum/go-ethereum v1.10.26 github.com/hyle-team/bridgeless-core v0.0.0-20240814124421-34bfbc7e857c github.com/rabbitmq/amqp091-go v1.10.0 github.com/spf13/cast v1.6.0 - github.com/stretchr/testify v1.9.0 gitlab.com/distributed_lab/figure/v3 v3.1.4 ) @@ -44,6 +46,9 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect + github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/confio/ics23/go v0.9.0 // indirect @@ -56,6 +61,7 @@ require ( github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set v1.8.0 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect @@ -121,6 +127,7 @@ require ( github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.18.2 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect diff --git a/go.sum b/go.sum index ea69921..538f9e7 100644 --- a/go.sum +++ b/go.sum @@ -1187,6 +1187,7 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= @@ -1248,14 +1249,36 @@ github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+Wji github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/btcsuite/btcd v0.22.2 h1:vBZ+lGGd1XubpOWO67ITJpAEsICWhA0YzqkcpkgNBfo= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ= -github.com/btcsuite/btcd/btcutil v1.1.2/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= @@ -1345,6 +1368,7 @@ github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuA github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -1357,6 +1381,7 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPc github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= @@ -1789,6 +1814,8 @@ github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= @@ -1798,6 +1825,7 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -1824,6 +1852,7 @@ github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwf github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= @@ -1976,6 +2005,7 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= @@ -1991,6 +2021,8 @@ github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkA github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= @@ -2343,6 +2375,7 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -2450,6 +2483,7 @@ golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/internal/bridge/chain/config.go b/internal/bridge/chain/config.go new file mode 100644 index 0000000..bfb6e2d --- /dev/null +++ b/internal/bridge/chain/config.go @@ -0,0 +1,80 @@ +package chain + +import ( + "github.com/btcsuite/btcd/rpcclient" + "gitlab.com/distributed_lab/figure/v3" + "gitlab.com/distributed_lab/kit/comfig" + "gitlab.com/distributed_lab/kit/kv" + "gitlab.com/distributed_lab/logan/v3/errors" + "reflect" +) + +type Chainer interface { + Chains() []Chain +} + +type chainer struct { + once comfig.Once + getter kv.Getter +} + +func NewChainer(getter kv.Getter) Chainer { + return &chainer{ + getter: getter, + } +} + +func (c *chainer) Chains() []Chain { + return c.once.Do(func() interface{} { + var cfg struct { + Chains []Chain `fig:"list,required"` + } + + if err := figure. + Out(&cfg). + With(figure.BaseHooks, figure.EthereumHooks, bitcoinHooks). + From(kv.MustGetStringMap(c.getter, "chains")). + Please(); err != nil { + panic(errors.Wrap(err, "failed to figure out chains")) + } + + if len(cfg.Chains) == 0 { + panic(errors.New("no chains were configured")) + } + + return cfg.Chains + }).([]Chain) +} + +var bitcoinHooks = figure.Hooks{ + "*rpcclient.Client": func(value interface{}) (reflect.Value, error) { + switch v := value.(type) { + case map[string]interface{}: + var clientConfig struct { + Host string `fig:"host,required"` + User string `fig:"user,required"` + Pass string `fig:"pass,required"` + DisableTLS bool `fig:"disable_tls"` + } + + if err := figure.Out(&clientConfig).With(figure.BaseHooks).From(v).Please(); err != nil { + return reflect.Value{}, errors.Wrap(err, "failed to figure out bitcoin rpc client config") + } + + client, err := rpcclient.New(&rpcclient.ConnConfig{ + Host: clientConfig.Host, + User: clientConfig.User, + Pass: clientConfig.Pass, + HTTPPostMode: true, + DisableTLS: clientConfig.DisableTLS, + }, nil) + if err != nil { + return reflect.Value{}, errors.Wrap(err, "failed to create bitcoin rpc client") + } + + return reflect.ValueOf(client), nil + default: + return reflect.Value{}, errors.Errorf("unsupported conversion from %T", value) + } + }, +} diff --git a/internal/bridge/chain/main.go b/internal/bridge/chain/main.go new file mode 100644 index 0000000..b8217bc --- /dev/null +++ b/internal/bridge/chain/main.go @@ -0,0 +1,112 @@ +package chain + +import ( + "fmt" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/rpcclient" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/pkg/errors" +) + +type Network string + +const ( + NetworkMainnet Network = "mainnet" + NetworkTestnet Network = "testnet" +) + +func (n Network) Validate() error { + switch n { + case NetworkMainnet, NetworkTestnet: + return nil + default: + return errors.New("invalid network") + } +} + +type Chain struct { + Id string `fig:"id,required"` + Type types.ChainType `fig:"type,required"` + Confirmations uint64 `fig:"confirmations,required"` + + // EVM configuration + EvmRpc *ethclient.Client `fig:"evm_rpc"` + BridgeAddress common.Address `fig:"bridge_address"` + + // Bitcoin configuration + BitcoinRpc *rpcclient.Client `fig:"bitcoin_rpc"` + // BitcoinReceivers is a list of allowed addresses (of different types) to receive deposits + BitcoinReceivers []string `fig:"bitcoin_receivers"` + Network Network `fig:"network"` +} + +type Bitcoin struct { + Rpc *rpcclient.Client + Receivers []btcutil.Address + Confirmations uint64 + Params *chaincfg.Params +} + +type Evm struct { + Rpc *ethclient.Client + BridgeAddress common.Address + Confirmations uint64 +} + +func (c Chain) Bitcoin() (Bitcoin, error) { + if c.Type != types.ChainTypeBitcoin { + return Bitcoin{}, errors.New("invalid chain type") + } + if c.BitcoinRpc == nil { + return Bitcoin{}, errors.New("rpc client is nil") + } + if len(c.BitcoinReceivers) == 0 { + return Bitcoin{}, errors.New("receivers list is empty") + } + + var params *chaincfg.Params + if c.Network == NetworkMainnet { + params = &chaincfg.MainNetParams + } + if c.Network == NetworkTestnet { + params = &chaincfg.TestNet3Params + } + + receivers := make([]btcutil.Address, len(c.BitcoinReceivers)) + for i, raw := range c.BitcoinReceivers { + addr, err := btcutil.DecodeAddress(raw, params) + if err != nil { + return Bitcoin{}, errors.Wrap(err, fmt.Sprintf("failed to decode bitcoin receiver %s", raw)) + } + + receivers[i] = addr + } + + return Bitcoin{ + Rpc: c.BitcoinRpc, + Receivers: receivers, + Confirmations: c.Confirmations, + Params: params, + }, nil +} + +func (c Chain) Evm() (Evm, error) { + if c.Type != types.ChainTypeEVM { + return Evm{}, errors.New("invalid chain type") + } + if c.EvmRpc == nil { + return Evm{}, errors.New("rpc client is nil") + } + if c.BridgeAddress == (common.Address{}) { + return Evm{}, errors.New("bridge address is empty") + } + + return Evm{ + Rpc: c.EvmRpc, + BridgeAddress: c.BridgeAddress, + Confirmations: c.Confirmations, + }, nil +} diff --git a/internal/bridge/evm/chain/config.go b/internal/bridge/evm/chain/config.go deleted file mode 100644 index f39df47..0000000 --- a/internal/bridge/evm/chain/config.go +++ /dev/null @@ -1,45 +0,0 @@ -package chain - -import ( - "gitlab.com/distributed_lab/figure/v3" - "gitlab.com/distributed_lab/kit/comfig" - "gitlab.com/distributed_lab/kit/kv" - "gitlab.com/distributed_lab/logan/v3/errors" -) - -type Chainer interface { - Chains() []Chain -} - -type chainer struct { - once comfig.Once - getter kv.Getter -} - -func NewChainer(getter kv.Getter) Chainer { - return &chainer{ - getter: getter, - } -} - -func (c *chainer) Chains() []Chain { - return c.once.Do(func() interface{} { - var cfg struct { - Chains []Chain `fig:"list,required"` - } - - if err := figure. - Out(&cfg). - With(figure.BaseHooks, figure.EthereumHooks). - From(kv.MustGetStringMap(c.getter, "chains")). - Please(); err != nil { - panic(errors.Wrap(err, "failed to figure out chains")) - } - - if len(cfg.Chains) == 0 { - panic(errors.New("no chains were configured")) - } - - return cfg.Chains - }).([]Chain) -} diff --git a/internal/bridge/evm/chain/main.go b/internal/bridge/evm/chain/main.go deleted file mode 100644 index 7a63ad8..0000000 --- a/internal/bridge/evm/chain/main.go +++ /dev/null @@ -1,15 +0,0 @@ -package chain - -import ( - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" -) - -type Chain struct { - Id *big.Int `fig:"id,required"` - Rpc *ethclient.Client `fig:"rpc,required"` - BridgeAddress common.Address `fig:"bridge_address,required"` - Confirmations int64 `fig:"confirmations,required"` -} diff --git a/internal/bridge/evm/proxies_repository.go b/internal/bridge/evm/proxies_repository.go deleted file mode 100644 index e4a0afa..0000000 --- a/internal/bridge/evm/proxies_repository.go +++ /dev/null @@ -1,43 +0,0 @@ -package evm - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/hyle-team/bridgeless-signer/internal/bridge/evm/chain" - bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" - "github.com/pkg/errors" -) - -type proxiesRepository struct { - proxies map[string]bridgeTypes.Proxy -} - -func NewProxiesRepository(chains []chain.Chain, signer common.Address) (bridgeTypes.ProxiesRepository, error) { - proxiesMap := make(map[string]bridgeTypes.Proxy) - - for _, c := range chains { - proxy, err := NewBridgeProxy(c, signer) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("failed to create proxy for chain %s", c.Id.String())) - } - - proxiesMap[c.Id.String()] = proxy - } - - return &proxiesRepository{proxies: proxiesMap}, nil -} - -func (p proxiesRepository) Proxy(chainId string) (bridgeTypes.Proxy, error) { - proxy, ok := p.proxies[chainId] - if !ok { - return nil, bridgeTypes.ErrChainNotSupported - } - - return proxy, nil -} - -func (p proxiesRepository) SupportsChain(chainId string) bool { - _, ok := p.proxies[chainId] - return ok -} diff --git a/internal/bridge/evm/transaction.go b/internal/bridge/evm/transaction.go deleted file mode 100644 index d6be350..0000000 --- a/internal/bridge/evm/transaction.go +++ /dev/null @@ -1,35 +0,0 @@ -package evm - -import ( - "context" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" - "github.com/pkg/errors" -) - -func (p *bridgeProxy) GetTransactionReceipt(txHash common.Hash) (*types.Receipt, error) { - ctx := context.Background() - tx, pending, err := p.chain.Rpc.TransactionByHash(ctx, txHash) - if err != nil { - if err.Error() == "not found" { - return nil, bridgeTypes.ErrTxNotFound - } - - return nil, errors.Wrap(err, "failed to get transaction by hash") - } - if pending { - return nil, bridgeTypes.ErrTxPending - } - - receipt, err := p.chain.Rpc.TransactionReceipt(context.Background(), tx.Hash()) - if err != nil { - return nil, errors.Wrap(err, "failed to get tx receipt") - } - if receipt == nil { - return nil, errors.New("receipt is nil") - } - - return receipt, nil -} diff --git a/internal/bridge/processor/form_withdrawal.go b/internal/bridge/processor/form_withdrawal.go index 8c62a8a..09fa120 100644 --- a/internal/bridge/processor/form_withdrawal.go +++ b/internal/bridge/processor/form_withdrawal.go @@ -1,16 +1,14 @@ package processor import ( - ethTypes "github.com/ethereum/go-ethereum/core/types" bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" - "github.com/hyle-team/bridgeless-signer/pkg/tokens" "github.com/pkg/errors" ) func (p *Processor) ProcessFormWithdrawalRequest(req bridgeTypes.FormWithdrawalRequest) (request *bridgeTypes.WithdrawalRequest, reprocessable bool, err error) { - defer func() { err = p.updateInvalidDepositStatus(req.DepositDbId, err, reprocessable) }() + defer func() { err = p.updateInvalidDepositStatus(err, reprocessable, req.DepositDbId) }() - proxy, err := p.proxies.Proxy(req.Data.DestinationChainId.String()) + proxy, err := p.proxies.Proxy(req.Data.DestinationChainId) if err != nil { if errors.Is(err, bridgeTypes.ErrChainNotSupported) { return nil, false, bridgeTypes.ErrChainNotSupported @@ -18,46 +16,14 @@ func (p *Processor) ProcessFormWithdrawalRequest(req bridgeTypes.FormWithdrawalR return nil, true, errors.Wrap(err, "failed to get proxy") } - dstTokenAddress, err := p.tokenPairer.GetDestinationTokenAddress( - req.Data.DepositIdentifier.GetChainId(), - req.Data.TokenAddress, - req.Data.DestinationChainId, - ) + tx, err := proxy.FormWithdrawalTransaction(req.Data) if err != nil { - reprocessable = true - if errors.Is(err, tokens.ErrSourceTokenNotSupported) || - errors.Is(err, tokens.ErrDestinationTokenNotSupported) || - errors.Is(err, tokens.ErrPairNotFound) { - reprocessable = false - } - - return nil, reprocessable, errors.Wrap(err, "failed to get destination token address") - } - req.Data.DestinationTokenAddress = &dstTokenAddress - - var tx *ethTypes.Transaction - txConn := p.db.New() - err = txConn.Transaction(func() error { - tmpErr := txConn.SetDepositData(req.Data) - if tmpErr != nil { - return errors.Wrap(tmpErr, "failed to save deposit data") - } - - tx, tmpErr = proxy.FormWithdrawalTransaction(req.Data) - return errors.Wrap(tmpErr, "failed to form withdrawal transaction") - }) - if err == nil { - return &bridgeTypes.WithdrawalRequest{ - Data: req.Data, - DepositDbId: req.DepositDbId, - Transaction: tx, - }, false, nil - } - - reprocessable = true - if errors.Is(err, bridgeTypes.ErrInvalidReceiverAddress) { - reprocessable = false + return nil, true, errors.Wrap(err, "failed to form withdrawal transaction") } - return nil, reprocessable, errors.Wrap(err, "failed to form withdrawal transaction") + return &bridgeTypes.WithdrawalRequest{ + Data: req.Data, + DepositDbId: req.DepositDbId, + Transaction: tx, + }, false, nil } diff --git a/internal/bridge/processor/get_deposit.go b/internal/bridge/processor/get_deposit.go index 8c41d70..6f8116c 100644 --- a/internal/bridge/processor/get_deposit.go +++ b/internal/bridge/processor/get_deposit.go @@ -1,34 +1,80 @@ package processor import ( + "fmt" + "github.com/hyle-team/bridgeless-signer/internal/bridge/proxy/btc" bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/hyle-team/bridgeless-signer/pkg/tokens" "github.com/pkg/errors" ) func (p *Processor) ProcessGetDepositRequest(req bridgeTypes.GetDepositRequest) (data *bridgeTypes.FormWithdrawalRequest, reprocessable bool, err error) { - defer func() { err = p.updateInvalidDepositStatus(req.DepositDbId, err, reprocessable) }() + defer func() { err = p.updateInvalidDepositStatus(err, reprocessable, req.DepositDbId) }() proxy, err := p.proxies.Proxy(req.DepositIdentifier.ChainId) if err != nil { if errors.Is(err, bridgeTypes.ErrChainNotSupported) { - return data, false, bridgeTypes.ErrChainNotSupported + return data, false, errors.Wrap(err, fmt.Sprintf("source chain id: %v", req.DepositIdentifier.ChainId)) } - return data, true, errors.Wrap(err, "failed to get proxy") + return data, true, errors.Wrap(err, "failed to get source proxy") } depositData, err := proxy.GetDepositData(req.DepositIdentifier) - if err == nil { - return &bridgeTypes.FormWithdrawalRequest{ - DepositDbId: req.DepositDbId, - Data: *depositData, - }, false, nil + if err != nil { + reprocessable = true + if errors.Is(err, bridgeTypes.ErrTxFailed) || + errors.Is(err, bridgeTypes.ErrDepositNotFound) || + errors.Is(err, bridgeTypes.ErrInvalidDepositedAmount) || + errors.Is(err, bridgeTypes.ErrInvalidScriptPubKey) { + reprocessable = false + } + + return nil, reprocessable, errors.Wrap(err, "failed to get deposit data") + } + + dstProxy, err := p.proxies.Proxy(depositData.DestinationChainId) + if err != nil { + if errors.Is(err, bridgeTypes.ErrChainNotSupported) { + return data, false, errors.Wrap(err, fmt.Sprintf("destination chain id: %v", depositData.DestinationChainId)) + } + return data, true, errors.Wrap(err, "failed to get destination proxy") + } + if !dstProxy.AddressValid(depositData.DestinationAddress) { + return data, false, errors.Wrap(bridgeTypes.ErrInvalidReceiverAddress, depositData.DestinationAddress) + } + + switch dstProxy.Type() { + case bridgeTypes.ChainTypeBitcoin: + if depositData.Amount.Int64() < btc.MinSatoshisPerOutput { + return data, false, bridgeTypes.ErrInvalidDepositedAmount + } + case bridgeTypes.ChainTypeEVM: + depositData.DestinationTokenAddress, err = p.tokenPairer.GetDestinationTokenAddress( + depositData.ChainId, + depositData.TokenAddress, + depositData.DestinationChainId, + ) + if err != nil { + reprocessable = true + if errors.Is(err, tokens.ErrSourceTokenNotSupported) || + errors.Is(err, tokens.ErrDestinationTokenNotSupported) || + errors.Is(err, tokens.ErrPairNotFound) { + reprocessable = false + } + + return nil, reprocessable, errors.Wrap(err, "failed to get destination token address") + } + default: + return data, false, errors.Wrap(err, fmt.Sprintf("invalid chain type: %v", dstProxy.Type())) } - reprocessable = true - if errors.Is(err, bridgeTypes.ErrTxFailed) || - errors.Is(err, bridgeTypes.ErrDepositNotFound) { - reprocessable = false + if err = p.db.New().SetDepositData(*depositData); err != nil { + return nil, true, errors.Wrap(err, "failed to save deposit data") } - return nil, reprocessable, errors.Wrap(err, "failed to get deposit data") + return &bridgeTypes.FormWithdrawalRequest{ + DepositDbId: req.DepositDbId, + Data: *depositData, + Destination: dstProxy.Type(), + }, false, nil } diff --git a/internal/bridge/processor/main.go b/internal/bridge/processor/main.go index b5a7c14..ec73178 100644 --- a/internal/bridge/processor/main.go +++ b/internal/bridge/processor/main.go @@ -1,21 +1,25 @@ package processor import ( + coretypes "github.com/hyle-team/bridgeless-core/x/bridge/types" "github.com/hyle-team/bridgeless-signer/internal/bridge/signer" bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" - "github.com/hyle-team/bridgeless-signer/internal/connectors/core" "github.com/hyle-team/bridgeless-signer/internal/data" "github.com/hyle-team/bridgeless-signer/pkg/tokens" "github.com/hyle-team/bridgeless-signer/pkg/types" "github.com/pkg/errors" ) +type TxSubmitter interface { + SubmitDeposits(depositTxs ...coretypes.Transaction) error +} + type Processor struct { - proxies bridgeTypes.ProxiesRepository - db data.DepositsQ - signer *signer.Signer - tokenPairer tokens.TokenPairer - coreConnector *core.Connector + proxies bridgeTypes.ProxiesRepository + db data.DepositsQ + signer *signer.Signer + tokenPairer tokens.TokenPairer + submitter TxSubmitter } func New( @@ -23,26 +27,26 @@ func New( db data.DepositsQ, signer *signer.Signer, tokenPairer tokens.TokenPairer, - coreConnector *core.Connector, + submitter TxSubmitter, ) *Processor { - return &Processor{proxies: proxies, db: db, signer: signer, tokenPairer: tokenPairer, coreConnector: coreConnector} + return &Processor{proxies: proxies, db: db, signer: signer, tokenPairer: tokenPairer, submitter: submitter} } -func (p *Processor) SetWithdrawStatusFailed(id int64) error { - return errors.Wrap(p.db.UpdateWithdrawalStatus(id, types.WithdrawalStatus_FAILED), "failed to update deposit status") +func (p *Processor) SetWithdrawStatusFailed(ids ...int64) error { + return errors.Wrap(p.db.UpdateWithdrawalStatus(types.WithdrawalStatus_FAILED, ids...), "failed to update deposit status") } func (p *Processor) SetSubmitStatusFailed(ids ...int64) error { return errors.Wrap(p.db.UpdateSubmitStatus(types.SubmitWithdrawalStatus_SUBMIT_FAILED, ids...), "failed to update submit status") } -func (p *Processor) updateInvalidDepositStatus(id int64, err error, reprocessable bool) error { +func (p *Processor) updateInvalidDepositStatus(err error, reprocessable bool, ids ...int64) error { if err == nil || reprocessable { return err } - if tempErr := p.db.UpdateWithdrawalStatus(id, types.WithdrawalStatus_INVALID); tempErr != nil { + if tempErr := p.db.UpdateWithdrawalStatus(types.WithdrawalStatus_INVALID, ids...); tempErr != nil { return errors.Wrap(tempErr, "failed to update deposit status") } diff --git a/internal/bridge/processor/processor_test.go b/internal/bridge/processor/processor_test.go deleted file mode 100644 index fbe428b..0000000 --- a/internal/bridge/processor/processor_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package processor - -import ( - "fmt" - "testing" - - "github.com/hyle-team/bridgeless-signer/internal/bridge/evm" - bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" - "github.com/hyle-team/bridgeless-signer/internal/config" - "github.com/hyle-team/bridgeless-signer/internal/data" - "github.com/hyle-team/bridgeless-signer/internal/data/pg" - "github.com/hyle-team/bridgeless-signer/pkg/types" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "gitlab.com/distributed_lab/kit/kv" -) - -const ( - depositTxHash = "0x28246db1eccf7d7d431d6b3f05a5e1a34d3155ebbd421b3b0efbeeb339c8af77" - depositTxEventId = 1 - amoyChainId = "80002" -) - -func TestProcessor_HappyPath(t *testing.T) { - cfg := config.New(kv.MustFromEnv()) - - proxies, err := evm.NewProxiesRepository(cfg.Chains(), cfg.Signer().Address()) - if err != nil { - t.Fatal(errors.Wrap(err, "failed to create proxies repository")) - } - - db := pg.NewDepositsQ(cfg.DB()) - - processor := New(proxies, db, cfg.Signer(), cfg.TokenPairer()) - - deposit := data.Deposit{ - DepositIdentifier: data.DepositIdentifier{ - TxHash: depositTxHash, - TxEventId: depositTxEventId, - ChainId: amoyChainId, - }, - Status: types.WithdrawalStatus_PROCESSING, - } - - deposit.Id, err = db.Insert(deposit) - if err != nil { - if !errors.Is(err, data.ErrAlreadySubmitted) { - t.Fatal(errors.Wrap(err, "failed to insert deposit")) - } - } - - req := bridgeTypes.GetDepositRequest{ - DepositDbId: deposit.Id, - DepositIdentifier: deposit.DepositIdentifier, - } - - formWithdrawRequest, _, err := processor.ProcessGetDepositRequest(req) - if err != nil { - t.Fatal(errors.Wrap(err, "failed to process get deposit request")) - } - - assert.Equal(t, "100000000000000000000", formWithdrawRequest.Data.Amount.String()) - - // modify the amount to send a different amount - formWithdrawRequest.Data.Amount.SetString("123456", 10) - - formedWithRequest, _, err := processor.ProcessFormWithdrawalRequest(*formWithdrawRequest) - if err != nil { - t.Fatal(errors.Wrap(err, "failed to process form withdraw request")) - } - - signedRequest, _, err := processor.ProcessSignWithdrawalRequest(*formedWithRequest) - if err != nil { - t.Fatal(errors.Wrap(err, "failed to process sign withdraw request")) - } - - _, err = processor.ProcessSendWithdrawalRequest(*signedRequest) - if err != nil { - t.Fatal(errors.Wrap(err, "failed to process send withdraw request")) - } - - fmt.Println(signedRequest.Transaction.Hash().String()) -} diff --git a/internal/bridge/processor/send_bitcoin_withdraw.go b/internal/bridge/processor/send_bitcoin_withdraw.go new file mode 100644 index 0000000..18f822d --- /dev/null +++ b/internal/bridge/processor/send_bitcoin_withdraw.go @@ -0,0 +1,56 @@ +package processor + +import ( + bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/hyle-team/bridgeless-signer/internal/data" + "github.com/pkg/errors" + "math/big" +) + +func (p *Processor) SendBitcoinWithdrawals(reqs ...bridgeTypes.BitcoinWithdrawalRequest) (reprocessable bool, err error) { + if len(reqs) == 0 { + return false, nil + } + + var ( + params = make(map[string]*big.Int, len(reqs)) + withdrawalTxs = make([]data.WithdrawalTx, len(reqs)) + depositIds = make([]int64, len(reqs)) + ) + for i, req := range reqs { + params[req.Data.DestinationAddress] = req.Data.Amount + depositIds[i] = req.DepositDbId + } + + defer func() { err = p.updateInvalidDepositStatus(err, reprocessable, depositIds...) }() + + proxy, err := p.proxies.Proxy(reqs[0].Data.DestinationChainId) + if err != nil { + if errors.Is(err, bridgeTypes.ErrChainNotSupported) { + return false, bridgeTypes.ErrChainNotSupported + } + return true, errors.Wrap(err, "failed to get proxy") + } + if proxy.Type() != bridgeTypes.ChainTypeBitcoin { + return false, bridgeTypes.ErrChainNotSupported + } + + hash, err := proxy.SendBitcoins(params) + if err != nil { + return true, errors.Wrap(err, "failed to send withdrawals") + } + + for i, req := range reqs { + withdrawalTxs[i] = data.WithdrawalTx{ + DepositId: req.DepositDbId, + ChainId: req.Data.DestinationChainId, + TxHash: hash, + } + } + + if err = p.db.New().SetWithdrawalTxs(withdrawalTxs...); err != nil { + return false, errors.Wrap(err, "failed to set withdrawals") + } + + return true, nil +} diff --git a/internal/bridge/processor/send_withdrawal.go b/internal/bridge/processor/send_withdrawal.go index f9ae72c..5c48fb5 100644 --- a/internal/bridge/processor/send_withdrawal.go +++ b/internal/bridge/processor/send_withdrawal.go @@ -2,11 +2,12 @@ package processor import ( bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/hyle-team/bridgeless-signer/internal/data" "github.com/pkg/errors" ) func (p *Processor) ProcessSendWithdrawalRequest(req bridgeTypes.WithdrawalRequest) (reprocessable bool, err error) { - defer func() { err = p.updateInvalidDepositStatus(req.DepositDbId, err, reprocessable) }() + defer func() { err = p.updateInvalidDepositStatus(err, reprocessable, req.DepositDbId) }() // ensure that withdrawal request was not already processed deposit, err := p.db.Get(req.Data.DepositIdentifier) @@ -20,7 +21,7 @@ func (p *Processor) ProcessSendWithdrawalRequest(req bridgeTypes.WithdrawalReque return false, errors.New("withdrawal transaction was already sent") } - proxy, err := p.proxies.Proxy(req.Data.DestinationChainId.String()) + proxy, err := p.proxies.Proxy(req.Data.DestinationChainId) if err != nil { if errors.Is(err, bridgeTypes.ErrChainNotSupported) { return false, bridgeTypes.ErrChainNotSupported @@ -31,9 +32,11 @@ func (p *Processor) ProcessSendWithdrawalRequest(req bridgeTypes.WithdrawalReque // rollback if transaction failed to be sent txConn := p.db.New() err = txConn.Transaction(func() error { - if tempErr := txConn.SetWithdrawalTx( - req.DepositDbId, req.Transaction.Hash().Hex(), req.Data.DestinationChainId.String(), - ); tempErr != nil { + if tempErr := txConn.SetWithdrawalTxs(data.WithdrawalTx{ + DepositId: req.DepositDbId, + TxHash: req.Transaction.Hash().Hex(), + ChainId: req.Data.DestinationChainId, + }); tempErr != nil { return errors.Wrap(tempErr, "failed to set withdrawal tx") } diff --git a/internal/bridge/processor/sign_withdrawal.go b/internal/bridge/processor/sign_withdrawal.go index 6dbc99e..e928ccd 100644 --- a/internal/bridge/processor/sign_withdrawal.go +++ b/internal/bridge/processor/sign_withdrawal.go @@ -3,12 +3,18 @@ package processor import ( bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" "github.com/pkg/errors" + "math/big" ) func (p *Processor) ProcessSignWithdrawalRequest(req bridgeTypes.WithdrawalRequest) (res *bridgeTypes.WithdrawalRequest, reprocessable bool, err error) { - defer func() { err = p.updateInvalidDepositStatus(req.DepositDbId, err, reprocessable) }() + defer func() { err = p.updateInvalidDepositStatus(err, reprocessable, req.DepositDbId) }() - tx, err := p.signer.SignTx(req.Transaction, req.Data.DestinationChainId) + chainId, set := new(big.Int).SetString(req.Data.DestinationChainId, 10) + if !set { + return nil, false, errors.New("invalid destination chain id") + } + + tx, err := p.signer.SignTx(req.Transaction, chainId) if err != nil { // TODO: should be reprocessable or not? return res, true, errors.Wrap(err, "failed to sign withdrawal transaction") diff --git a/internal/bridge/processor/submit_transaction.go b/internal/bridge/processor/submit_transaction.go index 146b170..111d997 100644 --- a/internal/bridge/processor/submit_transaction.go +++ b/internal/bridge/processor/submit_transaction.go @@ -36,7 +36,7 @@ func (p *Processor) SubmitTransactions(reqs ...bridgeTypes.SubmitTransactionRequ return errors.Wrap(tmperr, "failed to set deposits submitted") } - return errors.Wrap(p.coreConnector.SubmitDeposits(depositTxs...), "failed to submit deposits") + return errors.Wrap(p.submitter.SubmitDeposits(depositTxs...), "failed to submit deposits") }) return err != nil, err diff --git a/internal/bridge/proxy/btc/deposit.go b/internal/bridge/proxy/btc/deposit.go new file mode 100644 index 0000000..cf701d5 --- /dev/null +++ b/internal/bridge/proxy/btc/deposit.go @@ -0,0 +1,179 @@ +package btc + +import ( + "encoding/hex" + "fmt" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/hyle-team/bridgeless-signer/internal/data" + "github.com/pkg/errors" + "math/big" + "slices" + "strings" +) + +const ( + defaultDecimals = 8 + defaultDepositorAddressOutputIdx = 0 +) + +func (p *proxy) GetDepositData(id data.DepositIdentifier) (*data.DepositData, error) { + var ( + depositIdx = id.TxEventId + dstDataIdx = depositIdx + 1 + ) + + tx, err := p.getTx(id.TxHash) + if err != nil { + return nil, errors.Wrap(err, "failed to get transaction") + } + + if tx.BlockHash == "" { + return nil, bridgeTypes.ErrTxPending + } + blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) + if err != nil { + return nil, errors.Wrap(err, "failed to decode block hash") + } + block, err := p.chain.Rpc.GetBlockVerbose(blockHash) + if err != nil { + return nil, errors.Wrap(err, "failed to get block") + } + if tx.Confirmations < p.chain.Confirmations { + return nil, bridgeTypes.ErrTxNotConfirmed + } + + if len(tx.Vout) < dstDataIdx+1 || len(tx.Vin) == 0 { + return nil, bridgeTypes.ErrDepositNotFound + } + + amount, err := p.parseDepositOutput(tx.Vout[depositIdx]) + if err != nil { + return nil, errors.Wrap(err, "failed to get deposit amount") + } + + addr, chainId, err := p.parseDestinationOutput(tx.Vout[dstDataIdx]) + if err != nil { + return nil, errors.Wrap(err, "failed to get destination address") + } + + depositor, err := p.parseSenderAddress(tx.Vin[defaultDepositorAddressOutputIdx]) + if err != nil { + return nil, errors.Wrap(err, "failed to get depositor") + } + + return &data.DepositData{ + DepositIdentifier: id, + DestinationChainId: chainId, + DestinationAddress: addr, + SourceAddress: depositor, + Amount: amount, + // no token address here + Block: block.Height, + }, nil +} + +func (p *proxy) parseSenderAddress(in btcjson.Vin) (addr string, err error) { + prevTx, err := p.getTx(in.Txid) + if err != nil { + return "", errors.Wrap(err, "failed to get previous transaction to identify sender") + } + + if len(prevTx.Vout) < int(in.Vout)+1 { + return "", errors.New("sender vout not found") + } + + scriptRaw, err := hex.DecodeString(prevTx.Vout[in.Vout].ScriptPubKey.Hex) + if err != nil { + return "", errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, err.Error()) + } + + _, addrs, _, err := txscript.ExtractPkScriptAddrs(scriptRaw, p.chain.Params) + if err != nil { + return "", errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, err.Error()) + } + if len(addrs) == 0 { + return "", errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, "empty sender address") + } + + return addrs[0].String(), nil +} + +func (p *proxy) parseDestinationOutput(out btcjson.Vout) (addr, chainId string, err error) { + if len(out.ScriptPubKey.Hex) == 0 { + return addr, chainId, errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, "empty destination") + } + + scriptRaw, err := hex.DecodeString(out.ScriptPubKey.Hex) + if scriptRaw[0] != txscript.OP_RETURN && len(scriptRaw) <= 3 { + return addr, chainId, errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, "destination data missing") + } + + // Omitting: OP_RETURN OP_PUSH [return data length] (first three bytes) + dstData := scriptRaw[3:] + + params := strings.Split(string(dstData), "-") + if len(params) != 2 { + return addr, chainId, errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, "invalid destination params count") + } + + return params[0], params[1], nil +} + +var supportedScriptTypes = []txscript.ScriptClass{ + txscript.PubKeyHashTy, + txscript.WitnessV0PubKeyHashTy, + // TODO: remove or not? + txscript.WitnessV1TaprootTy, +} + +func (p *proxy) parseDepositOutput(out btcjson.Vout) (*big.Int, error) { + scriptRaw, err := hex.DecodeString(out.ScriptPubKey.Hex) + if err != nil { + return nil, errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, err.Error()) + } + + stype, addrs, _, err := txscript.ExtractPkScriptAddrs(scriptRaw, p.chain.Params) + if err != nil { + return nil, errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, err.Error()) + } + if !slices.Contains(supportedScriptTypes, stype) || len(addrs) != 1 { + return nil, errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, fmt.Sprintf("unsupported type %s", stype)) + } + if !p.bridgeAddr(addrs[0]) { + return nil, errors.Wrap(bridgeTypes.ErrInvalidScriptPubKey, "receiver address is not bridge") + } + if out.Value == 0 { + return nil, bridgeTypes.ErrInvalidDepositedAmount + } + + return toBigint(out.Value, defaultDecimals), nil +} + +func (p *proxy) bridgeAddr(addr btcutil.Address) bool { + for _, receiver := range p.chain.Receivers { + if addr.String() == receiver.String() { + return true + } + } + + return false +} + +func toBigint(val float64, decimals int64) *big.Int { + bigval := new(big.Float) + bigval.SetFloat64(val) + + coin := new(big.Float) + coin.SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(decimals), nil)) + + bigval.Mul(bigval, coin) + + result := new(big.Int) + bigval.Int(result) + + return result +} diff --git a/internal/bridge/proxy/btc/proxy.go b/internal/bridge/proxy/btc/proxy.go new file mode 100644 index 0000000..550e2ca --- /dev/null +++ b/internal/bridge/proxy/btc/proxy.go @@ -0,0 +1,44 @@ +package btc + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/hyle-team/bridgeless-signer/internal/bridge/chain" + bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/hyle-team/bridgeless-signer/internal/data" + "regexp" +) + +// MinSatoshisPerOutput calculated for P2PKH +const MinSatoshisPerOutput = 547 + +var txHashPattern = regexp.MustCompile("^[a-fA-F0-9]{64}$") + +type proxy struct { + chain chain.Bitcoin +} + +func NewBridgeProxy(ch chain.Bitcoin) bridgeTypes.Proxy { + return &proxy{chain: ch} +} + +func (*proxy) Type() bridgeTypes.ChainType { + return bridgeTypes.ChainTypeBitcoin +} + +func (p *proxy) FormWithdrawalTransaction(data data.DepositData) (*types.Transaction, error) { + return nil, bridgeTypes.ErrNotImplemented +} + +func (p *proxy) SendWithdrawalTransaction(signedTx *types.Transaction) error { + return bridgeTypes.ErrNotImplemented +} + +func (p *proxy) AddressValid(addr string) bool { + _, err := btcutil.DecodeAddress(addr, p.chain.Params) + return err == nil +} + +func (p *proxy) TransactionHashValid(hash string) bool { + return txHashPattern.MatchString(hash) +} diff --git a/internal/bridge/proxy/btc/transaction.go b/internal/bridge/proxy/btc/transaction.go new file mode 100644 index 0000000..2077c45 --- /dev/null +++ b/internal/bridge/proxy/btc/transaction.go @@ -0,0 +1,44 @@ +package btc + +import ( + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/pkg/errors" + "strings" +) + +func (p *proxy) GetTransactionStatus(txHash string) (bridgeTypes.TransactionStatus, error) { + tx, err := p.getTx(txHash) + if err != nil { + if errors.Is(err, bridgeTypes.ErrTxNotFound) { + return bridgeTypes.TransactionStatusNotFound, nil + } + + return bridgeTypes.TransactionStatusUnknown, errors.Wrap(err, "failed to get raw transaction") + } + + // At least one confirmation means that block is mined + if tx.Confirmations > 0 { + return bridgeTypes.TransactionStatusSuccessful, nil + } else { + return bridgeTypes.TransactionStatusPending, nil + } +} + +func (p *proxy) getTx(txHash string) (*btcjson.TxRawResult, error) { + hash, err := chainhash.NewHashFromStr(txHash) + if err != nil { + return nil, errors.Wrap(err, "failed to parse tx hash") + } + + tx, err := p.chain.Rpc.GetRawTransactionVerbose(hash) + if err != nil { + if strings.Contains(err.Error(), "No such mempool or blockchain transaction") { + return nil, bridgeTypes.ErrTxNotFound + } + return nil, errors.Wrap(err, "failed to get raw transaction") + } + + return tx, nil +} diff --git a/internal/bridge/proxy/btc/withdraw.go b/internal/bridge/proxy/btc/withdraw.go new file mode 100644 index 0000000..a921e52 --- /dev/null +++ b/internal/bridge/proxy/btc/withdraw.go @@ -0,0 +1,37 @@ +package btc + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/pkg/errors" + "math/big" +) + +func (p *proxy) SendBitcoins(data map[string]*big.Int) (string, error) { + if len(data) == 0 { + return "", errors.New("empty data") + } + + amounts := make(map[btcutil.Address]btcutil.Amount, len(data)) + for adrRaw, amount := range data { + addr, err := btcutil.DecodeAddress(adrRaw, p.chain.Params) + if err != nil { + return "", errors.Wrap(err, "failed to decode address") + } + if amount == nil { + return "", errors.New("amount is nil") + } + value := amount.Int64() + if value < MinSatoshisPerOutput { + return "", errors.New("amount is too small") + } + + amounts[addr] = btcutil.Amount(value) + } + + hash, err := p.chain.Rpc.SendMany("", amounts) + if err != nil { + return "", errors.Wrap(err, "failed to send transaction") + } + + return hash.String(), nil +} diff --git a/internal/bridge/evm/deposit.go b/internal/bridge/proxy/evm/deposit.go similarity index 82% rename from internal/bridge/evm/deposit.go rename to internal/bridge/proxy/evm/deposit.go index 0030f66..c21f3c0 100644 --- a/internal/bridge/evm/deposit.go +++ b/internal/bridge/proxy/evm/deposit.go @@ -11,7 +11,7 @@ import ( "github.com/pkg/errors" ) -func (p *bridgeProxy) GetDepositData(id data.DepositIdentifier) (*data.DepositData, error) { +func (p *proxy) GetDepositData(id data.DepositIdentifier) (*data.DepositData, error) { txReceipt, err := p.GetTransactionReceipt(common.HexToHash(id.TxHash)) if err != nil { return nil, errors.Wrap(err, "failed to get transaction receipt") @@ -20,19 +20,19 @@ func (p *bridgeProxy) GetDepositData(id data.DepositIdentifier) (*data.DepositDa return nil, bridgeTypes.ErrTxFailed } - if err = p.validateConfirmations(txReceipt); err != nil { - return nil, errors.Wrap(err, "failed to validate confirmations") - } - if len(txReceipt.Logs) < id.TxEventId+1 { return nil, bridgeTypes.ErrDepositNotFound } log := txReceipt.Logs[id.TxEventId] - if !p.IsDepositLog(log) { + if !p.isDepositLog(log) { return nil, bridgeTypes.ErrDepositNotFound } + if err = p.validateConfirmations(txReceipt); err != nil { + return nil, errors.Wrap(err, "failed to validate confirmations") + } + var event contracts.BridgeBridgeIn if err = p.contractABI.UnpackIntoInterface(&event, DepositEvent, log.Data); err != nil { return nil, errors.Wrap(err, "failed to unpack deposit event") @@ -42,23 +42,23 @@ func (p *bridgeProxy) GetDepositData(id data.DepositIdentifier) (*data.DepositDa return &data.DepositData{ DepositIdentifier: id, - DestinationChainId: event.ChainId, + DestinationChainId: event.ChainId.String(), DestinationAddress: event.DstAddress, - SourceAddress: event.SrcAddress, + SourceAddress: event.SrcAddress.String(), Amount: event.Amount, TokenAddress: event.Token, Block: int64(log.BlockNumber), }, nil } -func (p *bridgeProxy) validateConfirmations(receipt *types.Receipt) error { +func (p *proxy) validateConfirmations(receipt *types.Receipt) error { curHeight, err := p.chain.Rpc.BlockNumber(context.Background()) if err != nil { return errors.Wrap(err, "failed to get current block number") } // including the current block - if receipt.BlockNumber.Uint64()+uint64(p.chain.Confirmations)-1 > curHeight { + if receipt.BlockNumber.Uint64()+p.chain.Confirmations-1 > curHeight { return bridgeTypes.ErrTxNotConfirmed } diff --git a/internal/bridge/evm/proxy.go b/internal/bridge/proxy/evm/proxy.go similarity index 59% rename from internal/bridge/evm/proxy.go rename to internal/bridge/proxy/evm/proxy.go index b40a9b5..e390dc3 100644 --- a/internal/bridge/evm/proxy.go +++ b/internal/bridge/proxy/evm/proxy.go @@ -3,6 +3,9 @@ package evm import ( "bytes" "context" + "github.com/hyle-team/bridgeless-signer/internal/bridge/chain" + "math/big" + "regexp" "strings" "sync" @@ -10,15 +13,16 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/hyle-team/bridgeless-signer/contracts" - "github.com/hyle-team/bridgeless-signer/internal/bridge/evm/chain" bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" "github.com/pkg/errors" ) const DepositEvent = "BridgeIn" -type bridgeProxy struct { - chain chain.Chain +var txHashPattern = regexp.MustCompile(`^0x[0-9a-fA-F]{64}$`) + +type proxy struct { + chain chain.Evm bridgeContract *contracts.Bridge contractABI abi.ABI depositEvent abi.Event @@ -27,7 +31,9 @@ type bridgeProxy struct { nonceM sync.Mutex } -func NewBridgeProxy(chain chain.Chain, signerAddr common.Address) (bridgeTypes.Proxy, error) { +// NewBridgeProxy creates a new bridge proxy for the given chain. +// We need signer address to obtain the nonce for the signer when forming a new transaction. +func NewBridgeProxy(chain chain.Evm, signerAddr common.Address) (bridgeTypes.Proxy, error) { bridgeAbi, err := abi.JSON(strings.NewReader(contracts.BridgeMetaData.ABI)) if err != nil { return nil, errors.Wrap(err, "failed to parse bridge ABI") @@ -48,7 +54,7 @@ func NewBridgeProxy(chain chain.Chain, signerAddr common.Address) (bridgeTypes.P return nil, errors.Wrap(err, "failed to get signer nonce") } - return &bridgeProxy{ + return &proxy{ chain: chain, contractABI: bridgeAbi, depositEvent: depositEvent, @@ -58,10 +64,26 @@ func NewBridgeProxy(chain chain.Chain, signerAddr common.Address) (bridgeTypes.P }, nil } -func (p *bridgeProxy) IsDepositLog(log *types.Log) bool { - if log == nil || log.Topics == nil { +func (p *proxy) Type() bridgeTypes.ChainType { + return bridgeTypes.ChainTypeEVM +} + +func (p *proxy) isDepositLog(log *types.Log) bool { + if log == nil || len(log.Topics) == 0 { return false } return bytes.Equal(log.Topics[0].Bytes(), p.depositEvent.ID.Bytes()) && len(log.Topics) == 2 } + +func (p *proxy) AddressValid(addr string) bool { + return common.IsHexAddress(addr) && common.HexToAddress(addr) != (common.Address{}) +} + +func (p *proxy) SendBitcoins(map[string]*big.Int) (txHash string, err error) { + return "", bridgeTypes.ErrNotImplemented +} + +func (p *proxy) TransactionHashValid(hash string) bool { + return txHashPattern.MatchString(hash) +} diff --git a/internal/bridge/proxy/evm/transaction.go b/internal/bridge/proxy/evm/transaction.go new file mode 100644 index 0000000..0429ec3 --- /dev/null +++ b/internal/bridge/proxy/evm/transaction.go @@ -0,0 +1,59 @@ +package evm + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + ethTypes "github.com/ethereum/go-ethereum/core/types" + bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/pkg/errors" +) + +func (p *proxy) GetTransactionReceipt(txHash common.Hash) (*types.Receipt, error) { + ctx := context.Background() + tx, pending, err := p.chain.Rpc.TransactionByHash(ctx, txHash) + if err != nil { + if err.Error() == "not found" { + return nil, bridgeTypes.ErrTxNotFound + } + + return nil, errors.Wrap(err, "failed to get transaction by hash") + } + if pending { + return nil, bridgeTypes.ErrTxPending + } + + receipt, err := p.chain.Rpc.TransactionReceipt(context.Background(), tx.Hash()) + if err != nil { + return nil, errors.Wrap(err, "failed to get tx receipt") + } + if receipt == nil { + return nil, errors.New("receipt is nil") + } + + return receipt, nil +} + +func (p *proxy) GetTransactionStatus(txHash string) (bridgeTypes.TransactionStatus, error) { + receipt, err := p.GetTransactionReceipt(common.HexToHash(txHash)) + if err != nil { + if errors.Is(err, bridgeTypes.ErrTxPending) { + return bridgeTypes.TransactionStatusPending, nil + } + if errors.Is(err, bridgeTypes.ErrTxNotFound) { + return bridgeTypes.TransactionStatusNotFound, nil + } + + return bridgeTypes.TransactionStatusUnknown, errors.Wrap(err, "failed to get transaction receipt") + } + + switch receipt.Status { + case ethTypes.ReceiptStatusSuccessful: + return bridgeTypes.TransactionStatusSuccessful, nil + case ethTypes.ReceiptStatusFailed: + return bridgeTypes.TransactionStatusFailed, nil + default: + return bridgeTypes.TransactionStatusUnknown, nil + } +} diff --git a/internal/bridge/evm/withdraw.go b/internal/bridge/proxy/evm/withdraw.go similarity index 60% rename from internal/bridge/evm/withdraw.go rename to internal/bridge/proxy/evm/withdraw.go index 2484a84..7efb448 100644 --- a/internal/bridge/evm/withdraw.go +++ b/internal/bridge/proxy/evm/withdraw.go @@ -5,41 +5,27 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" "github.com/hyle-team/bridgeless-signer/internal/data" - "github.com/pkg/errors" "math/big" ) -func (p *bridgeProxy) FormWithdrawalTransaction(data data.DepositData) (*types.Transaction, error) { - if data.DestinationChainId == nil || data.DestinationChainId.String() != p.chain.Id.String() { - return nil, errors.New("invalid destination chain id") - } - - if !common.IsHexAddress(data.DestinationAddress) { - return nil, bridgeTypes.ErrInvalidReceiverAddress - } - - if data.DestinationTokenAddress == nil { - return nil, bridgeTypes.ErrDestinationTokenAddressRequired - } - +func (p *proxy) FormWithdrawalTransaction(data data.DepositData) (*types.Transaction, error) { // transact opts prevent the transaction from being sent to // the network, returning the transaction object only return p.bridgeContract.BridgeOut( bridgeOutTransactOpts(p.getTransactionNonce()), - *data.DestinationTokenAddress, + data.DestinationTokenAddress, common.HexToAddress(data.DestinationAddress), data.Amount, data.OriginTxId(), ) } -func (p *bridgeProxy) SendWithdrawalTransaction(signedTx *types.Transaction) error { +func (p *proxy) SendWithdrawalTransaction(signedTx *types.Transaction) error { return p.chain.Rpc.SendTransaction(context.Background(), signedTx) } -func (p *bridgeProxy) getTransactionNonce() *big.Int { +func (p *proxy) getTransactionNonce() *big.Int { p.nonceM.Lock() defer p.nonceM.Unlock() diff --git a/internal/bridge/proxy/repo.go b/internal/bridge/proxy/repo.go new file mode 100644 index 0000000..54580df --- /dev/null +++ b/internal/bridge/proxy/repo.go @@ -0,0 +1,63 @@ +package proxy + +import ( + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/hyle-team/bridgeless-signer/internal/bridge/chain" + "github.com/hyle-team/bridgeless-signer/internal/bridge/proxy/btc" + "github.com/hyle-team/bridgeless-signer/internal/bridge/proxy/evm" + bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/pkg/errors" +) + +type proxiesRepository struct { + proxies map[string]bridgeTypes.Proxy +} + +func NewProxiesRepository(chains []chain.Chain, signer common.Address) (proxyRepo bridgeTypes.ProxiesRepository, err error) { + proxiesMap := make(map[string]bridgeTypes.Proxy) + + for _, ch := range chains { + var proxy bridgeTypes.Proxy + + switch ch.Type { + case bridgeTypes.ChainTypeEVM: + var evmChain chain.Evm + evmChain, err = ch.Evm() + if err != nil { + return nil, errors.Wrap(err, "failed to init evm chain") + } + proxy, err = evm.NewBridgeProxy(evmChain, signer) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to create proxy for chain %s", ch.Id)) + } + case bridgeTypes.ChainTypeBitcoin: + var bitcoinChain chain.Bitcoin + bitcoinChain, err = ch.Bitcoin() + if err != nil { + return nil, errors.Wrap(err, "failed to init bitcoin") + } + proxy = btc.NewBridgeProxy(bitcoinChain) + default: + return nil, errors.Errorf("unknown chain type %s", ch.Type) + } + + proxiesMap[ch.Id] = proxy + } + + return &proxiesRepository{proxies: proxiesMap}, nil +} + +func (p proxiesRepository) Proxy(chainId string) (bridgeTypes.Proxy, error) { + proxy, ok := p.proxies[chainId] + if !ok { + return nil, bridgeTypes.ErrChainNotSupported + } + + return proxy, nil +} + +func (p proxiesRepository) SupportsChain(chainId string) bool { + _, ok := p.proxies[chainId] + return ok +} diff --git a/internal/bridge/types/evm.go b/internal/bridge/types/evm.go deleted file mode 100644 index b520325..0000000 --- a/internal/bridge/types/evm.go +++ /dev/null @@ -1,32 +0,0 @@ -package types - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/hyle-team/bridgeless-signer/internal/data" - "github.com/pkg/errors" -) - -var ( - ErrChainNotSupported = errors.New("chain not supported") - ErrTxPending = errors.New("transaction is pending") - ErrTxFailed = errors.New("transaction failed") - ErrTxNotFound = errors.New("transaction not found") - ErrDepositNotFound = errors.New("deposit not found") - ErrTxNotConfirmed = errors.New("transaction not confirmed") - ErrInvalidReceiverAddress = errors.New("invalid receiver address") - ErrDestinationTokenAddressRequired = errors.New("destination token address is required") -) - -type Proxy interface { - GetDepositData(id data.DepositIdentifier) (*data.DepositData, error) - IsDepositLog(log *types.Log) bool - GetTransactionReceipt(txHash common.Hash) (*types.Receipt, error) - FormWithdrawalTransaction(data data.DepositData) (*types.Transaction, error) - SendWithdrawalTransaction(signedTx *types.Transaction) error -} - -type ProxiesRepository interface { - Proxy(chainId string) (Proxy, error) - SupportsChain(chainId string) bool -} diff --git a/internal/bridge/types/processor.go b/internal/bridge/types/processor.go index 65203e4..2a7e18b 100644 --- a/internal/bridge/types/processor.go +++ b/internal/bridge/types/processor.go @@ -18,9 +18,19 @@ type GetDepositRequest struct { type FormWithdrawalRequest struct { DepositDbId int64 + Destination ChainType Data data.DepositData } +type BitcoinWithdrawalRequest struct { + DepositDbId int64 + Data data.DepositData +} + +func (b BitcoinWithdrawalRequest) Id() int64 { + return b.DepositDbId +} + type SubmitTransactionRequest struct { DepositDbId int64 } diff --git a/internal/bridge/types/proxy.go b/internal/bridge/types/proxy.go new file mode 100644 index 0000000..44ceb90 --- /dev/null +++ b/internal/bridge/types/proxy.go @@ -0,0 +1,69 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/core/types" + "github.com/hyle-team/bridgeless-signer/internal/data" + "github.com/pkg/errors" + "math/big" +) + +var ( + ErrChainNotSupported = errors.New("chain not supported") + ErrTxPending = errors.New("transaction is pending") + ErrTxFailed = errors.New("transaction failed") + ErrTxNotFound = errors.New("transaction not found") + ErrDepositNotFound = errors.New("deposit not found") + ErrTxNotConfirmed = errors.New("transaction not confirmed") + ErrInvalidReceiverAddress = errors.New("invalid receiver address") + ErrInvalidDepositedAmount = errors.New("invalid deposited amount") + ErrNotImplemented = errors.New("not implemented") + ErrInvalidScriptPubKey = errors.New("invalid script pub key") +) + +type ChainType string + +const ( + ChainTypeEVM ChainType = "evm" + ChainTypeBitcoin ChainType = "bitcoin" + ChainTypeOther ChainType = "other" +) + +func (c ChainType) Validate() error { + switch c { + case ChainTypeEVM, ChainTypeBitcoin, ChainTypeOther: + return nil + default: + return errors.New("invalid chain type") + } +} + +type TransactionStatus int8 + +const ( + TransactionStatusPending TransactionStatus = iota + TransactionStatusSuccessful + TransactionStatusFailed + TransactionStatusNotFound + TransactionStatusUnknown +) + +type Proxy interface { + Type() ChainType + GetTransactionStatus(txHash string) (TransactionStatus, error) + GetDepositData(id data.DepositIdentifier) (*data.DepositData, error) + + AddressValid(addr string) bool + TransactionHashValid(hash string) bool + + // Ethereum-specific methods + FormWithdrawalTransaction(data data.DepositData) (*types.Transaction, error) + SendWithdrawalTransaction(signedTx *types.Transaction) error + + // Bitcoin-specific methods + SendBitcoins(map[string]*big.Int) (txHash string, err error) +} + +type ProxiesRepository interface { + Proxy(chainId string) (Proxy, error) + SupportsChain(chainId string) bool +} diff --git a/internal/cli/service.go b/internal/cli/service.go index 6b13003..9feddb6 100644 --- a/internal/cli/service.go +++ b/internal/cli/service.go @@ -2,9 +2,9 @@ package cli import ( "context" + "github.com/hyle-team/bridgeless-signer/internal/bridge/proxy" "sync" - "github.com/hyle-team/bridgeless-signer/internal/bridge/evm" bridgeprocessor "github.com/hyle-team/bridgeless-signer/internal/bridge/processor" "github.com/hyle-team/bridgeless-signer/internal/config" coreconnector "github.com/hyle-team/bridgeless-signer/internal/connectors/core" @@ -23,7 +23,7 @@ func RunService(ctx context.Context, cfg config.Config) error { rabbitCfg = cfg.RabbitMQConfig() ) - proxiesRepo, err := evm.NewProxiesRepository(cfg.Chains(), serviceSigner.Address()) + proxiesRepo, err := proxy.NewProxiesRepository(cfg.Chains(), serviceSigner.Address()) if err != nil { return errors.Wrap(err, "failed to create proxiesRepo repository") } @@ -37,6 +37,7 @@ func RunService(ctx context.Context, cfg config.Config) error { core.RunServer(ctx, &wg, cfg, proxiesRepo, producer) core.RunConsumers(ctx, &wg, cfg, producer, processor) + wg.Wait() return ctx.Err() diff --git a/internal/config/main.go b/internal/config/main.go index c7cd85b..c92f9ca 100644 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -1,7 +1,7 @@ package config import ( - "github.com/hyle-team/bridgeless-signer/internal/bridge/evm/chain" + "github.com/hyle-team/bridgeless-signer/internal/bridge/chain" "github.com/hyle-team/bridgeless-signer/internal/bridge/signer" core "github.com/hyle-team/bridgeless-signer/internal/connectors/core/config" api "github.com/hyle-team/bridgeless-signer/internal/core/api/config" diff --git a/internal/connectors/core/main.go b/internal/connectors/core/main.go index a9b198e..575924f 100644 --- a/internal/connectors/core/main.go +++ b/internal/connectors/core/main.go @@ -15,7 +15,6 @@ import ( pkgTypes "github.com/hyle-team/bridgeless-signer/pkg/types" "github.com/pkg/errors" "google.golang.org/grpc" - "math/big" "strings" txclient "github.com/cosmos/cosmos-sdk/types/tx" @@ -53,14 +52,14 @@ func NewConnector(conn *grpc.ClientConn, settings ConnectorSettings) *Connector } func (c *Connector) GetDestinationTokenAddress( - srcChainId *big.Int, + srcChainId string, srcTokenAddr common.Address, - dstChainId *big.Int, + dstChainId string, ) (common.Address, error) { req := bridgetypes.QueryGetTokenPair{ - SrcChain: srcChainId.String(), + SrcChain: srcChainId, SrcAddress: strings.ToLower(srcTokenAddr.String()), - DstChain: dstChainId.String(), + DstChain: dstChainId, } resp, err := c.bridger.GetTokenPair(context.Background(), &req) diff --git a/internal/core/api/handler/check.go b/internal/core/api/handler/check.go index 979d8a3..b2f7d7b 100644 --- a/internal/core/api/handler/check.go +++ b/internal/core/api/handler/check.go @@ -2,14 +2,10 @@ package handler import ( "context" - - "github.com/ethereum/go-ethereum/common" - ethTypes "github.com/ethereum/go-ethereum/core/types" bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" "github.com/hyle-team/bridgeless-signer/internal/data" "github.com/hyle-team/bridgeless-signer/pkg/types" - "github.com/pkg/errors" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -42,25 +38,21 @@ func (h *ServiceHandler) CheckWithdrawal(_ context.Context, request *types.Check return nil, ErrInternal // should not happen if the chain is supported, but just in case } - receipt, err := proxy.GetTransactionReceipt(common.HexToHash(*tx.WithdrawalTxHash)) + st, err := proxy.GetTransactionStatus(*tx.WithdrawalTxHash) if err != nil { - // omitting only pending txs - if !errors.Is(err, bridgeTypes.ErrTxPending) { - // if the tx is still pending, we return the same status - // otherwise, render error - h.logger.WithError(err).Error("failed to get tx receipt") - return nil, ErrInternal - } - } else { - switch receipt.Status { - case ethTypes.ReceiptStatusFailed: + h.logger.WithError(err).Error("failed to get tx receipt") + return nil, ErrInternal + } + + if st != bridgeTypes.TransactionStatusPending { + switch st { + case bridgeTypes.TransactionStatusFailed: tx.Status = types.WithdrawalStatus_TX_FAILED - case ethTypes.ReceiptStatusSuccessful: + case bridgeTypes.TransactionStatusSuccessful: tx.Status = types.WithdrawalStatus_TX_SUCCESSFUL } - // updating in the db - if err = dbconn.UpdateWithdrawalStatus(tx.Id, tx.Status); err != nil { + if err = dbconn.UpdateWithdrawalStatus(tx.Status, tx.Id); err != nil { h.logger.WithError(err).Error("failed to update transaction status") return nil, ErrInternal } diff --git a/internal/core/api/handler/submit.go b/internal/core/api/handler/submit.go index 9ae9a0c..254be62 100644 --- a/internal/core/api/handler/submit.go +++ b/internal/core/api/handler/submit.go @@ -34,7 +34,7 @@ func (h *ServiceHandler) SubmitWithdrawal(_ context.Context, request *types.With } deposit.Status = types.WithdrawalStatus_REPROCESSING - if err = dbconn.UpdateWithdrawalStatus(deposit.Id, deposit.Status); err != nil { + if err = dbconn.UpdateWithdrawalStatus(deposit.Status, deposit.Id); err != nil { h.logger.WithError(err).Error("failed to update transaction status") return ErrInternal } diff --git a/internal/core/api/handler/validation.go b/internal/core/api/handler/validation.go index b4c4767..a01c8da 100644 --- a/internal/core/api/handler/validation.go +++ b/internal/core/api/handler/validation.go @@ -1,17 +1,15 @@ package handler import ( - "regexp" + bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + "github.com/pkg/errors" "strconv" "strings" validation "github.com/go-ozzo/ozzo-validation" "github.com/hyle-team/bridgeless-signer/pkg/types" - "gitlab.com/distributed_lab/logan/v3/errors" ) -var txHashPattern = regexp.MustCompile(`^0x[0-9a-fA-F]{64}$`) - func (h *ServiceHandler) ValidateWithdrawalRequest(request *types.WithdrawalRequest) error { if request == nil { return errors.New("request is not provided") @@ -23,18 +21,25 @@ func (h *ServiceHandler) ValidateWithdrawalRequest(request *types.WithdrawalRequ } err := validation.Errors{ - "tx_hash": validation.Validate(deposit.TxHash, validation.Required, validation.Match(txHashPattern)), + "tx_hash": validation.Validate(deposit.TxHash, validation.Required), "tx_event_index": validation.Validate(deposit.TxEventIndex, validation.Min(0)), "chain_id": validation.Validate(deposit.ChainId, validation.Required), }.Filter() - if err == nil { - if !h.proxies.SupportsChain(deposit.ChainId) { - return ErrChainNotSupported + proxy, err := h.proxies.Proxy(deposit.ChainId) + if err != nil { + if errors.Is(err, bridgeTypes.ErrChainNotSupported) { + return err } + + return errors.Wrap(err, "failed to get proxy") + } + + if !proxy.TransactionHashValid(deposit.TxHash) { + return validation.Errors{"tx_hash": errors.New("invalid transaction hash")} } - return err + return nil } func (h *ServiceHandler) CheckWithdrawalRequest(request *types.CheckWithdrawalRequest) (*types.WithdrawalRequest, error) { diff --git a/internal/core/main.go b/internal/core/main.go index e177eba..96792cc 100644 --- a/internal/core/main.go +++ b/internal/core/main.go @@ -74,7 +74,7 @@ func RunConsumers( } } - wg.Add(1) + wg.Add(2) go func() { defer wg.Done() @@ -91,6 +91,23 @@ func RunConsumers( logger.WithError(err).Error(fmt.Sprintf("failed to consume for %s", consumer.SubmitTransactionConsumerPrefix)) } }() + go func() { + defer wg.Done() + + logger.Info(fmt.Sprintf("starting batch consumer %s...", consumer.SubmitBitcoinWithdrawalConsumerPrefix)) + err := consumer.NewBatch[bridgeTypes.BitcoinWithdrawalRequest]( + rabbitCfg.NewChannel(), + consumer.SubmitBitcoinWithdrawalConsumerPrefix, + logger.WithField("consumer", consumer.SubmitBitcoinWithdrawalConsumerPrefix), + consumerProcessors.NewSubmitBitcoinWithdrawalHandler(processor, producer), + producer, + rabbitCfg.BitcoinSubmitterOpts, + ).Consume(ctx, rabbitTypes.SubmitBitcoinWithdrawalQueue) + if err != nil { + logger.WithError(err).Error(fmt.Sprintf("failed to consume for %s", consumer.SubmitBitcoinWithdrawalConsumerPrefix)) + } + }() + } func RunServer( diff --git a/internal/core/rabbitmq/config/types.go b/internal/core/rabbitmq/config/types.go index 0dc8257..a4123a2 100644 --- a/internal/core/rabbitmq/config/types.go +++ b/internal/core/rabbitmq/config/types.go @@ -9,10 +9,11 @@ import ( ) type Config struct { - Connection *amqp.Connection `fig:"url,required"` - ConsumerInstances uint `fig:"consumer_instances,required"` - ResendParams ResendParams `fig:"resend_params,required"` - TxSubmitterOpts BatchConsumingOpts `fig:"tx_submitter,required"` + Connection *amqp.Connection `fig:"url,required"` + ConsumerInstances uint `fig:"consumer_instances,required"` + ResendParams ResendParams `fig:"resend_params,required"` + TxSubmitterOpts BatchConsumingOpts `fig:"tx_submitter,required"` + BitcoinSubmitterOpts BatchConsumingOpts `fig:"bitcoin_submitter,required"` } type BatchConsumingOpts struct { diff --git a/internal/core/rabbitmq/consumer/batch.go b/internal/core/rabbitmq/consumer/batch.go index 6ae8146..2afed5d 100644 --- a/internal/core/rabbitmq/consumer/batch.go +++ b/internal/core/rabbitmq/consumer/batch.go @@ -12,7 +12,8 @@ import ( ) const ( - SubmitTransactionConsumerPrefix = "submit_transaction_consumer" + SubmitTransactionConsumerPrefix = "submit_transaction_consumer" + SubmitBitcoinWithdrawalConsumerPrefix = "submit_bitcoin_withdrawal_consumer" ) type amqpParsedEntry[T rabbitTypes.Identifiable] struct { diff --git a/internal/core/rabbitmq/consumer/processors/get_deposit.go b/internal/core/rabbitmq/consumer/processors/get_deposit.go index a968198..cf51f45 100644 --- a/internal/core/rabbitmq/consumer/processors/get_deposit.go +++ b/internal/core/rabbitmq/consumer/processors/get_deposit.go @@ -2,6 +2,7 @@ package processors import ( "encoding/json" + "fmt" "github.com/hyle-team/bridgeless-signer/internal/bridge/processor" bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" @@ -48,8 +49,20 @@ func (h *GetDepositHandler) ProcessDelivery(delivery amqp.Delivery) (reprocessab return reprocessable, rprFailCallback, errors.Wrap(err, "failed to process get deposit request") } - if err = h.producer.SendFormWithdrawalRequest(*withdrawReq); err != nil { - return true, rprFailCallback, errors.Wrap(err, "failed to send form withdraw request") + switch withdrawReq.Destination { + case bridgeTypes.ChainTypeEVM: + if err = h.producer.SendFormWithdrawalRequest(*withdrawReq); err != nil { + return true, rprFailCallback, errors.Wrap(err, "failed to send form withdraw request") + } + case bridgeTypes.ChainTypeBitcoin: + if err = h.producer.SendSubmitBitcoinWithdrawalRequest(bridgeTypes.BitcoinWithdrawalRequest{ + DepositDbId: request.DepositDbId, + Data: withdrawReq.Data, + }); err != nil { + return true, rprFailCallback, errors.Wrap(err, "failed to send submit withdraw request") + } + default: + return false, nil, errors.New(fmt.Sprintf("invalid destination type: %v", withdrawReq.Destination)) } return false, nil, nil diff --git a/internal/core/rabbitmq/consumer/processors/submit_bitcoin_withdrawal.go b/internal/core/rabbitmq/consumer/processors/submit_bitcoin_withdrawal.go new file mode 100644 index 0000000..51f2b4b --- /dev/null +++ b/internal/core/rabbitmq/consumer/processors/submit_bitcoin_withdrawal.go @@ -0,0 +1,55 @@ +package processors + +import ( + "github.com/hyle-team/bridgeless-signer/internal/bridge/processor" + bridgeTypes "github.com/hyle-team/bridgeless-signer/internal/bridge/types" + rabbitTypes "github.com/hyle-team/bridgeless-signer/internal/core/rabbitmq/types" + "github.com/pkg/errors" +) + +type SubmitBitcoinWithdrawalHandler struct { + processor *processor.Processor + producer rabbitTypes.Producer +} + +func NewSubmitBitcoinWithdrawalHandler( + processor *processor.Processor, + producer rabbitTypes.Producer, +) rabbitTypes.BatchProcessor[bridgeTypes.BitcoinWithdrawalRequest] { + return &SubmitBitcoinWithdrawalHandler{ + processor: processor, + producer: producer, + } +} + +func (h *SubmitBitcoinWithdrawalHandler) ProcessBatch(batch []bridgeTypes.BitcoinWithdrawalRequest) (reprocessable bool, rprFailCallback func(ids ...int64) error, err error) { + if len(batch) == 0 { + return false, nil, nil + } + + defer func() { + if reprocessable { + rprFailCallback = func(ids ...int64) error { + return errors.Wrap( + h.processor.SetWithdrawStatusFailed(ids...), + "failed to set withdraw status failed", + ) + } + } + + }() + + reprocessable, err = h.processor.SendBitcoinWithdrawals(batch...) + if err != nil { + return reprocessable, rprFailCallback, errors.Wrap(err, "failed to process send bitcoin withdrawal request") + } + + for _, entry := range batch { + submitTxReq := bridgeTypes.SubmitTransactionRequest{DepositDbId: entry.DepositDbId} + if err = h.producer.SendSubmitTransactionRequest(submitTxReq); err != nil { + return true, rprFailCallback, errors.Wrap(err, "failed to send submit transaction request") + } + } + + return false, nil, nil +} diff --git a/internal/core/rabbitmq/consumer/processors/submit_transaction.go b/internal/core/rabbitmq/consumer/processors/submit_transaction.go index 5bf7f1a..91274d5 100644 --- a/internal/core/rabbitmq/consumer/processors/submit_transaction.go +++ b/internal/core/rabbitmq/consumer/processors/submit_transaction.go @@ -20,6 +20,10 @@ func NewSubmitTransactionHandler( } func (s SubmitTransactionHandler) ProcessBatch(batch []bridgeTypes.SubmitTransactionRequest) (reprocessable bool, rprFailCallback func(ids ...int64) error, err error) { + if len(batch) == 0 { + return false, nil, nil + } + defer func() { if reprocessable { rprFailCallback = func(ids ...int64) error { diff --git a/internal/core/rabbitmq/producer/main.go b/internal/core/rabbitmq/producer/main.go index 435316e..3747390 100644 --- a/internal/core/rabbitmq/producer/main.go +++ b/internal/core/rabbitmq/producer/main.go @@ -27,6 +27,7 @@ func New(ch *amqp.Channel, resendParams config.ResendParams) (rabbitTypes.Produc rabbitTypes.FormWithdrawalQueue, rabbitTypes.SignWithdrawalQueue, rabbitTypes.SubmitWithdrawalQueue, + rabbitTypes.SubmitBitcoinWithdrawalQueue, rabbitTypes.SubmitTransactionQueue, } @@ -91,6 +92,10 @@ func (p *Producer) SendSubmitWithdrawalRequest(request bridgeTypes.WithdrawalReq return p.publish(rabbitTypes.SubmitWithdrawalQueue, request) } +func (p *Producer) SendSubmitBitcoinWithdrawalRequest(request bridgeTypes.BitcoinWithdrawalRequest) error { + return p.publish(rabbitTypes.SubmitBitcoinWithdrawalQueue, request) +} + func (p *Producer) SendSubmitTransactionRequest(request bridgeTypes.SubmitTransactionRequest) error { return p.publish(rabbitTypes.SubmitTransactionQueue, request) } diff --git a/internal/core/rabbitmq/types/main.go b/internal/core/rabbitmq/types/main.go index 66b0fcb..edd6e1c 100644 --- a/internal/core/rabbitmq/types/main.go +++ b/internal/core/rabbitmq/types/main.go @@ -24,11 +24,12 @@ const ( HeaderDelayKey = "delay" HeaderRetryCountKey = "x-retry-count" - GetDepositQueue = "get-deposit-queue" - FormWithdrawalQueue = "form-withdrawal-queue" - SignWithdrawalQueue = "sign-withdrawal-queue" - SubmitWithdrawalQueue = "submit-withdrawal-queue" - SubmitTransactionQueue = "submit-transaction-queue" + GetDepositQueue = "get-deposit-queue" + FormWithdrawalQueue = "form-withdrawal-queue" + SignWithdrawalQueue = "sign-withdrawal-queue" + SubmitWithdrawalQueue = "submit-withdrawal-queue" + SubmitBitcoinWithdrawalQueue = "submit-bitcoin-withdrawal-queue" + SubmitTransactionQueue = "submit-transaction-queue" ) var ErrorMaxResendReached = errors.New("max resend count reached") @@ -38,6 +39,7 @@ type Producer interface { SendFormWithdrawalRequest(request bridgeTypes.FormWithdrawalRequest) error SendSignWithdrawalRequest(request bridgeTypes.WithdrawalRequest) error SendSubmitWithdrawalRequest(request bridgeTypes.WithdrawalRequest) error + SendSubmitBitcoinWithdrawalRequest(request bridgeTypes.BitcoinWithdrawalRequest) error SendSubmitTransactionRequest(request bridgeTypes.SubmitTransactionRequest) error DeliveryResender } diff --git a/internal/data/deposits.go b/internal/data/deposits.go index 9eac8fa..d861f02 100644 --- a/internal/data/deposits.go +++ b/internal/data/deposits.go @@ -19,12 +19,18 @@ type DepositsQ interface { Select(selector DepositsSelector) ([]Deposit, error) Get(identifier DepositIdentifier) (*Deposit, error) SetDepositData(data DepositData) error - UpdateWithdrawalStatus(id int64, status types.WithdrawalStatus) error + UpdateWithdrawalStatus(status types.WithdrawalStatus, ids ...int64) error UpdateSubmitStatus(status types.SubmitWithdrawalStatus, ids ...int64) error - SetWithdrawalTx(depositId int64, txHash, chainId string) error + SetWithdrawalTxs(txs ...WithdrawalTx) error Transaction(f func() error) error } +type WithdrawalTx struct { + DepositId int64 + TxHash string + ChainId string +} + type DepositIdentifier struct { TxHash string `structs:"tx_hash" db:"tx_hash"` TxEventId int `structs:"tx_event_id" db:"tx_event_id"` @@ -36,15 +42,6 @@ type DepositsSelector struct { Submitted *bool } -func (d DepositIdentifier) GetChainId() *big.Int { - id, ok := new(big.Int).SetString(d.ChainId, 10) - if !ok { - return big.NewInt(0) - } - - return id -} - func (d DepositIdentifier) String() string { return fmt.Sprintf(OriginTxIdPattern, d.TxHash, d.TxEventId, d.ChainId) } @@ -113,7 +110,7 @@ func (d Deposit) ToTransaction() bridgetypes.Transaction { tx := bridgetypes.Transaction{ DepositTxHash: d.TxHash, DepositTxIndex: uint64(d.TxEventId), - DepositChainId: d.GetChainId().String(), + DepositChainId: d.ChainId, WithdrawalTxHash: stringOrEmpty(d.WithdrawalTxHash), Depositor: stringOrEmpty(d.Depositor), Amount: stringOrEmpty(d.Amount), @@ -132,14 +129,17 @@ func (d Deposit) ToTransaction() bridgetypes.Transaction { type DepositData struct { DepositIdentifier + DestinationChainId string + + SourceAddress string + DestinationAddress string + + Amount *big.Int - DestinationChainId *big.Int - SourceAddress common.Address - DestinationAddress string - Amount *big.Int TokenAddress common.Address - DestinationTokenAddress *common.Address - Block int64 + DestinationTokenAddress common.Address + + Block int64 } func (d DepositData) OriginTxId() string { diff --git a/internal/data/pg/deposits.go b/internal/data/pg/deposits.go index 25fce8e..0726ab0 100644 --- a/internal/data/pg/deposits.go +++ b/internal/data/pg/deposits.go @@ -2,6 +2,8 @@ package pg import ( "database/sql" + "github.com/ethereum/go-ethereum/common" + "github.com/lib/pq" "strings" "github.com/Masterminds/squirrel" @@ -41,14 +43,37 @@ func (d *depositsQ) New() data.DepositsQ { return NewDepositsQ(d.db.Clone()) } -func (d *depositsQ) SetWithdrawalTx(depositId int64, txHash, chainId string) error { - stmt := squirrel.Update(depositsTable). - Set(depositsStatus, types.WithdrawalStatus_TX_PENDING). - Set(depositsWithdrawalTxHash, txHash). - Set(depositsWithdrawalChainId, chainId). - Where(squirrel.Eq{depositsId: depositId}) +func (d *depositsQ) SetWithdrawalTxs(txs ...data.WithdrawalTx) error { + if len(txs) == 0 { + return nil + } - return d.db.Exec(stmt) + var ( + hashes = make(pq.StringArray, len(txs)) + chains = make(pq.StringArray, len(txs)) + ids = make(pq.Int64Array, len(txs)) + ) + for i, tx := range txs { + hashes[i] = strings.ToLower(tx.TxHash) + chains[i] = tx.ChainId + ids[i] = tx.DepositId + } + + const query string = ` +UPDATE deposits +SET + status = $1, + withdrawal_tx_hash = unnested_data.tx_hash, + withdrawal_chain_id = unnested_data.chain_id +FROM ( + SELECT unnest($2::text[]) as tx_hash, + unnest($3::text[]) as chain_id, + unnest($4::bigint[]) as deposit_id +) as unnested_data +WHERE deposits.id = unnested_data.deposit_id +` + + return d.db.ExecRaw(query, types.WithdrawalStatus_TX_PENDING, hashes, chains, ids) } func (d *depositsQ) Insert(deposit data.Deposit) (int64, error) { @@ -99,10 +124,10 @@ func (d *depositsQ) Select(selector data.DepositsSelector) ([]data.Deposit, erro return deposits, nil } -func (d *depositsQ) UpdateWithdrawalStatus(id int64, status types.WithdrawalStatus) error { +func (d *depositsQ) UpdateWithdrawalStatus(status types.WithdrawalStatus, ids ...int64) error { stmt := squirrel.Update(depositsTable). Set(depositsStatus, status). - Where(squirrel.Eq{depositsId: id}) + Where(squirrel.Eq{depositsId: ids}) return d.db.Exec(stmt) } @@ -117,14 +142,18 @@ func (d *depositsQ) UpdateSubmitStatus(status types.SubmitWithdrawalStatus, ids func (d *depositsQ) SetDepositData(data data.DepositData) error { fields := map[string]interface{}{ - depositsDepositor: strings.ToLower(data.SourceAddress.String()), depositsAmount: data.Amount.String(), - depositsDepositToken: strings.ToLower(data.TokenAddress.String()), depositsReceiver: strings.ToLower(data.DestinationAddress), depositsDepositBlock: data.Block, } - if data.DestinationTokenAddress != nil { + if data.TokenAddress != (common.Address{}) { + fields[depositsDepositToken] = strings.ToLower(data.TokenAddress.String()) + } + if data.SourceAddress != "" { + fields[depositsDepositor] = strings.ToLower(data.SourceAddress) + } + if data.DestinationTokenAddress != (common.Address{}) { fields[depositsWithdrawalToken] = strings.ToLower(data.DestinationTokenAddress.String()) } diff --git a/pkg/tokens/config/config.go b/pkg/tokens/config/config.go deleted file mode 100644 index 70a1a31..0000000 --- a/pkg/tokens/config/config.go +++ /dev/null @@ -1,52 +0,0 @@ -package config - -import ( - "github.com/hyle-team/bridgeless-signer/pkg/tokens" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "gitlab.com/distributed_lab/figure/v3" - "gitlab.com/distributed_lab/kit/comfig" - "gitlab.com/distributed_lab/kit/kv" -) - -type configTokenPairerConfiger struct { - once comfig.Once - getter kv.Getter -} - -func NewConfigTokenPairerConfiger(getter kv.Getter) tokens.TokenPairerConfiger { - return &configTokenPairerConfiger{ - getter: getter, - } -} - -func (c *configTokenPairerConfiger) TokenPairer() tokens.TokenPairer { - return c.once.Do(func() interface{} { - const tokenPairerConfigurerConfigKey = "tokens" - var cfg struct { - TokenPairs []TokenPairs `fig:"list,required"` - } - - if err := figure. - Out(&cfg). - With(figure.BaseHooks, figure.EthereumHooks). - From(kv.MustGetStringMap(c.getter, tokenPairerConfigurerConfigKey)). - Please(); err != nil { - panic(err) - } - - return NewTokenPairer(cfg.TokenPairs) - }).(tokens.TokenPairer) - -} - -type Token struct { - ChainId *big.Int `fig:"chain_id,required"` - Address common.Address `fig:"address,required"` -} - -type TokenPairs struct { - Token Token `fig:"token,required"` - Pairs []Token `fig:"pairs,required"` -} diff --git a/pkg/tokens/config/config_test.yaml b/pkg/tokens/config/config_test.yaml deleted file mode 100644 index 2136cd5..0000000 --- a/pkg/tokens/config/config_test.yaml +++ /dev/null @@ -1,28 +0,0 @@ -tokens: - list: - - - token: - chain_id: "80002" - address: "0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627" - pairs: - - chain_id: "1" - address: "0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627" - - chain_id: "2" - address: "0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627" - - chain_id: "3" - address: "0x0000000000000000000000000000000000000000" - - chain_id: "4" - address: "0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627" - - - token: - chain_id: "80003" - address: "0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627" - pairs: - - chain_id: "1" - address: "0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627" - - chain_id: "2" - address: "0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627" - - chain_id: "3" - address: "0x0000000000000000000000000000000000000000" - - chain_id: "4" - address: "0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627" diff --git a/pkg/tokens/config/main.go b/pkg/tokens/config/main.go deleted file mode 100644 index b77dbc1..0000000 --- a/pkg/tokens/config/main.go +++ /dev/null @@ -1,52 +0,0 @@ -package config - -import ( - "fmt" - "github.com/hyle-team/bridgeless-signer/pkg/tokens" - "math/big" - "strings" - - "github.com/ethereum/go-ethereum/common" -) - -type configTokenPairer struct { - tokenPairInfo map[string]map[string]common.Address -} - -func NewTokenPairer(tokenPairs []TokenPairs) tokens.TokenPairer { - tokenPairInfo := make(map[string]map[string]common.Address) - for _, token := range tokenPairs { - pairs := make(map[string]common.Address) - for _, pair := range token.Pairs { - pairs[pair.ChainId.String()] = pair.Address - } - - tokenPairInfo[formTokenKey(token.Token.ChainId, token.Token.Address)] = pairs - } - - return &configTokenPairer{tokenPairInfo: tokenPairInfo} -} - -func (p *configTokenPairer) GetDestinationTokenAddress( - srcChainId *big.Int, - srcTokenAddr common.Address, - dstChainId *big.Int, -) (common.Address, error) { - key := formTokenKey(srcChainId, srcTokenAddr) - - pairs, ok := p.tokenPairInfo[key] - if !ok { - return common.Address{}, tokens.ErrSourceTokenNotSupported - } - - dstTokenAddr, ok := pairs[dstChainId.String()] - if !ok { - return common.Address{}, tokens.ErrDestinationTokenNotSupported - } - - return dstTokenAddr, nil -} - -func formTokenKey(srcChainId *big.Int, srcTokenAddr common.Address) string { - return fmt.Sprintf("%s-%s", srcChainId.String(), strings.ToLower(srcTokenAddr.String())) -} diff --git a/pkg/tokens/config/main_test.go b/pkg/tokens/config/main_test.go deleted file mode 100644 index 445290c..0000000 --- a/pkg/tokens/config/main_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package config - -import ( - "errors" - "github.com/hyle-team/bridgeless-signer/pkg/tokens" - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "gitlab.com/distributed_lab/kit/kv" -) - -func Test_TokenPairer(t *testing.T) { - getter := kv.NewViperFile("config_test.yaml") - pairer := NewConfigTokenPairerConfiger(getter).TokenPairer() - - testCases := []struct { - name string - srcChainId *big.Int - srcToken common.Address - dstChainId *big.Int - expected common.Address - err error - }{ - { - name: "success for chain 80002", - srcChainId: big.NewInt(80002), - srcToken: common.HexToAddress("0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627"), - dstChainId: big.NewInt(1), - expected: common.HexToAddress("0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627"), - err: nil, - }, - { - name: "success for chain 80003", - srcChainId: big.NewInt(80003), - srcToken: common.HexToAddress("0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627"), - dstChainId: big.NewInt(3), - expected: common.HexToAddress("0x0000000000000000000000000000000000000000"), - err: nil, - }, - { - name: "src token not supported", - srcChainId: big.NewInt(80003), - srcToken: common.HexToAddress("0x555b83Ed9dd4cF8A385b6e318Fb97Cdfc320b555"), - dstChainId: big.NewInt(3), - expected: common.HexToAddress("0x0000000000000000000000000000000000000000"), - err: tokens.ErrSourceTokenNotSupported, - }, - { - name: "dst token not supported", - srcChainId: big.NewInt(80003), - srcToken: common.HexToAddress("0x9c9b83Ed9dd4cF8A385b6e318Fb97Cdfc320b627"), - dstChainId: big.NewInt(7), - expected: common.HexToAddress("0x0000000000000000000000000000000000000000"), - err: tokens.ErrDestinationTokenNotSupported, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - addr, err := pairer.GetDestinationTokenAddress(tc.srcChainId, tc.srcToken, tc.dstChainId) - if err == nil { - if addr != tc.expected { - t.Fatalf("expected address %v, got %v", tc.expected, addr) - } - } else { - if !errors.Is(err, tc.err) { - t.Fatalf("expected error %v, got %v", tc.err, err) - } - } - }) - } -} diff --git a/pkg/tokens/types.go b/pkg/tokens/types.go index 18a4d0f..04507bc 100644 --- a/pkg/tokens/types.go +++ b/pkg/tokens/types.go @@ -1,10 +1,8 @@ package tokens import ( - "github.com/pkg/errors" - "math/big" - "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" ) var ( @@ -19,8 +17,8 @@ type TokenPairerConfiger interface { type TokenPairer interface { GetDestinationTokenAddress( - srcChainId *big.Int, + srcChainId string, srcTokenAddr common.Address, - dstChainId *big.Int, + dstChainId string, ) (common.Address, error) }