mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
The swap form's four questions were split across two services: the indexer answered what can be traded and what it pays, while a TypeScript service answered what to approve and what to sign. The last two need the route the first two produced, so the split meant a quote crossed a process boundary to be turned into calldata. HandleSwap encodes the router call from a quote and bounds it by the caller's slippage. It reads no state — the quote already carries the route, the amounts and each hop's fee, and re-reading here would be a second opinion about a price the caller has already accepted, a block later than the one they saw. HandleCheckApproval reads the allowance the router holds and returns the grant when it falls short. Silence from a token is not an allowance of zero, and an unreachable node is not either; both would skip an approval and leave the swap to revert. The conversation with a chain moves to one type. It was a method on the quoter, which left the approval endpoint either reaching through a quoter to ask an unrelated question or growing a second copy of the batching, timeout and decode. Calldata is asserted against bytes recorded from the service being retired, across exact-input, exact-output, single-hop and multi-hop. A wallet signs these, so a byte is the unit of correctness. Co-authored-by: Hanzo Dev <[email protected]>
99 lines
2.7 KiB
Go
99 lines
2.7 KiB
Go
package engine
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
)
|
|
|
|
// How this package talks to a chain.
|
|
//
|
|
// One type, because "ask the node something" is one job. The quoter reads pool
|
|
// state, the approval endpoint reads an allowance, and both want the same
|
|
// thing: a batch of eth_calls at head, answered positionally. Two copies of
|
|
// that would be two places for a timeout, a status check, or a decode to
|
|
// differ, and the difference would show up as one endpoint working while its
|
|
// neighbour reports a revert that never happened.
|
|
|
|
// node is a chain's read side: an endpoint and the client that dials it.
|
|
type node struct {
|
|
rpc string
|
|
client *http.Client
|
|
}
|
|
|
|
func dial(rpc string) *node {
|
|
return &node{rpc: rpc, client: &http.Client{Timeout: quoteBudget}}
|
|
}
|
|
|
|
type ethCall struct {
|
|
To string
|
|
Data string
|
|
}
|
|
|
|
type rpcRequest struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Method string `json:"method"`
|
|
Params []any `json:"params"`
|
|
}
|
|
|
|
func ethCallReq(id int, to, data string) rpcRequest {
|
|
return rpcRequest{JSONRPC: "2.0", ID: id, Method: "eth_call",
|
|
Params: []any{map[string]string{"to": to, "data": data}, "latest"}}
|
|
}
|
|
|
|
// call runs a batch of eth_calls at head and returns each result positionally.
|
|
func (n *node) call(ctx context.Context, calls []ethCall) ([]string, error) {
|
|
reqs := make([]rpcRequest, len(calls))
|
|
for i, c := range calls {
|
|
reqs[i] = ethCallReq(i, c.To, c.Data)
|
|
}
|
|
return n.batch(ctx, reqs)
|
|
}
|
|
|
|
// batch sends one JSON-RPC batch and returns each result positionally by id.
|
|
//
|
|
// A call that reverted comes back empty, and that is an answer — "this contract
|
|
// does not do that" is exactly how a pool's venue is settled. A batch that does
|
|
// not come back at all is an error, because then nothing is known and an empty
|
|
// result would be read as a revert that never happened.
|
|
func (n *node) batch(ctx context.Context, reqs []rpcRequest) ([]string, error) {
|
|
out := make([]string, len(reqs))
|
|
if len(reqs) == 0 {
|
|
return out, nil
|
|
}
|
|
body, err := json.Marshal(reqs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, n.rpc, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := n.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("rpc: %s", resp.Status)
|
|
}
|
|
|
|
var results []struct {
|
|
ID int `json:"id"`
|
|
Result string `json:"result"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range results {
|
|
if r.ID >= 0 && r.ID < len(out) {
|
|
out[r.ID] = r.Result
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|