mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
527 lines
19 KiB
Go
527 lines
19 KiB
Go
package bitstamp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
|
|
"github.com/hanzo-fi/payments/ee/plugins/bitstamp/client"
|
|
"github.com/hanzo-fi/payments/ee/plugins/bitstamp/mappers"
|
|
"github.com/hanzo-fi/payments/pkg/domain/plugins"
|
|
"github.com/hanzo-fi/payments/pkg/domain/models"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
var _ = Describe("Bitstamp Plugin Orders", func() {
|
|
var (
|
|
ctrl *gomock.Controller
|
|
m *client.MockClient
|
|
plg *Plugin
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
ctrl = gomock.NewController(GinkgoT())
|
|
m = client.NewMockClient(ctrl)
|
|
plg = &Plugin{
|
|
Plugin: plugins.NewBasePlugin(),
|
|
client: m,
|
|
logger: logging.NewDefaultLogger(GinkgoWriter, true, false, false),
|
|
currencies: map[string]int{
|
|
"USD": 2,
|
|
"EUR": 2,
|
|
"BTC": 8,
|
|
"ETH": 18,
|
|
},
|
|
currLastSync: time.Now(),
|
|
}
|
|
})
|
|
|
|
AfterEach(func() {
|
|
ctrl.Finish()
|
|
})
|
|
|
|
// fromPayloadWithMarkets builds a FetchNextOrdersRequest.FromPayload
|
|
// containing a PSPAccount whose metadata lists the given markets.
|
|
// The engine unwraps its own envelope before calling the plugin, so
|
|
// FromPayload is just the raw PSPAccount JSON.
|
|
// Markets must be in slash format (e.g. "BTC/USD"); reference is the
|
|
// currency that owns the orders being fetched.
|
|
fromPayloadWithMarkets := func(reference string, markets []string) json.RawMessage {
|
|
marketsRaw, _ := json.Marshal(markets)
|
|
account := models.PSPAccount{
|
|
Reference: reference,
|
|
Metadata: map[string]string{mappers.MetadataKeyTradableMarkets: string(marketsRaw)},
|
|
}
|
|
payload, _ := json.Marshal(account)
|
|
return payload
|
|
}
|
|
|
|
Context("fetching next orders", func() {
|
|
It("returns error when FromPayload is empty", func(ctx SpecContext) {
|
|
_, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{})
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).To(ContainSubstring("missing payload in FromPayload"))
|
|
})
|
|
|
|
It("passes the market name directly to the API", func(ctx SpecContext) {
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{}, nil)
|
|
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.Orders).To(BeEmpty())
|
|
})
|
|
|
|
It("logs Info and skips market on 404 instead of erroring", func(ctx SpecContext) {
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return(nil, &client.NotFoundError{Endpoint: "/api/v2/account_order_data/", Message: "not found"})
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "ETH/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{}, nil)
|
|
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD", "ETH/USD"}),
|
|
})
|
|
// 404 must not bubble up as an error; the cycle continues.
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.Orders).To(BeEmpty())
|
|
})
|
|
|
|
It("returns error and halts when a market fails with non-404", func(ctx SpecContext) {
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return(nil, errors.New("boom"))
|
|
|
|
_, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD", "ETH/USD"}),
|
|
})
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
|
|
It("emits OPEN PSPOrder for order_created with no fills", func(ctx SpecContext) {
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{{
|
|
Event: "order_created",
|
|
EventID: "00000000-0000-0000-0000-000000000001",
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("1000"), IDStr: "1000",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709892",
|
|
Amount: json.Number("0.00028416"),
|
|
AmountStr: "0.00028416",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "0.00028416",
|
|
PriceStr: "77400.00",
|
|
},
|
|
}}, nil)
|
|
|
|
// BUY order: source is quote (USD), so reference must be "USD".
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.Orders).To(HaveLen(1))
|
|
o := resp.Orders[0]
|
|
Expect(o.Reference).To(Equal("1000"))
|
|
Expect(o.Direction).To(Equal(models.ORDER_DIRECTION_BUY))
|
|
Expect(o.Status).To(Equal(models.ORDER_STATUS_OPEN))
|
|
Expect(o.Type).To(Equal(models.ORDER_TYPE_LIMIT))
|
|
Expect(o.TimeInForce).To(Equal(models.TIME_IN_FORCE_GOOD_UNTIL_CANCELLED))
|
|
Expect(o.BaseQuantityOrdered.Int64()).To(Equal(int64(28416)))
|
|
Expect(o.BaseQuantityFilled.Sign()).To(Equal(0))
|
|
Expect(o.LimitPrice.Int64()).To(Equal(int64(7740000)))
|
|
Expect(o.SourceAsset).To(Equal("USD/2"))
|
|
Expect(o.DestinationAsset).To(Equal("BTC/8"))
|
|
Expect(o.SourceAccountReference).ToNot(BeNil())
|
|
Expect(*o.SourceAccountReference).To(Equal("USD"))
|
|
Expect(o.DestinationAccountReference).To(BeNil())
|
|
Expect(resp.HasMore).To(BeFalse())
|
|
})
|
|
|
|
It("emits FILLED PSPOrder for order_deleted with full fill", func(ctx SpecContext) {
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{{
|
|
Event: "order_deleted",
|
|
EventID: "00000000-0000-0000-0000-000000000002",
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("1001"), IDStr: "1001",
|
|
OrderType: 1, OrderSubtype: 0,
|
|
Datetime: "1779717950",
|
|
Amount: json.Number("0.00028416"),
|
|
AmountStr: "0",
|
|
AmountTraded: "0.00028416",
|
|
AmountAtCreate: "0.00028416",
|
|
PriceStr: "77400.00",
|
|
},
|
|
}}, nil)
|
|
|
|
// SELL order: source is base (BTC), so reference must be "BTC".
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("BTC", []string{"BTC/USD"}),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.Orders).To(HaveLen(1))
|
|
o := resp.Orders[0]
|
|
Expect(o.Reference).To(Equal("1001"))
|
|
Expect(o.Direction).To(Equal(models.ORDER_DIRECTION_SELL))
|
|
Expect(o.Status).To(Equal(models.ORDER_STATUS_FILLED))
|
|
Expect(o.BaseQuantityFilled.Int64()).To(Equal(int64(28416)))
|
|
Expect(o.SourceAccountReference).ToNot(BeNil())
|
|
Expect(*o.SourceAccountReference).To(Equal("BTC"))
|
|
Expect(o.DestinationAccountReference).To(BeNil())
|
|
})
|
|
|
|
It("emits CANCELLED PSPOrder for order_deleted with remaining amount", func(ctx SpecContext) {
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{{
|
|
Event: "order_deleted",
|
|
EventID: "00000000-0000-0000-0000-000000000003",
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("1002"), IDStr: "1002",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779717951",
|
|
Amount: json.Number("0.00028416"),
|
|
AmountStr: "0.00028416",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "0.00028416",
|
|
PriceStr: "77400.00",
|
|
},
|
|
}}, nil)
|
|
|
|
// BUY order: source is quote (USD), so reference must be "USD".
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.Orders).To(HaveLen(1))
|
|
Expect(resp.Orders[0].Status).To(Equal(models.ORDER_STATUS_CANCELLED))
|
|
})
|
|
|
|
It("handles scientific notation price (7.74E+4 = 77400)", func(ctx SpecContext) {
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{{
|
|
Event: "order_created",
|
|
EventID: "00000000-0000-0000-0000-000000000004",
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("1003"), IDStr: "1003",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709892",
|
|
Amount: json.Number("0.00028416"),
|
|
AmountStr: "0.00028416",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "0.00028416",
|
|
PriceStr: "7.74E+4",
|
|
},
|
|
}}, nil)
|
|
|
|
// BUY order: source is quote (USD), so reference must be "USD".
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.Orders).To(HaveLen(1))
|
|
// 7.74E+4 = 77400.00 → 7740000 at quotePrec=2
|
|
Expect(resp.Orders[0].LimitPrice.Int64()).To(Equal(int64(7740000)))
|
|
})
|
|
|
|
It("advances the since_id cursor and passes it on the next call", func(ctx SpecContext) {
|
|
const eventID = "a1b2c3d4-e5f6-a1b2-c3d4-e5f6a1b2c3d4"
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{{
|
|
Event: "order_created",
|
|
EventID: eventID,
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("2000"), IDStr: "2000",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709892",
|
|
Amount: json.Number("0.5"),
|
|
AmountStr: "0.5",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "0.5",
|
|
PriceStr: "60000.00",
|
|
},
|
|
}}, nil)
|
|
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
var state ordersState
|
|
Expect(json.Unmarshal(resp.NewState, &state)).To(Succeed())
|
|
// State key is the market name as passed in FromPayload.
|
|
Expect(state.LastSeenEventIDPerMarket["BTC/USD"]).To(Equal(eventID))
|
|
|
|
// Second call: since_id=eventID passed for the same market.
|
|
expectedSinceID := eventID
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", &expectedSinceID).
|
|
Return([]client.AccountOrderDataEvent{}, nil)
|
|
|
|
_, err = plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
State: resp.NewState,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("skips the event whose ID equals the since_id cursor to avoid duplicate imports", func(ctx SpecContext) {
|
|
const sinceID = "a1b2c3d4-e5f6-a1b2-c3d4-e5f6a1b2c3d4"
|
|
const newEventID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
|
|
|
|
initialState, _ := json.Marshal(ordersState{
|
|
LastSeenEventIDPerMarket: map[string]string{"BTC/USD": sinceID},
|
|
})
|
|
|
|
// API returns the since_id event first (as Bitstamp does), followed by a new event.
|
|
expectedSinceID := sinceID
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", &expectedSinceID).
|
|
Return([]client.AccountOrderDataEvent{
|
|
{
|
|
Event: "order_created",
|
|
EventID: sinceID,
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("1000"), IDStr: "1000",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709892",
|
|
Amount: json.Number("0.5"),
|
|
AmountStr: "0.5",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "0.5",
|
|
PriceStr: "60000.00",
|
|
},
|
|
},
|
|
{
|
|
Event: "order_created",
|
|
EventID: newEventID,
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("1001"), IDStr: "1001",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709900",
|
|
Amount: json.Number("0.5"),
|
|
AmountStr: "0.5",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "0.5",
|
|
PriceStr: "60000.00",
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
State: initialState,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
// The since_id event must be dropped; only the new event is returned.
|
|
Expect(resp.Orders).To(HaveLen(1))
|
|
Expect(resp.Orders[0].Reference).To(Equal("1001"))
|
|
|
|
var state ordersState
|
|
Expect(json.Unmarshal(resp.NewState, &state)).To(Succeed())
|
|
Expect(state.LastSeenEventIDPerMarket["BTC/USD"]).To(Equal(newEventID))
|
|
})
|
|
|
|
It("truncates orders within a market when the API returns more events than the page size", func(ctx SpecContext) {
|
|
const firstEventID = "a1b2c3d4-e5f6-a1b2-c3d4-e5f6a1b2c3d4"
|
|
const secondEventID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{
|
|
{
|
|
Event: "order_created",
|
|
EventID: firstEventID,
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("5000"), IDStr: "5000",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709892",
|
|
Amount: json.Number("0.5"),
|
|
AmountStr: "0.5",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "0.5",
|
|
PriceStr: "60000.00",
|
|
},
|
|
},
|
|
{
|
|
Event: "order_created",
|
|
EventID: secondEventID,
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("5001"), IDStr: "5001",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709893",
|
|
Amount: json.Number("0.5"),
|
|
AmountStr: "0.5",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "0.5",
|
|
PriceStr: "60000.00",
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
PageSize: 1,
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.HasMore).To(BeTrue())
|
|
Expect(resp.Orders).To(HaveLen(1))
|
|
Expect(resp.Orders[0].Reference).To(Equal("5000"))
|
|
|
|
var state ordersState
|
|
Expect(json.Unmarshal(resp.NewState, &state)).To(Succeed())
|
|
Expect(state.HasMoreCurrentMarket).To(Equal("BTC/USD"))
|
|
// Cursor must point to the last event included in this page, not the one we didn't return.
|
|
Expect(state.LastSeenEventIDPerMarket["BTC/USD"]).To(Equal(firstEventID))
|
|
})
|
|
|
|
It("returns HasMore=true and saves HasMoreCurrentMarket when page limit is reached", func(ctx SpecContext) {
|
|
const eventID = "a1b2c3d4-e5f6-a1b2-c3d4-e5f6a1b2c3d4"
|
|
// BTC/USD returns one order (fills the page), ETH/USD must not be called.
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{{
|
|
Event: "order_created",
|
|
EventID: eventID,
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("3000"), IDStr: "3000",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709892",
|
|
Amount: json.Number("0.5"),
|
|
AmountStr: "0.5",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "0.5",
|
|
PriceStr: "60000.00",
|
|
},
|
|
}}, nil)
|
|
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
PageSize: 1,
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD", "ETH/USD"}),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.HasMore).To(BeTrue())
|
|
Expect(resp.Orders).To(HaveLen(1))
|
|
|
|
var state ordersState
|
|
Expect(json.Unmarshal(resp.NewState, &state)).To(Succeed())
|
|
Expect(state.HasMoreCurrentMarket).To(Equal("BTC/USD"))
|
|
Expect(state.LastSeenEventIDPerMarket["BTC/USD"]).To(Equal(eventID))
|
|
})
|
|
|
|
It("resumes from HasMoreCurrentMarket and skips earlier markets", func(ctx SpecContext) {
|
|
const btcEventID = "a1b2c3d4-e5f6-a1b2-c3d4-e5f6a1b2c3d4"
|
|
const ethEventID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
|
|
|
|
// Simulate state saved after first page stopped at BTC/USD.
|
|
initialState, _ := json.Marshal(ordersState{
|
|
LastSeenEventIDPerMarket: map[string]string{"BTC/USD": btcEventID},
|
|
HasMoreCurrentMarket: "BTC/USD",
|
|
})
|
|
|
|
// BTC/USD is re-fetched from its since_id; ETH/USD is now called.
|
|
expectedSinceID := btcEventID
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", &expectedSinceID).
|
|
Return([]client.AccountOrderDataEvent{}, nil)
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "ETH/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{{
|
|
Event: "order_created",
|
|
EventID: ethEventID,
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("4000"), IDStr: "4000",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709892",
|
|
Amount: json.Number("1.0"),
|
|
AmountStr: "1.0",
|
|
AmountTraded: "0",
|
|
AmountAtCreate: "1.0",
|
|
PriceStr: "2000.00",
|
|
},
|
|
}}, nil)
|
|
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
PageSize: 10,
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD", "ETH/USD"}),
|
|
State: initialState,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.HasMore).To(BeFalse())
|
|
Expect(resp.Orders).To(HaveLen(1))
|
|
|
|
var state ordersState
|
|
Expect(json.Unmarshal(resp.NewState, &state)).To(Succeed())
|
|
Expect(state.HasMoreCurrentMarket).To(BeEmpty())
|
|
Expect(state.LastSeenEventIDPerMarket["ETH/USD"]).To(Equal(ethEventID))
|
|
})
|
|
|
|
It("clears HasMoreCurrentMarket when all markets fit within page size", func(ctx SpecContext) {
|
|
initialState, _ := json.Marshal(ordersState{
|
|
LastSeenEventIDPerMarket: map[string]string{},
|
|
HasMoreCurrentMarket: "BTC/USD",
|
|
})
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{}, nil)
|
|
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
PageSize: 10,
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
State: initialState,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.HasMore).To(BeFalse())
|
|
|
|
var state ordersState
|
|
Expect(json.Unmarshal(resp.NewState, &state)).To(Succeed())
|
|
Expect(state.HasMoreCurrentMarket).To(BeEmpty())
|
|
})
|
|
|
|
It("tracks the last EventID across multiple events in one batch", func(ctx SpecContext) {
|
|
const firstEventID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
|
const lastEventID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
|
|
m.EXPECT().GetAccountOrderData(gomock.Any(), "BTC/USD", gomock.Nil()).
|
|
Return([]client.AccountOrderDataEvent{
|
|
{
|
|
Event: "order_created",
|
|
EventID: firstEventID,
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("100"), IDStr: "100",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779709892", Amount: json.Number("0.5"), AmountStr: "0.5",
|
|
AmountTraded: "0", AmountAtCreate: "0.5", PriceStr: "60000.00",
|
|
},
|
|
},
|
|
{
|
|
Event: "order_deleted",
|
|
EventID: "cccccccc-cccc-cccc-cccc-cccccccccccc",
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("100"), IDStr: "100",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779717950", Amount: json.Number("0.5"), AmountStr: "0",
|
|
AmountTraded: "0.5", AmountAtCreate: "0.5", PriceStr: "60000.00",
|
|
},
|
|
},
|
|
{
|
|
Event: "order_created",
|
|
EventID: lastEventID,
|
|
Data: client.AccountOrderDataItem{
|
|
ID: json.Number("200"), IDStr: "200",
|
|
OrderType: 0, OrderSubtype: 0,
|
|
Datetime: "1779717960", Amount: json.Number("1.0"), AmountStr: "1.0",
|
|
AmountTraded: "0", AmountAtCreate: "1.0", PriceStr: "60000.00",
|
|
},
|
|
},
|
|
}, nil)
|
|
|
|
// All three are BUY orders (orderType=0), so reference must be "USD".
|
|
resp, err := plg.FetchNextOrders(ctx, models.FetchNextOrdersRequest{
|
|
FromPayload: fromPayloadWithMarkets("USD", []string{"BTC/USD"}),
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.Orders).To(HaveLen(3))
|
|
|
|
var state ordersState
|
|
Expect(json.Unmarshal(resp.NewState, &state)).To(Succeed())
|
|
// Only the last EventID in the batch is kept as the cursor.
|
|
Expect(state.LastSeenEventIDPerMarket["BTC/USD"]).To(Equal(lastEventID))
|
|
})
|
|
})
|
|
})
|