add estimated time for swap to complete for swap_getOngoing (#351)

This commit is contained in:
noot
2023-03-20 22:49:17 -04:00
committed by GitHub
parent 310bafbc7e
commit 58d94723e1
5 changed files with 189 additions and 99 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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))

View File

@@ -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)
}
}