Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ type UpdateLeverageAction struct {

// UpdateIsolatedMarginAction represents isolated margin update
type UpdateIsolatedMarginAction struct {
Type string `json:"type" msgpack:"type"`
Asset int `json:"asset" msgpack:"asset"`
IsBuy bool `json:"isBuy" msgpack:"isBuy"`
Ntli float64 `json:"ntli" msgpack:"ntli"`
Type string `json:"type" msgpack:"type"`
Asset int `json:"asset" msgpack:"asset"`
IsBuy bool `json:"isBuy" msgpack:"isBuy"`
Ntli int64 `json:"ntli" msgpack:"ntli"`
}

// OrderWire represents the wire format for orders with deterministic field ordering
Expand Down
4 changes: 2 additions & 2 deletions actions_easyjson.go

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

63 changes: 58 additions & 5 deletions exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ func (e *Exchange) signAgent(
return SignAgent(e.privateKey, agentAddress, agentName, nonce, mainnet)
}

// executeAction executes an action and unmarshals the response into the given result
func (e *Exchange) executeAction(ctx context.Context, action, result any) error {
// signAndPost signs an L1 action and posts it, returning the raw response body.
func (e *Exchange) signAndPost(ctx context.Context, action any) ([]byte, error) {
nonce := e.nextNonce()

sig, err := e.signL1Action(
Expand All @@ -159,20 +159,73 @@ func (e *Exchange) executeAction(ctx context.Context, action, result any) error
e.expiresAfter,
e.client.baseURL == MainnetAPIURL,
)
if err != nil {
return nil, err
}

return e.postAction(ctx, action, sig, nonce)
}

// executeAction executes an action and unmarshals the response into the given result.
// The result type is responsible for reporting a rejected action; use
// executeActionChecked for result types that do not decode the status envelope.
func (e *Exchange) executeAction(ctx context.Context, action, result any) error {
resp, err := e.signAndPost(ctx, action)
if err != nil {
return err
}

resp, err := e.postAction(ctx, action, sig, nonce)
return json.Unmarshal(resp, result)
}

// executeActionChecked is executeAction for result types that ignore the
// {"status":"err","response":"..."} envelope. Without this check such a
// response unmarshals into a zero-value result and reports no error.
func (e *Exchange) executeActionChecked(ctx context.Context, action, result any) error {
resp, err := e.signAndPost(ctx, action)
if err != nil {
return err
}

if err := json.Unmarshal(resp, result); err != nil {
if err := exchangeActionError(resp); err != nil {
return err
}

return nil
return json.Unmarshal(resp, result)
}

// ExchangeActionError is returned when the exchange rejects an action,
// as opposed to the request failing to be sent or decoded.
type ExchangeActionError struct {
Message string
}

func (e *ExchangeActionError) Error() string {
return e.Message
}

// exchangeActionError reports the exchange's rejection of an action, or nil if
// the response is not a rejection. A body that does not parse as the envelope
// is left for the caller's json.Unmarshal to reject.
func exchangeActionError(resp []byte) error {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please rename to parseExchangeActionError

var envelope struct {
Status string `json:"status"`
Response json.RawMessage `json:"response"`
}
if err := json.Unmarshal(resp, &envelope); err != nil {
return nil
}
if envelope.Status != "err" {
return nil
}
var msg string
if err := json.Unmarshal(envelope.Response, &msg); err == nil && msg != "" {
return &ExchangeActionError{Message: msg}
}
if len(envelope.Response) > 0 {
return &ExchangeActionError{Message: string(envelope.Response)}
}
return &ExchangeActionError{Message: "exchange action failed"}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not create structs for errors; Instead, let's use error sentinels

var (
  ErrExchangeAction = errors.New("exchange action failed")
)
// and then in some other place

if len(envelope.Response) > 0 {
		return fmt.Errorf("%w, %s", ErrExchangeAction, envelope.Response)
}

}

func (e *Exchange) postAction(
Expand Down
11 changes: 8 additions & 3 deletions exchange_others.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"math"
"math/big"
"sort"
"strings"
Expand Down Expand Up @@ -33,7 +34,7 @@ func (e *Exchange) UpdateLeverage(
}

var result UserState
if err := e.executeAction(ctx, action, &result); err != nil {
if err := e.executeActionChecked(ctx, action, &result); err != nil {
return nil, err
}
return &result, nil
Expand All @@ -53,16 +54,20 @@ func (e *Exchange) UpdateIsolatedMargin(
Type: "updateIsolatedMargin",
Asset: asset,
IsBuy: amount > 0,
Ntli: abs(amount),
Ntli: updateIsolatedMarginNtli(amount),
}

var result UserState
if err := e.executeAction(ctx, action, &result); err != nil {
if err := e.executeActionChecked(ctx, action, &result); err != nil {
return nil, err
}
return &result, nil
}

func updateIsolatedMarginNtli(amount float64) int64 {
return int64(math.Ceil(abs(amount) * 1_000_000))
}

// SlippagePrice calculates the slippage price for market orders
func (e *Exchange) SlippagePrice(
ctx context.Context,
Expand Down
57 changes: 57 additions & 0 deletions exchange_others_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package hyperliquid

import (
"context"
"encoding/json"
"testing"

"github.com/sonirico/vago/ent"
Expand All @@ -17,6 +18,62 @@ func setupExchange(t *testing.T) *Exchange {
return exchange
}

func TestUpdateIsolatedMarginNtli(t *testing.T) {
tests := []struct {
name string
amount float64
want int64
}{
{name: "whole dollar", amount: 64, want: 64_000_000},
{name: "fractional dollars", amount: 64.71885, want: 64_718_850},
{name: "rounds up sub micro dollar", amount: 0.0000001, want: 1},
{name: "negative withdraw", amount: -1.25, want: 1_250_000},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, updateIsolatedMarginNtli(tt.amount))
})
}
}

func TestUpdateIsolatedMarginActionJSONUsesIntegerNtli(t *testing.T) {
action := UpdateIsolatedMarginAction{
Type: "updateIsolatedMargin",
Asset: 110066,
IsBuy: true,
Ntli: 64_718_850,
}

body, err := json.Marshal(action)
require.NoError(t, err)
require.NotContains(t, string(body), "64718850.0")
require.JSONEq(t, `{"type":"updateIsolatedMargin","asset":110066,"isBuy":true,"ntli":64718850}`, string(body))

var decoded UpdateIsolatedMarginAction
require.NoError(t, json.Unmarshal(body, &decoded))
require.Equal(t, action, decoded)
}

func TestExchangeActionError(t *testing.T) {
err := exchangeActionError([]byte(`{"status":"err","response":"Cannot switch leverage type with open position."}`))
require.EqualError(t, err, "Cannot switch leverage type with open position.")

var actionErr *ExchangeActionError
require.ErrorAs(t, err, &actionErr)
require.Equal(t, "Cannot switch leverage type with open position.", actionErr.Message)

// A non-string "response" is surfaced verbatim rather than swallowed.
err = exchangeActionError([]byte(`{"status":"err","response":{"code":42}}`))
require.EqualError(t, err, `{"code":42}`)

// An "err" status with no response body still reports a failure.
require.EqualError(t, exchangeActionError([]byte(`{"status":"err"}`)), "exchange action failed")

require.NoError(t, exchangeActionError([]byte(`{"status":"ok","response":{"type":"default"}}`)))
require.NoError(t, exchangeActionError([]byte(`not json`)))
}

func TestPerpDeployHaltTrading(t *testing.T) {
t.Run("halt trading success response", func(t *testing.T) {
exchange := setupExchange(t)
Expand Down
Loading