Skip to content

Commit

Permalink
Merge pull request #12 from gateway-fm/API-100
Browse files Browse the repository at this point in the history
API-100: Position liquidated event
  • Loading branch information
Pashteto authored Sep 8, 2023
2 parents a8893d0 + f25135a commit 1c32f31
Show file tree
Hide file tree
Showing 14 changed files with 703 additions and 4 deletions.
2 changes: 1 addition & 1 deletion contracts/perpsMarketGoerli/contract.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type IEvents interface {
// ListenMarketUpdates is used to listen to all 'MarketUpdated' contract events and return them as models.MarketUpdate
// struct and return errors on ErrChan chanel
ListenMarketUpdates() (*MarketUpdateSubscription, error)

// ListenLiquidations is used to listen to all 'PositionLiquidated' contract events and return them as models.Liquidation
// struct and return errors on ErrChan chanel
ListenLiquidations() (*LiquidationSubscription, error)
}

// Events implements IEvents interface
Expand Down
76 changes: 76 additions & 0 deletions events/liquidations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package events

import (
"context"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/event"
"github.com/gateway-fm/perpsv3-Go/contracts/perpsMarketGoerli"
"github.com/gateway-fm/perpsv3-Go/errors"
"github.com/gateway-fm/perpsv3-Go/models"
"github.com/gateway-fm/perpsv3-Go/pkg/logger"
"math/big"
)

// LiquidationSubscription is a struct for listening to all 'PositionLiquidated' contract events and return them as models.Liquidation struct
type LiquidationSubscription struct {
*basicSubscription
LiquidationsChan chan *models.Liquidation
contractEventChan chan *perpsMarketGoerli.PerpsMarketGoerliPositionLiquidated
}

func (e *Events) ListenLiquidations() (*LiquidationSubscription, error) {
contractEventChan := make(chan *perpsMarketGoerli.PerpsMarketGoerliPositionLiquidated)

contractSub, err := e.perpsMarket.WatchPositionLiquidated(nil, contractEventChan, nil, nil)
if err != nil {
logger.Log().WithField("layer", "Events-PositionLiquidated").Errorf("error watch order committed: %v", err.Error())
return nil, errors.GetEventListenErr(err, "PositionLiquidated")
}

orderSub := newLiquidationSubscription(contractSub, contractEventChan)

go orderSub.listen(e.rpcClient)

return orderSub, nil
}

// newLiquidationSubscription is used to create new LiquidationSubscription instance
func newLiquidationSubscription(eventSub event.Subscription, contractEventChan chan *perpsMarketGoerli.PerpsMarketGoerliPositionLiquidated) *LiquidationSubscription {
return &LiquidationSubscription{
basicSubscription: newBasicSubscription(eventSub),
contractEventChan: contractEventChan,
LiquidationsChan: make(chan *models.Liquidation),
}
}

// listen is used to run a goroutine
func (s *LiquidationSubscription) listen(rpcClient *ethclient.Client) {
for {
select {
case <-s.stop:
close(s.LiquidationsChan)
close(s.contractEventChan)
return
case err := <-s.eventSub.Err():
logger.Log().WithField("layer", "Events-PositionLiquidated").Errorf("error listening position liquidated: %v", err.Error())
s.ErrChan <- err
continue
case positionLiquidated := <-s.contractEventChan:
block, err := rpcClient.HeaderByNumber(context.Background(), big.NewInt(int64(positionLiquidated.Raw.BlockNumber)))
time := uint64(0)
if err != nil {
logger.Log().WithField("layer", "Events-PositionLiquidated").Warningf(
"error fetching block number %v: %v; order event time set to 0 ",
positionLiquidated.Raw.BlockNumber, err.Error(),
)
s.ErrChan <- err
} else {
time = block.Time
}

order := models.GetLiquidationFromEvent(positionLiquidated, time)

s.LiquidationsChan <- order
}
}
}
71 changes: 71 additions & 0 deletions events/liquidations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package events

import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/gateway-fm/perpsv3-Go/contracts/coreGoerli"
"github.com/gateway-fm/perpsv3-Go/contracts/perpsMarketGoerli"
"github.com/gateway-fm/perpsv3-Go/contracts/spotMarketGoerli"
perps_test "github.com/gateway-fm/perpsv3-Go/utils/testing-contracts/perps-test"
"github.com/stretchr/testify/require"
"log"
"os"
"testing"
"time"
)

