Skip to content

Commit

Permalink
Merge pull request #8 from gateway-fm/API-24
Browse files Browse the repository at this point in the history
API-24: Positions
  • Loading branch information
asolovov authored Aug 29, 2023
2 parents 6b6b1f6 + 47e46bf commit fed4028
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 11 deletions.
50 changes: 40 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# perpsv3-Go

This repository contains a Go library for interacting with smart contracts related to a [Synthetix V3](https://docs.synthetix.io/v/v3/)
DeFi protocol. It includes components to work with Core, Spot Market, and Perps Market contracts deployed on the Optimism
This repository contains a Go library for interacting with smart contracts related to a [Synthetix V3](https://docs.synthetix.io/v/v3/)
DeFi protocol. It includes components to work with Core, Spot Market, and Perps Market contracts deployed on the Optimism
mainnet and Optimistic Goerli testnet.

## Table of Contents
Expand Down Expand Up @@ -66,7 +66,7 @@ func GetGoerliDefaultPerpsvConfig() *PerpsvConfig {
}
```

And a configuration for Optimism mainnet. Be informed that mainnet configs will return an error at this version due to
And a configuration for Optimism mainnet. Be informed that mainnet configs will return an error at this version due to
the luck of Perps Market contract address on mainnnet.

```go
Expand Down Expand Up @@ -104,8 +104,8 @@ func main() {
conf := &config.PerpsvConfig{
RPC: "https://rpc.optimism.gateway.fm",
//...
}
}

perpsLib, err := perpsv3_Go.Create(conf)
if err != nil {
log.Fatal(err)
Expand All @@ -120,7 +120,7 @@ func main() {

### == Model ==

Using Trades services you operate with Trades model which represents a `OrderSettled` event of Perps Market smart-contract
Using Trades services you operate with Trades model which represents a `OrderSettled` event of Perps Market smart-contract
with some additional fields:

```go
Expand Down Expand Up @@ -167,14 +167,14 @@ If you want to query more than 20 000 block or query old block be sure you use a

### == ListenTrades ==

To subscribe on the contract `OrederSettled` event use the ListenTrades function.
To subscribe on the contract `OrederSettled` event use the ListenTrades function.

```go
func ListenTrades() (*events.TradeSubscription, error) {}
```

The goroutine will return events as a `Trade` model on the `TradesChan` chanel and errors on the `ErrChan` chanel. To
close the subscription use the `Close` function.
The goroutine will return events as a `Trade` model on the `TradesChan` chanel and errors on the `ErrChan` chanel. To
close the subscription use the `Close` function.

You can see an [example](examples/trades_events.go) of the usage here:

Expand All @@ -195,7 +195,7 @@ func main() {
if err != nil {
log.Fatal(err)
}

subs, err := lib.ListenTrades()
if err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -229,5 +229,35 @@ func main() {
}
```

### === Positions ===

### == Model ==

Using Positions services you operate with Position model which represents a `OpenPosition` data struct of Perps Market
smart-contract with some additional fields:

```go
type Position struct {
// Data from the contract
TotalPnl *big.Int // Represents the total profit and loss for the position
AccruedFunding *big.Int // Represents the accrued funding for the position
PositionSize *big.Int // Represents the size of the position
// Data from the latest block
BlockNumber uint64 // Represents the block number at which the position data was fetched
BlockTimestamp uint64 // Represents the timestamp of the block at which the position data was fetched
}
```

### == GetPosition ==

To get `Position` by reading contract with `getOpenPosition` method use the GetPosition function:

```go
func GetPosition(accountID *big.Int, marketID *big.Int) (*models.Position, error) {}
```

It will return data from the contract in the latest block. Function can return contract error if the market ID is invalid.
If account ID is invalid it will return model with blank fields.

## License
This project is licensed under the MIT License.
12 changes: 12 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ var (
FilterErr = fmt.Errorf("contract filter error")
// ListenEventErr is used when error occurred while event listening
ListenEventErr = fmt.Errorf("event listen error")
// ReadContractErr is used when error occurred while using contract view functions
ReadContractErr = fmt.Errorf("contract error read")
// RPCErr is used when error returned from RPC provider
RPCErr = fmt.Errorf("rpc provider error")
)

func GetDialRPCErr(err error) error {
Expand All @@ -34,3 +38,11 @@ func GetFilterErr(err error, contract string) error {
func GetEventListenErr(err error, event string) error {
return fmt.Errorf("%v %w: %w", event, ListenEventErr, err)
}

func GetReadContractErr(err error, contract string, method string) error {
return fmt.Errorf("%v %w %v method: %w", contract, ReadContractErr, method, err)
}

func GetRPCProviderErr(err error, method string) error {
return fmt.Errorf("%w using %v: %w", RPCErr, method, err)
}
16 changes: 16 additions & 0 deletions mocks/service/mockService.go

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

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

import "math/big"

// Position
// - TotalPnl: Represents the total profit and loss for the position.
// - AccruedFunding: Represents the accrued funding for the position.
// - PositionSize: Represents the size of the position.
// - BlockNumber: Represents the block number at which the position data was fetched.
// - BlockTimestamp: Represents the timestamp of the block at which the position data was fetched.
type Position struct {
TotalPnl *big.Int
AccruedFunding *big.Int
PositionSize *big.Int
BlockNumber uint64
BlockTimestamp uint64
}

// positionContract is a data struct received from contract
type positionContract struct {
TotalPnl *big.Int
AccruedFunding *big.Int
PositionSize *big.Int
}

// GetPositionFromContract is used to get Position struct from given contract data struct and block values
func GetPositionFromContract(position positionContract, blockN uint64, blockT uint64) *Position {
return &Position{
TotalPnl: position.TotalPnl,
AccruedFunding: position.AccruedFunding,
PositionSize: position.PositionSize,
BlockNumber: blockN,
BlockTimestamp: blockT,
}
}
51 changes: 51 additions & 0 deletions models/position_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package models

import (
"math/big"
"testing"
"time"

"github.com/stretchr/testify/require"
)

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

testCases := []struct {
name string
obj positionContract
blockN uint64
time uint64
want *Position
}{
{
name: "blank object",
want: &Position{},
},
{
name: "full object",
obj: positionContract{
TotalPnl: big.NewInt(1),
AccruedFunding: big.NewInt(2),
PositionSize: big.NewInt(3),
},
time: uint64(timeNow.Unix()),
blockN: uint64(4),
want: &Position{
TotalPnl: big.NewInt(1),
AccruedFunding: big.NewInt(2),
PositionSize: big.NewInt(3),
BlockTimestamp: uint64(timeNow.Unix()),
BlockNumber: uint64(4),
},
},
}

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

require.Equal(t, tt.want, res)
})
}
}
File renamed without changes.
22 changes: 22 additions & 0 deletions perpsv3.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package perpsv3_Go

import (
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/gateway-fm/perpsv3-Go/config"
Expand All @@ -26,6 +28,13 @@ type IPerpsv3 interface {
// To close the subscription use events.TradeSubscription `Close` function
ListenTrades() (*events.TradeSubscription, error)

// GetPosition is used to get position data struct from latest block with given params
// Function can return contract error if market ID is invalid
GetPosition(accountID *big.Int, marketID *big.Int) (*models.Position, error)

// Config is used to get current lib config
Config() *config.PerpsvConfig

// Close used to stop the lib work
Close()
}
Expand Down Expand Up @@ -69,6 +78,19 @@ func (p *Perpsv3) ListenTrades() (*events.TradeSubscription, error) {
return sub, nil
}

func (p *Perpsv3) GetPosition(accountID *big.Int, marketID *big.Int) (*models.Position, error) {
pos, err := p.service.GetPosition(accountID, marketID)
if err != nil {
return nil, err
}

return pos, nil
}

func (p *Perpsv3) Config() *config.PerpsvConfig {
return p.config
}

func (p *Perpsv3) Close() {
p.rpcClient.Close()
}
Expand Down
49 changes: 49 additions & 0 deletions perpsv3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,52 @@ func TestPerpsv3_ListenTrades(t *testing.T) {
})
}
}

