mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-10 06:38:04 -05:00
add estimated time for swap to complete for swap_getOngoing (#351)
This commit is contained in:
@@ -672,10 +672,12 @@ func runGetOngoingSwap(ctx *cli.Context) error {
|
||||
fmt.Printf("Receiving: %s %s\n", info.ExpectedAmount.Text('f'), receivedCoin)
|
||||
fmt.Printf("Exchange Rate: %s ETH/XMR\n", info.ExchangeRate)
|
||||
fmt.Printf("Status: %s\n", info.Status)
|
||||
fmt.Printf("Time status was last updated: %s\n", info.LastStatusUpdateTime.Format(common.TimeFmtSecs))
|
||||
if info.Timeout0 != nil && info.Timeout1 != nil {
|
||||
fmt.Printf("First timeout: %s\n", info.Timeout0.Format(common.TimeFmtSecs))
|
||||
fmt.Printf("Second timeout: %s\n", info.Timeout1.Format(common.TimeFmtSecs))
|
||||
}
|
||||
fmt.Printf("Estimated time to completion: %s\n", info.EstimatedTimeToCompletion)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -37,19 +37,20 @@ func TestDatabase_OfferTable(t *testing.T) {
|
||||
|
||||
// put swap to ensure iterator over offers is ok
|
||||
infoA := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: types.Hash{0x1},
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("0.1"),
|
||||
ExpectedAmount: coins.StrToDecimal("1"),
|
||||
ExchangeRate: coins.StrToExchangeRate("0.1"),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.ExpectingKeys,
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: time.Now().Add(-30 * time.Minute),
|
||||
EndTime: nil,
|
||||
Timeout0: nil,
|
||||
Timeout1: nil,
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: types.Hash{0x1},
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("0.1"),
|
||||
ExpectedAmount: coins.StrToDecimal("1"),
|
||||
ExchangeRate: coins.StrToExchangeRate("0.1"),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.ExpectingKeys,
|
||||
LastStatusUpdateTime: time.Now(),
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: time.Now().Add(-30 * time.Minute),
|
||||
EndTime: nil,
|
||||
Timeout0: nil,
|
||||
Timeout1: nil,
|
||||
}
|
||||
|
||||
err = db.PutSwap(infoA)
|
||||
@@ -103,19 +104,20 @@ func TestDatabase_GetAllOffers_InvalidEntry(t *testing.T) {
|
||||
|
||||
// Put a swap entry tied to the bad offer in the database
|
||||
swapEntry := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: badOfferID,
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("0.1"),
|
||||
ExpectedAmount: coins.StrToDecimal("1"),
|
||||
ExchangeRate: coins.StrToExchangeRate("0.1"),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.ExpectingKeys,
|
||||
MoneroStartHeight: 12345,
|
||||
Timeout0: nil,
|
||||
Timeout1: nil,
|
||||
StartTime: time.Now().Add(-30 * time.Minute),
|
||||
EndTime: nil,
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: badOfferID,
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("0.1"),
|
||||
ExpectedAmount: coins.StrToDecimal("1"),
|
||||
ExchangeRate: coins.StrToExchangeRate("0.1"),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.ExpectingKeys,
|
||||
LastStatusUpdateTime: time.Now(),
|
||||
MoneroStartHeight: 12345,
|
||||
Timeout0: nil,
|
||||
Timeout1: nil,
|
||||
StartTime: time.Now().Add(-30 * time.Minute),
|
||||
EndTime: nil,
|
||||
}
|
||||
err = db.PutSwap(swapEntry)
|
||||
require.NoError(t, err)
|
||||
@@ -174,37 +176,39 @@ func TestDatabase_SwapTable(t *testing.T) {
|
||||
timeout1 := time.Now().Add(60 * time.Minute)
|
||||
|
||||
infoA := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: offerA.ID,
|
||||
Provides: offerA.Provides,
|
||||
ProvidedAmount: offerA.MinAmount,
|
||||
ExpectedAmount: offerA.MinAmount,
|
||||
ExchangeRate: offerA.ExchangeRate,
|
||||
EthAsset: offerA.EthAsset,
|
||||
Status: types.ContractReady,
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: startTime,
|
||||
EndTime: nil,
|
||||
Timeout0: &timeout0,
|
||||
Timeout1: &timeout1,
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: offerA.ID,
|
||||
Provides: offerA.Provides,
|
||||
ProvidedAmount: offerA.MinAmount,
|
||||
ExpectedAmount: offerA.MinAmount,
|
||||
ExchangeRate: offerA.ExchangeRate,
|
||||
EthAsset: offerA.EthAsset,
|
||||
Status: types.ContractReady,
|
||||
LastStatusUpdateTime: time.Now(),
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: startTime,
|
||||
EndTime: nil,
|
||||
Timeout0: &timeout0,
|
||||
Timeout1: &timeout1,
|
||||
}
|
||||
err = db.PutSwap(infoA)
|
||||
require.NoError(t, err)
|
||||
|
||||
infoB := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: types.Hash{0x2},
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("1.5"),
|
||||
ExpectedAmount: coins.StrToDecimal("0.15"),
|
||||
ExchangeRate: coins.ToExchangeRate(coins.StrToDecimal("0.1")),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.XMRLocked,
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: startTime,
|
||||
EndTime: nil,
|
||||
Timeout0: &timeout0,
|
||||
Timeout1: &timeout1,
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: types.Hash{0x2},
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("1.5"),
|
||||
ExpectedAmount: coins.StrToDecimal("0.15"),
|
||||
ExchangeRate: coins.ToExchangeRate(coins.StrToDecimal("0.1")),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.XMRLocked,
|
||||
LastStatusUpdateTime: time.Now(),
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: startTime,
|
||||
EndTime: nil,
|
||||
Timeout0: &timeout0,
|
||||
Timeout1: &timeout1,
|
||||
}
|
||||
err = db.PutSwap(infoB)
|
||||
require.NoError(t, err)
|
||||
@@ -230,19 +234,20 @@ func TestDatabase_GetAllSwaps_InvalidEntry(t *testing.T) {
|
||||
timeout1 := time.Now().Add(60 * time.Minute)
|
||||
|
||||
goodInfo := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: types.Hash{0x1, 0x2, 0x3},
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("1.5"),
|
||||
ExpectedAmount: coins.StrToDecimal("0.15"),
|
||||
ExchangeRate: coins.ToExchangeRate(coins.StrToDecimal("0.1")),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.ETHLocked,
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: startTime,
|
||||
EndTime: nil,
|
||||
Timeout0: &timeout0,
|
||||
Timeout1: &timeout1,
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: types.Hash{0x1, 0x2, 0x3},
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("1.5"),
|
||||
ExpectedAmount: coins.StrToDecimal("0.15"),
|
||||
ExchangeRate: coins.ToExchangeRate(coins.StrToDecimal("0.1")),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.ETHLocked,
|
||||
LastStatusUpdateTime: time.Now(),
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: startTime,
|
||||
EndTime: nil,
|
||||
Timeout0: &timeout0,
|
||||
Timeout1: &timeout1,
|
||||
}
|
||||
err = db.PutSwap(goodInfo)
|
||||
require.NoError(t, err)
|
||||
@@ -292,19 +297,20 @@ func TestDatabase_SwapTable_Update(t *testing.T) {
|
||||
timeout1 := time.Now().Add(60 * time.Minute)
|
||||
|
||||
infoA := &swap.Info{
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: id,
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("0.1"),
|
||||
ExpectedAmount: coins.StrToDecimal("1"),
|
||||
ExchangeRate: coins.StrToExchangeRate("0.1"),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.XMRLocked,
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: startTime,
|
||||
EndTime: nil,
|
||||
Timeout0: &timeout0,
|
||||
Timeout1: &timeout1,
|
||||
Version: swap.CurInfoVersion,
|
||||
ID: id,
|
||||
Provides: coins.ProvidesXMR,
|
||||
ProvidedAmount: coins.StrToDecimal("0.1"),
|
||||
ExpectedAmount: coins.StrToDecimal("1"),
|
||||
ExchangeRate: coins.StrToExchangeRate("0.1"),
|
||||
EthAsset: types.EthAsset{},
|
||||
Status: types.XMRLocked,
|
||||
LastStatusUpdateTime: time.Now(),
|
||||
MoneroStartHeight: 12345,
|
||||
StartTime: startTime,
|
||||
EndTime: nil,
|
||||
Timeout0: &timeout0,
|
||||
Timeout1: &timeout1,
|
||||
}
|
||||
err = db.PutSwap(infoA)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -35,6 +35,8 @@ type Info struct {
|
||||
ExchangeRate *coins.ExchangeRate `json:"exchangeRate" validate:"required"`
|
||||
EthAsset types.EthAsset `json:"ethAsset"`
|
||||
Status Status `json:"status" validate:"required"`
|
||||
// LastStatusUpdateTime is the time at which the status was last updated.
|
||||
LastStatusUpdateTime time.Time `json:"lastStatusUpdateTime" validate:"required"`
|
||||
// MoneroStartHeight is the Monero block number when the swap begins.
|
||||
MoneroStartHeight uint64 `json:"moneroStartHeight" validate:"required"`
|
||||
// StartTime is the time at which the swap is initiated via
|
||||
@@ -71,17 +73,18 @@ func NewInfo(
|
||||
statusCh chan types.Status,
|
||||
) *Info {
|
||||
info := &Info{
|
||||
Version: CurInfoVersion,
|
||||
ID: id,
|
||||
Provides: provides,
|
||||
ProvidedAmount: providedAmount,
|
||||
ExpectedAmount: expectedAmount,
|
||||
ExchangeRate: exchangeRate,
|
||||
EthAsset: ethAsset,
|
||||
Status: status,
|
||||
MoneroStartHeight: moneroStartHeight,
|
||||
statusCh: statusCh,
|
||||
StartTime: time.Now(),
|
||||
Version: CurInfoVersion,
|
||||
ID: id,
|
||||
Provides: provides,
|
||||
ProvidedAmount: providedAmount,
|
||||
ExpectedAmount: expectedAmount,
|
||||
ExchangeRate: exchangeRate,
|
||||
EthAsset: ethAsset,
|
||||
Status: status,
|
||||
LastStatusUpdateTime: time.Now(),
|
||||
MoneroStartHeight: moneroStartHeight,
|
||||
statusCh: statusCh,
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
return info
|
||||
}
|
||||
@@ -94,6 +97,7 @@ func (i *Info) StatusCh() chan types.Status {
|
||||
// SetStatus ...
|
||||
func (i *Info) SetStatus(s Status) {
|
||||
i.Status = s
|
||||
i.LastStatusUpdateTime = time.Now()
|
||||
}
|
||||
|
||||
// UnmarshalInfo deserializes a JSON Info struct, checking the version for compatibility
|
||||
|
||||
@@ -29,6 +29,7 @@ func Test_InfoMarshal(t *testing.T) {
|
||||
)
|
||||
err := info.StartTime.UnmarshalJSON([]byte("\"2023-02-20T17:29:43.471020297-05:00\""))
|
||||
require.NoError(t, err)
|
||||
info.LastStatusUpdateTime = info.StartTime
|
||||
|
||||
infoBytes, err := vjson.MarshalStruct(info)
|
||||
require.NoError(t, err)
|
||||
@@ -43,6 +44,7 @@ func Test_InfoMarshal(t *testing.T) {
|
||||
"ethAsset": "ETH",
|
||||
"moneroStartHeight": 200,
|
||||
"status": "Success",
|
||||
"lastStatusUpdateTime": "2023-02-20T17:29:43.471020297-05:00",
|
||||
"startTime": "2023-02-20T17:29:43.471020297-05:00"
|
||||
}`
|
||||
require.JSONEq(t, expectedJSON, string(infoBytes))
|
||||
|
||||
96
rpc/swap.go
96
rpc/swap.go
@@ -120,15 +120,17 @@ func (s *SwapService) GetPast(_ *http.Request, req *GetPastRequest, resp *GetPas
|
||||
|
||||
// OngoingSwap represents an ongoing swap returned by swap_getOngoing.
|
||||
type OngoingSwap struct {
|
||||
ID types.Hash `json:"id" validate:"required"`
|
||||
Provided coins.ProvidesCoin `json:"provided" validate:"required"`
|
||||
ProvidedAmount *apd.Decimal `json:"providedAmount" validate:"required"`
|
||||
ExpectedAmount *apd.Decimal `json:"expectedAmount" validate:"required"`
|
||||
ExchangeRate *coins.ExchangeRate `json:"exchangeRate" validate:"required"`
|
||||
Status types.Status `json:"status" validate:"required"`
|
||||
StartTime time.Time `json:"startTime" validate:"required"`
|
||||
Timeout0 *time.Time `json:"timeout0"`
|
||||
Timeout1 *time.Time `json:"timeout1"`
|
||||
ID types.Hash `json:"id" validate:"required"`
|
||||
Provided coins.ProvidesCoin `json:"provided" validate:"required"`
|
||||
ProvidedAmount *apd.Decimal `json:"providedAmount" validate:"required"`
|
||||
ExpectedAmount *apd.Decimal `json:"expectedAmount" validate:"required"`
|
||||
ExchangeRate *coins.ExchangeRate `json:"exchangeRate" validate:"required"`
|
||||
Status types.Status `json:"status" validate:"required"`
|
||||
LastStatusUpdateTime time.Time `json:"lastStatusUpdateTime" validate:"required"`
|
||||
StartTime time.Time `json:"startTime" validate:"required"`
|
||||
Timeout0 *time.Time `json:"timeout0"`
|
||||
Timeout1 *time.Time `json:"timeout1"`
|
||||
EstimatedTimeToCompletion time.Duration `json:"estimatedTimeToCompletion" validate:"required"`
|
||||
}
|
||||
|
||||
// GetOngoingRequest ...
|
||||
@@ -143,6 +145,8 @@ type GetOngoingResponse struct {
|
||||
|
||||
// GetOngoing returns information about the ongoing swap with the given ID, if there is one.
|
||||
func (s *SwapService) GetOngoing(_ *http.Request, req *GetOngoingRequest, resp *GetOngoingResponse) error {
|
||||
env := s.backend.Env()
|
||||
|
||||
var (
|
||||
swaps []*swap.Info
|
||||
err error
|
||||
@@ -154,7 +158,7 @@ func (s *SwapService) GetOngoing(_ *http.Request, req *GetOngoingRequest, resp *
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
info, err := s.sm.GetOngoingSwap(*req.OfferID)
|
||||
info, err := s.sm.GetOngoingSwap(*req.OfferID) //nolint:govet
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,9 +175,15 @@ func (s *SwapService) GetOngoing(_ *http.Request, req *GetOngoingRequest, resp *
|
||||
swap.ExpectedAmount = info.ExpectedAmount
|
||||
swap.ExchangeRate = info.ExchangeRate
|
||||
swap.Status = info.Status
|
||||
swap.LastStatusUpdateTime = info.LastStatusUpdateTime
|
||||
swap.StartTime = info.StartTime
|
||||
swap.Timeout0 = info.Timeout0
|
||||
swap.Timeout1 = info.Timeout1
|
||||
swap.EstimatedTimeToCompletion, err = estimatedTimeToCompletion(env, info.Status, info.LastStatusUpdateTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to estimate time to completion for swap %s: %w", info.ID, err)
|
||||
}
|
||||
|
||||
resp.Swaps[i] = swap
|
||||
}
|
||||
|
||||
@@ -345,3 +355,69 @@ func (s *SwapService) SuggestedExchangeRate(_ *http.Request, _ *interface{}, res
|
||||
resp.ExchangeRate = exchangeRate
|
||||
return nil
|
||||
}
|
||||
|
||||
// estimatedTimeToCompletionreturns the estimated time for the swap to complete
|
||||
// in the optimistic case based on the given status and the time the status was updated.
|
||||
func estimatedTimeToCompletion(
|
||||
env common.Environment,
|
||||
status types.Status,
|
||||
lastStatusUpdateTime time.Time,
|
||||
) (time.Duration, error) {
|
||||
if time.Until(lastStatusUpdateTime) > 0 {
|
||||
return 0, fmt.Errorf("last status update time must be less than now")
|
||||
}
|
||||
|
||||
timeForStatus, err := estimatedTimeToCompletionForStatus(env, status)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
estimatedTime := timeForStatus - time.Since(lastStatusUpdateTime)
|
||||
if estimatedTime < 0 {
|
||||
// TODO: add explanation as to why time to completion can't be estimated,
|
||||
// probably because we need to wait for the countparty to refund, or
|
||||
// monero block times were longer than expected.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return estimatedTime.Round(time.Second), nil
|
||||
}
|
||||
|
||||
// estimatedTimeToCompletionForStatus returns the estimated time for the swap to complete
|
||||
// in the optimistic case based on the given status, assuming the status was updated just now.
|
||||
func estimatedTimeToCompletionForStatus(env common.Environment, status types.Status) (time.Duration, error) {
|
||||
var (
|
||||
moneroBlockTime time.Duration
|
||||
ethBlockTime time.Duration
|
||||
)
|
||||
|
||||
switch env {
|
||||
case common.Development:
|
||||
moneroBlockTime = time.Second
|
||||
ethBlockTime = time.Second
|
||||
default:
|
||||
moneroBlockTime = time.Minute * 2
|
||||
ethBlockTime = time.Second * 12
|
||||
}
|
||||
|
||||
// we assume the Monero lock step will take 10 blocks, and for the taker,
|
||||
// there is the additional 2 blocks to transfer the funds from the swap wallet
|
||||
// to the original wallet.
|
||||
//
|
||||
// we also assume all Ethereum txs will take at maximum 2 blocks
|
||||
// to be included.
|
||||
switch status {
|
||||
case types.ExpectingKeys:
|
||||
return (moneroBlockTime * 12) + (ethBlockTime * 6), nil
|
||||
case types.KeysExchanged:
|
||||
return (moneroBlockTime * 10) + (ethBlockTime * 6), nil
|
||||
case types.ETHLocked:
|
||||
return (moneroBlockTime * 12) + (ethBlockTime * 4), nil
|
||||
case types.XMRLocked:
|
||||
return (moneroBlockTime * 10) + (ethBlockTime * 4), nil
|
||||
case types.ContractReady:
|
||||
return (moneroBlockTime * 2) + (ethBlockTime * 2), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid status %s; must be ongoing status type", status)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user