func TestEvents_ListenLiquidations_OnChain(t *testing.T) {
if os.Getenv("CI") != "" {
t.Skip("Skipping testing in CI environment")
}

if testing.Short() {
t.Skip("skipping test in short mode.")
}

rpc := os.Getenv("TEST_RPC_EVENTS")
if rpc == "" {
log.Fatal("no rpc in env vars")
}

rpcClient, _ := ethclient.Dial(rpc)

coreC, _ := coreGoerli.NewCoreGoerli(common.HexToAddress("0x76490713314fCEC173f44e99346F54c6e92a8E42"), rpcClient)
spot, _ := spotMarketGoerli.NewSpotMarketGoerli(common.HexToAddress("0x5FF4b3aacdeC86782d8c757FAa638d8790799E83"), rpcClient)
perps, _ := perpsMarketGoerli.NewPerpsMarketGoerli(common.HexToAddress("0xf272382cB3BE898A8CdB1A23BE056fA2Fcf4513b"), rpcClient)

e := NewEvents(rpcClient, coreC, spot, perps)

subs, err := e.ListenLiquidations()
require.NoError(t, err)

stopChan := make(chan struct{})

perpsTest := perps_test.GetTestPerpsMarket(
rpc,
"0xf272382cB3BE898A8CdB1A23BE056fA2Fcf4513b",
"0xe487Ad4291019b33e2230F8E2FB1fb6490325260",
420,
)
defer perpsTest.Close()

go func() {
for {
select {
case <-stopChan:
subs.Close()
return
case err = <-subs.ErrChan:
require.NoError(t, err)
case positionLiquidated := <-subs.LiquidationsChan:
log.Printf("liquidation received, block: %v", positionLiquidated.BlockNumber)
require.NotNil(t, positionLiquidated)
}
}
}()

//perpsTest.CommitOrder("100", "10000000000000000")
time.Sleep(10 * time.Second)

close(stopChan)
}
15 changes: 15 additions & 0 deletions mocks/events/mockEvents.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions mocks/service/mockService.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions models/liquidation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package models

import (
"math/big"

"github.com/gateway-fm/perpsv3-Go/contracts/perpsMarketGoerli"
"github.com/gateway-fm/perpsv3-Go/pkg/logger"
)

// Liquidation is a position liquidation model
// - MarketID: ID of the market used for the order.
// - AccountID: ID of the account used for the order.
// - AmountLiquidated: amount liquidated.
// - CurrentPositionSize: position size after liquidation.
// - BlockNumber: Block number where the order was committed.
// - BlockTimestamp: Timestamp of the block where the order was committed.
type Liquidation struct {
MarketID uint64
AccountID uint64
AmountLiquidated *big.Int
CurrentPositionSize *big.Int
BlockNumber uint64
BlockTimestamp uint64
}

// GetLiquidationFromEvent is used to get Liquidation struct from given contract event
func GetLiquidationFromEvent(event *perpsMarketGoerli.PerpsMarketGoerliPositionLiquidated, time uint64) *Liquidation {
if event == nil {
logger.Log().WithField("layer", "Models-Liquidation").Warning("nil event received")
return &Liquidation{}
}

marketID := uint64(0)
if event.MarketId != nil {
marketID = event.MarketId.Uint64()
}

accountID := uint64(0)
if event.AccountId != nil {
accountID = event.AccountId.Uint64()
}

return &Liquidation{
MarketID: marketID,
AccountID: accountID,
AmountLiquidated: event.AmountLiquidated,
CurrentPositionSize: event.CurrentPositionSize,
BlockNumber: event.Raw.BlockNumber,
BlockTimestamp: time,
}
}
73 changes: 73 additions & 0 deletions models/liquidation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package models

import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/gateway-fm/perpsv3-Go/contracts/perpsMarketGoerli"
"github.com/stretchr/testify/require"
"math/big"
"testing"
"time"
)

func TestGetLiquidationFromEvent(t *testing.T) {
timeNow := time.Now()

testCases := []struct {
name string
event *perpsMarketGoerli.PerpsMarketGoerliPositionLiquidated
time uint64
want *Liquidation
}{
{
name: "nil event",
want: &Liquidation{},
},
{
name: "only market ID",
event: &perpsMarketGoerli.PerpsMarketGoerliPositionLiquidated{
MarketId: big.NewInt(1),
},
want: &Liquidation{
MarketID: uint64(1),
},
},
{
name: "only account ID",
event: &perpsMarketGoerli.PerpsMarketGoerliPositionLiquidated{
AccountId: big.NewInt(1),
},
want: &Liquidation{
AccountID: uint64(1),
},
},
{
name: "full event",
event: &perpsMarketGoerli.PerpsMarketGoerliPositionLiquidated{
MarketId: big.NewInt(1),
AccountId: big.NewInt(2),
AmountLiquidated: big.NewInt(3),
CurrentPositionSize: big.NewInt(4),
Raw: types.Log{
BlockNumber: 5,
},
},
time: uint64(timeNow.Unix()),
want: &Liquidation{
MarketID: uint64(1),
AccountID: uint64(2),
AmountLiquidated: big.NewInt(3),
CurrentPositionSize: big.NewInt(4),
BlockNumber: 5,
BlockTimestamp: uint64(timeNow.Unix()),
},
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
res := GetLiquidationFromEvent(tt.event, tt.time)

require.Equal(t, tt.want, res)
})
}
}
Loading

0 comments on commit 1c32f31

Please sign in to comment.