Skip to content

Commit 4bfcfcd

Browse files
authored
multi: Bridge UI (#3484)
* multi: Bridge UI Adds a UI to the wallets page that allows users to bridge EVM assets between chains. Additionally: - Communicates fees for bridge completion transactions to the initiator - Adds an index to the eth transaction db to be able to query bridge initiations by asset ID. * Fix simnet bridge and add fee balance check for bridge completion. * Make bridge history pagination button style match tx history popup's buttons.
1 parent 9f92dd0 commit 4bfcfcd

35 files changed

+3845
-309
lines changed

client/asset/eth/eth.go

Lines changed: 109 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,11 +1014,11 @@ func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error)
10141014
w.bridges[acrossBridgeName] = acrossBridge
10151015
}
10161016

1017-
if usdcBridge, err := newUsdcBridge(w.assetID, w.net, w.node.contractBackend(), w.addr, w.node); err != nil {
1017+
/* if usdcBridge, err := newUsdcBridge(w.assetID, w.net, w.node.contractBackend(), w.addr, w.node); err != nil {
10181018
w.log.Errorf("Failed to initialize USDC bridge: %v", err)
10191019
} else {
10201020
w.bridges[usdcBridgeName] = usdcBridge
1021-
}
1021+
} */
10221022

10231023
if w.assetID == ethID {
10241024
if polygonBridge, err := newPolygonBridgeEth(ctx, w.node.contractBackend(), w.net, w.addr, w.node, w.log); err != nil {
@@ -1034,6 +1034,15 @@ func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error)
10341034
}
10351035
}
10361036