func TestPerpsv3_GetPosition(t *testing.T) {
testCases := []struct {
name string
accountID *big.Int
marketID *big.Int
wantRes *models.Position
wantErr error
}{
{
name: "no error",
accountID: big.NewInt(0),
marketID: big.NewInt(100),
wantRes: &models.Position{
TotalPnl: big.NewInt(1),
AccruedFunding: big.NewInt(2),
PositionSize: big.NewInt(3),
BlockTimestamp: uint64(time.Now().Unix()),
BlockNumber: uint64(4),
},
},
{
name: "error",
wantErr: errors.ReadContractErr,
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockService := mock_services.NewMockIService(ctrl)

p, _ := createTest(config.GetGoerliDefaultPerpsvConfig())
p.service = mockService

mockService.EXPECT().GetPosition(tt.accountID, tt.marketID).Return(tt.wantRes, tt.wantErr)

res, err := p.GetPosition(tt.accountID, tt.marketID)

if tt.wantErr == nil {
require.NoError(t, err)
require.Equal(t, tt.wantRes, res)
} else {
require.ErrorIs(t, tt.wantErr, err)
}
})
}
}
41 changes: 41 additions & 0 deletions services/positions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package services

import (
"context"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/gateway-fm/perpsv3-Go/errors"
"github.com/gateway-fm/perpsv3-Go/models"
"github.com/gateway-fm/perpsv3-Go/pkg/logger"
)

func (s *Service) GetPosition(accountID *big.Int, marketID *big.Int) (*models.Position, error) {
latest, err := s.rpcClient.BlockNumber(context.Background())
if err != nil {
logger.Log().WithField("layer", "Service-GetPositions").Errorf(
"error get latest block: %v", err.Error(),
)
return nil, errors.GetRPCProviderErr(err, "BlockNumber")
}

block, err := s.rpcClient.HeaderByNumber(context.Background(), big.NewInt(int64(latest)))
if err != nil {
logger.Log().WithField("layer", "Service-GetPositions").Errorf(
"get block by numer: %v error: %v", latest, err.Error(),
)
return nil, errors.GetRPCProviderErr(err, "HeaderByNumber")
}

opts := &bind.CallOpts{BlockNumber: big.NewInt(int64(latest))}

positionContract, err := s.perpsMarket.GetOpenPosition(opts, accountID, marketID)
if err != nil {
logger.Log().WithField("layer", "Service-GetPositions").Errorf(
"contract getOpenPosition with accountID: %v, marketID: %v error: %v", accountID, marketID, err.Error(),
)
return nil, errors.GetReadContractErr(err, "PerpsMarket", "getOpenPosition")
}

return models.GetPositionFromContract(positionContract, block.Number.Uint64(), block.Time), nil
}
Loading

0 comments on commit fed4028

Please sign in to comment.