diff --git a/exp/ingest/io/ledger_reader.go b/exp/ingest/io/ledger_reader.go index 9208d2ec6c..689c875d6b 100644 --- a/exp/ingest/io/ledger_reader.go +++ b/exp/ingest/io/ledger_reader.go @@ -10,14 +10,18 @@ import ( ) // DBLedgerReader is a database-backed implementation of the io.LedgerReader interface. +// Use NewDBLedgerReader to create a new instance. type DBLedgerReader struct { - sequence uint32 - backend ledgerbackend.LedgerBackend - header xdr.LedgerHeaderHistoryEntry - transactions []LedgerTransaction - readIdx int - initOnce sync.Once - readMutex sync.Mutex + sequence uint32 + backend ledgerbackend.LedgerBackend + header xdr.LedgerHeaderHistoryEntry + transactions []LedgerTransaction + upgradeChanges []Change + readMutex sync.Mutex + readIdx int + upgradeReadIdx int + readUpgradeChangeCalled bool + ignoreUpgradeChanges bool } // Ensure DBLedgerReader implements LedgerReader @@ -30,8 +34,7 @@ func NewDBLedgerReader(sequence uint32, backend ledgerbackend.LedgerBackend) (*D backend: backend, } - var err error - reader.initOnce.Do(func() { err = reader.init() }) + err := reader.init() if err != nil { return nil, err } @@ -46,25 +49,12 @@ func (dblrc *DBLedgerReader) GetSequence() uint32 { // GetHeader returns the XDR Header data associated with the stored ledger. func (dblrc *DBLedgerReader) GetHeader() xdr.LedgerHeaderHistoryEntry { - var err error - dblrc.initOnce.Do(func() { err = dblrc.init() }) - if err != nil { - // TODO, object should be initialized in constructor. - // Not returning error here, makes this much simpler. - panic(err) - } return dblrc.header } // Read returns the next transaction in the ledger, ordered by tx number, each time it is called. When there // are no more transactions to return, an EOF error is returned. func (dblrc *DBLedgerReader) Read() (LedgerTransaction, error) { - var err error - dblrc.initOnce.Do(func() { err = dblrc.init() }) - if err != nil { - return LedgerTransaction{}, err - } - // Protect all accesses to dblrc.readIdx dblrc.readMutex.Lock() defer dblrc.readMutex.Unlock() @@ -76,10 +66,38 @@ func (dblrc *DBLedgerReader) Read() (LedgerTransaction, error) { return LedgerTransaction{}, io.EOF } +// ReadUpgradeChange returns the next upgrade change in the ledger, each time it +// is called. When there are no more upgrades to return, an EOF error is returned. +func (dblrc *DBLedgerReader) ReadUpgradeChange() (Change, error) { + // Protect all accesses to dblrc.upgradeReadIdx + dblrc.readMutex.Lock() + defer dblrc.readMutex.Unlock() + dblrc.readUpgradeChangeCalled = true + + if dblrc.upgradeReadIdx < len(dblrc.upgradeChanges) { + dblrc.upgradeReadIdx++ + return dblrc.upgradeChanges[dblrc.upgradeReadIdx-1], nil + } + return Change{}, io.EOF +} + +// GetUpgradeChanges returns all ledger upgrade changes. +func (dblrc *DBLedgerReader) GetUpgradeChanges() []Change { + return dblrc.upgradeChanges +} + +func (dblrc *DBLedgerReader) IgnoreUpgradeChanges() { + dblrc.ignoreUpgradeChanges = true +} + // Close moves the read pointer so that subsequent calls to Read() will return EOF. func (dblrc *DBLedgerReader) Close() error { dblrc.readMutex.Lock() dblrc.readIdx = len(dblrc.transactions) + if !dblrc.ignoreUpgradeChanges && + (!dblrc.readUpgradeChangeCalled || dblrc.upgradeReadIdx != len(dblrc.upgradeChanges)) { + return errors.New("Ledger upgrade changes not read! Use ReadUpgradeChange() method.") + } dblrc.readMutex.Unlock() return nil @@ -100,6 +118,11 @@ func (dblrc *DBLedgerReader) init() error { dblrc.storeTransactions(ledgerCloseMeta) + for _, upgradeChanges := range ledgerCloseMeta.UpgradesMeta { + changes := getChangesFromLedgerEntryChanges(upgradeChanges) + dblrc.upgradeChanges = append(dblrc.upgradeChanges, changes...) + } + return nil } diff --git a/exp/ingest/io/ledger_transaction.go b/exp/ingest/io/ledger_transaction.go index 1cbae2a89b..3af2f1346c 100644 --- a/exp/ingest/io/ledger_transaction.go +++ b/exp/ingest/io/ledger_transaction.go @@ -1,6 +1,9 @@ package io import ( + "bytes" + + "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" ) @@ -13,8 +16,77 @@ import ( // If an entry is removed: Pre is not nil and Post is nil. type Change struct { Type xdr.LedgerEntryType - Pre *xdr.LedgerEntryData - Post *xdr.LedgerEntryData + Pre *xdr.LedgerEntry + Post *xdr.LedgerEntry +} + +// AccountChangedExceptSigners returns true if account has changed WITHOUT +// checking the signers (except master key weight!). In other words, if the only +// change is connected to signers, this function will return false. +func (c *Change) AccountChangedExceptSigners() (bool, error) { + if c.Type != xdr.LedgerEntryTypeAccount { + panic("This should not be called on changes other than Account changes") + } + + // New account + if c.Pre == nil { + return true, nil + } + + // Account merged + // c.Pre != nil at this point. + if c.Post == nil { + return true, nil + } + + // c.Pre != nil && c.Post != nil at this point. + if c.Pre.LastModifiedLedgerSeq != c.Post.LastModifiedLedgerSeq { + return true, nil + } + + // Don't use short assignment statement (:=) to ensure variables below + // are not pointers (if `xdr` package changes in the future)! + var preAccountEntry, postAccountEntry xdr.AccountEntry + preAccountEntry = c.Pre.Data.MustAccount() + postAccountEntry = c.Post.Data.MustAccount() + + // preAccountEntry and postAccountEntry are copies so it's fine to + // modify them here, EXCEPT pointers inside them! + if preAccountEntry.Ext.V == 0 { + preAccountEntry.Ext.V = 1 + preAccountEntry.Ext.V1 = &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 0, + Selling: 0, + }, + } + } + + preAccountEntry.Signers = nil + + if postAccountEntry.Ext.V == 0 { + postAccountEntry.Ext.V = 1 + postAccountEntry.Ext.V1 = &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 0, + Selling: 0, + }, + } + } + + postAccountEntry.Signers = nil + + preBinary, err := preAccountEntry.MarshalBinary() + if err != nil { + return false, errors.Wrap(err, "Error running preAccountEntry.MarshalBinary") + } + + postBinary, err := postAccountEntry.MarshalBinary() + if err != nil { + return false, errors.Wrap(err, "Error running postAccountEntry.MarshalBinary") + } + + return !bytes.Equal(preBinary, postBinary), nil } // AccountSignersChanged returns true if account signers have changed. @@ -36,8 +108,8 @@ func (c *Change) AccountSignersChanged() bool { } // c.Pre != nil && c.Post != nil at this point. - preAccountEntry := c.Pre.MustAccount() - postAccountEntry := c.Post.MustAccount() + preAccountEntry := c.Pre.Data.MustAccount() + postAccountEntry := c.Post.Data.MustAccount() preSigners := preAccountEntry.SignerSummary() postSigners := postAccountEntry.SignerSummary() @@ -68,18 +140,27 @@ func (t *LedgerTransaction) GetChanges() []Change { changes := getChangesFromLedgerEntryChanges(t.FeeChanges) // Transaction meta - v1Meta, ok := t.Meta.GetV1() - if ok { + switch t.Meta.V { + case 0: + for _, operationMeta := range *t.Meta.Operations { + opChanges := getChangesFromLedgerEntryChanges( + operationMeta.Changes, + ) + changes = append(changes, opChanges...) + } + case 1: + v1Meta := t.Meta.MustV1() txChanges := getChangesFromLedgerEntryChanges(v1Meta.TxChanges) changes = append(changes, txChanges...) - } - // Operation meta - for _, operationMeta := range t.Meta.OperationsMeta() { - ledgerEntryChanges := operationMeta.Changes - opChanges := getChangesFromLedgerEntryChanges(ledgerEntryChanges) - - changes = append(changes, opChanges...) + for _, operationMeta := range v1Meta.Operations { + opChanges := getChangesFromLedgerEntryChanges( + operationMeta.Changes, + ) + changes = append(changes, opChanges...) + } + default: + panic("Unkown TransactionMeta version") } return changes @@ -106,21 +187,21 @@ func getChangesFromLedgerEntryChanges(ledgerEntryChanges xdr.LedgerEntryChanges) changes = append(changes, Change{ Type: created.Data.Type, Pre: nil, - Post: &created.Data, + Post: &created, }) case xdr.LedgerEntryChangeTypeLedgerEntryUpdated: state := ledgerEntryChanges[i-1].MustState() updated := entryChange.MustUpdated() changes = append(changes, Change{ Type: state.Data.Type, - Pre: &state.Data, - Post: &updated.Data, + Pre: &state, + Post: &updated, }) case xdr.LedgerEntryChangeTypeLedgerEntryRemoved: state := ledgerEntryChanges[i-1].MustState() changes = append(changes, Change{ Type: state.Data.Type, - Pre: &state.Data, + Pre: &state, Post: nil, }) case xdr.LedgerEntryChangeTypeLedgerEntryState: diff --git a/exp/ingest/io/ledger_transaction_test.go b/exp/ingest/io/ledger_transaction_test.go index 520e23b1c6..0c2a312c49 100644 --- a/exp/ingest/io/ledger_transaction_test.go +++ b/exp/ingest/io/ledger_transaction_test.go @@ -7,6 +7,233 @@ import ( "github.com/stretchr/testify/assert" ) +func TestChangeAccountChangedExceptSignersInvalidType(t *testing.T) { + change := Change{ + Type: xdr.LedgerEntryTypeOffer, + } + + assert.Panics(t, func() { + change.AccountChangedExceptSigners() + }) +} + +func TestChangeAccountChangedExceptSignersLastModifiedLedgerSeq(t *testing.T) { + change := Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + }, + } + changed, err := change.AccountChangedExceptSigners() + assert.NoError(t, err) + assert.True(t, changed) +} + +func TestChangeAccountChangedExceptSignersNoPre(t *testing.T) { + change := Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + }, + } + changed, err := change.AccountChangedExceptSigners() + assert.NoError(t, err) + assert.True(t, changed) +} + +func TestChangeAccountChangedExceptSignersNoPost(t *testing.T) { + change := Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + }, + Post: nil, + } + changed, err := change.AccountChangedExceptSigners() + assert.NoError(t, err) + assert.True(t, changed) +} + +func TestChangeAccountChangedExceptSignersMasterKeyRemoved(t *testing.T) { + change := Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 1 + Thresholds: [4]byte{1, 1, 1, 1}, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 0 + Thresholds: [4]byte{0, 1, 1, 1}, + }, + }, + }, + } + + changed, err := change.AccountChangedExceptSigners() + assert.NoError(t, err) + assert.True(t, changed) +} + +func TestChangeAccountChangedExceptSignersSignerChange(t *testing.T) { + change := Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 2, + }, + }, + }, + }, + }, + } + + changed, err := change.AccountChangedExceptSigners() + assert.NoError(t, err) + assert.False(t, changed) +} + +func TestChangeAccountChangedExceptSignersNoChanges(t *testing.T) { + inflationDest := xdr.MustAddress("GBAH2GBLJB54JAROJ3FVO4ZTTJJI3XKOBTMJOZFUJ3UHYIVNJTLJUYFY") + change := Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Balance: 1000, + SeqNum: 432894732, + NumSubEntries: 2, + InflationDest: &inflationDest, + Flags: 4, + HomeDomain: "stellar.org", + Thresholds: [4]byte{1, 1, 1, 1}, + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 10, + Selling: 20, + }, + }, + }, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Balance: 1000, + SeqNum: 432894732, + NumSubEntries: 2, + InflationDest: &inflationDest, + Flags: 4, + HomeDomain: "stellar.org", + Thresholds: [4]byte{1, 1, 1, 1}, + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 10, + Selling: 20, + }, + }, + }, + }, + }, + }, + } + + changed, err := change.AccountChangedExceptSigners() + assert.NoError(t, err) + assert.False(t, changed) + + // Make sure pre and post not modified + assert.NotNil(t, change.Pre.Data.Account.Signers) + assert.Len(t, change.Pre.Data.Account.Signers, 1) + + assert.NotNil(t, change.Post.Data.Account.Signers) + assert.Len(t, change.Post.Data.Account.Signers, 1) +} + func TestChangeAccountSignersChangedInvalidType(t *testing.T) { change := Change{ Type: xdr.LedgerEntryTypeOffer, @@ -21,10 +248,13 @@ func TestChangeAccountSignersChangedNoPre(t *testing.T) { change := Change{ Type: xdr.LedgerEntryTypeAccount, Pre: nil, - Post: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, }, }, } @@ -35,12 +265,15 @@ func TestChangeAccountSignersChangedNoPre(t *testing.T) { func TestChangeAccountSignersChangedNoPostMasterKey(t *testing.T) { change := Change{ Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 1 - Thresholds: [4]byte{1, 1, 1, 1}, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 1 + Thresholds: [4]byte{1, 1, 1, 1}, + }, }, }, Post: nil, @@ -52,12 +285,15 @@ func TestChangeAccountSignersChangedNoPostMasterKey(t *testing.T) { func TestChangeAccountSignersChangedNoPostNoMasterKey(t *testing.T) { change := Change{ Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 1 - Thresholds: [4]byte{0, 1, 1, 1}, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 0 + Thresholds: [4]byte{0, 1, 1, 1}, + }, }, }, Post: nil, @@ -70,20 +306,26 @@ func TestChangeAccountSignersChangedNoPostNoMasterKey(t *testing.T) { func TestChangeAccountSignersChangedMasterKeyRemoved(t *testing.T) { change := Change{ Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 1 - Thresholds: [4]byte{1, 1, 1, 1}, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 1 + Thresholds: [4]byte{1, 1, 1, 1}, + }, }, }, - Post: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 1 - Thresholds: [4]byte{0, 1, 1, 1}, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 0 + Thresholds: [4]byte{0, 1, 1, 1}, + }, }, }, } @@ -94,20 +336,26 @@ func TestChangeAccountSignersChangedMasterKeyRemoved(t *testing.T) { func TestChangeAccountSignersChangedMasterKeyAdded(t *testing.T) { change := Change{ Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 1 - Thresholds: [4]byte{0, 1, 1, 1}, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 0 + Thresholds: [4]byte{0, 1, 1, 1}, + }, }, }, - Post: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 1 - Thresholds: [4]byte{1, 1, 1, 1}, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 1 + Thresholds: [4]byte{1, 1, 1, 1}, + }, }, }, } @@ -118,21 +366,27 @@ func TestChangeAccountSignersChangedMasterKeyAdded(t *testing.T) { func TestChangeAccountSignersChangedSignerAdded(t *testing.T) { change := Change{ Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{}, - }, - }, - Post: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - xdr.Signer{ - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{}, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, }, }, }, @@ -145,23 +399,29 @@ func TestChangeAccountSignersChangedSignerAdded(t *testing.T) { func TestChangeAccountSignersChangedSignerRemoved(t *testing.T) { change := Change{ Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - xdr.Signer{ - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, }, }, }, }, - Post: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{}, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{}, + }, }, }, } @@ -172,26 +432,32 @@ func TestChangeAccountSignersChangedSignerRemoved(t *testing.T) { func TestChangeAccountSignersChangedSignerWeightChanged(t *testing.T) { change := Change{ Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - xdr.Signer{ - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, }, }, }, }, - Post: &xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - xdr.Signer{ - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 2, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 2, + }, }, }, }, diff --git a/exp/ingest/io/main.go b/exp/ingest/io/main.go index 7d5d5d687b..a649cd24cc 100644 --- a/exp/ingest/io/main.go +++ b/exp/ingest/io/main.go @@ -42,12 +42,29 @@ type LedgerReader interface { // Read should return the next transaction. If there are no more // transactions it should return `io.EOF` error. Read() (LedgerTransaction, error) + // Read should return the next ledger entry change from ledger upgrades. If + // there are no more changes it should return `io.EOF` error. + // Ledger upgrades MUST be processed AFTER all transactions and only ONCE. + // If app is tracking state in more than one store, all of them need to + // be updated with upgrade changes. + // Values returned by this method must not be modified. + ReadUpgradeChange() (Change, error) + // IgnoreLedgerEntryChanges will change `Close()`` behaviour to not error + // when changes returned by `ReadUpgradeChange` are not fully read. + IgnoreUpgradeChanges() // Close should be called when reading is finished. This is especially // helpful when there are still some transactions available so reader can stop // streaming them. + // Close should return error if `ReadUpgradeChange` are not fully read or + // `ReadUpgradeChange` was not called even once. However, this behaviour can + // be disabled by calling `IgnoreUpgradeChanges()`. Close() error } +type UpgradeChangesContainer interface { + GetUpgradeChanges() []Change +} + // LedgerWriter provides convenient, streaming access to the transactions within a ledger. type LedgerWriter interface { // Write is used to pass a transaction to the next processor. It can return @@ -64,9 +81,12 @@ type LedgerWriter interface { // LedgerTransaction represents the data for a single transaction within a ledger. type LedgerTransaction struct { - Index uint32 - Envelope xdr.TransactionEnvelope - Result xdr.TransactionResultPair - Meta xdr.TransactionMeta + Index uint32 + Envelope xdr.TransactionEnvelope + Result xdr.TransactionResultPair + // FeeChanges and Meta are low level values. + // Use LedgerTransaction.GetChanges() for higher level access to ledger + // entry changes. FeeChanges xdr.LedgerEntryChanges + Meta xdr.TransactionMeta } diff --git a/exp/ingest/io/mock_ledger_reader.go b/exp/ingest/io/mock_ledger_reader.go index b125008ae5..aa05bde538 100644 --- a/exp/ingest/io/mock_ledger_reader.go +++ b/exp/ingest/io/mock_ledger_reader.go @@ -26,6 +26,20 @@ func (m *MockLedgerReader) Read() (LedgerTransaction, error) { return args.Get(0).(LedgerTransaction), args.Error(1) } +func (m *MockLedgerReader) ReadUpgradeChange() (Change, error) { + args := m.Called() + return args.Get(0).(Change), args.Error(1) +} + +func (m *MockLedgerReader) GetUpgradeChanges() []Change { + args := m.Called() + return args.Get(0).([]Change) +} + +func (m *MockLedgerReader) IgnoreUpgradeChanges() { + m.Called() +} + func (m *MockLedgerReader) Close() error { args := m.Called() return args.Error(0) diff --git a/exp/ingest/ledgerbackend/database_backend.go b/exp/ingest/ledgerbackend/database_backend.go index b7f7b63119..2a0d1d7222 100644 --- a/exp/ingest/ledgerbackend/database_backend.go +++ b/exp/ingest/ledgerbackend/database_backend.go @@ -13,6 +13,7 @@ const ( txHistoryQuery = "select txbody, txresult, txmeta, txindex from txhistory where ledgerseq = ? " ledgerHeaderQuery = "select ledgerhash, data from ledgerheaders where ledgerseq = ? " txFeeHistoryQuery = "select txchanges, txindex from txfeehistory where ledgerseq = ? " + upgradeHistoryQuery = "select ledgerseq, upgradeindex, upgrade, changes from upgradehistory where ledgerseq = ? order by upgradeindex asc" orderBy = "order by txindex asc" dbDriver = "postgres" ) @@ -113,6 +114,21 @@ func (dbb *DatabaseBackend) GetLedger(sequence uint32) (bool, LedgerCloseMeta, e lcm.TransactionFeeChanges = append(lcm.TransactionFeeChanges, tx.TXChanges) } + // Query - upgradehistory + var upgradeHistoryRows []upgradeHistory + err = dbb.session.SelectRaw(&upgradeHistoryRows, upgradeHistoryQuery, sequence) + // Return errors... + if err != nil { + return false, lcm, errors.Wrap(err, "Error getting upgradeHistoryRows") + } + + // ...otherwise store the data + var upgradesMeta []xdr.LedgerEntryChanges + for _, upgradeHistoryRow := range upgradeHistoryRows { + upgradesMeta = append(upgradesMeta, upgradeHistoryRow.Changes) + } + lcm.UpgradesMeta = upgradesMeta + return true, lcm, nil } diff --git a/exp/ingest/ledgerbackend/ledger_backend.go b/exp/ingest/ledgerbackend/ledger_backend.go index 76704187e1..22faf4be61 100644 --- a/exp/ingest/ledgerbackend/ledger_backend.go +++ b/exp/ingest/ledgerbackend/ledger_backend.go @@ -25,6 +25,7 @@ type LedgerCloseMeta struct { TransactionResult []xdr.TransactionResultPair TransactionMeta []xdr.TransactionMeta TransactionFeeChanges []xdr.LedgerEntryChanges + UpgradesMeta []xdr.LedgerEntryChanges } // ledgerHeaderHistory is a helper struct used to unmarshall header fields from a stellar-core DB. @@ -69,9 +70,9 @@ type txFeeHistory struct { // } // upgradeHistory holds a row of data from the stellar-core `upgradehistory` table. -// type upgradeHistory struct { -// LedgerSeq uint32 `db:"ledgerseq"` -// UpgradeIndex uint32 `db:"upgradeindex"` -// Upgrade string `db:"upgrade"` -// Changes string `db:"changes"` -// } +type upgradeHistory struct { + LedgerSeq uint32 `db:"ledgerseq"` + UpgradeIndex uint32 `db:"upgradeindex"` + Upgrade xdr.LedgerUpgrade `db:"upgrade"` + Changes xdr.LedgerEntryChanges `db:"changes"` +} diff --git a/exp/ingest/main.go b/exp/ingest/main.go index da0fd4397f..2d829d8e69 100644 --- a/exp/ingest/main.go +++ b/exp/ingest/main.go @@ -1,6 +1,7 @@ package ingest import ( + "fmt" "sync" "github.com/stellar/go/clients/stellarcore" @@ -159,3 +160,13 @@ func (r reporterLedgerReader) Read() (io.LedgerTransaction, error) { return entry, err } + +func (r reporterLedgerReader) GetUpgradeChanges() []io.Change { + // upgradeChangesContainer is implemented by *io.DBLedgerReader and readerWrapperLedger. + reader, ok := r.LedgerReader.(io.UpgradeChangesContainer) + if !ok { + panic(fmt.Sprintf("Cannot get upgrade changes from unknown reader type: %T", r.LedgerReader)) + } + + return reader.GetUpgradeChanges() +} diff --git a/exp/ingest/pipeline/ledger_pipeline_test.go b/exp/ingest/pipeline/ledger_pipeline_test.go new file mode 100644 index 0000000000..e8df0eab2c --- /dev/null +++ b/exp/ingest/pipeline/ledger_pipeline_test.go @@ -0,0 +1,100 @@ +package pipeline_test + +import ( + "context" + stdio "io" + "testing" + + "github.com/stellar/go/exp/ingest/io" + "github.com/stellar/go/exp/ingest/pipeline" + "github.com/stellar/go/exp/ingest/processors" + supportPipeline "github.com/stellar/go/exp/support/pipeline" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestUpgradeChangesPassed(t *testing.T) { + mockLedgerReader := &io.MockLedgerReader{} + + account := xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Thresholds: [4]byte{1, 1, 1, 1}, + } + + mockLedgerReader.On("GetSequence").Return(uint32(1000)) + mockLedgerReader.On("GetHeader").Return(xdr.LedgerHeaderHistoryEntry{}) + mockLedgerReader.On("Read").Return(io.LedgerTransaction{}, stdio.EOF) + mockLedgerReader.On("GetUpgradeChanges").Return([]io.Change{ + io.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &account, + }, + LastModifiedLedgerSeq: 1000, + }, + }, + }) + mockLedgerReader.On("IgnoreUpgradeChanges").Once() + mockLedgerReader.On("Close").Return(nil).Once() + + // Ensure upgrade changes are available to processors to read + ledgerPipeline := &pipeline.LedgerPipeline{} + ledgerPipeline.SetRoot( + pipeline.LedgerNode(&processors.RootProcessor{}). + Pipe( + pipeline.LedgerNode(&testLedgerProcessor{t}). + Pipe( + pipeline.LedgerNode(&testLedgerProcessor{t}), + ), + pipeline.LedgerNode(&testLedgerProcessor{t}). + Pipe( + pipeline.LedgerNode(&testLedgerProcessor{t}), + ), + ), + ) + + err := <-ledgerPipeline.Process(mockLedgerReader) + assert.NoError(t, err) +} + +type testLedgerProcessor struct { + t *testing.T +} + +func (p *testLedgerProcessor) ProcessLedger(ctx context.Context, store *supportPipeline.Store, r io.LedgerReader, w io.LedgerWriter) (err error) { + defer func() { + // io.LedgerReader.Close() returns error if upgrade changes have not + // been processed so it's worth checking the error. + closeErr := r.Close() + // Do not overwrite the previous error + if err == nil { + err = closeErr + } + }() + defer w.Close() + + _, err = r.Read() + assert.Error(p.t, err) + assert.Equal(p.t, stdio.EOF, err) + + change, err := r.ReadUpgradeChange() + assert.NoError(p.t, err) + assert.Equal(p.t, change.Type, xdr.LedgerEntryTypeAccount) + + _, err = r.ReadUpgradeChange() + assert.Error(p.t, err) + assert.Equal(p.t, stdio.EOF, err) + + return nil +} + +func (*testLedgerProcessor) Name() string { + return "Test processor" +} + +func (*testLedgerProcessor) Reset() { + // +} diff --git a/exp/ingest/pipeline/main.go b/exp/ingest/pipeline/main.go index e129cff82a..a363afb4e9 100644 --- a/exp/ingest/pipeline/main.go +++ b/exp/ingest/pipeline/main.go @@ -11,8 +11,9 @@ import ( type ContextKey string const ( - LedgerSequenceContextKey ContextKey = "ledger_sequence" - LedgerHeaderContextKey ContextKey = "ledger_header" + LedgerSequenceContextKey ContextKey = "ledger_sequence" + LedgerHeaderContextKey ContextKey = "ledger_header" + LedgerUpgradeChangesContextKey ContextKey = "ledger_upgrade_changes" ) func GetLedgerSequenceFromContext(ctx context.Context) uint32 { @@ -35,6 +36,16 @@ func GetLedgerHeaderFromContext(ctx context.Context) xdr.LedgerHeaderHistoryEntr return v.(xdr.LedgerHeaderHistoryEntry) } +func GetLedgerUpgradeChangesFromContext(ctx context.Context) []io.Change { + v := ctx.Value(LedgerUpgradeChangesContextKey) + + if v == nil { + panic("ledger upgrade changes not found in context") + } + + return v.([]io.Change) +} + type StatePipeline struct { supportPipeline.Pipeline } @@ -123,11 +134,23 @@ type LedgerProcessor interface { // The first argument `ctx` is a context with cancel. Processor should monitor // `ctx.Done()` channel and exit when it returns a value. This can happen when // pipeline execution is interrupted, ex. due to an error. + // Please note that processor can filter transactions (by not passing them to + // `io.LedgerWriter`) but it cannot filter ledger upgrade changes + // (`io.LeaderReader.ReadUpgradeChange`). All upgrade changes will be available + // for the next processor to read. // // Given all information above `ProcessLedger` should always look like this: // - // func (p *Processor) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) error { - // defer r.Close() + // func (p *Processor) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) (err error) { + // defer func() { + // // io.LedgerReader.Close() returns error if upgrade changes have not + // // been processed so it's worth checking the error. + // closeErr := r.Close() + // // Do not overwrite the previous error + // if err == nil { + // err = closeErr + // } + // }() // defer w.Close() // // // Some pre code... @@ -165,6 +188,18 @@ type LedgerProcessor interface { // } // } // + // for { + // change, err := r.ReadUpgradeChange() + // if err != nil { + // if err == stdio.EOF { + // break + // } else { + // return err + // } + // + // // Process ledger upgrade change... + // } + // // // Some post code... // // return nil @@ -217,6 +252,12 @@ var _ io.StateReader = &readerWrapperState{} // readerWrapperLedger wraps pipeline.Reader to implement LedgerReader interface. type readerWrapperLedger struct { supportPipeline.Reader + + upgradeChanges []io.Change + // currentUpgrade points to the upgrade to be read by `ReadUpgradeChange`. + currentUpgradeChange int + readUpgradeChangeCalled bool + ignoreUpgradeChanges bool } var _ io.LedgerReader = &readerWrapperLedger{} diff --git a/exp/ingest/pipeline/wrappers.go b/exp/ingest/pipeline/wrappers.go index 6b311ad0d2..1cea00bb58 100644 --- a/exp/ingest/pipeline/wrappers.go +++ b/exp/ingest/pipeline/wrappers.go @@ -2,6 +2,8 @@ package pipeline import ( "context" + "fmt" + stdio "io" "github.com/stellar/go/exp/ingest/io" supportPipeline "github.com/stellar/go/exp/support/pipeline" @@ -22,7 +24,10 @@ func (w *ledgerProcessorWrapper) Process(ctx context.Context, store *supportPipe return w.LedgerProcessor.ProcessLedger( ctx, store, - &readerWrapperLedger{reader}, + &readerWrapperLedger{ + Reader: reader, + upgradeChanges: GetLedgerUpgradeChangesFromContext(reader.GetContext()), + }, &writerWrapperLedger{writer}, ) } @@ -45,6 +50,16 @@ func (w *ledgerReaderWrapper) GetContext() context.Context { ctx := context.Background() ctx = context.WithValue(ctx, LedgerSequenceContextKey, w.LedgerReader.GetSequence()) ctx = context.WithValue(ctx, LedgerHeaderContextKey, w.LedgerReader.GetHeader()) + + // Save upgrade changes in context. UpgradeChangesContainer is implemented by + // *io.DBLedgerReader and readerWrapperLedger. + reader, ok := w.LedgerReader.(io.UpgradeChangesContainer) + if !ok { + panic(fmt.Sprintf("Cannot get upgrade changes from unknown reader type: %T", w.LedgerReader)) + } + + ctx = context.WithValue(ctx, LedgerUpgradeChangesContextKey, reader.GetUpgradeChanges()) + return ctx } @@ -88,6 +103,44 @@ func (w *readerWrapperLedger) Read() (io.LedgerTransaction, error) { return entry, nil } +// ReadUpgradeChange returns the next ledger upgrade change or EOF if there are +// no more upgrade changes. Not safe for concurrent use! +func (w *readerWrapperLedger) ReadUpgradeChange() (io.Change, error) { + w.readUpgradeChangeCalled = true + + if w.currentUpgradeChange < len(w.upgradeChanges) { + change := w.upgradeChanges[w.currentUpgradeChange] + w.currentUpgradeChange++ + return change, nil + } + + return io.Change{}, stdio.EOF +} + +func (w *readerWrapperLedger) IgnoreUpgradeChanges() { + w.ignoreUpgradeChanges = true +} + +func (w *readerWrapperLedger) GetUpgradeChanges() []io.Change { + return w.upgradeChanges +} + +func (w *readerWrapperLedger) Close() error { + if !w.ignoreUpgradeChanges && + (!w.readUpgradeChangeCalled || w.currentUpgradeChange != len(w.upgradeChanges)) { + return errors.New("Ledger upgrade changes not read! Use ReadUpgradeChange() method.") + } + + // Call IgnoreUpgradeChanges on a wrapped reader because `readerWrapperLedger` + // is responsible for streaming ledger upgrade changes now. + wrapper, ok := w.Reader.(*ledgerReaderWrapper) + if ok { + wrapper.LedgerReader.IgnoreUpgradeChanges() + } + + return w.Reader.Close() +} + func (w *writerWrapperState) Write(entry xdr.LedgerEntryChange) error { return w.Writer.Write(entry) } diff --git a/exp/ingest/processors/csv_printer.go b/exp/ingest/processors/csv_printer.go index 766d7f70ca..180b177aa6 100644 --- a/exp/ingest/processors/csv_printer.go +++ b/exp/ingest/processors/csv_printer.go @@ -172,9 +172,18 @@ func (p *CSVPrinter) ProcessState(ctx context.Context, store *pipeline.Store, r return nil } -func (p *CSVPrinter) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) error { - defer r.Close() +func (p *CSVPrinter) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) (err error) { + defer func() { + // io.LedgerReader.Close() returns error if upgrade changes have not + // been processed so it's worth checking the error. + closeErr := r.Close() + // Do not overwrite the previous error + if err == nil { + err = closeErr + } + }() defer w.Close() + r.IgnoreUpgradeChanges() f, err := p.fileHandle() if err != nil { diff --git a/exp/ingest/processors/root_processor.go b/exp/ingest/processors/root_processor.go index d58b1b5aaf..c6b9bc6f03 100644 --- a/exp/ingest/processors/root_processor.go +++ b/exp/ingest/processors/root_processor.go @@ -42,9 +42,18 @@ func (p *RootProcessor) ProcessState(ctx context.Context, store *pipeline.Store, return nil } -func (p *RootProcessor) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) error { - defer r.Close() +func (p *RootProcessor) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) (err error) { + defer func() { + // io.LedgerReader.Close() returns error if upgrade changes have not + // been processed so it's worth checking the error. + closeErr := r.Close() + // Do not overwrite the previous error + if err == nil { + err = closeErr + } + }() defer w.Close() + r.IgnoreUpgradeChanges() for { transaction, err := r.Read() diff --git a/exp/orderbook/batch.go b/exp/orderbook/batch.go index f58676a3a0..b9cddb169e 100644 --- a/exp/orderbook/batch.go +++ b/exp/orderbook/batch.go @@ -50,7 +50,7 @@ func (tx *orderBookBatchedUpdates) removeOffer(offerID xdr.Int64) *orderBookBatc } // apply will attempt to apply all the updates in the batch to the order book -func (tx *orderBookBatchedUpdates) apply() error { +func (tx *orderBookBatchedUpdates) apply(ledger uint32) error { tx.orderbook.lock.Lock() defer tx.orderbook.lock.Unlock() @@ -60,6 +60,10 @@ func (tx *orderBookBatchedUpdates) apply() error { } tx.committed = true + if tx.orderbook.lastLedger > 0 && ledger != tx.orderbook.lastLedger+1 { + return errUnexpectedLedger + } + for _, operation := range tx.operations { switch operation.operationType { case addOfferOperationType: @@ -75,5 +79,7 @@ func (tx *orderBookBatchedUpdates) apply() error { } } + tx.orderbook.lastLedger = ledger + return nil } diff --git a/exp/orderbook/dfs.go b/exp/orderbook/dfs.go index 2d7456afe8..1fe25ced68 100644 --- a/exp/orderbook/dfs.go +++ b/exp/orderbook/dfs.go @@ -289,9 +289,12 @@ func consumeOffersForSellingAsset( totalConsumed += xdr.Int64(buyingUnitsFromOffer) currentAssetAmount -= xdr.Int64(sellingUnitsFromOffer) - if currentAssetAmount <= 0 { + if currentAssetAmount == 0 { return totalConsumed, nil } + if currentAssetAmount < 0 { + return -1, errSoldTooMuch + } } return -1, nil @@ -319,7 +322,15 @@ func consumeOffersForBuyingAsset( // otherwise consume entire offer and move on to the next one amountSold, err := price.MulFractionRoundDown(int64(currentAssetAmount), d, n) if err == nil { - if amountSoldXDR := xdr.Int64(amountSold); amountSoldXDR > 0 && amountSoldXDR <= offer.Amount { + amountSoldXDR := xdr.Int64(amountSold) + if amountSoldXDR == 0 { + // we do not have enough of the buying asset to consume the offer + return -1, nil + } + if amountSoldXDR < 0 { + return -1, errSoldTooMuch + } + if amountSoldXDR <= offer.Amount { totalConsumed += amountSoldXDR return totalConsumed, nil } @@ -344,9 +355,12 @@ func consumeOffersForBuyingAsset( totalConsumed += xdr.Int64(sellingUnitsFromOffer) currentAssetAmount -= xdr.Int64(buyingUnitsFromOffer) - if currentAssetAmount <= 0 { + if currentAssetAmount == 0 { return totalConsumed, nil } + if currentAssetAmount < 0 { + return -1, errSoldTooMuch + } } return -1, nil diff --git a/exp/orderbook/graph.go b/exp/orderbook/graph.go index 20f335440a..6a4ba0e963 100644 --- a/exp/orderbook/graph.go +++ b/exp/orderbook/graph.go @@ -12,7 +12,9 @@ var ( errOfferNotPresent = errors.New("offer is not present in the order book graph") errEmptyOffers = errors.New("offers is empty") errAssetAmountIsZero = errors.New("current asset amount is 0") + errSoldTooMuch = errors.New("sold more than current balance") errBatchAlreadyApplied = errors.New("cannot apply batched updates more than once") + errUnexpectedLedger = errors.New("cannot apply unexpected ledger") ) type sortByType string @@ -46,6 +48,8 @@ type OrderBookGraph struct { // batchedUpdates is internal batch of updates to this graph. Users can // create multiple batches using `Batch()` method but sometimes only one // batch is enough. + // the orderbook graph is accurate up to lastLedger + lastLedger uint32 batchedUpdates *orderBookBatchedUpdates lock sync.RWMutex } @@ -85,8 +89,8 @@ func (graph *OrderBookGraph) Discard() { // Apply will attempt to apply all the updates in the internal batch to the order book. // When Apply is successful, a new empty, instance of internal batch will be created. -func (graph *OrderBookGraph) Apply() error { - err := graph.batchedUpdates.apply() +func (graph *OrderBookGraph) Apply(ledger uint32) error { + err := graph.batchedUpdates.apply(ledger) if err != nil { return err } @@ -119,6 +123,55 @@ func (graph *OrderBookGraph) batch() *orderBookBatchedUpdates { } } +// findOffers returns all offers for a given trading pair +// The offers will be sorted by price from cheapest to most expensive +// The returned offers will span at most `maxPriceLevels` price levels +func (graph *OrderBookGraph) findOffers( + selling, buying string, maxPriceLevels int, +) []xdr.OfferEntry { + results := []xdr.OfferEntry{} + edges, ok := graph.edgesForSellingAsset[selling] + if !ok { + return results + } + offers, ok := edges[buying] + if !ok { + return results + } + + for _, offer := range offers { + if len(results) == 0 || results[len(results)-1].Price != offer.Price { + maxPriceLevels-- + } + if maxPriceLevels < 0 { + return results + } + + results = append(results, offer) + } + return results +} + +// FindAsksAndBids returns all asks and bids for a given trading pair +// Asks consists of all offers which sell `selling` in exchange for `buying` sorted by +// price (in terms of `buying`) from cheapest to most expensive +// Bids consists of all offers which sell `buying` in exchange for `selling` sorted by +// price (in terms of `selling`) from cheapest to most expensive +// Both Asks and Bids will span at most `maxPriceLevels` price levels +func (graph *OrderBookGraph) FindAsksAndBids( + selling, buying xdr.Asset, maxPriceLevels int, +) ([]xdr.OfferEntry, []xdr.OfferEntry, uint32) { + buyingString := buying.String() + sellingString := selling.String() + + graph.lock.RLock() + defer graph.lock.RUnlock() + asks := graph.findOffers(sellingString, buyingString, maxPriceLevels) + bids := graph.findOffers(buyingString, sellingString, maxPriceLevels) + + return asks, bids, graph.lastLedger +} + // add inserts a given offer into the order book graph func (graph *OrderBookGraph) add(offer xdr.OfferEntry) error { if _, contains := graph.tradingPairForOffer[offer.OfferId]; contains { @@ -197,7 +250,7 @@ func (graph *OrderBookGraph) FindPaths( sourceAssetBalances []xdr.Int64, validateSourceBalance bool, maxAssetsPerPath int, -) ([]Path, error) { +) ([]Path, uint32, error) { destinationAssetString := destinationAsset.String() sourceAssetsMap := map[string]xdr.Int64{} for i, sourceAsset := range sourceAssets { @@ -224,16 +277,18 @@ func (graph *OrderBookGraph) FindPaths( destinationAsset, destinationAmount, ) + lastLedger := graph.lastLedger graph.lock.RUnlock() if err != nil { - return nil, errors.Wrap(err, "could not determine paths") + return nil, lastLedger, errors.Wrap(err, "could not determine paths") } - return sortAndFilterPaths( + paths, err := sortAndFilterPaths( searchState.paths, maxAssetsPerPath, sortBySourceAsset, ) + return paths, lastLedger, err } // FindFixedPaths returns a list of payment paths where the source and destination @@ -247,7 +302,7 @@ func (graph *OrderBookGraph) FindFixedPaths( amountToSpend xdr.Int64, destinationAssets []xdr.Asset, maxAssetsPerPath int, -) ([]Path, error) { +) ([]Path, uint32, error) { target := map[string]bool{} for _, destinationAsset := range destinationAssets { destinationAssetString := destinationAsset.String() @@ -271,20 +326,22 @@ func (graph *OrderBookGraph) FindFixedPaths( sourceAsset, amountToSpend, ) + lastLedger := graph.lastLedger graph.lock.RUnlock() if err != nil { - return nil, errors.Wrap(err, "could not determine paths") + return nil, lastLedger, errors.Wrap(err, "could not determine paths") } sort.Slice(searchState.paths, func(i, j int) bool { return searchState.paths[i].DestinationAmount > searchState.paths[j].DestinationAmount }) - return sortAndFilterPaths( + paths, err := sortAndFilterPaths( searchState.paths, maxAssetsPerPath, sortByDestinationAsset, ) + return paths, lastLedger, err } // compareSourceAsset will group payment paths by `SourceAsset` diff --git a/exp/orderbook/graph_test.go b/exp/orderbook/graph_test.go index 982abcae4c..bf65f65e7d 100644 --- a/exp/orderbook/graph_test.go +++ b/exp/orderbook/graph_test.go @@ -301,6 +301,45 @@ func TestRemoveEdgeSet(t *testing.T) { }) } +func TestApplyOutdatedLedger(t *testing.T) { + graph := NewOrderBookGraph() + if graph.lastLedger != 0 { + t.Fatalf("expected last ledger to be %v but got %v", 0, graph.lastLedger) + } + + err := graph. + AddOffer(fiftyCentsOffer). + Apply(2) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if graph.lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, graph.lastLedger) + } + + err = graph. + AddOffer(eurOffer). + Apply(1) + if err != errUnexpectedLedger { + t.Fatalf("expected error %v but got %v", errUnexpectedLedger, err) + } + if graph.lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, graph.lastLedger) + } + + graph.Discard() + + err = graph. + AddOffer(eurOffer). + Apply(4) + if err != errUnexpectedLedger { + t.Fatalf("expected error %v but got %v", errUnexpectedLedger, err) + } + if graph.lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, graph.lastLedger) + } +} + func TestAddOfferOrderBook(t *testing.T) { graph := NewOrderBookGraph() @@ -311,10 +350,13 @@ func TestAddOfferOrderBook(t *testing.T) { AddOffer(twoEurOffer). AddOffer(quarterOffer). AddOffer(fiftyCentsOffer). - Apply() + Apply(1) if err != nil { t.Fatalf("unexpected error %v", err) } + if graph.lastLedger != 1 { + t.Fatalf("expected last ledger to be %v but got %v", 1, graph.lastLedger) + } eurUsdOffer := xdr.OfferEntry{ SellerId: issuer, @@ -355,10 +397,13 @@ func TestAddOfferOrderBook(t *testing.T) { AddOffer(eurUsdOffer). AddOffer(otherEurUsdOffer). AddOffer(usdEurOffer). - Apply() + Apply(2) if err != nil { t.Fatalf("unexpected error %v", err) } + if graph.lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, graph.lastLedger) + } expectedGraph := &OrderBookGraph{ edgesForSellingAsset: map[string]edgeSet{ @@ -432,10 +477,13 @@ func TestAddOfferOrderBook(t *testing.T) { AddOffer(usdEurOffer). AddOffer(dollarOffer). AddOffer(threeEurOffer). - Apply() + Apply(3) if err != nil { t.Fatalf("unexpected error %v", err) } + if graph.lastLedger != 3 { + t.Fatalf("expected last ledger to be %v but got %v", 3, graph.lastLedger) + } assertGraphEquals(t, graph, expectedGraph) } @@ -454,10 +502,13 @@ func TestUpdateOfferOrderBook(t *testing.T) { AddOffer(twoEurOffer). AddOffer(quarterOffer). AddOffer(fiftyCentsOffer). - Apply() + Apply(1) if err != nil { t.Fatalf("unexpected error %v", err) } + if graph.lastLedger != 1 { + t.Fatalf("expected last ledger to be %v but got %v", 1, graph.lastLedger) + } if graph.IsEmpty() { t.Fatal("expected graph to not be empty") @@ -502,10 +553,13 @@ func TestUpdateOfferOrderBook(t *testing.T) { AddOffer(eurUsdOffer). AddOffer(otherEurUsdOffer). AddOffer(usdEurOffer). - Apply() + Apply(2) if err != nil { t.Fatalf("unexpected error %v", err) } + if graph.lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, graph.lastLedger) + } usdEurOffer.Price.N = 4 usdEurOffer.Price.D = 1 @@ -519,10 +573,13 @@ func TestUpdateOfferOrderBook(t *testing.T) { AddOffer(usdEurOffer). AddOffer(otherEurUsdOffer). AddOffer(dollarOffer). - Apply() + Apply(3) if err != nil { t.Fatalf("unexpected error %v", err) } + if graph.lastLedger != 3 { + t.Fatalf("expected last ledger to be %v but got %v", 3, graph.lastLedger) + } expectedGraph := &OrderBookGraph{ edgesForSellingAsset: map[string]edgeSet{ @@ -604,22 +661,32 @@ func TestDiscard(t *testing.T) { AddOffer(quarterOffer). AddOffer(fiftyCentsOffer). Discard() - if err := graph.Apply(); err != nil { + if graph.lastLedger != 0 { + t.Fatalf("expected last ledger to be %v but got %v", 0, graph.lastLedger) + } + + if err := graph.Apply(1); err != nil { t.Fatalf("unexpected error %v", err) } if !graph.IsEmpty() { t.Fatal("expected graph to be empty") } + if graph.lastLedger != 1 { + t.Fatalf("expected last ledger to be %v but got %v", 1, graph.lastLedger) + } err := graph. AddOffer(dollarOffer). - Apply() + Apply(2) if err != nil { t.Fatalf("unexpected error %v", err) } if graph.IsEmpty() { t.Fatal("expected graph to be not empty") } + if graph.lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, graph.lastLedger) + } expectedOffers := []xdr.OfferEntry{dollarOffer} assertOfferListEquals(t, graph.Offers(), expectedOffers) @@ -638,10 +705,13 @@ func TestRemoveOfferOrderBook(t *testing.T) { AddOffer(twoEurOffer). AddOffer(quarterOffer). AddOffer(fiftyCentsOffer). - Apply() + Apply(1) if err != nil { t.Fatalf("unexpected error %v", err) } + if graph.lastLedger != 1 { + t.Fatalf("expected last ledger to be %v but got %v", 1, graph.lastLedger) + } eurUsdOffer := xdr.OfferEntry{ SellerId: issuer, @@ -685,10 +755,13 @@ func TestRemoveOfferOrderBook(t *testing.T) { RemoveOffer(usdEurOffer.OfferId). RemoveOffer(otherEurUsdOffer.OfferId). RemoveOffer(dollarOffer.OfferId). - Apply() + Apply(2) if err != nil { t.Fatalf("unexpected error %v", err) } + if graph.lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, graph.lastLedger) + } expectedGraph := &OrderBookGraph{ edgesForSellingAsset: map[string]edgeSet{ @@ -746,10 +819,13 @@ func TestRemoveOfferOrderBook(t *testing.T) { RemoveOffer(twoEurOffer.OfferId). RemoveOffer(threeEurOffer.OfferId). RemoveOffer(eurUsdOffer.OfferId). - Apply() + Apply(3) if err != nil { t.Fatalf("unexpected error %v", err) } + if graph.lastLedger != 3 { + t.Fatalf("expected last ledger to be %v but got %v", 3, graph.lastLedger) + } expectedGraph.edgesForSellingAsset = map[string]edgeSet{} expectedGraph.tradingPairForOffer = map[xdr.Int64]tradingPair{} @@ -760,6 +836,155 @@ func TestRemoveOfferOrderBook(t *testing.T) { } } +func TestFindOffers(t *testing.T) { + graph := NewOrderBookGraph() + + assertOfferListEquals( + t, + []xdr.OfferEntry{}, + graph.findOffers(nativeAsset.String(), eurAsset.String(), 0), + ) + + assertOfferListEquals( + t, + []xdr.OfferEntry{}, + graph.findOffers(nativeAsset.String(), eurAsset.String(), 5), + ) + + err := graph. + AddOffer(threeEurOffer). + AddOffer(eurOffer). + AddOffer(twoEurOffer). + Apply(1) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + assertOfferListEquals( + t, + []xdr.OfferEntry{}, + graph.findOffers(nativeAsset.String(), eurAsset.String(), 0), + ) + assertOfferListEquals( + t, + []xdr.OfferEntry{eurOffer, twoEurOffer}, + graph.findOffers(nativeAsset.String(), eurAsset.String(), 2), + ) + + extraTwoEurOffers := []xdr.OfferEntry{} + for i := 0; i < 4; i++ { + otherTwoEurOffer := twoEurOffer + otherTwoEurOffer.OfferId += xdr.Int64(i + 17) + graph.AddOffer(otherTwoEurOffer) + extraTwoEurOffers = append(extraTwoEurOffers, otherTwoEurOffer) + } + if err := graph.Apply(2); err != nil { + t.Fatalf("unexpected error %v", err) + } + + assertOfferListEquals( + t, + append([]xdr.OfferEntry{eurOffer, twoEurOffer}, extraTwoEurOffers...), + graph.findOffers(nativeAsset.String(), eurAsset.String(), 2), + ) + assertOfferListEquals( + t, + append(append([]xdr.OfferEntry{eurOffer, twoEurOffer}, extraTwoEurOffers...), threeEurOffer), + graph.findOffers(nativeAsset.String(), eurAsset.String(), 3), + ) +} + +func TestFindAsksAndBids(t *testing.T) { + graph := NewOrderBookGraph() + + asks, bids, lastLedger := graph.FindAsksAndBids(nativeAsset, eurAsset, 0) + assertOfferListEquals( + t, + []xdr.OfferEntry{}, + asks, + ) + assertOfferListEquals( + t, + []xdr.OfferEntry{}, + bids, + ) + if lastLedger != 0 { + t.Fatalf("expected last ledger to be %v but got %v", 0, lastLedger) + } + + asks, bids, lastLedger = graph.FindAsksAndBids(nativeAsset, eurAsset, 5) + assertOfferListEquals( + t, + []xdr.OfferEntry{}, + asks, + ) + assertOfferListEquals( + t, + []xdr.OfferEntry{}, + bids, + ) + if lastLedger != 0 { + t.Fatalf("expected last ledger to be %v but got %v", 0, lastLedger) + } + + err := graph. + AddOffer(threeEurOffer). + AddOffer(eurOffer). + AddOffer(twoEurOffer). + Apply(1) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + asks, bids, lastLedger = graph.FindAsksAndBids(nativeAsset, eurAsset, 0) + assertOfferListEquals( + t, + []xdr.OfferEntry{}, + asks, + ) + assertOfferListEquals( + t, + []xdr.OfferEntry{}, + bids, + ) + if lastLedger != 1 { + t.Fatalf("expected last ledger to be %v but got %v", 1, lastLedger) + } + + extraTwoEurOffers := []xdr.OfferEntry{} + for i := 0; i < 4; i++ { + otherTwoEurOffer := twoEurOffer + otherTwoEurOffer.OfferId += xdr.Int64(i + 17) + graph.AddOffer(otherTwoEurOffer) + extraTwoEurOffers = append(extraTwoEurOffers, otherTwoEurOffer) + } + if err := graph.Apply(2); err != nil { + t.Fatalf("unexpected error %v", err) + } + + sellEurOffer := twoEurOffer + sellEurOffer.Buying, sellEurOffer.Selling = sellEurOffer.Selling, sellEurOffer.Buying + sellEurOffer.OfferId = 35 + if err := graph.AddOffer(sellEurOffer).Apply(3); err != nil { + t.Fatalf("unexpected error %v", err) + } + + asks, bids, lastLedger = graph.FindAsksAndBids(nativeAsset, eurAsset, 3) + assertOfferListEquals( + t, + append(append([]xdr.OfferEntry{eurOffer, twoEurOffer}, extraTwoEurOffers...), threeEurOffer), + asks, + ) + assertOfferListEquals( + t, + []xdr.OfferEntry{sellEurOffer}, + bids, + ) + if lastLedger != 3 { + t.Fatalf("expected last ledger to be %v but got %v", 3, lastLedger) + } +} + func TestConsumeOffersForSellingAsset(t *testing.T) { kp, err := keypair.Random() if err != nil { @@ -926,8 +1151,15 @@ func TestConsumeOffersForBuyingAsset(t *testing.T) { "offer denominator cannot be zero", []xdr.OfferEntry{denominatorZeroOffer}, 10000, - 0, - price.ErrDivisionByZero, + -1, + nil, + }, + { + "balance too low to consume offers", + []xdr.OfferEntry{twoEurOffer}, + 1, + -1, + nil, }, { "not enough offers to consume", @@ -1200,7 +1432,7 @@ func TestFindPaths(t *testing.T) { AddOffer(twoEurOffer). AddOffer(quarterOffer). AddOffer(fiftyCentsOffer). - Apply() + Apply(1) if err != nil { t.Fatalf("unexpected error %v", err) } @@ -1270,7 +1502,7 @@ func TestFindPaths(t *testing.T) { AddOffer(usdEurOffer). AddOffer(chfEurOffer). AddOffer(yenChfOffer). - Apply() + Apply(2) if err != nil { t.Fatalf("unexpected error %v", err) } @@ -1281,7 +1513,7 @@ func TestFindPaths(t *testing.T) { } ignoreOffersFrom := xdr.MustAddress(kp.Address()) - paths, err := graph.FindPaths( + paths, lastLedger, err := graph.FindPaths( 3, nativeAsset, 20, @@ -1301,8 +1533,11 @@ func TestFindPaths(t *testing.T) { t.Fatalf("unexpected error %v", err) } assertPathEquals(t, paths, []Path{}) + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } - paths, err = graph.FindPaths( + paths, lastLedger, err = graph.FindPaths( 3, nativeAsset, 20, @@ -1321,6 +1556,9 @@ func TestFindPaths(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } expectedPaths := []Path{ Path{ @@ -1353,7 +1591,7 @@ func TestFindPaths(t *testing.T) { assertPathEquals(t, paths, expectedPaths) - paths, err = graph.FindPaths( + paths, lastLedger, err = graph.FindPaths( 3, nativeAsset, 20, @@ -1373,8 +1611,11 @@ func TestFindPaths(t *testing.T) { t.Fatalf("unexpected error %v", err) } assertPathEquals(t, paths, expectedPaths) + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } - paths, err = graph.FindPaths( + paths, lastLedger, err = graph.FindPaths( 4, nativeAsset, 20, @@ -1390,6 +1631,9 @@ func TestFindPaths(t *testing.T) { true, 5, ) + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } expectedPaths = []Path{ Path{ @@ -1437,7 +1681,7 @@ func TestFindPaths(t *testing.T) { assertPathEquals(t, paths, expectedPaths) - paths, err = graph.FindPaths( + paths, lastLedger, err = graph.FindPaths( 4, nativeAsset, 20, @@ -1453,6 +1697,9 @@ func TestFindPaths(t *testing.T) { true, 5, ) + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } expectedPaths = []Path{ Path{ @@ -1511,7 +1758,7 @@ func TestFindPathsStartingAt(t *testing.T) { AddOffer(twoEurOffer). AddOffer(quarterOffer). AddOffer(fiftyCentsOffer). - Apply() + Apply(1) if err != nil { t.Fatalf("unexpected error %v", err) } @@ -1581,12 +1828,12 @@ func TestFindPathsStartingAt(t *testing.T) { AddOffer(usdEurOffer). AddOffer(chfEurOffer). AddOffer(yenChfOffer). - Apply() + Apply(2) if err != nil { t.Fatalf("unexpected error %v", err) } - paths, err := graph.FindFixedPaths( + paths, lastLedger, err := graph.FindFixedPaths( 3, usdAsset, 5, @@ -1596,6 +1843,9 @@ func TestFindPathsStartingAt(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } expectedPaths := []Path{ Path{ @@ -1618,7 +1868,7 @@ func TestFindPathsStartingAt(t *testing.T) { assertPathEquals(t, paths, expectedPaths) - paths, err = graph.FindFixedPaths( + paths, lastLedger, err = graph.FindFixedPaths( 2, yenAsset, 5, @@ -1628,12 +1878,15 @@ func TestFindPathsStartingAt(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } expectedPaths = []Path{} assertPathEquals(t, paths, expectedPaths) - paths, err = graph.FindFixedPaths( + paths, lastLedger, err = graph.FindFixedPaths( 3, yenAsset, 5, @@ -1643,6 +1896,9 @@ func TestFindPathsStartingAt(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } expectedPaths = []Path{ Path{ @@ -1659,7 +1915,7 @@ func TestFindPathsStartingAt(t *testing.T) { assertPathEquals(t, paths, expectedPaths) - paths, err = graph.FindFixedPaths( + paths, lastLedger, err = graph.FindFixedPaths( 5, yenAsset, 5, @@ -1669,6 +1925,9 @@ func TestFindPathsStartingAt(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } expectedPaths = []Path{ Path{ @@ -1696,7 +1955,7 @@ func TestFindPathsStartingAt(t *testing.T) { assertPathEquals(t, paths, expectedPaths) - paths, err = graph.FindFixedPaths( + paths, lastLedger, err = graph.FindFixedPaths( 5, yenAsset, 5, @@ -1706,6 +1965,9 @@ func TestFindPathsStartingAt(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } + if lastLedger != 2 { + t.Fatalf("expected last ledger to be %v but got %v", 2, lastLedger) + } expectedPaths = []Path{ Path{ diff --git a/exp/tools/horizon-demo/database_processor.go b/exp/tools/horizon-demo/database_processor.go index 73edc6591e..30f0463413 100644 --- a/exp/tools/horizon-demo/database_processor.go +++ b/exp/tools/horizon-demo/database_processor.go @@ -128,19 +128,19 @@ func (p *DatabaseProcessor) processLedgerAccountsForSigner(transaction io.Ledger continue } - accountEntry := change.Pre.MustAccount() + accountEntry := change.Pre.Data.MustAccount() account := accountEntry.AccountId.Address() // This removes all Pre signers adds Post signers but can be // improved by finding a diff - for _, signer := range change.Pre.MustAccount().Signers { + for _, signer := range change.Pre.Data.MustAccount().Signers { _, err := p.Database.RemoveAccountSigner(account, signer.Key.Address()) if err != nil { return errors.Wrap(err, "Error removing a signer") } } - for _, signer := range change.Post.MustAccount().Signers { + for _, signer := range change.Post.Data.MustAccount().Signers { _, err := p.Database.InsertAccountSigner(account, signer.Key.Address()) if err != nil { return errors.Wrap(err, "Error inserting a signer") diff --git a/exp/tools/horizon-demo/orderbook_processor.go b/exp/tools/horizon-demo/orderbook_processor.go index 3bf12633e3..6fb89631aa 100644 --- a/exp/tools/horizon-demo/orderbook_processor.go +++ b/exp/tools/horizon-demo/orderbook_processor.go @@ -73,11 +73,11 @@ func (p *OrderbookProcessor) ProcessLedger(ctx context.Context, store *pipeline. switch { case change.Post != nil: // Created or updated - offer := change.Post.MustOffer() + offer := change.Post.Data.MustOffer() p.OrderBookGraph.AddOffer(offer) case change.Pre != nil && change.Post == nil: // Removed - offer := change.Pre.MustOffer() + offer := change.Pre.Data.MustOffer() p.OrderBookGraph.RemoveOffer(offer.OfferId) } } diff --git a/exp/tools/horizon-demo/pipelines.go b/exp/tools/horizon-demo/pipelines.go index 3423340f9b..ba246c9d79 100644 --- a/exp/tools/horizon-demo/pipelines.go +++ b/exp/tools/horizon-demo/pipelines.go @@ -91,7 +91,7 @@ func addPipelineHooks( wg.Add(2) go func() { - err = orderBookGraph.Apply() + err = orderBookGraph.Apply(ledgerSeq) wg.Done() }() diff --git a/go.list b/go.list index b69deeb330..15a09a456a 100644 --- a/go.list +++ b/go.list @@ -26,6 +26,7 @@ github.com/golang/protobuf v1.3.1 github.com/gomodule/redigo v2.0.0+incompatible github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 +github.com/gorilla/schema v1.1.0 github.com/graph-gophers/graphql-go v0.0.0-20190225005345-3e8838d4614c github.com/guregu/null v2.1.3-0.20151024101046-79c5bd36b615+incompatible github.com/hashicorp/golang-lru v0.5.0 diff --git a/go.mod b/go.mod index fa5cc54189..f69b81aee2 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d github.com/gomodule/redigo v2.0.0+incompatible github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 // indirect + github.com/gorilla/schema v1.1.0 github.com/graph-gophers/graphql-go v0.0.0-20190225005345-3e8838d4614c github.com/guregu/null v2.1.3-0.20151024101046-79c5bd36b615+incompatible github.com/hashicorp/golang-lru v0.5.0 // indirect diff --git a/go.sum b/go.sum index dd0d37bb48..2de345f5ef 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 h1:oERTZ1buO github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= +github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/graph-gophers/graphql-go v0.0.0-20190225005345-3e8838d4614c h1:YyFUsspLqAt3noyPCLz7EFK/o1LpC1j/6MjU0bSVOQ4= github.com/graph-gophers/graphql-go v0.0.0-20190225005345-3e8838d4614c/go.mod h1:uJhtPXrcJLqyi0H5IuMFh+fgW+8cMMakK3Txrbk/WJE= github.com/guregu/null v2.1.3-0.20151024101046-79c5bd36b615+incompatible h1:SZmF1M6CdAm4MmTPYYTG+x9EC8D3FOxUq9S4D37irQg= diff --git a/protocols/horizon/effects/main.go b/protocols/horizon/effects/main.go index 2f45418cc5..c52a64d595 100644 --- a/protocols/horizon/effects/main.go +++ b/protocols/horizon/effects/main.go @@ -257,7 +257,7 @@ type TrustlineDeauthorized struct { type Trade struct { Base Seller string `json:"seller"` - // Action needed in release: horizon-v0.23.0 + // Action needed in release: horizon-v0.25.0 OfferID int64 `json:"offer_id"` SoldAmount string `json:"sold_amount"` SoldAssetType string `json:"sold_asset_type"` diff --git a/protocols/horizon/main.go b/protocols/horizon/main.go index cb80e4c899..699d00acf4 100644 --- a/protocols/horizon/main.go +++ b/protocols/horizon/main.go @@ -50,6 +50,12 @@ type Account struct { Balances []Balance `json:"balances"` Signers []Signer `json:"signers"` Data map[string]string `json:"data"` + PT string `json:"paging_token"` +} + +// PagingToken implementation for hal.Pageable +func (res Account) PagingToken() string { + return res.PT } // GetAccountID returns the Stellar account ID. This is to satisfy the @@ -224,7 +230,7 @@ type Offer struct { OfferMaker hal.Link `json:"offer_maker"` } `json:"_links"` - // Action needed in release: horizon-v0.23.0 + // Action needed in release: horizon-v0.25.0 ID int64 `json:"id"` PT string `json:"paging_token"` Seller string `json:"seller"` @@ -290,6 +296,8 @@ type Root struct { Offers *hal.Link `json:"offers,omitempty"` OrderBook hal.Link `json:"order_book"` Self hal.Link `json:"self"` + StrictReceivePaths *hal.Link `json:"strict_receive_paths"` + StrictSendPaths *hal.Link `json:"strict_send_paths"` Transaction hal.Link `json:"transaction"` Transactions hal.Link `json:"transactions"` } `json:"_links"` @@ -417,7 +425,7 @@ type Transaction struct { LedgerCloseTime time.Time `json:"created_at"` Account string `json:"source_account"` AccountSequence string `json:"source_account_sequence"` - // Action needed in release: horizon-v0.23.0 + // Action needed in release: horizon-v0.25.0 // Action needed in release: horizonclient-v2.0.0 // Remove this field. FeePaid int32 `json:"fee_paid"` diff --git a/protocols/horizon/operations/main.go b/protocols/horizon/operations/main.go index 26aae2bd3e..184f460c22 100644 --- a/protocols/horizon/operations/main.go +++ b/protocols/horizon/operations/main.go @@ -151,7 +151,7 @@ type CreatePassiveSellOffer struct { // is ManageSellOffer. type ManageSellOffer struct { Offer - // Action needed in release: horizon-v0.23.0 + // Action needed in release: horizon-v0.25.0 OfferID int64 `json:"offer_id"` } @@ -159,7 +159,7 @@ type ManageSellOffer struct { // is ManageBuyOffer. type ManageBuyOffer struct { Offer - // Action needed in release: horizon-v0.23.0 + // Action needed in release: horizon-v0.25.0 OfferID int64 `json:"offer_id"` } diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 2a6c89dc49..a5067e167c 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -6,6 +6,24 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). As this project is pre 1.0, breaking changes may happen for minor version bumps. A breaking change will get clearly notified in this log. +## v0.23.0 + +* New features in experimental ingestion (to enable: set `--enable-experimental-ingestion` CLI param or `ENABLE_EXPERIMENTAL_INGESTION=true` env variable): + * All state-related endpoints (i.e. ledger entries) are now served from Horizon DB (except `/account/{account_id}`) + + * `/order_book` offers data is served from in-memory store ([#1761](https://github.com/stellar/go/pull/1761)) + + * Add `Latest-Ledger` header with the sequence number of the most recent ledger processed by the experimental ingestion system. Endpoints built on the experimental ingestion system will always respond with data which is consistent with the ledger in `Latest-Ledger` ([#1830](https://github.com/stellar/go/pull/1830)) + + * Add experimental support for filtering accounts who are trustees to an asset via `/accounts`. Example:\ + `/accounts?asset=COP:GC2GFGZ5CZCFCDJSQF3YYEAYBOS3ZREXJSPU7LUJ7JU3LP3BQNHY7YKS`\ + returns all accounts who have a trustline to the asset `COP` issued by account `GC2GFG...` ([#1835](https://github.com/stellar/go/pull/1835)) + + * Experimental "Accounts For Signers" end-point now returns a full account resource ([#1876](https://github.com/stellar/go/issues/1875)) +* Prevent "`multiple response.WriteHeader calls`" errors when streaming ([#1870](https://github.com/stellar/go/issues/1870)) +* Fix an interpolation bug in `/fee_stats` ([#1857](https://github.com/stellar/go/pull/1857)) +* Fix a bug in `/paths/strict-send` where occasionally bad paths were returned ([#1863](https://github.com/stellar/go/pull/1863)) + ## v0.22.2 * Fixes a bug in accounts for signer ingestion processor. diff --git a/services/horizon/internal/action.go b/services/horizon/internal/action.go index 15fecffe9d..1644118845 100644 --- a/services/horizon/internal/action.go +++ b/services/horizon/internal/action.go @@ -2,11 +2,11 @@ package horizon import ( "context" + "database/sql" "net/http" "net/url" "strings" - "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/actions" "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/db2/core" @@ -15,12 +15,9 @@ import ( "github.com/stellar/go/services/horizon/internal/ledger" hProblem "github.com/stellar/go/services/horizon/internal/render/problem" "github.com/stellar/go/services/horizon/internal/render/sse" - "github.com/stellar/go/services/horizon/internal/resourceadapter" "github.com/stellar/go/services/horizon/internal/toid" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/log" - "github.com/stellar/go/support/render/httpjson" - "github.com/stellar/go/support/render/problem" ) // Action is the "base type" for all actions in horizon. It provides @@ -179,17 +176,30 @@ type showActionQueryParams struct { // getAccountInfo returns the information about an account based on the provided param. func (w *web) getAccountInfo(ctx context.Context, qp *showActionQueryParams) (interface{}, error) { - return actions.AccountInfo(ctx, &core.Q{w.coreSession(ctx)}, qp.AccountID) -} + // Use AppFromContext to prevent larger refactoring of actions code. Will + // be removed once this endpoint is migrated to use new actions design. + app := AppFromContext(ctx) + var historyQ *history.Q -// getAccountPage returns a page containing the account records. -func (w *web) getAccountPage(ctx context.Context, qp *indexActionQueryParams) (interface{}, error) { - horizonSession, err := w.horizonSession(ctx) - if err != nil { - return nil, errors.Wrap(err, "getting horizon db session") + if app.config.EnableExperimentalIngestion { + horizonSession, err := w.horizonSession(ctx) + if err != nil { + return nil, errors.Wrap(err, "getting horizon db session") + } + + err = horizonSession.BeginTx(&sql.TxOptions{ + Isolation: sql.LevelRepeatableRead, + ReadOnly: true, + }) + if err != nil { + return nil, errors.Wrap(err, "error starting transaction") + } + + defer horizonSession.Rollback() + historyQ = &history.Q{horizonSession} } - return actions.AccountPage(ctx, &history.Q{horizonSession}, qp.Signer, qp.PagingParams) + return actions.AccountInfo(ctx, &core.Q{w.coreSession(ctx)}, historyQ, qp.AccountID, app.config.EnableExperimentalIngestion) } // getTransactionPage returns a page containing the transaction records of an account or a ledger. @@ -221,36 +231,3 @@ func (w *web) streamTransactions(ctx context.Context, s *sse.Stream, qp *indexAc return actions.StreamTransactions(ctx, s, &history.Q{horizonSession}, qp.AccountID, qp.LedgerID, qp.IncludeFailedTxs, qp.PagingParams) } - -// getOfferRecord returns a single offer resource. -func getOfferResource(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - offerID, err := getInt64ParamFromURL(r, "id") - if err != nil { - problem.Render(ctx, w, errors.Wrap(err, "couldn't parse offer id")) - return - } - - app := AppFromContext(ctx) - record, err := app.HistoryQ().GetOfferByID(offerID) - if err != nil { - problem.Render(ctx, w, err) - return - } - - ledger := new(history.Ledger) - err = app.HistoryQ().LedgerBySequence( - ledger, - int32(record.LastModifiedLedger), - ) - if app.HistoryQ().NoRows(err) { - ledger = nil - } else if err != nil { - problem.Render(ctx, w, err) - return - } - - var offerResponse horizon.Offer - resourceadapter.PopulateHistoryOffer(ctx, &offerResponse, record, ledger) - httpjson.Render(w, offerResponse, httpjson.HALJSON) -} diff --git a/services/horizon/internal/action_offers_expingest_test.go b/services/horizon/internal/action_offers_expingest_test.go deleted file mode 100644 index d0370fc943..0000000000 --- a/services/horizon/internal/action_offers_expingest_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package horizon - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/go-chi/chi" - "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/services/horizon/internal/actions" - "github.com/stellar/go/services/horizon/internal/db2/history" - "github.com/stellar/go/services/horizon/internal/render/problem" - "github.com/stellar/go/services/horizon/internal/render/sse" - "github.com/stellar/go/services/horizon/internal/test" - "github.com/stellar/go/xdr" -) - -var ( - issuer = xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") - seller = xdr.MustAddress("GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2") - - nativeAsset = xdr.MustNewNativeAsset() - eurAsset = xdr.MustNewCreditAsset("EUR", issuer.Address()) - - eurOffer = xdr.OfferEntry{ - SellerId: issuer, - OfferId: xdr.Int64(4), - Buying: eurAsset, - Selling: nativeAsset, - Price: xdr.Price{ - N: 1, - D: 1, - }, - Flags: 1, - Amount: xdr.Int64(500), - } - twoEurOffer = xdr.OfferEntry{ - SellerId: seller, - OfferId: xdr.Int64(5), - Buying: eurAsset, - Selling: nativeAsset, - Price: xdr.Price{ - N: 2, - D: 1, - }, - Flags: 2, - Amount: xdr.Int64(500), - } -) - -func TestOfferActions_Show(t *testing.T) { - ht := StartHTTPTest(t, "base") - ht.App.config.EnableExperimentalIngestion = true - defer ht.Finish() - q := &history.Q{ht.HorizonSession()} - - ht.Assert.NoError(q.UpdateLastLedgerExpIngest(3)) - - _, err := q.InsertOffer(eurOffer, 3) - ht.Assert.NoError(err) - _, err = q.InsertOffer(twoEurOffer, 20) - ht.Assert.NoError(err) - - w := ht.Get(fmt.Sprintf("/offers/%v", eurOffer.OfferId)) - - if ht.Assert.Equal(200, w.Code) { - var result horizon.Offer - err := json.Unmarshal(w.Body.Bytes(), &result) - ht.Require.NoError(err) - ht.Assert.Equal(int64(eurOffer.OfferId), result.ID) - ht.Assert.Equal("native", result.Selling.Type) - ht.Assert.Equal("credit_alphanum4", result.Buying.Type) - ht.Assert.Equal(issuer.Address(), result.Seller) - ht.Assert.Equal(issuer.Address(), result.Buying.Issuer) - ht.Assert.Equal(int32(3), result.LastModifiedLedger) - - ledger := new(history.Ledger) - err = q.LedgerBySequence(ledger, 3) - - ht.Assert.NoError(err) - ht.Assert.True(ledger.ClosedAt.Equal(*result.LastModifiedTime)) - } - - w = ht.Get(fmt.Sprintf("/offers/%v", twoEurOffer.OfferId)) - - if ht.Assert.Equal(200, w.Code) { - var result horizon.Offer - err := json.Unmarshal(w.Body.Bytes(), &result) - ht.Require.NoError(err) - ht.Assert.Equal(int32(20), result.LastModifiedLedger) - ht.Assert.Nil(result.LastModifiedTime) - } -} - -func TestOfferActions_OfferDoesNotExist(t *testing.T) { - ht := StartHTTPTestWithoutScenario(t) - ht.App.config.EnableExperimentalIngestion = true - defer ht.Finish() - q := &history.Q{ht.HorizonSession()} - ht.Assert.NoError(q.UpdateLastLedgerExpIngest(3)) - - w := ht.Get("/offers/123456") - - ht.Assert.Equal(404, w.Code) -} - -func TestOfferActionsStillIngesting_Show(t *testing.T) { - ht := StartHTTPTestWithoutScenario(t) - ht.App.config.EnableExperimentalIngestion = true - defer ht.Finish() - q := &history.Q{ht.HorizonSession()} - ht.Assert.NoError(q.UpdateLastLedgerExpIngest(0)) - - w := ht.Get("/offers/123456") - ht.Assert.Equal(problem.StillIngesting.Status, w.Code) -} - -func TestOfferActionsRequiresExperimentalIngestion(t *testing.T) { - ht := StartHTTPTestWithoutScenario(t) - ht.App.config.EnableExperimentalIngestion = true - defer ht.Finish() - q := &history.Q{ht.HorizonSession()} - ht.Assert.NoError(q.UpdateLastLedgerExpIngest(0)) - - w := ht.Get("/offers") - ht.Assert.Equal(problem.StillIngesting.Status, w.Code) - - ht.Assert.NoError(q.UpdateLastLedgerExpIngest(2)) - w = ht.Get("/offers") - ht.Assert.Equal(http.StatusOK, w.Code) - - ht.App.config.EnableExperimentalIngestion = false - w = ht.Get("/offers") - ht.Assert.Equal(http.StatusNotFound, w.Code) -} - -func TestOfferActionsExperimentalIngestion(t *testing.T) { - tt := test.Start(t) - defer tt.Finish() - - test.ResetHorizonDB(t, tt.HorizonDB) - q := &history.Q{tt.HorizonSession()} - app := &App{ - historyQ: q, - } - app.config.EnableExperimentalIngestion = true - - handler := actions.GetAccountOffersHandler{HistoryQ: q} - client := accountOffersClient(tt, app, handler) - - tt.Assert.NoError(q.UpdateLastLedgerExpIngest(0)) - w := client.Get(fmt.Sprintf("/accounts/%s/offers", issuer.Address())) - tt.Assert.Equal(problem.StillIngesting.Status, w.Code) - - tt.Assert.NoError(q.UpdateLastLedgerExpIngest(3)) - w = client.Get(fmt.Sprintf("/accounts/%s/offers", issuer.Address())) - tt.Assert.Equal(http.StatusOK, w.Code) - - app.config.EnableExperimentalIngestion = false - w = client.Get(fmt.Sprintf("/accounts/%s/offers", issuer.Address())) - tt.Assert.Equal(http.StatusNotFound, w.Code) -} - -func accountOffersClient( - tt *test.T, - app *App, - handler actions.GetAccountOffersHandler, -) test.RequestHelper { - router := chi.NewRouter() - router.Use(appContextMiddleware(app)) - - installAccountOfferRoute(handler, sse.StreamHandler{}, true, router) - return test.NewRequestHelper(router) -} diff --git a/services/horizon/internal/action_test.go b/services/horizon/internal/action_test.go index e7e4f9df78..3349a89272 100644 --- a/services/horizon/internal/action_test.go +++ b/services/horizon/internal/action_test.go @@ -27,7 +27,12 @@ func TestGetAccountInfo(t *testing.T) { w := mustInitWeb(context.Background(), &history.Q{tt.HorizonSession()}, &core.Q{tt.CoreSession()}, time.Duration(5), 0, true) - res, err := w.getAccountInfo(tt.Ctx, &showActionQueryParams{AccountID: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU"}) + ctx := withAppContext(tt.Ctx, &App{ + config: Config{ + EnableExperimentalIngestion: false, + }, + }) + res, err := w.getAccountInfo(ctx, &showActionQueryParams{AccountID: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU"}) tt.Assert.NoError(err) account, ok := res.(*horizon.Account) @@ -46,7 +51,7 @@ func TestGetAccountInfo(t *testing.T) { } } - _, err = w.getAccountInfo(tt.Ctx, &showActionQueryParams{AccountID: "GDBAPLDCAEJV6LSEDFEAUDAVFYSNFRUYZ4X75YYJJMMX5KFVUOHX46SQ"}) + _, err = w.getAccountInfo(ctx, &showActionQueryParams{AccountID: "GDBAPLDCAEJV6LSEDFEAUDAVFYSNFRUYZ4X75YYJJMMX5KFVUOHX46SQ"}) tt.Assert.Equal(errors.Cause(err), sql.ErrNoRows) } diff --git a/services/horizon/internal/actions/account.go b/services/horizon/internal/actions/account.go index a9a5813f6f..2fcaa8f459 100644 --- a/services/horizon/internal/actions/account.go +++ b/services/horizon/internal/actions/account.go @@ -2,18 +2,24 @@ package actions import ( "context" + "encoding/json" + "fmt" + "net/http" + "strings" protocol "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/db2/core" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/resourceadapter" "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" "github.com/stellar/go/support/render/hal" + "github.com/stellar/go/support/render/problem" + "github.com/stellar/go/xdr" ) // AccountInfo returns the information about an account identified by addr. -func AccountInfo(ctx context.Context, cq *core.Q, addr string) (*protocol.Account, error) { +func AccountInfo(ctx context.Context, cq *core.Q, hq *history.Q, addr string, enableExperimentalIngestion bool) (*protocol.Account, error) { var ( coreRecord core.Account coreData []core.AccountData @@ -50,36 +56,378 @@ func AccountInfo(ctx context.Context, cq *core.Q, addr string) (*protocol.Accoun coreSigners, coreTrustlines, ) + if err != nil { + return nil, errors.Wrap(err, "populating account") + } + + if enableExperimentalIngestion { + c, err := json.Marshal(resource) + if err != nil { + return nil, errors.Wrap(err, "error marshaling resource") + } + + // We send JSON bytes to compareAccountResults to prevent modifying + // `resource` in any way. + err = compareAccountResults(ctx, hq, c, addr) + if err != nil { + log.Ctx(ctx).WithFields(log.F{ + "err": err, + "accounts_check": true, // So it's easy to find all diffs + }).Warn("error comparing core and horizon accounts") + } + } - return &resource, errors.Wrap(err, "populating account") + return &resource, nil } -// AccountPage returns a page containing the account records that -// have `signer` as a signer. -// This doesn't return full account details resource because of the -// limitations of existing ingestion architecture. In a future, when -// the new ingestion system is fully integrated, this endpoint can be -// used to find accounts for signer but also accounts for assets, -// home domain, inflation_dest etc. -func AccountPage(ctx context.Context, hq history.QSigners, signer string, pq db2.PageQuery) (hal.Page, error) { - records, err := hq.AccountsForSigner(signer, pq) +func compareAccountResults( + ctx context.Context, + hq *history.Q, + expectedResourceBytes []byte, + addr string, +) error { + var ( + horizonRecord history.AccountEntry + horizonData []history.Data + horizonSigners []history.AccountSigner + horizonTrustLines []history.TrustLine + newResource protocol.Account + ) + + horizonRecord, err := hq.GetAccountByID(addr) + if err != nil { + return err + } + + horizonData, err = hq.GetAccountDataByAccountID(addr) + if err != nil { + return err + } + + horizonSigners, err = hq.GetAccountSignersByAccountID(addr) if err != nil { - return hal.Page{}, errors.Wrap(err, "loading account records") + return err + } + + horizonTrustLines, err = hq.GetTrustLinesByAccountID(addr) + if err != nil { + return err + } + + err = resourceadapter.PopulateAccountEntry( + ctx, + &newResource, + horizonRecord, + horizonData, + horizonSigners, + horizonTrustLines, + ) + if err != nil { + return err + } + + var expectedResource protocol.Account + err = json.Unmarshal(expectedResourceBytes, &expectedResource) + if err != nil { + return errors.Wrap(err, "Error unmarshaling expectedResourceBytes") + } + + if err = accountResourcesEqual(newResource, expectedResource); err != nil { + return errors.Wrap( + err, + fmt.Sprintf( + "Core and Horizon accounts responses do not match: %+v %+v", + expectedResource, newResource, + )) + + } + + return nil +} + +// accountResourcesEqual compares two protocol.Account objects and returns an +// error if they are different but only if `LastModifiedLedger` fields are the +// same. +func accountResourcesEqual(actual, expected protocol.Account) error { + if actual.Links != expected.Links { + return errors.New("Links are different") + } + + if actual.LastModifiedLedger != expected.LastModifiedLedger { + // Modified at different ledgers so values will be different + return nil + } + + if actual.ID != expected.ID || + actual.AccountID != expected.AccountID || + actual.Sequence != expected.Sequence || + actual.SubentryCount != expected.SubentryCount || + actual.InflationDestination != expected.InflationDestination || + actual.HomeDomain != expected.HomeDomain || + actual.Thresholds != expected.Thresholds || + actual.Flags != expected.Flags { + return errors.New("Main fields are different") + } + + // Ignore PT + + // Balances + balances := map[string]protocol.Balance{} + for _, balance := range expected.Balances { + id := balance.Asset.Type + balance.Asset.Code + balance.Asset.Issuer + balances[id] = balance + } + + for _, actualBalance := range actual.Balances { + id := actualBalance.Asset.Type + actualBalance.Asset.Code + actualBalance.Asset.Issuer + expectedBalance := balances[id] + delete(balances, id) + + if expectedBalance.LastModifiedLedger != actualBalance.LastModifiedLedger { + // Modified at different ledgers so values will be different + continue + } + + if expectedBalance.Balance != actualBalance.Balance || + expectedBalance.Limit != actualBalance.Limit || + expectedBalance.BuyingLiabilities != actualBalance.BuyingLiabilities || + expectedBalance.SellingLiabilities != actualBalance.SellingLiabilities { + return errors.New("Balance " + id + " is different") + } + + if expectedBalance.IsAuthorized == nil && actualBalance.IsAuthorized == nil { + continue + } + + if expectedBalance.IsAuthorized != nil && actualBalance.IsAuthorized != nil && + *expectedBalance.IsAuthorized == *actualBalance.IsAuthorized { + continue + } + + return errors.New("IsAuthorized is different for " + id) + } + + if len(balances) > 0 { + return errors.New("Some extra balances") + } + + // Signers + signers := map[string]protocol.Signer{} + for _, signer := range expected.Signers { + signers[signer.Key] = signer + } + + for _, actualSigner := range actual.Signers { + expectedSigner := signers[actualSigner.Key] + delete(signers, actualSigner.Key) + + if expectedSigner != actualSigner { + return errors.New("Signer is different") + } } - page := hal.Page{ - Cursor: pq.Cursor, - Order: pq.Order, - Limit: pq.Limit, + if len(signers) > 0 { + return errors.New("Extra signers") + } + + // Data + data := map[string]string{} + for key, value := range expected.Data { + data[key] = value + } + + for actualKey, actualValue := range actual.Data { + expectedValue := data[actualKey] + delete(data, actualKey) + + if expectedValue != actualValue { + return errors.New("Data is different") + } + } + + if len(data) > 0 { + return errors.New("Extra data") + } + + return nil +} + +// AccountsQuery query struct for accounts end-point +type AccountsQuery struct { + Signer string `schema:"signer" valid:"accountID,optional"` + AssetFilter string `schema:"asset" valid:"asset,optional"` +} + +// URITemplate returns a rfc6570 URI template the query struct +func (q AccountsQuery) URITemplate() string { + return "/accounts{?" + strings.Join(GetURIParams(&q, true), ",") + "}" +} + +var invalidAccountsParams = problem.P{ + Type: "invalid_accounts_params", + Title: "Invalid Accounts Parameters", + Status: http.StatusBadRequest, + Detail: "A filter is required. Please ensure that you are including a signer or an asset.", +} + +// Validate runs custom validations. +func (q AccountsQuery) Validate() error { + if q.AssetFilter == "native" { + return problem.MakeInvalidFieldProblem( + "asset", + errors.New("you can't filter by asset: native"), + ) + } + + if len(q.Signer) == 0 && q.Asset() == nil { + return invalidAccountsParams + } + + if len(q.Signer) > 0 && q.Asset() != nil { + return problem.MakeInvalidFieldProblem( + "signer", + errors.New("you can't filter by signer and asset at the same time"), + ) + } + + return nil +} + +// Asset returns an xdr.Asset representing the Asset we want to find the trustees by. +func (q AccountsQuery) Asset() *xdr.Asset { + if len(q.AssetFilter) == 0 { + return nil + } + + parts := strings.Split(q.AssetFilter, ":") + asset := xdr.MustNewCreditAsset(parts[0], parts[1]) + + return &asset +} + +// GetAccountsHandler is the action handler for the /accounts endpoint +type GetAccountsHandler struct { +} + +// GetResourcePage returns a page containing the account records that have +// `signer` as a signer or have a trustline to the given asset. +func (handler GetAccountsHandler) GetResourcePage( + w HeaderWriter, + r *http.Request, +) ([]hal.Pageable, error) { + ctx := r.Context() + pq, err := GetPageQuery(r, DisableCursorValidation) + if err != nil { + return nil, err + } + + historyQ, err := historyQFromRequest(r) + if err != nil { + return nil, err + } + + qp := AccountsQuery{} + err = GetParams(&qp, r) + if err != nil { + return nil, err + } + + var records []history.AccountEntry + + if len(qp.Signer) > 0 { + records, err = historyQ.AccountEntriesForSigner(qp.Signer, pq) + if err != nil { + return nil, errors.Wrap(err, "loading account records") + } + } else { + records, err = historyQ.AccountsForAsset(*qp.Asset(), pq) + if err != nil { + return nil, errors.Wrap(err, "loading account records") + } + } + + accounts := make([]hal.Pageable, 0, len(records)) + + if len(records) == 0 { + // early return + return accounts, nil + } + + accountIDs := make([]string, 0, len(records)) + for _, record := range records { + accountIDs = append(accountIDs, record.AccountID) + } + + signers, err := handler.loadSigners(historyQ, accountIDs) + if err != nil { + return nil, err + } + + trustlines, err := handler.loadTrustlines(historyQ, accountIDs) + if err != nil { + return nil, err + } + + data, err := handler.loadData(historyQ, accountIDs) + if err != nil { + return nil, err + } + + for _, record := range records { + var res protocol.Account + s := signers[record.AccountID] + t := trustlines[record.AccountID] + d := data[record.AccountID] + + resourceadapter.PopulateAccountEntry(ctx, &res, record, d, s, t) + + accounts = append(accounts, res) + } + + return accounts, nil +} + +func (handler GetAccountsHandler) loadData(historyQ *history.Q, accounts []string) (map[string][]history.Data, error) { + data := make(map[string][]history.Data) + + records, err := historyQ.GetAccountDataByAccountsID(accounts) + if err != nil { + return data, errors.Wrap(err, "loading account data records by accounts id") + } + + for _, record := range records { + data[record.AccountID] = append(data[record.AccountID], record) + } + + return data, nil +} + +func (handler GetAccountsHandler) loadTrustlines(historyQ *history.Q, accounts []string) (map[string][]history.TrustLine, error) { + trustLines := make(map[string][]history.TrustLine) + + records, err := historyQ.GetTrustLinesByAccountsID(accounts) + if err != nil { + return trustLines, errors.Wrap(err, "loading trustline records by accounts") + } + + for _, record := range records { + trustLines[record.AccountID] = append(trustLines[record.AccountID], record) + } + + return trustLines, nil +} + +func (handler GetAccountsHandler) loadSigners(historyQ *history.Q, accounts []string) (map[string][]history.AccountSigner, error) { + signers := make(map[string][]history.AccountSigner) + + records, err := historyQ.SignersForAccounts(accounts) + if err != nil { + return signers, errors.Wrap(err, "loading account signers by account") } for _, record := range records { - var res protocol.AccountSigner - resourceadapter.PopulateAccountSigner(ctx, &res, record) - page.Add(res) + signers[record.Account] = append(signers[record.Account], record) } - page.FullURL = FullURL(ctx) - page.PopulateLinks() - return page, nil + return signers, nil } diff --git a/services/horizon/internal/actions/account_test.go b/services/horizon/internal/actions/account_test.go index 67b2df3107..21f7750aee 100644 --- a/services/horizon/internal/actions/account_test.go +++ b/services/horizon/internal/actions/account_test.go @@ -1,22 +1,171 @@ package actions import ( - "context" + "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" + protocol "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/db2/core" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/test" - "github.com/stretchr/testify/assert" + "github.com/stellar/go/support/render/problem" + "github.com/stellar/go/xdr" +) + +var ( + trustLineIssuer = "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" + accountOne = "GABGMPEKKDWR2WFH5AJOZV5PDKLJEHGCR3Q24ALETWR5H3A7GI3YTS7V" + accountTwo = "GADTXHUTHIAESMMQ2ZWSTIIGBZRLHUCBLCHPLLUEIAWDEFRDC4SYDKOZ" + accountThree = "GDP347UYM2ZKE6ED6T5OM3BQ5IAS76NKRVEUPNB5PCQ26Z5D7Q7PJOMI" + signer = "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU" + usd = xdr.MustNewCreditAsset("USD", trustLineIssuer) + euro = xdr.MustNewCreditAsset("EUR", trustLineIssuer) + + account1 = xdr.AccountEntry{ + AccountId: xdr.MustAddress(accountOne), + Balance: 20000, + SeqNum: 223456789, + NumSubEntries: 10, + Flags: 1, + HomeDomain: "stellar.org", + Thresholds: xdr.Thresholds{1, 2, 3, 4}, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 3, + Selling: 4, + }, + }, + }, + } + + account2 = xdr.AccountEntry{ + AccountId: xdr.MustAddress(accountTwo), + Balance: 50000, + SeqNum: 648736, + NumSubEntries: 10, + Flags: 2, + HomeDomain: "meridian.stellar.org", + Thresholds: xdr.Thresholds{5, 6, 7, 8}, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 30, + Selling: 40, + }, + }, + }, + } + + account3 = xdr.AccountEntry{ + AccountId: xdr.MustAddress(signer), + Balance: 50000, + SeqNum: 648736, + NumSubEntries: 10, + Flags: 2, + Thresholds: xdr.Thresholds{5, 6, 7, 8}, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 30, + Selling: 40, + }, + }, + }, + } + + eurTrustLine = xdr.TrustLineEntry{ + AccountId: xdr.MustAddress(accountOne), + Asset: euro, + Balance: 20000, + Limit: 223456789, + Flags: 1, + Ext: xdr.TrustLineEntryExt{ + V: 1, + V1: &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 3, + Selling: 4, + }, + }, + }, + } + + usdTrustLine = xdr.TrustLineEntry{ + AccountId: xdr.MustAddress(accountTwo), + Asset: usd, + Balance: 10000, + Limit: 123456789, + Flags: 0, + Ext: xdr.TrustLineEntryExt{ + V: 1, + V1: &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 1, + Selling: 2, + }, + }, + }, + } + + data1 = xdr.DataEntry{ + AccountId: xdr.MustAddress(accountOne), + DataName: "test data", + // This also tests if base64 encoding is working as 0 is invalid UTF-8 byte + DataValue: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + } + + data2 = xdr.DataEntry{ + AccountId: xdr.MustAddress(accountTwo), + DataName: "test data2", + DataValue: []byte{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, + } + + accountSigners = []history.AccountSigner{ + history.AccountSigner{ + Account: accountOne, + Signer: accountOne, + Weight: 1, + }, + history.AccountSigner{ + Account: accountTwo, + Signer: accountTwo, + Weight: 1, + }, + history.AccountSigner{ + Account: accountOne, + Signer: signer, + Weight: 1, + }, + history.AccountSigner{ + Account: accountTwo, + Signer: signer, + Weight: 2, + }, + history.AccountSigner{ + Account: signer, + Signer: signer, + Weight: 3, + }, + } ) func TestAccountInfo(t *testing.T) { tt := test.Start(t).Scenario("allow_trust") defer tt.Finish() - account, err := AccountInfo(tt.Ctx, &core.Q{tt.CoreSession()}, "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU") + account, err := AccountInfo( + tt.Ctx, + &core.Q{tt.CoreSession()}, + &history.Q{tt.HorizonSession()}, + signer, + false, + ) tt.Assert.NoError(err) tt.Assert.Equal("8589934593", account.Sequence) @@ -30,79 +179,250 @@ func TestAccountInfo(t *testing.T) { } } } +func TestGetAccountsHandlerPageNoResults(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) -func TestAccountPageNoResults(t *testing.T) { - mockQ := &history.MockQSigners{} + q := &history.Q{tt.HorizonSession()} + handler := &GetAccountsHandler{} + records, err := handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{ + "signer": signer, + }, + map[string]string{}, + q.Session, + ), + ) + tt.Assert.NoError(err) + tt.Assert.Len(records, 0) +} - mockQ.On("GetLastLedgerExpIngestNonBlocking").Return(uint32(10), nil).Once() +func TestGetAccountsHandlerPageResultsBySigner(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) - mockQ. - On( - "AccountsForSigner", - "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - db2.PageQuery{}, - ). - Return([]history.AccountSigner{}, nil).Once() + q := &history.Q{tt.HorizonSession()} + handler := &GetAccountsHandler{} + + _, err := q.InsertAccount(account1, 1234) + tt.Assert.NoError(err) + _, err = q.InsertAccount(account2, 1234) + tt.Assert.NoError(err) + _, err = q.InsertAccount(account3, 1234) + tt.Assert.NoError(err) + + for _, row := range accountSigners { + q.CreateAccountSigner(row.Account, row.Signer, row.Weight) + } - page, err := AccountPage( - context.Background(), - mockQ, - "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - db2.PageQuery{}, + records, err := handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{ + "signer": signer, + }, + map[string]string{}, + q.Session, + ), ) - assert.NoError(t, err) - assert.Equal(t, 0, len(page.Embedded.Records)) -} -func TestAccountPageResults(t *testing.T) { - mockQ := &history.MockQSigners{} + tt.Assert.NoError(err) + tt.Assert.Equal(3, len(records)) - mockQ.On("GetLastLedgerExpIngestNonBlocking").Return(uint32(10), nil).Once() + want := map[string]bool{ + accountOne: true, + accountTwo: true, + signer: true, + } - pq := db2.PageQuery{ - Order: "asc", - Limit: 100, + for _, row := range records { + result := row.(protocol.Account) + tt.Assert.True(want[result.AccountID]) + delete(want, result.AccountID) } - rows := []history.AccountSigner{ - history.AccountSigner{ - Account: "GABGMPEKKDWR2WFH5AJOZV5PDKLJEHGCR3Q24ALETWR5H3A7GI3YTS7V", - Signer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - Weight: 1, - }, - history.AccountSigner{ - Account: "GADTXHUTHIAESMMQ2ZWSTIIGBZRLHUCBLCHPLLUEIAWDEFRDC4SYDKOZ", - Signer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - Weight: 2, - }, - history.AccountSigner{ - Account: "GDP347UYM2ZKE6ED6T5OM3BQ5IAS76NKRVEUPNB5PCQ26Z5D7Q7PJOMI", - Signer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - Weight: 3, - }, + tt.Assert.Empty(want) + + records, err = handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{ + "signer": signer, + "cursor": accountOne, + }, + map[string]string{}, + q.Session, + ), + ) + + tt.Assert.NoError(err) + tt.Assert.Equal(2, len(records)) + + want = map[string]bool{ + accountTwo: true, + signer: true, + } + + for _, row := range records { + result := row.(protocol.Account) + tt.Assert.True(want[result.AccountID]) + delete(want, result.AccountID) + } + + tt.Assert.Empty(want) +} + +func TestGetAccountsHandlerPageResultsByAsset(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &history.Q{tt.HorizonSession()} + handler := &GetAccountsHandler{} + + _, err := q.InsertAccount(account1, 1234) + tt.Assert.NoError(err) + _, err = q.InsertAccount(account2, 1234) + tt.Assert.NoError(err) + + for _, row := range accountSigners { + _, err = q.CreateAccountSigner(row.Account, row.Signer, row.Weight) + tt.Assert.NoError(err) } - mockQ. - On( - "AccountsForSigner", - "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - pq, - ). - Return(rows, nil).Once() + _, err = q.InsertAccountData(data1, 1234) + assert.NoError(t, err) + _, err = q.InsertAccountData(data2, 1234) + assert.NoError(t, err) - page, err := AccountPage( - context.Background(), - mockQ, - "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - pq, + var assetType, code, issuer string + usd.MustExtract(&assetType, &code, &issuer) + params := map[string]string{ + "asset": code + ":" + issuer, + } + + records, err := handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + params, + map[string]string{}, + q.Session, + ), ) + + tt.Assert.NoError(err) + tt.Assert.Equal(0, len(records)) + + _, err = q.InsertTrustLine(eurTrustLine, 1234) + assert.NoError(t, err) + _, err = q.InsertTrustLine(usdTrustLine, 1235) assert.NoError(t, err) - assert.Equal(t, 3, len(page.Embedded.Records)) - for i, row := range rows { - result := page.Embedded.Records[i].(protocol.AccountSigner) - assert.Equal(t, row.Account, result.AccountID) - assert.Equal(t, row.Signer, result.Signer.Key) - assert.Equal(t, row.Weight, result.Signer.Weight) + records, err = handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + params, + map[string]string{}, + q.Session, + ), + ) + + tt.Assert.NoError(err) + tt.Assert.Equal(1, len(records)) + result := records[0].(protocol.Account) + tt.Assert.Equal(accountTwo, result.AccountID) + tt.Assert.Len(result.Balances, 2) + tt.Assert.Len(result.Signers, 2) + + _, ok := result.Data[string(data2.DataName)] + tt.Assert.True(ok) +} + +func TestGetAccountsHandlerInvalidParams(t *testing.T) { + testCases := []struct { + desc string + params map[string]string + expectedInvalidField string + expectedErr string + isInvalidAccountsParams bool + }{ + { + desc: "empty filters", + isInvalidAccountsParams: true, + }, + { + desc: "signer and seller", + params: map[string]string{ + "signer": accountOne, + "asset": "USD" + ":" + accountOne, + }, + expectedInvalidField: "signer", + expectedErr: "you can't filter by signer and asset at the same time", + }, + { + desc: "filtering by native asset", + params: map[string]string{ + "asset": "native", + }, + expectedInvalidField: "asset", + expectedErr: "you can't filter by asset: native", + }, + { + desc: "invalid asset", + params: map[string]string{ + "asset_issuer": accountOne, + "asset": "USDCOP:someissuer", + }, + expectedInvalidField: "asset", + expectedErr: customTagsErrorMessages["asset"], + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + q := &history.Q{tt.HorizonSession()} + handler := &GetAccountsHandler{} + + _, err := handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + tc.params, + map[string]string{}, + q.Session, + ), + ) + tt.Assert.Error(err) + if tc.isInvalidAccountsParams { + tt.Assert.Equal(invalidAccountsParams, err) + } else { + if tt.Assert.IsType(&problem.P{}, err) { + p := err.(*problem.P) + tt.Assert.Equal("bad_request", p.Type) + tt.Assert.Equal(tc.expectedInvalidField, p.Extras["invalid_field"]) + tt.Assert.Equal( + tc.expectedErr, + p.Extras["reason"], + ) + } + } + }) } } + +func TestAccountQueryURLTemplate(t *testing.T) { + tt := assert.New(t) + expected := "/accounts{?signer,asset,cursor,limit,order}" + accountsQuery := AccountsQuery{} + tt.Equal(expected, accountsQuery.URITemplate()) +} diff --git a/services/horizon/internal/actions/asset.go b/services/horizon/internal/actions/asset.go new file mode 100644 index 0000000000..785e74411f --- /dev/null +++ b/services/horizon/internal/actions/asset.go @@ -0,0 +1,163 @@ +package actions + +import ( + "fmt" + "net/http" + "strings" + + "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/services/horizon/internal/db2" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/services/horizon/internal/resourceadapter" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/render/hal" + "github.com/stellar/go/support/render/problem" + "github.com/stellar/go/xdr" +) + +// AssetStatsHandler is the action handler for the /asset endpoint +type AssetStatsHandler struct { +} + +func (handler AssetStatsHandler) validateAssetParams(code, issuer string, pq db2.PageQuery) error { + if code != "" { + if !xdr.ValidAssetCode.MatchString(code) { + return problem.MakeInvalidFieldProblem( + "asset_code", + fmt.Errorf("%s is not a valid asset code", code), + ) + } + } + + if issuer != "" { + if _, err := xdr.AddressToAccountId(issuer); err != nil { + return problem.MakeInvalidFieldProblem( + "asset_issuer", + fmt.Errorf("%s is not a valid asset issuer", issuer), + ) + } + } + + if pq.Cursor != "" { + parts := strings.SplitN(pq.Cursor, "_", 3) + if len(parts) != 3 { + return problem.MakeInvalidFieldProblem( + "cursor", + errors.New("cursor must contain exactly one colon"), + ) + } + + cursorCode, cursorIssuer, assetType := parts[0], parts[1], parts[2] + if !xdr.ValidAssetCode.MatchString(cursorCode) { + return problem.MakeInvalidFieldProblem( + "cursor", + fmt.Errorf("%s is not a valid asset code", cursorCode), + ) + } + + if _, err := xdr.AddressToAccountId(cursorIssuer); err != nil { + return problem.MakeInvalidFieldProblem( + "cursor", + fmt.Errorf("%s is not a valid asset issuer", cursorIssuer), + ) + } + + if _, ok := xdr.StringToAssetType[assetType]; !ok { + return problem.MakeInvalidFieldProblem( + "cursor", + fmt.Errorf("%s is not a valid asset type", assetType), + ) + } + + } + + return nil +} + +func (handler AssetStatsHandler) findIssuersForAssets( + historyQ *history.Q, + assetStats []history.ExpAssetStat, +) (map[string]history.AccountEntry, error) { + issuerSet := map[string]bool{} + issuers := []string{} + for _, assetStat := range assetStats { + if issuerSet[assetStat.AssetIssuer] { + continue + } + issuerSet[assetStat.AssetIssuer] = true + issuers = append(issuers, assetStat.AssetIssuer) + } + + accountsByID := map[string]history.AccountEntry{} + accounts, err := historyQ.GetAccountsByIDs(issuers) + if err != nil { + return nil, err + } + for _, account := range accounts { + accountsByID[account.AccountID] = account + delete(issuerSet, account.AccountID) + } + + // Note it's possible that no accounts can be found for certain issuers. + // That can occur because an account can be removed when there are only empty trustlines + // pointing to it. We still continue to serve asset stats for such issuers. + + return accountsByID, nil +} + +// GetResourcePage returns a page of offers. +func (handler AssetStatsHandler) GetResourcePage( + w HeaderWriter, + r *http.Request, +) ([]hal.Pageable, error) { + ctx := r.Context() + + code, err := GetString(r, "asset_code") + if err != nil { + return nil, err + } + + issuer, err := GetString(r, "asset_issuer") + if err != nil { + return nil, err + } + + pq, err := GetPageQuery(r, DisableCursorValidation) + if err != nil { + return nil, err + } + + if err = handler.validateAssetParams(code, issuer, pq); err != nil { + return nil, err + } + + historyQ, err := historyQFromRequest(r) + if err != nil { + return nil, err + } + + assetStats, err := historyQ.GetAssetStats(code, issuer, pq) + if err != nil { + return nil, err + } + + issuerAccounts, err := handler.findIssuersForAssets(historyQ, assetStats) + if err != nil { + return nil, err + } + + var response []hal.Pageable + for _, record := range assetStats { + var assetStatResponse horizon.AssetStat + + resourceadapter.PopulateExpAssetStat( + ctx, + &assetStatResponse, + record, + issuerAccounts[record.AssetIssuer], + ) + response = append(response, assetStatResponse) + } + + return response, nil +} diff --git a/services/horizon/internal/actions/asset_test.go b/services/horizon/internal/actions/asset_test.go new file mode 100644 index 0000000000..25985a2dc3 --- /dev/null +++ b/services/horizon/internal/actions/asset_test.go @@ -0,0 +1,363 @@ +package actions + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/support/render/hal" + "github.com/stellar/go/support/render/problem" + "github.com/stellar/go/xdr" +) + +func TestAssetStatsValidation(t *testing.T) { + handler := AssetStatsHandler{} + + for _, testCase := range []struct { + name string + queryParams map[string]string + expectedErrorField string + expectedError string + }{ + { + "invalid asset code", + map[string]string{ + "asset_code": "tooooooooolong", + }, + "asset_code", + "not a valid asset code", + }, + { + "invalid asset issuer", + map[string]string{ + "asset_issuer": "invalid", + }, + "asset_issuer", + "not a valid asset issuer", + }, + { + "cursor has too many underscores", + map[string]string{ + "cursor": "ABC_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4_", + }, + "cursor", + "credit_alphanum4_ is not a valid asset type", + }, + { + "invalid cursor code", + map[string]string{ + "cursor": "tooooooooolong_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum12", + }, + "cursor", + "not a valid asset code", + }, + { + "invalid cursor issuer", + map[string]string{ + "cursor": "ABC_invalidissuer_credit_alphanum4", + }, + "cursor", + "not a valid asset issuer", + }, + { + "invalid cursor type", + map[string]string{ + "cursor": "ABC_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum123", + }, + "cursor", + "credit_alphanum123 is not a valid asset type", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + r := makeRequest(t, testCase.queryParams, map[string]string{}, nil) + _, err := handler.GetResourcePage(httptest.NewRecorder(), r) + if err == nil { + t.Fatalf("expected error %v but got %v", testCase.expectedError, err) + } + + problem := err.(*problem.P) + if field := problem.Extras["invalid_field"]; field != testCase.expectedErrorField { + t.Fatalf( + "expected error field %v but got %v", + testCase.expectedErrorField, + field, + ) + } + + reason := problem.Extras["reason"] + if !strings.Contains(reason.(string), testCase.expectedError) { + t.Fatalf("expected reason %v but got %v", testCase.expectedError, reason) + } + }) + } +} + +func TestAssetStats(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &history.Q{tt.HorizonSession()} + handler := AssetStatsHandler{} + + issuer := history.AccountEntry{ + AccountID: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + Flags: uint32(xdr.AccountFlagsAuthRequiredFlag) | + uint32(xdr.AccountFlagsAuthImmutableFlag), + } + issuerFlags := horizon.AccountFlags{ + AuthRequired: true, + AuthImmutable: true, + } + otherIssuer := history.AccountEntry{ + AccountID: "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + HomeDomain: "xim.com", + } + + usdAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: issuer.AccountID, + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + usdAssetStatResponse := horizon.AssetStat{ + Amount: "0.0000001", + NumAccounts: usdAssetStat.NumAccounts, + Asset: base.Asset{ + Type: "credit_alphanum4", + Code: usdAssetStat.AssetCode, + Issuer: usdAssetStat.AssetIssuer, + }, + PT: usdAssetStat.PagingToken(), + Flags: issuerFlags, + } + + etherAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: issuer.AccountID, + AssetCode: "ETHER", + Amount: "23", + NumAccounts: 1, + } + etherAssetStatResponse := horizon.AssetStat{ + Amount: "0.0000023", + NumAccounts: etherAssetStat.NumAccounts, + Asset: base.Asset{ + Type: "credit_alphanum4", + Code: etherAssetStat.AssetCode, + Issuer: etherAssetStat.AssetIssuer, + }, + PT: etherAssetStat.PagingToken(), + Flags: issuerFlags, + } + + otherUSDAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: otherIssuer.AccountID, + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + otherUSDAssetStatResponse := horizon.AssetStat{ + Amount: "0.0000001", + NumAccounts: otherUSDAssetStat.NumAccounts, + Asset: base.Asset{ + Type: "credit_alphanum4", + Code: otherUSDAssetStat.AssetCode, + Issuer: otherUSDAssetStat.AssetIssuer, + }, + PT: otherUSDAssetStat.PagingToken(), + } + otherUSDAssetStatResponse.Links.Toml = hal.NewLink( + "https://" + otherIssuer.HomeDomain + "/.well-known/stellar.toml", + ) + + eurAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: otherIssuer.AccountID, + AssetCode: "EUR", + Amount: "111", + NumAccounts: 3, + } + eurAssetStatResponse := horizon.AssetStat{ + Amount: "0.0000111", + NumAccounts: eurAssetStat.NumAccounts, + Asset: base.Asset{ + Type: "credit_alphanum4", + Code: eurAssetStat.AssetCode, + Issuer: eurAssetStat.AssetIssuer, + }, + PT: eurAssetStat.PagingToken(), + } + eurAssetStatResponse.Links.Toml = hal.NewLink( + "https://" + otherIssuer.HomeDomain + "/.well-known/stellar.toml", + ) + + for _, assetStat := range []history.ExpAssetStat{ + etherAssetStat, + eurAssetStat, + otherUSDAssetStat, + usdAssetStat, + } { + numChanged, err := q.InsertAssetStat(assetStat) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + } + + for _, account := range []history.AccountEntry{ + issuer, + otherIssuer, + } { + accountEntry := xdr.AccountEntry{ + Flags: xdr.Uint32(account.Flags), + HomeDomain: xdr.String32(account.HomeDomain), + } + if err := accountEntry.AccountId.SetAddress(account.AccountID); err != nil { + t.Fatalf("unexpected error %v", err) + } + numChanged, err := q.InsertAccount(accountEntry, 3) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + } + + for _, testCase := range []struct { + name string + queryParams map[string]string + expected []horizon.AssetStat + }{ + { + "default parameters", + map[string]string{}, + []horizon.AssetStat{ + etherAssetStatResponse, + eurAssetStatResponse, + otherUSDAssetStatResponse, + usdAssetStatResponse, + }, + }, + { + "with cursor", + map[string]string{ + "cursor": etherAssetStatResponse.PagingToken(), + }, + []horizon.AssetStat{ + eurAssetStatResponse, + otherUSDAssetStatResponse, + usdAssetStatResponse, + }, + }, + { + "descending order", + map[string]string{"order": "desc"}, + []horizon.AssetStat{ + usdAssetStatResponse, + otherUSDAssetStatResponse, + eurAssetStatResponse, + etherAssetStatResponse, + }, + }, + { + "filter by asset code", + map[string]string{ + "asset_code": "USD", + }, + []horizon.AssetStat{ + otherUSDAssetStatResponse, + usdAssetStatResponse, + }, + }, + { + "filter by asset issuer", + map[string]string{ + "asset_issuer": issuer.AccountID, + }, + []horizon.AssetStat{ + etherAssetStatResponse, + usdAssetStatResponse, + }, + }, + { + "filter by both asset code and asset issuer", + map[string]string{ + "asset_code": "USD", + "asset_issuer": issuer.AccountID, + }, + []horizon.AssetStat{ + usdAssetStatResponse, + }, + }, + { + "filter produces empty set", + map[string]string{ + "asset_code": "XYZ", + "asset_issuer": issuer.AccountID, + }, + []horizon.AssetStat{}, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + r := makeRequest(t, testCase.queryParams, map[string]string{}, q.Session) + results, err := handler.GetResourcePage(httptest.NewRecorder(), r) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + if len(results) != len(testCase.expected) { + t.Fatalf( + "expectes results to have length %v but got %v", + len(results), + len(testCase.expected), + ) + } + + for i, item := range results { + assetStat := item.(horizon.AssetStat) + if assetStat != testCase.expected[i] { + t.Fatalf("expected %v but got %v", testCase.expected[i], assetStat) + } + } + }) + } +} + +func TestAssetStatsIssuerDoesNotExist(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &history.Q{tt.HorizonSession()} + handler := AssetStatsHandler{} + + usdAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + numChanged, err := q.InsertAssetStat(usdAssetStat) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + + r := makeRequest(t, map[string]string{}, map[string]string{}, q.Session) + results, err := handler.GetResourcePage(httptest.NewRecorder(), r) + tt.Assert.NoError(err) + + expectedAssetStatResponse := horizon.AssetStat{ + Amount: "0.0000001", + NumAccounts: usdAssetStat.NumAccounts, + Asset: base.Asset{ + Type: "credit_alphanum4", + Code: usdAssetStat.AssetCode, + Issuer: usdAssetStat.AssetIssuer, + }, + PT: usdAssetStat.PagingToken(), + } + + tt.Assert.Len(results, 1) + assetStat := results[0].(horizon.AssetStat) + tt.Assert.Equal(assetStat, expectedAssetStatResponse) +} diff --git a/services/horizon/internal/actions/helpers.go b/services/horizon/internal/actions/helpers.go index 0fc16d6b54..295ec506ea 100644 --- a/services/horizon/internal/actions/helpers.go +++ b/services/horizon/internal/actions/helpers.go @@ -6,12 +6,13 @@ import ( "mime" "net/http" "net/url" - "regexp" + "reflect" "strconv" - "strings" "unicode/utf8" + "github.com/asaskevich/govalidator" "github.com/go-chi/chi" + "github.com/gorilla/schema" "github.com/stellar/go/amount" "github.com/stellar/go/services/horizon/internal/assets" @@ -49,8 +50,6 @@ const ( maxAssetCodeLength = 12 ) -var validAssetCode = regexp.MustCompile("^[[:alnum:]]{1,12}$") - // GetCursor retrieves a string from either the URLParams, form or query string. // This method uses the priority (URLParams, Form, Query). func (base *Base) GetCursor(name string) (cursor string) { @@ -176,24 +175,34 @@ func (base *Base) GetString(name string) (result string) { } // GetInt64 retrieves an int64 from the action parameter of the given name. -// Populates err if the value is not a valid int64 -func (base *Base) GetInt64(name string) int64 { - if base.Err != nil { - return 0 +func GetInt64(r *http.Request, name string) (int64, error) { + asStr, err := GetString(r, name) + if err != nil { + return 0, err } - - asStr := base.GetString(name) if asStr == "" { - return 0 + return 0, nil } asI64, err := strconv.ParseInt(asStr, 10, 64) if err != nil { - base.SetInvalidField(name, errors.New("unparseable value")) + return 0, problem.MakeInvalidFieldProblem(name, errors.New("unparseable value")) + } + + return asI64, nil +} + +// GetInt64 retrieves an int64 from the action parameter of the given name. +// Populates err if the value is not a valid int64 +func (base *Base) GetInt64(name string) int64 { + if base.Err != nil { return 0 } - return asI64 + var parsed int64 + parsed, base.Err = GetInt64(base.R, name) + + return parsed } // GetInt32 retrieves an int32 from the action parameter of the given name. @@ -375,8 +384,8 @@ func GetAccountID(r *http.Request, name string) (xdr.AccountId, error) { return xdr.AccountId{}, err } - result := xdr.AccountId{} - if err := result.SetAddress(value); err != nil { + result, err := xdr.AddressToAccountId(value) + if err != nil { return result, problem.MakeInvalidFieldProblem( name, errors.New("invalid address"), @@ -530,56 +539,13 @@ func GetAssets(r *http.Request, name string) ([]xdr.Asset, error) { return nil, err } - var assets []xdr.Asset - if s == "" { - return assets, nil - } - - assetStrings := strings.Split(s, ",") - for _, assetString := range assetStrings { - var asset xdr.Asset - - // Technically https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0011.md allows - // any string up to 12 characters not containing an unescaped colon to represent XLM - // however, this function only accepts the string "native" to represent XLM - if strings.ToLower(assetString) == "native" { - if err := asset.SetNative(); err != nil { - return nil, err - } - } else { - parts := strings.Split(assetString, ":") - if len(parts) != 2 { - return nil, problem.MakeInvalidFieldProblem( - name, - fmt.Errorf("%s is not a valid asset", assetString), - ) - } - - code := parts[0] - if !validAssetCode.MatchString(code) { - return nil, problem.MakeInvalidFieldProblem( - name, - fmt.Errorf("%s is not a valid asset, it contains an invalid asset code", assetString), - ) - } - - issuer := xdr.AccountId{} - if err := issuer.SetAddress(parts[1]); err != nil { - return nil, problem.MakeInvalidFieldProblem( - name, - fmt.Errorf("%s is not a valid asset, it contains an invalid issuer", assetString), - ) - } - - if err := asset.SetCredit(string(code), issuer); err != nil { - return nil, problem.MakeInvalidFieldProblem( - name, - fmt.Errorf("%s is not a valid asset", assetString), - ) - } - } + assets, err := xdr.BuildAssets(s) - assets = append(assets, asset) + if err != nil { + return nil, problem.MakeInvalidFieldProblem( + name, + err, + ) } return assets, nil @@ -701,3 +667,160 @@ func FullURL(ctx context.Context) *url.URL { } return url } + +// Note from chi: it is a good idea to set a Decoder instance as a package +// global, because it caches meta-data about structs, and an instance can be +// shared safely: +var decoder = schema.NewDecoder() + +// GetParams fills a struct with values read from a request's query parameters. +func GetParams(dst interface{}, r *http.Request) error { + query := r.URL.Query() + + // Merge chi's URLParams with URL Query Params. Given + // `/accounts/{account_id}/transactions?foo=bar`, chi's URLParams will + // contain `account_id` and URL Query params will contain `foo`. + if rctx := chi.RouteContext(r.Context()); rctx != nil { + for _, key := range rctx.URLParams.Keys { + val := query.Get(key) + if len(val) > 0 { + return problem.MakeInvalidFieldProblem( + key, + errors.New("The parameter should not be included in the request"), + ) + } + + query.Set(key, rctx.URLParam(key)) + } + } + + decoder.IgnoreUnknownKeys(true) + if err := decoder.Decode(dst, query); err != nil { + return errors.Wrap(err, "error decoding Request query") + } + + if _, err := govalidator.ValidateStruct(dst); err != nil { + field, message := getErrorFieldMessage(err) + err = problem.MakeInvalidFieldProblem( + getSchemaTag(dst, field), + errors.New(message), + ) + + return err + } + + if v, ok := dst.(Validateable); ok { + if err := v.Validate(); err != nil { + return err + } + } + + return nil +} + +func getSchemaTag(params interface{}, field string) string { + v := reflect.ValueOf(params).Elem() + qt := v.Type() + f, _ := qt.FieldByName(field) + return f.Tag.Get("schema") +} + +// GetURIParams returns a list of query parameters for a given query struct +func GetURIParams(query interface{}, paginated bool) []string { + params := getSchemaTags(reflect.ValueOf(query).Elem()) + if paginated { + pagingParams := []string{ + "cursor", + "limit", + "order", + } + params = append(params, pagingParams...) + } + return params +} + +func getSchemaTags(v reflect.Value) []string { + qt := v.Type() + fields := make([]string, 0, v.NumField()) + + for i := 0; i < qt.NumField(); i++ { + f := qt.Field(i) + // Query structs can have embedded query structs + if f.Type.Kind() == reflect.Struct { + fields = append(fields, getSchemaTags(v.Field(i))...) + } else { + tag, ok := f.Tag.Lookup("schema") + if ok { + fields = append(fields, tag) + } + } + } + + return fields +} + +// ValidateAssetParams runs multiple checks on an asset query parameter +func ValidateAssetParams(aType, code, issuer, prefix string) error { + // If asset type is not present but code or issuer are, then there is a + // missing parameter and the request is unprocessable. + if len(aType) == 0 { + if len(code) > 0 || len(issuer) > 0 { + return problem.MakeInvalidFieldProblem( + prefix+"asset_type", + errors.New("Missing parameter"), + ) + } + + return nil + } + + t, err := assets.Parse(aType) + if err != nil { + return problem.MakeInvalidFieldProblem( + prefix+"asset_type", + err, + ) + } + + var validLen int + switch t { + case xdr.AssetTypeAssetTypeNative: + // If asset type is native, issuer or code should not be included in the + // request + switch { + case len(code) > 0: + return problem.MakeInvalidFieldProblem( + prefix+"asset_code", + errors.New("native asset does not have a code"), + ) + case len(issuer) > 0: + return problem.MakeInvalidFieldProblem( + prefix+"asset_issuer", + errors.New("native asset does not have an issuer"), + ) + } + + return nil + case xdr.AssetTypeAssetTypeCreditAlphanum4: + validLen = len(xdr.AssetAlphaNum4{}.AssetCode) + case xdr.AssetTypeAssetTypeCreditAlphanum12: + validLen = len(xdr.AssetAlphaNum12{}.AssetCode) + } + + codeLen := len(code) + if codeLen == 0 || codeLen > validLen { + return problem.MakeInvalidFieldProblem( + prefix+"asset_code", + errors.New("Asset code must be 1-12 alphanumeric characters"), + ) + } + + if len(issuer) == 0 { + return problem.MakeInvalidFieldProblem( + prefix+"asset_issuer", + errors.New("Missing parameter"), + ) + } + + return nil +} diff --git a/services/horizon/internal/actions/helpers_test.go b/services/horizon/internal/actions/helpers_test.go index 81edd4f15e..d9beffde95 100644 --- a/services/horizon/internal/actions/helpers_test.go +++ b/services/horizon/internal/actions/helpers_test.go @@ -15,6 +15,8 @@ import ( "github.com/stellar/go/services/horizon/internal/ledger" "github.com/stellar/go/services/horizon/internal/test" "github.com/stellar/go/services/horizon/internal/toid" + "github.com/stellar/go/support/db" + "github.com/stellar/go/support/errors" "github.com/stellar/go/support/render/problem" "github.com/stellar/go/xdr" ) @@ -569,6 +571,108 @@ func TestFullURL(t *testing.T) { tt.Assert.Equal("http:///foo-bar/blah?limit=2&cursor=123456", url.String()) } +func TestGetParams(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + + type QueryParams struct { + SellingBuyingAssetQueryParams `valid:"-"` + Account string `schema:"account_id" valid:"accountID"` + } + + account := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" + usd := xdr.MustNewCreditAsset("USD", account) + + // Simulate chi's URL params. The following would be equivalent to having a + // chi route like the following `/accounts/{account_id}` + urlParams := map[string]string{ + "account_id": account, + "selling_asset_type": "credit_alphanum4", + "selling_asset_code": "USD", + "selling_asset_issuer": account, + } + + r := makeAction("/transactions?limit=2&cursor=123456&order=desc", urlParams).R + qp := QueryParams{} + err := GetParams(&qp, r) + + tt.Assert.NoError(err) + tt.Assert.Equal(account, qp.Account) + tt.Assert.True(usd.Equals(*qp.Selling())) + + urlParams = map[string]string{ + "account_id": account, + "selling_asset_type": "native", + } + + r = makeAction("/transactions?limit=2&cursor=123456&order=desc", urlParams).R + qp = QueryParams{} + err = GetParams(&qp, r) + + tt.Assert.NoError(err) + native := xdr.MustNewNativeAsset() + tt.Assert.True(native.Equals(*qp.Selling())) + + urlParams = map[string]string{"account_id": "1"} + r = makeAction("/transactions?limit=2&cursor=123456&order=desc", urlParams).R + qp = QueryParams{} + err = GetParams(&qp, r) + + if tt.Assert.IsType(&problem.P{}, err) { + p := err.(*problem.P) + tt.Assert.Equal("bad_request", p.Type) + tt.Assert.Equal("account_id", p.Extras["invalid_field"]) + tt.Assert.Equal( + "Account ID must start with `G` and contain 56 alphanum characters", + p.Extras["reason"], + ) + } + + urlParams = map[string]string{ + "account_id": account, + } + r = makeAction(fmt.Sprintf("/transactions?account_id=%s", account), urlParams).R + err = GetParams(&qp, r) + + tt.Assert.Error(err) + if tt.Assert.IsType(&problem.P{}, err) { + p := err.(*problem.P) + tt.Assert.Equal("bad_request", p.Type) + tt.Assert.Equal("account_id", p.Extras["invalid_field"]) + tt.Assert.Equal( + "The parameter should not be included in the request", + p.Extras["reason"], + ) + } +} + +type ParamsValidator struct { + Account string `schema:"account_id" valid:"required"` +} + +func (pv ParamsValidator) Validate() error { + return problem.MakeInvalidFieldProblem( + "Name", + errors.New("Invalid"), + ) +} + +func TestGetParamsCustomValidator(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + + urlParams := map[string]string{"account_id": "1"} + r := makeAction("/transactions", urlParams).R + qp := ParamsValidator{} + err := GetParams(&qp, r) + + if tt.Assert.IsType(&problem.P{}, err) { + p := err.(*problem.P) + tt.Assert.Equal("bad_request", p.Type) + tt.Assert.Equal("Name", p.Extras["invalid_field"]) + } +} + func makeTestAction() *Base { return makeAction("/foo-bar/blah?limit=2&cursor=123456", testURLParams()) } @@ -615,3 +719,53 @@ func testURLParams() map[string]string { "long_12_asset_issuer": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", } } + +func makeRequest( + t *testing.T, + queryParams map[string]string, + routeParams map[string]string, + session *db.Session, +) *http.Request { + request, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + query := url.Values{} + for key, value := range queryParams { + query.Set(key, value) + } + request.URL.RawQuery = query.Encode() + + chiRouteContext := chi.NewRouteContext() + for key, value := range routeParams { + chiRouteContext.URLParams.Add(key, value) + } + ctx := context.WithValue( + context.WithValue(context.Background(), chi.RouteCtxKey, chiRouteContext), + &horizonContext.SessionContextKey, + session, + ) + + return request.WithContext(ctx) +} + +func TestGetURIParams(t *testing.T) { + tt := assert.New(t) + type QueryParams struct { + SellingBuyingAssetQueryParams `valid:"-"` + Account string `schema:"account_id" valid:"accountID"` + } + + expected := []string{ + "selling_asset_type", + "selling_asset_issuer", + "selling_asset_code", + "buying_asset_type", + "buying_asset_issuer", + "buying_asset_code", + "account_id", + } + + qp := QueryParams{} + tt.Equal(expected, GetURIParams(&qp, false)) +} diff --git a/services/horizon/internal/actions/offer.go b/services/horizon/internal/actions/offer.go index 5823670ce6..7b30efce58 100644 --- a/services/horizon/internal/actions/offer.go +++ b/services/horizon/internal/actions/offer.go @@ -3,51 +3,106 @@ package actions import ( "context" "net/http" + "strings" "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/resourceadapter" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/render/hal" - "github.com/stellar/go/xdr" ) -// GetOffersHandler is the action handler for the /offers endpoint -type GetOffersHandler struct { - HistoryQ *history.Q +// GetOfferByID is the action handler for the /offers/{id} endpoint +type GetOfferByID struct { } -// GetResourcePage returns a page of offers. -func (handler GetOffersHandler) GetResourcePage(r *http.Request) ([]hal.Pageable, error) { +// GetResource returns an offer by id. +func (handler GetOfferByID) GetResource( + w HeaderWriter, + r *http.Request, +) (hal.Pageable, error) { ctx := r.Context() - pq, err := GetPageQuery(r) + offerID, err := GetInt64(r, "id") if err != nil { return nil, err } - seller, err := GetString(r, "seller") + historyQ, err := historyQFromRequest(r) if err != nil { return nil, err } - var selling *xdr.Asset - if sellingAsset, found := MaybeGetAsset(r, "selling_"); found { - selling = &sellingAsset + record, err := historyQ.GetOfferByID(offerID) + if err != nil { + return nil, err } - var buying *xdr.Asset - if buyingAsset, found := MaybeGetAsset(r, "buying_"); found { - buying = &buyingAsset + ledger := new(history.Ledger) + err = historyQ.LedgerBySequence( + ledger, + int32(record.LastModifiedLedger), + ) + if historyQ.NoRows(err) { + ledger = nil + } else if err != nil { + return nil, err + } + + var offerResponse horizon.Offer + resourceadapter.PopulateHistoryOffer(ctx, &offerResponse, record, ledger) + return offerResponse, nil +} + +// OffersQuery query struct for offers end-point +type OffersQuery struct { + SellingBuyingAssetQueryParams `valid:"-"` + Seller string `schema:"seller" valid:"accountID,optional"` +} + +// URITemplate returns a rfc6570 URI template the query struct +func (q OffersQuery) URITemplate() string { + return "/offers{?" + strings.Join(GetURIParams(&q, true), ",") + "}" +} + +// Validate runs custom validations. +func (q OffersQuery) Validate() error { + return q.SellingBuyingAssetQueryParams.Validate() +} + +// GetOffersHandler is the action handler for the /offers endpoint +type GetOffersHandler struct { +} + +// GetResourcePage returns a page of offers. +func (handler GetOffersHandler) GetResourcePage( + w HeaderWriter, + r *http.Request, +) ([]hal.Pageable, error) { + ctx := r.Context() + qp := OffersQuery{} + err := GetParams(&qp, r) + if err != nil { + return nil, err + } + + pq, err := GetPageQuery(r) + if err != nil { + return nil, err } query := history.OffersQuery{ PageQuery: pq, - SellerID: seller, - Selling: selling, - Buying: buying, + SellerID: qp.Seller, + Selling: qp.Selling(), + Buying: qp.Buying(), + } + + historyQ, err := historyQFromRequest(r) + if err != nil { + return nil, err } - offers, err := getOffersPage(ctx, handler.HistoryQ, query) + offers, err := getOffersPage(ctx, historyQ, query) if err != nil { return nil, err } @@ -58,7 +113,6 @@ func (handler GetOffersHandler) GetResourcePage(r *http.Request) ([]hal.Pageable // GetAccountOffersHandler is the action handler for the // `/accounts/{account_id}/offers` endpoint when using experimental ingestion. type GetAccountOffersHandler struct { - HistoryQ *history.Q } func (handler GetAccountOffersHandler) parseOffersQuery(r *http.Request) (history.OffersQuery, error) { @@ -81,14 +135,22 @@ func (handler GetAccountOffersHandler) parseOffersQuery(r *http.Request) (histor } // GetResourcePage returns a page of offers for a given account. -func (handler GetAccountOffersHandler) GetResourcePage(r *http.Request) ([]hal.Pageable, error) { +func (handler GetAccountOffersHandler) GetResourcePage( + w HeaderWriter, + r *http.Request, +) ([]hal.Pageable, error) { ctx := r.Context() query, err := handler.parseOffersQuery(r) if err != nil { return nil, err } - offers, err := getOffersPage(ctx, handler.HistoryQ, query) + historyQ, err := historyQFromRequest(r) + if err != nil { + return nil, err + } + + offers, err := getOffersPage(ctx, historyQ, query) if err != nil { return nil, err } diff --git a/services/horizon/internal/actions/offer_test.go b/services/horizon/internal/actions/offer_test.go index 5a74f7cdc2..533a217ea9 100644 --- a/services/horizon/internal/actions/offer_test.go +++ b/services/horizon/internal/actions/offer_test.go @@ -1,20 +1,21 @@ package actions import ( - "context" + "database/sql" "net/http" - "net/url" + "net/http/httptest" "testing" "time" - "github.com/go-chi/chi" "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/db2/core" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/ingest" "github.com/stellar/go/services/horizon/internal/test" "github.com/stellar/go/support/render/hal" + "github.com/stellar/go/support/render/problem" "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" ) var ( @@ -63,48 +64,114 @@ var ( } ) -func makeOffersRequest(t *testing.T, queryParams map[string]string) *http.Request { - request, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatalf("unexpected error %v", err) - } - query := url.Values{} - for key, value := range queryParams { - query.Set(key, value) - } - request.URL.RawQuery = query.Encode() +func TestGetOfferByIDHandler(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) - ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chi.NewRouteContext()) - return request.WithContext(ctx) -} + q := &history.Q{tt.HorizonSession()} + handler := GetOfferByID{} + ingestion := ingest.Ingestion{DB: tt.HorizonSession()} -func makeAccountOffersRequest( - t *testing.T, - accountID string, - queryParams map[string]string, -) *http.Request { - request, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatalf("unexpected error %v", err) - } - query := url.Values{} - for key, value := range queryParams { - query.Set(key, value) - } - request.URL.RawQuery = query.Encode() + ledgerCloseTime := time.Now().Unix() + tt.Assert.NoError(ingestion.Start()) + ingestion.Ledger( + 1, + &core.LedgerHeader{Sequence: 3, CloseTime: ledgerCloseTime}, + 0, + 0, + 0, + ) + tt.Assert.NoError(ingestion.Flush()) + tt.Assert.NoError(ingestion.Close()) - chiRouteContext := chi.NewRouteContext() - chiRouteContext.URLParams.Add("account_id", accountID) - ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiRouteContext) - return request.WithContext(ctx) -} + _, err := q.InsertOffer(eurOffer, 3) + tt.Assert.NoError(err) + _, err = q.InsertOffer(usdOffer, 4) + tt.Assert.NoError(err) -func pageableToOffers(t *testing.T, page []hal.Pageable) []horizon.Offer { - var offers []horizon.Offer - for _, entry := range page { - offers = append(offers, entry.(horizon.Offer)) + for _, testCase := range []struct { + name string + request *http.Request + expectedError func(error) + expectedOffer func(hal.Pageable) + }{ + { + "offer id is invalid", + makeRequest( + t, map[string]string{}, map[string]string{"id": "invalid"}, q.Session, + ), + func(err error) { + tt.Assert.Error(err) + p := err.(*problem.P) + tt.Assert.Equal("bad_request", p.Type) + tt.Assert.Equal("id", p.Extras["invalid_field"]) + }, + func(response hal.Pageable) { + tt.Assert.Nil(response) + }, + }, + { + "offer does not exist", + makeRequest( + t, map[string]string{}, map[string]string{"id": "1234567"}, q.Session, + ), + func(err error) { + tt.Assert.Equal(err, sql.ErrNoRows) + }, + func(response hal.Pageable) { + tt.Assert.Nil(response) + }, + }, + { + "offer with ledger close time", + makeRequest( + t, map[string]string{}, map[string]string{"id": "4"}, q.Session, + ), + func(err error) { + tt.Assert.NoError(err) + }, + func(response hal.Pageable) { + offer := response.(horizon.Offer) + tt.Assert.Equal(int64(eurOffer.OfferId), offer.ID) + tt.Assert.Equal("native", offer.Selling.Type) + tt.Assert.Equal("credit_alphanum4", offer.Buying.Type) + tt.Assert.Equal("EUR", offer.Buying.Code) + tt.Assert.Equal(issuer.Address(), offer.Seller) + tt.Assert.Equal(issuer.Address(), offer.Buying.Issuer) + tt.Assert.Equal(int32(3), offer.LastModifiedLedger) + tt.Assert.Equal(ledgerCloseTime, offer.LastModifiedTime.Unix()) + }, + }, + { + "offer without ledger close time", + makeRequest( + t, map[string]string{}, map[string]string{"id": "6"}, q.Session, + ), + func(err error) { + tt.Assert.NoError(err) + }, + func(response hal.Pageable) { + offer := response.(horizon.Offer) + tt.Assert.Equal(int64(usdOffer.OfferId), offer.ID) + tt.Assert.Equal("credit_alphanum4", offer.Selling.Type) + tt.Assert.Equal("EUR", offer.Selling.Code) + tt.Assert.Equal("credit_alphanum4", offer.Buying.Type) + tt.Assert.Equal("USD", offer.Buying.Code) + tt.Assert.Equal(issuer.Address(), offer.Seller) + tt.Assert.Equal(issuer.Address(), offer.Selling.Issuer) + tt.Assert.Equal(issuer.Address(), offer.Buying.Issuer) + tt.Assert.Equal(int32(4), offer.LastModifiedLedger) + tt.Assert.Nil(offer.LastModifiedTime) + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + offer, err := handler.GetResource(httptest.NewRecorder(), testCase.request) + testCase.expectedError(err) + testCase.expectedOffer(offer) + }) } - return offers } func TestGetOffersHandler(t *testing.T) { @@ -113,7 +180,7 @@ func TestGetOffersHandler(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &history.Q{tt.HorizonSession()} - handler := GetOffersHandler{HistoryQ: q} + handler := GetOffersHandler{} ingestion := ingest.Ingestion{DB: tt.HorizonSession()} ledgerCloseTime := time.Now().Unix() @@ -136,7 +203,12 @@ func TestGetOffersHandler(t *testing.T) { tt.Assert.NoError(err) t.Run("No filter", func(t *testing.T) { - records, err := handler.GetResourcePage(makeOffersRequest(t, map[string]string{})) + records, err := handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, map[string]string{}, map[string]string{}, q.Session, + ), + ) tt.Assert.NoError(err) tt.Assert.Len(records, 3) @@ -152,12 +224,17 @@ func TestGetOffersHandler(t *testing.T) { }) t.Run("Filter by seller", func(t *testing.T) { - records, err := handler.GetResourcePage(makeOffersRequest( - t, - map[string]string{ - "seller": issuer.Address(), - }, - )) + records, err := handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{ + "seller": issuer.Address(), + }, + map[string]string{}, + q.Session, + ), + ) tt.Assert.NoError(err) tt.Assert.Len(records, 2) @@ -165,18 +242,43 @@ func TestGetOffersHandler(t *testing.T) { for _, offer := range offers { tt.Assert.Equal(issuer.Address(), offer.Seller) } + + _, err = handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{ + "seller": "GCXEWJ6U4KPGTNTBY5HX4WQ2EEVPWV2QKXEYIQ32IDYIX", + }, + map[string]string{}, + q.Session, + ), + ) + tt.Assert.Error(err) + tt.Assert.IsType(&problem.P{}, err) + p := err.(*problem.P) + tt.Assert.Equal("bad_request", p.Type) + tt.Assert.Equal("seller", p.Extras["invalid_field"]) + tt.Assert.Equal( + "Account ID must start with `G` and contain 56 alphanum characters", + p.Extras["reason"], + ) }) t.Run("Filter by selling asset", func(t *testing.T) { asset := horizon.Asset{} nativeAsset.Extract(&asset.Type, &asset.Code, &asset.Issuer) - - records, err := handler.GetResourcePage(makeOffersRequest( - t, - map[string]string{ - "selling_asset_type": asset.Type, - }, - )) + records, err := handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{ + "selling_asset_type": asset.Type, + }, + map[string]string{}, + q.Session, + ), + ) tt.Assert.NoError(err) tt.Assert.Len(records, 2) @@ -188,14 +290,19 @@ func TestGetOffersHandler(t *testing.T) { asset = horizon.Asset{} eurAsset.Extract(&asset.Type, &asset.Code, &asset.Issuer) - records, err = handler.GetResourcePage(makeOffersRequest( - t, - map[string]string{ - "selling_asset_type": asset.Type, - "selling_asset_code": asset.Code, - "selling_asset_issuer": asset.Issuer, - }, - )) + records, err = handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{ + "selling_asset_type": asset.Type, + "selling_asset_code": asset.Code, + "selling_asset_issuer": asset.Issuer, + }, + map[string]string{}, + q.Session, + ), + ) tt.Assert.NoError(err) tt.Assert.Len(records, 1) @@ -207,14 +314,19 @@ func TestGetOffersHandler(t *testing.T) { asset := horizon.Asset{} eurAsset.Extract(&asset.Type, &asset.Code, &asset.Issuer) - records, err := handler.GetResourcePage(makeOffersRequest( - t, - map[string]string{ - "buying_asset_type": asset.Type, - "buying_asset_code": asset.Code, - "buying_asset_issuer": asset.Issuer, - }, - )) + records, err := handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{ + "buying_asset_type": asset.Type, + "buying_asset_code": asset.Code, + "buying_asset_issuer": asset.Issuer, + }, + map[string]string{}, + q.Session, + ), + ) tt.Assert.NoError(err) tt.Assert.Len(records, 2) @@ -226,14 +338,19 @@ func TestGetOffersHandler(t *testing.T) { asset = horizon.Asset{} usdAsset.Extract(&asset.Type, &asset.Code, &asset.Issuer) - records, err = handler.GetResourcePage(makeOffersRequest( - t, - map[string]string{ - "buying_asset_type": asset.Type, - "buying_asset_code": asset.Code, - "buying_asset_issuer": asset.Issuer, - }, - )) + records, err = handler.GetResourcePage( + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{ + "buying_asset_type": asset.Type, + "buying_asset_code": asset.Code, + "buying_asset_issuer": asset.Issuer, + }, + map[string]string{}, + q.Session, + ), + ) tt.Assert.NoError(err) tt.Assert.Len(records, 1) @@ -250,9 +367,7 @@ func TestGetAccountOffersHandler(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &history.Q{tt.HorizonSession()} - handler := GetAccountOffersHandler{ - HistoryQ: q, - } + handler := GetAccountOffersHandler{} _, err := q.InsertOffer(eurOffer, 3) tt.Assert.NoError(err) @@ -262,7 +377,13 @@ func TestGetAccountOffersHandler(t *testing.T) { tt.Assert.NoError(err) records, err := handler.GetResourcePage( - makeAccountOffersRequest(t, issuer.Address(), map[string]string{}), + httptest.NewRecorder(), + makeRequest( + t, + map[string]string{}, + map[string]string{"account_id": issuer.Address()}, + q.Session, + ), ) tt.Assert.NoError(err) tt.Assert.Len(records, 2) @@ -273,3 +394,18 @@ func TestGetAccountOffersHandler(t *testing.T) { tt.Assert.Equal(issuer.Address(), offer.Seller) } } + +func pageableToOffers(t *testing.T, page []hal.Pageable) []horizon.Offer { + var offers []horizon.Offer + for _, entry := range page { + offers = append(offers, entry.(horizon.Offer)) + } + return offers +} + +func TestOffersQueryURLTemplate(t *testing.T) { + tt := assert.New(t) + expected := "/offers{?selling_asset_type,selling_asset_issuer,selling_asset_code,buying_asset_type,buying_asset_issuer,buying_asset_code,seller,cursor,limit,order}" + offersQuery := OffersQuery{} + tt.Equal(expected, offersQuery.URITemplate()) +} diff --git a/services/horizon/internal/actions/orderbook.go b/services/horizon/internal/actions/orderbook.go new file mode 100644 index 0000000000..245e04caa3 --- /dev/null +++ b/services/horizon/internal/actions/orderbook.go @@ -0,0 +1,173 @@ +package actions + +import ( + "context" + "math/big" + "net/http" + "strconv" + + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/orderbook" + protocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/services/horizon/internal/resourceadapter" + "github.com/stellar/go/support/render/problem" + "github.com/stellar/go/xdr" +) + +// LastLedgerHeaderName is the header which is set on all experimental ingestion endpoints +const LastLedgerHeaderName = "Latest-Ledger" + +// HeaderWriter is an interface for setting HTTP response headers +type HeaderWriter interface { + Header() http.Header +} + +// SetLastLedgerHeader sets the Latest-Ledger header +func SetLastLedgerHeader(w HeaderWriter, lastLedger uint32) { + w.Header().Set(LastLedgerHeaderName, strconv.FormatUint(uint64(lastLedger), 10)) +} + +// StreamableObjectResponse is an interface for objects returned by streamable object endpoints +// A streamable object endpoint is an SSE endpoint which returns a single JSON object response +// instead of a page of items. +type StreamableObjectResponse interface { + Equals(other StreamableObjectResponse) bool +} + +// OrderBookResponse is the response for the /order_book endpoint +// OrderBookResponse implements StreamableObjectResponse +type OrderBookResponse struct { + protocol.OrderBookSummary +} + +func priceLevelsEqual(a, b []protocol.PriceLevel) bool { + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + +// Equals returns true if the OrderBookResponse is equal to `other` +func (o OrderBookResponse) Equals(other StreamableObjectResponse) bool { + otherOrderBook, ok := other.(OrderBookResponse) + if !ok { + return false + } + return otherOrderBook.Selling == o.Selling && + otherOrderBook.Buying == o.Buying && + priceLevelsEqual(otherOrderBook.Bids, o.Bids) && + priceLevelsEqual(otherOrderBook.Asks, o.Asks) +} + +var invalidOrderBook = problem.P{ + Type: "invalid_order_book", + Title: "Invalid Order Book Parameters", + Status: http.StatusBadRequest, + Detail: "The parameters that specify what order book to view are invalid in some way. " + + "Please ensure that your type parameters (selling_asset_type and buying_asset_type) are one the " + + "following valid values: native, credit_alphanum4, credit_alphanum12. Also ensure that you " + + "have specified selling_asset_code and selling_asset_issuer if selling_asset_type is not 'native', as well " + + "as buying_asset_code and buying_asset_issuer if buying_asset_type is not 'native'", +} + +// GetOrderbookHandler is the action handler for the /order_book endpoint +type GetOrderbookHandler struct { + OrderBookGraph *orderbook.OrderBookGraph +} + +func offersToPriceLevels(offers []xdr.OfferEntry, invert bool) ([]protocol.PriceLevel, error) { + result := []protocol.PriceLevel{} + + amountForPrice := map[xdr.Price]*big.Int{} + for _, offer := range offers { + offerAmount := big.NewInt(int64(offer.Amount)) + if amount, ok := amountForPrice[offer.Price]; ok { + amount.Add(amount, offerAmount) + } else { + amountForPrice[offer.Price] = offerAmount + } + } + for _, offer := range offers { + total, ok := amountForPrice[offer.Price] + if !ok { + continue + } + delete(amountForPrice, offer.Price) + + offerPrice := offer.Price + if invert { + offerPrice.Invert() + } + + amountString, err := amount.IntStringToAmount(total.String()) + if err != nil { + return nil, err + } + + result = append(result, protocol.PriceLevel{ + PriceR: protocol.Price{ + N: int32(offerPrice.N), + D: int32(offerPrice.D), + }, + Price: offerPrice.String(), + Amount: amountString, + }) + } + + return result, nil +} + +func (handler GetOrderbookHandler) orderBookSummary( + ctx context.Context, selling, buying xdr.Asset, limit int, +) (protocol.OrderBookSummary, uint32, error) { + response := protocol.OrderBookSummary{} + if err := resourceadapter.PopulateAsset(ctx, &response.Selling, selling); err != nil { + return response, 0, err + } + if err := resourceadapter.PopulateAsset(ctx, &response.Buying, buying); err != nil { + return response, 0, err + } + + var err error + asks, bids, lastLedger := handler.OrderBookGraph.FindAsksAndBids(selling, buying, limit) + if response.Asks, err = offersToPriceLevels(asks, false); err != nil { + return response, 0, err + } + + if response.Bids, err = offersToPriceLevels(bids, true); err != nil { + return response, 0, err + } + + return response, lastLedger, nil +} + +// GetResource implements the /order_book endpoint +func (handler GetOrderbookHandler) GetResource(w HeaderWriter, r *http.Request) (StreamableObjectResponse, error) { + selling, err := GetAsset(r, "selling_") + if err != nil { + return nil, invalidOrderBook + } + buying, err := GetAsset(r, "buying_") + if err != nil { + return nil, invalidOrderBook + } + limit, err := GetLimit(r, "limit", 20, 200) + if err != nil { + return nil, invalidOrderBook + } + + summary, lastLedger, err := handler.orderBookSummary(r.Context(), selling, buying, int(limit)) + if err != nil { + return nil, err + } + + SetLastLedgerHeader(w, lastLedger) + return OrderBookResponse{summary}, nil +} diff --git a/services/horizon/internal/actions/orderbook_test.go b/services/horizon/internal/actions/orderbook_test.go new file mode 100644 index 0000000000..e0262b4628 --- /dev/null +++ b/services/horizon/internal/actions/orderbook_test.go @@ -0,0 +1,665 @@ +package actions + +import ( + "math" + "net/http/httptest" + "strconv" + "testing" + + "github.com/stellar/go/exp/orderbook" + protocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/xdr" +) + +type intObject int + +func (i intObject) Equals(other StreamableObjectResponse) bool { + return i == other.(intObject) +} + +func TestOrderBookResponseEquals(t *testing.T) { + for _, testCase := range []struct { + name string + response protocol.OrderBookSummary + other StreamableObjectResponse + expected bool + }{ + { + "empty orderbook summary", + protocol.OrderBookSummary{}, + OrderBookResponse{}, + true, + }, + { + "types don't match", + protocol.OrderBookSummary{}, + intObject(0), + false, + }, + { + "buying asset doesn't match", + protocol.OrderBookSummary{ + Buying: protocol.Asset{ + Type: "native", + }, + Selling: protocol.Asset{ + Type: "native", + }, + }, + OrderBookResponse{ + protocol.OrderBookSummary{ + Buying: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Selling: protocol.Asset{ + Type: "native", + }, + }, + }, + false, + }, + { + "selling asset doesn't match", + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "native", + }, + Buying: protocol.Asset{ + Type: "native", + }, + }, + OrderBookResponse{ + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + }, + }, + false, + }, + { + "bid lengths don't match", + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Bids: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 1}, + Price: "1.0", + Amount: "123", + }, + }, + }, + OrderBookResponse{ + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Bids: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + }, + }, + }, + false, + }, + { + "ask lengths don't match", + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Asks: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 1}, + Price: "1.0", + Amount: "123", + }, + }, + }, + OrderBookResponse{ + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Asks: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + }, + }, + }, + false, + }, + { + "bids don't match", + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Bids: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 1}, + Price: "1.0", + Amount: "123", + }, + }, + }, + OrderBookResponse{ + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Bids: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: 2, D: 1}, + Price: "2.0", + Amount: "123", + }, + }, + }, + }, + false, + }, + { + "asks don't match", + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Asks: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 1}, + Price: "1.0", + Amount: "123", + }, + }, + }, + OrderBookResponse{ + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Asks: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 1}, + Price: "1.0", + Amount: "12", + }, + }, + }, + }, + false, + }, + { + "orderbook summaries match", + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Asks: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 1}, + Price: "1.0", + Amount: "123", + }, + }, + Bids: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 3}, + Price: "0.33", + Amount: "13", + }, + }, + }, + OrderBookResponse{ + protocol.OrderBookSummary{ + Selling: protocol.Asset{ + Type: "credit_alphanum4", + Code: "USD", + Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", + }, + Buying: protocol.Asset{ + Type: "native", + }, + Bids: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 3}, + Price: "0.33", + Amount: "13", + }, + }, + Asks: []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 2}, + Price: "0.5", + Amount: "123", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: 1, D: 1}, + Price: "1.0", + Amount: "123", + }, + }, + }, + }, + true, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + equals := (OrderBookResponse{testCase.response}).Equals(testCase.other) + if equals != testCase.expected { + t.Fatalf("expected %v but got %v", testCase.expected, equals) + } + }) + } +} + +func TestOrderbookGetResourceValidation(t *testing.T) { + graph := orderbook.NewOrderBookGraph() + handler := GetOrderbookHandler{ + OrderBookGraph: graph, + } + + var eurAssetType, eurAssetCode, eurAssetIssuer string + if err := eurAsset.Extract(&eurAssetType, &eurAssetCode, &eurAssetIssuer); err != nil { + t.Fatalf("cound not extract eur asset: %v", err) + } + var usdAssetType, usdAssetCode, usdAssetIssuer string + if err := eurAsset.Extract(&usdAssetType, &usdAssetCode, &usdAssetIssuer); err != nil { + t.Fatalf("cound not extract usd asset: %v", err) + } + + for _, testCase := range []struct { + name string + queryParams map[string]string + }{ + { + "missing all params", + map[string]string{}, + }, + { + "missing buying asset", + map[string]string{ + "selling_asset_type": eurAssetType, + "selling_asset_code": eurAssetCode, + "selling_asset_issuer": eurAssetIssuer, + "limit": "25", + }, + }, + { + "missing selling asset", + map[string]string{ + "buying_asset_type": eurAssetType, + "buying_asset_code": eurAssetCode, + "buying_asset_issuer": eurAssetIssuer, + "limit": "25", + }, + }, + { + "invalid buying asset", + map[string]string{ + "buying_asset_type": "invalid", + "buying_asset_code": eurAssetCode, + "buying_asset_issuer": eurAssetIssuer, + "selling_asset_type": usdAssetType, + "selling_asset_code": usdAssetCode, + "selling_asset_issuer": usdAssetIssuer, + "limit": "25", + }, + }, + { + "invalid selling asset", + map[string]string{ + "buying_asset_type": eurAssetType, + "buying_asset_code": eurAssetCode, + "buying_asset_issuer": eurAssetIssuer, + "selling_asset_type": "invalid", + "selling_asset_code": usdAssetCode, + "selling_asset_issuer": usdAssetIssuer, + "limit": "25", + }, + }, + { + "limit is not a number", + map[string]string{ + "buying_asset_type": eurAssetType, + "buying_asset_code": eurAssetCode, + "buying_asset_issuer": eurAssetIssuer, + "selling_asset_type": usdAssetType, + "selling_asset_code": usdAssetCode, + "selling_asset_issuer": usdAssetIssuer, + "limit": "avcdef", + }, + }, + { + "limit is negative", + map[string]string{ + "buying_asset_type": eurAssetType, + "buying_asset_code": eurAssetCode, + "buying_asset_issuer": eurAssetIssuer, + "selling_asset_type": usdAssetType, + "selling_asset_code": usdAssetCode, + "selling_asset_issuer": usdAssetIssuer, + "limit": "-1", + }, + }, + { + "limit is too high", + map[string]string{ + "buying_asset_type": eurAssetType, + "buying_asset_code": eurAssetCode, + "buying_asset_issuer": eurAssetIssuer, + "selling_asset_type": usdAssetType, + "selling_asset_code": usdAssetCode, + "selling_asset_issuer": usdAssetIssuer, + "limit": "20000", + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + r := makeRequest(t, testCase.queryParams, map[string]string{}, nil) + w := httptest.NewRecorder() + _, err := handler.GetResource(w, r) + if err == nil || err.Error() != invalidOrderBook.Error() { + t.Fatalf("expected error %v but got %v", invalidOrderBook, err) + } + if lastLedger := w.Header().Get(LastLedgerHeaderName); lastLedger != "" { + t.Fatalf("expected last ledger to be not set but got %v", lastLedger) + } + }) + } +} + +func TestOrderbookGetResource(t *testing.T) { + var eurAssetType, eurAssetCode, eurAssetIssuer string + if err := eurAsset.Extract(&eurAssetType, &eurAssetCode, &eurAssetIssuer); err != nil { + t.Fatalf("cound not extract eur asset: %v", err) + } + + empty := OrderBookResponse{ + OrderBookSummary: protocol.OrderBookSummary{ + Bids: []protocol.PriceLevel{}, + Asks: []protocol.PriceLevel{}, + Selling: protocol.Asset{ + Type: "native", + }, + Buying: protocol.Asset{ + Type: eurAssetType, + Code: eurAssetCode, + Issuer: eurAssetIssuer, + }, + }, + } + + asksButNoBidsGraph := orderbook.NewOrderBookGraph() + if err := asksButNoBidsGraph.AddOffer(twoEurOffer).Apply(1); err != nil { + t.Fatalf("unexpected error %v", err) + } + asksButNoBidsResponse := empty + asksButNoBidsResponse.Asks = []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: int32(twoEurOffer.Price.N), D: int32(twoEurOffer.Price.D)}, + Price: "2.0000000", + Amount: "0.0000500", + }, + } + + sellEurOffer := twoEurOffer + sellEurOffer.Buying, sellEurOffer.Selling = sellEurOffer.Selling, sellEurOffer.Buying + sellEurOffer.OfferId = 15 + bidsButNoAsksGraph := orderbook.NewOrderBookGraph() + if err := bidsButNoAsksGraph.AddOffer(sellEurOffer).Apply(2); err != nil { + t.Fatalf("unexpected error %v", err) + } + bidsButNoAsksResponse := empty + bidsButNoAsksResponse.Bids = []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: int32(sellEurOffer.Price.D), D: int32(sellEurOffer.Price.N)}, + Price: "0.5000000", + Amount: "0.0000500", + }, + } + + fullGraph := orderbook.NewOrderBookGraph() + if err := fullGraph.AddOffer(twoEurOffer).Apply(3); err != nil { + t.Fatalf("unexpected error %v", err) + } + otherEurOffer := twoEurOffer + otherEurOffer.Amount = xdr.Int64(math.MaxInt64) + otherEurOffer.OfferId = 16 + if err := fullGraph.AddOffer(otherEurOffer).Apply(4); err != nil { + t.Fatalf("unexpected error %v", err) + } + threeEurOffer := twoEurOffer + threeEurOffer.Price.N = 3 + threeEurOffer.OfferId = 20 + if err := fullGraph.AddOffer(threeEurOffer).Apply(5); err != nil { + t.Fatalf("unexpected error %v", err) + } + + sellEurOffer.Price.N = 9 + sellEurOffer.Price.D = 10 + if err := fullGraph.AddOffer(sellEurOffer).Apply(6); err != nil { + t.Fatalf("unexpected error %v", err) + } + otherSellEurOffer := sellEurOffer + otherSellEurOffer.OfferId = 17 + otherSellEurOffer.Price.N *= 2 + if err := fullGraph.AddOffer(otherSellEurOffer).Apply(7); err != nil { + t.Fatalf("unexpected error %v", err) + } + + fullResponse := empty + fullResponse.Asks = []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: int32(twoEurOffer.Price.N), D: int32(twoEurOffer.Price.D)}, + Price: "2.0000000", + Amount: "922337203685.4776307", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: int32(threeEurOffer.Price.N), D: int32(threeEurOffer.Price.D)}, + Price: "3.0000000", + Amount: "0.0000500", + }, + } + fullResponse.Bids = []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: int32(sellEurOffer.Price.D), D: int32(sellEurOffer.Price.N)}, + Price: "1.1111111", + Amount: "0.0000500", + }, + protocol.PriceLevel{ + PriceR: protocol.Price{N: int32(otherSellEurOffer.Price.D), D: int32(otherSellEurOffer.Price.N)}, + Price: "0.5555556", + Amount: "0.0000500", + }, + } + + limitResponse := empty + limitResponse.Asks = []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: int32(twoEurOffer.Price.N), D: int32(twoEurOffer.Price.D)}, + Price: "2.0000000", + Amount: "922337203685.4776307", + }, + } + limitResponse.Bids = []protocol.PriceLevel{ + protocol.PriceLevel{ + PriceR: protocol.Price{N: int32(sellEurOffer.Price.D), D: int32(sellEurOffer.Price.N)}, + Price: "1.1111111", + Amount: "0.0000500", + }, + } + + for _, testCase := range []struct { + name string + graph *orderbook.OrderBookGraph + limit int + expected OrderBookResponse + lastLedger string + }{ + { + "empty orderbook", + orderbook.NewOrderBookGraph(), + 10, + empty, + "0", + }, + { + "orderbook with asks but no bids", + asksButNoBidsGraph, + 10, + asksButNoBidsResponse, + "1", + }, + { + "orderbook with bids but no asks", + bidsButNoAsksGraph, + 10, + bidsButNoAsksResponse, + "2", + }, + { + "full orderbook", + fullGraph, + 10, + fullResponse, + "7", + }, + { + "limit request", + fullGraph, + 1, + limitResponse, + "7", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + handler := GetOrderbookHandler{ + OrderBookGraph: testCase.graph, + } + r := makeRequest( + t, + map[string]string{ + "buying_asset_type": eurAssetType, + "buying_asset_code": eurAssetCode, + "buying_asset_issuer": eurAssetIssuer, + "selling_asset_type": "native", + "limit": strconv.Itoa(testCase.limit), + }, + map[string]string{}, + nil, + ) + w := httptest.NewRecorder() + response, err := handler.GetResource(w, r) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if !response.Equals(testCase.expected) { + t.Fatalf("expected %v but got %v", testCase.expected, response) + } + lastLedger := w.Header().Get(LastLedgerHeaderName) + if lastLedger != testCase.lastLedger { + t.Fatalf( + "expected last ledger to be %v but got %v", + testCase.lastLedger, + lastLedger, + ) + } + }) + } +} diff --git a/services/horizon/internal/actions/query_params.go b/services/horizon/internal/actions/query_params.go new file mode 100644 index 0000000000..b7bd1b17d0 --- /dev/null +++ b/services/horizon/internal/actions/query_params.go @@ -0,0 +1,67 @@ +package actions + +import ( + "github.com/stellar/go/xdr" +) + +// SellingBuyingAssetQueryParams query struct for end-points requiring a selling +// and buying asset +type SellingBuyingAssetQueryParams struct { + SellingAssetType string `schema:"selling_asset_type" valid:"assetType,optional"` + SellingAssetIssuer string `schema:"selling_asset_issuer" valid:"accountID,optional"` + SellingAssetCode string `schema:"selling_asset_code" valid:"-"` + BuyingAssetType string `schema:"buying_asset_type" valid:"assetType,optional"` + BuyingAssetIssuer string `schema:"buying_asset_issuer" valid:"accountID,optional"` + BuyingAssetCode string `schema:"buying_asset_code" valid:"-"` +} + +// Validate runs custom validations buying and selling +func (q SellingBuyingAssetQueryParams) Validate() error { + err := ValidateAssetParams(q.SellingAssetType, q.SellingAssetCode, q.SellingAssetIssuer, "selling_") + if err != nil { + return err + } + err = ValidateAssetParams(q.BuyingAssetType, q.BuyingAssetCode, q.BuyingAssetIssuer, "buying_") + if err != nil { + return err + } + return nil +} + +// Selling returns an xdr.Asset representing the selling side of the offer. +func (q SellingBuyingAssetQueryParams) Selling() *xdr.Asset { + if len(q.SellingAssetType) == 0 { + return nil + } + + selling, err := xdr.BuildAsset( + q.SellingAssetType, + q.SellingAssetIssuer, + q.SellingAssetCode, + ) + + if err != nil { + panic(err) + } + + return &selling +} + +// Buying returns an *xdr.Asset representing the buying side of the offer. +func (q SellingBuyingAssetQueryParams) Buying() *xdr.Asset { + if len(q.BuyingAssetType) == 0 { + return nil + } + + buying, err := xdr.BuildAsset( + q.BuyingAssetType, + q.BuyingAssetIssuer, + q.BuyingAssetCode, + ) + + if err != nil { + panic(err) + } + + return &buying +} diff --git a/services/horizon/internal/actions/query_params_test.go b/services/horizon/internal/actions/query_params_test.go new file mode 100644 index 0000000000..c111fe3a76 --- /dev/null +++ b/services/horizon/internal/actions/query_params_test.go @@ -0,0 +1,211 @@ +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/support/render/problem" +) + +func TestSellingBuyingAssetQueryParams(t *testing.T) { + testCases := []struct { + desc string + urlParams map[string]string + expectedInvalidField string + expectedErr string + }{ + { + desc: "Invalid selling_asset_type", + urlParams: map[string]string{ + "selling_asset_type": "invalid", + }, + expectedInvalidField: "selling_asset_type", + expectedErr: "Asset type must be native, credit_alphanum4 or credit_alphanum12", + }, + { + desc: "Invalid buying_asset_type", + urlParams: map[string]string{ + "buying_asset_type": "invalid", + }, + expectedInvalidField: "buying_asset_type", + expectedErr: "Asset type must be native, credit_alphanum4 or credit_alphanum12", + }, { + desc: "Invalid selling_asset_code for credit_alphanum4", + urlParams: map[string]string{ + "selling_asset_type": "credit_alphanum4", + "selling_asset_code": "invalid", + "selling_asset_issuer": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + }, + expectedInvalidField: "selling_asset_code", + expectedErr: "Asset code must be 1-12 alphanumeric characters", + }, { + desc: "Invalid buying_asset_code for credit_alphanum4", + urlParams: map[string]string{ + "buying_asset_type": "credit_alphanum4", + "buying_asset_code": "invalid", + }, + expectedInvalidField: "buying_asset_code", + expectedErr: "Asset code must be 1-12 alphanumeric characters", + }, { + desc: "Empty selling_asset_code for credit_alphanum4", + urlParams: map[string]string{ + "selling_asset_type": "credit_alphanum4", + "selling_asset_code": "", + }, + expectedInvalidField: "selling_asset_code", + expectedErr: "Asset code must be 1-12 alphanumeric characters", + }, { + desc: "Empty buying_asset_code for credit_alphanum4", + urlParams: map[string]string{ + "buying_asset_type": "credit_alphanum4", + "buying_asset_code": "", + }, + expectedInvalidField: "buying_asset_code", + expectedErr: "Asset code must be 1-12 alphanumeric characters", + }, { + desc: "Empty selling_asset_code for credit_alphanum12", + urlParams: map[string]string{ + "selling_asset_type": "credit_alphanum12", + "selling_asset_code": "", + }, + expectedInvalidField: "selling_asset_code", + expectedErr: "Asset code must be 1-12 alphanumeric characters", + }, { + desc: "Empty buying_asset_code for credit_alphanum12", + urlParams: map[string]string{ + "buying_asset_type": "credit_alphanum12", + "buying_asset_code": "", + }, + expectedInvalidField: "buying_asset_code", + expectedErr: "Asset code must be 1-12 alphanumeric characters", + }, { + desc: "Invalid selling_asset_code for credit_alphanum12", + urlParams: map[string]string{ + "selling_asset_type": "credit_alphanum12", + "selling_asset_code": "OHLOOOOOOOOOONG", + }, + expectedInvalidField: "selling_asset_code", + expectedErr: "Asset code must be 1-12 alphanumeric characters", + }, { + desc: "Invalid buying_asset_code for credit_alphanum12", + urlParams: map[string]string{ + "buying_asset_type": "credit_alphanum12", + "buying_asset_code": "OHLOOOOOOOOOONG", + }, + expectedInvalidField: "buying_asset_code", + expectedErr: "Asset code must be 1-12 alphanumeric characters", + }, { + desc: "Invalid selling_asset_issuer", + urlParams: map[string]string{ + "selling_asset_issuer": "GFOOO", + }, + expectedInvalidField: "selling_asset_issuer", + expectedErr: "Account ID must start with `G` and contain 56 alphanum characters", + }, { + desc: "Invalid buying_asset_issuer", + urlParams: map[string]string{ + "buying_asset_issuer": "GFOOO", + }, + expectedInvalidField: "buying_asset_issuer", + expectedErr: "Account ID must start with `G` and contain 56 alphanum characters", + }, { + desc: "Missing selling_asset_type", + urlParams: map[string]string{ + "selling_asset_code": "OHLOOOOOOOOOONG", + "selling_asset_issuer": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + }, + expectedInvalidField: "selling_asset_type", + expectedErr: "Missing parameter", + }, { + desc: "Missing buying_asset_type", + urlParams: map[string]string{ + "buying_asset_code": "OHLOOOOOOOOOONG", + "buying_asset_issuer": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + }, + expectedInvalidField: "buying_asset_type", + expectedErr: "Missing parameter", + }, { + desc: "Missing selling_asset_issuer", + urlParams: map[string]string{ + "selling_asset_type": "credit_alphanum4", + "selling_asset_code": "USD", + }, + expectedInvalidField: "selling_asset_issuer", + expectedErr: "Missing parameter", + }, { + desc: "Missing buying_asset_issuer", + urlParams: map[string]string{ + "buying_asset_type": "credit_alphanum4", + "buying_asset_code": "USD", + }, + expectedInvalidField: "buying_asset_issuer", + expectedErr: "Missing parameter", + }, { + desc: "Native with issued asset info: buying_asset_issuer", + urlParams: map[string]string{ + "buying_asset_type": "native", + "buying_asset_issuer": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + }, + expectedInvalidField: "buying_asset_issuer", + expectedErr: "native asset does not have an issuer", + }, { + desc: "Native with issued asset info: buying_asset_code", + urlParams: map[string]string{ + "buying_asset_type": "native", + "buying_asset_code": "USD", + }, + expectedInvalidField: "buying_asset_code", + expectedErr: "native asset does not have a code", + }, { + desc: "Native with issued asset info: selling_asset_issuer", + urlParams: map[string]string{ + "selling_asset_type": "native", + "selling_asset_issuer": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + }, + expectedInvalidField: "selling_asset_issuer", + expectedErr: "native asset does not have an issuer", + }, { + desc: "Native with issued asset info: selling_asset_code", + urlParams: map[string]string{ + "selling_asset_type": "native", + "selling_asset_code": "USD", + }, + expectedInvalidField: "selling_asset_code", + expectedErr: "native asset does not have a code", + }, { + desc: "Valid parameters", + urlParams: map[string]string{ + "buying_asset_type": "credit_alphanum4", + "buying_asset_code": "USD", + "buying_asset_issuer": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + "selling_asset_type": "credit_alphanum4", + "selling_asset_code": "EUR", + "selling_asset_issuer": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + }, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + tt := assert.New(t) + r := makeAction("/", tc.urlParams).R + qp := SellingBuyingAssetQueryParams{} + err := GetParams(&qp, r) + + if len(tc.expectedInvalidField) == 0 { + tt.NoError(err) + } else { + if tt.IsType(&problem.P{}, err) { + p := err.(*problem.P) + tt.Equal("bad_request", p.Type) + tt.Equal(tc.expectedInvalidField, p.Extras["invalid_field"]) + tt.Equal( + tc.expectedErr, + p.Extras["reason"], + ) + } + } + + }) + } +} diff --git a/services/horizon/internal/actions/session.go b/services/horizon/internal/actions/session.go new file mode 100644 index 0000000000..b9e3297b88 --- /dev/null +++ b/services/horizon/internal/actions/session.go @@ -0,0 +1,19 @@ +package actions + +import ( + "net/http" + + horizonContext "github.com/stellar/go/services/horizon/internal/context" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/support/db" + "github.com/stellar/go/support/errors" +) + +func historyQFromRequest(request *http.Request) (*history.Q, error) { + ctx := request.Context() + session, ok := ctx.Value(&horizonContext.SessionContextKey).(*db.Session) + if !ok { + return nil, errors.New("missing session in request context") + } + return &history.Q{session}, nil +} diff --git a/services/horizon/internal/actions/validators.go b/services/horizon/internal/actions/validators.go new file mode 100644 index 0000000000..b5c2891f07 --- /dev/null +++ b/services/horizon/internal/actions/validators.go @@ -0,0 +1,116 @@ +package actions + +import ( + "strings" + + "github.com/asaskevich/govalidator" + + "github.com/stellar/go/amount" + "github.com/stellar/go/services/horizon/internal/assets" + "github.com/stellar/go/xdr" +) + +// Validateable allow structs to define their own custom validations. +type Validateable interface { + Validate() error +} + +func init() { + govalidator.TagMap["accountID"] = govalidator.Validator(isAccountID) + govalidator.TagMap["amount"] = govalidator.Validator(isAmount) + govalidator.TagMap["assetType"] = govalidator.Validator(isAssetType) + govalidator.TagMap["asset"] = govalidator.Validator(isAsset) +} + +var customTagsErrorMessages = map[string]string{ + "accountID": "Account ID must start with `G` and contain 56 alphanum characters", + "amount": "Amount must be positive", + "asset": "Asset must be the string \"native\" or a string of the form \"Code:IssuerAccountID\" for issued assets.", + "assetType": "Asset type must be native, credit_alphanum4 or credit_alphanum12", +} + +// isAsset validates if string contains a valid SEP11 asset +func isAsset(assetString string) bool { + var asset xdr.Asset + + if strings.ToLower(assetString) == "native" { + if err := asset.SetNative(); err != nil { + return false + } + } else { + + parts := strings.Split(assetString, ":") + if len(parts) != 2 { + return false + } + + code := parts[0] + if !xdr.ValidAssetCode.MatchString(code) { + return false + } + + issuer, err := xdr.AddressToAccountId(parts[1]) + if err != nil { + return false + } + + if err := asset.SetCredit(code, issuer); err != nil { + return false + } + } + + return true +} + +func getErrorFieldMessage(err error) (string, string) { + var field, message string + + switch err := err.(type) { + case govalidator.Error: + field = err.Name + validator := err.Validator + m, ok := customTagsErrorMessages[validator] + // Give priority to inline custom error messages. + // CustomErrorMessageExists when the validator is defined like: + // `validatorName~custom message` + if !ok || err.CustomErrorMessageExists { + m = err.Err.Error() + } + message = m + case govalidator.Errors: + for _, item := range err.Errors() { + field, message = getErrorFieldMessage(item) + break + } + } + + return field, message +} + +func isAssetType(str string) bool { + if _, err := assets.Parse(str); err != nil { + return false + } + + return true +} + +func isAccountID(str string) bool { + if _, err := xdr.AddressToAccountId(str); err != nil { + return false + } + + return true +} + +func isAmount(str string) bool { + parsed, err := amount.Parse(str) + switch { + case err != nil: + return false + case parsed <= 0: + return false + } + + return true +} diff --git a/services/horizon/internal/actions/validators_test.go b/services/horizon/internal/actions/validators_test.go new file mode 100644 index 0000000000..1870af0606 --- /dev/null +++ b/services/horizon/internal/actions/validators_test.go @@ -0,0 +1,213 @@ +package actions + +import ( + "testing" + + "github.com/asaskevich/govalidator" + "github.com/stretchr/testify/assert" +) + +func TestAssetTypeValidator(t *testing.T) { + type Query struct { + AssetType string `valid:"assetType,optional"` + } + + for _, testCase := range []struct { + assetType string + valid bool + }{ + { + "native", + true, + }, + { + "credit_alphanum4", + true, + }, + { + "credit_alphanum12", + true, + }, + { + "", + true, + }, + { + "stellar_asset_type", + false, + }, + } { + t.Run(testCase.assetType, func(t *testing.T) { + tt := assert.New(t) + + q := Query{ + AssetType: testCase.assetType, + } + + result, err := govalidator.ValidateStruct(q) + if testCase.valid { + tt.NoError(err) + tt.True(result) + } else { + tt.Equal("AssetType: stellar_asset_type does not validate as assetType", err.Error()) + } + }) + } +} + +func TestAccountIDValidator(t *testing.T) { + type Query struct { + Account string `valid:"accountID,optional"` + } + + for _, testCase := range []struct { + name string + value string + expectedError string + }{ + { + "invalid stellar address", + "FON4WOTCFSASG3J6SGLLQZURDDUVNBQANAHEQJ3PBNDZ74X63UZWQPZW", + "Account: FON4WOTCFSASG3J6SGLLQZURDDUVNBQANAHEQJ3PBNDZ74X63UZWQPZW does not validate as accountID", + }, + { + "valid stellar address", + "GAN4WOTCFSASG3J6SGLLQZURDDUVNBQANAHEQJ3PBNDZ74X63UZWQPZW", + "", + }, + { + "empty stellar address should not be validated", + "", + "", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + tt := assert.New(t) + + q := Query{ + Account: testCase.value, + } + + result, err := govalidator.ValidateStruct(q) + if testCase.expectedError == "" { + tt.NoError(err) + tt.True(result) + } else { + tt.Equal(testCase.expectedError, err.Error()) + } + }) + } +} + +func TestAssetValidator(t *testing.T) { + type Query struct { + Asset string `valid:"asset"` + } + + for _, testCase := range []struct { + desc string + asset string + valid bool + }{ + { + "native", + "native", + true, + }, + { + "credit_alphanum4", + "USD:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + true, + }, + { + "credit_alphanum12", + "SDFUSD:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + true, + }, + { + "invalid credit_alphanum12", + "SDFUSDSDFUSDSDFUSD:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + false, + }, + { + "invalid no issuer", + "FOO", + false, + }, + { + "invalid issuer", + "FOO:BAR", + false, + }, + { + "empty colon", + ":", + false, + }, + } { + t.Run(testCase.desc, func(t *testing.T) { + tt := assert.New(t) + + q := Query{ + Asset: testCase.asset, + } + + result, err := govalidator.ValidateStruct(q) + if testCase.valid { + tt.NoError(err) + tt.True(result) + } else { + tt.Error(err) + } + }) + } +} + +func TestAmountValidator(t *testing.T) { + type Query struct { + Amount string `valid:"amount"` + } + + for _, testCase := range []struct { + name string + value string + expectedError string + }{ + { + "valid", + "10", + "", + }, + { + "zero", + "0", + "Amount: 0 does not validate as amount", + }, + { + "negative", + "-1", + "Amount: -1 does not validate as amount", + }, + { + "non-number", + "one", + "Amount: one does not validate as amount", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + tt := assert.New(t) + + q := Query{ + Amount: testCase.value, + } + + result, err := govalidator.ValidateStruct(q) + if testCase.expectedError == "" { + tt.NoError(err) + tt.True(result) + } else { + tt.Equal(testCase.expectedError, err.Error()) + } + }) + } +} diff --git a/services/horizon/internal/actions_account_test.go b/services/horizon/internal/actions_account_test.go index 7b3a9be157..763fafdbb0 100644 --- a/services/horizon/internal/actions_account_test.go +++ b/services/horizon/internal/actions_account_test.go @@ -5,8 +5,6 @@ import ( "testing" "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/services/horizon/internal/db2/history" - "github.com/stellar/go/services/horizon/internal/render/problem" ) func TestAccountActions_Show(t *testing.T) { @@ -39,18 +37,6 @@ func TestAccountActions_Show(t *testing.T) { ht.Assert.Equal(404, w.Code) } -func TestAccountActionsStillIngesting_Show(t *testing.T) { - ht := StartHTTPTest(t, "base") - ht.App.config.EnableExperimentalIngestion = true - - defer ht.Finish() - q := &history.Q{ht.HorizonSession()} - ht.Assert.NoError(q.UpdateLastLedgerExpIngest(0)) - - w := ht.Get("/accounts?signer=GDBAPLDCAEJV6LSEDFEAUDAVFYSNFRUYZ4X75YYJJMMX5KFVUOHX46SQ") - ht.Assert.Equal(problem.StillIngesting.Status, w.Code) -} - func TestAccountActions_ShowRegressions(t *testing.T) { ht := StartHTTPTest(t, "base") defer ht.Finish() diff --git a/services/horizon/internal/actions_operation_fee_stats_test.go b/services/horizon/internal/actions_operation_fee_stats_test.go index 3ff8b6fd9c..8ef354921d 100644 --- a/services/horizon/internal/actions_operation_fee_stats_test.go +++ b/services/horizon/internal/actions_operation_fee_stats_test.go @@ -68,9 +68,9 @@ func TestOperationFeeTestsActions_Show(t *testing.T) { "100", "200", "400", - "260", // p10 - "320", - "380", + "200", // p10 + "300", + "400", "400", "400", "400", @@ -145,9 +145,9 @@ func TestOperationFeeTestsActions_ShowMultiOp(t *testing.T) { ht.Assert.Equal("100", result["min_accepted_fee"], "min") ht.Assert.Equal("200", result["mode_accepted_fee"], "mode") ht.Assert.Equal("100", result["last_ledger_base_fee"], "base_fee") - ht.Assert.Equal("130", result["p10_accepted_fee"], "p10") - ht.Assert.Equal("160", result["p20_accepted_fee"], "p20") - ht.Assert.Equal("190", result["p30_accepted_fee"], "p30") + ht.Assert.Equal("100", result["p10_accepted_fee"], "p10") + ht.Assert.Equal("150", result["p20_accepted_fee"], "p20") + ht.Assert.Equal("200", result["p30_accepted_fee"], "p30") ht.Assert.Equal("200", result["p40_accepted_fee"], "p40") ht.Assert.Equal("200", result["p50_accepted_fee"], "p50") ht.Assert.Equal("200", result["p60_accepted_fee"], "p60") @@ -159,3 +159,41 @@ func TestOperationFeeTestsActions_ShowMultiOp(t *testing.T) { ht.Assert.Equal("0.06", result["ledger_capacity_usage"], "ledger_capacity_usage") } } + +func TestOperationFeeTestsActions_NotInterpolating(t *testing.T) { + ht := StartHTTPTest(t, "operation_fee_stats_3") + defer ht.Finish() + + // Update max_tx_set_size on ledgers + _, err := ht.HorizonSession().ExecRaw("UPDATE history_ledgers SET max_tx_set_size = 50") + ht.Require.NoError(err) + + // Update one tx to a huge fee + _, err = ht.HorizonSession().ExecRaw("UPDATE history_transactions SET max_fee = 256000, operation_count = 16 WHERE transaction_hash = '6a349e7331e93a251367287e274fb1699abaf723bde37aebe96248c76fd3071a'") + ht.Require.NoError(err) + + ht.App.UpdateOperationFeeStatsState() + + w := ht.Get("/fee_stats") + + if ht.Assert.Equal(200, w.Code) { + var result map[string]string + err := json.Unmarshal(w.Body.Bytes(), &result) + ht.Require.NoError(err) + ht.Assert.Equal("200", result["min_accepted_fee"], "min") + ht.Assert.Equal("400", result["mode_accepted_fee"], "mode") + ht.Assert.Equal("100", result["last_ledger_base_fee"], "base_fee") + ht.Assert.Equal("200", result["p10_accepted_fee"], "p10") + ht.Assert.Equal("300", result["p20_accepted_fee"], "p20") + ht.Assert.Equal("400", result["p30_accepted_fee"], "p30") + ht.Assert.Equal("400", result["p40_accepted_fee"], "p40") + ht.Assert.Equal("400", result["p50_accepted_fee"], "p50") + ht.Assert.Equal("400", result["p60_accepted_fee"], "p60") + ht.Assert.Equal("400", result["p70_accepted_fee"], "p70") + ht.Assert.Equal("400", result["p80_accepted_fee"], "p80") + ht.Assert.Equal("16000", result["p90_accepted_fee"], "p90") + ht.Assert.Equal("16000", result["p95_accepted_fee"], "p95") + ht.Assert.Equal("16000", result["p99_accepted_fee"], "p99") + ht.Assert.Equal("0.09", result["ledger_capacity_usage"], "ledger_capacity_usage") + } +} diff --git a/services/horizon/internal/actions_path.go b/services/horizon/internal/actions_path.go index bf9413ecdc..22a55dbd28 100644 --- a/services/horizon/internal/actions_path.go +++ b/services/horizon/internal/actions_path.go @@ -4,7 +4,9 @@ import ( "context" "fmt" "net/http" + "strings" + "github.com/stellar/go/amount" "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/actions" "github.com/stellar/go/services/horizon/internal/db2/core" @@ -25,11 +27,86 @@ type FindPathsHandler struct { staleThreshold uint maxPathLength uint checkHistoryIsStale bool + setLastLedgerHeader bool maxAssetsParamLength int pathFinder paths.Finder coreQ *core.Q } +// StrictReceivePathsQuery query struct for paths/strict-send end-point +type StrictReceivePathsQuery struct { + SourceAssets string `schema:"source_assets" valid:"-"` + SourceAccount string `schema:"source_account" valid:"accountID,optional"` + DestinationAccount string `schema:"destination_account" valid:"accountID,optional"` + DestinationAssetType string `schema:"destination_asset_type" valid:"assetType"` + DestinationAssetIssuer string `schema:"destination_asset_issuer" valid:"accountID,optional"` + DestinationAssetCode string `schema:"destination_asset_code" valid:"-"` + DestinationAmount string `schema:"destination_amount" valid:"amount"` +} + +// Assets returns a list of xdr.Asset +func (q StrictReceivePathsQuery) Assets() ([]xdr.Asset, error) { + return xdr.BuildAssets(q.SourceAssets) +} + +// Amount returns source amount +func (q StrictReceivePathsQuery) Amount() xdr.Int64 { + parsed, err := amount.Parse(q.DestinationAmount) + if err != nil { + panic(err) + } + return parsed +} + +// DestinationAsset returns an xdr.Asset +func (q StrictReceivePathsQuery) DestinationAsset() xdr.Asset { + asset, err := xdr.BuildAsset( + q.DestinationAssetType, + q.DestinationAssetIssuer, + q.DestinationAssetCode, + ) + + if err != nil { + panic(err) + } + + return asset +} + +// URITemplate returns a rfc6570 URI template for the query struct +func (q StrictReceivePathsQuery) URITemplate() string { + return "/paths/strict-receive{?" + strings.Join(actions.GetURIParams(&q, false), ",") + "}" +} + +// Validate runs custom validations. +func (q StrictReceivePathsQuery) Validate() error { + if (len(q.SourceAccount) > 0) == (len(q.SourceAssets) > 0) { + return sourceAssetsOrSourceAccount + } + + err := actions.ValidateAssetParams( + q.DestinationAssetType, + q.DestinationAssetCode, + q.DestinationAssetIssuer, + "destination_", + ) + + if err != nil { + return err + } + + _, err = q.Assets() + + if err != nil { + return problem.MakeInvalidFieldProblem( + "source_assets", + err, + ) + } + + return nil +} + var sourceAssetsOrSourceAccount = problem.P{ Type: "bad_request", Title: "Bad Request", @@ -51,31 +128,17 @@ func (handler FindPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request problem.Render(ctx, w, err) return } - - query := paths.Query{} - var err error - query.DestinationAmount, err = actions.GetPositiveAmount(r, "destination_amount") + qp := StrictReceivePathsQuery{} + err := actions.GetParams(&qp, r) if err != nil { problem.Render(ctx, w, err) return } - sourceAccount, err := getAccountID(r, "source_account", false) - if err != nil { - problem.Render(ctx, w, err) - return - } - - query.SourceAssets, err = actions.GetAssets(r, "source_assets") - if err != nil { - problem.Render(ctx, w, err) - return - } - - if (len(query.SourceAssets) > 0) == (len(sourceAccount) > 0) { - problem.Render(ctx, w, sourceAssetsOrSourceAccount) - return - } + query := paths.Query{} + query.DestinationAmount = qp.Amount() + sourceAccount := qp.SourceAccount + query.SourceAssets, _ = qp.Assets() if len(query.SourceAssets) > handler.maxAssetsParamLength { p := problem.MakeInvalidFieldProblem( @@ -85,11 +148,7 @@ func (handler FindPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request problem.Render(ctx, w, p) return } - - if query.DestinationAsset, err = actions.GetAsset(r, "destination_"); err != nil { - problem.Render(ctx, w, err) - return - } + query.DestinationAsset = qp.DestinationAsset() if sourceAccount != "" { sourceAccount := xdr.MustAddress(sourceAccount) @@ -110,7 +169,8 @@ func (handler FindPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request records := []paths.Path{} if len(query.SourceAssets) > 0 { - records, err = handler.pathFinder.Find(query, handler.maxPathLength) + var lastIngestedLedger uint32 + records, lastIngestedLedger, err = handler.pathFinder.Find(query, handler.maxPathLength) if err == simplepath.ErrEmptyInMemoryOrderBook { err = horizonProblem.StillIngesting } @@ -118,6 +178,11 @@ func (handler FindPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request problem.Render(ctx, w, err) return } + + if handler.setLastLedgerHeader { + // only set the last ingested ledger header if + actions.SetLastLedgerHeader(w, lastIngestedLedger) + } } renderPaths(ctx, records, w) @@ -143,6 +208,7 @@ func renderPaths(ctx context.Context, records []paths.Path, w http.ResponseWrite type FindFixedPathsHandler struct { maxPathLength uint maxAssetsParamLength int + setLastLedgerHeader bool pathFinder paths.Finder coreQ *core.Q } @@ -155,27 +221,93 @@ var destinationAssetsOrDestinationAccount = problem.P{ "Both fields cannot be present.", } -// ServeHTTP implements the http.Handler interface -func (handler FindFixedPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() +// FindFixedPathsQuery query struct for paths/strict-send end-point +type FindFixedPathsQuery struct { + DestinationAccount string `schema:"destination_account" valid:"accountID,optional"` + DestinationAssets string `schema:"destination_assets" valid:"-"` + SourceAssetType string `schema:"source_asset_type" valid:"assetType"` + SourceAssetIssuer string `schema:"source_asset_issuer" valid:"accountID,optional"` + SourceAssetCode string `schema:"source_asset_code" valid:"-"` + SourceAmount string `schema:"source_amount" valid:"amount"` +} + +// URITemplate returns a rfc6570 URI template for the query struct +func (q FindFixedPathsQuery) URITemplate() string { + return "/paths/strict-send{?" + strings.Join(actions.GetURIParams(&q, false), ",") + "}" +} + +// Validate runs custom validations. +func (q FindFixedPathsQuery) Validate() error { + if (len(q.DestinationAccount) > 0) == (len(q.DestinationAssets) > 0) { + return destinationAssetsOrDestinationAccount + } + + err := actions.ValidateAssetParams( + q.SourceAssetType, + q.SourceAssetCode, + q.SourceAssetIssuer, + "source_", + ) - destinationAccount, err := getAccountID(r, "destination_account", false) if err != nil { - problem.Render(ctx, w, err) - return + return err } - destinationAssets, err := actions.GetAssets(r, "destination_assets") + _, err = q.Assets() + if err != nil { - problem.Render(ctx, w, err) - return + return problem.MakeInvalidFieldProblem( + "destination_assets", + err, + ) + } + + return nil +} + +// Assets returns a list of xdr.Asset +func (q FindFixedPathsQuery) Assets() ([]xdr.Asset, error) { + return xdr.BuildAssets(q.DestinationAssets) +} + +// Amount returns source amount +func (q FindFixedPathsQuery) Amount() xdr.Int64 { + parsed, err := amount.Parse(q.SourceAmount) + if err != nil { + panic(err) } + return parsed +} - if (len(destinationAccount) > 0) == (len(destinationAssets) > 0) { - problem.Render(ctx, w, destinationAssetsOrDestinationAccount) +// SourceAsset returns an xdr.Asset +func (q FindFixedPathsQuery) SourceAsset() xdr.Asset { + asset, err := xdr.BuildAsset( + q.SourceAssetType, + q.SourceAssetIssuer, + q.SourceAssetCode, + ) + + if err != nil { + panic(err) + } + + return asset +} + +// ServeHTTP implements the http.Handler interface +func (handler FindFixedPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + qp := FindFixedPathsQuery{} + err := actions.GetParams(&qp, r) + if err != nil { + problem.Render(ctx, w, err) return } + destinationAccount := qp.DestinationAccount + destinationAssets, _ := qp.Assets() + if len(destinationAssets) > handler.maxAssetsParamLength { p := problem.MakeInvalidFieldProblem( "destination_assets", @@ -195,21 +327,13 @@ func (handler FindFixedPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Re } } - sourceAsset, err := actions.GetAsset(r, "source_") - if err != nil { - problem.Render(ctx, w, err) - return - } - - amountToSpend, err := actions.GetPositiveAmount(r, "source_amount") - if err != nil { - problem.Render(ctx, w, err) - return - } + sourceAsset := qp.SourceAsset() + amountToSpend := qp.Amount() records := []paths.Path{} if len(destinationAssets) > 0 { - records, err = handler.pathFinder.FindFixedPaths( + var lastIngestedLedger uint32 + records, lastIngestedLedger, err = handler.pathFinder.FindFixedPaths( sourceAsset, amountToSpend, destinationAssets, @@ -222,6 +346,11 @@ func (handler FindFixedPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Re problem.Render(ctx, w, err) return } + + if handler.setLastLedgerHeader { + // only set the last ingested ledger header if + actions.SetLastLedgerHeader(w, lastIngestedLedger) + } } renderPaths(ctx, records, w) diff --git a/services/horizon/internal/actions_path_test.go b/services/horizon/internal/actions_path_test.go index 3454d5eda0..22495c12e5 100644 --- a/services/horizon/internal/actions_path_test.go +++ b/services/horizon/internal/actions_path_test.go @@ -7,35 +7,91 @@ import ( "strconv" "strings" "testing" - "time" "github.com/go-chi/chi" "github.com/stellar/go/exp/orderbook" "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/services/horizon/internal/actions" "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/db2/core" - "github.com/stellar/go/services/horizon/internal/paths" horizonProblem "github.com/stellar/go/services/horizon/internal/render/problem" "github.com/stellar/go/services/horizon/internal/simplepath" "github.com/stellar/go/services/horizon/internal/test" "github.com/stellar/go/support/render/problem" "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" ) -func pathFindingClient(tt *test.T, pathFinder paths.Finder, maxAssetsParamLength int) test.RequestHelper { +func inMemoryPathFindingClient( + tt *test.T, + graph *orderbook.OrderBookGraph, + maxAssetsParamLength int, +) test.RequestHelper { router := chi.NewRouter() findPaths := FindPathsHandler{ - pathFinder: pathFinder, + pathFinder: simplepath.NewInMemoryFinder(graph), maxAssetsParamLength: maxAssetsParamLength, + setLastLedgerHeader: true, coreQ: &core.Q{tt.CoreSession()}, } findFixedPaths := FindFixedPathsHandler{ - pathFinder: pathFinder, + pathFinder: simplepath.NewInMemoryFinder(graph), maxAssetsParamLength: maxAssetsParamLength, + setLastLedgerHeader: true, coreQ: &core.Q{tt.CoreSession()}, } - installPathFindingRoutes(findPaths, findFixedPaths, router, false) + installPathFindingRoutes( + findPaths, + findFixedPaths, + router, + false, + &ExperimentalIngestionMiddleware{ + EnableExperimentalIngestion: true, + HorizonSession: tt.HorizonSession(), + StateReady: func() bool { + return true + }, + }, + ) + return test.NewRequestHelper(router) +} + +func dbPathFindingClient( + tt *test.T, + maxAssetsParamLength int, +) test.RequestHelper { + router := chi.NewRouter() + findPaths := FindPathsHandler{ + pathFinder: &simplepath.Finder{ + Q: &core.Q{tt.CoreSession()}, + }, + maxAssetsParamLength: maxAssetsParamLength, + setLastLedgerHeader: false, + coreQ: &core.Q{tt.CoreSession()}, + } + findFixedPaths := FindFixedPathsHandler{ + pathFinder: &simplepath.Finder{ + Q: &core.Q{tt.CoreSession()}, + }, + maxAssetsParamLength: maxAssetsParamLength, + setLastLedgerHeader: false, + coreQ: &core.Q{tt.CoreSession()}, + } + + installPathFindingRoutes( + findPaths, + findFixedPaths, + router, + false, + &ExperimentalIngestionMiddleware{ + EnableExperimentalIngestion: false, + HorizonSession: tt.HorizonSession(), + StateReady: func() bool { + return false + }, + }, + ) return test.NewRequestHelper(router) } @@ -43,11 +99,8 @@ func TestPathActions_Index(t *testing.T) { tt := test.Start(t).Scenario("paths") assertions := &Assertions{tt.Assert} defer tt.Finish() - rh := pathFindingClient( + rh := dbPathFindingClient( tt, - &simplepath.Finder{ - Q: &core.Q{tt.CoreSession()}, - }, 3, ) @@ -78,6 +131,7 @@ func TestPathActions_Index(t *testing.T) { w = rh.Get(uri + "?" + q.Encode()) assertions.Equal(200, w.Code) assertions.PageOf(3, w.Body) + assertions.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) } } @@ -85,9 +139,9 @@ func TestPathActionsStillIngesting(t *testing.T) { tt := test.Start(t).Scenario("paths") defer tt.Finish() assertions := &Assertions{tt.Assert} - rh := pathFindingClient( + rh := inMemoryPathFindingClient( tt, - simplepath.NewInMemoryFinder(orderbook.NewOrderBookGraph()), + orderbook.NewOrderBookGraph(), 3, ) @@ -113,39 +167,16 @@ func TestPathActionsStillIngesting(t *testing.T) { w := rh.Get(uri + "?" + q.Encode()) assertions.Equal(horizonProblem.StillIngesting.Status, w.Code) assertions.Problem(w.Body, horizonProblem.StillIngesting) + assertions.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) } } -func TestPathActionsStateInvalid(t *testing.T) { - rh := StartHTTPTest(t, "paths") - defer rh.Finish() - - rh.App.config.EnableExperimentalIngestion = true - rh.App.web.router = chi.NewRouter() - orderBookGraph := orderbook.NewOrderBookGraph() - rh.App.web.mustInstallMiddlewares(rh.App, time.Minute) - rh.App.web.mustInstallActions( - rh.App.config, - simplepath.NewInMemoryFinder(orderBookGraph), - ) - rh.RH = test.NewRequestHelper(rh.App.web.router) - - w := rh.Get("/paths") - // Still ingesting - rh.Assert.Equal(503, w.Code) - - err := rh.App.historyQ.UpdateLastLedgerExpIngest(10) - rh.Assert.NoError(err) - - err = rh.App.historyQ.UpdateExpStateInvalid(true) - rh.Assert.NoError(err) - - w = rh.Get("/paths") - // State invalid - rh.Assert.Equal(500, w.Code) -} - -func loadOffers(tt *test.T, orderBookGraph *orderbook.OrderBookGraph, fromAddress string) { +func loadOffers( + tt *test.T, + orderBookGraph *orderbook.OrderBookGraph, + fromAddress string, + ledger uint32, +) { coreQ := &core.Q{tt.CoreSession()} offers := []core.Offer{} pageQuery := db2.PageQuery{ @@ -165,7 +196,7 @@ func loadOffers(tt *test.T, orderBookGraph *orderbook.OrderBookGraph, fromAddres Price: xdr.Price{N: xdr.Int32(offer.Price * 100), D: 100}, }) } - tt.Assert.NoError(orderBookGraph.Apply()) + tt.Assert.NoError(orderBookGraph.Apply(ledger)) } func TestPathActionsInMemoryFinder(t *testing.T) { @@ -178,21 +209,18 @@ func TestPathActionsInMemoryFinder(t *testing.T) { sourceAssets, _, err := coreQ.AssetsForAddress(sourceAccount) tt.Assert.NoError(err) - inMemoryPathsClient := pathFindingClient( + inMemoryPathsClient := inMemoryPathFindingClient( tt, - simplepath.NewInMemoryFinder(orderBookGraph), + orderBookGraph, len(sourceAssets), ) - dbPathsClient := pathFindingClient( + dbPathsClient := dbPathFindingClient( tt, - &simplepath.Finder{ - Q: &core.Q{tt.CoreSession()}, - }, len(sourceAssets), ) - loadOffers(tt, orderBookGraph, "GA2NC4ZOXMXLVQAQQ5IQKJX47M3PKBQV2N5UV5Z4OXLQJ3CKMBA2O2YL") - loadOffers(tt, orderBookGraph, "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") + loadOffers(tt, orderBookGraph, "GA2NC4ZOXMXLVQAQQ5IQKJX47M3PKBQV2N5UV5Z4OXLQJ3CKMBA2O2YL", 1) + loadOffers(tt, orderBookGraph, "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", 2) var withSourceAccount = make(url.Values) withSourceAccount.Add( @@ -223,11 +251,13 @@ func TestPathActionsInMemoryFinder(t *testing.T) { tt.Assert.Equal(http.StatusOK, w.Code) inMemorySourceAccountResponse := []horizon.Path{} tt.UnmarshalPage(w.Body, &inMemorySourceAccountResponse) + tt.Assert.Equal("2", w.Header().Get(actions.LastLedgerHeaderName)) w = dbPathsClient.Get(uri + "?" + withSourceAccount.Encode()) tt.Assert.Equal(http.StatusOK, w.Code) dbSourceAccountResponse := []horizon.Path{} tt.UnmarshalPage(w.Body, &dbSourceAccountResponse) + tt.Assert.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) tt.Assert.True(len(inMemorySourceAccountResponse) > 0) tt.Assert.Equal(inMemorySourceAccountResponse, dbSourceAccountResponse) @@ -236,11 +266,13 @@ func TestPathActionsInMemoryFinder(t *testing.T) { tt.Assert.Equal(http.StatusOK, w.Code) inMemorySourceAssetsResponse := []horizon.Path{} tt.UnmarshalPage(w.Body, &inMemorySourceAssetsResponse) + tt.Assert.Equal("2", w.Header().Get(actions.LastLedgerHeaderName)) w = dbPathsClient.Get(uri + "?" + withSourceAccount.Encode()) tt.Assert.Equal(http.StatusOK, w.Code) dbSourceAssetsResponse := []horizon.Path{} tt.UnmarshalPage(w.Body, &dbSourceAssetsResponse) + tt.Assert.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) tt.Assert.Equal(inMemorySourceAssetsResponse, dbSourceAssetsResponse) tt.Assert.Equal(inMemorySourceAssetsResponse, inMemorySourceAccountResponse) @@ -252,21 +284,18 @@ func TestPathActionsEmptySourceAcount(t *testing.T) { defer tt.Finish() orderBookGraph := orderbook.NewOrderBookGraph() assertions := &Assertions{tt.Assert} - inMemoryPathsClient := pathFindingClient( + inMemoryPathsClient := inMemoryPathFindingClient( tt, - simplepath.NewInMemoryFinder(orderBookGraph), + orderBookGraph, 3, ) - dbPathsClient := pathFindingClient( + dbPathsClient := dbPathFindingClient( tt, - &simplepath.Finder{ - Q: &core.Q{tt.CoreSession()}, - }, 3, ) - loadOffers(tt, orderBookGraph, "GA2NC4ZOXMXLVQAQQ5IQKJX47M3PKBQV2N5UV5Z4OXLQJ3CKMBA2O2YL") - loadOffers(tt, orderBookGraph, "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") + loadOffers(tt, orderBookGraph, "GA2NC4ZOXMXLVQAQQ5IQKJX47M3PKBQV2N5UV5Z4OXLQJ3CKMBA2O2YL", 1) + loadOffers(tt, orderBookGraph, "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", 2) var q = make(url.Values) @@ -293,12 +322,14 @@ func TestPathActionsEmptySourceAcount(t *testing.T) { inMemoryResponse := []horizon.Path{} tt.UnmarshalPage(w.Body, &inMemoryResponse) assertions.Empty(inMemoryResponse) + tt.Assert.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) w = dbPathsClient.Get(uri + "?" + q.Encode()) assertions.Equal(http.StatusOK, w.Code) dbResponse := []horizon.Path{} tt.UnmarshalPage(w.Body, &dbResponse) assertions.Empty(dbResponse) + tt.Assert.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) } } @@ -307,9 +338,9 @@ func TestPathActionsSourceAssetsValidation(t *testing.T) { defer tt.Finish() assertions := &Assertions{tt.Assert} orderBookGraph := orderbook.NewOrderBookGraph() - rh := pathFindingClient( + rh := inMemoryPathFindingClient( tt, - simplepath.NewInMemoryFinder(orderBookGraph), + orderBookGraph, 3, ) @@ -375,6 +406,7 @@ func TestPathActionsSourceAssetsValidation(t *testing.T) { w := rh.Get("/paths/strict-receive?" + testCase.q.Encode()) assertions.Equal(testCase.expectedProblem.Status, w.Code) assertions.Problem(w.Body, testCase.expectedProblem) + assertions.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) }) } } @@ -384,9 +416,9 @@ func TestPathActionsDestinationAssetsValidation(t *testing.T) { defer tt.Finish() assertions := &Assertions{tt.Assert} orderBookGraph := orderbook.NewOrderBookGraph() - rh := pathFindingClient( + rh := inMemoryPathFindingClient( tt, - simplepath.NewInMemoryFinder(orderBookGraph), + orderBookGraph, 3, ) @@ -456,6 +488,7 @@ func TestPathActionsDestinationAssetsValidation(t *testing.T) { w := rh.Get("/paths/strict-send?" + testCase.q.Encode()) assertions.Equal(testCase.expectedProblem.Status, w.Code) assertions.Problem(w.Body, testCase.expectedProblem) + assertions.Equal("", w.Header().Get(actions.LastLedgerHeaderName)) }) } } @@ -471,14 +504,14 @@ func TestPathActionsStrictSend(t *testing.T) { destinationAssets, _, err := coreQ.AssetsForAddress(destinationAccount) tt.Assert.NoError(err) - rh := pathFindingClient( + rh := inMemoryPathFindingClient( tt, - simplepath.NewInMemoryFinder(orderBookGraph), + orderBookGraph, len(destinationAssets), ) - loadOffers(tt, orderBookGraph, "GA2NC4ZOXMXLVQAQQ5IQKJX47M3PKBQV2N5UV5Z4OXLQJ3CKMBA2O2YL") - loadOffers(tt, orderBookGraph, "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") + loadOffers(tt, orderBookGraph, "GA2NC4ZOXMXLVQAQQ5IQKJX47M3PKBQV2N5UV5Z4OXLQJ3CKMBA2O2YL", 1) + loadOffers(tt, orderBookGraph, "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", 2) var q = make(url.Values) @@ -499,6 +532,7 @@ func TestPathActionsStrictSend(t *testing.T) { accountResponse := []horizon.Path{} tt.UnmarshalPage(w.Body, &accountResponse) assertions.Len(accountResponse, 12) + assertions.Equal("2", w.Header().Get(actions.LastLedgerHeaderName)) for i, path := range accountResponse { assertions.Equal(path.SourceAssetCode, "USD") @@ -534,6 +568,7 @@ func TestPathActionsStrictSend(t *testing.T) { tt.UnmarshalPage(w.Body, &assetListResponse) assertions.Len(assetListResponse, 12) tt.Assert.Equal(accountResponse, assetListResponse) + assertions.Equal("2", w.Header().Get(actions.LastLedgerHeaderName)) } func assetsToURLParam(xdrAssets []xdr.Asset) string { @@ -550,3 +585,34 @@ func assetsToURLParam(xdrAssets []xdr.Asset) string { return strings.Join(assets, ",") } + +func TestFindFixedPathsQueryQueryURLTemplate(t *testing.T) { + tt := assert.New(t) + params := []string{ + "destination_account", + "destination_assets", + "source_asset_type", + "source_asset_issuer", + "source_asset_code", + "source_amount", + } + expected := "/paths/strict-send{?" + strings.Join(params, ",") + "}" + qp := FindFixedPathsQuery{} + tt.Equal(expected, qp.URITemplate()) +} + +func TestStrictReceivePathsQueryURLTemplate(t *testing.T) { + tt := assert.New(t) + params := []string{ + "source_assets", + "source_account", + "destination_account", + "destination_asset_type", + "destination_asset_issuer", + "destination_asset_code", + "destination_amount", + } + expected := "/paths/strict-receive{?" + strings.Join(params, ",") + "}" + qp := StrictReceivePathsQuery{} + tt.Equal(expected, qp.URITemplate()) +} diff --git a/services/horizon/internal/actions_root.go b/services/horizon/internal/actions_root.go index 6f03adb673..527d8b6638 100644 --- a/services/horizon/internal/actions_root.go +++ b/services/horizon/internal/actions_root.go @@ -20,6 +20,12 @@ type RootAction struct { // JSON renders the json response for RootAction func (action *RootAction) JSON() error { var res horizon.Root + templates := map[string]string{ + "accounts": actions.AccountsQuery{}.URITemplate(), + "offers": actions.OffersQuery{}.URITemplate(), + "strictReceivePaths": StrictReceivePathsQuery{}.URITemplate(), + "strictSendPaths": FindFixedPathsQuery{}.URITemplate(), + } resourceadapter.PopulateRoot( action.R.Context(), &res, @@ -31,6 +37,7 @@ func (action *RootAction) JSON() error { action.App.coreSupportedProtocolVersion, action.App.config.FriendbotURL, action.App.config.EnableExperimentalIngestion, + templates, ) hal.Render(action.W, res) diff --git a/services/horizon/internal/actions_root_test.go b/services/horizon/internal/actions_root_test.go index be72c2f5a0..144ad75362 100644 --- a/services/horizon/internal/actions_root_test.go +++ b/services/horizon/internal/actions_root_test.go @@ -2,6 +2,7 @@ package horizon import ( "encoding/json" + "strings" "testing" "github.com/stellar/go/protocols/horizon" @@ -41,3 +42,71 @@ func TestRootAction(t *testing.T) { ht.Assert.Equal(int32(3), actual.CurrentProtocolVersion) } } + +func TestRootActionWithIngestion(t *testing.T) { + ht := StartHTTPTest(t, "base") + defer ht.Finish() + + server := test.NewStaticMockServer(`{ + "info": { + "network": "test", + "build": "test-core", + "ledger": { + "version": 3 + }, + "protocol_version": 4 + } + }`) + defer server.Close() + + ht.App.horizonVersion = "test-horizon" + ht.App.config.StellarCoreURL = server.URL + ht.App.config.NetworkPassphrase = "test" + ht.App.UpdateStellarCoreInfo() + ht.App.config.EnableExperimentalIngestion = true + + w := ht.Get("/") + + if ht.Assert.Equal(200, w.Code) { + var actual horizon.Root + err := json.Unmarshal(w.Body.Bytes(), &actual) + ht.Require.NoError(err) + ht.Assert.Equal( + "http://localhost/accounts{?signer,asset,cursor,limit,order}", + actual.Links.Accounts.Href, + ) + ht.Assert.Equal( + "http://localhost/offers{?selling_asset_type,selling_asset_issuer,selling_asset_code,buying_asset_type,buying_asset_issuer,buying_asset_code,seller,cursor,limit,order}", + actual.Links.Offers.Href, + ) + + params := []string{ + "destination_account", + "destination_assets", + "source_asset_type", + "source_asset_issuer", + "source_asset_code", + "source_amount", + } + + ht.Assert.Equal( + "http://localhost/paths/strict-send{?"+strings.Join(params, ",")+"}", + actual.Links.StrictSendPaths.Href, + ) + + params = []string{ + "source_assets", + "source_account", + "destination_account", + "destination_asset_type", + "destination_asset_issuer", + "destination_asset_code", + "destination_amount", + } + + ht.Assert.Equal( + "http://localhost/paths/strict-receive{?"+strings.Join(params, ",")+"}", + actual.Links.StrictReceivePaths.Href, + ) + } +} diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index b672ab0a34..a6ab2e5f56 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -78,8 +78,6 @@ func NewApp(config Config) *App { // Serve starts the horizon web server, binding it to a socket, setting up // the shutdown signals. func (a *App) Serve() { - http.Handle("/", a.web.router) - addr := fmt.Sprintf(":%d", a.config.Port) srv := &graceful.Server{ @@ -87,7 +85,7 @@ func (a *App) Serve() { Server: &http.Server{ Addr: addr, - Handler: http.DefaultServeMux, + Handler: a.web.router, ReadHeaderTimeout: 5 * time.Second, }, @@ -428,8 +426,18 @@ func (a *App) init() { // This parameter will be removed soon. a.web.mustInstallMiddlewares(a, a.config.ConnectionTimeout) + requiresExperimentalIngestion := &ExperimentalIngestionMiddleware{ + EnableExperimentalIngestion: a.config.EnableExperimentalIngestion, + HorizonSession: a.historyQ.Session, + StateReady: func() bool { + if !a.config.EnableExperimentalIngestion { + return false + } + return a.expingester.StateReady() + }, + } // web.actions - a.web.mustInstallActions(a.config, a.paths) + a.web.mustInstallActions(a.config, a.paths, orderBookGraph, requiresExperimentalIngestion) // metrics and log.metrics a.metrics = metrics.NewRegistry() diff --git a/services/horizon/internal/context/context_key.go b/services/horizon/internal/context/context_key.go index 7986f1ba5b..9547544b3b 100644 --- a/services/horizon/internal/context/context_key.go +++ b/services/horizon/internal/context/context_key.go @@ -5,3 +5,4 @@ type CtxKey string var AppContextKey = CtxKey("app") var RequestContextKey = CtxKey("request") var ClientContextKey = CtxKey("client") +var SessionContextKey = CtxKey("session") diff --git a/services/horizon/internal/db2/history/account_data.go b/services/horizon/internal/db2/history/account_data.go new file mode 100644 index 0000000000..8f24146e09 --- /dev/null +++ b/services/horizon/internal/db2/history/account_data.go @@ -0,0 +1,154 @@ +package history + +import ( + "encoding/base64" + + sq "github.com/Masterminds/squirrel" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func (q *Q) CountAccountsData() (int, error) { + sql := sq.Select("count(*)").From("accounts_data") + + var count int + if err := q.Get(&count, sql); err != nil { + return 0, errors.Wrap(err, "could not run select query") + } + + return count, nil +} + +// GetAccountDataByAccountID loads account data for a given account ID +func (q *Q) GetAccountDataByAccountID(id string) ([]Data, error) { + var data []Data + sql := selectAccountData.Where(sq.Eq{"account_id": id}) + err := q.Select(&data, sql) + return data, err +} + +// GetAccountDataByKeys loads a row from the `accounts_data` table, selected by multiple keys. +func (q *Q) GetAccountDataByKeys(keys []xdr.LedgerKeyData) ([]Data, error) { + var data []Data + lkeys := make([]string, 0, len(keys)) + for _, key := range keys { + lkey, err := ledgerKeyDataToString(key) + if err != nil { + return nil, errors.Wrap(err, "Error running ledgerKeyTrustLineToString") + } + lkeys = append(lkeys, lkey) + } + sql := selectAccountData.Where(map[string]interface{}{"accounts_data.ledger_key": lkeys}) + err := q.Select(&data, sql) + return data, err +} + +func ledgerKeyDataToString(data xdr.LedgerKeyData) (string, error) { + ledgerKey := &xdr.LedgerKey{} + err := ledgerKey.SetData(data.AccountId, string(data.DataName)) + if err != nil { + return "", errors.Wrap(err, "Error running ledgerKey.SetData") + } + key, err := ledgerKey.MarshalBinary() + if err != nil { + return "", errors.Wrap(err, "Error running MarshalBinaryCompress") + } + + return base64.StdEncoding.EncodeToString(key), nil +} + +func dataEntryToLedgerKeyString(data xdr.DataEntry) (string, error) { + ledgerKey := &xdr.LedgerKey{} + err := ledgerKey.SetData(data.AccountId, string(data.DataName)) + if err != nil { + return "", errors.Wrap(err, "Error running ledgerKey.SetTrustline") + } + key, err := ledgerKey.MarshalBinary() + if err != nil { + return "", errors.Wrap(err, "Error running MarshalBinaryCompress") + } + + return base64.StdEncoding.EncodeToString(key), nil +} + +// InsertAccountData creates a row in the accounts_data table. +// Returns number of rows affected and error. +func (q *Q) InsertAccountData(data xdr.DataEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + // Add lkey only when inserting rows + key, err := dataEntryToLedgerKeyString(data) + if err != nil { + return 0, errors.Wrap(err, "Error running dataEntryToLedgerKeyString") + } + + sql := sq.Insert("accounts_data"). + Columns("ledger_key", "account_id", "name", "value", "last_modified_ledger"). + Values( + key, + data.AccountId.Address(), + data.DataName, + AccountDataValue(data.DataValue), + lastModifiedLedger, + ) + + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// UpdateAccountData updates a row in the accounts_data table. +// Returns number of rows affected and error. +func (q *Q) UpdateAccountData(data xdr.DataEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + key, err := dataEntryToLedgerKeyString(data) + if err != nil { + return 0, errors.Wrap(err, "Error running dataEntryToLedgerKeyString") + } + + sql := sq.Update("accounts_data"). + SetMap(map[string]interface{}{ + "value": AccountDataValue(data.DataValue), + "last_modified_ledger": lastModifiedLedger, + }). + Where(sq.Eq{"ledger_key": key}) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// RemoveAccountData deletes a row in the accounts_data table. +// Returns number of rows affected and error. +func (q *Q) RemoveAccountData(key xdr.LedgerKeyData) (int64, error) { + lkey, err := ledgerKeyDataToString(key) + if err != nil { + return 0, errors.Wrap(err, "Error running ledgerKeyDataToString") + } + + sql := sq.Delete("accounts_data"). + Where(sq.Eq{"ledger_key": lkey}) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// GetAccountDataByAccountsID loads account data for a list of account ID +func (q *Q) GetAccountDataByAccountsID(id []string) ([]Data, error) { + var data []Data + sql := selectAccountData.Where(sq.Eq{"account_id": id}) + err := q.Select(&data, sql) + return data, err +} + +var selectAccountData = sq.Select(` + account_id, + name, + value, + last_modified_ledger +`).From("accounts_data") diff --git a/services/horizon/internal/db2/history/account_data_batch_insert_builder.go b/services/horizon/internal/db2/history/account_data_batch_insert_builder.go new file mode 100644 index 0000000000..bb948215f9 --- /dev/null +++ b/services/horizon/internal/db2/history/account_data_batch_insert_builder.go @@ -0,0 +1,26 @@ +package history + +import ( + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func (i *accountDataBatchInsertBuilder) Add(data xdr.DataEntry, lastModifiedLedger xdr.Uint32) error { + // Add ledger_key only when inserting rows + key, err := dataEntryToLedgerKeyString(data) + if err != nil { + return errors.Wrap(err, "Error running dataEntryToLedgerKeyString") + } + + return i.builder.Row(map[string]interface{}{ + "ledger_key": key, + "account_id": data.AccountId.Address(), + "name": data.DataName, + "value": AccountDataValue(data.DataValue), + "last_modified_ledger": lastModifiedLedger, + }) +} + +func (i *accountDataBatchInsertBuilder) Exec() error { + return i.builder.Exec() +} diff --git a/services/horizon/internal/db2/history/account_data_test.go b/services/horizon/internal/db2/history/account_data_test.go new file mode 100644 index 0000000000..db29cb41e5 --- /dev/null +++ b/services/horizon/internal/db2/history/account_data_test.go @@ -0,0 +1,156 @@ +package history + +import ( + "testing" + + "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +var ( + data1 = xdr.DataEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + DataName: "test data", + // This also tests if base64 encoding is working as 0 is invalid UTF-8 byte + DataValue: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + } + + data2 = xdr.DataEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + DataName: "test data2", + DataValue: []byte{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, + } +) + +func TestInsertAccountData(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + rows, err := q.InsertAccountData(data1, 1234) + assert.NoError(t, err) + tt.Assert.Equal(int64(1), rows) + + rows, err = q.InsertAccountData(data2, 1235) + assert.NoError(t, err) + tt.Assert.Equal(int64(1), rows) + + keys := []xdr.LedgerKeyData{ + {AccountId: data1.AccountId, DataName: data1.DataName}, + {AccountId: data2.AccountId, DataName: data2.DataName}, + } + + datas, err := q.GetAccountDataByKeys(keys) + assert.NoError(t, err) + assert.Len(t, datas, 2) + + tt.Assert.Equal(data1.DataName, xdr.String64(datas[0].Name)) + tt.Assert.Equal([]byte(data1.DataValue), []byte(datas[0].Value)) + + tt.Assert.Equal(data2.DataName, xdr.String64(datas[1].Name)) + tt.Assert.Equal([]byte(data2.DataValue), []byte(datas[1].Value)) +} + +func TestUpdateAccountData(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + rows, err := q.InsertAccountData(data1, 1234) + assert.NoError(t, err) + tt.Assert.Equal(int64(1), rows) + + modifiedData := data1 + modifiedData.DataValue[0] = 1 + + rows, err = q.UpdateAccountData(modifiedData, 1235) + assert.NoError(t, err) + tt.Assert.Equal(int64(1), rows) + + keys := []xdr.LedgerKeyData{ + {AccountId: data1.AccountId, DataName: data1.DataName}, + } + datas, err := q.GetAccountDataByKeys(keys) + assert.NoError(t, err) + assert.Len(t, datas, 1) + + tt.Assert.Equal(modifiedData.DataName, xdr.String64(datas[0].Name)) + tt.Assert.Equal([]byte(modifiedData.DataValue), []byte(datas[0].Value)) + tt.Assert.Equal(uint32(1235), datas[0].LastModifiedLedger) +} + +func TestRemoveAccountData(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + rows, err := q.InsertAccountData(data1, 1234) + assert.NoError(t, err) + tt.Assert.Equal(int64(1), rows) + + key := xdr.LedgerKeyData{AccountId: data1.AccountId, DataName: data1.DataName} + rows, err = q.RemoveAccountData(key) + assert.NoError(t, err) + tt.Assert.Equal(int64(1), rows) + + datas, err := q.GetAccountDataByKeys([]xdr.LedgerKeyData{key}) + assert.NoError(t, err) + assert.Len(t, datas, 0) + + // Doesn't exist anymore + rows, err = q.RemoveAccountData(key) + assert.NoError(t, err) + tt.Assert.Equal(int64(0), rows) +} + +func TestGetAccountDataByAccountsID(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + _, err := q.InsertAccountData(data1, 1234) + assert.NoError(t, err) + _, err = q.InsertAccountData(data2, 1235) + assert.NoError(t, err) + + ids := []string{ + data1.AccountId.Address(), + data2.AccountId.Address(), + } + datas, err := q.GetAccountDataByAccountsID(ids) + assert.NoError(t, err) + assert.Len(t, datas, 2) + + tt.Assert.Equal(data1.DataName, xdr.String64(datas[0].Name)) + tt.Assert.Equal([]byte(data1.DataValue), []byte(datas[0].Value)) + + tt.Assert.Equal(data2.DataName, xdr.String64(datas[1].Name)) + tt.Assert.Equal([]byte(data2.DataValue), []byte(datas[1].Value)) +} + +func TestGetAccountDataByAccountID(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + _, err := q.InsertAccountData(data1, 1234) + assert.NoError(t, err) + _, err = q.InsertAccountData(data2, 1235) + assert.NoError(t, err) + + records, err := q.GetAccountDataByAccountID(data1.AccountId.Address()) + assert.NoError(t, err) + assert.Len(t, records, 2) + + tt.Assert.Equal(data1.DataName, xdr.String64(records[0].Name)) + tt.Assert.Equal([]byte(data1.DataValue), []byte(records[0].Value)) + + tt.Assert.Equal(data2.DataName, xdr.String64(records[1].Name)) + tt.Assert.Equal([]byte(data2.DataValue), []byte(records[1].Value)) +} diff --git a/services/horizon/internal/db2/history/account_data_value.go b/services/horizon/internal/db2/history/account_data_value.go new file mode 100644 index 0000000000..efcd8d319b --- /dev/null +++ b/services/horizon/internal/db2/history/account_data_value.go @@ -0,0 +1,30 @@ +package history + +import ( + "database/sql" + "database/sql/driver" + "encoding/base64" +) + +var _ driver.Valuer = (*AccountDataValue)(nil) +var _ sql.Scanner = (*AccountDataValue)(nil) + +// Scan base64 decodes into an []byte +func (t *AccountDataValue) Scan(src interface{}) error { + decoded, err := base64.StdEncoding.DecodeString(src.(string)) + if err != nil { + return err + } + + *t = decoded + return nil +} + +// Value implements driver.Valuer +func (value AccountDataValue) Value() (driver.Value, error) { + return driver.Value([]uint8(base64.StdEncoding.EncodeToString(value))), nil +} + +func (value AccountDataValue) Base64() string { + return base64.StdEncoding.EncodeToString(value) +} diff --git a/services/horizon/internal/db2/history/account_signers.go b/services/horizon/internal/db2/history/account_signers.go index 17e083ea93..93e9da4728 100644 --- a/services/horizon/internal/db2/history/account_signers.go +++ b/services/horizon/internal/db2/history/account_signers.go @@ -2,23 +2,27 @@ package history import ( sq "github.com/Masterminds/squirrel" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/support/errors" ) -func (q *Q) CountAccounts() (int, error) { - sql := sq.Select("count(distinct account)").From("accounts_signers") +func (q *Q) GetAccountSignersByAccountID(id string) ([]AccountSigner, error) { + sql := selectAccountSigners. + Where(sq.Eq{"accounts_signers.account_id": id}). + OrderBy("accounts_signers.signer asc") - var count int - if err := q.Get(&count, sql); err != nil { - return 0, errors.Wrap(err, "could not run select query") + var results []AccountSigner + if err := q.Select(&results, sql); err != nil { + return nil, errors.Wrap(err, "could not run select query") } - return count, nil + return results, nil } func (q *Q) SignersForAccounts(accounts []string) ([]AccountSigner, error) { - sql := selectAccountSigners.Where(map[string]interface{}{"accounts_signers.account": accounts}) + sql := selectAccountSigners. + Where(map[string]interface{}{"accounts_signers.account_id": accounts}) var results []AccountSigner if err := q.Select(&results, sql); err != nil { @@ -31,7 +35,7 @@ func (q *Q) SignersForAccounts(accounts []string) ([]AccountSigner, error) { // AccountsForSigner returns a list of `AccountSigner` rows for a given signer func (q *Q) AccountsForSigner(signer string, page db2.PageQuery) ([]AccountSigner, error) { sql := selectAccountSigners.Where("accounts_signers.signer = ?", signer) - sql, err := page.ApplyToUsingCursor(sql, "accounts_signers.account", page.Cursor) + sql, err := page.ApplyToUsingCursor(sql, "accounts_signers.account_id", page.Cursor) if err != nil { return nil, errors.Wrap(err, "could not apply query to page") } @@ -48,7 +52,7 @@ func (q *Q) AccountsForSigner(signer string, page db2.PageQuery) ([]AccountSigne // Returns number of rows affected and error. func (q *Q) CreateAccountSigner(account, signer string, weight int32) (int64, error) { sql := sq.Insert("accounts_signers"). - Columns("account", "signer", "weight"). + Columns("account_id", "signer", "weight"). Values(account, signer, weight) result, err := q.Exec(sql) @@ -63,8 +67,8 @@ func (q *Q) CreateAccountSigner(account, signer string, weight int32) (int64, er // Returns number of rows affected and error. func (q *Q) RemoveAccountSigner(account, signer string) (int64, error) { sql := sq.Delete("accounts_signers").Where(sq.Eq{ - "account": account, - "signer": signer, + "account_id": account, + "signer": signer, }) result, err := q.Exec(sql) diff --git a/services/horizon/internal/db2/history/account_signers_batch_insert_builder.go b/services/horizon/internal/db2/history/account_signers_batch_insert_builder.go index d327e63483..bee28de9c9 100644 --- a/services/horizon/internal/db2/history/account_signers_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/account_signers_batch_insert_builder.go @@ -2,9 +2,9 @@ package history func (i *accountSignersBatchInsertBuilder) Add(signer AccountSigner) error { return i.builder.Row(map[string]interface{}{ - "account": signer.Account, - "signer": signer.Signer, - "weight": signer.Weight, + "account_id": signer.Account, + "signer": signer.Signer, + "weight": signer.Weight, }) } diff --git a/services/horizon/internal/db2/history/account_signers_test.go b/services/horizon/internal/db2/history/account_signers_test.go index 7b68c631bb..a93f48a293 100644 --- a/services/horizon/internal/db2/history/account_signers_test.go +++ b/services/horizon/internal/db2/history/account_signers_test.go @@ -123,3 +123,37 @@ func TestRemoveAccountSigner(t *testing.T) { tt.Assert.NoError(err) tt.Assert.Len(results, 0) } + +func TestGetAccountSignersByAccountID(t *testing.T) { + tt := test.Start(t).Scenario("base") + defer tt.Finish() + q := &Q{tt.HorizonSession()} + + account := "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH6" + signer := "GC23QF2HUE52AMXUFUH3AYJAXXGXXV2VHXYYR6EYXETPKDXZSAW67XO7" + weight := int32(123) + _, err := q.CreateAccountSigner(account, signer, weight) + tt.Assert.NoError(err) + + signer2 := "GC2WJF6YWMAEHGGAK2UOMZCIOMH4RU7KY2CQEWZQJV2ZQJVXJ335ZSXG" + weight2 := int32(100) + _, err = q.CreateAccountSigner(account, signer2, weight2) + tt.Assert.NoError(err) + + expected := []AccountSigner{ + AccountSigner{ + Account: account, + Signer: signer, + Weight: weight, + }, + AccountSigner{ + Account: account, + Signer: signer2, + Weight: weight2, + }, + } + results, err := q.GetAccountSignersByAccountID(account) + tt.Assert.NoError(err) + tt.Assert.Len(results, 2) + tt.Assert.Equal(expected, results) +} diff --git a/services/horizon/internal/db2/history/account_test.go b/services/horizon/internal/db2/history/account_test.go index 0904fb4d38..97af9a8469 100644 --- a/services/horizon/internal/db2/history/account_test.go +++ b/services/horizon/internal/db2/history/account_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stellar/go/services/horizon/internal/test" + "github.com/stretchr/testify/assert" ) func TestAccountQueries(t *testing.T) { @@ -19,3 +20,32 @@ func TestAccountQueries(t *testing.T) { tt.Assert.Len(acs, 4) } } + +func TestIsAuthRequired(t *testing.T) { + tt := assert.New(t) + + account := AccountEntry{Flags: 1} + tt.True(account.IsAuthRequired()) + + account = AccountEntry{Flags: 0} + tt.False(account.IsAuthRequired()) +} + +func TestIsAuthRevocable(t *testing.T) { + tt := assert.New(t) + + account := AccountEntry{Flags: 2} + tt.True(account.IsAuthRevocable()) + + account = AccountEntry{Flags: 1} + tt.False(account.IsAuthRevocable()) +} +func TestIsAuthImmutable(t *testing.T) { + tt := assert.New(t) + + account := AccountEntry{Flags: 4} + tt.True(account.IsAuthImmutable()) + + account = AccountEntry{Flags: 0} + tt.False(account.IsAuthImmutable()) +} diff --git a/services/horizon/internal/db2/history/accounts.go b/services/horizon/internal/db2/history/accounts.go new file mode 100644 index 0000000000..3b4aeaf083 --- /dev/null +++ b/services/horizon/internal/db2/history/accounts.go @@ -0,0 +1,195 @@ +package history + +import ( + sq "github.com/Masterminds/squirrel" + + "github.com/stellar/go/services/horizon/internal/db2" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +// IsAuthRequired returns true if the account has the "AUTH_REQUIRED" option +// turned on. +func (account AccountEntry) IsAuthRequired() bool { + return xdr.AccountFlags(account.Flags).IsAuthRequired() +} + +// IsAuthRevocable returns true if the account has the "AUTH_REVOCABLE" option +// turned on. +func (account AccountEntry) IsAuthRevocable() bool { + return xdr.AccountFlags(account.Flags).IsAuthRevocable() +} + +// IsAuthImmutable returns true if the account has the "AUTH_IMMUTABLE" option +// turned on. +func (account AccountEntry) IsAuthImmutable() bool { + return xdr.AccountFlags(account.Flags).IsAuthImmutable() +} + +func (q *Q) CountAccounts() (int, error) { + sql := sq.Select("count(*)").From("accounts") + + var count int + if err := q.Get(&count, sql); err != nil { + return 0, errors.Wrap(err, "could not run select query") + } + + return count, nil +} + +func (q *Q) GetAccountByID(id string) (AccountEntry, error) { + var account AccountEntry + sql := selectAccounts.Where(sq.Eq{"account_id": id}) + err := q.Get(&account, sql) + return account, err +} + +func (q *Q) GetAccountsByIDs(ids []string) ([]AccountEntry, error) { + var accounts []AccountEntry + sql := selectAccounts.Where(map[string]interface{}{"accounts.account_id": ids}) + err := q.Select(&accounts, sql) + return accounts, err +} + +func accountToMap(account xdr.AccountEntry, lastModifiedLedger xdr.Uint32) map[string]interface{} { + var buyingliabilities, sellingliabilities xdr.Int64 + if account.Ext.V1 != nil { + v1 := account.Ext.V1 + buyingliabilities = v1.Liabilities.Buying + sellingliabilities = v1.Liabilities.Selling + } + + var inflationDestination = "" + if account.InflationDest != nil { + inflationDestination = account.InflationDest.Address() + } + + return map[string]interface{}{ + "account_id": account.AccountId.Address(), + "balance": account.Balance, + "buying_liabilities": buyingliabilities, + "selling_liabilities": sellingliabilities, + "sequence_number": account.SeqNum, + "num_subentries": account.NumSubEntries, + "inflation_destination": inflationDestination, + "flags": account.Flags, + "home_domain": account.HomeDomain, + "master_weight": account.MasterKeyWeight(), + "threshold_low": account.ThresholdLow(), + "threshold_medium": account.ThresholdMedium(), + "threshold_high": account.ThresholdHigh(), + "last_modified_ledger": lastModifiedLedger, + } +} + +// InsertAccount creates a row in the accounts table. +// Returns number of rows affected and error. +func (q *Q) InsertAccount(account xdr.AccountEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + m := accountToMap(account, lastModifiedLedger) + + sql := sq.Insert("accounts").SetMap(m) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// UpdateAccount updates a row in the offers table. +// Returns number of rows affected and error. +func (q *Q) UpdateAccount(account xdr.AccountEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + m := accountToMap(account, lastModifiedLedger) + + accountID := m["account_id"] + delete(m, "account_id") + + sql := sq.Update("accounts").SetMap(m).Where(sq.Eq{"account_id": accountID}) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// RemoveAccount deletes a row in the offers table. +// Returns number of rows affected and error. +func (q *Q) RemoveAccount(accountID string) (int64, error) { + sql := sq.Delete("accounts").Where(sq.Eq{"account_id": accountID}) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// AccountsForAsset returns a list of `AccountEntry` rows who are trustee to an +// asset +func (q *Q) AccountsForAsset(asset xdr.Asset, page db2.PageQuery) ([]AccountEntry, error) { + var assetType, code, issuer string + asset.MustExtract(&assetType, &code, &issuer) + + sql := sq. + Select("accounts.*"). + From("accounts"). + Join("trust_lines ON accounts.account_id = trust_lines.account_id"). + Where(map[string]interface{}{ + "trust_lines.asset_type": int32(asset.Type), + "trust_lines.asset_issuer": issuer, + "trust_lines.asset_code": code, + }) + + sql, err := page.ApplyToUsingCursor(sql, "trust_lines.account_id", page.Cursor) + if err != nil { + return nil, errors.Wrap(err, "could not apply query to page") + } + + var results []AccountEntry + if err := q.Select(&results, sql); err != nil { + return nil, errors.Wrap(err, "could not run select query") + } + + return results, nil +} + +// AccountEntriesForSigner returns a list of `AccountEntry` rows for a given signer +func (q *Q) AccountEntriesForSigner(signer string, page db2.PageQuery) ([]AccountEntry, error) { + sql := sq. + Select("accounts.*"). + From("accounts"). + Join("accounts_signers ON accounts.account_id = accounts_signers.account_id"). + Where(map[string]interface{}{ + "accounts_signers.signer": signer, + }) + + sql, err := page.ApplyToUsingCursor(sql, "accounts_signers.account_id", page.Cursor) + if err != nil { + return nil, errors.Wrap(err, "could not apply query to page") + } + + var results []AccountEntry + if err := q.Select(&results, sql); err != nil { + return nil, errors.Wrap(err, "could not run select query") + } + + return results, nil +} + +var selectAccounts = sq.Select(` + account_id, + balance, + buying_liabilities, + selling_liabilities, + sequence_number, + num_subentries, + inflation_destination, + flags, + home_domain, + master_weight, + threshold_low, + threshold_medium, + threshold_high, + last_modified_ledger +`).From("accounts") diff --git a/services/horizon/internal/db2/history/accounts_batch_insert_builder.go b/services/horizon/internal/db2/history/accounts_batch_insert_builder.go new file mode 100644 index 0000000000..9b2bcca263 --- /dev/null +++ b/services/horizon/internal/db2/history/accounts_batch_insert_builder.go @@ -0,0 +1,11 @@ +package history + +import "github.com/stellar/go/xdr" + +func (i *accountsBatchInsertBuilder) Add(account xdr.AccountEntry, lastModifiedLedger xdr.Uint32) error { + return i.builder.Row(accountToMap(account, lastModifiedLedger)) +} + +func (i *accountsBatchInsertBuilder) Exec() error { + return i.builder.Exec() +} diff --git a/services/horizon/internal/db2/history/accounts_test.go b/services/horizon/internal/db2/history/accounts_test.go new file mode 100644 index 0000000000..65112f2505 --- /dev/null +++ b/services/horizon/internal/db2/history/accounts_test.go @@ -0,0 +1,336 @@ +package history + +import ( + "testing" + + "github.com/stellar/go/services/horizon/internal/db2" + "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +var ( + inflationDest = xdr.MustAddress("GBUH7T6U36DAVEKECMKN5YEBQYZVRBPNSZAAKBCO6P5HBMDFSQMQL4Z4") + + account1 = xdr.AccountEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Balance: 20000, + SeqNum: 223456789, + NumSubEntries: 10, + InflationDest: &inflationDest, + Flags: 1, + HomeDomain: "stellar.org", + Thresholds: xdr.Thresholds{1, 2, 3, 4}, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 3, + Selling: 4, + }, + }, + }, + } + + account2 = xdr.AccountEntry{ + AccountId: xdr.MustAddress("GCT2NQM5KJJEF55NPMY444C6M6CA7T33HRNCMA6ZFBIIXKNCRO6J25K7"), + Balance: 50000, + SeqNum: 648736, + NumSubEntries: 10, + InflationDest: &inflationDest, + Flags: 2, + HomeDomain: "meridian.stellar.org", + Thresholds: xdr.Thresholds{5, 6, 7, 8}, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 30, + Selling: 40, + }, + }, + }, + } + + account3 = xdr.AccountEntry{ + AccountId: xdr.MustAddress("GDPGOMFSP4IF7A4P7UBKA4UC4QTRLEHGBD6IMDIS3W3KBDNBFAQ7FXDY"), + Balance: 50000, + SeqNum: 648736, + NumSubEntries: 10, + InflationDest: &inflationDest, + Flags: 2, + Thresholds: xdr.Thresholds{5, 6, 7, 8}, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 30, + Selling: 40, + }, + }, + }, + } +) + +func TestInsertAccount(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + rows, err := q.InsertAccount(account1, 1234) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + rows, err = q.InsertAccount(account2, 1235) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + accounts, err := q.GetAccountsByIDs([]string{ + "GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB", + "GCT2NQM5KJJEF55NPMY444C6M6CA7T33HRNCMA6ZFBIIXKNCRO6J25K7", + }) + assert.NoError(t, err) + assert.Len(t, accounts, 2) + + assert.Equal(t, "GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB", accounts[0].AccountID) + assert.Equal(t, int64(20000), accounts[0].Balance) + assert.Equal(t, int64(223456789), accounts[0].SequenceNumber) + assert.Equal(t, uint32(10), accounts[0].NumSubEntries) + assert.Equal(t, "GBUH7T6U36DAVEKECMKN5YEBQYZVRBPNSZAAKBCO6P5HBMDFSQMQL4Z4", accounts[0].InflationDestination) + assert.Equal(t, uint32(1), accounts[0].Flags) + assert.Equal(t, "stellar.org", accounts[0].HomeDomain) + assert.Equal(t, byte(1), accounts[0].MasterWeight) + assert.Equal(t, byte(2), accounts[0].ThresholdLow) + assert.Equal(t, byte(3), accounts[0].ThresholdMedium) + assert.Equal(t, byte(4), accounts[0].ThresholdHigh) + assert.Equal(t, int64(3), accounts[0].BuyingLiabilities) + assert.Equal(t, int64(4), accounts[0].SellingLiabilities) +} + +func TestUpdateAccount(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + rows, err := q.InsertAccount(account1, 1234) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + modifiedAccount := account1 + modifiedAccount.Balance = 32847893 + + rows, err = q.UpdateAccount(modifiedAccount, 1235) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + keys := []string{ + "GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB", + "GCT2NQM5KJJEF55NPMY444C6M6CA7T33HRNCMA6ZFBIIXKNCRO6J25K7", + } + accounts, err := q.GetAccountsByIDs(keys) + assert.NoError(t, err) + assert.Len(t, accounts, 1) + + expectedBinary, err := modifiedAccount.MarshalBinary() + assert.NoError(t, err) + + dbEntry := xdr.AccountEntry{ + AccountId: xdr.MustAddress(accounts[0].AccountID), + Balance: xdr.Int64(accounts[0].Balance), + SeqNum: xdr.SequenceNumber(accounts[0].SequenceNumber), + NumSubEntries: xdr.Uint32(accounts[0].NumSubEntries), + InflationDest: &inflationDest, + Flags: xdr.Uint32(accounts[0].Flags), + HomeDomain: xdr.String32(accounts[0].HomeDomain), + Thresholds: xdr.Thresholds{ + accounts[0].MasterWeight, + accounts[0].ThresholdLow, + accounts[0].ThresholdMedium, + accounts[0].ThresholdHigh, + }, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: xdr.Int64(accounts[0].BuyingLiabilities), + Selling: xdr.Int64(accounts[0].SellingLiabilities), + }, + }, + }, + } + + actualBinary, err := dbEntry.MarshalBinary() + assert.NoError(t, err) + assert.Equal(t, expectedBinary, actualBinary) + assert.Equal(t, uint32(1235), accounts[0].LastModifiedLedger) +} + +func TestRemoveAccount(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + rows, err := q.InsertAccount(account1, 1234) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + rows, err = q.RemoveAccount("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB") + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + accounts, err := q.GetAccountsByIDs([]string{"GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"}) + assert.NoError(t, err) + assert.Len(t, accounts, 0) + + // Doesn't exist anymore + rows, err = q.RemoveAccount("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB") + assert.NoError(t, err) + assert.Equal(t, int64(0), rows) +} + +func TestAccountsForAsset(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + eurTrustLine.AccountId = account1.AccountId + usdTrustLine.AccountId = account2.AccountId + + _, err := q.InsertAccount(account1, 1234) + tt.Assert.NoError(err) + _, err = q.InsertAccount(account2, 1235) + tt.Assert.NoError(err) + + _, err = q.InsertTrustLine(eurTrustLine, 1234) + tt.Assert.NoError(err) + _, err = q.InsertTrustLine(usdTrustLine, 1235) + tt.Assert.NoError(err) + + pq := db2.PageQuery{ + Order: db2.OrderAscending, + Limit: db2.DefaultPageSize, + Cursor: "", + } + + accounts, err := q.AccountsForAsset(eurTrustLine.Asset, pq) + assert.NoError(t, err) + tt.Assert.Len(accounts, 1) + tt.Assert.Equal(account1.AccountId.Address(), accounts[0].AccountID) + + accounts, err = q.AccountsForAsset(usdTrustLine.Asset, pq) + assert.NoError(t, err) + tt.Assert.Len(accounts, 1) + tt.Assert.Equal(account2.AccountId.Address(), accounts[0].AccountID) + + pq.Cursor = account2.AccountId.Address() + accounts, err = q.AccountsForAsset(usdTrustLine.Asset, pq) + assert.NoError(t, err) + tt.Assert.Len(accounts, 0) +} + +func TestAccountEntriesForSigner(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + eurTrustLine.AccountId = account1.AccountId + usdTrustLine.AccountId = account2.AccountId + + _, err := q.InsertAccount(account1, 1234) + tt.Assert.NoError(err) + _, err = q.InsertAccount(account2, 1235) + tt.Assert.NoError(err) + _, err = q.InsertAccount(account3, 1235) + tt.Assert.NoError(err) + + _, err = q.InsertTrustLine(eurTrustLine, 1234) + tt.Assert.NoError(err) + _, err = q.InsertTrustLine(usdTrustLine, 1235) + tt.Assert.NoError(err) + + _, err = q.CreateAccountSigner(account1.AccountId.Address(), account1.AccountId.Address(), 1) + tt.Assert.NoError(err) + _, err = q.CreateAccountSigner(account2.AccountId.Address(), account2.AccountId.Address(), 1) + tt.Assert.NoError(err) + _, err = q.CreateAccountSigner(account3.AccountId.Address(), account3.AccountId.Address(), 1) + tt.Assert.NoError(err) + _, err = q.CreateAccountSigner(account1.AccountId.Address(), account3.AccountId.Address(), 1) + tt.Assert.NoError(err) + _, err = q.CreateAccountSigner(account2.AccountId.Address(), account3.AccountId.Address(), 1) + tt.Assert.NoError(err) + + pq := db2.PageQuery{ + Order: db2.OrderAscending, + Limit: db2.DefaultPageSize, + Cursor: "", + } + + accounts, err := q.AccountEntriesForSigner(account1.AccountId.Address(), pq) + assert.NoError(t, err) + tt.Assert.Len(accounts, 1) + tt.Assert.Equal(account1.AccountId.Address(), accounts[0].AccountID) + + accounts, err = q.AccountEntriesForSigner(account2.AccountId.Address(), pq) + assert.NoError(t, err) + tt.Assert.Len(accounts, 1) + tt.Assert.Equal(account2.AccountId.Address(), accounts[0].AccountID) + + want := map[string]bool{ + account1.AccountId.Address(): true, + account2.AccountId.Address(): true, + account3.AccountId.Address(): true, + } + + accounts, err = q.AccountEntriesForSigner(account3.AccountId.Address(), pq) + assert.NoError(t, err) + tt.Assert.Len(accounts, 3) + + for _, account := range accounts { + tt.Assert.True(want[account.AccountID]) + delete(want, account.AccountID) + } + + tt.Assert.Len(want, 0) + + pq.Cursor = accounts[len(accounts)-1].AccountID + accounts, err = q.AccountEntriesForSigner(account3.AccountId.Address(), pq) + assert.NoError(t, err) + tt.Assert.Len(accounts, 0) + + pq.Order = "desc" + accounts, err = q.AccountEntriesForSigner(account3.AccountId.Address(), pq) + assert.NoError(t, err) + tt.Assert.Len(accounts, 2) +} + +func TestGetAccountByID(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + _, err := q.InsertAccount(account1, 1234) + tt.Assert.NoError(err) + + resultAccount, err := q.GetAccountByID(account1.AccountId.Address()) + assert.NoError(t, err) + + assert.Equal(t, "GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB", resultAccount.AccountID) + assert.Equal(t, int64(20000), resultAccount.Balance) + assert.Equal(t, int64(223456789), resultAccount.SequenceNumber) + assert.Equal(t, uint32(10), resultAccount.NumSubEntries) + assert.Equal(t, "GBUH7T6U36DAVEKECMKN5YEBQYZVRBPNSZAAKBCO6P5HBMDFSQMQL4Z4", resultAccount.InflationDestination) + assert.Equal(t, uint32(1), resultAccount.Flags) + assert.Equal(t, "stellar.org", resultAccount.HomeDomain) + assert.Equal(t, byte(1), resultAccount.MasterWeight) + assert.Equal(t, byte(2), resultAccount.ThresholdLow) + assert.Equal(t, byte(3), resultAccount.ThresholdMedium) + assert.Equal(t, byte(4), resultAccount.ThresholdHigh) + assert.Equal(t, int64(3), resultAccount.BuyingLiabilities) + assert.Equal(t, int64(4), resultAccount.SellingLiabilities) +} diff --git a/services/horizon/internal/db2/history/asset_stats.go b/services/horizon/internal/db2/history/asset_stats.go new file mode 100644 index 0000000000..ec914ede7a --- /dev/null +++ b/services/horizon/internal/db2/history/asset_stats.go @@ -0,0 +1,181 @@ +package history + +import ( + "fmt" + "strings" + + sq "github.com/Masterminds/squirrel" + "github.com/stellar/go/services/horizon/internal/db2" + "github.com/stellar/go/support/db" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func assetStatToMap(assetStat ExpAssetStat) map[string]interface{} { + return map[string]interface{}{ + "asset_type": assetStat.AssetType, + "asset_code": assetStat.AssetCode, + "asset_issuer": assetStat.AssetIssuer, + "amount": assetStat.Amount, + "num_accounts": assetStat.NumAccounts, + } +} + +func assetStatToPrimaryKeyMap(assetStat ExpAssetStat) map[string]interface{} { + return map[string]interface{}{ + "asset_type": assetStat.AssetType, + "asset_code": assetStat.AssetCode, + "asset_issuer": assetStat.AssetIssuer, + } +} + +// InsertAssetStats a set of asset stats into the exp_asset_stats +func (q *Q) InsertAssetStats(assetStats []ExpAssetStat, batchSize int) error { + builder := &db.BatchInsertBuilder{ + Table: q.GetTable("exp_asset_stats"), + MaxBatchSize: batchSize, + } + + for _, assetStat := range assetStats { + if err := builder.Row(assetStatToMap(assetStat)); err != nil { + return errors.Wrap(err, "could not insert asset assetStat row") + } + } + + if err := builder.Exec(); err != nil { + return errors.Wrap(err, "could not exec asset assetStats insert builder") + } + + return nil +} + +// InsertAssetStat a single asset assetStat row into the exp_asset_stats +// Returns number of rows affected and error. +func (q *Q) InsertAssetStat(assetStat ExpAssetStat) (int64, error) { + sql := sq.Insert("exp_asset_stats").SetMap(assetStatToMap(assetStat)) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// UpdateAssetStat updates a row in the exp_asset_stats table. +// Returns number of rows affected and error. +func (q *Q) UpdateAssetStat(assetStat ExpAssetStat) (int64, error) { + sql := sq.Update("exp_asset_stats"). + SetMap(assetStatToMap(assetStat)). + Where(assetStatToPrimaryKeyMap(assetStat)) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// RemoveAssetStat removes a row in the exp_asset_stats table. +func (q *Q) RemoveAssetStat(assetType xdr.AssetType, assetCode, assetIssuer string) (int64, error) { + sql := sq.Delete("exp_asset_stats"). + Where(map[string]interface{}{ + "asset_type": assetType, + "asset_code": assetCode, + "asset_issuer": assetIssuer, + }) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// GetAssetStat returns a row in the exp_asset_stats table. +func (q *Q) GetAssetStat(assetType xdr.AssetType, assetCode, assetIssuer string) (ExpAssetStat, error) { + sql := selectAssetStats.Where(map[string]interface{}{ + "asset_type": assetType, + "asset_code": assetCode, + "asset_issuer": assetIssuer, + }) + var assetStat ExpAssetStat + err := q.Get(&assetStat, sql) + return assetStat, err +} + +func parseAssetStatsCursor(cursor string) (string, string, error) { + parts := strings.SplitN(cursor, "_", 3) + if len(parts) != 3 { + return "", "", fmt.Errorf("invalid asset stats cursor: %v", cursor) + } + + code, issuer, assetType := parts[0], parts[1], parts[2] + var issuerAccount xdr.AccountId + var asset xdr.Asset + + if err := issuerAccount.SetAddress(issuer); err != nil { + return "", "", errors.Wrap( + err, + fmt.Sprintf("invalid issuer in asset stats cursor: %v", cursor), + ) + } + + if err := asset.SetCredit(code, issuerAccount); err != nil { + return "", "", errors.Wrap( + err, + fmt.Sprintf("invalid asset stats cursor: %v", cursor), + ) + } + + if _, ok := xdr.StringToAssetType[assetType]; !ok { + return "", "", errors.Errorf("invalid asset type in asset stats cursor: %v", cursor) + } + + return code, issuer, nil +} + +// GetAssetStats returns a page of exp_asset_stats rows. +func (q *Q) GetAssetStats(assetCode, assetIssuer string, page db2.PageQuery) ([]ExpAssetStat, error) { + sql := selectAssetStats + filters := map[string]interface{}{} + if assetCode != "" { + filters["asset_code"] = assetCode + } + if assetIssuer != "" { + filters["asset_issuer"] = assetIssuer + } + + if len(filters) > 0 { + sql = sql.Where(filters) + } + + var cursorComparison, orderBy string + switch page.Order { + case "asc": + cursorComparison, orderBy = ">", "asc" + case "desc": + cursorComparison, orderBy = "<", "desc" + default: + return nil, fmt.Errorf("invalid page order %s", page.Order) + } + + if page.Cursor != "" { + cursorCode, cursorIssuer, err := parseAssetStatsCursor(page.Cursor) + if err != nil { + return nil, err + } + + sql = sql.Where("((asset_code, asset_issuer) "+cursorComparison+" (?,?))", cursorCode, cursorIssuer) + } + + sql = sql.OrderBy("(asset_code, asset_issuer) " + orderBy).Limit(page.Limit) + + var results []ExpAssetStat + if err := q.Select(&results, sql); err != nil { + return nil, errors.Wrap(err, "could not run select query") + } + + return results, nil +} + +var selectAssetStats = sq.Select("exp_asset_stats.*").From("exp_asset_stats") diff --git a/services/horizon/internal/db2/history/asset_stats_test.go b/services/horizon/internal/db2/history/asset_stats_test.go new file mode 100644 index 0000000000..7924a3f916 --- /dev/null +++ b/services/horizon/internal/db2/history/asset_stats_test.go @@ -0,0 +1,618 @@ +package history + +import ( + "database/sql" + "testing" + + "github.com/stellar/go/services/horizon/internal/db2" + "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/xdr" +) + +func TestInsertAssetStats(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + tt.Assert.NoError(q.InsertAssetStats([]ExpAssetStat{}, 1)) + + assetStats := []ExpAssetStat{ + ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + }, + ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "ETHER", + Amount: "23", + NumAccounts: 1, + }, + } + tt.Assert.NoError(q.InsertAssetStats(assetStats, 1)) + + for _, assetStat := range assetStats { + got, err := q.GetAssetStat(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + tt.Assert.NoError(err) + tt.Assert.Equal(got, assetStat) + } +} + +func TestInsertAssetStat(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + assetStats := []ExpAssetStat{ + ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + }, + ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "ETHER", + Amount: "23", + NumAccounts: 1, + }, + } + + for _, assetStat := range assetStats { + numChanged, err := q.InsertAssetStat(assetStat) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + + got, err := q.GetAssetStat(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + tt.Assert.NoError(err) + tt.Assert.Equal(got, assetStat) + } +} + +func TestInsertAssetStatAlreadyExistsError(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + assetStat := ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + + numChanged, err := q.InsertAssetStat(assetStat) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + + numChanged, err = q.InsertAssetStat(assetStat) + tt.Assert.Error(err) + tt.Assert.Equal(numChanged, int64(0)) + + assetStat.NumAccounts = 4 + assetStat.Amount = "3" + numChanged, err = q.InsertAssetStat(assetStat) + tt.Assert.Error(err) + tt.Assert.Equal(numChanged, int64(0)) + + assetStat.NumAccounts = 2 + assetStat.Amount = "1" + got, err := q.GetAssetStat(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + tt.Assert.NoError(err) + tt.Assert.Equal(got, assetStat) +} + +func TestUpdateAssetStatDoesNotExistsError(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + assetStat := ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + + numChanged, err := q.UpdateAssetStat(assetStat) + tt.Assert.Nil(err) + tt.Assert.Equal(numChanged, int64(0)) + + _, err = q.GetAssetStat(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + tt.Assert.Equal(err, sql.ErrNoRows) +} + +func TestUpdateStat(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + assetStat := ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + + numChanged, err := q.InsertAssetStat(assetStat) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + + got, err := q.GetAssetStat(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + tt.Assert.NoError(err) + tt.Assert.Equal(got, assetStat) + + assetStat.NumAccounts = 50 + assetStat.Amount = "23" + + numChanged, err = q.UpdateAssetStat(assetStat) + tt.Assert.Nil(err) + tt.Assert.Equal(numChanged, int64(1)) + + got, err = q.GetAssetStat(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + tt.Assert.NoError(err) + tt.Assert.Equal(got, assetStat) +} + +func TestGetAssetStatDoesNotExist(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + assetStat := ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + + _, err := q.GetAssetStat(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + tt.Assert.Equal(err, sql.ErrNoRows) +} + +func TestRemoveAssetStat(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + assetStat := ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + + numChanged, err := q.RemoveAssetStat( + assetStat.AssetType, + assetStat.AssetCode, + assetStat.AssetIssuer, + ) + tt.Assert.Nil(err) + tt.Assert.Equal(numChanged, int64(0)) + + numChanged, err = q.InsertAssetStat(assetStat) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + + got, err := q.GetAssetStat(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + tt.Assert.NoError(err) + tt.Assert.Equal(got, assetStat) + + numChanged, err = q.RemoveAssetStat( + assetStat.AssetType, + assetStat.AssetCode, + assetStat.AssetIssuer, + ) + tt.Assert.Nil(err) + tt.Assert.Equal(numChanged, int64(1)) + + _, err = q.GetAssetStat(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + tt.Assert.Equal(err, sql.ErrNoRows) +} + +func TestGetAssetStatsCursorValidation(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + for _, testCase := range []struct { + name string + cursor string + expectedError string + }{ + { + "cursor does not use underscore as serpator", + "usdc-GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + "invalid asset stats cursor", + }, + { + "cursor has no underscore", + "usdc", + "invalid asset stats cursor", + }, + { + "cursor has too many underscores", + "usdc_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4_", + "invalid asset type in asset stats cursor", + }, + { + "issuer in cursor is invalid", + "usd_abcdefghijklmnopqrstuv_credit_alphanum4", + "invalid issuer in asset stats cursor", + }, + { + "asset type in cursor is invalid", + "usd_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum", + "invalid asset type in asset stats cursor", + }, + { + "asset code in cursor is too long", + "abcdefghijklmnopqrstuv_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum12", + "invalid asset stats cursor", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + page := db2.PageQuery{ + Cursor: testCase.cursor, + Order: "asc", + Limit: 5, + } + results, err := q.GetAssetStats("", "", page) + tt.Assert.Empty(results) + tt.Assert.NotNil(err) + tt.Assert.Contains(err.Error(), testCase.expectedError) + }) + } +} + +func TestGetAssetStatsOrderValidation(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + page := db2.PageQuery{ + Order: "invalid", + Limit: 5, + } + results, err := q.GetAssetStats("", "", page) + tt.Assert.Empty(results) + tt.Assert.NotNil(err) + tt.Assert.Contains(err.Error(), "invalid page order") +} + +func reverseAssetStats(a []ExpAssetStat) { + for i := len(a)/2 - 1; i >= 0; i-- { + opp := len(a) - 1 - i + a[i], a[opp] = a[opp], a[i] + } +} + +func TestGetAssetStatsFiltersAndCursor(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + usdAssetStat := ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + etherAssetStat := ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "ETHER", + Amount: "23", + NumAccounts: 1, + } + otherUSDAssetStat := ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + AssetCode: "USD", + Amount: "1", + NumAccounts: 2, + } + eurAssetStat := ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + AssetCode: "EUR", + Amount: "111", + NumAccounts: 3, + } + assetStats := []ExpAssetStat{ + etherAssetStat, + eurAssetStat, + otherUSDAssetStat, + usdAssetStat, + } + for _, assetStat := range assetStats { + numChanged, err := q.InsertAssetStat(assetStat) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + } + + for _, testCase := range []struct { + name string + assetCode string + assetIssuer string + cursor string + order string + expected []ExpAssetStat + }{ + { + "no filter without cursor", + "", + "", + "", + "asc", + []ExpAssetStat{ + etherAssetStat, + eurAssetStat, + otherUSDAssetStat, + usdAssetStat, + }, + }, + { + "no filter with cursor", + "", + "", + "ABC_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "asc", + []ExpAssetStat{ + etherAssetStat, + eurAssetStat, + otherUSDAssetStat, + usdAssetStat, + }, + }, + { + "no filter with cursor descending", + "", + "", + "ZZZ_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4", + "desc", + []ExpAssetStat{ + usdAssetStat, + otherUSDAssetStat, + eurAssetStat, + etherAssetStat, + }, + }, + { + "no filter with cursor and offset", + "", + "", + "ETHER_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum12", + "asc", + []ExpAssetStat{ + eurAssetStat, + otherUSDAssetStat, + usdAssetStat, + }, + }, + { + "no filter with cursor and offset descending", + "", + "", + "EUR_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "desc", + []ExpAssetStat{ + etherAssetStat, + }, + }, + { + "no filter with cursor and offset descending including eur", + "", + "", + "EUR_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4", + "desc", + []ExpAssetStat{ + eurAssetStat, + etherAssetStat, + }, + }, + { + "filter on code without cursor", + "USD", + "", + "", + "asc", + []ExpAssetStat{ + otherUSDAssetStat, + usdAssetStat, + }, + }, + { + "filter on code with cursor", + "USD", + "", + "USD_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "asc", + []ExpAssetStat{ + usdAssetStat, + }, + }, + { + "filter on code with cursor descending", + "USD", + "", + "USD_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4", + "desc", + []ExpAssetStat{ + otherUSDAssetStat, + }, + }, + { + "filter on issuer without cursor", + "", + "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "", + "asc", + []ExpAssetStat{ + eurAssetStat, + otherUSDAssetStat, + }, + }, + { + "filter on issuer with cursor", + "", + "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "EUR_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "asc", + []ExpAssetStat{ + otherUSDAssetStat, + }, + }, + { + "filter on issuer with cursor descending", + "", + "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "USD_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "desc", + []ExpAssetStat{ + eurAssetStat, + }, + }, + { + "filter on non existant code without cursor", + "BTC", + "", + "", + "asc", + nil, + }, + { + "filter on non existant code with cursor", + "BTC", + "", + "BTC_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "asc", + nil, + }, + { + "filter on non existant issuer without cursor", + "", + "GAEIHD6U4WSBHJGA2HPWOQ3OQEFQ3Y7QZE2DR76YKZNKPW5YDLYW4UGF", + "", + "asc", + nil, + }, + { + "filter on non existant issuer with cursor", + "", + "GAEIHD6U4WSBHJGA2HPWOQ3OQEFQ3Y7QZE2DR76YKZNKPW5YDLYW4UGF", + "AAA_GAEIHD6U4WSBHJGA2HPWOQ3OQEFQ3Y7QZE2DR76YKZNKPW5YDLYW4UGF_credit_alphanum4", + "asc", + nil, + }, + { + "filter on non existant code and non existant issuer without cursor", + "BTC", + "GAEIHD6U4WSBHJGA2HPWOQ3OQEFQ3Y7QZE2DR76YKZNKPW5YDLYW4UGF", + "", + "asc", + nil, + }, + { + "filter on non existant code and non existant issuer with cursor", + "BTC", + "GAEIHD6U4WSBHJGA2HPWOQ3OQEFQ3Y7QZE2DR76YKZNKPW5YDLYW4UGF", + "AAA_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "asc", + nil, + }, + { + "filter on both code and issuer without cursor", + "USD", + "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "", + "asc", + []ExpAssetStat{ + otherUSDAssetStat, + }, + }, + { + "filter on both code and issuer with cursor", + "USD", + "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "USC_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "asc", + []ExpAssetStat{ + otherUSDAssetStat, + }, + }, + { + "filter on both code and issuer with cursor descending", + "USD", + "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "USE_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "desc", + []ExpAssetStat{ + otherUSDAssetStat, + }, + }, + { + "cursor negates filter", + "USD", + "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + "USD_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", + "asc", + nil, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + page := db2.PageQuery{ + Order: testCase.order, + Cursor: testCase.cursor, + Limit: 5, + } + results, err := q.GetAssetStats(testCase.assetCode, testCase.assetIssuer, page) + tt.Assert.NoError(err) + tt.Assert.Equal(testCase.expected, results) + + page.Limit = 1 + results, err = q.GetAssetStats(testCase.assetCode, testCase.assetIssuer, page) + tt.Assert.NoError(err) + if len(testCase.expected) == 0 { + tt.Assert.Equal(testCase.expected, results) + } else { + tt.Assert.Equal(testCase.expected[:1], results) + } + + if page.Cursor == "" { + page = page.Invert() + page.Limit = 5 + + results, err = q.GetAssetStats(testCase.assetCode, testCase.assetIssuer, page) + tt.Assert.NoError(err) + reverseAssetStats(results) + tt.Assert.Equal(testCase.expected, results) + } + }) + } +} diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index a3e15a98f7..476455861c 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -3,6 +3,7 @@ package history import ( + "fmt" "sync" "time" @@ -111,8 +112,12 @@ const ( // ExperimentalIngestionTables is a list of tables populated by the experimental // ingestion system var ExperimentalIngestionTables = []string{ + "accounts", + "accounts_data", "accounts_signers", + "exp_asset_stats", "offers", + "trust_lines", } // Account is a row of data from the `history_accounts` table @@ -129,9 +134,45 @@ type AccountsQ struct { sql sq.SelectBuilder } +// AccountEntry is a row of data from the `account` table +type AccountEntry struct { + AccountID string `db:"account_id"` + Balance int64 `db:"balance"` + BuyingLiabilities int64 `db:"buying_liabilities"` + SellingLiabilities int64 `db:"selling_liabilities"` + SequenceNumber int64 `db:"sequence_number"` + NumSubEntries uint32 `db:"num_subentries"` + InflationDestination string `db:"inflation_destination"` + HomeDomain string `db:"home_domain"` + Flags uint32 `db:"flags"` + MasterWeight byte `db:"master_weight"` + ThresholdLow byte `db:"threshold_low"` + ThresholdMedium byte `db:"threshold_medium"` + ThresholdHigh byte `db:"threshold_high"` + LastModifiedLedger uint32 `db:"last_modified_ledger"` +} + +type AccountsBatchInsertBuilder interface { + Add(account xdr.AccountEntry, lastModifiedLedger xdr.Uint32) error + Exec() error +} + +// accountsBatchInsertBuilder is a simple wrapper around db.BatchInsertBuilder +type accountsBatchInsertBuilder struct { + builder db.BatchInsertBuilder +} + +// QAccounts defines account related queries. +type QAccounts interface { + NewAccountsBatchInsertBuilder(maxBatchSize int) AccountsBatchInsertBuilder + InsertAccount(account xdr.AccountEntry, lastModifiedLedger xdr.Uint32) (int64, error) + UpdateAccount(account xdr.AccountEntry, lastModifiedLedger xdr.Uint32) (int64, error) + RemoveAccount(accountID string) (int64, error) +} + // AccountSigner is a row of data from the `accounts_signers` table type AccountSigner struct { - Account string `db:"account"` + Account string `db:"account_id"` Signer string `db:"signer"` Weight int32 `db:"weight"` } @@ -146,6 +187,34 @@ type accountSignersBatchInsertBuilder struct { builder db.BatchInsertBuilder } +// Data is a row of data from the `account_data` table +type Data struct { + AccountID string `db:"account_id"` + Name string `db:"name"` + Value AccountDataValue `db:"value"` + LastModifiedLedger uint32 `db:"last_modified_ledger"` +} + +type AccountDataValue []byte + +type AccountDataBatchInsertBuilder interface { + Add(data xdr.DataEntry, lastModifiedLedger xdr.Uint32) error + Exec() error +} + +// accountDataBatchInsertBuilder is a simple wrapper around db.BatchInsertBuilder +type accountDataBatchInsertBuilder struct { + builder db.BatchInsertBuilder +} + +// QData defines account data related queries. +type QData interface { + NewAccountDataBatchInsertBuilder(maxBatchSize int) AccountDataBatchInsertBuilder + InsertAccountData(data xdr.DataEntry, lastModifiedLedger xdr.Uint32) (int64, error) + UpdateAccountData(data xdr.DataEntry, lastModifiedLedger xdr.Uint32) (int64, error) + RemoveAccountData(key xdr.LedgerKeyData) (int64, error) +} + // Asset is a row of data from the `history_assets` table type Asset struct { ID int64 `db:"id"` @@ -163,6 +232,35 @@ type AssetStat struct { Toml string `db:"toml"` } +// ExpAssetStat is a row in the exp_asset_stats table representing the stats per Asset +type ExpAssetStat struct { + AssetType xdr.AssetType `db:"asset_type"` + AssetCode string `db:"asset_code"` + AssetIssuer string `db:"asset_issuer"` + Amount string `db:"amount"` + NumAccounts int32 `db:"num_accounts"` +} + +// PagingToken returns a cursor for this asset stat +func (e ExpAssetStat) PagingToken() string { + return fmt.Sprintf( + "%s_%s_%s", + e.AssetCode, + e.AssetIssuer, + xdr.AssetTypeToString[e.AssetType], + ) +} + +// QAssetStats defines exp_asset_stats related queries. +type QAssetStats interface { + InsertAssetStats(stats []ExpAssetStat, batchSize int) error + InsertAssetStat(stat ExpAssetStat) (int64, error) + UpdateAssetStat(stat ExpAssetStat) (int64, error) + GetAssetStat(assetType xdr.AssetType, assetCode, assetIssuer string) (ExpAssetStat, error) + RemoveAssetStat(assetType xdr.AssetType, assetCode, assetIssuer string) (int64, error) + GetAssetStats(assetCode, assetIssuer string, page db2.PageQuery) ([]ExpAssetStat, error) +} + // Effect is a row of data from the `history_effects` table type Effect struct { HistoryAccountID int64 `db:"history_account_id"` @@ -281,13 +379,13 @@ type Operation struct { TransactionSuccessful *bool `db:"transaction_successful"` } -// Offer is row of data from the `offers` table from stellar-core +// Offer is row of data from the `offers` table from horizon DB type Offer struct { - SellerID string `db:"sellerid"` - OfferID xdr.Int64 `db:"offerid"` + SellerID string `db:"seller_id"` + OfferID xdr.Int64 `db:"offer_id"` - SellingAsset xdr.Asset `db:"sellingasset"` - BuyingAsset xdr.Asset `db:"buyingasset"` + SellingAsset xdr.Asset `db:"selling_asset"` + BuyingAsset xdr.Asset `db:"buying_asset"` Amount xdr.Int64 `db:"amount"` Pricen int32 `db:"pricen"` @@ -430,6 +528,47 @@ type TransactionsQ struct { includeFailed bool } +// TrustLine is row of data from the `trust_lines` table from horizon DB +type TrustLine struct { + AccountID string `db:"account_id"` + AssetType xdr.AssetType `db:"asset_type"` + AssetIssuer string `db:"asset_issuer"` + AssetCode string `db:"asset_code"` + Balance int64 `db:"balance"` + Limit int64 `db:"trust_line_limit"` + BuyingLiabilities int64 `db:"buying_liabilities"` + SellingLiabilities int64 `db:"selling_liabilities"` + Flags uint32 `db:"flags"` + LastModifiedLedger uint32 `db:"last_modified_ledger"` +} + +// QTrustLines defines trust lines related queries. +type QTrustLines interface { + NewTrustLinesBatchInsertBuilder(maxBatchSize int) TrustLinesBatchInsertBuilder + InsertTrustLine(trustLine xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) (int64, error) + UpdateTrustLine(trustLine xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) (int64, error) + RemoveTrustLine(key xdr.LedgerKeyTrustLine) (int64, error) +} + +type TrustLinesBatchInsertBuilder interface { + Add(trustLine xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) error + Exec() error +} + +// trustLinesBatchInsertBuilder is a simple wrapper around db.BatchInsertBuilder +type trustLinesBatchInsertBuilder struct { + builder db.BatchInsertBuilder +} + +func (q *Q) NewAccountsBatchInsertBuilder(maxBatchSize int) AccountsBatchInsertBuilder { + return &accountsBatchInsertBuilder{ + builder: db.BatchInsertBuilder{ + Table: q.GetTable("accounts"), + MaxBatchSize: maxBatchSize, + }, + } +} + func (q *Q) NewAccountSignersBatchInsertBuilder(maxBatchSize int) AccountSignersBatchInsertBuilder { return &accountSignersBatchInsertBuilder{ builder: db.BatchInsertBuilder{ @@ -439,6 +578,15 @@ func (q *Q) NewAccountSignersBatchInsertBuilder(maxBatchSize int) AccountSigners } } +func (q *Q) NewAccountDataBatchInsertBuilder(maxBatchSize int) AccountDataBatchInsertBuilder { + return &accountDataBatchInsertBuilder{ + builder: db.BatchInsertBuilder{ + Table: q.GetTable("accounts_data"), + MaxBatchSize: maxBatchSize, + }, + } +} + func (q *Q) NewOffersBatchInsertBuilder(maxBatchSize int) OffersBatchInsertBuilder { return &offersBatchInsertBuilder{ builder: db.BatchInsertBuilder{ @@ -448,6 +596,15 @@ func (q *Q) NewOffersBatchInsertBuilder(maxBatchSize int) OffersBatchInsertBuild } } +func (q *Q) NewTrustLinesBatchInsertBuilder(maxBatchSize int) TrustLinesBatchInsertBuilder { + return &trustLinesBatchInsertBuilder{ + builder: db.BatchInsertBuilder{ + Table: q.GetTable("trust_lines"), + MaxBatchSize: maxBatchSize, + }, + } +} + // ElderLedger loads the oldest ledger known to the history database func (q *Q) ElderLedger(dest interface{}) error { return q.GetRaw(dest, `SELECT COALESCE(MIN(sequence), 0) FROM history_ledgers`) diff --git a/services/horizon/internal/db2/history/mock_account_data_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_account_data_batch_insert_builder.go new file mode 100644 index 0000000000..d1eec1fdc9 --- /dev/null +++ b/services/horizon/internal/db2/history/mock_account_data_batch_insert_builder.go @@ -0,0 +1,20 @@ +package history + +import ( + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/mock" +) + +type MockAccountDataBatchInsertBuilder struct { + mock.Mock +} + +func (m *MockAccountDataBatchInsertBuilder) Add(data xdr.DataEntry, lastModifiedLedger xdr.Uint32) error { + a := m.Called(data, lastModifiedLedger) + return a.Error(0) +} + +func (m *MockAccountDataBatchInsertBuilder) Exec() error { + a := m.Called() + return a.Error(0) +} diff --git a/services/horizon/internal/db2/history/mock_accounts_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_accounts_batch_insert_builder.go new file mode 100644 index 0000000000..5624f58100 --- /dev/null +++ b/services/horizon/internal/db2/history/mock_accounts_batch_insert_builder.go @@ -0,0 +1,20 @@ +package history + +import ( + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/mock" +) + +type MockAccountsBatchInsertBuilder struct { + mock.Mock +} + +func (m *MockAccountsBatchInsertBuilder) Add(accounts xdr.AccountEntry, lastModifiedLedger xdr.Uint32) error { + a := m.Called(accounts, lastModifiedLedger) + return a.Error(0) +} + +func (m *MockAccountsBatchInsertBuilder) Exec() error { + a := m.Called() + return a.Error(0) +} diff --git a/services/horizon/internal/db2/history/mock_q_accounts.go b/services/horizon/internal/db2/history/mock_q_accounts.go new file mode 100644 index 0000000000..85b47d2949 --- /dev/null +++ b/services/horizon/internal/db2/history/mock_q_accounts.go @@ -0,0 +1,37 @@ +package history + +import ( + "github.com/stretchr/testify/mock" + + "github.com/stellar/go/xdr" +) + +// MockQAccounts is a mock implementation of the QAccounts interface +type MockQAccounts struct { + mock.Mock +} + +func (m *MockQAccounts) GetAccountsByIDs(ids []string) ([]AccountEntry, error) { + a := m.Called() + return a.Get(0).([]AccountEntry), a.Error(1) +} + +func (m *MockQAccounts) NewAccountsBatchInsertBuilder(maxBatchSize int) AccountsBatchInsertBuilder { + a := m.Called(maxBatchSize) + return a.Get(0).(AccountsBatchInsertBuilder) +} + +func (m *MockQAccounts) InsertAccount(account xdr.AccountEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + a := m.Called(account, lastModifiedLedger) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQAccounts) UpdateAccount(account xdr.AccountEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + a := m.Called(account, lastModifiedLedger) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQAccounts) RemoveAccount(accountID string) (int64, error) { + a := m.Called(accountID) + return a.Get(0).(int64), a.Error(1) +} diff --git a/services/horizon/internal/db2/history/mock_q_asset_stats.go b/services/horizon/internal/db2/history/mock_q_asset_stats.go new file mode 100644 index 0000000000..b782478433 --- /dev/null +++ b/services/horizon/internal/db2/history/mock_q_asset_stats.go @@ -0,0 +1,42 @@ +package history + +import ( + "github.com/stellar/go/services/horizon/internal/db2" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/mock" +) + +// MockQAssetStats is a mock implementation of the QAssetStats interface +type MockQAssetStats struct { + mock.Mock +} + +func (m *MockQAssetStats) InsertAssetStats(assetStats []ExpAssetStat, batchSize int) error { + a := m.Called(assetStats, batchSize) + return a.Error(0) +} + +func (m *MockQAssetStats) InsertAssetStat(assetStat ExpAssetStat) (int64, error) { + a := m.Called(assetStat) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQAssetStats) UpdateAssetStat(assetStat ExpAssetStat) (int64, error) { + a := m.Called(assetStat) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQAssetStats) GetAssetStat(assetType xdr.AssetType, assetCode, assetIssuer string) (ExpAssetStat, error) { + a := m.Called(assetType, assetCode, assetIssuer) + return a.Get(0).(ExpAssetStat), a.Error(1) +} + +func (m *MockQAssetStats) RemoveAssetStat(assetType xdr.AssetType, assetCode, assetIssuer string) (int64, error) { + a := m.Called(assetType, assetCode, assetIssuer) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQAssetStats) GetAssetStats(assetCode, assetIssuer string, page db2.PageQuery) ([]ExpAssetStat, error) { + a := m.Called(assetCode, assetIssuer, page) + return a.Get(0).([]ExpAssetStat), a.Error(1) +} diff --git a/services/horizon/internal/db2/history/mock_q_data.go b/services/horizon/internal/db2/history/mock_q_data.go new file mode 100644 index 0000000000..74191fc3fa --- /dev/null +++ b/services/horizon/internal/db2/history/mock_q_data.go @@ -0,0 +1,37 @@ +package history + +import ( + "github.com/stretchr/testify/mock" + + "github.com/stellar/go/xdr" +) + +// MockQData is a mock implementation of the QAccounts interface +type MockQData struct { + mock.Mock +} + +func (m *MockQData) GetAccountDataByKeys(keys []xdr.LedgerKeyData) ([]Data, error) { + a := m.Called() + return a.Get(0).([]Data), a.Error(1) +} + +func (m *MockQData) NewAccountDataBatchInsertBuilder(maxBatchSize int) AccountDataBatchInsertBuilder { + a := m.Called(maxBatchSize) + return a.Get(0).(AccountDataBatchInsertBuilder) +} + +func (m *MockQData) InsertAccountData(data xdr.DataEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + a := m.Called(data, lastModifiedLedger) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQData) UpdateAccountData(data xdr.DataEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + a := m.Called(data, lastModifiedLedger) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQData) RemoveAccountData(key xdr.LedgerKeyData) (int64, error) { + a := m.Called(key) + return a.Get(0).(int64), a.Error(1) +} diff --git a/services/horizon/internal/db2/history/q_offers_mock.go b/services/horizon/internal/db2/history/mock_q_offers.go similarity index 100% rename from services/horizon/internal/db2/history/q_offers_mock.go rename to services/horizon/internal/db2/history/mock_q_offers.go diff --git a/services/horizon/internal/db2/history/q_signers_mock.go b/services/horizon/internal/db2/history/mock_q_signers.go similarity index 100% rename from services/horizon/internal/db2/history/q_signers_mock.go rename to services/horizon/internal/db2/history/mock_q_signers.go diff --git a/services/horizon/internal/db2/history/mock_q_trust_lines.go b/services/horizon/internal/db2/history/mock_q_trust_lines.go new file mode 100644 index 0000000000..e0cd6c6072 --- /dev/null +++ b/services/horizon/internal/db2/history/mock_q_trust_lines.go @@ -0,0 +1,32 @@ +package history + +import ( + "github.com/stretchr/testify/mock" + + "github.com/stellar/go/xdr" +) + +// MockQTrustLines is a mock implementation of the QOffers interface +type MockQTrustLines struct { + mock.Mock +} + +func (m *MockQTrustLines) NewTrustLinesBatchInsertBuilder(maxBatchSize int) TrustLinesBatchInsertBuilder { + a := m.Called(maxBatchSize) + return a.Get(0).(TrustLinesBatchInsertBuilder) +} + +func (m *MockQTrustLines) InsertTrustLine(trustLine xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + a := m.Called(trustLine, lastModifiedLedger) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQTrustLines) UpdateTrustLine(trustLine xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + a := m.Called(trustLine, lastModifiedLedger) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQTrustLines) RemoveTrustLine(key xdr.LedgerKeyTrustLine) (int64, error) { + a := m.Called(key) + return a.Get(0).(int64), a.Error(1) +} diff --git a/services/horizon/internal/db2/history/mock_trust_lines_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_trust_lines_batch_insert_builder.go new file mode 100644 index 0000000000..18ec1a25a6 --- /dev/null +++ b/services/horizon/internal/db2/history/mock_trust_lines_batch_insert_builder.go @@ -0,0 +1,20 @@ +package history + +import ( + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/mock" +) + +type MockTrustLinesBatchInsertBuilder struct { + mock.Mock +} + +func (m *MockTrustLinesBatchInsertBuilder) Add(trustLines xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) error { + a := m.Called(trustLines, lastModifiedLedger) + return a.Error(0) +} + +func (m *MockTrustLinesBatchInsertBuilder) Exec() error { + a := m.Called() + return a.Error(0) +} diff --git a/services/horizon/internal/db2/history/offers.go b/services/horizon/internal/db2/history/offers.go index 8289dec9c6..482963086a 100644 --- a/services/horizon/internal/db2/history/offers.go +++ b/services/horizon/internal/db2/history/offers.go @@ -21,7 +21,7 @@ func (q *Q) CountOffers() (int, error) { // GetOfferByID loads a row from the `offers` table, selected by offerid. func (q *Q) GetOfferByID(id int64) (Offer, error) { var offer Offer - sql := selectOffers.Where("offers.offerid = ?", id) + sql := selectOffers.Where("offers.offer_id = ?", id) err := q.Get(&offer, sql) return offer, err } @@ -29,7 +29,7 @@ func (q *Q) GetOfferByID(id int64) (Offer, error) { // GetOffersByIDs loads a row from the `offers` table, selected by multiple offerid. func (q *Q) GetOffersByIDs(ids []int64) ([]Offer, error) { var offers []Offer - sql := selectOffers.Where(map[string]interface{}{"offers.offerid": ids}) + sql := selectOffers.Where(map[string]interface{}{"offers.offer_id": ids}) err := q.Select(&offers, sql) return offers, err } @@ -37,14 +37,14 @@ func (q *Q) GetOffersByIDs(ids []int64) ([]Offer, error) { // GetOffers loads rows from `offers` by paging query. func (q *Q) GetOffers(query OffersQuery) ([]Offer, error) { sql := selectOffers - sql, err := query.PageQuery.ApplyTo(sql, "offers.offerid") + sql, err := query.PageQuery.ApplyTo(sql, "offers.offer_id") if err != nil { return nil, errors.Wrap(err, "could not apply query to page") } if query.SellerID != "" { - sql = sql.Where("offers.sellerid = ?", query.SellerID) + sql = sql.Where("offers.seller_id = ?", query.SellerID) } if query.Selling != nil { @@ -52,7 +52,7 @@ func (q *Q) GetOffers(query OffersQuery) ([]Offer, error) { if err != nil { return nil, errors.Wrap(err, "cannot marshal selling asset") } - sql = sql.Where("offers.sellingasset = ?", sellingAsset) + sql = sql.Where("offers.selling_asset = ?", sellingAsset) } if query.Buying != nil { @@ -60,7 +60,7 @@ func (q *Q) GetOffers(query OffersQuery) ([]Offer, error) { if err != nil { return nil, errors.Wrap(err, "cannot marshal Buying asset") } - sql = sql.Where("offers.buyingasset = ?", buyingAsset) + sql = sql.Where("offers.buying_asset = ?", buyingAsset) } var offers []Offer @@ -95,10 +95,10 @@ func offerToMap(offer xdr.OfferEntry, lastModifiedLedger xdr.Uint32) (map[string } return map[string]interface{}{ - "sellerid": offer.SellerId.Address(), - "offerid": offer.OfferId, - "sellingasset": sellingAsset, - "buyingasset": buyingAsset, + "seller_id": offer.SellerId.Address(), + "offer_id": offer.OfferId, + "selling_asset": sellingAsset, + "buying_asset": buyingAsset, "amount": offer.Amount, "pricen": offer.Price.N, "priced": offer.Price.D, @@ -133,10 +133,10 @@ func (q *Q) UpdateOffer(offer xdr.OfferEntry, lastModifiedLedger xdr.Uint32) (in return 0, err } - offerID := m["offerid"] - delete(m, "offerid") + offerID := m["offer_id"] + delete(m, "offer_id") - sql := sq.Update("offers").SetMap(m).Where(sq.Eq{"offerid": offerID}) + sql := sq.Update("offers").SetMap(m).Where(sq.Eq{"offer_id": offerID}) result, err := q.Exec(sql) if err != nil { return 0, err @@ -148,7 +148,7 @@ func (q *Q) UpdateOffer(offer xdr.OfferEntry, lastModifiedLedger xdr.Uint32) (in // RemoveOffer deletes a row in the offers table. // Returns number of rows affected and error. func (q *Q) RemoveOffer(offerID xdr.Int64) (int64, error) { - sql := sq.Delete("offers").Where(sq.Eq{"offerid": offerID}) + sql := sq.Delete("offers").Where(sq.Eq{"offer_id": offerID}) result, err := q.Exec(sql) if err != nil { return 0, err @@ -158,10 +158,10 @@ func (q *Q) RemoveOffer(offerID xdr.Int64) (int64, error) { } var selectOffers = sq.Select(` - sellerid, - offerid, - sellingasset, - buyingasset, + seller_id, + offer_id, + selling_asset, + buying_asset, amount, pricen, priced, diff --git a/services/horizon/internal/db2/history/offers_batch_insert_builder.go b/services/horizon/internal/db2/history/offers_batch_insert_builder.go index 3e6463a164..d420fe511b 100644 --- a/services/horizon/internal/db2/history/offers_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/offers_batch_insert_builder.go @@ -24,10 +24,10 @@ func (i *offersBatchInsertBuilder) Add(offer xdr.OfferEntry, lastModifiedLedger } return i.builder.Row(map[string]interface{}{ - "sellerid": offer.SellerId.Address(), - "offerid": offer.OfferId, - "sellingasset": sellingAsset, - "buyingasset": buyingAsset, + "seller_id": offer.SellerId.Address(), + "offer_id": offer.OfferId, + "selling_asset": sellingAsset, + "buying_asset": buyingAsset, "amount": offer.Amount, "pricen": offer.Price.N, "priced": offer.Price.D, diff --git a/services/horizon/internal/db2/history/operation.go b/services/horizon/internal/db2/history/operation.go index e868228cd0..70bee4c2da 100644 --- a/services/horizon/internal/db2/history/operation.go +++ b/services/horizon/internal/db2/history/operation.go @@ -46,17 +46,17 @@ func (q *Q) OperationFeeStats(currentSeq int32, dest *FeeStats) error { SELECT ceil(min(max_fee/operation_count))::bigint AS "min", ceil(mode() within group (order by max_fee/operation_count))::bigint AS "mode", - ceil(percentile_cont(0.10) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p10", - ceil(percentile_cont(0.20) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p20", - ceil(percentile_cont(0.30) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p30", - ceil(percentile_cont(0.40) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p40", - ceil(percentile_cont(0.50) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p50", - ceil(percentile_cont(0.60) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p60", - ceil(percentile_cont(0.70) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p70", - ceil(percentile_cont(0.80) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p80", - ceil(percentile_cont(0.90) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p90", - ceil(percentile_cont(0.95) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p95", - ceil(percentile_cont(0.99) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p99" + ceil(percentile_disc(0.10) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p10", + ceil(percentile_disc(0.20) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p20", + ceil(percentile_disc(0.30) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p30", + ceil(percentile_disc(0.40) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p40", + ceil(percentile_disc(0.50) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p50", + ceil(percentile_disc(0.60) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p60", + ceil(percentile_disc(0.70) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p70", + ceil(percentile_disc(0.80) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p80", + ceil(percentile_disc(0.90) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p90", + ceil(percentile_disc(0.95) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p95", + ceil(percentile_disc(0.99) WITHIN GROUP (ORDER BY max_fee/operation_count))::bigint AS "p99" FROM history_transactions WHERE ledger_sequence > $1 AND ledger_sequence <= $2 `, currentSeq-5, currentSeq) diff --git a/services/horizon/internal/db2/history/trust_lines.go b/services/horizon/internal/db2/history/trust_lines.go new file mode 100644 index 0000000000..e8bb48727e --- /dev/null +++ b/services/horizon/internal/db2/history/trust_lines.go @@ -0,0 +1,188 @@ +package history + +import ( + "encoding/base64" + + sq "github.com/Masterminds/squirrel" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +// IsAuthorized returns true if issuer has authorized account to perform +// transactions with its credit +func (trustLine TrustLine) IsAuthorized() bool { + return xdr.TrustLineFlags(trustLine.Flags).IsAuthorized() +} + +func (q *Q) CountTrustLines() (int, error) { + sql := sq.Select("count(*)").From("trust_lines") + + var count int + if err := q.Get(&count, sql); err != nil { + return 0, errors.Wrap(err, "could not run select query") + } + + return count, nil +} + +func (q *Q) GetTrustLinesByAccountID(id string) ([]TrustLine, error) { + var trustLines []TrustLine + sql := selectTrustLines.Where(sq.Eq{"account_id": id}) + err := q.Select(&trustLines, sql) + return trustLines, err +} + +// GetTrustLinesByKeys loads a row from the `trust_lines` table, selected by multiple keys. +func (q *Q) GetTrustLinesByKeys(keys []xdr.LedgerKeyTrustLine) ([]TrustLine, error) { + var trustLines []TrustLine + lkeys := make([]string, 0, len(keys)) + for _, key := range keys { + lkey, err := ledgerKeyTrustLineToString(key) + if err != nil { + return nil, errors.Wrap(err, "Error running ledgerKeyTrustLineToString") + } + lkeys = append(lkeys, lkey) + } + sql := selectTrustLines.Where(map[string]interface{}{"trust_lines.ledger_key": lkeys}) + err := q.Select(&trustLines, sql) + return trustLines, err +} + +// InsertTrustLine creates a row in the trust lines table. +// Returns number of rows affected and error. +func (q *Q) InsertTrustLine(trustLine xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + m := trustLineToMap(trustLine, lastModifiedLedger) + + // Add lkey only when inserting rows + key, err := trustLineEntryToLedgerKeyString(trustLine) + if err != nil { + return 0, errors.Wrap(err, "Error running trustLineEntryToLedgerKeyString") + } + m["ledger_key"] = key + + sql := sq.Insert("trust_lines").SetMap(m) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// UpdateTrustLine updates a row in the trust lines table. +// Returns number of rows affected and error. +func (q *Q) UpdateTrustLine(trustLine xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) (int64, error) { + ledgerKey := xdr.LedgerKey{} + err := ledgerKey.SetTrustline(trustLine.AccountId, trustLine.Asset) + if err != nil { + return 0, errors.Wrap(err, "Error creating ledger key") + } + + key, err := trustLineEntryToLedgerKeyString(trustLine) + if err != nil { + return 0, errors.Wrap(err, "Error running trustLineEntryToLedgerKeyString") + } + + sql := sq.Update("trust_lines"). + SetMap(trustLineToMap(trustLine, lastModifiedLedger)). + Where(map[string]interface{}{"ledger_key": key}) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// RemoveTrustLine deletes a row in the trust lines table. +// Returns number of rows affected and error. +func (q *Q) RemoveTrustLine(ledgerKey xdr.LedgerKeyTrustLine) (int64, error) { + key, err := ledgerKeyTrustLineToString(ledgerKey) + if err != nil { + return 0, errors.Wrap(err, "Error ledgerKeyTrustLineToString MarshalBinaryCompress") + } + + sql := sq.Delete("trust_lines"). + Where(map[string]interface{}{"ledger_key": key}) + result, err := q.Exec(sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + +// GetTrustLinesByAccountsID loads trust lines for a list of accounts ID +func (q *Q) GetTrustLinesByAccountsID(id []string) ([]TrustLine, error) { + var data []TrustLine + sql := selectTrustLines.Where(sq.Eq{"account_id": id}) + err := q.Select(&data, sql) + return data, err +} + +func trustLineEntryToLedgerKeyString(trustLine xdr.TrustLineEntry) (string, error) { + ledgerKey := &xdr.LedgerKey{} + err := ledgerKey.SetTrustline(trustLine.AccountId, trustLine.Asset) + if err != nil { + return "", errors.Wrap(err, "Error running ledgerKey.SetTrustline") + } + key, err := ledgerKey.MarshalBinary() + if err != nil { + return "", errors.Wrap(err, "Error running MarshalBinaryCompress") + } + + return base64.StdEncoding.EncodeToString(key), nil +} + +func ledgerKeyTrustLineToString(trustLineKey xdr.LedgerKeyTrustLine) (string, error) { + ledgerKey := &xdr.LedgerKey{} + err := ledgerKey.SetTrustline(trustLineKey.AccountId, trustLineKey.Asset) + if err != nil { + return "", errors.Wrap(err, "Error running ledgerKey.SetTrustline") + } + key, err := ledgerKey.MarshalBinary() + if err != nil { + return "", errors.Wrap(err, "Error running MarshalBinaryCompress") + } + + return base64.StdEncoding.EncodeToString(key), nil +} + +func trustLineToMap(trustLine xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) map[string]interface{} { + var assetType xdr.AssetType + var assetCode, assetIssuer string + trustLine.Asset.MustExtract(&assetType, &assetCode, &assetIssuer) + + var buyingliabilities, sellingliabilities xdr.Int64 + if trustLine.Ext.V1 != nil { + v1 := trustLine.Ext.V1 + buyingliabilities = v1.Liabilities.Buying + sellingliabilities = v1.Liabilities.Selling + } + + return map[string]interface{}{ + "account_id": trustLine.AccountId.Address(), + "asset_type": assetType, + "asset_issuer": assetIssuer, + "asset_code": assetCode, + "balance": trustLine.Balance, + "trust_line_limit": trustLine.Limit, + "buying_liabilities": buyingliabilities, + "selling_liabilities": sellingliabilities, + "flags": trustLine.Flags, + "last_modified_ledger": lastModifiedLedger, + } +} + +var selectTrustLines = sq.Select(` + account_id, + asset_type, + asset_issuer, + asset_code, + balance, + trust_line_limit, + buying_liabilities, + selling_liabilities, + flags, + last_modified_ledger +`).From("trust_lines") diff --git a/services/horizon/internal/db2/history/trust_lines_batch_insert_builder.go b/services/horizon/internal/db2/history/trust_lines_batch_insert_builder.go new file mode 100644 index 0000000000..3228304dc8 --- /dev/null +++ b/services/horizon/internal/db2/history/trust_lines_batch_insert_builder.go @@ -0,0 +1,25 @@ +package history + +import ( + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +// Add adds a new trust line entry to the batch. `lastModifiedLedger` is another +// parameter because `xdr.TrustLineEntry` does not have a field to hold this value. +func (i *trustLinesBatchInsertBuilder) Add(trustLine xdr.TrustLineEntry, lastModifiedLedger xdr.Uint32) error { + m := trustLineToMap(trustLine, lastModifiedLedger) + + // Add lkey only when inserting rows + key, err := trustLineEntryToLedgerKeyString(trustLine) + if err != nil { + return errors.Wrap(err, "Error running trustLineEntryToLedgerKeyString") + } + m["ledger_key"] = key + + return i.builder.Row(m) +} + +func (i *trustLinesBatchInsertBuilder) Exec() error { + return i.builder.Exec() +} diff --git a/services/horizon/internal/db2/history/trust_lines_test.go b/services/horizon/internal/db2/history/trust_lines_test.go new file mode 100644 index 0000000000..b5054e2474 --- /dev/null +++ b/services/horizon/internal/db2/history/trust_lines_test.go @@ -0,0 +1,236 @@ +package history + +import ( + "testing" + + "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +var ( + trustLineIssuer = xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") + + eurTrustLine = xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 20000, + Limit: 223456789, + Flags: 1, + Ext: xdr.TrustLineEntryExt{ + V: 1, + V1: &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 3, + Selling: 4, + }, + }, + }, + } + + usdTrustLine = xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GCYVFGI3SEQJGBNQQG7YCMFWEYOHK3XPVOVPA6C566PXWN4SN7LILZSM"), + Asset: xdr.MustNewCreditAsset("USDUSD", trustLineIssuer.Address()), + Balance: 10000, + Limit: 123456789, + Flags: 0, + Ext: xdr.TrustLineEntryExt{ + V: 1, + V1: &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 1, + Selling: 2, + }, + }, + }, + } + + usdTrustLine2 = xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GBYSBDAJZMHL5AMD7QXQ3JEP3Q4GLKADWIJURAAHQALNAWD6Z5XF2RAC"), + Asset: xdr.MustNewCreditAsset("USDUSD", trustLineIssuer.Address()), + Balance: 10000, + Limit: 123456789, + Flags: 0, + Ext: xdr.TrustLineEntryExt{ + V: 1, + V1: &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 1, + Selling: 2, + }, + }, + }, + } +) + +func TestIsAuthorized(t *testing.T) { + tt := assert.New(t) + tl := TrustLine{ + Flags: 1, + } + tt.True(tl.IsAuthorized()) + + tl = TrustLine{ + Flags: 0, + } + tt.False(tl.IsAuthorized()) +} +func TestInsertTrustLine(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + rows, err := q.InsertTrustLine(eurTrustLine, 1234) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + rows, err = q.InsertTrustLine(usdTrustLine, 1235) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + keys := []xdr.LedgerKeyTrustLine{ + {Asset: eurTrustLine.Asset, AccountId: eurTrustLine.AccountId}, + {Asset: usdTrustLine.Asset, AccountId: usdTrustLine.AccountId}, + } + + lines, err := q.GetTrustLinesByKeys(keys) + assert.NoError(t, err) + assert.Len(t, lines, 2) +} + +func TestUpdateTrustLine(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + rows, err := q.InsertTrustLine(eurTrustLine, 1234) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + modifiedTrustLine := eurTrustLine + modifiedTrustLine.Balance = 30000 + + rows, err = q.UpdateTrustLine(modifiedTrustLine, 1235) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + keys := []xdr.LedgerKeyTrustLine{ + {Asset: eurTrustLine.Asset, AccountId: eurTrustLine.AccountId}, + } + lines, err := q.GetTrustLinesByKeys(keys) + assert.NoError(t, err) + assert.Len(t, lines, 1) + + expectedBinary, err := modifiedTrustLine.MarshalBinary() + assert.NoError(t, err) + + dbEntry := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress(lines[0].AccountID), + Asset: xdr.MustNewCreditAsset(lines[0].AssetCode, lines[0].AssetIssuer), + Balance: xdr.Int64(lines[0].Balance), + Limit: xdr.Int64(lines[0].Limit), + Flags: xdr.Uint32(lines[0].Flags), + Ext: xdr.TrustLineEntryExt{ + V: 1, + V1: &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: xdr.Int64(lines[0].BuyingLiabilities), + Selling: xdr.Int64(lines[0].SellingLiabilities), + }, + }, + }, + } + + actualBinary, err := dbEntry.MarshalBinary() + assert.NoError(t, err) + assert.Equal(t, expectedBinary, actualBinary) + assert.Equal(t, uint32(1235), lines[0].LastModifiedLedger) +} + +func TestRemoveTrustLine(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + rows, err := q.InsertTrustLine(eurTrustLine, 1234) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + key := xdr.LedgerKeyTrustLine{Asset: eurTrustLine.Asset, AccountId: eurTrustLine.AccountId} + rows, err = q.RemoveTrustLine(key) + assert.NoError(t, err) + assert.Equal(t, int64(1), rows) + + lines, err := q.GetTrustLinesByKeys([]xdr.LedgerKeyTrustLine{key}) + assert.NoError(t, err) + assert.Len(t, lines, 0) + + // Doesn't exist anymore + rows, err = q.RemoveTrustLine(key) + assert.NoError(t, err) + assert.Equal(t, int64(0), rows) +} +func TestGetTrustLinesByAccountsID(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + _, err := q.InsertTrustLine(eurTrustLine, 1234) + tt.Assert.NoError(err) + _, err = q.InsertTrustLine(usdTrustLine, 1235) + tt.Assert.NoError(err) + _, err = q.InsertTrustLine(usdTrustLine2, 1235) + tt.Assert.NoError(err) + + ids := []string{ + eurTrustLine.AccountId.Address(), + usdTrustLine.AccountId.Address(), + } + + records, err := q.GetTrustLinesByAccountsID(ids) + tt.Assert.NoError(err) + tt.Assert.Len(records, 2) + + m := map[string]xdr.TrustLineEntry{ + eurTrustLine.AccountId.Address(): eurTrustLine, + usdTrustLine.AccountId.Address(): usdTrustLine, + } + + for _, record := range records { + xtl, ok := m[record.AccountID] + tt.Assert.True(ok) + asset := xdr.MustNewCreditAsset(record.AssetCode, record.AssetIssuer) + tt.Assert.Equal(xtl.Asset, asset) + tt.Assert.Equal(xtl.AccountId.Address(), record.AccountID) + delete(m, record.AccountID) + } + + tt.Assert.Len(m, 0) +} + +func TestGetTrustLinesByAccountID(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + _, err := q.InsertTrustLine(eurTrustLine, 1234) + tt.Assert.NoError(err) + + record, err := q.GetTrustLinesByAccountID(eurTrustLine.AccountId.Address()) + tt.Assert.NoError(err) + + asset := xdr.MustNewCreditAsset(record[0].AssetCode, record[0].AssetIssuer) + tt.Assert.Equal(eurTrustLine.Asset, asset) + tt.Assert.Equal(eurTrustLine.AccountId.Address(), record[0].AccountID) + tt.Assert.Equal(int64(eurTrustLine.Balance), record[0].Balance) + tt.Assert.Equal(int64(eurTrustLine.Limit), record[0].Limit) + tt.Assert.Equal(uint32(eurTrustLine.Flags), record[0].Flags) + tt.Assert.Equal(int64(eurTrustLine.Ext.V1.Liabilities.Buying), record[0].BuyingLiabilities) + tt.Assert.Equal(int64(eurTrustLine.Ext.V1.Liabilities.Selling), record[0].SellingLiabilities) + +} diff --git a/services/horizon/internal/db2/schema/bindata.go b/services/horizon/internal/db2/schema/bindata.go index c634eba7a9..da47e64dcc 100644 --- a/services/horizon/internal/db2/schema/bindata.go +++ b/services/horizon/internal/db2/schema/bindata.go @@ -13,6 +13,10 @@ // migrations/1_initial_schema.sql (9.977kB) // migrations/20_account_for_signer_index.sql (140B) // migrations/21_trades_remove_zero_amount_constraints.sql (765B) +// migrations/22_trust_lines.sql (955B) +// migrations/23_exp_asset_stats.sql (883B) +// migrations/24_accounts.sql (1.402kB) +// migrations/25_expingest_rename_columns.sql (641B) // migrations/2_index_participants_by_toid.sql (277B) // migrations/3_use_sequence_in_history_accounts.sql (447B) // migrations/4_add_protocol_version.sql (188B) @@ -350,6 +354,86 @@ func migrations21_trades_remove_zero_amount_constraintsSql() (*asset, error) { return a, nil } +var _migrations22_trust_linesSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x93\xc1\x6e\x9b\x40\x10\x86\xef\x3c\xc5\x1c\x6d\xd5\x54\x6d\xd5\xe4\xe2\x93\x5d\xa3\xca\x8a\x83\x23\x8a\xa5\xe6\xb4\x1a\x76\xc7\x64\xd4\x65\xd7\xdd\x59\x5a\xf1\xf6\x95\x89\x62\x93\x04\xa5\xce\x71\xc5\xc7\x3f\x2c\xdf\x3f\x69\x0a\x1f\x1a\xae\x03\x46\x82\xdd\x21\x49\xbe\x15\xd9\xa2\xcc\xa0\x5c\x2c\x37\x19\xc4\xd0\x4a\x54\x96\x1d\x09\x4c\x12\x00\x80\x34\x05\x4b\xa6\xa6\xa0\x7e\x51\x07\x2c\x80\xb0\xe9\xcf\x37\xd4\x41\x83\x41\x1e\xd0\x92\x81\x56\xd8\xd5\x70\xfb\x78\x5e\xb2\xc3\xd0\x3d\xbd\x8e\xce\x40\x85\x42\xd7\x5f\x53\x72\xda\x9b\x9e\x26\x03\xd1\x43\xe5\xbd\x44\x38\x50\xd8\xfb\x06\x9d\x26\xf0\x7b\x10\xdf\x10\xfc\x6e\x29\x30\xc9\xc7\x3e\x63\x30\x5f\x3f\x60\x40\x1d\x29\xc0\x1f\x0c\x1d\xbb\x7a\xf2\xf9\xea\xd3\x14\xf2\x6d\x09\xf9\x6e\xb3\x99\xf5\x3c\x6a\xed\x5b\x17\x15\x9b\x11\xfe\xea\xfa\x15\x2e\x42\x51\xc5\xee\x40\xc0\x2e\x8e\x3e\x64\x91\x96\xc2\x3b\xd2\x8e\xd7\x1c\xfb\xd8\x2f\x2f\xf1\x0a\x6d\x7f\xf1\x8a\xeb\xd7\xc3\xcf\x36\x94\xe5\x86\xe3\x38\x55\xb5\xc7\x6c\x65\x19\x2b\xb6\x1c\x99\x64\x9c\x13\xb2\xf6\x22\x70\x6f\xb1\x96\x91\x7f\x61\x51\xa2\x6a\xbc\xe1\x3d\x93\x51\x8f\x56\x60\x9d\x97\x2f\xb0\xbb\x62\x7d\xbb\x28\xee\xe1\x26\xbb\x87\xc9\xd9\xdd\x34\x99\xce\x4f\x6d\x5b\xe7\xab\xec\xe7\xb0\x6d\xaa\xea\xd4\xc0\xdb\x36\x7f\x56\xc5\xdd\x8f\x75\xfe\x1d\x96\x65\x91\x65\x93\x33\x35\x9d\xbf\x19\x77\x34\xda\x8b\x78\xd2\xf7\x56\xe8\xa9\x03\xb3\x81\xc1\xd9\x33\xfd\xff\x19\x77\xe9\x90\x53\x58\x32\xdc\xc4\x95\xff\xeb\x92\x64\x55\x6c\xef\x46\x36\x51\xa3\x68\x34\x34\xff\x17\x00\x00\xff\xff\x7a\x87\x20\x42\xbb\x03\x00\x00") + +func migrations22_trust_linesSqlBytes() ([]byte, error) { + return bindataRead( + _migrations22_trust_linesSql, + "migrations/22_trust_lines.sql", + ) +} + +func migrations22_trust_linesSql() (*asset, error) { + bytes, err := migrations22_trust_linesSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/22_trust_lines.sql", size: 955, mode: os.FileMode(0644), modTime: time.Unix(1572526040, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe7, 0xd9, 0x90, 0x83, 0xc9, 0xb3, 0x1b, 0xc4, 0xe9, 0xc4, 0xbb, 0xcb, 0xb5, 0x92, 0x15, 0xaa, 0xef, 0x5d, 0x4e, 0xcf, 0x16, 0x6b, 0x49, 0xef, 0x85, 0x1a, 0xbf, 0xb6, 0x71, 0xb3, 0x92, 0x33}} + return a, nil +} + +var _migrations23_exp_asset_statsSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x93\x51\x6f\xd3\x30\x14\x85\xdf\xf3\x2b\xce\x63\x2b\xda\x49\x20\x81\x90\xfa\x14\xd6\x68\x54\x94\x74\x0a\x29\xda\x9e\x22\xd7\xb9\x6b\x2d\x1c\xc7\xf8\xde\xd2\xe5\xdf\xa3\x8c\x6c\xb8\xb4\x13\xbd\x4f\x96\xfc\xe9\x9c\x73\x8f\xe5\xe9\x14\x6f\x1a\xb3\x0d\x4a\x08\x6b\x9f\x4c\xa7\xa0\x47\x5f\x29\x66\x92\x8a\x45\x09\xc3\x30\xac\xf9\x41\x90\x1d\x81\x1e\x0d\x8b\x71\x5b\xc4\x80\xa8\x8d\x25\x3c\x98\xc0\x82\x9a\x1e\x8c\xa3\x1a\xc6\xe1\x63\xa5\x03\x29\xa1\x58\xac\x7a\x62\xaf\xf8\xa7\xfd\xe3\xa4\xc9\x0b\x64\xa7\xe4\x9c\xab\x6f\xfd\xde\x2a\xa1\x1a\x9b\x6e\x70\xf7\x14\x4c\x43\x4e\x94\x85\x71\x5b\x62\x31\xad\x03\x77\x2c\xd4\xe0\xb0\x33\x96\xf0\x9a\x46\xef\x37\xc8\x58\xda\x2a\xdd\x9d\x08\x5c\x61\xe5\x34\x5d\x60\x14\xc8\x5b\xa5\x89\x63\xb1\xe1\x4a\x76\xe4\x7a\xa7\x03\x41\x2b\x87\x40\x4d\xfb\xeb\x28\x53\x92\x5c\x17\x59\x5a\x66\x28\xd3\x4f\xcb\xec\x64\xeb\x51\x02\x60\xe0\xa5\xf3\x84\xa7\x59\xe4\x25\xf2\x55\x89\x7c\xbd\x5c\x4e\x22\x42\xb7\xf5\x40\x7c\x4f\x8b\xeb\xcf\x69\x31\x7a\xfb\x6e\x7c\x96\x34\xcc\x7b\x0a\x31\xf9\xfe\xc3\x09\xd9\xb4\x7b\x27\x78\x99\x32\xbb\x2b\x9f\xcf\xc7\xa4\xdb\x37\x95\xd2\xba\xc7\x79\xc8\x97\xdd\x64\xc5\x19\xf2\xb6\x58\x7c\x4d\x8b\x7b\x7c\xc9\xee\x47\x7f\x33\x4f\x8e\x52\x4d\xa2\x7d\xc7\xc9\x78\xf6\x52\xd1\x22\x9f\x67\x77\xff\x56\x54\x6d\xba\xe7\x6d\x56\xf9\x49\x7f\xeb\x6f\x8b\xfc\x06\x1b\x09\x44\x18\xc5\x26\xe3\xd9\x7f\x55\x87\x36\x2f\x53\xed\xe1\x3e\x6a\xfc\x7d\xe6\xed\xc1\x25\xf3\x62\x75\xfb\xca\xe3\x6a\xc5\x5a\xd5\x34\xfb\x1d\x00\x00\xff\xff\x1c\x2f\xe1\x06\x73\x03\x00\x00") + +func migrations23_exp_asset_statsSqlBytes() ([]byte, error) { + return bindataRead( + _migrations23_exp_asset_statsSql, + "migrations/23_exp_asset_stats.sql", + ) +} + +func migrations23_exp_asset_statsSql() (*asset, error) { + bytes, err := migrations23_exp_asset_statsSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/23_exp_asset_stats.sql", size: 883, mode: os.FileMode(0644), modTime: time.Unix(1572444420, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x5f, 0x23, 0x96, 0xcb, 0x81, 0x52, 0xd5, 0xb, 0x3c, 0xd4, 0xb9, 0xd9, 0x24, 0xd3, 0x1a, 0x3d, 0x1a, 0xe0, 0xd2, 0x4, 0x40, 0xf5, 0x75, 0xe2, 0x1d, 0x26, 0xd3, 0x19, 0xcf, 0x70, 0xf, 0x36}} + return a, nil +} + +var _migrations24_accountsSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x9c\x94\x5d\x6f\xda\x4c\x10\x85\xef\xfd\x2b\xe6\x12\xf4\x86\x57\xfd\x48\x90\x2a\xae\xa0\x58\x15\x0a\x31\x29\x05\xa9\xb9\x5a\x8d\xbd\x83\x3d\xea\x7e\x24\xbb\xeb\x20\xfe\x7d\x65\x53\x62\xc7\x18\x25\xed\x9d\x57\x7e\xf6\xec\x9e\x99\x33\x3b\x1a\xc1\x7f\x9a\x73\x87\x81\x60\xfb\x18\x45\x5f\xd7\xf1\x74\x13\xc3\x66\x3a\x5b\xc6\x80\x59\x66\x4b\x13\x3c\x0c\x22\x00\x38\x2d\x05\x4b\xc8\x0a\x74\x98\x05\x72\xf0\x8c\xee\xc0\x26\x1f\xdc\x8c\x87\x90\xac\x36\x90\x6c\x97\xcb\xab\x1a\x4f\x51\xa1\xc9\x08\x52\xce\xd9\x84\xee\xcf\xb2\xda\x25\x14\x63\xca\x8a\x03\x93\xef\xe7\x3c\x29\xf5\x4e\xf0\xa9\x24\x93\x91\x30\xa5\x4e\xc9\xf5\x43\xa6\xd4\xc2\x97\x29\x99\xe0\x2a\xa1\x73\x80\xcd\x4e\x61\x60\x6b\x84\x24\x1f\xd8\xd4\xdf\xef\x72\xbb\x53\x98\xf7\x29\x16\x56\x93\x90\x56\x23\xf7\xe9\x7c\xfe\xd4\xd5\xd1\xe8\x03\x39\xb1\x27\xce\x8b\x00\x5e\x63\xe5\xbf\x2b\x1a\x0a\x47\xbe\xb0\x4a\x0a\x65\xf7\x6f\x43\x9a\x24\x97\xfa\x6d\xae\xe0\xbc\xb8\x44\x29\xf4\x41\x68\x2b\x79\xc7\x24\x85\x22\x99\x93\x83\x45\xb2\xe9\x60\xf7\xeb\xc5\xdd\x74\xfd\x00\xb7\xf1\x03\x0c\x9a\xc0\x0c\xa3\xe1\xe4\x25\x5c\x8b\x64\x1e\xff\x7c\x09\x97\xe8\xaf\xf9\x2a\x69\xe2\xb7\xfd\xb1\x48\xbe\xc1\x6c\xb3\x8e\xe3\x41\x2f\x3d\x9c\x5c\xd0\x6e\x57\xff\x92\x62\x8b\x69\x5d\xf2\xf5\x04\x08\x89\x01\xff\x8c\xc1\x68\x04\x47\xfb\xe2\x17\x1d\x80\x3d\x20\x2c\xeb\xf5\x2d\x1d\x40\xa3\xf3\x05\x2a\x92\x50\x7a\x36\x39\xdc\x1d\xd7\x33\x36\xe8\x0e\xa7\xed\x68\x24\xa4\xe8\x69\x7c\x3d\x22\x93\x59\x59\xd3\x24\x21\x58\x48\xad\xf5\x01\x1e\xc9\xed\xac\xae\xa7\xc7\xee\xc0\x5b\x4d\xf0\x54\x52\x95\xd9\xff\x8f\xcd\x68\xce\x3f\xcf\xd4\xc7\x9b\x0f\xdd\x50\xfd\xe5\xe4\x1a\xd4\xd4\x03\x8e\xaf\xbb\xe0\x33\xaa\xb2\x8f\xfc\xd2\xbe\x41\xe5\xb8\xe3\x76\x7c\x0d\xe9\x21\x90\xff\xe7\x64\x35\x05\x78\x95\xac\x6d\xb2\xf8\xbe\x3d\x0b\x41\xd5\x3b\xd1\x94\x40\xd4\xf6\x5a\x69\x38\x36\xb7\x1d\x89\x06\xbe\xaa\x8b\x51\x1d\xd1\x7e\x29\xe7\x76\x6f\xa2\x68\xbe\x5e\xdd\x77\x5f\xca\x0c\x7d\x86\x92\x26\x7d\x3f\x8f\xe7\x9c\x88\xdf\x01\x00\x00\xff\xff\xd2\xd6\x65\xae\x7a\x05\x00\x00") + +func migrations24_accountsSqlBytes() ([]byte, error) { + return bindataRead( + _migrations24_accountsSql, + "migrations/24_accounts.sql", + ) +} + +func migrations24_accountsSql() (*asset, error) { + bytes, err := migrations24_accountsSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/24_accounts.sql", size: 1402, mode: os.FileMode(0644), modTime: time.Unix(1572526640, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xa5, 0xf8, 0xf7, 0xeb, 0xe2, 0x3d, 0xda, 0xe, 0xc2, 0x78, 0x88, 0x16, 0x22, 0xbf, 0x22, 0xa8, 0x5a, 0x17, 0x72, 0xd9, 0xab, 0x56, 0xa8, 0x55, 0x5a, 0x3f, 0x47, 0xf6, 0x18, 0xfa, 0x43, 0xa7}} + return a, nil +} + +var _migrations25_expingest_rename_columnsSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x9c\x90\xcd\xaa\x83\x30\x10\x85\xf7\x3e\xc5\xec\x2f\x3e\xc1\x5d\xd9\x36\x3b\x7f\x40\x74\x3d\x58\x8d\x61\xc0\xc6\xe2\x28\xa5\x6f\x5f\x4c\x0c\xa4\x76\xd1\xd8\xdd\x99\x09\x5f\x0e\xf3\xc5\x31\xfc\xdd\x48\x4d\xcd\x2c\xa1\xbe\x47\x51\x92\x56\xa2\x84\x2a\x39\xa5\x02\x9a\xb6\x1d\x17\x3d\x33\x32\x29\x2d\x27\x86\x52\xe4\x49\x26\xe0\x5c\xa4\x75\x96\xbb\x67\xa8\x0a\x17\x91\xba\xff\xf7\x2f\xc6\xbe\xff\x04\x59\x0e\x83\x9c\xa8\x5b\x49\x9b\x0d\xf8\x95\x33\x4b\x8b\x99\x18\x46\xad\x0d\xa4\x55\xc3\x2c\x67\xd7\x48\x5a\xa1\x59\x04\xf0\xd7\xe5\xe9\xe3\x76\x74\x74\xe4\xfb\xbb\x8c\x0f\xfd\x8b\x41\xb4\x37\x6d\x53\xb8\x41\xf4\x15\x86\x1b\x44\x4f\xe1\x11\x83\xb8\x57\x78\xcc\x20\xee\x14\x6e\xf4\x2b\x00\x00\xff\xff\x84\x7d\x6a\x64\x81\x02\x00\x00") + +func migrations25_expingest_rename_columnsSqlBytes() ([]byte, error) { + return bindataRead( + _migrations25_expingest_rename_columnsSql, + "migrations/25_expingest_rename_columns.sql", + ) +} + +func migrations25_expingest_rename_columnsSql() (*asset, error) { + bytes, err := migrations25_expingest_rename_columnsSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/25_expingest_rename_columns.sql", size: 641, mode: os.FileMode(0644), modTime: time.Unix(1572526794, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x80, 0x44, 0x81, 0x62, 0xeb, 0xcf, 0x82, 0xaa, 0x9, 0x14, 0x4c, 0xb6, 0xd2, 0x2c, 0x41, 0x2d, 0xf0, 0x34, 0x4a, 0x18, 0x5a, 0x95, 0x3e, 0x8d, 0x60, 0xbe, 0x5, 0x10, 0xf5, 0xc2, 0x9c, 0x3a}} + return a, nil +} + var _migrations2_index_participants_by_toidSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x8f\xb1\xca\xc2\x50\x0c\x46\xf7\x3c\x45\xc6\xff\x47\xfa\x04\x9d\xc4\x16\xe9\xd2\x4a\xb5\xe0\x76\x49\xdb\x8b\xcd\xe0\xcd\x25\x37\x20\x7d\x7b\x41\x07\x5b\xbb\xb8\x86\x8f\x73\x72\xb2\x0c\x77\x77\xbe\x29\x99\xc7\x2e\x02\x1c\xda\x72\x7f\x29\xb1\xaa\x8b\xf2\x8a\x93\x44\xd7\xcf\x6e\x12\x1e\xb1\xa9\x71\xe2\x64\xa2\xb3\x93\xe8\x95\x8c\x25\xb8\x48\x6a\x3c\x70\xa4\x60\x09\xbb\x73\x55\x1f\xb1\x37\xf5\x1e\xff\xb6\x5b\x1e\xff\xf3\x2f\xbc\xbd\xf1\xb6\xc6\x9b\x52\x48\x34\xfc\x28\x58\xae\x5f\x0a\x58\x26\x15\xf2\x08\x00\x45\xdb\x9c\xb6\x49\xf9\xea\xfe\xf9\x25\x87\x67\x00\x00\x00\xff\xff\x33\xec\x54\x7a\x15\x01\x00\x00") func migrations2_index_participants_by_toidSqlBytes() ([]byte, error) { @@ -647,6 +731,14 @@ var _bindata = map[string]func() (*asset, error){ "migrations/21_trades_remove_zero_amount_constraints.sql": migrations21_trades_remove_zero_amount_constraintsSql, + "migrations/22_trust_lines.sql": migrations22_trust_linesSql, + + "migrations/23_exp_asset_stats.sql": migrations23_exp_asset_statsSql, + + "migrations/24_accounts.sql": migrations24_accountsSql, + + "migrations/25_expingest_rename_columns.sql": migrations25_expingest_rename_columnsSql, + "migrations/2_index_participants_by_toid.sql": migrations2_index_participants_by_toidSql, "migrations/3_use_sequence_in_history_accounts.sql": migrations3_use_sequence_in_history_accountsSql, @@ -721,6 +813,10 @@ var _bintree = &bintree{nil, map[string]*bintree{ "1_initial_schema.sql": &bintree{migrations1_initial_schemaSql, map[string]*bintree{}}, "20_account_for_signer_index.sql": &bintree{migrations20_account_for_signer_indexSql, map[string]*bintree{}}, "21_trades_remove_zero_amount_constraints.sql": &bintree{migrations21_trades_remove_zero_amount_constraintsSql, map[string]*bintree{}}, + "22_trust_lines.sql": &bintree{migrations22_trust_linesSql, map[string]*bintree{}}, + "23_exp_asset_stats.sql": &bintree{migrations23_exp_asset_statsSql, map[string]*bintree{}}, + "24_accounts.sql": &bintree{migrations24_accountsSql, map[string]*bintree{}}, + "25_expingest_rename_columns.sql": &bintree{migrations25_expingest_rename_columnsSql, map[string]*bintree{}}, "2_index_participants_by_toid.sql": &bintree{migrations2_index_participants_by_toidSql, map[string]*bintree{}}, "3_use_sequence_in_history_accounts.sql": &bintree{migrations3_use_sequence_in_history_accountsSql, map[string]*bintree{}}, "4_add_protocol_version.sql": &bintree{migrations4_add_protocol_versionSql, map[string]*bintree{}}, diff --git a/services/horizon/internal/db2/schema/migrations/22_trust_lines.sql b/services/horizon/internal/db2/schema/migrations/22_trust_lines.sql new file mode 100644 index 0000000000..df7701665c --- /dev/null +++ b/services/horizon/internal/db2/schema/migrations/22_trust_lines.sql @@ -0,0 +1,26 @@ +-- +migrate Up + +CREATE TABLE trust_lines ( + -- ledger_key is a LedgerKey marshaled using MarshalBinary + -- and base64-encoded used to boost perfomance of some queries. + ledger_key character varying(150) NOT NULL, + account_id character varying(56) NOT NULL, + asset_type int NOT NULL, + asset_issuer character varying(56) NOT NULL, + asset_code character varying(12) NOT NULL, + balance bigint NOT NULL, + trust_line_limit bigint NOT NULL, + buying_liabilities bigint NOT NULL, + selling_liabilities bigint NOT NULL, + flags int NOT NULL, + last_modified_ledger INT NOT NULL, + PRIMARY KEY (ledger_key) +); + +CREATE INDEX trust_lines_by_account_id ON trust_lines USING BTREE(account_id); +CREATE INDEX trust_lines_by_type_code_issuer ON trust_lines USING BTREE(asset_type, asset_code, asset_issuer); +CREATE INDEX trust_lines_by_issuer ON trust_lines USING BTREE(asset_issuer); + +-- +migrate Down + +DROP TABLE trust_lines cascade; \ No newline at end of file diff --git a/services/horizon/internal/db2/schema/migrations/23_exp_asset_stats.sql b/services/horizon/internal/db2/schema/migrations/23_exp_asset_stats.sql new file mode 100644 index 0000000000..01a41fbd94 --- /dev/null +++ b/services/horizon/internal/db2/schema/migrations/23_exp_asset_stats.sql @@ -0,0 +1,20 @@ +-- +migrate Up +-- exp_asset_stats is like the existing asset_stats table first defined in 8_create_asset_stats_table.sql +-- except that exp_asset_stats is populated by the experimental ingestion system while asset_stats is populated +-- by the legacy ingestion system. Once the experimental ingestion system replaces the legacy system then +-- we can remove asset_stats + +CREATE TABLE exp_asset_stats ( + asset_type INT NOT NULL, + asset_code VARCHAR(12) NOT NULL, + asset_issuer VARCHAR(56) NOT NULL, + amount TEXT NOT NULL, + num_accounts INTEGER NOT NULL, + PRIMARY KEY(asset_code, asset_issuer, asset_type) +); + +CREATE INDEX exp_asset_stats_by_issuer ON exp_asset_stats USING btree (asset_issuer); +CREATE INDEX exp_asset_stats_by_code ON exp_asset_stats USING btree (asset_code); + +-- +migrate Down +DROP TABLE exp_asset_stats cascade; \ No newline at end of file diff --git a/services/horizon/internal/db2/schema/migrations/24_accounts.sql b/services/horizon/internal/db2/schema/migrations/24_accounts.sql new file mode 100644 index 0000000000..aa678a0b96 --- /dev/null +++ b/services/horizon/internal/db2/schema/migrations/24_accounts.sql @@ -0,0 +1,40 @@ +-- +migrate Up + +CREATE TABLE accounts ( + account_id character varying(56) NOT NULL, + balance bigint NOT NULL, + buying_liabilities bigint NOT NULL, + selling_liabilities bigint NOT NULL, + sequence_number bigint NOT NULL, + num_subentries int NOT NULL, + inflation_destination character varying(56) NOT NULL, + flags int NOT NULL, + home_domain character varying(32) NOT NULL, + master_weight smallint NOT NULL, + threshold_low smallint NOT NULL, + threshold_medium smallint NOT NULL, + threshold_high smallint NOT NULL, + last_modified_ledger INT NOT NULL, + PRIMARY KEY (account_id) +); + +CREATE INDEX accounts_inflation_destination ON accounts USING BTREE(inflation_destination); +CREATE INDEX accounts_home_domain ON accounts USING BTREE(home_domain); + +CREATE TABLE accounts_data ( + -- ledger_key is a LedgerKey marshaled using MarshalBinary + -- and base64-encoded used to boost perfomance of some queries. + ledger_key character varying(150) NOT NULL, + account_id character varying(56) NOT NULL, + name character varying(64) NOT NULL, + value character varying(90) NOT NULL, -- base64-encoded 64 bytes + last_modified_ledger INT NOT NULL, + PRIMARY KEY (ledger_key) +); + +CREATE UNIQUE INDEX accounts_data_account_id_name ON accounts_data USING BTREE(account_id, name); + +-- +migrate Down + +DROP TABLE accounts cascade; +DROP TABLE accounts_data cascade; \ No newline at end of file diff --git a/services/horizon/internal/db2/schema/migrations/25_expingest_rename_columns.sql b/services/horizon/internal/db2/schema/migrations/25_expingest_rename_columns.sql new file mode 100644 index 0000000000..a0026d2c6b --- /dev/null +++ b/services/horizon/internal/db2/schema/migrations/25_expingest_rename_columns.sql @@ -0,0 +1,17 @@ +-- +migrate Up + +ALTER TABLE accounts_signers RENAME COLUMN account TO account_id; + +ALTER TABLE offers RENAME COLUMN sellerid TO seller_id; +ALTER TABLE offers RENAME COLUMN offerid TO offer_id; +ALTER TABLE offers RENAME COLUMN sellingasset TO selling_asset; +ALTER TABLE offers RENAME COLUMN buyingasset TO buying_asset; + +-- +migrate Down + +ALTER TABLE accounts_signers RENAME COLUMN account_id TO account; + +ALTER TABLE offers RENAME COLUMN seller_id TO sellerid; +ALTER TABLE offers RENAME COLUMN offer_id TO offerid; +ALTER TABLE offers RENAME COLUMN selling_asset TO sellingasset; +ALTER TABLE offers RENAME COLUMN buying_asset TO buyingasset; diff --git a/services/horizon/internal/docs/reference/endpoints/accounts.md b/services/horizon/internal/docs/reference/endpoints/accounts.md new file mode 100644 index 0000000000..17adadbf9a --- /dev/null +++ b/services/horizon/internal/docs/reference/endpoints/accounts.md @@ -0,0 +1,174 @@ +--- +title: Accounts +--- + +This endpoint allows filtering accounts who have a given `signer` or have a trustline to an `asset`. The result is a list of [accounts](../resources/account.md). + +To find all accounts who are trustees to an asset, pass the query parameter `asset` using the canonical representation for an issued assets which is `Code:IssuerAccountID`. Read more about canonical representation of assets in [SEP-0011](https://github.com/stellar/stellar-protocol/blob/0c675fb3a482183dcf0f5db79c12685acf82a95c/ecosystem/sep-0011.md#values). + +**Note**: This endpoint is still experimental and available only if Horizon is running the [new ingestion system](https://github.com/stellar/go/blob/master/services/horizon/internal/expingest/BETA_TESTING.md). + +## Request + +``` +GET /accounts{?signer,asset,cursor,limit,order} +``` + +### Arguments + +| name | notes | description | example | +| ---- | ----- | ----------- | ------- | +| `?signer` | optional, string | Account ID | GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB | +| `?asset` | optional, string | An issued asset represented as "Code:IssuerAccountID". | `USD:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V,native` | +| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36` | +| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | +| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | + +### curl Example Request + +```sh +curl "https://horizon-testnet.stellar.org/accounts?signer=GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K" +``` + + + + + + + + + + + + + + + + + +## Response + +This endpoint responds with the details of all accounts matching the filters. See [account resource](../resources/account.md) for reference. + +### Example Response +```json +{ + "_links": { + "self": { + "href": "https://horizon-testnet.stellar.org/accounts?cursor=\u0026limit=10\u0026order=asc\u0026signer=GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K" + }, + "next": { + "href": "https://horizon-testnet.stellar.org/accounts?cursor=GDRREYWHQWJDICNH4SAH4TT2JRBYRPTDYIMLK4UWBDT3X3ZVVYT6I4UQ\u0026limit=10\u0026order=asc\u0026signer=GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K" + }, + "prev": { + "href": "https://horizon-testnet.stellar.org/accounts?cursor=GDRREYWHQWJDICNH4SAH4TT2JRBYRPTDYIMLK4UWBDT3X3ZVVYT6I4UQ\u0026limit=10\u0026order=desc\u0026signer=GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K" + } + }, + "_embedded": { + "records": [ + { + "_links": { + "self": { + "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB" + }, + "transactions": { + "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/transactions{?cursor,limit,order}", + "templated": true + }, + "operations": { + "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/operations{?cursor,limit,order}", + "templated": true + }, + "payments": { + "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/payments{?cursor,limit,order}", + "templated": true + }, + "effects": { + "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/effects{?cursor,limit,order}", + "templated": true + }, + "offers": { + "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/offers{?cursor,limit,order}", + "templated": true + }, + "trades": { + "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/trades{?cursor,limit,order}", + "templated": true + }, + "data": { + "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/data/{key}", + "templated": true + } + }, + "id": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", + "paging_token": "", + "account_id": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", + "sequence": 7275146318446606, + "last_modified_ledger": 22379074, + "subentry_count": 4, + "thresholds": { + "low_threshold": 0, + "med_threshold": 0, + "high_threshold": 0 + }, + "flags": { + "auth_required": false, + "auth_revocable": false, + "auth_immutable": false + }, + "balances": [ + { + "balance": "1000000.0000000", + "limit": "922337203685.4775807", + "buying_liabilities": "0.0000000", + "selling_liabilities": "0.0000000", + "last_modified_ledger": 632070, + "asset_type": "credit_alphanum4", + "asset_code": "FOO", + "asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR" + }, + { + "balance": "10000.0000000", + "buying_liabilities": "0.0000000", + "selling_liabilities": "0.0000000", + "asset_type": "native" + } + ], + "signers": [ + { + "public_key": "GDLEPBJBC2VSKJCLJB264F2WDK63X4NKOG774A3QWVH2U6PERGDPUCS4", + "weight": 1, + "key": "GDLEPBJBC2VSKJCLJB264F2WDK63X4NKOG774A3QWVH2U6PERGDPUCS4", + "type": "ed25519_public_key" + }, + { + "public_key": "GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K", + "weight": 1, + "key": "GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K", + "type": "sha256_hash" + }, + { + "public_key": "GDUDIN23QQTB23Q3Q6GUL6I7CEAQY4CWCFVRXFWPF4UJAQO47SPUFCXG", + "weight": 1, + "key": "GDUDIN23QQTB23Q3Q6GUL6I7CEAQY4CWCFVRXFWPF4UJAQO47SPUFCXG", + "type": "preauth_tx" + }, + { + "public_key": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", + "weight": 1, + "key": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", + "type": "ed25519_public_key" + } + ], + "data": { + "best_friend": "c3Ryb29weQ==" + } + } + ] + } +} +``` + +## Possible Errors + +- The [standard errors](../errors.md#Standard-Errors). diff --git a/services/horizon/internal/docs/reference/endpoints/offer-details.md b/services/horizon/internal/docs/reference/endpoints/offer-details.md new file mode 100644 index 0000000000..e3a5ac750f --- /dev/null +++ b/services/horizon/internal/docs/reference/endpoints/offer-details.md @@ -0,0 +1,72 @@ +--- +title: Offer Details +--- + +Returns information and links relating to a single [offer](../resources/offer.md). + +**Note**: This endpoint is still experimental and available only if Horizon is running the [new ingestion system](https://github.com/stellar/go/blob/master/services/horizon/internal/expingest/BETA_TESTING.md). + +## Request + +``` +GET /offers/{offer} +``` + +### Arguments + +| name | notes | description | example | +| ---- | ----- | ----------- | ------- | +| `offer` | required, string | Offer ID | `126628073` | + +### curl Example Request + +```sh +curl "https://horizon-testnet.stellar.org/offers/1347876" +``` + + + +## Response + +This endpoint responds with the details of a single offer for a given ID. See [offer resource](../resources/offer.md) for reference. + +### Example Response + +```json +{ + "_links": { + "self": { + "href": "https://horizon-testnet.stellar.org/offers/1347876" + }, + "offer_maker": { + "href": "https://horizon-testnet.stellar.org/accounts/GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C" + } + }, + "id": 1347876, + "paging_token": "1347876", + "seller": "GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C", + "selling": { + "asset_type": "credit_alphanum4", + "asset_code": "DSQ", + "asset_issuer": "GBDQPTQJDATT7Z7EO4COS4IMYXH44RDLLI6N6WIL5BZABGMUOVMLWMQF" + }, + "buying": { + "asset_type": "credit_alphanum4", + "asset_code": "USD", + "asset_issuer": "GAA4MFNZGUPJAVLWWG6G5XZJFZDHLKQNG3Q6KB24BAD6JHNNVXDCF4XG" + }, + "amount": "60.4544008", + "price_r": { + "n": 84293, + "d": 2000000 + }, + "price": "0.0421465", + "last_modified_ledger": 1429506, + "last_modified_time": "2019-10-29T22:08:23Z" +} +``` + +## Possible Errors + +- The [standard errors](../errors.md#Standard_Errors). +- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no offer whose ID matches the `offer` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/offers.md b/services/horizon/internal/docs/reference/endpoints/offers.md new file mode 100644 index 0000000000..328da3a7d1 --- /dev/null +++ b/services/horizon/internal/docs/reference/endpoints/offers.md @@ -0,0 +1,127 @@ +--- +title: Offers +--- + +People on the Stellar network can make [offers](../resources/offer.md) to buy or sell assets. This +endpoint represents all the current offers, allowing filtering by `seller`, `selling_asset` or `buying_asset`. + +**Note**: This endpoint is still experimental and available only if Horizon is running the [new ingestion system](https://github.com/stellar/go/blob/master/services/horizon/internal/expingest/BETA_TESTING.md). + +## Request + +``` +GET /offers{?selling_asset_type,selling_asset_issuer,selling_asset_code,buying_asset_type,buying_asset_issuer,buying_asset_code,seller,cursor,limit,order} +``` + +### Arguments + +| name | notes | description | example | +| ---- | ----- | ----------- | ------- | +| `?seller` | optional, string | Account ID of the offer creator | `GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36` | +| `?selling_asset_type` | required, string | Type of the Asset being sold | `native` | +| `?selling_asset_code` | required if `selling_asset_type` is not `native`, string | Code of the Asset being sold | `USD` | +| `?selling_asset_issuer` | required if `selling_asset_type` is not `native`, string | Account ID of the issuer of the Asset being sold | `GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36` | +| `?buying_asset_type` | required, string | Type of the Asset being bought | `credit_alphanum4` | +| `?buying_asset_code` | required if buying_asset_type is not `native`, string | Code of the Asset being bought | `BTC` | +| `?buying_asset_issuer` | required if buying_asset_type is not `native`, string | Account ID of the issuer of the Asset being bought | `GD6VWBXI6NY3AOOR55RLVQ4MNIDSXE5JSAVXUTF35FRRI72LYPI3WL6Z` | +| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | +| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | +| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | + +### curl Example Request + +```sh +curl "https://horizon-testnet.stellar.org/offers{?selling_asset_type,selling_asset_issuer,selling_asset_code,buying_asset_type,buying_asset_issuer,buying_asset_code,seller,cursor,limit,order}" +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## Response + +The list of offers. + +### Example Response + +```json +{ + "_links": { + "self": { + "href": "https://horizon-testnet.stellar.org/offers?cursor=&limit=10&order=asc" + }, + "next": { + "href": "https://horizon-testnet.stellar.org/offers?cursor=5443256&limit=10&order=asc" + }, + "prev": { + "href": "https://horizon-testnet.stellar.org/offers?cursor=5443256&limit=10&order=desc" + } + }, + "_embedded": { + "records": [ + { + "_links": { + "self": { + "href": "https://horizon-testnet.stellar.org/offers/5443256" + }, + "offer_maker": { + "href": "https://horizon-testnet.stellar.org/ + } + }, + "id": 5443256, + "paging_token": "5443256", + "seller": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", + "selling": { + "asset_type": "native" + }, + "buying": { + "asset_type": "credit_alphanum4", + "asset_code": "FOO", + "asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR" + }, + "amount": "10.0000000", + "price_r": { + "n": 1, + "d": 1 + }, + "price": "1.0000000", + "last_modified_ledger": 694974, + "last_modified_time": "2019-04-09T17:14:22Z" + } + ] + } +} +``` + +## Possible Errors + +- The [standard errors](../errors.md#Standard_Errors). diff --git a/services/horizon/internal/docs/reference/endpoints/path-finding-strict-receive.md b/services/horizon/internal/docs/reference/endpoints/path-finding-strict-receive.md new file mode 100644 index 0000000000..b5f6809af4 --- /dev/null +++ b/services/horizon/internal/docs/reference/endpoints/path-finding-strict-receive.md @@ -0,0 +1,102 @@ +--- +title: Strict Receive Payment Paths +clientData: + laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=paths&endpoint=all +--- + +The Stellar Network allows payments to be made across assets through _path payments_. A path +payment specifies a series of assets to route a payment through, from source asset (the asset +debited from the payer) to destination asset (the asset credited to the payee). + +A [Path Payment Strict Receive](../../../guides/concepts/list-of-operations.html#path-payment-strict-receive) allows a user to specify the *amount of the asset received*. The amount sent varies based on offers in the order books. If you would like to search for a path specifying the amount to be sent, use the [Find Payment Paths (Strict Send)](./path-finding-strict-send.html). + +A strict receive path search is specified using: + +- The source account id or source assets. +- The asset and amount that the destination account should receive. + +As part of the search, horizon will load a list of assets available to the source account id and +will find any payment paths from those source assets to the desired destination asset. The search's +amount parameter will be used to determine if a given path can satisfy a payment of the +desired amount. + +## Request + +``` +GET /paths/strict-receive?source_account={sa}&destination_asset_type={at}&destination_asset_code={ac}&destination_asset_issuer={di}&destination_amount={amount}&destination_account={da} +``` + +## Arguments + +| name | notes | description | example | +| ---- | ----- | ----------- | ------- | +| `?source_account` | string | The sender's account id. Any returned path must use an asset that the sender has a trustline to. | `GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP` | +| `?source_assets` | string | A comma separated list of assets. Any returned path must use an asset included in this list | `USD:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V,native` | +| `?destination_account` | string | The destination account that any returned path should use | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | +| `?destination_asset_type` | string | The type of the destination asset | `credit_alphanum4` | +| `?destination_asset_code` | required if `destination_asset_type` is not `native`, string | The destination asset code, if destination_asset_type is not "native" | `USD` | +| `?destination_asset_issuer` | required if `destination_asset_type` is not `native`, string | The issuer for the destination asset, if destination_asset_type is not "native" | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | +| `?destination_amount` | string | The amount, denominated in the destination asset, that any returned path should be able to satisfy | `10.1` | + +The endpoint will not allow requests which provide both a `source_account` and a `source_assets` parameter. All requests must provide one or the other. +The assets in `source_assets` are expected to be encoded using the following format: + +XLM should be represented as `"native"`. Issued assets should be represented as `"Code:IssuerAccountID"`. `"Code"` must consist of alphanumeric ASCII characters. + + +### curl Example Request + +```sh +curl "https://horizon-testnet.stellar.org/paths/strict-receive?destination_account=GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V&source_account=GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP&destination_asset_type=native&destination_amount=20" +``` + +### JavaScript Example Request + +```javascript +var StellarSdk = require('stellar-sdk'); +var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); + +var sourceAccount = "GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP"; +var destinationAsset = StellarSdk.Asset.native(); +var destinationAmount = "20"; + +server.paths(sourceAccount, destinationAsset, destinationAmount) + .call() + .then(function (pathResult) { + console.log(pathResult.records); + }) + .catch(function (err) { + console.log(err) + }) +``` + +## Response + +This endpoint responds with a page of path resources. See [path resource](../resources/path.md) for reference. + +### Example Response + +```json +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "FOO", + "source_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "FOO", + "destination_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", + "destination_amount": "100.0000000", + "path": [] + } + ] + } +} +``` + +## Possible Errors + +- The [standard errors](../errors.md#Standard-Errors). +- [not_found](../errors/not-found.md): A `not_found` error will be returned if no paths could be found to fulfill this payment request diff --git a/services/horizon/internal/docs/reference/endpoints/path-finding-strict-send.md b/services/horizon/internal/docs/reference/endpoints/path-finding-strict-send.md new file mode 100644 index 0000000000..ab51fccbce --- /dev/null +++ b/services/horizon/internal/docs/reference/endpoints/path-finding-strict-send.md @@ -0,0 +1,106 @@ +--- +title: Strict Send Payment Paths +clientData: + laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=paths&endpoint=all +--- + +The Stellar Network allows payments to be made across assets through _path payments_. A path +payment specifies a series of assets to route a payment through, from source asset (the asset +debited from the payer) to destination asset (the asset credited to the payee). + +A [Path Payment Strict Send](../../../guides/concepts/list-of-operations.html#path-payment-strict-send) allows a user to specify the amount of the asset to send. The amount received will vary based on offers in the order books. + + +A path payment strict send search is specified using: + +- The destination account id or destination assets. +- The source asset. +- The source amount. + +As part of the search, horizon will load a list of assets available to the source account id or use the assets passed in the request and will find any payment paths from those source assets to the desired destination asset. The source's amount parameter will be used to determine if a given path can satisfy a payment of the desired amount. + +**Note**: This endpoint is still experimental and available only if Horizon is running the [new ingestion system](https://github.com/stellar/go/blob/master/services/horizon/internal/expingest/BETA_TESTING.md). + +## Request + +``` +https://horizon-testnet.stellar.org/paths/strict-send?&source_amount={sa}&source_asset_type={at}&source_asset_code={ac}&source_asset_issuer={ai}&destination_account={da} +``` + +## Arguments + +| name | notes | description | example | +| ---- | ----- | ----------- | ------- | +| `?source_amount` | string | The amount, denominated in the source asset, that any returned path should be able to satisfy | `10.1` | +| `?source_asset_type` | string | The type of the source asset | `credit_alphanum4` | +| `?source_asset_code` | string, required if `source_asset_type` is not `native`, string | The source asset code, if source_asset_type is not "native" | `USD` | +| `?source_asset_issuer` | string, required if `source_asset_type` is not `native`, string | The issuer for the source asset, if source_asset_type is not "native" | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | +| `?destination_account` | string optional | The destination account that any returned path should use | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | +| `?destination_assets` | string optional | A comma separated list of assets. Any returned path must use an asset included in this list | `USD:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V,native` | + +The endpoint will not allow requests which provide both a `destination_account` and `destination_assets` parameter. All requests must provide one or the other. +The assets in `destination_assets` are expected to be encoded using the following format: + +XLM should be represented as `"native"`. Issued assets should be represented as `"Code:IssuerAccountID"`. `"Code"` must consist of alphanumeric ASCII characters. + + +### curl Example Request + +```sh +curl "https://horizon-testnet.stellar.org/paths/strict-send?&source_amount=10&source_asset_type=native&destination_assets=MXN:GC2GFGZ5CZCFCDJSQF3YYEAYBOS3ZREXJSPU7LUJ7JU3LP3BQNHY7YKS" +``` + +### JavaScript Example Request + +```javascript +var StellarSdk = require('stellar-sdk'); +var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); + +var sourceAsset = StellarSdk.Asset.native(); +var sourceAmount = "20"; +var destinationAsset = new StellarSdk.Asset( + 'USD', + 'GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN' +) + + +server.strictSendPaths(sourceAsset, sourceAmount, [destinationAsset]) + .call() + .then(function (pathResult) { + console.log(pathResult.records); + }) + .catch(function (err) { + console.log(err) + }) +``` + +## Response + +This endpoint responds with a page of path resources. See [path resource](../resources/path.md) for reference. + +### Example Response + +```json +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "FOO", + "source_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "FOO", + "destination_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", + "destination_amount": "100.0000000", + "path": [] + } + ] + } +} +``` + +## Possible Errors + +- The [standard errors](../errors.md#Standard-Errors). +- [not_found](../errors/not-found.md): A `not_found` error will be returned if no paths could be found to fulfill this payment request diff --git a/services/horizon/internal/docs/reference/endpoints/path-finding.md b/services/horizon/internal/docs/reference/endpoints/path-finding.md index e411053cb1..815d4116b1 100644 --- a/services/horizon/internal/docs/reference/endpoints/path-finding.md +++ b/services/horizon/internal/docs/reference/endpoints/path-finding.md @@ -4,6 +4,9 @@ clientData: laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=paths&endpoint=all --- +**Note**: This endpoint will be deprecated, use [/path/strict-receive](./path-finding-strict-receive.html) instead. There are no differences between both endpoints, `/paths` is an alias for `/path/strict-receive`. + + The Stellar Network allows payments to be made across assets through _path payments_. A path payment specifies a series of assets to route a payment through, from source asset (the asset debited from the payer) to destination asset (the asset credited to the payee). diff --git a/services/horizon/internal/docs/reference/endpoints/transactions-all.md b/services/horizon/internal/docs/reference/endpoints/transactions-all.md index 81f6bd8bea..0af6e27354 100644 --- a/services/horizon/internal/docs/reference/endpoints/transactions-all.md +++ b/services/horizon/internal/docs/reference/endpoints/transactions-all.md @@ -119,8 +119,8 @@ See [transaction resource](../resources/transaction.md) for reference. "created_at": "2015-09-24T10:07:09Z", "account": "GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K", "account_sequence": 279172874343, - "max_fee": 0, - "fee_paid": 0, + "max_fee": 100, + "fee_charged": 100, "operation_count": 1, "envelope_xdr": "AAAAAGXNhLrhGtltTwCpmqlarh7s1DB2hIkbP//jgzn4Fos/AAAACgAAAEEAAABnAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA2ddmTOFAgr21Crs2RXRGLhiAKxicZb/IERyEZL/Y2kUAAAAXSHboAAAAAAAAAAAB+BaLPwAAAECDEEZmzbgBr5fc3mfJsCjWPDtL6H8/vf16me121CC09ONyWJZnw0PUvp4qusmRwC6ZKfLDdk8F3Rq41s+yOgQD", "result_xdr": "AAAAAAAAAAoAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", @@ -160,8 +160,8 @@ See [transaction resource](../resources/transaction.md) for reference. "created_at": "2015-09-24T07:49:38Z", "account": "GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K", "account_sequence": 279172874342, - "max_fee": 0, - "fee_paid": 0, + "max_fee": 100, + "fee_charged": 100, "operation_count": 1, "envelope_xdr": "AAAAAGXNhLrhGtltTwCpmqlarh7s1DB2hIkbP//jgzn4Fos/AAAACgAAAEEAAABmAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAMPT7P7buwqnMueFS4NV10vE2q3C/mcAy4jx03/RdSGsAAAAXSHboAAAAAAAAAAAB+BaLPwAAAEBPWWMNSWyPBbQlhRheXyvAFDVx1rnf68fdDOUHPdDIkHdUczBpzvCjpdgwhQ2NYOX5ga1ZgOIWLy789YNnuIcL", "result_xdr": "AAAAAAAAAAoAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", diff --git a/services/horizon/internal/docs/reference/endpoints/transactions-create.md b/services/horizon/internal/docs/reference/endpoints/transactions-create.md index c5ec6a0d0f..c63df49b3c 100644 --- a/services/horizon/internal/docs/reference/endpoints/transactions-create.md +++ b/services/horizon/internal/docs/reference/endpoints/transactions-create.md @@ -36,7 +36,7 @@ If you are encountering this error it means that either: The former case may happen because there was no room for your transaction in the 3 consecutive ledgers. In such case, Core server removes a transaction from a queue. To solve this you can either: * Keep resubmitting the same transaction (with the same sequence number) and wait until it finally is added to a new ledger or: -* Increase the [fee](/developers/guides/concepts/fees.html). +* Increase the [fee](../../../guides/concepts/fees.html). ## Request diff --git a/services/horizon/internal/docs/reference/endpoints/transactions-for-ledger.md b/services/horizon/internal/docs/reference/endpoints/transactions-for-ledger.md index bf255cf25a..eea110a8b7 100644 --- a/services/horizon/internal/docs/reference/endpoints/transactions-for-ledger.md +++ b/services/horizon/internal/docs/reference/endpoints/transactions-for-ledger.md @@ -102,7 +102,8 @@ resource](../resources/transaction.md) for reference. "created_at": "2019-04-09T20:14:25Z", "source_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", "source_account_sequence": "4660039994869", - "fee_paid": 100, + "fee_charged": 100, + "max_fee": 100, "operation_count": 1, "envelope_xdr": "AAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAZAAABD0AB031AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAFIMRkFZ9gZifhRSlklQpsz/9P04Earv0dzS3MkIM1cYAAAAXSHboAAAAAAAAAAABhlbgnAAAAEA+biIjrDy8yi+SvhFElIdWGBRYlDscnSSHkPchePy2JYDJn4wvJYDBumXI7/NmttUey3+cGWbBFfnnWh1H5EoD", "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", @@ -148,7 +149,8 @@ resource](../resources/transaction.md) for reference. "created_at": "2019-04-09T20:14:25Z", "source_account": "GAZ4A54KE6MTMXYEPM7T3IDLZWGNCCKB5ME422NZ3MAMTHWWP37RPEBW", "source_account_sequence": "2994107601387521", - "fee_paid": 100, + "fee_charged": 100, + "max_fee": 100, "operation_count": 1, "envelope_xdr": "AAAAADPAd4onmTZfBHs/PaBrzYzRCUHrCc1pudsAyZ7Wfv8XAAAAZAAKoyAAAAABAAAAAAAAAAEAAAAQMkExVjZKNTcwM0c0N1hIWQAAAAEAAAABAAAAADPAd4onmTZfBHs/PaBrzYzRCUHrCc1pudsAyZ7Wfv8XAAAAAQAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAAAAAAAAhFKDAAAAAAAAAAAB1n7/FwAAAEBJdXuYg13Glzx1RinVCXd/cc1usrhU/0f5HFZ7lyIR8kS3T6PRrW78TQDNqXz+ukUiPwlB1A8MqxoW/SAL5FIB", "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", @@ -194,7 +196,8 @@ resource](../resources/transaction.md) for reference. "created_at": "2019-04-09T20:14:25Z", "source_account": "GABRMXDIJCTDSMPC67J64NSAMWRSYXVCXYTXVFC73DTHBKELHNKWANXP", "source_account_sequence": "122518237256298", - "fee_paid": 100, + "fee_charged": 100, + "max_fee": 100, "operation_count": 1, "envelope_xdr": "AAAAAAMWXGhIpjkx4vfT7jZAZaMsXqK+J3qUX9jmcKiLO1VgAAAAZAAAb24AAppqAAAAAQAAAAAAAAAAAAAAAFys/kkAAAABAAAABVdIQUxFAAAAAAAAAQAAAAAAAAAAAAAAAKrN4k6edFMb0WEyPzEEjWUAji0pvvALw+BAH4OnekA5AAAAAAcnDgAAAAAAAAAAAYs7VWAAAABAYd9uIm+TjIcAjTU90YJoNg/r+6PU3Uss7ewUb1w3yMa+HyoSvDq8sDz/SYmDBH7F+0ACIeBF4kkVEKVBJMh0AQ==", "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", diff --git a/services/horizon/internal/docs/reference/endpoints/transactions-single.md b/services/horizon/internal/docs/reference/endpoints/transactions-single.md index 60da18880b..da48891b2f 100644 --- a/services/horizon/internal/docs/reference/endpoints/transactions-single.md +++ b/services/horizon/internal/docs/reference/endpoints/transactions-single.md @@ -89,7 +89,8 @@ This endpoint responds with a single Transaction. See [transaction resource](.. "created_at": "2019-04-09T20:14:25Z", "source_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", "source_account_sequence": "4660039994869", - "fee_paid": 100, + "fee_charged": 100, + "max_fee": 100, "operation_count": 1, "envelope_xdr": "AAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAZAAABD0AB031AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAFIMRkFZ9gZifhRSlklQpsz/9P04Earv0dzS3MkIM1cYAAAAXSHboAAAAAAAAAAABhlbgnAAAAEA+biIjrDy8yi+SvhFElIdWGBRYlDscnSSHkPchePy2JYDJn4wvJYDBumXI7/NmttUey3+cGWbBFfnnWh1H5EoD", "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", diff --git a/services/horizon/internal/docs/reference/resources/transaction.md b/services/horizon/internal/docs/reference/resources/transaction.md index db4aa80ef4..e77b5542e7 100644 --- a/services/horizon/internal/docs/reference/resources/transaction.md +++ b/services/horizon/internal/docs/reference/resources/transaction.md @@ -20,7 +20,8 @@ To learn more about the concept of transactions in the Stellar network, take a l | created_at | ISO8601 string | | | source_account | string | | | source_account_sequence | string | | -| fee_paid | number | The fee paid by the source account of this transaction when the transaction was applied to the ledger. | +| max_fee | number | The the maximum fee the source account was willing to pay. | +| fee_charged | number | The fee paid by the source account of this transaction when the transaction was applied to the ledger. | | operation_count | number | The number of operations that are contained within this transaction. | | envelope_xdr | string | A base64 encoded string of the raw `TransactionEnvelope` xdr struct for this transaction | | result_xdr | string | A base64 encoded string of the raw `TransactionResult` xdr struct for this transaction | @@ -81,7 +82,8 @@ To learn more about the concept of transactions in the Stellar network, take a l "created_at": "2019-02-21T21:44:13Z", "source_account": "GCDLRUXOD6KA53G5ILL435TZAISNLPS4EKIHSOVY3MVD3DVJ333NO4DT", "source_account_sequence": "10105916313567234", - "fee_paid": 100, + "max_fee": 100, + "fee_charged":100, "operation_count": 1, "envelope_xdr": "AAAAAIa40u4flA7s3ULXzfZ5AiTVvlwikHk6uNsqPY6p3vbXAAAAZAAj50cAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAB2Fmc2RmYXMAAAAAAQAAAAAAAAABAAAAAIa40u4flA7s3ULXzfZ5AiTVvlwikHk6uNsqPY6p3vbXAAAAAAAAAAEqBfIAAAAAAAAAAAGp3vbXAAAAQKElK3CoNo1f8fWIGeJm98lw2AaFiyVVFhx3uFok0XVW3MHV9MubtEhfA+n1iLPrxmzHtHfmZsumWk+sOEQlSwI=", "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", diff --git a/services/horizon/internal/docs/reference/tutorials/follow-received-payments.md b/services/horizon/internal/docs/reference/tutorials/follow-received-payments.md index daf2bcd978..6470f91536 100644 --- a/services/horizon/internal/docs/reference/tutorials/follow-received-payments.md +++ b/services/horizon/internal/docs/reference/tutorials/follow-received-payments.md @@ -168,9 +168,9 @@ New payment: ## Testing it out -We now know how to get a stream of transactions to an account. Let's check if our solution actually works and if new payments appear. Let's watch as we send a payment ([`create_account` operation](/developers/guides/concepts/list-of-operations.html#create-account)) from our account to another account. +We now know how to get a stream of transactions to an account. Let's check if our solution actually works and if new payments appear. Let's watch as we send a payment ([`create_account` operation](../../../guides/concepts/list-of-operations.html#create-account)) from our account to another account. -We use the `create_account` operation because we are sending payment to a new, unfunded account. If we were sending payment to an account that is already funded, we would use the [`payment` operation](/developers/guides/concepts/list-of-operations.html#payment). +We use the `create_account` operation because we are sending payment to a new, unfunded account. If we were sending payment to an account that is already funded, we would use the [`payment` operation](../../../guides/concepts/list-of-operations.html#payment). First, let's check our account sequence number so we can create a payment transaction. To do this we send a request to horizon: diff --git a/services/horizon/internal/expingest/BETA_TESTING.md b/services/horizon/internal/expingest/BETA_TESTING.md index 5761fe82aa..dad0e58163 100644 --- a/services/horizon/internal/expingest/BETA_TESTING.md +++ b/services/horizon/internal/expingest/BETA_TESTING.md @@ -13,7 +13,13 @@ The new ingestion system solves issues found in the previous version like: incon ## Prerequisities * The init stage (state ingestion) for public network requires around 1.5GB of RAM. The memory is released after the state ingestion. State ingestion is performed only once, restarting the server will not trigger it unless Horizon has been upgraded to a newer version (with updated ingestion pipeline). We are currently working on alternative solutions to make RAM requirements smaller however we believe that it will become smaller and smaller as more buckets are [CAP-20](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0020.md) compatible. The CPU footprint of the new ingestion is really small. We were able to run experimental ingestion on `c5.large` instance on AWS. The init stage takes a few minutes on `c5.large`. -* The state data requires additional 1.3GB DB disk space (as of version 0.20.1). +* The state data requires additional 5GB DB disk space for public network (as of version 0.22.0): + * `accounts_signers` table: 1914 MB + * `trust_lines` table: 1780 MB + * `accounts` table: 1332 MB + * `offers` table: 26 MB + * `accounts_data` table: 13 MB + * `exp_asset_stats` table: less than 1 MB * Flags needed to enable experimental ingestion: * `ENABLE_EXPERIMENTAL_INGESTION=true` * `HISTORY_ARCHIVE_URLS="archive1,archive2,archive3"` (for public network you can use one of SDF's archives, ex. `https://history.stellar.org/prd/core-live/core_live_001`) diff --git a/services/horizon/internal/expingest/load_orderbook_graph_test.go b/services/horizon/internal/expingest/load_orderbook_graph_test.go index 30472da451..77a9790e27 100644 --- a/services/horizon/internal/expingest/load_orderbook_graph_test.go +++ b/services/horizon/internal/expingest/load_orderbook_graph_test.go @@ -64,7 +64,7 @@ func TestLoadOrderBookGraphFromEmptyDB(t *testing.T) { q := &history.Q{tt.HorizonSession()} graph := orderbook.NewOrderBookGraph() - err := loadOrderBookGraphFromDB(q, graph) + err := loadOrderBookGraphFromDB(q, graph, 1) tt.Assert.NoError(err) tt.Assert.True(graph.IsEmpty()) } @@ -82,7 +82,7 @@ func TestLoadOrderBookGraph(t *testing.T) { _, err = q.InsertOffer(twoEurOffer, 123) tt.Assert.NoError(err) - err = loadOrderBookGraphFromDB(q, graph) + err = loadOrderBookGraphFromDB(q, graph, 1) tt.Assert.NoError(err) tt.Assert.False(graph.IsEmpty()) diff --git a/services/horizon/internal/expingest/main.go b/services/horizon/internal/expingest/main.go index aa38eb11c0..310569c4b7 100644 --- a/services/horizon/internal/expingest/main.go +++ b/services/horizon/internal/expingest/main.go @@ -28,14 +28,18 @@ const ( // // Version history: // - 1: Initial version - // - 2: We added the orderbook, offers processors and distributed - // ingestion. - // - 3: Fixes a bug that could potentialy result in invalid state + // - 2: Added the orderbook, offers processors and distributed ingestion. + // - 3: Fixed a bug that could potentialy result in invalid state // (#1722). Update the version to clear the state. - // - 4: Fixes a bug in AccountSignersChanged method. - // - 5: Fixes AccountSigners processor to remove preauth tx signer + // - 4: Fixed a bug in AccountSignersChanged method. + // - 5: Added trust lines. + // - 6: Added accounts and accounts data. + // - 7: Fixes a bug in AccountSignersChanged method. + // - 8: Fixes AccountSigners processor to remove preauth tx signer // when preauth tx is failed. - CurrentVersion = 5 + // - 9: Fixes a bug in asset stats processor that counted unauthorized + // trustlines. + CurrentVersion = 9 ) var log = ilog.DefaultLogger.WithField("service", "expingest") @@ -86,6 +90,8 @@ type System struct { historySession dbSession graph *orderbook.OrderBookGraph retry retry + stateReady bool + stateReadyLock sync.RWMutex // stateVerificationRunning is true when verification routine is currently // running. @@ -280,7 +286,7 @@ func (s *System) Run() { log.WithField("last_ledger", lastIngestedLedger). Info("Resuming ingestion system from last processed ledger...") - err = loadOrderBookGraphFromDB(s.historyQ, s.graph) + err = loadOrderBookGraphFromDB(s.historyQ, s.graph, lastIngestedLedger) if err != nil { return errors.Wrap(err, "Error loading order book graph from db") } @@ -291,7 +297,11 @@ func (s *System) Run() { }) } -func loadOrderBookGraphFromDB(historyQ dbQ, graph *orderbook.OrderBookGraph) error { +func loadOrderBookGraphFromDB( + historyQ dbQ, + graph *orderbook.OrderBookGraph, + lastIngestedLedger uint32, +) error { defer graph.Discard() log.Info("Loading offers from a database into memory store...") @@ -318,7 +328,7 @@ func loadOrderBookGraphFromDB(historyQ dbQ, graph *orderbook.OrderBookGraph) err }) } - err = graph.Apply() + err = graph.Apply(lastIngestedLedger) if err == nil { log.WithField( "duration", @@ -349,6 +359,19 @@ func (s *System) resumeFromLedger(lastIngestedLedger uint32) { }) } +// StateReady returns true if the ingestion system has finished running it's state pipelines +func (s *System) StateReady() bool { + s.stateReadyLock.RLock() + defer s.stateReadyLock.RUnlock() + return s.stateReady +} + +func (s *System) setStateReady() { + s.stateReadyLock.Lock() + defer s.stateReadyLock.Unlock() + s.stateReady = true +} + func (s *System) Shutdown() { log.Info("Shutting down ingestion system...") s.session.Shutdown() diff --git a/services/horizon/internal/expingest/pipeline_hooks_test.go b/services/horizon/internal/expingest/pipeline_hooks_test.go index 0b79810929..e85ffcbd9e 100644 --- a/services/horizon/internal/expingest/pipeline_hooks_test.go +++ b/services/horizon/internal/expingest/pipeline_hooks_test.go @@ -18,6 +18,7 @@ func TestStatePreProcessingHook(t *testing.T) { tt := test.Start(t).Scenario("base") defer tt.Finish() + system := &System{} session := tt.HorizonSession() defer session.Rollback() ctx := context.WithValue( @@ -30,10 +31,11 @@ func TestStatePreProcessingHook(t *testing.T) { tt.Assert.Nil(historyQ.UpdateLastLedgerExpIngest(0)) tt.Assert.Nil(session.GetTx()) - newCtx, err := preProcessingHook(ctx, pipelineType, session) + newCtx, err := preProcessingHook(ctx, pipelineType, system, session) tt.Assert.NoError(err) tt.Assert.NotNil(session.GetTx()) tt.Assert.Nil(newCtx.Value(horizonProcessors.IngestUpdateDatabase)) + tt.Assert.False(system.StateReady()) tt.Assert.Nil(session.Rollback()) tt.Assert.Nil(session.GetTx()) @@ -41,16 +43,18 @@ func TestStatePreProcessingHook(t *testing.T) { tt.Assert.Nil(session.Begin()) tt.Assert.NotNil(session.GetTx()) - newCtx, err = preProcessingHook(ctx, pipelineType, session) + newCtx, err = preProcessingHook(ctx, pipelineType, system, session) tt.Assert.NoError(err) tt.Assert.NotNil(session.GetTx()) tt.Assert.Nil(newCtx.Value(horizonProcessors.IngestUpdateDatabase)) + tt.Assert.False(system.StateReady()) } func TestLedgerPreProcessingHook(t *testing.T) { tt := test.Start(t).Scenario("base") defer tt.Finish() + system := &System{} session := tt.HorizonSession() defer session.Rollback() ctx := context.WithValue( @@ -63,36 +67,47 @@ func TestLedgerPreProcessingHook(t *testing.T) { tt.Assert.Nil(historyQ.UpdateLastLedgerExpIngest(1)) tt.Assert.Nil(session.GetTx()) - newCtx, err := preProcessingHook(ctx, pipelineType, session) + newCtx, err := preProcessingHook(ctx, pipelineType, system, session) tt.Assert.NoError(err) tt.Assert.NotNil(session.GetTx()) tt.Assert.Equal(newCtx.Value(horizonProcessors.IngestUpdateDatabase), true) + tt.Assert.True(system.StateReady()) tt.Assert.Nil(session.Rollback()) tt.Assert.Nil(session.GetTx()) + system.stateReady = false + tt.Assert.False(system.StateReady()) tt.Assert.Nil(session.Begin()) tt.Assert.NotNil(session.GetTx()) - newCtx, err = preProcessingHook(ctx, pipelineType, session) + newCtx, err = preProcessingHook(ctx, pipelineType, system, session) tt.Assert.NoError(err) tt.Assert.NotNil(session.GetTx()) tt.Assert.Equal(newCtx.Value(horizonProcessors.IngestUpdateDatabase), true) + tt.Assert.True(system.StateReady()) tt.Assert.Nil(session.Rollback()) tt.Assert.Nil(session.GetTx()) + system.stateReady = false + tt.Assert.False(system.StateReady()) tt.Assert.Nil(historyQ.UpdateLastLedgerExpIngest(2)) - newCtx, err = preProcessingHook(ctx, pipelineType, session) + newCtx, err = preProcessingHook(ctx, pipelineType, system, session) tt.Assert.NoError(err) tt.Assert.Nil(session.GetTx()) tt.Assert.Nil(newCtx.Value(horizonProcessors.IngestUpdateDatabase)) + tt.Assert.True(system.StateReady()) tt.Assert.Nil(session.Begin()) tt.Assert.NotNil(session.GetTx()) - newCtx, err = preProcessingHook(ctx, pipelineType, session) + system.stateReady = false + tt.Assert.False(system.StateReady()) + + newCtx, err = preProcessingHook(ctx, pipelineType, system, session) tt.Assert.NoError(err) tt.Assert.Nil(session.GetTx()) tt.Assert.Nil(newCtx.Value(horizonProcessors.IngestUpdateDatabase)) + tt.Assert.True(system.StateReady()) } func TestPostProcessingHook(t *testing.T) { diff --git a/services/horizon/internal/expingest/pipelines.go b/services/horizon/internal/expingest/pipelines.go index f5aa0fc9ad..0da04719c6 100644 --- a/services/horizon/internal/expingest/pipelines.go +++ b/services/horizon/internal/expingest/pipelines.go @@ -26,9 +26,13 @@ const ( ledgerPipeline pType = "ledger_pipeline" ) -func accountForSignerStateNode(q *history.Q) *supportPipeline.PipelineNode { +func accountsStateNode(q *history.Q) *supportPipeline.PipelineNode { return pipeline.StateNode(&processors.EntryTypeFilter{Type: xdr.LedgerEntryTypeAccount}). Pipe( + pipeline.StateNode(&horizonProcessors.DatabaseProcessor{ + AccountsQ: q, + Action: horizonProcessors.Accounts, + }), pipeline.StateNode(&horizonProcessors.DatabaseProcessor{ SignersQ: q, Action: horizonProcessors.AccountsForSigner, @@ -36,6 +40,16 @@ func accountForSignerStateNode(q *history.Q) *supportPipeline.PipelineNode { ) } +func dataDBStateNode(q *history.Q) *supportPipeline.PipelineNode { + return pipeline.StateNode(&processors.EntryTypeFilter{Type: xdr.LedgerEntryTypeData}). + Pipe( + pipeline.StateNode(&horizonProcessors.DatabaseProcessor{ + DataQ: q, + Action: horizonProcessors.Data, + }), + ) +} + func orderBookDBStateNode(q *history.Q) *supportPipeline.PipelineNode { return pipeline.StateNode(&processors.EntryTypeFilter{Type: xdr.LedgerEntryTypeOffer}). Pipe( @@ -55,35 +69,34 @@ func orderBookGraphStateNode(graph *orderbook.OrderBookGraph) *supportPipeline.P ) } +func trustLinesDBStateNode(q *history.Q) *supportPipeline.PipelineNode { + return pipeline.StateNode(&processors.EntryTypeFilter{Type: xdr.LedgerEntryTypeTrustline}). + Pipe( + pipeline.StateNode(&horizonProcessors.DatabaseProcessor{ + TrustLinesQ: q, + AssetStatsQ: q, + Action: horizonProcessors.TrustLines, + }), + ) +} + func buildStatePipeline(historyQ *history.Q, graph *orderbook.OrderBookGraph) *pipeline.StatePipeline { statePipeline := &pipeline.StatePipeline{} statePipeline.SetRoot( pipeline.StateNode(&processors.RootProcessor{}). Pipe( - accountForSignerStateNode(historyQ), + accountsStateNode(historyQ), + dataDBStateNode(historyQ), orderBookDBStateNode(historyQ), orderBookGraphStateNode(graph), + trustLinesDBStateNode(historyQ), ), ) return statePipeline } -func accountForSignerLedgerNode(q *history.Q) *supportPipeline.PipelineNode { - return pipeline.LedgerNode(&horizonProcessors.DatabaseProcessor{ - SignersQ: q, - Action: horizonProcessors.AccountsForSigner, - }) -} - -func orderBookDBLedgerNode(q *history.Q) *supportPipeline.PipelineNode { - return pipeline.LedgerNode(&horizonProcessors.DatabaseProcessor{ - OffersQ: q, - Action: horizonProcessors.Offers, - }) -} - func orderBookGraphLedgerNode(graph *orderbook.OrderBookGraph) *supportPipeline.PipelineNode { return pipeline.LedgerNode(&horizonProcessors.OrderbookProcessor{ OrderBookGraph: graph, @@ -99,8 +112,15 @@ func buildLedgerPipeline(historyQ *history.Q, graph *orderbook.OrderBookGraph) * // This subtree will only run when `IngestUpdateDatabase` is set. pipeline.LedgerNode(&horizonProcessors.ContextFilter{horizonProcessors.IngestUpdateDatabase}). Pipe( - accountForSignerLedgerNode(historyQ), - orderBookDBLedgerNode(historyQ), + pipeline.LedgerNode(&horizonProcessors.DatabaseProcessor{ + AccountsQ: historyQ, + DataQ: historyQ, + OffersQ: historyQ, + SignersQ: historyQ, + TrustLinesQ: historyQ, + AssetStatsQ: historyQ, + Action: horizonProcessors.All, + }), ), orderBookGraphLedgerNode(graph), ), @@ -112,6 +132,7 @@ func buildLedgerPipeline(historyQ *history.Q, graph *orderbook.OrderBookGraph) * func preProcessingHook( ctx context.Context, pipelineType pType, + system *System, historySession *db.Session, ) (context.Context, error) { historyQ := &history.Q{historySession} @@ -142,6 +163,10 @@ func preProcessingHook( // from a database is done outside the pipeline. updateDatabase = true } else { + // mark the system as ready because we have progressed to running + // the ledger pipeline + system.setStateReady() + if lastIngestedLedger+1 == ledgerSeq { // lastIngestedLedger+1 == ledgerSeq what means that this instance // is the main ingesting instance in this round and should update a @@ -229,7 +254,7 @@ func postProcessingHook( } } - err = graph.Apply() + err = graph.Apply(ledgerSeq) if err != nil { return errors.Wrap(err, "Error applying order book changes") } @@ -289,7 +314,7 @@ func addPipelineHooks( } p.AddPreProcessingHook(func(ctx context.Context) (context.Context, error) { - return preProcessingHook(ctx, pipelineType, historySession) + return preProcessingHook(ctx, pipelineType, system, historySession) }) p.AddPostProcessingHook(func(ctx context.Context, err error) error { diff --git a/services/horizon/internal/expingest/processors/accounts_data_processor_test.go b/services/horizon/internal/expingest/processors/accounts_data_processor_test.go new file mode 100644 index 0000000000..9621479626 --- /dev/null +++ b/services/horizon/internal/expingest/processors/accounts_data_processor_test.go @@ -0,0 +1,418 @@ +package processors + +import ( + "context" + stdio "io" + "testing" + + "github.com/stellar/go/exp/ingest/io" + supportPipeline "github.com/stellar/go/exp/support/pipeline" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/suite" +) + +func TestAccountsDataProcessorTestSuiteState(t *testing.T) { + suite.Run(t, new(AccountsDataProcessorTestSuiteState)) +} + +type AccountsDataProcessorTestSuiteState struct { + suite.Suite + processor *DatabaseProcessor + mockQ *history.MockQData + mockBatchInsertBuilder *history.MockAccountDataBatchInsertBuilder + mockStateReader *io.MockStateReader + mockStateWriter *io.MockStateWriter +} + +func (s *AccountsDataProcessorTestSuiteState) SetupTest() { + s.mockQ = &history.MockQData{} + s.mockBatchInsertBuilder = &history.MockAccountDataBatchInsertBuilder{} + s.mockStateReader = &io.MockStateReader{} + s.mockStateWriter = &io.MockStateWriter{} + + s.processor = &DatabaseProcessor{ + Action: Data, + DataQ: s.mockQ, + } + + // Reader and Writer should be always closed and once + s.mockStateReader.On("Close").Return(nil).Once() + s.mockStateWriter.On("Close").Return(nil).Once() + + s.mockQ. + On("NewAccountDataBatchInsertBuilder", maxBatchSize). + Return(s.mockBatchInsertBuilder).Once() +} + +func (s *AccountsDataProcessorTestSuiteState) TearDownTest() { + s.mockQ.AssertExpectations(s.T()) + s.mockBatchInsertBuilder.AssertExpectations(s.T()) + s.mockStateReader.AssertExpectations(s.T()) + s.mockStateWriter.AssertExpectations(s.T()) +} + +func (s *AccountsDataProcessorTestSuiteState) TestNoEntries() { + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{}, stdio.EOF).Once() + + s.mockBatchInsertBuilder.On("Exec").Return(nil).Once() + + err := s.processor.ProcessState( + context.Background(), + &supportPipeline.Store{}, + s.mockStateReader, + s.mockStateWriter, + ) + + s.Assert().NoError(err) +} + +func (s *AccountsDataProcessorTestSuiteState) TestInvalidEntry() { + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + }, nil).Once() + + err := s.processor.ProcessState( + context.Background(), + &supportPipeline.Store{}, + s.mockStateReader, + s.mockStateWriter, + ) + + s.Assert().EqualError(err, "DatabaseProcessor requires LedgerEntryChangeTypeLedgerEntryState changes only") +} + +func (s *AccountsDataProcessorTestSuiteState) TestCreatesAccounts() { + data := xdr.DataEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + DataName: "test", + DataValue: []byte{1, 1, 1, 1}, + } + lastModifiedLedgerSeq := xdr.Uint32(123) + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeData, + Data: &data, + }, + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + }, + }, nil).Once() + + s.mockBatchInsertBuilder.On("Add", data, lastModifiedLedgerSeq).Return(nil).Once() + + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{}, stdio.EOF).Once() + + s.mockBatchInsertBuilder.On("Exec").Return(nil).Once() + + err := s.processor.ProcessState( + context.Background(), + &supportPipeline.Store{}, + s.mockStateReader, + s.mockStateWriter, + ) + + s.Assert().NoError(err) +} + +func TestAccountsDataProcessorTestSuiteLedger(t *testing.T) { + suite.Run(t, new(AccountsDataProcessorTestSuiteLedger)) +} + +type AccountsDataProcessorTestSuiteLedger struct { + suite.Suite + processor *DatabaseProcessor + mockQ *history.MockQData + mockLedgerReader *io.MockLedgerReader + mockLedgerWriter *io.MockLedgerWriter +} + +func (s *AccountsDataProcessorTestSuiteLedger) SetupTest() { + s.mockQ = &history.MockQData{} + s.mockLedgerReader = &io.MockLedgerReader{} + s.mockLedgerWriter = &io.MockLedgerWriter{} + + s.processor = &DatabaseProcessor{ + Action: Data, + DataQ: s.mockQ, + } + + // Reader and Writer should be always closed and once + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + + s.mockLedgerReader. + On("Close"). + Return(nil).Once() + + s.mockLedgerWriter. + On("Close"). + Return(nil).Once() +} + +func (s *AccountsDataProcessorTestSuiteLedger) TearDownTest() { + s.mockQ.AssertExpectations(s.T()) + s.mockLedgerReader.AssertExpectations(s.T()) + s.mockLedgerWriter.AssertExpectations(s.T()) +} + +func (s *AccountsDataProcessorTestSuiteLedger) TestNoTransactions() { + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + +func (s *AccountsDataProcessorTestSuiteLedger) TestNewAccount() { + data := xdr.DataEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + DataName: "test", + DataValue: []byte{1, 1, 1, 1}, + } + lastModifiedLedgerSeq := xdr.Uint32(123) + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeData, + Data: &data, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "InsertAccountData", + data, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + + updatedData := xdr.DataEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + DataName: "test", + DataValue: []byte{2, 2, 2, 2}, + } + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeData, + Data: &data, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeData, + Data: &updatedData, + }, + }, + }, + }, + }, + }), + }, nil).Once() + s.mockQ.On( + "UpdateAccountData", + updatedData, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + +func (s *AccountsDataProcessorTestSuiteLedger) TestRemoveAccount() { + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeData, + Data: &xdr.DataEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + DataName: "test", + DataValue: []byte{1, 1, 1, 1}, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryRemoved, + Removed: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeData, + Data: &xdr.LedgerKeyData{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + DataName: "test", + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "RemoveAccountData", + xdr.LedgerKeyData{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + DataName: "test", + }, + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + +func (s *AccountsDataProcessorTestSuiteLedger) TestProcessUpgradeChange() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + + data := xdr.DataEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + DataName: "test", + DataValue: []byte{1, 1, 1, 1}, + } + lastModifiedLedgerSeq := xdr.Uint32(123) + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeData, + Data: &data, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "InsertAccountData", + data, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + // Process ledger entry upgrades + modifiedData := data + modifiedData.DataValue = []byte{2, 2, 2, 2} + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return( + io.Change{ + Type: xdr.LedgerEntryTypeData, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeData, + Data: &data, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq + 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeData, + Data: &modifiedData, + }, + }, + }, nil).Once() + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + + s.mockLedgerReader.On("Close").Return(nil).Once() + + s.mockQ.On( + "UpdateAccountData", + modifiedData, + lastModifiedLedgerSeq+1, + ).Return(int64(1), nil).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} diff --git a/services/horizon/internal/expingest/processors/accounts_processor_test.go b/services/horizon/internal/expingest/processors/accounts_processor_test.go new file mode 100644 index 0000000000..1adfe3e692 --- /dev/null +++ b/services/horizon/internal/expingest/processors/accounts_processor_test.go @@ -0,0 +1,422 @@ +package processors + +import ( + "context" + stdio "io" + "testing" + + "github.com/stellar/go/exp/ingest/io" + supportPipeline "github.com/stellar/go/exp/support/pipeline" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/suite" +) + +func TestAccountsProcessorTestSuiteState(t *testing.T) { + suite.Run(t, new(AccountsProcessorTestSuiteState)) +} + +type AccountsProcessorTestSuiteState struct { + suite.Suite + processor *DatabaseProcessor + mockQ *history.MockQAccounts + mockBatchInsertBuilder *history.MockAccountsBatchInsertBuilder + mockStateReader *io.MockStateReader + mockStateWriter *io.MockStateWriter +} + +func (s *AccountsProcessorTestSuiteState) SetupTest() { + s.mockQ = &history.MockQAccounts{} + s.mockBatchInsertBuilder = &history.MockAccountsBatchInsertBuilder{} + s.mockStateReader = &io.MockStateReader{} + s.mockStateWriter = &io.MockStateWriter{} + + s.processor = &DatabaseProcessor{ + Action: Accounts, + AccountsQ: s.mockQ, + } + + // Reader and Writer should be always closed and once + s.mockStateReader.On("Close").Return(nil).Once() + s.mockStateWriter.On("Close").Return(nil).Once() + + s.mockQ. + On("NewAccountsBatchInsertBuilder", maxBatchSize). + Return(s.mockBatchInsertBuilder).Once() +} + +func (s *AccountsProcessorTestSuiteState) TearDownTest() { + s.mockQ.AssertExpectations(s.T()) + s.mockBatchInsertBuilder.AssertExpectations(s.T()) + s.mockStateReader.AssertExpectations(s.T()) + s.mockStateWriter.AssertExpectations(s.T()) +} + +func (s *AccountsProcessorTestSuiteState) TestNoEntries() { + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{}, stdio.EOF).Once() + + s.mockBatchInsertBuilder.On("Exec").Return(nil).Once() + + err := s.processor.ProcessState( + context.Background(), + &supportPipeline.Store{}, + s.mockStateReader, + s.mockStateWriter, + ) + + s.Assert().NoError(err) +} + +func (s *AccountsProcessorTestSuiteState) TestInvalidEntry() { + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + }, nil).Once() + + err := s.processor.ProcessState( + context.Background(), + &supportPipeline.Store{}, + s.mockStateReader, + s.mockStateWriter, + ) + + s.Assert().EqualError(err, "DatabaseProcessor requires LedgerEntryChangeTypeLedgerEntryState changes only") +} + +func (s *AccountsProcessorTestSuiteState) TestCreatesAccounts() { + account := xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Thresholds: [4]byte{1, 1, 1, 1}, + } + lastModifiedLedgerSeq := xdr.Uint32(123) + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &account, + }, + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + }, + }, nil).Once() + + s.mockBatchInsertBuilder.On("Add", account, lastModifiedLedgerSeq).Return(nil).Once() + + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{}, stdio.EOF).Once() + + s.mockBatchInsertBuilder.On("Exec").Return(nil).Once() + + err := s.processor.ProcessState( + context.Background(), + &supportPipeline.Store{}, + s.mockStateReader, + s.mockStateWriter, + ) + + s.Assert().NoError(err) +} + +func TestAccountsProcessorTestSuiteLedger(t *testing.T) { + suite.Run(t, new(AccountsProcessorTestSuiteLedger)) +} + +type AccountsProcessorTestSuiteLedger struct { + suite.Suite + processor *DatabaseProcessor + mockQ *history.MockQAccounts + mockLedgerReader *io.MockLedgerReader + mockLedgerWriter *io.MockLedgerWriter +} + +func (s *AccountsProcessorTestSuiteLedger) SetupTest() { + s.mockQ = &history.MockQAccounts{} + s.mockLedgerReader = &io.MockLedgerReader{} + s.mockLedgerWriter = &io.MockLedgerWriter{} + + s.processor = &DatabaseProcessor{ + Action: Accounts, + AccountsQ: s.mockQ, + } + + // Reader and Writer should be always closed and once + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + + s.mockLedgerReader. + On("Close"). + Return(nil).Once() + + s.mockLedgerWriter. + On("Close"). + Return(nil).Once() +} + +func (s *AccountsProcessorTestSuiteLedger) TearDownTest() { + s.mockQ.AssertExpectations(s.T()) + s.mockLedgerReader.AssertExpectations(s.T()) + s.mockLedgerWriter.AssertExpectations(s.T()) +} + +func (s *AccountsProcessorTestSuiteLedger) TestNoTransactions() { + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + +func (s *AccountsProcessorTestSuiteLedger) TestNewAccount() { + account := xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Thresholds: [4]byte{1, 1, 1, 1}, + } + lastModifiedLedgerSeq := xdr.Uint32(123) + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &account, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "InsertAccount", + account, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + + updatedAccount := xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Thresholds: [4]byte{0, 1, 2, 3}, + HomeDomain: "stellar.org", + } + + // failed tx shouldn't be ignored in accounts processor (seqnum and fees!) + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Result: xdr.TransactionResultPair{ + Result: xdr.TransactionResult{ + Result: xdr.TransactionResultResult{ + Code: xdr.TransactionResultCodeTxFailed, + }, + }, + }, + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &account, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &updatedAccount, + }, + }, + }, + }, + }, + }), + }, nil).Once() + s.mockQ.On( + "UpdateAccount", + updatedAccount, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + +func (s *AccountsProcessorTestSuiteLedger) TestRemoveAccount() { + // add offer + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Thresholds: [4]byte{1, 1, 1, 1}, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryRemoved, + Removed: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "RemoveAccount", + "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + +func (s *AccountsProcessorTestSuiteLedger) TestProcessUpgradeChange() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + + account := xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Thresholds: [4]byte{1, 1, 1, 1}, + } + lastModifiedLedgerSeq := xdr.Uint32(123) + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &account, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "InsertAccount", + account, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + + updatedAccount := xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Thresholds: [4]byte{0, 1, 2, 3}, + HomeDomain: "stellar.org", + } + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return( + io.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &account, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq + 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &updatedAccount, + }, + }, + }, nil).Once() + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + + s.mockLedgerReader.On("Close").Return(nil).Once() + + s.mockQ.On( + "UpdateAccount", + updatedAccount, + lastModifiedLedgerSeq+1, + ).Return(int64(1), nil).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} diff --git a/services/horizon/internal/expingest/processors/accounts_signer_processor_test.go b/services/horizon/internal/expingest/processors/accounts_signer_processor_test.go index e02a6f8ac9..951dd1b152 100644 --- a/services/horizon/internal/expingest/processors/accounts_signer_processor_test.go +++ b/services/horizon/internal/expingest/processors/accounts_signer_processor_test.go @@ -177,6 +177,10 @@ func (s *AccountsSignerProcessorTestSuiteLedger) SetupTest() { } // Reader and Writer should be always closed and once + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + s.mockLedgerReader. On("Close"). Return(nil).Once() @@ -599,6 +603,10 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestRemoveAccount() { } func (s *AccountsSignerProcessorTestSuiteLedger) TestNewAccountNoRowsAffected() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + s.mockLedgerReader.On("Close").Return(nil).Once() + s.mockLedgerReader. On("Read"). Return(io.LedgerTransaction{ @@ -642,13 +650,17 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestNewAccountNoRowsAffected() s.Assert().IsType(verify.StateError{}, errors.Cause(err)) s.Assert().EqualError( err, - "Error in processLedgerAccountsForSigner: No rows affected when inserting "+ + "Error in AccountsForSigner handler: No rows affected when inserting "+ "account=GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML "+ "signer=GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML to database", ) } func (s *AccountsSignerProcessorTestSuiteLedger) TestRemoveAccountNoRowsAffected() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + s.mockLedgerReader.On("Close").Return(nil).Once() + s.mockLedgerReader. On("Read"). Return(io.LedgerTransaction{ @@ -700,12 +712,170 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestRemoveAccountNoRowsAffected s.Assert().IsType(verify.StateError{}, errors.Cause(err)) s.Assert().EqualError( err, - "Error in processLedgerAccountsForSigner: Expected "+ + "Error in AccountsForSigner handler: Expected "+ "account=GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML "+ "signer=GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML in database but not found when removing", ) } +func (s *AccountsSignerProcessorTestSuiteLedger) TestProcessUpgradeChange() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), + Weight: 10, + }, + }, + }, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), + Weight: 10, + }, + xdr.Signer{ + Key: xdr.MustSigner("GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS"), + Weight: 15, + }, + }, + }, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + // Remove old signer + s.mockQ. + On( + "RemoveAccountSigner", + "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + ). + Return(int64(1), nil).Once() + + // Create new and old signer + s.mockQ. + On( + "CreateAccountSigner", + "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + int32(10), + ). + Return(int64(1), nil).Once() + + s.mockQ. + On( + "CreateAccountSigner", + "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + "GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS", + int32(15), + ). + Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return( + io.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 1000, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), + Weight: 10, + }, + }, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 1001, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + xdr.Signer{ + Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), + Weight: 12, + }, + }, + }, + }, + }, + }, nil).Once() + + // Update signer + s.mockQ. + On( + "RemoveAccountSigner", + "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + ). + Return(int64(1), nil).Once() + + s.mockQ. + On( + "CreateAccountSigner", + "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + int32(12), + ). + Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + + s.mockLedgerReader.On("Close").Return(nil).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + func createTransactionMeta(opMeta []xdr.OperationMeta) xdr.TransactionMeta { return xdr.TransactionMeta{ V: 1, diff --git a/services/horizon/internal/expingest/processors/asset_stats_set.go b/services/horizon/internal/expingest/processors/asset_stats_set.go new file mode 100644 index 0000000000..ee12344136 --- /dev/null +++ b/services/horizon/internal/expingest/processors/asset_stats_set.go @@ -0,0 +1,80 @@ +package processors + +import ( + "math/big" + + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +type assetStatKey struct { + assetType xdr.AssetType + assetCode string + assetIssuer string +} +type assetStatValue struct { + amount *big.Int + numAccounts int32 +} + +// AssetStatSet represents a collection of asset stats +type AssetStatSet map[assetStatKey]*assetStatValue + +// Add updates the set with a trustline entry from a history archive snapshot +// if the trustline is authorized +func (s AssetStatSet) Add(trustLine xdr.TrustLineEntry) error { + if !xdr.TrustLineFlags(trustLine.Flags).IsAuthorized() { + return nil + } + + var key assetStatKey + if err := trustLine.Asset.Extract(&key.assetType, &key.assetCode, &key.assetIssuer); err != nil { + return errors.Wrap(err, "could not extract asset info from trustline") + } + + if current, ok := s[key]; !ok { + s[key] = &assetStatValue{ + amount: big.NewInt(int64(trustLine.Balance)), + numAccounts: 1, + } + } else { + current.amount.Add(current.amount, big.NewInt(int64(trustLine.Balance))) + current.numAccounts++ + } + return nil +} + +// Remove deletes an asset stat from the set +func (s AssetStatSet) Remove(assetType xdr.AssetType, assetCode string, assetIssuer string) (history.ExpAssetStat, bool) { + key := assetStatKey{assetType: assetType, assetIssuer: assetIssuer, assetCode: assetCode} + value, ok := s[key] + if !ok { + return history.ExpAssetStat{}, false + } + + delete(s, key) + + return history.ExpAssetStat{ + AssetType: key.assetType, + AssetCode: key.assetCode, + AssetIssuer: key.assetIssuer, + Amount: value.amount.String(), + NumAccounts: value.numAccounts, + }, true +} + +// All returns a list of all `history.ExpAssetStat` contained within the set +func (s AssetStatSet) All() []history.ExpAssetStat { + assetStats := make([]history.ExpAssetStat, 0, len(s)) + for key, value := range s { + assetStats = append(assetStats, history.ExpAssetStat{ + AssetType: key.assetType, + AssetCode: key.assetCode, + AssetIssuer: key.assetIssuer, + Amount: value.amount.String(), + NumAccounts: value.numAccounts, + }) + } + return assetStats +} diff --git a/services/horizon/internal/expingest/processors/asset_stats_set_test.go b/services/horizon/internal/expingest/processors/asset_stats_set_test.go new file mode 100644 index 0000000000..ef842ec898 --- /dev/null +++ b/services/horizon/internal/expingest/processors/asset_stats_set_test.go @@ -0,0 +1,200 @@ +package processors + +import ( + "math" + "sort" + "testing" + + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/xdr" +) + +func TestEmptyAssetStatSet(t *testing.T) { + set := AssetStatSet{} + if all := set.All(); len(all) != 0 { + t.Fatalf("expected empty list but got %v", all) + } + + _, ok := set.Remove( + xdr.AssetTypeAssetTypeCreditAlphanum4, + "USD", + "GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB", + ) + if ok { + t.Fatal("expected remove to return false") + } +} + +func assertAllEquals(t *testing.T, set AssetStatSet, expected []history.ExpAssetStat) { + all := set.All() + if len(all) != len(expected) { + t.Fatalf("expected list of %v asset stats but got %v", len(expected), all) + } + sort.Slice(all, func(i, j int) bool { + return all[i].AssetCode < all[j].AssetCode + }) + for i, got := range all { + if expected[i] != got { + t.Fatalf("expected asset stat to be %v but got %v", expected[i], got) + } + } +} + +func TestAssetStatSetIgnoresUnauthorizedTrustlines(t *testing.T) { + set := AssetStatSet{} + err := set.Add(xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 1, + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if all := set.All(); len(all) != 0 { + t.Fatalf("expected empty list but got %v", all) + } +} + +func TestAddAndRemoveAssetStats(t *testing.T) { + set := AssetStatSet{} + eur := "EUR" + eurAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetCode: eur, + AssetIssuer: trustLineIssuer.Address(), + Amount: "1", + NumAccounts: 1, + } + + err := set.Add(xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset(eur, trustLineIssuer.Address()), + Balance: 1, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + assertAllEquals(t, set, []history.ExpAssetStat{eurAssetStat}) + + err = set.Add(xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset(eur, trustLineIssuer.Address()), + Balance: 24, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + eurAssetStat.Amount = "25" + eurAssetStat.NumAccounts++ + assertAllEquals(t, set, []history.ExpAssetStat{eurAssetStat}) + + usd := "USD" + err = set.Add(xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Asset: xdr.MustNewCreditAsset(usd, trustLineIssuer.Address()), + Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + ether := "ETHER" + err = set.Add(xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Asset: xdr.MustNewCreditAsset(ether, trustLineIssuer.Address()), + Balance: 3, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + expected := []history.ExpAssetStat{ + history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, + AssetCode: ether, + AssetIssuer: trustLineIssuer.Address(), + Amount: "3", + NumAccounts: 1, + }, + eurAssetStat, + history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetCode: usd, + AssetIssuer: trustLineIssuer.Address(), + Amount: "10", + NumAccounts: 1, + }, + } + assertAllEquals(t, set, expected) + + for i, assetStat := range expected { + removed, ok := set.Remove(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + if !ok { + t.Fatal("expected remove to return true") + } + if removed != assetStat { + t.Fatalf("expected removed asset stat to be %v but got %v", assetStat, removed) + } + + assertAllEquals(t, set, expected[i+1:]) + } +} + +func TestOverflowAssetStatSet(t *testing.T) { + set := AssetStatSet{} + eur := "EUR" + err := set.Add(xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset(eur, trustLineIssuer.Address()), + Balance: math.MaxInt64, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + all := set.All() + if len(all) != 1 { + t.Fatalf("expected list of 1 asset stat but got %v", all) + } + + eurAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetCode: eur, + AssetIssuer: trustLineIssuer.Address(), + Amount: "9223372036854775807", + NumAccounts: 1, + } + if all[0] != eurAssetStat { + t.Fatalf("expected asset stat to be %v but got %v", eurAssetStat, all[0]) + } + + err = set.Add(xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset(eur, trustLineIssuer.Address()), + Balance: math.MaxInt64, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + all = set.All() + if len(all) != 1 { + t.Fatalf("expected list of 1 asset stat but got %v", all) + } + + eurAssetStat = history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetCode: eur, + AssetIssuer: trustLineIssuer.Address(), + Amount: "18446744073709551614", + NumAccounts: 2, + } + if all[0] != eurAssetStat { + t.Fatalf("expected asset stat to be %v but got %v", eurAssetStat, all[0]) + } +} diff --git a/services/horizon/internal/expingest/processors/database_processor.go b/services/horizon/internal/expingest/processors/database_processor.go index 373b79bb07..04fd3076d8 100644 --- a/services/horizon/internal/expingest/processors/database_processor.go +++ b/services/horizon/internal/expingest/processors/database_processor.go @@ -2,8 +2,10 @@ package processors import ( "context" + "database/sql" "fmt" stdio "io" + "math/big" "github.com/stellar/go/exp/ingest/io" ingestpipeline "github.com/stellar/go/exp/ingest/pipeline" @@ -21,15 +23,25 @@ func (p *DatabaseProcessor) ProcessState(ctx context.Context, store *pipeline.St defer w.Close() var ( + accountsBatch history.AccountsBatchInsertBuilder + accountDataBatch history.AccountDataBatchInsertBuilder accountSignerBatch history.AccountSignersBatchInsertBuilder offersBatch history.OffersBatchInsertBuilder + trustLinesBatch history.TrustLinesBatchInsertBuilder ) + assetStats := AssetStatSet{} switch p.Action { + case Accounts: + accountsBatch = p.AccountsQ.NewAccountsBatchInsertBuilder(maxBatchSize) + case Data: + accountDataBatch = p.DataQ.NewAccountDataBatchInsertBuilder(maxBatchSize) case AccountsForSigner: accountSignerBatch = p.SignersQ.NewAccountSignersBatchInsertBuilder(maxBatchSize) case Offers: offersBatch = p.OffersQ.NewOffersBatchInsertBuilder(maxBatchSize) + case TrustLines: + trustLinesBatch = p.TrustLinesQ.NewTrustLinesBatchInsertBuilder(maxBatchSize) default: return errors.Errorf("Invalid action type (%s)", p.Action) } @@ -49,6 +61,32 @@ func (p *DatabaseProcessor) ProcessState(ctx context.Context, store *pipeline.St } switch p.Action { + case Accounts: + // We're interested in accounts only + if entryChange.EntryType() != xdr.LedgerEntryTypeAccount { + continue + } + + err = accountsBatch.Add( + entryChange.MustState().Data.MustAccount(), + entryChange.MustState().LastModifiedLedgerSeq, + ) + if err != nil { + return errors.Wrap(err, "Error adding row to accountSignerBatch") + } + case Data: + // We're interested in data only + if entryChange.EntryType() != xdr.LedgerEntryTypeData { + continue + } + + err = accountDataBatch.Add( + entryChange.MustState().Data.MustData(), + entryChange.MustState().LastModifiedLedgerSeq, + ) + if err != nil { + return errors.Wrap(err, "Error adding row to accountSignerBatch") + } case AccountsForSigner: // We're interested in accounts only if entryChange.EntryType() != xdr.LedgerEntryTypeAccount { @@ -80,6 +118,25 @@ func (p *DatabaseProcessor) ProcessState(ctx context.Context, store *pipeline.St if err != nil { return errors.Wrap(err, "Error adding row to offersBatch") } + case TrustLines: + // We're interested in trust lines only + if entryChange.EntryType() != xdr.LedgerEntryTypeTrustline { + continue + } + + trustline := entryChange.MustState().Data.MustTrustLine() + err = assetStats.Add(trustline) + if err != nil { + return errors.Wrap(err, "Error adding trustline to asset stats set") + } + + err = trustLinesBatch.Add( + trustline, + entryChange.MustState().LastModifiedLedgerSeq, + ) + if err != nil { + return errors.Wrap(err, "Error adding row to trustLinesBatch") + } default: return errors.New("Unknown action") } @@ -95,10 +152,19 @@ func (p *DatabaseProcessor) ProcessState(ctx context.Context, store *pipeline.St var err error switch p.Action { + case Accounts: + err = accountsBatch.Exec() + case Data: + err = accountDataBatch.Exec() case AccountsForSigner: err = accountSignerBatch.Exec() case Offers: err = offersBatch.Exec() + case TrustLines: + err = trustLinesBatch.Exec() + if err == nil { + err = p.AssetStatsQ.InsertAssetStats(assetStats.All(), maxBatchSize) + } default: return errors.Errorf("Invalid action type (%s)", p.Action) } @@ -110,10 +176,37 @@ func (p *DatabaseProcessor) ProcessState(ctx context.Context, store *pipeline.St return nil } -func (p *DatabaseProcessor) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) error { - defer r.Close() +func (p *DatabaseProcessor) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) (err error) { + defer func() { + // io.LedgerReader.Close() returns error if upgrade changes have not + // been processed so it's worth checking the error. + closeErr := r.Close() + // Do not overwrite the previous error + if err == nil { + err = closeErr + } + }() defer w.Close() + actionHandlers := map[DatabaseProcessorActionType]func(change io.Change) error{ + Accounts: p.processLedgerAccounts, + AccountsForSigner: p.processLedgerAccountSigners, + Data: p.processLedgerAccountData, + Offers: p.processLedgerOffers, + TrustLines: p.processLedgerTrustLines, + } + + actions := []DatabaseProcessorActionType{} + + if p.Action == All { + actions = []DatabaseProcessorActionType{ + Accounts, AccountsForSigner, Data, Offers, TrustLines, + } + } else { + actions = append(actions, p.Action) + } + + // Process transaction meta for { transaction, err := r.Read() if err != nil { @@ -124,21 +217,58 @@ func (p *DatabaseProcessor) ProcessLedger(ctx context.Context, store *pipeline.S } } - switch p.Action { - case AccountsForSigner: + for _, action := range actions { + handler, ok := actionHandlers[action] + if !ok { + return errors.New("Unknown action") + } + // Remember that it's possible that transaction can remove a preauth // tx signer even when it's a failed transaction. - err := p.processLedgerAccountsForSigner(transaction) - if err != nil { - return errors.Wrap(err, "Error in processLedgerAccountsForSigner") + + for _, change := range transaction.GetChanges() { + err := handler(change) + if err != nil { + return errors.Wrap( + err, + fmt.Sprintf("Error in %s handler", action), + ) + } } - case Offers: - err := p.processLedgerOffers(transaction, r.GetSequence()) + } + + select { + case <-ctx.Done(): + return nil + default: + continue + } + } + + // Process upgrades meta + for { + change, err := r.ReadUpgradeChange() + if err != nil { + if err == stdio.EOF { + break + } else { + return err + } + } + + for _, action := range actions { + handler, ok := actionHandlers[action] + if !ok { + return errors.New("Unknown action") + } + + err := handler(change) if err != nil { - return errors.Wrap(err, "Error in processLedgerOffers") + return errors.Wrap( + err, + fmt.Sprintf("Error in %s handler", action), + ) } - default: - return errors.New("Unknown action") } select { @@ -152,100 +282,422 @@ func (p *DatabaseProcessor) ProcessLedger(ctx context.Context, store *pipeline.S return nil } -func (p *DatabaseProcessor) processLedgerAccountsForSigner(transaction io.LedgerTransaction) error { - for _, change := range transaction.GetChanges() { - if change.Type != xdr.LedgerEntryTypeAccount { - continue - } +func (p *DatabaseProcessor) processLedgerAccounts(change io.Change) error { + if change.Type != xdr.LedgerEntryTypeAccount { + return nil + } - if !change.AccountSignersChanged() { - continue + changed, err := change.AccountChangedExceptSigners() + if err != nil { + return errors.Wrap(err, "Error running change.AccountChangedExceptSigners") + } + + if !changed { + return nil + } + + var rowsAffected int64 + var action string + var accountID string + + switch { + case change.Pre == nil && change.Post != nil: + // Created + action = "inserting" + account := change.Post.Data.MustAccount() + accountID = account.AccountId.Address() + rowsAffected, err = p.AccountsQ.InsertAccount(account, change.Post.LastModifiedLedgerSeq) + case change.Pre != nil && change.Post == nil: + // Removed + action = "removing" + account := change.Pre.Data.MustAccount() + accountID = account.AccountId.Address() + rowsAffected, err = p.AccountsQ.RemoveAccount(accountID) + default: + // Updated + action = "updating" + account := change.Post.Data.MustAccount() + accountID = account.AccountId.Address() + rowsAffected, err = p.AccountsQ.UpdateAccount(account, change.Post.LastModifiedLedgerSeq) + } + + if err != nil { + return err + } + + if rowsAffected != 1 { + return verify.NewStateError(errors.Errorf( + "No rows affected when %s account %s", + action, + accountID, + )) + } + + return nil +} + +func (p *DatabaseProcessor) processLedgerAccountData(change io.Change) error { + if change.Type != xdr.LedgerEntryTypeData { + return nil + } + + var rowsAffected int64 + var err error + var action string + var ledgerKey xdr.LedgerKey + + switch { + case change.Pre == nil && change.Post != nil: + // Created + action = "inserting" + data := change.Post.Data.MustData() + err = ledgerKey.SetData(data.AccountId, string(data.DataName)) + if err != nil { + return errors.Wrap(err, "Error creating ledger key") + } + rowsAffected, err = p.DataQ.InsertAccountData(data, change.Post.LastModifiedLedgerSeq) + case change.Pre != nil && change.Post == nil: + // Removed + action = "removing" + data := change.Pre.Data.MustData() + err = ledgerKey.SetData(data.AccountId, string(data.DataName)) + if err != nil { + return errors.Wrap(err, "Error creating ledger key") } + rowsAffected, err = p.DataQ.RemoveAccountData(*ledgerKey.Data) + default: + // Updated + action = "updating" + data := change.Post.Data.MustData() + err = ledgerKey.SetData(data.AccountId, string(data.DataName)) + if err != nil { + return errors.Wrap(err, "Error creating ledger key") + } + rowsAffected, err = p.DataQ.UpdateAccountData(data, change.Post.LastModifiedLedgerSeq) + } - // The code below removes all Pre signers adds Post signers but - // can be improved by finding a diff (check performance first). - if change.Pre != nil { - preAccountEntry := change.Pre.MustAccount() - for signer := range preAccountEntry.SignerSummary() { - rowsAffected, err := p.SignersQ.RemoveAccountSigner(preAccountEntry.AccountId.Address(), signer) - if err != nil { - return errors.Wrap(err, "Error removing a signer") - } + if err != nil { + return err + } - if rowsAffected != 1 { - return verify.NewStateError(errors.Errorf( - "Expected account=%s signer=%s in database but not found when removing", - preAccountEntry.AccountId.Address(), - signer, - )) - } + if rowsAffected != 1 { + return verify.NewStateError(errors.Errorf( + "No rows affected when %s data: %s %s", + action, + ledgerKey.Data.AccountId.Address(), + ledgerKey.Data.DataName, + )) + } + + return nil +} + +func (p *DatabaseProcessor) processLedgerAccountSigners(change io.Change) error { + if change.Type != xdr.LedgerEntryTypeAccount { + return nil + } + + if !change.AccountSignersChanged() { + return nil + } + + // The code below removes all Pre signers adds Post signers but + // can be improved by finding a diff (check performance first). + if change.Pre != nil { + preAccountEntry := change.Pre.Data.MustAccount() + for signer := range preAccountEntry.SignerSummary() { + rowsAffected, err := p.SignersQ.RemoveAccountSigner(preAccountEntry.AccountId.Address(), signer) + if err != nil { + return errors.Wrap(err, "Error removing a signer") + } + + if rowsAffected != 1 { + return verify.NewStateError(errors.Errorf( + "Expected account=%s signer=%s in database but not found when removing", + preAccountEntry.AccountId.Address(), + signer, + )) } } + } - if change.Post != nil { - postAccountEntry := change.Post.MustAccount() - for signer, weight := range postAccountEntry.SignerSummary() { - rowsAffected, err := p.SignersQ.CreateAccountSigner(postAccountEntry.AccountId.Address(), signer, weight) - if err != nil { - return errors.Wrap(err, "Error inserting a signer") - } + if change.Post != nil { + postAccountEntry := change.Post.Data.MustAccount() + for signer, weight := range postAccountEntry.SignerSummary() { + rowsAffected, err := p.SignersQ.CreateAccountSigner(postAccountEntry.AccountId.Address(), signer, weight) + if err != nil { + return errors.Wrap(err, "Error inserting a signer") + } - if rowsAffected != 1 { - return verify.NewStateError(errors.Errorf( - "No rows affected when inserting account=%s signer=%s to database", - postAccountEntry.AccountId.Address(), - signer, - )) - } + if rowsAffected != 1 { + return verify.NewStateError(errors.Errorf( + "No rows affected when inserting account=%s signer=%s to database", + postAccountEntry.AccountId.Address(), + signer, + )) } } } + return nil } -func (p *DatabaseProcessor) processLedgerOffers(transaction io.LedgerTransaction, currentLedger uint32) error { - for _, change := range transaction.GetChanges() { - if change.Type != xdr.LedgerEntryTypeOffer { - continue +func (p *DatabaseProcessor) processLedgerOffers(change io.Change) error { + if change.Type != xdr.LedgerEntryTypeOffer { + return nil + } + + var rowsAffected int64 + var err error + var action string + var offerID xdr.Int64 + + switch { + case change.Pre == nil && change.Post != nil: + // Created + action = "inserting" + offer := change.Post.Data.MustOffer() + offerID = offer.OfferId + rowsAffected, err = p.OffersQ.InsertOffer(offer, change.Post.LastModifiedLedgerSeq) + case change.Pre != nil && change.Post == nil: + // Removed + action = "removing" + offer := change.Pre.Data.MustOffer() + offerID = offer.OfferId + rowsAffected, err = p.OffersQ.RemoveOffer(offer.OfferId) + default: + // Updated + action = "updating" + offer := change.Post.Data.MustOffer() + offerID = offer.OfferId + rowsAffected, err = p.OffersQ.UpdateOffer(offer, change.Post.LastModifiedLedgerSeq) + } + + if err != nil { + return err + } + + if rowsAffected != 1 { + return verify.NewStateError(errors.Errorf( + "No rows affected when %s offer %d", + action, + offerID, + )) + } + return nil +} + +func (p *DatabaseProcessor) adjustAssetStat( + preTrustline *xdr.TrustLineEntry, + postTrustline *xdr.TrustLineEntry, +) error { + var deltaBalance xdr.Int64 + var deltaAccounts int32 + var trustline xdr.TrustLineEntry + + if preTrustline != nil && postTrustline == nil { + trustline = *preTrustline + // removing a trustline + if xdr.TrustLineFlags(preTrustline.Flags).IsAuthorized() { + deltaAccounts = -1 + deltaBalance = -preTrustline.Balance } + } else if preTrustline == nil && postTrustline != nil { + trustline = *postTrustline + // adding a trustline + if xdr.TrustLineFlags(postTrustline.Flags).IsAuthorized() { + deltaAccounts = 1 + deltaBalance = postTrustline.Balance + } + } else if preTrustline != nil && postTrustline != nil { + trustline = *postTrustline + // updating a trustline + if xdr.TrustLineFlags(preTrustline.Flags).IsAuthorized() && + xdr.TrustLineFlags(postTrustline.Flags).IsAuthorized() { + // trustline remains authorized + deltaAccounts = 0 + deltaBalance = postTrustline.Balance - preTrustline.Balance + } else if xdr.TrustLineFlags(preTrustline.Flags).IsAuthorized() && + !xdr.TrustLineFlags(postTrustline.Flags).IsAuthorized() { + // trustline was authorized and became unauthorized + deltaAccounts = -1 + deltaBalance = -preTrustline.Balance + } else if !xdr.TrustLineFlags(preTrustline.Flags).IsAuthorized() && + xdr.TrustLineFlags(postTrustline.Flags).IsAuthorized() { + // trustline was unauthorized and became authorized + deltaAccounts = 1 + deltaBalance = postTrustline.Balance + } + // else, trustline was unauthorized and remains unauthorized + // so there is no change to accounts or balances + } else { + return verify.NewStateError(errors.New("both pre and post trustlines cannot be nil")) + } - var rowsAffected int64 - var err error - var action string - var offerID xdr.Int64 - - switch { - case change.Pre == nil && change.Post != nil: - // Created - action = "inserting" - offer := change.Post.MustOffer() - offerID = offer.OfferId - rowsAffected, err = p.OffersQ.InsertOffer(offer, xdr.Uint32(currentLedger)) - case change.Pre != nil && change.Post == nil: - // Removed - action = "removing" - offer := change.Pre.MustOffer() - offerID = offer.OfferId - rowsAffected, err = p.OffersQ.RemoveOffer(offer.OfferId) - default: - // Updated - action = "updating" - offer := change.Post.MustOffer() - offerID = offer.OfferId - rowsAffected, err = p.OffersQ.UpdateOffer(offer, xdr.Uint32(currentLedger)) + if deltaBalance == 0 && deltaAccounts == 0 { + return nil + } + + var assetType xdr.AssetType + var assetIssuer, assetCode string + if err := trustline.Asset.Extract(&assetType, &assetCode, &assetIssuer); err != nil { + return errors.Wrap(err, "could not extract asset info from trustline") + } + + stat, err := p.AssetStatsQ.GetAssetStat(assetType, assetCode, assetIssuer) + assetStatNotFound := err == sql.ErrNoRows + if !assetStatNotFound && err != nil { + return errors.Wrap(err, "could not fetch asset stat from db") + } + + currentBalance := big.NewInt(0) + if assetStatNotFound { + stat.AssetType = assetType + stat.AssetCode = assetCode + stat.AssetIssuer = assetIssuer + } else { + _, ok := currentBalance.SetString(stat.Amount, 10) + if !ok { + return verify.NewStateError(errors.Errorf( + "Could not parse asset stat amount %s when processing trustline: %s %s", + stat.Amount, + trustline.AccountId.Address(), + trustline.Asset.String(), + )) } + } + currentBalance = currentBalance.Add(currentBalance, big.NewInt(int64(deltaBalance))) + stat.Amount = currentBalance.String() + stat.NumAccounts += deltaAccounts + + if currentBalance.Cmp(big.NewInt(0)) < 0 { + return verify.NewStateError(errors.Errorf( + "Asset stat has negative amount %s when processing trustline: %s %s", + stat.Amount, + trustline.AccountId.Address(), + trustline.Asset.String(), + )) + } + if stat.NumAccounts < 0 { + return verify.NewStateError(errors.Errorf( + "Asset stat has negative num accounts when processing trustline: %s %s", + trustline.AccountId.Address(), + trustline.Asset.String(), + )) + } + + var rowsAffected int64 + if assetStatNotFound { + // deltaAccounts is 0 if we are updating an account + // deltaAccounts is < 0 if we are removing an account + // therefore if deltaAccounts <= 0 the asset stat must exist in the db + if deltaAccounts <= 0 { + return verify.NewStateError(errors.Errorf( + "Expected asset stat to exist when processing trustline: %s %s", + trustline.AccountId.Address(), + trustline.Asset.String(), + )) + } + rowsAffected, err = p.AssetStatsQ.InsertAssetStat(stat) if err != nil { - return err + return errors.Wrap(err, "could not insert asset stat") } - - if rowsAffected != 1 { + } else if stat.NumAccounts == 0 { + if currentBalance.Cmp(big.NewInt(0)) != 0 { return verify.NewStateError(errors.Errorf( - "No rows affected when %s offer %d", - action, - offerID, + "Expected asset stat with no accounts to have amount of 0 "+ + "(amount was %s) when processing trustline: %s %s", + stat.Amount, + trustline.AccountId.Address(), + trustline.Asset.String(), )) } + rowsAffected, err = p.AssetStatsQ.RemoveAssetStat(assetType, assetCode, assetIssuer) + if err != nil { + return errors.Wrap(err, "could not update asset stat") + } + } else { + rowsAffected, err = p.AssetStatsQ.UpdateAssetStat(stat) + if err != nil { + return errors.Wrap(err, "could not update asset stat") + } + } + + if rowsAffected != 1 { + return verify.NewStateError(errors.Errorf( + "No rows affected when adjusting asset stat from trustline: %s %s", + trustline.AccountId.Address(), + trustline.Asset.String(), + )) + } + return nil +} + +func (p *DatabaseProcessor) processLedgerTrustLines(change io.Change) error { + if change.Type != xdr.LedgerEntryTypeTrustline { + return nil + } + + var rowsAffected int64 + var err error + var action string + var ledgerKey xdr.LedgerKey + + switch { + case change.Pre == nil && change.Post != nil: + // Created + action = "inserting" + trustLine := change.Post.Data.MustTrustLine() + err = ledgerKey.SetTrustline(trustLine.AccountId, trustLine.Asset) + if err != nil { + return errors.Wrap(err, "Error creating ledger key") + } + err = p.adjustAssetStat(nil, &trustLine) + if err != nil { + return errors.Wrap(err, "Error adjusting asset stat") + } + rowsAffected, err = p.TrustLinesQ.InsertTrustLine(trustLine, change.Post.LastModifiedLedgerSeq) + case change.Pre != nil && change.Post == nil: + // Removed + action = "removing" + trustLine := change.Pre.Data.MustTrustLine() + err = ledgerKey.SetTrustline(trustLine.AccountId, trustLine.Asset) + if err != nil { + return errors.Wrap(err, "Error creating ledger key") + } + err = p.adjustAssetStat(&trustLine, nil) + if err != nil { + return errors.Wrap(err, "Error adjusting asset stat") + } + rowsAffected, err = p.TrustLinesQ.RemoveTrustLine(*ledgerKey.TrustLine) + default: + // Updated + action = "updating" + preTrustLine := change.Pre.Data.MustTrustLine() + trustLine := change.Post.Data.MustTrustLine() + err = ledgerKey.SetTrustline(trustLine.AccountId, trustLine.Asset) + if err != nil { + return errors.Wrap(err, "Error creating ledger key") + } + err = p.adjustAssetStat(&preTrustLine, &trustLine) + if err != nil { + return errors.Wrap(err, "Error adjusting asset stat") + } + rowsAffected, err = p.TrustLinesQ.UpdateTrustLine(trustLine, change.Post.LastModifiedLedgerSeq) + } + + if err != nil { + return err + } + + if rowsAffected != 1 { + return verify.NewStateError(errors.Errorf( + "No rows affected when %s trustline: %s %s", + action, + ledgerKey.TrustLine.AccountId.Address(), + ledgerKey.TrustLine.Asset.String(), + )) } return nil } diff --git a/services/horizon/internal/expingest/processors/main.go b/services/horizon/internal/expingest/processors/main.go index bb427301db..9e49ccc5be 100644 --- a/services/horizon/internal/expingest/processors/main.go +++ b/services/horizon/internal/expingest/processors/main.go @@ -14,8 +14,12 @@ const ( type DatabaseProcessorActionType string const ( + Accounts DatabaseProcessorActionType = "Accounts" AccountsForSigner DatabaseProcessorActionType = "AccountsForSigner" + Data DatabaseProcessorActionType = "Data" Offers DatabaseProcessorActionType = "Offers" + TrustLines DatabaseProcessorActionType = "TrustLines" + All DatabaseProcessorActionType = "All" ) // DatabaseProcessor is a processor (both state and ledger) that's responsible @@ -24,9 +28,13 @@ const ( // *history.Q object to share a common transaction. `Action` defines what each // processor is responsible for. type DatabaseProcessor struct { - SignersQ history.QSigners - OffersQ history.QOffers - Action DatabaseProcessorActionType + AccountsQ history.QAccounts + DataQ history.QData + SignersQ history.QSigners + OffersQ history.QOffers + TrustLinesQ history.QTrustLines + AssetStatsQ history.QAssetStats + Action DatabaseProcessorActionType } // OrderbookProcessor is a processor (both state and ledger) that's responsible diff --git a/services/horizon/internal/expingest/processors/offers_processor_test.go b/services/horizon/internal/expingest/processors/offers_processor_test.go index e422c280dc..c9afc13178 100644 --- a/services/horizon/internal/expingest/processors/offers_processor_test.go +++ b/services/horizon/internal/expingest/processors/offers_processor_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/suite" ) -func TestOffersrocessorTestSuiteState(t *testing.T) { +func TestOffersProcessorTestSuiteState(t *testing.T) { suite.Run(t, new(OffersProcessorTestSuiteState)) } @@ -112,11 +112,15 @@ func (s *OffersProcessorTestSuiteLedger) SetupTest() { s.mockLedgerWriter = &io.MockLedgerWriter{} s.processor = &DatabaseProcessor{ - Action: Offers, + Action: All, OffersQ: s.mockQ, } // Reader and Writer should be always closed and once + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + s.mockLedgerReader. On("Close"). Return(nil).Once() @@ -133,29 +137,30 @@ func (s *OffersProcessorTestSuiteLedger) TearDownTest() { } func (s *OffersProcessorTestSuiteLedger) TestInsertOffer() { - // should be ignored because it's not an offer type - s.mockLedgerReader. - On("Read"). - Return(io.LedgerTransaction{ - Meta: createTransactionMeta([]xdr.OperationMeta{ - xdr.OperationMeta{ - Changes: []xdr.LedgerEntryChange{ - xdr.LedgerEntryChange{ - Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, - Created: &xdr.LedgerEntry{ - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Thresholds: [4]byte{1, 1, 1, 1}, - }, + accountTransaction := io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Thresholds: [4]byte{1, 1, 1, 1}, }, }, }, }, }, - }), - }, nil).Once() + }, + }), + } + // should be ignored because it's not an offer type + s.Assert().NoError( + s.processor.processLedgerOffers(accountTransaction.GetChanges()[0]), + ) // add offer offer := xdr.OfferEntry{ @@ -172,6 +177,7 @@ func (s *OffersProcessorTestSuiteLedger) TestInsertOffer() { xdr.LedgerEntryChange{ Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, Data: xdr.LedgerEntryData{ Type: xdr.LedgerEntryTypeOffer, Offer: &offer, @@ -182,7 +188,6 @@ func (s *OffersProcessorTestSuiteLedger) TestInsertOffer() { }, }), }, nil).Once() - s.mockLedgerReader.On("GetSequence").Return(uint32(lastModifiedLedgerSeq)) s.mockQ.On( "InsertOffer", @@ -203,6 +208,7 @@ func (s *OffersProcessorTestSuiteLedger) TestInsertOffer() { xdr.LedgerEntryChange{ Type: xdr.LedgerEntryChangeTypeLedgerEntryState, State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, Data: xdr.LedgerEntryData{ Type: xdr.LedgerEntryTypeOffer, Offer: &offer, @@ -213,6 +219,7 @@ func (s *OffersProcessorTestSuiteLedger) TestInsertOffer() { xdr.LedgerEntryChange{ Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, Data: xdr.LedgerEntryData{ Type: xdr.LedgerEntryTypeOffer, Offer: &updatedOffer, @@ -244,8 +251,11 @@ func (s *OffersProcessorTestSuiteLedger) TestInsertOffer() { } func (s *OffersProcessorTestSuiteLedger) TestUpdateOfferNoRowsAffected() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + s.mockLedgerReader.On("Close").Return(nil).Once() + lastModifiedLedgerSeq := xdr.Uint32(1234) - s.mockLedgerReader.On("GetSequence").Return(uint32(lastModifiedLedgerSeq)) offer := xdr.OfferEntry{ OfferId: xdr.Int64(2), @@ -264,6 +274,7 @@ func (s *OffersProcessorTestSuiteLedger) TestUpdateOfferNoRowsAffected() { xdr.LedgerEntryChange{ Type: xdr.LedgerEntryChangeTypeLedgerEntryState, State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, Data: xdr.LedgerEntryData{ Type: xdr.LedgerEntryTypeOffer, Offer: &offer, @@ -274,6 +285,7 @@ func (s *OffersProcessorTestSuiteLedger) TestUpdateOfferNoRowsAffected() { xdr.LedgerEntryChange{ Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, Data: xdr.LedgerEntryData{ Type: xdr.LedgerEntryTypeOffer, Offer: &updatedOffer, @@ -299,7 +311,7 @@ func (s *OffersProcessorTestSuiteLedger) TestUpdateOfferNoRowsAffected() { s.Assert().Error(err) s.Assert().IsType(verify.StateError{}, errors.Cause(err)) - s.Assert().EqualError(err, "Error in processLedgerOffers: No rows affected when updating offer 2") + s.Assert().EqualError(err, "Error in Offers handler: No rows affected when updating offer 2") } func (s *OffersProcessorTestSuiteLedger) TestRemoveOffer() { @@ -334,7 +346,6 @@ func (s *OffersProcessorTestSuiteLedger) TestRemoveOffer() { }, }), }, nil).Once() - s.mockLedgerReader.On("GetSequence").Return(uint32(123)) s.mockQ.On( "RemoveOffer", @@ -355,7 +366,100 @@ func (s *OffersProcessorTestSuiteLedger) TestRemoveOffer() { s.Assert().NoError(err) } +func (s *OffersProcessorTestSuiteLedger) TestProcessUpgradeChange() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + + // add offer + offer := xdr.OfferEntry{ + OfferId: xdr.Int64(2), + Price: xdr.Price{1, 2}, + } + lastModifiedLedgerSeq := xdr.Uint32(1234) + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &offer, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "InsertOffer", + offer, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + updatedOffer := xdr.OfferEntry{ + OfferId: xdr.Int64(2), + Price: xdr.Price{1, 6}, + } + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return( + io.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &offer, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &updatedOffer, + }, + }, + }, nil).Once() + + s.mockQ.On( + "UpdateOffer", + updatedOffer, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + + s.mockLedgerReader.On("Close").Return(nil).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + func (s *OffersProcessorTestSuiteLedger) TestRemoveOfferNoRowsAffected() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + s.mockLedgerReader.On("Close").Return(nil).Once() + // add offer s.mockLedgerReader.On("Read"). Return(io.LedgerTransaction{ @@ -387,7 +491,6 @@ func (s *OffersProcessorTestSuiteLedger) TestRemoveOfferNoRowsAffected() { }, }), }, nil).Once() - s.mockLedgerReader.On("GetSequence").Return(uint32(123)) s.mockQ.On( "RemoveOffer", @@ -403,5 +506,5 @@ func (s *OffersProcessorTestSuiteLedger) TestRemoveOfferNoRowsAffected() { s.Assert().Error(err) s.Assert().IsType(verify.StateError{}, errors.Cause(err)) - s.Assert().EqualError(err, "Error in processLedgerOffers: No rows affected when removing offer 3") + s.Assert().EqualError(err, "Error in Offers handler: No rows affected when removing offer 3") } diff --git a/services/horizon/internal/expingest/processors/orderbook_processor.go b/services/horizon/internal/expingest/processors/orderbook_processor.go index a7e6534141..faec72b8b9 100644 --- a/services/horizon/internal/expingest/processors/orderbook_processor.go +++ b/services/horizon/internal/expingest/processors/orderbook_processor.go @@ -39,8 +39,16 @@ func (p *OrderbookProcessor) ProcessState(ctx context.Context, store *pipeline.S return nil } -func (p *OrderbookProcessor) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) error { - defer r.Close() +func (p *OrderbookProcessor) ProcessLedger(ctx context.Context, store *pipeline.Store, r io.LedgerReader, w io.LedgerWriter) (err error) { + defer func() { + // io.LedgerReader.Close() returns error if upgrade changes have not + // been processed so it's worth checking the error. + closeErr := r.Close() + // Do not overwrite the previous error + if err == nil { + err = closeErr + } + }() defer w.Close() for { @@ -58,22 +66,30 @@ func (p *OrderbookProcessor) ProcessLedger(ctx context.Context, store *pipeline. } for _, change := range transaction.GetChanges() { - if change.Type != xdr.LedgerEntryTypeOffer { - continue - } + p.processChange(change) + } - switch { - case change.Post != nil: - // Created or updated - offer := change.Post.MustOffer() - p.OrderBookGraph.AddOffer(offer) - case change.Pre != nil && change.Post == nil: - // Removed - offer := change.Pre.MustOffer() - p.OrderBookGraph.RemoveOffer(offer.OfferId) + select { + case <-ctx.Done(): + return nil + default: + continue + } + } + + // Process upgrades meta + for { + change, err := r.ReadUpgradeChange() + if err != nil { + if err == stdio.EOF { + break + } else { + return err } } + p.processChange(change) + select { case <-ctx.Done(): return nil @@ -85,6 +101,23 @@ func (p *OrderbookProcessor) ProcessLedger(ctx context.Context, store *pipeline. return nil } +func (p *OrderbookProcessor) processChange(change io.Change) { + if change.Type != xdr.LedgerEntryTypeOffer { + return + } + + switch { + case change.Post != nil: + // Created or updated + offer := change.Post.Data.MustOffer() + p.OrderBookGraph.AddOffer(offer) + case change.Pre != nil && change.Post == nil: + // Removed + offer := change.Pre.Data.MustOffer() + p.OrderBookGraph.RemoveOffer(offer.OfferId) + } +} + func (p *OrderbookProcessor) Name() string { return fmt.Sprintf("OrderbookProcessor") } diff --git a/services/horizon/internal/expingest/processors/orderbook_processor_test.go b/services/horizon/internal/expingest/processors/orderbook_processor_test.go index b75e9fa6b8..b944f152e4 100644 --- a/services/horizon/internal/expingest/processors/orderbook_processor_test.go +++ b/services/horizon/internal/expingest/processors/orderbook_processor_test.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/go/exp/ingest/io" "github.com/stellar/go/exp/orderbook" "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" ) func TestProcessOrderBookState(t *testing.T) { @@ -23,7 +24,7 @@ func TestProcessOrderBookState(t *testing.T) { t.Fatalf("unexpected error %v", err) } writer.AssertExpectations(t) - if err := graph.Apply(); err != nil { + if err := graph.Apply(1); err != nil { t.Fatalf("unexpected error %v", err) } @@ -86,7 +87,7 @@ func TestProcessOrderBookState(t *testing.T) { writer.AssertExpectations(t) reader.AssertExpectations(t) - if err := graph.Apply(); err != nil { + if err := graph.Apply(2); err != nil { t.Fatalf("unexpected error %v", err) } @@ -115,6 +116,7 @@ func TestProcessOrderBookLedger(t *testing.T) { processor := OrderbookProcessor{graph} reader.On("Read").Return(io.LedgerTransaction{}, stdio.EOF).Once() + reader.On("ReadUpgradeChange").Return(io.Change{}, stdio.EOF).Once() reader.On("Close").Return(nil).Once() writer.On("Close").Return(nil).Once() if err := processor.ProcessLedger(context.Background(), nil, reader, writer); err != nil { @@ -122,7 +124,7 @@ func TestProcessOrderBookLedger(t *testing.T) { } writer.AssertExpectations(t) reader.AssertExpectations(t) - if err := graph.Apply(); err != nil { + if err := graph.Apply(1); err != nil { t.Fatalf("unexpected error %v", err) } @@ -314,6 +316,7 @@ func TestProcessOrderBookLedger(t *testing.T) { reader.On("Read"). Return(io.LedgerTransaction{}, stdio.EOF).Once() + reader.On("ReadUpgradeChange").Return(io.Change{}, stdio.EOF).Once() reader.On("Close").Return(nil).Once() writer.On("Close").Return(nil).Once() @@ -323,7 +326,7 @@ func TestProcessOrderBookLedger(t *testing.T) { writer.AssertExpectations(t) reader.AssertExpectations(t) - if err := graph.Apply(); err != nil { + if err := graph.Apply(2); err != nil { t.Fatalf("unexpected error %v", err) } @@ -345,3 +348,82 @@ func TestProcessOrderBookLedger(t *testing.T) { t.Fatal("expected offers does not match offers in graph") } } + +func TestProcessOrderBookLedgerProcessUpgradeChanges(t *testing.T) { + reader := &io.MockLedgerReader{} + writer := &io.MockLedgerWriter{} + graph := orderbook.NewOrderBookGraph() + processor := OrderbookProcessor{graph} + + // add offer + reader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + OfferId: xdr.Int64(1), + Price: xdr.Price{1, 2}, + }, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + reader.On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + // Process upgrade changes + reader.On("ReadUpgradeChange").Return(io.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + OfferId: xdr.Int64(1), + Price: xdr.Price{1, 2}, + }, + }, + }, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + OfferId: xdr.Int64(1), + Price: xdr.Price{100, 2}, + }, + }, + }, + }, nil).Once() + + reader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + + reader.On("Close").Return(nil).Once() + writer.On("Close").Return(nil).Once() + + if err := processor.ProcessLedger(context.Background(), nil, reader, writer); err != nil { + t.Fatalf("unexpected error %v", err) + } + + writer.AssertExpectations(t) + reader.AssertExpectations(t) + + if err := graph.Apply(2); err != nil { + t.Fatalf("unexpected error %v", err) + } + + offers := graph.Offers() + assert.Equal(t, xdr.Int32(100), offers[0].Price.N) + assert.Equal(t, xdr.Int32(2), offers[0].Price.D) +} diff --git a/services/horizon/internal/expingest/processors/trust_lines_processor_test.go b/services/horizon/internal/expingest/processors/trust_lines_processor_test.go new file mode 100644 index 0000000000..9bd0e7338b --- /dev/null +++ b/services/horizon/internal/expingest/processors/trust_lines_processor_test.go @@ -0,0 +1,926 @@ +package processors + +import ( + "context" + "database/sql" + stdio "io" + "testing" + + "github.com/stellar/go/exp/ingest/io" + "github.com/stellar/go/exp/ingest/verify" + supportPipeline "github.com/stellar/go/exp/support/pipeline" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/suite" +) + +var trustLineIssuer = xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") + +func TestTrustLinesProcessorTestSuiteState(t *testing.T) { + suite.Run(t, new(TrustLinesProcessorTestSuiteState)) +} + +type TrustLinesProcessorTestSuiteState struct { + suite.Suite + processor *DatabaseProcessor + mockQ *history.MockQTrustLines + mockAssetStatsQ *history.MockQAssetStats + mockBatchInsertBuilder *history.MockTrustLinesBatchInsertBuilder + mockStateReader *io.MockStateReader + mockStateWriter *io.MockStateWriter +} + +func (s *TrustLinesProcessorTestSuiteState) SetupTest() { + s.mockQ = &history.MockQTrustLines{} + s.mockAssetStatsQ = &history.MockQAssetStats{} + s.mockBatchInsertBuilder = &history.MockTrustLinesBatchInsertBuilder{} + s.mockStateReader = &io.MockStateReader{} + s.mockStateWriter = &io.MockStateWriter{} + + s.processor = &DatabaseProcessor{ + Action: TrustLines, + TrustLinesQ: s.mockQ, + AssetStatsQ: s.mockAssetStatsQ, + } + + // Reader and Writer should be always closed and once + s.mockStateReader.On("Close").Return(nil).Once() + s.mockStateWriter.On("Close").Return(nil).Once() + + s.mockQ. + On("NewTrustLinesBatchInsertBuilder", maxBatchSize). + Return(s.mockBatchInsertBuilder).Once() +} + +func (s *TrustLinesProcessorTestSuiteState) TearDownTest() { + s.mockQ.AssertExpectations(s.T()) + s.mockAssetStatsQ.AssertExpectations(s.T()) + s.mockBatchInsertBuilder.AssertExpectations(s.T()) + s.mockStateReader.AssertExpectations(s.T()) + s.mockStateWriter.AssertExpectations(s.T()) +} + +func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLineWithAssetStat() { + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + lastModifiedLedgerSeq := xdr.Uint32(123) + s.mockStateReader. + On("Read").Return( + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + }, + }, + nil, + ).Once() + + s.mockBatchInsertBuilder. + On("Add", trustLine, lastModifiedLedgerSeq).Return(nil).Once() + + s.mockAssetStatsQ.On("InsertAssetStats", []history.ExpAssetStat{ + history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "0", + NumAccounts: 1, + }, + }, maxBatchSize).Return(nil).Once() + + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{}, stdio.EOF).Once() + + s.mockBatchInsertBuilder.On("Exec").Return(nil).Once() + + err := s.processor.ProcessState( + context.Background(), + &supportPipeline.Store{}, + s.mockStateReader, + s.mockStateWriter, + ) + + s.Assert().NoError(err) +} + +func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLineWithoutAssetStat() { + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + } + lastModifiedLedgerSeq := xdr.Uint32(123) + s.mockStateReader. + On("Read").Return( + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + }, + }, + nil, + ).Once() + + s.mockBatchInsertBuilder. + On("Add", trustLine, lastModifiedLedgerSeq).Return(nil).Once() + + s.mockAssetStatsQ. + On("InsertAssetStats", []history.ExpAssetStat{}, maxBatchSize).Return(nil).Once() + + s.mockStateReader. + On("Read"). + Return(xdr.LedgerEntryChange{}, stdio.EOF).Once() + + s.mockBatchInsertBuilder.On("Exec").Return(nil).Once() + + err := s.processor.ProcessState( + context.Background(), + &supportPipeline.Store{}, + s.mockStateReader, + s.mockStateWriter, + ) + + s.Assert().NoError(err) +} + +func TestTrustLinesProcessorTestSuiteLedger(t *testing.T) { + suite.Run(t, new(TrustLinesProcessorTestSuiteLedger)) +} + +type TrustLinesProcessorTestSuiteLedger struct { + suite.Suite + processor *DatabaseProcessor + mockQ *history.MockQTrustLines + mockAssetStatsQ *history.MockQAssetStats + mockLedgerReader *io.MockLedgerReader + mockLedgerWriter *io.MockLedgerWriter +} + +func (s *TrustLinesProcessorTestSuiteLedger) SetupTest() { + s.mockQ = &history.MockQTrustLines{} + s.mockLedgerReader = &io.MockLedgerReader{} + s.mockLedgerWriter = &io.MockLedgerWriter{} + s.mockAssetStatsQ = &history.MockQAssetStats{} + + s.processor = &DatabaseProcessor{ + Action: All, + TrustLinesQ: s.mockQ, + AssetStatsQ: s.mockAssetStatsQ, + } + + // Reader and Writer should be always closed and once + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + + s.mockLedgerReader. + On("Close"). + Return(nil).Once() + + s.mockLedgerWriter. + On("Close"). + Return(nil).Once() +} + +func (s *TrustLinesProcessorTestSuiteLedger) TearDownTest() { + s.mockQ.AssertExpectations(s.T()) + s.mockAssetStatsQ.AssertExpectations(s.T()) + s.mockLedgerReader.AssertExpectations(s.T()) + s.mockLedgerWriter.AssertExpectations(s.T()) +} + +func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { + accountTransaction := io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Thresholds: [4]byte{1, 1, 1, 1}, + }, + }, + }, + }, + }, + }, + }), + } + // should be ignored because it's not an trust line type + s.Assert().NoError( + s.processor.processLedgerTrustLines(accountTransaction.GetChanges()[0]), + ) + + // add trust line + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + unauthorizedTrustline := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 0, + } + lastModifiedLedgerSeq := xdr.Uint32(1234) + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // Created + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &unauthorizedTrustline, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "InsertTrustLine", + trustLine, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + s.mockQ.On( + "InsertTrustLine", + unauthorizedTrustline, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() + s.mockAssetStatsQ.On("InsertAssetStat", history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "0", + NumAccounts: 1, + }).Return(int64(1), nil).Once() + + updatedTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + updatedUnauthorizedTrustline := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 10, + } + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &updatedTrustLine, + }, + }, + }, + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &unauthorizedTrustline, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &updatedUnauthorizedTrustline, + }, + }, + }, + }, + }, + }), + }, nil).Once() + s.mockQ.On( + "UpdateTrustLine", + updatedTrustLine, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + s.mockQ.On( + "UpdateTrustLine", + updatedUnauthorizedTrustline, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "0", + NumAccounts: 1, + }, nil).Once() + s.mockAssetStatsQ.On("UpdateAssetStat", history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "10", + NumAccounts: 1, + }).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + +func (s *TrustLinesProcessorTestSuiteLedger) TestUpdateTrustLineNoRowsAffected() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + s.mockLedgerReader.On("Close").Return(nil).Once() + + lastModifiedLedgerSeq := xdr.Uint32(1234) + + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + updatedTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &updatedTrustLine, + }, + }, + }, + }, + }, + }), + }, nil).Once() + s.mockQ.On( + "UpdateTrustLine", + updatedTrustLine, + lastModifiedLedgerSeq, + ).Return(int64(0), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "0", + NumAccounts: 1, + }, nil).Once() + s.mockAssetStatsQ.On("UpdateAssetStat", history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "10", + NumAccounts: 1, + }).Return(int64(1), nil).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().Error(err) + s.Assert().IsType(verify.StateError{}, errors.Cause(err)) + s.Assert().EqualError(err, "Error in TrustLines handler: No rows affected when updating trustline: GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB credit_alphanum4/EUR/GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") +} + +func (s *TrustLinesProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() { + lastModifiedLedgerSeq := xdr.Uint32(1234) + + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 100, + } + updatedTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + + otherTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 100, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + otherUpdatedTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 10, + } + + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &updatedTrustLine, + }, + }, + }, + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &otherTrustLine, + }, + }, + }, + // Updated + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &otherUpdatedTrustLine, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "UpdateTrustLine", + updatedTrustLine, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() + s.mockAssetStatsQ.On("InsertAssetStat", history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "10", + NumAccounts: 1, + }).Return(int64(1), nil).Once() + + s.mockQ.On( + "UpdateTrustLine", + otherUpdatedTrustLine, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "USD", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "USD", + Amount: "100", + NumAccounts: 1, + }, nil).Once() + s.mockAssetStatsQ.On("RemoveAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "USD", + trustLineIssuer.Address(), + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} +func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveTrustLine() { + unauthorizedTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + Balance: 0, + } + + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryRemoved, + Removed: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &unauthorizedTrustLine, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryRemoved, + Removed: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "RemoveTrustLine", + xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + }, + ).Return(int64(1), nil).Once() + s.mockQ.On( + "RemoveTrustLine", + xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()), + }, + ).Return(int64(1), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "0", + NumAccounts: 1, + }, nil).Once() + s.mockAssetStatsQ.On("RemoveAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + +func (s *TrustLinesProcessorTestSuiteLedger) TestProcessUpgradeChange() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + + // add trust line + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + lastModifiedLedgerSeq := xdr.Uint32(1234) + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + // State + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "InsertTrustLine", + trustLine, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() + s.mockAssetStatsQ.On("InsertAssetStat", history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "0", + NumAccounts: 1, + }).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("Read"). + Return(io.LedgerTransaction{}, stdio.EOF).Once() + + updatedTrustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 10, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + } + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return( + io.Change{ + Type: xdr.LedgerEntryTypeTrustline, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &updatedTrustLine, + }, + }, + }, nil).Once() + + s.mockQ.On( + "UpdateTrustLine", + updatedTrustLine, + lastModifiedLedgerSeq, + ).Return(int64(1), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "0", + NumAccounts: 1, + }, nil).Once() + s.mockAssetStatsQ.On("UpdateAssetStat", history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "10", + NumAccounts: 1, + }).Return(int64(1), nil).Once() + + s.mockLedgerReader. + On("ReadUpgradeChange"). + Return(io.Change{}, stdio.EOF).Once() + + s.mockLedgerReader.On("Close").Return(nil).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().NoError(err) +} + +func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveTrustlineNoRowsAffected() { + // Removes ReadUpgradeChange assertion + s.mockLedgerReader = &io.MockLedgerReader{} + s.mockLedgerReader.On("Close").Return(nil).Once() + + s.mockLedgerReader.On("Read"). + Return(io.LedgerTransaction{ + Meta: createTransactionMeta([]xdr.OperationMeta{ + xdr.OperationMeta{ + Changes: []xdr.LedgerEntryChange{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + Balance: 0, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryRemoved, + Removed: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + }, + }, + }, + }, + }, + }), + }, nil).Once() + + s.mockQ.On( + "RemoveTrustLine", + xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()), + }, + ).Return(int64(0), nil).Once() + s.mockAssetStatsQ.On("GetAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Amount: "0", + NumAccounts: 1, + }, nil).Once() + s.mockAssetStatsQ.On("RemoveAssetStat", + xdr.AssetTypeAssetTypeCreditAlphanum4, + "EUR", + trustLineIssuer.Address(), + ).Return(int64(1), nil).Once() + + err := s.processor.ProcessLedger( + context.Background(), + &supportPipeline.Store{}, + s.mockLedgerReader, + s.mockLedgerWriter, + ) + + s.Assert().Error(err) + s.Assert().IsType(verify.StateError{}, errors.Cause(err)) + s.Assert().EqualError(err, "Error in TrustLines handler: No rows affected when removing trustline: GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB credit_alphanum4/EUR/GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") +} diff --git a/services/horizon/internal/expingest/verify.go b/services/horizon/internal/expingest/verify.go index f022cea3b2..03cfee3086 100644 --- a/services/horizon/internal/expingest/verify.go +++ b/services/horizon/internal/expingest/verify.go @@ -2,12 +2,15 @@ package expingest import ( "database/sql" + "fmt" "time" "github.com/stellar/go/exp/ingest/adapters" "github.com/stellar/go/exp/ingest/io" "github.com/stellar/go/exp/ingest/verify" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/services/horizon/internal/expingest/processors" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/historyarchive" ilog "github.com/stellar/go/support/log" @@ -15,6 +18,7 @@ import ( ) const verifyBatchSize = 50000 +const assetStatsBatchSize = 500 // stateVerifierExpectedIngestionVersion defines a version of ingestion system // required by state verifier. This is done to prevent situations where @@ -22,7 +26,7 @@ const verifyBatchSize = 50000 // check them. // There is a test that checks it, to fix it: update the actual `verifyState` // method instead of just updating this value! -const stateVerifierExpectedIngestionVersion = 5 +const stateVerifierExpectedIngestionVersion = 9 // verifyState is called as a go routine from pipeline post hook every 64 // ledgers. It checks if the state is correct. If another go routine is already @@ -121,6 +125,7 @@ func (s *System) verifyState() error { TransformFunction: transformEntry, } + assetStats := processors.AssetStatSet{} total := 0 for { var keys []xdr.LedgerKey @@ -134,13 +139,19 @@ func (s *System) verifyState() error { } accounts := make([]string, 0, verifyBatchSize) + data := make([]xdr.LedgerKeyData, 0, verifyBatchSize) offers := make([]int64, 0, verifyBatchSize) + trustLines := make([]xdr.LedgerKeyTrustLine, 0, verifyBatchSize) for _, key := range keys { switch key.Type { case xdr.LedgerEntryTypeAccount: accounts = append(accounts, key.Account.AccountId.Address()) + case xdr.LedgerEntryTypeData: + data = append(data, *key.Data) case xdr.LedgerEntryTypeOffer: offers = append(offers, int64(key.Offer.OfferId)) + case xdr.LedgerEntryTypeTrustline: + trustLines = append(trustLines, *key.TrustLine) default: return errors.New("GetLedgerKeys return unexpected type") } @@ -151,11 +162,21 @@ func (s *System) verifyState() error { return errors.Wrap(err, "addAccountsToStateVerifier failed") } + err = addDataToStateVerifier(verifier, historyQ, data) + if err != nil { + return errors.Wrap(err, "addDataToStateVerifier failed") + } + err = addOffersToStateVerifier(verifier, historyQ, offers) if err != nil { return errors.Wrap(err, "addOffersToStateVerifier failed") } + err = addTrustLinesToStateVerifier(verifier, assetStats, historyQ, trustLines) + if err != nil { + return errors.Wrap(err, "addTrustLinesToStateVerifier failed") + } + total += len(keys) localLog.WithField("total", total).Info("Batch added to StateVerifier") } @@ -167,81 +188,162 @@ func (s *System) verifyState() error { return errors.Wrap(err, "Error running historyQ.CountAccounts") } + countData, err := historyQ.CountAccountsData() + if err != nil { + return errors.Wrap(err, "Error running historyQ.CountData") + } + countOffers, err := historyQ.CountOffers() if err != nil { return errors.Wrap(err, "Error running historyQ.CountOffers") } - err = verifier.Verify(countAccounts + countOffers) + countTrustLines, err := historyQ.CountTrustLines() + if err != nil { + return errors.Wrap(err, "Error running historyQ.CountTrustLines") + } + + err = verifier.Verify(countAccounts + countData + countOffers + countTrustLines) if err != nil { return errors.Wrap(err, "verifier.Verify failed") } + err = checkAssetStats(assetStats, historyQ) + if err != nil { + return errors.Wrap(err, "checkAssetStats failed") + } + localLog.Info("State correct") return nil } +func checkAssetStats(set processors.AssetStatSet, q *history.Q) error { + page := db2.PageQuery{ + Order: "asc", + Limit: assetStatsBatchSize, + } + + for { + assetStats, err := q.GetAssetStats("", "", page) + if err != nil { + return errors.Wrap(err, "could not fetch asset stats from db") + } + if len(assetStats) == 0 { + break + } + + for _, assetStat := range assetStats { + fromSet, removed := set.Remove(assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) + if !removed { + return verify.NewStateError( + fmt.Errorf( + "db contains asset stat with code %s issuer %s which is missing from HAS", + assetStat.AssetCode, assetStat.AssetIssuer, + ), + ) + } + + if fromSet != assetStat { + return verify.NewStateError( + fmt.Errorf( + "db asset stat with code %s issuer %s does not match asset stat from HAS", + assetStat.AssetCode, assetStat.AssetIssuer, + ), + ) + } + } + + page.Cursor = assetStats[len(assetStats)-1].PagingToken() + } + + if len(set) > 0 { + return verify.NewStateError( + fmt.Errorf( + "HAS contains %d more asset stats than db", + len(set), + ), + ) + } + return nil +} + func addAccountsToStateVerifier(verifier *verify.StateVerifier, q *history.Q, ids []string) error { if len(ids) == 0 { return nil } + accounts, err := q.GetAccountsByIDs(ids) + if err != nil { + return errors.Wrap(err, "Error running history.Q.GetAccountsByIDs") + } + signers, err := q.SignersForAccounts(ids) if err != nil { return errors.Wrap(err, "Error running history.Q.SignersForAccounts") } - var account *xdr.AccountEntry + masterWeightMap := make(map[string]int32) + signersMap := make(map[string][]xdr.Signer) for _, row := range signers { - if account == nil || account.AccountId.Address() != row.Account { - if account != nil { - // Sort signers - account.Signers = xdr.SortSignersByKey(account.Signers) - - entry := xdr.LedgerEntry{ - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: account, - }, - } - err = verifier.Write(entry) - if err != nil { - return err - } - } - - account = &xdr.AccountEntry{ - AccountId: xdr.MustAddress(row.Account), - Signers: []xdr.Signer{}, - } - } - if row.Account == row.Signer { - // Master key - account.Thresholds = [4]byte{ - // Store master weight only - byte(row.Weight), 0, 0, 0, - } + masterWeightMap[row.Account] = row.Weight } else { - // Normal signer - account.Signers = append(account.Signers, xdr.Signer{ - Key: xdr.MustSigner(row.Signer), - Weight: xdr.Uint32(row.Weight), - }) + signersMap[row.Account] = append( + signersMap[row.Account], + xdr.Signer{ + Key: xdr.MustSigner(row.Signer), + Weight: xdr.Uint32(row.Weight), + }, + ) } } - if account != nil { - // Sort signers - account.Signers = xdr.SortSignersByKey(account.Signers) + for _, row := range accounts { + var inflationDest *xdr.AccountId + if row.InflationDestination != "" { + t := xdr.MustAddress(row.InflationDestination) + inflationDest = &t + } + + // Ensure master weight matches, if not it's a state error! + if int32(row.MasterWeight) != masterWeightMap[row.AccountID] { + return verify.NewStateError(errors.New("Master key weight in accounts does not match ")) + } + + account := &xdr.AccountEntry{ + AccountId: xdr.MustAddress(row.AccountID), + Balance: xdr.Int64(row.Balance), + SeqNum: xdr.SequenceNumber(row.SequenceNumber), + NumSubEntries: xdr.Uint32(row.NumSubEntries), + InflationDest: inflationDest, + Flags: xdr.Uint32(row.Flags), + HomeDomain: xdr.String32(row.HomeDomain), + Thresholds: xdr.Thresholds{ + row.MasterWeight, + row.ThresholdLow, + row.ThresholdMedium, + row.ThresholdHigh, + }, + Signers: xdr.SortSignersByKey(signersMap[row.AccountID]), + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: xdr.Int64(row.BuyingLiabilities), + Selling: xdr.Int64(row.SellingLiabilities), + }, + }, + }, + } - // Add last created in a loop account entry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: xdr.Uint32(row.LastModifiedLedger), Data: xdr.LedgerEntryData{ Type: xdr.LedgerEntryTypeAccount, Account: account, }, } + err = verifier.Write(entry) if err != nil { return err @@ -251,6 +353,37 @@ func addAccountsToStateVerifier(verifier *verify.StateVerifier, q *history.Q, id return nil } +func addDataToStateVerifier(verifier *verify.StateVerifier, q *history.Q, keys []xdr.LedgerKeyData) error { + if len(keys) == 0 { + return nil + } + + data, err := q.GetAccountDataByKeys(keys) + if err != nil { + return errors.Wrap(err, "Error running history.Q.GetAccountDataByKeys") + } + + for _, row := range data { + entry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: xdr.Uint32(row.LastModifiedLedger), + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeData, + Data: &xdr.DataEntry{ + AccountId: xdr.MustAddress(row.AccountID), + DataName: xdr.String64(row.Name), + DataValue: xdr.DataValue(row.Value), + }, + }, + } + err := verifier.Write(entry) + if err != nil { + return err + } + } + + return nil +} + func addOffersToStateVerifier(verifier *verify.StateVerifier, q *history.Q, ids []int64) error { if len(ids) == 0 { return nil @@ -289,40 +422,99 @@ func addOffersToStateVerifier(verifier *verify.StateVerifier, q *history.Q, ids return nil } -func transformEntry(entry xdr.LedgerEntry) (bool, xdr.LedgerEntry) { - switch entry.Data.Type { - case xdr.LedgerEntryTypeAccount: - accountEntry := entry.Data.Account +func addTrustLinesToStateVerifier( + verifier *verify.StateVerifier, + assetStats processors.AssetStatSet, + q *history.Q, + keys []xdr.LedgerKeyTrustLine, +) error { + if len(keys) == 0 { + return nil + } - // We don't store accounts with no signers and no master. - // Ignore such accounts. - if accountEntry.MasterKeyWeight() == 0 && len(accountEntry.Signers) == 0 { - return true, xdr.LedgerEntry{} - } + trustLines, err := q.GetTrustLinesByKeys(keys) + if err != nil { + return errors.Wrap(err, "Error running history.Q.GetTrustLinesByKeys") + } - // We store account id, master weight and signers only - return false, xdr.LedgerEntry{ - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: accountEntry.AccountId, - Thresholds: [4]byte{ - // Store master weight only - accountEntry.Thresholds[0], 0, 0, 0, + for _, row := range trustLines { + asset := xdr.MustNewCreditAsset(row.AssetCode, row.AssetIssuer) + trustline := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress(row.AccountID), + Asset: asset, + Balance: xdr.Int64(row.Balance), + Limit: xdr.Int64(row.Limit), + Flags: xdr.Uint32(row.Flags), + Ext: xdr.TrustLineEntryExt{ + V: 1, + V1: &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: xdr.Int64(row.BuyingLiabilities), + Selling: xdr.Int64(row.SellingLiabilities), }, - Signers: xdr.SortSignersByKey(accountEntry.Signers), }, }, } + entry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: xdr.Uint32(row.LastModifiedLedger), + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustline, + }, + } + if err := verifier.Write(entry); err != nil { + return err + } + if err := assetStats.Add(trustline); err != nil { + return verify.NewStateError( + errors.Wrap(err, "could not add trustline to asset stats"), + ) + } + } + + return nil +} + +func transformEntry(entry xdr.LedgerEntry) (bool, xdr.LedgerEntry) { + switch entry.Data.Type { + case xdr.LedgerEntryTypeAccount: + accountEntry := entry.Data.Account + // Sort signers + accountEntry.Signers = xdr.SortSignersByKey(accountEntry.Signers) + // Account can have ext=0. For those, create ext=1 + // with 0 liabilities. + if accountEntry.Ext.V == 0 { + accountEntry.Ext.V = 1 + accountEntry.Ext.V1 = &xdr.AccountEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 0, + Selling: 0, + }, + } + } + + return false, entry case xdr.LedgerEntryTypeOffer: // Full check of offer object return false, entry case xdr.LedgerEntryTypeTrustline: - // Ignore - return true, xdr.LedgerEntry{} + // Trust line can have ext=0. For those, create ext=1 + // with 0 liabilities. + trustLineEntry := entry.Data.TrustLine + if trustLineEntry.Ext.V == 0 { + trustLineEntry.Ext.V = 1 + trustLineEntry.Ext.V1 = &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 0, + Selling: 0, + }, + } + } + + return false, entry case xdr.LedgerEntryTypeData: - // Ignore - return true, xdr.LedgerEntry{} + // Full check of data object + return false, entry default: panic("Invalid type") } diff --git a/services/horizon/internal/handler.go b/services/horizon/internal/handler.go index 8bde0d6b20..8423736599 100644 --- a/services/horizon/internal/handler.go +++ b/services/horizon/internal/handler.go @@ -223,31 +223,6 @@ func showActionHandler(jfn interface{}) http.HandlerFunc { }) } -// accountIndexActionHandler handles /accounts index endpoints. -func accountIndexActionHandler(jfn interface{}) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - contentType := render.Negotiate(r) - if jfn == nil || (contentType != render.MimeHal && contentType != render.MimeJSON) { - problem.Render(ctx, w, hProblem.NotAcceptable) - return - } - - params, err := getAccountsIndexActionQueryParams(r) - if err != nil { - problem.Render(ctx, w, err) - return - } - - h, err := hal.Handler(jfn, params) - if err != nil { - panic(err) - } - - h.ServeHTTP(w, r) - }) -} - // getAccountID retrieves the account id by the provided key. The key is // usually "account_id", "source_account", and "destination_account". The // function would return an error if the account id is empty and the required @@ -271,27 +246,6 @@ func getAccountID(r *http.Request, key string, required bool) (string, error) { return val, nil } -// getSignerKey retrieves the signer key by the provided key. The key is -// usually "signer". The function would return an error if the account id is -// empty and the required flag is true. -func getSignerKey(r *http.Request, key string, required bool) (string, error) { - val, err := hchi.GetStringFromURL(r, key) - if err != nil { - return "", err - } - - if val == "" && !required { - return val, nil - } - - version, _, err := strkey.DecodeAny(val) - if err != nil || version == strkey.VersionByteSeed { - return "", problem.MakeInvalidFieldProblem(key, errors.New("invalid signer")) - } - - return val, nil -} - // getShowActionQueryParams gets the available query params for all non-indexable endpoints. func getShowActionQueryParams(r *http.Request, requireAccountID bool) (*showActionQueryParams, error) { txHash, err := hchi.GetStringFromURL(r, "tx_id") @@ -310,24 +264,6 @@ func getShowActionQueryParams(r *http.Request, requireAccountID bool) (*showActi }, nil } -// getAccountsIndexActionQueryParams gets the available query params for /accounts endpoints. -func getAccountsIndexActionQueryParams(r *http.Request) (*indexActionQueryParams, error) { - signer, err := getSignerKey(r, "signer", true) - if err != nil { - return nil, errors.Wrap(err, "getting signer key") - } - - pq, err := getAccountsPageQuery(r) - if err != nil { - return nil, errors.Wrap(err, "getting page query") - } - - return &indexActionQueryParams{ - Signer: signer, - PagingParams: pq, - }, nil -} - // getIndexActionQueryParams gets the available query params for all indexable endpoints. func getIndexActionQueryParams(r *http.Request, ingestFailedTransactions bool) (*indexActionQueryParams, error) { addr, err := getAccountID(r, "account_id", false) @@ -403,8 +339,107 @@ func validateCursorWithinHistory(pq db2.PageQuery) error { return nil } +type objectAction interface { + GetResource( + w actions.HeaderWriter, + r *http.Request, + ) (hal.Pageable, error) +} + +type objectActionHandler struct { + action objectAction +} + +func (handler objectActionHandler) ServeHTTP( + w http.ResponseWriter, + r *http.Request, +) { + switch render.Negotiate(r) { + case render.MimeHal, render.MimeJSON: + response, err := handler.action.GetResource(w, r) + if err != nil { + problem.Render(r.Context(), w, err) + return + } + + httpjson.Render( + w, + response, + httpjson.HALJSON, + ) + return + } + + problem.Render(r.Context(), w, hProblem.NotAcceptable) +} + +const singleObjectStreamLimit = 10 + +type streamableObjectAction interface { + GetResource( + w actions.HeaderWriter, + r *http.Request, + ) (actions.StreamableObjectResponse, error) +} + +type streamableObjectActionHandler struct { + action streamableObjectAction + streamHandler sse.StreamHandler +} + +func (handler streamableObjectActionHandler) ServeHTTP( + w http.ResponseWriter, + r *http.Request, +) { + switch render.Negotiate(r) { + case render.MimeHal, render.MimeJSON: + response, err := handler.action.GetResource(w, r) + if err != nil { + problem.Render(r.Context(), w, err) + return + } + + httpjson.Render( + w, + response, + httpjson.HALJSON, + ) + return + case render.MimeEventStream: + handler.renderStream(w, r) + return + } + + problem.Render(r.Context(), w, hProblem.NotAcceptable) +} + +func (handler streamableObjectActionHandler) renderStream( + w http.ResponseWriter, + r *http.Request, +) { + var lastResponse actions.StreamableObjectResponse + + handler.streamHandler.ServeStream( + w, + r, + singleObjectStreamLimit, + func() ([]sse.Event, error) { + response, err := handler.action.GetResource(w, r) + if err != nil { + return nil, err + } + + if lastResponse == nil || !lastResponse.Equals(response) { + lastResponse = response + return []sse.Event{sse.Event{Data: response}}, nil + } + return []sse.Event{}, nil + }, + ) +} + type pageAction interface { - GetResourcePage(r *http.Request) ([]hal.Pageable, error) + GetResourcePage(w actions.HeaderWriter, r *http.Request) ([]hal.Pageable, error) } type pageActionHandler struct { @@ -429,7 +464,7 @@ func streamablePageHandler( } func (handler pageActionHandler) renderPage(w http.ResponseWriter, r *http.Request) { - records, err := handler.action.GetResourcePage(r) + records, err := handler.action.GetResourcePage(w, r) if err != nil { problem.Render(r.Context(), w, err) return @@ -461,7 +496,7 @@ func (handler pageActionHandler) renderStream(w http.ResponseWriter, r *http.Req r, int(pq.Limit), func() ([]sse.Event, error) { - records, err := handler.action.GetResourcePage(r) + records, err := handler.action.GetResourcePage(w, r) if err != nil { return nil, err } @@ -500,7 +535,9 @@ func (handler pageActionHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques } func buildPage(r *http.Request, records []hal.Pageable) (hal.Page, error) { - pageQuery, err := actions.GetPageQuery(r) + // Always DisableCursorValidation - we can assume it's valid since the + // validation is done in GetResourcePage. + pageQuery, err := actions.GetPageQuery(r, actions.DisableCursorValidation) if err != nil { return hal.Page{}, err } diff --git a/services/horizon/internal/ledger/ledger_source.go b/services/horizon/internal/ledger/ledger_source.go index c6ff13ee84..324ed7d5d2 100644 --- a/services/horizon/internal/ledger/ledger_source.go +++ b/services/horizon/internal/ledger/ledger_source.go @@ -2,6 +2,7 @@ package ledger import ( "context" + "sync" "time" ) @@ -62,6 +63,7 @@ func (source HistoryDBSource) NextLedger(currentSequence uint32) chan uint32 { type TestingSource struct { currentLedger uint32 newLedgers chan uint32 + lock *sync.RWMutex } // NewTestingSource returns a TestingSource. @@ -69,11 +71,14 @@ func NewTestingSource(currentLedger uint32) *TestingSource { return &TestingSource{ currentLedger: currentLedger, newLedgers: make(chan uint32), + lock: &sync.RWMutex{}, } } // CurrentLedger returns the current ledger. func (source *TestingSource) CurrentLedger() uint32 { + source.lock.RLock() + defer source.lock.RUnlock() return source.currentLedger } @@ -106,5 +111,20 @@ func (source *TestingSource) TryAddLedger( // NextLedger returns a channel which yields every time there is a new ledger. func (source *TestingSource) NextLedger(currentSequence uint32) chan uint32 { - return source.newLedgers + response := make(chan uint32, 1) + + go func() { + for { + nextLedger := <-source.newLedgers + if nextLedger > source.currentLedger { + source.lock.Lock() + defer source.lock.Unlock() + source.currentLedger = nextLedger + response <- nextLedger + return + } + } + }() + + return response } diff --git a/services/horizon/internal/middleware.go b/services/horizon/internal/middleware.go index fa6dc43377..85f6f66784 100644 --- a/services/horizon/internal/middleware.go +++ b/services/horizon/internal/middleware.go @@ -2,6 +2,7 @@ package horizon import ( "context" + "database/sql" "net/http" "strings" "time" @@ -9,11 +10,15 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" + "github.com/stellar/go/services/horizon/internal/actions" + horizonContext "github.com/stellar/go/services/horizon/internal/context" + "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/errors" "github.com/stellar/go/services/horizon/internal/hchi" "github.com/stellar/go/services/horizon/internal/httpx" "github.com/stellar/go/services/horizon/internal/render" hProblem "github.com/stellar/go/services/horizon/internal/render/problem" + "github.com/stellar/go/support/db" "github.com/stellar/go/support/log" "github.com/stellar/go/support/render/problem" ) @@ -58,11 +63,20 @@ const ( appVersionHeader = "X-App-Version" ) +func newWrapResponseWriter(w http.ResponseWriter, r *http.Request) middleware.WrapResponseWriter { + mw, ok := w.(middleware.WrapResponseWriter) + if !ok { + mw = middleware.NewWrapResponseWriter(w, r.ProtoMajor) + } + + return mw +} + // loggerMiddleware logs http requests and resposnes to the logging subsytem of horizon. func loggerMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - mw := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + mw := newWrapResponseWriter(w, r) logger := log.WithField("req", middleware.GetReqID(ctx)) ctx = log.Set(ctx, logger) @@ -82,6 +96,29 @@ func loggerMiddleware(h http.Handler) http.Handler { }) } +// timeoutMiddleware ensures the request is terminated after the given timeout +func timeoutMiddleware(timeout time.Duration) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + mw := newWrapResponseWriter(w, r) + ctx, cancel := context.WithTimeout(r.Context(), timeout) + defer func() { + cancel() + if ctx.Err() == context.DeadlineExceeded { + if mw.Status() == 0 { + // only write the header if it hasn't been written yet + mw.WriteHeader(http.StatusGatewayTimeout) + } + } + }() + + r = r.WithContext(ctx) + next.ServeHTTP(mw, r) + } + return http.HandlerFunc(fn) + } +} + // getClientData gets client data (name or version) from header or GET parameter // (useful when not possible to set headers, like in EventStream). func getClientData(r *http.Request, headerName string) string { @@ -99,6 +136,11 @@ func getClientData(r *http.Request, headerName string) string { } func logStartOfRequest(ctx context.Context, r *http.Request, streaming bool) { + referer := r.Referer() + if referer == "" { + referer = "undefined" + } + log.Ctx(ctx).WithFields(log.F{ "client_name": getClientData(r, clientNameHeader), "client_version": getClientData(r, clientVersionHeader), @@ -111,7 +153,7 @@ func logStartOfRequest(ctx context.Context, r *http.Request, streaming bool) { "method": r.Method, "path": r.URL.String(), "streaming": streaming, - "referer": r.Referer(), + "referer": referer, }).Info("Starting request") } @@ -123,6 +165,11 @@ func logEndOfRequest(ctx context.Context, r *http.Request, duration time.Duratio routePattern = "undefined" } + referer := r.Referer() + if referer == "" { + referer = "undefined" + } + log.Ctx(ctx).WithFields(log.F{ "bytes": mw.BytesWritten(), "client_name": getClientData(r, clientNameHeader), @@ -139,7 +186,7 @@ func logEndOfRequest(ctx context.Context, r *http.Request, duration time.Duratio "route": routePattern, "status": mw.Status(), "streaming": streaming, - "referer": r.Referer(), + "referer": referer, }).Info("Finished request") } @@ -176,7 +223,7 @@ func recoverMiddleware(h http.Handler) http.Handler { func requestMetricsMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { app := AppFromContext(r.Context()) - mw := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + mw := newWrapResponseWriter(w, r) app.web.requestTimer.Time(func() { h.ServeHTTP(mw.(http.ResponseWriter), r) @@ -206,47 +253,75 @@ func acceptOnlyJSON(h http.Handler) http.Handler { }) } -// requiresExperimentalIngestion is a middleware which enables a handler +// ExperimentalIngestionMiddleware is a middleware which enables a handler // if the experimental ingestion system is enabled and initialized. // It also ensures that state (ledger entries) has been verified and are // correct. Otherwise returns `500 Internal Server Error` to prevent // returning invalid data to the user. -func requiresExperimentalIngestion(h http.Handler) http.Handler { +type ExperimentalIngestionMiddleware struct { + EnableExperimentalIngestion bool + HorizonSession *db.Session + StateReady func() bool +} + +// Wrap executes the middleware on a given http handler +func (m *ExperimentalIngestionMiddleware) Wrap(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - app := AppFromContext(ctx) - if !app.config.EnableExperimentalIngestion { + if !m.EnableExperimentalIngestion { problem.Render(r.Context(), w, problem.NotFound) return } - localLog := log.Ctx(ctx) - - lastIngestedLedger, err := app.HistoryQ().GetLastLedgerExpIngestNonBlocking() - if err != nil { - localLog.WithField("err", err).Error("Error running GetLastLedgerExpIngestNonBlocking") - problem.Render(r.Context(), w, err) - return - } - // expingest has not finished processing any ledger so no data. - if lastIngestedLedger == 0 { + if !m.StateReady() { problem.Render(r.Context(), w, hProblem.StillIngesting) return } - stateInvalid, err := app.HistoryQ().GetExpStateInvalid() + localLog := log.Ctx(ctx) + session := m.HorizonSession.Clone() + session.Ctx = r.Context() + q := &history.Q{session} + + if render.Negotiate(r) != render.MimeEventStream { + err := session.BeginTx(&sql.TxOptions{ + Isolation: sql.LevelRepeatableRead, + ReadOnly: true, + }) + if err != nil { + localLog.WithField("err", err).Error("Error starting exp ingestion read transaction") + problem.Render(r.Context(), w, err) + return + } + defer session.Rollback() + + lastIngestedLedger, err := q.GetLastLedgerExpIngestNonBlocking() + if err != nil { + localLog.WithField("err", err).Error("Error running GetLastLedgerExpIngestNonBlocking") + problem.Render(r.Context(), w, err) + return + } + actions.SetLastLedgerHeader(w, lastIngestedLedger) + } + + stateInvalid, err := q.GetExpStateInvalid() if err != nil { localLog.WithField("err", err).Error("Error running GetExpStateInvalid") problem.Render(r.Context(), w, err) return } - if stateInvalid { problem.Render(r.Context(), w, problem.ServerError) return } - h.ServeHTTP(w, r) + h.ServeHTTP(w, r.WithContext( + context.WithValue( + r.Context(), + &horizonContext.SessionContextKey, + session, + ), + )) }) } diff --git a/services/horizon/internal/middleware_test.go b/services/horizon/internal/middleware_test.go index 645d031174..0e93d63b97 100644 --- a/services/horizon/internal/middleware_test.go +++ b/services/horizon/internal/middleware_test.go @@ -1,10 +1,17 @@ package horizon import ( + "net/http" + "net/http/httptest" "strconv" "testing" + "github.com/stellar/go/services/horizon/internal/actions" + horizonContext "github.com/stellar/go/services/horizon/internal/context" + "github.com/stellar/go/services/horizon/internal/db2/history" + hProblem "github.com/stellar/go/services/horizon/internal/render/problem" "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/support/db" "github.com/stellar/throttled" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -142,3 +149,66 @@ func TestRateLimit_Redis(t *testing.T) { w = rh.Get("/", test.RequestHelperRemoteAddr("127.0.0.2")) assert.Equal(t, 200, w.Code) } + +func TestRequiresExperimentalIngestion(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + request, err := http.NewRequest("GET", "http://localhost", nil) + if err != nil { + tt.Assert.NoError(err) + } + expectTransaction := true + + endpoint := func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value(&horizonContext.SessionContextKey).(*db.Session) + if (session.GetTx() == nil) == expectTransaction { + t.Fatalf("expected transaction to be in session: %v", expectTransaction) + } + w.WriteHeader(http.StatusOK) + } + ready := false + requiresExperimentalIngestion := &ExperimentalIngestionMiddleware{ + EnableExperimentalIngestion: false, + HorizonSession: tt.HorizonSession(), + StateReady: func() bool { + return ready + }, + } + handler := requiresExperimentalIngestion.Wrap(http.HandlerFunc(endpoint)) + q := &history.Q{tt.HorizonSession()} + + // requiresExperimentalIngestion responds with 404 if experimental ingestion is not enabled + w := httptest.NewRecorder() + handler.ServeHTTP(w, request) + tt.Assert.Equal(http.StatusNotFound, w.Code) + + requiresExperimentalIngestion.EnableExperimentalIngestion = true + // requiresExperimentalIngestion responds with hProblem.StillIngesting + // if Ready() is false + w = httptest.NewRecorder() + handler.ServeHTTP(w, request) + tt.Assert.Equal(hProblem.StillIngesting.Status, w.Code) + + ready = true + tt.Assert.NoError(q.UpdateExpStateInvalid(true)) + // requiresExperimentalIngestion responds with 500 if q.GetExpStateInvalid returns true + w = httptest.NewRecorder() + handler.ServeHTTP(w, request) + tt.Assert.Equal(http.StatusInternalServerError, w.Code) + + tt.Assert.NoError(q.UpdateLastLedgerExpIngest(3)) + tt.Assert.NoError(q.UpdateExpStateInvalid(false)) + w = httptest.NewRecorder() + handler.ServeHTTP(w, request) + tt.Assert.Equal(http.StatusOK, w.Code) + tt.Assert.Equal(w.Header().Get(actions.LastLedgerHeaderName), "3") + + request.Header.Set("Accept", "text/event-stream") + expectTransaction = false + w = httptest.NewRecorder() + handler.ServeHTTP(w, request) + tt.Assert.Equal(http.StatusOK, w.Code) + tt.Assert.Equal(w.Header().Get(actions.LastLedgerHeaderName), "") +} diff --git a/services/horizon/internal/paths/main.go b/services/horizon/internal/paths/main.go index 3363c6d33e..a7ffcee5c0 100644 --- a/services/horizon/internal/paths/main.go +++ b/services/horizon/internal/paths/main.go @@ -27,15 +27,18 @@ type Path struct { // Finder finds paths. type Finder interface { - // Returns path for a Query of a maximum length `maxLength` - Find(q Query, maxLength uint) ([]Path, error) - // FindFixedPaths return a list of payment paths each of which - // start by spending `amountToSpend` of `sourceAsset` and end - // with delivering a postive amount of `destinationAsset` + // Return a list of payment paths and the most recent ledger + // for a Query of a maximum length `maxLength`. The payment paths + // are accurate and consistent with the returned ledger sequence number + Find(q Query, maxLength uint) ([]Path, uint32, error) + // FindFixedPaths return a list of payment paths the most recent ledger + // Each of the payment paths start by spending `amountToSpend` of `sourceAsset` and end + // with delivering a postive amount of `destinationAsset`. + // The payment paths are accurate and consistent with the returned ledger sequence number FindFixedPaths( sourceAsset xdr.Asset, amountToSpend xdr.Int64, destinationAssets []xdr.Asset, maxLength uint, - ) ([]Path, error) + ) ([]Path, uint32, error) } diff --git a/services/horizon/internal/render/sse/stream_handler.go b/services/horizon/internal/render/sse/stream_handler.go index fb2b5ef166..1a25eef85f 100644 --- a/services/horizon/internal/render/sse/stream_handler.go +++ b/services/horizon/internal/render/sse/stream_handler.go @@ -74,6 +74,7 @@ func (handler StreamHandler) ServeStream( case currentLedgerSequence = <-handler.LedgerSource.NextLedger(currentLedgerSequence): continue case <-ctx.Done(): + stream.Done() return } } diff --git a/services/horizon/internal/render/sse/stream_handler_test.go b/services/horizon/internal/render/sse/stream_handler_test.go new file mode 100644 index 0000000000..b6b2f5ef77 --- /dev/null +++ b/services/horizon/internal/render/sse/stream_handler_test.go @@ -0,0 +1,38 @@ +package sse + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stellar/go/services/horizon/internal/ledger" +) + +func TestSendByeByeOnContextDone(t *testing.T) { + ledgerSource := ledger.NewTestingSource(1) + handler := StreamHandler{ + LedgerSource: ledgerSource, + } + + r, err := http.NewRequest("GET", "http://localhost", nil) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + ctx, cancel := context.WithCancel(context.Background()) + r = r.WithContext(ctx) + + w := httptest.NewRecorder() + + handler.ServeStream(w, r, 10, func() ([]Event, error) { + cancel() + return []Event{}, nil + }) + + expected := "retry: 1000\nevent: open\ndata: \"hello\"\n\n" + + "retry: 10\nevent: close\ndata: \"byebye\"\n\n" + + if got := w.Body.String(); got != expected { + t.Fatalf("expected '%v' but got '%v'", expected, got) + } +} diff --git a/services/horizon/internal/resourceadapter/account_entry.go b/services/horizon/internal/resourceadapter/account_entry.go new file mode 100644 index 0000000000..7e9e8bfba0 --- /dev/null +++ b/services/horizon/internal/resourceadapter/account_entry.go @@ -0,0 +1,102 @@ +package resourceadapter + +import ( + "context" + "fmt" + "strconv" + + protocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/services/horizon/internal/httpx" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/render/hal" + "github.com/stellar/go/xdr" +) + +// PopulateAccountEntry fills out the resource's fields +func PopulateAccountEntry( + ctx context.Context, + dest *protocol.Account, + account history.AccountEntry, + accountData []history.Data, + accountSigners []history.AccountSigner, + trustLines []history.TrustLine, +) error { + dest.ID = account.AccountID + dest.PT = account.AccountID + dest.AccountID = account.AccountID + dest.Sequence = strconv.FormatInt(account.SequenceNumber, 10) + dest.SubentryCount = int32(account.NumSubEntries) + dest.InflationDestination = account.InflationDestination + dest.HomeDomain = account.HomeDomain + dest.LastModifiedLedger = account.LastModifiedLedger + + dest.Flags.AuthRequired = account.IsAuthRequired() + dest.Flags.AuthRevocable = account.IsAuthRevocable() + dest.Flags.AuthImmutable = account.IsAuthImmutable() + + dest.Thresholds.LowThreshold = account.ThresholdLow + dest.Thresholds.MedThreshold = account.ThresholdMedium + dest.Thresholds.HighThreshold = account.ThresholdHigh + + // populate balances + dest.Balances = make([]protocol.Balance, len(trustLines)+1) + for i, tl := range trustLines { + err := PopulateHistoryBalance(&dest.Balances[i], tl) + if err != nil { + return errors.Wrap(err, "populating balance") + } + } + + // add native balance + err := PopulateNativeBalance( + &dest.Balances[len(dest.Balances)-1], + xdr.Int64(account.Balance), + xdr.Int64(account.BuyingLiabilities), + xdr.Int64(account.SellingLiabilities), + ) + if err != nil { + return errors.Wrap(err, "populating native balance") + } + + // populate data + dest.Data = make(map[string]string) + for _, d := range accountData { + dest.Data[d.Name] = d.Value.Base64() + } + + masterKeyIncluded := false + + // populate signers + dest.Signers = make([]protocol.Signer, len(accountSigners)) + for i, signer := range accountSigners { + dest.Signers[i].Weight = signer.Weight + dest.Signers[i].Key = signer.Signer + dest.Signers[i].Type = protocol.MustKeyTypeFromAddress(signer.Signer) + + if account.AccountID == signer.Signer { + masterKeyIncluded = true + } + } + + if !masterKeyIncluded { + dest.Signers = append(dest.Signers, protocol.Signer{ + Weight: int32(account.MasterWeight), + Key: account.AccountID, + Type: protocol.MustKeyTypeFromAddress(account.AccountID), + }) + } + + lb := hal.LinkBuilder{httpx.BaseURL(ctx)} + self := fmt.Sprintf("/accounts/%s", account.AccountID) + dest.Links.Self = lb.Link(self) + dest.Links.Transactions = lb.PagedLink(self, "transactions") + dest.Links.Operations = lb.PagedLink(self, "operations") + dest.Links.Payments = lb.PagedLink(self, "payments") + dest.Links.Effects = lb.PagedLink(self, "effects") + dest.Links.Offers = lb.PagedLink(self, "offers") + dest.Links.Trades = lb.PagedLink(self, "trades") + dest.Links.Data = lb.Link(self, "data/{key}") + dest.Links.Data.PopulateTemplated() + return nil +} diff --git a/services/horizon/internal/resourceadapter/account_entry_test.go b/services/horizon/internal/resourceadapter/account_entry_test.go new file mode 100644 index 0000000000..1455b07e8b --- /dev/null +++ b/services/horizon/internal/resourceadapter/account_entry_test.go @@ -0,0 +1,243 @@ +package resourceadapter + +import ( + "encoding/base64" + "encoding/json" + "strconv" + "testing" + + "github.com/stellar/go/amount" + . "github.com/stellar/go/protocols/horizon" + protocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/services/horizon/internal/assets" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/support/test" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +var ( + accountID = xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB") + + data = []history.Data{ + history.Data{ + AccountID: accountID.Address(), + Name: "test", + Value: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + LastModifiedLedger: 1, + }, + history.Data{ + AccountID: accountID.Address(), + Name: "test2", + Value: []byte{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, + LastModifiedLedger: 2, + }, + } + + inflationDest = xdr.MustAddress("GBUH7T6U36DAVEKECMKN5YEBQYZVRBPNSZAAKBCO6P5HBMDFSQMQL4Z4") + + account = history.AccountEntry{ + AccountID: accountID.Address(), + Balance: 20000, + SequenceNumber: 223456789, + NumSubEntries: 10, + InflationDestination: inflationDest.Address(), + Flags: 1, + HomeDomain: "stellar.org", + ThresholdLow: 1, + ThresholdMedium: 2, + ThresholdHigh: 3, + SellingLiabilities: 4, + BuyingLiabilities: 3, + LastModifiedLedger: 1000, + } + + trustLineIssuer = xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") + + trustLines = []history.TrustLine{ + history.TrustLine{ + AccountID: accountID.Address(), + AssetCode: "EUR", + AssetIssuer: trustLineIssuer.Address(), + AssetType: 1, + Balance: 20000, + Limit: 223456789, + Flags: 1, + SellingLiabilities: 3, + BuyingLiabilities: 4, + LastModifiedLedger: 900, + }, + history.TrustLine{ + AccountID: accountID.Address(), + AssetCode: "USD", + AssetIssuer: trustLineIssuer.Address(), + AssetType: 1, + Balance: 10000, + Limit: 123456789, + Flags: 0, + SellingLiabilities: 2, + BuyingLiabilities: 1, + LastModifiedLedger: 900, + }, + } + + signers = []history.AccountSigner{ + history.AccountSigner{ + Account: accountID.Address(), + Signer: accountID.Address(), + Weight: int32(3), + }, + + history.AccountSigner{ + Account: accountID.Address(), + Signer: "GCMQBJWOLTCSSMWNVDJAXL6E42SADH563IL5MN5B6RBBP4XP7TBRLJKE", + Weight: int32(1), + }, + history.AccountSigner{ + Account: accountID.Address(), + Signer: "GBXSGN5GX4PZOSBHB4JJF67CEGSGT56IN2N7LF3VGJ7WQ56BYWRVNNDX", + Weight: int32(2), + }, + history.AccountSigner{ + Account: accountID.Address(), + Signer: "GBPXUGDRAOU5QUNUAXX6LYPBIOXYG45GLTKIRWKOCQ6HXP5QE5OCPFBY", + Weight: int32(3), + }, + } +) + +func TestPopulateAccountEntry(t *testing.T) { + tt := assert.New(t) + ctx, _ := test.ContextWithLogBuffer() + hAccount := Account{} + err := PopulateAccountEntry(ctx, &hAccount, account, data, signers, trustLines) + tt.NoError(err) + + tt.Equal(account.AccountID, hAccount.ID) + tt.Equal(account.AccountID, hAccount.AccountID) + tt.Equal(account.AccountID, hAccount.PT) + tt.Equal(strconv.FormatInt(account.SequenceNumber, 10), hAccount.Sequence) + tt.Equal(int32(account.NumSubEntries), hAccount.SubentryCount) + tt.Equal(account.InflationDestination, hAccount.InflationDestination) + tt.Equal(account.HomeDomain, hAccount.HomeDomain) + tt.Equal(account.LastModifiedLedger, hAccount.LastModifiedLedger) + + wantAccountThresholds := AccountThresholds{ + LowThreshold: account.ThresholdLow, + MedThreshold: account.ThresholdMedium, + HighThreshold: account.ThresholdHigh, + } + tt.Equal(wantAccountThresholds, hAccount.Thresholds) + + wantFlags := AccountFlags{ + AuthRequired: account.IsAuthRequired(), + AuthRevocable: account.IsAuthRevocable(), + AuthImmutable: account.IsAuthImmutable(), + } + + tt.Equal(wantFlags, hAccount.Flags) + + for _, d := range data { + want, e := base64.StdEncoding.DecodeString(hAccount.Data[d.Name]) + tt.NoError(e) + tt.Equal(d.Value, history.AccountDataValue(want)) + } + + tt.Len(hAccount.Balances, 3) + + for i, t := range trustLines { + ht := hAccount.Balances[i] + tt.Equal(t.AssetIssuer, ht.Issuer) + tt.Equal(t.AssetCode, ht.Code) + wantType, e := assets.String(t.AssetType) + tt.NoError(e) + tt.Equal(wantType, ht.Type) + + tt.Equal(amount.StringFromInt64(t.Balance), ht.Balance) + tt.Equal(amount.StringFromInt64(t.BuyingLiabilities), ht.BuyingLiabilities) + tt.Equal(amount.StringFromInt64(t.SellingLiabilities), ht.SellingLiabilities) + tt.Equal(amount.StringFromInt64(t.Limit), ht.Limit) + tt.Equal(t.LastModifiedLedger, ht.LastModifiedLedger) + tt.Equal(t.IsAuthorized(), *ht.IsAuthorized) + } + + native := hAccount.Balances[len(hAccount.Balances)-1] + + tt.Equal("native", native.Type) + tt.Equal("0.0020000", native.Balance) + tt.Equal("0.0000003", native.BuyingLiabilities) + tt.Equal("0.0000004", native.SellingLiabilities) + tt.Equal("", native.Limit) + tt.Equal("", native.Issuer) + tt.Equal("", native.Code) + + tt.Len(hAccount.Signers, 4) + for i, s := range signers { + hs := hAccount.Signers[i] + tt.Equal(s.Signer, hs.Key) + tt.Equal(s.Weight, hs.Weight) + tt.Equal(protocol.MustKeyTypeFromAddress(s.Signer), hs.Type) + } + + links, err := json.Marshal(hAccount.Links) + want := ` + { + "data": { + "href": "/accounts/GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB/data/{key}", + "templated": true + }, + "effects": { + "href": "/accounts/GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB/effects{?cursor,limit,order}", + "templated": true + }, + "offers": { + "href": "/accounts/GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB/offers{?cursor,limit,order}", + "templated": true + }, + "operations": { + "href": "/accounts/GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB/operations{?cursor,limit,order}", + "templated": true + }, + "payments": { + "href": "/accounts/GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB/payments{?cursor,limit,order}", + "templated": true + }, + "self": { + "href": "/accounts/GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB" + }, + "trades": { + "href": "/accounts/GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB/trades{?cursor,limit,order}", + "templated": true + }, + "transactions": { + "href": "/accounts/GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB/transactions{?cursor,limit,order}", + "templated": true + } + } + ` + tt.JSONEq(want, string(links)) +} + +func TestPopulateAccountEntryMasterMissingInSigners(t *testing.T) { + tt := assert.New(t) + ctx, _ := test.ContextWithLogBuffer() + hAccount := Account{} + + account.MasterWeight = 0 + signers = []history.AccountSigner{ + history.AccountSigner{ + Account: accountID.Address(), + Signer: "GCMQBJWOLTCSSMWNVDJAXL6E42SADH563IL5MN5B6RBBP4XP7TBRLJKE", + Weight: int32(3), + }, + } + err := PopulateAccountEntry(ctx, &hAccount, account, data, signers, trustLines) + tt.NoError(err) + + tt.Len(hAccount.Signers, 2) + + signer := hAccount.Signers[1] + tt.Equal(account.AccountID, signer.Key) + tt.Equal(int32(account.MasterWeight), signer.Weight) + tt.Equal(protocol.MustKeyTypeFromAddress(account.AccountID), signer.Type) +} diff --git a/services/horizon/internal/resourceadapter/asset_stat.go b/services/horizon/internal/resourceadapter/asset_stat.go index 17b1d7a6f1..372302f836 100644 --- a/services/horizon/internal/resourceadapter/asset_stat.go +++ b/services/horizon/internal/resourceadapter/asset_stat.go @@ -2,10 +2,12 @@ package resourceadapter import ( "context" + "strings" "github.com/stellar/go/amount" protocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/db2/assets" + "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/render/hal" "github.com/stellar/go/xdr" @@ -36,3 +38,37 @@ func PopulateAssetStat( res.Links.Toml = hal.NewLink(row.Toml) return } + +// PopulateExpAssetStat populates an AssetStat using asset stats and account entries +// generated from the experimental ingestion system. PopulateAssetStat() is similar except +// it uses asset stats rows from the legacy ingestion system +func PopulateExpAssetStat( + ctx context.Context, + res *protocol.AssetStat, + row history.ExpAssetStat, + issuer history.AccountEntry, +) (err error) { + res.Asset.Type = xdr.AssetTypeToString[row.AssetType] + res.Asset.Code = row.AssetCode + res.Asset.Issuer = row.AssetIssuer + res.Amount, err = amount.IntStringToAmount(row.Amount) + if err != nil { + return errors.Wrap(err, "Invalid amount in PopulateAssetStat") + } + res.NumAccounts = row.NumAccounts + flags := int8(issuer.Flags) + res.Flags = protocol.AccountFlags{ + (flags & int8(xdr.AccountFlagsAuthRequiredFlag)) != 0, + (flags & int8(xdr.AccountFlagsAuthRevocableFlag)) != 0, + (flags & int8(xdr.AccountFlagsAuthImmutableFlag)) != 0, + } + res.PT = row.PagingToken() + + trimmed := strings.TrimSpace(issuer.HomeDomain) + var toml string + if trimmed != "" { + toml = "https://" + issuer.HomeDomain + "/.well-known/stellar.toml" + } + res.Links.Toml = hal.NewLink(toml) + return +} diff --git a/services/horizon/internal/resourceadapter/asset_stat_test.go b/services/horizon/internal/resourceadapter/asset_stat_test.go index 087f98d291..2332aa60e4 100644 --- a/services/horizon/internal/resourceadapter/asset_stat_test.go +++ b/services/horizon/internal/resourceadapter/asset_stat_test.go @@ -4,8 +4,11 @@ import ( "context" "testing" + "github.com/stellar/go/protocols/horizon" protocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/db2/assets" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" ) @@ -31,3 +34,54 @@ func TestLargeAmount(t *testing.T) { assert.Equal(t, int32(429), res.NumAccounts) assert.Equal(t, "https://xim.com/.well-known/stellar.toml", res.Links.Toml.Href) } + +func TestPopulateExpAssetStat(t *testing.T) { + row := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetCode: "XIM", + AssetIssuer: "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", + Amount: "100000000000000000000", // 10T + NumAccounts: 429, + } + issuer := history.AccountEntry{ + AccountID: "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", + Flags: 0, + HomeDomain: "xim.com", + } + + var res protocol.AssetStat + err := PopulateExpAssetStat(context.Background(), &res, row, issuer) + assert.NoError(t, err) + + assert.Equal(t, "credit_alphanum4", res.Type) + assert.Equal(t, "XIM", res.Code) + assert.Equal(t, "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", res.Issuer) + assert.Equal(t, "10000000000000.0000000", res.Amount) + assert.Equal(t, int32(429), res.NumAccounts) + assert.Equal(t, horizon.AccountFlags{}, res.Flags) + assert.Equal(t, "https://xim.com/.well-known/stellar.toml", res.Links.Toml.Href) + assert.Equal(t, row.PagingToken(), res.PagingToken()) + + issuer.HomeDomain = "" + issuer.Flags = uint32(xdr.AccountFlagsAuthRequiredFlag) | + uint32(xdr.AccountFlagsAuthImmutableFlag) + + err = PopulateExpAssetStat(context.Background(), &res, row, issuer) + assert.NoError(t, err) + + assert.Equal(t, "credit_alphanum4", res.Type) + assert.Equal(t, "XIM", res.Code) + assert.Equal(t, "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", res.Issuer) + assert.Equal(t, "10000000000000.0000000", res.Amount) + assert.Equal(t, int32(429), res.NumAccounts) + assert.Equal( + t, + horizon.AccountFlags{ + AuthRequired: true, + AuthImmutable: true, + }, + res.Flags, + ) + assert.Equal(t, "", res.Links.Toml.Href) + assert.Equal(t, row.PagingToken(), res.PagingToken()) +} diff --git a/services/horizon/internal/resourceadapter/balance.go b/services/horizon/internal/resourceadapter/balance.go index 4e7eaa7084..b0fd3f2445 100644 --- a/services/horizon/internal/resourceadapter/balance.go +++ b/services/horizon/internal/resourceadapter/balance.go @@ -5,6 +5,7 @@ import ( protocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/assets" "github.com/stellar/go/services/horizon/internal/db2/core" + "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" ) @@ -27,6 +28,24 @@ func PopulateBalance(dest *protocol.Balance, row core.Trustline) (err error) { return } +func PopulateHistoryBalance(dest *protocol.Balance, row history.TrustLine) (err error) { + dest.Type, err = assets.String(row.AssetType) + if err != nil { + return errors.Wrap(err, "getting the string representation from the provided xdr asset type") + } + + dest.Balance = amount.StringFromInt64(row.Balance) + dest.BuyingLiabilities = amount.StringFromInt64(row.BuyingLiabilities) + dest.SellingLiabilities = amount.StringFromInt64(row.SellingLiabilities) + dest.Limit = amount.StringFromInt64(row.Limit) + dest.Issuer = row.AssetIssuer + dest.Code = row.AssetCode + dest.LastModifiedLedger = row.LastModifiedLedger + isAuthorized := row.IsAuthorized() + dest.IsAuthorized = &isAuthorized + return +} + func PopulateNativeBalance(dest *protocol.Balance, stroops, buyingLiabilities, sellingLiabilities xdr.Int64) (err error) { dest.Type, err = assets.String(xdr.AssetTypeAssetTypeNative) if err != nil { diff --git a/services/horizon/internal/resourceadapter/balance_test.go b/services/horizon/internal/resourceadapter/balance_test.go index 11d61f3897..fbf013e99b 100644 --- a/services/horizon/internal/resourceadapter/balance_test.go +++ b/services/horizon/internal/resourceadapter/balance_test.go @@ -5,6 +5,7 @@ import ( . "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/db2/core" + "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" ) @@ -52,6 +53,49 @@ func TestPopulateBalance(t *testing.T) { assert.Equal(t, false, *want.IsAuthorized) } +func TestPopulateHistoryBalance(t *testing.T) { + testAssetCode1 := "TEST_ASSET_1" + testAssetCode2 := "TEST_ASSET_2" + authorizedTrustline := history.TrustLine{ + AccountID: "testID", + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, + AssetIssuer: "", + AssetCode: testAssetCode1, + Limit: 100, + Balance: 10, + Flags: 1, + } + unauthorizedTrustline := history.TrustLine{ + AccountID: "testID", + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, + AssetIssuer: "", + AssetCode: testAssetCode2, + Limit: 100, + Balance: 10, + Flags: 2, + } + + want := Balance{} + err := PopulateHistoryBalance(&want, authorizedTrustline) + assert.NoError(t, err) + assert.Equal(t, "credit_alphanum12", want.Type) + assert.Equal(t, "0.0000010", want.Balance) + assert.Equal(t, "0.0000100", want.Limit) + assert.Equal(t, "", want.Issuer) + assert.Equal(t, testAssetCode1, want.Code) + assert.Equal(t, true, *want.IsAuthorized) + + want = Balance{} + err = PopulateHistoryBalance(&want, unauthorizedTrustline) + assert.NoError(t, err) + assert.Equal(t, "credit_alphanum12", want.Type) + assert.Equal(t, "0.0000010", want.Balance) + assert.Equal(t, "0.0000100", want.Limit) + assert.Equal(t, "", want.Issuer) + assert.Equal(t, testAssetCode2, want.Code) + assert.Equal(t, false, *want.IsAuthorized) +} + func TestPopulateNativeBalance(t *testing.T) { want := Balance{} err := PopulateNativeBalance(&want, 10, 10, 10) diff --git a/services/horizon/internal/resourceadapter/root.go b/services/horizon/internal/resourceadapter/root.go index 48493d0591..136d77221c 100644 --- a/services/horizon/internal/resourceadapter/root.go +++ b/services/horizon/internal/resourceadapter/root.go @@ -21,6 +21,7 @@ func PopulateRoot( coreSupportedProtocolVersion int32, friendBotURL *url.URL, experimentalIngestionEnabled bool, + templates map[string]string, ) { dest.ExpHorizonSequence = ledgerState.ExpHistoryLatest dest.HorizonSequence = ledgerState.HistoryLatest @@ -45,12 +46,16 @@ func PopulateRoot( dest.Links.Metrics = lb.Link("/metrics") if experimentalIngestionEnabled { - accountsLink := lb.Link("/accounts?{signer}") + accountsLink := lb.Link(templates["accounts"]) offerLink := lb.Link("/offers/{offer_id}") - offersLink := lb.Link("/offers{?seller,selling_asset_type,selling_asset_code,selling_asset_issuer,buying_asset_type,buying_asset_code,buying_asset_issuer,cursor,limit,order}") + offersLink := lb.Link(templates["offers"]) + strictReceivePaths := lb.Link(templates["strictReceivePaths"]) + strictSendPaths := lb.Link(templates["strictSendPaths"]) dest.Links.Accounts = &accountsLink dest.Links.Offer = &offerLink dest.Links.Offers = &offersLink + dest.Links.StrictReceivePaths = &strictReceivePaths + dest.Links.StrictSendPaths = &strictSendPaths } dest.Links.OrderBook = lb.Link("/order_book{?selling_asset_type,selling_asset_code,selling_asset_issuer,buying_asset_type,buying_asset_code,buying_asset_issuer,limit}") diff --git a/services/horizon/internal/resourceadapter/root_test.go b/services/horizon/internal/resourceadapter/root_test.go index 3c415976c0..9751cd57c8 100644 --- a/services/horizon/internal/resourceadapter/root_test.go +++ b/services/horizon/internal/resourceadapter/root_test.go @@ -13,6 +13,13 @@ import ( func TestPopulateRoot(t *testing.T) { res := &horizon.Root{} + templates := map[string]string{ + "accounts": "/accounts{?signer,asset_type,asset_issuer,asset_code}", + "offers": "/offers", + "strictReceivePaths": "/paths/strict-receive", + "strictSendPaths": "/paths/strict-send", + } + PopulateRoot(context.Background(), res, ledger.State{CoreLatest: 1, HistoryLatest: 3, HistoryElder: 2}, @@ -23,6 +30,7 @@ func TestPopulateRoot(t *testing.T) { 101, urlMustParse(t, "https://friendbot.example.com"), false, + templates, ) assert.Equal(t, int32(1), res.CoreSequence) @@ -35,6 +43,8 @@ func TestPopulateRoot(t *testing.T) { assert.Empty(t, res.Links.Accounts) assert.Empty(t, res.Links.Offer) assert.Empty(t, res.Links.Offers) + assert.Empty(t, res.Links.StrictReceivePaths) + assert.Empty(t, res.Links.StrictSendPaths) // Without testbot res = &horizon.Root{} @@ -48,6 +58,7 @@ func TestPopulateRoot(t *testing.T) { 101, nil, false, + templates, ) assert.Equal(t, int32(1), res.CoreSequence) @@ -70,11 +81,26 @@ func TestPopulateRoot(t *testing.T) { 101, urlMustParse(t, "https://friendbot.example.com"), true, + templates, ) - assert.Equal(t, "/accounts?{signer}", res.Links.Accounts.Href) + assert.Equal(t, templates["accounts"], res.Links.Accounts.Href) assert.Equal(t, "/offers/{offer_id}", res.Links.Offer.Href) - assert.Equal(t, "/offers{?seller,selling_asset_type,selling_asset_code,selling_asset_issuer,buying_asset_type,buying_asset_code,buying_asset_issuer,cursor,limit,order}", res.Links.Offers.Href) + assert.Equal( + t, + templates["offers"], + res.Links.Offers.Href, + ) + assert.Equal( + t, + templates["strictReceivePaths"], + res.Links.StrictReceivePaths.Href, + ) + assert.Equal( + t, + templates["strictSendPaths"], + res.Links.StrictSendPaths.Href, + ) } func urlMustParse(t *testing.T, s string) *url.URL { diff --git a/services/horizon/internal/resourceadapter/signer.go b/services/horizon/internal/resourceadapter/signer.go index ae2e931569..2dd74ac41f 100644 --- a/services/horizon/internal/resourceadapter/signer.go +++ b/services/horizon/internal/resourceadapter/signer.go @@ -15,7 +15,7 @@ func PopulateSigner(ctx context.Context, dest *protocol.Signer, row core.Signer) dest.Type = protocol.MustKeyTypeFromAddress(dest.Key) } -// PopulateMaster fills out the fields of the signer, using a stellar account to +// PopulateMasterSigner fills out the fields of the signer, using a stellar account to // provide the data. func PopulateMasterSigner(dest *protocol.Signer, row core.Account) { dest.Weight = int32(row.Thresholds[0]) diff --git a/services/horizon/internal/simplepath/finder.go b/services/horizon/internal/simplepath/finder.go index 56dabc59e8..dbb568821e 100644 --- a/services/horizon/internal/simplepath/finder.go +++ b/services/horizon/internal/simplepath/finder.go @@ -21,7 +21,7 @@ type Finder struct { var _ paths.Finder = &Finder{} // Find performs a path find with the provided query. -func (f *Finder) Find(q paths.Query, maxLength uint) (result []paths.Path, err error) { +func (f *Finder) Find(q paths.Query, maxLength uint) (result []paths.Path, lastLedger uint32, err error) { log.WithField("source_assets", q.SourceAssets). WithField("destination_asset", q.DestinationAsset). WithField("destination_amount", q.DestinationAmount). @@ -65,6 +65,6 @@ func (f *Finder) FindFixedPaths( amountToSpend xdr.Int64, destinationAssets []xdr.Asset, maxLength uint, -) ([]paths.Path, error) { - return nil, errors.New("Not implemented") +) ([]paths.Path, uint32, error) { + return nil, 0, errors.New("Not implemented") } diff --git a/services/horizon/internal/simplepath/finder_test.go b/services/horizon/internal/simplepath/finder_test.go index 0eec44c9e0..84a9fb6427 100644 --- a/services/horizon/internal/simplepath/finder_test.go +++ b/services/horizon/internal/simplepath/finder_test.go @@ -45,7 +45,7 @@ func TestFinder(t *testing.T) { SourceAssets: []xdr.Asset{usd}, } - p, err := finder.Find(query, MaxPathLength) + p, _, err := finder.Find(query, MaxPathLength) if tt.Assert.NoError(err) { tt.Assert.Len(p, 3) @@ -84,7 +84,7 @@ func TestFinder(t *testing.T) { } query.DestinationAmount = xdr.Int64(200000001) - p, err = finder.Find(query, MaxPathLength) + p, _, err = finder.Find(query, MaxPathLength) if tt.Assert.NoError(err) { tt.Assert.Len(p, 2) @@ -105,7 +105,7 @@ func TestFinder(t *testing.T) { } query.DestinationAmount = xdr.Int64(500000001) - p, err = finder.Find(query, MaxPathLength) + p, _, err = finder.Find(query, MaxPathLength) if tt.Assert.NoError(err) { tt.Assert.Len(p, 0) } @@ -117,7 +117,7 @@ func TestFinder(t *testing.T) { DestinationAmount: xdr.Int64(1), SourceAssets: []xdr.Asset{usd, native}, } - p, err = finder.Find(query, MaxPathLength) + p, _, err = finder.Find(query, MaxPathLength) if tt.Assert.NoError(err) { tt.Assert.Len(p, 2) } @@ -152,7 +152,7 @@ func TestFinder(t *testing.T) { DestinationAmount: xdr.Int64(100000000), // 10.0 SourceAssets: []xdr.Asset{aaa}, } - p, err = finder.Find(query, MaxPathLength) + p, _, err = finder.Find(query, MaxPathLength) if tt.Assert.NoError(err) { if tt.Assert.Len(p, 1) { tt.Assert.Equal(p[0].Source.String(), aaa.String()) diff --git a/services/horizon/internal/simplepath/inmemory.go b/services/horizon/internal/simplepath/inmemory.go index 7e24604bfb..c48e53782c 100644 --- a/services/horizon/internal/simplepath/inmemory.go +++ b/services/horizon/internal/simplepath/inmemory.go @@ -32,19 +32,19 @@ func NewInMemoryFinder(graph *orderbook.OrderBookGraph) InMemoryFinder { } // Find implements the path payments finder interface -func (finder InMemoryFinder) Find(q paths.Query, maxLength uint) ([]paths.Path, error) { +func (finder InMemoryFinder) Find(q paths.Query, maxLength uint) ([]paths.Path, uint32, error) { if finder.graph.IsEmpty() { - return nil, ErrEmptyInMemoryOrderBook + return nil, 0, ErrEmptyInMemoryOrderBook } if maxLength == 0 { maxLength = MaxInMemoryPathLength } if maxLength > MaxInMemoryPathLength { - return nil, errors.New("invalid value of maxLength") + return nil, 0, errors.New("invalid value of maxLength") } - orderbookPaths, err := finder.graph.FindPaths( + orderbookPaths, lastLedger, err := finder.graph.FindPaths( int(maxLength), q.DestinationAsset, q.DestinationAmount, @@ -64,7 +64,7 @@ func (finder InMemoryFinder) Find(q paths.Query, maxLength uint) ([]paths.Path, DestinationAmount: path.DestinationAmount, } } - return results, err + return results, lastLedger, err } // FindFixedPaths returns a list of payment paths where the source and destination @@ -77,19 +77,19 @@ func (finder InMemoryFinder) FindFixedPaths( amountToSpend xdr.Int64, destinationAssets []xdr.Asset, maxLength uint, -) ([]paths.Path, error) { +) ([]paths.Path, uint32, error) { if finder.graph.IsEmpty() { - return nil, ErrEmptyInMemoryOrderBook + return nil, 0, ErrEmptyInMemoryOrderBook } if maxLength == 0 { maxLength = MaxInMemoryPathLength } if maxLength > MaxInMemoryPathLength { - return nil, errors.New("invalid value of maxLength") + return nil, 0, errors.New("invalid value of maxLength") } - orderbookPaths, err := finder.graph.FindFixedPaths( + orderbookPaths, lastLedger, err := finder.graph.FindFixedPaths( int(maxLength), sourceAsset, amountToSpend, @@ -106,5 +106,5 @@ func (finder InMemoryFinder) FindFixedPaths( DestinationAmount: path.DestinationAmount, } } - return results, err + return results, lastLedger, err } diff --git a/services/horizon/internal/stream_handler_test.go b/services/horizon/internal/stream_handler_test.go index f19ef37526..100d712ec7 100644 --- a/services/horizon/internal/stream_handler_test.go +++ b/services/horizon/internal/stream_handler_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/go-chi/chi" + "github.com/stellar/go/services/horizon/internal/actions" "github.com/stellar/go/services/horizon/internal/ledger" "github.com/stellar/go/services/horizon/internal/render/sse" "github.com/stellar/go/support/render/hal" @@ -20,46 +21,78 @@ import ( // StreamTest utility struct to wrap SSE related tests. type StreamTest struct { - action pageAction - ledgerSource *ledger.TestingSource - cancel context.CancelFunc - wg *sync.WaitGroup - ctx context.Context + ledgerSource *ledger.TestingSource + cancel context.CancelFunc + wg *sync.WaitGroup + w *httptest.ResponseRecorder + checkResponse func(w *httptest.ResponseRecorder) + ctx context.Context } -// NewStreamTest executes an SSE related test, letting you simulate ledger closings via -// AddLedger. -func NewStreamTest( - action pageAction, - currentLedger uint32, +func newStreamTest( + handler http.HandlerFunc, + ledgerSource *ledger.TestingSource, request *http.Request, checkResponse func(w *httptest.ResponseRecorder), ) *StreamTest { s := &StreamTest{ - action: action, - ledgerSource: ledger.NewTestingSource(currentLedger), - wg: &sync.WaitGroup{}, + ledgerSource: ledgerSource, + w: httptest.NewRecorder(), + checkResponse: checkResponse, + wg: &sync.WaitGroup{}, } s.ctx, s.cancel = context.WithCancel(request.Context()) - streamHandler := sse.StreamHandler{ - LedgerSource: s.ledgerSource, - } - handler := streamablePageHandler(s.action, streamHandler) - s.wg.Add(1) go func() { - w := httptest.NewRecorder() - handler.renderStream(w, request.WithContext(s.ctx)) + handler(s.w, request.WithContext(s.ctx)) s.wg.Done() s.cancel() - - checkResponse(w) }() return s } +// NewstreamableObjectTest tests the SSE functionality of a pageAction +func NewStreamablePageTest( + action *testPageAction, + currentLedger uint32, + request *http.Request, + checkResponse func(w *httptest.ResponseRecorder), +) *StreamTest { + ledgerSource := ledger.NewTestingSource(currentLedger) + action.ledgerSource = ledgerSource + streamHandler := sse.StreamHandler{LedgerSource: ledgerSource} + handler := streamablePageHandler(action, streamHandler) + + return newStreamTest( + handler.renderStream, + ledgerSource, + request, + checkResponse, + ) +} + +// NewstreamableObjectTest tests the SSE functionality of a streamableObjectAction +func NewstreamableObjectTest( + action *testObjectAction, + currentLedger uint32, + request *http.Request, + checkResponse func(w *httptest.ResponseRecorder), +) *StreamTest { + ledgerSource := ledger.NewTestingSource(currentLedger) + action.ledgerSource = ledgerSource + streamHandler := sse.StreamHandler{LedgerSource: ledgerSource} + handler := streamableObjectActionHandler{action, streamHandler} + + return newStreamTest( + handler.renderStream, + ledgerSource, + request, + checkResponse, + ) +} + // AddLedger pushes a new ledger to the stream handler. AddLedger() will block until // the new ledger has been read by the stream handler func (s *StreamTest) AddLedger(sequence uint32) { @@ -81,10 +114,11 @@ func (s *StreamTest) Wait(expectLimitReached bool) { if !expectLimitReached { // first send a ledger to the stream handler so we can ensure that at least one // iteration of the stream loop has been executed - s.TryAddLedger(0) + s.TryAddLedger(s.ledgerSource.CurrentLedger() + 1) s.cancel() } s.wg.Wait() + s.checkResponse(s.w) } type testPage struct { @@ -97,19 +131,18 @@ func (p testPage) PagingToken() string { } type testPageAction struct { - objects []string - lock sync.Mutex + objects map[uint32][]string + ledgerSource ledger.Source } -func (action *testPageAction) appendObjects(objects ...string) { - action.lock.Lock() - defer action.lock.Unlock() - action.objects = append(action.objects, objects...) -} - -func (action *testPageAction) GetResourcePage(r *http.Request) ([]hal.Pageable, error) { - action.lock.Lock() - defer action.lock.Unlock() +func (action *testPageAction) GetResourcePage( + w actions.HeaderWriter, + r *http.Request, +) ([]hal.Pageable, error) { + objects, ok := action.objects[action.ledgerSource.CurrentLedger()] + if !ok { + return nil, fmt.Errorf("unexpected ledger") + } cursor := r.Header.Get("Last-Event-ID") if cursor == "" { @@ -123,7 +156,7 @@ func (action *testPageAction) GetResourcePage(r *http.Request) ([]hal.Pageable, return nil, err } - limit := len(action.objects) + limit := len(objects) if limitParam := r.URL.Query().Get("limit"); limitParam != "" { limit, err = strconv.Atoi(limitParam) if err != nil { @@ -135,11 +168,12 @@ func (action *testPageAction) GetResourcePage(r *http.Request) ([]hal.Pageable, return nil, fmt.Errorf("cursor cannot be negative") } - if parsedCursor >= len(action.objects) { + if parsedCursor >= len(objects) { return []hal.Pageable{}, nil } + response := []hal.Pageable{} - for i, object := range action.objects[parsedCursor:] { + for i, object := range objects[parsedCursor:] { if len(response) >= limit { break } @@ -161,18 +195,31 @@ func streamRequest(t *testing.T, queryParams string) *http.Request { return request } -func expectResponse(t *testing.T, expectedResponse []string) func(*httptest.ResponseRecorder) { +func unmarashalPage(jsonString string) (string, error) { + var page testPage + err := json.Unmarshal([]byte(jsonString), &page) + return page.Value, err +} + +func expectResponse( + t *testing.T, + unmarshal func(string) (string, error), + expectedResponse []string, +) func(*httptest.ResponseRecorder) { return func(w *httptest.ResponseRecorder) { var response []string for _, line := range strings.Split(w.Body.String(), "\n") { - if strings.HasPrefix(line, "data: {") { + if line == "data: \"hello\"" || line == "data: \"byebye\"" { + continue + } + + if strings.HasPrefix(line, "data: ") { jsonString := line[len("data: "):] - var page testPage - err := json.Unmarshal([]byte(jsonString), &page) + value, err := unmarshal(jsonString) if err != nil { t.Fatalf("could not parse json %v", err) } - response = append(response, page.Value) + response = append(response, value) } } @@ -188,91 +235,230 @@ func expectResponse(t *testing.T, expectedResponse []string) func(*httptest.Resp } } -func TestRenderStream(t *testing.T) { - action := &testPageAction{ - objects: []string{"a", "b", "c"}, - } - +func TestPageStream(t *testing.T) { t.Run("without offset", func(t *testing.T) { request := streamRequest(t, "") - st := NewStreamTest( + action := &testPageAction{ + objects: map[uint32][]string{ + 3: []string{"a", "b", "c"}, + 4: []string{"a", "b", "c", "d", "e"}, + 6: []string{"a", "b", "c", "d", "e", "f"}, + 7: []string{"a", "b", "c", "d", "e", "f"}, + }, + } + st := NewStreamablePageTest( action, 3, request, - expectResponse(t, []string{"a", "b", "c", "d", "e", "f"}), + expectResponse(t, unmarashalPage, []string{"a", "b", "c", "d", "e", "f"}), ) st.AddLedger(4) - action.appendObjects("d", "e") - st.AddLedger(6) - action.appendObjects("f") st.Wait(false) }) - action.objects = []string{"a", "b", "c"} t.Run("with offset", func(t *testing.T) { request := streamRequest(t, "cursor=1") - st := NewStreamTest( + action := &testPageAction{ + objects: map[uint32][]string{ + 3: []string{"a", "b", "c"}, + 4: []string{"a", "b", "c", "d", "e"}, + 6: []string{"a", "b", "c", "d", "e", "f"}, + 7: []string{"a", "b", "c", "d", "e", "f"}, + }, + } + st := NewStreamablePageTest( action, 3, request, - expectResponse(t, []string{"b", "c", "d", "e", "f"}), + expectResponse(t, unmarashalPage, []string{"b", "c", "d", "e", "f"}), ) st.AddLedger(4) - action.appendObjects("d", "e") - st.AddLedger(6) - action.appendObjects("f") st.Wait(false) }) - action.objects = []string{"a", "b", "c"} t.Run("with limit", func(t *testing.T) { request := streamRequest(t, "limit=2") - st := NewStreamTest( + action := &testPageAction{ + objects: map[uint32][]string{ + 3: []string{"a", "b", "c"}, + }, + } + st := NewStreamablePageTest( action, 3, request, - expectResponse(t, []string{"a", "b"}), + expectResponse(t, unmarashalPage, []string{"a", "b"}), ) st.Wait(true) }) - action.objects = []string{"a", "b", "c", "d", "e"} t.Run("with limit and offset", func(t *testing.T) { request := streamRequest(t, "limit=2&cursor=1") - st := NewStreamTest( + action := &testPageAction{ + objects: map[uint32][]string{ + 3: []string{"a", "b", "c", "d", "e"}, + }, + } + st := NewStreamablePageTest( action, 3, request, - expectResponse(t, []string{"b", "c"}), + expectResponse(t, unmarashalPage, []string{"b", "c"}), ) st.Wait(true) }) - action.objects = []string{"a"} t.Run("reach limit after multiple iterations", func(t *testing.T) { request := streamRequest(t, "limit=3&cursor=1") - st := NewStreamTest( + action := &testPageAction{ + objects: map[uint32][]string{ + 3: []string{"a"}, + 4: []string{"a", "b"}, + 5: []string{"a", "b", "c", "d", "e", "f", "g"}, + }, + } + st := NewStreamablePageTest( action, 3, request, - expectResponse(t, []string{"b", "c", "d"}), + expectResponse(t, unmarashalPage, []string{"b", "c", "d"}), ) st.AddLedger(4) - action.appendObjects("b") + st.AddLedger(5) + + st.Wait(true) + }) +} +type stringObject string + +func (s stringObject) Equals(other actions.StreamableObjectResponse) bool { + otherString, ok := other.(stringObject) + if !ok { + return false + } + return s == otherString +} + +func unmarashalString(jsonString string) (string, error) { + var object stringObject + err := json.Unmarshal([]byte(jsonString), &object) + return string(object), err +} + +type testObjectAction struct { + objects map[uint32]stringObject + ledgerSource ledger.Source +} + +func (action *testObjectAction) GetResource( + w actions.HeaderWriter, + r *http.Request, +) (actions.StreamableObjectResponse, error) { + ledger := action.ledgerSource.CurrentLedger() + object, ok := action.objects[ledger] + if !ok { + return nil, fmt.Errorf("unexpected ledger") + } + + return object, nil +} + +func TestObjectStream(t *testing.T) { + t.Run("without interior duplicates", func(t *testing.T) { + request := streamRequest(t, "") + action := &testObjectAction{ + objects: map[uint32]stringObject{ + 3: "a", + 4: "b", + 5: "c", + 6: "c", + }, + } + + st := NewstreamableObjectTest( + action, + 3, + request, + expectResponse(t, unmarashalString, []string{"a", "b", "c"}), + ) + + st.AddLedger(4) + st.AddLedger(5) + st.Wait(false) + }) + + t.Run("with interior duplicates", func(t *testing.T) { + request := streamRequest(t, "") + action := &testObjectAction{ + objects: map[uint32]stringObject{ + 3: "a", + 4: "b", + 5: "b", + 6: "c", + 7: "c", + }, + } + + st := NewstreamableObjectTest( + action, + 3, + request, + expectResponse(t, unmarashalString, []string{"a", "b", "c"}), + ) + + st.AddLedger(4) st.AddLedger(5) - action.appendObjects("c", "d", "e", "f", "g") + st.AddLedger(6) + + st.Wait(false) + }) + + t.Run("limit reached", func(t *testing.T) { + request := streamRequest(t, "") + action := &testObjectAction{ + objects: map[uint32]stringObject{ + 1: "a", + 2: "b", + 3: "b", + 4: "c", + 5: "d", + 6: "e", + 7: "f", + 8: "g", + 9: "h", + 10: "i", + 11: "j", + 12: "k", + }, + } + + st := NewstreamableObjectTest( + action, + 1, + request, + expectResponse( + t, + unmarashalString, + []string{ + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", + }, + ), + ) + + for i := uint32(1); i <= 11; i++ { + st.AddLedger(i) + } - st.TryAddLedger(0) st.Wait(true) }) } diff --git a/services/horizon/internal/test/scenarios/base-core.sql b/services/horizon/internal/test/scenarios/base-core.sql index 0b5a2e4acc..de23defaac 100644 --- a/services/horizon/internal/test/scenarios/base-core.sql +++ b/services/horizon/internal/test/scenarios/base-core.sql @@ -113,16 +113,16 @@ CREATE TABLE accountdata ( CREATE TABLE accounts ( accountid character varying(56) NOT NULL, balance bigint NOT NULL, + buyingliabilities bigint, + sellingliabilities bigint, seqnum bigint NOT NULL, numsubentries integer NOT NULL, inflationdest character varying(56), homedomain character varying(44) NOT NULL, thresholds text NOT NULL, flags integer NOT NULL, - lastmodified integer NOT NULL, - buyingliabilities bigint, - sellingliabilities bigint, signers text, + lastmodified integer NOT NULL, CONSTRAINT accounts_balance_check CHECK ((balance >= 0)), CONSTRAINT accounts_buyingliabilities_check CHECK ((buyingliabilities >= 0)), CONSTRAINT accounts_numsubentries_check CHECK ((numsubentries >= 0)), @@ -265,10 +265,10 @@ CREATE TABLE trustlines ( assetcode character varying(12) NOT NULL, tlimit bigint NOT NULL, balance bigint NOT NULL, - flags integer NOT NULL, - lastmodified integer NOT NULL, buyingliabilities bigint, sellingliabilities bigint, + flags integer NOT NULL, + lastmodified integer NOT NULL, CONSTRAINT trustlines_balance_check CHECK ((balance >= 0)), CONSTRAINT trustlines_buyingliabilities_check CHECK ((buyingliabilities >= 0)), CONSTRAINT trustlines_sellingliabilities_check CHECK ((sellingliabilities >= 0)), @@ -327,10 +327,10 @@ CREATE TABLE upgradehistory ( -- Data for Name: accounts; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO accounts VALUES ('GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', 999999996999999700, 3, 0, NULL, '', 'AQAAAA==', 0, 2, NULL, NULL, NULL); -INSERT INTO accounts VALUES ('GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2', 1000000000, 8589934592, 0, NULL, '', 'AQAAAA==', 0, 2, NULL, NULL, NULL); -INSERT INTO accounts VALUES ('GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU', 949999900, 8589934593, 0, NULL, '', 'AQAAAA==', 0, 3, NULL, NULL, NULL); -INSERT INTO accounts VALUES ('GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON', 1050000000, 8589934592, 0, NULL, '', 'AQAAAA==', 0, 3, NULL, NULL, NULL); +INSERT INTO accounts VALUES ('GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', 999999996999999700, NULL, NULL, 3, 0, NULL, '', 'AQAAAA==', 0, NULL, 2); +INSERT INTO accounts VALUES ('GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2', 1000000000, NULL, NULL, 8589934592, 0, NULL, '', 'AQAAAA==', 0, NULL, 2); +INSERT INTO accounts VALUES ('GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU', 949999900, NULL, NULL, 8589934593, 0, NULL, '', 'AQAAAA==', 0, NULL, 3); +INSERT INTO accounts VALUES ('GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON', 1050000000, NULL, NULL, 8589934592, 0, NULL, '', 'AQAAAA==', 0, NULL, 3); -- @@ -344,8 +344,8 @@ INSERT INTO accounts VALUES ('GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCS -- INSERT INTO ledgerheaders VALUES ('63d98f536ee68d1b27b5b89f23af5311b7569a24faf1403ad0b52b633b07be99', '0000000000000000000000000000000000000000000000000000000000000000', '572a2e32ff248a07b0e70fd1f6d318c1facd20b6cc08c33d5775259868125a16', 1, 0, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABXKi4y/ySKB7DnD9H20xjB+s0gtswIwz1XdSWYaBJaFgAAAAEN4Lazp2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAX14QAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); -INSERT INTO ledgerheaders VALUES ('53e51e5309f32d15500e5b607324be76ef6bb31f42a541d16634681bcb698a22', '63d98f536ee68d1b27b5b89f23af5311b7569a24faf1403ad0b52b633b07be99', '194640b91070017780cd171e5b96d2b40fec01b707c89d52bc33897a6a4c2f7f', 2, 1559579641, 'AAAAC2PZj1Nu5o0bJ7W4nyOvUxG3Vpok+vFAOtC1K2M7B76ZlmEdOpVCM5HLr9FNj55qa6w2HKMtqTPFLvG8yPU/aAoAAAAAXPVL+QAAAAIAAAAIAAAAAQAAAAsAAAAIAAAAAwAPQkAAAAAARUAVxJm1lDMwwqujKcyQzs97F/AETiCgQPrw63wqaPEZRkC5EHABd4DNFx5bltK0D+wBtwfInVK8M4l6akwvfwAAAAIN4Lazp2QAAAAAAAAAAAEsAAAAAAAAAAAAAAAAAAAAZAX14QAAD0JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); -INSERT INTO ledgerheaders VALUES ('d4facd59e215fb88bcdb7ff869e6f53943a5d2cee9a19201e51f34862acba3d0', '53e51e5309f32d15500e5b607324be76ef6bb31f42a541d16634681bcb698a22', 'cb6ba90cc364fe859c8f68570dc66788fc96472a3562f2c1febd1a1f792fc2f0', 3, 1559579642, 'AAAAC1PlHlMJ8y0VUA5bYHMkvnbva7MfQqVB0WY0aBvLaYoiaH4bAXKCIxwtnSZyhkkleQogdRpNb77USlj0ug+K770AAAAAXPVL+gAAAAAAAAAAFMKJva6QmOlDLtejYbhpYI7SUKOfeJbIdkqj9wO1AtrLa6kMw2T+hZyPaFcNxmeI/JZHKjVi8sH+vRofeS/C8AAAAAMN4Lazp2QAAAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAZAX14QAAD0JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); +INSERT INTO ledgerheaders VALUES ('9b7c8bfa1a9c5311b826007f90fb756ac043ed1422a3c292088c231a6206e660', '63d98f536ee68d1b27b5b89f23af5311b7569a24faf1403ad0b52b633b07be99', '8eb63d15a9e8c24469fc0382b02678bb9ea79abbfd04861fc693cc840e6ee71e', 2, 1572527985, 'AAAAAGPZj1Nu5o0bJ7W4nyOvUxG3Vpok+vFAOtC1K2M7B76ZlmEdOpVCM5HLr9FNj55qa6w2HKMtqTPFLvG8yPU/aAoAAAAAXbrfcQAAAAAAAAAARUAVxJm1lDMwwqujKcyQzs97F/AETiCgQPrw63wqaPGOtj0VqejCRGn8A4KwJni7nqeau/0Ehh/Gk8yEDm7nHgAAAAIN4Lazp2QAAAAAAAAAAAEsAAAAAAAAAAAAAAAAAAAAZAX14QAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); +INSERT INTO ledgerheaders VALUES ('55a91b5668c4ea95bc9f0f044abf2c30c386add87730ebe564bd55d09a6df71f', '9b7c8bfa1a9c5311b826007f90fb756ac043ed1422a3c292088c231a6206e660', 'ff7fc3046e8222730e02040cb5ead9ad58615b1c7b9ac04ee15fc204dc5cd78a', 3, 1572527986, 'AAAADJt8i/oanFMRuCYAf5D7dWrAQ+0UIqPCkgiMIxpiBuZgSP1aMoPYp3qCSCfZ1BjWWxnystfryrQnN5fe8YZZ1xcAAAAAXbrfcgAAAAIAAAAIAAAAAQAAAAwAAAAIAAAAAwAPQkAAAAAAFMKJva6QmOlDLtejYbhpYI7SUKOfeJbIdkqj9wO1Atr/f8MEboIicw4CBAy16tmtWGFbHHuawE7hX8IE3FzXigAAAAMN4Lazp2QAAAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAZAX14QAAD0JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); -- @@ -382,15 +382,15 @@ INSERT INTO ledgerheaders VALUES ('d4facd59e215fb88bcdb7ff869e6f53943a5d2cee9a19 -- Data for Name: scphistory; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO scphistory VALUES ('GCZ5EOQHT2LN7VPAJVW7SXZBJ2VZBKUVT4PZBAMUYHV3PKPZMJM7HOGD', 2, 'AAAAALPSOgeelt/V4E1t+V8hTquQqpWfH5CBlMHrt6n5YlnzAAAAAAAAAAIAAAACAAAAAQAAAEiWYR06lUIzkcuv0U2PnmprrDYcoy2pM8Uu8bzI9T9oCgAAAABc9Uv5AAAAAgAAAAgAAAABAAAACwAAAAgAAAADAA9CQAAAAAAAAAABzZaFcNCgGFd+DaUmonAp695KDV6PJip6Xb8t0WgtwmUAAABAhiMudKiUptBfIm77gkjc3BZbkq9Kfm3K41g9f2GCI7TDJUTRsAcqeN7e3Vh3KHqMQi72h1nc4TTyDFGSCvJtAw=='); -INSERT INTO scphistory VALUES ('GCZ5EOQHT2LN7VPAJVW7SXZBJ2VZBKUVT4PZBAMUYHV3PKPZMJM7HOGD', 3, 'AAAAALPSOgeelt/V4E1t+V8hTquQqpWfH5CBlMHrt6n5YlnzAAAAAAAAAAMAAAACAAAAAQAAADBofhsBcoIjHC2dJnKGSSV5CiB1Gk1vvtRKWPS6D4rvvQAAAABc9Uv6AAAAAAAAAAAAAAABzZaFcNCgGFd+DaUmonAp695KDV6PJip6Xb8t0WgtwmUAAABAuxrqQkWmRsMaN9cY3mLEEEdy3cxiRj+qta9QmLeNhstvfAJBn6cdjHSsly64/21V8GT2r6xnZPEyewv2R2B/AA=='); +INSERT INTO scphistory VALUES ('GD5K7UYXGXMVDRXPGXAFF6B4PPBX5NYPFM5SNKSOE7LQHZLXM2QDXKNF', 2, 'AAAAAPqv0xc12VHG7zXAUvg8e8N+tw8rOyaqTifXA+V3ZqA7AAAAAAAAAAIAAAACAAAAAQAAADCWYR06lUIzkcuv0U2PnmprrDYcoy2pM8Uu8bzI9T9oCgAAAABdut9xAAAAAAAAAAAAAAAB9E/mAofkecxFf+H5XKAHyLaswFqKwQizQCxIg5U5hJIAAABAW6KjWavxKHPNJZxXw2ZZ24MecXl/Lj3lmrpT1e/38eo7uraymRdNkrfPGVVdOkcQGOzoUrAQ/QuGad35MXxyAQ=='); +INSERT INTO scphistory VALUES ('GD5K7UYXGXMVDRXPGXAFF6B4PPBX5NYPFM5SNKSOE7LQHZLXM2QDXKNF', 3, 'AAAAAPqv0xc12VHG7zXAUvg8e8N+tw8rOyaqTifXA+V3ZqA7AAAAAAAAAAMAAAACAAAAAQAAAEhI/Voyg9ineoJIJ9nUGNZbGfKy1+vKtCc3l97xhlnXFwAAAABdut9yAAAAAgAAAAgAAAABAAAADAAAAAgAAAADAA9CQAAAAAAAAAAB9E/mAofkecxFf+H5XKAHyLaswFqKwQizQCxIg5U5hJIAAABACreeS0hRIfwpQAAiLu/7s/rrlSlXvSnReNsPXLg3NDjAybBYBlJDr4MjNbFsVO8nppi7v5kh/3k6SSfbZednBA=='); -- -- Data for Name: scpquorums; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO scpquorums VALUES ('cd968570d0a018577e0da526a27029ebde4a0d5e8f262a7a5dbf2dd1682dc265', 3, 'AAAAAQAAAAEAAAAAs9I6B56W39XgTW35XyFOq5CqlZ8fkIGUweu3qfliWfMAAAAA'); +INSERT INTO scpquorums VALUES ('f44fe60287e479cc457fe1f95ca007c8b6acc05a8ac108b3402c488395398492', 3, 'AAAAAQAAAAEAAAAA+q/TFzXZUcbvNcBS+Dx7w363Dys7JqpOJ9cD5XdmoDsAAAAA'); -- @@ -400,8 +400,9 @@ INSERT INTO scpquorums VALUES ('cd968570d0a018577e0da526a27029ebde4a0d5e8f262a7a INSERT INTO storestate VALUES ('databaseschema ', '10'); INSERT INTO storestate VALUES ('networkpassphrase ', 'Test SDF Network ; September 2015'); INSERT INTO storestate VALUES ('forcescponnextlaunch ', 'false'); +INSERT INTO storestate VALUES ('lastscpdata2 ', 'AAAAAgAAAAD6r9MXNdlRxu81wFL4PHvDfrcPKzsmqk4n1wPld2agOwAAAAAAAAACAAAAA/RP5gKH5HnMRX/h+VygB8i2rMBaisEIs0AsSIOVOYSSAAAAAQAAADCWYR06lUIzkcuv0U2PnmprrDYcoy2pM8Uu8bzI9T9oCgAAAABdut9xAAAAAAAAAAAAAAABAAAAMJZhHTqVQjORy6/RTY+eamusNhyjLakzxS7xvMj1P2gKAAAAAF2633EAAAAAAAAAAAAAAEDlawnyL9qwGWLmFgUepzwtU7XhODteYOsjTarw6ueobwb+nD5qWj1z8sigsrGwafHJzM9qpr+JYO/BdnRES54OAAAAAPqv0xc12VHG7zXAUvg8e8N+tw8rOyaqTifXA+V3ZqA7AAAAAAAAAAIAAAACAAAAAQAAADCWYR06lUIzkcuv0U2PnmprrDYcoy2pM8Uu8bzI9T9oCgAAAABdut9xAAAAAAAAAAAAAAAB9E/mAofkecxFf+H5XKAHyLaswFqKwQizQCxIg5U5hJIAAABAW6KjWavxKHPNJZxXw2ZZ24MecXl/Lj3lmrpT1e/38eo7uraymRdNkrfPGVVdOkcQGOzoUrAQ/QuGad35MXxyAQAAAAFj2Y9TbuaNGye1uJ8jr1MRt1aaJPrxQDrQtStjOwe+mQAAAAMAAAAAYvwdC9CRsrYcDdZWNGsqaNfTR8bywsjubQRHAlb8BfcAAABkAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msoAAAAAAAAAAAFW/AX3AAAAQIPOq+RAFCg0AmJ89FcOguG+3JxPeUU8JDnWCR2wUdoE1bDTlL9WFbReCSvQIE8Tg1oVXYqZyzdnAuaJvhNGswsAAAAAYvwdC9CRsrYcDdZWNGsqaNfTR8bywsjubQRHAlb8BfcAAABkAAAAAAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA7msoAAAAAAAAAAAFW/AX3AAAAQMm6XW0sYsXhXHC3R0MJUR/q1vmXhvIysaAKn6VVkybFy3niI1/abG2PHox3lkngTgOVyx/joQCEEZJQ+yfx9gMAAAAAYvwdC9CRsrYcDdZWNGsqaNfTR8bywsjubQRHAlb8BfcAAABkAAAAAAAAAAIAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAA7YL8A7jlgEPe0dUU7VHcDQx6Q/wlHqc3UD15aJ3Ii1QAAAAA7msoAAAAAAAAAAAFW/AX3AAAAQBIRmJlt4XAKysoGcoi6z/TlW0kMGuCiy6EtD9TpdSPi3BbKGztS1LgIx7E4zompx+okrXRaUGSTCfiDw8h+MgYAAAABAAAAAQAAAAEAAAAA+q/TFzXZUcbvNcBS+Dx7w363Dys7JqpOJ9cD5XdmoDsAAAAA'); INSERT INTO storestate VALUES ('ledgerupgrades ', '{ - "time": 0, + "time": 1572527985, "version": { "has": false }, @@ -415,14 +416,14 @@ INSERT INTO storestate VALUES ('ledgerupgrades ', '{ "has": false } }'); -INSERT INTO storestate VALUES ('lastclosedledger ', 'd4facd59e215fb88bcdb7ff869e6f53943a5d2cee9a19201e51f34862acba3d0'); +INSERT INTO storestate VALUES ('lastclosedledger ', '55a91b5668c4ea95bc9f0f044abf2c30c386add87730ebe564bd55d09a6df71f'); INSERT INTO storestate VALUES ('historyarchivestate ', '{ "version": 1, - "server": "v11.1.0", + "server": "v12.0.0rc2", "currentLedger": 3, "currentBuckets": [ { - "curr": "774f1ace2de37f74a90ebb4823410b952e17716063fe5ef5ae9f798372b6c04f", + "curr": "f6daafe6467d72aa01beae7a04385891fefb81fc5c8d11aa706aee80832c82d1", "next": { "state": 0 }, @@ -501,7 +502,7 @@ INSERT INTO storestate VALUES ('historyarchivestate ', '{ } ] }'); -INSERT INTO storestate VALUES ('lastscpdata ', 'AAAAAgAAAACz0joHnpbf1eBNbflfIU6rkKqVnx+QgZTB67ep+WJZ8wAAAAAAAAADAAAAA82WhXDQoBhXfg2lJqJwKeveSg1ejyYqel2/LdFoLcJlAAAAAQAAAJhofhsBcoIjHC2dJnKGSSV5CiB1Gk1vvtRKWPS6D4rvvQAAAABc9Uv6AAAAAAAAAAEAAAAAs9I6B56W39XgTW35XyFOq5CqlZ8fkIGUweu3qfliWfMAAABAKBicJcTiKsrcN/TjNfz9R49c9c35tkMWtaq/5pPseKrH/IW0RVhH2KDtx3jZ4RXpKSX7upVkKC0TYiyJibJSCgAAAAEAAACYaH4bAXKCIxwtnSZyhkkleQogdRpNb77USlj0ug+K770AAAAAXPVL+gAAAAAAAAABAAAAALPSOgeelt/V4E1t+V8hTquQqpWfH5CBlMHrt6n5YlnzAAAAQCgYnCXE4irK3Df04zX8/UePXPXN+bZDFrWqv+aT7Hiqx/yFtEVYR9ig7cd42eEV6Skl+7qVZCgtE2IsiYmyUgoAAABAdUy5sWuI5u5bR7MfNt6RzTrLLql1H+KVFzZnpTizja82J6eUsy7caafyHoc9r07EAxf9prDuuf50bJQ8Hj2fCgAAAACz0joHnpbf1eBNbflfIU6rkKqVnx+QgZTB67ep+WJZ8wAAAAAAAAADAAAAAgAAAAEAAAAwaH4bAXKCIxwtnSZyhkkleQogdRpNb77USlj0ug+K770AAAAAXPVL+gAAAAAAAAAAAAAAAc2WhXDQoBhXfg2lJqJwKeveSg1ejyYqel2/LdFoLcJlAAAAQLsa6kJFpkbDGjfXGN5ixBBHct3MYkY/qrWvUJi3jYbLb3wCQZ+nHYx0rJcuuP9tVfBk9q+sZ2TxMnsL9kdgfwAAAAABU+UeUwnzLRVQDltgcyS+du9rsx9CpUHRZjRoG8tpiiIAAAABAAAAAK6jei3jmoI8TGlD/egc37PXtHKKzWV8wViZBaCu5L5MAAAAZAAAAAIAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAAAL68IAAAAAAAAAAAa7kvkwAAABA9Pu9pjykcRS60lqOLqN8FHz244QP8baYNeTTJZIlr3SbRC13qEr9uP4ORDgyCB/gcug2GKrDMuK0ST3QOaKUBwAAAAEAAAABAAAAAQAAAACz0joHnpbf1eBNbflfIU6rkKqVnx+QgZTB67ep+WJZ8wAAAAA='); +INSERT INTO storestate VALUES ('lastscpdata3 ', 'AAAAAgAAAAD6r9MXNdlRxu81wFL4PHvDfrcPKzsmqk4n1wPld2agOwAAAAAAAAADAAAAA/RP5gKH5HnMRX/h+VygB8i2rMBaisEIs0AsSIOVOYSSAAAAAQAAAEhI/Voyg9ineoJIJ9nUGNZbGfKy1+vKtCc3l97xhlnXFwAAAABdut9yAAAAAgAAAAgAAAABAAAADAAAAAgAAAADAA9CQAAAAAAAAAABAAAASEj9WjKD2Kd6gkgn2dQY1lsZ8rLX68q0JzeX3vGGWdcXAAAAAF2633IAAAACAAAACAAAAAEAAAAMAAAACAAAAAMAD0JAAAAAAAAAAECI3qi7S0TN5ajJ3xMDI+Vy/DFSZvdpmIgqXNB/ggKFNQ5Y78GWkn1llttH67DASUMTc1zwU3S+dcRRWmg3tH8DAAAAAPqv0xc12VHG7zXAUvg8e8N+tw8rOyaqTifXA+V3ZqA7AAAAAAAAAAMAAAACAAAAAQAAAEhI/Voyg9ineoJIJ9nUGNZbGfKy1+vKtCc3l97xhlnXFwAAAABdut9yAAAAAgAAAAgAAAABAAAADAAAAAgAAAADAA9CQAAAAAAAAAAB9E/mAofkecxFf+H5XKAHyLaswFqKwQizQCxIg5U5hJIAAABACreeS0hRIfwpQAAiLu/7s/rrlSlXvSnReNsPXLg3NDjAybBYBlJDr4MjNbFsVO8nppi7v5kh/3k6SSfbZednBAAAAAGbfIv6GpxTEbgmAH+Q+3VqwEPtFCKjwpIIjCMaYgbmYAAAAAEAAAAArqN6LeOagjxMaUP96Bzfs9e0corNZXzBWJkFoK7kvkwAAABkAAAAAgAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAAAAAAAAvrwgAAAAAAAAAABruS+TAAAAED0+72mPKRxFLrSWo4uo3wUfPbjhA/xtpg15NMlkiWvdJtELXeoSv24/g5EODIIH+By6DYYqsMy4rRJPdA5opQHAAAAAQAAAAEAAAABAAAAAPqv0xc12VHG7zXAUvg8e8N+tw8rOyaqTifXA+V3ZqA7AAAAAA=='); -- @@ -517,7 +518,7 @@ INSERT INTO storestate VALUES ('lastscpdata ', 'AAAAAgAAAACz INSERT INTO txfeehistory VALUES ('2374e99349b9ef7dba9a5db3339b78fda8f34777b1af33ba468ad5c0df946d4d', 2, 1, 'AAAAAgAAAAMAAAABAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/+cAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); INSERT INTO txfeehistory VALUES ('164a5064eba64f2cdbadb856bf3448485fc626247ada3ed39cddf0f6902133b6', 2, 2, 'AAAAAgAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/+cAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/84AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); INSERT INTO txfeehistory VALUES ('2b2e82dbabb024b27a0c3140ca71d8ac9bc71831f9f5a3bd69eca3d88fb0ec5c', 2, 3, 'AAAAAgAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/84AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/7UAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); -INSERT INTO txfeehistory VALUES ('cebb875a00ff6e1383aef0fd251a76f22c1f9ab2a2dffcb077855736ade2659a', 3, 1, 'AAAAAgAAAAMAAAACAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msmcAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); +INSERT INTO txfeehistory VALUES ('cebb875a00ff6e1383aef0fd251a76f22c1f9ab2a2dffcb077855736ade2659a', 3, 1, 'AAAAAgAAAAMAAAACAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msmcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); -- @@ -527,15 +528,15 @@ INSERT INTO txfeehistory VALUES ('cebb875a00ff6e1383aef0fd251a76f22c1f9ab2a2dffc INSERT INTO txhistory VALUES ('2374e99349b9ef7dba9a5db3339b78fda8f34777b1af33ba468ad5c0df946d4d', 2, 1, 'AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAArqN6LeOagjxMaUP96Bzfs9e0corNZXzBWJkFoK7kvkwAAAAAO5rKAAAAAAAAAAABVvwF9wAAAECDzqvkQBQoNAJifPRXDoLhvtycT3lFPCQ51gkdsFHaBNWw05S/VhW0Xgkr0CBPE4NaFV2Kmcs3ZwLmib4TRrML', 'I3Tpk0m57326ml2zM5t4/ajzR3exrzO6RorVwN+UbU0AAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAA==', 'AAAAAQAAAAAAAAABAAAAAwAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/7UAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrNryTTUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); INSERT INTO txhistory VALUES ('164a5064eba64f2cdbadb856bf3448485fc626247ada3ed39cddf0f6902133b6', 2, 2, 'AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAO2C/AO45YBD3tHVFO1R3A0MekP8JR6nN1A9eWidyItUAAAAAO5rKAAAAAAAAAAABVvwF9wAAAEASEZiZbeFwCsrKBnKIus/05VtJDBrgosuhLQ/U6XUj4twWyhs7UtS4CMexOM6JqcfqJK10WlBkkwn4g8PIfjIG', 'FkpQZOumTyzbrbhWvzRISF/GJiR62j7TnN3w9pAhM7YAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAA==', 'AAAAAQAAAAAAAAABAAAAAwAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrNryTTUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrMwLmrUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAA7YL8A7jlgEPe0dUU7VHcDQx6Q/wlHqc3UD15aJ3Ii1QAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); INSERT INTO txhistory VALUES ('2b2e82dbabb024b27a0c3140ca71d8ac9bc71831f9f5a3bd69eca3d88fb0ec5c', 2, 3, 'AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAADAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAO5rKAAAAAAAAAAABVvwF9wAAAEDJul1tLGLF4Vxwt0dDCVEf6tb5l4byMrGgCp+lVZMmxct54iNf2mxtjx6Md5ZJ4E4Dlcsf46EAhBGSUPsn8fYD', 'Ky6C26uwJLJ6DDFAynHYrJvHGDH59aO9aeyj2I+w7FwAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAA==', 'AAAAAQAAAAAAAAABAAAAAwAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrMwLmrUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrL0k6DUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); -INSERT INTO txhistory VALUES ('cebb875a00ff6e1383aef0fd251a76f22c1f9ab2a2dffcb077855736ade2659a', 3, 1, 'AAAAAK6jei3jmoI8TGlD/egc37PXtHKKzWV8wViZBaCu5L5MAAAAZAAAAAIAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAAAL68IAAAAAAAAAAAa7kvkwAAABA9Pu9pjykcRS60lqOLqN8FHz244QP8baYNeTTJZIlr3SbRC13qEr9uP4ORDgyCB/gcug2GKrDMuK0ST3QOaKUBw==', 'zruHWgD/bhODrvD9JRp28iwfmrKi3/ywd4VXNq3iZZoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAA==', 'AAAAAQAAAAIAAAADAAAAAwAAAAAAAAAArqN6LeOagjxMaUP96Bzfs9e0corNZXzBWJkFoK7kvkwAAAAAO5rJnAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAAAAAAArqN6LeOagjxMaUP96Bzfs9e0corNZXzBWJkFoK7kvkwAAAAAO5rJnAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMAAAACAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA+lbqAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msmcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA4n9kcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); +INSERT INTO txhistory VALUES ('cebb875a00ff6e1383aef0fd251a76f22c1f9ab2a2dffcb077855736ade2659a', 3, 1, 'AAAAAK6jei3jmoI8TGlD/egc37PXtHKKzWV8wViZBaCu5L5MAAAAZAAAAAIAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAAAL68IAAAAAAAAAAAa7kvkwAAABA9Pu9pjykcRS60lqOLqN8FHz244QP8baYNeTTJZIlr3SbRC13qEr9uP4ORDgyCB/gcug2GKrDMuK0ST3QOaKUBw==', 'zruHWgD/bhODrvD9JRp28iwfmrKi3/ywd4VXNq3iZZoAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAA==', 'AAAAAQAAAAAAAAABAAAABAAAAAMAAAACAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA+lbqAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msmcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA4n9kcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='); -- -- Data for Name: upgradehistory; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO upgradehistory VALUES (2, 1, 'AAAAAQAAAAs=', 'AAAAAA=='); -INSERT INTO upgradehistory VALUES (2, 2, 'AAAAAwAPQkA=', 'AAAAAA=='); +INSERT INTO upgradehistory VALUES (3, 1, 'AAAAAQAAAAw=', 'AAAAAA=='); +INSERT INTO upgradehistory VALUES (3, 2, 'AAAAAwAPQkA=', 'AAAAAA=='); -- diff --git a/services/horizon/internal/test/scenarios/base-horizon.sql b/services/horizon/internal/test/scenarios/base-horizon.sql index c83198092e..680c132f68 100644 --- a/services/horizon/internal/test/scenarios/base-horizon.sql +++ b/services/horizon/internal/test/scenarios/base-horizon.sql @@ -19,7 +19,15 @@ ALTER TABLE IF EXISTS ONLY public.history_trades DROP CONSTRAINT IF EXISTS histo ALTER TABLE IF EXISTS ONLY public.history_trades DROP CONSTRAINT IF EXISTS history_trades_base_asset_id_fkey; ALTER TABLE IF EXISTS ONLY public.history_trades DROP CONSTRAINT IF EXISTS history_trades_base_account_id_fkey; ALTER TABLE IF EXISTS ONLY public.asset_stats DROP CONSTRAINT IF EXISTS asset_stats_id_fkey; +DROP INDEX IF EXISTS public.trust_lines_by_type_code_issuer; +DROP INDEX IF EXISTS public.trust_lines_by_issuer; +DROP INDEX IF EXISTS public.trust_lines_by_account_id; DROP INDEX IF EXISTS public.trade_effects_by_order_book; +DROP INDEX IF EXISTS public.signers_by_account; +DROP INDEX IF EXISTS public.offers_by_selling_asset; +DROP INDEX IF EXISTS public.offers_by_seller; +DROP INDEX IF EXISTS public.offers_by_last_modified_ledger; +DROP INDEX IF EXISTS public.offers_by_buying_asset; DROP INDEX IF EXISTS public.index_history_transactions_on_id; DROP INDEX IF EXISTS public.index_history_operations_on_type; DROP INDEX IF EXISTS public.index_history_operations_on_transaction_id; @@ -50,26 +58,35 @@ DROP INDEX IF EXISTS public.hist_tx_p_id; DROP INDEX IF EXISTS public.hist_op_p_id; DROP INDEX IF EXISTS public.hist_e_id; DROP INDEX IF EXISTS public.hist_e_by_order; +DROP INDEX IF EXISTS public.exp_asset_stats_by_issuer; +DROP INDEX IF EXISTS public.exp_asset_stats_by_code; DROP INDEX IF EXISTS public.by_ledger; DROP INDEX IF EXISTS public.by_hash; DROP INDEX IF EXISTS public.by_account; DROP INDEX IF EXISTS public.asset_by_issuer; DROP INDEX IF EXISTS public.asset_by_code; -DROP INDEX IF EXISTS public.offers_by_seller; -DROP INDEX IF EXISTS public.offers_by_last_modified_ledger; -DROP INDEX IF EXISTS public.offers_by_selling_asset; -DROP INDEX IF EXISTS public.offers_by_buying_asset; +DROP INDEX IF EXISTS public.accounts_inflation_destination; +DROP INDEX IF EXISTS public.accounts_home_domain; +DROP INDEX IF EXISTS public.accounts_data_account_id_name; +ALTER TABLE IF EXISTS ONLY public.trust_lines DROP CONSTRAINT IF EXISTS trust_lines_pkey; +ALTER TABLE IF EXISTS ONLY public.offers DROP CONSTRAINT IF EXISTS offers_pkey; +ALTER TABLE IF EXISTS ONLY public.key_value_store DROP CONSTRAINT IF EXISTS key_value_store_pkey; ALTER TABLE IF EXISTS ONLY public.history_transaction_participants DROP CONSTRAINT IF EXISTS history_transaction_participants_pkey; ALTER TABLE IF EXISTS ONLY public.history_operation_participants DROP CONSTRAINT IF EXISTS history_operation_participants_pkey; ALTER TABLE IF EXISTS ONLY public.history_assets DROP CONSTRAINT IF EXISTS history_assets_pkey; ALTER TABLE IF EXISTS ONLY public.history_assets DROP CONSTRAINT IF EXISTS history_assets_asset_code_asset_type_asset_issuer_key; ALTER TABLE IF EXISTS ONLY public.gorp_migrations DROP CONSTRAINT IF EXISTS gorp_migrations_pkey; -ALTER TABLE IF EXISTS ONLY public.accounts_signers DROP CONSTRAINT IF EXISTS accounts_signers_pkey; +ALTER TABLE IF EXISTS ONLY public.exp_asset_stats DROP CONSTRAINT IF EXISTS exp_asset_stats_pkey; ALTER TABLE IF EXISTS ONLY public.asset_stats DROP CONSTRAINT IF EXISTS asset_stats_pkey; -ALTER TABLE IF EXISTS ONLY public.offers DROP CONSTRAINT IF EXISTS offers_pkey; +ALTER TABLE IF EXISTS ONLY public.accounts_signers DROP CONSTRAINT IF EXISTS accounts_signers_pkey; +ALTER TABLE IF EXISTS ONLY public.accounts DROP CONSTRAINT IF EXISTS accounts_pkey; +ALTER TABLE IF EXISTS ONLY public.accounts_data DROP CONSTRAINT IF EXISTS accounts_data_pkey; ALTER TABLE IF EXISTS public.history_transaction_participants ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.history_operation_participants ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.history_assets ALTER COLUMN id DROP DEFAULT; +DROP TABLE IF EXISTS public.trust_lines; +DROP TABLE IF EXISTS public.offers; +DROP TABLE IF EXISTS public.key_value_store; DROP TABLE IF EXISTS public.history_transactions; DROP SEQUENCE IF EXISTS public.history_transaction_participants_id_seq; DROP TABLE IF EXISTS public.history_transaction_participants; @@ -84,9 +101,11 @@ DROP TABLE IF EXISTS public.history_assets; DROP TABLE IF EXISTS public.history_accounts; DROP SEQUENCE IF EXISTS public.history_accounts_id_seq; DROP TABLE IF EXISTS public.gorp_migrations; +DROP TABLE IF EXISTS public.exp_asset_stats; DROP TABLE IF EXISTS public.asset_stats; DROP TABLE IF EXISTS public.accounts_signers; -DROP TABLE IF EXISTS public.offers; +DROP TABLE IF EXISTS public.accounts_data; +DROP TABLE IF EXISTS public.accounts; DROP AGGREGATE IF EXISTS public.min_price(numeric[]); DROP AGGREGATE IF EXISTS public.max_price(numeric[]); DROP AGGREGATE IF EXISTS public.last(anyelement); @@ -209,12 +228,47 @@ SET default_tablespace = ''; SET default_with_oids = false; +-- +-- Name: accounts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE accounts ( + account_id character varying(56) NOT NULL, + balance bigint NOT NULL, + buying_liabilities bigint NOT NULL, + selling_liabilities bigint NOT NULL, + sequence_number bigint NOT NULL, + num_subentries integer NOT NULL, + inflation_destination character varying(56) NOT NULL, + flags integer NOT NULL, + home_domain character varying(32) NOT NULL, + master_weight smallint NOT NULL, + threshold_low smallint NOT NULL, + threshold_medium smallint NOT NULL, + threshold_high smallint NOT NULL, + last_modified_ledger integer NOT NULL +); + + +-- +-- Name: accounts_data; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE accounts_data ( + ledger_key character varying(150) NOT NULL, + account_id character varying(56) NOT NULL, + name character varying(64) NOT NULL, + value character varying(90) NOT NULL, + last_modified_ledger integer NOT NULL +); + + -- -- Name: accounts_signers; Type: TABLE; Schema: public; Owner: - -- -CREATE TABLE public.accounts_signers ( - account character varying(64) NOT NULL, +CREATE TABLE accounts_signers ( + account_id character varying(64) NOT NULL, signer character varying(64) NOT NULL, weight integer NOT NULL ); @@ -233,6 +287,19 @@ CREATE TABLE asset_stats ( ); +-- +-- Name: exp_asset_stats; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE exp_asset_stats ( + asset_type integer NOT NULL, + asset_code character varying(12) NOT NULL, + asset_issuer character varying(56) NOT NULL, + amount text NOT NULL, + num_accounts integer NOT NULL +); + + -- -- Name: gorp_migrations; Type: TABLE; Schema: public; Owner: - -- @@ -400,9 +467,9 @@ CREATE TABLE history_trades ( price_d bigint, base_offer_id bigint, counter_offer_id bigint, - CONSTRAINT history_trades_base_amount_check CHECK ((base_amount > 0)), + CONSTRAINT history_trades_base_amount_check CHECK ((base_amount >= 0)), CONSTRAINT history_trades_check CHECK ((base_asset_id < counter_asset_id)), - CONSTRAINT history_trades_counter_amount_check CHECK ((counter_amount > 0)) + CONSTRAINT history_trades_counter_amount_check CHECK ((counter_amount >= 0)) ); @@ -416,22 +483,6 @@ CREATE TABLE history_transaction_participants ( history_account_id bigint NOT NULL ); --- --- Name: offers; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE offers ( - sellerid character varying(56) NOT NULL, - offerid bigint, - sellingasset text NOT NULL, - buyingasset text NOT NULL, - amount bigint NOT NULL, - pricen integer NOT NULL, - priced integer NOT NULL, - price double precision NOT NULL, - flags integer NOT NULL, - last_modified_ledger integer NOT NULL -); -- -- Name: history_transaction_participants_id_seq; Type: SEQUENCE; Schema: public; Owner: - @@ -480,6 +531,53 @@ CREATE TABLE history_transactions ( ); +-- +-- Name: key_value_store; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE key_value_store ( + key character varying(255) NOT NULL, + value character varying(255) NOT NULL +); + + +-- +-- Name: offers; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE offers ( + seller_id character varying(56) NOT NULL, + offer_id bigint NOT NULL, + selling_asset text NOT NULL, + buying_asset text NOT NULL, + amount bigint NOT NULL, + pricen integer NOT NULL, + priced integer NOT NULL, + price double precision NOT NULL, + flags integer NOT NULL, + last_modified_ledger integer NOT NULL +); + + +-- +-- Name: trust_lines; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE trust_lines ( + ledger_key character varying(150) NOT NULL, + account_id character varying(56) NOT NULL, + asset_type integer NOT NULL, + asset_issuer character varying(56) NOT NULL, + asset_code character varying(12) NOT NULL, + balance bigint NOT NULL, + trust_line_limit bigint NOT NULL, + buying_liabilities bigint NOT NULL, + selling_liabilities bigint NOT NULL, + flags integer NOT NULL, + last_modified_ledger integer NOT NULL +); + + -- -- Name: history_assets id; Type: DEFAULT; Schema: public; Owner: - -- @@ -501,44 +599,74 @@ ALTER TABLE ONLY history_operation_participants ALTER COLUMN id SET DEFAULT next ALTER TABLE ONLY history_transaction_participants ALTER COLUMN id SET DEFAULT nextval('history_transaction_participants_id_seq'::regclass); +-- +-- Data for Name: accounts; Type: TABLE DATA; Schema: public; Owner: - +-- + + + +-- +-- Data for Name: accounts_data; Type: TABLE DATA; Schema: public; Owner: - +-- + + + +-- +-- Data for Name: accounts_signers; Type: TABLE DATA; Schema: public; Owner: - +-- + + + -- -- Data for Name: asset_stats; Type: TABLE DATA; Schema: public; Owner: - -- +-- +-- Data for Name: exp_asset_stats; Type: TABLE DATA; Schema: public; Owner: - +-- + + + -- -- Data for Name: gorp_migrations; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO gorp_migrations VALUES ('1_initial_schema.sql', '2019-06-03 18:28:47.032496+02'); -INSERT INTO gorp_migrations VALUES ('2_index_participants_by_toid.sql', '2019-06-03 18:28:47.039657+02'); -INSERT INTO gorp_migrations VALUES ('3_use_sequence_in_history_accounts.sql', '2019-06-03 18:28:47.044048+02'); -INSERT INTO gorp_migrations VALUES ('4_add_protocol_version.sql', '2019-06-03 18:28:47.054532+02'); -INSERT INTO gorp_migrations VALUES ('5_create_trades_table.sql', '2019-06-03 18:28:47.063028+02'); -INSERT INTO gorp_migrations VALUES ('6_create_assets_table.sql', '2019-06-03 18:28:47.068415+02'); -INSERT INTO gorp_migrations VALUES ('7_modify_trades_table.sql', '2019-06-03 18:28:47.081625+02'); -INSERT INTO gorp_migrations VALUES ('8_create_asset_stats_table.sql', '2019-06-03 18:28:47.087463+02'); -INSERT INTO gorp_migrations VALUES ('8_add_aggregators.sql', '2019-06-03 18:28:47.090109+02'); -INSERT INTO gorp_migrations VALUES ('9_add_header_xdr.sql', '2019-06-03 18:28:47.092718+02'); -INSERT INTO gorp_migrations VALUES ('10_add_trades_price.sql', '2019-06-03 18:28:47.095973+02'); -INSERT INTO gorp_migrations VALUES ('11_add_trades_account_index.sql', '2019-06-03 18:28:47.099698+02'); -INSERT INTO gorp_migrations VALUES ('12_asset_stats_amount_string.sql', '2019-06-03 18:28:47.107549+02'); -INSERT INTO gorp_migrations VALUES ('13_trade_offer_ids.sql', '2019-06-03 18:28:47.112768+02'); -INSERT INTO gorp_migrations VALUES ('14_fix_asset_toml_field.sql', '2019-06-03 18:28:47.115116+02'); -INSERT INTO gorp_migrations VALUES ('15_ledger_failed_txs.sql', '2019-06-03 18:28:47.116796+02'); -INSERT INTO gorp_migrations VALUES ('16_ingest_failed_transactions.sql', '2019-06-03 18:28:47.117989+02'); -INSERT INTO gorp_migrations VALUES ('17_transaction_fee_paid.sql', '2019-06-03 18:28:47.120034+02'); -INSERT INTO gorp_migrations VALUES ('18_account_for_signers.sql', '2019-06-03 18:28:47.120034+02'); -INSERT INTO gorp_migrations VALUES ('19_offers.sql', '2019-06-03 18:28:47.120034+02'); +INSERT INTO gorp_migrations VALUES ('1_initial_schema.sql', '2019-10-31 14:19:49.03833+01'); +INSERT INTO gorp_migrations VALUES ('2_index_participants_by_toid.sql', '2019-10-31 14:19:49.04267+01'); +INSERT INTO gorp_migrations VALUES ('3_use_sequence_in_history_accounts.sql', '2019-10-31 14:19:49.045926+01'); +INSERT INTO gorp_migrations VALUES ('4_add_protocol_version.sql', '2019-10-31 14:19:49.054147+01'); +INSERT INTO gorp_migrations VALUES ('5_create_trades_table.sql', '2019-10-31 14:19:49.061804+01'); +INSERT INTO gorp_migrations VALUES ('6_create_assets_table.sql', '2019-10-31 14:19:49.067093+01'); +INSERT INTO gorp_migrations VALUES ('7_modify_trades_table.sql', '2019-10-31 14:19:49.081047+01'); +INSERT INTO gorp_migrations VALUES ('8_add_aggregators.sql', '2019-10-31 14:19:49.085128+01'); +INSERT INTO gorp_migrations VALUES ('8_create_asset_stats_table.sql', '2019-10-31 14:19:49.089574+01'); +INSERT INTO gorp_migrations VALUES ('9_add_header_xdr.sql', '2019-10-31 14:19:49.092366+01'); +INSERT INTO gorp_migrations VALUES ('10_add_trades_price.sql', '2019-10-31 14:19:49.095671+01'); +INSERT INTO gorp_migrations VALUES ('11_add_trades_account_index.sql', '2019-10-31 14:19:49.099289+01'); +INSERT INTO gorp_migrations VALUES ('12_asset_stats_amount_string.sql', '2019-10-31 14:19:49.105961+01'); +INSERT INTO gorp_migrations VALUES ('13_trade_offer_ids.sql', '2019-10-31 14:19:49.111757+01'); +INSERT INTO gorp_migrations VALUES ('14_fix_asset_toml_field.sql', '2019-10-31 14:19:49.113736+01'); +INSERT INTO gorp_migrations VALUES ('15_ledger_failed_txs.sql', '2019-10-31 14:19:49.115578+01'); +INSERT INTO gorp_migrations VALUES ('16_ingest_failed_transactions.sql', '2019-10-31 14:19:49.116928+01'); +INSERT INTO gorp_migrations VALUES ('17_transaction_fee_paid.sql', '2019-10-31 14:19:49.118562+01'); +INSERT INTO gorp_migrations VALUES ('18_account_for_signers.sql', '2019-10-31 14:19:49.123835+01'); +INSERT INTO gorp_migrations VALUES ('19_offers.sql', '2019-10-31 14:19:49.133107+01'); +INSERT INTO gorp_migrations VALUES ('20_account_for_signer_index.sql', '2019-10-31 14:19:49.135499+01'); +INSERT INTO gorp_migrations VALUES ('21_trades_remove_zero_amount_constraints.sql', '2019-10-31 14:19:49.138031+01'); +INSERT INTO gorp_migrations VALUES ('22_trust_lines.sql', '2019-10-31 14:19:49.144708+01'); +INSERT INTO gorp_migrations VALUES ('23_exp_asset_stats.sql', '2019-10-31 14:19:49.15222+01'); +INSERT INTO gorp_migrations VALUES ('24_accounts.sql', '2019-10-31 14:19:49.160844+01'); +INSERT INTO gorp_migrations VALUES ('25_expingest_rename_columns.sql', '2019-10-31 14:19:49.163717+01'); -- -- Data for Name: history_accounts; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO history_accounts VALUES (1, 'GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON'); -INSERT INTO history_accounts VALUES (2, 'GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU'); +INSERT INTO history_accounts VALUES (1, 'GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU'); +INSERT INTO history_accounts VALUES (2, 'GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON'); INSERT INTO history_accounts VALUES (3, 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H'); INSERT INTO history_accounts VALUES (4, 'GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2'); @@ -567,40 +695,40 @@ SELECT pg_catalog.setval('history_assets_id_seq', 1, false); -- Data for Name: history_effects; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO history_effects VALUES (1, 12884905985, 1, 2, '{"amount": "5.0000000", "asset_type": "native"}'); -INSERT INTO history_effects VALUES (2, 12884905985, 2, 3, '{"amount": "5.0000000", "asset_type": "native"}'); -INSERT INTO history_effects VALUES (2, 8589938689, 1, 0, '{"starting_balance": "100.0000000"}'); +INSERT INTO history_effects VALUES (2, 12884905985, 1, 2, '{"amount": "5.0000000", "asset_type": "native"}'); +INSERT INTO history_effects VALUES (1, 12884905985, 2, 3, '{"amount": "5.0000000", "asset_type": "native"}'); +INSERT INTO history_effects VALUES (1, 8589938689, 1, 0, '{"starting_balance": "100.0000000"}'); INSERT INTO history_effects VALUES (3, 8589938689, 2, 3, '{"amount": "100.0000000", "asset_type": "native"}'); -INSERT INTO history_effects VALUES (2, 8589938689, 3, 10, '{"weight": 1, "public_key": "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU"}'); +INSERT INTO history_effects VALUES (1, 8589938689, 3, 10, '{"weight": 1, "public_key": "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU"}'); INSERT INTO history_effects VALUES (4, 8589942785, 1, 0, '{"starting_balance": "100.0000000"}'); INSERT INTO history_effects VALUES (3, 8589942785, 2, 3, '{"amount": "100.0000000", "asset_type": "native"}'); INSERT INTO history_effects VALUES (4, 8589942785, 3, 10, '{"weight": 1, "public_key": "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2"}'); -INSERT INTO history_effects VALUES (1, 8589946881, 1, 0, '{"starting_balance": "100.0000000"}'); +INSERT INTO history_effects VALUES (2, 8589946881, 1, 0, '{"starting_balance": "100.0000000"}'); INSERT INTO history_effects VALUES (3, 8589946881, 2, 3, '{"amount": "100.0000000", "asset_type": "native"}'); -INSERT INTO history_effects VALUES (1, 8589946881, 3, 10, '{"weight": 1, "public_key": "GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON"}'); +INSERT INTO history_effects VALUES (2, 8589946881, 3, 10, '{"weight": 1, "public_key": "GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON"}'); -- -- Data for Name: history_ledgers; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO history_ledgers VALUES (3, 'd4facd59e215fb88bcdb7ff869e6f53943a5d2cee9a19201e51f34862acba3d0', '53e51e5309f32d15500e5b607324be76ef6bb31f42a541d16634681bcb698a22', 1, 1, '2019-06-03 16:34:02', '2019-06-03 16:34:04.799911', '2019-06-03 16:34:04.799911', 12884901888, 16, 1000000000000000000, 400, 100, 100000000, 1000000, 11, 'AAAAC1PlHlMJ8y0VUA5bYHMkvnbva7MfQqVB0WY0aBvLaYoiaH4bAXKCIxwtnSZyhkkleQogdRpNb77USlj0ug+K770AAAAAXPVL+gAAAAAAAAAAFMKJva6QmOlDLtejYbhpYI7SUKOfeJbIdkqj9wO1AtrLa6kMw2T+hZyPaFcNxmeI/JZHKjVi8sH+vRofeS/C8AAAAAMN4Lazp2QAAAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAZAX14QAAD0JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 1, 0); -INSERT INTO history_ledgers VALUES (2, '53e51e5309f32d15500e5b607324be76ef6bb31f42a541d16634681bcb698a22', '63d98f536ee68d1b27b5b89f23af5311b7569a24faf1403ad0b52b633b07be99', 3, 3, '2019-06-03 16:34:01', '2019-06-03 16:34:04.816248', '2019-06-03 16:34:04.816248', 8589934592, 16, 1000000000000000000, 300, 100, 100000000, 1000000, 11, 'AAAAC2PZj1Nu5o0bJ7W4nyOvUxG3Vpok+vFAOtC1K2M7B76ZlmEdOpVCM5HLr9FNj55qa6w2HKMtqTPFLvG8yPU/aAoAAAAAXPVL+QAAAAIAAAAIAAAAAQAAAAsAAAAIAAAAAwAPQkAAAAAARUAVxJm1lDMwwqujKcyQzs97F/AETiCgQPrw63wqaPEZRkC5EHABd4DNFx5bltK0D+wBtwfInVK8M4l6akwvfwAAAAIN4Lazp2QAAAAAAAAAAAEsAAAAAAAAAAAAAAAAAAAAZAX14QAAD0JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 3, 0); -INSERT INTO history_ledgers VALUES (1, '63d98f536ee68d1b27b5b89f23af5311b7569a24faf1403ad0b52b633b07be99', NULL, 0, 0, '1970-01-01 00:00:00', '2019-06-03 16:34:04.827353', '2019-06-03 16:34:04.827354', 4294967296, 16, 1000000000000000000, 0, 100, 100000000, 100, 0, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABXKi4y/ySKB7DnD9H20xjB+s0gtswIwz1XdSWYaBJaFgAAAAEN4Lazp2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAX14QAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 0, 0); +INSERT INTO history_ledgers VALUES (3, '55a91b5668c4ea95bc9f0f044abf2c30c386add87730ebe564bd55d09a6df71f', '9b7c8bfa1a9c5311b826007f90fb756ac043ed1422a3c292088c231a6206e660', 1, 1, '2019-10-31 13:19:46', '2019-10-31 13:19:49.394864', '2019-10-31 13:19:49.394864', 12884901888, 16, 1000000000000000000, 400, 100, 100000000, 1000000, 12, 'AAAADJt8i/oanFMRuCYAf5D7dWrAQ+0UIqPCkgiMIxpiBuZgSP1aMoPYp3qCSCfZ1BjWWxnystfryrQnN5fe8YZZ1xcAAAAAXbrfcgAAAAIAAAAIAAAAAQAAAAwAAAAIAAAAAwAPQkAAAAAAFMKJva6QmOlDLtejYbhpYI7SUKOfeJbIdkqj9wO1Atr/f8MEboIicw4CBAy16tmtWGFbHHuawE7hX8IE3FzXigAAAAMN4Lazp2QAAAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAZAX14QAAD0JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 1, 0); +INSERT INTO history_ledgers VALUES (2, '9b7c8bfa1a9c5311b826007f90fb756ac043ed1422a3c292088c231a6206e660', '63d98f536ee68d1b27b5b89f23af5311b7569a24faf1403ad0b52b633b07be99', 3, 3, '2019-10-31 13:19:45', '2019-10-31 13:19:49.409603', '2019-10-31 13:19:49.409603', 8589934592, 16, 1000000000000000000, 300, 100, 100000000, 100, 0, 'AAAAAGPZj1Nu5o0bJ7W4nyOvUxG3Vpok+vFAOtC1K2M7B76ZlmEdOpVCM5HLr9FNj55qa6w2HKMtqTPFLvG8yPU/aAoAAAAAXbrfcQAAAAAAAAAARUAVxJm1lDMwwqujKcyQzs97F/AETiCgQPrw63wqaPGOtj0VqejCRGn8A4KwJni7nqeau/0Ehh/Gk8yEDm7nHgAAAAIN4Lazp2QAAAAAAAAAAAEsAAAAAAAAAAAAAAAAAAAAZAX14QAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 3, 0); +INSERT INTO history_ledgers VALUES (1, '63d98f536ee68d1b27b5b89f23af5311b7569a24faf1403ad0b52b633b07be99', NULL, 0, 0, '1970-01-01 00:00:00', '2019-10-31 13:19:49.421622', '2019-10-31 13:19:49.421622', 4294967296, 16, 1000000000000000000, 0, 100, 100000000, 100, 0, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABXKi4y/ySKB7DnD9H20xjB+s0gtswIwz1XdSWYaBJaFgAAAAEN4Lazp2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAX14QAAAABkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 0, 0); -- -- Data for Name: history_operation_participants; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO history_operation_participants VALUES (1, 12884905985, 2); -INSERT INTO history_operation_participants VALUES (2, 12884905985, 1); +INSERT INTO history_operation_participants VALUES (1, 12884905985, 1); +INSERT INTO history_operation_participants VALUES (2, 12884905985, 2); INSERT INTO history_operation_participants VALUES (3, 8589938689, 3); -INSERT INTO history_operation_participants VALUES (4, 8589938689, 2); +INSERT INTO history_operation_participants VALUES (4, 8589938689, 1); INSERT INTO history_operation_participants VALUES (5, 8589942785, 3); INSERT INTO history_operation_participants VALUES (6, 8589942785, 4); INSERT INTO history_operation_participants VALUES (7, 8589946881, 3); -INSERT INTO history_operation_participants VALUES (8, 8589946881, 1); +INSERT INTO history_operation_participants VALUES (8, 8589946881, 2); -- @@ -630,14 +758,14 @@ INSERT INTO history_operations VALUES (8589946881, 8589946880, 1, 0, '{"funder": -- Data for Name: history_transaction_participants; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO history_transaction_participants VALUES (1, 12884905984, 2); -INSERT INTO history_transaction_participants VALUES (2, 12884905984, 1); -INSERT INTO history_transaction_participants VALUES (3, 8589938688, 2); -INSERT INTO history_transaction_participants VALUES (4, 8589938688, 3); +INSERT INTO history_transaction_participants VALUES (1, 12884905984, 1); +INSERT INTO history_transaction_participants VALUES (2, 12884905984, 2); +INSERT INTO history_transaction_participants VALUES (3, 8589938688, 3); +INSERT INTO history_transaction_participants VALUES (4, 8589938688, 1); INSERT INTO history_transaction_participants VALUES (5, 8589942784, 3); INSERT INTO history_transaction_participants VALUES (6, 8589942784, 4); INSERT INTO history_transaction_participants VALUES (7, 8589946880, 3); -INSERT INTO history_transaction_participants VALUES (8, 8589946880, 1); +INSERT INTO history_transaction_participants VALUES (8, 8589946880, 2); -- @@ -651,23 +779,53 @@ SELECT pg_catalog.setval('history_transaction_participants_id_seq', 8, true); -- Data for Name: history_transactions; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO history_transactions VALUES ('cebb875a00ff6e1383aef0fd251a76f22c1f9ab2a2dffcb077855736ade2659a', 3, 1, 'GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU', 8589934593, 100, 1, '2019-06-03 16:34:04.800088', '2019-06-03 16:34:04.800088', 12884905984, 'AAAAAK6jei3jmoI8TGlD/egc37PXtHKKzWV8wViZBaCu5L5MAAAAZAAAAAIAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAAAL68IAAAAAAAAAAAa7kvkwAAABA9Pu9pjykcRS60lqOLqN8FHz244QP8baYNeTTJZIlr3SbRC13qEr9uP4ORDgyCB/gcug2GKrDMuK0ST3QOaKUBw==', 'AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=', 'AAAAAQAAAAIAAAADAAAAAwAAAAAAAAAArqN6LeOagjxMaUP96Bzfs9e0corNZXzBWJkFoK7kvkwAAAAAO5rJnAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAAAAAAArqN6LeOagjxMaUP96Bzfs9e0corNZXzBWJkFoK7kvkwAAAAAO5rJnAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMAAAACAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA+lbqAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msmcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA4n9kcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', 'AAAAAgAAAAMAAAACAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msmcAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', '{9Pu9pjykcRS60lqOLqN8FHz244QP8baYNeTTJZIlr3SbRC13qEr9uP4ORDgyCB/gcug2GKrDMuK0ST3QOaKUBw==}', 'none', NULL, NULL, true, 100); -INSERT INTO history_transactions VALUES ('2374e99349b9ef7dba9a5db3339b78fda8f34777b1af33ba468ad5c0df946d4d', 2, 1, 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', 1, 100, 1, '2019-06-03 16:34:04.816369', '2019-06-03 16:34:04.816369', 8589938688, 'AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAArqN6LeOagjxMaUP96Bzfs9e0corNZXzBWJkFoK7kvkwAAAAAO5rKAAAAAAAAAAABVvwF9wAAAECDzqvkQBQoNAJifPRXDoLhvtycT3lFPCQ51gkdsFHaBNWw05S/VhW0Xgkr0CBPE4NaFV2Kmcs3ZwLmib4TRrML', 'AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=', 'AAAAAQAAAAAAAAABAAAAAwAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/7UAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrNryTTUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', 'AAAAAgAAAAMAAAABAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/+cAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', '{g86r5EAUKDQCYnz0Vw6C4b7cnE95RTwkOdYJHbBR2gTVsNOUv1YVtF4JK9AgTxODWhVdipnLN2cC5om+E0azCw==}', 'none', NULL, NULL, true, 100); -INSERT INTO history_transactions VALUES ('164a5064eba64f2cdbadb856bf3448485fc626247ada3ed39cddf0f6902133b6', 2, 2, 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', 2, 100, 1, '2019-06-03 16:34:04.816514', '2019-06-03 16:34:04.816514', 8589942784, 'AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAO2C/AO45YBD3tHVFO1R3A0MekP8JR6nN1A9eWidyItUAAAAAO5rKAAAAAAAAAAABVvwF9wAAAEASEZiZbeFwCsrKBnKIus/05VtJDBrgosuhLQ/U6XUj4twWyhs7UtS4CMexOM6JqcfqJK10WlBkkwn4g8PIfjIG', 'AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=', 'AAAAAQAAAAAAAAABAAAAAwAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrNryTTUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrMwLmrUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAA7YL8A7jlgEPe0dUU7VHcDQx6Q/wlHqc3UD15aJ3Ii1QAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', 'AAAAAgAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/+cAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/84AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', '{EhGYmW3hcArKygZyiLrP9OVbSQwa4KLLoS0P1Ol1I+LcFsobO1LUuAjHsTjOianH6iStdFpQZJMJ+IPDyH4yBg==}', 'none', NULL, NULL, true, 100); -INSERT INTO history_transactions VALUES ('2b2e82dbabb024b27a0c3140ca71d8ac9bc71831f9f5a3bd69eca3d88fb0ec5c', 2, 3, 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', 3, 100, 1, '2019-06-03 16:34:04.816623', '2019-06-03 16:34:04.816623', 8589946880, 'AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAADAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAO5rKAAAAAAAAAAABVvwF9wAAAEDJul1tLGLF4Vxwt0dDCVEf6tb5l4byMrGgCp+lVZMmxct54iNf2mxtjx6Md5ZJ4E4Dlcsf46EAhBGSUPsn8fYD', 'AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=', 'AAAAAQAAAAAAAAABAAAAAwAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrMwLmrUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrL0k6DUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', 'AAAAAgAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/84AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/7UAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', '{ybpdbSxixeFccLdHQwlRH+rW+ZeG8jKxoAqfpVWTJsXLeeIjX9psbY8ejHeWSeBOA5XLH+OhAIQRklD7J/H2Aw==}', 'none', NULL, NULL, true, 100); +INSERT INTO history_transactions VALUES ('cebb875a00ff6e1383aef0fd251a76f22c1f9ab2a2dffcb077855736ade2659a', 3, 1, 'GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU', 8589934593, 100, 1, '2019-10-31 13:19:49.395016', '2019-10-31 13:19:49.395016', 12884905984, 'AAAAAK6jei3jmoI8TGlD/egc37PXtHKKzWV8wViZBaCu5L5MAAAAZAAAAAIAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAAAAAAAL68IAAAAAAAAAAAa7kvkwAAABA9Pu9pjykcRS60lqOLqN8FHz244QP8baYNeTTJZIlr3SbRC13qEr9uP4ORDgyCB/gcug2GKrDMuK0ST3QOaKUBw==', 'AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=', 'AAAAAQAAAAAAAAABAAAABAAAAAMAAAACAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA+lbqAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msmcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA4n9kcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', 'AAAAAgAAAAMAAAACAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msmcAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', '{9Pu9pjykcRS60lqOLqN8FHz244QP8baYNeTTJZIlr3SbRC13qEr9uP4ORDgyCB/gcug2GKrDMuK0ST3QOaKUBw==}', 'none', NULL, NULL, true, 100); +INSERT INTO history_transactions VALUES ('2374e99349b9ef7dba9a5db3339b78fda8f34777b1af33ba468ad5c0df946d4d', 2, 1, 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', 1, 100, 1, '2019-10-31 13:19:49.409714', '2019-10-31 13:19:49.409714', 8589938688, 'AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAABAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAArqN6LeOagjxMaUP96Bzfs9e0corNZXzBWJkFoK7kvkwAAAAAO5rKAAAAAAAAAAABVvwF9wAAAECDzqvkQBQoNAJifPRXDoLhvtycT3lFPCQ51gkdsFHaBNWw05S/VhW0Xgkr0CBPE4NaFV2Kmcs3ZwLmib4TRrML', 'AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=', 'AAAAAQAAAAAAAAABAAAAAwAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/7UAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrNryTTUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAACuo3ot45qCPExpQ/3oHN+z17Ryis1lfMFYmQWgruS+TAAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', 'AAAAAgAAAAMAAAABAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/+cAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', '{g86r5EAUKDQCYnz0Vw6C4b7cnE95RTwkOdYJHbBR2gTVsNOUv1YVtF4JK9AgTxODWhVdipnLN2cC5om+E0azCw==}', 'none', NULL, NULL, true, 100); +INSERT INTO history_transactions VALUES ('164a5064eba64f2cdbadb856bf3448485fc626247ada3ed39cddf0f6902133b6', 2, 2, 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', 2, 100, 1, '2019-10-31 13:19:49.409839', '2019-10-31 13:19:49.409839', 8589942784, 'AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAO2C/AO45YBD3tHVFO1R3A0MekP8JR6nN1A9eWidyItUAAAAAO5rKAAAAAAAAAAABVvwF9wAAAEASEZiZbeFwCsrKBnKIus/05VtJDBrgosuhLQ/U6XUj4twWyhs7UtS4CMexOM6JqcfqJK10WlBkkwn4g8PIfjIG', 'AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=', 'AAAAAQAAAAAAAAABAAAAAwAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrNryTTUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrMwLmrUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAA7YL8A7jlgEPe0dUU7VHcDQx6Q/wlHqc3UD15aJ3Ii1QAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', 'AAAAAgAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/+cAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/84AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', '{EhGYmW3hcArKygZyiLrP9OVbSQwa4KLLoS0P1Ol1I+LcFsobO1LUuAjHsTjOianH6iStdFpQZJMJ+IPDyH4yBg==}', 'none', NULL, NULL, true, 100); +INSERT INTO history_transactions VALUES ('2b2e82dbabb024b27a0c3140ca71d8ac9bc71831f9f5a3bd69eca3d88fb0ec5c', 2, 3, 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H', 3, 100, 1, '2019-10-31 13:19:49.409953', '2019-10-31 13:19:49.409953', 8589946880, 'AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAADAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAbmgm1V2dg5V1mq1elMcG1txjSYKZ9wEgoSBaeW8UiFoAAAAAO5rKAAAAAAAAAAABVvwF9wAAAEDJul1tLGLF4Vxwt0dDCVEf6tb5l4byMrGgCp+lVZMmxct54iNf2mxtjx6Md5ZJ4E4Dlcsf46EAhBGSUPsn8fYD', 'AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=', 'AAAAAQAAAAAAAAABAAAAAwAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrMwLmrUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrL0k6DUAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAABuaCbVXZ2DlXWarV6UxwbW3GNJgpn3ASChIFp5bxSIWgAAAAA7msoAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', 'AAAAAgAAAAMAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/84AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtrOnY/7UAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==', '{ybpdbSxixeFccLdHQwlRH+rW+ZeG8jKxoAqfpVWTJsXLeeIjX9psbY8ejHeWSeBOA5XLH+OhAIQRklD7J/H2Aw==}', 'none', NULL, NULL, true, 100); + -- -- Data for Name: key_value_store; Type: TABLE DATA; Schema: public; Owner: - -- -INSERT INTO public.key_value_store VALUES ('exp_ingest_last_ledger', '0'); +INSERT INTO key_value_store VALUES ('exp_ingest_last_ledger', '0'); + + +-- +-- Data for Name: offers; Type: TABLE DATA; Schema: public; Owner: - +-- + + + +-- +-- Data for Name: trust_lines; Type: TABLE DATA; Schema: public; Owner: - +-- + + + +-- +-- Name: accounts_data accounts_data_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY accounts_data + ADD CONSTRAINT accounts_data_pkey PRIMARY KEY (ledger_key); + + +-- +-- Name: accounts accounts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY accounts + ADD CONSTRAINT accounts_pkey PRIMARY KEY (account_id); + -- -- Name: accounts_signers accounts_signers_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.accounts_signers - ADD CONSTRAINT accounts_signers_pkey PRIMARY KEY (signer, account); +ALTER TABLE ONLY accounts_signers + ADD CONSTRAINT accounts_signers_pkey PRIMARY KEY (signer, account_id); -- @@ -678,6 +836,14 @@ ALTER TABLE ONLY asset_stats ADD CONSTRAINT asset_stats_pkey PRIMARY KEY (id); +-- +-- Name: exp_asset_stats exp_asset_stats_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY exp_asset_stats + ADD CONSTRAINT exp_asset_stats_pkey PRIMARY KEY (asset_code, asset_issuer, asset_type); + + -- -- Name: gorp_migrations gorp_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -717,12 +883,51 @@ ALTER TABLE ONLY history_operation_participants ALTER TABLE ONLY history_transaction_participants ADD CONSTRAINT history_transaction_participants_pkey PRIMARY KEY (id); + +-- +-- Name: key_value_store key_value_store_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY key_value_store + ADD CONSTRAINT key_value_store_pkey PRIMARY KEY (key); + + -- -- Name: offers offers_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- -ALTER TABLE ONLY public.offers - ADD CONSTRAINT offers_pkey PRIMARY KEY (offerid); +ALTER TABLE ONLY offers + ADD CONSTRAINT offers_pkey PRIMARY KEY (offer_id); + + +-- +-- Name: trust_lines trust_lines_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY trust_lines + ADD CONSTRAINT trust_lines_pkey PRIMARY KEY (ledger_key); + + +-- +-- Name: accounts_data_account_id_name; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX accounts_data_account_id_name ON accounts_data USING btree (account_id, name); + + +-- +-- Name: accounts_home_domain; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX accounts_home_domain ON accounts USING btree (home_domain); + + +-- +-- Name: accounts_inflation_destination; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX accounts_inflation_destination ON accounts USING btree (inflation_destination); + -- -- Name: asset_by_code; Type: INDEX; Schema: public; Owner: - @@ -759,6 +964,20 @@ CREATE INDEX by_hash ON history_transactions USING btree (transaction_hash); CREATE INDEX by_ledger ON history_transactions USING btree (ledger_sequence, application_order); +-- +-- Name: exp_asset_stats_by_code; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX exp_asset_stats_by_code ON exp_asset_stats USING btree (asset_code); + + +-- +-- Name: exp_asset_stats_by_issuer; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX exp_asset_stats_by_issuer ON exp_asset_stats USING btree (asset_issuer); + + -- -- Name: hist_e_by_order; Type: INDEX; Schema: public; Owner: - -- @@ -970,35 +1189,66 @@ CREATE UNIQUE INDEX index_history_transactions_on_id ON history_transactions USI -- --- Name: trade_effects_by_order_book; Type: INDEX; Schema: public; Owner: - +-- Name: offers_by_buying_asset; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX trade_effects_by_order_book ON history_effects USING btree (((details ->> 'sold_asset_type'::text)), ((details ->> 'sold_asset_code'::text)), ((details ->> 'sold_asset_issuer'::text)), ((details ->> 'bought_asset_type'::text)), ((details ->> 'bought_asset_code'::text)), ((details ->> 'bought_asset_issuer'::text))) WHERE (type = 33); +CREATE INDEX offers_by_buying_asset ON offers USING btree (buying_asset); + + +-- +-- Name: offers_by_last_modified_ledger; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX offers_by_last_modified_ledger ON offers USING btree (last_modified_ledger); + -- -- Name: offers_by_seller; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX offers_by_seller ON offers USING BTREE(sellerid); +CREATE INDEX offers_by_seller ON offers USING btree (seller_id); + -- -- Name: offers_by_selling_asset; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX offers_by_selling_asset ON offers USING BTREE(sellingasset); +CREATE INDEX offers_by_selling_asset ON offers USING btree (selling_asset); + -- --- Name: offers_by_buying_asset; Type: INDEX; Schema: public; Owner: - +-- Name: signers_by_account; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX offers_by_buying_asset ON offers USING BTREE(buyingasset); +CREATE INDEX signers_by_account ON accounts_signers USING btree (account_id); -- --- Name: offers_by_last_modified_ledger; Type: INDEX; Schema: public; Owner: - +-- Name: trade_effects_by_order_book; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX trade_effects_by_order_book ON history_effects USING btree (((details ->> 'sold_asset_type'::text)), ((details ->> 'sold_asset_code'::text)), ((details ->> 'sold_asset_issuer'::text)), ((details ->> 'bought_asset_type'::text)), ((details ->> 'bought_asset_code'::text)), ((details ->> 'bought_asset_issuer'::text))) WHERE (type = 33); + + +-- +-- Name: trust_lines_by_account_id; Type: INDEX; Schema: public; Owner: - -- -CREATE INDEX offers_by_last_modified_ledger ON public.offers USING btree (last_modified_ledger); +CREATE INDEX trust_lines_by_account_id ON trust_lines USING btree (account_id); + + +-- +-- Name: trust_lines_by_issuer; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX trust_lines_by_issuer ON trust_lines USING btree (asset_issuer); + + +-- +-- Name: trust_lines_by_type_code_issuer; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX trust_lines_by_type_code_issuer ON trust_lines USING btree (asset_type, asset_code, asset_issuer); -- @@ -1045,8 +1295,3 @@ ALTER TABLE ONLY history_trades -- PostgreSQL database dump complete -- -CREATE TABLE key_value_store ( - key varchar(255) NOT NULL, - value varchar(255) NOT NULL, - PRIMARY KEY (key) -); diff --git a/services/horizon/internal/test/scenarios/bindata.go b/services/horizon/internal/test/scenarios/bindata.go index af2b80ca8e..f84c82b0ab 100644 --- a/services/horizon/internal/test/scenarios/bindata.go +++ b/services/horizon/internal/test/scenarios/bindata.go @@ -24,8 +24,8 @@ // asset_stat_trustlines_7-horizon.sql (48.894kB) // bad_cost-core.sql (29.849kB) // bad_cost-horizon.sql (34.334kB) -// base-core.sql (28.545kB) -// base-horizon.sql (41.431kB) +// base-core.sql (29.713kB) +// base-horizon.sql (47.813kB) // change_trust-core.sql (33.104kB) // change_trust-horizon.sql (43.637kB) // core_database_schema_version_8-core.sql (8.369kB) @@ -614,7 +614,7 @@ func bad_costHorizonSql() (*asset, error) { return a, nil } -var _baseCoreSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x7d\x57\x93\xa3\xca\xf2\xe7\xfb\x7c\x0a\x62\x5e\x7a\x26\xba\xcf\x11\xde\xcc\xec\xdc\x08\x90\x77\xc8\xdb\x8d\x8d\x8e\x02\xaa\x10\x12\x02\x84\x91\xe9\x8d\xfb\xdd\x37\x00\x19\x24\x21\xb5\x5a\xea\xb9\xe7\x6e\xfc\x87\x87\x36\x22\xf9\xd5\xaf\xb2\x32\x93\xaa\x2c\x94\xb8\x81\x65\x19\x96\x8e\xb9\x50\x35\x1c\xf8\x25\xfe\x85\x21\xc3\x32\xbc\x09\xd4\xbe\xfc\xf5\xd7\x97\xbf\xfe\xc2\x9a\xb6\xe7\xeb\x2e\xec\xb4\x6a\x98\x06\x7c\xa0\x00\x0f\x62\x5a\x30\x77\xc2\xd3\xe1\xf9\x5c\x30\x77\xa0\x86\x21\xd7\x9e\x1f\x04\x96\xd0\xf5\x0c\xdb\xc2\x84\xbf\xd9\xbf\x89\x84\x94\xb2\xc1\x1c\xfd\x35\xbc\xfc\x44\xe4\x4b\x27\xdf\xc5\x3c\x1f\xf8\x70\x0e\x2d\xff\xd5\x37\xe6\xd0\x0e\x7c\xec\x17\x86\xff\x8c\x4e\x99\xb6\x3a\x3b\xff\xd4\xd0\x4c\xf8\x6a\x58\xaf\xbe\x0b\x2c\x0f\xa8\xbe\x61\x5b\xaf\x1e\xf4\x42\xdc\x73\x61\xd5\x34\x42\x68\x68\xa9\xb6\x16\xf6\xfa\x17\xf6\xd4\xeb\x16\xf8\xa7\x9f\xbb\xb6\x2d\x0d\xb8\xda\xab\x6a\x5b\xc8\x76\xe7\x86\xa5\xbf\x7a\xbe\x6b\x58\xba\x87\xfd\xc2\x6c\x6b\x8b\x31\x81\xea\xec\x15\x05\x56\xdc\x96\x62\x6b\x06\x0c\xcf\x23\x60\x7a\xf0\xa8\x99\xb9\x61\xbd\xce\xa1\xe7\x01\x3d\x12\x58\x01\x37\x54\x75\x2c\xe2\xda\xab\x57\x0f\xaa\x81\x6b\xf8\x9b\x10\x1c\xa1\x9f\x5b\x05\x40\xe0\xaa\x93\x57\x07\xf8\x13\xec\x17\xe6\x04\x8a\x69\xa8\x2f\xa1\xc6\x54\xe0\x03\xd3\xd6\x7f\x7e\xf9\x92\x6b\x37\x9a\x58\x59\xce\xe5\x87\x58\xb9\x80\xe5\x87\xe5\x4e\xb7\xb3\x95\xfc\x3b\x70\x74\x17\x68\x70\x62\x78\xbe\xb2\xf1\xe0\xe2\xe7\x55\x69\x4f\x75\x16\x81\xed\x06\x73\xef\x36\x61\x68\x2d\x6f\x91\x34\xa1\xa6\x43\xf7\x16\xc9\x90\x27\x82\xf0\x46\xc9\x1b\xc4\x14\xe8\xf9\x36\x42\xd0\x35\x2c\x0d\xae\xaf\xcb\x02\x55\xb5\x03\xcb\x57\x80\x09\x2c\x15\x7a\x3f\xbf\x88\xb5\x6e\xbe\x8d\x75\x45\xa9\x96\x4f\x48\x37\xe4\xda\x28\x45\xbd\xb6\xbb\xc1\x22\xf4\x6c\x43\xee\x74\xdb\x62\x59\xee\x26\x2e\x3a\x16\x7c\x75\x66\x70\x73\x0b\xbe\xbf\x7e\x1f\x7a\x2f\xf3\x01\x54\x04\x6f\xe0\x9c\x14\xbb\x1d\xdb\x0d\x3c\xdf\x34\x2c\xe8\x5d\x43\xde\x0b\xdd\x8c\x1b\xb2\x80\x51\x34\xb8\x82\x7b\x10\xba\x1d\x77\x6f\xf2\xd7\x70\xf7\x42\x37\xe3\xc6\xf2\x86\x85\xec\x2b\xb8\x07\xa1\x9b\x71\x9d\x40\xf1\x02\xe5\x0a\x66\x2c\xf0\x11\x3c\xd3\xf0\x26\x8b\x00\x06\xd7\x34\x9b\x14\xbb\x1d\x1b\x42\xf7\x9a\x5a\xa3\xf3\x37\xa3\x45\x6e\x7c\x0d\x2e\x16\xb8\x19\x2f\x8e\x4a\x13\x08\xb4\xeb\xb0\x47\x72\xbf\x19\x7d\x1b\x29\xe1\xe2\xf5\xc6\x66\x14\x60\x5d\x01\x57\x80\x75\x33\xe1\x6d\xf4\xbb\xc6\x75\x27\xf2\x51\xcc\x70\x0e\xf0\x3e\x6c\x28\xb5\x45\x8e\x64\x4f\x81\x53\x43\xee\x75\xd9\x7d\x68\x7c\x4f\xec\x10\xe8\xde\x91\xdc\x07\xae\xeb\x72\x87\x40\xf4\x8e\xdc\x3e\xb0\xbc\x2b\x77\x13\xbf\x43\x40\xb9\x2e\x17\x07\x89\x77\x65\xf6\x2e\xff\x8e\x64\xe8\xc7\xd7\x45\x62\xdf\xbc\x2e\x73\xe4\x0a\xd7\x45\x15\x60\x5d\x17\xd8\x99\xea\x4d\x52\xa1\xe5\x6d\x05\xf3\xc3\x6e\x5e\xee\x94\x1b\x72\x52\xd8\x74\x74\x6f\x61\x6e\x25\x3a\xd9\x52\xbe\x2e\x9e\x61\xfd\xdc\xce\x8d\x65\x30\x87\x3f\x76\x9f\x61\xdd\x8d\x03\x7f\x6c\x2f\xf9\x89\x75\xd4\x09\x9c\x83\x1f\xd8\x5f\x3f\xb1\xc6\xca\x82\xee\x0f\xec\xaf\x68\xca\x9c\x6d\xe7\xc5\x6e\x7e\x87\xbc\xc3\xfb\x72\x84\x78\x7c\x72\x0b\x9c\x6d\xd4\xeb\x79\xb9\x7b\x05\x39\x16\xc0\x1a\xf2\x31\x00\x56\xee\x60\x4f\xbb\xf9\xed\xee\x33\x2f\x02\x79\x3a\x6d\x79\xd7\xfd\x6d\x9b\x7b\x0d\xbd\xdb\x9f\x23\x5d\xca\x8d\xee\x89\x3e\xb1\x41\xb9\x5b\xda\xd3\x4a\x4e\x68\x8f\x9a\x3f\xa0\x9c\x10\xf9\x48\xe7\xcf\x40\x22\x05\x34\x6b\x19\x47\x0f\x57\x31\x8e\x6b\xab\x50\x0b\x5c\x60\x62\x26\xb0\xf4\x00\xe8\x30\x52\xc3\x8d\x13\xf0\x50\x4c\x83\x08\x04\xa6\xff\xea\x03\xc5\x84\x9e\x03\x54\x18\xae\x26\x9e\x4e\xce\xae\x0c\x7f\xf2\x6a\x1b\x5a\x62\x81\x70\xd4\xd9\xa4\x41\x6e\xbb\x19\x99\xee\xa1\x93\x3b\x03\x48\x53\x78\x6c\xe5\xc9\xa0\xfb\xed\x0b\x86\x61\xbb\x4f\x0c\x0d\x53\x27\xc0\x05\xaa\x0f\x5d\x6c\x09\xdc\x8d\x61\xe9\xdf\x18\xf6\x7b\x34\x36\x72\xaf\x56\x7b\x89\xa4\xc3\x0b\x2d\x30\x87\x29\xc2\x3c\x9f\x26\xbc\x04\x66\x90\x26\x4d\x10\xe4\xa9\xb8\x09\x3c\x7f\x6e\x6b\x06\x32\xa0\x86\x19\x96\x0f\x75\xe8\xee\x45\xbe\x7c\x3f\x1d\xfb\xbd\x17\x3f\xa8\x0b\xef\x2e\x45\x6c\x17\x02\x98\x62\xe8\x86\xe5\x9f\x9c\xf4\xe0\xc2\x0a\xe6\xe9\xe7\xac\x60\xee\x05\x0a\xb4\x7c\x37\x5c\x0a\x9e\x76\x33\x96\x31\x2c\x64\x82\x70\xc5\xa8\x41\xcf\x4f\xa7\x13\x0b\x4e\xec\x39\xd4\xec\x39\x30\xac\x14\x29\x9a\x3e\x25\xed\x4f\x5c\xe8\x4d\x6c\x53\xf3\x30\x1f\xae\x4f\x99\x21\x13\xe8\x97\x18\x5d\x1d\x9b\xad\x46\x82\xb0\x55\xd3\x00\x8a\x61\x1a\x7e\xd8\xb9\xb8\xff\x3b\x95\x98\xe6\xb5\xd3\x86\x6e\x85\x73\xa1\x90\x56\xfc\x49\x62\x36\xb0\x9f\x5a\x6c\x95\xfe\x1a\x2d\xab\xb1\x6c\x29\x9f\xad\x62\xdf\xbe\xed\x86\xe2\x5f\xbf\x30\xfc\xfb\xf7\x2b\x57\x9f\x12\x3c\xc5\x39\xeb\xc0\x7b\x88\x47\x63\x79\x82\x76\x3c\xce\xef\x21\x9d\xab\xe7\x04\x2e\x45\x7f\x31\xe6\xb9\x63\x84\xf7\xbf\x7b\x7d\x22\x9c\x32\xc6\xee\x60\xd9\x1a\x4c\xfa\xc2\x91\x0f\x9c\x37\x7a\x7c\x7f\xbe\xb7\xf9\xe3\x89\x71\x4c\x64\xfb\x19\xf0\x26\x09\x32\xec\x99\x6d\x3b\x2e\x5c\xbe\x2b\xa4\x04\xea\x0c\xfa\xa6\xe1\xf9\xef\x8a\xee\x67\xdb\x3b\x73\x8f\x3f\x56\x4d\xdb\x83\xbe\x31\xbf\xe0\xf9\x51\x60\x4d\xf1\xad\xc4\x98\x1f\x4f\xea\xf7\x78\x27\xe3\x7d\x68\xe7\x82\xe9\x5c\x5a\x1b\x1c\xc3\x1c\x7a\x71\xc9\x5a\xb6\x93\xaf\x7b\x47\x6c\xbb\xf0\xfa\xb6\x77\x72\xe8\xde\x18\x41\xe3\xcc\x8b\x76\x29\x82\x46\xe6\x0e\x3c\x0f\xfa\x69\xfa\x8c\x7d\xf5\xe2\x69\x30\x0f\xdd\x2a\x1d\xda\x71\x0d\x15\x5a\x17\x82\x58\x74\xf2\x52\x84\x8b\x4e\x62\x9a\x1d\x28\x26\x0c\xed\x4d\x35\xa2\x8c\xe4\xa7\x46\xd1\xc4\x08\x6f\x97\xac\x71\x5f\x4e\xc6\x75\xdb\xc1\x0b\xb6\xb1\xbd\x72\xab\xe1\x93\x4b\x77\x7a\xbf\x64\x10\xf1\x84\xfd\x5e\x7b\x88\x97\xf5\xb1\x39\x18\x4e\xda\x8d\x9f\x39\xf3\x5c\xdb\xf5\xf7\xda\xc8\xe5\x0b\x62\xaf\xd6\xc5\xf0\xd3\xdb\x26\x5c\xfb\xc0\xf7\xe1\xdc\xf1\xb1\xd0\x2d\x3c\x1f\xcc\x1d\x2c\x9c\x32\xd9\x41\xfc\x09\xf6\x66\x5b\xf0\xfc\x66\x8b\x80\x61\x06\x6e\xe2\x56\x7b\xa9\x05\x7f\xe3\xc0\xf7\x07\x25\x4e\x4b\x24\x70\xcf\xc3\xfe\xbe\xc5\x0b\xa3\xb3\xcd\x6c\xd8\xee\xe9\xa0\x7e\x8b\x34\xf1\x2f\x0c\xff\x8e\x89\x72\x0e\x8b\xff\xfd\x5f\xbf\x30\x96\x61\x28\xe6\x7b\xea\x58\x25\x97\x61\x77\x0f\x59\x32\xcb\x93\x8c\xb9\x17\xb4\x11\x27\xda\x42\xaf\x4b\x25\x14\xae\x1d\x1f\xa0\xe2\x05\xca\x96\x84\x0b\xbd\xa3\x1b\x10\x95\x3a\x63\x74\x21\xd8\xfb\xd2\x39\x9f\xc4\x9a\xf7\x5e\x4e\x89\x64\xdd\x0d\x77\xc6\x98\xd8\xc2\x83\xd7\xee\x30\xe7\x3c\x13\x6b\xf8\x7b\x79\x1e\x20\x6e\xe7\x79\x76\x93\x3b\x39\x0f\xad\x25\x34\x6d\x07\xbe\x73\x4b\x3b\x34\xfd\xc0\x8d\x28\x91\xee\x78\x40\x05\xbb\x7c\xed\xb7\x5b\xc6\xe1\x60\x45\xef\x29\x62\x71\xe1\x46\x73\xac\x84\x5d\x1e\xf8\x08\xf1\x54\x11\x47\xad\x5d\x54\xc6\x21\x47\x74\xb7\x32\x0e\x49\xf1\x6f\x07\xbf\x3d\x5e\xbc\xa5\xf8\xd4\x35\xef\x4e\x64\xb8\xee\x65\x95\xd8\x02\xb8\x67\xd9\x15\xdd\xf1\xaf\x44\x6a\xc3\xf3\x02\xe8\xde\x0e\xa5\xda\x5a\xea\xea\xf4\x4c\x2d\xbe\x69\xcc\x8d\x0b\x33\x8a\xab\x6b\xc1\x7f\x72\x55\x95\xb0\xce\xc4\xae\xca\x5d\xab\xa8\xe4\xf5\x9f\xb5\x8e\x4a\x60\xde\xbf\xfe\xb9\x86\x1a\x0f\xda\x09\xd2\x76\x24\xff\x95\xee\x78\x47\xe9\xde\xbb\x8d\x3c\xb9\x87\x16\x9b\xb9\xbf\x3e\x0a\xc5\x37\xac\x37\x4e\x0d\x70\x1d\xed\x52\x5e\x3c\xab\x4e\x80\xa5\xc3\xd4\x85\x7d\x52\x39\xc9\x6d\xbb\xfb\x63\xf5\x21\x77\x7e\xbf\x8a\xfe\xc3\xfa\x51\x6c\x6d\x93\xa6\x1c\x7f\xed\x42\x2f\x30\x53\xa3\xbb\xbf\x9e\xc3\x77\xd7\x73\x87\x2d\xd6\xfb\xf5\x79\xb2\x6f\x71\xaf\x52\x4f\x76\x9c\xbf\xdd\xa4\xb8\xed\x45\xd7\xb4\xb7\x15\x49\x53\xc4\x6d\x66\x77\xb2\xc3\x7d\x8f\xa2\x72\xe1\xca\x1a\xd9\xee\x3b\xc9\x50\x2c\x27\x76\xc5\x77\x74\x76\x1d\xd2\xfb\x30\x5e\x59\xee\xe4\xdb\x5d\xac\x2c\x77\x1b\x87\xa4\x62\x5f\xac\xf5\xf2\x1d\xec\xdb\x53\x51\x6a\x37\x47\xa5\x72\x8d\xcc\x96\xa9\x82\xdc\xa2\xa5\x61\xad\x50\x97\x73\xb5\x42\xa5\x27\x37\x7b\x64\x69\x44\x8d\xeb\x85\x4e\xa9\x21\xf7\xb2\xf9\x86\xd8\x19\x70\xad\x2c\xd7\x18\x92\xa5\xa7\x17\x4c\xd8\x1e\x6c\xfc\x8b\xc3\xf1\x17\x8c\x7a\xc1\xf0\x97\x58\xcb\xd8\xd3\xd3\x0b\xf6\x24\xb6\x44\x51\x14\x7f\xfd\x7a\x8a\x4e\x90\xbb\x73\x87\x9f\xdf\x7f\xbe\xc7\x50\x64\x06\x52\x73\x24\x32\x23\x7a\x20\xe6\x4b\xc3\x41\x9b\xec\x55\x1b\x64\xaf\x41\x4b\xbd\x62\xa9\xd7\xe2\xe8\x7c\xaf\x59\x6d\xc8\x64\xab\xd4\xa7\x07\xed\x52\xa3\xdc\x96\xab\xd5\x12\xf9\xf4\x82\x11\xf8\xee\x78\xc1\x78\x86\x17\x04\x8a\x66\x04\xf2\x37\x50\xcc\x0e\xab\x45\xb6\x2d\xd3\x0d\xb9\x9c\x6f\x66\xeb\x72\x41\xe2\x28\x52\xa4\x29\x76\xcc\x34\xe5\x5c\xa7\x5d\x2b\x0e\xaa\x5c\x51\xaa\x65\xeb\xad\x5a\xb9\xd0\xa0\x3b\x5c\x7e\x34\xe8\xf7\x42\x25\xd2\x91\xf2\x8e\x18\xbe\xa3\x44\xea\x1e\x86\xd2\xb0\xd8\xaa\x0c\xfa\xb5\x41\x63\x54\x2a\xd4\xfa\xdd\xea\xa0\xcf\x14\x8a\x25\x91\xaa\xc9\xa3\x11\x59\x69\x55\xeb\x5c\x43\xac\x88\xbd\x7c\xab\xd0\x63\x6b\xcd\x6c\x27\x5f\xe8\x0f\x1b\x72\xa4\x44\xe6\xc3\x4a\x4c\xa7\x78\xc1\xb2\x4f\x33\x82\x0f\x38\xc9\xe5\x3c\xdf\x47\x3d\xe5\x38\xd7\xb7\xd7\x23\x4b\x69\x02\x8f\x18\x8a\x85\x90\xe5\x35\x42\x21\x39\x85\x51\x78\x01\x91\x14\x40\x0c\x45\x10\x0a\xc7\xb0\x02\x20\x69\x04\x10\x41\xe3\x14\xd0\x70\x85\x21\x15\x96\xa2\x14\x9c\x53\xa0\x20\x84\xaa\xc2\x1f\x3c\x42\x0c\x86\x23\x01\x09\x29\x12\x21\x92\xe6\x01\xce\x29\x38\xe4\x70\xa4\x11\x88\xd5\x28\x82\x57\x09\x04\x54\x8d\xc4\x15\x56\x55\x71\x5e\xa5\x28\x8d\xe1\x38\x86\x64\x04\x9e\xe5\x09\x92\x01\x04\x1b\x8e\x6b\x34\x52\x4f\xe2\x7f\xed\x21\x0d\xab\x06\xbd\xc9\x6c\x3a\x55\x89\xcb\x59\x39\xa1\x44\xe2\xeb\xa9\xf4\xec\xe1\xba\xef\xad\xca\xab\x37\x62\xa8\x75\x06\x23\x20\x55\x40\x41\x0f\xe5\xf3\x32\x5d\x03\x6f\x0e\xd9\x7a\x17\x79\x2c\x0e\x09\x3a\x12\x93\x66\xff\x81\x8e\x7c\xea\xf1\x74\xe2\xeb\x17\x0c\x95\xa1\x20\x43\x40\x86\xc2\x05\x44\x91\x1a\xc1\x30\x38\x0e\x19\x85\xc5\x39\x8a\xa4\x15\xc8\xb1\x10\xb1\x8a\x42\x11\x88\x26\x01\x43\x13\x1a\xc1\xb2\x14\xcd\xf2\x84\xa2\x2a\xac\xc0\x03\x32\x8c\x9e\x9f\x62\xec\x84\x40\xb3\x34\xae\x08\x04\xce\xe1\x38\xc1\x71\x3c\xae\x6a\x04\x47\x40\x46\x11\x58\x8d\x54\x68\x1c\x41\x15\x27\x14\x0e\xe7\x54\x5e\xd0\x18\x52\x51\x29\x8a\x17\x38\xc0\x02\x5a\x25\x11\x87\x9e\xa2\x88\x4c\x30\x8c\xc0\x70\x02\x4b\x13\x5b\x8b\xcd\x92\xcd\xf1\x94\x90\x03\xc6\xc6\x95\x0a\x37\xa0\xad\x4d\x63\xd9\x5b\x17\xa9\xbe\x63\xcf\x9e\x97\x05\xb1\xe1\x67\x89\x2a\x59\xe7\x24\x8e\x1d\x9b\xf3\xbc\xd6\x70\xfa\xd9\x3a\x53\xaa\xb9\x42\x41\x9e\x32\xcc\x02\xb0\x2b\xb2\x54\xad\xfb\x8b\x6e\xb3\x50\x5b\x16\xf9\x4d\xb3\x97\x01\xa2\x1d\x69\x78\xd8\xec\xd7\x9e\x23\xeb\x28\xef\x7f\xc4\xe1\xcd\x3b\xfc\xbf\x12\x9b\xad\xad\xed\xb4\x7b\x62\x7f\x5d\x99\x13\x66\xae\xbe\x5a\x2d\x82\x69\x55\xdd\xb4\xde\x3c\x81\x2b\x64\xc4\x7c\xd7\xc8\xea\xad\xa6\xbb\x62\xa9\xd5\x02\x34\xf3\xe3\xf6\x2c\xcb\xe4\x4b\xa2\xa4\xd1\x39\xb9\xb0\x66\x14\xd3\xaf\xe2\xb9\xe7\x95\xe4\xaf\x50\xd9\xea\x57\xf9\x3a\x6d\xb2\x60\xb6\x5a\xa2\x55\xd4\x54\x8a\x45\xe7\xbd\x34\xab\xd8\x59\x74\x0e\xaf\xfc\x46\xdb\xfb\x3d\xc7\x8d\x16\xad\xd1\x61\x50\x63\x04\x48\x12\x0c\x52\x78\x5e\x51\x35\x85\x43\x88\x67\x05\xc8\x22\x86\x12\x68\x0a\x30\x1a\xa9\x42\x28\x00\x42\x20\x71\x02\x32\x04\xa2\x68\x9e\x25\x81\xaa\x00\x4a\x8b\xc3\xe6\x27\x78\x85\xaa\xb0\x0a\x10\x70\x55\xa5\x58\x1a\x41\x9e\x11\x54\x1e\xb1\x3c\xc3\xe1\x9a\xca\xb2\x1c\xcf\x23\x55\x60\x69\x8e\x04\x14\xc3\x92\x88\x54\x09\x04\x15\x8d\x00\x04\xe2\x04\x12\xa9\x24\x0a\x79\x50\x09\x8b\x26\x77\x16\x4d\x34\xcd\x92\x59\xaf\xf0\x1b\xbc\xdf\x13\x19\x65\x54\xaa\xcf\x96\x96\xb2\x04\x5c\x1d\xb5\x16\x7d\x09\x1f\x8c\x70\x20\x2d\x6b\x60\x64\x1b\xa0\x44\x2b\xe2\xb0\x9a\x2d\xaf\x57\xbe\xd5\x19\x6f\x26\xb3\x99\x09\x5b\xb6\xae\xb5\x1d\x59\xe1\xb8\x5e\xc7\x9c\xe2\x81\xfe\x5c\xe5\x38\x3c\xd2\x70\x64\xd1\xfa\x41\xe3\x85\x7a\xb5\xb2\x04\x6c\x6b\xde\x30\x73\x35\x1f\x4e\x47\xca\xc4\x19\x95\xb9\x4e\xaf\xda\x40\xb0\xa2\x94\xb5\xd9\x62\x2a\xac\x1a\x84\xe8\xbb\x35\xc0\xce\xea\x2b\xb2\xfb\x3c\x19\x6f\x9a\xa0\xa0\xca\xeb\x39\x2c\x67\x2a\xe3\x52\x75\xda\x37\x78\xaf\xf4\xbc\x6c\xdb\x08\x76\x32\x59\x3e\x42\xae\xa7\x58\x6c\x31\x35\x20\xff\xff\x6e\xb1\x17\x66\x20\x29\x1b\x56\x0f\xcc\x67\xce\x77\x3b\x1e\x01\xbb\x94\x8e\x7f\x0c\xf3\x34\xa3\xfe\x00\xda\x85\x7c\xf8\x03\x88\x17\x32\xd7\x1f\x9d\x0d\x26\xb2\xd7\x89\x49\xff\x98\xc9\x37\x5a\xa5\x2e\x59\x93\xb9\x7e\x53\xac\xf4\x07\x5c\x67\x38\x96\x2a\x64\x7f\x2c\x55\x7b\xfd\x2e\xdd\x1c\x4b\x62\xbd\x37\x2a\xf5\xa9\x66\xb5\x39\xae\x57\xea\x5c\xa9\x51\xcc\xc5\x77\xb4\x78\xde\x55\x6b\x76\x1a\x3a\x84\xa6\x9f\xe9\xd3\x79\xc2\x7f\xee\xf3\x93\xee\x22\x68\x2d\x9c\x01\x2a\x31\x59\xc9\xac\x97\x5c\x9f\xb5\x98\x91\x69\xbd\x1d\xac\x2f\xba\x01\x65\xa3\x3f\x43\xcf\xca\x1b\x83\x51\x1b\x67\xcd\x5e\xf9\x6d\xa6\x06\x4b\xbc\x47\x36\xad\xb9\xe3\xba\xb9\x91\x6a\x6f\x48\xa7\xce\xf7\x02\x5e\x79\x2b\x0b\x5d\xc1\xce\x46\x31\x40\x52\x85\xde\x92\x89\xae\xd7\xf7\x3f\xa4\x08\x74\xb5\xff\x3f\x27\x8a\x42\x36\xe1\xb8\xd2\xdb\x38\xf4\xfe\xac\x5e\x2c\x68\xcf\x39\xd0\x9b\xdb\x96\xe8\xb0\x02\x53\xcd\xf5\xd9\x66\xc5\x70\xd8\xa1\xc2\xfb\xf8\x40\xf7\x57\xf3\x5e\x04\x37\x31\xea\x81\x56\x35\x7a\x8e\x2f\xa1\xf2\x9c\xe3\xf4\xd9\x54\xa5\xa4\xb1\x32\x5b\x08\x55\x34\xa7\xaa\x34\xa1\x0b\x88\x2c\x66\xcb\x5c\x37\x57\xe9\x75\xdb\x9e\xa8\x2e\xa0\xcc\x41\xaa\x3f\xa1\xaa\xa5\x45\xbd\x65\x70\xe4\x84\xb0\x54\xba\xdb\xdd\xe4\x0a\xc5\x4e\x76\x59\xf1\xc5\xd5\xaf\x5f\xa7\x77\x8b\x4f\x1e\x1a\xea\xa1\xa1\xa9\x1f\x0f\x4d\x4e\xb2\xd1\xc4\x93\x54\xbb\x3c\x2d\x65\x49\xad\x62\x55\x8b\x9d\x4e\x9f\xc9\x1a\x12\x51\x9c\x11\xcb\xa5\xdf\xae\x0e\x9a\x1d\x36\x47\xbb\xcb\x65\x6b\x3f\x34\xec\x49\xb4\xf9\xb0\xea\x83\xb5\xbb\x68\xcd\x06\xf3\xb6\x57\x07\xb2\xa0\x8e\xa8\x79\x2d\x9f\xcf\x6b\x1b\x4a\x5d\x1b\xed\xe9\xf3\xc2\x07\x42\x6b\x5e\x83\xf2\xc4\xf3\x97\x48\xac\x48\x16\xab\x6a\xd3\x52\xc7\x33\x37\x2c\x9d\x21\x89\x3e\x5f\xec\x92\x2e\xbb\xb6\xc6\xcd\xfc\x06\xae\x96\x64\x9b\x94\x32\xd1\xda\xee\x72\xd8\xbb\xb0\x3d\x72\x87\x9f\xed\xb6\x48\xf6\x83\xa9\x6a\x42\x7c\x7b\xc5\x01\x4e\xf0\x0c\xc7\x41\x5c\x03\x0c\xc9\x02\x92\xc3\x49\x01\x2a\x1a\xa4\x01\xae\x31\x90\x47\x24\x4b\x02\x0e\x30\x9a\x82\x48\x4d\x23\x58\x9e\xd4\x54\x92\x65\x92\x83\x1a\x29\x39\x1f\xfd\xe9\x09\x65\x56\x62\xd8\x01\x25\x0c\xf5\xee\x80\x62\x86\x9b\x42\x63\xc1\x64\x17\xe6\x98\x47\xb3\x72\xb1\xb7\x82\x01\xb5\x40\xa6\x31\x40\xf5\xf7\x62\xfe\x85\xed\x90\x0f\x77\xfe\xb0\x25\x72\x98\xf4\x6c\xbf\x83\x14\x3f\xe0\x88\x9d\x1d\xd1\x14\x1b\x3f\xf3\x89\x14\x24\x0b\xfa\x2b\xdb\x9d\x39\xc0\xf3\x9c\x89\x0b\x3c\x98\x82\xd4\x85\x9e\x8f\x75\x72\x05\x4c\x8e\x85\xb1\x9f\x58\x07\x3a\x3e\x9c\x2b\xd0\xc5\x48\x9c\x60\x6e\x69\x08\xd9\xae\x0a\x3d\xd5\xb1\x2d\x0b\xae\x7d\x13\x04\x96\x3a\x39\x6d\x28\x7a\x80\xf0\x16\xb0\x78\x2e\xb8\x4d\xd0\x79\xe9\xfd\xff\xbf\x51\x3e\xef\xab\x6f\xcc\xe1\xd7\x1f\x18\x1e\xa7\xf7\xbe\x6e\xbf\x91\xf5\xf5\x07\x16\x9f\x8f\x3e\x9c\x00\xef\xeb\x8f\xf8\xf1\xc5\xe8\xc3\x7f\x6f\x85\x11\x84\xb7\x09\xce\xc1\xda\x5f\x7b\xc6\xdb\x8d\xe2\x2e\xf4\xa0\xbb\x7c\x4f\xf8\xcb\xbf\x6f\x52\x05\xf0\xfc\xe8\xb9\x1b\x6d\xbb\xff\x9d\xa2\x8a\x87\xe7\xc8\x37\xf0\xd8\x46\x5b\xe0\xaa\x13\x63\xb9\x3d\x79\x61\x48\x0e\x63\x40\x6c\x15\x12\xa9\xc3\xfd\xfa\x03\xfb\xba\x24\x88\xbf\x89\xbf\xf1\xaf\xdb\x13\x6a\xe0\xba\xd0\xf2\x6b\x51\xd7\xbe\xfe\xc0\xa8\xe3\xcf\xa5\xe8\xd1\xa8\x50\x6f\xff\x7b\xaf\xc8\x83\x4a\xf7\x92\x21\x30\xc7\xd1\x88\x00\x2a\x24\x35\x48\x71\x88\xa3\x81\x80\x43\x45\xa1\x79\x92\xa2\x09\x5c\x11\x18\x12\x12\x1c\x47\xb0\x38\x4b\x21\xc8\x40\xc4\x00\x28\x20\x4e\xe0\x29\x8e\x54\x58\x15\xa7\xd1\x96\xd1\x1e\x37\xb4\xe2\xa3\x01\xdc\x9f\x89\xfa\x1e\x9a\xdc\xd1\xa9\x7f\x9f\x5c\xef\x59\xc0\x09\x79\x41\x44\x11\x80\xc4\x01\x25\xf0\x10\x72\x94\x0a\x49\x92\xe4\x18\x08\x78\x82\xe3\x38\x9e\x55\x80\xca\xd0\x2c\xc3\x22\x8a\xd2\x54\x95\x46\x14\x82\x2a\x8b\x6b\x0c\xa3\x69\x88\x08\x57\xe2\x5f\xbf\xa4\xb4\x70\x41\x07\x8f\xa6\x98\x3e\xae\x03\xe2\xe5\xfc\x9c\x1d\xf8\x4e\xe0\x7f\x6e\xdf\xaf\x69\xf8\xe1\x5e\xff\x57\x6b\xf8\x36\x2b\xfb\xa3\x83\x3f\x3a\xf8\xa3\x83\x3f\x3a\xf8\xa3\x83\x3f\x3a\xf8\xa3\x83\x3f\x3a\xf8\x8d\x3a\x88\xfe\xfa\x3f\xb7\x2f\x5e\x3c\xd5\x89\xbe\x80\x90\x76\x3c\xed\x56\xe8\x51\xf2\x29\xfb\x86\x4f\xed\x92\xe5\x28\x88\x80\x92\xac\x20\x13\x95\x7b\xac\x3b\xab\x2e\xfa\xd6\xfa\xb9\xa5\x8f\xbb\x12\xcb\x41\xe7\x79\x50\x19\xf3\xab\x7d\x9a\x24\x17\xfd\xe4\xc9\xc1\x64\x98\x6b\xd9\xd2\x64\x88\x74\xd2\xac\x2c\x2a\xab\x2a\x5c\xc2\x8e\x4e\xc0\xe9\x66\xb4\x80\x26\x99\xa9\x69\x05\xbb\xa6\x56\xcc\x7d\x46\xa0\x32\x79\x30\x4d\x73\x4f\x46\x41\x12\xab\x92\xa1\x56\xd4\xae\x51\xf5\x5c\x55\xce\x74\xa7\x32\x7a\x13\xda\xb4\xa0\x0a\x2a\xc5\xf8\xb3\xfa\xc0\x07\x8b\x0c\xe3\x34\x3d\x58\x75\x4b\x99\xf2\x00\x6f\xf7\x27\x25\xb2\x9a\xf3\xd7\xd4\x74\x4c\xb7\x87\x4e\xb5\x33\xe4\x02\xa7\x3f\xab\x66\xf1\xee\xc8\xd8\x54\x0c\xa5\xd2\x89\x33\x7c\x21\x9f\xec\xe8\xc1\x5d\x02\xe9\x9e\x34\x58\x2b\xab\x8f\xac\xec\x30\x4f\x1b\x6e\x95\xca\x21\x9c\x7e\x1b\xf2\x99\x1e\x6c\x0e\x9b\x43\xf9\x59\x19\xe7\x0a\xee\x60\xb1\x7c\x06\x5d\xae\x64\x2c\xd6\x99\x4d\xc1\xcf\xf7\x47\x6d\xc1\xd0\x39\x55\xa3\x49\x98\xef\xb3\x9d\x99\xf9\xcc\x2d\xfa\xe3\xac\xee\xe7\xc9\xb2\x67\x8c\xe6\x9b\x9e\x6e\x47\x74\xb4\xde\x86\xf1\x06\x41\x99\x09\x18\xa5\xcd\xd5\x91\xec\xb3\xed\xb7\xae\x5b\xab\x2d\x4c\xa2\xf4\x5c\xed\x17\xde\xc6\x96\xd3\x35\xde\xa6\x80\x27\x2b\x2c\xec\x79\x1b\x4e\x05\x00\x6d\x4a\xb6\x2a\xb8\x38\x97\x17\xd7\x48\x70\xdc\x5c\x10\x20\x06\x57\x2a\x2d\xbe\x34\x25\x51\xf6\x51\x7b\xdb\xeb\x5b\x5c\x3d\xba\x2b\x13\x1d\xea\xc7\xec\xb7\x55\xf3\x00\x3b\xab\x14\x9c\x99\x92\x2b\x4e\xd1\xb0\x28\x33\xc6\x5a\x92\x4a\xaa\x4f\xd5\x47\xb3\x51\x66\xe1\x0e\x96\xbd\x8a\x41\x4d\x47\x4a\x4d\xa1\x56\xd9\xd6\xf8\xd9\x2a\x8d\xd6\xb8\x5b\x51\x83\xa0\x29\xf8\x7d\x24\xcd\x84\xc5\xb3\x37\x26\xbb\xeb\xba\xe5\xd5\x84\x99\xa6\xc7\x7b\x8f\xa2\xd4\x7b\xee\xc1\xde\xca\x7a\xab\xb5\xfb\xad\x9c\xe9\xeb\xea\xa6\xf3\xac\x05\x82\xeb\xad\x85\xac\xd3\x2b\xb5\xc7\xd3\xb6\x5d\xe4\x7d\xc7\x30\xca\x07\x7b\xa9\xb2\x53\x68\x50\xd3\xb9\x5d\xe6\xbb\x45\x33\x97\x81\xba\x4a\x71\xcd\xa1\x5f\xaa\x56\xdf\x06\x7d\x7e\xd5\x37\xc6\x12\xc8\x06\x4c\x8d\x89\xd2\x6a\xe3\x43\x46\x5b\x3a\x4d\x7b\x9e\xfa\x97\x32\xd7\xe7\x44\x9f\xd4\x74\xa6\x4f\xcc\x17\x04\x34\xeb\x6a\x91\xf0\xd7\xd3\xce\xa8\x3a\x16\x56\x79\xdd\xee\x48\x00\x0e\xf8\x9e\x51\xb0\x13\x30\x35\x96\x2f\x27\xfe\x05\xdc\x6c\x39\x5b\x45\xf0\x42\x33\x10\x9c\xe9\x66\xa6\xb6\x3b\x2c\x6e\x2e\x1a\xb5\x85\xcc\x17\x4a\x6f\x24\x4d\xb7\x9a\xbc\x02\x46\x32\xec\x76\x2b\xe3\xb2\xe9\x52\x1d\xa5\x9d\x25\xa8\x45\xde\x15\x82\x26\xdd\x68\xe7\xf4\x4d\x56\xca\xe8\x6a\xa0\x93\xc5\xaa\x9b\xab\x07\x55\xbc\xd3\xa5\x5a\x0d\x50\xed\x49\xab\x3d\x5f\xe9\x90\x71\xfc\xb0\x7d\x5d\x4b\xb5\x5e\x78\xcc\xf9\x81\x4d\x92\x8b\x4f\x95\x7e\x34\x83\x79\xf4\x64\xe9\x3e\xf6\x93\x14\x47\x43\x41\xa0\x68\x41\x11\x20\xe2\x34\x05\x08\x80\xd1\x14\x8a\xa2\x04\x85\xe3\x91\x06\x78\x44\xd1\x1c\xc7\x29\x04\x40\x14\xa5\x00\x9a\xe5\x81\xc6\xa8\xb8\x86\x04\x9a\xd5\x68\x6d\xfb\x08\xc0\xd1\x1d\xa2\x7e\x62\x21\x92\x91\x91\xf0\x1a\x5e\x29\x6e\xfc\xc9\x4a\x26\xcc\x11\x0e\x36\x8e\x4d\x08\x72\x69\xbd\xac\x65\x37\x0d\xc6\x97\xf2\x6a\xb6\xbf\x5c\x15\x84\x15\xa5\xfb\x6e\xc3\x1a\x8b\x37\x1c\x17\x1f\x25\x89\x46\x38\xfb\x48\xfb\xa3\xcc\xb3\x7a\x6a\xe1\xb7\xb5\x7f\xbe\x07\x92\xae\x77\x82\xa5\x01\x83\xb3\x34\x54\x00\x4b\x23\x52\xd5\x14\xa0\x29\x3c\xc3\x2a\x88\xa2\x69\x9e\xe6\x19\xa4\xb2\x24\x4b\xd2\x1c\xd0\x00\x05\x35\x4a\x50\x35\x0d\xe1\x88\x15\x70\x92\xa0\x28\x85\x8d\xf5\x4e\x9e\xeb\xfd\x1f\xea\xf7\x67\xe9\x9d\xa7\x0f\xd7\x97\x7f\x83\xde\x49\x85\x84\x3c\xa9\x29\x40\x51\x70\x92\x56\x48\x0e\xe0\x2a\x45\xd0\xb8\x0a\x38\x42\xe3\x81\x2a\x28\x2a\x47\xf0\x14\x81\x04\xc4\x00\x4a\xd1\x58\x01\xaa\x80\xd2\x78\x1e\x29\x38\x54\x19\x35\xd6\x3b\xf5\xe9\x7a\xbf\xb7\xdf\x9f\xa5\x77\xae\x77\xb8\xbe\xfe\x1b\xf4\xae\x42\x45\xe1\x39\x06\xe0\x38\x42\x2c\x24\x28\x9e\x02\x10\xe1\x48\x23\x19\x02\x70\x2c\x22\x49\x95\x40\x02\x50\x48\x40\x6a\x08\xa9\x0a\xce\x71\x3c\xc3\x70\x14\x0b\x34\x48\xb2\x8c\x00\xb6\x0f\x66\x5c\xd5\x7b\x36\xb0\x29\xdb\xa7\x99\x45\xb6\x99\x5f\x3b\xad\x0c\x65\x97\xe4\xe7\x37\x82\x6b\x6f\x0c\x8f\x30\x51\xbd\x30\x9a\xb7\x06\xba\x1b\x74\x9e\xbb\xf1\x05\xdc\xdc\xdb\xde\x8d\x4e\xef\xf4\xb7\xeb\x3d\xf7\x58\xfb\x73\xf5\xbe\xf6\xaf\x6e\xf8\xa5\x3f\x63\xff\xf1\x1b\xc6\xef\xbe\x5b\x14\x6b\x7c\xa9\xb5\x6c\xcd\x94\x2a\x59\x12\xa9\x41\x7f\xda\x76\xab\xf3\xe9\x10\xc7\x51\x91\xf7\x6a\x65\x6e\x8e\xe7\xdb\xab\xca\x20\x23\x0e\x29\x71\x3f\x1f\x89\x8e\x2b\xf3\x91\xf8\x70\x17\x32\x5b\x83\x0d\xa0\x4f\xd7\x75\xd0\x6b\x0a\xac\xf4\x86\x3c\x01\xe2\xaa\xed\xca\xe3\xe1\x9b\x34\xa8\xcc\x0a\x76\x75\x37\xdf\x10\xc5\x06\xe3\x56\x93\x78\xb1\x77\x84\x83\x9c\xcd\xbd\x2d\x96\xb3\x96\xd4\xb2\x65\xb1\x62\xa0\x66\x7b\x98\xb3\x6b\x93\xa5\xbf\x51\xbb\x94\x59\x68\x66\x5b\x0c\xa1\xcf\x34\xaf\x50\x02\x92\x3c\x58\xe1\x4c\x27\xd3\x9f\x0c\xf0\xa1\x3e\x73\xf1\xac\xd4\xcc\xd3\x32\x28\xf4\xc9\xea\x5c\xf5\xa8\xf1\xaa\x36\x37\x14\xba\xdb\x76\xeb\xb5\x70\x4d\x55\xa6\xba\xce\x0c\x9f\x33\x1c\x45\xb2\x73\x93\x7c\xab\x33\x3e\x9d\x01\xd3\xb7\x36\x05\xd7\xee\x5b\x83\x6d\xdb\x6e\x7f\x25\x3f\xf7\x94\x1e\xbe\x27\x76\x45\x07\x07\xcb\x38\xda\x51\x4d\xc8\x46\x5d\xfd\x8c\x68\x75\x6f\xb4\x10\x3f\x21\x5a\xc9\xee\xa6\xdb\x7d\xa0\x7d\x51\xfc\xe7\xa2\x46\x5a\xb4\xfc\xdd\x53\x83\xfb\x9d\x2c\x7b\xc2\xfe\xcc\xe0\x1a\x64\x36\x23\x36\x68\x66\x24\xe5\x28\xbf\xd4\x2f\x34\x88\x36\x25\xe2\x75\x38\x6b\xf2\x95\x36\x6b\xc9\x84\x28\xc0\x81\xa1\x6d\xca\x7e\x3c\x5e\x97\x9d\x4c\xec\xe4\xc7\xc6\x58\x81\x85\x55\xd6\x73\xab\x92\x55\x2d\x07\x5e\x06\x67\xfa\x7e\x25\x27\xb9\xba\xed\x05\x93\x5a\x2b\xd3\x63\x87\xbd\x29\xed\xaf\x06\x9b\x89\xc7\xf5\xfc\x0e\x9d\xad\xc3\x75\xa3\xce\x56\x16\x2a\x5a\x54\xaa\x04\x3e\x30\xa5\xd9\x6c\x65\xd1\x3a\xdf\x2c\xa3\x69\xb9\x18\x3a\x42\x61\xe6\xb4\xc6\x8d\x60\xde\xdd\xbc\x29\xae\x32\x19\x2c\xdf\xda\xe5\x4e\x21\x53\xac\x18\x6d\x96\x9c\x72\x5d\x4b\xa6\x56\x82\x23\x4e\xea\xdc\x68\x4f\xec\x9f\x77\xb2\x47\x8d\xfc\x51\x27\xab\xaf\x6a\x73\xf7\x13\x9d\x4c\xe4\x46\x35\x5e\xe4\xa6\xa6\x9e\x6f\x42\x5c\xeb\xf5\xb8\x7e\x49\xcd\xb5\xd6\x6c\x2b\xb3\x32\x4b\x0b\x95\xea\xe5\x08\x06\x54\xa8\xb2\x41\xc4\x98\xbf\xdb\xc9\x3e\x79\x1e\x78\xbf\x93\xe5\x4e\xd8\x9f\x19\xdc\x47\x57\xd6\x97\x9d\x2c\x57\x09\x4c\xc2\xaf\x15\x6b\x05\xba\xbf\x5e\xf9\xb8\x96\xcb\xf6\xf3\x88\xf5\x15\xc6\xa4\x95\x4d\xdd\x2d\xea\x59\xe7\xd9\xec\x8f\xeb\xf3\xb5\xea\x33\xb4\x21\x23\x72\xbe\xf6\xa7\x6b\xb6\xae\x31\xe3\x0a\x9d\xa7\x73\xa6\xea\x21\x9a\xcd\x8b\x13\xa9\xd8\xe9\x35\x3d\x8b\x47\xa3\x5c\xe8\x08\xd5\x0d\x9b\x25\xd9\x60\x55\xa9\x55\xd8\x5c\xae\x20\x6e\xac\xd2\xc8\xad\x2c\x4b\xc5\x5c\x89\x11\x40\x43\x00\x70\x33\x25\xcb\xcf\x2b\xae\x70\x48\xd1\xfc\xf3\x4e\xf6\xa8\x91\x3f\xea\x64\x35\x7c\xc6\xe6\x3e\xd1\xc9\xa4\x00\x64\x95\xfe\x70\x4c\xe6\xcc\xe1\x00\xb8\x7d\xb6\xb7\x5e\x29\x03\xaa\x28\x57\x74\xc7\xa2\xc4\x4e\x76\x52\x2e\x38\x8c\xb2\xee\x94\x07\xb1\x53\xfd\x6e\x27\xfb\xe4\x49\xff\xff\xf8\xf4\x55\xec\x10\x6f\x6e\x50\x1a\xe8\xb9\x8c\x32\x69\xe4\xdc\x65\x4e\xa8\xb4\x1d\x92\x37\x56\x68\xee\x56\x0d\x2a\xb3\x59\x69\x74\x7f\x28\x2f\x28\x63\x3c\x3e\x70\x4d\x77\xb6\xfc\xf1\x80\x1e\x39\x5b\xd4\xb1\x38\x38\x1d\x7c\xf6\x9e\x69\x75\xc5\x3a\x8c\xc3\x85\xe3\x42\xa2\xe1\xc8\xd9\x3f\xa5\xfd\x0b\x81\xe6\x7a\xfb\xf1\x45\x67\xc1\xe6\x3f\xe8\x6c\x62\xca\x62\xf3\xc3\xed\x3f\x9b\xca\xe2\x81\xf6\xeb\xe2\xe7\x2e\x76\xef\x4a\x2e\x3d\xd2\x3e\x6d\x09\xb3\x3b\xdb\xbf\xba\xd8\xbe\xf2\x05\xec\x8f\xae\xb8\x4f\xbe\x84\xbd\x8b\xa3\xc9\x25\x73\xfc\x55\xa7\x83\xaf\xa6\x04\xe2\xcb\x28\xfb\x35\x41\xfc\x05\xa9\x33\x94\x8b\x45\x23\xcf\x6b\xe9\xee\x8b\x65\xee\xbe\xa8\xfd\x4e\x37\x93\x45\x7d\xa3\x52\xbe\x09\xc4\x68\xaf\x52\xcc\xe5\x52\xca\xdb\xed\x1b\xc4\x9a\xed\x72\x5d\x6c\x8f\xb0\x6a\x7e\x84\x7d\xdb\x57\xfe\x78\xd9\x97\x95\xbc\x58\xe4\xf1\xa4\xbc\xf0\x67\x11\xf7\xae\xb0\xf6\xae\x51\x4e\x2b\xba\x77\xa8\xa8\xfc\x30\x3d\x05\x58\x69\xcc\x76\x0d\x1c\x93\x8a\xeb\xfc\x5c\xaf\xc8\x77\xb5\x94\xf4\xc3\x74\x8f\xc0\xd3\x88\x5f\x69\x1d\xeb\xc9\xe5\x56\x2f\x8f\x1d\xbe\xfc\xff\xa1\x9e\x7c\x8e\xbe\x3f\xd8\x81\xf3\x31\x38\x14\x29\xbc\x50\x60\xef\xa8\x00\xf9\xc3\x7c\x63\xb0\x34\xa2\x89\x66\x8e\x19\x6e\xcb\xbe\xa5\x97\x7b\x4b\x56\x5b\x7f\x98\x5c\x84\x95\xc6\xed\xd0\xc8\x31\x35\xc3\x79\x89\x8a\xc0\x5d\x2d\x6f\x96\x52\x65\xfe\x71\xa6\x09\xc8\x54\xc2\xa7\x4d\xa6\x0d\xfa\x85\x1a\x68\x47\xb5\xf6\x3f\x83\xa9\x17\x28\x17\x38\xee\x9a\x39\x66\x17\x95\x4f\xbb\x52\x10\xed\xec\x05\x03\x0f\x93\x3c\x00\xa6\x11\x3d\x69\xee\xa6\x18\x96\xf8\xb6\xca\xe9\x6b\x16\x1e\x66\x7b\x00\x4c\x63\x7b\xd2\xdc\x31\xdb\x5d\x59\xb1\x2b\x65\xbb\xce\x5e\x37\xf1\x38\xdf\x3d\x60\x2a\xdf\xe3\xe6\x8e\xf9\xee\x2b\x7f\x5d\xa9\xe8\x75\xf6\xde\x8d\x87\x09\x1f\x00\xd3\x08\x9f\x34\x77\x71\x6a\x10\x57\xf4\x7a\x39\x94\xeb\xba\x5a\xb2\x29\xe5\xbd\x24\x8f\xf7\x23\x01\x99\xda\x93\xd3\x26\xd3\xa2\x84\x07\x17\x2f\xbb\x1a\x45\x97\xcb\x29\x9d\xbe\xaf\xe5\x13\xb8\x5f\x25\xfe\x10\xeb\x93\x29\x6a\xda\x5b\x6c\x1e\xe6\x7f\x0c\x9a\xd6\x89\x94\x66\x2f\xf6\x24\x59\xea\xe8\xd2\x54\x73\xff\x86\x9f\x2d\xf7\xe8\x7d\x40\xb7\x15\x61\x8a\x5f\x1d\x74\x82\x83\x35\xe4\xc3\x2c\xb6\xd7\x29\xcb\x45\x4c\xf1\x5d\x08\xb1\x5d\xc5\xb5\xef\xd8\xa0\x94\x6f\xe7\xb1\x64\x05\xb6\x43\x59\x9d\xf3\x89\xe6\xf1\x4b\x8b\xee\x65\x79\x0c\x13\x92\xdc\xce\x53\x8e\x28\x26\xcb\xf0\xbe\x24\x8b\xee\xbe\xc4\xc5\x70\xcf\xd8\x1d\xde\xbc\x74\x2f\xb1\x3d\x42\xc8\xe9\xe0\x18\x47\xb4\x2e\x4f\x15\x8f\xde\x11\xf5\x08\x85\x1d\x48\xcc\x22\x11\x60\x6e\x24\x72\xf4\x5a\xab\x7b\x89\x24\x41\x42\x22\xc7\x13\xe1\x1b\x99\x1c\xbd\x8a\xeb\x5e\x26\x49\x90\x90\x49\xe2\x3b\xc1\xb7\xd3\x38\x7a\x7d\xd8\x03\x4c\x92\x38\x5b\x32\xbb\x59\xc2\x31\x99\x64\xa9\xcd\x6b\xe1\xeb\x31\x46\xa7\x40\x21\xa5\x93\xd8\xf8\xae\x8e\x2e\xbd\x35\x0f\x53\xed\xb9\x63\x42\x1f\x46\xcd\xfe\xbf\x00\x00\x00\xff\xff\x1d\x97\x73\xe9\x81\x6f\x00\x00") +var _baseCoreSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x7d\x69\xb3\x9a\x4a\xf7\xef\xfb\x7c\x0a\x2a\x6f\x76\x52\x3b\xe7\xc8\x3c\x24\x37\x4f\x15\x0a\xce\xe2\x3c\xde\xba\x95\x6a\xa0\x41\x94\x41\x19\x44\xbd\xf5\x7c\xf7\x5b\x80\x03\x2a\x0e\x7b\xbb\x73\xce\xbf\xea\xc6\x17\x3b\x11\x16\xbf\x5e\xfd\xeb\xb5\x56\x4f\xb8\xda\x0d\x6c\xdb\xb0\x75\xc4\x85\x8a\xb1\x80\x9f\x92\x7f\x10\xcd\xb0\x0d\x6f\x0a\xd5\x4f\x7f\xfd\xf5\xe9\xaf\xbf\x90\x96\xe3\xf9\xba\x0b\xbb\xed\x3a\xa2\x02\x1f\xc8\xc0\x83\x88\x1a\x58\x8b\xe8\x76\x74\x5f\x08\xac\x05\x54\x11\xcd\x75\xac\xa3\xc0\x0a\xba\x9e\xe1\xd8\x08\xf7\x37\xfd\x37\x96\x92\x92\x37\xc8\x42\xff\x15\x3d\x7e\x26\xf2\xa9\x2b\xf6\x10\xcf\x07\x3e\xb4\xa0\xed\xff\xf2\x0d\x0b\x3a\x81\x8f\xfc\x44\xd0\x1f\xf1\x2d\xd3\x51\xe6\x97\x57\x0d\xd5\x84\xbf\x0c\xfb\x97\xef\x02\xdb\x03\x8a\x6f\x38\xf6\x2f\x0f\x7a\x11\xee\xa5\xb0\x62\x1a\x11\x34\xb4\x15\x47\x8d\x6a\xfd\x13\x79\xe9\xf7\x8a\xec\xcb\x8f\x7d\xd9\xb6\x0a\x5c\xf5\x97\xe2\xd8\x9a\xe3\x5a\x86\xad\xff\xf2\x7c\xd7\xb0\x75\x0f\xf9\x89\x38\xf6\x0e\x63\x0a\x95\xf9\x2f\x2d\xb0\x93\xb2\x64\x47\x35\x60\x74\x5f\x03\xa6\x07\x4f\x8a\xb1\x0c\xfb\x97\x05\x3d\x0f\xe8\xb1\x40\x08\xdc\x88\xea\x44\xc4\x75\xc2\x5f\x1e\x54\x02\xd7\xf0\x37\x11\xb8\xa6\xfd\xd8\x11\x00\x81\xab\x4c\x7f\x2d\x80\x3f\x45\x7e\x22\x8b\x40\x36\x0d\xe5\x5b\xc4\x98\x02\x7c\x60\x3a\xfa\x8f\x4f\x9f\x84\x4e\xb3\x85\x54\x24\x41\x1c\x21\x95\x22\x22\x8e\x2a\xdd\x5e\x77\x27\xf9\x77\xb0\xd0\x5d\xa0\xc2\xa9\xe1\xf9\xf2\xc6\x83\xcb\x1f\x37\xa5\x3d\x65\xb1\x0c\x1c\x37\xb0\xbc\xc7\x84\xa1\xbd\x7a\x44\xd2\x84\xaa\x0e\xdd\x47\x24\x23\x3d\x35\x08\x1f\x94\x7c\x40\x4c\x86\x9e\xef\x68\x1a\x74\x0d\x5b\x85\xeb\xdb\xb2\x40\x51\x9c\xc0\xf6\x65\x60\x02\x5b\x81\xde\x8f\x4f\x7c\xbd\x27\x76\x90\x1e\x9f\xaf\x8b\x29\xe9\xa6\x54\x1f\x67\xd0\xeb\xb8\x1b\x24\x46\x2f\x34\xa5\x6e\xaf\xc3\x57\xa4\x5e\xea\xa1\x53\xc1\x5f\x8b\x39\xdc\x3c\x82\xef\xaf\xef\x43\x1f\x64\xde\x80\xaa\xc1\x07\x74\x4e\x8b\x3d\x8e\xed\x06\x9e\x6f\x1a\x36\xf4\x6e\x21\x1f\x84\x1e\xc6\x8d\xb4\x80\x71\x34\xb8\x81\x7b\x14\x7a\x1c\xf7\x60\xf2\xb7\x70\x0f\x42\x0f\xe3\x26\xf2\x86\xad\x39\x37\x70\x8f\x42\x0f\xe3\x2e\x02\xd9\x0b\xe4\x1b\x98\x89\xc0\x5b\xf0\x4c\xc3\x9b\x2e\x03\x18\xdc\x62\x36\x2d\xf6\x38\x36\x84\xee\x2d\x5a\xe3\xfb\x0f\xa3\xc5\x6e\x7c\x0b\x2e\x11\x78\x18\x2f\x89\x4a\x53\x08\xd4\xdb\xb0\x27\x72\xbf\x19\x7d\x17\x29\xe1\xf2\xd7\x83\xc5\xc8\xc0\xbe\x01\x2e\x03\xfb\x61\x85\x77\xd1\xef\x96\xae\x7b\x91\xb7\x62\x46\x63\x80\xfb\xb0\x91\xd4\x0e\x39\x96\x3d\x07\xce\x0c\xb9\xb7\x65\x0f\xa1\xf1\x9e\xd8\x31\xd0\xdd\x91\x3c\x04\xae\xdb\x72\xc7\x40\x74\x47\xee\x10\x58\xee\xca\x3d\xa4\xdf\x31\xa0\xdc\x96\x4b\x82\xc4\x5d\x99\x83\xcb\xdf\x91\x8c\xfc\xf8\xb6\x48\xe2\x9b\xb7\x65\x4e\x5c\xe1\xb6\xa8\x0c\xec\xdb\x02\x7b\x53\x7d\x48\x2a\xb2\xbc\x9d\xa0\x38\xea\x89\x52\xb7\xd2\x94\xd2\xc2\xe6\x42\xf7\x96\xe6\x4e\xa2\x5b\x28\x8b\x0d\xfe\x02\xeb\xc7\x6e\x6c\x2c\x01\x0b\x7e\xdf\x5f\x43\x7a\x9b\x05\xfc\xbe\x7b\xe4\x07\xd2\x55\xa6\xd0\x02\xdf\x91\xbf\x7e\x20\xcd\xd0\x86\xee\x77\xe4\xaf\x78\xc8\x5c\xe8\x88\x7c\x4f\xdc\x23\xef\xf1\x3e\x9d\x20\x9e\xde\xdc\x01\x17\x9a\x8d\x86\x28\xf5\x6e\x20\x27\x02\x48\x53\x3a\x05\x40\x2a\x5d\xe4\x65\x3f\xbe\xdd\x5f\xf3\x62\x90\x97\xf3\x92\xf7\xd5\xdf\x95\x79\x60\xe8\x6e\x7d\x4e\xb8\x94\x9a\xbd\x33\x3e\x91\x61\xa5\x57\x3e\xa8\x95\x1e\xd0\x9e\x14\x7f\x44\x39\x53\xe4\x2d\x95\xbf\x00\x89\x09\x68\xd5\x73\x0b\x3d\x9a\xc5\x2c\x5c\x47\x81\x6a\xe0\x02\x13\x31\x81\xad\x07\x40\x87\x31\x0d\x0f\x0e\xc0\x23\x31\x15\x6a\x20\x30\xfd\x5f\x3e\x90\x4d\xe8\x2d\x80\x02\xa3\xd9\xc4\xcb\xd9\xdd\xd0\xf0\xa7\xbf\x1c\x43\x4d\x4d\x10\x4e\x2a\x9b\x36\xc8\x5d\x35\x63\xd3\x3d\x56\x72\x6f\x00\x59\x84\x27\x56\x9e\x0e\xba\x5f\x3e\x21\x08\xb2\xbf\x62\xa8\x88\x32\x05\x2e\x50\x7c\xe8\x22\x2b\xe0\x6e\x0c\x5b\xff\x42\xd1\x5f\xe3\xb6\x91\xfa\xf5\xfa\xb7\x58\x3a\x7a\xd0\x06\x16\xcc\x10\x66\xd9\x2c\xe1\x15\x30\x83\x2c\x69\x0c\xc3\xcf\xc5\x4d\xe0\xf9\x96\xa3\x1a\x9a\x01\x55\xc4\xb0\x7d\xa8\x43\xf7\x20\xf2\xe9\xeb\x79\xdb\x1f\xbc\xf8\x49\x2e\xbc\x77\x11\xb1\x9b\x08\x20\xb2\xa1\x1b\xb6\x7f\x7e\x33\x88\x9e\x32\x0d\x20\x1b\xa6\xe1\x47\x53\xbe\x44\x2c\xb9\xeb\x41\xd3\xbc\x79\x7b\x69\x07\x56\x36\xb0\x1d\x58\x5e\x20\x43\xdb\x77\xa3\xa7\xce\x39\x4a\x64\x0c\x5b\x33\x41\x34\xdd\x54\xa1\xe7\x67\xd7\x25\x11\x9c\x3a\x16\x54\x1d\x0b\x18\x76\x86\x14\x49\x9e\xd7\xd8\x9f\xba\xd0\x9b\x3a\xa6\xea\x21\x3e\x5c\x9f\x6b\xa6\x99\x40\xbf\xa6\x91\x67\xe8\x76\x34\xd0\x89\x1e\x7b\xa0\xa9\x13\x91\xd4\x68\xe0\x30\xb4\xd8\x91\xfe\x2b\x9e\x56\x23\x85\xb2\x58\xa8\x21\x5f\xbe\xec\x9b\xe2\x3f\x3f\x11\xf4\xeb\xd7\x1b\x4f\x9f\xb7\xca\x39\xce\x45\xab\xdd\x43\x3c\x69\x8e\x33\xb4\xd3\xa6\xba\x87\x74\x69\x13\x67\x70\x19\x46\x93\x60\x5e\x3a\x46\xd4\xff\xbd\xd7\x27\xa2\x21\x63\xe2\x0e\xb6\xa3\xc2\xb4\x2f\x9c\xf8\xc0\x65\xa1\xa7\xfd\xf3\x7b\x8b\x3f\x1d\x18\x27\x8a\xec\xae\x01\x6f\x9a\x52\x86\xbe\x30\xcf\x85\x0b\x57\x77\x85\xe4\x40\x99\x43\xdf\x34\x3c\xff\xae\xe8\x61\xb4\xbd\xb7\xcf\xe4\xb2\x62\x3a\x1e\xf4\x0d\xeb\x8a\xe7\xc7\x81\x35\xc3\x3d\x52\x6d\x7e\x3a\xa8\x3f\xe0\x9d\xb5\xf7\xb1\x9c\x2b\xa6\x73\x6d\x6e\x70\x0a\x73\xac\xc5\x35\x6b\xd9\x0d\xbe\xde\xdb\x62\xbb\x89\xd7\x97\x43\x64\x83\xee\x83\x11\x34\x59\x79\x51\xb3\x79\xdc\x99\x3b\xf0\x3c\xe8\x67\xf1\x99\xf8\xea\xd5\xdb\xc0\x8a\xdc\x2a\x1b\x7a\xe1\x1a\x0a\xb4\xaf\x44\x9d\xf8\xe6\xb5\x90\x14\xdf\x44\x54\x27\x90\x4d\x18\xd9\x9b\x62\xc4\x2b\x92\x8f\x07\xc2\xb7\x85\xbd\xdd\x94\x35\xa9\xcb\x59\xbb\xee\x2a\x78\xc5\x36\x76\x4f\xee\x18\x3e\x7b\x74\xcf\xfb\x35\x83\x48\x06\xec\xef\xb5\x87\x64\x5a\x9f\x98\x83\xb1\xc8\xea\xf8\xa9\x0b\xcf\x75\x5c\xff\xc0\x86\x20\x16\xf9\x7e\xbd\x87\xa0\xe7\x3d\x1f\x5c\xfb\xc0\xf7\xa1\xb5\xf0\x91\xc8\x2d\x3c\x1f\x58\x0b\x24\x1a\x32\x39\x41\x72\x05\xd9\x3a\x36\xbc\xec\x2f\x35\x60\x98\x81\x9b\xea\x2d\xaf\x95\xe0\x6f\x16\xf0\x7e\xa3\x24\xcb\x12\x29\xdc\xcb\xb0\x7f\x28\xf1\x4a\xeb\xec\x56\x36\x1c\xf7\xbc\x51\xbf\xc4\x4c\xfc\x07\x41\xbf\x22\xbc\x24\x20\xc9\xd7\xff\xf5\x13\xa1\x29\x8a\xa0\xbe\x66\xb6\x55\x7a\x1a\xf6\xee\x26\x4b\xaf\xf2\xa4\x63\xee\xb5\xee\x3c\x5e\x68\x8b\xbc\x2e\x53\xa1\x68\xee\xf8\x84\x2a\x5e\x20\xef\x94\x70\xa1\x77\xd2\x01\x11\x99\x23\x46\x17\x82\x83\x2f\x5d\xea\x93\x9a\xf3\xbe\x57\xa7\xd4\x62\xdd\x03\x3d\x63\xa2\xd8\xd2\x83\xb7\x7a\x98\x4b\x3d\x53\x73\xf8\xf7\xea\x79\x84\x78\x5c\xcf\x8b\x4e\xee\xec\x3e\xb4\x57\xd0\x74\x16\xf0\x4e\x97\x76\x2c\xfa\x89\x8e\x28\xb5\xdc\xf1\x04\x05\xfb\xf5\xda\x2f\x8f\xb4\xc3\xd1\x8a\xee\x11\xb1\xbc\xd2\xd1\x9c\x92\xb0\x5f\x07\x3e\x41\x3c\x27\xe2\xa4\xb4\xab\x64\x1c\xd7\x88\xde\x4d\xc6\x71\x51\xfc\xcb\xd1\x6f\x4f\x27\x6f\x19\x3e\x75\xcb\xbb\x53\x2b\x5c\xef\xd5\x2a\xb5\x05\xf0\x9e\x69\x57\xdc\xe3\xdf\x88\xd4\x86\xe7\x05\xd0\x7d\x1c\x4a\x71\xd4\xcc\xd9\xe9\x05\x2d\xbe\x69\x58\xc6\x95\x11\xc5\xef\x9b\x0b\x7e\xdc\x60\x22\xb5\xab\xf2\xae\x59\x54\xfa\xf9\x8f\x9a\x47\xa5\x30\xdf\x3f\xff\xb9\x85\x9a\x34\xda\x19\xd2\xae\x25\xff\x93\xed\x78\x27\xcb\xbd\xef\x36\xf2\xf4\x1e\x5a\x62\xe6\xfe\xfa\x24\x14\x3f\x30\xdf\x38\x37\xc0\x75\xbc\x4b\x79\xf5\xae\x32\x05\xb6\x0e\x33\xe7\xe6\x69\x72\xd2\xdb\x76\xef\x8f\xd5\xc7\xb5\xf3\xf7\x53\xf4\x0f\xf3\x23\x3b\xea\x26\x8b\x1c\x7f\xed\x42\x2f\x30\x33\xa3\xbb\xbf\xb6\xe0\xdd\xf9\xdc\x71\x8b\xf5\xfd\x7c\x9e\xed\x5b\xbc\x97\xd4\xb3\x1d\xe7\x2f\x0f\x11\xb7\x7b\xe8\x16\x7b\x3b\x91\x2c\x22\x1e\x33\xbb\xb3\x1d\xee\xf7\x10\x25\x44\x33\x6b\xcd\x71\xef\x2c\x86\x22\x02\xdf\xe3\xef\x70\x76\x1b\xd2\x7b\x33\x5e\x45\xea\x8a\x9d\x1e\x52\x91\x7a\xcd\xe3\xa2\xe2\x80\xaf\xf7\xc5\x2e\xf2\xe5\xa5\x94\xef\xb4\xc6\xe5\x4a\x1d\x2f\x54\x88\xa2\xd4\x26\xf3\xa3\x7a\xb1\x21\x09\xf5\x62\xb5\x2f\xb5\xfa\x78\x79\x4c\x4c\x1a\xc5\x6e\xb9\x29\xf5\x0b\x62\x93\xef\x0e\x99\x76\x81\x69\x8e\xf0\xf2\xcb\x37\x84\xdb\x7d\xe8\xe4\x1f\x06\x45\xbf\x25\xfc\xee\xfe\x12\xdf\x90\xc3\x95\x97\x97\x6f\xc8\x0b\xdf\xe6\x79\x9e\xff\xf9\xf3\x25\x75\x03\xff\xfa\xe3\x9e\x86\x3c\x35\xcc\xb7\xc6\x3c\x35\x26\x87\xbc\x58\x1e\x0d\x3b\x78\xbf\xd6\xc4\xfb\x4d\x32\xdf\x2f\x95\xfb\x6d\x86\x14\xfb\xad\x5a\x53\xc2\xdb\xe5\x01\x39\xec\x94\x9b\x95\x8e\x54\xab\x95\xf1\x97\x6f\x08\x86\xee\x3f\xa7\x9a\xb1\x14\xcb\x71\x04\x49\x71\xf8\x07\xa9\x58\x18\xd5\x4a\x74\x47\x22\x9b\x52\x45\x6c\x15\x1a\x52\x31\xcf\x10\x38\x4f\x12\xf4\x84\x6a\x49\x42\xb7\x53\x2f\x0d\x6b\x4c\x29\x5f\x2f\x34\xda\xf5\x4a\xb1\x49\x76\x19\x71\x3c\x1c\xf4\x23\x12\xc9\x98\xbc\xab\x1a\x3e\x44\x22\x71\x5f\xc3\xfc\xa8\xd4\xae\x0e\x07\xf5\x61\x73\x5c\x2e\xd6\x07\xbd\xda\x70\x40\x15\x4b\x65\x9e\xa8\x4b\xe3\x31\x5e\x6d\xd7\x1a\x4c\x93\xaf\xf2\x7d\xb1\x5d\xec\xd3\xf5\x56\xa1\x2b\x16\x07\xa3\xa6\x14\x93\x48\x7d\x00\x89\xc4\x75\x67\x39\x5f\x11\x7c\xc2\x49\xae\xaf\xf3\xbd\xd5\x53\x4e\xd7\xfa\x0e\x3c\xd2\x84\xca\xb1\x1a\x45\xd0\x10\xd2\xac\x8a\xc9\x38\x23\x53\x32\xcb\x69\x38\x01\x34\x8a\xc0\x30\x99\xa1\x68\x0e\xe0\xa4\x06\x34\x8c\x44\x09\xa0\xa2\x32\x85\xcb\x34\x41\xc8\x28\x23\x43\x8e\x8b\xe8\x41\x9f\xfc\x44\x18\x14\x83\x03\x1c\x12\xb8\xa6\xe1\x24\x0b\x50\x46\x46\x21\x83\x6a\x2a\xa6\xd1\x2a\x81\xb1\x0a\xa6\x01\x45\xc5\x51\x99\x56\x14\x94\x55\x08\x42\xa5\x18\x86\xc2\x29\x8e\xa5\x59\x0c\xa7\x00\x46\x47\xed\x1a\xb7\xce\x0b\xff\x3f\xf6\x93\x1f\xd5\x0c\x72\x93\xdb\x74\x6b\x79\x46\xb0\x05\xae\x8c\xa3\xeb\x59\xfe\xd5\x43\x75\xdf\x0b\x2b\xe1\x16\x1b\xa9\xdd\xe1\x18\xe4\xab\xa0\xa8\x47\xf2\xa2\x44\xd6\xc1\x76\x81\xb7\xef\x22\x4f\xf8\x11\x46\xc6\x62\xf9\xf9\x3f\x50\x91\x0f\xfd\xbc\x9c\xf9\xfa\x15\x43\xe5\x64\x46\x61\x65\x0d\x60\x80\x53\x62\xc3\x64\x71\x1a\x45\x19\x8d\x43\xb5\xc8\x46\x81\x82\x92\x04\x54\x31\x12\xc7\x01\xa1\xe0\x1c\x8e\xb2\xac\x82\x13\x18\xa0\x71\x94\x86\x34\x1d\x1b\xd9\x47\x18\x3b\x0b\x65\x9a\x50\x31\x0a\x70\x90\x55\x70\x92\xa4\x39\x4d\x41\x09\x16\x97\x51\x9c\x66\x58\x59\xe6\x20\x60\x38\x20\xcb\x9a\x8a\x92\x2c\x8d\x69\x0a\xcd\x11\x8a\xc2\x92\x28\xa4\x21\x64\x30\xf8\xf2\x0d\xc1\xbf\x21\x18\xc5\xe0\x14\xce\x70\x2c\xb5\xb7\xd8\x52\x6b\x32\xc3\xa4\x80\x72\x50\xb9\xca\x0c\x49\x7b\xd3\x5c\xf5\xd7\x25\x62\xb0\x70\xe6\xaf\xab\x22\xdf\xf4\x0b\x58\x0d\x6f\x30\x79\x86\x9e\x98\x96\xa8\x36\x17\x83\x42\x83\x2a\xd7\x5d\xae\x28\xcd\x28\x6a\x09\xe8\x10\x2f\xd7\x1a\xfe\xb2\xd7\x2a\xd6\x57\x25\x76\xd3\xea\xe7\x00\xef\xc4\xd0\x23\xd9\xd5\x94\x94\x11\x75\xfa\xfc\x60\x5d\xb5\x30\x53\x68\x84\xe1\x32\x98\xd5\x94\x4d\x7b\xeb\x71\x4c\x31\xc7\x8b\x3d\xa3\xa0\xb7\x5b\x6e\x48\x13\xe1\x12\xb4\x4a\x4d\x7f\x86\x0e\x96\x70\x56\xe8\x94\x6c\x96\x27\x6b\x61\xd5\x36\x18\x7b\x09\x41\x90\x43\xc5\xe9\x34\x57\x9a\xb3\x1b\x51\xb0\x18\xbb\x1c\x5b\x6c\x25\xc3\x62\x45\x2f\xab\xd5\xff\x3f\xb0\x58\x8a\x02\x1c\x26\x53\x34\xcd\x2a\x24\x04\x1c\x25\x2b\x9c\x86\x6a\x28\x49\x02\x59\xc3\x15\x02\x55\x08\x96\x06\xaa\xca\x32\x0c\x81\x42\x19\x52\x34\x29\xab\x14\xa5\xa2\x1c\xa0\x55\x8d\xc1\xb4\xc8\xda\x3e\xc2\xea\x35\x8d\xd1\x14\x02\x25\x69\xc8\xe2\x38\x1e\x15\x86\xe2\x28\x89\x2a\x32\x05\x81\xca\x01\x95\x62\x69\x8c\x92\x31\x85\x91\xb9\x08\x0f\x42\x8c\xd2\x14\x1c\x25\x55\x85\x52\x54\x86\x05\x2f\xf1\xc8\xe7\x60\xb1\xf4\xce\x62\x85\xaa\xcf\x1a\x39\x07\xd8\xc5\x46\x27\x28\x8c\x79\x8d\x12\x18\x75\xe8\xf2\xed\x57\xb4\x5f\x59\xb6\x0a\x73\xdd\x68\x54\xd6\x0b\x23\x1f\x4c\xf4\x6e\x0b\x03\x0d\xa7\x35\x5e\x10\xcb\x42\xb7\xa0\x4d\xb0\xfc\x6c\x38\x5c\xdb\x1b\xcf\xd7\xdc\x8d\xdb\xb6\x25\x4a\x83\xec\x78\x32\xc1\xd6\x4a\xcc\x70\x6c\xb1\x89\x45\x1d\xfe\x24\x9d\x70\x78\xfc\x1e\xf2\xad\xf6\xce\x76\x8a\x8d\x5a\x75\x05\xe8\xb6\xd5\x34\x85\xba\x0f\x67\x63\x79\xba\x18\x57\x98\x6e\xbf\xd6\xd4\x60\x55\xae\xa8\xf3\xe5\x8c\x0b\x9b\x18\xef\xbb\x39\x8d\x6d\x88\xb2\x53\x31\x94\x90\x2c\xe4\xf9\x0d\x46\xfb\x96\x3f\x2c\x15\xe5\x72\x39\x00\xa1\xc8\x4c\x47\x6c\x45\x24\x8a\xdb\x91\x11\x97\xdf\xc8\xb0\xe8\x52\x66\x40\xde\x5b\xb4\x80\x56\x7f\xa3\xed\xfd\x9e\xcf\xcb\xf5\xc1\x4c\xc6\x86\xd5\x13\xe3\x99\xcb\xdd\x8e\x67\xc0\xae\x2d\xc7\x3f\x87\x79\xbe\xa2\xfe\x04\xda\x95\xf5\xf0\x27\x10\xaf\xac\x5c\xbf\x75\x34\x98\x5a\xbd\x3e\x0e\xa9\x05\xaa\xc6\xf4\xc7\xa3\xd2\xa8\x31\x10\x3a\xa3\x56\x69\xc4\x17\x8b\x74\x9e\x6c\xb5\xf2\x23\x4a\x1a\xb7\x8a\x0d\xaa\x2b\xd5\xba\x4d\x91\xa9\xb7\xcb\x93\xfa\xa8\x81\xb7\x85\x51\x4d\x2a\x26\x3d\x5a\xd2\x8b\xb5\x96\x2b\x74\xad\x60\xf8\xa0\x5c\x62\xb6\x23\xbe\xbf\xd2\x59\xc8\x4a\xaf\x7e\xc8\xba\xcd\x0d\x58\xf6\x0c\x6d\xc4\xbf\x0e\x88\xc9\x92\x67\x8e\xd6\x17\xbb\x73\x21\xfe\x6f\xe4\x59\x42\x61\x38\xee\xa0\xb4\xd9\xaf\x6c\xe7\x4a\xb0\x42\xfb\x78\xcb\xb6\x16\xae\x2b\x8c\x15\x67\x83\x2f\x1a\x6c\x3f\x60\xe5\x6d\x85\xeb\x71\x4e\x21\xf6\xd1\xbc\x1a\xf8\xdc\xfa\xcc\xa4\xf3\x9c\x98\xb3\x78\x47\x9b\x43\x65\x5d\xd4\x5e\xcb\xd4\xa8\xc6\x97\x37\x75\xe0\x85\xc5\x65\x2d\x6c\x1b\xdb\x76\x61\x5d\xd1\xa9\x3e\x35\xad\x46\xe5\xe7\xf9\x21\x5d\x9b\x0d\xc1\x6a\x5d\x2b\xb7\xa4\xea\x64\x3d\x0a\xf1\xc9\x04\x27\x1b\x50\x19\x99\xb9\xfa\x8c\x30\x2d\x77\xd1\xc3\x60\x8e\x60\xa1\xc3\x04\x2e\xd8\x58\x1d\x55\x9a\xbb\x5a\xab\x34\x18\xa8\xcd\xb9\xd2\x2e\x35\xb7\x4e\xdf\xe5\xdb\xb9\x76\x50\x02\x2a\x41\x35\x46\xeb\x0d\xdf\xfe\xf9\xf3\xbc\xb7\xf8\x60\xea\x89\xa7\xa8\x6f\x9c\x52\x2f\x4e\x2b\xb9\x81\xb3\xd1\x39\xc3\x86\x4e\xb5\x52\xe5\xec\x7e\x49\x9a\xc8\x25\xad\xb6\xc1\x5e\x57\x35\xbf\xa0\x10\x26\xc7\xac\xa7\xa6\x3d\x2a\x86\x07\xea\x37\xf1\xf3\xfa\xe1\x4f\x3e\xfa\x23\x1c\x2f\x0a\x3c\xcf\x15\xda\x4f\x34\x4d\xc1\x85\xb0\x8b\x4e\x3b\x15\x2d\x5c\xb4\x79\xde\xa8\x07\x39\xc6\xcb\xb9\xae\xd9\x35\x47\xab\xae\xdd\x81\x92\xd7\x1a\xd5\x75\x42\x12\x66\xfc\x46\xce\x8f\xf3\x66\x55\x70\xc9\xc6\x4c\x92\x8b\xde\xa0\xc9\xda\x8b\x85\xc1\xac\xa8\xf9\x34\x47\xcc\xe9\x6e\x57\x93\x27\x50\xb5\xf3\x7c\xd2\x34\xd7\xfd\x2c\x6b\x7b\xe4\x1d\x7e\xb6\xdf\x22\x39\x34\xb6\x46\x92\x1a\xa4\x51\x9c\x65\x20\xc9\x70\x8a\x42\x52\x8c\x06\x31\x8d\xa3\x14\x80\xa2\x51\x77\x4f\x03\x45\x41\x29\xc0\x02\x05\x43\x59\x99\x20\x51\x5c\x21\x59\x96\xe0\x28\x82\x63\x49\x0e\x4f\x37\x7a\xcc\xaa\x18\xff\xf7\x75\x99\xeb\x15\xb7\xa3\x49\x5f\x91\x57\x92\x92\xef\xbe\x0a\x6b\x26\x24\x68\x42\xd8\x78\x4c\x75\xb9\x68\x56\x39\x45\xa0\x46\xaa\xe5\x08\xde\xbd\x98\x7f\x65\x3b\xe4\xcd\x95\x3f\x6e\x89\x1c\x2a\xbf\xff\x0d\x52\xf2\x82\x23\x72\xf1\x89\x06\x2c\x18\x7a\xe1\x33\x19\x48\x36\xf4\x43\xc7\x9d\x2f\x80\xe7\x2d\xa6\x2e\xf0\x60\x06\x52\x0f\x7a\x3e\xd2\x15\x8a\x88\x94\x08\x23\x3f\x90\x2e\x5c\xf8\xd0\x92\xa1\x8b\xe0\x28\x46\x3d\x52\x90\xe6\xb8\x0a\xf4\x94\x85\x63\xdb\x70\xed\x9b\x20\xb0\x95\xe9\x79\x41\xf1\x0b\x84\x8f\x80\x99\xc0\xf3\x3d\x65\x11\xd1\x80\x5f\xd6\x3e\x01\x4b\x79\x0e\xed\x72\x8d\x91\xa4\x9a\x9d\x75\xc0\x62\x61\xb1\x4e\xb6\xca\x2b\x41\x73\x95\x56\x6d\xeb\x59\xcb\x39\x69\x63\x61\xcb\x54\x71\xa0\x37\xc3\x83\x7b\x25\xee\x9c\xeb\xb4\x28\xbd\x56\xa6\xca\x76\xa3\x33\xca\x4d\x5f\x07\x1b\x3d\xcf\x1a\xb8\xdb\xc8\x03\xc3\x13\x2b\x1e\xca\x7b\xdd\x4a\x73\xd0\x1c\x77\xbb\x1f\x1d\x79\xe3\xc0\x52\x9d\x4c\xcb\xbd\xe5\xa0\x3d\x6b\x76\x36\x74\xae\xd3\x1b\xbf\x42\x60\x05\x9e\x34\xdd\xcc\xea\x60\xbe\x5d\x77\x99\xf5\xaa\x31\xc3\x5a\xb8\x5e\x8b\x9f\x2a\xe2\x34\x41\x88\xa7\x48\xa2\x60\x82\xd0\xde\xd4\xb9\x65\x58\x1a\xd6\xad\xa2\xde\x87\x8b\x6d\xe8\xf7\x99\xd1\xb4\x29\xf8\x70\xdc\xf4\x66\x3d\xe0\x86\x74\x00\x1d\x39\x94\x5f\x6d\x81\x5a\x0e\x67\xd8\x96\xf5\x0c\xdd\x73\x4b\x21\xd0\xca\xd5\x6d\x83\x5b\x2e\xdc\xd7\xea\xb8\x99\xcb\xab\x76\x47\xec\x52\x64\xf3\x4f\x4f\x95\xd5\x53\xc5\x8d\x30\xc3\xc7\x5c\x4f\x0e\x80\x54\xda\x40\x2c\xa8\xb2\x33\x17\x6b\x74\x7c\x0c\x80\x6a\xcb\x5d\xb7\x05\xb7\xed\x77\xfd\x59\x33\x84\xaf\x56\xfb\xd0\x7d\xf0\xe3\x55\xa8\x16\xb8\x42\xc7\x73\xc7\x8a\xa0\x4e\x86\x52\xc9\x5b\x02\x49\xeb\x75\x58\x79\x13\x7a\xb3\x40\x6e\x77\xca\xbc\x29\xb3\x79\x4d\x39\x9b\xef\x9d\xb5\xf7\xc5\xf7\x42\xe0\x10\x8e\x4f\x52\xcb\x42\x4b\x5c\x2f\xda\x39\xc2\x29\x4b\xaf\x5b\x8c\xe9\x6c\x0c\x0f\x33\xb5\x46\x71\x6c\xb5\x87\xba\x1b\x74\x5f\x7b\xc9\x03\x8c\xe5\x39\xa9\xe7\x8b\xc3\x1c\x3f\x22\xe2\xe6\xaa\xb4\x9a\xcb\xd7\x0e\x5f\x2c\xe8\x28\x6f\x55\x59\xae\xa8\x34\xf5\xa0\xf4\x4a\x54\xd7\x2d\xd8\xef\xb3\x55\xc1\x1e\x16\x3a\x78\xd8\x57\x1d\x11\x93\x85\x9e\x59\xe7\x86\x45\xb9\x03\x0b\xdd\x55\xbb\x22\xb2\x3d\x1d\x73\x06\xa3\xf1\x72\xb2\xd9\xaa\x36\x1f\x80\xea\x6a\x2a\x95\xbc\xd0\x7b\xb2\xfe\x8d\x3b\xf5\xcf\x07\xa0\x20\x0f\x46\x13\x5c\x30\x47\x43\xe0\x0e\xe8\xfe\x3a\x94\x87\x44\x49\xaa\xea\x0b\x9b\xe0\xbb\x85\x69\xa5\xb8\xa0\xe4\x75\xb7\x32\xd4\xef\xd4\xbf\x61\xd1\xa3\x21\xea\x8d\xbd\xd1\x74\x54\x2e\x10\x1d\xb4\x51\xed\x77\x72\x4b\x6c\x65\x8d\xa6\xab\xca\xc6\x03\x7c\xcd\xa6\x07\x83\xf9\x46\x2e\x6e\x08\xdb\xa8\x60\x39\x20\x97\xf0\x56\xd9\x59\x13\xe6\xdc\xd6\x7b\x7a\x73\xb0\x59\xe7\x66\x4e\xbb\x20\x8a\x93\x6a\xfb\x75\xa3\xad\x39\xfd\xd9\xf6\xaf\xdc\xa9\x3f\xcf\x8c\xeb\x2c\xcf\xcc\x4c\x5d\x6c\x41\x54\xed\xf7\x99\x41\x59\x11\xda\x6b\xba\x9d\x0b\xcd\xf2\x52\x21\xfa\x02\x46\x81\x2a\x51\x31\xb0\xf6\x9d\xfa\xe7\x2b\x1d\xab\x6a\xfa\xe4\x88\xaf\x6d\x3c\xa7\xa4\x38\x06\xbd\xcd\xf5\xcc\x21\x3a\x6f\x94\x82\x82\xb1\xa1\x45\x5f\xe0\x7a\x0b\xb5\xdb\x32\x88\xbc\x5c\x2b\x6d\xfd\x2e\x56\xd7\x2b\x6b\x46\x24\xb7\x8e\xb5\x58\xbf\x3a\x73\x77\xd4\x01\xfd\x52\xb7\x57\xd0\x0c\x21\x64\xa7\xaf\x0d\x7d\xcc\xef\xe3\xdd\xb3\x3d\xf1\xdd\x7e\x23\x5e\x63\xd8\x6d\xec\x78\xd9\xfd\xc6\xff\x8d\xf7\x81\x3e\xfb\x86\x05\x3f\x7f\x4f\x2f\x31\x25\xd7\x77\x3f\xe9\xfd\xfc\x1d\x49\x04\xe3\x8b\x53\xe0\x7d\xfe\x9e\xbc\xff\x1e\x5f\xfc\xef\x4e\x58\x83\xf0\x31\x41\x0b\xac\xfd\xb5\x67\x6c\x1f\x14\x77\xa1\x07\xdd\xd5\x3d\xe1\x4f\xff\x7d\xb4\x2f\x8d\x5f\xdc\x54\x77\x2f\x50\x65\x70\xf2\xf4\x22\xcc\x03\x7a\xec\x86\xf3\xc0\x55\xa6\xc6\x6a\x77\xf3\x4a\xdb\x1c\xdb\x00\xdb\x11\x12\xd3\xe1\x7e\xfe\x8e\x7c\x5e\x61\xf8\xdf\xe8\xdf\xa8\xab\xe0\x9f\x77\xf7\x94\xc0\x75\xa1\xed\xd7\xe3\xda\x7d\xfe\x8e\x10\xa7\xd7\xf3\xf1\xeb\xb5\x11\x75\xff\xfb\xc0\xe5\x91\xd5\x83\x64\x84\xad\xd1\x2a\x00\x1a\xa4\x49\x9a\x51\x19\x1c\x00\x14\x93\x21\x80\x0c\x40\x49\x82\xa5\x58\x0e\xd3\xa0\x26\xb3\x98\xa6\x50\x0a\xab\x62\x18\x00\x0c\x4a\x03\x08\x59\x94\x25\x70\x85\xc5\x55\x6c\xa7\xd1\x01\x37\x1a\x09\x9d\xb4\xe1\xe1\x4e\x5c\xfd\xcf\xdf\x11\xf4\xe4\xd6\x7f\xcf\x9e\xf7\x6c\xb0\x88\xf4\x82\x1a\x81\x01\x1c\x05\x04\xc7\x42\xc8\x10\x0a\xc4\x71\x9c\xa1\x20\x60\x31\x86\x61\x58\x5a\x06\x0a\x45\xd2\x14\xad\x11\x84\xaa\x28\xa4\x46\x68\x50\xa1\xd1\xa8\x81\x54\x0d\xa3\x09\x95\xfb\xfc\x29\xa3\x84\x2b\x1c\x3c\xbb\x4d\xf1\x76\x0e\xb0\x6f\x97\xf7\x9c\xc0\x5f\x04\xfe\xc7\xd6\xfd\x16\xc3\x4f\xd7\xfa\x7f\x34\xc3\x8f\x59\xd9\x1f\x0e\xfe\x70\xf0\x87\x83\x3f\x1c\xfc\xe1\xe0\x0f\x07\x7f\x38\xf8\xc3\xc1\x6f\xe4\x20\xfe\xdf\xff\x79\x7c\xfe\xb2\x5b\x0b\x24\xce\x6b\x80\x7c\xcc\x5a\x60\xb2\x0a\xff\x9e\xb5\xc0\x7f\x68\x2b\x20\xfa\xd3\x15\x67\xdc\x70\x56\x13\xf0\x9a\x4a\xeb\x73\xdd\xc6\xd5\xf6\x18\x33\xbd\x09\xeb\xd6\x47\x34\xbb\x44\xab\x5b\x38\x22\x56\xa5\xd2\x50\x55\x46\xc9\xb4\x1e\xa7\x09\xe2\xb8\x16\x57\x38\xae\x1d\xa4\xf6\x33\x1a\xa7\x3b\xb1\x62\xa1\x42\x2c\x0d\xa6\x8b\xf6\x24\x0a\xcc\xaa\xc4\xba\x21\x54\x5e\x07\x9b\x9c\x50\xec\x4e\x56\xea\xc2\xaa\xe8\xcb\x91\x94\xcf\xe9\x7a\xad\x28\xb5\xa9\x31\xc3\x96\x86\x73\x1b\x33\x4d\xdf\x2f\xd3\x8c\xc0\x77\xfb\x8d\x9e\x82\x6d\xc3\x3e\xd1\x7d\x55\x95\x4e\x67\x68\xe9\x84\x5f\x66\x93\xaa\xfd\xd9\x7a\x79\xe3\xd6\x4b\xf4\x29\xc9\x5a\x65\x45\x97\x16\xeb\x9e\x28\xeb\x16\x5f\x7e\x6d\xbf\x12\x83\x65\x28\xb6\xfc\x62\xa1\x36\x0b\x17\x95\xca\xac\xd0\x00\x63\x5d\xb6\xc6\xa9\xb5\x21\x77\x29\xd1\x75\xd8\x04\xfa\x6c\xdd\x00\xfd\x16\x47\xe7\xb7\x9a\xc7\x41\x54\x71\x5c\x69\x32\xda\xe6\x87\xd5\x79\xd1\xa9\x31\xf3\xd5\x3c\x3c\xae\x35\xe9\x59\x6b\x4b\xa9\xef\xc9\x7b\x28\x6f\x5d\x6b\x4b\x3e\x2b\x37\x4c\x7d\xcd\x1f\xd6\x22\x45\x01\x7d\x65\x70\xab\x55\xeb\xac\x8b\x75\xb7\x3b\x74\xc8\xc0\x21\xc2\xbe\xd6\x92\x67\x53\x3e\xb7\xf6\x17\x3a\x46\x49\x0d\x73\x6e\x0c\x57\x6a\xd5\x17\xeb\x23\xe8\x74\x57\x38\x99\xd3\x29\xb1\x29\x54\x2a\xe5\xd7\xfc\x86\x16\xc6\xe3\xa5\xd7\xd8\x90\x6e\xa7\xda\x52\x79\xca\x59\xb4\xcb\x67\x6b\x4d\xf9\xf7\xd9\xdf\xcd\xad\xaf\x2b\x3f\x3b\x79\x62\xd3\xfa\xea\x5b\xfe\x6f\xdd\x51\x3a\x79\xd3\xff\x10\x47\x71\x82\x21\x21\xc7\x11\x24\x27\x73\x50\x63\x54\x19\x70\x80\x52\x65\x82\x20\x38\x99\x61\x35\x15\xb0\x1a\x41\x32\x0c\x23\x63\x40\x23\x08\x19\x90\x34\x0b\x54\x4a\x41\x55\x8d\x23\x69\x95\x54\x77\xaf\x64\x9d\x44\xdb\xc6\x91\xdd\xa4\x61\x8d\x5c\x1e\xad\xa3\xd5\xd2\xc6\x9f\x86\x12\x66\x8e\x51\xb0\x59\x38\x18\x27\x95\xd7\xab\x7a\x61\xd3\xa4\xfc\xbc\xa8\x14\x06\xab\xb0\xc8\x85\x84\xee\xbb\x4d\x7b\xc2\x3f\xf0\xb9\xfa\x6a\x5f\xdc\xba\x85\x67\xca\x1f\xe7\x5e\x95\x4c\x6b\xbf\x5b\xfe\xe5\x9e\x75\x36\xef\x18\x4d\x02\x0a\xa5\x49\x28\x03\x9a\xd4\x70\x45\x95\x81\x2a\xb3\x14\x2d\x6b\x04\x49\xb2\x24\x4b\x69\x0a\x8d\xd3\x38\xc9\x00\x15\x10\x50\x25\x38\x45\x55\x35\x54\xa3\x39\x14\xc7\x08\x42\xa6\x13\xde\xf1\x4b\xde\xff\xa5\x7a\x7f\x14\xef\x2c\x79\x7c\xfe\x7c\x85\xfb\x23\x78\xc7\x65\x1c\xb2\xb8\x2a\x03\x59\x46\x71\x52\xc6\x19\x80\x2a\x04\x46\xa2\x0a\x60\x30\x95\x05\x0a\x27\x2b\x0c\xc6\x12\x98\xc6\x69\x14\x20\x64\x95\xe6\xa0\x02\x08\x95\x65\x35\x19\x85\x0a\xa5\x24\xbc\x13\x1f\xce\xfb\x7b\xeb\xfd\x51\xbc\x33\xfd\xe3\xf3\xe7\x3b\x2b\x1f\xc1\xbb\x02\x65\x99\x65\x28\x80\xa2\x9a\x46\x43\x8c\x60\x09\x00\x35\x54\x53\x71\x0a\x03\x0c\xad\xe1\xb8\x82\x69\x1c\x90\x71\x80\xab\x9a\xa6\xc8\x28\xc3\xb0\x14\xc5\x10\x34\x50\x21\x4e\x53\xdc\xfe\x45\xba\x9b\xbc\x3f\xb1\xe3\xa5\x5f\xe3\xf6\x2e\xef\xc2\x73\xe5\x5b\xca\xb1\xfc\xb7\xc7\x99\xab\x7d\xc6\xc7\x74\x18\xbf\xbb\xb7\x28\xd5\xd9\x72\x7b\xd5\x9e\xcb\x35\xbc\xcc\x13\xc3\xc1\xac\xe3\xd6\xac\xd9\x08\x45\xb5\x12\xeb\xd5\x2b\x8c\x85\x8a\x9d\xb0\x7a\xd8\x87\x4a\xf5\x0a\xf9\x33\x3a\xce\xbf\xbf\x71\xac\xc3\xf3\x4d\xca\xad\xa5\xf1\x12\xef\x88\xc7\xbe\xc2\x76\xb9\x9a\xb7\xf3\x6d\x47\xe2\xab\x86\xd6\xea\x8c\x04\xa7\x3e\x5d\xf9\x1b\xa5\x47\x98\xc5\x56\xa1\x4d\x61\xfa\x5c\xf5\x8a\x65\x90\x97\x86\x21\x4a\x75\x73\x83\xe9\x10\x1d\xe9\x73\x17\x2d\xe4\x5b\x22\x29\x81\xe2\x00\xaf\x59\x8a\x47\x4c\xc2\xba\x65\xc8\x64\xaf\xe3\x36\xea\xd1\xfc\xa4\x42\xf4\x16\x73\xd4\xa2\x18\x02\xa7\x2d\x13\xdf\x36\x28\x9f\xcc\x81\xd9\xb6\x43\xc0\xb5\xbb\x6d\xd2\x1d\xc7\x1d\x84\xd2\x6b\x5f\xee\xa3\x07\xc5\x6e\x70\x70\xb4\x8c\x93\x37\x5c\x52\xb2\x71\x55\x3f\x22\x5a\xbd\x37\x5a\xf0\x1f\x10\xad\x24\x77\xd3\xeb\x3d\x51\x3e\xcf\xff\x7b\x51\x23\x2b\x5a\xfe\xee\xa1\xc1\xfb\x9d\xac\x70\xa6\xfd\x85\xc1\x35\xf1\x42\x8e\x6f\x92\xd4\x38\x2f\x10\x7e\x79\x50\x6c\x62\x1d\x82\x47\x1b\x70\xde\x62\xab\x1d\xda\x96\x30\x9e\x83\x43\x43\xdd\x54\xfc\xa4\xbd\xae\x3b\x19\xdf\x15\x27\xc6\x44\x86\xc5\xb0\xe0\xb9\xb5\xbc\x5d\xab\x04\x5e\x0e\xa5\x06\x7e\x55\xc8\xbb\xba\xe3\x05\xd3\x7a\x3b\xd7\xa7\x47\xfd\x19\xe9\x87\xc3\xcd\xd4\x63\xfa\x7e\x97\x2c\x34\xe0\xba\xd9\xa0\xab\x4b\x45\x5b\x56\x6b\x18\x3a\x34\xf3\xf3\x79\x68\x93\x3a\xdb\xaa\x68\xb3\x4a\x29\x72\x84\xe2\x7c\xd1\x9e\x34\x03\xab\xb7\xd9\xca\xae\x3c\x1d\xae\xb6\x9d\x4a\xb7\x98\x2b\x55\x8d\x0e\x8d\xcf\x98\x9e\x2d\x11\x21\xb7\xe0\xa7\x0d\x66\x7c\x50\xec\xdf\x77\xb2\x67\x8d\xfc\x59\x27\x6b\x84\x75\xcb\xfd\x40\x27\x7b\xe2\x65\x84\xdf\xe3\x64\x1f\x3c\x0e\x7c\xbf\x93\x09\x67\xda\x5f\x18\x9c\x6c\xe9\x16\x36\xc0\x55\x9d\x1a\x60\xd6\x12\x83\x66\x43\x29\x61\xfe\x7a\xd6\x1d\xd7\x26\x5c\x28\xea\x4e\x37\x0f\xe0\x90\xed\x1b\xc5\x84\xb0\xeb\x4e\x26\x54\x03\x13\xf3\xeb\xa5\x7a\x91\x1c\xac\x43\x1f\x55\x85\xc2\x40\xd4\x68\x5f\xa6\x4c\x52\xde\x34\xdc\x92\x5e\x58\xbc\x9a\x83\x49\xc3\x5a\x2b\x3e\x45\x1a\x92\x86\x5b\x6b\x7f\xb6\xa6\x1b\x2a\x35\xa9\x92\x22\x29\x98\x8a\xa7\x91\xb4\xc8\x4f\xf3\xa5\x6e\xbf\xe5\xd9\xac\x36\x16\x22\x47\xa8\x6d\xe8\x02\x4e\x07\x61\xb5\x5e\xa5\x05\xa1\xc8\x6f\xec\xf2\xd8\xad\xae\xca\x25\xa1\x4c\x71\xa0\xc9\x01\xb8\x99\xe1\x95\xd7\x90\x29\x1e\x97\xd7\xfe\x7d\x27\x7b\xd6\xc8\x9f\x75\xb2\x3a\x3a\xa7\x85\x0f\x74\xb2\x27\xde\x78\xfa\x3d\x4e\xf6\xc1\x83\xfe\x1a\x3d\x83\x06\x31\xb3\x9c\x0a\xdb\x2b\x99\x42\x0e\xea\x0a\xc1\xb4\x46\x7e\xb9\x56\xdb\x0e\x07\x6c\x38\x30\x26\x79\x50\x08\xa8\x3a\x15\x33\x99\x18\x58\x3c\x99\xba\x31\x5c\x4c\x06\xdd\x6f\x75\xb2\xe4\x53\xa7\xd9\xf4\x5c\x0d\x1c\x96\xce\x78\xae\x15\x70\x8b\xd9\x66\xae\x74\xba\x34\x6a\x2e\x9b\xf5\xa5\xc4\x16\xcb\x5b\x9c\x24\xdb\x2d\x56\x06\x63\x09\xf6\x7a\xd5\x49\xc5\x74\x89\xae\xdc\x29\x60\xc4\x52\x74\xb9\xa0\x45\x36\x3b\x82\xbe\x29\xe4\x73\xba\x12\xe8\x78\xa9\xe6\x0a\x8d\xa0\x86\x76\x7b\x44\xbb\x09\x6a\xfd\x7c\x98\x38\xc4\xd6\x0d\xca\x43\x5d\xc8\xc9\xd3\xa6\xe0\xae\x04\xae\xda\x59\xe0\xac\x11\x6a\x96\x5b\x33\x88\xdc\x26\x54\xc9\xc1\x48\x5a\x12\xc6\x64\x72\xd4\x35\xdb\xd9\xc4\xd3\x06\xbd\xe2\x6c\xc9\x03\x17\xce\xf6\x0f\x1a\x1b\x9f\x31\xd9\x7a\x73\xf9\xaf\xa6\xbc\x7c\xa2\xfc\x06\xff\xef\x4d\xf6\xb2\xea\xff\xe6\xf2\x49\x9b\x9b\xff\x8e\xc9\xe6\x8d\x84\x10\x6f\x9d\x71\x9e\x25\x85\xd8\xc7\x91\x74\x0c\x48\x7e\xc8\x76\xb4\xd5\x8c\x40\x74\x1d\xe5\x30\x26\x4e\x7e\xfe\x76\x81\x72\x35\x89\xed\x65\x6e\xef\x43\xf2\xde\x7d\xe2\x88\x3b\xd5\x4c\x27\x19\x8f\x53\x8b\xa7\x10\xe3\x7d\x2f\x5e\x10\x32\xd2\x6d\x1e\x0a\x44\x5a\x9d\x4a\x83\xef\x8c\x91\x9a\x38\x46\xbe\x1c\x32\x11\x7d\x3b\xa4\xb9\xbd\x9a\x74\xf6\x2c\xdd\xf9\x47\x29\xee\xdd\xd0\xda\xbb\xa5\x72\x56\x12\xd0\x63\x86\xf7\xa7\xd5\x93\x81\x9d\xa5\xd9\xbe\x80\x53\xa5\x92\xbc\x63\xb7\x33\x84\xde\x4c\x6d\xff\xb4\xba\x27\xe0\x59\x8a\xdf\x28\x1d\xe9\x4b\x95\x76\x5f\x44\x8e\xc9\x48\xde\x54\x93\x8f\xe1\xfb\x8d\x15\xb8\x6c\x83\x63\xd2\xd4\x2b\x09\x3f\x4f\x0e\x44\x78\x5a\xdf\x04\x2c\x4b\xd1\x54\x31\xa7\x1a\xee\xd2\x50\x66\xa7\x9f\x4c\x9f\xfe\xf0\xb4\x72\x31\x56\x96\x6e\xc7\x42\x4e\x55\x33\x16\xdf\xe2\xa4\x94\x37\xd3\x2d\x66\x9c\x7a\xf1\xbc\xa6\x29\xc8\x4c\x85\xcf\x8b\xcc\x6a\xf4\x2b\x39\x19\x4f\xce\xfe\xf8\x08\x4d\xbd\x40\xbe\xa2\xe3\xbe\x98\x53\xed\xe2\x74\x8e\x37\x12\x34\x5e\x1c\x78\xf2\xb4\x92\x47\xc0\x2c\x45\xcf\x8a\x7b\x28\x86\xa5\x7e\x3d\x77\x7e\xec\xcb\xd3\xda\x1e\x01\xb3\xb4\x3d\x2b\xee\x54\xdb\x7d\x9a\xc3\x1b\x69\x04\x2f\x8e\xbf\x79\x5e\xdf\x03\x60\xa6\xbe\xa7\xc5\x9d\xea\x7b\xc8\x44\x78\x23\xc3\xe0\xc5\x39\x40\x4f\x2b\x7c\x04\xcc\x52\xf8\xac\xb8\xab\x43\x83\x24\xc3\xe0\xb7\x63\xfa\xc0\x9b\x29\xe4\x32\xce\x49\x7a\xbe\x1e\x29\xc8\xcc\x9a\x9c\x17\x99\x15\x25\x3c\xb8\xfc\xb6\xcf\x99\x76\x3d\xbd\xdb\xf9\xf9\x51\x1f\xa0\xfb\x4d\xc5\x9f\xd2\xfa\x6c\x88\x9a\x75\xaa\xd6\xd3\xfa\x9f\x82\x66\x55\x22\xa3\xd8\xab\x35\x49\xa7\x5e\xbb\x36\xd4\x3c\x9c\x38\xb6\xd3\x3d\x3e\x9f\xec\xb1\xa4\x70\xc9\x51\x66\x67\x38\x48\x53\x3a\x8e\x62\xfb\xdd\x8a\x54\x42\x64\xdf\x85\x10\xd9\x67\x80\xfc\x8a\x0c\xcb\x62\x47\x44\xd2\x19\x21\x8f\x69\xbe\x2e\x07\x9a\xa7\x87\xa8\xbd\x57\xcb\x53\x98\x48\xc9\xdd\x38\xe5\x44\xc5\x74\x5a\xf0\x6f\xe9\x24\xe0\xdf\x92\xe4\xdc\x17\xda\x1d\x4f\x82\x7b\xaf\x62\x07\x84\x48\xa7\xa3\x63\x9c\xa8\x75\x7d\xa8\x78\x72\x66\xdd\x33\x2a\xec\x41\x12\x2d\x52\x01\xe6\x41\x45\x4e\x8e\xd9\x7b\xaf\x22\x69\x90\x48\x91\xd3\x81\xf0\x83\x9a\x9c\x1c\x0d\xf8\x5e\x4d\xd2\x20\x91\x26\xa9\x1c\x06\x8f\xab\x71\x72\x9c\xe1\x13\x9a\xa4\x71\x76\xca\xec\x47\x09\xa7\xca\xa4\x53\xff\xde\x0a\x5f\xcf\x69\x74\x0e\x14\xa9\x74\x16\x1b\xef\x72\x74\xed\x14\x4f\x44\x71\xac\x85\x09\x7d\x18\x17\xfb\xff\x02\x00\x00\xff\xff\xec\x37\x72\x59\x11\x74\x00\x00") func baseCoreSqlBytes() ([]byte, error) { return bindataRead( @@ -629,12 +629,12 @@ func baseCoreSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "base-core.sql", size: 28545, mode: os.FileMode(0644), modTime: time.Unix(1560770544, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xae, 0xf6, 0x70, 0x51, 0x51, 0x28, 0xaf, 0xb0, 0xed, 0x39, 0x7f, 0xf6, 0xa8, 0xb5, 0xbc, 0xe7, 0xa9, 0xea, 0xe5, 0x21, 0xf4, 0xa2, 0xbc, 0x72, 0x53, 0x75, 0x7, 0x4f, 0xc0, 0x57, 0x35, 0x19}} + info := bindataFileInfo{name: "base-core.sql", size: 29713, mode: os.FileMode(0644), modTime: time.Unix(1572527986, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x40, 0x3d, 0x5a, 0xed, 0xf, 0x89, 0xca, 0xf8, 0xbc, 0x2c, 0x52, 0x34, 0x28, 0xd0, 0x15, 0x30, 0xa9, 0x9c, 0x79, 0x3, 0x6a, 0x85, 0xd9, 0x75, 0xf5, 0xb1, 0x78, 0x3a, 0xed, 0xa5, 0x48, 0x31}} return a, nil } -var _baseHorizonSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x7d\x69\x6f\xe2\xc8\xf3\xf0\xfb\xf9\x14\xd6\x68\xa5\xcc\x28\x99\x89\xef\x23\xf3\x9f\x95\x0c\x98\x40\xb8\xef\x24\xab\x15\x6a\xdb\x6d\xe2\x60\x6c\x62\x9b\x00\xb3\xfa\x7d\xf7\x47\xbe\xc0\x36\xbe\x38\x32\xbb\x0f\x1a\x8d\x82\x5d\x5d\x57\x57\x55\x57\x57\x37\xdd\xdf\xbe\x7d\xfa\xf6\x0d\xe9\x1a\x96\x3d\x33\xe1\xa0\xd7\x44\x64\x60\x03\x11\x58\x10\x91\x57\x8b\xe5\xa7\x6f\xdf\x3e\x39\xef\x2b\xab\xc5\x12\xca\x88\x62\x1a\x8b\x3d\xc0\x3b\x34\x2d\xd5\xd0\x11\xee\x3b\xfd\x1d\x0b\x41\x89\x5b\x64\x39\x9b\x3a\xcd\x63\x20\x9f\x06\xc2\x10\xb1\x6c\x60\xc3\x05\xd4\xed\xa9\xad\x2e\xa0\xb1\xb2\x91\x9f\x08\xfa\xc3\x7d\xa5\x19\xd2\xfc\xf0\xa9\xa4\xa9\x0e\x34\xd4\x25\x43\x56\xf5\x19\xf2\x13\xb9\x1a\x0d\xab\xec\xd5\x8f\x00\x9d\x2e\x03\x53\x9e\x4a\x86\xae\x18\xe6\x42\xd5\x67\x53\xcb\x36\x55\x7d\x66\x21\x3f\x11\x43\xf7\x71\xbc\x40\x69\x3e\x55\x56\xba\x64\xab\x86\x3e\x15\x0d\x59\x85\xce\x7b\x05\x68\x16\x8c\x90\x59\xa8\xfa\x74\x01\x2d\x0b\xcc\x5c\x80\x35\x30\x75\x55\x9f\xfd\xf0\x79\x87\xc0\x94\x5e\xa6\x4b\x60\xbf\x20\x3f\x91\xe5\x4a\xd4\x54\xe9\xc6\x11\x56\x02\x36\xd0\x0c\x07\x8c\x6f\x0e\x85\x3e\x32\xe4\x4b\x4d\x01\xa9\x57\x11\xe1\xb1\x3e\x18\x0e\x90\x4e\xbb\xf9\xe4\xc3\x7f\x7f\x51\x2d\xdb\x30\xb7\x53\xdb\x04\x32\xb4\x90\x4a\xbf\xd3\x45\xca\x9d\xf6\x60\xd8\xe7\xeb\xed\x61\xa8\x51\x14\x70\x2a\x19\x2b\xdd\x86\xe6\x14\x58\x16\xb4\xa7\xaa\x3c\x55\xe6\x70\xfb\xe3\x77\x10\x94\xdc\xbf\x7e\x07\x49\xc7\xae\x7e\x9f\x80\x1e\xb5\xe3\xa5\xf3\x18\x74\x0c\x39\x8b\x58\x08\x6a\x8f\xdc\x05\xaf\xb7\x2b\xc2\x63\x08\xd2\x47\xeb\x72\x35\x85\x8a\x02\x25\xdb\x9a\x8a\xdb\xa9\x61\xca\xd0\x9c\x8a\x86\x31\xcf\x6e\xa8\xea\x32\xdc\x4c\x43\xc2\xe9\x16\x70\x0d\xdd\x9a\x1a\xfa\x54\x95\x8f\x69\x6d\x2c\xa1\x09\x76\x6d\xed\xed\x12\x9e\xd1\x7a\xcf\xc9\x59\x5c\x1c\xd7\x56\x83\xf2\x0c\x9a\x6e\x43\x0b\xbe\xad\xa0\x2e\x1d\x25\x42\xa8\xf9\xd2\x84\xef\xaa\xb1\xb2\xfc\x67\xd3\x17\x60\xbd\x9c\x88\xea\x7c\x0c\xea\x62\x69\x98\x8e\x3b\xfa\x31\xf5\x54\x34\xa7\xea\x52\xd2\x0c\x0b\xca\x53\x60\x1f\xd3\x3e\x30\xe6\x13\x4c\xc9\xf7\xcb\x13\x98\x0e\xb7\x04\xb2\x6c\x42\xcb\xca\x6e\xfe\x62\x9b\xb2\x3b\xee\x4c\x35\xc3\x98\xaf\x96\x05\xa0\x97\x79\x2c\x79\x50\x40\x35\x8f\x44\x1c\x04\xdd\xc2\x0d\x9c\x38\xa1\x28\xd0\x2c\x06\x1a\xa0\x3f\xa1\x89\xaf\xd6\x62\x8d\xdc\xd0\x7a\x04\x91\x70\x28\xce\x6b\xb1\x74\x1a\xbc\xd8\xb9\x3d\x60\x45\x02\x90\xb8\xcd\x35\xa3\x97\x9d\xa7\x17\x01\x36\x3c\x3e\x8c\x5c\x40\xd5\xb2\xa7\xf6\x66\xba\xcc\x47\xe9\x40\x1a\xcb\xa2\x90\xb0\x28\x58\x30\x94\x64\x03\x8b\x81\xbb\xe7\x82\xe5\x47\x31\x71\x5b\xac\x33\xbd\x31\xd2\xd1\xb6\x65\xad\xf2\x28\xef\x80\x25\x43\xce\x09\x25\xae\xe5\xb9\x63\xa8\x05\x35\x2d\x0f\xf1\x1e\x5a\x03\x96\x3d\x5d\x18\xb2\xaa\xa8\x50\x2e\xa4\x8e\x28\x25\x27\xfb\x74\xd9\x2c\xda\x48\x5c\x6d\x43\x6d\x8e\x4a\x75\x76\x96\xbd\x04\xa6\xad\x4a\xea\x12\xe8\x99\xf9\x48\x5e\xd3\xe9\xf2\xc8\x74\x6b\x37\x48\x1f\xcb\x41\x72\xc3\xa3\xe9\xbb\x4a\x2b\x42\xcf\x03\xfc\x70\xfc\x9e\x7d\x3a\xc6\xe9\xff\xe9\x0c\x79\x41\x36\xeb\xda\xf7\xb4\x20\x07\x33\xc3\x5c\x4e\x17\xea\xcc\xcf\x81\x32\x58\x88\x41\x16\x96\x71\x37\x46\x5a\xea\x4c\x87\x66\x66\x1e\x1b\x03\x2d\x4e\xe3\xe8\x34\xb9\x28\x66\xcf\x7f\x32\x90\xfa\x0e\x96\x85\xaf\xa8\x43\x79\xad\xcb\x9d\xe6\xa8\xd5\x46\x54\xd9\x23\x5a\x11\xaa\xfc\xa8\x39\x2c\x88\x3b\xc5\x51\x2e\x80\xd9\x37\xd1\x6c\x4c\xee\xb7\xe2\xe2\x07\xc9\xd2\x40\xe8\x8d\x84\x76\xf9\x04\x9d\x39\xd3\x1d\x0b\xbe\x1d\x4d\x39\x82\xa4\x70\x6b\x19\x16\x84\xdd\x4f\x2a\x0a\x4b\x98\x12\xa9\x8e\x91\x2f\x19\x45\xb1\xb6\x7e\xfa\x5d\x0c\xd8\xcf\xb5\x0b\xcb\xe6\x47\xad\x63\x64\xf1\x9a\x14\x84\xf5\xc3\x46\x71\x7e\x82\x38\x53\x84\xa3\x58\xdc\xcb\x06\x0e\x85\x98\x1c\xc0\x58\xa8\xcb\x86\xf6\x82\x8c\x0f\xc3\xdf\xdf\xf7\x85\x7b\x7e\x98\x00\xb7\x50\x9d\xa9\xa4\x2a\xc1\x2f\xfa\x6a\x01\x4d\x55\xfa\xeb\xef\xaf\x05\x5a\x81\xcd\x09\xad\x9c\x04\xe6\x0b\xd0\xb7\x50\x73\x6b\x6c\x05\x5a\x28\xaa\x99\xd8\xa4\x3a\x6a\x97\x87\xf5\x4e\x3b\x43\x9e\x29\x98\xcd\xf6\xdc\xdd\x20\x07\x8c\x66\xe0\x08\xa4\x3b\x03\x87\x9b\xac\x39\xcd\xf7\xcc\xdf\x20\xc7\x08\xe2\x8a\x5e\x00\x83\xf0\x38\x14\xda\x83\x18\x0a\x6d\x39\xb3\xde\xb4\xc0\xba\xcb\x35\xa1\xc5\x1f\x50\xf8\xf1\xc9\x2b\xaf\xb6\xc1\x02\xde\x05\xcf\x90\xe1\x76\x09\xef\xfc\x26\x3f\x90\x81\xf4\x02\x17\xe0\x0e\xf9\xf6\x03\xe9\xac\x75\x68\xde\x21\xdf\xdc\xaa\x6b\xb9\x2f\x38\xfd\xe5\x63\x0e\xf0\x7d\x8a\x60\x8c\xbe\xf4\x11\x97\x3b\xad\x96\xd0\x1e\x66\x60\xf6\x00\x90\x4e\x3b\x8a\x00\xa9\x0f\x90\xab\xa0\x9e\x1a\x3c\xb3\x5c\x24\x57\x71\xca\x81\xf8\x3e\xcd\x9d\x86\x72\xe5\x89\xe8\xb2\xdd\x19\xc6\xf4\x89\x4c\xea\xc3\xda\x8e\xad\x70\x61\x35\x42\x7e\x8f\x25\xc6\xc8\x31\xc2\x1f\x20\x71\x15\xd0\x6d\xde\x2e\x67\x83\x5e\x13\x59\x9a\x86\x04\xe5\x95\x09\x34\x44\x03\xfa\x6c\x05\x66\xd0\x55\x43\xc1\x42\x70\x98\xdd\x7c\x43\xf3\xd9\x0f\x6c\x75\xcf\x7f\xd0\xb7\x49\xba\xdc\x59\x76\x2e\x7e\xa4\x2f\x0c\x47\xfd\xf6\x20\xf4\xec\x13\x82\x20\x48\x93\x6f\xdf\x8f\xf8\x7b\x01\x71\xa5\x6f\xb5\x46\x5e\xa8\x1b\x0c\xfb\xf5\xf2\xd0\x85\xe0\x07\xc8\x1f\xd3\x3f\x90\x81\xd0\x14\xca\x43\xe4\x0f\xcc\xf9\x16\xef\x8d\x5c\x47\x3c\x4f\xba\x3c\xf4\x17\x13\x0e\x4f\x12\xae\x48\xa4\x3a\x4f\xbe\x02\x14\x76\x22\xee\x1e\x9d\x24\xe1\x97\x4f\x08\x52\xe6\x07\x02\x32\xa9\x09\x6d\xe4\x0f\xec\x2f\xec\xef\xdb\x3f\xb0\xbf\xf0\xbf\xff\xfc\x03\x77\xff\xc6\xff\xc2\xff\x46\x86\xde\x4b\x44\x68\x0e\x04\x47\x29\x42\xbb\xf2\x35\x51\x33\x05\xc6\x81\x33\x35\x93\x4f\xe1\xa3\x35\xf3\x7f\xa7\x68\xe6\x70\x4c\xf5\xf5\xb0\x1b\x87\x8b\x29\x62\x3f\x6c\x1f\x60\x74\x39\x46\x90\x81\xa3\x2b\xe4\xe7\x3e\x02\xdc\x78\x8f\x87\x4f\x5d\x01\xf9\x19\xf6\x88\xaf\x49\x5e\x7b\x51\x1e\xe3\x08\x63\x2c\x06\x6e\x5c\x9c\xc3\xc4\x14\xe8\x5c\x2e\x93\x90\xc6\x38\x8d\x38\x64\x94\xdd\xbd\x95\x1d\x72\x9b\x94\xe6\x9d\xcd\x6d\x02\xd2\x38\xb7\x61\x27\xc9\xe4\xd6\x19\xb9\x64\xa8\x80\x95\x66\x4f\x6d\x20\x6a\xd0\x5a\x02\x09\x22\x3f\x91\xab\xab\x1f\xd1\xb7\x6b\xd5\x7e\x99\x1a\xaa\x1c\x5a\x23\x8d\xc8\x7a\x90\x28\xfb\x72\xba\x5e\x56\x4c\x46\xcf\x21\xd3\xca\x11\x9e\x88\xfe\x63\x44\x7a\x01\x26\x90\x6c\x68\x22\xef\xc0\xdc\xaa\xfa\xec\x0b\x4d\x7e\x75\xb3\x87\xf6\xa8\xd9\xf4\x64\xf6\x5a\x16\x02\x5d\x43\x75\xf6\x62\x23\xaa\x6e\xc3\x19\x34\x77\x2f\x0f\xbb\x34\x3c\x71\x38\x55\xc2\x70\x15\xc4\x93\x4a\x95\x11\x51\x9d\xa9\xba\x1d\x63\x0b\x2c\x92\x85\x8d\x81\xe9\xab\xc5\x6e\xae\x74\x20\x83\x07\xa2\x68\x60\x66\x21\xd6\x02\x68\xda\x21\x19\xdb\x58\x68\x09\x6a\xc2\x29\xea\x6b\x86\x2a\xe2\x13\xae\x53\xd5\x11\x2f\x6d\xed\x54\x62\xc3\xcd\x81\x42\x96\x4b\x4d\x75\xd7\x9c\x10\x5b\x5d\x40\xcb\x06\x8b\x25\xe2\x98\xa6\xfb\x15\xf9\x65\xe8\xf0\x90\xd1\xb4\xe9\x64\x90\x76\xfb\xf3\xd0\x62\x3c\xef\x66\xad\x29\x58\x7d\x6f\xe3\xfb\x43\x2f\x71\xc5\xdc\x07\xf5\x76\xb9\x2f\xb8\x59\x66\xe9\xc9\x7f\xd4\xee\x20\xad\x7a\x7b\xcc\x37\x47\xc2\xee\x3b\xff\xb8\xff\x5e\xe6\xcb\x35\x01\xc1\xf2\x84\x39\x59\xed\x71\x44\x07\xa6\xe8\x57\x8b\x10\x1d\x6e\xec\x77\xa0\x7d\xb9\x4a\x91\xf8\xea\xee\xce\x84\x33\x49\x03\x96\x15\x77\x2b\x7f\xad\x2d\xd9\x05\x33\x3a\xca\x2b\x2a\x9c\x2d\x99\x57\x0a\xdb\xc9\x95\xec\x19\xfb\xc2\x6c\xa1\x48\xb1\x2f\xe9\x26\x80\x63\x78\x32\xb8\x57\xeb\x4d\x68\x40\xd1\x59\x1e\x96\x5c\x97\xb9\x90\xd9\x86\x71\xfe\x36\xa3\xcd\x12\x04\xe9\x4c\xda\x42\x05\x29\x3d\xe5\x48\xe4\x95\x36\xb3\x05\xda\xe1\x8a\xbd\xfe\xae\xca\x69\xbc\x05\xc5\xb2\x73\xad\xce\xc7\xe3\x9b\x5d\xcc\x67\xa6\x69\x91\xfe\xb0\x36\x98\x06\xf9\xd9\x5d\xb8\xfb\x9c\x62\xcd\xae\x1d\x27\xbf\x92\xa1\x0d\x54\xcd\x42\x5e\x2d\x43\x17\xd3\x8d\x2d\xa8\x30\x9e\xab\x07\x1f\x8f\xaf\x87\x60\xdf\x45\x0a\x6f\xa1\xcd\x10\x85\xbc\x30\x69\x1f\x46\x72\x43\x5f\x2d\xa1\x92\xb2\x97\x40\x04\x7c\x04\x51\x0e\x8d\x51\xd8\x77\x44\x31\xf8\xdd\x66\x88\xd8\xc0\x64\xac\xec\xfd\xd8\x14\x6f\x63\x42\x60\xe7\x36\xf2\x60\x57\x4b\xb9\x30\xec\xce\x74\xfc\xaf\xb1\x7d\x22\x07\xb2\x60\x07\xf9\x80\x0d\xb4\xa9\x64\xa8\xba\x95\x6c\x83\x0a\x84\xd3\xa5\x61\x68\xc9\x6f\xdd\x95\x7b\x05\xa6\xf5\xb5\xfb\xda\x84\x16\x34\xdf\xd3\x40\x9c\x74\xdb\xde\x4c\xdd\x34\x49\xfd\x95\x06\xb5\x34\x0d\xdb\x90\x0c\x2d\x55\xae\x78\x1f\x05\xc6\x02\x81\x0c\x4d\x37\xbd\xf0\x13\xc5\x95\x24\x41\xcb\x52\x56\xda\x34\xd5\x50\x7c\xc1\x81\xaa\x41\x39\x1d\x2a\xdd\xad\x52\x8a\xfe\xe7\x7a\x59\xca\x42\x52\xce\x98\x57\x3c\xda\xe4\xc7\xaf\x63\x45\xbe\xec\x30\x96\x49\xe3\x77\x0d\x6b\x47\x09\x7a\xe6\x30\x97\x49\xeb\x70\xd8\x4b\x06\xcf\x18\x06\x43\x4b\x62\x17\xb3\xcd\xbc\x69\x4e\x74\x57\x60\xca\x54\xc8\xc9\xfc\x25\x4f\x14\x77\x04\x3c\x73\x00\xf4\x3d\xdf\x58\x99\xd2\x6e\x9b\x51\xca\xd0\x13\x84\x93\xab\xab\xbb\xbb\xf4\xa9\x58\xba\x1f\xf8\x2b\x92\xe7\xaa\xd3\xdf\xcb\xfa\xe5\xa2\xf9\x82\x1f\x12\x4f\x19\xbd\xdc\xc5\xae\x54\xb2\xb1\x9d\xb4\x59\x40\xfe\xe6\xde\x2c\x10\x6f\x1e\x9c\x08\x70\xb8\x27\x39\x07\x2e\x93\xdc\x0e\x2a\x83\xa2\xcb\x92\x6a\xf9\x1b\x89\x10\xd1\x30\x34\x08\xf4\x60\x4c\x52\x25\x38\xd5\x23\xe3\xaf\xf7\x2c\x3a\x26\xef\x77\xc3\x4d\x63\xa3\x75\x64\x3f\x5e\xfc\x65\x68\x6b\x43\xe2\xce\x65\x97\xeb\xa9\xbb\xb7\x1d\x29\xd7\x84\x72\x03\xf9\xf2\x25\xac\xc1\x3f\x11\xf4\xeb\xd7\x3c\x54\x49\xcd\x03\xa5\xfd\xdf\x81\x1e\x0b\xe0\x8b\xe8\x34\x86\x3e\xa6\x70\x97\xc1\x4c\x57\x4a\xde\x1a\x70\x01\xe7\x4a\xde\xec\x51\x70\x24\x2d\x12\xc2\x0a\x8e\xa5\x61\xb9\xfd\x05\xe5\x53\xa5\xf3\x77\xc5\x04\x29\xb8\x63\xae\xaa\x9c\x33\x0d\x0d\x39\x77\xcc\xf8\xfc\xed\x6c\x6e\xc7\x27\x15\x67\xbc\x9d\x6b\xa9\xaf\xb3\x5c\xca\x75\x11\x3d\x35\xcf\x53\x25\x98\xd6\x01\xee\x4b\x44\x36\x56\xa2\x06\x9d\x69\x81\xa4\xba\xa9\x60\x52\xf5\x2b\x25\x02\x26\x6c\xf0\x4b\x2e\x04\x1e\x63\x90\x97\xcd\x72\x72\xa8\xfc\xae\x3c\xe7\x48\x61\xcf\xcc\x74\x72\xa8\x1d\xe6\x3a\x69\x0d\x32\xb2\x9d\xc8\x26\xa7\x0b\xc6\x90\xc0\xe7\xc2\x2c\x15\x9e\xdc\xfa\x63\x72\xce\x94\xb9\x68\x42\x74\x4c\xc5\x3c\x88\x4c\x3b\xd2\x89\xce\xea\xcc\xce\xd2\xa7\x77\x69\x13\xe7\x7f\x65\xea\x6b\x6f\xa6\x50\x7f\x87\x9a\xb1\x84\x49\x21\xc9\xde\x38\x13\xd1\x95\x96\x18\xaf\xec\xcd\x74\x01\x6d\x90\xf2\xca\x99\x02\xa7\xbd\xb6\xd4\x99\x0e\xec\x95\x09\x93\x2a\x9f\x1c\xfd\xf5\xaf\xbf\xf7\x39\xe5\x3f\xff\x4b\xca\x2a\xff\xfa\x3b\xae\x73\xb8\x30\x52\x8a\x94\x7b\x5c\xba\xa1\xc3\xcc\x1c\x75\x8f\xeb\x10\x8d\x2f\x99\xba\x80\x53\xd1\x58\xe9\xb2\x1b\x2f\x59\x13\xe8\x33\x18\x9f\x25\x47\x53\x1e\x47\x13\x0e\xb6\xd9\x3e\x46\xe7\x95\x32\x11\x55\x0e\xbc\x2d\xd8\x93\x58\x24\x44\x78\xee\xe6\x6e\xfb\xcc\xd9\xee\x38\x10\x86\x19\xf5\xeb\x70\xa5\x30\x5c\xbd\x3e\x6e\x7e\x77\x39\x21\x0a\xee\x06\xcd\x14\x2a\x73\x5e\x58\x44\xc8\xd4\x0c\xe8\x62\x62\x16\xde\x50\x9b\x29\x68\xce\xb0\x90\x2c\x6a\x05\xd8\x00\x51\x0c\x33\x67\x15\x0f\xa9\xf0\x43\x3e\x47\xbc\x14\x94\x59\xab\x61\x45\xd0\xd6\xdb\x03\xa1\x3f\x44\xea\xed\x61\xe7\x60\x45\xcc\x1d\xa0\x07\xc8\x97\x2b\x6c\xaa\xea\xaa\xad\x02\x6d\xea\x6d\xc2\xfa\x6e\xbd\x69\x57\x37\xc8\x15\x8e\x62\xdc\x37\x94\xfe\x86\x12\x08\xc6\xde\xe1\xec\x1d\xc9\x7c\x47\x09\x9c\xe4\xe8\x6b\x14\xbf\xfa\xfa\xa3\x18\x76\x7c\xea\xfd\x6a\x2a\xa2\x55\x71\x3b\xb5\x0d\x55\xce\xa6\xc4\xd1\x14\x73\x0c\x25\x62\xba\xb2\xe0\x6e\x94\x99\xaa\xfa\xc1\x2f\xb5\x32\xe9\x91\x24\x4a\xb2\xc7\xd0\x23\xa7\x40\x96\xa7\xf1\x7a\x61\x26\x0d\x8a\xa4\x08\xfc\x18\x1a\xd4\xd4\x1b\xd3\x82\x59\x8f\xbb\x9c\x9e\x49\x82\x26\x50\xfc\x28\x31\xe8\x80\x84\x1f\xc1\x0a\x90\x60\x49\x8c\x3a\x86\x04\xe3\xa5\xc2\xdb\xe2\x52\xb0\x18\x8d\x1f\x45\x82\x8d\x48\xe1\xef\xf3\x2f\x40\x87\x21\x69\xe2\x38\x3a\x4e\xa7\x83\xd9\xcc\x84\x33\x60\x1b\x66\xb6\x4d\x71\x28\x86\x72\xc7\xa0\xe7\x5c\xf4\x5e\x2d\x79\xba\x91\xcd\x6c\xec\x38\x83\x1d\xd5\xd5\x18\xea\xa2\xf7\x7b\xc1\x9d\xe4\x64\x13\xa0\x38\xe6\x28\xed\x60\x58\x98\xc0\x6e\x4a\xea\x04\x80\x6c\x42\x1c\xcd\x1d\x27\x09\x1e\xe9\x68\xbf\x08\xe0\xfd\x20\x3f\x8b\x12\x86\x32\x14\x79\x54\x8f\x60\x84\x27\xce\xae\x74\x92\xd9\xe3\x18\x86\x33\xf4\x71\x92\x90\x53\x45\xdd\x04\xbf\xe4\x31\x16\xda\x54\x51\xa1\x96\x19\x1a\x31\x8c\xc2\xb0\xa3\x82\x30\x46\x05\x6b\x5a\xc1\x5a\xc3\x26\x47\x0c\x9a\x39\x2e\xcc\x63\xf4\x54\xd5\x67\xd0\xb2\xa7\x87\xab\x19\x39\xa4\x18\x8e\x3d\xae\x47\x98\xc8\x70\xed\x2e\x1b\x81\xec\xc1\x04\xc3\x51\x94\x20\x8f\x22\xc2\xee\xcc\x57\x31\xcc\x60\xbf\xd0\x85\x69\x70\x9e\x51\x15\x47\x9b\x92\x26\x64\x6e\xdf\x38\x36\x4f\x38\xd8\xc2\x11\xf0\x8b\xdd\x20\x57\xf7\xa5\xc7\xfb\xde\xc3\x64\xdc\x9c\x74\x9e\x6a\xd5\xe6\x78\xd8\x98\x8c\xa9\xea\x7d\x8d\x27\x9a\xed\xa7\x27\xfc\xa1\xd7\x68\x31\x1d\xfe\x81\x1f\x09\xbd\xea\x88\x6e\x76\xcb\x03\xa1\x3a\x7e\xec\xb4\xe3\x3a\x49\x25\x82\x3b\x44\xca\x8f\x8d\x7b\xba\xdf\x26\x3b\xed\xba\xd0\x2d\xb7\xda\xd5\x12\x43\xe0\x3c\x49\xd0\xcf\x54\xb7\x5d\x19\xf4\x9b\xf7\x93\x06\x73\x5f\x6a\x96\x5b\xbd\x66\xbd\xda\x21\x07\x8c\xf0\x34\x19\x8f\x0a\x13\x21\x5c\x49\xfa\xdd\xa7\x5a\xbd\x89\x97\xeb\x44\xb5\xdd\x23\x4b\x8f\xcd\x6a\xab\x5d\x69\x56\x1f\x46\xed\xee\x08\xaf\x3d\x11\xcf\xad\xea\xa0\xd6\x69\x8f\xca\x42\x87\x1f\x4c\x98\x5e\x99\xe9\x3c\xe2\xb5\xc2\x44\x48\x87\x08\x4f\x4d\x4a\xdd\x27\x9e\x7a\x22\x27\xbc\x50\x7b\x9c\xf4\xf1\x51\xa3\x83\x8f\x3a\x64\x69\x74\x5f\x1b\xf5\x18\x52\x18\x75\x1b\x9d\x36\xde\xab\x8d\xc9\x49\xbf\xd6\xa9\xf7\xdb\x8d\x46\x2d\xd2\xd7\x47\x6d\x37\x72\xb2\xdc\x9c\xbe\xf6\x77\xa2\xee\x37\x91\x7f\xb7\x60\xf6\x56\x9c\x1b\x84\xbc\x41\x6c\x73\x05\x0b\x58\xe0\xe1\x26\x9b\x63\xd2\xdf\x63\x36\x76\x5c\x44\xd2\xc8\xa4\xed\x06\xc1\x6e\xbc\x6d\x88\xf9\x82\x26\x6d\xec\x38\xd5\xd3\x82\xcd\x1d\x21\x47\xc3\x70\x96\x25\x39\x94\xe2\x58\xca\xe5\xca\x71\x8b\x7f\x3e\x7b\xc3\xdc\xe7\x3b\xe4\x33\xf5\x1d\xf5\x3e\x9f\x6f\x90\xcf\xfb\xcd\x46\xce\x2b\x1d\xd8\xea\x3b\xfc\xfc\xbf\x34\x43\x8d\x53\xc3\x63\xd4\xf0\x1b\x84\xf8\x50\x6a\x2c\xc5\x72\x1c\xc1\xd2\x2c\xe7\x8a\x86\xba\xc4\x2c\xdb\x99\x1f\xe8\xb3\xa9\x08\x34\xa0\x4b\x2e\x6e\x0c\x45\x77\x84\x0b\x13\x20\xa2\x04\x12\xa4\x09\xa3\xbd\xb4\x3c\xc4\x0d\x82\x79\x02\x79\x9b\x3f\x3f\xdf\x39\x22\x7e\xf6\x4c\x61\x3a\x87\x5b\x87\xc6\xa9\xf1\xad\x38\x57\xa4\xcf\x15\x89\x33\xbe\x01\x7d\x90\x96\x7d\x02\x1f\xad\xe5\x98\x3c\xc5\xb4\x7c\x62\xec\x2d\xce\x15\x16\x70\x45\xb3\x2c\xf6\xa1\x5a\xf6\x08\x7c\xb4\x96\x63\xf2\x14\xd3\xf2\x89\x09\x81\xc7\x55\x4e\x90\x4d\xda\x35\x76\x6a\x90\x0d\x76\x8e\x85\x73\x00\x99\x54\x80\x24\x53\x1c\xc4\x31\x4a\x11\x59\x56\x94\x64\x91\x51\x14\x96\xe6\x20\xad\x50\x04\x47\x12\x80\x92\x71\x09\x42\x0e\x60\x1c\x8e\x62\x90\xc2\x14\x82\x64\x69\x1c\x48\x22\x20\x64\xd4\xc9\xd9\x28\x02\x52\x18\xa4\x08\x94\x53\x08\x5c\xc6\x28\x0a\x45\x21\x25\xd2\x28\x43\xe0\xa4\x08\x19\x1a\x2a\xb4\x28\x12\x98\x42\xe2\x80\x22\x31\x19\xa3\x69\x82\xa4\x59\x4c\x94\x44\x9a\x63\x01\x8e\x7b\xa3\x0e\x16\xcb\xfe\xe8\x3b\x82\xbc\x43\xf1\x78\x52\xe8\x3d\x26\xbf\x33\x1c\xc7\x61\x58\xee\x5b\x3f\xae\x63\x2c\xcb\xde\x20\x18\xed\xf4\xe7\xc1\xe7\x06\x21\x9d\xff\x30\xff\xbf\xe0\x21\xb6\xfb\xc3\x61\x8d\xe7\x79\xbe\x8c\x75\xb5\x9a\xd6\x7a\x60\xb7\xe8\x78\xc4\x53\xe2\x53\xad\x35\x7f\xd7\xc5\x77\xc0\xb4\x94\xde\xdb\xb8\x84\x4e\x9e\x50\x50\x7a\x6f\x82\x27\x43\x05\x35\x52\xe4\x1f\x1b\xe5\xfa\x66\x6d\xeb\x83\xe7\xed\xcb\x7c\xae\xc1\x9e\x31\x93\xfb\xcb\xb6\xc8\x30\xa3\x81\xf6\x8a\xae\x66\xd7\x0d\x86\x41\x1d\xd4\xfc\x63\x77\xdc\xbc\x9e\xf1\xbb\x4f\xb5\xd5\x78\x78\x07\x74\x6f\xd1\xd1\x2a\x4d\x1b\xbe\x3e\x89\x2f\xcb\xa7\x3a\x33\x18\x35\x3a\x0a\x7c\x10\xeb\xf2\xfc\xed\x95\x5b\x77\x30\xde\x36\x9b\x80\x9e\xb7\xd6\xf8\xf0\xfa\xe5\x79\xdb\x05\x55\xa9\xbd\x59\xc0\xfa\xed\xc3\x73\xad\xf1\x3a\x56\x59\xab\x76\xfd\xde\x37\x14\x38\xb8\x2d\xb3\x2e\xe6\x56\x9b\x6c\x82\x5f\x4b\xbc\xb7\x27\xc6\xdf\x87\xbf\xec\x3e\xcf\xfc\x23\x46\xf6\x78\xbe\x82\x3e\x24\xbd\xfe\x4f\x7f\x3c\xa3\x42\x53\xfc\x3e\xee\x0a\xf8\x65\xcc\xf8\x8a\x26\x64\x8e\x55\x28\x82\x86\x90\x66\x65\x4c\xc4\x19\x91\x12\x59\x4e\xc1\x09\xa0\x50\x04\x86\x89\x0c\x45\x73\x00\x27\x15\xa0\x60\x24\x4a\x00\x19\x15\x29\x5c\xa4\x09\x42\x44\x19\x11\x72\xdc\x95\x1b\x73\x88\x44\xab\x4e\x35\x76\x16\xa3\x71\x92\xcd\x7d\xeb\x0d\xd2\x24\xc5\xe1\x19\x9e\x40\x14\xf4\x04\xbc\xfb\xfc\x8a\xb5\x57\x94\x81\x8a\x0f\xcc\x84\xd4\xb7\x9d\xf7\xd1\xe6\x9e\x18\x2f\x8d\xf9\xf5\x7b\x95\xef\xd8\x65\xac\x81\xb7\x98\x12\x43\x3f\x6b\x0b\x41\xee\x2c\xc7\xe5\x16\x55\x6b\x9a\x5c\xb5\xfd\x4a\x51\x6f\x80\x5e\xe3\xb5\x46\xcb\x7e\x1b\x76\xab\xcd\xf7\x7b\x76\xdb\x1d\xdd\x02\xde\x70\x7b\xce\xf5\x04\xd7\x1e\xeb\xbb\xff\x78\xf7\xbb\xb5\xff\xbe\xe6\xbb\xbd\xb9\xd7\xd3\xfd\x11\x3f\xde\x3c\x2c\x30\xad\xd2\x5a\xaf\xdf\x56\xaf\x0d\x69\xdb\xfb\x65\x71\x4c\xf5\x96\x17\x86\x6a\x79\xd6\xeb\x9a\x6b\x9a\x58\xbf\x81\xae\xf0\xdc\x9f\x97\x29\xa1\xc6\x97\x64\xb2\xd2\xae\x6e\x28\x51\xb3\x1b\x68\xe5\x7a\x5d\xb2\xd7\x4a\x5d\x1f\x37\xd8\x16\xa9\xd1\x60\xbe\x7e\x57\xd6\x2e\xa9\x04\x4f\x11\xac\x24\x6b\xfb\xff\xdc\x53\x88\xe2\x9e\x82\x5d\xc6\xca\xdd\x55\x33\x27\x59\x70\x86\x57\x8c\x63\xd0\x6f\x28\xf6\x0d\xc5\x10\x14\xbd\x73\xff\xa5\x5a\x33\xce\x10\x14\x91\xf9\x96\x74\x66\x6b\x38\x47\x72\x34\x83\x73\x74\x86\xad\x27\x5b\xba\xc7\xd2\xbf\xdd\x29\xe9\x9f\xd2\x63\x43\x25\xb7\xb7\xdb\x41\xa3\xc4\x54\xf4\x0a\x57\xc3\xd1\xcd\x6b\xe9\xda\x42\x67\xb6\xb5\xae\xaf\x7f\x61\x8f\xf2\x60\xf2\x04\x4a\x0f\xa0\xea\x0e\x27\x42\x82\x11\x27\x7f\x02\x23\xe6\xf9\xd2\xfc\x37\x08\x72\xd1\xcf\x95\x67\x4c\xf9\x09\x55\x81\xfd\xc2\xa7\xe6\x57\x29\xcb\x8d\x69\x73\x5a\x3c\xc5\xe3\x72\xd0\xc4\x27\xab\xd8\x69\x68\x62\x93\x44\xe2\x34\x2c\x64\x6c\xaa\x79\x1a\x16\x2a\x36\xb7\x39\x0d\x0b\x1d\xc5\x42\x9e\x86\x85\x89\xcd\x00\x4e\xc3\xc2\xc6\xe6\x45\x97\xd9\xcb\x7d\x91\x82\x4f\xf6\x82\xf6\x0d\xc2\x16\x2d\x74\xa5\xec\x68\x3e\xdb\x7b\x42\x1e\x13\xb1\xf3\xdd\x17\x72\x37\x5f\xf8\xe7\xb3\x6d\x9c\x35\x05\xbb\x41\x3e\x2b\xa6\xb1\x38\xab\x24\xe1\x4c\x3a\x8f\xaa\x13\x7d\x40\x7d\x37\x41\x79\x61\xbf\xdc\xfd\xcd\x86\xe6\xe8\xca\x4a\x97\xa1\xe9\xa9\xef\xb4\x42\xb0\x2b\xa3\x57\x29\x3d\x57\x83\xf9\x05\x83\x0f\x28\x58\xa7\x69\xcd\x8f\x20\xbb\xbf\xc9\x0f\xd5\xda\xa9\x45\x9a\xff\x9c\xd6\xbc\x58\xb7\xfb\x1b\xfd\x50\xad\x9d\xe1\xf1\x1f\xae\xb5\x9c\xc0\x99\xf0\xbb\x85\x33\x36\xc8\x1c\xb5\x85\xfb\xd4\xe0\x9c\xba\xc5\x28\x31\xb9\x21\xd3\x33\x81\x5c\x44\x78\x3c\xd2\x9f\x8a\x88\x88\x86\xbd\x93\x19\x22\xa3\x78\xd2\x12\x82\x5c\x3c\xb1\x80\x72\x32\x1e\x3a\x8a\x27\x2d\xcd\xc9\xc5\xc3\x44\x5d\xf5\x64\x7e\xd8\x98\xcb\x1f\xbd\x11\xee\x23\x93\x9d\xbc\x4d\x6d\x47\xa4\x3b\xa9\x5b\x9a\x2f\xe0\x53\xa1\x15\x71\x09\x8a\x22\xcb\x50\x00\x45\x15\x85\x86\x18\xc1\x12\x00\x2a\xa8\x22\xe3\x14\x06\x18\x5a\xc1\x71\x09\x53\x38\x20\xe2\x00\x97\x15\x45\x12\x51\x86\x61\x29\x8a\x21\x68\x20\x43\x9c\xa6\x38\xe0\xcd\xe6\xb1\x73\x72\x8c\x50\xa9\x88\x08\xa6\xc8\x69\x13\x6e\x14\x45\xd9\xf4\xd2\x53\xf0\x36\xe2\xd1\xde\xdc\xba\x41\xbf\x42\x95\x78\x5d\x18\x75\x76\x78\xaf\x55\x6e\xe1\x4c\x22\x98\xee\xa3\x5d\x6b\x34\x7e\x4d\xc6\xec\x7a\xac\x3e\x97\x40\x79\x45\x35\xa9\x16\xef\xce\x4d\xf9\xa0\xf6\x53\x8a\x4d\xfd\x42\xdf\x05\xf7\x7f\x71\x31\x5b\x60\x63\x5c\x9e\x51\x63\x6c\xf1\x86\x41\xad\x25\xdd\x63\xf6\xe6\x75\xf0\xd4\x78\xe6\xd6\xc2\xcc\x18\x94\x00\x9c\xb0\x23\xb5\x6a\x84\xd0\x34\x69\xb6\x1e\xfa\x0a\x98\xf9\xfb\x7c\xed\xa2\xe7\xba\x2b\x6e\xf9\xba\x9d\x4b\xfd\x01\x8d\x6a\x6f\x9d\xe6\x5b\x9b\xad\xd6\x7e\xe1\x24\xd9\xeb\xb2\x22\x78\x6a\xc3\xe1\xf0\xe1\xb9\xae\x99\xc4\x40\xec\x97\x31\xe2\x4d\x30\xb9\x55\x97\xec\xf4\x2b\xb3\x6d\xb9\x74\x3b\x93\x56\x33\xfc\xbe\x61\x56\x5a\xab\x06\x3a\x18\x12\xbd\x0e\x68\x8c\x4a\xeb\x9f\x3f\xaf\xc2\x75\x86\x70\x05\xb6\x97\x24\x1b\xbf\x87\xdf\x17\xc7\x2a\xee\xf7\xf5\x1e\xc8\x7c\x6b\xd3\x4d\xd8\x01\xb3\xd7\x4d\x0b\x8c\xba\x1c\x5d\xfa\xa5\x58\x1c\x44\x25\xc3\x6c\x3f\x3f\xfe\x2a\x4d\x1e\xe6\x55\xa3\x11\xc8\xc6\xf3\x1d\xca\x7c\xd0\xf7\xba\x4d\xf9\x08\x69\x2f\x4a\x17\xa6\x1f\xef\xdb\x42\xf4\xbd\x46\xae\x99\x94\x43\xaf\x56\xa0\x2c\x8e\x1f\x9f\xf1\x8a\xf6\x38\x01\xe6\x98\x1e\x6d\xd6\xe2\x84\xb8\x6f\x3f\xcc\x96\x3a\xc1\x0f\xca\x2f\xf5\xea\x92\x12\x37\x83\xfa\xc4\x2b\xbb\x33\x0b\xcb\xb7\x87\x59\x86\x2a\x52\xcb\x28\x2e\x8f\x95\x33\xe8\x5f\x6b\xe2\xdb\x19\xf4\x5b\x31\xfa\xe5\x95\x41\x18\x36\x49\xbd\x95\xbb\xc2\x66\xd9\xbb\x25\x8c\x5a\xfb\xfa\x17\xc6\xf4\xb7\xaa\x85\x69\x4a\xab\xfa\xb4\xe8\x4d\x66\xe6\x6a\x70\x3d\xe4\x03\xf9\x17\xd2\x9e\x7e\x8a\xce\x0b\xcb\x7f\x34\x7d\x52\xe7\xe6\x27\xd2\x0f\xf9\xd2\x8c\x4f\xb0\x85\x53\x74\x71\x49\x5b\x38\xb7\x2f\x8e\xa1\xef\xe9\xe2\x9f\x8f\x0a\x5a\x6e\x72\xec\xfe\x20\x22\x28\xe2\x7a\xff\x3b\x83\xa8\x3b\x58\xe4\xe7\x11\xe1\xad\xd1\x04\x43\x42\x67\xa8\xe1\x44\x0e\x2a\x8c\x2c\x02\x0e\x50\xb2\x48\x10\x04\x27\x32\xac\x22\x03\x56\x21\x48\x86\x61\x44\x0c\x28\x04\x21\x02\x92\x66\x81\x4c\x49\xa8\xac\x70\x24\x2d\x93\xf2\x95\xbb\x24\x8c\x9d\x93\xaf\x7b\xc5\x84\xcc\x41\x0e\xa3\x09\x9a\xcb\x58\x5f\xf1\xde\x86\xb3\x44\xcf\x16\xef\x9b\x6c\xad\xf7\xde\x9b\x8b\x0d\xbc\xc6\x13\x93\xf1\x6b\xdf\x6c\x2c\x5e\x1f\x51\x54\xb9\x67\xad\x66\x9d\x59\xa0\x42\x7f\xfd\x30\xb9\xe5\x1f\x09\x7e\x37\xc6\xb9\x9f\x8c\x31\xce\xfb\x9c\x10\x67\x1b\x61\x7c\xe3\xf7\x75\x95\x73\x5e\x09\xe5\xca\xaf\xb7\xf7\x79\xaf\xd4\x33\xda\xfc\x83\xaa\x74\xfb\x8f\x15\xa3\xf9\xf2\x6e\x6f\xa5\x21\xa1\x55\xbb\xe5\x1e\x85\xcd\xe6\xb2\x55\xad\x81\x52\x7b\xb2\x46\xa9\xc1\xed\xf8\x65\x82\x3e\xce\xe6\x26\x5a\x2e\x75\x05\xb2\x0d\xaa\x63\xbc\xb1\x90\x2c\xe2\x79\xdd\x5c\xa8\x22\x39\xec\x9b\xad\x66\x81\xb1\x2d\x62\xb4\xd1\xb1\x2d\x24\xb3\xcb\xfe\x41\x6c\x57\x6f\x4b\x68\x13\x7d\xb8\xdf\xda\x2f\xeb\x36\xa6\x3d\xa1\x60\xbb\x34\x30\xae\x5d\xdb\xbc\x37\xcb\xdb\x0e\x65\x97\x04\xa9\xec\xc9\x48\xcc\x6c\xb3\xa3\x3f\xdd\x32\xa3\x7d\xfb\x56\x22\x13\x39\xfe\x7c\x06\xfd\xb6\xb9\x1d\x0e\xcf\xa0\xcf\xf3\xff\x5e\x3c\x4b\x8c\xad\xa5\xd3\x75\xd1\xd1\x9f\x33\xc5\xcc\xd3\xc5\xb9\x7d\xe1\xd8\xc2\xb5\x14\xc3\x77\x94\x2e\xfe\x99\xb1\xb4\x49\x09\xfc\xa8\x51\xe9\x95\x9f\xf4\x5f\xe8\x78\x4d\x97\x49\x91\x91\x74\x81\xa3\xfa\xc3\xf5\xbc\x23\x3f\x3d\xd4\xc4\x52\x1f\x9f\x0d\xc7\x56\xbb\x33\x7a\xc7\x9e\xc6\x76\x95\x7c\x68\x70\xfc\x6c\xb8\xe9\x54\x26\x2f\x63\x59\x5d\xea\xcd\x36\x2e\x95\x29\x63\x71\x2d\xa0\xe0\x57\xf9\xe2\xb1\x15\xa3\x49\x40\xa1\x34\x09\x45\x40\x93\x0a\x2e\xc9\x22\x90\x45\x96\xa2\x45\x85\x20\x49\x96\x64\x29\x45\xa2\x71\x1a\x27\x19\x20\x03\x02\xca\x04\x27\xc9\xb2\x82\x2a\x34\x87\xe2\x18\x41\x88\xb4\x17\x5b\xf1\xf3\x62\x2b\x9e\x1f\x5b\x29\x8c\xcc\x88\xad\xde\xdb\xf0\x8c\xf7\xdc\xd8\x5a\x8e\x75\xea\x41\x6c\xed\xe0\xe5\x5b\xbe\x43\x52\x4f\xa5\x0a\x61\xd7\xc6\xd5\x0e\xd6\x27\x78\xb4\x05\xe7\x5d\xf6\xa1\x4f\xeb\x6d\x8c\xe7\xe0\x44\x95\xb7\x75\xdb\x73\xe9\xf4\xd8\xca\x0f\x84\x67\xf5\x59\x84\xd5\x75\xd9\x32\x1b\x25\xbd\x51\x5f\x59\xb7\x28\x35\xb6\x1f\x2a\x25\x73\x66\x58\xab\x97\x66\xef\x76\x44\x3f\x8e\x5e\x49\x7b\x3d\xd9\xbe\x58\xcc\xc8\x1e\x90\xe5\x16\xdc\x74\x5a\xf4\xc3\x9b\xa4\xbc\x3d\x34\x30\x74\xa2\x95\xe6\xf3\xb5\x4e\xce\xd8\x6e\x5d\x79\xad\xdf\xff\xb7\x62\xeb\xb9\xb1\xed\x5c\x7f\x6e\xad\x9b\x0b\xf3\x82\xb1\x95\x67\x9e\x9a\x2c\xcf\xbc\x6a\x33\xa1\x0b\x51\x79\x34\x62\xc6\x35\xa9\xd2\xdb\xd0\xbd\xdb\xb5\x56\x7b\x93\x88\x51\x05\xa3\xc0\x03\x51\x57\x31\x0f\xe7\xa5\x63\xeb\xbf\x14\xdb\xf8\x0b\xc5\x56\x96\xdc\xb7\x4f\x99\x53\x66\xc5\x56\xe1\xe5\xfe\x69\x31\x21\x5e\x24\xde\x6c\x6c\x67\xcf\x5b\xb5\x69\x76\xb9\xce\x58\x1c\xf4\xd6\x80\x6c\x34\x9b\xc6\x00\xed\x62\x1d\x0d\xab\x5f\x37\xa5\xaa\x65\x88\x1d\xac\x39\x5a\xf1\xaf\x35\x6b\xf8\xda\x51\x81\x5e\xa3\xd5\x81\x2d\x57\x97\xbd\xe7\x87\xd6\xc3\x75\xbd\x5b\xd9\xd6\xc8\x6d\x69\x76\xf1\xbc\x55\xc4\x21\x8b\xcb\x22\x10\x45\x14\x27\x45\x9c\x01\xa8\x44\x60\x24\x2a\x01\x06\x93\x59\x20\x71\xa2\xc4\x60\x2c\x81\x29\x9c\x42\x01\x42\x94\x69\x0e\x4a\x80\x90\x59\x56\x11\x51\x28\x51\xd2\xd5\x6e\x2b\xe3\x19\xb1\x35\xb7\x38\x83\xd1\x34\x9e\xbe\x57\x22\x78\x1b\xae\xde\x9d\x1b\x5b\x2b\xb1\x4e\x3d\x88\xad\xc7\xd6\x66\xd2\x63\x6b\xe5\x61\xa5\x61\x76\xf3\xbe\x59\x25\xc7\x9b\xb5\x8d\xca\x95\xf2\x58\x50\x68\x5b\xa4\x34\x52\xdc\xb6\xcc\xfb\x59\x79\x79\xad\x8d\x9f\x5b\x8b\x8d\x64\x53\xa4\xda\x56\xf0\xc5\xc6\x7e\xdd\xd0\x2d\x99\x7a\x7e\x20\x05\xb2\xa2\x49\x96\x42\xd2\x02\xff\x52\xba\x1f\x8c\xba\x96\xce\x2a\x4f\x95\xff\x56\x6c\x3d\x37\xb6\x9d\xeb\xcf\x4d\x74\x4e\x57\x2e\x18\x5b\x7f\x67\x4d\xe6\x23\x62\xeb\xa9\xb1\x8d\xbf\x50\x6c\x3d\x75\x0e\xe3\xc7\xd6\xad\xb8\x94\xc5\xc1\x46\xdd\xc0\xaa\x24\x35\xe5\x5a\x6f\xad\xf5\x6b\xd7\xe6\xe4\xfa\x19\xde\xb3\xaf\x8d\x8d\xc1\xbf\x29\xcb\xf1\x64\xf8\x60\x3d\x36\x21\xac\xbf\x3e\x72\x4b\x4b\x7c\x62\xe1\x6b\x0d\x4e\x06\xb0\xd4\xe1\xa9\xc7\x66\xed\xba\xf3\xc2\xd7\x7b\xfd\xb9\x56\x61\x1e\x6e\x6b\x38\x5f\x30\x6f\x4d\x2e\xae\xcf\xe1\x76\xfa\x0e\xb4\x15\x9c\x3a\xd1\x16\x9e\x55\x57\xf7\x0f\xb8\x8e\xa1\xdc\xc7\x6c\xb8\x59\x06\x3f\xd0\x73\xcf\xa6\xf1\x76\xb6\x39\xac\xa3\x57\xf1\x63\x68\x0e\x4e\xc9\x4e\xbe\x9a\x6b\x77\x15\x46\x70\x44\xd3\xb1\x3f\xe1\x4f\x39\x94\xdb\x3d\xff\x81\xaf\x54\xc2\x87\x3f\x25\x72\x80\x74\xfb\xf5\x16\xdf\x7f\x42\x1a\xc2\x13\xf2\xc5\x7b\x73\x13\x80\x66\x1d\xb2\x7d\x78\x19\xd8\xd9\xb2\x84\x30\x26\xf2\x1f\x23\x18\x65\x5d\x95\xf3\xce\xc1\x4e\xbe\x80\xed\x6c\xae\x63\x58\x93\x38\x4f\x22\x9c\xcb\x7d\xec\x20\x8d\xd8\xcf\xad\x0a\x5e\x60\x77\xb6\x74\x51\xb2\x49\xc2\x9d\xc4\x18\x32\x6a\xd7\x7b\x23\x01\xf9\xb2\x07\xbf\x09\x1d\xf8\x7c\x13\x39\x9e\xf9\x48\xd5\x5c\xa6\x5b\x8f\x16\xfc\xa8\x4e\x4d\xd9\xfd\x95\xb3\xc5\xea\xb2\x92\x25\x13\xc9\x92\x34\x83\xad\xc2\x92\xa7\x2e\x07\xe7\xae\xb8\x5e\x56\xfa\x34\x32\x59\xf2\x67\xb2\x96\xa4\x81\xc3\x13\xe3\x22\xd7\x1d\x5e\x2a\xfa\x7b\x38\x93\x38\x0f\x51\x8b\xf2\xe7\x1f\x23\x77\x30\x6c\x45\xee\x6d\xf5\xf9\x73\x6f\x46\x2d\x76\xee\x96\x77\x89\x6a\x04\x0b\xd2\x69\xc7\x1d\x76\x34\xa8\xb7\xef\x11\xd1\x36\x21\x0c\x47\x80\x94\xe1\x66\x7f\xe5\xec\xd9\xfc\xf8\xc7\xbd\x17\xe2\x28\x25\xf6\x84\xae\xcb\x3d\x95\x9d\x3d\x8a\x30\x27\x91\x79\x62\x94\x1f\x0f\xf8\xe6\xe0\x14\xb0\x24\xe6\xdc\x0b\x7f\xcf\xe0\xcc\x3d\x0c\xad\x10\x5b\xf1\x23\xd4\x92\xb8\xf1\xaf\xe5\x3d\x83\x1f\xff\xdc\xbf\x42\x1c\xc5\xce\x67\xbb\x39\x3c\x8a\x2d\x31\x2c\x85\xaf\x5d\x3e\x9e\x53\x7f\x24\xf3\x18\x8e\xa1\x0b\xb3\x1d\xfc\x52\x30\xc2\x71\xd2\x69\xb1\x37\xc1\xc9\xb0\x69\xcc\xee\xcf\x7d\x3a\x93\x4d\x55\x2e\xcc\xe0\xfe\x68\xcc\x9b\xc4\x23\x6e\x73\x98\x0e\x6e\xca\xbe\x04\xdf\x3e\xae\x30\xeb\x29\xc3\xe9\x49\x92\x24\x0b\x10\x5c\x0a\x7e\x09\x01\x7c\x5c\x29\x36\x7d\xa2\x08\xd1\x73\x4e\x0f\x85\x08\x5d\x81\x7e\xaa\x37\x86\x70\x9c\xaa\xfc\x6c\x45\xc7\xee\x74\x3f\x57\xd7\x51\x74\x61\x96\x83\xdf\x24\x45\x78\x4c\xe6\xe8\xf0\x5e\xfa\xf3\xd9\x3a\xc0\x59\x2c\xbc\x25\x31\x18\xba\x61\xff\xe4\x6e\xdd\xe3\x38\xdd\x24\xf3\xcc\xcf\x36\x65\xf7\x0e\xf5\xd0\xe1\xd3\x67\x30\x7c\x88\x2c\xc6\xb9\x0c\x63\x7c\xc6\x4e\xbd\xce\x66\xd0\x4d\x8e\x2e\xc3\x9e\x8b\xaa\x10\x73\xc1\xd9\x48\xa9\xac\xc5\xce\xd3\x3e\x9b\xbf\x18\xbe\x3c\x26\x0f\x8f\xf3\xce\xe5\xf4\x32\x7a\x8c\x60\x2b\xca\x65\xae\x36\x2f\xc3\x5b\x21\x9e\xb2\x79\x09\x38\xd6\x0c\x63\xbe\x5a\x9e\xc7\x51\x14\x57\xe1\x1e\x0d\x0e\x0c\x4f\xe4\x6f\x09\x54\x73\xea\x9e\x3e\x7a\x09\x0e\xe3\xd8\x8a\xf9\xad\xcf\xe0\xcd\xc1\x19\xe7\x37\x07\xe7\xe4\xa7\x08\x71\x81\xb8\xed\xe3\xc9\xe3\xf8\xc8\xec\xc8\xc1\x7a\x31\xed\x1e\xa1\xd8\x5c\xbd\x79\x47\x4e\x1e\x9c\x67\x64\xe8\x53\xff\xf2\xb0\x73\x15\x9a\x4b\x20\x32\x4f\x0b\x8e\x86\x8a\xce\x8c\x3c\xc0\x23\x78\x3f\xdf\x0e\xb2\x70\xe7\x73\x9c\xe0\x65\x51\x84\x7e\x16\xee\xe0\xb3\xb7\xcb\xd3\xe7\xe0\x99\x58\x73\xd3\x7e\x07\x28\x87\x51\x3f\x87\x72\x50\xee\x8c\xe8\x42\xdc\x26\xa1\xce\x4d\xdf\x8a\x5a\x72\x08\xf9\xa5\x8d\x21\x82\xfa\x94\x7c\x33\x1d\x5d\xec\xa6\xa8\xcb\x2b\xfa\xe0\x2e\xaa\x5c\xf6\x63\x0d\x8a\x0b\x13\xba\x1a\xec\xc3\xf4\x1f\xbe\x7e\x2c\x4f\x92\x10\x6c\x71\x21\x92\x2e\x3a\xfb\x30\x69\x12\x6f\x55\xcb\x13\x2b\xa9\x51\x71\xf9\x82\x22\xca\x87\xc9\xb4\x3b\xca\x3e\x4f\x8e\xd4\x6a\x57\x14\xf5\xfe\x57\x85\x1f\xe1\xda\x71\xec\x89\x13\xe0\x63\x1d\x3c\x8a\x34\x3a\x85\xba\x90\x87\x67\x91\x28\x22\x43\xce\xbc\x2e\x93\xd8\xe5\x86\xaf\x43\xc4\x85\x78\xcf\x1f\xc4\xc2\x93\xed\x8f\x30\x9b\x43\xfc\x27\x4f\xf5\xbd\x13\x74\x83\x81\x3c\xa8\x30\x4e\x45\xc3\x98\x9f\xac\xe5\x0c\x9c\xb9\x29\xc2\x97\x2f\xc1\xb5\x5d\xdf\xfe\xfc\x13\xb9\xb2\x0c\x4d\x0e\xad\xf8\x5d\xdd\xdd\xd9\x70\x63\x7f\xfd\x7a\x83\xa4\x03\x4a\x86\x5c\x0c\xd0\xab\xc5\xa7\x83\x8a\xc6\x6a\xf6\x62\x17\x22\x1f\x01\xcd\x66\x20\x02\x1a\x63\xe1\x2b\x32\xa9\x09\x7d\xc1\x33\x32\xe4\x27\x42\x10\xc9\xeb\x3d\x8e\x52\xbd\xfb\x7d\x4e\xee\xa4\x38\x22\xa7\x67\xfc\xc5\x24\xaf\x43\x4a\xc3\xbe\x20\x7c\x09\x6e\x11\xca\xe6\x43\xd5\x67\x9e\x40\x17\x62\x67\x87\x2f\x83\xab\xe0\xee\xa1\x74\xce\xbc\x0b\x8a\x2e\xc6\x58\x18\x5d\x0a\x5f\xa1\x2b\x91\x0e\x3c\x6d\x8f\x28\xe9\x0a\xa2\x0b\xf0\x97\x78\xb3\x51\xa7\x1d\x5d\xce\x8b\xe5\x28\x09\x4d\x0a\x6f\xcf\x50\xe5\xa9\x12\x5a\x6f\xac\x36\x7e\xcf\x26\x0d\x9f\x2c\x52\xed\xf4\x85\xfa\x7d\x7b\xb7\x30\x8a\xf4\x85\xaa\xd0\x17\xda\x65\x61\x10\x5b\x87\x73\xdf\x76\xda\xc8\xa8\x5b\x71\xf4\xd6\x17\x06\xc3\x7e\xbd\x3c\x74\x1e\x55\x84\xa6\x30\x14\x90\x32\x3f\x28\xf3\x15\x21\xfb\x46\xbf\xd8\xd7\x69\xac\xf8\x77\x39\x65\x44\xe9\xe4\x2c\x1d\xa7\x71\x12\xd5\x4f\xbc\x50\x99\xa8\x2c\x7f\x6a\x99\xb3\xce\x9e\xaa\x09\xbf\x78\xf2\xaf\xeb\x21\xcc\x47\x92\x16\x82\xba\x54\xb6\xc1\x1c\xa7\x81\xc3\x32\xe6\xbf\xa8\x86\x14\x66\xa2\xba\x48\x28\xbc\x5e\xd6\x28\xe2\x45\xb5\xff\x82\x42\xd2\x4d\xe3\xa0\x6a\x59\xd4\x3a\xba\x86\x65\xcf\x4c\x38\xe8\x35\x11\x19\xd8\xc0\x31\x31\x44\x5e\x2d\x96\x88\x64\x2c\x96\x1a\xb4\xe1\xa7\x83\x0b\xc4\xe2\x5b\x01\xbf\x7c\x42\x9c\x67\xc8\x3b\x30\xa5\x17\x60\x7e\xc1\x29\x2a\x7a\x53\x97\x0b\x9c\xfe\x3a\xb2\x05\x63\x0e\xb7\xee\x85\x8a\xff\x2f\x00\x00\xff\xff\xa4\x8b\x5e\x46\xd7\xa1\x00\x00") +var _baseHorizonSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x7d\x79\x6f\xa3\x48\xf3\xff\xff\xfb\x2a\xd0\x68\xa5\xcc\x68\x32\x1b\xee\x63\xf6\x3b\x8f\x84\x6d\x7c\xc4\xf7\x15\x27\x59\xad\x50\x03\x8d\x4d\x82\xc1\x01\x1c\xdb\xb3\x7a\xde\xfb\x4f\x1c\xb6\x01\x73\xda\xce\x3c\xfb\x8b\x56\xb3\xb6\xa9\xfe\xd4\x41\x55\x75\xf5\x01\xfd\xed\xdb\x6f\xdf\xbe\x21\x03\xd3\x76\xe6\x16\x1c\x0f\x3b\x88\x02\x1c\x20\x01\x1b\x22\xca\x7a\xb9\xfa\xed\xdb\xb7\xdf\xdc\xeb\xb5\xf5\x72\x05\x15\x44\xb5\xcc\xe5\x91\xe0\x1d\x5a\xb6\x66\x1a\x08\xf7\x07\xfd\x07\x16\xa2\x92\x76\xc8\x6a\x2e\xba\xcd\x63\x24\xbf\x8d\x85\x09\x62\x3b\xc0\x81\x4b\x68\x38\xa2\xa3\x2d\xa1\xb9\x76\x90\x1f\x08\xfa\xa7\x77\x49\x37\xe5\xd7\xd3\x5f\x65\x5d\x73\xa9\xa1\x21\x9b\x8a\x66\xcc\x91\x1f\xc8\xcd\x74\x52\x67\x6f\xfe\xdc\xc3\x19\x0a\xb0\x14\x51\x36\x0d\xd5\xb4\x96\x9a\x31\x17\x6d\xc7\xd2\x8c\xb9\x8d\xfc\x40\x4c\x23\xc0\x58\x40\xf9\x55\x54\xd7\x86\xec\x68\xa6\x21\x4a\xa6\xa2\x41\xf7\xba\x0a\x74\x1b\x46\xd8\x2c\x35\x43\x5c\x42\xdb\x06\x73\x8f\x60\x03\x2c\x43\x33\xe6\x7f\x06\xb2\x43\x60\xc9\x0b\x71\x05\x9c\x05\xf2\x03\x59\xad\x25\x5d\x93\x6f\x5d\x65\x65\xe0\x00\xdd\x74\xc9\xf8\xce\x44\x18\x21\x13\xbe\xd2\x11\x90\x56\x1d\x11\x1e\x5b\xe3\xc9\x18\xe9\xf7\x3a\x4f\x01\xfd\x1f\x0b\xcd\x76\x4c\x6b\x27\x3a\x16\x50\xa0\x8d\xd4\x46\xfd\x01\x52\xed\xf7\xc6\x93\x11\xdf\xea\x4d\x42\x8d\xa2\x84\xa2\x6c\xae\x0d\x07\x5a\x22\xb0\x6d\xe8\x88\x9a\x22\xaa\xaf\x70\xf7\xe7\xaf\x60\x28\x7b\x9f\x7e\x05\x4b\xd7\xaf\x7e\x9d\x82\x3e\xb7\xf2\xda\xf9\x02\xba\x8e\x9c\xc5\x2c\x44\x75\x04\xf7\xc8\x5b\xbd\x9a\xf0\x18\xa2\x0c\x60\x1d\x6b\x6d\x3b\xa2\xae\x19\xae\x68\x3b\xd1\xd9\xad\xa0\x28\x9b\x0a\x14\x35\xdb\x5e\x43\xab\x54\xe3\x33\x9a\x1c\x0d\x91\xd7\x0c\x28\x50\x84\xaa\x0a\x65\xc7\x6b\x68\x5a\x0a\xb4\x44\xc9\x34\x5f\xb3\x1b\xda\xda\xdc\x80\x56\x98\x57\x36\xbd\xa9\xaa\x01\xb9\x0d\x75\xdd\x0d\x6c\xcf\xa4\x65\x1a\xe5\x99\xe0\x48\xad\x03\xdb\x11\x97\xa6\xa2\xa9\x1a\x54\x44\x1d\x2a\xf3\xe2\x6d\xa5\xf5\xae\xa0\x74\x9a\xa1\xc0\xad\x18\x72\x43\xc3\x06\x5e\x4a\xb2\x45\xd3\xc8\xb5\x7c\xb4\xb5\xb9\x82\x16\x38\xb4\x75\xbd\xe5\x82\xd6\x47\x49\x2e\x92\xa2\x5c\x5b\xdf\xca\x5e\x43\x1b\xbe\xad\xa1\x21\x97\x52\x21\xd4\x7c\x65\xc1\x77\xcd\x5c\xdb\xc1\x6f\xe2\x02\xd8\x8b\x33\xa1\x2e\x47\xd0\x96\x2b\xd3\x72\x13\x67\xd0\xfb\x9d\x0b\x73\xae\x2d\x65\xdd\xb4\xa1\x22\x82\x52\xbe\xb8\x8f\xe7\x33\x5c\x29\x08\xe6\x33\x84\x0e\xb7\x04\x8a\x62\x41\xdb\xce\x6e\xbe\x70\x2c\xc5\xab\x10\x44\xdd\x34\x5f\xd7\xab\x02\xd4\xab\x3c\x91\x7c\x2a\xa0\x59\x25\x81\xf7\xdd\x63\xe1\x06\x6e\xaa\x74\x73\x46\x31\xd2\x3d\xfc\x19\x4d\x0a\x65\xd7\x7d\x23\xaf\x13\x2c\xc1\x24\xdc\x69\xe6\xb5\x58\xb9\x0d\x16\x4e\xee\x1d\xb0\x23\x09\xc8\xed\xbe\xf2\x5b\x04\x71\x5a\x84\xd8\xf4\xe5\x30\x73\x09\x35\xdb\x11\x9d\xad\xb8\xca\x87\x74\x29\xcd\x55\x51\x4a\x58\x94\x6c\xdf\x9b\x66\x13\xc3\xed\x4a\x0c\x57\x17\x05\xfb\xfb\x84\x66\x6e\x79\x91\xdd\x48\xda\x15\xea\x0c\x5d\xfb\xe6\x66\xcc\xa2\x1d\xbf\x2f\x64\x41\xad\x0e\xc4\xf9\xba\x1c\xd2\x8d\x66\xa8\xba\xd7\x69\x89\x0a\xb4\x1d\xcd\xf0\x3e\x17\x6c\xbb\x30\x97\x50\x54\xcc\x25\xd0\x8a\xb6\x70\x07\x4c\xe1\x32\xd3\x00\x4b\x58\xa4\xcc\x0c\xd5\x67\x19\x65\x66\xb8\x8a\x5b\x15\x2c\x60\xfd\xd2\x25\x03\x34\xa8\x6d\x8a\xe2\xbd\xc2\x9d\xf8\x0e\xf4\x35\x14\xdd\xbc\x0e\x33\x80\x63\x94\x85\x39\x24\x94\x4c\xe2\x0a\x58\x8e\x26\x6b\x2b\x60\x64\xd6\xe1\x79\x4d\x4b\xcb\x70\x28\x79\xca\x4a\x90\xdc\xb0\x34\x7f\xcf\xe3\x8b\xf0\xf3\x09\x3f\x1c\xdf\x8f\x40\x6f\xa4\xe2\x7f\xf4\x46\x2e\xc1\x28\xce\x8b\x60\xb1\xa0\x04\x73\xd3\x5a\x89\x4b\x6d\x1e\x54\x94\x19\x22\xc4\x28\x0b\xeb\x18\xcb\x81\x19\x1c\xe2\xd9\xb2\x28\x87\xf2\x83\xc3\xc2\xc8\xfb\x84\x12\x0c\xa4\xb2\xe0\x63\xa4\xa5\x79\x14\xc1\x2e\x2d\xb7\x9b\x08\x8b\x00\x7b\x09\x33\x0b\xbd\x68\x52\xf0\x5b\x57\xfb\x9d\x69\xb7\x87\x68\x8a\xcf\xbb\x26\xd4\xf9\x69\x67\x52\x10\x3b\x25\xd8\xaf\x80\x1c\x84\x59\x36\x92\xf7\x2d\x05\x28\x94\xf9\xb3\x09\xfd\x6c\x9e\x4d\x13\x4b\xcc\xd9\xc4\x49\x03\xd8\xa0\xc5\x58\x18\x4e\x85\x5e\xf5\x8c\xbb\xe5\x76\x8d\x36\x7c\x2b\xcd\x39\x02\x52\xb8\xb5\x92\x67\xb4\xd3\x01\x6e\x61\x0d\x53\xf2\x7c\x19\xfd\x92\x21\x8a\xb5\x0d\x86\x82\xc5\x88\x83\x71\x5f\x61\xdd\x82\x9c\x5f\x46\x17\xbf\x49\x41\xda\x20\x07\x14\x97\xe7\x50\xd3\x15\x90\x28\xd6\x6b\x64\x13\xc7\x3a\x80\x6c\xe2\xe2\x84\xb1\xcc\x5c\x90\xda\x4d\x89\xc5\x48\x03\x2a\xbe\xd1\x18\x09\x0d\x7e\x92\x40\xb9\xd4\x0c\x71\x65\x69\x32\xfc\x6c\xac\x97\xd0\xd2\xe4\xbf\xfe\xfe\x52\xa0\x15\xd8\x9e\xd1\x4a\x07\xb6\xf3\x19\x18\x3b\xa8\x7b\x73\xfe\x05\x5a\xa8\x9a\x95\xd8\xa4\x3e\xed\x55\x27\xad\x7e\x2f\x43\x1f\x11\xcc\xe7\x47\xe9\x6e\x91\x13\x41\x33\x30\xf6\xda\x5d\x80\xe1\x4d\x21\xba\xcd\x8f\xc2\xdf\x22\x65\x14\xf1\x54\x2f\x80\x20\x3c\x4e\x84\xde\x38\x06\xa1\xaf\xe6\xf6\x9b\xbe\x8f\x9b\x6a\x53\xe8\xf2\x27\x1c\xfe\xfc\xcd\x5f\xee\xe9\x81\x25\xfc\xbe\xff\x0d\x99\xec\x56\xf0\x7b\xd0\xe4\x4f\x64\x2c\x2f\xe0\x12\x7c\x47\xbe\xfd\x89\xf4\x37\x06\xb4\xbe\x23\xdf\xbc\x55\xa0\xea\x48\x70\xef\x57\x80\xbc\xc7\xfb\x2d\x82\x18\xbd\x18\x00\x57\xfb\xdd\xae\xd0\x9b\x64\x20\xfb\x04\x48\xbf\x17\x05\x40\x5a\x63\xe4\x66\xbf\xbe\xb3\xff\xcd\xf6\x40\x6e\xe2\x9c\xf7\xea\x07\x3c\x0f\x16\xca\xd5\x27\x62\xcb\x5e\x7f\x12\xb3\x27\x32\x6b\x4d\x9a\x07\xb1\xc2\x0b\x3d\x11\xf6\x47\x94\x98\x20\x65\x94\x3f\x01\xf1\x0c\x30\xe8\xdc\xad\xe6\xe3\x61\x07\x59\x59\xa6\x0c\x95\xb5\x05\x74\x44\x07\xc6\x7c\x0d\xe6\xd0\x33\x43\xc1\x85\xa9\xb0\xb8\xf9\x8e\x16\x88\xbf\xf7\xd5\xa3\xfc\xfb\x7b\x9b\x64\xcb\x83\x67\xe7\xe2\x23\x23\x61\x32\x1d\xf5\xc6\xa1\xdf\x7e\x43\x10\x04\xe9\xf0\xbd\xc6\x94\x6f\x08\x88\xa7\x7d\xb7\x3b\xf5\x93\xdd\x78\x32\x6a\x55\x27\x1e\x05\x3f\x46\x7e\x17\x7f\x47\xc6\x42\x47\xa8\x4e\x90\xdf\x31\xf7\x5b\xfc\x6e\xe4\x06\xe2\x65\xda\xe5\xc1\x5f\x4d\x39\x3c\x49\xb9\x22\x99\xea\x32\xfd\x0a\x70\x38\xa8\x78\xf8\xe9\x2c\x0d\x3f\xff\x86\x20\x55\x7e\x2c\x20\xb3\xa6\xd0\x43\x7e\xc7\xfe\xc2\xfe\xbe\xfb\x1d\xfb\x0b\xff\xfb\x3f\xbf\xe3\xde\x67\xfc\x2f\xfc\x6f\x64\xe2\x5f\x44\x84\xce\x58\x70\x8d\x22\xf4\x6a\x5f\x12\x2d\x53\xa0\x1f\xb8\xd0\x32\xf9\x1c\x3e\xda\x32\xff\x77\x8e\x65\x4e\xfb\xd4\xc0\x0e\x87\x7e\xb8\x98\x21\x8e\xdd\xf6\x09\xa2\x27\x31\x82\x8c\x5d\x5b\x21\x3f\x8e\x19\xe0\xd6\xff\x79\xf2\x34\x10\x90\x1f\xe1\x88\xf8\x92\x14\xb5\x57\x95\x31\x0e\x18\x13\x71\x1f\xc6\xc5\x25\x4c\x2c\x81\x2e\x95\x32\x09\x34\x26\x69\x24\x20\xa3\xe2\x1e\xbd\xec\x54\xda\xa4\x32\xef\x62\x69\x13\x40\xe3\xd2\x86\x83\x24\x53\x5a\xb7\xe7\x52\xa0\x0a\xd6\xba\x23\x3a\x40\xd2\xa1\xbd\x02\x32\x44\x7e\x20\x37\x37\x7f\x46\xaf\x6e\x34\x67\x21\x9a\x9a\x12\xda\xb3\x11\xd1\xf5\x50\xfc\x06\xfa\x79\xd1\x55\x4c\x37\x3f\x10\x0f\xf3\x1e\xbe\x2e\xc7\xd9\x5a\x44\x5e\x00\x0b\xc8\x0e\xb4\x90\x77\x60\xed\x34\x63\xfe\x99\xa2\xbf\x78\x95\x42\x6f\xda\xe9\xf8\xfa\x49\x40\x07\x86\x0c\x11\x49\x9b\x6b\x86\x13\xbf\xe8\xaf\x0e\xeb\x1a\x90\x34\x5d\x73\x34\x68\x27\xd3\xed\x17\xb9\x0b\x10\xfa\x6b\xa5\xa2\xb1\x5e\x4a\xd0\x4a\x26\x32\xd6\x4b\xd1\x5e\x4b\xd0\x70\x2c\x17\x48\x33\x1c\x38\x87\x56\x8c\x28\x71\x1e\xbc\x90\xc6\xaa\x0e\xe6\x69\xa8\xa1\x19\xf2\x04\x2c\x02\x8f\x63\x2d\x81\xed\x40\x4b\xdc\x40\x6d\xbe\x70\x10\x7b\x09\x5c\x3b\xc4\xf5\x71\x16\x16\xb4\x17\xa6\xae\x88\xba\xb9\xc9\x27\x5a\x42\x45\x5b\x2f\xf3\xe9\x16\xda\x7c\x91\x46\x95\xb4\x25\xe0\x44\xe5\xd3\xb8\x8b\x8e\xd9\x2e\x75\x48\x7f\xd2\xcc\xf7\xca\x60\xc9\xeb\x15\xee\x12\xec\x8a\x51\x68\xdc\xb0\x25\xbd\xd8\x00\x4b\x98\x40\x48\x93\x71\x42\x6f\x9e\x28\x81\x92\x3b\x91\xe0\x52\x13\xee\x07\xc9\x17\x5b\x71\x3f\x65\x5a\x20\xbc\x4f\xf5\xf5\x1b\x17\x22\x0d\x9c\xb8\x80\x8a\xa1\x09\x83\xb3\xb5\x0b\x4d\x35\xfb\x8a\x69\x4a\x72\x36\x00\x4b\x57\xdf\x53\x0d\x12\x92\xc6\x21\x13\x26\x07\xb7\x1f\xf8\x69\x71\x65\x2e\xf5\x04\x33\xe1\x14\xf5\x25\xc3\x14\xf1\x89\x96\x73\xcd\x11\x9f\xdb\x0f\xee\xf5\x61\x49\x22\x45\xa3\xe3\xf2\x45\x52\x54\x9d\x64\xab\xf0\xba\x46\xa1\xb0\x0a\x6c\xef\xc0\x6d\x52\x8e\x4e\x35\xf7\xa9\x9d\xe2\xb3\x57\xe7\xda\x29\xbe\xca\x72\x70\x9d\x04\x11\xc1\x6a\xa5\x6b\xde\x66\x12\xc4\xd1\x96\xd0\x76\xc0\x72\x85\xb8\xbd\xb1\xf7\x15\xf9\x69\x1a\xf0\x54\xd0\xb4\xb9\xb9\xfd\x4c\x43\x30\xa9\x57\x4c\xe6\xc3\x14\x60\x0a\x6a\x50\x60\xf0\xa3\x89\x3f\x56\xc7\xbc\x1f\x5a\xbd\xea\x48\xf0\x06\xd6\x95\xa7\xe0\xa7\x5e\x1f\xe9\xb6\x7a\x0f\x7c\x67\x2a\x1c\xbe\xf3\x8f\xc7\xef\x55\xbe\xda\x14\x10\x2c\x4f\x99\xb3\xcd\x1e\x07\x3a\x09\xd9\x60\xd2\x1f\x31\xe0\xd6\x79\x07\xfa\xe7\x9b\x14\x8d\x6f\xbe\x7f\xb7\xe0\x5c\xd6\x81\x6d\x9f\xf8\x9a\xbf\x89\x26\x39\x55\x65\xdc\x28\x7f\x86\xf6\x62\xcd\xfc\x15\x8d\x83\x5e\x59\xf1\xe6\x05\x64\x91\x8c\xfa\x71\xe1\x99\x67\x8f\x2b\xbb\x6d\x18\xf3\x97\x39\x6d\x96\x22\x48\x7f\xd6\x13\x6a\x48\xe5\x29\x47\x23\x7f\x85\x2a\x5b\xa1\x03\x56\xec\xf2\x1f\x9a\x92\x26\xdb\x7e\xe5\xe1\x52\xaf\x0b\x70\x02\xb7\x8b\xc5\x8c\x98\xd6\x23\x9e\x2e\xb4\xa4\x51\x7e\xf2\x76\xe4\x7c\x4a\xf1\xe6\x8c\x8e\x45\x81\x0e\xd0\x74\x1b\x79\xb1\x4d\x43\x4a\x77\xb6\xfd\x72\xcd\xa5\x76\x08\x70\x02\x3b\xec\x07\x09\x29\xb2\x85\x76\x39\x16\x8a\xc2\xa4\x0d\x96\xc9\x0d\x03\xb3\x84\xd6\xe7\xbc\x1b\x71\x90\x63\x9f\xe5\xd0\x18\x87\xe3\x8d\x28\x46\x7f\xd8\xe5\x18\xeb\x98\xcc\xb5\x73\xec\x9b\xe2\x6d\x2c\x08\x9c\xdc\x46\x3e\xed\x7a\xa5\x14\xa6\x3d\xb8\x4e\xf0\x35\xb6\x01\xf4\x44\x17\xec\xa4\x6e\x72\x80\x2e\xca\xa6\x66\xa4\x0c\xf9\x54\x08\xc5\x95\x69\xea\x29\x23\x4c\x60\x43\x51\x85\x69\xf7\xda\xbb\x6c\x41\x1b\x5a\xef\x69\x24\x4b\xb0\x15\x9d\xad\xe8\xd5\x4e\xda\xcf\x34\xaa\x95\x65\x3a\xa6\x6c\xea\xa9\x7a\xc5\xef\xd1\xde\x59\x20\x50\xa0\xe5\x95\x17\x41\x41\xbd\x96\x65\x68\xdb\xea\x5a\x17\x53\x1d\x25\x50\x1c\x68\x3a\x54\xd2\xa9\xd2\xc3\x2a\x65\x05\xf5\xd2\x28\x4b\xd9\x0f\x90\xd3\xe7\x15\xcf\x36\xf9\xf9\xab\xac\xca\xd7\xed\xc6\x32\x79\xfc\xaa\x6e\xad\x94\xa2\x17\x76\x73\x99\xbc\x4e\xbb\xbd\x64\xf2\x8c\x6e\x30\xb4\xbf\xe0\x6a\xbe\x99\x37\x1c\x8c\x6e\xf7\x4f\x19\x32\xba\x95\xbf\xec\xab\xe2\xf5\x80\x17\x76\x80\x41\xe4\x9b\x6b\x4b\x3e\xec\x1f\x4e\xe9\x7a\xf6\xe9\xe4\xe6\xe6\xfb\xf7\xf4\x21\x6b\x7a\x1c\x04\xdb\x3b\x2e\x35\x67\xf0\x38\xd1\xe7\xab\xd6\x0b\x41\x4a\x3c\xa7\xf7\xf2\xb6\xf1\xa4\xb2\x8d\x3d\xcc\x94\x45\x14\x3c\x5f\x95\x45\xe2\x8f\x59\x13\x09\x4e\x1f\x0b\xcb\xa1\xcb\x64\x77\xa0\xca\xe0\xe8\x89\xa4\xd9\xc1\x13\x3d\x88\x64\x9a\x3a\x04\xc6\xbe\x4f\xd2\x64\x28\x1a\x91\xfe\xd7\xff\x2d\xda\x27\x1f\xb7\xb9\x8b\xb1\xde\x3a\xb2\xd1\x3e\x7e\x31\xb4\x51\x2d\xf1\xe1\x31\x4f\x6a\xd1\x7b\xbc\x10\xa9\x36\x85\x6a\x1b\xf9\xfc\x39\x6c\xc1\xff\xfc\x40\xd0\x2f\x5f\xf2\xb0\x92\xda\xef\xad\xf6\x7f\x27\x86\x2c\x80\x17\x31\x6a\x0c\x3e\x66\x71\x5f\xc2\xcc\x60\x4a\xde\x69\x75\x85\xf0\x4a\xde\xb5\x57\xb0\x2f\x2d\x92\xc4\x2e\xe9\x4d\xf3\xf6\xa9\x5d\xa7\x3f\xcd\xe1\xf2\xab\x7a\xd4\x92\xca\x5e\xd8\xa7\xe6\x70\x3b\xed\x55\xd3\x1a\x64\xf4\xab\x91\xbd\x89\x57\xf4\xd5\xbd\x7f\x86\x45\x2a\x3c\x8c\x0a\xb2\x7f\xce\xe0\xac\x68\xd7\x9b\xdd\x8b\x26\xaf\x05\x1c\x58\x27\xc6\x8b\x3b\x0e\x48\x1f\x48\xa4\x0d\xd1\xfe\x27\x83\x2c\x67\x2b\x42\xe3\x1d\xea\xe6\x0a\x26\x4d\x5c\x3a\x5b\x77\xc8\xb3\xd6\x13\x27\x5e\x9d\xad\xb8\x84\x0e\x48\xb9\xe4\x0e\xb6\xd2\x2e\xdb\xda\xdc\x00\xce\xda\x82\x49\x73\x6c\x1c\xfd\xe5\xaf\xbf\x8f\xd5\xcb\x3f\xff\x4d\xaa\x5f\xfe\xfa\x3b\x6e\x73\xb8\x34\x53\xa6\xc3\x8e\x58\x86\x69\xc0\xcc\x6a\xe8\x88\x75\x0a\x13\x68\xa6\x2d\xa1\x28\x99\x6b\x43\xf1\x26\x9b\x59\x0b\x18\x73\x18\x1f\x8f\x45\x3b\x57\xd7\x12\x2e\xda\x1c\x2a\xe9\x03\xae\xf8\xce\xe1\x73\x63\x2d\xfe\x10\x89\x1f\x66\xc9\x8b\x5d\x91\x15\x85\xec\x45\xa9\x9c\xc5\x87\x60\x6f\xf4\xb9\x42\x07\x4f\xd2\xec\xe7\x5c\xdc\xfa\xa4\xe8\x72\x5b\x76\x39\x17\x79\xe4\x39\xc9\x13\xc3\x0f\x1d\x27\xce\xdc\x67\x14\x54\x5e\x81\x64\xa4\x8e\xf2\x35\x19\xa6\x75\xbe\xde\x45\x44\x31\xd7\x92\x0e\x91\x95\x05\x65\xcd\x9b\x08\x28\xbe\x38\x7c\xe6\x8a\x60\x78\xaf\xfb\xb9\xf7\x2a\xfc\x3c\xd5\x2f\x59\x50\x2d\xb8\xf4\x54\x66\x2d\xa9\xdc\x64\x78\xe6\xbe\x84\xa3\x39\x44\x5d\x5b\x6a\x69\xb5\xf7\xb5\x77\x2f\x7c\x80\x73\xc4\xd6\x1f\x34\x65\xef\x22\xfb\xc7\x28\x8a\x14\x29\xbe\x8f\x78\xcf\xad\xe4\x3c\xa1\x31\x16\x26\x19\x6b\x35\xe1\x59\xf1\xf0\x4a\x4d\xb9\xb9\x8c\xeb\x29\x51\xf0\x01\x96\x4c\xa5\x32\xe7\x40\x8a\x28\x99\x5a\xeb\x5f\x4d\xcd\xc2\xcf\x00\x65\x2a\x9a\x53\x98\x26\xab\x5a\x03\x0e\x40\x54\xd3\xca\xda\x8b\x84\xd4\xf8\x09\x9f\xa3\x5b\x0e\xde\xe9\x7e\x92\x6b\x80\x26\xed\xb0\xb8\x04\x37\x65\x1d\xff\x02\xc8\xac\xed\x01\x17\xc0\x66\xad\xa6\x17\x81\x6d\xf5\xc6\xc2\x68\x82\xb4\x7a\x93\xfe\xc9\x8a\xba\x37\xec\x1a\x23\x9f\x6f\x30\x51\x33\x34\x47\x03\xba\xe8\xef\x5b\xff\xc3\x7e\xd3\x6f\x6e\x91\x1b\x1c\xc5\xb8\x6f\x18\xfa\x8d\xc0\x10\x8c\xfc\x8e\x71\xdf\x49\xee\x0f\x94\x60\x09\xe2\x2b\x8a\xdd\x7c\xf9\xb3\x18\x38\x2e\xfa\x6f\x53\x88\x38\xaa\xb4\x13\x1d\x53\x53\x32\x19\x91\x38\xcd\x94\x61\x44\x88\x6b\x1b\x1e\x46\x0e\xa2\x66\x9c\xbc\xc0\x21\x9b\x1d\xc5\xe1\x74\x19\x7e\xa4\x08\x14\x45\x8c\xaf\x36\x64\xf2\xa0\x48\x8c\x2c\xa5\x13\x25\xfa\xe3\x94\xfd\x94\x89\xb7\xff\x30\x93\x05\x8d\xb1\x28\x59\x86\x05\xbd\x67\x11\xf4\x09\x05\x58\x30\x28\x57\xca\x05\x18\xbf\xb7\xdc\x15\xd7\x82\xc5\xd0\x72\x86\x62\xbd\x9b\x01\xe6\x73\x0b\xce\x81\x63\x5a\xd9\xf7\x9a\xa5\x30\x9c\x2d\x07\x1f\x36\x52\xf0\x44\x6c\x01\x35\x38\x8a\x29\x75\x33\x38\x4f\x0d\x7f\x25\x4a\xdc\x2a\x56\x26\x3a\x87\x13\x74\x29\x8f\xc5\x50\x0f\x3e\xb8\x0b\x5e\x91\x9c\xcd\x80\xa2\x19\xac\x14\x03\x2c\xcc\xe0\x50\x87\xba\xf1\x9f\xcd\x88\xc3\x59\xae\x14\x23\x3c\x72\x27\x82\x19\x44\xff\x8d\x6a\x59\x9c\x30\x94\xe2\xe8\x72\x2a\x11\xbe\x3a\x87\x89\xd7\x4c\xcf\xc2\x30\x8c\xa1\x4a\x39\x2e\x46\x8a\xaa\xb6\xdd\x3f\x92\x6e\x2e\x75\x51\xd5\xa0\x9e\x99\x19\x31\x8c\x60\x88\x72\x37\x9e\xda\xaf\x88\xef\x57\x2a\xb7\x39\x6a\x50\x14\x53\x2a\x40\x30\x5a\xd4\x8c\x39\xb4\x1d\xf1\x74\x2d\x34\x87\x15\xcd\x95\x8b\x45\x8c\x89\x14\x40\xde\xa2\x33\xc8\xee\x4b\x30\x8c\xa5\x68\xbc\x14\x13\xf6\xe0\xbe\xaa\x69\xed\xeb\x8f\x4c\x1e\x38\xc1\x12\x54\x29\x1e\x9c\xef\x54\xd9\xb0\x04\x81\xa1\xa5\x3c\x0a\x47\x13\x44\xcf\x0f\x42\x8c\xa0\x48\xae\x54\x10\xe2\xd8\x3e\xd2\x2d\xb8\x34\xdf\xa1\xf8\x13\x5a\xe6\x61\x36\xdf\x34\x6c\xc7\x02\x5a\x4e\xb7\x8b\x11\x2c\x4a\x94\x0a\x48\x1c\x17\x43\x43\xe4\x4c\x6c\x92\x64\xd0\x52\xae\x85\x13\x62\xac\x8e\xcb\xc4\xa7\x70\xbc\x94\x53\xe1\x64\xa1\x52\x04\xa3\x51\x96\x2c\xd5\x6d\xe0\x94\x2b\x77\x10\x80\x16\x34\xc0\x12\x8a\xb2\xa9\xaf\x97\x39\xb1\x47\x13\x0c\xb6\xf7\xad\x94\x02\x34\x73\x63\x61\xd9\x0a\xf4\x64\x73\xe1\x5e\x03\xec\x16\xb9\x69\x54\x1f\xdb\x0d\x7a\xd4\x23\xfb\xbd\x96\x30\xa8\x76\x7b\xf5\x0a\x43\xe0\x3c\x49\xd0\xcf\xd4\xa0\x57\x1b\x8f\x3a\x8d\x59\x9b\x69\x54\x3a\xd5\xee\xb0\xd3\xaa\xf7\xc9\x31\x23\x3c\xcd\x1e\xa6\x71\x2b\xa5\x32\xc1\x5d\x26\x95\xc7\xc6\xf0\x7e\xf6\xd0\x99\xf5\x9f\x9a\xf5\xce\xc3\xa4\x3d\x7b\xa0\xea\x8d\x26\x4f\x74\x7a\x4f\x4f\xf8\xfd\xb0\xdd\x65\xfa\xfc\x3d\x3f\x15\x86\xf5\x29\xdd\x19\x54\xc7\x42\xfd\xe1\xb1\xdf\x2b\xcc\x84\xf0\x98\x8c\x06\x4f\xcd\x56\x07\xaf\xb6\x88\x7a\x6f\x48\x56\x1e\x3b\xf5\x6e\xaf\xd6\xa9\xdf\x4f\x7b\x83\x29\xde\x7c\x22\x9e\xbb\xf5\x71\xb3\xdf\x9b\x56\x85\x3e\x3f\x9e\x31\xc3\x2a\xd3\x7f\xc4\x9b\x85\x99\x90\x2e\x13\x9e\x9a\x55\x06\x4f\x3c\xf5\x44\xce\x78\xa1\xf9\x38\x1b\xe1\xd3\x76\x1f\x9f\xf6\xc9\xca\xb4\xd1\x9c\x0e\x19\x52\x98\x0e\xda\xfd\x1e\x3e\x6c\x3e\x90\xb3\x51\xb3\xdf\x1a\xf5\xda\xed\x26\x7e\x73\xee\x46\x58\x77\x4c\x9a\x73\xaf\x83\xc7\xc2\x8e\x4f\x74\xfe\x61\xc3\xec\x4d\xa2\xb7\x08\x79\x8b\x38\xd6\x1a\x16\xf0\xc0\xd3\xed\x9f\x65\x06\x56\x65\xb6\x1c\x5e\x45\xd3\xc8\x14\xcb\x2d\x82\xdd\xfa\xcf\x04\xe5\x2b\x9a\xb4\xe5\xf0\xdc\x48\xdb\x6f\x3b\x0c\xc5\x00\x86\xb3\x2c\xc9\xa1\x14\xc7\x52\x9e\x54\x6e\x58\xfc\xf3\xc9\x4f\xdb\x9f\xbe\x23\x9f\xa8\x3f\x50\xff\xef\xd3\x2d\xf2\xe9\x38\x39\xe8\x5e\x32\x80\xa3\xbd\xc3\x4f\xff\x4d\x73\xd4\x38\x37\x2c\xc6\x0d\xbf\x45\x88\x0f\xe5\xc6\x52\x2c\xc7\x11\x2c\xcd\x72\x9e\x6a\xa8\xc7\xcc\x76\xdc\xa1\xa7\x31\x17\x83\x89\x46\x17\x1b\x43\xd1\x03\xe3\xc2\x0c\x88\x28\x83\x04\x6d\xc2\xb0\xd7\xd6\x87\xb8\x45\x30\x5f\x21\xff\xf1\x8d\x4f\xdf\x5d\x15\x3f\xf9\xae\x20\xbe\xc2\x9d\xcb\xe3\xdc\x24\x5a\x5c\x2a\x32\x90\x8a\xc4\x99\xc0\x81\x3e\xc8\xca\x01\x83\x8f\xb6\x72\x4c\x9f\x62\x56\x3e\x33\xf7\x16\x97\x0a\xdf\x4b\x45\xb3\x2c\xf6\xa1\x56\xf6\x19\x7c\xb4\x95\x63\xfa\x14\xb3\xf2\x99\x7d\xb5\x2f\x55\x4e\x92\x4d\xda\xcf\x7c\x6e\x92\xdd\xef\x69\x0e\xd7\x00\x14\x05\x38\x4c\xa2\x68\x9a\x95\x49\x08\x38\x4a\x92\x39\x15\x55\x51\x92\x04\x92\x8a\xcb\x04\x2a\x13\x2c\x0d\x14\x85\x65\x18\x02\x85\x12\xa4\x68\x52\x52\x28\x4a\x41\x39\x40\x2b\x2a\x83\xa9\x6e\xcd\xc6\x49\x8c\xcc\x4a\x2a\xc0\x00\x27\x53\x04\x86\x49\x2c\x4e\xa3\x28\xa3\x72\xa8\x2a\x31\x14\x0d\x64\x94\x24\xa0\x82\x91\x38\x0e\x08\x19\xe7\x70\x94\x65\x65\x9c\xc0\x00\x8d\xa3\x34\xa4\x69\xd4\xef\x75\xb0\x58\xf5\x47\x78\xd5\x1f\x1d\x2f\x0a\x89\xa0\x28\x24\x38\x92\xa5\xc9\xdc\xab\x41\x5e\xc7\x58\x96\xbd\x45\x30\xda\xbd\x9f\x27\x7f\xb7\x08\xe9\xfe\x83\x05\xff\xec\x7f\xc4\x0e\x1f\xdc\xae\x87\xe7\x79\xbe\x76\xef\xb0\xda\x9d\x09\x8c\x7a\x77\xb4\xae\x3e\xf1\x2a\x55\x63\x94\x99\xc5\x0f\xbf\xa2\xd3\xd6\xdb\xa0\xfa\x3a\xd7\xba\xad\xed\x4a\xab\xac\x9f\xe7\xe3\x01\x06\xba\xe6\xe0\x69\x45\xbc\x55\xc7\x55\xf5\x19\xab\xbc\xcc\x66\x5b\x63\x67\x3b\xaa\xb5\xb3\x86\x46\x8f\x52\x21\xfb\xf4\xfc\x8c\x6d\x65\x17\x9a\x7f\x94\x2c\x55\x9e\xbb\x9f\x5a\x87\x7f\xf8\xa1\xfb\xcf\xe6\xf8\x7d\xc3\x0f\x86\xaf\xde\x27\xbe\xde\x6d\xdf\xbf\x03\x7a\xb8\xec\xeb\xb5\x8e\x03\x5f\x9e\xa4\xc5\xea\xa9\xc5\x8c\xa7\xed\xbe\x0a\xef\xa5\x96\xf2\xfa\xf6\xc2\x6d\xfa\x18\xef\x58\x77\x2a\xdb\x15\x24\xb3\xa5\xc9\x1b\xb2\x5a\xe1\x77\x18\xed\x2c\x9d\x59\xa3\x2e\x35\x9b\x6b\xb0\x11\x98\xc5\x23\xdb\x12\x88\xfa\xcf\x47\xcd\xe3\xdf\xed\x91\x1d\xf0\x73\x85\x0f\xf9\xe3\x5f\x23\xfc\xe5\xf0\xf7\xcc\x3f\x62\xe4\x90\xe7\x6b\xe8\x7d\xd2\xe5\x7f\xf5\x9f\xef\x74\x68\x4a\x5e\x88\x87\x0a\x7e\x1d\x37\xbf\xa1\x09\x85\x63\x55\x8a\xa0\x21\xa4\x59\x05\x93\x70\x46\xa2\x24\x96\x53\x71\x02\xa8\x1e\x26\x43\xd1\x1c\xc0\x49\x15\xa8\x18\x89\x12\x40\x41\x25\x0a\x97\x68\x82\x90\x50\x46\x82\x1c\x77\xe3\xe5\x24\x22\xd1\xeb\xa9\xb4\x60\x20\x51\x8e\x46\x89\xdc\xab\x7e\x27\x4e\x52\x1c\x9e\x11\x29\x44\x4a\xa4\xf8\x89\xdf\xb3\x6c\x63\xf0\xfc\x82\xf5\xd6\x94\x89\x4a\xf7\xcc\x8c\x34\x76\xfd\xf7\xe9\xb6\x41\x3c\xac\xcc\xd7\xaf\xef\x75\xbe\xef\x54\xb1\x36\xde\x65\x2a\x0c\xfd\xac\x2f\x05\xa5\xbf\x7a\xa8\x76\xa9\x66\xc7\xe2\xea\xbd\x17\x8a\x7a\x03\xf4\x06\x6f\xb6\xbb\xce\xdb\x64\x50\xef\xbc\x37\xd8\xdd\x60\x7a\x07\x78\xd3\x83\xf6\x82\x24\xe4\x8a\xa3\x29\xff\xb0\xbd\x5f\x62\x7a\xad\xbb\xd9\xbc\xad\x5f\xda\xf2\x6e\xf8\xd3\xe6\x98\xfa\x1d\x2f\x4c\xb4\xea\x7c\x38\xb0\x36\x34\xb1\x79\x03\x83\x46\xdf\x79\x41\x1f\xde\xe0\x4b\x75\xd4\x30\x58\x9e\x6c\x6f\xee\x0d\x8d\x31\xde\x20\x58\xdf\xa1\xc2\x62\x71\xd7\x78\x65\x77\x42\x6d\xc9\x18\x4d\x3f\x08\x13\x82\x40\xb0\x93\x1c\x69\x1f\x04\x3c\x5f\x79\xfd\x50\x8f\xfd\x80\x3f\xdf\x9d\x8a\x06\x01\x76\x1d\x07\xf6\x56\xa8\x91\xc0\x63\x30\x8e\x41\xbf\xa1\xd8\x37\x14\x43\x50\xf4\xbb\xf7\x5f\xaa\xa3\xe2\x18\x8d\xe3\xb9\x57\x49\x9c\x23\x39\x9a\xc1\x39\x3a\xc3\x8d\x73\x9d\xf8\x5f\xf9\x57\x79\x6c\x6b\xe4\xee\x6e\x37\x6e\x57\x98\x9a\x51\xe3\x9a\x38\xba\x7d\xa9\x7c\xb5\xd1\xb9\x63\x6f\x5a\x9b\x9f\xd8\xa3\x32\x9e\x3d\x81\xca\x3d\xa8\x7b\x4e\x2c\x24\x38\x71\xf2\xdf\xff\xe7\x4e\x8c\xfa\x4e\x9c\x53\x4b\x15\x78\x88\xe5\xdc\xd2\x2a\x65\x5f\x40\xda\x00\x13\x4b\x89\xb8\x1c\x98\xf8\xa8\x18\x3f\x0f\x26\x36\x3e\x24\xce\x43\x21\x63\xc3\xd8\xf3\x50\xa8\xd8\xb0\xe6\x3c\x14\x3a\x8a\x42\x9e\x87\xc2\xc4\x8a\xff\xf3\x50\xd8\xd8\x88\xe5\x3a\x0f\x18\x5d\x65\xae\x27\x7b\xe7\xc9\x2d\xc2\x16\x9d\xe3\x4a\x79\xcc\xe6\xe2\xe8\x09\x45\x4c\x24\x5c\x0e\x5f\xc8\xc3\x50\xe1\x9f\x4f\x8e\x79\xd1\xe8\xeb\x16\xf9\xa4\x5a\xe6\xf2\xa2\xd9\x08\x77\xbc\x59\x6a\x8a\xe8\x03\xe6\x8f\x13\x8c\x17\x8e\xcb\xc3\x67\x36\x34\x3c\x57\xd7\x86\x02\x2d\xdf\x7c\xe7\xcd\x01\x7b\x3a\xfa\x93\xa4\x97\x5a\x30\x7f\xae\xe0\x03\xe6\xaa\xd3\xac\x16\x64\x90\xc3\x67\xf2\x43\xad\x76\xee\xfc\xcc\xbf\xce\x6a\x7e\xae\x3b\x7c\x46\x3f\xd4\x6a\x17\x44\xfc\x87\x5b\x2d\x27\x71\x26\x3c\x4c\x77\xc1\xae\xab\x52\x4f\x15\x9d\x9b\x9c\x53\xf7\x02\x26\x16\x37\x64\x7a\x25\x90\x0b\x84\xc7\x80\xd2\xca\x9b\x5c\x20\x22\x9a\xf6\xd2\x3a\xf2\x5c\x1c\x32\x96\x3e\xcf\xc5\x89\x25\x94\xb3\xe5\xa1\xa3\x38\x69\x65\x4e\x2e\x0e\x13\x0d\xd5\xb3\xe5\x61\xa3\x38\xe9\xa5\x4e\xd9\x07\xa2\xae\x51\xec\xe4\xed\x3e\x2d\x51\xee\xa4\x3e\xfd\x74\x85\x98\x0a\x2d\x8f\xcb\x50\x92\x58\x86\x02\x28\xaa\xaa\x34\xc4\x08\x96\x00\x50\x45\x55\x05\xa7\x30\xc0\xd0\x2a\x8e\xcb\x98\xca\x01\x09\x07\xb8\xa2\xaa\xb2\x84\x32\x0c\x4b\x51\x0c\x41\x03\x05\xe2\x34\xc5\x01\x7f\x34\x7f\xd1\x1a\x75\x68\x16\x88\xd8\x0f\x91\x53\xa7\x58\x29\x14\xcb\x98\x9e\x0d\xae\x46\x22\xda\x1f\x5b\xb7\xe9\x17\xa8\x11\x2f\x4b\xb3\xc5\x4e\x1a\x7a\xed\x0e\xce\x65\x82\x19\x3c\x3a\xcd\x76\xfb\xe7\xec\x81\xdd\x3c\x68\xcf\x15\x50\x5d\x53\x1d\xaa\xcb\x7b\x63\x53\x7e\x3f\x01\x5a\x89\x0d\xfd\x42\xdf\x05\xef\x5f\x69\x39\x5f\x62\x0f\xb8\x32\xa7\x1e\xb0\xe5\x1b\x06\xf5\xae\xdc\xc0\x9c\xed\xcb\xf8\xa9\xfd\xcc\x6d\x84\xb9\x39\xae\x00\x38\x63\xa7\x5a\xdd\x0c\xc1\x74\x68\xb6\x15\xfa\x0a\x98\xd7\xf7\xd7\x8d\x07\xcf\x0d\xd6\xdc\xea\x65\xf7\x2a\x8f\xc6\x34\xaa\xbf\xf5\x3b\x6f\x3d\xb6\xde\xfc\x89\x93\xe4\x70\xc0\x4a\xe0\xa9\x07\x27\x93\xfb\xe7\x96\x6e\x11\x63\x69\x54\xc5\x88\x37\xc1\xe2\xd6\x03\xb2\x3f\xaa\xcd\x77\xd5\xca\xdd\x5c\x5e\xcf\xf1\x46\xdb\xaa\x75\xd7\x6d\x74\x3c\x21\x86\x7d\xd0\x9e\x56\x36\x3f\x7e\xdc\x84\xe7\x19\xc2\x93\xab\xc3\x24\xdd\xf8\x23\x7d\xec\xba\x4f\xe4\x99\xa9\x1a\x32\xcb\x1a\x54\xa5\x87\xc7\x67\xbc\xa6\x3f\xce\x80\xf5\x40\x4f\xb7\x1b\x69\x46\x34\x7a\xf7\xf3\x95\x41\xf0\xe3\xea\xa2\x55\x5f\x51\xd2\x76\xdc\x9a\x79\xf3\x04\x3c\xb3\xb4\x03\x7b\xcc\x33\x06\xda\xa9\xd3\x08\x9e\xed\x6b\x17\xf0\xff\xaa\x4b\x6f\x17\xf0\xef\xc6\xf8\x57\xd7\x26\x61\x3a\x24\xf5\x56\x1d\x08\xdb\xd5\xf0\x8e\x30\x9b\xbd\xaf\x3f\x31\x66\xb4\xd3\x6c\x4c\x57\xbb\xf5\xa7\xe5\x70\x36\xb7\xd6\xe3\xaf\x13\x7e\xaf\xff\x52\x3e\xf2\x17\x2e\xd4\xbf\x34\x7f\xd2\xe0\x5e\xcf\xe4\x1f\xf2\xa5\x39\x9f\xe0\x0b\xe7\xd8\xe2\x9a\xbe\xf0\x2b\xef\x85\x6f\x8b\x7f\x3e\x2a\x68\xbd\xe2\xd0\x7b\x76\x70\x3f\x89\xe9\xff\xeb\x76\x22\x5e\xb2\xcc\xef\x47\x23\x9b\xb8\x18\x12\xba\xa9\x96\x93\x38\xa8\x32\x8a\x04\x38\x40\x29\x12\x41\x10\x9c\xc4\xb0\xaa\x02\x58\x95\x20\x19\x86\x91\x30\xa0\x12\x84\x04\x48\x9a\x05\x0a\x25\xa3\x8a\xca\x91\xb4\x42\x2a\x37\xde\x6a\x28\x76\x49\xbd\xea\x0f\xa6\xb3\x92\x3c\x89\x72\x0c\x96\xba\xca\x76\xb8\x1a\xae\x92\x82\x45\x80\x0e\xdb\x1c\xbe\x0f\x5f\xa5\x36\xde\xe4\x89\xd9\xc3\xcb\xc8\x6a\x2f\x5f\x1e\x51\x54\x6d\xb0\x76\xa7\xc5\x2c\x51\x61\xb4\xb9\x9f\xdd\xf1\x8f\x04\x7f\xc8\xf1\xde\x5f\x46\x8e\xf7\xff\xac\xb7\x1e\xdd\x81\x7d\x30\x7f\xd9\x76\xc1\x74\xc0\xd1\x95\x9f\xaa\xcd\x41\x54\x36\xad\xde\xf3\xe3\xcf\xca\xec\xfe\xb5\x6e\xb6\xf7\x39\x9c\xe7\xfb\x94\xd5\x0e\xe3\x3d\xbc\x6f\xea\x9c\x7b\x49\xa8\xd6\x7e\xbe\xbd\xbf\x0e\x2b\x43\xb3\xc7\xdf\x6b\xea\x60\xf4\x58\x33\x3b\x8b\x77\x67\x27\x4f\x08\xbd\x3e\xa8\x0e\x29\x6c\xfe\xaa\xd8\xf5\x26\xa8\xf4\x66\x1b\x94\x1a\xdf\x3d\x2c\x66\xe8\xe3\xfc\xd5\x42\xab\x95\x81\x40\xf6\x40\xfd\x01\x6f\x2f\x65\x9b\x78\xde\x74\x96\x9a\x44\x4e\x46\x56\xb7\x53\x20\xb7\x47\x9c\x36\x2d\xb7\xfb\x0b\x7f\x27\xb9\x5d\xbb\xab\xa0\x1d\xf4\xbe\xb1\x73\x16\x9b\x1e\xa6\x3f\xa1\x60\xb7\x32\x31\xae\xd7\xdc\xbe\x77\xaa\xbb\x3e\xe5\x54\x04\xb9\xea\xeb\x48\xcc\x1d\xab\x6f\x3c\xdd\x31\xd3\x63\xfb\x6e\xa2\x10\x39\xf1\x7c\x01\xff\x9e\xb5\x9b\x4c\x2e\xe0\xcf\xf3\xff\xbb\x7c\x96\x98\x5b\x2b\xe7\xdb\xa2\x6f\x3c\x67\xaa\x99\x67\x8b\x4b\xef\x85\xeb\x0b\x5f\xe5\x18\x5e\x29\x5b\xfc\x33\x67\x69\x8b\x12\xf8\x69\xbb\x36\xac\x3e\x19\x3f\xd1\x87\x0d\x5d\x25\x25\x46\x36\x04\x8e\x1a\x4d\x36\xaf\x7d\xe5\xe9\xbe\x29\x55\x46\xf8\x7c\xf2\x60\xf7\xfa\xd3\x77\xec\xe9\xc1\xa9\x93\xf7\x6d\x8e\x9f\x4f\xb6\xfd\xda\x6c\xf1\xa0\x68\x2b\xa3\xd3\xc3\xe5\x2a\x65\x2e\xbf\x0a\x28\xf8\x59\xbd\x7a\x6e\xc5\x68\x12\x50\x28\x4d\x42\x09\xd0\xa4\x8a\xcb\x8a\x04\x14\x89\xa5\x68\x49\x25\x48\x92\x25\x59\x4a\x95\x69\x9c\xc6\x49\x06\x28\x80\x80\x0a\xc1\xc9\x8a\xa2\xa2\x2a\xcd\xa1\x38\x46\x10\x12\xed\xe7\x56\xfc\xb2\xdc\x8a\xe7\xe7\x56\x96\xe0\x32\x72\xab\x7f\x35\x3c\xe2\xbb\x34\xb7\x56\x63\x37\xf5\x24\xb7\xf6\xf1\xea\x1d\xdf\x27\xa9\xa7\x4a\x8d\x70\x9a\x0f\xf5\x3e\x36\x22\x78\xb4\x0b\x5f\x07\xec\xfd\x88\x36\x7a\x18\xcf\xc1\x99\xa6\xec\x5a\x8e\x1f\xd2\xe9\xb9\x95\x1f\x0b\xcf\xda\xb3\x04\xeb\x9b\xaa\x6d\xb5\x2b\x46\xbb\xb5\xb6\xef\x50\xea\xc1\xb9\xaf\x55\xac\xb9\x69\xaf\x17\x9d\xe1\xdd\x94\x7e\x9c\xbe\x90\xce\x66\xb6\x5b\xd8\xcc\xd4\x19\x93\xd5\x2e\xdc\xf6\xbb\xf4\xfd\x9b\xac\xbe\xdd\xb7\x31\x74\xa6\x57\x5e\x5f\x37\x06\x39\x67\x07\x2d\xf5\xa5\xd5\xf8\x77\xe5\xd6\x4b\x73\xdb\xa5\xf1\xdc\xdd\x74\x96\xd6\x15\x73\x2b\xcf\x3c\x75\x58\x9e\x79\xd1\xe7\xc2\x00\xa2\xca\x74\xca\x3c\x34\xe5\xda\x70\x4b\x0f\xef\x36\x7a\xf3\x4d\x26\xa6\x35\x8c\x02\xf7\x44\x4b\xc3\x7c\xcc\x6b\xe7\xd6\xff\x51\x6e\xe3\xaf\x94\x5b\x59\xf2\xd8\xbe\x55\xda\x16\xff\x08\x8b\xc6\xd3\x72\x46\x2c\x64\xde\x6a\xef\xe6\xcf\x3b\xad\x63\x0d\xb8\xfe\x83\x34\x1e\x6e\x00\xd9\xee\x74\xcc\x31\x3a\xc0\xfa\x3a\xd6\xfa\xda\x91\xeb\xb6\x29\xf5\xb1\xce\x74\xcd\xbf\x34\xed\xc9\x4b\x5f\x03\x46\x93\xd6\xc6\x8e\x52\x5f\x0d\x9f\xef\xbb\xf7\x5f\x5b\x83\xda\xae\x49\xee\x2a\xf3\xab\xd7\xad\x12\x0e\x59\x5c\x91\x80\x24\xa1\x38\x29\xe1\x0c\x40\x65\x02\x23\x51\x19\x30\x98\xc2\x02\x99\x93\x64\x06\x63\x09\x4c\xe5\x54\x0a\x10\x92\x42\x73\x50\x06\x84\xc2\xb2\xaa\x84\x42\x99\x92\x6f\x0e\xbb\xf8\x2e\xc8\xad\x79\x93\x13\x24\xca\x71\x54\xd6\x96\x17\xff\x6a\x78\xf6\xea\xd2\xdc\x5a\x8b\xdd\xd4\x93\xdc\x5a\x76\x6e\x22\x3d\xb7\xd6\xee\xd7\x3a\xe6\x74\x1a\x9d\x3a\xf9\xb0\xdd\x38\xa8\x52\xab\x3e\x08\x2a\xed\x48\x94\x4e\x4a\xbb\xae\xd5\x98\x57\x57\x5f\xf5\x87\xe7\xee\x72\x2b\x3b\x14\xa9\xf5\x54\x7c\xb9\x75\x5e\xb6\x74\x57\xa1\x9e\xef\x49\x81\xac\xe9\xb2\xad\x92\xb4\xc0\x2f\x2a\x8d\xf1\x74\x60\x1b\xac\xfa\x54\xfb\x77\xe5\xd6\x4b\x73\xdb\xa5\xf1\xdc\x41\x5f\xe9\xda\x15\x73\xeb\xaf\x9c\x93\xf9\x88\xdc\x7a\x6e\x6e\xe3\xaf\x94\x5b\xcf\x1d\xc3\x04\xb9\x75\x27\xad\x14\x69\xbc\xd5\xb6\xb0\x2e\xcb\x1d\xa5\x39\xdc\xe8\xa3\xe6\x57\x6b\xf6\xf5\x19\x36\xd8\x97\xf6\xd6\xe4\xdf\xd4\xd5\xc3\x6c\x72\x6f\x3f\x76\x20\x6c\xbd\x3c\x72\x2b\x5b\x7a\x62\xe1\x4b\x13\xce\xc6\xb0\xd2\xe7\xa9\xc7\x4e\xf3\x6b\x7f\xc1\xb7\x86\xa3\x57\xbd\xc6\xdc\xdf\x35\x71\xbe\x60\xdd\x9a\x32\xbb\x9c\xf5\xaa\x9f\xb2\x13\xcb\xf1\xd7\xfd\x1c\xb2\x35\xdc\xae\xf6\x4f\x3c\x7a\xef\x03\xf1\xf7\x74\xb9\x42\xa3\x19\xcb\x55\x09\xef\xf1\xb9\x60\x99\x2a\xed\x75\x33\xe5\x1f\x8b\x89\x9e\xe3\x91\x70\xc4\xed\xe1\xa0\xb8\xfd\xcb\xfc\xca\xbe\x01\x23\x82\xe9\xbd\x46\x85\xaf\xd5\xc2\x2f\x07\x3c\x65\x8a\x0c\x46\xad\x2e\x3f\x7a\x42\xda\xc2\x13\xf2\xf9\xf8\x16\x9c\xd4\x83\x38\x62\x87\xfe\x5e\x4d\xe6\x4c\x71\x4f\x25\x3d\xbe\x7f\x27\xf7\xc8\x90\x94\x23\x90\xaf\x67\xed\x00\x36\x53\x83\x30\xeb\xa8\x26\xfe\x95\x5b\x24\x4b\xa3\xd0\x51\x16\x27\xc7\x45\x5f\xae\xc7\x11\x31\x51\x85\x18\xc3\xa8\xf4\x09\xd2\xc6\x0f\xdf\x48\x3c\x3e\xfb\x62\xa9\x63\xa8\x49\x92\x27\x31\x8e\x79\xd1\xe1\x25\x4a\xb7\x91\x37\x30\xdd\x86\x5e\xd8\x94\x77\xf8\x46\xf2\x01\xe4\x17\xeb\x17\x43\x4d\xd2\x2f\x89\x71\xee\xdd\x89\xbd\xd1\x28\xf6\x24\x5d\xc1\x03\xdc\x2f\xd6\x2e\xca\x36\x49\xb9\xb3\x04\x43\xa6\xbd\xd6\x70\x2a\x24\xdd\x58\x97\x3e\x7a\x93\x4b\x9a\xe6\x3a\xb7\xb5\xb4\xe2\xa5\x6e\x6a\xca\xee\xbe\x9c\x2d\x74\xd7\xd5\x2c\x99\x49\x96\xa6\x19\x62\x15\xd6\x3c\x75\xb9\x3f\x77\x45\xfd\xba\xda\xa7\xb1\xc9\xd2\x3f\x53\xb4\x5c\x0b\xc4\xab\xa7\xd8\xf7\x2b\xe9\x17\x43\x4d\x52\x27\x89\x71\x54\xfa\xa4\xba\x22\x78\x6d\xa2\xff\xbf\x2b\x09\xeb\x83\x25\xc9\x18\x62\x13\x15\x6d\xff\xe6\x91\xac\xd7\x0d\x86\x3f\x5f\x49\xd2\x10\x62\x92\xb8\x71\x86\xa5\xab\x35\xbf\xd0\x3b\x96\x16\xa2\x01\x96\x87\x62\xbd\xd5\xab\x09\x8f\xc5\x5e\x9b\x18\xe4\x55\xaf\x45\x36\x38\xd2\xef\xc5\x2a\xdd\xe9\xb8\xd5\x6b\x20\x92\x63\x41\x18\xae\xdb\x6e\xbd\x53\xe6\xd2\x25\x0f\x9d\x1d\x78\x86\xc0\x31\x49\xc3\x07\x11\x86\x04\x8c\xca\x16\x22\x4a\x17\x2b\xf1\xa0\xc4\xcb\x05\x4c\x3e\x7f\x31\x55\xd4\x44\xf2\x94\xba\x51\xda\x79\x1d\xe1\xf9\x32\x86\x51\x5c\x91\x62\xfd\x64\xf4\xfe\x1e\x3a\xde\x74\x69\xfc\xee\xf7\x72\x79\x82\xb7\x65\x16\x92\x28\xa5\xcb\x97\x0e\x6f\x4b\x38\x5b\x9c\x23\x44\x58\x92\xc8\xf4\x63\x52\x04\xdc\x9e\xbc\x87\x39\x49\xb8\x05\xb0\x17\x97\x48\xe6\xbd\x8e\xba\x90\x58\xf1\x97\x58\x27\x49\xe3\x27\x9c\x4b\xe4\x09\x5e\xe4\x59\x48\xa2\xd8\x1b\xb2\x6f\x4f\x5f\x86\x9d\x37\xf4\xb8\xd8\xf5\x53\xf0\x5c\xf9\xe3\xa3\x9c\xa2\x51\x90\x00\x79\x61\x3c\xa4\x22\x16\x14\x33\xa3\x1a\x16\xa1\x8b\xe6\xd9\xfa\xd2\x4e\x23\x06\x17\x76\x81\xfd\x73\xec\xd1\x64\x9c\x70\xca\xc6\xed\xfe\x44\x8d\x34\x61\x8f\xef\x10\xbd\x50\x4c\x4d\x29\x2c\x60\xb8\x47\x3b\x43\x68\x73\x25\xae\xae\x25\x77\x80\x15\x16\x3d\x65\x44\x70\x96\x26\xc9\x0a\x38\xdb\xeb\x29\x10\x60\xa5\xe4\x87\x33\x55\x88\x9e\x0e\x71\xaa\x84\xb9\x72\xbd\x72\x61\x9e\xa5\x43\x20\xfc\x11\xe3\x5c\xe3\x67\x1b\xfa\x70\xda\x9a\x1b\xdc\x97\xdb\x3a\x0a\x17\x16\x79\xff\xd8\x6c\xb4\xda\x48\x94\x28\x6c\xd7\x6b\x89\x75\x82\x59\xac\xab\x48\x12\xd0\xf1\x6f\x89\x73\xc9\x6d\x3d\x62\x9c\xef\x92\x79\xee\xe7\x58\x8a\xcb\x24\x7c\x68\xcf\x05\x02\x9f\x82\xc5\x24\x57\x60\x4c\xce\xd8\x69\x41\xd9\x02\x7a\xc3\xa4\xeb\x88\xe7\x41\x15\x12\x2e\x75\x6c\xb6\xc7\x8b\x9d\x43\x74\xb1\x7c\x31\xbc\x3c\x21\x4f\x8f\x41\xca\x95\xf4\x3a\x76\x8c\xa0\x15\x95\x32\xd7\x9a\xd7\x91\xad\x90\x4c\xd9\xb2\xec\x25\xd6\x4d\xf3\x75\xbd\xba\x4c\xa2\x28\x56\xe1\x3b\xba\x3f\x67\x29\x51\xbe\x15\xd0\x2c\xd1\x3b\x4b\xe3\x1a\x12\xc6\xd1\x8a\xc5\x6d\x20\xe0\xed\xc9\xd1\x50\xb7\x27\xe7\x8b\xa5\x28\x71\x85\xbc\x1d\xe0\xe4\x49\x5c\xb2\x3a\x72\x51\xaf\x66\xdd\x12\x86\xcd\xb5\x9b\xff\xae\xed\x93\xb7\xed\x99\x86\x18\x1c\xba\x7c\xa9\x41\x73\x19\x44\xc6\xbc\x89\x13\x04\x01\x61\x09\xd9\x2f\xf7\x83\x2c\xec\x7c\x89\x13\xa2\x2c\x0a\x18\x54\xe1\x2e\x9e\xb3\x5b\x9d\x3f\xa8\xcb\x44\xcd\x2d\xfb\x13\x97\x84\xa2\x90\x41\x0d\xe5\x42\x1e\x9c\xe8\x4a\xd2\x26\x41\xe7\x96\x6f\x45\x3d\x39\x04\x7e\x6d\x67\x88\x40\x9f\x53\x6f\xa6\xc3\xc5\x4e\xd8\xbd\xbe\xa1\x4f\xce\xf0\xcd\x15\x3f\xd6\xa0\xb8\x32\xa1\x23\x95\x3f\xcc\xfe\xe1\x63\x9b\xf3\x34\x09\xd1\x16\x57\x22\xe9\x80\xe8\x0f\xd3\x26\xf1\x34\xea\x3c\xb5\x92\x1a\x15\xd7\x6f\x3f\x21\xf5\x61\x3a\x1d\x0e\x66\xcb\xd3\x23\x75\xe6\x30\x0a\x7d\x7c\xf0\xfd\x23\x42\x3b\x8e\x9e\x38\x00\x2e\x1b\xe0\x51\xd0\xe8\x10\xea\x4a\x11\x9e\xc5\xa2\x88\x0e\x39\xe3\xba\x4c\x66\xd7\xeb\xbe\x4e\x81\x0b\xc9\x9e\xdf\x89\x85\x07\xdb\x1f\xe1\x36\xa7\xf8\x67\x0f\xf5\x83\x45\x3d\x77\x60\x19\x3a\x09\xed\x6c\x03\x27\xc3\xb9\xd2\x05\x6b\x95\xd1\x32\x3c\x44\x93\x21\x59\xd2\x79\x56\x57\x90\x30\xf1\x98\xac\x14\x49\x93\x68\x33\x24\xf6\x8f\xb0\xbb\x82\x8c\xc1\x59\xbd\x29\x52\x1d\x4e\xca\xcb\x11\xe5\x9a\xf7\x35\x7a\xa2\x5e\x86\x60\xe9\x77\x76\xbf\x9f\xeb\x0a\x0b\x48\xa7\x50\x91\x35\xd4\xfd\x2e\xb6\x94\x65\xd4\x84\x05\x6b\xa0\xc0\x43\x59\xbb\x9f\x6f\x17\x25\xd3\x7c\x3d\x5b\xc4\x0c\xcc\xdc\x82\xf9\xf3\xe7\xfd\xe1\xdf\xdf\xfe\xf3\x1f\xe4\xc6\x36\x75\x25\xb4\x85\xe7\xe6\xfb\x77\x07\x6e\x9d\x2f\x5f\x6e\x91\x74\x42\xd9\x54\x8a\x11\xfa\x4b\x19\xe9\xa4\x92\xb9\x9e\x2f\x9c\x42\xec\x23\xa4\xd9\x02\x44\x48\x63\x22\x7c\x41\x66\x4d\x61\x24\xf8\x29\x17\xf9\x81\x10\x44\xd6\x0e\x83\x90\x0f\x5c\xd2\xd1\xa5\x22\xba\x37\x2b\xbc\xa1\xa1\xb8\x4f\x45\x00\x2f\x5c\xb0\x4a\x44\xcb\x16\x2d\x6b\xa1\x2a\x06\xe7\x6d\x0c\xf3\x36\x8a\x5d\x57\xcc\x38\x6e\x01\x81\xc3\x7b\xce\x4e\x37\x19\x16\xde\xe6\xa9\x29\xa2\x1a\xda\x6d\x52\x6f\xff\x9a\xcd\x9e\x01\x5b\xa4\xde\x1f\x09\xad\x46\xef\xb0\x01\x09\x19\x09\x75\x61\x24\xf4\xaa\xc2\x38\xb6\xf0\xee\x5d\xed\xf7\x90\xe9\xa0\xe6\x9a\x71\x24\x8c\x27\xa3\x56\x75\xe2\xfe\x54\x13\x3a\xc2\x44\x40\xaa\xfc\xb8\xca\xd7\x84\xec\xe3\xfa\x63\x5f\xc5\xd8\x0c\xf5\xf5\x8c\x11\xe5\x93\xb3\x45\x2b\x4d\x92\xa8\x7d\xe2\xb3\xe9\x89\xc6\x0a\x52\x7b\xce\x7e\xb6\x54\x4b\x04\x33\x7c\xff\x73\x3b\x84\xe5\x48\xb2\xc2\x7e\xf2\x34\xdb\x61\xca\x59\xe0\x74\xae\xfd\x7f\x68\x86\x14\x61\xa2\xb6\x48\x58\x1d\xb8\xae\x53\xc4\x67\x7e\xff\x0d\x06\x49\x77\x8d\x93\xa9\xf5\xa2\xde\x31\x30\x6d\x67\x6e\xc1\xf1\xb0\x83\x28\xc0\x01\xae\x8b\x21\xca\x7a\xb9\x42\x64\x73\xb9\xd2\xa1\x03\x3d\x1d\xfe\x5f\x00\x00\x00\xff\xff\x25\xee\x12\x24\xc5\xba\x00\x00") func baseHorizonSqlBytes() ([]byte, error) { return bindataRead( @@ -649,8 +649,8 @@ func baseHorizonSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "base-horizon.sql", size: 41431, mode: os.FileMode(0644), modTime: time.Unix(1567680202, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xcf, 0x6a, 0x6c, 0x4a, 0x93, 0xc7, 0xea, 0x27, 0x8c, 0xb7, 0xb7, 0x85, 0xcc, 0x81, 0xa2, 0x5, 0xf2, 0x63, 0x22, 0xc7, 0xe8, 0x3a, 0x35, 0xfd, 0x3f, 0x27, 0x4e, 0x22, 0x1a, 0x16, 0x88, 0x23}} + info := bindataFileInfo{name: "base-horizon.sql", size: 47813, mode: os.FileMode(0644), modTime: time.Unix(1572527989, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x16, 0x5c, 0x14, 0xde, 0x13, 0xc4, 0x44, 0xaf, 0xdd, 0x2b, 0xcd, 0xeb, 0x61, 0x71, 0xa4, 0x80, 0xee, 0x80, 0x83, 0x49, 0x27, 0xd1, 0xb4, 0x51, 0xb6, 0x25, 0xf8, 0xde, 0xc2, 0xf4, 0x34, 0xa7}} return a, nil } @@ -1269,7 +1269,7 @@ func paths_strict_sendCoreSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "paths_strict_send-core.sql", size: 70852, mode: os.FileMode(0644), modTime: time.Unix(1570470182, 0)} + info := bindataFileInfo{name: "paths_strict_send-core.sql", size: 70852, mode: os.FileMode(0644), modTime: time.Unix(1570634484, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x8a, 0x70, 0x0, 0x65, 0xda, 0x8, 0x46, 0x75, 0x2a, 0xd4, 0x93, 0x75, 0xf1, 0xc1, 0xdd, 0xd0, 0x4b, 0x30, 0x60, 0xa5, 0xcb, 0x14, 0xef, 0xfb, 0x10, 0x72, 0xda, 0x22, 0xab, 0xfc, 0x4f, 0xd4}} return a, nil } @@ -1289,7 +1289,7 @@ func paths_strict_sendHorizonSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "paths_strict_send-horizon.sql", size: 92631, mode: os.FileMode(0644), modTime: time.Unix(1570470186, 0)} + info := bindataFileInfo{name: "paths_strict_send-horizon.sql", size: 92631, mode: os.FileMode(0644), modTime: time.Unix(1570634484, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x92, 0x78, 0x10, 0x66, 0x93, 0xbc, 0x69, 0x4e, 0xdb, 0x2f, 0x5d, 0xe1, 0x7b, 0x7d, 0x2c, 0xa5, 0xf7, 0x3d, 0x99, 0xd3, 0x9d, 0x54, 0x71, 0xe5, 0x90, 0xb9, 0x3, 0x3, 0xa9, 0x18, 0x89, 0x3}} return a, nil } diff --git a/services/horizon/internal/urlparams.go b/services/horizon/internal/urlparams.go index 082832e8aa..b5cb0389a3 100644 --- a/services/horizon/internal/urlparams.go +++ b/services/horizon/internal/urlparams.go @@ -86,30 +86,6 @@ func getLimit(r *http.Request, defaultSize, maxSize uint64) (uint64, error) { return uint64(limitInt64), nil } -// getAccountsPageQuery gets the page query for /accounts -func getAccountsPageQuery(r *http.Request) (db2.PageQuery, error) { - cursor, err := hchi.GetStringFromURL(r, actions.ParamCursor) - if err != nil { - return db2.PageQuery{}, errors.Wrap(err, "getting param cursor") - } - - order, err := getOrder(r) - if err != nil { - return db2.PageQuery{}, errors.Wrap(err, "getting param order") - } - - limit, err := getLimit(r, db2.DefaultPageSize, db2.MaxPageSize) - if err != nil { - return db2.PageQuery{}, errors.Wrap(err, "getting param limit") - } - - return db2.PageQuery{ - Cursor: cursor, - Order: order, - Limit: limit, - }, nil -} - // getPageQuery gets the page query and does the pair validation if // disablePairValidation is false. func getPageQuery(r *http.Request, disablePairValidation bool) (db2.PageQuery, error) { @@ -162,25 +138,6 @@ func getInt32ParamFromURL(r *http.Request, key string) (int32, error) { return int32(asI64), nil } -// getInt64ParamFromURL gets the int64 param with the provided key. It errors -// if the param value cannot be parsed as int64. -func getInt64ParamFromURL(r *http.Request, key string) (int64, error) { - val, err := hchi.GetStringFromURL(r, key) - if err != nil { - return 0, errors.Wrapf(err, "loading %s from URL", key) - } - if val == "" { - return 0, nil - } - - asI64, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return 0, problem.MakeInvalidFieldProblem(key, errors.New("invalid int64 value")) - } - - return asI64, nil -} - // getBoolParamFromURL gets the bool param with the provided key. It errors if // the value is not "true" or "false" or "". func getBoolParamFromURL(r *http.Request, key string) (bool, error) { diff --git a/services/horizon/internal/web.go b/services/horizon/internal/web.go index e26718ab17..2e338b7165 100644 --- a/services/horizon/internal/web.go +++ b/services/horizon/internal/web.go @@ -14,6 +14,7 @@ import ( "github.com/rs/cors" "github.com/sebest/xff" + "github.com/stellar/go/exp/orderbook" "github.com/stellar/go/services/horizon/internal/actions" "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/db2/core" @@ -94,7 +95,6 @@ func (w *web) mustInstallMiddlewares(app *App, connTimeout time.Duration) { } r := w.router - r.Use(chimiddleware.Timeout(connTimeout)) r.Use(chimiddleware.StripSlashes) //TODO: remove this middleware @@ -105,6 +105,7 @@ func (w *web) mustInstallMiddlewares(app *App, connTimeout time.Duration) { r.Use(contextMiddleware) r.Use(xff.Handler) r.Use(loggerMiddleware) + r.Use(timeoutMiddleware(connTimeout)) r.Use(requestMetricsMiddleware) r.Use(recoverMiddleware) r.Use(chimiddleware.Compress(flate.DefaultCompression, "application/hal+json")) @@ -123,11 +124,12 @@ func installPathFindingRoutes( findFixedPaths FindFixedPathsHandler, r *chi.Mux, expIngest bool, + requiresExperimentalIngestion *ExperimentalIngestionMiddleware, ) { r.Group(func(r chi.Router) { r.Use(acceptOnlyJSON) if expIngest { - r.Use(requiresExperimentalIngestion) + r.Use(requiresExperimentalIngestion.Wrap) } r.Method("GET", "/paths", findPaths) r.Method("GET", "/paths/strict-receive", findPaths) @@ -140,10 +142,11 @@ func installAccountOfferRoute( streamHandler sse.StreamHandler, enableExperimentalIngestion bool, r *chi.Mux, + requiresExperimentalIngestion *ExperimentalIngestionMiddleware, ) { path := "/accounts/{account_id}/offers" if enableExperimentalIngestion { - r.With(requiresExperimentalIngestion).Method( + r.With(requiresExperimentalIngestion.Wrap).Method( http.MethodGet, path, streamablePageHandler(offersAction, streamHandler), @@ -155,7 +158,12 @@ func installAccountOfferRoute( // mustInstallActions installs the routing configuration of horizon onto the // provided app. All route registration should be implemented here. -func (w *web) mustInstallActions(config Config, pathFinder paths.Finder) { +func (w *web) mustInstallActions( + config Config, + pathFinder paths.Finder, + orderBookGraph *orderbook.OrderBookGraph, + requiresExperimentalIngestion *ExperimentalIngestionMiddleware, +) { if w == nil { log.Fatal("missing web instance for installing web actions") } @@ -178,8 +186,12 @@ func (w *web) mustInstallActions(config Config, pathFinder paths.Finder) { // account actions r.Route("/accounts", func(r chi.Router) { - r.With(requiresExperimentalIngestion). - Get("/", accountIndexActionHandler(w.getAccountPage)) + r.With(requiresExperimentalIngestion.Wrap). + Method( + http.MethodGet, + "/", + restPageHandler(actions.GetAccountsHandler{}), + ) r.Route("/{account_id}", func(r chi.Router) { r.Get("/", w.streamShowActionHandler(w.getAccountInfo, true)) r.Get("/transactions", w.streamIndexActionHandler(w.getTransactionPage, w.streamTransactions)) @@ -190,9 +202,6 @@ func (w *web) mustInstallActions(config Config, pathFinder paths.Finder) { r.Get("/data/{key}", DataShowAction{}.Handle) }) }) - offersHandler := actions.GetAccountOffersHandler{ - HistoryQ: w.historyQ, - } streamHandler := sse.StreamHandler{ RateLimiter: w.rateLimiter, @@ -200,10 +209,11 @@ func (w *web) mustInstallActions(config Config, pathFinder paths.Finder) { } installAccountOfferRoute( - offersHandler, + actions.GetAccountOffersHandler{}, streamHandler, config.EnableExperimentalIngestion, r, + requiresExperimentalIngestion, ) // transaction history actions @@ -235,17 +245,35 @@ func (w *web) mustInstallActions(config Config, pathFinder paths.Finder) { r.Get("/trade_aggregations", TradeAggregateIndexAction{}.Handle) r.Route("/offers", func(r chi.Router) { - r.With(requiresExperimentalIngestion). + r.With(requiresExperimentalIngestion.Wrap). Method( http.MethodGet, "/", - restPageHandler(actions.GetOffersHandler{HistoryQ: w.historyQ}), + restPageHandler(actions.GetOffersHandler{}), + ) + r.With(acceptOnlyJSON, requiresExperimentalIngestion.Wrap). + Method( + http.MethodGet, + "/{id}", + objectActionHandler{actions.GetOfferByID{}}, ) - r.With(acceptOnlyJSON, requiresExperimentalIngestion). - Get("/{id}", getOfferResource) r.Get("/{offer_id}/trades", TradeIndexAction{}.Handle) }) - r.Get("/order_book", OrderBookShowAction{}.Handle) + + if config.EnableExperimentalIngestion { + r.With(requiresExperimentalIngestion.Wrap).Method( + http.MethodGet, + "/order_book", + streamableObjectActionHandler{ + streamHandler: streamHandler, + action: actions.GetOrderbookHandler{ + OrderBookGraph: orderBookGraph, + }, + }, + ) + } else { + r.Get("/order_book", OrderBookShowAction{}.Handle) + } // Transaction submission API r.Post("/transactions", TransactionCreateAction{}.Handle) @@ -253,6 +281,7 @@ func (w *web) mustInstallActions(config Config, pathFinder paths.Finder) { findPaths := FindPathsHandler{ staleThreshold: config.StaleThreshold, checkHistoryIsStale: !config.EnableExperimentalIngestion, + setLastLedgerHeader: config.EnableExperimentalIngestion, maxPathLength: config.MaxPathLength, maxAssetsParamLength: maxAssetsForPathFinding, pathFinder: pathFinder, @@ -260,14 +289,26 @@ func (w *web) mustInstallActions(config Config, pathFinder paths.Finder) { } findFixedPaths := FindFixedPathsHandler{ maxPathLength: config.MaxPathLength, + setLastLedgerHeader: config.EnableExperimentalIngestion, maxAssetsParamLength: maxAssetsForPathFinding, pathFinder: pathFinder, coreQ: w.coreQ, } - installPathFindingRoutes(findPaths, findFixedPaths, w.router, config.EnableExperimentalIngestion) + installPathFindingRoutes( + findPaths, + findFixedPaths, + w.router, + config.EnableExperimentalIngestion, + requiresExperimentalIngestion, + ) - if config.EnableAssetStats { - // Asset related endpoints + if config.EnableExperimentalIngestion { + r.With(requiresExperimentalIngestion.Wrap).Method( + http.MethodGet, + "/assets", + restPageHandler(actions.AssetStatsHandler{}), + ) + } else if config.EnableAssetStats { r.Get("/assets", AssetsAction{}.Handle) } diff --git a/support/db/main.go b/support/db/main.go index d1a8a76f85..3fb1376837 100644 --- a/support/db/main.go +++ b/support/db/main.go @@ -114,6 +114,22 @@ type Session struct { tx *sqlx.Tx } +type SessionInterface interface { + Begin() error + Rollback() error + TruncateTables(tables []string) error + Clone() *Session + Close() error + Get(dest interface{}, query squirrel.Sqlizer) error + GetRaw(dest interface{}, query string, args ...interface{}) error + Select(dest interface{}, query squirrel.Sqlizer) error + SelectRaw(dest interface{}, query string, args ...interface{}) error + GetTable(name string) *Table + Exec(query squirrel.Sqlizer) (sql.Result, error) + ExecRaw(query string, args ...interface{}) (sql.Result, error) + NoRows(err error) bool +} + // Table helps to build sql queries against a given table. It logically // represents a SQL table on the database that `Session` is connected to. type Table struct { diff --git a/support/db/mock_session.go b/support/db/mock_session.go new file mode 100644 index 0000000000..fe96d6226d --- /dev/null +++ b/support/db/mock_session.go @@ -0,0 +1,84 @@ +package db + +import ( + "database/sql" + + "github.com/Masterminds/squirrel" + sq "github.com/Masterminds/squirrel" + "github.com/stretchr/testify/mock" +) + +var _ SessionInterface = (*MockSession)(nil) + +type MockSession struct { + mock.Mock +} + +func (m *MockSession) Begin() error { + args := m.Called() + return args.Error(0) +} + +func (m *MockSession) Rollback() error { + args := m.Called() + return args.Error(0) +} + +func (m *MockSession) TruncateTables(tables []string) error { + args := m.Called(tables) + return args.Error(0) +} + +func (m *MockSession) Clone() *Session { + args := m.Called() + return args.Get(0).(*Session) +} + +func (m *MockSession) Close() error { + args := m.Called() + return args.Error(0) +} + +func (m *MockSession) Get(dest interface{}, query sq.Sqlizer) error { + args := m.Called(dest, query) + return args.Error(0) +} + +func (m *MockSession) GetRaw(dest interface{}, query string, args ...interface{}) error { + argss := m.Called(dest, query, args) + return argss.Error(0) +} + +func (m *MockSession) Select(dest interface{}, query squirrel.Sqlizer) error { + argss := m.Called(dest, query) + return argss.Error(0) +} + +func (m *MockSession) SelectRaw( + dest interface{}, + query string, + args ...interface{}, +) error { + argss := m.Called(dest, query, args) + return argss.Error(0) +} + +func (m *MockSession) GetTable(name string) *Table { + args := m.Called(name) + return args.Get(0).(*Table) +} + +func (m *MockSession) Exec(query squirrel.Sqlizer) (sql.Result, error) { + args := m.Called(query) + return args.Get(0).(sql.Result), args.Error(1) +} + +func (m *MockSession) ExecRaw(query string, args ...interface{}) (sql.Result, error) { + argss := m.Called(query, args) + return argss.Get(0).(sql.Result), argss.Error(1) +} + +func (m *MockSession) NoRows(err error) bool { + args := m.Called(err) + return args.Get(0).(bool) +} diff --git a/xdr/account_entry.go b/xdr/account_entry.go index 4b708f2dd4..6e6f208bbd 100644 --- a/xdr/account_entry.go +++ b/xdr/account_entry.go @@ -14,5 +14,17 @@ func (a *AccountEntry) SignerSummary() map[string]int32 { } func (a *AccountEntry) MasterKeyWeight() byte { - return a.Thresholds[0] + return a.Thresholds.MasterKeyWeight() +} + +func (a *AccountEntry) ThresholdLow() byte { + return a.Thresholds.ThresholdLow() +} + +func (a *AccountEntry) ThresholdMedium() byte { + return a.Thresholds.ThresholdMedium() +} + +func (a *AccountEntry) ThresholdHigh() byte { + return a.Thresholds.ThresholdHigh() } diff --git a/xdr/account_flags.go b/xdr/account_flags.go new file mode 100644 index 0000000000..6bb32c504a --- /dev/null +++ b/xdr/account_flags.go @@ -0,0 +1,19 @@ +package xdr + +// IsAuthRequired returns true if the account has the "AUTH_REQUIRED" option +// turned on. +func (accountFlags AccountFlags) IsAuthRequired() bool { + return (accountFlags & AccountFlagsAuthRequiredFlag) != 0 +} + +// IsAuthRevocable returns true if the account has the "AUTH_REVOCABLE" option +// turned on. +func (accountFlags AccountFlags) IsAuthRevocable() bool { + return (accountFlags & AccountFlagsAuthRevocableFlag) != 0 +} + +// IsAuthImmutable returns true if the account has the "AUTH_IMMUTABLE" option +// turned on. +func (accountFlags AccountFlags) IsAuthImmutable() bool { + return (accountFlags & AccountFlagsAuthImmutableFlag) != 0 +} diff --git a/xdr/account_flags_test.go b/xdr/account_flags_test.go new file mode 100644 index 0000000000..d73da5b747 --- /dev/null +++ b/xdr/account_flags_test.go @@ -0,0 +1,57 @@ +package xdr_test + +import ( + "testing" + + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestIsAuthRequired(t *testing.T) { + tt := assert.New(t) + + flag := xdr.AccountFlags(1) + tt.True(flag.IsAuthRequired()) + + flag = xdr.AccountFlags(0) + tt.False(flag.IsAuthRequired()) + + flag = xdr.AccountFlags(2) + tt.False(flag.IsAuthRequired()) + + flag = xdr.AccountFlags(4) + tt.False(flag.IsAuthRequired()) + +} + +func TestIsAuthRevocable(t *testing.T) { + tt := assert.New(t) + + flag := xdr.AccountFlags(2) + tt.True(flag.IsAuthRevocable()) + + flag = xdr.AccountFlags(0) + tt.False(flag.IsAuthRevocable()) + + flag = xdr.AccountFlags(1) + tt.False(flag.IsAuthRevocable()) + + flag = xdr.AccountFlags(4) + tt.False(flag.IsAuthRevocable()) + +} +func TestIsAuthImmutable(t *testing.T) { + tt := assert.New(t) + + flag := xdr.AccountFlags(4) + tt.True(flag.IsAuthImmutable()) + + flag = xdr.AccountFlags(0) + tt.False(flag.IsAuthImmutable()) + + flag = xdr.AccountFlags(1) + tt.False(flag.IsAuthImmutable()) + + flag = xdr.AccountFlags(2) + tt.False(flag.IsAuthImmutable()) +} diff --git a/xdr/account_id.go b/xdr/account_id.go index 49455e13b7..115626806b 100644 --- a/xdr/account_id.go +++ b/xdr/account_id.go @@ -84,6 +84,15 @@ func MustAddress(address string) AccountId { return aid } +// AddressToAccountId returns an AccountId for a given address string. +// If the address is not valid the error returned will not be nil +func AddressToAccountId(address string) (AccountId, error) { + result := AccountId{} + err := result.SetAddress(address) + + return result, err +} + // SetAddress modifies the receiver, setting it's value to the AccountId form // of the provided address. func (aid *AccountId) SetAddress(address string) error { diff --git a/xdr/account_id_test.go b/xdr/account_id_test.go index 0b845e6a66..ad3a7a011b 100644 --- a/xdr/account_id_test.go +++ b/xdr/account_id_test.go @@ -1,10 +1,10 @@ package xdr_test import ( - . "github.com/stellar/go/xdr" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + . "github.com/stellar/go/xdr" ) var _ = Describe("xdr.AccountId#Address()", func() { @@ -47,3 +47,17 @@ var _ = Describe("xdr.AccountId#LedgerKey()", func() { Expect(packed.Equals(aid)).To(BeTrue()) }) }) + +var _ = Describe("xdr.AddressToAccountID()", func() { + It("works", func() { + address := "GCR22L3WS7TP72S4Z27YTO6JIQYDJK2KLS2TQNHK6Y7XYPA3AGT3X4FH" + accountID, err := AddressToAccountId(address) + + Expect(accountID.Address()).To(Equal("GCR22L3WS7TP72S4Z27YTO6JIQYDJK2KLS2TQNHK6Y7XYPA3AGT3X4FH")) + Expect(err).ShouldNot(HaveOccurred()) + + _, err = AddressToAccountId("GCR22L3") + + Expect(err).Should(HaveOccurred()) + }) +}) diff --git a/xdr/account_thresholds.go b/xdr/account_thresholds.go new file mode 100644 index 0000000000..a9a6bf6d09 --- /dev/null +++ b/xdr/account_thresholds.go @@ -0,0 +1,17 @@ +package xdr + +func (t Thresholds) MasterKeyWeight() byte { + return t[0] +} + +func (t Thresholds) ThresholdLow() byte { + return t[1] +} + +func (t Thresholds) ThresholdMedium() byte { + return t[2] +} + +func (t Thresholds) ThresholdHigh() byte { + return t[3] +} diff --git a/xdr/asset.go b/xdr/asset.go index a83d0eae02..a8d1e5b952 100644 --- a/xdr/asset.go +++ b/xdr/asset.go @@ -3,6 +3,7 @@ package xdr import ( "errors" "fmt" + "regexp" "strings" "github.com/stellar/go/strkey" @@ -10,6 +11,20 @@ import ( // This file contains helpers for working with xdr.Asset structs +// AssetTypeToString maps an xdr.AssetType to its string representation +var AssetTypeToString = map[AssetType]string{ + AssetTypeAssetTypeNative: "native", + AssetTypeAssetTypeCreditAlphanum4: "credit_alphanum4", + AssetTypeAssetTypeCreditAlphanum12: "credit_alphanum12", +} + +// StringToAssetType maps an strings to its xdr.AssetType representation +var StringToAssetType = map[string]AssetType{ + "native": AssetTypeAssetTypeNative, + "credit_alphanum4": AssetTypeAssetTypeCreditAlphanum4, + "credit_alphanum12": AssetTypeAssetTypeCreditAlphanum12, +} + // MustNewNativeAsset returns a new native asset, panicking if it can't. func MustNewNativeAsset() Asset { a := Asset{} @@ -32,6 +47,96 @@ func MustNewCreditAsset(code string, issuer string) Asset { return a } +// BuildAsset creates a new asset from a given `assetType`, `code`, and `issuer`. +// +// Valid assetTypes are: +// - `native` +// - `credit_alphanum4` +// - `credit_alphanum12` +func BuildAsset(assetType, issuer, code string) (Asset, error) { + t, ok := StringToAssetType[assetType] + + if !ok { + return Asset{}, errors.New("invalid asset type: was not one of 'native', 'credit_alphanum4', 'credit_alphanum12'") + } + + var asset Asset + switch t { + case AssetTypeAssetTypeNative: + if err := asset.SetNative(); err != nil { + return Asset{}, err + } + default: + issuerAccountID := AccountId{} + if err := issuerAccountID.SetAddress(issuer); err != nil { + return Asset{}, err + } + + if err := asset.SetCredit(code, issuerAccountID); err != nil { + return Asset{}, err + } + } + + return asset, nil +} + +var ValidAssetCode = regexp.MustCompile("^[[:alnum:]]{1,12}$") + +// BuildAssets parses a list of assets from a given string. +// The string is expected to be a comma separated list of assets +// encoded in the format (Code:Issuer or "native") defined by SEP-0011 +// https://github.com/stellar/stellar-protocol/pull/313 +// If the string is empty, BuildAssets will return an empty list of assets +func BuildAssets(s string) ([]Asset, error) { + var assets []Asset + if s == "" { + return assets, nil + } + + assetStrings := strings.Split(s, ",") + for _, assetString := range assetStrings { + var asset Asset + + // Technically https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0011.md allows + // any string up to 12 characters not containing an unescaped colon to represent XLM + // however, this function only accepts the string "native" to represent XLM + if strings.ToLower(assetString) == "native" { + if err := asset.SetNative(); err != nil { + return nil, err + } + } else { + parts := strings.Split(assetString, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("%s is not a valid asset", assetString) + } + + code := parts[0] + if !ValidAssetCode.MatchString(code) { + return nil, fmt.Errorf( + "%s is not a valid asset, it contains an invalid asset code", + assetString, + ) + } + + issuer, err := AddressToAccountId(parts[1]) + if err != nil { + return nil, fmt.Errorf( + "%s is not a valid asset, it contains an invalid issuer", + assetString, + ) + } + + if err := asset.SetCredit(code, issuer); err != nil { + return nil, fmt.Errorf("%s is not a valid asset", assetString) + } + } + + assets = append(assets, asset) + } + + return assets, nil +} + // SetCredit overwrites `a` with a credit asset using `code` and `issuer`. The // asset type (CreditAlphanum4 or CreditAlphanum12) is chosen automatically // based upon the length of `code`. @@ -176,14 +281,7 @@ func (a Asset) Extract(typ interface{}, code interface{}, issuer interface{}) er case *AssetType: *typ = a.Type case *string: - switch a.Type { - case AssetTypeAssetTypeNative: - *typ = "native" - case AssetTypeAssetTypeCreditAlphanum4: - *typ = "credit_alphanum4" - case AssetTypeAssetTypeCreditAlphanum12: - *typ = "credit_alphanum12" - } + *typ = AssetTypeToString[a.Type] default: return errors.New("can't extract type") } diff --git a/xdr/asset_test.go b/xdr/asset_test.go index 1f4c59ce70..b6ac37af96 100644 --- a/xdr/asset_test.go +++ b/xdr/asset_test.go @@ -5,8 +5,9 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - . "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" + + . "github.com/stellar/go/xdr" ) var _ = Describe("xdr.Asset#Extract()", func() { @@ -228,3 +229,154 @@ func TestToAllowTrustOpAsset_Error(t *testing.T) { _, err := a.ToAllowTrustOpAsset("") assert.EqualError(t, err, "Asset code length is invalid") } + +func TestBuildAssets(t *testing.T) { + for _, testCase := range []struct { + name string + value string + expectedAssets []Asset + expectedError string + }{ + { + "empty list", + "", + []Asset{}, + "", + }, + { + "native", + "native", + []Asset{MustNewNativeAsset()}, + "", + }, + { + "asset does not contain :", + "invalid-asset", + []Asset{}, + "invalid-asset is not a valid asset", + }, + { + "asset contains more than one :", + "usd:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V:", + []Asset{}, + "is not a valid asset", + }, + { + "unicode asset code", + "üsd:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V", + []Asset{}, + "contains an invalid asset code", + }, + { + "asset code must be alpha numeric", + "!usd:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V", + []Asset{}, + "contains an invalid asset code", + }, + { + "asset code contains backslash", + "usd\\x23:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V", + []Asset{}, + "contains an invalid asset code", + }, + { + "contains null characters", + "abcde\\x00:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V", + []Asset{}, + "contains an invalid asset code", + }, + { + "asset code is too short", + ":GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V", + []Asset{}, + "is not a valid asset", + }, + { + "asset code is too long", + "0123456789abc:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V", + []Asset{}, + "is not a valid asset", + }, + { + "issuer is empty", + "usd:", + []Asset{}, + "contains an invalid issuer", + }, + { + "issuer is invalid", + "usd:kkj9808;l", + []Asset{}, + "contains an invalid issuer", + }, + { + "validation succeeds", + "usd:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V,usdabc:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V", + []Asset{ + MustNewCreditAsset("usd", "GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V"), + MustNewCreditAsset("usdabc", "GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V"), + }, + "", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + tt := assert.New(t) + assets, err := BuildAssets(testCase.value) + if testCase.expectedError == "" { + tt.NoError(err) + tt.Len(assets, len(testCase.expectedAssets)) + for i := range assets { + tt.Equal(testCase.expectedAssets[i], assets[i]) + } + } else { + tt.Error(err) + tt.Contains(err.Error(), testCase.expectedError) + } + }) + } +} + +func TestBuildAsset(t *testing.T) { + testCases := []struct { + assetType string + code string + issuer string + valid bool + }{ + { + assetType: "native", + valid: true, + }, + { + assetType: "credit_alphanum4", + code: "USD", + issuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + valid: true, + }, + { + assetType: "credit_alphanum12", + code: "SPOOON", + issuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + valid: true, + }, + { + assetType: "invalid", + }, + } + for _, tc := range testCases { + t.Run(tc.assetType, func(t *testing.T) { + asset, err := BuildAsset(tc.assetType, tc.issuer, tc.code) + + if tc.valid { + assert.NoError(t, err) + var assetType, code, issuer string + asset.Extract(&assetType, &code, &issuer) + assert.Equal(t, tc.assetType, assetType) + assert.Equal(t, tc.code, code) + assert.Equal(t, tc.issuer, issuer) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/xdr/ledger_key_test.go b/xdr/ledger_key_test.go new file mode 100644 index 0000000000..abbde1594f --- /dev/null +++ b/xdr/ledger_key_test.go @@ -0,0 +1,24 @@ +package xdr_test + +import ( + "encoding/base64" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestLedgerKeyTrustLineBinaryMaxLength(t *testing.T) { + key := &xdr.LedgerKey{} + err := key.SetTrustline( + xdr.MustAddress("GBFLTCDLOE6YQ74B66RH3S2UW5I2MKZ5VLTM75F4YMIWUIXRIFVNRNIF"), + xdr.MustNewCreditAsset("123456789012", "GBFLTCDLOE6YQ74B66RH3S2UW5I2MKZ5VLTM75F4YMIWUIXRIFVNRNIF"), + ) + assert.NoError(t, err) + + compressed, err := key.MarshalBinary() + assert.NoError(t, err) + assert.Equal(t, len(compressed), 92) + bcompressed := base64.StdEncoding.EncodeToString(compressed) + assert.Equal(t, len(bcompressed), 124) +} diff --git a/xdr/trust_line_flags.go b/xdr/trust_line_flags.go new file mode 100644 index 0000000000..bdae1fb266 --- /dev/null +++ b/xdr/trust_line_flags.go @@ -0,0 +1,7 @@ +package xdr + +// IsAuthorized returns true if issuer has authorized account to perform +// transactions with its credit +func (e TrustLineFlags) IsAuthorized() bool { + return (e & TrustLineFlagsAuthorizedFlag) != 0 +} diff --git a/xdr/trust_line_flags_test.go b/xdr/trust_line_flags_test.go new file mode 100644 index 0000000000..a50b5b21c3 --- /dev/null +++ b/xdr/trust_line_flags_test.go @@ -0,0 +1,21 @@ +package xdr_test + +import ( + "testing" + + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestIsAuthorized(t *testing.T) { + tt := assert.New(t) + + flag := xdr.TrustLineFlags(1) + tt.True(flag.IsAuthorized()) + + flag = xdr.TrustLineFlags(0) + tt.False(flag.IsAuthorized()) + + flag = xdr.TrustLineFlags(2) + tt.False(flag.IsAuthorized()) +}