1037+
// Register simnet bridge for testing
1038+
if w.net == dex.Simnet {
1039+
if simnetBridge, err := newSimnetBridge(w.assetID, w.net, w.node.contractBackend(), w.addr, w.node.chainConfig().ChainID, w.log); err != nil {
1040+
w.log.Debugf("Simnet bridge not available: %v", err)
1041+
} else {
1042+
w.bridges[simnetBridgeName] = simnetBridge
1043+
}
1044+
}
1045+
10371046
w.bridgeManager, err = newBridgeManager(&bridgeManagerConfig{
10381047
ctx: ctx,
10391048
baseChainID: w.baseChainID,
@@ -1454,7 +1463,7 @@ func newBridgeManager(cfg *bridgeManagerConfig) (*bridgeManager, error) {
14541463
monitorInterval: cfg.monitorInterval,
14551464
}
14561465

1457-
pendingBridges, err := cfg.txDB.getPendingBridges()
1466+
pendingBridges, err := cfg.txDB.getPendingBridges(nil)
14581467
if err != nil {
14591468
return nil, fmt.Errorf("error getting pending bridges: %v", err)
14601469
}
@@ -1551,7 +1560,7 @@ func (bm *bridgeManager) checkPendingBridges(ctx context.Context) {
15511560
// markBridgeComplete is called when the destination wallet has confirmed the
15521561
// completion of a bridge. The pending bridge is removed from the manager and
15531562
// the db is updated to reflect the completion.
1554-
func (bm *bridgeManager) markBridgeComplete(initiationTxID string, completionTxIDs []string, amtReceived uint64, isComplete bool) {
1563+
func (bm *bridgeManager) markBridgeComplete(initiationTxID string, completionTxIDs []string, amtReceived, fees uint64, isComplete bool) {
15551564
var deletePendingBridge bool
15561565
defer func() {
15571566
if !deletePendingBridge {
@@ -1582,6 +1591,7 @@ func (bm *bridgeManager) markBridgeComplete(initiationTxID string, completionTxI
15821591
bridgeTx.BridgeCounterpartTx.IDs = completionTxIDs
15831592
bridgeTx.BridgeCounterpartTx.Complete = isComplete
15841593
bridgeTx.BridgeCounterpartTx.AmountReceived = amtReceived
1594+
bridgeTx.BridgeCounterpartTx.Fees = fees
15851595
if err := bm.txDB.storeTx(bridgeTx); err != nil {
15861596
bm.log.Errorf("error storing completed bridge tx: %v", err)
15871597
return
@@ -3446,7 +3456,7 @@ func (w *assetWallet) BridgeContractApprovalStatus(ctx context.Context, bridgeNa
34463456
return w.bridgeContractApprovalStatus(ctx, bridge)
34473457
}
34483458

3449-
func (w *assetWallet) approveBridgeContract(ctx context.Context, bridge bridge) (string, error) {
3459+
func (w *assetWallet) approveBridgeContract(ctx context.Context, bridge bridge, onConfirm func()) (string, error) {
34503460
approvalStatus, err := w.bridgeContractApprovalStatus(ctx, bridge)
34513461
if err != nil {
34523462
return "", fmt.Errorf("error checking approval status: %w", err)
@@ -3504,7 +3514,7 @@ func (w *assetWallet) approveBridgeContract(ctx context.Context, bridge bridge)
35043514
delete(w.approvalCache, bridgeAddr)
35053515
w.pendingApprovals[bridgeAddr] = &pendingApproval{
35063516
txHash: tx.Hash(),
3507-
onConfirm: func() {},
3517+
onConfirm: onConfirm,
35083518
}
35093519
w.approvalsMtx.Unlock()
35103520

@@ -3518,15 +3528,15 @@ func (w *assetWallet) approveBridgeContract(ctx context.Context, bridge bridge)
35183528

35193529
// ApproveBridgeContract approves the bridge contract to spend tokens on behalf
35203530
// of the account handled by the wallet.
3521-
func (w *assetWallet) ApproveBridgeContract(ctx context.Context, bridgeName string) (string, error) {
3531+
func (w *assetWallet) ApproveBridgeContract(ctx context.Context, bridgeName string, onConfirm func()) (string, error) {
35223532
bridge, ok := w.bridges[bridgeName]
35233533
if !ok {
35243534
return "", fmt.Errorf("bridge %s not found", bridgeName)
35253535
}
3526-
return w.approveBridgeContract(ctx, bridge)
3536+
return w.approveBridgeContract(ctx, bridge, onConfirm)
35273537
}
35283538

3529-
func (w *assetWallet) unapproveBridgeContract(ctx context.Context, bridge bridge) (string, error) {
3539+
func (w *assetWallet) unapproveBridgeContract(ctx context.Context, bridge bridge, onConfirm func()) (string, error) {
35303540
approvalStatus, err := w.bridgeContractApprovalStatus(ctx, bridge)
35313541
if err != nil {
35323542
return "", fmt.Errorf("error checking approval status: %w", err)
@@ -3581,7 +3591,7 @@ func (w *assetWallet) unapproveBridgeContract(ctx context.Context, bridge bridge
35813591
delete(w.approvalCache, bridgeAddr)
35823592
w.pendingApprovals[bridgeAddr] = &pendingApproval{
35833593
txHash: tx.Hash(),
3584-
onConfirm: func() {},
3594+
onConfirm: onConfirm,
35853595
}
35863596
w.approvalsMtx.Unlock()
35873597

@@ -3594,12 +3604,12 @@ func (w *assetWallet) unapproveBridgeContract(ctx context.Context, bridge bridge
35943604
}
35953605

35963606
// UnapproveBridgeContract removes the approval for the bridge contract.
3597-
func (w *assetWallet) UnapproveBridgeContract(ctx context.Context, bridgeName string) (string, error) {
3607+
func (w *assetWallet) UnapproveBridgeContract(ctx context.Context, bridgeName string, onConfirm func()) (string, error) {
35983608
bridge, ok := w.bridges[bridgeName]
35993609
if !ok {
36003610
return "", fmt.Errorf("bridge %s not found", bridgeName)
36013611
}
3602-
return w.unapproveBridgeContract(ctx, bridge)
3612+
return w.unapproveBridgeContract(ctx, bridge, onConfirm)
36033613
}
36043614

36053615
func (w *assetWallet) initiateBridge(ctx context.Context, amt uint64, dest uint32, bridgeName string) (txID string, err error) {
@@ -3674,6 +3684,43 @@ func (w *assetWallet) InitiateBridge(ctx context.Context, amt uint64, dest uint3
36743684
return "", fmt.Errorf("insufficient balance: %d < %d", balance.Available, amt)
36753685
}
36763686

3687+
// Calculate the fee for the bridge initiation
3688+
bridge, ok := w.bridges[bridgeName]
3689+
if !ok {
3690+
return "", fmt.Errorf("bridge %s not found", bridgeName)
3691+
}
3692+
initiateGas := bridge.initiateBridgeGas(w.assetID)
3693+
maxFeeRateWei, _, err := w.recommendedMaxFeeRate(ctx)
3694+
if err != nil {
3695+
return "", fmt.Errorf("error calculating bridge fee rate: %w", err)
3696+
}
3697+
maxFeeRateGwei := dexeth.WeiToGweiCeil(maxFeeRateWei)
3698+
fee := initiateGas * maxFeeRateGwei
3699+
3700+
// Check fee balance
3701+
isToken := w.assetID != w.baseChainID
3702+
if isToken {
3703+
// For tokens, the fee is paid in the base asset
3704+
w.walletsMtx.RLock()
3705+
baseWallet := w.wallets[w.baseChainID]
3706+
w.walletsMtx.RUnlock()
3707+
if baseWallet == nil {
3708+
return "", fmt.Errorf("base wallet not found")
3709+
}
3710+
baseBal, err := baseWallet.balance()
3711+
if err != nil {
3712+
return "", fmt.Errorf("error getting base asset balance: %w", err)
3713+
}
3714+
if baseBal.Available < fee {
3715+
return "", fmt.Errorf("insufficient fee balance: required %d, available %d", fee, baseBal.Available)
3716+
}
3717+
} else {
3718+
// For base assets, the fee comes from the same balance as the amount
3719+
if balance.Available < amt+fee {
3720+
return "", fmt.Errorf("insufficient balance for amount + fee: required %d, available %d", amt+fee, balance.Available)
3721+
}
3722+
}
3723+
36773724
txID, err := w.initiateBridge(ctx, amt, dest, bridgeName)
36783725
if err != nil {
36793726
return "", err
@@ -3686,12 +3733,12 @@ func (w *assetWallet) InitiateBridge(ctx context.Context, amt uint64, dest uint3
36863733

36873734
// MarkBridgeComplete is called when the bridge completion transaction has
36883735
// been confirmed on the destination chain.
3689-
func (w *assetWallet) MarkBridgeComplete(initiationTxID string, completionTxIDs []string, amtReceived uint64, complete bool) {
3690-
w.bridgeManager.markBridgeComplete(initiationTxID, completionTxIDs, amtReceived, complete)
3736+
func (w *assetWallet) MarkBridgeComplete(initiationTxID string, completionTxIDs []string, amtReceived, fees uint64, complete bool) {
3737+
w.bridgeManager.markBridgeComplete(initiationTxID, completionTxIDs, amtReceived, fees, complete)
36913738
}
36923739

36933740
func (w *assetWallet) pendingBridges() ([]*asset.WalletTransaction, error) {
3694-
pendingBridges, err := w.txDB.getPendingBridges()
3741+
pendingBridges, err := w.txDB.getPendingBridges(&w.assetID)
36953742
if err != nil {
36963743
return nil, fmt.Errorf("error getting pending bridges: %w", err)
36973744
}
@@ -3715,7 +3762,7 @@ func (w *assetWallet) bridgeHistory(n int, refID *string, past bool) ([]*asset.W
37153762
rh := common.HexToHash(*refID)
37163763
refHash = &rh
37173764
}
3718-
return w.txDB.getBridges(n, refHash, past)
3765+
return w.txDB.getBridges(&w.assetID, n, refHash, past)
37193766
}
37203767

37213768
// BridgeHistory retrieves a record of bridge initiations on the blockchain.
@@ -3875,10 +3922,11 @@ func (w *assetWallet) completeBridgeIfNeeded(ctx context.Context, bridgeTx *asse
38753922
}
38763923

38773924
var completionTxIDs []string
3925+
var fees uint64
38783926
var sendNote, isComplete bool
38793927
defer func() {
38803928
if sendNote {
3881-
w.emit.BridgeCompleted(bridgeTx.AssetID, bridgeTx.IDs[0], completionTxIDs, amount, isComplete)
3929+
w.emit.BridgeCompleted(bridgeTx.AssetID, bridgeTx.IDs[0], completionTxIDs, amount, fees, isComplete)
38823930
}
38833931
}()
38843932

@@ -3913,6 +3961,7 @@ func (w *assetWallet) completeBridgeIfNeeded(ctx context.Context, bridgeTx *asse
39133961
if err != nil {
39143962
return fmt.Errorf("error completing bridge: %w", err)
39153963
}
3964+
// Send notification with isComplete=false. Fees are 0 since tx isn't confirmed.
39163965
completionTxIDs = []string{txID}
39173966
sendNote = true
39183967
return nil
@@ -3926,6 +3975,7 @@ func (w *assetWallet) completeBridgeIfNeeded(ctx context.Context, bridgeTx *asse
39263975
if !bridge.requiresFollowUpCompletion(w.assetID) {
39273976
isComplete = true
39283977
completionTxIDs = []string{wt.ID}
3978+
fees = wt.Fees
39293979
sendNote = true
39303980
return nil
39313981
}
@@ -3940,6 +3990,10 @@ func (w *assetWallet) completeBridgeIfNeeded(ctx context.Context, bridgeTx *asse
39403990
completionTxIDs = []string{wt.PreviousBridgeCompletionID, wt.ID}
39413991
isComplete = complete
39423992
if isComplete {
3993+
fees = wt.Fees
3994+
if prevTx, err := w.txDB.getTx(common.HexToHash(wt.PreviousBridgeCompletionID)); err == nil && prevTx != nil {
3995+
fees += prevTx.Fees
3996+
}
39433997
sendNote = true
39443998
}
39453999
return nil
@@ -3954,6 +4008,7 @@ func (w *assetWallet) completeBridgeIfNeeded(ctx context.Context, bridgeTx *asse
39544008
}
39554009
if !required {
39564010
completionTxIDs = []string{wt.ID}
4011+
fees = wt.Fees
39574012
isComplete = true
39584013
sendNote = true
39594014
return nil
@@ -3963,9 +4018,9 @@ func (w *assetWallet) completeBridgeIfNeeded(ctx context.Context, bridgeTx *asse
39634018
if err != nil {
39644019
return fmt.Errorf("error completing bridge: %w", err)
39654020
}
4021+
// Send notification with isComplete=false. Fees are 0 since follow-up tx isn't confirmed.
39664022
completionTxIDs = []string{wt.ID, txID}
39674023
sendNote = true
3968-
39694024
return nil
39704025
}
39714026

@@ -3976,7 +4031,7 @@ func (w *assetWallet) CompleteBridge(ctx context.Context, bridgeTx *asset.Bridge
39764031
}
39774032

39784033
// SupportedDestinations returns the list of asset IDs that the wallet can bridge funds to.
3979-
func (w *assetWallet) SupportedDestinations() (map[uint32][]string, error) {
4034+
func (w *assetWallet) SupportedDestinations() map[uint32][]string {
39804035
allBridgeDestinations := map[uint32][]string{}
39814036

39824037
for bridgeName, bridge := range w.bridges {
@@ -3991,7 +4046,7 @@ func (w *assetWallet) SupportedDestinations() (map[uint32][]string, error) {
39914046
allBridgeDestinations[destination] = append(allBridgeDestinations[destination], bridgeName)
39924047
}
39934048
}
3994-
return allBridgeDestinations, nil
4049+
return allBridgeDestinations
39954050
}
39964051

39974052
// BridgeInitiationFeesAndLimits returns the estimated fees, limits, and whether the
@@ -4008,7 +4063,7 @@ func (w *assetWallet) BridgeInitiationFeesAndLimits(bridgeName string, destAsset
40084063
if err != nil {
40094064
return 0, [2]uint64{}, false, fmt.Errorf("error calculating bridge fee rate: %w", err)
40104065
}
4011-
maxFeeRateGwei := dexeth.WeiToGwei(maxFeeRateWei)
4066+
maxFeeRateGwei := dexeth.WeiToGweiCeil(maxFeeRateWei)
40124067

40134068
minBig, maxBig, hasLimits, err := bridge.bridgeLimits(w.assetID, destAssetID)
40144069
if err != nil {
@@ -4021,23 +4076,50 @@ func (w *assetWallet) BridgeInitiationFeesAndLimits(bridgeName string, destAsset
40214076
return initiateGas * maxFeeRateGwei, [2]uint64{min, max}, hasLimits, nil
40224077
}
40234078

4024-
// BridgeCompletionFees returns the estimated fees for completing a bridge.
4025-
func (w *assetWallet) BridgeCompletionFees(bridgeName string) (uint64, error) {
4079+
// BridgeCompletionFees returns the estimated fees for completing a bridge,
4080+
// and whether the wallet has sufficient balance available to pay those fees.
4081+
func (w *assetWallet) BridgeCompletionFees(bridgeName string) (uint64, bool, error) {
40264082
bridge, found := w.bridges[bridgeName]
40274083
if !found {
4028-
return 0, fmt.Errorf("bridge %s not found", bridgeName)
4084+
return 0, false, fmt.Errorf("bridge %s not found", bridgeName)
40294085
}
40304086

40314087
completeGas := bridge.completeBridgeGas(w.assetID)
40324088
followUpCompleteGas := bridge.followUpCompleteBridgeGas()
40334089

40344090
maxFeeRateWei, _, err := w.recommendedMaxFeeRate(w.ctx)
40354091
if err != nil {
4036-
return 0, fmt.Errorf("error calculating bridge fee rate: %w", err)
4092+
return 0, false, fmt.Errorf("error calculating bridge fee rate: %w", err)
4093+
}
4094+
maxFeeRateGwei := dexeth.WeiToGweiCeil(maxFeeRateWei)
4095+
4096+
fees := (completeGas + followUpCompleteGas) * maxFeeRateGwei
4097+
4098+
// Get available fee balance
4099+
var availableFeeBalance uint64
4100+
if w.assetID == w.baseChainID {
4101+
// Base asset - fees come from own balance
4102+
bal, err := w.balance()
4103+
if err != nil {
4104+
return fees, false, fmt.Errorf("error getting balance: %w", err)
4105+
}
4106+
availableFeeBalance = bal.Available
4107+
} else {
4108+
// Token - fees come from parent asset
4109+
w.walletsMtx.RLock()
4110+
baseWallet := w.wallets[w.baseChainID]
4111+
w.walletsMtx.RUnlock()
4112+
if baseWallet == nil {
4113+
return fees, false, fmt.Errorf("base wallet not found")
4114+
}
4115+
bal, err := baseWallet.balance()
4116+
if err != nil {
4117+
return fees, false, fmt.Errorf("error getting base asset balance: %w", err)
4118+
}
4119+
availableFeeBalance = bal.Available
40374120
}
4038-
maxFeeRateGwei := dexeth.WeiToGwei(maxFeeRateWei)
40394121

4040-
return (completeGas + followUpCompleteGas) * maxFeeRateGwei, nil
4122+
return fees, availableFeeBalance >= fees, nil
40414123
}
40424124

40434125
func (w *ETHWallet) canRedeemWithBundler(lotSize uint64, gases *dexeth.Gases, n uint64) (bool, error) {

0 commit comments

Comments
 (0)