Commit fd48574b authored by luxq's avatar luxq

add backend code

parent e15355cc
.idea
build
data
FROM golang:1.24-alpine AS build
# Set up dependencies
ENV PACKAGES build-base
# Install dependencies
RUN apk add --update $PACKAGES
# Add source files
WORKDIR /build
COPY ./ /build/bridge-backend
RUN cd /build/bridge-backend && go mod tidy && go build -ldflags="-s -w" -o /tmp/bridgebackend ./cmd/backend
FROM alpine
WORKDIR /app
COPY ./config.toml /app/config.toml
COPY --from=build /tmp/bridgebackend /usr/bin/bridgebackend
EXPOSE 8080
\ No newline at end of file
.PHONY: default all clean
GOBIN = $(shell pwd)/build/bin
BUILD_FLAGS = -ldflags "-s -w"
default: bridgebackend
bridgebackend:
go build $(BUILD_FLAGS) -v -o=${GOBIN}/$@ ./cmd/backend
docker:
docker build --no-cache -t bridgebackend:latest -f ./Dockerfile .
docker tag bridgebackend:latest bridgebackend:$${TAG:-latest}
.PHONY: docker
start:
docker compose up -d
.PHONY: start
stop:
docker compose down
.PHONY: stop
clean:stop
sudo rm -rf data
.PHONY: clean
\ No newline at end of file
# bridge-backend # How to run
1. make docker
2. modify config.yml for chain and mysql password.
3. modify docker-compose.yml for mysql password.
4. docker compose up -d
5. docker compose down
# How to use
1. make docker
2. prepare a validator private key, write it to `val.pk`
3. prepare a aes key.
```
docker run -it -v "${PWD}:/app" --rm token-bridge:latest validator genkey --output aes.key
```
4. crypt the private key with aes key, named it as validator.fpk
```
docker run -it -v "${PWD}:/app" --rm token-bridge:latest validator encrypt --in val.pk --out val.fpk --aes aes.key
```
5. generate one-time-key for the validator.
```
docker run -it -v "${PWD}:/app" --rm token-bridge:latest validator onetime --in val.fpk --out val.otp
```
6. keep the `val.pk` safe and delete `val.pk`, `val.fpk` from the server. Must keep `aes.key` and `val.otp` exist.
7. start the validator service.
```
docker compose up -d
```
Tips: once you need restart the validator service, you need to generate a new one-time-key with the same command in step 5.
package chain
import (
"code.wuban.net.cn/movabridge/bridge-backend/config"
"code.wuban.net.cn/movabridge/bridge-backend/contract/bridge"
"code.wuban.net.cn/movabridge/bridge-backend/dao"
"context"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
log "github.com/sirupsen/logrus"
"math/big"
"strings"
"sync"
"time"
)
var _ dao.ChainInterface = (*ChainSync)(nil)
type ChainSync struct {
chain *config.ChainConfig
d *dao.Dao
name string
heightKey string
bridgeCa *bridge.BridgeContract
quit chan struct{}
stopOnce sync.Once
wg sync.WaitGroup
syncmode bool
}
func (s *ChainSync) ParseTransferOut(log types.Log) (*bridge.BridgeContractTransferOut, error) {
return s.bridgeCa.ParseTransferOut(log)
}
func (s *ChainSync) ParseTransferIn(log types.Log) (*bridge.BridgeContractTransferIn, error) {
return s.bridgeCa.ParseTransferIn(log)
}
func (s *ChainSync) ParseTransferInExecution(log types.Log) (*bridge.BridgeContractTransferInExecution, error) {
return s.bridgeCa.ParseTransferInExecution(log)
}
func (s *ChainSync) ParseTransferInRejection(log types.Log) (*bridge.BridgeContractTransferInRejection, error) {
return s.bridgeCa.ParseTransferInRejection(log)
}
func (s *ChainSync) ParseTransferInConfirmation(log types.Log) (*bridge.BridgeContractTransferInConfirmation, error) {
return s.bridgeCa.ParseTransferInConfirmation(log)
}
func (s *ChainSync) ParseTokenConfigChanged(log types.Log) (*bridge.BridgeContractTokenConfigChanged, error) {
return s.bridgeCa.ParseTokenConfigChanged(log)
}
func (s *ChainSync) GetReceiveToken(token common.Address, toChainId int64) (string, error) {
callOpt := &bind.CallOpts{
BlockNumber: nil,
From: common.HexToAddress(s.chain.BridgeContract),
Context: context.TODO(),
}
param, err := s.bridgeCa.OutConfiguration(callOpt, token, big.NewInt(toChainId))
if err != nil {
return "", err
}
return strings.ToLower(param.ReceiveToken.Hex()), nil
}
func NewChainSync(_chain *config.ChainConfig, _d *dao.Dao) (sync *ChainSync) {
bridgeCa, err := bridge.NewBridgeContract(common.HexToAddress(_chain.BridgeContract), _d.ChainClient(_chain.ChainId))
if err != nil {
panic(err)
}
sync = &ChainSync{
chain: _chain,
d: _d,
name: _chain.Name,
heightKey: fmt.Sprintf("%d_%s", _chain.ChainId, "height"),
bridgeCa: bridgeCa,
quit: make(chan struct{}),
syncmode: true,
}
return sync
}
func (s *ChainSync) loop() {
defer s.wg.Done()
finishedHeight, err := s.d.GetStorageHeight(s.heightKey)
if err != nil {
if err == dao.ErrRecordNotFound {
finishedHeight = s.chain.InitialHeight
} else {
log.WithField("chain", s.name).WithField("chain", s.name).WithError(err).Error("get last block height")
return
}
}
var latestHeight int64
var beginHeight = finishedHeight + 1
log.WithField("chain", s.name).WithField("begin height", beginHeight).Info("last backend block height")
tm := time.NewTicker(time.Second)
defer tm.Stop()
for {
select {
case <-s.quit:
log.WithField("chain", s.name).Info("chain sync stopped")
return
case <-tm.C:
var endHeight = beginHeight + int64(s.chain.BatchBlock)
latestHeight, err = s.d.GetBlockHeight(s.chain, s.chain.BehindBlock)
if err != nil {
log.WithField("chain", s.name).WithError(err).Error("get latest block height")
continue
}
if latestHeight <= beginHeight {
s.syncmode = false
continue
}
if beginTime, err := s.d.GetBlockTime(s.chain, beginHeight); err == nil {
blockTime := time.Unix(int64(beginTime), 0)
if time.Since(blockTime) < time.Minute {
s.syncmode = false
} else {
s.syncmode = true
}
log.WithField("chain", s.name).WithFields(log.Fields{
"begin height": beginHeight,
"block time": blockTime,
"sync mode": s.syncmode,
"sinceTime": time.Since(blockTime).String(),
}).Debug("begin block time")
}
if latestHeight < endHeight {
endHeight = latestHeight
}
if err := s.SyncLogs(beginHeight, endHeight); err != nil {
log.WithField("chain", s.name).WithFields(log.Fields{
"begin height": beginHeight,
"end height": endHeight,
}).WithError(err).Error("sync logs failed")
continue
}
if err = s.d.SetStorageHeight(s.heightKey, endHeight); err != nil {
log.WithField("chain", s.name).WithError(err).Error("set last block height")
}
log.WithField("chain", s.name).WithFields(log.Fields{
"begin height": beginHeight,
"end height": endHeight,
"latest height": latestHeight,
"diff height": latestHeight - endHeight,
}).Info("backend block")
beginHeight = endHeight + 1
}
}
}
func (s *ChainSync) Name() string {
return s.name
}
func (s *ChainSync) GetChain() *config.ChainConfig {
return s.chain
}
func (s *ChainSync) Start() {
s.wg.Add(1)
go s.loop()
}
func (s *ChainSync) Stop() {
close(s.quit)
s.wg.Wait()
}
func (s *ChainSync) SyncLogs(beginHeight, endHeight int64) error {
if endHeight < 0 {
return nil
}
if beginHeight < 0 {
beginHeight = 0
}
topics := []string{
dao.TransferOutEvent.ID.Hex(),
dao.TransferInEvent.ID.Hex(),
//dao.TransferInConfirmationEvent.ID.Hex(),
dao.TransferInRejectionEvent.ID.Hex(),
dao.TransferInExecutionEvent.ID.Hex(),
dao.TokenConfigChangedEvent.ID.Hex(),
}
logs, err := s.d.GetLogs(s.chain, beginHeight, endHeight, topics, []string{
s.chain.BridgeContract,
})
if err != nil {
log.WithField("chain", s.name).WithFields(log.Fields{"begin": beginHeight, "end": endHeight}).WithError(err).Error("rpc: get logs")
return err
}
if len(logs) > 0 {
log.WithField("chain", s.name).WithFields(log.Fields{"begin": beginHeight, "end": endHeight}).Infof("get %d logs", len(logs))
}
return s.d.HandleEvents(s, logs)
}
func (s *ChainSync) IsSyncing() bool {
return s.syncmode
}
package chainlist
import (
"code.wuban.net.cn/movabridge/bridge-backend/types"
"encoding/json"
"errors"
log "github.com/sirupsen/logrus"
"os"
"sync"
"time"
)
// get latest chain list by download and parse https://chainlist.org/rpcs.json.
type ChainRepo struct {
repo map[int]types.ChainInfo
lock sync.RWMutex
quit chan struct{}
}
type ChainData struct {
Name string `json:"name"`
Chain string `json:"chain"`
Rpc []struct {
Url string `json:"url"`
} `json:"rpc"`
NativeCurrency struct {
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
} `json:"nativeCurrency"`
InfoURL string `json:"infoURL"`
ShortName string `json:"shortName"`
ChainId int `json:"chainId"`
NetworkId int `json:"networkId"`
Icon string `json:"icon"`
Explorers []struct {
Name string `json:"name"`
Url string `json:"url"`
Standard string `json:"standard"`
} `json:"explorers"`
}
func New(local string) *ChainRepo {
cr := &ChainRepo{
repo: make(map[int]types.ChainInfo),
quit: make(chan struct{}),
}
if newRepo, err := cr.loadChainFromFile(local); err != nil {
log.WithError(err).Errorf("load chainlist from file %s failed", local)
} else {
cr.repo = newRepo
log.Infof("load chainlist from file %s, total %d chains", local, len(cr.repo))
}
return cr
}
func (cr *ChainRepo) loadChainFromFile(fil string) (map[int]types.ChainInfo, error) {
if fil == "" {
return nil, errors.New("file name is empty")
}
data, err := os.ReadFile(fil)
if err != nil {
return nil, err
}
return cr.parseChainDataToMap(data)
}
func (cr *ChainRepo) parseChainDataToMap(data []byte) (map[int]types.ChainInfo, error) {
chainList := make([]ChainData, 0)
err := json.Unmarshal(data, &chainList)
if err != nil {
return nil, err
}
newMap := make(map[int]types.ChainInfo)
for _, cd := range chainList {
ci := types.ChainInfo{
ChainId: int64(cd.ChainId),
Chain: cd.Chain,
Name: cd.Name,
Explorer: "",
Rpc: "",
}
if len(cd.Rpc) > 0 {
ci.Rpc = cd.Rpc[0].Url
}
if len(cd.Explorers) > 0 {
ci.Explorer = cd.Explorers[0].Url
}
newMap[cd.ChainId] = ci
}
return newMap, nil
}
func (cr *ChainRepo) Get(chainId int64) (types.ChainInfo, bool) {
cr.lock.RLock()
defer cr.lock.RUnlock()
ci, ok := cr.repo[int(chainId)]
return ci, ok
}
func (cr *ChainRepo) Start() {
go cr.loop()
}
func (cr *ChainRepo) Stop() {
close(cr.quit)
}
func (cr *ChainRepo) loop() {
tc := time.NewTicker(time.Minute * 5)
defer tc.Stop()
for {
select {
case <-cr.quit:
return
case <-tc.C:
var data = []byte{}
// todo: fetch data from https://chainlist.org/rpcs.json
if len(data) == 0 {
continue
}
if newRepo, err := cr.parseChainDataToMap(data); err != nil {
log.WithError(err).Error("parse chainlist data failed")
} else {
cr.lock.Lock()
cr.repo = newRepo
cr.lock.Unlock()
log.Infof("update chainlist from remote, total %d chains", len(cr.repo))
}
}
}
}
package main
import (
"code.wuban.net.cn/movabridge/bridge-backend/chain"
"code.wuban.net.cn/movabridge/bridge-backend/chainlist"
"code.wuban.net.cn/movabridge/bridge-backend/config"
"code.wuban.net.cn/movabridge/bridge-backend/dao"
"code.wuban.net.cn/movabridge/bridge-backend/server"
"os"
"os/signal"
"syscall"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
confPath string
)
func init() {
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
}
var rootCmd = &cobra.Command{
Use: "backend",
Short: "Token bridge backend service",
Long: "A backend service for the token bridge system that syncs with multiple blockchain networks",
Run: func(cmd *cobra.Command, args []string) {
conf, err := config.New(confPath)
if err != nil {
panic(err)
}
if conf.Debug {
log.SetLevel(log.DebugLevel)
}
chainRepo := chainlist.New(conf.ChainListFile)
d, err := dao.New(conf, chainRepo)
if err != nil {
panic(err)
}
go func() {
if err := server.StartRestApi(d, conf); err != nil {
log.WithError(err).Error("start rest api")
}
}()
syncers := make([]*chain.ChainSync, 0)
for _, chainConfig := range conf.Chains {
syncer := chain.NewChainSync(chainConfig, d)
syncer.Start()
syncers = append(syncers, syncer)
d.AddSyncer(chainConfig.ChainId, syncer)
}
// Set up signal handling
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// Wait for termination signal
sig := <-sigCh
log.WithField("signal", sig.String()).Info("received termination signal, shutting down")
// Stop all chain sync instances
for _, syncer := range syncers {
syncer.Stop()
}
log.Info("graceful shutdown completed")
os.Exit(0)
},
}
func init() {
rootCmd.Flags().StringVarP(&confPath, "config", "c", "config.toml", "config file path")
}
func main() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
debug = true
chain_list_file = "rpcs.json"
[[chains]]
name = "hole"
rpc = "https://rpc.hole.bitheart.org"
initial_height = 868351
batch_block = 1000
confirm_block_count = 2
bridge_contract = "0xceEC8799139C698De532e363DA7395E25F409775"
#[[chains]]
#name = "movadev"
#rpc = "https://rpc.mova.bitheart.org"
#initial_height = 869401
#batch_block = 1000
#confirm_block_count = 2
#bridge_contract = "0xA2d532F956770611647EcBab19d87d380145d0Cf"
[mongodb]
#host = "bridgedb"
host = "localhost"
port = 27017
database = "bridge"
username = "root"
password = "XN2UARuys3zy4Oux"
[server]
listen = ":8080"
invalid_headers = [
"Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)",
]
\ No newline at end of file
package config
import (
"github.com/BurntSushi/toml"
)
type Config struct {
Debug bool
ChainListFile string `toml:"chain_list_file"`
Chains map[string]*ChainConfig `toml:"chains"`
Mongo MongoConfig `toml:"mongodb"`
Server ServerConfig `toml:"server"`
}
type ChainConfig struct {
Name string `toml:"name"`
RPC string `toml:"rpc"`
InitialHeight int64 `toml:"initial_height"`
BatchBlock int `toml:"batch_block"`
BehindBlock int `toml:"behind_block"`
BridgeContract string `toml:"bridge_contract"`
ChainId int64 `toml:"chain_id"` // Will be populated by code
}
type MySQLConfig struct {
Host string
Port int
User string
Password string
Database string
MaxConn int `toml:"max_conn"`
MaxIdleConn int `toml:"max_idle_conn"`
}
type MongoConfig struct {
Host string `toml:"host"`
Port int `toml:"port"`
User string `toml:"username"`
Password string `toml:"password"`
Database string `toml:"database"`
}
type ServerConfig struct {
Listen string
InvalidHeaders []string `toml:"invalid_headers"`
}
func New(confPath string) (*Config, error) {
var cfg Config
cfg.Chains = make(map[string]*ChainConfig)
// Parse the TOML configuration
_, err := toml.DecodeFile(confPath, &cfg)
if err != nil {
return nil, err
}
// Process the chains from array to map using name as key
var chainArray []ChainConfig
_, err = toml.DecodeFile(confPath, &struct {
Chains *[]ChainConfig `toml:"chains"`
*Config
}{
Chains: &chainArray,
Config: &cfg,
})
if err != nil {
return nil, err
}
// Convert to map for easier access
for _, chain := range chainArray {
chainCopy := chain // Create a copy to avoid pointer issues
cfg.Chains[chain.Name] = &chainCopy
}
return &cfg, nil
}
package constant
const JwtSecret = "uEj7AgDNCREwsvnTaCEtzDXt0I5eFDl8"
const (
InvalidParam = "invalid param"
UnsupportedPlatform = "unsupported platform"
InternalError = "internal error"
)
type TransferStatus int
const (
TransferChainNoProcess TransferStatus = iota
TransferChainWaitConfirm TransferStatus = 1
TransferChainExecuted TransferStatus = 2
TransferChainRejected TransferStatus = 3
)
func (ts TransferStatus) String() string {
switch ts {
case TransferChainNoProcess:
return "NoProcess"
case TransferChainWaitConfirm:
return "WaitConfirm"
case TransferChainExecuted:
return "Executed"
case TransferChainRejected:
return "Rejected"
default:
return "Unknown"
}
}
const (
ValidatorStatusNoPrecess = 0
ValidatorStatusConfirmation = 1
ValidatorStatusRejection = 2
ValidatorStatusFailure = 3
)
const (
EVENT_TRANSFER_OUT = "TransferOut"
EVENT_TRANSFER_IN = "TransferIn"
EVENT_TRANSFER_IN_CONFIRMATION = "TransferInConfirmation"
EVENT_TRANSFER_IN_REJECTION = "TransferInRejection"
EVENT_TRANSFER_IN_EXECUTION = "TransferInExecution"
EVENT_TOKENCONFIGCHANGED = "TokenConfigChanged"
)
type ValidatorOp int
func (op ValidatorOp) String() string {
switch op {
case ValidatorStatusConfirmation:
return "TransferInConfirmation"
case ValidatorStatusRejection:
return "TransferInRejection"
default:
return "Unknown"
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
package dao
import (
"code.wuban.net.cn/movabridge/bridge-backend/config"
. "code.wuban.net.cn/movabridge/bridge-backend/constant"
"code.wuban.net.cn/movabridge/bridge-backend/contract/bridge"
dbModel "code.wuban.net.cn/movabridge/bridge-backend/model/db"
"context"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
log "github.com/sirupsen/logrus"
"math/big"
"strings"
)
type ChainInterface interface {
Name() string
GetChain() *config.ChainConfig
IsSyncing() bool
ParseTransferOut(log types.Log) (*bridge.BridgeContractTransferOut, error)
ParseTransferIn(log types.Log) (*bridge.BridgeContractTransferIn, error)
ParseTransferInExecution(log types.Log) (*bridge.BridgeContractTransferInExecution, error)
ParseTransferInRejection(log types.Log) (*bridge.BridgeContractTransferInRejection, error)
ParseTransferInConfirmation(log types.Log) (*bridge.BridgeContractTransferInConfirmation, error)
ParseTokenConfigChanged(log types.Log) (*bridge.BridgeContractTokenConfigChanged, error)
GetReceiveToken(token common.Address, toChainId int64) (string, error)
}
var (
bridgeAbi, _ = bridge.BridgeContractMetaData.GetAbi()
TransferOutEvent = bridgeAbi.Events[EVENT_TRANSFER_OUT]
TransferInEvent = bridgeAbi.Events[EVENT_TRANSFER_IN]
TransferInExecutionEvent = bridgeAbi.Events[EVENT_TRANSFER_IN_EXECUTION]
TransferInRejectionEvent = bridgeAbi.Events[EVENT_TRANSFER_IN_REJECTION]
TransferInConfirmationEvent = bridgeAbi.Events[EVENT_TRANSFER_IN_CONFIRMATION]
TokenConfigChangedEvent = bridgeAbi.Events[EVENT_TOKENCONFIGCHANGED]
)
func (s *Dao) HandleEvents(chain ChainInterface, logs []types.Log) error {
s.handleMux.Lock()
defer s.handleMux.Unlock()
cname := chain.Name()
// begin orm transaction
ctx := context.Background()
ormTx, err := s.BeginTx(ctx)
if err != nil {
log.WithField("chain", cname).WithError(err).Error("begin db transaction")
return err
}
var ormTxErr error
for _, txLog := range logs {
if err := s.filterTransferOut(chain, txLog, ormTx); err != nil {
ormTxErr = err
break
}
if err := s.filterTransferIn(chain, txLog, ormTx); err != nil {
ormTxErr = err
break
}
if err := s.filterTokenConfigChanged(chain, txLog, ormTx); err != nil {
ormTxErr = err
break
}
}
// Commit or rollback transaction based on error
if ormTxErr != nil {
if rbErr := ormTx.Rollback(); rbErr != nil {
log.WithField("chain", cname).WithError(rbErr).Error("failed to rollback transaction")
}
log.WithField("chain", cname).WithError(ormTxErr).Error("error processing logs, transaction rolled back")
} else {
if cmtErr := ormTx.Commit(); cmtErr != nil {
log.WithField("chain", cname).WithError(cmtErr).Error("failed to commit transaction")
}
}
return nil
}
// filterTransferOut 用户从当前链跨出事件.
func (s *Dao) filterTransferOut(chain ChainInterface, txLog types.Log, tx *Transaction) error {
if len(txLog.Topics) == 0 {
return nil
}
if txLog.Topics[0].Hex() == TransferOutEvent.ID.Hex() {
event, err := chain.ParseTransferOut(txLog)
if err != nil {
log.WithField("chain", chain.Name()).WithError(err).Error("parse TransferOut log")
return err
}
blocktime, _ := s.GetBlockTime(chain.GetChain(), int64(txLog.BlockNumber))
dbEvent := &dbModel.BridgeEvent{
FromChain: event.FromChainID.Int64(),
OutId: event.OutId.Int64(),
OutTimestamp: int64(blocktime),
FromChainTxHash: strings.ToLower(txLog.TxHash.String()),
FromAddress: strings.ToLower(event.Sender.String()),
FromToken: strings.ToLower(event.Token.String()),
SendAmount: event.Amount.Text(10),
FeeAmount: event.Fee.Text(10),
ToChain: event.ToChainID.Int64(),
Receiver: strings.ToLower(event.Receiver.String()),
ToToken: strings.ToLower(event.ReceiveToken.String()),
ReceiveAmount: new(big.Int).Sub(event.Amount, event.Fee).Text(10),
ToChainStatus: int(TransferChainNoProcess),
}
if err := s.FillOutTransferEventInfo(tx, dbEvent); err != nil {
log.WithField("chain", chain.Name()).WithError(err).Error("fill out transfer event info")
return err
}
return nil
}
return nil
}
// filterTransferIn 用户从目标链跨入事件及执行结束事件.
func (s *Dao) filterTransferIn(chain ChainInterface, txLog types.Log, tx *Transaction) error {
if len(txLog.Topics) == 0 {
return nil
}
switch txLog.Topics[0].Hex() {
case TransferInEvent.ID.Hex():
event, err := chain.ParseTransferIn(txLog)
if err != nil {
log.WithField("chain", chain.Name()).WithError(err).Error("parse TransferIn log")
return err
}
blocktime, _ := s.GetBlockTime(chain.GetChain(), int64(txLog.BlockNumber))
dbEvent := &dbModel.BridgeEvent{
FromChain: event.FromChainID.Int64(),
OutId: event.OutId.Int64(),
InId: event.InId.Int64(),
Receiver: strings.ToLower(event.Receiver.String()),
ToToken: strings.ToLower(event.Token.String()),
ReceiveAmount: event.Amount.Text(10),
ToChainStatus: int(TransferChainWaitConfirm),
ToContract: strings.ToLower(txLog.Address.String()),
InTimestamp: int64(blocktime),
ToChain: chain.GetChain().ChainId,
ToChainTxHash: strings.ToLower(txLog.TxHash.String()),
}
if err := s.FillInTransferEventInfo(tx, dbEvent); err != nil {
log.WithField("chain", chain.Name()).WithFields(log.Fields{
"error": err.Error(),
}).Error("db fill in transfer event")
return err
}
return nil
case TransferInExecutionEvent.ID.Hex():
event, err := chain.ParseTransferInExecution(txLog)
if err != nil {
log.WithField("chain", chain.Name()).WithError(err).Error("parse TransferInExecution log")
return err
}
if err := s.UpdateBridgeResult(tx, chain.GetChain().ChainId, event.InId.Int64(), TransferChainExecuted); err != nil {
log.WithField("chain", chain.Name()).WithFields(log.Fields{
"error": err.Error(),
}).Error("db update transfer in execution event")
return err
}
case TransferInRejectionEvent.ID.Hex():
event, err := chain.ParseTransferInRejection(txLog)
if err != nil {
log.WithField("chain", chain.Name()).WithError(err).Error("parse TransferInExecution log")
return err
}
if err := s.UpdateBridgeResult(tx, chain.GetChain().ChainId, event.InId.Int64(), TransferChainRejected); err != nil {
log.WithField("chain", chain.Name()).WithFields(log.Fields{
"error": err.Error(),
}).Error("db update transfer in execution event")
return err
}
}
return nil
}
func (s *Dao) filterTokenConfigChanged(chain ChainInterface, txLog types.Log, tx *Transaction) error {
if len(txLog.Topics) == 0 {
return nil
}
if txLog.Topics[0].Hex() == TokenConfigChangedEvent.ID.Hex() {
configure, err := chain.ParseTokenConfigChanged(txLog)
if err != nil {
return err
}
info := &dbModel.BridgeTokenInfo{
ChainId: chain.GetChain().ChainId,
Token: strings.ToLower(configure.Token.String()),
ToChainId: configure.ToChainID.Int64(),
Enabled: configure.Enabled,
Contract: strings.ToLower(txLog.Address.String()),
}
// get receive token from contract.
info.ToToken, err = chain.GetReceiveToken(configure.Token, info.ToChainId)
if err != nil {
log.WithFields(log.Fields{
"chain": chain.Name(),
"token": configure.Token.Hex(),
"toChainId": info.ToChainId,
}).WithError(err).Error("get receive token config failed")
return err
}
err = s.CreateOrUpdateBridgeTokenInfo(tx, info)
if err != nil {
log.WithFields(log.Fields{
"chain": chain.Name(),
"token": configure.Token.Hex(),
"toChainId": info.ToChainId,
"enabled": info.Enabled,
}).WithError(err).Error("db create or update token config failed")
return err
}
}
return nil
}
package dao
import (
"code.wuban.net.cn/movabridge/bridge-backend/chainlist"
"code.wuban.net.cn/movabridge/bridge-backend/config"
"context"
"crypto/ecdsa"
"fmt"
"github.com/ethereum/go-ethereum/ethclient"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"sync"
)
type ChainInfo struct {
conf *config.ChainConfig
cli *ethclient.Client
}
type Dao struct {
c *config.Config
db *mongo.Database
chainGroup map[int64]ChainInfo
syncer map[int64]ChainInterface
wg sync.WaitGroup
handleMux sync.Mutex
validatorPk *ecdsa.PrivateKey
chainList *chainlist.ChainRepo
}
func New(_c *config.Config, clist *chainlist.ChainRepo) (dao *Dao, err error) {
dao = &Dao{
c: _c,
chainGroup: make(map[int64]ChainInfo),
syncer: make(map[int64]ChainInterface),
chainList: clist,
}
// Connect to all configured chains
for name, chainConfig := range _c.Chains {
var client *ethclient.Client
client, err = ethclient.Dial(chainConfig.RPC)
if err != nil {
return nil, fmt.Errorf("failed to connect to %s chain: %w", name, err)
}
// Get and store chain ID
chainId, err := client.ChainID(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get %s chain ID: %w", name, err)
}
// Update the chain ID in the config
chainConfig.ChainId = chainId.Int64()
dao.chainGroup[chainId.Int64()] = ChainInfo{
conf: chainConfig,
cli: client,
}
fmt.Printf("Connected to %s chain with ID %d\n", name, chainConfig.ChainId)
}
// MongoDB connection
mongoURI := fmt.Sprintf("mongodb://%s:%s@%s:%d/%s?authSource=admin&authMechanism=SCRAM-SHA-256",
_c.Mongo.User, _c.Mongo.Password, _c.Mongo.Host, _c.Mongo.Port, _c.Mongo.Database)
clientOptions := options.Client().ApplyURI(mongoURI)
mongoClient, err := mongo.Connect(context.Background(), clientOptions)
if err != nil {
return nil, fmt.Errorf("failed to connect to MongoDB: %w", err)
}
// Ping to verify connection
err = mongoClient.Ping(context.Background(), nil)
if err != nil {
return nil, fmt.Errorf("failed to ping MongoDB: %w", err)
}
dao.db = mongoClient.Database(_c.Mongo.Database)
return dao, nil
}
func (d *Dao) ChainClient(chainId int64) *ethclient.Client {
chainInfo := d.chainGroup[chainId]
if chainInfo.cli != nil {
return chainInfo.cli
}
return nil
}
func (d *Dao) Quit() {
if d.db != nil {
d.db.Client().Disconnect(context.Background())
}
d.chainGroup = nil
d.db = nil
d.c = nil
}
func (d *Dao) AddSyncer(chainId int64, syncer ChainInterface) {
d.handleMux.Lock()
defer d.handleMux.Unlock()
d.syncer[chainId] = syncer
}
package dao
import (
"code.wuban.net.cn/movabridge/bridge-backend/constant"
apiModel "code.wuban.net.cn/movabridge/bridge-backend/model/api"
"context"
log "github.com/sirupsen/logrus"
"sort"
"time"
dbModel "code.wuban.net.cn/movabridge/bridge-backend/model/db"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// GetStorageHeight 获取上次缓存的高度
func (d *Dao) GetStorageHeight(key string) (value int64, err error) {
collection := d.db.Collection(new(dbModel.Height).TableName())
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var storage dbModel.Height
err = collection.FindOne(ctx, bson.M{"key": key}).Decode(&storage)
if err == mongo.ErrNoDocuments {
return 0, ErrRecordNotFound
}
return storage.IntValue, err
}
// SetStorageHeight 设置上次缓存的高度
func (d *Dao) SetStorageHeight(key string, intValue int64) (err error) {
collection := d.db.Collection(new(dbModel.Height).TableName())
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
filter := bson.M{"key": key}
update := bson.M{
"$set": bson.D{
{"key", key},
{"int_value", intValue},
},
}
opts := options.Update().SetUpsert(true)
_, err = collection.UpdateOne(ctx, filter, update, opts)
return err
}
func (d *Dao) GetBridgeConfig() (config apiModel.BridgeConfig, err error) {
collection := d.db.Collection(new(dbModel.BridgeTokenInfo).TableName())
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, bson.M{})
if err != nil {
return config, err
}
defer cursor.Close(ctx)
var tokens []dbModel.BridgeTokenInfo
err = cursor.All(ctx, &tokens)
if err != nil {
return config, err
}
config.Chains = make(map[string]apiModel.ChainConfig)
// Convert database model to API model
for _, info := range tokens {
if !info.Enabled {
continue
}
chainInfo, exist := d.chainList.Get(info.ChainId)
if !exist {
log.WithFields(log.Fields{
"chain_id": info.ChainId,
"token": info.Token,
}).Error("not found chain info with chainlist, skip bridge config")
continue
}
var chainConfig apiModel.ChainConfig
if _chainConfig, exist := config.Chains[chainInfo.Name]; !exist {
chainConfig = apiModel.ChainConfig{
ChainId: info.ChainId,
Chain: chainInfo.Name,
RpcUrl: chainInfo.Rpc,
ExplorerUrl: chainInfo.Explorer,
BridgeContract: info.Contract,
SupportTokens: make(map[string]apiModel.ToToken),
}
} else {
chainConfig = _chainConfig
}
chainConfig.SupportTokens[info.Token] = apiModel.ToToken{
ToChainId: info.ToChainId,
ToToken: info.ToToken,
}
config.Chains[chainInfo.Name] = chainConfig
}
return config, nil
}
func (d *Dao) GetHistoryInfo(user string) (history apiModel.History, err error) {
collection := d.db.Collection(new(dbModel.BridgeEvent).TableName())
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, bson.M{"from_address": user})
if err != nil {
return history, err
}
defer cursor.Close(ctx)
var events []dbModel.BridgeEvent
err = cursor.All(ctx, &events)
if err != nil {
return history, err
}
pendingHistory := make([]*apiModel.HistoryInfo, 0, 1000)
completedHistory := make([]*apiModel.HistoryInfo, 0, 1000)
for _, event := range events {
if event.FromChainTxHash == "" || event.ToChainTxHash == "" {
continue
}
fromChain, _ := d.chainList.Get(event.FromChain)
toChain, _ := d.chainList.Get(event.ToChain)
record := &apiModel.HistoryInfo{
FromChain: fromChain.Chain,
ToChain: toChain.Chain,
TxHash: event.FromChainTxHash,
CreateTime: event.OutTimestamp,
Amount: event.SendAmount,
Token: event.FromToken,
Status: constant.TransferStatus(event.ToChainStatus).String(),
}
if event.ToChainStatus <= int(constant.TransferChainWaitConfirm) {
pendingHistory = append(pendingHistory, record)
} else {
completedHistory = append(completedHistory, record)
}
}
// sort pending by CreateTime desc
sort.Slice(pendingHistory, func(i, j int) bool {
return pendingHistory[i].CreateTime > pendingHistory[j].CreateTime
})
sort.Slice(completedHistory, func(i, j int) bool {
return completedHistory[i].CreateTime > completedHistory[j].CreateTime
})
// sort completed by CreateTime desc
history.Pending = pendingHistory
history.Finish = completedHistory
return history, nil
}
package dao
import (
"code.wuban.net.cn/movabridge/bridge-backend/constant"
dbModel "code.wuban.net.cn/movabridge/bridge-backend/model/db"
"context"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var (
ErrRecordNotFound = mongo.ErrNoDocuments
)
// Transaction represents a MongoDB session with transaction
type Transaction struct {
session mongo.Session
ctx context.Context
}
// BeginTx starts a new MongoDB transaction
func (d *Dao) BeginTx(ctx context.Context) (*Transaction, error) {
client := d.db.Client()
session, err := client.StartSession()
if err != nil {
return nil, err
}
err = session.StartTransaction()
if err != nil {
session.EndSession(ctx)
return nil, err
}
return &Transaction{
session: session,
ctx: mongo.NewSessionContext(ctx, session),
}, nil
}
// Commit commits the transaction
func (tx *Transaction) Commit() error {
defer tx.session.EndSession(tx.ctx)
return tx.session.CommitTransaction(tx.ctx)
}
// Rollback aborts the transaction
func (tx *Transaction) Rollback() error {
defer tx.session.EndSession(tx.ctx)
return tx.session.AbortTransaction(tx.ctx)
}
func (d *Dao) FillInTransferEventInfo(tx *Transaction, inEvent *dbModel.BridgeEvent) error {
collection := d.db.Collection(inEvent.TableName())
filter := bson.M{"from_chain": inEvent.FromChain, "out_id": inEvent.OutId}
update := bson.M{
"$set": bson.M{
"from_chain": inEvent.FromChain,
"out_id": inEvent.OutId,
"in_id": inEvent.InId,
"receiver": inEvent.Receiver,
"to_token": inEvent.ToToken,
"receive_amount": inEvent.ReceiveAmount,
"to_chain_status": inEvent.ToChainStatus,
},
}
_, err := collection.UpdateOne(tx.ctx, filter, update)
return err
}
func (d *Dao) FillOutTransferEventInfo(tx *Transaction, outEvent *dbModel.BridgeEvent) error {
collection := d.db.Collection(outEvent.TableName())
filter := bson.M{"from_chain": outEvent.FromChain, "out_id": outEvent.OutId}
update := bson.M{
"$set": bson.M{
"from_chain": outEvent.FromChain,
"out_id": outEvent.OutId,
"out_timestamp": outEvent.OutTimestamp,
"from_chain_tx_hash": outEvent.FromChainTxHash,
"from_address": outEvent.FromAddress,
"from_token": outEvent.FromToken,
"send_amount": outEvent.SendAmount,
"fee_amount": outEvent.FeeAmount,
"to_chain": outEvent.ToChain,
"receiver": outEvent.Receiver,
"to_token": outEvent.ToToken,
"receive_amount": outEvent.ReceiveAmount,
},
}
_, err := collection.UpdateOne(tx.ctx, filter, update)
return err
}
func (d *Dao) UpdateBridgeResult(tx *Transaction, toChainId int64, inId int64, result constant.TransferStatus) error {
collection := d.db.Collection(new(dbModel.BridgeEvent).TableName())
filter := bson.M{"to_chain": toChainId, "in_id": inId}
update := bson.M{
"$set": bson.M{
"to_chain_status": int(result),
},
}
_, err := collection.UpdateOne(tx.ctx, filter, update)
return err
}
func (d *Dao) CreateOrUpdateBridgeTokenInfo(tx *Transaction, info *dbModel.BridgeTokenInfo) error {
collection := d.db.Collection(info.TableName())
filter := bson.M{"chain_id": info.ChainId, "token": info.Token, "to_chain_id": info.ToChainId}
update := bson.D{
{"$set", info},
}
opts := options.Update().SetUpsert(true)
_, err := collection.UpdateOne(tx.ctx, filter, update, opts)
return err
}
package dao
import (
"code.wuban.net.cn/movabridge/bridge-backend/config"
"context"
"errors"
"github.com/ethereum/go-ethereum/ethclient"
"math/big"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
func (d *Dao) GetBlockHeight(chain *config.ChainConfig, behindBlock ...int) (height int64, err error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if _, ok := d.chainGroup[chain.ChainId]; !ok {
return 0, errors.New("chain client not support")
}
chaininfo, ok := d.chainGroup[chain.ChainId]
if !ok {
return 0, errors.New("chain client not support")
}
n, err := chaininfo.cli.BlockNumber(ctx)
if len(behindBlock) > 0 {
n -= uint64(behindBlock[0])
if n < 0 {
n = 0
}
}
return int64(n), err
}
func (d *Dao) GetLatestBockHash(chain *config.ChainConfig) (hash string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
chainInfo, ok := d.chainGroup[chain.ChainId]
if !ok {
return "", errors.New("chain client not support")
}
block, err := chainInfo.cli.BlockByNumber(ctx, nil)
if err != nil {
return
}
return block.Hash().Hex(), nil
}
func (d *Dao) GetBlockTime(chain *config.ChainConfig, height int64) (timestamp int64, err error) {
chainInfo, ok := d.chainGroup[chain.ChainId]
if !ok {
return 0, errors.New("chain client not support")
}
for i := 0; i < 2; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
block, err := chainInfo.cli.BlockByNumber(ctx, big.NewInt(int64(height)))
if err == nil {
return int64(block.Time()), nil
}
}
return
}
func (d *Dao) GetLogs(chain *config.ChainConfig, beginHeight, endHeight int64, topics, addresses []string) (logs []types.Log, err error) {
chainInfo, ok := d.chainGroup[chain.ChainId]
if !ok {
return nil, errors.New("chain client not support")
}
for i := 0; i < 2; i++ {
// 重试2次
logs, err = d.getLogs(chainInfo.cli, beginHeight, endHeight, topics, addresses)
if err == nil {
return logs, nil
}
}
return
}
func (d *Dao) getLogs(client *ethclient.Client, beginHeight, endHeight int64, topics []string, addresses []string) (logs []types.Log, err error) {
addrs := make([]common.Address, 0)
for _, addr := range addresses {
addrs = append(addrs, common.HexToAddress(addr))
}
q := ethereum.FilterQuery{
FromBlock: big.NewInt(int64(beginHeight)),
ToBlock: big.NewInt(int64(endHeight)),
Topics: [][]common.Hash{{}},
Addresses: addrs,
}
for _, topic := range topics {
q.Topics[0] = append(q.Topics[0], common.HexToHash(topic))
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return client.FilterLogs(ctx, q)
}
func (d *Dao) CheckEventValid() bool {
// Implement the logic to check if the event is valid.
// This is a placeholder implementation.
return true
}
package dao
import (
"code.wuban.net.cn/movabridge/bridge-backend/contract/bridge"
"context"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"math/big"
"testing"
)
func TestGetReceiveToken(t *testing.T) {
client, _ := ethclient.Dial("http://rpc.hole.bitheart.org")
ct, _ := bridge.NewBridgeContract(common.HexToAddress("0xceEC8799139C698De532e363DA7395E25F409775"), client)
latest, _ := client.BlockNumber(context.Background())
opt := &bind.CallOpts{
Pending: false,
Context: context.Background(),
BlockNumber: big.NewInt(int64(latest)),
From: common.HexToAddress("0xfeed6dB33622Fb526a89c84A0861C29f483f1d0E"),
}
result, err := ct.OutConfiguration(opt, common.HexToAddress("0x9F225b7BCC4697414D4F6aC1CB985D07d34dAe0a"), big.NewInt(8891))
if err != nil {
t.Fatal(err)
}
fmt.Println(result)
}
networks:
default:
name: bridge-network
services:
bridgedb:
image: mongo:7
volumes:
- ./data/db:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: "root"
MONGO_INITDB_ROOT_PASSWORD: "XN2UARuys3zy4Oux"
MONGO_INITDB_DATABASE: "bridge"
ports:
- "27017:27017"
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
backend:
image: bridgebackend:latest
depends_on:
bridgedb:
condition: service_healthy
volumes:
- ./config.toml:/app/config.toml
- ./testrpcs.json:/app/rpcs.json
command:
- "/bin/sh"
- "-c"
- "/usr/bin/bridgebackend -c /app/config.toml"
restart: unless-stopped
\ No newline at end of file
module code.wuban.net.cn/movabridge/bridge-backend
go 1.24.0
require (
github.com/BurntSushi/toml v1.5.0
github.com/ethereum/go-ethereum v1.10.24
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
go.mongodb.org/mongo-driver v1.17.4
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/bits-and-blooms/bitset v1.20.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/consensys/gnark-crypto v0.18.0 // indirect
github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rjeczalik/notify v0.9.1 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/supranational/blst v0.3.14 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
This diff is collapsed.
package middleware
import (
"math/rand"
"strings"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func CheckHeaderMiddleware(invalidHeaders []string) gin.HandlerFunc {
for _, invalidHeader := range invalidHeaders {
log.WithField("invalid-header", invalidHeader).Debug("init invalid header")
}
return func(c *gin.Context) {
// 获取user-agent
userAgent := c.Request.Header.Get("User-Agent")
for _, invalidHeader := range invalidHeaders {
if strings.Contains(userAgent, invalidHeader) {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)+10))
c.JSON(200, gin.H{
"code": 0,
"msg": "ok",
"data": "",
})
log.WithFields(log.Fields{"user-agent": userAgent}).Debug("invalid header, return fake data")
c.Abort()
return
}
}
c.Next()
}
}
package middleware
import (
"bytes"
"io"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func PrintRequestResponseBodyMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 读取请求 body
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
}
log.WithFields(log.Fields{"method": c.Request.Method, "uri": c.Request.RequestURI, "body": string(requestBody)}).Debug("request body")
bodyWriter := &responseBodyWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = bodyWriter
c.Next()
responseBody := bodyWriter.body.String()
log.WithFields(log.Fields{"status": c.Writer.Status(), "body": responseBody}).Debug("response body")
}
}
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (r *responseBodyWriter) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
package middleware
package api
type ToToken struct {
ToChainId int64 `json:"to_chain_id" bson:"to_chain_id"`
ToToken string `json:"to_token" bson:"to_token"`
}
type ChainConfig struct {
Chain string `json:"chain" bson:"chain"`
ChainId int64 `json:"chain_id" bson:"chain_id"`
BridgeContract string `json:"contract" bson:"contract"`
SupportTokens map[string]ToToken `json:"support_tokens" bson:"support_tokens"`
ExplorerUrl string `json:"explorer_url" bson:"explorer_url"`
RpcUrl string `json:"rpc" bson:"rpc"`
}
type BridgeConfig struct {
Chains map[string]ChainConfig `json:"chains" bson:"chains"`
}
type HistoryInfo struct {
FromChain string `json:"from_chain" bson:"from_chain"`
ToChain string `json:"to_chain" bson:"to_chain"`
TxHash string `json:"tx_hash" bson:"tx_hash"`
CreateTime int64 `json:"create_time" bson:"create_time"`
Amount string `json:"amount" bson:"amount"`
Token string `json:"token" bson:"token"`
Status string `json:"status" bson:"status"`
}
type History struct {
Pending []*HistoryInfo `json:"pending" bson:"pending"`
Finish []*HistoryInfo `json:"finish" bson:"finish"`
}
type Querier interface {
GetBridgeConfig() (config BridgeConfig, err error)
GetHistoryInfo(user string) (history History, err error)
}
package dbModel
import (
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Height struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Key string `bson:"key"`
IntValue int64 `bson:"int_value"`
}
func (h *Height) TableName() string {
return "heights"
}
type BridgeEvent struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
FromChain int64 `bson:"from_chain"`
OutTimestamp int64 `bson:"out_timestamp"`
FromToken string `bson:"from_token"`
FromAddress string `bson:"from_address"`
FromChainTxHash string `bson:"from_chain_tx_hash"`
SendAmount string `bson:"send_amount"`
FeeAmount string `bson:"fee_amount"`
Receiver string `bson:"receiver"`
ToChain int64 `bson:"to_chain"`
ToToken string `bson:"to_token"`
ReceiveAmount string `bson:"receive_amount"`
OutId int64 `bson:"out_id"`
InTimestamp int64 `bson:"in_timestamp"`
InId int64 `bson:"in_id"`
ToContract string `bson:"to_contract"`
ToChainTxHash string `bson:"to_chain_tx_hash"`
ToChainStatus int `bson:"to_chain_status"`
}
func (b *BridgeEvent) TableName() string {
return "bridge_events"
}
type BridgeTokenInfo struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
ChainId int64 `bson:"chain_id"`
Contract string `bson:"contract"`
Token string `bson:"token"`
ToChainId int64 `bson:"to_chain_id"`
ToToken string `bson:"to_token"`
Enabled bool `bson:"enabled"`
}
func (b *BridgeTokenInfo) TableName() string {
return "bridge_token_info"
}
This diff is collapsed.
package server
import (
"code.wuban.net.cn/movabridge/bridge-backend/constant"
"github.com/ethereum/go-ethereum/common"
log "github.com/sirupsen/logrus"
"strings"
"github.com/gin-gonic/gin"
)
func getParam(c *gin.Context) {
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
config, err := _querier.GetBridgeConfig()
if err != nil {
log.Errorf("get bridge config error: %v", err)
c.JSON(500, withError(constant.InternalError))
return
}
c.JSON(200, withSuccess(config))
}
func getHistory(c *gin.Context) {
address := c.DefaultQuery("address", "")
if !common.IsHexAddress(address) {
c.JSON(200, withError(constant.InvalidParam))
return
}
if _querier == nil {
log.Error("querier is nil")
c.JSON(500, withError(constant.InternalError))
return
}
history, err := _querier.GetHistoryInfo(strings.ToLower(common.HexToAddress(address).Hex()))
if err != nil {
log.Errorf("get history error: %v", err)
c.JSON(500, withError(constant.InternalError))
return
}
c.JSON(200, withSuccess(history))
}
package server
import (
"code.wuban.net.cn/movabridge/bridge-backend/config"
"code.wuban.net.cn/movabridge/bridge-backend/middleware"
"github.com/gin-gonic/gin"
)
func initRouter(conf *config.Config, e *gin.Engine) {
e.Use(middleware.PrintRequestResponseBodyMiddleware())
e.Use(middleware.CheckHeaderMiddleware(conf.Server.InvalidHeaders))
v1 := e.Group("/api/v1")
{
user := v1.Group("/user")
user.GET("/history", getHistory)
}
v1.GET("/params", getParam)
}
package server
import (
"code.wuban.net.cn/movabridge/bridge-backend/config"
apiModel "code.wuban.net.cn/movabridge/bridge-backend/model/api"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
var (
_querier apiModel.Querier
)
func StartRestApi(querier apiModel.Querier, _conf *config.Config) error {
if !_conf.Debug {
gin.SetMode(gin.ReleaseMode)
}
_querier = querier
engine := gin.Default()
_cors := cors.DefaultConfig()
_cors.AllowAllOrigins = true
_cors.AllowHeaders = []string{"*"}
engine.Use(cors.New(_cors))
initRouter(_conf, engine)
log.Infof("start http server listening %s", _conf.Server.Listen)
return engine.Run(_conf.Server.Listen)
}
func withSuccess(obj interface{}) interface{} {
return gin.H{
"code": 0,
"msg": "ok",
"data": obj,
}
}
func withError(msg string) interface{} {
return gin.H{
"code": 1,
"error": msg,
"data": "",
}
}
This diff is collapsed.
package types
type ChainInfo struct {
ChainId int64 `json:"chainId,omitempty"`
Chain string `json:"chain,omitempty"`
Name string `json:"name,omitempty"`
Rpc string `json:"rpc,omitempty"`
Explorer string `json:"explorer,omitempty"`
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment