Skip to content

Commit 71b7b69

Browse files
feat(solver): add quote endpoint and fee (#3120)
Add quote endpoint and a 30 bip fee. issue: none
1 parent 1434524 commit 71b7b69

File tree

9 files changed

+555
-95
lines changed

9 files changed

+555
-95
lines changed

lib/contracts/solvernet/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (s OrderStatus) Uint8() uint8 {
6060
return uint8(s)
6161
}
6262

63-
// Expense is a solver expense on the destination (matches bindings.SolverNetExpense).
63+
// Expense is a solver expense on the destination (matches bindings.SolverNetTokenExpense).
6464
type Expense struct {
6565
Spender common.Address `json:"spender"`
6666
Token common.Address `json:"token"`

solver/app/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func Run(ctx context.Context, cfg Config) error {
115115

116116
log.Info(ctx, "Serving API", "address", cfg.APIAddr)
117117
apiChan := serveAPI(cfg.APIAddr, map[string]http.Handler{
118+
"/api/v1/quote": newQuoteHandler(quoter),
118119
"/api/v1/check": newCheckHandler(newChecker(backends, solverAddr, addrs.SolverNetInbox, addrs.SolverNetOutbox)),
119120
"/api/v1/contracts": newContractsHandler(addrs),
120121
})

solver/app/check.go

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func getNextOrderID(ctx context.Context, client ethclient.Client, inboxAddr comm
103103
return orderID, nil
104104
}
105105

106-
// getFillOriginData returns packed fill origin data for a quote request.
106+
// getFillOriginData returns packed fill origin data for a check request.
107107
func getFillOriginData(req CheckRequest) ([]byte, error) {
108108
fillOriginData := bindings.SolverNetFillOriginData{
109109
FillDeadline: req.FillDeadline,
@@ -121,17 +121,17 @@ func getFillOriginData(req CheckRequest) ([]byte, error) {
121121
return fillOriginDataBz, nil
122122
}
123123

124-
// newCheckHandler returns a handler for the /quote endpoint.
125-
// It is responsible to http request / response handling, and delegates
126-
// logic to a quoteFunc.
124+
// newCheckHandler returns a handler for the /check endpoint.
125+
// It is responsible for http request / response handling, and delegates
126+
// logic to a checkFunc.
127127
func newCheckHandler(checkFunc checkFunc) http.Handler {
128128
return http.HandlerFunc(func(w http.ResponseWriter, rr *http.Request) {
129129
ctx := rr.Context()
130130

131131
w.Header().Set("Content-Type", "application/json")
132132

133133
writeError := func(statusCode int, err error) {
134-
log.DebugErr(ctx, "Error handling /quote request", err)
134+
log.DebugErr(ctx, "Error handling /check request", err)
135135

136136
writeJSON(ctx, w, CheckResponse{
137137
Error: &JSONErrorResponse{
@@ -163,42 +163,6 @@ func newCheckHandler(checkFunc checkFunc) http.Handler {
163163
})
164164
}
165165

166-
// getQuote returns payment in `depositTkns` required to pay for `expenses`.
167-
//
168-
// For now, this is a simple quote that requires a single expense, paid
169-
// for by an equal amount of an equivalent deposit token. Token equivalence is
170-
// determined by symbol (ex arbitrum "ETH" is equivalent to optimism "ETH").
171-
func getQuote(depositTkns []Token, expenses []Payment) ([]Payment, error) {
172-
if len(depositTkns) != 1 {
173-
return nil, newRejection(rejectInvalidDeposit, errors.New("only single deposit token supported"))
174-
}
175-
176-
if len(expenses) != 1 {
177-
return nil, newRejection(rejectInvalidExpense, errors.New("only single expense supported"))
178-
}
179-
180-
expense := expenses[0]
181-
depositTkn := depositTkns[0]
182-
183-
if expense.Token.Symbol != depositTkn.Symbol {
184-
return nil, newRejection(rejectInvalidDeposit, errors.New("deposit token must match expense token"))
185-
}
186-
187-
// make sure chain class (e.g. mainnet, testnet) matches
188-
// we should reject with UnsupportedDestChain before this. the solver is
189-
// initialized by network, which only includes chains of the same class
190-
if expense.Token.ChainClass != depositTkn.ChainClass {
191-
return nil, newRejection(rejectInvalidDeposit, errors.New("deposit and expense must be of the same chain class (e.g. mainnet, testnet)"))
192-
}
193-
194-
return []Payment{
195-
{
196-
Token: depositTkn,
197-
Amount: expense.Amount,
198-
},
199-
}, nil
200-
}
201-
202166
// coversQuote checks if `deposits` match or exceed a `quote` for expenses.
203167
func coversQuote(deposits, quote []Payment) error {
204168
byTkn := func(ps []Payment) map[Token]*big.Int {

solver/app/check_internal_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestCheck(t *testing.T) {
4949
require.NoError(t, err)
5050

5151
ctx := context.Background()
52-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "api/v1/quote", bytes.NewBuffer(body))
52+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "api/v1/check", bytes.NewBuffer(body))
5353
require.NoError(t, err)
5454

5555
rr := httptest.NewRecorder()

solver/app/internal_testutils.go

Lines changed: 91 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -192,23 +192,7 @@ func rejectTestCases(t *testing.T, solver, outbox common.Address) []rejectTestCa
192192
tests = append(tests, toRejectTestCase(t, tt, outbox))
193193
}
194194

195-
// not shared with quote
196-
additional := []rejectTestCase{
197-
toRejectTestCase(t, orderTestCase{
198-
name: "insufficient deposit",
199-
reason: rejectInsufficientDeposit,
200-
reject: true,
201-
order: testOrder{
202-
srcChainID: evmchain.IDBaseSepolia,
203-
dstChainID: evmchain.IDHolesky,
204-
deposits: []Deposit{{Amount: big.NewInt(1)}},
205-
calls: []Call{{Value: big.NewInt(2)}},
206-
expenses: []Expense{{Amount: big.NewInt(2)}},
207-
},
208-
}, outbox),
209-
}
210-
211-
return append(tests, additional...)
195+
return tests
212196
}
213197

214198
func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
@@ -225,12 +209,12 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
225209
// request 1 native OMNI for 1 erc20 OMNI on omega
226210
srcChainID: evmchain.IDHolesky,
227211
dstChainID: evmchain.IDOmniOmega,
228-
deposits: []Deposit{{Amount: big.NewInt(1), Token: omegaOMNIAddr}},
229-
calls: []Call{{Value: big.NewInt(1)}},
230-
expenses: []Expense{{Amount: big.NewInt(1)}},
212+
deposits: []Deposit{{Amount: ether(1), Token: omegaOMNIAddr}},
213+
calls: []Call{{Value: ether(1)}},
214+
expenses: []Expense{{Amount: ether(1)}},
231215
},
232216
mock: func(clients MockClients) {
233-
mockNativeBalance(t, clients.Client(t, evmchain.IDOmniOmega), solver, big.NewInt(0))
217+
mockNativeBalance(t, clients.Client(t, evmchain.IDOmniOmega), solver, ether(0))
234218
},
235219
},
236220
{
@@ -241,12 +225,13 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
241225
// request 1 native OMNI for 1 erc20 OMNI on omega
242226
srcChainID: evmchain.IDHolesky,
243227
dstChainID: evmchain.IDOmniOmega,
244-
deposits: []Deposit{{Amount: big.NewInt(1), Token: omegaOMNIAddr}},
245-
calls: []Call{{Value: big.NewInt(1)}},
246-
expenses: []Expense{{Amount: big.NewInt(1)}},
228+
// OMNI does not require fee
229+
deposits: []Deposit{{Amount: ether(1), Token: omegaOMNIAddr}},
230+
calls: []Call{{Value: ether(1)}},
231+
expenses: []Expense{{Amount: ether(1)}},
247232
},
248233
mock: func(clients MockClients) {
249-
mockNativeBalance(t, clients.Client(t, evmchain.IDOmniOmega), solver, big.NewInt(1))
234+
mockNativeBalance(t, clients.Client(t, evmchain.IDOmniOmega), solver, ether(1))
250235
},
251236
},
252237
{
@@ -257,11 +242,11 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
257242
// request 1 erc20 OMNI for 1 native OMNI on omega
258243
srcChainID: evmchain.IDOmniOmega,
259244
dstChainID: evmchain.IDHolesky,
260-
deposits: []Deposit{{Amount: big.NewInt(1)}},
261-
expenses: []Expense{{Amount: big.NewInt(1), Token: omegaOMNIAddr}},
245+
deposits: []Deposit{{Amount: ether(1)}},
246+
expenses: []Expense{{Amount: ether(1), Token: omegaOMNIAddr}},
262247
},
263248
mock: func(clients MockClients) {
264-
mockERC20Balance(t, clients.Client(t, evmchain.IDHolesky), omegaOMNIAddr, big.NewInt(0))
249+
mockERC20Balance(t, clients.Client(t, evmchain.IDHolesky), omegaOMNIAddr, ether(0))
265250
},
266251
},
267252
{
@@ -272,11 +257,12 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
272257
// request 1 erc20 OMNI for 1 native OMNI on omega
273258
srcChainID: evmchain.IDOmniOmega,
274259
dstChainID: evmchain.IDHolesky,
275-
deposits: []Deposit{{Amount: big.NewInt(1)}},
276-
expenses: []Expense{{Amount: big.NewInt(1), Token: omegaOMNIAddr}},
260+
// OMNI does not require fee
261+
deposits: []Deposit{{Amount: ether(1)}},
262+
expenses: []Expense{{Amount: ether(1), Token: omegaOMNIAddr}},
277263
},
278264
mock: func(clients MockClients) {
279-
mockERC20Balance(t, clients.Client(t, evmchain.IDHolesky), omegaOMNIAddr, big.NewInt(1))
265+
mockERC20Balance(t, clients.Client(t, evmchain.IDHolesky), omegaOMNIAddr, ether(1))
280266
mockERC20Allowance(t, clients.Client(t, evmchain.IDHolesky), omegaOMNIAddr)
281267
},
282268
},
@@ -291,11 +277,11 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
291277
// request 1 erc20 OMNI for 1 native OMNI on omega
292278
srcChainID: evmchain.IDOmniOmega,
293279
dstChainID: evmchain.IDHolesky,
294-
deposits: []Deposit{{Amount: big.NewInt(1)}},
295-
expenses: []Expense{{Amount: big.NewInt(1), Token: omegaOMNIAddr}},
280+
deposits: []Deposit{{Amount: ether(1)}},
281+
expenses: []Expense{{Amount: ether(1), Token: omegaOMNIAddr}},
296282
},
297283
mock: func(clients MockClients) {
298-
mockERC20Balance(t, clients.Client(t, evmchain.IDHolesky), omegaOMNIAddr, big.NewInt(1))
284+
mockERC20Balance(t, clients.Client(t, evmchain.IDHolesky), omegaOMNIAddr, ether(1))
299285
mockERC20Allowance(t, clients.Client(t, evmchain.IDHolesky), omegaOMNIAddr)
300286
},
301287
},
@@ -306,8 +292,8 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
306292
order: testOrder{
307293
srcChainID: evmchain.IDOmniOmega,
308294
dstChainID: evmchain.IDHolesky,
309-
deposits: []Deposit{{Amount: big.NewInt(1)}},
310-
expenses: []Expense{{Amount: big.NewInt(1), Token: common.HexToAddress("0x01")}},
295+
deposits: []Deposit{{Amount: ether(1)}},
296+
expenses: []Expense{{Amount: ether(1), Token: common.HexToAddress("0x01")}},
311297
},
312298
},
313299
{
@@ -326,9 +312,9 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
326312
order: testOrder{
327313
srcChainID: evmchain.IDHolesky,
328314
dstChainID: evmchain.IDOmniOmega,
329-
deposits: []Deposit{{Amount: big.NewInt(1)}},
330-
calls: []Call{{Value: big.NewInt(1)}},
331-
expenses: []Expense{{Amount: big.NewInt(1)}},
315+
deposits: []Deposit{{Amount: ether(1)}},
316+
calls: []Call{{Value: ether(1)}},
317+
expenses: []Expense{{Amount: ether(1)}},
332318
},
333319
},
334320
{
@@ -339,10 +325,10 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
339325
srcChainID: evmchain.IDHolesky,
340326
dstChainID: evmchain.IDBaseSepolia,
341327
// wstETH on holesky
342-
deposits: []Deposit{{Amount: big.NewInt(1), Token: common.HexToAddress("0x8d09a4502cc8cf1547ad300e066060d043f6982d")}},
328+
deposits: []Deposit{{Amount: ether(1), Token: common.HexToAddress("0x8d09a4502cc8cf1547ad300e066060d043f6982d")}},
343329
// native eth on base
344-
calls: []Call{{Value: big.NewInt(1)}},
345-
expenses: []Expense{{Amount: big.NewInt(1)}},
330+
calls: []Call{{Value: ether(1)}},
331+
expenses: []Expense{{Amount: ether(1)}},
346332
},
347333
},
348334
{
@@ -352,9 +338,9 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
352338
order: testOrder{
353339
srcChainID: evmchain.IDHolesky,
354340
dstChainID: evmchain.IDOmniOmega,
355-
deposits: []Deposit{{Amount: big.NewInt(1), Token: omegaOMNIAddr}, {Amount: big.NewInt(1)}},
356-
calls: []Call{{Value: big.NewInt(1)}},
357-
expenses: []Expense{{Amount: big.NewInt(1)}},
341+
deposits: []Deposit{{Amount: ether(1), Token: omegaOMNIAddr}, {Amount: ether(1)}},
342+
calls: []Call{{Value: ether(1)}},
343+
expenses: []Expense{{Amount: ether(1)}},
358344
},
359345
},
360346
{
@@ -364,9 +350,9 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
364350
order: testOrder{
365351
srcChainID: evmchain.IDHolesky,
366352
dstChainID: evmchain.IDOptimism,
367-
deposits: []Deposit{{Amount: big.NewInt(1)}},
368-
calls: []Call{{Value: big.NewInt(1)}},
369-
expenses: []Expense{{Amount: big.NewInt(1)}},
353+
deposits: []Deposit{{Amount: ether(1)}},
354+
calls: []Call{{Value: ether(1)}},
355+
expenses: []Expense{{Amount: ether(1)}},
370356
},
371357
},
372358
{
@@ -376,14 +362,59 @@ func orderTestCases(t *testing.T, solver common.Address) []orderTestCase {
376362
order: testOrder{
377363
srcChainID: evmchain.IDOmniOmega,
378364
dstChainID: evmchain.IDHolesky,
379-
deposits: []Deposit{{Amount: big.NewInt(2)}},
380-
calls: []Call{{Value: big.NewInt(1)}},
365+
deposits: []Deposit{{Amount: ether(2)}},
366+
calls: []Call{{Value: ether(1)}},
381367
expenses: []Expense{
382-
{Amount: big.NewInt(1)},
383-
{Amount: big.NewInt(1), Token: common.HexToAddress("0x8d09a4502cc8cf1547ad300e066060d043f6982d")},
368+
{Amount: ether(1)},
369+
{Amount: ether(1), Token: common.HexToAddress("0x8d09a4502cc8cf1547ad300e066060d043f6982d")},
384370
},
385371
},
386372
},
373+
{
374+
name: "insufficient deposit",
375+
reason: rejectInsufficientDeposit,
376+
reject: true,
377+
order: testOrder{
378+
srcChainID: evmchain.IDBaseSepolia,
379+
dstChainID: evmchain.IDHolesky,
380+
// does not include fee
381+
deposits: []Deposit{{Amount: ether(1)}},
382+
calls: []Call{{Value: ether(1)}},
383+
expenses: []Expense{{Amount: ether(1)}},
384+
},
385+
},
386+
{
387+
name: "sufficient deposit",
388+
order: testOrder{
389+
srcChainID: evmchain.IDBaseSepolia,
390+
dstChainID: evmchain.IDHolesky,
391+
// includes fee
392+
deposits: []Deposit{{Amount: depositFor(ether(1), standardFeeBips)}},
393+
calls: []Call{{Value: ether(1)}},
394+
expenses: []Expense{{Amount: ether(1)}},
395+
},
396+
mock: func(clients MockClients) {
397+
mockNativeBalance(t, clients.Client(t, evmchain.IDHolesky), solver, ether(1))
398+
},
399+
},
400+
{
401+
name: "more than sufficient deposit",
402+
order: testOrder{
403+
srcChainID: evmchain.IDBaseSepolia,
404+
dstChainID: evmchain.IDHolesky,
405+
deposits: []Deposit{{
406+
Amount: new(big.Int).Add(
407+
depositFor(ether(1), standardFeeBips), // required deposit
408+
gwei(1), // a little more
409+
),
410+
}},
411+
calls: []Call{{Value: ether(1)}},
412+
expenses: []Expense{{Amount: ether(1)}},
413+
},
414+
mock: func(clients MockClients) {
415+
mockNativeBalance(t, clients.Client(t, evmchain.IDHolesky), solver, ether(2))
416+
},
417+
},
387418
}
388419
}
389420

@@ -641,3 +672,11 @@ func mustGetABI(metadata *bind.MetaData) *abi.ABI {
641672

642673
return abi
643674
}
675+
676+
func ether(x int64) *big.Int {
677+
return new(big.Int).Mul(big.NewInt(x), big.NewInt(1e18))
678+
}
679+
680+
func gwei(x int64) *big.Int {
681+
return new(big.Int).Mul(big.NewInt(x), big.NewInt(1e9))
682+
}

0 commit comments

Comments
 (0)