diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9f748c43a6..f8c67d5838 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,22 +1,28 @@ -If you're making a doc PR or something tiny where the below is irrelevant, just delete this template and use a short description. +If you're making a doc PR or something tiny where the below is irrelevant, just delete this +template and use a short description. ### PR Structure -* [ ] This PR has reasonably narrow scope (if not, break it down into smaller PRs) -* [ ] This PR avoids mixing refactoring changes with feature changes (split into two PRs otherwise) -* [ ] This PR's title starts with name of package that is most changed in the PR, ex. `services/friendbot` - +* [ ] This PR has reasonably narrow scope (if not, break it down into smaller PRs). +* [ ] This PR avoids mixing refactoring changes with feature changes (split into two PRs + otherwise). +* [ ] This PR's title starts with name of package that is most changed in the PR, ex. + `services/friendbot` ### Thoroughness * [ ] This PR adds tests for the most critical parts of the new functionality or fixes. -* [ ] I've updated any docs ([developer docs](https://www.stellar.org/developers/reference/), `.md` files, etc...) affected by this change +* [ ] I've updated any docs ([developer docs](https://www.stellar.org/developers/reference/), `.md` + files, etc... affected by this change). Take a look in the `docs` folder for a given service, + like [this one](https://github.com/stellar/go/tree/master/services/horizon/internal/docs). ### Release planning -* [ ] I've updated the relevant CHANGELOG ([here](services/horizon/CHANGELOG.md) for Horizon) if needed with deprecations, added features, breaking changes, and DB schema changes -* [ ] I've decided if this PR requires a new major/minor version according to [semver](https://semver.org/), or if it's monly a patch change. The PR is targeted at the next release branch if it's not a patch change. - +* [ ] I've updated the relevant CHANGELOG ([here](services/horizon/CHANGELOG.md) for Horizon) if + needed with deprecations, added features, breaking changes, and DB schema changes. +* [ ] I've decided if this PR requires a new major/minor version according to + [semver](https://semver.org/), or if it's mainly a patch change. The PR is targeted at the next + release branch if it's not a patch change. ## Summary diff --git a/CHANGELOG.md b/CHANGELOG.md index a9e4dc63e6..b966a9d61b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,22 @@ # Changelog - -All notable changes to this project will be documented in this -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. - -NOTE: this changelog represents the changes that are associated with the library code in this repo (rather than the tools or services in this repo). - -## [Unreleased] - -### Added - -- xdr: added support for new signer types -- build: `Signer` learned support for new signer types -- strkey: added support for new signer types -- network: Added the `HashTransaction` helper func to get the hash of a transaction targeted to a specific stellar network. -- trades: Added Server-Sent Events endpoint to support streaming of trades -- trades: add `base_offer_id` and `counter_offer_id` to trade resources. -- trade aggregation: Added an optional `offset` parameter that lets you offset the bucket timestamps in hour-long increments. Can only be used if the `resolution` parameter is greater than 1 hour. `offset` must also be in whole-hours and less than 24 hours. - - -### Changed: - -- build: _BREAKING CHANGE_: A transaction built and signed using the `build` package no longer default to the test network. -- trades for offer endpoint will query for trades that match the given offer on either side of trades, rather than just the "sell" offer. - -[Unreleased]: https://github.com/stellar/go/commits/master \ No newline at end of file +This repository adheres to [Semantic Versioning](http://semver.org/). + +This monorepo contains a number of projects, individually versioned and released. Please consult the relevant changelog: + +* `horizon server` ([changelog](./services/horizon/CHANGELOG.md)) +* `horizonclient` ([changelog](./clients/horizonclient/CHANGELOG.md)) +* `txnbuild` ([changelog](./txnbuild/CHANGELOG.md)) +* `bridge` ([changelog](./services/bridge/CHANGELOG.md)) +* `compliance` ([changelog](./services/compliance/CHANGELOG.md)) +* `federation` ([changelog](./services/federation/CHANGELOG.md)) +* `bifrost` ([changelog](./services/bifrost/CHANGELOG.md)) +* `ticker` (experimental) ([changelog](./exp/ticker/CHANGELOG.md)) +* `stellar-vanity-gen` ([changelog](./tools/stellar-vanity-gen/CHANGELOG.md)) +* `stellar-sign` ([changelog](./tools/stellar-sign/CHANGELOG.md)) +* `stellar-archivist` ([changelog](./tools/stellar-archivist/CHANGELOG.md)) +* `stellar-hd-wallet` ([changelog](./tools/stellar-hd-wallet/CHANGELOG.md)) + +If a project is pre-v1.0, breaking changes may happen for minor version +bumps. A breaking change will be clearly notified in the corresponding changelog. + +Official project releases may be found here: https://github.com/stellar/go/releases diff --git a/README.md b/README.md index a9d124a8e7..7d25b07ae0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ This repo is the home for all of the public go code produced by SDF. In additio ## Package Index * [Horizon Server](services/horizon): Full-featured API server for Stellar network -* [Go Clients (Horizon SDK)](clients): Go SDK for making requests to Horizon Server +* [Go Horizon SDK - horizonclient](clients/horizonclient): Client for Horizon server (queries and transaction submission) +* [Go Horizon SDK - txnbuild](txnbuild): Construct Stellar transactions and operations * [Bifrost](services/bifrost): Bitcoin/Ethereum -> Stellar bridge * Servers for Anchors & Financial Institutions * [Bridge Server](services/bridge): send payments and take action when payments are received diff --git a/clients/README.md b/clients/README.md index 59a6090418..216862fb8b 100644 --- a/clients/README.md +++ b/clients/README.md @@ -1,11 +1,16 @@ # Clients package -Packages contained by this package provide client libraries for accessing the ecosystem of stellar services. At present, it only contains a simple horizon client library, but in the future it will contain clients to interact with stellar-core, federation, the bridge server and more. +Packages here provide client libraries for accessing the ecosystem of Stellar services. -See [godoc](https://godoc.org/github.com/stellar/go/clients) for details about each package. +* `horizonclient` - programmatic client access to Horizon (use in conjunction with [txnbuild](../txnbuild)) +* `stellartoml` - parse Stellar.toml files from the internet +* `federation` - resolve federation addresses into stellar account IDs, suitable for use within a transaction +* `horizon` (DEPRECATED) - the original Horizon client, now superceded by `horizonclient` -## Adding new client packages +See [GoDoc](https://godoc.org/github.com/stellar/go/clients) for more details. + +## For developers: Adding new client packages Ideally, each one of our client packages will have commonalities in their API to ease the cost of learning each. It's recommended that we follow a pattern similar to the `net/http` package's client shape: -A type, `Client`, is the central type of any client package, and its methods should provide the bulk of the functionality for the package. A `DefaultClient` var is provided for consumers that don't need client-level customization of behavior. Each method on the `Client` type should have a corresponding func at the package level that proxies a call through to the default client. For example, `http.Get()` is the equivalent of `http.DefaultClient.Get()`. \ No newline at end of file +A type, `Client`, is the central type of any client package, and its methods should provide the bulk of the functionality for the package. A `DefaultClient` var is provided for consumers that don't need client-level customization of behavior. Each method on the `Client` type should have a corresponding func at the package level that proxies a call through to the default client. For example, `http.Get()` is the equivalent of `http.DefaultClient.Get()`. diff --git a/clients/horizon/main.go b/clients/horizon/main.go index 4b350626fc..81fe6d6c3c 100644 --- a/clients/horizon/main.go +++ b/clients/horizon/main.go @@ -1,9 +1,6 @@ -// Package horizon provides client access to a horizon server, allowing an -// application to post transactions and lookup ledger information. +// Package horizon is DEPRECATED in favour of clients/horizonclient! It used to provide client access to a horizon +// server, allowing an application to post transactions and lookup ledger information. // -// Create an instance of `Client` to customize the server used, or alternatively -// use `DefaultTestNetClient` or `DefaultPublicNetClient` to access the SDF run -// horizon servers. // Deprecated: clients/horizon package with all its exported methods and variables will no longer be // maintained. It will be removed in future versions of the SDK. // Use clients/horizonclient (https://godoc.org/github.com/stellar/go/clients/horizonclient) instead. diff --git a/clients/horizonclient/CHANGELOG.md b/clients/horizonclient/CHANGELOG.md index 17ef48d627..46ab970390 100644 --- a/clients/horizonclient/CHANGELOG.md +++ b/clients/horizonclient/CHANGELOG.md @@ -4,7 +4,20 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## [v1.0.1] - (draft release) +## [v1.1.0](https://github.com/stellar/go/releases/tag/horizonclient-v1.1.0) - 2019-02-02 ### Added -- `Client.Root()` method for querying the root endpoint of a horizon server. \ No newline at end of file + +- `Client.Root()` method for querying the root endpoint of a horizon server. +- Support for returning concrete effect types[#1217](https://github.com/stellar/go/pull/1217) +- Fix when no HTTP client is provided + +### Changes + +- `Client.Fund()` now returns `TransactionSuccess` instead of a http response pointer. + +- Querying the effects endpoint now supports returning the concrete effect type for each effect. This is also supported in streaming mode. See the [docs](https://godoc.org/github.com/stellar/go/clients/horizonclient#Client.Effects) for examples. + +## [v1.0.0](https://github.com/stellar/go/releases/tag/horizonclient-v1.0) - 2019-04-26 + + * Initial release \ No newline at end of file diff --git a/clients/horizonclient/README.md b/clients/horizonclient/README.md index 413d60c10c..4cc481383d 100644 --- a/clients/horizonclient/README.md +++ b/clients/horizonclient/README.md @@ -26,20 +26,20 @@ This library is aimed at developers building Go applications that interact with import hClient "github.com/stellar/go/clients/horizonclient" ... - // use the default pubnet client + // Use the default pubnet client client := hClient.DefaultPublicNetClient - // create an account request + + // Create an account request accountRequest := hClient.AccountRequest{AccountID: "GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU"} - // load the account detail from the network + // Load the account detail from the network account, err := client.AccountDetail(accountRequest) if err != nil { fmt.Println(err) return } - // account contains information about the stellar account + // Account contains information about the stellar account fmt.Print(account) - ``` For more examples, refer to the [documentation](https://godoc.org/github.com/stellar/go/clients/horizonclient). @@ -47,7 +47,9 @@ For more examples, refer to the [documentation](https://godoc.org/github.com/ste Run the unit tests from the package directory: `go test` ## Contributing -Please read [CONTRIBUTING](../../CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. +Please read [Code of Conduct](https://github.com/stellar/.github/blob/master/CODE_OF_CONDUCT.md) to understand this project's communication rules. + +To submit improvements and fixes to this library, please see [CONTRIBUTING](../CONTRIBUTING.md). ## License This project is licensed under the Apache License - see the [LICENSE](../../LICENSE-APACHE.txt) file for details. diff --git a/clients/horizonclient/client.go b/clients/horizonclient/client.go index be3e1f8db9..9665dc4978 100644 --- a/clients/horizonclient/client.go +++ b/clients/horizonclient/client.go @@ -16,8 +16,8 @@ import ( "github.com/manucorporat/sse" hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/protocols/horizon/effects" "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/app" "github.com/stellar/go/support/errors" ) @@ -53,7 +53,7 @@ func (c *Client) sendRequestURL(requestURL string, method string, a interface{}) return errors.Wrap(err, "error creating HTTP request") } c.setClientAppHeaders(req) - + c.setDefaultClient() if c.horizonTimeOut == 0 { c.horizonTimeOut = HorizonTimeOut } @@ -93,7 +93,7 @@ func (c *Client) stream( return errors.Wrap(err, "error creating HTTP request") } req.Header.Set("Accept", "text/event-stream") - // to do: confirm name and version + c.setDefaultClient() c.setClientAppHeaders(req) // We can use c.HTTP here because we set Timeout per request not on the client. See sendRequest() @@ -198,11 +198,18 @@ func (c *Client) stream( func (c *Client) setClientAppHeaders(req *http.Request) { req.Header.Set("X-Client-Name", "go-stellar-sdk") - req.Header.Set("X-Client-Version", app.Version()) + req.Header.Set("X-Client-Version", c.Version()) req.Header.Set("X-App-Name", c.AppName) req.Header.Set("X-App-Version", c.AppVersion) } +// setDefaultClient sets the default HTTP client when none is provided. +func (c *Client) setDefaultClient() { + if c.HTTP == nil { + c.HTTP = http.DefaultClient + } +} + // fixHorizonURL strips all slashes(/) at the end of HorizonURL if any, then adds a single slash func (c *Client) fixHorizonURL() string { return strings.TrimRight(c.HorizonURL, "/") + "/" @@ -251,7 +258,7 @@ func (c *Client) AccountData(request AccountRequest) (accountData hProtocol.Acco // Effects returns effects(https://www.stellar.org/developers/horizon/reference/resources/effect.html) // It can be used to return effects for an account, a ledger, an operation, a transaction and all effects on the network. -func (c *Client) Effects(request EffectRequest) (effects hProtocol.EffectsPage, err error) { +func (c *Client) Effects(request EffectRequest) (effects effects.EffectsPage, err error) { err = c.sendRequest(request, &effects) return } @@ -263,13 +270,6 @@ func (c *Client) Assets(request AssetRequest) (assets hProtocol.AssetsPage, err return } -// Stream is for endpoints that support streaming -func (c *Client) Stream(ctx context.Context, request StreamRequest, handler func(interface{})) (err error) { - - err = request.Stream(ctx, c, handler) - return -} - // Ledgers returns information about all ledgers. // See https://www.stellar.org/developers/horizon/reference/endpoints/ledgers-all.html func (c *Client) Ledgers(request LedgerRequest) (ledgers hProtocol.LedgersPage, err error) { @@ -422,11 +422,13 @@ func (c *Client) Trades(request TradeRequest) (tds hProtocol.TradesPage, err err // Fund creates a new account funded from friendbot. It only works on test networks. See // https://www.stellar.org/developers/guides/get-started/create-account.html for more information. -func (c *Client) Fund(addr string) (*http.Response, error) { +func (c *Client) Fund(addr string) (txSuccess hProtocol.TransactionSuccess, err error) { if !c.isTestNet { - return nil, errors.New("Can't fund account from friendbot on production network") + return txSuccess, errors.New("can't fund account from friendbot on production network") } - return http.Get(c.HorizonURL + "friendbot?addr=" + addr) + friendbotURL := fmt.Sprintf("%sfriendbot?addr=%s", c.fixHorizonURL(), addr) + err = c.sendRequestURL(friendbotURL, "get", &txSuccess) + return } // StreamTrades streams executed trades. It can be used to stream all trades, trades for an account and @@ -520,5 +522,10 @@ func (c *Client) Root() (root hProtocol.Root, err error) { return } +// Version returns the current version. +func (c *Client) Version() string { + return version +} + // ensure that the horizon client implements ClientInterface var _ ClientInterface = &Client{} diff --git a/clients/horizonclient/effect_request.go b/clients/horizonclient/effect_request.go index f3d0d7144d..232e3e059f 100644 --- a/clients/horizonclient/effect_request.go +++ b/clients/horizonclient/effect_request.go @@ -11,59 +11,38 @@ import ( ) // EffectHandler is a function that is called when a new effect is received -type EffectHandler func(effects.Base) +type EffectHandler func(effects.Effect) // BuildURL creates the endpoint to be queried based on the data in the EffectRequest struct. // If no data is set, it defaults to the build the URL for all effects func (er EffectRequest) BuildURL() (endpoint string, err error) { - nParams := countParams(er.ForAccount, er.ForLedger, er.ForOperation, er.ForTransaction) if nParams > 1 { - err = errors.New("invalid request: too many parameters") - } - - if err != nil { - return endpoint, err + return endpoint, errors.New("invalid request: too many parameters") } endpoint = "effects" if er.ForAccount != "" { - endpoint = fmt.Sprintf( - "accounts/%s/effects", - er.ForAccount, - ) + endpoint = fmt.Sprintf("accounts/%s/effects", er.ForAccount) } if er.ForLedger != "" { - endpoint = fmt.Sprintf( - "ledgers/%s/effects", - er.ForLedger, - ) + endpoint = fmt.Sprintf("ledgers/%s/effects", er.ForLedger) } if er.ForOperation != "" { - endpoint = fmt.Sprintf( - "operations/%s/effects", - er.ForOperation, - ) + endpoint = fmt.Sprintf("operations/%s/effects", er.ForOperation) } if er.ForTransaction != "" { - endpoint = fmt.Sprintf( - "transactions/%s/effects", - er.ForTransaction, - ) + endpoint = fmt.Sprintf("transactions/%s/effects", er.ForTransaction) } queryParams := addQueryParams(cursor(er.Cursor), limit(er.Limit), er.Order) if queryParams != "" { - endpoint = fmt.Sprintf( - "%s?%s", - endpoint, - queryParams, - ) + endpoint = fmt.Sprintf("%s?%s", endpoint, queryParams) } _, err = url.Parse(endpoint) @@ -77,24 +56,27 @@ func (er EffectRequest) BuildURL() (endpoint string, err error) { // StreamEffects streams horizon effects. It can be used to stream all effects or account specific effects. // Use context.WithCancel to stop streaming or context.Background() if you want to stream indefinitely. // EffectHandler is a user-supplied function that is executed for each streamed effect received. -func (er EffectRequest) StreamEffects( - ctx context.Context, - client *Client, - handler EffectHandler, -) (err error) { +func (er EffectRequest) StreamEffects(ctx context.Context, client *Client, handler EffectHandler) error { endpoint, err := er.BuildURL() if err != nil { - return errors.Wrap(err, "unable to build endpoint") + return errors.Wrap(err, "unable to build endpoint for effects request") } url := fmt.Sprintf("%s%s", client.fixHorizonURL(), endpoint) return client.stream(ctx, url, func(data []byte) error { - var effect effects.Base - err = json.Unmarshal(data, &effect) + var baseEffect effects.Base + // unmarshal into the base effect type + if err = json.Unmarshal(data, &baseEffect); err != nil { + return errors.Wrap(err, "error unmarshaling data for effects request") + } + + // unmarshal into the concrete effect type + effs, err := effects.UnmarshalEffect(baseEffect.GetType(), data) if err != nil { - return errors.Wrap(err, "error unmarshaling data") + return errors.Wrap(err, "unmarshaling to the correct effect type") } - handler(effect) + + handler(effs) return nil }) } diff --git a/clients/horizonclient/effect_request_test.go b/clients/horizonclient/effect_request_test.go index 4e51f24e1a..f6fb09e098 100644 --- a/clients/horizonclient/effect_request_test.go +++ b/clients/horizonclient/effect_request_test.go @@ -76,7 +76,7 @@ func ExampleClient_StreamEffects() { cancel() }() - printHandler := func(e effects.Base) { + printHandler := func(e effects.Effect) { fmt.Println(e) } err := client.StreamEffects(ctx, effectRequest, printHandler) @@ -100,14 +100,14 @@ func TestEffectRequestStreamEffects(t *testing.T) { "https://localhost/effects?cursor=now", ).ReturnString(200, effectStreamResponse) - effectStream := make([]effects.Base, 1) - err := client.StreamEffects(ctx, effectRequest, func(effect effects.Base) { + effectStream := make([]effects.Effect, 1) + err := client.StreamEffects(ctx, effectRequest, func(effect effects.Effect) { effectStream[0] = effect cancel() }) if assert.NoError(t, err) { - assert.Equal(t, effectStream[0].Type, "account_credited") + assert.Equal(t, effectStream[0].GetType(), "account_credited") } // Account effects @@ -119,13 +119,13 @@ func TestEffectRequestStreamEffects(t *testing.T) { "https://localhost/accounts/GBNZN27NAOHRJRCMHQF2ZN2F6TAPVEWKJIGZIRNKIADWIS2HDENIS6CI/effects?cursor=now", ).ReturnString(200, effectStreamResponse) - err = client.StreamEffects(ctx, effectRequest, func(effect effects.Base) { + err = client.StreamEffects(ctx, effectRequest, func(effect effects.Effect) { effectStream[0] = effect cancel() }) if assert.NoError(t, err) { - assert.Equal(t, effectStream[0].Account, "GBNZN27NAOHRJRCMHQF2ZN2F6TAPVEWKJIGZIRNKIADWIS2HDENIS6CI") + assert.Equal(t, effectStream[0].GetAccount(), "GBNZN27NAOHRJRCMHQF2ZN2F6TAPVEWKJIGZIRNKIADWIS2HDENIS6CI") } // test error @@ -137,7 +137,7 @@ func TestEffectRequestStreamEffects(t *testing.T) { "https://localhost/effects?cursor=now", ).ReturnString(500, effectStreamResponse) - err = client.StreamEffects(ctx, effectRequest, func(effect effects.Base) { + err = client.StreamEffects(ctx, effectRequest, func(effect effects.Effect) { effectStream[0] = effect cancel() }) diff --git a/clients/horizonclient/main.go b/clients/horizonclient/main.go index 891980b7c9..ab3699ef8d 100644 --- a/clients/horizonclient/main.go +++ b/clients/horizonclient/main.go @@ -18,6 +18,7 @@ import ( "time" hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/protocols/horizon/effects" "github.com/stellar/go/protocols/horizon/operations" "github.com/stellar/go/support/render/problem" "github.com/stellar/go/txnbuild" @@ -108,13 +109,20 @@ type HTTP interface { PostForm(url string, data url.Values) (resp *http.Response, err error) } -// Client struct contains data for creating a horizon client that connects to the stellar network +// Client struct contains data for creating a horizon client that connects to the stellar network. type Client struct { - HorizonURL string - HTTP HTTP - horizonTimeOut time.Duration - AppName string + // URL of Horizon server to connect + HorizonURL string + + // HTTP client to make requests with + HTTP HTTP + + // AppName is the name of the application using the horizonclient package + AppName string + + // AppVersion is the version of the application using the horizonclient package AppVersion string + horizonTimeOut time.Duration isTestNet bool } @@ -122,12 +130,11 @@ type Client struct { type ClientInterface interface { AccountDetail(request AccountRequest) (hProtocol.Account, error) AccountData(request AccountRequest) (hProtocol.AccountData, error) - Effects(request EffectRequest) (hProtocol.EffectsPage, error) + Effects(request EffectRequest) (effects.EffectsPage, error) Assets(request AssetRequest) (hProtocol.AssetsPage, error) Ledgers(request LedgerRequest) (hProtocol.LedgersPage, error) LedgerDetail(sequence uint32) (hProtocol.Ledger, error) Metrics() (hProtocol.Metrics, error) - Stream(ctx context.Context, request StreamRequest, handler func(interface{})) error FeeStats() (hProtocol.FeeStats, error) Offers(request OfferRequest) (hProtocol.OffersPage, error) Operations(request OperationRequest) (operations.OperationsPage, error) @@ -141,7 +148,7 @@ type ClientInterface interface { Payments(request OperationRequest) (operations.OperationsPage, error) TradeAggregations(request TradeAggregationRequest) (hProtocol.TradeAggregationsPage, error) Trades(request TradeRequest) (hProtocol.TradesPage, error) - Fund(addr string) (*http.Response, error) + Fund(addr string) (hProtocol.TransactionSuccess, error) StreamTransactions(ctx context.Context, request TransactionRequest, handler TransactionHandler) error StreamTrades(ctx context.Context, request TradeRequest, handler TradeHandler) error StreamEffects(ctx context.Context, request EffectRequest, handler EffectHandler) error @@ -153,7 +160,7 @@ type ClientInterface interface { Root() (hProtocol.Root, error) } -// DefaultTestNetClient is a default client to connect to test network +// DefaultTestNetClient is a default client to connect to test network. var DefaultTestNetClient = &Client{ HorizonURL: "https://horizon-testnet.stellar.org/", HTTP: http.DefaultClient, @@ -161,32 +168,30 @@ var DefaultTestNetClient = &Client{ isTestNet: true, } -// DefaultPublicNetClient is a default client to connect to public network +// DefaultPublicNetClient is a default client to connect to public network. var DefaultPublicNetClient = &Client{ HorizonURL: "https://horizon.stellar.org/", HTTP: http.DefaultClient, horizonTimeOut: HorizonTimeOut, } -// HorizonRequest contains methods implemented by request structs for horizon endpoints +// HorizonRequest contains methods implemented by request structs for horizon endpoints. type HorizonRequest interface { BuildURL() (string, error) } -// StreamRequest contains methods implemented by request structs for endpoints that support streaming -type StreamRequest interface { - Stream(ctx context.Context, client *Client, handler func(interface{})) error -} - -// AccountRequest struct contains data for making requests to the accounts endpoint of a horizon server +// AccountRequest struct contains data for making requests to the accounts endpoint of a horizon server. +// "AccountID" and "DataKey" fields should both be set when retrieving AccountData. +// When getting the AccountDetail, only "AccountID" needs to be set. type AccountRequest struct { AccountID string DataKey string } // EffectRequest struct contains data for getting effects from a horizon server. -// ForAccount, ForLedger, ForOperation and ForTransaction: Not more than one of these can be set at a time. If none are set, the default is to return all effects. -// The query parameters (Order, Cursor and Limit) can all be set at the same time +// "ForAccount", "ForLedger", "ForOperation" and "ForTransaction": Not more than one of these +// can be set at a time. If none are set, the default is to return all effects. +// The query parameters (Order, Cursor and Limit) are optional. All or none can be set. type EffectRequest struct { ForAccount string ForLedger string @@ -198,6 +203,8 @@ type EffectRequest struct { } // AssetRequest struct contains data for getting asset details from a horizon server. +// If "ForAssetCode" and "ForAssetIssuer" are not set, it returns all assets. +// The query parameters (Order, Cursor and Limit) are optional. All or none can be set. type AssetRequest struct { ForAssetCode string ForAssetIssuer string @@ -207,6 +214,7 @@ type AssetRequest struct { } // LedgerRequest struct contains data for getting ledger details from a horizon server. +// The query parameters (Order, Cursor and Limit) are optional. All or none can be set. type LedgerRequest struct { Order Order Cursor string @@ -222,7 +230,9 @@ type feeStatsRequest struct { endpoint string } -// OfferRequest struct contains data for getting offers made by an account from a horizon server +// OfferRequest struct contains data for getting offers made by an account from a horizon server. +// "ForAccount" is required. +// The query parameters (Order, Cursor and Limit) are optional. All or none can be set. type OfferRequest struct { ForAccount string Order Order @@ -230,7 +240,10 @@ type OfferRequest struct { Limit uint } -// OperationRequest struct contains data for getting operation details from a horizon servers +// OperationRequest struct contains data for getting operation details from a horizon server. +// "ForAccount", "ForLedger", "ForTransaction": Only one of these can be set at a time. If none +// are provided, the default is to return all operations. +// The query parameters (Order, Cursor, Limit and IncludeFailed) are optional. All or none can be set. type OperationRequest struct { ForAccount string ForLedger uint @@ -248,7 +261,10 @@ type submitRequest struct { transactionXdr string } -// TransactionRequest struct contains data for getting transaction details from a horizon server +// TransactionRequest struct contains data for getting transaction details from a horizon server. +// "ForAccount", "ForLedger": Only one of these can be set at a time. If none are provided, the +// default is to return all transactions. +// The query parameters (Order, Cursor, Limit and IncludeFailed) are optional. All or none can be set. type TransactionRequest struct { ForAccount string ForLedger uint @@ -259,7 +275,8 @@ type TransactionRequest struct { IncludeFailed bool } -// OrderBookRequest struct contains data for getting the orderbook for an asset pair from a horizon server +// OrderBookRequest struct contains data for getting the orderbook for an asset pair from a horizon server. +// Limit is optional. All other parameters are required. type OrderBookRequest struct { SellingAssetType AssetType SellingAssetCode string @@ -270,7 +287,8 @@ type OrderBookRequest struct { Limit uint } -// PathsRequest struct contains data for getting available payment paths from a horizon server +// PathsRequest struct contains data for getting available payment paths from a horizon server. +// All parameters are required. type PathsRequest struct { DestinationAccount string DestinationAssetType AssetType @@ -280,7 +298,10 @@ type PathsRequest struct { SourceAccount string } -// TradeRequest struct contains data for getting trade details from a horizon server +// TradeRequest struct contains data for getting trade details from a horizon server. +// "ForAccount", "ForOfferID": Only one of these can be set at a time. If none are provided, the +// default is to return all trades. +// All other query parameters are optional. All or none can be set. type TradeRequest struct { ForOfferID string ForAccount string @@ -295,7 +316,9 @@ type TradeRequest struct { Limit uint } -// TradeAggregationRequest struct contains data for getting trade aggregations from a horizon server +// TradeAggregationRequest struct contains data for getting trade aggregations from a horizon server. +// The query parameters (Order and Limit) are optional. All or none can be set. +// All other parameters are required. type TradeAggregationRequest struct { StartTime time.Time EndTime time.Time diff --git a/clients/horizonclient/main_test.go b/clients/horizonclient/main_test.go index 6505a6028b..46cf3fd0f8 100644 --- a/clients/horizonclient/main_test.go +++ b/clients/horizonclient/main_test.go @@ -2,12 +2,14 @@ package horizonclient import ( "fmt" + "net/http" "testing" "time" "github.com/stellar/go/txnbuild" hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/protocols/horizon/effects" "github.com/stellar/go/protocols/horizon/operations" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/http/httptest" @@ -47,7 +49,14 @@ func ExampleClient_Effects() { fmt.Println(err) return } - fmt.Print(effect) + records := effect.Embedded.Records + if records[0].GetType() == "account_created" { + acc, ok := records[0].(effects.AccountCreated) + if ok { + fmt.Print(acc.Account) + fmt.Print(acc.StartingBalance) + } + } } func ExampleClient_Assets() { @@ -282,6 +291,86 @@ func ExampleClient_Payments() { } } +func ExampleClient_Fund() { + client := DefaultTestNetClient + // fund an account + resp, err := client.Fund("GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU") + if err != nil { + fmt.Println(err) + return + } + fmt.Print(resp) +} + +func TestFixHTTP(t *testing.T) { + client := &Client{ + HorizonURL: "https://localhost/", + } + // No HTTP client is provided + assert.Nil(t, client.HTTP, "client HTTP is nil") + client.Root() + // When a request is made, default HTTP client is set + assert.IsType(t, client.HTTP, &http.Client{}) +} + +func TestClientFund(t *testing.T) { + hmock := httptest.NewClient() + client := &Client{ + HorizonURL: "https://localhost/", + HTTP: hmock, + } + + testAccount := "GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU" + + // not testnet + hmock.On( + "GET", + "https://localhost/friendbot?addr=GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU", + ).ReturnString(200, txSuccess) + + _, err := client.Fund(testAccount) + // error case: not testnet + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "can't fund account from friendbot on production network") + } + + // happy path + hmock.On( + "GET", + "https://localhost/friendbot?addr=GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU", + ).ReturnString(200, txSuccess) + + client.isTestNet = true + resp, err := client.Fund(testAccount) + + if assert.NoError(t, err) { + assert.IsType(t, resp, hProtocol.TransactionSuccess{}) + assert.Equal(t, resp.Links.Transaction.Href, "https://horizon-testnet.stellar.org/transactions/bcc7a97264dca0a51a63f7ea971b5e7458e334489673078bb2a34eb0cce910ca") + assert.Equal(t, resp.Hash, "bcc7a97264dca0a51a63f7ea971b5e7458e334489673078bb2a34eb0cce910ca") + assert.Equal(t, resp.Ledger, int32(354811)) + assert.Equal(t, resp.Env, `AAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAZAAABD0AAuV/AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAyTBGxOgfSApppsTnb/YRr6gOR8WT0LZNrhLh4y3FCgoAAAAXSHboAAAAAAAAAAABhlbgnAAAAEAivKe977CQCxMOKTuj+cWTFqc2OOJU8qGr9afrgu2zDmQaX5Q0cNshc3PiBwe0qw/+D/qJk5QqM5dYeSUGeDQP`) + assert.Equal(t, resp.Result, "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=") + assert.Equal(t, resp.Meta, `AAAAAQAAAAIAAAADAAVp+wAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwACBP/TuycHAAABD0AAuV+AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAVp+wAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwACBP/TuycHAAABD0AAuV/AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAMABWn7AAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAAIE/9O7JwcAAAEPQAC5X8AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEABWn7AAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAAIE+gGdbQcAAAEPQAC5X8AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAABWn7AAAAAAAAAADJMEbE6B9ICmmmxOdv9hGvqA5HxZPQtk2uEuHjLcUKCgAAABdIdugAAAVp+wAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==`) + } + + // failure response + hmock.On( + "GET", + "https://localhost/friendbot?addr=GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU", + ).ReturnString(400, transactionFailure) + + _, err = client.Fund(testAccount) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "horizon error") + horizonError, ok := errors.Cause(err).(*Error) + assert.Equal(t, ok, true) + assert.Equal(t, horizonError.Problem.Title, "Transaction Failed") + resultString, err := horizonError.ResultString() + assert.Nil(t, err) + assert.Equal(t, resultString, "AAAAAAAAAAD////4AAAAAA==") + } +} + func TestAccountDetail(t *testing.T) { hmock := httptest.NewClient() client := &Client{ @@ -427,10 +516,30 @@ func TestEffectsRequest(t *testing.T) { "https://localhost/effects", ).ReturnString(200, effectsResponse) - effects, err := client.Effects(effectRequest) + effs, err := client.Effects(effectRequest) if assert.NoError(t, err) { - assert.IsType(t, effects, hProtocol.EffectsPage{}) + assert.IsType(t, effs, effects.EffectsPage{}) + links := effs.Links + assert.Equal(t, links.Self.Href, "https://horizon-testnet.stellar.org/operations/43989725060534273/effects?cursor=&limit=10&order=asc") + + assert.Equal(t, links.Next.Href, "https://horizon-testnet.stellar.org/operations/43989725060534273/effects?cursor=43989725060534273-3&limit=10&order=asc") + assert.Equal(t, links.Prev.Href, "https://horizon-testnet.stellar.org/operations/43989725060534273/effects?cursor=43989725060534273-1&limit=10&order=desc") + + adEffect := effs.Embedded.Records[0] + acEffect := effs.Embedded.Records[1] + arEffect := effs.Embedded.Records[2] + assert.IsType(t, adEffect, effects.AccountDebited{}) + assert.IsType(t, acEffect, effects.AccountCredited{}) + // account_removed effect does not have a struct. Defaults to effects.Base + assert.IsType(t, arEffect, effects.Base{}) + + c, ok := acEffect.(effects.AccountCredited) + assert.Equal(t, ok, true) + assert.Equal(t, c.ID, "0043989725060534273-0000000002") + assert.Equal(t, c.Amount, "9999.9999900") + assert.Equal(t, c.Account, "GBO7LQUWCC7M237TU2PAXVPOLLYNHYCYYFCLVMX3RBJCML4WA742X3UB") + assert.Equal(t, c.Asset.Type, "native") } effectRequest = EffectRequest{ForAccount: "GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU"} @@ -439,9 +548,9 @@ func TestEffectsRequest(t *testing.T) { "https://localhost/accounts/GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU/effects", ).ReturnString(200, effectsResponse) - effects, err = client.Effects(effectRequest) + effs, err = client.Effects(effectRequest) if assert.NoError(t, err) { - assert.IsType(t, effects, hProtocol.EffectsPage{}) + assert.IsType(t, effs, effects.EffectsPage{}) } // too many parameters @@ -456,7 +565,6 @@ func TestEffectsRequest(t *testing.T) { if assert.Error(t, err) { assert.Contains(t, err.Error(), "too many parameters") } - } func TestAssetsRequest(t *testing.T) { diff --git a/clients/horizonclient/mocks.go b/clients/horizonclient/mocks.go index 736a498556..88a2c60671 100644 --- a/clients/horizonclient/mocks.go +++ b/clients/horizonclient/mocks.go @@ -2,9 +2,9 @@ package horizonclient import ( "context" - "net/http" hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/protocols/horizon/effects" "github.com/stellar/go/protocols/horizon/operations" "github.com/stellar/go/txnbuild" "github.com/stretchr/testify/mock" @@ -28,9 +28,9 @@ func (m *MockClient) AccountData(request AccountRequest) (hProtocol.AccountData, } // Effects is a mocking method -func (m *MockClient) Effects(request EffectRequest) (hProtocol.EffectsPage, error) { +func (m *MockClient) Effects(request EffectRequest) (effects.EffectsPage, error) { a := m.Called(request) - return a.Get(0).(hProtocol.EffectsPage), a.Error(1) + return a.Get(0).(effects.EffectsPage), a.Error(1) } // Assets is a mocking method @@ -39,15 +39,6 @@ func (m *MockClient) Assets(request AssetRequest) (hProtocol.AssetsPage, error) return a.Get(0).(hProtocol.AssetsPage), a.Error(1) } -// Stream is a mocking method -func (m *MockClient) Stream(ctx context.Context, - request StreamRequest, - handler func(interface{}), -) error { - a := m.Called(ctx, request, handler) - return a.Error(0) -} - // Ledgers is a mocking method func (m *MockClient) Ledgers(request LedgerRequest) (hProtocol.LedgersPage, error) { a := m.Called(request) @@ -145,9 +136,9 @@ func (m *MockClient) Trades(request TradeRequest) (hProtocol.TradesPage, error) } // Fund is a mocking method -func (m *MockClient) Fund(addr string) (*http.Response, error) { +func (m *MockClient) Fund(addr string) (hProtocol.TransactionSuccess, error) { a := m.Called(addr) - return nil, a.Error(1) + return a.Get(0).(hProtocol.TransactionSuccess), a.Error(1) } // StreamTransactions is a mocking method diff --git a/clients/horizonclient/version.go b/clients/horizonclient/version.go new file mode 100644 index 0000000000..862c1174d3 --- /dev/null +++ b/clients/horizonclient/version.go @@ -0,0 +1,5 @@ +package horizonclient + +// version is the current version of the horizonclient. +// This is updated for every release. +const version = "1.1" diff --git a/services/keystore/attachments/2019-04-24-keystore-auth-flows.png b/exp/services/keystore/attachments/2019-04-24-keystore-auth-flows.png similarity index 100% rename from services/keystore/attachments/2019-04-24-keystore-auth-flows.png rename to exp/services/keystore/attachments/2019-04-24-keystore-auth-flows.png diff --git a/services/keystore/spec.md b/exp/services/keystore/spec.md similarity index 87% rename from services/keystore/spec.md rename to exp/services/keystore/spec.md index 0d3802b4f8..a6a738a513 100644 --- a/services/keystore/spec.md +++ b/exp/services/keystore/spec.md @@ -65,7 +65,7 @@ interface EncryptedKeyData { Store Keys Request: ```typescript -interface StoreKeyRequest { +interface StoreKeysRequest { encryptedKeys: EncryptedKey[]; } ``` @@ -73,7 +73,7 @@ interface StoreKeyRequest { Store Keys Response: ```typescript -interface StoreKeyResponse { +interface StoreKeysResponse { encryptedKeys: EncryptedKeyData[]; } ``` @@ -144,6 +144,35 @@ TBD ``` +### /update-keys + +Update Keys Request: + +```typescript +interface UpdateKeysRequest { + encryptedKeys: EncryptedKey[]; +} +``` + +Update Keys Response: + +```typescript +interface UpdateKeysResponse { + encryptedKeys: EncryptedKeyData[]; +} +``` +
Errors + +TBD +```json +{ + "code": "some error code", + "message": "some error message", + "retriable": false, +} +``` +
+ ### /remove-key Remove Key Request: diff --git a/exp/ticker/cmd/generate.go b/exp/ticker/cmd/generate.go index b58c3c24d3..d5c8757c54 100644 --- a/exp/ticker/cmd/generate.go +++ b/exp/ticker/cmd/generate.go @@ -76,7 +76,7 @@ var cmdGenerateAssetData = &cobra.Command{ Logger.Infof("Starting asset data generation, outputting to: %s\n", AssetsOutFile) err = ticker.GenerateAssetsFile(&session, Logger, AssetsOutFile) if err != nil { - Logger.Fatal("could not generate market data:", err) + Logger.Fatal("could not generate asset data:", err) } }, } diff --git a/exp/ticker/cmd/ingest.go b/exp/ticker/cmd/ingest.go index 8f9190d112..618678108d 100644 --- a/exp/ticker/cmd/ingest.go +++ b/exp/ticker/cmd/ingest.go @@ -16,6 +16,7 @@ func init() { rootCmd.AddCommand(cmdIngest) cmdIngest.AddCommand(cmdIngestAssets) cmdIngest.AddCommand(cmdIngestTrades) + cmdIngest.AddCommand(cmdIngestOrderbooks) cmdIngestTrades.Flags().BoolVar( &ShouldStream, @@ -96,3 +97,26 @@ var cmdIngestTrades = &cobra.Command{ } }, } + +var cmdIngestOrderbooks = &cobra.Command{ + Use: "orderbooks", + Short: "Refreshes the orderbook stats database with new data retrieved from Horizon.", + Run: func(cmd *cobra.Command, args []string) { + Logger.Info("Refreshing the asset database") + dbInfo, err := pq.ParseURL(DatabaseURL) + if err != nil { + Logger.Fatal("could not parse db-url:", err) + } + + session, err := tickerdb.CreateSession("postgres", dbInfo) + if err != nil { + Logger.Fatal("could not connect to db:", err) + } + defer session.DB.Close() + + err = ticker.RefreshOrderbookEntries(&session, Client, Logger) + if err != nil { + Logger.Fatal("could not refresh error database:", err) + } + }, +} diff --git a/exp/ticker/docker/conf/crontab.txt b/exp/ticker/docker/conf/crontab.txt index be442ec159..8a1481f158 100644 --- a/exp/ticker/docker/conf/crontab.txt +++ b/exp/ticker/docker/conf/crontab.txt @@ -4,6 +4,9 @@ # Refresh the database of assets, hourly: @hourly /opt/stellar/bin/ticker ingest assets --db-url=postgres://127.0.0.1:5432/ticker > /home/stellar/last-ingest-assets.log 2>&1 +# Refresh the database of orderbooks, every 10 minutes: +*/10 * * * * /opt/stellar/bin/ticker ingest orderbooks --db-url=postgres://127.0.0.1:5432/ticker > /home/stellar/last-ingest-orderbooks.log 2>&1 + # Backfill the database of trades (including possible new assets), every 6 hours: 0 */6 * * * /opt/stellar/bin/ticker ingest trades --db-url=postgres://127.0.0.1:5432/ticker > /home/stellar/last-ingest-trades.log 2>&1 diff --git a/exp/ticker/docker/setup b/exp/ticker/docker/setup index 576816f052..acef2e9fab 100644 --- a/exp/ticker/docker/setup +++ b/exp/ticker/docker/setup @@ -10,7 +10,7 @@ mkdir -p /opt/stellar/www chown -R stellar:stellar /opt/stellar/www mkdir -p /opt/stellar/postgresql/data -wget -O ticker.tar.gz https://github.com/accordeiro/ticker-releases/releases/download/v0.4-alpha/ticker.tar.gz +wget -O ticker.tar.gz https://github.com/accordeiro/ticker-releases/releases/download/v0.5.3-alpha/ticker.tar.gz tar -xvzf ticker.tar.gz mv ticker /opt/stellar/bin/ticker chmod +x /opt/stellar/bin/ticker diff --git a/exp/ticker/docker/start b/exp/ticker/docker/start index 03cbd7402a..efbb0edde6 100644 --- a/exp/ticker/docker/start +++ b/exp/ticker/docker/start @@ -28,6 +28,7 @@ function main() { populate_assets populate_trades + populate_orderbooks generate_assets_file generate_markets_file @@ -70,6 +71,19 @@ function populate_trades() { } +function populate_orderbooks() { + if [ -f $PGHOME/.orderbooks-populated ]; then + echo "ticker: orderbooks already pre-populated" + return 0 + fi + echo "" + echo "Populating initial orderbook database" + echo "" + sudo -u stellar $STELLAR_BIN/ticker ingest orderbooks --db-url=$PGURL + touch $PGHOME/.orderbooks-populated +} + + function generate_assets_file() { if [ -f $STELLAR_HOME/www/assets.json ]; then echo "ticker: assets.json already pre-populated" diff --git a/exp/ticker/internal/actions_market.go b/exp/ticker/internal/actions_market.go index f5bc8c5106..ee892d1627 100644 --- a/exp/ticker/internal/actions_market.go +++ b/exp/ticker/internal/actions_market.go @@ -59,6 +59,8 @@ func GenerateMarketSummary(s *tickerdb.TickerSession) (ms MarketSummary, err err func dbMarketToMarketStats(m tickerdb.Market) MarketStats { closeTime := utils.TimeToUnixEpoch(m.LastPriceCloseTime) + + spread, spreadMidPoint := utils.CalcSpread(m.HighestBid, m.LowestAsk) return MarketStats{ TradePairName: m.TradePair, BaseVolume24h: m.BaseVolume24h, @@ -77,6 +79,14 @@ func dbMarketToMarketStats(m tickerdb.Market) MarketStats { Change7d: m.PriceChange7d, Price: m.LastPrice, Close: m.LastPrice, + BidCount: m.NumBids, + BidVolume: m.BidVolume, + BidMax: m.HighestBid, + AskCount: m.NumAsks, + AskVolume: m.AskVolume, + AskMin: m.LowestAsk, + Spread: spread, + SpreadMidPoint: spreadMidPoint, CloseTime: closeTime, } } diff --git a/exp/ticker/internal/actions_orderbook.go b/exp/ticker/internal/actions_orderbook.go new file mode 100644 index 0000000000..3ca66de306 --- /dev/null +++ b/exp/ticker/internal/actions_orderbook.go @@ -0,0 +1,65 @@ +package ticker + +import ( + "time" + + horizonclient "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/exp/ticker/internal/scraper" + "github.com/stellar/go/exp/ticker/internal/tickerdb" + "github.com/stellar/go/support/errors" + hlog "github.com/stellar/go/support/log" +) + +// RefreshOrderbookEntries updates the orderbook entries for the relevant markets that were active +// in the past 7-day interval +func RefreshOrderbookEntries(s *tickerdb.TickerSession, c *horizonclient.Client, l *hlog.Entry) error { + sc := scraper.ScraperConfig{ + Client: c, + Logger: l, + } + + // Retrieve relevant markets for the past 7 days (168 hours): + mkts, err := s.Retrieve7DRelevantMarkets() + if err != nil { + return errors.Wrap(err, "could not retrieve partial markets") + } + + for _, mkt := range mkts { + ob, err := sc.FetchOrderbookForAssets( + mkt.BaseAssetType, + mkt.BaseAssetCode, + mkt.BaseAssetIssuer, + mkt.CounterAssetType, + mkt.CounterAssetCode, + mkt.CounterAssetIssuer, + ) + if err != nil { + l.Error(errors.Wrap(err, "could not fetch orderbook for assets")) + continue + } + + dbOS := orderbookStatsToDBOrderbookStats(ob, mkt.BaseAssetID, mkt.CounterAssetID) + err = s.InsertOrUpdateOrderbookStats(&dbOS, []string{"base_asset_id", "counter_asset_id"}) + if err != nil { + l.Error(errors.Wrap(err, "could not insert orderbook stats into db")) + } + } + + return nil +} + +func orderbookStatsToDBOrderbookStats(os scraper.OrderbookStats, bID, cID int32) tickerdb.OrderbookStats { + return tickerdb.OrderbookStats{ + BaseAssetID: bID, + CounterAssetID: cID, + NumBids: os.NumBids, + BidVolume: os.BidVolume, + HighestBid: os.HighestBid, + NumAsks: os.NumAsks, + AskVolume: os.AskVolume, + LowestAsk: os.LowestAsk, + Spread: os.Spread, + SpreadMidPoint: os.SpreadMidPoint, + UpdatedAt: time.Now(), + } +} diff --git a/exp/ticker/internal/gql/main.go b/exp/ticker/internal/gql/main.go index c538286922..eb600114a1 100644 --- a/exp/ticker/internal/gql/main.go +++ b/exp/ticker/internal/gql/main.go @@ -38,6 +38,7 @@ type asset struct { Countries string Status string IssuerID int32 + OrderbookStats orderbookStats } // partialMarket represents the aggregated market data for a @@ -58,6 +59,20 @@ type partialMarket struct { Close float64 IntervalStart graphql.Time FirstLedgerCloseTime graphql.Time + OrderbookStats orderbookStats +} + +// orderbookStats represents the orderbook stats for a +// specific pair of assets (aggregated or not) +type orderbookStats struct { + BidCount BigInt + BidVolume float64 + BidMax float64 + AskCount BigInt + AskVolume float64 + AskMin float64 + Spread float64 + SpreadMidPoint float64 } type resolver struct { diff --git a/exp/ticker/internal/gql/resolvers_market.go b/exp/ticker/internal/gql/resolvers_market.go index ba666056a8..866ee555da 100644 --- a/exp/ticker/internal/gql/resolvers_market.go +++ b/exp/ticker/internal/gql/resolvers_market.go @@ -5,6 +5,7 @@ import ( "github.com/graph-gophers/graphql-go" "github.com/stellar/go/exp/ticker/internal/tickerdb" + "github.com/stellar/go/exp/ticker/internal/utils" ) // Markets resolves the markets() GraphQL query. @@ -83,6 +84,18 @@ func validateNumHoursAgo(n *int32) (int, error) { // dbMarketToPartialMarket converts a tickerdb.PartialMarket to a *partialMarket func dbMarketToPartialMarket(dbMarket tickerdb.PartialMarket) *partialMarket { + spread, spreadMidPoint := utils.CalcSpread(dbMarket.HighestBid, dbMarket.LowestAsk) + os := orderbookStats{ + BidCount: BigInt(dbMarket.NumBids), + BidVolume: dbMarket.BidVolume, + BidMax: dbMarket.HighestBid, + AskCount: BigInt(dbMarket.NumAsks), + AskVolume: dbMarket.AskVolume, + AskMin: dbMarket.LowestAsk, + Spread: spread, + SpreadMidPoint: spreadMidPoint, + } + return &partialMarket{ TradePair: dbMarket.TradePairName, BaseAssetCode: dbMarket.BaseAssetCode, @@ -99,5 +112,6 @@ func dbMarketToPartialMarket(dbMarket tickerdb.PartialMarket) *partialMarket { Close: dbMarket.Close, IntervalStart: graphql.Time{Time: dbMarket.IntervalStart}, FirstLedgerCloseTime: graphql.Time{Time: dbMarket.FirstLedgerCloseTime}, + OrderbookStats: os, } } diff --git a/exp/ticker/internal/gql/static/bindata.go b/exp/ticker/internal/gql/static/bindata.go index 988efc76d9..a593582b36 100644 --- a/exp/ticker/internal/gql/static/bindata.go +++ b/exp/ticker/internal/gql/static/bindata.go @@ -116,7 +116,7 @@ func bindataGraphiqlhtml() (*asset, error) { size: 1182, md5checksum: "", mode: os.FileMode(420), - modTime: time.Unix(1556028957, 0), + modTime: time.Unix(1556057220, 0), } a := &asset{bytes: bytes, info: info} @@ -125,33 +125,36 @@ func bindataGraphiqlhtml() (*asset, error) { } var _bindataSchemagql = []byte( - "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x54\x4d\x6f\xe3\x36\x10\x3d\x4b\xbf\x62\x82\xbd\x24\x17\x1f\x8a\x9e" + - "\x84\xb6\x80\x93\xb4\x68\xd0\xec\xa2\x5d\x67\x8b\x02\x41\x51\x8c\xc5\xb1\x44\x84\x1f\xda\xe1\xd0\x59\xa3\xc8\x7f" + - "\x2f\x48\xc9\x59\xca\x4a\xdd\x73\x4f\x12\xdf\xcc\x1b\x72\x1e\xdf\x30\xb4\x3d\x59\x84\xbf\xeb\xea\x73\x24\x3e\x34" + - "\x50\xfd\x96\xbe\xf5\x4b\x5d\xcb\x61\x20\xc8\xab\x14\x7e\x07\x4c\xc2\x9a\xf6\x04\x68\x0c\xec\xd1\x68\x85\x42\x0a" + - "\x30\x04\x92\x00\xde\x81\xf4\x04\x1b\x21\x63\x90\xc1\x91\x3c\x7b\x7e\x5a\xd5\xd5\x18\x6f\xe0\x71\x9d\x7e\x2e\xfe" + - "\xbc\xa8\xcf\x14\xd3\x21\x44\xe2\x33\xd5\xa6\x84\x06\x1e\xef\xf2\xdf\xa2\x9e\x30\x2a\x82\x20\x28\x01\x76\xec\x6d" + - "\xae\x63\x30\x08\x7c\xe7\xa2\xfd\xd9\x47\x0e\xeb\xce\xff\x00\x7d\xfa\x4b\xcc\x4b\x45\x3b\x8c\x46\xe0\x7b\xf8\xe6" + - "\xdb\x11\xbe\x5a\x81\x1f\x44\x7b\x87\xc6\x1c\x60\x60\xbf\xd7\x8a\xa0\xf5\xd1\x09\x31\xa0\x53\x89\xb7\xc5\x40\x63" + - "\xf3\xa0\xdd\xce\xc3\xce\x33\xec\xb4\x11\x62\xed\xba\x55\x5d\x59\xe4\x27\x92\x70\x59\x57\x55\x4a\xcd\xdd\xdf\x78" + - "\x45\x0d\x6c\x24\xa5\x94\xf8\xd8\x4b\x11\x99\xf6\x7a\x8b\x54\x86\x16\xbc\xa2\xc5\x06\xee\x9c\xd4\xd5\x55\x03\x8f" + - "\xef\xf3\x51\x16\xca\x77\x1d\x53\x97\x65\x9f\x89\xe6\xf9\x5f\x34\x4b\xec\xac\xcf\x9b\xf2\x24\x8e\x43\x4b\xe0\x77" + - "\xf9\x7f\xac\x39\xa0\x66\xb8\xa4\x55\x52\xe4\x1d\xfc\x71\xff\xfe\xaf\xeb\x87\x9b\xab\xb9\x58\xc0\x14\xa2\x91\xb0" + - "\xaa\x2b\xd1\xed\x13\x71\xd2\x2c\x11\x3f\xa0\xa5\xff\x6c\x6e\xfd\xda\xc6\x6b\x9b\x2f\x75\x1d\x5a\x4c\xc6\xb9\xd6" + - "\x5d\x4a\x9c\x56\x0f\xda\xd2\xe4\xeb\x2c\x5f\xf2\x75\x5b\xa8\x7b\x71\xf4\xd7\xba\xcd\x2a\x17\x78\x22\x15\x4b\x17" + - "\xed\x94\x13\xf2\x51\x2e\xea\x0a\xa3\xf4\x1f\xe9\x73\xd4\x4c\xaa\x81\x6b\xef\x0d\xa1\x7b\xc5\xf7\xbe\xc5\xad\xa1" + - "\x59\xc0\x8e\x7b\xfc\x64\x3c\xe6\x02\xe3\x65\x3b\x61\x6f\x0c\xa9\xeb\xc3\xad\xb7\xa8\xdd\x8c\xe2\xda\xde\x2f\x5d" + - "\x31\x8f\x3c\xcc\x8f\xaa\x43\x46\xd7\x39\x61\x7e\x34\xa5\xc3\x60\xf0\x70\x4b\xad\xb6\x68\x42\x33\xc9\x95\xfa\x2b" + - "\x94\x4f\x89\x14\xda\x62\xd9\x7a\xa7\x74\x32\x40\x28\xc0\x9d\xfe\x42\xea\x43\xb4\xdb\x64\xc8\xd7\x42\x16\xbf\x2c" + - "\x30\x1d\x3e\x39\xa3\xad\x96\xf9\x69\x98\x14\xd9\xec\xab\x3b\x17\x84\x63\x7b\xba\x43\xeb\x8d\x41\x21\x46\xb3\x56" + - "\x8a\x29\x04\x3a\x1b\xdd\xe8\xce\xa1\x44\x3e\xc9\x8a\x2e\xf9\xbf\xc4\x92\xef\x63\x58\x98\xe0\xee\x76\xba\xda\xe3" + - "\x5b\x38\xfa\x2b\x99\x26\x7b\xfb\x57\xd4\x5c\x90\xde\x1c\xf2\x12\x9f\x0f\xeb\xf1\x2c\x6f\x0c\xf9\x49\x68\xc1\x4b" + - "\x15\x7f\xf7\x26\xa6\x2b\x3a\x9a\x67\x22\x9c\xc2\xf9\xa0\x37\xa3\xcf\x46\xf1\xfd\x40\xee\x6b\xdc\xf8\xe7\xaf\x8b" + - "\x5e\x77\x7d\x51\xb1\x47\xd7\x95\x3b\x18\x1f\x8a\xa5\x4e\xdb\xed\xd1\x6c\x04\x59\x9a\x3c\x5a\xd9\x04\x1c\xe4\x9e" + - "\x54\x47\x7c\x93\xf2\x13\x7c\x0c\x1e\x65\x3c\x1d\xd8\x73\x82\xfe\x8f\xdb\x1c\xaf\x2d\x35\x37\xc4\xad\xd1\xed\x2f" + - "\x74\x28\x1f\x90\xf9\x80\x45\x36\xe5\x63\xe3\xad\xf9\xf4\xf1\xbe\x1c\x2e\x52\xc4\x98\x06\x62\x43\xbc\x9f\xb9\x21" + - "\xbd\x2f\x0b\x50\x18\x5d\xd8\x11\x2f\x02\xcf\xb4\x5d\x47\xe9\x7f\x74\x6a\xf0\x7a\xf6\xc2\x29\x1a\x7c\xd0\xb2\x60" + - "\x78\xee\x1e\x9e\xb5\x48\x09\xbe\xd4\xff\x04\x00\x00\xff\xff\x75\xd6\x43\x72\x38\x08\x00\x00") + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xdc\x54\x4f\x6f\xe3\xb6\x13\x3d\x4b\x9f\x62\x82\xbd\x24\x17\x1f\x7e\xf8" + + "\x9d\x84\xb6\x80\x93\xb4\x68\xd0\x78\xbb\x5d\x67\x8b\x02\x41\x51\x8c\xc5\xb1\x3c\x30\x45\x6a\x87\xa4\x13\x63\x91" + + "\xef\x5e\x90\x92\x1d\x4a\x4a\xd3\x7b\x4f\xd2\xfc\x79\xc3\x99\xc7\xc7\x71\xf5\x8e\x5a\x84\x6f\x65\xf1\x35\x90\x1c" + + "\x2b\x28\x7e\x8b\xdf\xf2\xa5\x2c\xfd\xb1\x23\x48\x56\x0c\x7f\x00\x21\x2f\x4c\x07\x02\xd4\x1a\x0e\xa8\x59\xa1\x27" + + "\x05\xe8\x1c\x79\x07\xd6\x80\xdf\x11\xac\x3d\x69\x8d\x02\x86\xfc\x93\x95\xfd\xa2\x2c\xfa\x78\x05\x8f\xcb\xf8\x73" + + "\xf1\xe7\x45\xf9\x4e\x31\x76\x2e\x90\xbc\x53\x6d\x48\xa8\xe0\xf1\x2e\xfd\xcd\xea\x79\x41\x45\xe0\x3c\x7a\x07\x5b" + + "\xb1\x6d\xaa\xa3\xd1\x79\xf8\xce\x84\xf6\x67\x1b\xc4\x2d\x1b\xfb\x03\xec\xe2\x5f\x44\x5e\x2a\xda\x62\xd0\x1e\xbe" + + "\x87\xff\xfd\xbf\x77\x5f\x2d\xc0\x76\x9e\xad\x41\xad\x8f\xd0\x89\x3d\xb0\x22\xa8\x6d\x30\x9e\x04\xd0\xa8\x88\xdb" + + "\xa0\xa3\x7e\x78\x60\xb3\xb5\xb0\xb5\x02\x5b\xd6\x9e\x84\x4d\xb3\x28\x8b\x16\x65\x4f\xde\x5d\x96\x45\x11\x53\xd3" + + "\xf4\x37\x56\x51\x05\x6b\x1f\x53\x72\x7f\x3f\x4b\x16\x19\xce\x7a\x0b\x94\x87\x66\xb8\x6c\xc4\x0a\xee\x8c\x2f\x8b" + + "\xab\x0a\x1e\x57\xa9\x95\x19\xf3\x4d\x23\xd4\x24\xda\x47\xa4\x59\xf9\x07\xce\x22\x3a\xf1\xf3\x26\x3d\x11\x63\xb0" + + "\x25\xb0\xdb\xf4\xdf\xd7\xec\x90\x05\x2e\x69\x11\x19\xf9\x00\x7f\xdc\xaf\xfe\xba\x7e\xb8\xb9\x1a\x93\x05\x42\x2e" + + "\x68\xef\x16\x65\xe1\xb9\xde\x93\x44\xce\x22\xf0\x23\xb6\xf4\xaf\xc3\x2d\xcf\x63\x9c\xc7\x7c\x29\x4b\x57\x63\x14" + + "\xce\x35\x37\x31\x71\xb0\x1e\xb8\xa5\x41\xd7\x89\xbe\xa8\xeb\x3a\x63\xf7\xe2\xa4\xaf\x65\x9d\x58\xce\xfc\x11\x94" + + "\x99\x26\xb4\x43\x8e\x4b\xad\x5c\x94\x05\x06\xbf\xfb\x4c\x5f\x03\x0b\xa9\x0a\xae\xad\xd5\x84\xe6\xec\x3f\xd8\x1a" + + "\x37\x9a\x46\x81\xb6\x3f\xe3\x27\x6d\x31\x15\xe8\x2f\xdb\x78\xb1\x5a\x93\xba\x3e\xde\xda\x16\xd9\x8c\x20\xa6\xde" + + "\xd9\xb9\x2a\xc6\x91\x87\x71\xab\xec\x92\x77\x99\x12\xc6\xad\x29\x76\x9d\xc6\xe3\x2d\xd5\xdc\xa2\x76\xd5\x40\x57" + + "\x9c\x2f\x63\x3e\x26\x92\xab\x33\xb3\xb6\x46\x71\x14\x80\xcb\x9c\x5b\x7e\x26\xf5\x31\xb4\x9b\x28\xc8\x73\xa1\x16" + + "\x9f\x67\x3e\x76\x5f\x8c\xe6\x96\xfd\xb8\x1b\x21\x45\x6d\xd2\xd5\x9d\x71\x5e\x42\x3d\x3d\xa1\xb6\x5a\xa3\x27\x41" + + "\xbd\x54\x4a\xc8\x39\x7a\x37\xba\xe6\xc6\xa0\x0f\x32\xc9\x0a\x26\xea\x3f\xf7\x45\xdd\x07\x37\x13\xc1\xdd\xed\x70" + + "\xb5\xa7\x5d\xd8\xeb\x2b\x8a\x26\x69\xfb\x13\xb2\x64\xa0\x37\x1f\x79\xee\x1f\x3f\xd6\x53\x2f\x6f\x3c\xf2\x49\x68" + + "\x86\x8b\x15\x7f\xb7\x3a\xc4\x2b\x3a\x89\x67\x00\x4c\xdd\xa9\xd1\x9b\x5e\x67\x3d\xf9\xb6\x23\xf3\x1a\xd7\xf6\xe9" + + "\xd5\xd8\x71\xb3\xcb\x2a\xee\xd0\x34\xf9\x09\xda\xba\xcc\xe4\x78\xdc\x01\xf5\xda\xa3\xf8\x2a\x3d\xad\x24\x02\x71" + + "\xfe\x9e\x54\x43\x72\x13\xf3\xa3\xfb\x1c\xb4\xa2\x48\x36\xd6\xee\xd7\x71\xd1\x54\xf0\xeb\xc8\x7e\xe5\x79\xfa\xa2" + + "\xdf\x63\xfc\xbf\xcc\xc3\xd8\x0f\xdf\x4a\x28\x36\xac\x86\x29\xce\xaf\x69\xc3\x6a\x3a\xed\x86\xd5\x0a\x9f\xf3\xcd" + + "\xb2\x9f\xa2\xd0\xed\xa7\x28\x74\xfb\x15\x67\x9c\xb8\x4e\x08\xd5\xd4\x5e\xb1\xfa\x64\x39\xdb\x5b\xa7\x6e\x7b\x99" + + "\xc6\xbb\xea\xc2\x46\x73\xfd\x0b\x1d\xf3\x85\x39\x5e\x28\x41\x74\xbe\x5c\x6d\xab\xbf\x7c\xbe\xcf\x97\x09\x29\x12" + + "\x8c\x0b\x60\x4d\x72\x18\xa9\x3f\xee\xd3\x99\xd3\x0b\x1a\xb7\x25\x99\x05\x9e\x68\xb3\x0c\x7e\xf7\xa3\x51\x5d\xdf" + + "\x75\xb6\xd3\x3a\xeb\xd8\xcf\x10\x56\x9a\x87\x27\xf6\x3e\x77\xbe\x94\x7f\x07\x00\x00\xff\xff\x3f\x67\xd7\xeb\x28" + + "\x09\x00\x00") func bindataSchemagqlBytes() ([]byte, error) { return bindataRead( @@ -168,10 +171,10 @@ func bindataSchemagql() (*asset, error) { info := bindataFileInfo{ name: "schema.gql", - size: 2104, + size: 2344, md5checksum: "", mode: os.FileMode(420), - modTime: time.Unix(1556055697, 0), + modTime: time.Unix(1556718973, 0), } a := &asset{bytes: bytes, info: info} diff --git a/exp/ticker/internal/gql/static/schema.gql b/exp/ticker/internal/gql/static/schema.gql index 3b28efa701..86a96ba85e 100644 --- a/exp/ticker/internal/gql/static/schema.gql +++ b/exp/ticker/internal/gql/static/schema.gql @@ -75,6 +75,7 @@ type Market { close: Float! intervalStart: Time! firstLedgerCloseTime: Time! + orderbookStats: OrderbookStats! } type AggregatedMarket { @@ -89,6 +90,18 @@ type AggregatedMarket { close: Float! intervalStart: Time! firstLedgerCloseTime: Time! + orderbookStats: OrderbookStats! +} + +type OrderbookStats { + bidCount: BigInt! + bidVolume: Float! + bidMax: Float! + askCount: BigInt! + askVolume: Float! + askMin: Float! + spread: Float! + spreadMidPoint: Float! } type Issuer { diff --git a/exp/ticker/internal/main.go b/exp/ticker/internal/main.go index 58ce1d21fa..096fde4334 100644 --- a/exp/ticker/internal/main.go +++ b/exp/ticker/internal/main.go @@ -32,6 +32,14 @@ type MarketStats struct { Price float64 `json:"price"` Close float64 `json:"close"` CloseTime int64 `json:"close_time"` + BidCount int `json:"bid_count"` + BidVolume float64 `json:"bid_volume"` + BidMax float64 `json:"bid_max"` + AskCount int `json:"ask_count"` + AskVolume float64 `json:"ask_volume"` + AskMin float64 `json:"ask_min"` + Spread float64 `json:"spread"` + SpreadMidPoint float64 `json:"spread_mid_point"` } // Asset Sumary represents the collection of valid assets. diff --git a/exp/ticker/internal/scraper/main.go b/exp/ticker/internal/scraper/main.go index 7a5ce4dd9f..e0fd88fcd1 100644 --- a/exp/ticker/internal/scraper/main.go +++ b/exp/ticker/internal/scraper/main.go @@ -91,6 +91,24 @@ type FinalAsset struct { Status string `json:"status"` } +// OrderbookStats represents the Orderbook stats for a given asset +type OrderbookStats struct { + BaseAssetCode string + BaseAssetType string + BaseAssetIssuer string + CounterAssetCode string + CounterAssetType string + CounterAssetIssuer string + NumBids int + BidVolume float64 + HighestBid float64 + NumAsks int + AskVolume float64 + LowestAsk float64 + Spread float64 + SpreadMidPoint float64 +} + // FetchAllAssets fetches assets from the Horizon public net. If limit = 0, will fetch all assets. func (c *ScraperConfig) FetchAllAssets(limit int, parallelism int) (assets []FinalAsset, err error) { dirtyAssets, err := c.retrieveAssets(limit) @@ -127,3 +145,9 @@ func (c *ScraperConfig) StreamNewTrades(cursor string, h horizonclient.TradeHand c.Logger.Info("Starting to stream trades with cursor at:", cursor) return c.streamTrades(h, cursor) } + +// FetchOrderbookForAssets fetches the orderbook stats for the base and counter assets provided in the parameters +func (c *ScraperConfig) FetchOrderbookForAssets(bType, bCode, bIssuer, cType, cCode, cIssuer string) (OrderbookStats, error) { + c.Logger.Infof("Fetching orderbook info for %s:%s / %s:%s\n", bCode, bIssuer, cCode, cIssuer) + return c.fetchOrderbook(bType, bCode, bIssuer, cType, cCode, cIssuer) +} diff --git a/exp/ticker/internal/scraper/orderbook_scraper.go b/exp/ticker/internal/scraper/orderbook_scraper.go new file mode 100644 index 0000000000..e7dcdc8d8a --- /dev/null +++ b/exp/ticker/internal/scraper/orderbook_scraper.go @@ -0,0 +1,119 @@ +package scraper + +import ( + "math" + "strconv" + + "github.com/pkg/errors" + horizonclient "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/exp/ticker/internal/utils" + hProtocol "github.com/stellar/go/protocols/horizon" +) + +// fetchOrderbook fetches the orderbook stats for the base and counter assets provided in the parameters +func (c *ScraperConfig) fetchOrderbook(bType, bCode, bIssuer, cType, cCode, cIssuer string) (OrderbookStats, error) { + obStats := OrderbookStats{ + BaseAssetCode: bType, + BaseAssetType: bCode, + BaseAssetIssuer: bIssuer, + CounterAssetCode: cType, + CounterAssetType: cCode, + CounterAssetIssuer: cIssuer, + HighestBid: math.Inf(-1), // start with -Inf to make sure we catch the correct max bid + LowestAsk: math.Inf(1), // start with +Inf to make sure we catch the correct min ask + } + r := createOrderbookRequest(bType, bCode, bIssuer, cType, cCode, cIssuer) + summary, err := c.Client.OrderBook(r) + if err != nil { + return obStats, errors.Wrap(err, "could not fetch orderbook summary") + } + + err = calcOrderbookStats(&obStats, summary) + return obStats, errors.Wrap(err, "could not calculate orderbook stats") +} + +// calcOrderbookStats calculates the NumBids, BidVolume, BidMax, NumAsks, AskVolume and AskMin +// statistics for a given OrdebookStats instance +func calcOrderbookStats(obStats *OrderbookStats, summary hProtocol.OrderBookSummary) error { + // Calculate Bid Data: + obStats.NumBids = len(summary.Bids) + if obStats.NumBids == 0 { + obStats.HighestBid = 0 + } + for _, bid := range summary.Bids { + pricef := float64(bid.PriceR.N) / float64(bid.PriceR.D) + if pricef > obStats.HighestBid { + obStats.HighestBid = pricef + } + + amountf, err := strconv.ParseFloat(bid.Amount, 64) + if err != nil { + return errors.Wrap(err, "invalid bid amount") + } + obStats.BidVolume += amountf + } + + // Calculate Ask Data: + obStats.NumAsks = len(summary.Asks) + if obStats.NumAsks == 0 { + obStats.LowestAsk = 0 + } + for _, ask := range summary.Asks { + pricef := float64(ask.PriceR.N) / float64(ask.PriceR.D) + amountf, err := strconv.ParseFloat(ask.Amount, 64) + if err != nil { + return errors.Wrap(err, "invalid ask amount") + } + + // On Horizon, Ask prices are in units of counter, but + // amount is in units of base. Therefore, real amount = amount * price + // See: https://github.com/stellar/go/issues/612 + obStats.AskVolume += pricef * amountf + if pricef < obStats.LowestAsk { + obStats.LowestAsk = pricef + } + } + + obStats.Spread, obStats.SpreadMidPoint = utils.CalcSpread(obStats.HighestBid, obStats.LowestAsk) + + // Clean up remaining infinity values: + if math.IsInf(obStats.LowestAsk, 0) { + obStats.LowestAsk = 0 + } + + if math.IsInf(obStats.HighestBid, 0) { + obStats.HighestBid = 0 + } + + return nil +} + +// createOrderbookRequest generates a horizonclient.OrderBookRequest based on the base +// and counter asset parameters provided +func createOrderbookRequest(bType, bCode, bIssuer, cType, cCode, cIssuer string) horizonclient.OrderBookRequest { + r := horizonclient.OrderBookRequest{ + SellingAssetType: horizonclient.AssetType(bType), + BuyingAssetType: horizonclient.AssetType(cType), + // NOTE (Alex C, 2019-05-02): + // Orderbook requests are currently not paginated on Horizon. + // This limit has been added to ensure we capture at least 200 + // orderbook entries once pagination is added. + Limit: 200, + } + + // The Horizon API requires *AssetCode and *AssetIssuer fields to be empty + // when an Asset is native. As we store "XLM" as the asset code for native, + // we should only add Code and Issuer info in case we're dealing with + // non-native assets. + // See: https://www.stellar.org/developers/horizon/reference/endpoints/orderbook-details.html + if bType != string(horizonclient.AssetTypeNative) { + r.SellingAssetCode = bCode + r.SellingAssetIssuer = bIssuer + } + if cType != string(horizonclient.AssetTypeNative) { + r.BuyingAssetCode = cCode + r.BuyingAssetIssuer = cIssuer + } + + return r +} diff --git a/exp/ticker/internal/tickerdb/helpers.go b/exp/ticker/internal/tickerdb/helpers.go index 34db1d130c..22f8aabf16 100644 --- a/exp/ticker/internal/tickerdb/helpers.go +++ b/exp/ticker/internal/tickerdb/helpers.go @@ -5,6 +5,8 @@ import ( "fmt" "reflect" "strings" + + "github.com/stellar/go/exp/ticker/internal/utils" ) // getDBFieldTags returns all "db" tags for a given struct, optionally excluding the "id". @@ -115,3 +117,21 @@ func getBaseAndCounterCodes(pairName string) (string, string, error) { return assets[0], assets[1], nil } + +// performUpsertQuery introspects a dbStruct interface{} and performs an insert query +// (if the conflictConstraint isn't broken), otherwise it updates the instance on the +// db, preserving the old values for the fields in preserveFields +func (s *TickerSession) performUpsertQuery(dbStruct interface{}, tableName string, conflictConstraint string, preserveFields []string) error { + dbFields := getDBFieldTags(dbStruct, true) + dbFieldsString := strings.Join(dbFields, ", ") + dbValues := getDBFieldValues(dbStruct, true) + + cleanPreservedFields := sanitizeFieldNames(preserveFields) + toUpdateFields := utils.SliceDiff(dbFields, cleanPreservedFields) + + qs := fmt.Sprintf("INSERT INTO %s (", tableName) + dbFieldsString + ")" + qs += " VALUES (" + generatePlaceholders(dbValues) + ")" + qs += " " + createOnConflictFragment(conflictConstraint, toUpdateFields) + ";" + _, err := s.ExecRaw(qs, dbValues...) + return err +} diff --git a/exp/ticker/internal/tickerdb/main.go b/exp/ticker/internal/tickerdb/main.go index 5c156959db..1b2a6569e2 100644 --- a/exp/ticker/internal/tickerdb/main.go +++ b/exp/ticker/internal/tickerdb/main.go @@ -82,6 +82,22 @@ type Trade struct { Price float64 `db:"price"` } +// OrderbookStats represents an entry on the orderbook_stats table +type OrderbookStats struct { + ID int32 `db:"id"` + BaseAssetID int32 `db:"base_asset_id"` + CounterAssetID int32 `db:"counter_asset_id"` + NumBids int `db:"num_bids"` + BidVolume float64 `db:"bid_volume"` + HighestBid float64 `db:"highest_bid"` + NumAsks int `db:"num_asks"` + AskVolume float64 `db:"ask_volume"` + LowestAsk float64 `db:"lowest_ask"` + Spread float64 `db:"spread"` + SpreadMidPoint float64 `db:"spread_mid_point"` + UpdatedAt time.Time `db:"updated_at"` +} + // Market represent the aggregated market data retrieved from the database. // Note: this struct does *not* directly map to a db entity. type Market struct { @@ -102,19 +118,28 @@ type Market struct { PriceChange7d float64 `db:"price_change_7d"` LastPriceCloseTime time.Time `db:"close_time"` LastPrice float64 `db:"last_price"` + NumBids int `db:"num_bids"` + BidVolume float64 `db:"bid_volume"` + HighestBid float64 `db:"highest_bid"` + NumAsks int `db:"num_asks"` + AskVolume float64 `db:"ask_volume"` + LowestAsk float64 `db:"lowest_ask"` } // PartialMarket represents the aggregated market data for a // specific pair of assets (or asset codes) during an arbitrary // time range. +// Note: this struct does *not* directly map to a db entity. type PartialMarket struct { TradePairName string `db:"trade_pair_name"` BaseAssetID int32 `db:"base_asset_id"` BaseAssetCode string `db:"base_asset_code"` BaseAssetIssuer string `db:"base_asset_issuer"` + BaseAssetType string `db:"base_asset_type"` CounterAssetID int32 `db:"counter_asset_id"` CounterAssetCode string `db:"counter_asset_code"` CounterAssetIssuer string `db:"counter_asset_issuer"` + CounterAssetType string `db:"counter_asset_type"` BaseVolume float64 `db:"base_volume"` CounterVolume float64 `db:"counter_volume"` TradeCount int32 `db:"trade_count"` @@ -123,6 +148,12 @@ type PartialMarket struct { High float64 `db:"highest_price"` Change float64 `db:"price_change"` Close float64 `db:"last_price"` + NumBids int `db:"num_bids"` + BidVolume float64 `db:"bid_volume"` + HighestBid float64 `db:"highest_bid"` + NumAsks int `db:"num_asks"` + AskVolume float64 `db:"ask_volume"` + LowestAsk float64 `db:"lowest_ask"` IntervalStart time.Time `db:"interval_start"` FirstLedgerCloseTime time.Time `db:"first_ledger_close_time"` } diff --git a/exp/ticker/internal/tickerdb/migrations/20190425110313-add_orderbook_stats.sql b/exp/ticker/internal/tickerdb/migrations/20190425110313-add_orderbook_stats.sql new file mode 100644 index 0000000000..5dba26e32b --- /dev/null +++ b/exp/ticker/internal/tickerdb/migrations/20190425110313-add_orderbook_stats.sql @@ -0,0 +1,26 @@ + +-- +migrate Up +CREATE TABLE orderbook_stats ( + id serial NOT NULL PRIMARY KEY, + + base_asset_id integer REFERENCES assets (id) NOT NULL, + counter_asset_id integer REFERENCES assets (id) NOT NULL, + + num_bids bigint NOT NULL, + bid_volume double precision NOT NULL, + highest_bid double precision NOT NULL, + + num_asks bigint NOT NULL, + ask_volume double precision NOT NULL, + lowest_ask double precision NOT NULL, + + spread double precision NOT NULL, + spread_mid_point double precision NOT NULL, + + updated_at timestamptz NOT NULL +); +ALTER TABLE ONLY public.orderbook_stats + ADD CONSTRAINT orderbook_stats_base_counter_asset_key UNIQUE (base_asset_id, counter_asset_id); + +-- +migrate Down +DROP TABLE orderbook_stats; diff --git a/exp/ticker/internal/tickerdb/migrations/20190426092321-add_aggregated_orderbook_view.sql b/exp/ticker/internal/tickerdb/migrations/20190426092321-add_aggregated_orderbook_view.sql new file mode 100644 index 0000000000..844fc41c74 --- /dev/null +++ b/exp/ticker/internal/tickerdb/migrations/20190426092321-add_aggregated_orderbook_view.sql @@ -0,0 +1,20 @@ + +-- +migrate Up +CREATE OR REPLACE VIEW aggregated_orderbook AS + SELECT + concat(bAsset.code, '_', cAsset.code) as trade_pair_name, + bAsset.code as base_asset_code, + cAsset.code as counter_asset_code, + COALESCE(sum(os.num_bids), 0) AS num_bids, + COALESCE(sum(os.bid_volume), 0.0) AS bid_volume, + COALESCE(max(os.highest_bid), 0.0) AS highest_bid, + COALESCE(sum(os.num_asks), 0) AS num_asks, + COALESCE(sum(os.ask_volume), 0.0) AS ask_volume, + COALESCE(min(os.lowest_ask), 0.0) AS lowest_ask + FROM orderbook_stats AS os + JOIN assets AS bAsset ON os.base_asset_id = bAsset.id + JOIN assets AS cAsset on os.counter_asset_id = cAsset.id + GROUP BY trade_pair_name, base_asset_code, counter_asset_code; + +-- +migrate Down +DROP VIEW IF EXISTS aggregated_orderbook; diff --git a/exp/ticker/internal/tickerdb/migrations/bindata.go b/exp/ticker/internal/tickerdb/migrations/bindata.go index d0ab7d9afd..ac5d8054ef 100644 --- a/exp/ticker/internal/tickerdb/migrations/bindata.go +++ b/exp/ticker/internal/tickerdb/migrations/bindata.go @@ -8,6 +8,8 @@ // migrations/20190409172610-rename_assets_desc_description.sql // migrations/20190410094830-add_assets_issuer_account_field.sql // migrations/20190411165735-data_seed_and_indices.sql +// migrations/20190425110313-add_orderbook_stats.sql +// migrations/20190426092321-add_aggregated_orderbook_view.sql package bdata @@ -117,7 +119,7 @@ func bindataMigrations20190404184050initialsql() (*asset, error) { size: 820, md5checksum: "", mode: os.FileMode(420), - modTime: time.Unix(1556552251, 0), + modTime: time.Unix(1556565465, 0), } a := &asset{bytes: bytes, info: info} @@ -400,6 +402,87 @@ func bindataMigrations20190411165735dataseedandindicessql() (*asset, error) { return a, nil } +var _bindataMigrations20190425110313addorderbookstatssql = []byte( + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x9c\x92\xcd\x6e\xea\x30\x10\x85\xf7\x7e\x8a\x59\x82\x2e\xdc\x17\x60\x95" + + "\x4b\x7c\x25\xd4\x34\xa1\x26\x59\xb0\xb2\x1c\x3c\x0a\xa3\xfc\x38\xf2\x38\x45\xed\xd3\x57\x44\x15\x15\x69\x0b\x55" + + "\xd7\x3e\xe7\x7c\x23\xf9\x13\xcb\x25\xfc\x69\xa9\xf2\x26\x20\x14\xbd\x58\x2b\x19\xe5\x12\xf2\xe8\x5f\x22\xc1\x79" + + "\x8b\xbe\x74\xae\xd6\x1c\x4c\x60\x98\x09\x00\x00\xb2\xc0\xe8\xc9\x34\x90\x66\x39\xa4\x45\x92\xc0\x56\x6d\x1e\x23" + + "\xb5\x87\x07\xb9\x5f\x88\x31\x54\x1a\x46\x6d\x98\x31\x68\xb2\x40\x5d\xc0\x0a\x3d\x28\xf9\x5f\x2a\x99\xae\xe5\x0e" + + "\xc6\x37\x86\x19\xd9\xf9\x65\x67\x31\x56\x0f\x6e\xe8\x02\xfa\x5f\xb4\xc7\x7a\x37\xb4\xba\x24\xcb\x50\x52\x45\x5d" + + "\x98\x8c\x97\x64\xf5\xb3\x6b\x86\x16\xc1\xba\xa1\x6c\x10\x7a\x8f\x07\x62\x72\xdd\x24\x79\xa4\xea\x88\x1c\xce\x5b" + + "\xb7\xa2\x17\xa6\xe1\xfa\x1b\xa6\xe1\xfa\x87\xcc\xc6\x9d\xce\x48\xc3\xf5\x5d\x24\xf7\x1e\xcd\xcd\xcb\x3e\x52\xba" + + "\x25\xab\x7b\x77\xbe\xec\xde\xec\xd0\x5b\x13\xd0\x6a\x13\x20\x50\x8b\x1c\x4c\xdb\x87\xd7\x4b\x4a\xcc\x57\x22\x4a" + + "\x72\xa9\xde\x0d\xc9\xd2\x64\x0f\xfd\x50\x36\x74\xf8\x3b\xb1\x65\x9c\x8b\xe2\x18\xd6\x59\xba\xcb\x55\xb4\x49\xf3" + + "\xa9\x50\x7a\xb4\xe4\xfa\xbf\x6b\x7c\x81\x22\xdd\x3c\x15\x12\x66\x57\x12\x2d\x3e\x89\x31\x5f\x89\x2b\x7d\x63\x77" + + "\xea\x44\xac\xb2\xed\xd7\xfa\xae\xc4\x5b\x00\x00\x00\xff\xff\x06\x01\x94\xcd\xed\x02\x00\x00") + +func bindataMigrations20190425110313addorderbookstatssqlBytes() ([]byte, error) { + return bindataRead( + _bindataMigrations20190425110313addorderbookstatssql, + "migrations/20190425110313-add_orderbook_stats.sql", + ) +} + +func bindataMigrations20190425110313addorderbookstatssql() (*asset, error) { + bytes, err := bindataMigrations20190425110313addorderbookstatssqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{ + name: "migrations/20190425110313-add_orderbook_stats.sql", + size: 749, + md5checksum: "", + mode: os.FileMode(420), + modTime: time.Unix(1556565462, 0), + } + + a := &asset{bytes: bytes, info: info} + + return a, nil +} + +var _bindataMigrations20190426092321addaggregatedorderbookviewsql = []byte( + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x93\xcf\x6e\xb2\x40\x14\xc5\xf7\x3c\xc5\xdd\xa9\xf9\xd0\x7c\x7b\xd3" + + "\x05\xc5\xb1\xa1\xb1\x62\x40\xfb\x67\x35\xb9\x30\x13\x9c\x20\x8c\xe1\x0e\xb5\x8f\xdf\x0c\x44\x81\x88\x65\x45\x0e" + + "\xf7\x77\x39\x99\x73\xc6\x99\xcf\xe1\x5f\xa1\xb2\x0a\x8d\x84\xc3\xd9\xf1\x23\xe6\xed\x19\x84\x11\x44\x6c\xb7\xf1" + + "\x7c\x06\xef\x01\xfb\x00\xcc\xb2\x4a\x66\x68\xa4\xe0\xba\x12\xb2\x4a\xb4\xce\xc1\x8b\x1d\x00\x80\x98\x6d\x98\xbf" + + "\x6f\x5e\xed\x93\xea\x32\x45\x33\x4d\x3c\x22\x69\x16\xa9\x16\xd2\x85\x09\x9f\xb8\x90\x76\xca\x0c\x90\xc0\x54\x28" + + "\x24\x3f\xa3\xaa\x78\x89\x85\x74\x6f\x1b\x7a\xa8\x9d\x4b\x90\x24\x47\x2b\xf1\x66\x5b\xf7\xa7\xe1\x5c\xaa\xeb\xd2" + + "\xc8\x6a\x74\xd4\x0f\xbd\x0d\x8b\x7d\x36\xa5\xba\x98\x6a\x5a\x94\x75\xc1\x13\x25\x68\xe6\xc2\xff\x19\x78\x31\x5c" + + "\x85\xc7\x48\xa2\x04\xff\xd6\xa7\xba\x90\x16\x5a\xb4\x58\x27\x8e\x80\x05\xfe\x58\xf0\xa8\xb2\xa3\x24\x63\xd7\xf7" + + "\xc8\x9e\xfa\xb7\x4d\xa4\x7c\x68\xd3\x0a\x8f\x11\xa4\xfc\xde\x66\x27\x8e\xd9\x54\xa5\x05\x4f\xfa\x62\xfd\x20\xe5" + + "\x3d\xb0\x13\x1b\x6e\x1d\x85\x6f\x70\x2b\x00\x27\x83\x86\xec\x98\xa6\xe6\xf3\x6b\x18\x6c\xa1\x39\xfe\x46\x6d\x73" + + "\x84\x70\x0b\xf6\xf8\xba\x14\x95\x80\xa7\x6b\xc8\x4a\x8c\x91\x6d\xb2\xa0\x4b\x4b\x0e\x73\x6d\xe0\x74\x00\xbf\x44" + + "\xe1\x61\x07\xcf\x5f\x77\x8d\xba\xab\xce\x48\x47\x96\xce\xe0\x0a\xac\xf4\xa5\x74\x56\x51\xb8\x6b\x7b\x1f\xac\x81" + + "\x7d\x06\xf1\x3e\x1e\xbd\x01\x4b\xe7\x37\x00\x00\xff\xff\x72\xc3\x7e\xff\x3f\x03\x00\x00") + +func bindataMigrations20190426092321addaggregatedorderbookviewsqlBytes() ([]byte, error) { + return bindataRead( + _bindataMigrations20190426092321addaggregatedorderbookviewsql, + "migrations/20190426092321-add_aggregated_orderbook_view.sql", + ) +} + +func bindataMigrations20190426092321addaggregatedorderbookviewsql() (*asset, error) { + bytes, err := bindataMigrations20190426092321addaggregatedorderbookviewsqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{ + name: "migrations/20190426092321-add_aggregated_orderbook_view.sql", + size: 831, + md5checksum: "", + mode: os.FileMode(420), + modTime: time.Unix(1556565462, 0), + } + + a := &asset{bytes: bytes, info: info} + + return a, nil +} + // // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or @@ -471,6 +554,8 @@ var _bindata = map[string]func() (*asset, error){ "migrations/20190409172610-rename_assets_desc_description.sql": bindataMigrations20190409172610renameassetsdescdescriptionsql, "migrations/20190410094830-add_assets_issuer_account_field.sql": bindataMigrations20190410094830addassetsissueraccountfieldsql, "migrations/20190411165735-data_seed_and_indices.sql": bindataMigrations20190411165735dataseedandindicessql, + "migrations/20190425110313-add_orderbook_stats.sql": bindataMigrations20190425110313addorderbookstatssql, + "migrations/20190426092321-add_aggregated_orderbook_view.sql": bindataMigrations20190426092321addaggregatedorderbookviewsql, } // @@ -533,6 +618,8 @@ var _bintree = &bintree{Func: nil, Children: map[string]*bintree{ "20190409172610-rename_assets_desc_description.sql": {Func: bindataMigrations20190409172610renameassetsdescdescriptionsql, Children: map[string]*bintree{}}, "20190410094830-add_assets_issuer_account_field.sql": {Func: bindataMigrations20190410094830addassetsissueraccountfieldsql, Children: map[string]*bintree{}}, "20190411165735-data_seed_and_indices.sql": {Func: bindataMigrations20190411165735dataseedandindicessql, Children: map[string]*bintree{}}, + "20190425110313-add_orderbook_stats.sql": {Func: bindataMigrations20190425110313addorderbookstatssql, Children: map[string]*bintree{}}, + "20190426092321-add_aggregated_orderbook_view.sql": {Func: bindataMigrations20190426092321addaggregatedorderbookviewsql, Children: map[string]*bintree{}}, }}, }} diff --git a/exp/ticker/internal/tickerdb/queries_asset.go b/exp/ticker/internal/tickerdb/queries_asset.go index 78c1d6ed19..aed494f374 100644 --- a/exp/ticker/internal/tickerdb/queries_asset.go +++ b/exp/ticker/internal/tickerdb/queries_asset.go @@ -1,33 +1,9 @@ package tickerdb -import ( - "strings" - - "github.com/stellar/go/exp/ticker/internal/utils" -) - -// InsertAsset inserts a new Asset into the database -func (s *TickerSession) InsertAsset(a *Asset) (err error) { - tbl := s.GetTable("assets") - _, err = tbl.Insert(a).IgnoreCols("id").Exec() - return -} - // InsertOrUpdateAsset inserts an Asset on the database (if new), // or updates an existing one func (s *TickerSession) InsertOrUpdateAsset(a *Asset, preserveFields []string) (err error) { - dbFields := getDBFieldTags(*a, true) - dbFieldsString := strings.Join(dbFields, ", ") - dbValues := getDBFieldValues(*a, true) - - cleanPreservedFields := sanitizeFieldNames(preserveFields) - toUpdateFields := utils.SliceDiff(dbFields, cleanPreservedFields) - - qs := "INSERT INTO assets (" + dbFieldsString + ")" - qs += " VALUES (" + generatePlaceholders(dbValues) + ")" - qs += " " + createOnConflictFragment("assets_code_issuer_account", toUpdateFields) + ";" - _, err = s.ExecRaw(qs, dbValues...) - return + return s.performUpsertQuery(*a, "assets", "assets_code_issuer_account", preserveFields) } // GetAssetByCodeAndIssuerAccount searches for an Asset with the given code diff --git a/exp/ticker/internal/tickerdb/queries_market.go b/exp/ticker/internal/tickerdb/queries_market.go index ac749d8e87..72a5cfcaaf 100644 --- a/exp/ticker/internal/tickerdb/queries_market.go +++ b/exp/ticker/internal/tickerdb/queries_market.go @@ -92,6 +92,23 @@ func (s *TickerSession) RetrievePartialMarkets( return } +// Retrieve7DRelevantMarkets retrieves the base and counter asset data of the markets +// that were relevant in the last 7-day period. +func (s *TickerSession) Retrieve7DRelevantMarkets() (partialMkts []PartialMarket, err error) { + q := ` + SELECT + ba.id as base_asset_id, ba.type AS base_asset_type, ba.code AS base_asset_code, ba.issuer_account AS base_asset_issuer, + ca.id as counter_asset_id, ca.type AS counter_asset_type, ca.code AS counter_asset_code, ca.issuer_account AS counter_asset_issuer + FROM trades as t + JOIN assets AS ba ON t.base_asset_id = ba.id + JOIN assets AS ca ON t.counter_asset_id = ca.id + WHERE ba.is_valid = TRUE AND ca.is_valid = TRUE AND t.ledger_close_time > now() - interval '7 days' + GROUP BY ba.id, ba.type, ba.code, ba.issuer_account, ca.id, ca.type, ca.code, ca.issuer_account + ` + err = s.SelectRaw(&partialMkts, q) + return +} + var marketQuery = ` SELECT t2.trade_pair_name, @@ -112,7 +129,14 @@ SELECT COALESCE(open_price_7d, 0.0) as open_price_7d, COALESCE(last_price, 0.0) as last_price, - COALESCE(last_close_time, now()) as close_time + COALESCE(last_close_time_24h, last_close_time_7d) as close_time, + + COALESCE(os.num_bids, 0) as num_bids, + COALESCE(os.bid_volume, 0.0) as bid_volume, + COALESCE(os.highest_bid, 0.0) as highest_bid, + COALESCE(os.num_asks, 0) as num_asks, + COALESCE(os.ask_volume, 0.0) as ask_volume, + COALESCE(os.lowest_ask, 0.0) as lowest_ask FROM ( SELECT -- All valid trades for 24h period @@ -125,7 +149,7 @@ FROM ( (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1] AS open_price_24h, (array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] AS last_price, ((array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] - (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1]) AS price_change_24h, - max(t.ledger_close_time) AS last_close_time + max(t.ledger_close_time) AS last_close_time_24h FROM trades AS t JOIN assets AS bAsset ON t.base_asset_id = bAsset.id JOIN assets AS cAsset on t.counter_asset_id = cAsset.id @@ -143,24 +167,32 @@ FROM ( max(t.price) AS highest_price_7d, min(t.price) AS lowest_price_7d, (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1] AS open_price_7d, - ((array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] - (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1]) AS price_change_7d + ((array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] - (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1]) AS price_change_7d, + max(t.ledger_close_time) AS last_close_time_7d FROM trades AS t + LEFT JOIN orderbook_stats AS os + ON t.base_asset_id = os.base_asset_id AND t.counter_asset_id = os.counter_asset_id JOIN assets AS bAsset ON t.base_asset_id = bAsset.id JOIN assets AS cAsset on t.counter_asset_id = cAsset.id WHERE bAsset.is_valid = TRUE AND cAsset.is_valid = TRUE AND t.ledger_close_time > now() - interval '7 days' GROUP BY trade_pair_name - ) t2 ON t1.trade_pair_name = t2.trade_pair_name; + ) t2 ON t1.trade_pair_name = t2.trade_pair_name + LEFT JOIN aggregated_orderbook AS os ON t2.trade_pair_name = os.trade_pair_name; ` var partialMarketQuery = ` SELECT concat(bAsset.code, ':', bAsset.issuer_account, ' / ', cAsset.code, ':', cAsset.issuer_account) as trade_pair_name, + bAsset.id AS base_asset_id, bAsset.code AS base_asset_code, bAsset.issuer_account as base_asset_issuer, + bAsset.type as base_asset_type, + cAsset.id AS counter_asset_id, cAsset.code AS counter_asset_code, cAsset.issuer_account AS counter_asset_issuer, + cAsset.type as counter_asset_type, sum(t.base_amount) AS base_volume, sum(t.counter_amount) AS counter_volume, count(t.base_amount) AS trade_count, @@ -170,29 +202,59 @@ SELECT (array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] AS last_price, ((array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] - (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1]) AS price_change, (now() - interval '__NUMHOURS__ hours') AS interval_start, - min(t.ledger_close_time) AS first_ledger_close_time + min(t.ledger_close_time) AS first_ledger_close_time, + COALESCE((array_agg(os.num_bids))[1], 0) AS num_bids, + COALESCE((array_agg(os.bid_volume))[1], 0.0) AS bid_volume, + COALESCE((array_agg(os.highest_bid))[1], 0.0) AS highest_bid, + COALESCE((array_agg(os.num_asks))[1], 0) AS num_asks, + COALESCE((array_agg(os.ask_volume))[1], 0.0) AS ask_volume, + COALESCE((array_agg(os.lowest_ask))[1], 0.0) AS lowest_ask FROM trades AS t + LEFT JOIN orderbook_stats AS os ON t.base_asset_id = os.base_asset_id AND t.counter_asset_id = os.counter_asset_id JOIN assets AS bAsset ON t.base_asset_id = bAsset.id JOIN assets AS cAsset on t.counter_asset_id = cAsset.id __WHERECLAUSE__ -GROUP BY bAsset.code, bAsset.issuer_account, cAsset.code, cAsset.issuer_account; +GROUP BY bAsset.id, bAsset.code, bAsset.issuer_account, bAsset.type, cAsset.id, cAsset.code, cAsset.issuer_account, cAsset.type; ` var aggMarketQuery = ` SELECT - concat(bAsset.code, '_', cAsset.code) as trade_pair_name, - sum(t.base_amount) AS base_volume, - sum(t.counter_amount) AS counter_volume, - count(t.base_amount) AS trade_count, - max(t.price) AS highest_price, - min(t.price) AS lowest_price, - (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1] AS open_price, - (array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] AS last_price, - ((array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] - (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1]) AS price_change, - (now() - interval '__NUMHOURS__ hours') AS interval_start, - min(t.ledger_close_time) AS first_ledger_close_time -FROM trades AS t - JOIN assets AS bAsset ON t.base_asset_id = bAsset.id - JOIN assets AS cAsset on t.counter_asset_id = cAsset.id -__WHERECLAUSE__ -GROUP BY trade_pair_name;` + t1.trade_pair_name, + t1.base_volume, + t1.counter_volume, + t1.trade_count, + t1.highest_price, + t1.lowest_price, + t1.open_price, + t1.last_price, + t1.price_change, + t1.interval_start, + t1.first_ledger_close_time, + COALESCE(aob.base_asset_code, '') as base_asset_code, + COALESCE(aob.counter_asset_code, '') as counter_asset_code, + COALESCE(aob.num_bids, 0) AS num_bids, + COALESCE(aob.bid_volume, 0.0) AS bid_volume, + COALESCE(aob.highest_bid, 0.0) AS highest_bid, + COALESCE(aob.num_asks, 0) AS num_asks, + COALESCE(aob.ask_volume, 0.0) AS ask_volume, + COALESCE(aob.lowest_ask, 0.0) AS lowest_ask +FROM ( + SELECT + concat(bAsset.code, '_', cAsset.code) as trade_pair_name, + sum(t.base_amount) AS base_volume, + sum(t.counter_amount) AS counter_volume, + count(t.base_amount) AS trade_count, + max(t.price) AS highest_price, + min(t.price) AS lowest_price, + (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1] AS open_price, + (array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] AS last_price, + ((array_agg(t.price ORDER BY t.ledger_close_time DESC))[1] - (array_agg(t.price ORDER BY t.ledger_close_time ASC))[1]) AS price_change, + (now() - interval '__NUMHOURS__ hours') AS interval_start, + min(t.ledger_close_time) AS first_ledger_close_time + FROM trades AS t + LEFT JOIN orderbook_stats AS os ON t.base_asset_id = os.base_asset_id AND t.counter_asset_id = os.counter_asset_id + JOIN assets AS bAsset ON t.base_asset_id = bAsset.id + JOIN assets AS cAsset on t.counter_asset_id = cAsset.id + __WHERECLAUSE__ + GROUP BY trade_pair_name +) t1 LEFT JOIN aggregated_orderbook AS aob ON t1.trade_pair_name = aob.trade_pair_name;` diff --git a/exp/ticker/internal/tickerdb/queries_market_test.go b/exp/ticker/internal/tickerdb/queries_market_test.go index b48352e4dc..13c708dca4 100644 --- a/exp/ticker/internal/tickerdb/queries_market_test.go +++ b/exp/ticker/internal/tickerdb/queries_market_test.go @@ -154,6 +154,65 @@ func TestRetrieveMarketData(t *testing.T) { err = session.BulkInsertTrades(trades) require.NoError(t, err) + // Adding some orderbook stats: + obTime := time.Now() + orderbookStats := OrderbookStats{ + BaseAssetID: xlmAsset.ID, + CounterAssetID: ethAsset.ID, + NumBids: 15, + BidVolume: 0.15, + HighestBid: 200.0, + NumAsks: 17, + AskVolume: 30.0, + LowestAsk: 0.1, + Spread: 0.93, + SpreadMidPoint: 0.35, + UpdatedAt: obTime, + } + err = session.InsertOrUpdateOrderbookStats( + &orderbookStats, + []string{"base_asset_id", "counter_asset_id"}, + ) + require.NoError(t, err) + + var obBTCETH1 OrderbookStats + err = session.GetRaw(&obBTCETH1, ` + SELECT * + FROM orderbook_stats + ORDER BY id DESC + LIMIT 1`, + ) + require.NoError(t, err) + + orderbookStats = OrderbookStats{ + BaseAssetID: xlmAsset.ID, + CounterAssetID: btcAsset.ID, + NumBids: 1, + BidVolume: 0.1, + HighestBid: 20.0, + NumAsks: 1, + AskVolume: 15.0, + LowestAsk: 0.2, + Spread: 0.96, + SpreadMidPoint: 0.36, + UpdatedAt: obTime, + } + err = session.InsertOrUpdateOrderbookStats( + &orderbookStats, + []string{"base_asset_id", "counter_asset_id"}, + ) + require.NoError(t, err) + + var obBTCETH2 OrderbookStats + err = session.GetRaw(&obBTCETH2, ` + SELECT * + FROM orderbook_stats + ORDER BY id DESC + LIMIT 1`, + ) + require.NoError(t, err) + assert.NotEqual(t, obBTCETH1.ID, obBTCETH2.ID) + markets, err := session.RetrieveMarketData() require.NoError(t, err) fmt.Println(markets) @@ -233,6 +292,21 @@ func TestRetrieveMarketData(t *testing.T) { assert.True(t, priceChange7dDiff < 0.0000000000001) assert.Equal(t, priceChange24hDiff, priceChange7dDiff) + + // Analysing aggregated orderbook data: + assert.Equal(t, 15, xlmethMkt.NumBids) + assert.Equal(t, 0.15, xlmethMkt.BidVolume) + assert.Equal(t, 200.0, xlmethMkt.HighestBid) + assert.Equal(t, 17, xlmethMkt.NumAsks) + assert.Equal(t, 30.0, xlmethMkt.AskVolume) + assert.Equal(t, 0.1, xlmethMkt.LowestAsk) + + assert.Equal(t, 1, xlmbtcMkt.NumBids) + assert.Equal(t, 0.1, xlmbtcMkt.BidVolume) + assert.Equal(t, 20.0, xlmbtcMkt.HighestBid) + assert.Equal(t, 1, xlmbtcMkt.NumAsks) + assert.Equal(t, 15.0, xlmbtcMkt.AskVolume) + assert.Equal(t, 0.2, xlmbtcMkt.LowestAsk) } func TestRetrievePartialMarkets(t *testing.T) { @@ -389,6 +463,65 @@ func TestRetrievePartialMarkets(t *testing.T) { err = session.BulkInsertTrades(trades) require.NoError(t, err) + // Adding some orderbook stats: + obTime := time.Now() + orderbookStats := OrderbookStats{ + BaseAssetID: btcAsset.ID, + CounterAssetID: ethAsset1.ID, + NumBids: 15, + BidVolume: 0.15, + HighestBid: 200.0, + NumAsks: 17, + AskVolume: 30.0, + LowestAsk: 0.1, + Spread: 0.93, + SpreadMidPoint: 0.35, + UpdatedAt: obTime, + } + err = session.InsertOrUpdateOrderbookStats( + &orderbookStats, + []string{"base_asset_id", "counter_asset_id"}, + ) + require.NoError(t, err) + + var obBTCETH1 OrderbookStats + err = session.GetRaw(&obBTCETH1, ` + SELECT * + FROM orderbook_stats + ORDER BY id DESC + LIMIT 1`, + ) + require.NoError(t, err) + + orderbookStats = OrderbookStats{ + BaseAssetID: btcAsset.ID, + CounterAssetID: ethAsset2.ID, + NumBids: 1, + BidVolume: 0.1, + HighestBid: 20.0, + NumAsks: 1, + AskVolume: 15.0, + LowestAsk: 0.2, + Spread: 0.96, + SpreadMidPoint: 0.36, + UpdatedAt: obTime, + } + err = session.InsertOrUpdateOrderbookStats( + &orderbookStats, + []string{"base_asset_id", "counter_asset_id"}, + ) + require.NoError(t, err) + + var obBTCETH2 OrderbookStats + err = session.GetRaw(&obBTCETH2, ` + SELECT * + FROM orderbook_stats + ORDER BY id DESC + LIMIT 1`, + ) + require.NoError(t, err) + assert.NotEqual(t, obBTCETH1.ID, obBTCETH2.ID) + partialMkts, err := session.RetrievePartialMarkets( nil, nil, nil, nil, 12, ) @@ -441,6 +574,21 @@ func TestRetrievePartialMarkets(t *testing.T) { btceth2Mkt.FirstLedgerCloseTime.Local().Truncate(time.Millisecond), ) + // Analyzing non-aggregated orderbook data + assert.Equal(t, 15, btceth1Mkt.NumBids) + assert.Equal(t, 0.15, btceth1Mkt.BidVolume) + assert.Equal(t, 200.0, btceth1Mkt.HighestBid) + assert.Equal(t, 17, btceth1Mkt.NumAsks) + assert.Equal(t, 30.0, btceth1Mkt.AskVolume) + assert.Equal(t, 0.1, btceth1Mkt.LowestAsk) + + assert.Equal(t, 1, btceth2Mkt.NumBids) + assert.Equal(t, 0.1, btceth2Mkt.BidVolume) + assert.Equal(t, 20.0, btceth2Mkt.HighestBid) + assert.Equal(t, 1, btceth2Mkt.NumAsks) + assert.Equal(t, 15.0, btceth2Mkt.AskVolume) + assert.Equal(t, 0.2, btceth2Mkt.LowestAsk) + // Now let's use the same data, but aggregating by asset pair partialAggMkts, err := session.RetrievePartialAggMarkets(nil, 12) require.NoError(t, err) @@ -475,4 +623,12 @@ func TestRetrievePartialMarkets(t *testing.T) { require.NoError(t, err) assert.Equal(t, 1, len(partialAggMkts)) assert.Equal(t, int32(3), partialAggMkts[0].TradeCount) + + // Analyzing aggregated orderbook data: + assert.Equal(t, 16, partialAggMkt.NumBids) + assert.Equal(t, 0.25, partialAggMkt.BidVolume) + assert.Equal(t, 200.0, partialAggMkt.HighestBid) + assert.Equal(t, 18, partialAggMkt.NumAsks) + assert.Equal(t, 45.0, partialAggMkt.AskVolume) + assert.Equal(t, 0.1, partialAggMkt.LowestAsk) } diff --git a/exp/ticker/internal/tickerdb/queries_orderbook.go b/exp/ticker/internal/tickerdb/queries_orderbook.go new file mode 100644 index 0000000000..e31dfee1c9 --- /dev/null +++ b/exp/ticker/internal/tickerdb/queries_orderbook.go @@ -0,0 +1,7 @@ +package tickerdb + +// InsertOrUpdateOrderbookStats inserts an OrdebookStats entry on the database (if new), +// or updates an existing one +func (s *TickerSession) InsertOrUpdateOrderbookStats(o *OrderbookStats, preserveFields []string) (err error) { + return s.performUpsertQuery(*o, "orderbook_stats", "orderbook_stats_base_counter_asset_key", preserveFields) +} diff --git a/exp/ticker/internal/tickerdb/queries_orderbook_test.go b/exp/ticker/internal/tickerdb/queries_orderbook_test.go new file mode 100644 index 0000000000..bc85230656 --- /dev/null +++ b/exp/ticker/internal/tickerdb/queries_orderbook_test.go @@ -0,0 +1,195 @@ +package tickerdb + +import ( + "testing" + "time" + + migrate "github.com/rubenv/sql-migrate" + "github.com/stellar/go/support/db/dbtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInsertOrUpdateOrderbokStats(t *testing.T) { + db := dbtest.Postgres(t) + defer db.Close() + + var session TickerSession + session.DB = db.Open() + defer session.DB.Close() + + // Run migrations to make sure the tests are run + // on the most updated schema version + migrations := &migrate.FileMigrationSource{ + Dir: "./migrations", + } + _, err := migrate.Exec(session.DB.DB, "postgres", migrations, migrate.Up) + require.NoError(t, err) + + publicKey := "GCF3TQXKZJNFJK7HCMNE2O2CUNKCJH2Y2ROISTBPLC7C5EIA5NNG2XZB" + issuerAccount := "AM2FQXKZJNFJK7HCMNE2O2CUNKCJH2Y2ROISTBPLC7C5EIA5NNG2XZB" + name := "FOO BAR" + code := "XLM" + + // Adding a seed issuer to be used later: + issuer := Issuer{ + PublicKey: publicKey, + Name: name, + } + tbl := session.GetTable("issuers") + _, err = tbl.Insert(issuer).IgnoreCols("id").Exec() + require.NoError(t, err) + var dbIssuer Issuer + err = session.GetRaw(&dbIssuer, ` + SELECT * + FROM issuers + ORDER BY id DESC + LIMIT 1`, + ) + require.NoError(t, err) + + // Creating first asset: + firstTime := time.Now() + a := Asset{ + Code: code, + IssuerAccount: issuerAccount, + IssuerID: dbIssuer.ID, + LastValid: firstTime, + LastChecked: firstTime, + } + err = session.InsertOrUpdateAsset(&a, []string{"code", "issuer_account", "issuer_id"}) + require.NoError(t, err) + + var dbAsset1 Asset + err = session.GetRaw(&dbAsset1, ` + SELECT * + FROM assets + ORDER BY id DESC + LIMIT 1`, + ) + require.NoError(t, err) + + assert.Equal(t, code, dbAsset1.Code) + assert.Equal(t, issuerAccount, dbAsset1.IssuerAccount) + assert.Equal(t, dbIssuer.ID, dbAsset1.IssuerID) + assert.Equal( + t, + firstTime.Local().Truncate(time.Millisecond), + dbAsset1.LastValid.Local().Truncate(time.Millisecond), + ) + assert.Equal( + t, + firstTime.Local().Truncate(time.Millisecond), + dbAsset1.LastChecked.Local().Truncate(time.Millisecond), + ) + + // Creating Seconde Asset: + secondTime := time.Now() + a.LastValid = secondTime + a.LastChecked = secondTime + err = session.InsertOrUpdateAsset(&a, []string{"code", "issuer_account", "issuer_id"}) + require.NoError(t, err) + + var dbAsset2 Asset + err = session.GetRaw(&dbAsset2, ` + SELECT * + FROM assets + ORDER BY id DESC + LIMIT 1`, + ) + require.NoError(t, err) + + // Creating an orderbook_stats entry: + obTime := time.Now() + orderbookStats := OrderbookStats{ + BaseAssetID: dbAsset1.ID, + CounterAssetID: dbAsset2.ID, + NumBids: 15, + BidVolume: 0.15, + HighestBid: 200.0, + NumAsks: 17, + AskVolume: 30.0, + LowestAsk: 0.1, + Spread: 0.93, + SpreadMidPoint: 0.35, + UpdatedAt: obTime, + } + err = session.InsertOrUpdateOrderbookStats( + &orderbookStats, + []string{"base_asset_id", "counter_asset_id"}, + ) + require.NoError(t, err) + + var dbOS OrderbookStats + err = session.GetRaw(&dbOS, ` + SELECT * + FROM orderbook_stats + ORDER BY id DESC + LIMIT 1`, + ) + require.NoError(t, err) + + assert.Equal(t, dbAsset1.ID, dbOS.BaseAssetID) + assert.Equal(t, dbAsset2.ID, dbOS.CounterAssetID) + assert.Equal(t, 15, dbOS.NumBids) + assert.Equal(t, 0.15, dbOS.BidVolume) + assert.Equal(t, 200.0, dbOS.HighestBid) + assert.Equal(t, 17, dbOS.NumAsks) + assert.Equal(t, 30.0, dbOS.AskVolume) + assert.Equal(t, 0.1, dbOS.LowestAsk) + assert.Equal(t, 0.93, dbOS.Spread) + assert.Equal(t, 0.35, dbOS.SpreadMidPoint) + assert.Equal( + t, + obTime.Local().Truncate(time.Millisecond), + dbOS.UpdatedAt.Local().Truncate(time.Millisecond), + ) + + // Making sure we're upserting: + obTime2 := time.Now() + orderbookStats2 := OrderbookStats{ + BaseAssetID: dbAsset1.ID, + CounterAssetID: dbAsset2.ID, + NumBids: 30, + BidVolume: 0.3, + HighestBid: 400.0, + NumAsks: 34, + AskVolume: 60.0, + LowestAsk: 0.2, + Spread: 1.86, + SpreadMidPoint: 0.7, + UpdatedAt: obTime2, + } + err = session.InsertOrUpdateOrderbookStats( + &orderbookStats2, + []string{"base_asset_id", "counter_asset_id", "lowest_ask"}, + ) + require.NoError(t, err) + + var dbOS2 OrderbookStats + err = session.GetRaw(&dbOS2, ` + SELECT * + FROM orderbook_stats + ORDER BY id DESC + LIMIT 1`, + ) + require.NoError(t, err) + + assert.Equal(t, dbOS2.ID, dbOS.ID) // shouldn't create another instance + + assert.Equal(t, dbAsset1.ID, dbOS2.BaseAssetID) + assert.Equal(t, dbAsset2.ID, dbOS2.CounterAssetID) + assert.Equal(t, 30, dbOS2.NumBids) + assert.Equal(t, 0.3, dbOS2.BidVolume) + assert.Equal(t, 400.0, dbOS2.HighestBid) + assert.Equal(t, 34, dbOS2.NumAsks) + assert.Equal(t, 60.0, dbOS2.AskVolume) + assert.Equal(t, 0.1, dbOS2.LowestAsk) // should keep the old value, since on preserveFields + assert.Equal(t, 1.86, dbOS2.Spread) + assert.Equal(t, 0.7, dbOS2.SpreadMidPoint) + assert.Equal( + t, + obTime2.Local().Truncate(time.Millisecond), + dbOS2.UpdatedAt.Local().Truncate(time.Millisecond), + ) +} diff --git a/exp/ticker/internal/utils/main.go b/exp/ticker/internal/utils/main.go index 348980b0c2..d29942ce7f 100644 --- a/exp/ticker/internal/utils/main.go +++ b/exp/ticker/internal/utils/main.go @@ -58,3 +58,13 @@ func GetAssetString(assetType string, code string, issuer string) string { func TimeToUnixEpoch(t time.Time) int64 { return t.UnixNano() / 1000000 } + +// CalcSpread calculates the spread stats for the given bidMax and askMin orderbook values +func CalcSpread(bidMax float64, askMin float64) (spread float64, midPoint float64) { + if askMin == 0 || bidMax == 0 { + return 0, 0 + } + spread = (askMin - bidMax) / askMin + midPoint = bidMax + spread/2.0 + return +} diff --git a/protocols/horizon/effects/main.go b/protocols/horizon/effects/main.go index a33700a7d1..3fa472bc8d 100644 --- a/protocols/horizon/effects/main.go +++ b/protocols/horizon/effects/main.go @@ -1,12 +1,145 @@ package effects import ( + "encoding/json" "time" "github.com/stellar/go/protocols/horizon/base" "github.com/stellar/go/support/render/hal" ) +// Peter 30-04-2019: this is copied from the history package "github.com/stellar/go/services/horizon/internal/db2/history" +// Could not import this because internal package imports must share the same path prefix as the importer. +// Maybe this should be housed here and imported into internal packages? + +// EffectType is the numeric type for an effect +type EffectType int + +const ( + // account effects + + // EffectAccountCreated effects occur when a new account is created + EffectAccountCreated EffectType = 0 // from create_account + + // EffectAccountRemoved effects occur when one account is merged into another + EffectAccountRemoved EffectType = 1 // from merge_account + + // EffectAccountCredited effects occur when an account receives some currency + EffectAccountCredited EffectType = 2 // from create_account, payment, path_payment, merge_account + + // EffectAccountDebited effects occur when an account sends some currency + EffectAccountDebited EffectType = 3 // from create_account, payment, path_payment, create_account + + // EffectAccountThresholdsUpdated effects occur when an account changes its + // multisig thresholds. + EffectAccountThresholdsUpdated EffectType = 4 // from set_options + + // EffectAccountHomeDomainUpdated effects occur when an account changes its + // home domain. + EffectAccountHomeDomainUpdated EffectType = 5 // from set_options + + // EffectAccountFlagsUpdated effects occur when an account changes its + // account flags, either clearing or setting. + EffectAccountFlagsUpdated EffectType = 6 // from set_options + + // EffectAccountInflationDestinationUpdated effects occur when an account changes its + // inflation destination. + EffectAccountInflationDestinationUpdated EffectType = 7 // from set_options + + // signer effects + + // EffectSignerCreated occurs when an account gains a signer + EffectSignerCreated EffectType = 10 // from set_options + + // EffectSignerRemoved occurs when an account loses a signer + EffectSignerRemoved EffectType = 11 // from set_options + + // EffectSignerUpdated occurs when an account changes the weight of one of its + // signers. + EffectSignerUpdated EffectType = 12 // from set_options + + // trustline effects + + // EffectTrustlineCreated occurs when an account trusts an anchor + EffectTrustlineCreated EffectType = 20 // from change_trust + + // EffectTrustlineRemoved occurs when an account removes struct by setting the + // limit of a trustline to 0 + EffectTrustlineRemoved EffectType = 21 // from change_trust + + // EffectTrustlineUpdated occurs when an account changes a trustline's limit + EffectTrustlineUpdated EffectType = 22 // from change_trust, allow_trust + + // EffectTrustlineAuthorized occurs when an anchor has AUTH_REQUIRED flag set + // to true and it authorizes another account's trustline + EffectTrustlineAuthorized EffectType = 23 // from allow_trust + + // EffectTrustlineDeauthorized occurs when an anchor revokes access to a asset + // it issues. + EffectTrustlineDeauthorized EffectType = 24 // from allow_trust + + // trading effects + + // EffectOfferCreated occurs when an account offers to trade an asset + EffectOfferCreated EffectType = 30 // from manage_offer, creat_passive_offer + + // EffectOfferRemoved occurs when an account removes an offer + EffectOfferRemoved EffectType = 31 // from manage_offer, creat_passive_offer, path_payment + + // EffectOfferUpdated occurs when an offer is updated by the offering account. + EffectOfferUpdated EffectType = 32 // from manage_offer, creat_passive_offer, path_payment + + // EffectTrade occurs when a trade is initiated because of a path payment or + // offer operation. + EffectTrade EffectType = 33 // from manage_offer, creat_passive_offer, path_payment + + // data effects + + // EffectDataCreated occurs when an account gets a new data field + EffectDataCreated EffectType = 40 // from manage_data + + // EffectDataRemoved occurs when an account removes a data field + EffectDataRemoved EffectType = 41 // from manage_data + + // EffectDataUpdated occurs when an account changes a data field's value + EffectDataUpdated EffectType = 42 // from manage_data + + // EffectSequenceBumped occurs when an account bumps their sequence number + EffectSequenceBumped EffectType = 43 // from bump_sequence +) + +// Peter 30-04-2019: this is copied from the resourcadapter package +// "github.com/stellar/go/services/horizon/internal/resourceadapter" +// Could not import this because internal package imports must share the same path prefix as the importer. + +// EffectTypeNames stores a map of effect type ID and names +var EffectTypeNames = map[EffectType]string{ + EffectAccountCreated: "account_created", + EffectAccountRemoved: "account_removed", + EffectAccountCredited: "account_credited", + EffectAccountDebited: "account_debited", + EffectAccountThresholdsUpdated: "account_thresholds_updated", + EffectAccountHomeDomainUpdated: "account_home_domain_updated", + EffectAccountFlagsUpdated: "account_flags_updated", + EffectAccountInflationDestinationUpdated: "account_inflation_destination_updated", + EffectSignerCreated: "signer_created", + EffectSignerRemoved: "signer_removed", + EffectSignerUpdated: "signer_updated", + EffectTrustlineCreated: "trustline_created", + EffectTrustlineRemoved: "trustline_removed", + EffectTrustlineUpdated: "trustline_updated", + EffectTrustlineAuthorized: "trustline_authorized", + EffectTrustlineDeauthorized: "trustline_deauthorized", + EffectOfferCreated: "offer_created", + EffectOfferRemoved: "offer_removed", + EffectOfferUpdated: "offer_updated", + EffectTrade: "trade", + EffectDataCreated: "data_created", + EffectDataRemoved: "data_removed", + EffectDataUpdated: "data_updated", + EffectSequenceBumped: "sequence_bumped", +} + // Base provides the common structure for any effect resource effect. type Base struct { Links struct { @@ -23,9 +156,9 @@ type Base struct { LedgerCloseTime time.Time `json:"created_at"` } -// PagingToken implements `hal.Pageable` -func (this Base) PagingToken() string { - return this.PT +// PagingToken implements `hal.Pageable` and Effect +func (b Base) PagingToken() string { + return b.PT } type AccountCreated struct { @@ -135,6 +268,181 @@ type Trade struct { BoughtAssetIssuer string `json:"bought_asset_issuer,omitempty"` } +// Effect contains methods that are implemented by all effect types. +type Effect interface { + PagingToken() string + GetType() string + GetID() string + GetAccount() string +} + +// GetType implements Effect +func (b Base) GetType() string { + return b.Type +} + +// GetID implements Effect +func (b Base) GetID() string { + return b.ID +} + +// GetAccount implements Effect +func (b Base) GetAccount() string { + return b.Account +} + +// EffectsPage contains page of effects returned by Horizon. +type EffectsPage struct { + Links hal.Links `json:"_links"` + Embedded struct { + Records []Effect + } `json:"_embedded"` +} + +// UnmarshalJSON is the custom unmarshal method for EffectsPage +func (effects *EffectsPage) UnmarshalJSON(data []byte) error { + var effectsPage struct { + Links hal.Links `json:"_links"` + Embedded struct { + Records []interface{} + } `json:"_embedded"` + } + + if err := json.Unmarshal(data, &effectsPage); err != nil { + return err + } + + for _, j := range effectsPage.Embedded.Records { + var b Base + dataString, err := json.Marshal(j) + if err != nil { + return err + } + if err = json.Unmarshal(dataString, &b); err != nil { + return err + } + + ef, err := UnmarshalEffect(b.Type, dataString) + if err != nil { + return err + } + + effects.Embedded.Records = append(effects.Embedded.Records, ef) + } + + effects.Links = effectsPage.Links + return nil +} + +// UnmarshalEffect decodes responses to the correct effect struct +func UnmarshalEffect(effectType string, dataString []byte) (effects Effect, err error) { + switch effectType { + case EffectTypeNames[EffectAccountCreated]: + var effect AccountCreated + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectAccountCredited]: + var effect AccountCredited + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectAccountDebited]: + var effect AccountDebited + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectAccountThresholdsUpdated]: + var effect AccountThresholdsUpdated + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectAccountHomeDomainUpdated]: + var effect AccountHomeDomainUpdated + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectAccountFlagsUpdated]: + var effect AccountFlagsUpdated + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectSequenceBumped]: + var effect SequenceBumped + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectSignerCreated]: + var effect SignerCreated + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectSignerRemoved]: + var effect SignerRemoved + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectSignerUpdated]: + var effect SignerUpdated + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectTrustlineAuthorized]: + var effect TrustlineAuthorized + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectTrustlineCreated]: + var effect TrustlineCreated + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectTrustlineDeauthorized]: + var effect TrustlineDeauthorized + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectTrustlineRemoved]: + var effect TrustlineRemoved + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectTrustlineUpdated]: + var effect TrustlineUpdated + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + case EffectTypeNames[EffectTrade]: + var effect Trade + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + default: + var effect Base + if err = json.Unmarshal(dataString, &effect); err != nil { + return + } + effects = effect + } + return +} + // interface implementations var _ base.Rehydratable = &SignerCreated{} var _ base.Rehydratable = &SignerRemoved{} diff --git a/protocols/horizon/main.go b/protocols/horizon/main.go index bbe7094021..b50d51e33d 100644 --- a/protocols/horizon/main.go +++ b/protocols/horizon/main.go @@ -11,7 +11,6 @@ import ( "encoding/json" "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/effects" "github.com/stellar/go/strkey" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/render/hal" @@ -502,13 +501,6 @@ type AccountData struct { Value string `json:"value"` } -// EffectsPage contains page of effects returned by Horizon. -type EffectsPage struct { - Embedded struct { - Records []effects.Base - } `json:"_embedded"` -} - // TradeAggregationsPage returns a list of aggregated trade records, aggregated by resolution type TradeAggregationsPage struct { Links hal.Links `json:"_links"` diff --git a/tools/horizon-cmp/CHANGELOG.md b/tools/horizon-cmp/CHANGELOG.md new file mode 100644 index 0000000000..8c32c333b7 --- /dev/null +++ b/tools/horizon-cmp/CHANGELOG.md @@ -0,0 +1,3 @@ +## 2019-04-25 + +Initial version diff --git a/tools/horizon-cmp/README.md b/tools/horizon-cmp/README.md new file mode 100644 index 0000000000..83654a8ab5 --- /dev/null +++ b/tools/horizon-cmp/README.md @@ -0,0 +1,6 @@ +# Horizon cmp + +Tool that compares the responses of two Horizon servers and shows the diffs. +Useful for checking for regressions. + +TODO: add more info diff --git a/tools/horizon-verify/CHANGELOG.md b/tools/horizon-verify/CHANGELOG.md new file mode 100644 index 0000000000..8c32c333b7 --- /dev/null +++ b/tools/horizon-verify/CHANGELOG.md @@ -0,0 +1,3 @@ +## 2019-04-25 + +Initial version diff --git a/tools/horizon-verify/README.md b/tools/horizon-verify/README.md new file mode 100644 index 0000000000..a04bad962e --- /dev/null +++ b/tools/horizon-verify/README.md @@ -0,0 +1,9 @@ +# Horizon verify + +Tool that checks some invariants about Horizon responses: + +- successful response codes when getting transactions from ledgers +- successful transaction counts are correct +- failed transaction counts are correct + +TODO: add more info diff --git a/txnbuild/CHANGELOG.md b/txnbuild/CHANGELOG.md new file mode 100644 index 0000000000..0f1022183d --- /dev/null +++ b/txnbuild/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this +file. This project adheres to [Semantic Versioning](http://semver.org/). + +## [v1.1.0](https://github.com/stellar/go/releases/tag/horizonclient-v1.1.0) - 2019-02-02 + +* Support for multiple signatures ([#1198](https://github.com/stellar/go/pull/1198)) + +## [v1.0.0](https://github.com/stellar/go/releases/tag/horizonclient-v1.0) - 2019-04-26 + +* Initial release diff --git a/txnbuild/README.md b/txnbuild/README.md index 9543975159..73c96e9cb7 100644 --- a/txnbuild/README.md +++ b/txnbuild/README.md @@ -4,7 +4,7 @@ This project is maintained by the Stellar Development Foundation. -``` +```golang import ( "log" @@ -79,7 +79,9 @@ To see the SDK in action, build and run the demo: ## Contributing -Please read [CONTRIBUTING](../../CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. +Please read [Code of Conduct](https://github.com/stellar/.github/blob/master/CODE_OF_CONDUCT.md) to understand this project's communication rules. + +To submit improvements and fixes to this library, please see [CONTRIBUTING](../CONTRIBUTING.md). ## License This project is licensed under the Apache License - see the [LICENSE](../../LICENSE-APACHE.txt) file for details. diff --git a/txnbuild/account_merge.go b/txnbuild/account_merge.go index 895b3bce39..356c580dd1 100644 --- a/txnbuild/account_merge.go +++ b/txnbuild/account_merge.go @@ -8,7 +8,8 @@ import ( // AccountMerge represents the Stellar merge account operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type AccountMerge struct { - Destination string + Destination string + SourceAccount Account } // BuildXDR for AccountMerge returns a fully configured XDR Operation. @@ -22,6 +23,10 @@ func (am *AccountMerge) BuildXDR() (xdr.Operation, error) { opType := xdr.OperationTypeAccountMerge body, err := xdr.NewOperationBody(opType, xdrOp) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, am.SourceAccount) + return op, nil } diff --git a/txnbuild/allow_trust.go b/txnbuild/allow_trust.go index 7af18a582c..407d2dad34 100644 --- a/txnbuild/allow_trust.go +++ b/txnbuild/allow_trust.go @@ -8,9 +8,10 @@ import ( // AllowTrust represents the Stellar allow trust operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type AllowTrust struct { - Trustor string - Type Asset - Authorize bool + Trustor string + Type Asset + Authorize bool + SourceAccount Account } // BuildXDR for AllowTrust returns a fully configured XDR Operation. @@ -44,6 +45,10 @@ func (at *AllowTrust) BuildXDR() (xdr.Operation, error) { opType := xdr.OperationTypeAllowTrust body, err := xdr.NewOperationBody(opType, xdrOp) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, at.SourceAccount) + return op, nil } diff --git a/txnbuild/bump_sequence.go b/txnbuild/bump_sequence.go index 70bb11a529..cb900d73ab 100644 --- a/txnbuild/bump_sequence.go +++ b/txnbuild/bump_sequence.go @@ -8,7 +8,8 @@ import ( // BumpSequence represents the Stellar bump sequence operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type BumpSequence struct { - BumpTo int64 + BumpTo int64 + SourceAccount Account } // BuildXDR for BumpSequence returns a fully configured XDR Operation. @@ -16,6 +17,10 @@ func (bs *BumpSequence) BuildXDR() (xdr.Operation, error) { opType := xdr.OperationTypeBumpSequence xdrOp := xdr.BumpSequenceOp{BumpTo: xdr.SequenceNumber(bs.BumpTo)} body, err := xdr.NewOperationBody(opType, xdrOp) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, bs.SourceAccount) + return op, nil } diff --git a/txnbuild/change_trust.go b/txnbuild/change_trust.go index 5f37aec990..13ef94e554 100644 --- a/txnbuild/change_trust.go +++ b/txnbuild/change_trust.go @@ -9,8 +9,9 @@ import ( // ChangeTrust represents the Stellar change trust operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type ChangeTrust struct { - Line Asset - Limit string + Line Asset + Limit string + SourceAccount Account } // RemoveTrustlineOp returns a ChangeTrust operation to remove the trustline of the described asset, @@ -43,6 +44,10 @@ func (ct *ChangeTrust) BuildXDR() (xdr.Operation, error) { Limit: xdrLimit, } body, err := xdr.NewOperationBody(opType, xdrOp) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, ct.SourceAccount) + return xdr.Operation{Body: body}, nil } diff --git a/txnbuild/cmd/demo/operations/demo.go b/txnbuild/cmd/demo/operations/demo.go index ae1120b01c..7a3e1c58d7 100644 --- a/txnbuild/cmd/demo/operations/demo.go +++ b/txnbuild/cmd/demo/operations/demo.go @@ -301,7 +301,10 @@ func deleteTrustline(source *hProtocol.Account, asset txnbuild.Asset, signer Acc } func deleteOffer(source *hProtocol.Account, offerID int64, signer Account) (string, error) { - deleteOffer := txnbuild.DeleteOfferOp(offerID) + deleteOffer, err := txnbuild.DeleteOfferOp(offerID) + if err != nil { + return "", errors.Wrap(err, "building offer") + } tx := txnbuild.Transaction{ SourceAccount: source, diff --git a/txnbuild/create_account.go b/txnbuild/create_account.go index 558c43dfa4..0182a6ee53 100644 --- a/txnbuild/create_account.go +++ b/txnbuild/create_account.go @@ -9,8 +9,9 @@ import ( // CreateAccount represents the Stellar create account operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type CreateAccount struct { - Destination string - Amount string + Destination string + Amount string + SourceAccount Account } // BuildXDR for CreateAccount returns a fully configured XDR Operation. @@ -29,6 +30,10 @@ func (ca *CreateAccount) BuildXDR() (xdr.Operation, error) { opType := xdr.OperationTypeCreateAccount body, err := xdr.NewOperationBody(opType, xdrOp) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, ca.SourceAccount) + return op, nil } diff --git a/txnbuild/create_passive_offer.go b/txnbuild/create_passive_offer.go index 05b2c7ef05..bb68be0697 100644 --- a/txnbuild/create_passive_offer.go +++ b/txnbuild/create_passive_offer.go @@ -10,10 +10,11 @@ import ( // CreatePassiveSellOffer represents the Stellar create passive offer operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type CreatePassiveSellOffer struct { - Selling Asset - Buying Asset - Amount string - Price string + Selling Asset + Buying Asset + Amount string + Price string + SourceAccount Account } // BuildXDR for CreatePassiveSellOffer returns a fully configured XDR Operation. @@ -47,6 +48,10 @@ func (cpo *CreatePassiveSellOffer) BuildXDR() (xdr.Operation, error) { opType := xdr.OperationTypeCreatePassiveSellOffer body, err := xdr.NewOperationBody(opType, xdrOp) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, cpo.SourceAccount) + return op, nil } diff --git a/txnbuild/example_test.go b/txnbuild/example_test.go index 547f067445..0ce3c7ee16 100644 --- a/txnbuild/example_test.go +++ b/txnbuild/example_test.go @@ -305,7 +305,8 @@ func ExampleManageSellOffer() { buying := CreditAsset{"ABCD", "GAS4V4O2B7DW5T7IQRPEEVCRXMDZESKISR7DVIGKZQYYV3OSQ5SH5LVP"} sellAmount := "100" price := "0.01" - op := CreateOfferOp(selling, buying, sellAmount, price) + op, err := CreateOfferOp(selling, buying, sellAmount, price) + check(err) tx := Transaction{ SourceAccount: &sourceAccount, @@ -329,7 +330,8 @@ func ExampleManageSellOffer_deleteOffer() { check(err) offerID := int64(2921622) - op := DeleteOfferOp(offerID) + op, err := DeleteOfferOp(offerID) + check(err) tx := Transaction{ SourceAccount: &sourceAccount, @@ -357,7 +359,8 @@ func ExampleManageSellOffer_updateOffer() { sellAmount := "50" price := "0.02" offerID := int64(2497628) - op := UpdateOfferOp(selling, buying, sellAmount, price, offerID) + op, err := UpdateOfferOp(selling, buying, sellAmount, price, offerID) + check(err) tx := Transaction{ SourceAccount: &sourceAccount, diff --git a/txnbuild/helpers_test.go b/txnbuild/helpers_test.go index 77d7881286..841ea27288 100644 --- a/txnbuild/helpers_test.go +++ b/txnbuild/helpers_test.go @@ -27,18 +27,14 @@ func newKeypair(seed string) *keypair.Full { return myKeypair.(*keypair.Full) } -func buildSignEncode(tx Transaction, kp *keypair.Full, t *testing.T) (txeBase64 string) { - var err error - err = tx.Build() - assert.NoError(t, err) - - err = tx.Sign(kp) - assert.NoError(t, err) +func buildSignEncode(t *testing.T, tx Transaction, kps ...*keypair.Full) string { + assert.NoError(t, tx.Build()) + assert.NoError(t, tx.Sign(kps...)) - txeBase64, err = tx.Base64() + txeBase64, err := tx.Base64() assert.NoError(t, err) - return + return txeBase64 } func check(err error) { diff --git a/txnbuild/inflation.go b/txnbuild/inflation.go index 31e90d713d..c8f9ee8116 100644 --- a/txnbuild/inflation.go +++ b/txnbuild/inflation.go @@ -7,12 +7,18 @@ import ( // Inflation represents the Stellar inflation operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html -type Inflation struct{} +type Inflation struct { + SourceAccount Account +} // BuildXDR for Inflation returns a fully configured XDR Operation. func (inf *Inflation) BuildXDR() (xdr.Operation, error) { opType := xdr.OperationTypeInflation body, err := xdr.NewOperationBody(opType, nil) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, inf.SourceAccount) + return op, nil } diff --git a/txnbuild/manage_data.go b/txnbuild/manage_data.go index 43a3a194f8..81177e9509 100644 --- a/txnbuild/manage_data.go +++ b/txnbuild/manage_data.go @@ -8,8 +8,9 @@ import ( // ManageData represents the Stellar manage data operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type ManageData struct { - Name string - Value []byte + Name string + Value []byte + SourceAccount Account } // BuildXDR for ManageData returns a fully configured XDR Operation. @@ -26,6 +27,10 @@ func (md *ManageData) BuildXDR() (xdr.Operation, error) { opType := xdr.OperationTypeManageData body, err := xdr.NewOperationBody(opType, xdrOp) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, md.SourceAccount) + return op, nil } diff --git a/txnbuild/manage_offer.go b/txnbuild/manage_offer.go index 048eed7ecb..f65bfaf699 100644 --- a/txnbuild/manage_offer.go +++ b/txnbuild/manage_offer.go @@ -8,52 +8,78 @@ import ( ) //CreateOfferOp returns a ManageSellOffer operation to create a new offer, by -// setting the OfferID to "0". -func CreateOfferOp(selling, buying Asset, amount, price string) ManageSellOffer { - return ManageSellOffer{ +// setting the OfferID to "0". The sourceAccount is optional, and if not provided, +// will be that of the surrounding transaction. +func CreateOfferOp(selling, buying Asset, amount, price string, sourceAccount ...Account) (ManageSellOffer, error) { + if len(sourceAccount) > 1 { + return ManageSellOffer{}, errors.New("offer can't have multiple source accounts") + } + offer := ManageSellOffer{ Selling: selling, Buying: buying, Amount: amount, Price: price, OfferID: 0, } + if len(sourceAccount) == 1 { + offer.SourceAccount = sourceAccount[0] + } + return offer, nil } -//UpdateOfferOp returns a ManageSellOffer operation to update an offer. -func UpdateOfferOp(selling, buying Asset, amount, price string, offerID int64) ManageSellOffer { - return ManageSellOffer{ +// UpdateOfferOp returns a ManageSellOffer operation to update an offer. +// The sourceAccount is optional, and if not provided, will be that of +// the surrounding transaction. +func UpdateOfferOp(selling, buying Asset, amount, price string, offerID int64, sourceAccount ...Account) (ManageSellOffer, error) { + if len(sourceAccount) > 1 { + return ManageSellOffer{}, errors.New("offer can't have multiple source accounts") + } + offer := ManageSellOffer{ Selling: selling, Buying: buying, Amount: amount, Price: price, OfferID: offerID, } + if len(sourceAccount) == 1 { + offer.SourceAccount = sourceAccount[0] + } + return offer, nil } //DeleteOfferOp returns a ManageSellOffer operation to delete an offer, by -// setting the Amount to "0". -func DeleteOfferOp(offerID int64) ManageSellOffer { +// setting the Amount to "0". The sourceAccount is optional, and if not provided, +// will be that of the surrounding transaction. +func DeleteOfferOp(offerID int64, sourceAccount ...Account) (ManageSellOffer, error) { // It turns out Stellar core doesn't care about any of these fields except the amount. // However, Horizon will reject ManageSellOffer if it is missing fields. // Horizon will also reject if Buying == Selling. // Therefore unfortunately we have to make up some dummy values here. - return ManageSellOffer{ + if len(sourceAccount) > 1 { + return ManageSellOffer{}, errors.New("offer can't have multiple source accounts") + } + offer := ManageSellOffer{ Selling: NativeAsset{}, Buying: CreditAsset{Code: "FAKE", Issuer: "GBAQPADEYSKYMYXTMASBUIS5JI3LMOAWSTM2CHGDBJ3QDDPNCSO3DVAA"}, Amount: "0", Price: "1", OfferID: offerID, } + if len(sourceAccount) == 1 { + offer.SourceAccount = sourceAccount[0] + } + return offer, nil } // ManageSellOffer represents the Stellar manage offer operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type ManageSellOffer struct { - Selling Asset - Buying Asset - Amount string - Price string - OfferID int64 + Selling Asset + Buying Asset + Amount string + Price string + OfferID int64 + SourceAccount Account } // BuildXDR for ManageSellOffer returns a fully configured XDR Operation. @@ -87,6 +113,11 @@ func (mo *ManageSellOffer) BuildXDR() (xdr.Operation, error) { OfferId: xdr.Int64(mo.OfferID), } body, err := xdr.NewOperationBody(opType, xdrOp) + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, mo.SourceAccount) + return op, nil } diff --git a/txnbuild/operation.go b/txnbuild/operation.go index 56af68102a..8f835351cd 100644 --- a/txnbuild/operation.go +++ b/txnbuild/operation.go @@ -8,3 +8,13 @@ import ( type Operation interface { BuildXDR() (xdr.Operation, error) } + +// SetOpSourceAccount sets the source account ID on an Operation. +func SetOpSourceAccount(op *xdr.Operation, sourceAccount Account) { + if sourceAccount == nil { + return + } + var opSourceAccountID xdr.AccountId + opSourceAccountID.SetAddress(sourceAccount.GetAccountID()) + op.SourceAccount = &opSourceAccountID +} diff --git a/txnbuild/path_payment.go b/txnbuild/path_payment.go index eab706731f..f330e0b223 100644 --- a/txnbuild/path_payment.go +++ b/txnbuild/path_payment.go @@ -9,12 +9,13 @@ import ( // PathPayment represents the Stellar path payment operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type PathPayment struct { - SendAsset Asset - SendMax string - Destination string - DestAsset Asset - DestAmount string - Path []Asset + SendAsset Asset + SendMax string + Destination string + DestAsset Asset + DestAmount string + Path []Asset + SourceAccount Account } // BuildXDR for Payment returns a fully configured XDR Operation. @@ -77,6 +78,10 @@ func (pp *PathPayment) BuildXDR() (xdr.Operation, error) { Path: xdrPath, } body, err := xdr.NewOperationBody(opType, xdrOp) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, pp.SourceAccount) + return op, nil } diff --git a/txnbuild/payment.go b/txnbuild/payment.go index f77417028f..2efd8edcf5 100644 --- a/txnbuild/payment.go +++ b/txnbuild/payment.go @@ -9,9 +9,10 @@ import ( // Payment represents the Stellar payment operation. See // https://www.stellar.org/developers/guides/concepts/list-of-operations.html type Payment struct { - Destination string - Amount string - Asset Asset + Destination string + Amount string + Asset Asset + SourceAccount Account } // BuildXDR for Payment returns a fully configured XDR Operation. @@ -43,6 +44,10 @@ func (p *Payment) BuildXDR() (xdr.Operation, error) { Asset: xdrAsset, } body, err := xdr.NewOperationBody(opType, xdrOp) - - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR Operation") + } + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, p.SourceAccount) + return op, nil } diff --git a/txnbuild/set_options.go b/txnbuild/set_options.go index e20839bdbe..d4ca81b214 100644 --- a/txnbuild/set_options.go +++ b/txnbuild/set_options.go @@ -60,6 +60,7 @@ type SetOptions struct { HomeDomain *string Signer *Signer xdrOp xdr.SetOptionsOp + SourceAccount Account } // BuildXDR for SetOptions returns a fully configured XDR Operation. @@ -86,8 +87,13 @@ func (so *SetOptions) BuildXDR() (xdr.Operation, error) { opType := xdr.OperationTypeSetOptions body, err := xdr.NewOperationBody(opType, so.xdrOp) + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR OperationBody") + } - return xdr.Operation{Body: body}, errors.Wrap(err, "failed to build XDR OperationBody") + op := xdr.Operation{Body: body} + SetOpSourceAccount(&op, so.SourceAccount) + return op, nil } // handleInflation for SetOptions sets the XDR inflation destination. diff --git a/txnbuild/signers_test.go b/txnbuild/signers_test.go new file mode 100644 index 0000000000..a99634e6de --- /dev/null +++ b/txnbuild/signers_test.go @@ -0,0 +1,364 @@ +package txnbuild + +import ( + "testing" + + "github.com/stellar/go/network" + "github.com/stretchr/testify/assert" +) + +func TestAccountMergeMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + accountMerge := AccountMerge{ + Destination: "GAS4V4O2B7DW5T7IQRPEEVCRXMDZESKISR7DVIGKZQYYV3OSQ5SH5LVP", + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&accountMerge}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAAIAAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAAAAAAALqLnLFAAAAQAES8MwTufP7l2Rlbg4+1klxAeGgSyTb+vdGI7Or/Lp5xHGZwQ/KvWo0W1ot4hy+WkdJBCD1VF53skB4ZYTPFAnSh2R+AAAAQGPvZk8T2GDp2BpYGeS85VAV2UGKzyjGowt+YOfJwKbW5fjo+GLe47obXEEYxCQDZIsmwG4u5tJ9FUbjuvqi/g0=" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestAllowTrustMultSigners(t *testing.T) { + kp0 := newKeypair0() + opSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + txSourceAccount := makeTestAccount(kp1, "9606132444168199") + + issuedAsset := CreditAsset{"ABCD", kp1.Address()} + allowTrust := AllowTrust{ + Trustor: kp1.Address(), + Type: issuedAsset, + Authorize: true, + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&allowTrust}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAZAAiILoAAAAIAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAA4Nxt4XJcrGZRYrUvrOc1sooiQ+QdEk1suS1wo+oucsUAAAAHAAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAAUFCQ0QAAAABAAAAAAAAAALqLnLFAAAAQHm+8kcSuOMVfthbNRu5ItzonA0ACvL58h4lC6K0JG6OCSR5gRbLUOMqVu1xpQZu+6t9pHwKN9QoEPoXviT3rgDSh2R+AAAAQCr0qzbX9xroeFOzliJgb7+dZJEjyZMpmF3b90NwlEWtm4KPu+U2Lvr91ImeOYtt1/UGksDlGC+3aFq3FsbKBg8=" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestBumpSequenceMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + bumpSequence := BumpSequence{ + BumpTo: 9606132444168300, + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&bumpSequence}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAALACIgugAAAGwAAAAAAAAAAuoucsUAAABA5wbLXDFQdTkJ0Oo3mkW6VrcFeylOag0urj6lKXaQV3mGdFQA4J9OezChx5DynW+FxQtuyXbSBYTcgXUADapSCdKHZH4AAABAuuYmmuuwkMBGC3oX4RA6ZkM5PfYrdUuuAhEvOnuanfyynrOgD/RPs0ROOpd7PAOuZiSkWlJZPUCaJTCo8QZdDg==" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestChangeTrustMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + changeTrust := ChangeTrust{ + Line: CreditAsset{"ABCD", kp0.Address()}, + Limit: "10", + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&changeTrust}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAGAAAAAUFCQ0QAAAAA4Nxt4XJcrGZRYrUvrOc1sooiQ+QdEk1suS1wo+oucsUAAAAABfXhAAAAAAAAAAAC6i5yxQAAAEBGh9ocFFyY4gH19pd+mVn6cUbOxlp6K4e3zNHfYd/WJ22nqpD89FBDa+iuSQGGpeqEGWELdiMqY6lYWsN2sisD0odkfgAAAEDXG99N0TmrMCyg7OixF0COempqsfraKGbRnQBtYHeH4ZauJzm81kshSRnHGenlrWM0KzMtevfeLGBFnA1Y/s4I" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestCreateAccountMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + createAccount := CreateAccount{ + Destination: "GCCOBXW2XQNUSL467IEILE6MMCNRR66SSVL4YQADUNYYNUVREF3FIV2Z", + Amount: "10", + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&createAccount}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAAAAAAAAITg3tq8G0kvnvoIhZPMYJsY+9KVV8xAA6NxhtKxIXZUAAAAAAX14QAAAAAAAAAAAuoucsUAAABANXxsuIht++BXo21iiKkj0lrhVCYNdbD/uBPbL7AXKoleT1cynaR7luA74npsMfzE9AVFr+VclOY+dzQBqIWZDNKHZH4AAABA0EQ/a/U49VgXN6kAHnxMIfy/7rATGCk+stqym2Pa6fcbIKIFyoTRVi+uPTkIcS0u1wL1FvkWuU4YbfbtUPJ5Aw==" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestCreatePassiveSellOfferMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + createPassiveOffer := CreatePassiveSellOffer{ + Selling: NativeAsset{}, + Buying: CreditAsset{"ABCD", kp0.Address()}, + Amount: "10", + Price: "1.0", + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&createPassiveOffer}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAAEAAAAAAAAAAFBQkNEAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAAAX14QAAAAABAAAAAQAAAAAAAAAC6i5yxQAAAEDQA9vz3Yvc1g/xjJdkyiegE5gw4y1RmGmM6d9Kd+i7FD+i0WdGyzkxf9GjrDprUQj1/iDFGE2HpYOb5Zd5UUcP0odkfgAAAECq+9bggD7neBxaDYO4kxR/ltLjqBucqqAbYcIY7bwnGy32Ca/jvsglwnU2UgX3qhCEHSshN21bsGI/h5f+xcUH" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestInflationMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + inflation := Inflation{ + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&inflation}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAAJAAAAAAAAAALqLnLFAAAAQA10jZSBnnrqNR6RH6tbivfJropBxXa2KcH2yN9J3mBGoIxgBSyBCYEc9qKWnkrmZUGvmbTO+LNOd+PdQ4Y8CAPSh2R+AAAAQJ/b1BbMNzsXMGqGELrXH8bcEM5nXa+jq06xrMnIbgMRNRsj+NhSyKifzWP0PQQvHQ4C2yw6y+HBfWvwzAFPhQE=" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestManageDataMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + manageData := ManageData{ + Name: "Fruit preference", + Value: []byte("Apple"), + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&manageData}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAAKAAAAEEZydWl0IHByZWZlcmVuY2UAAAABAAAABUFwcGxlAAAAAAAAAAAAAALqLnLFAAAAQLCtVrhLtpfRGrFhiiGJq891ewFu6ju63po2fgjnYHgA2MgSFJ5rRo3H09uIyWpcmEeeBwXihnx6Ahh9R/pDng7Sh2R+AAAAQOSrmSI0GjSDSgp0+RFwrZYBgqjVtEE5aeCebZ3KR0JCEYXwzIG/q9t1WGz5/zyd8BmmqI1+rP0R6QSp1kDyhwo=" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestManageOfferCreateMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + selling := NativeAsset{} + buying := CreditAsset{"ABCD", kp0.Address()} + sellAmount := "100" + price := "0.01" + createOffer, err := CreateOfferOp(selling, buying, sellAmount, price, &opSourceAccount) + check(err) + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&createOffer}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAADAAAAAAAAAAFBQkNEAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAADuaygAAAAABAAAAZAAAAAAAAAAAAAAAAAAAAALqLnLFAAAAQBo6hZfPKEWgmpWi/TCZonjsQz/w3TCCg2Qcn218b/vCq6OjTezTukCJzJZuhEI7k/STp1/dEptolP9ysGsqegjSh2R+AAAAQLAcnq3rskk4p7shyvfRLuNnK1XgOnVtvho24UW6pqflv+wRaVWJg7Vp848Gi5bBFB8mPJRYMa3lbL78n4wYJA0=" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestManageOfferDeleteMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + offerID := int64(2921622) + deleteOffer, err := DeleteOfferOp(offerID, &opSourceAccount) + check(err) + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&deleteOffer}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAADAAAAAAAAAAFGQUtFAAAAAEEHgGTElYZi82AkGiJdSja2OBaU2aEcwwp3AY3tFJ2xAAAAAAAAAAAAAAABAAAAAQAAAAAALJSWAAAAAAAAAALqLnLFAAAAQFp2K2fnu4AY2GsFb5BL8b8uDwCoHvcdHi+e7oedEp524sJCws4nPwEWLFu5DJcMqLCvFkr5UgwtQwFrNJmMqwbSh2R+AAAAQLtQ2CiOJOG5OsmWydFWFUads6QJj51RcJbJb0mCyDewWBpZRLmh45IMyMRlMJoxk8wKJK4UR2Sfolz7aMmjrw4=" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestManageOfferUpdateMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + selling := NativeAsset{} + buying := CreditAsset{"ABCD", kp0.Address()} + sellAmount := "50" + price := "0.02" + offerID := int64(2497628) + updateOffer, err := UpdateOfferOp(selling, buying, sellAmount, price, offerID, &opSourceAccount) + check(err) + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&updateOffer}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAADAAAAAAAAAAFBQkNEAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAAB3NZQAAAAABAAAAMgAAAAAAJhxcAAAAAAAAAALqLnLFAAAAQOEvekP35V6i3XzbXLdxC5BHFg1pQkhC35KXHJKDYdXwGb5YjHh5amYL78JLtrmswu7NbpWz3MY/rbxFn+8I3gHSh2R+AAAAQLU+5Xee25nSTbRJnWx5zCtkIQ7KzDnb/V/r9nizHsizneito26JQqeEKBH/qz88d3kQxWWC4Lf053tPD6d+SA0=" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestPathPaymentMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + abcdAsset := CreditAsset{"ABCD", kp0.Address()} + pathPayment := PathPayment{ + SendAsset: NativeAsset{}, + SendMax: "10", + Destination: kp0.Address(), + DestAsset: NativeAsset{}, + DestAmount: "1", + Path: []Asset{abcdAsset}, + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&pathPayment}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAACAAAAAAAAAAAF9eEAAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAAAAAAAAAmJaAAAAAAQAAAAFBQkNEAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAAAAAAALqLnLFAAAAQKYqPzdMAYo7NrrOOE2HnXRzCIBixIT9jWteNysju07WVcGpJhoLJW597UrMlsRVWLB/QJk6e6jw6SzLRDXw5wLSh2R+AAAAQMz9ZqejVTk9KiWZCv0e/hoW+F4ua2mM6tHV/kuzCB9HqVGglbK9xN0aOGnrQwvwlp824cVOYnUkV8+HfwsnQgM=" + + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestPaymentMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + payment := Payment{ + Destination: "GB7BDSZU2Y27LYNLALKKALB52WS2IZWYBDGY6EQBLEED3TJOCVMZRH7H", + Amount: "10", + Asset: NativeAsset{}, + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&payment}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAABAAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAAAAAAAAF9eEAAAAAAAAAAALqLnLFAAAAQHzYkZeogiHztanqRvrXXxiNShH/Zf5EUjgabrb6wwgX1eOUBRjp5J92qq8s/o1B1sxrMNiPpViAq40tD/yGfwjSh2R+AAAAQNVC6YLIbAnFs3G/rdf7IxrWYFOxjOKUSZsN0q1Bm/MXk+7ydhcCbYBgq+VGa6eZf8BckgIdAtDI8VNWPoTyhAM=" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} + +func TestSetOptionsMultSigners(t *testing.T) { + kp0 := newKeypair0() + txSourceAccount := makeTestAccount(kp0, "9605939170639898") + + kp1 := newKeypair1() + opSourceAccount := makeTestAccount(kp1, "9606132444168199") + + setOptions := SetOptions{ + SetFlags: []AccountFlag{AuthRequired, AuthRevocable}, + SourceAccount: &opSourceAccount, + } + + tx := Transaction{ + SourceAccount: &txSourceAccount, + Operations: []Operation{&setOptions}, + Timebounds: NewInfiniteTimeout(), + Network: network.TestNetworkPassphrase, + } + + received := buildSignEncode(t, tx, kp0, kp1) + expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAAFAAAAAAAAAAAAAAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC6i5yxQAAAEA7Wgrkr6q1o1Cf9rzfopqkIUQWD9Se3TagU2GhMn9OjGT75flGAaOdQ+kHLDGQjThDKMMdB8jCJGe8IGc/dIQP0odkfgAAAEDni8seENXmyh0QgHkLjM4EmhHmBr5NvU6VpJaVBfv631yaaHP7lONfg9x8DyHjz8uh03S7ipShHIrQDFN+L+cM" + assert.Equal(t, expected, received, "Base 64 XDR should match") +} diff --git a/txnbuild/transaction.go b/txnbuild/transaction.go index b2537de9f2..0f44162a6e 100644 --- a/txnbuild/transaction.go +++ b/txnbuild/transaction.go @@ -128,7 +128,7 @@ func (tx *Transaction) Build() error { // Sign for Transaction signs a previously built transaction. A signed transaction may be // submitted to the network. -func (tx *Transaction) Sign(kp *keypair.Full) error { +func (tx *Transaction) Sign(kps ...*keypair.Full) error { // TODO: Only sign if Transaction has been previously built // TODO: Validate network set before sign // Initialise transaction envelope @@ -144,27 +144,27 @@ func (tx *Transaction) Sign(kp *keypair.Full) error { } // Sign the hash - // TODO: Allow multiple signers - sig, err := kp.SignDecorated(hash[:]) - if err != nil { - return errors.Wrap(err, "failed to sign transaction") + for _, kp := range kps { + sig, err := kp.SignDecorated(hash[:]) + if err != nil { + return errors.Wrap(err, "failed to sign transaction") + } + // Append the signature to the envelope + tx.xdrEnvelope.Signatures = append(tx.xdrEnvelope.Signatures, sig) } - // Append the signature to the envelope - tx.xdrEnvelope.Signatures = append(tx.xdrEnvelope.Signatures, sig) - return nil } // BuildSignEncode performs all the steps to produce a final transaction suitable // for submitting to the network. -func (tx *Transaction) BuildSignEncode(keypair *keypair.Full) (string, error) { +func (tx *Transaction) BuildSignEncode(keypairs ...*keypair.Full) (string, error) { err := tx.Build() if err != nil { return "", errors.Wrap(err, "couldn't build transaction") } - err = tx.Sign(keypair) + err = tx.Sign(keypairs...) if err != nil { return "", errors.Wrap(err, "couldn't sign transaction") } diff --git a/txnbuild/transaction_test.go b/txnbuild/transaction_test.go index f07eb5039c..479312546a 100644 --- a/txnbuild/transaction_test.go +++ b/txnbuild/transaction_test.go @@ -32,7 +32,7 @@ func TestInflation(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) // https://www.stellar.org/laboratory/#xdr-viewer?input=AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAMoj8AAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAJAAAAAAAAAAHqLnLFAAAAQP3NHWXvzKIHB3%2BjjhHITdc%2FtBPntWYj3SoTjpON%2BdxjKqU5ohFamSHeqi5ONXkhE9Uajr5sVZXjQfUcTTzsWAA%3D&type=TransactionEnvelope expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAMoj8AAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAJAAAAAAAAAAHqLnLFAAAAQP3NHWXvzKIHB3+jjhHITdc/tBPntWYj3SoTjpON+dxjKqU5ohFamSHeqi5ONXkhE9Uajr5sVZXjQfUcTTzsWAA=" assert.Equal(t, expected, received, "Base 64 XDR should match") @@ -54,7 +54,7 @@ func TestCreateAccount(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAaAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAITg3tq8G0kvnvoIhZPMYJsY+9KVV8xAA6NxhtKxIXZUAAAAAAX14QAAAAAAAAAAAeoucsUAAABAHsyMojA0Q5MiNsR5X5AiNpCn9mlXmqluRsNpTniCR91M4U5TFmrrqVNLkU58/l+Y8hUPwidDTRSzLZKbMUL/Bw==" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -76,7 +76,7 @@ func TestPayment(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAiII0AAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAAAAAAAAF9eEAAAAAAAAAAAHqLnLFAAAAQNcGQpjNOFCLf9eEmobN+H8SNoDH/jMrfEFPX8kM212ST+TGfirEdXH77GJXvaWplfGKmE3B+UDwLuYLwO+KbQQ=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -117,7 +117,7 @@ func TestBumpSequence(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp1, t) + received := buildSignEncode(t, tx, kp1) expected := "AAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAZAAiILoAAAAIAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAALACIgugAAAGwAAAAAAAAAAdKHZH4AAABAndjSSWeACpbr0ROAEK6jw5CzHiL/rCDpa6AO05+raHDowSUJBckkwlEuCjbBoO/A06tZNRT1Per3liTQrc8fCg==" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -137,7 +137,7 @@ func TestAccountMerge(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAALAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAAAAAAAHqLnLFAAAAQJ/UcOgE64+GQpwv0uXXa2jrKtFdmDsyZ6ZZ/udxryPS8cNCm2L784ixPYM4XRgkoQCdxC3YK8n5x5+CXLzrrwA=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -158,7 +158,7 @@ func TestManageData(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) // https://www.stellar.org/laboratory/#txsigner?xdr=AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAMoj8AAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAKAAAAEEZydWl0IHByZWZlcmVuY2UAAAABAAAABUFwcGxlAAAAAAAAAAAAAAA%3D expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAMoj8AAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAKAAAAEEZydWl0IHByZWZlcmVuY2UAAAABAAAABUFwcGxlAAAAAAAAAAAAAAHqLnLFAAAAQO1ELJBEoqBDyIsS7uSJwe1LOimV/E+09MyF1G/+yrxSggFVPEjD5LXcm/6POze3IsMuIYJU1et5Q2Vt9f73zQo=" assert.Equal(t, expected, received, "Base 64 XDR should match") @@ -179,7 +179,7 @@ func TestManageDataRemoveDataEntry(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAAWAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAKAAAAEEZydWl0IHByZWZlcmVuY2UAAAAAAAAAAAAAAAHqLnLFAAAAQB8rkFZgtffUTdCASzwJ3jRcMCzHpVbbuFbye7Ki2dLao6u5d2aSzz3M2ugNJjNFMfSu3io9adCqwVKKjk0UJQA=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -200,7 +200,7 @@ func TestSetOptionsInflationDestination(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAAcAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAFAAAAAQAAAAAlyvHaD8duz+iEXkJUUbsHkklIlH46oMrMMYrt0odkfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHqLnLFAAAAQB0RLe9DjdHzLM22whFja3ZT97L/818lvWpk5EOTETr9lmDH7/A0/EAzeCkTBzZMCi3C6pV1PrGBr0NJdRrPowg=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -220,7 +220,7 @@ func TestSetOptionsSetFlags(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAAfAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAFAAAAAAAAAAAAAAABAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6i5yxQAAAECfYTppxtp1A2zSbb6VzkOkyk9D/7xjaXRxR+ZIqgdK3lWkHQRkjyVBj2yaI61J3trdp7CswImptjkjLprt0WIO" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -240,7 +240,7 @@ func TestSetOptionsClearFlags(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAAgAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAFAAAAAAAAAAEAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6i5yxQAAAEANXPAN+RgvqjGF0kJ6MyNTiMnWaELw5vYNwxhv8+mi3KmGWMzojCxcmMAqni0zBMsEjl9z7H8JT9x05OlQ9nsD" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -260,7 +260,7 @@ func TestSetOptionsMasterWeight(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAAhAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAQAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6i5yxQAAAECIxH2W4XZ5fMsG658hdIEys2nlVSAK1FEjT5GADF6sWEThGFc+Wrmlw6GwKn6ZNAmxVULEgircjQx48aYSgFYD" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -282,7 +282,7 @@ func TestSetOptionsThresholds(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAAjAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAQAAAAIAAAABAAAAAgAAAAAAAAAAAAAAAAAAAAHqLnLFAAAAQFwRcFbzEtxoxZOtWlOQld3nURHZugNj5faEncpv0X/dcrfiQVU7k3fkTYDskiVExFiq78CBsYAr0uuvfH61IQs=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -302,7 +302,7 @@ func TestSetOptionsHomeDomain(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAAmAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAcTG92ZWx5THVtZW5zTG9va0x1bWlub3VzLmNvbQAAAAAAAAAAAAAAAeoucsUAAABAtC4HZzvRfyphRg5jjmz5jzBn86SANXCZS59GejRE8L1uCOxgXSEVoh1b+UetUEi7JN/n1ECBEVJrXgj0c34eBg==" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -342,7 +342,7 @@ func TestSetOptionsSigner(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAAmAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAABAAAAAAAAAAB6i5yxQAAAEBfgmUK+wNj8ROz78Sg0rQ2s7lmtvA4r5epHkqc9yoxLDr/GSkmgWneVqoKNxWF0JB9L+Gql1+f8M8p1McF4MsB" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -363,7 +363,7 @@ func TestMultipleOperations(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp1, t) + received := buildSignEncode(t, tx, kp1) expected := "AAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAyAAiILoAAAAIAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAJAAAAAAAAAAsAIiC6AAAAbAAAAAAAAAAB0odkfgAAAEDmf3Ag2Hw5NdlvzJpph4Km+aNKy8kfzS1EAhIVdKJwUnMVWhOpfdXSh/aekEVdoxXh2+ioocrxdtkWAZfS3sMF" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -385,7 +385,7 @@ func TestChangeTrust(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAAA9AAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAGAAAAAUFCQ0QAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAAABfXhAAAAAAAAAAAB6i5yxQAAAED7YSd1VdewEdtEURAYuyCy8dWbzALEf1vJn88/gCER4CNdIvojOEafJEhYhzZJhdG7oa+95UjfI9vMJO8qdWMK" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -426,7 +426,7 @@ func TestChangeTrustDeleteTrustline(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAABDAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAGAAAAAUFCQ0QAAAAAJcrx2g/Hbs/ohF5CVFG7B5JJSJR+OqDKzDGK7dKHZH4AAAAAAAAAAAAAAAAAAAAB6i5yxQAAAECgd2wkK35civvf6NKpsSFDyKpdyo/cs7wL+RYfZ2BCP7eGrUUpu2GfQFtf/Hm6aBwT6nJ+dONTSPXnyp7Dq18L" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -450,7 +450,7 @@ func TestAllowTrust(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp0, t) + received := buildSignEncode(t, tx, kp0) expected := "AAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAZAAAJLsAAABPAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAHAAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAAUFCQ0QAAAABAAAAAAAAAAHqLnLFAAAAQGGBSKitYxpHNMaVVOE2CIylWFJgwqxjhwnIvWauSSkLapntD18G1pMahLbs8Lqcr3+cEs5WjLI4eBhy6WiJhAk=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -464,7 +464,8 @@ func TestManageSellOfferNewOffer(t *testing.T) { buying := CreditAsset{"ABCD", kp0.Address()} sellAmount := "100" price := "0.01" - createOffer := CreateOfferOp(selling, buying, sellAmount, price) + createOffer, err := CreateOfferOp(selling, buying, sellAmount, price) + check(err) tx := Transaction{ SourceAccount: &sourceAccount, @@ -473,7 +474,7 @@ func TestManageSellOfferNewOffer(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp1, t) + received := buildSignEncode(t, tx, kp1) expected := "AAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAZAAAJWoAAAAFAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAADAAAAAAAAAAFBQkNEAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAADuaygAAAAABAAAAZAAAAAAAAAAAAAAAAAAAAAHSh2R+AAAAQAmXf4BnH8bWhy+Tnxf+7zgsij7pV0b7XC4rqfYWi9ZIVUaidWPbrFhaWjiQbXYB1NKdx0XjidzkcAgMInLqDgs=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -483,7 +484,8 @@ func TestManageSellOfferDeleteOffer(t *testing.T) { sourceAccount := makeTestAccount(kp1, "41137196761105") offerID := int64(2921622) - deleteOffer := DeleteOfferOp(offerID) + deleteOffer, err := DeleteOfferOp(offerID) + check(err) tx := Transaction{ SourceAccount: &sourceAccount, @@ -492,7 +494,7 @@ func TestManageSellOfferDeleteOffer(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp1, t) + received := buildSignEncode(t, tx, kp1) expected := "AAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAZAAAJWoAAAASAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAADAAAAAAAAAAFGQUtFAAAAAEEHgGTElYZi82AkGiJdSja2OBaU2aEcwwp3AY3tFJ2xAAAAAAAAAAAAAAABAAAAAQAAAAAALJSWAAAAAAAAAAHSh2R+AAAAQBSjRfpyEAIMnRQOPf1BBOx8HFC6Lm6bxxdljaegnUts8SmWJGQbZN5a8PQGzOTwGdBKBk9X9d+BIrBVc3kyyQ4=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -507,7 +509,8 @@ func TestManageSellOfferUpdateOffer(t *testing.T) { sellAmount := "50" price := "0.02" offerID := int64(2497628) - updateOffer := UpdateOfferOp(selling, buying, sellAmount, price, offerID) + updateOffer, err := UpdateOfferOp(selling, buying, sellAmount, price, offerID) + check(err) tx := Transaction{ SourceAccount: &sourceAccount, @@ -516,7 +519,7 @@ func TestManageSellOfferUpdateOffer(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp1, t) + received := buildSignEncode(t, tx, kp1) expected := "AAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAZAAAJWoAAAAKAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAADAAAAAAAAAAFBQkNEAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAAB3NZQAAAAABAAAAMgAAAAAAJhxcAAAAAAAAAAHSh2R+AAAAQAwqWg2C/oe/zH4D3Y7/yg5SlHqFvF6A3j6GQZ9NPh3ROqutovLyAE62+rvXxM7hqSNz1Rtx4frJaOhOabh6DAg=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -540,7 +543,7 @@ func TestCreatePassiveSellOffer(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp1, t) + received := buildSignEncode(t, tx, kp1) expected := "AAAAACXK8doPx27P6IReQlRRuweSSUiUfjqgyswxiu3Sh2R+AAAAZAAAJWoAAAANAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAEAAAAAAAAAAFBQkNEAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAAAX14QAAAAABAAAAAQAAAAAAAAAB0odkfgAAAEAgUD7M1UL7x2m2m26ySzcSHxIneOT7/r+s/HLsgWDj6CmpSi1GZrlvtBH+CNuegCwvW09TRZJhp7bLywkaFCoK" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -567,7 +570,7 @@ func TestPathPayment(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp2, t) + received := buildSignEncode(t, tx, kp2) expected := "AAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAZAAAql0AAAADAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAAF9eEAAAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAAAAAAAAAmJaAAAAAAQAAAAFBQkNEAAAAAODcbeFyXKxmUWK1L6znNbKKIkPkHRJNbLktcKPqLnLFAAAAAAAAAAEuFVmYAAAAQF2kLUL/RoFIy1cmt+GXdWn2tDUjJYV3YwF4A82zIBhqYSO6ogOoLPNRt3w+IGCAgfR4Q9lpax+wCXWoQERHSw4=" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -584,7 +587,7 @@ func TestMemoText(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp2, t) + received := buildSignEncode(t, tx, kp2) // https://www.stellar.org/laboratory/#txsigner?xdr=AAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAZAAMokEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAADFR3YXMgYnJpbGxpZwAAAAEAAAAAAAAACwAAAAAAAAABAAAAAAAAAAA%3D&network=test expected := "AAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAZAAMokEAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAADFR3YXMgYnJpbGxpZwAAAAEAAAAAAAAACwAAAAAAAAABAAAAAAAAAAEuFVmYAAAAQILT8/7MGTmWkfjMi6Y23n2cVWs+IMY67xOskTivSZehp7wWaDXLIdCbdijmG64+Nz+fPBT9HYMqSRDcLiZYDQ0=" assert.Equal(t, expected, received, "Base 64 XDR should match") @@ -602,7 +605,7 @@ func TestMemoID(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp2, t) + received := buildSignEncode(t, tx, kp2) expected := "AAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAZAAMLgoAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAEyy8AAAABAAAAAAAAAAsAAAAAAAAAAQAAAAAAAAABLhVZmAAAAEA5P/V/Veh6pjXj7CnqtWDATh8II+ci1z3/zmNk374XLuVLzx7jRve59AKnPMwIPwDJ8cXwEKz8+fYOIkfEI9AJ" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -619,7 +622,7 @@ func TestMemoHash(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp2, t) + received := buildSignEncode(t, tx, kp2) expected := "AAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAZAAMLgoAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAADAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAsAAAAAAAAAAQAAAAAAAAABLhVZmAAAAEAgauaUpqEGF1VeXYtkYg0I19QC3GJVrCPOqDHPIdXvGkQ9N+3Vt6yfKIN0sE/X5NuD6FhArQ3adwvZeaNDilwN" assert.Equal(t, expected, received, "Base 64 XDR should match") } @@ -636,7 +639,7 @@ func TestMemoReturn(t *testing.T) { Network: network.TestNetworkPassphrase, } - received := buildSignEncode(tx, kp2, t) + received := buildSignEncode(t, tx, kp2) expected := "AAAAAH4RyzTWNfXhqwLUoCw91aWkZtgIzY8SAVkIPc0uFVmYAAAAZAAMLgoAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAEAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAsAAAAAAAAAAQAAAAAAAAABLhVZmAAAAEAuLFTunY08pbWKompoepHdazLmr7uePUSOzA4P33+SVRKWiu+h2tngOsP8hga+wpLJXT9l/0uMQ3iziRVUrh0K" assert.Equal(t, expected, received, "Base 64 XDR should match") }