This repository has been archived by the owner on Apr 2, 2024. It is now read-only.
forked from stellar/go
-
Notifications
You must be signed in to change notification settings - Fork 3
/
path.go
414 lines (355 loc) · 12 KB
/
path.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
package actions
import (
"context"
"database/sql"
"fmt"
"net/http"
"github.com/xdbfoundation/go/amount"
"github.com/xdbfoundation/go/protocols/frontier"
frontierContext "github.com/xdbfoundation/go/services/frontier/internal/context"
"github.com/xdbfoundation/go/services/frontier/internal/db2/history"
"github.com/xdbfoundation/go/services/frontier/internal/paths"
frontierProblem "github.com/xdbfoundation/go/services/frontier/internal/render/problem"
"github.com/xdbfoundation/go/services/frontier/internal/resourceadapter"
"github.com/xdbfoundation/go/services/frontier/internal/simplepath"
"github.com/xdbfoundation/go/support/errors"
"github.com/xdbfoundation/go/support/render/hal"
"github.com/xdbfoundation/go/support/render/problem"
"github.com/xdbfoundation/go/xdr"
)
// FindPathsHandler is the http handler for the find payment paths endpoint
type FindPathsHandler struct {
StaleThreshold uint
MaxPathLength uint
SetLastLedgerHeader bool
MaxAssetsParamLength int
PathFinder paths.Finder
}
// StrictReceivePathsQuery query struct for paths/strict-send end-point
type StrictReceivePathsQuery struct {
SourceAssets string `schema:"source_assets" valid:"-"`
SourceAccount string `schema:"source_account" valid:"accountID,optional"`
DestinationAccount string `schema:"destination_account" valid:"accountID,optional"`
DestinationAssetType string `schema:"destination_asset_type" valid:"assetType"`
DestinationAssetIssuer string `schema:"destination_asset_issuer" valid:"accountID,optional"`
DestinationAssetCode string `schema:"destination_asset_code" valid:"-"`
DestinationAmount string `schema:"destination_amount" valid:"amount"`
}
// Assets returns a list of xdr.Asset
func (q StrictReceivePathsQuery) Assets() ([]xdr.Asset, error) {
return xdr.BuildAssets(q.SourceAssets)
}
// Amount returns source amount
func (q StrictReceivePathsQuery) Amount() xdr.Int64 {
parsed, err := amount.Parse(q.DestinationAmount)
if err != nil {
panic(err)
}
return parsed
}
// DestinationAsset returns an xdr.Asset
func (q StrictReceivePathsQuery) DestinationAsset() xdr.Asset {
asset, err := xdr.BuildAsset(
q.DestinationAssetType,
q.DestinationAssetIssuer,
q.DestinationAssetCode,
)
if err != nil {
panic(err)
}
return asset
}
// URITemplate returns a rfc6570 URI template for the query struct
func (q StrictReceivePathsQuery) URITemplate() string {
return getURITemplate(&q, "paths/strict-receive", false)
}
// Validate runs custom validations.
func (q StrictReceivePathsQuery) Validate() error {
if (len(q.SourceAccount) > 0) == (len(q.SourceAssets) > 0) {
return SourceAssetsOrSourceAccountProblem
}
err := validateAssetParams(
q.DestinationAssetType,
q.DestinationAssetCode,
q.DestinationAssetIssuer,
"destination_",
)
if err != nil {
return err
}
_, err = q.Assets()
if err != nil {
return problem.MakeInvalidFieldProblem(
"source_assets",
err,
)
}
return nil
}
// SourceAssetsOrSourceAccountProblem custom error where source assets or account is required
var SourceAssetsOrSourceAccountProblem = problem.P{
Type: "bad_request",
Title: "Bad Request",
Status: http.StatusBadRequest,
Detail: "The request requires either a list of source assets or a source account. " +
"Both fields cannot be present.",
}
// GetResource finds a list of strict receive paths
func (handler FindPathsHandler) GetResource(w HeaderWriter, r *http.Request) (interface{}, error) {
var err error
ctx := r.Context()
qp := StrictReceivePathsQuery{}
if err = getParams(&qp, r); err != nil {
return nil, err
}
query := paths.Query{}
query.DestinationAmount = qp.Amount()
sourceAccount := qp.SourceAccount
query.SourceAssets, _ = qp.Assets()
if len(query.SourceAssets) > handler.MaxAssetsParamLength {
return nil, problem.MakeInvalidFieldProblem(
"source_assets",
fmt.Errorf("list of assets exceeds maximum length of %d", handler.MaxPathLength),
)
}
query.DestinationAsset = qp.DestinationAsset()
if sourceAccount != "" {
sourceAccount := xdr.MustAddress(sourceAccount)
query.SourceAccount = &sourceAccount
query.ValidateSourceBalance = true
query.SourceAssets, query.SourceAssetBalances, err = assetsForAddress(r, query.SourceAccount.Address())
if err != nil {
return nil, err
}
} else {
for range query.SourceAssets {
query.SourceAssetBalances = append(query.SourceAssetBalances, 0)
}
}
// Rollback REPEATABLE READ transaction so that a DB connection is released
// to be used by other http requests.
historyQ, err := frontierContext.HistoryQFromRequest(r)
if err != nil {
return nil, errors.Wrap(err, "could not obtain historyQ from request")
}
err = historyQ.Rollback()
if err != nil {
return nil, errors.Wrap(err, "error in rollback")
}
records := []paths.Path{}
if len(query.SourceAssets) > 0 {
var lastIngestedLedger uint32
records, lastIngestedLedger, err = handler.PathFinder.Find(ctx, query, handler.MaxPathLength)
switch err {
case simplepath.ErrEmptyInMemoryOrderBook:
return nil, frontierProblem.StillIngesting
case paths.ErrRateLimitExceeded:
return nil, frontierProblem.ServerOverCapacity
default:
if err != nil {
return nil, err
}
}
if handler.SetLastLedgerHeader {
// To make the Last-Ledger header consistent with the response content,
// we need to extract it from the ledger and not the DB.
// Thus, we overwrite the header if it was previously set.
SetLastLedgerHeader(w, lastIngestedLedger)
}
}
return renderPaths(ctx, records)
}
func renderPaths(ctx context.Context, records []paths.Path) (hal.BasePage, error) {
var page hal.BasePage
page.Init()
for _, p := range records {
var res frontier.Path
if err := resourceadapter.PopulatePath(ctx, &res, p); err != nil {
return hal.BasePage{}, err
}
page.Add(res)
}
return page, nil
}
// FindFixedPathsHandler is the http handler for the find fixed payment paths endpoint
// Fixed payment paths are payment paths where both the source and destination asset are fixed
type FindFixedPathsHandler struct {
MaxPathLength uint
MaxAssetsParamLength int
SetLastLedgerHeader bool
PathFinder paths.Finder
}
// DestinationAssetsOrDestinationAccountProblem custom error where destination asserts or accounts are required
var DestinationAssetsOrDestinationAccountProblem = problem.P{
Type: "bad_request",
Title: "Bad Request",
Status: http.StatusBadRequest,
Detail: "The request requires either a list of destination assets or a destination account. " +
"Both fields cannot be present.",
}
// FindFixedPathsQuery query struct for paths/strict-send end-point
type FindFixedPathsQuery struct {
DestinationAccount string `schema:"destination_account" valid:"accountID,optional"`
DestinationAssets string `schema:"destination_assets" valid:"-"`
SourceAssetType string `schema:"source_asset_type" valid:"assetType"`
SourceAssetIssuer string `schema:"source_asset_issuer" valid:"accountID,optional"`
SourceAssetCode string `schema:"source_asset_code" valid:"-"`
SourceAmount string `schema:"source_amount" valid:"amount"`
}
// URITemplate returns a rfc6570 URI template for the query struct
func (q FindFixedPathsQuery) URITemplate() string {
return getURITemplate(&q, "paths/strict-send", false)
}
// Validate runs custom validations.
func (q FindFixedPathsQuery) Validate() error {
if (len(q.DestinationAccount) > 0) == (len(q.DestinationAssets) > 0) {
return DestinationAssetsOrDestinationAccountProblem
}
err := validateAssetParams(
q.SourceAssetType,
q.SourceAssetCode,
q.SourceAssetIssuer,
"source_",
)
if err != nil {
return err
}
_, err = q.Assets()
if err != nil {
return problem.MakeInvalidFieldProblem(
"destination_assets",
err,
)
}
return nil
}
// Assets returns a list of xdr.Asset
func (q FindFixedPathsQuery) Assets() ([]xdr.Asset, error) {
return xdr.BuildAssets(q.DestinationAssets)
}
// Amount returns source amount
func (q FindFixedPathsQuery) Amount() xdr.Int64 {
parsed, err := amount.Parse(q.SourceAmount)
if err != nil {
panic(err)
}
return parsed
}
// SourceAsset returns an xdr.Asset
func (q FindFixedPathsQuery) SourceAsset() xdr.Asset {
asset, err := xdr.BuildAsset(
q.SourceAssetType,
q.SourceAssetIssuer,
q.SourceAssetCode,
)
if err != nil {
panic(err)
}
return asset
}
// GetResource returns a list of strict send paths
func (handler FindFixedPathsHandler) GetResource(w HeaderWriter, r *http.Request) (interface{}, error) {
var err error
ctx := r.Context()
qp := FindFixedPathsQuery{}
if err = getParams(&qp, r); err != nil {
return nil, err
}
destinationAccount := qp.DestinationAccount
destinationAssets, _ := qp.Assets()
if len(destinationAssets) > handler.MaxAssetsParamLength {
return nil, problem.MakeInvalidFieldProblem(
"destination_assets",
fmt.Errorf("list of assets exceeds maximum length of %d", handler.MaxPathLength),
)
}
if destinationAccount != "" {
destinationAssets, _, err = assetsForAddress(r, destinationAccount)
if err != nil {
return nil, err
}
}
// Rollback REPEATABLE READ transaction so that a DB connection is released
// to be used by other http requests.
historyQ, err := frontierContext.HistoryQFromRequest(r)
if err != nil {
return nil, errors.Wrap(err, "could not obtain historyQ from request")
}
err = historyQ.Rollback()
if err != nil {
return nil, errors.Wrap(err, "error in rollback")
}
sourceAsset := qp.SourceAsset()
amountToSpend := qp.Amount()
records := []paths.Path{}
if len(destinationAssets) > 0 {
var lastIngestedLedger uint32
records, lastIngestedLedger, err = handler.PathFinder.FindFixedPaths(
ctx,
sourceAsset,
amountToSpend,
destinationAssets,
handler.MaxPathLength,
)
switch err {
case simplepath.ErrEmptyInMemoryOrderBook:
return nil, frontierProblem.StillIngesting
case paths.ErrRateLimitExceeded:
return nil, frontierProblem.ServerOverCapacity
default:
if err != nil {
return nil, err
}
}
if handler.SetLastLedgerHeader {
// To make the Last-Ledger header consistent with the response content,
// we need to extract it from the ledger and not the DB.
// Thus, we overwrite the header if it was previously set.
SetLastLedgerHeader(w, lastIngestedLedger)
}
}
return renderPaths(ctx, records)
}
func assetsForAddress(r *http.Request, addy string) ([]xdr.Asset, []xdr.Int64, error) {
historyQ, err := frontierContext.HistoryQFromRequest(r)
if err != nil {
return nil, nil, errors.Wrap(err, "could not obtain historyQ from request")
}
if historyQ.SessionInterface.GetTx() == nil {
return nil, nil, errors.New("cannot be called outside of a transaction")
}
if opts := historyQ.SessionInterface.GetTxOptions(); opts == nil || !opts.ReadOnly || opts.Isolation != sql.LevelRepeatableRead {
return nil, nil, errors.New("should only be called in a repeatable read transaction")
}
var account history.AccountEntry
account, err = historyQ.GetAccountByID(r.Context(), addy)
if historyQ.NoRows(err) {
return []xdr.Asset{}, []xdr.Int64{}, nil
} else if err != nil {
return nil, nil, errors.Wrap(err, "could not fetch account")
}
var trustlines []history.TrustLine
trustlines, err = historyQ.GetSortedTrustLinesByAccountID(r.Context(), addy)
if err != nil {
return nil, nil, errors.Wrap(err, "could not fetch trustlines for account")
}
var assets []xdr.Asset
var balances []xdr.Int64
for _, trustline := range trustlines {
// Ignore pool share assets because pool shares are not transferable and cannot be traded.
// Therefore, it doesn't make sense to send path payments where the source / destination assets are pool shares.
if trustline.AssetType == xdr.AssetTypeAssetTypePoolShare {
continue
}
var asset xdr.Asset
asset, err = xdr.NewCreditAsset(trustline.AssetCode, trustline.AssetIssuer)
if err != nil {
return nil, nil, errors.Wrap(err, "invalid trustline asset")
}
assets = append(assets, asset)
balances = append(balances, xdr.Int64(trustline.Balance))
}
assets = append(assets, xdr.MustNewNativeAsset())
balances = append(balances, xdr.Int64(account.Balance))
return assets, balances, nil
}