From 1a8d0d314c7a83fd36d3a435a38498276c192976 Mon Sep 17 00:00:00 2001 From: Scott Fleckenstein Date: Thu, 9 Mar 2017 10:05:42 -0600 Subject: [PATCH 1/4] Adds `created_at` to trade resource: - db2/history: adds LedgersBySequence to load batch of ledgers based upon the loaded page of trades - resource: Trade#Populate takes a history.Ledger to populate additional data --- .../stellar/horizon/actions_trade.go | 46 ++++++++++++++++++- .../stellar/horizon/actions_trade_test.go | 14 ++++++ .../stellar/horizon/db2/history/effect.go | 6 +++ .../stellar/horizon/db2/history/ledger.go | 15 ++++++ .../horizon/db2/history/ledger_test.go | 7 +++ .../stellar/horizon/resource/main.go | 25 +++++----- .../stellar/horizon/resource/trade.go | 7 ++- 7 files changed, 106 insertions(+), 14 deletions(-) diff --git a/src/github.com/stellar/horizon/actions_trade.go b/src/github.com/stellar/horizon/actions_trade.go index 0952151b..0a4896c7 100644 --- a/src/github.com/stellar/horizon/actions_trade.go +++ b/src/github.com/stellar/horizon/actions_trade.go @@ -1,6 +1,10 @@ package horizon import ( + "errors" + + "fmt" + "github.com/stellar/go/xdr" "github.com/stellar/horizon/db2" "github.com/stellar/horizon/db2/history" @@ -18,6 +22,9 @@ type TradeIndexAction struct { Buying xdr.Asset PagingParams db2.PageQuery Records []history.Effect + // LedgerRecords is a cache of loaded ledger data used to populate the time + // when a trade occurred. + LedgerRecords map[int32]history.Ledger Page hal.Page } @@ -27,6 +34,7 @@ func (action *TradeIndexAction) JSON() { action.EnsureHistoryFreshness, action.loadParams, action.loadRecords, + action.loadLedgers, action.loadPage, func() { hal.Render(action.W, action.Page) @@ -63,14 +71,50 @@ func (action *TradeIndexAction) loadRecords() { action.Err = trades.Page(action.PagingParams).Select(&action.Records) } +// loadLedgers collects the unique ledgers referenced in the loaded trades and loads the details for each. +func (action *TradeIndexAction) loadLedgers() { + if len(action.Records) == 0 { + return + } + + ledgerSequences := make([]interface{}, len(action.Records)) + + // populate the unique sequences + for i, trade := range action.Records { + ledgerSequences[i] = trade.LedgerSequence() + } + + var ledgers []history.Ledger + action.Err = action.HistoryQ().LedgersBySequence( + &ledgers, + ledgerSequences..., + ) + if action.Err != nil { + return + } + + action.LedgerRecords = map[int32]history.Ledger{} + for _, l := range ledgers { + action.LedgerRecords[l.Sequence] = l + } +} + // loadPage populates action.Page func (action *TradeIndexAction) loadPage() { for _, record := range action.Records { var res resource.Trade - action.Err = res.Populate(action.Ctx, record) + + ledger, found := action.LedgerRecords[record.LedgerSequence()] + if !found { + msg := fmt.Sprintf("could not find ledger data for sequence %d", record.LedgerSequence()) + action.Err = errors.New(msg) + } + + action.Err = res.Populate(action.Ctx, record, ledger) if action.Err != nil { return } + action.Page.Add(res) } diff --git a/src/github.com/stellar/horizon/actions_trade_test.go b/src/github.com/stellar/horizon/actions_trade_test.go index 51b3a401..c11e66b8 100644 --- a/src/github.com/stellar/horizon/actions_trade_test.go +++ b/src/github.com/stellar/horizon/actions_trade_test.go @@ -3,6 +3,10 @@ package horizon import ( "net/url" "testing" + "time" + + "github.com/stellar/horizon/db2/history" + "github.com/stellar/horizon/resource" ) func TestTradeActions_Index(t *testing.T) { @@ -15,6 +19,16 @@ func TestTradeActions_Index(t *testing.T) { ht.Assert.PageOf(1, w.Body) } + // ensure created_at is populated correctly + records := []resource.Trade{} + ht.UnmarshalPage(w.Body, &records) + + l := history.Ledger{} + hq := history.Q{Repo: ht.HorizonRepo()} + ht.Require.NoError(hq.LedgerBySequence(&l, 6)) + + ht.Assert.WithinDuration(l.ClosedAt, records[0].LedgerCloseTime, 1*time.Second) + // for order book var q = make(url.Values) q.Add("selling_asset_type", "credit_alphanum4") diff --git a/src/github.com/stellar/horizon/db2/history/effect.go b/src/github.com/stellar/horizon/db2/history/effect.go index 7a7a5730..022f1913 100644 --- a/src/github.com/stellar/horizon/db2/history/effect.go +++ b/src/github.com/stellar/horizon/db2/history/effect.go @@ -31,6 +31,12 @@ func (r *Effect) ID() string { return fmt.Sprintf("%019d-%010d", r.HistoryOperationID, r.Order) } +// LedgerSequence return the ledger in which the effect occurred. +func (r *Effect) LedgerSequence() int32 { + id := toid.Parse(r.HistoryOperationID) + return id.LedgerSequence +} + // PagingToken returns a cursor for this effect func (r *Effect) PagingToken() string { return fmt.Sprintf("%d-%d", r.HistoryOperationID, r.Order) diff --git a/src/github.com/stellar/horizon/db2/history/ledger.go b/src/github.com/stellar/horizon/db2/history/ledger.go index 2695750d..e8a3270e 100644 --- a/src/github.com/stellar/horizon/db2/history/ledger.go +++ b/src/github.com/stellar/horizon/db2/history/ledger.go @@ -1,7 +1,10 @@ package history import ( + "fmt" + sq "github.com/lann/squirrel" + "github.com/stellar/go/support/errors" "github.com/stellar/horizon/db2" ) @@ -23,6 +26,18 @@ func (q *Q) Ledgers() *LedgersQ { } } +// LedgersBySequence loads the a set of ledgers identified by the sequences +// `seqs` into `dest`. +func (q *Q) LedgersBySequence(dest interface{}, seqs ...interface{}) error { + if len(seqs) == 0 { + return errors.New("no sequence arguments provided") + } + in := fmt.Sprintf("sequence IN (%s)", sq.Placeholders(len(seqs))) + sql := selectLedger.Where(in, seqs...) + + return q.Select(dest, sql) +} + // Page specifies the paging constraints for the query being built by `q`. func (q *LedgersQ) Page(page db2.PageQuery) *LedgersQ { if q.Err != nil { diff --git a/src/github.com/stellar/horizon/db2/history/ledger_test.go b/src/github.com/stellar/horizon/db2/history/ledger_test.go index bf98d6df..c0aeeed0 100644 --- a/src/github.com/stellar/horizon/db2/history/ledger_test.go +++ b/src/github.com/stellar/horizon/db2/history/ledger_test.go @@ -27,4 +27,11 @@ func TestLedgerQueries(t *testing.T) { if tt.Assert.NoError(err) { tt.Assert.Len(ls, 3) } + + // LedgersBySequence + err = q.LedgersBySequence(&ls, 1, 2, 3) + + if tt.Assert.NoError(err) { + tt.Assert.Len(ls, 3) + } } diff --git a/src/github.com/stellar/horizon/resource/main.go b/src/github.com/stellar/horizon/resource/main.go index 6a557d13..cc4208df 100644 --- a/src/github.com/stellar/horizon/resource/main.go +++ b/src/github.com/stellar/horizon/resource/main.go @@ -193,18 +193,19 @@ type Trade struct { Buyer hal.Link `json:"buyer"` } `json:"_links"` - ID string `json:"id"` - PT string `json:"paging_token"` - Seller string `json:"seller"` - SoldAmount string `json:"sold_amount"` - SoldAssetType string `json:"sold_asset_type"` - SoldAssetCode string `json:"sold_asset_code,omitempty"` - SoldAssetIssuer string `json:"sold_asset_issuer,omitempty"` - Buyer string `json:"buyer"` - BoughtAmount string `json:"bought_amount"` - BoughtAssetType string `json:"bought_asset_type"` - BoughtAssetCode string `json:"bought_asset_code,omitempty"` - BoughtAssetIssuer string `json:"bought_asset_issuer,omitempty"` + ID string `json:"id"` + PT string `json:"paging_token"` + Seller string `json:"seller"` + SoldAmount string `json:"sold_amount"` + SoldAssetType string `json:"sold_asset_type"` + SoldAssetCode string `json:"sold_asset_code,omitempty"` + SoldAssetIssuer string `json:"sold_asset_issuer,omitempty"` + Buyer string `json:"buyer"` + BoughtAmount string `json:"bought_amount"` + BoughtAssetType string `json:"bought_asset_type"` + BoughtAssetCode string `json:"bought_asset_code,omitempty"` + BoughtAssetIssuer string `json:"bought_asset_issuer,omitempty"` + LedgerCloseTime time.Time `json:"created_at"` } // Transaction represents a single, successful transaction diff --git a/src/github.com/stellar/horizon/resource/trade.go b/src/github.com/stellar/horizon/resource/trade.go index 0c4993f3..f271a2d5 100644 --- a/src/github.com/stellar/horizon/resource/trade.go +++ b/src/github.com/stellar/horizon/resource/trade.go @@ -10,7 +10,11 @@ import ( ) // Populate fills out the details -func (res *Trade) Populate(ctx context.Context, row history.Effect) (err error) { +func (res *Trade) Populate( + ctx context.Context, + row history.Effect, + ledger history.Ledger, +) (err error) { if row.Type != history.EffectTrade { err = errors.New("invalid effect; not a trade") return @@ -19,6 +23,7 @@ func (res *Trade) Populate(ctx context.Context, row history.Effect) (err error) res.ID = row.PagingToken() res.PT = row.PagingToken() res.Buyer = row.Account + res.LedgerCloseTime = ledger.ClosedAt lb := hal.LinkBuilder{httpx.BaseURL(ctx)} res.Links.Self = lb.Link("/accounts", res.Seller) From 562211ea23d76542323f6d82f5f8fa0f5cb842e2 Mon Sep 17 00:00:00 2001 From: Scott Fleckenstein Date: Thu, 9 Mar 2017 14:18:35 -0600 Subject: [PATCH 2/4] db2/history: testing LedgersBySequence This commit adds a test that ensures LedgersBySequence returns the correct ledgers --- .../stellar/horizon/db2/history/ledger_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/github.com/stellar/horizon/db2/history/ledger_test.go b/src/github.com/stellar/horizon/db2/history/ledger_test.go index c0aeeed0..091ffc86 100644 --- a/src/github.com/stellar/horizon/db2/history/ledger_test.go +++ b/src/github.com/stellar/horizon/db2/history/ledger_test.go @@ -33,5 +33,14 @@ func TestLedgerQueries(t *testing.T) { if tt.Assert.NoError(err) { tt.Assert.Len(ls, 3) + + foundSeqs := make([]int32, len(ls)) + for i := range ls { + foundSeqs[i] = ls[i].Sequence + } + + tt.Assert.Contains(foundSeqs, int32(1)) + tt.Assert.Contains(foundSeqs, int32(2)) + tt.Assert.Contains(foundSeqs, int32(3)) } } From 3fae445e714eb60ce6d24ef460ab6778f5b607c6 Mon Sep 17 00:00:00 2001 From: Scott Fleckenstein Date: Thu, 9 Mar 2017 14:20:53 -0600 Subject: [PATCH 3/4] resource: prevent mismatched trade population --- src/github.com/stellar/horizon/resource/trade.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/github.com/stellar/horizon/resource/trade.go b/src/github.com/stellar/horizon/resource/trade.go index f271a2d5..73b826aa 100644 --- a/src/github.com/stellar/horizon/resource/trade.go +++ b/src/github.com/stellar/horizon/resource/trade.go @@ -19,6 +19,12 @@ func (res *Trade) Populate( err = errors.New("invalid effect; not a trade") return } + + if row.LedgerSequence() != ledger.Sequence { + err = errors.New("invalid ledger; different sequence than trade") + return + } + row.UnmarshalDetails(res) res.ID = row.PagingToken() res.PT = row.PagingToken() From a4574bb5b403163865352f3d0119a912e6de122c Mon Sep 17 00:00:00 2001 From: Scott Fleckenstein Date: Thu, 9 Mar 2017 14:21:47 -0600 Subject: [PATCH 4/4] Add missing error return --- src/github.com/stellar/horizon/actions_trade.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/github.com/stellar/horizon/actions_trade.go b/src/github.com/stellar/horizon/actions_trade.go index 0a4896c7..45b00daa 100644 --- a/src/github.com/stellar/horizon/actions_trade.go +++ b/src/github.com/stellar/horizon/actions_trade.go @@ -108,6 +108,7 @@ func (action *TradeIndexAction) loadPage() { if !found { msg := fmt.Sprintf("could not find ledger data for sequence %d", record.LedgerSequence()) action.Err = errors.New(msg) + return } action.Err = res.Populate(action.Ctx, record, ledger)