diff --git a/clients/horizonclient/client.go b/clients/horizonclient/client.go index 6a50b075d4..96937eec42 100644 --- a/clients/horizonclient/client.go +++ b/clients/horizonclient/client.go @@ -410,8 +410,21 @@ func (c *Client) OrderBook(request OrderBookRequest) (obs hProtocol.OrderBookSum return } -// Paths returns the available paths to make a payment. See https://www.stellar.org/developers/horizon/reference/endpoints/path-finding.html +// Paths returns the available paths to make a strict receive path payment. See https://www.stellar.org/developers/horizon/reference/endpoints/path-finding-strict-receive.html +// This function is an alias for `client.StrictReceivePaths` and will be deprecated, use `client.StrictReceivePaths` instead. func (c *Client) Paths(request PathsRequest) (paths hProtocol.PathsPage, err error) { + paths, err = c.StrictReceivePaths(request) + return +} + +// StrictReceivePaths returns the available paths to make a strict receive path payment. See https://www.stellar.org/developers/horizon/reference/endpoints/path-finding-strict-receive.html +func (c *Client) StrictReceivePaths(request PathsRequest) (paths hProtocol.PathsPage, err error) { + err = c.sendRequest(request, &paths) + return +} + +// StrictSendPaths returns the available paths to make a strict send path payment. See https://www.stellar.org/developers/horizon/reference/endpoints/path-finding-strict-send.html +func (c *Client) StrictSendPaths(request StrictSendPathsRequest) (paths hProtocol.PathsPage, err error) { err = c.sendRequest(request, &paths) return } diff --git a/clients/horizonclient/examples_test.go b/clients/horizonclient/examples_test.go index 853c1aa59a..50391cb683 100644 --- a/clients/horizonclient/examples_test.go +++ b/clients/horizonclient/examples_test.go @@ -513,7 +513,25 @@ func ExampleClient_Paths() { DestinationAssetType: horizonclient.AssetType4, SourceAccount: "GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", } - paths, err := client.Paths(pr) + paths, err := client.StrictReceivePaths(pr) + if err != nil { + fmt.Println(err) + return + } + fmt.Print(paths) +} + +func ExampleClient_StrictSendPaths() { + client := horizonclient.DefaultPublicNetClient + // Find paths for USD->EUR + pr := horizonclient.StrictSendPathsRequest{ + SourceAmount: "20", + SourceAssetCode: "USD", + SourceAssetIssuer: "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", + SourceAssetType: horizonclient.AssetType4, + DestinationAssets: "EURT:GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S", + } + paths, err := client.StrictSendPaths(pr) if err != nil { fmt.Println(err) return diff --git a/clients/horizonclient/main.go b/clients/horizonclient/main.go index 21ac76e2d8..9226ef76f6 100644 --- a/clients/horizonclient/main.go +++ b/clients/horizonclient/main.go @@ -335,8 +335,10 @@ type OrderBookRequest struct { Limit uint } -// PathsRequest struct contains data for getting available payment paths from a horizon server. -// All parameters are required. +// PathsRequest struct contains data for getting available strict receive path payments from a horizon server. +// All the Destination related parameters are required and you need to include either +// SourceAccount or SourceAssets. +// See https://www.stellar.org/developers/horizon/reference/endpoints/path-finding-strict-receive.html type PathsRequest struct { DestinationAccount string DestinationAssetType AssetType @@ -344,6 +346,20 @@ type PathsRequest struct { DestinationAssetIssuer string DestinationAmount string SourceAccount string + SourceAssets string +} + +// StrictSendPathsRequest struct contains data for getting available strict send path payments from a horizon server. +// All the Source related parameters are required and you need to include either +// DestinationAccount or DestinationAssets. +// See https://www.stellar.org/developers/horizon/reference/endpoints/path-finding-strict-send.html +type StrictSendPathsRequest struct { + DestinationAccount string + DestinationAssets string + SourceAssetType AssetType + SourceAssetCode string + SourceAssetIssuer string + SourceAmount string } // TradeRequest struct contains data for getting trade details from a horizon server. diff --git a/clients/horizonclient/paths_request.go b/clients/horizonclient/paths_request.go index a3b6e5c0f9..8f6c2bd309 100644 --- a/clients/horizonclient/paths_request.go +++ b/clients/horizonclient/paths_request.go @@ -20,6 +20,7 @@ func (pr PathsRequest) BuildURL() (endpoint string, err error) { paramMap["destination_asset_issuer"] = pr.DestinationAssetIssuer paramMap["destination_amount"] = pr.DestinationAmount paramMap["source_account"] = pr.SourceAccount + paramMap["source_assets"] = pr.SourceAssets queryParams := addQueryParams(paramMap) if queryParams != "" { diff --git a/clients/horizonclient/paths_request_test.go b/clients/horizonclient/paths_request_test.go index 0bf30fdb08..6354407570 100644 --- a/clients/horizonclient/paths_request_test.go +++ b/clients/horizonclient/paths_request_test.go @@ -25,13 +25,18 @@ func TestPathsRequestBuildUrl(t *testing.T) { DestinationAssetIssuer: "GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", DestinationAssetType: AssetType4, SourceAccount: "GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", + SourceAssets: "COP:GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", } endpoint, err = pr.BuildURL() // It should return valid assets endpoint and no errors require.NoError(t, err) - assert.Equal(t, "paths?destination_account=GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU&destination_amount=100&destination_asset_code=NGN&destination_asset_issuer=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM&destination_asset_type=credit_alphanum4&source_account=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", endpoint) + assert.Equal( + t, + "paths?destination_account=GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU&destination_amount=100&destination_asset_code=NGN&destination_asset_issuer=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM&destination_asset_type=credit_alphanum4&source_account=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM&source_assets=COP%3AGDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", + endpoint, + ) } @@ -57,7 +62,7 @@ func TestPathsRequest(t *testing.T) { "https://localhost/paths?destination_account=GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU&destination_amount=100&destination_asset_code=NGN&destination_asset_issuer=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM&destination_asset_type=credit_alphanum4&source_account=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", ).ReturnString(200, pathsResponse) - paths, err := client.Paths(pr) + paths, err := client.StrictReceivePaths(pr) if assert.NoError(t, err) { assert.IsType(t, paths, hProtocol.PathsPage{}) record := paths.Embedded.Records[0] @@ -73,7 +78,55 @@ func TestPathsRequest(t *testing.T) { "https://localhost/paths", ).ReturnString(400, badRequestResponse) - _, err = client.Paths(pr) + _, err = client.StrictReceivePaths(pr) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "horizon error") + horizonError, ok := err.(*Error) + assert.Equal(t, ok, true) + assert.Equal(t, horizonError.Problem.Title, "Bad Request") + } + +} + +func TestStrictReceivePathsRequest(t *testing.T) { + hmock := httptest.NewClient() + client := &Client{ + HorizonURL: "https://localhost/", + HTTP: hmock, + } + + pr := PathsRequest{ + DestinationAccount: "GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU", + DestinationAmount: "100", + DestinationAssetCode: "NGN", + DestinationAssetIssuer: "GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", + DestinationAssetType: AssetType4, + SourceAccount: "GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", + } + + // orderbook for XLM/USD + hmock.On( + "GET", + "https://localhost/paths?destination_account=GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU&destination_amount=100&destination_asset_code=NGN&destination_asset_issuer=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM&destination_asset_type=credit_alphanum4&source_account=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", + ).ReturnString(200, pathsResponse) + + paths, err := client.StrictReceivePaths(pr) + if assert.NoError(t, err) { + assert.IsType(t, paths, hProtocol.PathsPage{}) + record := paths.Embedded.Records[0] + assert.Equal(t, record.DestinationAmount, "20.0000000") + assert.Equal(t, record.DestinationAssetCode, "EUR") + assert.Equal(t, record.SourceAmount, "30.0000000") + } + + // failure response + pr = PathsRequest{} + hmock.On( + "GET", + "https://localhost/paths", + ).ReturnString(400, badRequestResponse) + + _, err = client.StrictReceivePaths(pr) if assert.Error(t, err) { assert.Contains(t, err.Error(), "horizon error") horizonError, ok := err.(*Error) diff --git a/clients/horizonclient/strict_send_paths_request.go b/clients/horizonclient/strict_send_paths_request.go new file mode 100644 index 0000000000..a1caaa0e26 --- /dev/null +++ b/clients/horizonclient/strict_send_paths_request.go @@ -0,0 +1,35 @@ +package horizonclient + +import ( + "fmt" + "net/url" + + "github.com/stellar/go/support/errors" +) + +// BuildURL creates the endpoint to be queried based on the data in the PathsRequest struct. +func (pr StrictSendPathsRequest) BuildURL() (endpoint string, err error) { + endpoint = "paths/strict-send" + + // add the parameters to a map here so it is easier for addQueryParams to populate the parameter list + // We can't use assetCode and assetIssuer types here because the parameter names are different + paramMap := make(map[string]string) + paramMap["destination_assets"] = pr.DestinationAssets + paramMap["destination_account"] = pr.DestinationAccount + paramMap["source_asset_type"] = string(pr.SourceAssetType) + paramMap["source_asset_code"] = pr.SourceAssetCode + paramMap["source_asset_issuer"] = pr.SourceAssetIssuer + paramMap["source_amount"] = pr.SourceAmount + + queryParams := addQueryParams(paramMap) + if queryParams != "" { + endpoint = fmt.Sprintf("%s?%s", endpoint, queryParams) + } + + _, err = url.Parse(endpoint) + if err != nil { + err = errors.Wrap(err, "failed to parse endpoint") + } + + return endpoint, err +} diff --git a/clients/horizonclient/strict_send_paths_request_test.go b/clients/horizonclient/strict_send_paths_request_test.go new file mode 100644 index 0000000000..ca515536f3 --- /dev/null +++ b/clients/horizonclient/strict_send_paths_request_test.go @@ -0,0 +1,117 @@ +package horizonclient + +import ( + "testing" + + "github.com/stellar/go/support/http/httptest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStrictSendPathsRequestBuildUrl(t *testing.T) { + pr := StrictSendPathsRequest{} + endpoint, err := pr.BuildURL() + + // It should return no errors and paths endpoint + // Horizon will return an error though because there are no parameters + require.NoError(t, err) + assert.Equal(t, "paths/strict-send", endpoint) + + pr = StrictSendPathsRequest{ + SourceAmount: "100", + SourceAssetCode: "NGN", + SourceAssetIssuer: "GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", + SourceAssetType: AssetType4, + DestinationAccount: "GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM", + } + + endpoint, err = pr.BuildURL() + + // It should return a valid endpoint and no errors + require.NoError(t, err) + assert.Equal( + t, + "paths/strict-send?destination_account=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM&source_amount=100&source_asset_code=NGN&source_asset_issuer=GDZST3XVCDTUJ76ZAV2HA72KYQODXXZ5PTMAPZGDHZ6CS7RO7MGG3DBM&source_asset_type=credit_alphanum4", + endpoint, + ) + + pr = StrictSendPathsRequest{ + SourceAmount: "100", + SourceAssetCode: "USD", + SourceAssetIssuer: "GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", + SourceAssetType: AssetType4, + DestinationAssets: "EURT:GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S,native", + } + + endpoint, err = pr.BuildURL() + + require.NoError(t, err) + assert.Equal( + t, + "paths/strict-send?destination_assets=EURT%3AGAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S%2Cnative&source_amount=100&source_asset_code=USD&source_asset_issuer=GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX&source_asset_type=credit_alphanum4", + endpoint, + ) +} +func TestStrictSendPathsRequest(t *testing.T) { + hmock := httptest.NewClient() + client := &Client{ + HorizonURL: "https://localhost/", + HTTP: hmock, + } + + pr := StrictSendPathsRequest{ + SourceAmount: "20", + SourceAssetCode: "USD", + SourceAssetIssuer: "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", + SourceAssetType: AssetType4, + DestinationAccount: "GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU", + } + + hmock.On( + "GET", + "https://localhost/paths/strict-send?destination_account=GCLWGQPMKXQSPF776IU33AH4PZNOOWNAWGGKVTBQMIC5IMKUNP3E6NVU&source_amount=20&source_asset_code=USD&source_asset_issuer=GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN&source_asset_type=credit_alphanum4", + ).ReturnString(200, pathsResponse) + + paths, err := client.StrictSendPaths(pr) + assert.NoError(t, err) + assert.Len(t, paths.Embedded.Records, 3) + + pr = StrictSendPathsRequest{ + SourceAmount: "20", + SourceAssetCode: "USD", + SourceAssetIssuer: "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", + SourceAssetType: AssetType4, + DestinationAssets: "EUR:GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", + } + + hmock.On( + "GET", + "https://localhost/paths/strict-send?destination_assets=EUR%3AGDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN&source_amount=20&source_asset_code=USD&source_asset_issuer=GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN&source_asset_type=credit_alphanum4", + ).ReturnString(200, pathsResponse) + + paths, err = client.StrictSendPaths(pr) + assert.NoError(t, err) + assert.Len(t, paths.Embedded.Records, 3) + + pr = StrictSendPathsRequest{ + SourceAmount: "20", + SourceAssetCode: "USD", + SourceAssetIssuer: "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", + SourceAssetType: AssetType4, + DestinationAssets: "EUR:GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", + DestinationAccount: "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", + } + + hmock.On( + "GET", + "https://localhost/paths/strict-send?destination_account=GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN&destination_assets=EUR%3AGDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN&source_amount=20&source_asset_code=USD&source_asset_issuer=GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN&source_asset_type=credit_alphanum4", + ).ReturnString(400, badRequestResponse) + + _, err = client.StrictSendPaths(pr) + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "horizon error") + horizonError, ok := err.(*Error) + assert.Equal(t, ok, true) + assert.Equal(t, horizonError.Problem.Title, "Bad Request") + } +}