diff --git a/Makefile b/Makefile index 60dae86a..2661df34 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ MORPHO_GRAPHQL_URL ?= https://api.morpho.org/graphql # Contracts whose ABIs are vendored via refresh-abi. ABIS come from the rfq Foundry build; the # CORE_MIRROR_ABIS (the 3F ThreeFAdapter, LiquidLane adapter, adapter factory, universal delegator, # and vault/ERC4626 interfaces) come from the core-mirror build, since they aren't in rfq/out. -ABIS := IRequest IVaultController IWhitelist Executor Reactor +ABIS := IRequest IVaultController IWhitelist Executor Reactor LiquidLaneLifiExecutor CORE_MIRROR_ABIS := ThreeFAdapter LiquidLaneAdapter IAdapterFactory IVaultV2 IERC4626 # api/abi/UniversalDelegator.json is hand-vendored to a minimal {limitOf} ABI (the full contract has # an overloaded deallocateAll that abigen rejects, and the solver only reads limitOf) — like Multicall3. @@ -54,16 +54,19 @@ CORE_MIRROR_ABIS := ThreeFAdapter LiquidLaneAdapter IAdapterFactory IVaultV2 IER BINDINGS_V2 := ThreeFAdapter:3f/adapter IRequest:3f/request \ IVaultController:3f/vaultcontroller IWhitelist:3f/whitelist \ LiquidLaneAdapter:liquidlane/adapter Executor:rfq/executor Reactor:rfq/reactor \ + LiquidLaneLifiExecutor:lifi/executor \ + ILifiInputSettler:lifi/inputsettler \ IAdapterFactory:adapterfactory UniversalDelegator:delegator IVaultV2:vaultv2 IERC4626:erc4626 \ SymbioticOevSolver:oev/callback RedStoneExecutor:oev/executor Morpho:oev/morpho \ AdaptiveCurveIrm:oev/irm MorphoOracle:oev/oracle \ - AggregatorV3:oev/aggregator \ + AggregatorV3:chainlink/aggregator \ ERC20:erc20 Multicall3:multicall3 # The OEV contracts (Morpho + its AdaptiveCurve IRM + market oracle, RedStone -# Executor, SymbioticOevSolver) plus a minimal ERC20 (decimals() only) aren't in our Foundry build, so their -# ABIs are hand-vendored under api/abi/ (not in ABIS/CORE_MIRROR_ABIS/refresh-abi). RedStoneExecutor avoids -# the rfq Executor name clash; solver ERC-20 reads (asset/balanceOf) reuse erc4626, the generic -# chain.Decimals reader uses erc20. +# Executor, SymbioticOevSolver), the LI.FI input settler ABI, plus a minimal ERC20 +# (decimals() only) aren't in our default Foundry build, so their ABIs are hand-vendored under +# api/abi/ (not in ABIS/CORE_MIRROR_ABIS/refresh-abi). RedStoneExecutor avoids the rfq Executor +# name clash; solver ERC-20 reads (asset/balanceOf) reuse erc4626, the generic chain.Decimals reader +# uses erc20. # Multicall3 is v2 like everything else — api/abi/Multicall3.json is hand-vendored (not a Foundry contract), # so it's in BINDINGS_V2 but not ABIS. The chain.Multicall transport packs/unpacks aggregate3 and does its # own eth_call. @@ -132,7 +135,7 @@ refresh-morpho-graphql-schema: ## Re-pull the live Morpho GraphQL schema SDL (MO .PHONY: bindings bindings: ## Generate Go bindings from vendored ABIs (grouped per integration; package = leaf dir) - @for pair in $(BINDINGS_V2); do \ + @set -e; for pair in $(BINDINGS_V2); do \ c="$${pair%%:*}"; rel="$${pair##*:}"; pkg="$${rel##*/}"; \ abi="api/abi/$$c.json"; \ if [[ ! -f "$$abi" ]]; then echo "missing $$abi (run make refresh-abi)"; exit 1; fi; \ @@ -146,10 +149,7 @@ bindings: ## Generate Go bindings from vendored ABIs (grouped per integration; p # backend's OpenAPI 3.1 spec; we use it for the 3F (3.0) and LI.FI order-server specs too for one toolchain. # $(OPENAPI_GENERATOR_VERSION) is the floor — 5.4.0/7.0.1 fail on the 3.1 spec. The generated package is # stdlib-only (no go.mod change); the recipes strip the generator's non-package cruft, keeping just the Go -# client. $(4) is optional extra generator flags — used only by the LI.FI recipe to pass -# --skip-validate-spec (its spec is labelled OpenAPI 3.0.0 but uses 3.1 JSON-Schema constructs — prefixItems / -# propertyNames — and has dangling oneOf $refs; the generator handles them fine but its strict validator -# rejects them). 3f/rfq keep validation on. +# client. $(4) is available for source-specific generator flags; current specs generate with validation on. define gen_openapi_client GO_POST_PROCESS_FILE='gofmt -w' OPENAPI_GENERATOR_VERSION=$(OPENAPI_GENERATOR_VERSION) bash ./hack/openapi-generator-cli.sh \ generate --enable-post-process-file $(4) -i ./$(1) -g go -o ./$(2) --package-name $(3) @@ -169,18 +169,7 @@ refresh-rfq-client: ## Generate the RFQ backend client (openapi-generator, Go) f .PHONY: refresh-lifi-client refresh-lifi-client: ## Generate the LI.FI order-server client (openapi-generator, Go) from the vendored spec @rm -f api/lifiorder/*.go - @# The raw vendored spec has two upstream defects that make the generated Go uncompilable (dangling - @# oneOf $refs in QuoteDto.order; multi-tag operations that duplicate request structs). We keep the - @# vendored file raw (contract of record) and generate from a normalized temp copy produced by - @# hack/lifi-openapi-normalize.py (see that script for the exact, documented fixes). Inlined rather than - @# using gen_openapi_client so the normalization + temp-file plumbing lives in one shell block; - @# --skip-validate-spec is still needed (the spec is labelled 3.0.0 but uses 3.1 JSON-Schema constructs). - tmp="$$(mktemp -p . --suffix=.lifi-normalized.json)"; \ - trap 'rm -f "$$tmp"' EXIT; \ - python3 hack/lifi-openapi-normalize.py < openapi/lifi-order.openapi.json > "$$tmp"; \ - GO_POST_PROCESS_FILE='gofmt -w' OPENAPI_GENERATOR_VERSION=$(OPENAPI_GENERATOR_VERSION) bash ./hack/openapi-generator-cli.sh \ - generate --enable-post-process-file --skip-validate-spec -i "$$tmp" -g go -o ./api/lifiorder --package-name lifiorder - cd api/lifiorder && rm -rf go.mod go.sum .gitignore .openapi-generator-ignore .travis.yml git_push.sh README.md api docs test .openapi-generator + $(call gen_openapi_client,openapi/lifi-order.openapi.json,api/lifiorder,lifiorder) .PHONY: refresh-morpho-graphql-client refresh-morpho-graphql-client: ## Generate the Morpho GraphQL client (genqlient) from the vendored schema + operations @@ -216,6 +205,10 @@ test: ## Run tests with race detector + coverage (hermetic only; fork/live suite test-oev-live: ## OEV live checks — Morpho API discovery plus optional Sepolia fork payload dump go test -tags live -run TestLive -v ./internal/solvers/redstoneoev/... +.PHONY: test-txmanager-anvil +test-txmanager-anvil: ## Exercise replacement/cancellation against an Anvil mempool with automine disabled + go test -race -tags integration -run TestAnvilTxManagerPendingLifecycle -v ./internal/txmanager + .PHONY: format format: ## Run golangci-lint with autofix golangci-lint run --fix diff --git a/README.md b/README.md index d684cba3..45a2c5fb 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ are listed under [Solvers](#solvers). - **`internal/solvers//`** — one self-contained package per integration; all protocol-specific logic lives here. - **`internal/{config,chain,signer,txmanager}`** — solver-agnostic infra: two-stage config, vault / - Multicall3 reads, a pluggable signer, and a nonce-serialized transaction sender shared across solvers. + Multicall3 reads, a pluggable signer, and a nonce-serialized transaction broadcaster with independent + receipt waits, shared across solvers. - **`api/`** — committed codegen: contract `bindings/` (abigen) and protocol API clients, each refreshable from upstream. @@ -39,8 +40,9 @@ and validated by its own solver. Adding a solver touches **no** framework code | `3f-bridge-facilitator` | 3F (Grunt) bridge-loan auctions | [plan](docs/3F-PLAN.md) | [yaml](config/3f.example.yaml) | | `rfq-filler` | Symbiotic RFQ quoting + order filling | [plan](docs/RFQ-PLAN.md) | [yaml](config/rfq.example.yaml) | | `redstone-oev` | RedStone OEV liquidations | [plan](docs/OEV-PLAN.md) | [yaml](config/redstone-oev.example.yaml) | +| `lifi-samechain` | LI.FI same-chain intents over LiquidLane | [plan](docs/LIFI-PLAN.md) | [yaml](config/lifi.example.yaml) | -The `3f-bridge-facilitator`, `rfq-filler`, and `redstone-oev` solvers expose a pluggable +All solvers expose a pluggable **strategy** — the built-in `default` or an external `webhook` you run; see [Strategies](#strategies). @@ -97,6 +99,47 @@ and roadmap: [`docs/OEV-PLAN.md`](docs/OEV-PLAN.md) · example [`config/redstone-oev.example.yaml`](config/redstone-oev.example.yaml). +### LI.FI Same-Chain Intents — `lifi-samechain` + +A same-chain LI.FI Intents solver for LiquidLane-backed RWA → underlying routes. It publishes gas-aware +standing quotes from current adapter liquidity and receives matched, already-opened escrow orders over the +LI.FI WebSocket feed. Before each fill it rechecks the canonical order status, adapter state, gas cost, and +strategy decision, then atomically claims the input, redeems it through LiquidLane, and fills the output via +`LiquidLaneLifiExecutor`. Capacity reserved by already-submitted fills is deducted from both later fill +decisions and standing quotes until those transactions complete. + +The executor contract is the registered LI.FI solver account. It is registered once through EIP-1271 using +a caller signature bound to the executor's EIP-712 domain, appears as `exclusiveFor` in quotes, and calls the +settler's direct finalise path. The framework signer is an authorized executor caller and transaction sender; +fills do not carry a per-order `AllowOpen` signature. +The owner manages callers, while ERC-1271 validates domain-separated registration signatures against the +current callers. + +Our deployment convention is one LI.FI API key per registered executor contract. LI.FI can register +multiple accounts under one key, but this deployment deliberately does not share a key across executors. +All processes using one executor therefore share its API key and LI.FI reputation; active/active operation +also requires external order coordination. The API key, executor owner key, and caller transaction key are +distinct credentials. + +Only on-chain escrow orders are supported; gasless Compact, Permit2/3009, Dutch auctions, and future-order +scheduling are out of scope. Dutch (`0x01`) and exclusive Dutch (`0xe1`) orders are ignored at WebSocket +admission and logged as unsupported. `solverMode: external` serves direct filler-authorized adapters. +`solverMode: internal` also enables signed private discounts through the shared backend. `tokensToQuote` uses the same `all`, +`permissioned`, and `permissionless` scopes as RFQ; permissioned inputs must execute through one physical +route. The order-server REST/WS endpoints are explicit required config, and each Chainlink gas feed has +its own required max age. The default strategy evaluates quote ranges as exact input across every allocation +transition; `rangeCount` sets the target number of ranges across available capacity. See the +plan for settlement, pricing, concurrency, and onboarding details: +[`docs/LIFI-PLAN.md`](docs/LIFI-PLAN.md) · example +[`config/lifi.example.yaml`](config/lifi.example.yaml). + +The opened-order settler must report `governanceFee() == 0`. The solver checks this at startup and again for +every admitted order. Startup fails closed; at runtime an unreadable or non-zero fee skips the order with an +error log before planning or submission. + +The implementation is ready for the opened-order path. The next live E2E requires deploying the current +executor build, registering it with LI.FI, and granting it filler authorization on the target adapter. + ### Strategies The solvers split protocol plumbing (reads, signing, submission — fixed) from the @@ -104,11 +147,18 @@ The solvers split protocol plumbing (reads, signing, submission — fixed) from - **`default`** — the built-in in-process strategy for that solver. - **`webhook`** — delegates each decision to an **external HTTP service you run**: the solver sends it - the raw facts as JSON and executes the plan it returns, so your service owns the logic. + the raw facts as JSON and executes the validated plan it returns, so your service owns the logic. + LI.FI also rejects returned fills that exceed current capacity or do not cover the order plus gas. + It uses `POST /decide-quotes` and `POST /decide-fill` under the configured webhook URL. This is the seam for customizing a solver without forking. Contract and trust model: [`docs/strategy-plan.md`](docs/strategy-plan.md). +The shared `txManager` fee-bumps pending transactions on `replacementIntervalMs`. After +`pendingTimeoutMs`, it cancels only the lowest unresolved nonce before allowing later queued nonces +to proceed. The required `maxFeeGwei` is the absolute ceiling; normal sends reserve one fee bump +inside that ceiling so cancellation still has headroom. + ## Requirements - Go (toolchain version pinned in [`go.mod`](./go.mod); auto-fetched by recent Go releases). @@ -122,6 +172,7 @@ This is the seam for customizing a solver without forking. Contract and trust mo make build # build ./bin/vault-solver ./bin/vault-solver version make test # go test -race -cover ./... +make test-txmanager-anvil # real pending replacement/cancellation against local Anvil make lint # golangci-lint ./bin/vault-solver run --config config/3f.example.yaml ``` @@ -141,7 +192,8 @@ implementation and hands the opaque `solver.config` block to that solver to type own fully annotated example under `config/` (see the *Example config* column above) — every field, including the shared `chain`/`signer`/`txManager`/`observability` blocks, is documented inline there. The `chain` block takes a primary `rpcUrl` plus optional `rpcFallbackUrls` — HTTP(S) endpoints tried -in order when the primary is unavailable. **Never commit a real key or live config** — keys are +in order when the primary is unavailable. LiquidLane state reads always use RPC `latest`; an archive +node is not required. **Never commit a real key or live config** — keys are supplied via env/file behind the `Signer` interface; `*.local.*` and `.env` are gitignored. ## Code generation diff --git a/api/abi/ILifiInputSettler.json b/api/abi/ILifiInputSettler.json new file mode 100644 index 00000000..0811c354 --- /dev/null +++ b/api/abi/ILifiInputSettler.json @@ -0,0 +1,1658 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "initialOwner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AlreadyInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyPurchased", + "type": "error" + }, + { + "inputs": [], + "name": "CallOutOfRange", + "type": "error" + }, + { + "inputs": [], + "name": "CodeSize0", + "type": "error" + }, + { + "inputs": [], + "name": "ContextOutOfRange", + "type": "error" + }, + { + "inputs": [], + "name": "Expired", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + } + ], + "name": "FillDeadlineAfterExpiry", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "expected", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "actual", + "type": "uint32" + } + ], + "name": "FilledTooLate", + "type": "error" + }, + { + "inputs": [], + "name": "GovernanceFeeChangeNotReady", + "type": "error" + }, + { + "inputs": [], + "name": "GovernanceFeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "HasDirtyBits", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOrderStatus", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPurchaser", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidShortString", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSigner", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTimestampLength", + "type": "error" + }, + { + "inputs": [], + "name": "NewOwnerIsZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "NoDestination", + "type": "error" + }, + { + "inputs": [], + "name": "NoHandoverRequest", + "type": "error" + }, + { + "inputs": [], + "name": "NotOrderOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "provided", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "computed", + "type": "bytes32" + } + ], + "name": "OrderIdMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyDetected", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [], + "name": "SignatureAndInputsNotEqual", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes1", + "name": "", + "type": "bytes1" + } + ], + "name": "SignatureNotSupported", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "str", + "type": "string" + } + ], + "name": "StringTooLong", + "type": "error" + }, + { + "inputs": [], + "name": "TimestampNotPassed", + "type": "error" + }, + { + "inputs": [], + "name": "TimestampPassed", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actual", + "type": "uint256" + } + ], + "name": "WrongChain", + "type": "error" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "solver", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "destination", + "type": "bytes32" + } + ], + "name": "Finalised", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "oldGovernanceFee", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "newGovernanceFee", + "type": "uint64" + } + ], + "name": "GovernanceFeeChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "nextGovernanceFee", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "nextGovernanceFeeTime", + "type": "uint64" + } + ], + "name": "NextGovernanceFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + } + ], + "name": "Open", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "originChainId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "address", + "name": "inputOracle", + "type": "address" + }, + { + "internalType": "uint256[2][]", + "name": "inputs", + "type": "uint256[2][]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "settler", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "token", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "callbackData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "context", + "type": "bytes" + } + ], + "internalType": "struct MandateOutput[]", + "name": "outputs", + "type": "tuple[]" + } + ], + "indexed": false, + "internalType": "struct StandardOrder", + "name": "order", + "type": "tuple" + } + ], + "name": "Open", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "solver", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "purchaser", + "type": "bytes32" + } + ], + "name": "OrderPurchased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pendingOwner", + "type": "address" + } + ], + "name": "OwnershipHandoverCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pendingOwner", + "type": "address" + } + ], + "name": "OwnershipHandoverRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + } + ], + "name": "Refunded", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "applyGovernanceFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "cancelOwnershipHandover", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pendingOwner", + "type": "address" + } + ], + "name": "completeOwnershipHandover", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "originChainId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "address", + "name": "inputOracle", + "type": "address" + }, + { + "internalType": "uint256[2][]", + "name": "inputs", + "type": "uint256[2][]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "settler", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "token", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "callbackData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "context", + "type": "bytes" + } + ], + "internalType": "struct MandateOutput[]", + "name": "outputs", + "type": "tuple[]" + } + ], + "internalType": "struct StandardOrder", + "name": "order", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "timestamp", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "solver", + "type": "bytes32" + } + ], + "internalType": "struct InputSettlerBase.SolveParams[]", + "name": "solveParams", + "type": "tuple[]" + }, + { + "internalType": "bytes32", + "name": "destination", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "call", + "type": "bytes" + } + ], + "name": "finalise", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "originChainId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "address", + "name": "inputOracle", + "type": "address" + }, + { + "internalType": "uint256[2][]", + "name": "inputs", + "type": "uint256[2][]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "settler", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "token", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "callbackData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "context", + "type": "bytes" + } + ], + "internalType": "struct MandateOutput[]", + "name": "outputs", + "type": "tuple[]" + } + ], + "internalType": "struct StandardOrder", + "name": "order", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "timestamp", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "solver", + "type": "bytes32" + } + ], + "internalType": "struct InputSettlerBase.SolveParams[]", + "name": "solveParams", + "type": "tuple[]" + }, + { + "internalType": "bytes32", + "name": "destination", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "call", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "orderOwnerSignature", + "type": "bytes" + } + ], + "name": "finaliseWithSignature", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "governanceFee", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextGovernanceFee", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextGovernanceFeeTime", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "originChainId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "address", + "name": "inputOracle", + "type": "address" + }, + { + "internalType": "uint256[2][]", + "name": "inputs", + "type": "uint256[2][]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "settler", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "token", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "callbackData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "context", + "type": "bytes" + } + ], + "internalType": "struct MandateOutput[]", + "name": "outputs", + "type": "tuple[]" + } + ], + "internalType": "struct StandardOrder", + "name": "order", + "type": "tuple" + } + ], + "name": "open", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "originChainId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "address", + "name": "inputOracle", + "type": "address" + }, + { + "internalType": "uint256[2][]", + "name": "inputs", + "type": "uint256[2][]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "settler", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "token", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "callbackData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "context", + "type": "bytes" + } + ], + "internalType": "struct MandateOutput[]", + "name": "outputs", + "type": "tuple[]" + } + ], + "internalType": "struct StandardOrder", + "name": "order", + "type": "tuple" + }, + { + "internalType": "address", + "name": "sponsor", + "type": "address" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "openFor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "originChainId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "address", + "name": "inputOracle", + "type": "address" + }, + { + "internalType": "uint256[2][]", + "name": "inputs", + "type": "uint256[2][]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "settler", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "token", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "callbackData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "context", + "type": "bytes" + } + ], + "internalType": "struct MandateOutput[]", + "name": "outputs", + "type": "tuple[]" + } + ], + "internalType": "struct StandardOrder", + "name": "order", + "type": "tuple" + }, + { + "internalType": "address", + "name": "sponsor", + "type": "address" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "address", + "name": "destination", + "type": "address" + }, + { + "internalType": "bytes", + "name": "call", + "type": "bytes" + } + ], + "name": "openForAndFinalise", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "originChainId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "address", + "name": "inputOracle", + "type": "address" + }, + { + "internalType": "uint256[2][]", + "name": "inputs", + "type": "uint256[2][]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "settler", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "token", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "callbackData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "context", + "type": "bytes" + } + ], + "internalType": "struct MandateOutput[]", + "name": "outputs", + "type": "tuple[]" + } + ], + "internalType": "struct StandardOrder", + "name": "order", + "type": "tuple" + } + ], + "name": "orderIdentifier", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + } + ], + "name": "orderStatus", + "outputs": [ + { + "internalType": "enum InputSettlerEscrow.OrderStatus", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "result", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pendingOwner", + "type": "address" + } + ], + "name": "ownershipHandoverExpiresAt", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "destination", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + }, + { + "internalType": "uint64", + "name": "discount", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "timeToBuy", + "type": "uint32" + } + ], + "internalType": "struct OrderPurchase", + "name": "orderPurchase", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "originChainId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "address", + "name": "inputOracle", + "type": "address" + }, + { + "internalType": "uint256[2][]", + "name": "inputs", + "type": "uint256[2][]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "settler", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "token", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "callbackData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "context", + "type": "bytes" + } + ], + "internalType": "struct MandateOutput[]", + "name": "outputs", + "type": "tuple[]" + } + ], + "internalType": "struct StandardOrder", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "orderSolvedByIdentifier", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "purchaser", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "expiryTimestamp", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "solverSignature", + "type": "bytes" + } + ], + "name": "purchaseOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "solver", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + } + ], + "name": "purchasedOrders", + "outputs": [ + { + "internalType": "uint32", + "name": "lastOrderTimestamp", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "purchaser", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "originChainId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "expires", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fillDeadline", + "type": "uint32" + }, + { + "internalType": "address", + "name": "inputOracle", + "type": "address" + }, + { + "internalType": "uint256[2][]", + "name": "inputs", + "type": "uint256[2][]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "oracle", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "settler", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "token", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "recipient", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "callbackData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "context", + "type": "bytes" + } + ], + "internalType": "struct MandateOutput[]", + "name": "outputs", + "type": "tuple[]" + } + ], + "internalType": "struct StandardOrder", + "name": "order", + "type": "tuple" + } + ], + "name": "refund", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "requestOwnershipHandover", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_nextGovernanceFee", + "type": "uint64" + } + ], + "name": "setGovernanceFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/api/abi/LiquidLaneLifiExecutor.json b/api/abi/LiquidLaneLifiExecutor.json new file mode 100644 index 00000000..7d0d0a25 --- /dev/null +++ b/api/abi/LiquidLaneLifiExecutor.json @@ -0,0 +1,557 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "inputSettler", + "type": "address", + "internalType": "address" + }, + { + "name": "outputSettler", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "INPUT_SETTLER", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "LIFI_REGISTRATION_TYPEHASH", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "OUTPUT_SETTLER", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "callers", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "eip712Domain", + "inputs": [], + "outputs": [ + { + "name": "fields", + "type": "bytes1", + "internalType": "bytes1" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "version", + "type": "string", + "internalType": "string" + }, + { + "name": "chainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "verifyingContract", + "type": "address", + "internalType": "address" + }, + { + "name": "salt", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "extensions", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "finaliseWithCurrentTimestamp", + "inputs": [ + { + "name": "order", + "type": "tuple", + "internalType": "struct IInputSettler.StandardOrder", + "components": [ + { + "name": "user", + "type": "address", + "internalType": "address" + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "originChainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "expires", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "fillDeadline", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "inputOracle", + "type": "address", + "internalType": "address" + }, + { + "name": "inputs", + "type": "uint256[2][]", + "internalType": "uint256[2][]" + }, + { + "name": "outputs", + "type": "tuple[]", + "internalType": "struct MandateOutput[]", + "components": [ + { + "name": "oracle", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "settler", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "chainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "token", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "recipient", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "callbackData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "context", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ] + }, + { + "name": "routes", + "type": "tuple[]", + "internalType": "struct ILiquidLaneLifiExecutor.FillRoute[]", + "components": [ + { + "name": "adapter", + "type": "address", + "internalType": "address" + }, + { + "name": "amountIn", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "amountOut", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "discount", + "type": "tuple", + "internalType": "struct ILiquidLaneLifiExecutor.FillDiscount", + "components": [ + { + "name": "discountId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "discountSwap", + "type": "tuple", + "internalType": "struct ILiquidLaneAdapter.DiscountSwap", + "components": [ + { + "name": "discount", + "type": "tuple", + "internalType": "struct ILiquidLaneAdapter.Discount", + "components": [ + { + "name": "tokenToRedeem", + "type": "address", + "internalType": "address" + }, + { + "name": "discount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "signer", + "type": "address", + "internalType": "address" + }, + { + "name": "protocol", + "type": "address", + "internalType": "address" + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint48", + "internalType": "uint48" + } + ] + }, + { + "name": "signerSignature", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "protocolDeadline", + "type": "uint48", + "internalType": "uint48" + } + ] + }, + { + "name": "protocolSignature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "owner_", + "type": "address", + "internalType": "address" + }, + { + "name": "initCallers", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "isCaller", + "inputs": [ + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isValidSignature", + "inputs": [ + { + "name": "hash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "lifiRegistrationDigest", + "inputs": [ + { + "name": "messageHash", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "orderFinalised", + "inputs": [ + { + "name": "inputs", + "type": "uint256[2][]", + "internalType": "uint256[2][]" + }, + { + "name": "executionData", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setCallers", + "inputs": [ + { + "name": "newCallers", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "EIP712DomainChanged", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint64", + "indexed": false, + "internalType": "uint64" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetCallers", + "inputs": [ + { + "name": "newCallers", + "type": "address[]", + "indexed": false, + "internalType": "address[]" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "InvalidInitialization", + "inputs": [] + }, + { + "type": "error", + "name": "NotCaller", + "inputs": [] + }, + { + "type": "error", + "name": "NotInitializing", + "inputs": [] + }, + { + "type": "error", + "name": "NotInputSettler", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + } +] diff --git a/api/bindings/oev/aggregator/AggregatorV3.go b/api/bindings/chainlink/aggregator/AggregatorV3.go similarity index 100% rename from api/bindings/oev/aggregator/AggregatorV3.go rename to api/bindings/chainlink/aggregator/AggregatorV3.go diff --git a/api/bindings/lifi/executor/LiquidLaneLifiExecutor.go b/api/bindings/lifi/executor/LiquidLaneLifiExecutor.go new file mode 100644 index 00000000..402a4843 --- /dev/null +++ b/api/bindings/lifi/executor/LiquidLaneLifiExecutor.go @@ -0,0 +1,940 @@ +// Code generated via abigen V2 - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package executor + +import ( + "bytes" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind/v2" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = bytes.Equal + _ = errors.New + _ = big.NewInt + _ = common.Big1 + _ = types.BloomLookup + _ = abi.ConvertType +) + +// IInputSettlerStandardOrder is an auto generated low-level Go binding around an user-defined struct. +type IInputSettlerStandardOrder struct { + User common.Address + Nonce *big.Int + OriginChainId *big.Int + Expires uint32 + FillDeadline uint32 + InputOracle common.Address + Inputs [][2]*big.Int + Outputs []MandateOutput +} + +// ILiquidLaneAdapterDiscount is an auto generated low-level Go binding around an user-defined struct. +type ILiquidLaneAdapterDiscount struct { + TokenToRedeem common.Address + Discount *big.Int + Signer common.Address + Protocol common.Address + Nonce *big.Int + Deadline *big.Int +} + +// ILiquidLaneAdapterDiscountSwap is an auto generated low-level Go binding around an user-defined struct. +type ILiquidLaneAdapterDiscountSwap struct { + Discount ILiquidLaneAdapterDiscount + SignerSignature []byte + ProtocolDeadline *big.Int +} + +// ILiquidLaneLifiExecutorFillDiscount is an auto generated low-level Go binding around an user-defined struct. +type ILiquidLaneLifiExecutorFillDiscount struct { + DiscountId [32]byte + DiscountSwap ILiquidLaneAdapterDiscountSwap + ProtocolSignature []byte +} + +// ILiquidLaneLifiExecutorFillRoute is an auto generated low-level Go binding around an user-defined struct. +type ILiquidLaneLifiExecutorFillRoute struct { + Adapter common.Address + AmountIn *big.Int + AmountOut *big.Int + Discount ILiquidLaneLifiExecutorFillDiscount +} + +// MandateOutput is an auto generated low-level Go binding around an user-defined struct. +type MandateOutput struct { + Oracle [32]byte + Settler [32]byte + ChainId *big.Int + Token [32]byte + Amount *big.Int + Recipient [32]byte + CallbackData []byte + Context []byte +} + +// LiquidLaneLifiExecutorMetaData contains all meta data concerning the LiquidLaneLifiExecutor contract. +var LiquidLaneLifiExecutorMetaData = bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"inputSettler\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"outputSettler\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"INPUT_SETTLER\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"LIFI_REGISTRATION_TYPEHASH\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"OUTPUT_SETTLER\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"callers\",\"inputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"eip712Domain\",\"inputs\":[],\"outputs\":[{\"name\":\"fields\",\"type\":\"bytes1\",\"internalType\":\"bytes1\"},{\"name\":\"name\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"salt\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"extensions\",\"type\":\"uint256[]\",\"internalType\":\"uint256[]\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"finaliseWithCurrentTimestamp\",\"inputs\":[{\"name\":\"order\",\"type\":\"tuple\",\"internalType\":\"structIInputSettler.StandardOrder\",\"components\":[{\"name\":\"user\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"nonce\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"originChainId\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"expires\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"fillDeadline\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"inputOracle\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"inputs\",\"type\":\"uint256[2][]\",\"internalType\":\"uint256[2][]\"},{\"name\":\"outputs\",\"type\":\"tuple[]\",\"internalType\":\"structMandateOutput[]\",\"components\":[{\"name\":\"oracle\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"settler\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"chainId\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"token\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"recipient\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"callbackData\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"context\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]}]},{\"name\":\"routes\",\"type\":\"tuple[]\",\"internalType\":\"structILiquidLaneLifiExecutor.FillRoute[]\",\"components\":[{\"name\":\"adapter\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amountIn\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"amountOut\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"discount\",\"type\":\"tuple\",\"internalType\":\"structILiquidLaneLifiExecutor.FillDiscount\",\"components\":[{\"name\":\"discountId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"discountSwap\",\"type\":\"tuple\",\"internalType\":\"structILiquidLaneAdapter.DiscountSwap\",\"components\":[{\"name\":\"discount\",\"type\":\"tuple\",\"internalType\":\"structILiquidLaneAdapter.Discount\",\"components\":[{\"name\":\"tokenToRedeem\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"discount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"signer\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"protocol\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"nonce\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"deadline\",\"type\":\"uint48\",\"internalType\":\"uint48\"}]},{\"name\":\"signerSignature\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"protocolDeadline\",\"type\":\"uint48\",\"internalType\":\"uint48\"}]},{\"name\":\"protocolSignature\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[{\"name\":\"owner_\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"initCallers\",\"type\":\"address[]\",\"internalType\":\"address[]\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"isCaller\",\"inputs\":[{\"name\":\"caller\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isValidSignature\",\"inputs\":[{\"name\":\"hash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"signature\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes4\",\"internalType\":\"bytes4\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"lifiRegistrationDigest\",\"inputs\":[{\"name\":\"messageHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"orderFinalised\",\"inputs\":[{\"name\":\"inputs\",\"type\":\"uint256[2][]\",\"internalType\":\"uint256[2][]\"},{\"name\":\"executionData\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setCallers\",\"inputs\":[{\"name\":\"newCallers\",\"type\":\"address[]\",\"internalType\":\"address[]\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"EIP712DomainChanged\",\"inputs\":[],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Initialized\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SetCallers\",\"inputs\":[{\"name\":\"newCallers\",\"type\":\"address[]\",\"indexed\":false,\"internalType\":\"address[]\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"InvalidInitialization\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotCaller\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotInitializing\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NotInputSettler\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"OwnableInvalidOwner\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"OwnableUnauthorizedAccount\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"SafeERC20FailedOperation\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]}]", + ID: "LiquidLaneLifiExecutor", +} + +// LiquidLaneLifiExecutor is an auto generated Go binding around an Ethereum contract. +type LiquidLaneLifiExecutor struct { + abi abi.ABI +} + +// NewLiquidLaneLifiExecutor creates a new instance of LiquidLaneLifiExecutor. +func NewLiquidLaneLifiExecutor() *LiquidLaneLifiExecutor { + parsed, err := LiquidLaneLifiExecutorMetaData.ParseABI() + if err != nil { + panic(errors.New("invalid ABI: " + err.Error())) + } + return &LiquidLaneLifiExecutor{abi: *parsed} +} + +// Instance creates a wrapper for a deployed contract instance at the given address. +// Use this to create the instance object passed to abigen v2 library functions Call, Transact, etc. +func (c *LiquidLaneLifiExecutor) Instance(backend bind.ContractBackend, addr common.Address) *bind.BoundContract { + return bind.NewBoundContract(addr, c.abi, backend, backend, backend) +} + +// PackConstructor is the Go binding used to pack the parameters required for +// contract deployment. +// +// Solidity: constructor(address inputSettler, address outputSettler) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackConstructor(inputSettler common.Address, outputSettler common.Address) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("", inputSettler, outputSettler) + if err != nil { + panic(err) + } + return enc +} + +// PackINPUTSETTLER is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xb627707d. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function INPUT_SETTLER() view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackINPUTSETTLER() []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("INPUT_SETTLER") + if err != nil { + panic(err) + } + return enc +} + +// TryPackINPUTSETTLER is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xb627707d. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function INPUT_SETTLER() view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackINPUTSETTLER() ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("INPUT_SETTLER") +} + +// UnpackINPUTSETTLER is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0xb627707d. +// +// Solidity: function INPUT_SETTLER() view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackINPUTSETTLER(data []byte) (common.Address, error) { + out, err := liquidLaneLifiExecutor.abi.Unpack("INPUT_SETTLER", data) + if err != nil { + return *new(common.Address), err + } + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + return out0, nil +} + +// PackLIFIREGISTRATIONTYPEHASH is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xd0c83dad. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function LIFI_REGISTRATION_TYPEHASH() view returns(bytes32) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackLIFIREGISTRATIONTYPEHASH() []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("LIFI_REGISTRATION_TYPEHASH") + if err != nil { + panic(err) + } + return enc +} + +// TryPackLIFIREGISTRATIONTYPEHASH is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xd0c83dad. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function LIFI_REGISTRATION_TYPEHASH() view returns(bytes32) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackLIFIREGISTRATIONTYPEHASH() ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("LIFI_REGISTRATION_TYPEHASH") +} + +// UnpackLIFIREGISTRATIONTYPEHASH is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0xd0c83dad. +// +// Solidity: function LIFI_REGISTRATION_TYPEHASH() view returns(bytes32) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackLIFIREGISTRATIONTYPEHASH(data []byte) ([32]byte, error) { + out, err := liquidLaneLifiExecutor.abi.Unpack("LIFI_REGISTRATION_TYPEHASH", data) + if err != nil { + return *new([32]byte), err + } + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + return out0, nil +} + +// PackOUTPUTSETTLER is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xc6d9d466. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function OUTPUT_SETTLER() view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackOUTPUTSETTLER() []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("OUTPUT_SETTLER") + if err != nil { + panic(err) + } + return enc +} + +// TryPackOUTPUTSETTLER is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xc6d9d466. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function OUTPUT_SETTLER() view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackOUTPUTSETTLER() ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("OUTPUT_SETTLER") +} + +// UnpackOUTPUTSETTLER is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0xc6d9d466. +// +// Solidity: function OUTPUT_SETTLER() view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackOUTPUTSETTLER(data []byte) (common.Address, error) { + out, err := liquidLaneLifiExecutor.abi.Unpack("OUTPUT_SETTLER", data) + if err != nil { + return *new(common.Address), err + } + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + return out0, nil +} + +// PackCallers is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xaa03fa3d. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function callers(uint256 ) view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackCallers(arg0 *big.Int) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("callers", arg0) + if err != nil { + panic(err) + } + return enc +} + +// TryPackCallers is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xaa03fa3d. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function callers(uint256 ) view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackCallers(arg0 *big.Int) ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("callers", arg0) +} + +// UnpackCallers is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0xaa03fa3d. +// +// Solidity: function callers(uint256 ) view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackCallers(data []byte) (common.Address, error) { + out, err := liquidLaneLifiExecutor.abi.Unpack("callers", data) + if err != nil { + return *new(common.Address), err + } + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + return out0, nil +} + +// PackEip712Domain is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x84b0196e. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackEip712Domain() []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("eip712Domain") + if err != nil { + panic(err) + } + return enc +} + +// TryPackEip712Domain is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x84b0196e. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackEip712Domain() ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("eip712Domain") +} + +// Eip712DomainOutput serves as a container for the return parameters of contract +// method Eip712Domain. +type Eip712DomainOutput struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +} + +// UnpackEip712Domain is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackEip712Domain(data []byte) (Eip712DomainOutput, error) { + out, err := liquidLaneLifiExecutor.abi.Unpack("eip712Domain", data) + outstruct := new(Eip712DomainOutput) + if err != nil { + return *outstruct, err + } + outstruct.Fields = *abi.ConvertType(out[0], new([1]byte)).(*[1]byte) + outstruct.Name = *abi.ConvertType(out[1], new(string)).(*string) + outstruct.Version = *abi.ConvertType(out[2], new(string)).(*string) + outstruct.ChainId = abi.ConvertType(out[3], new(big.Int)).(*big.Int) + outstruct.VerifyingContract = *abi.ConvertType(out[4], new(common.Address)).(*common.Address) + outstruct.Salt = *abi.ConvertType(out[5], new([32]byte)).(*[32]byte) + outstruct.Extensions = *abi.ConvertType(out[6], new([]*big.Int)).(*[]*big.Int) + return *outstruct, nil +} + +// PackFinaliseWithCurrentTimestamp is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xcdfb25e0. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function finaliseWithCurrentTimestamp((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, (address,uint256,uint256,(bytes32,((address,uint256,address,address,uint256,uint48),bytes,uint48),bytes))[] routes) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackFinaliseWithCurrentTimestamp(order IInputSettlerStandardOrder, routes []ILiquidLaneLifiExecutorFillRoute) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("finaliseWithCurrentTimestamp", order, routes) + if err != nil { + panic(err) + } + return enc +} + +// TryPackFinaliseWithCurrentTimestamp is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xcdfb25e0. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function finaliseWithCurrentTimestamp((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, (address,uint256,uint256,(bytes32,((address,uint256,address,address,uint256,uint48),bytes,uint48),bytes))[] routes) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackFinaliseWithCurrentTimestamp(order IInputSettlerStandardOrder, routes []ILiquidLaneLifiExecutorFillRoute) ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("finaliseWithCurrentTimestamp", order, routes) +} + +// PackInitialize is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x946d9204. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function initialize(address owner_, address[] initCallers) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackInitialize(owner common.Address, initCallers []common.Address) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("initialize", owner, initCallers) + if err != nil { + panic(err) + } + return enc +} + +// TryPackInitialize is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x946d9204. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function initialize(address owner_, address[] initCallers) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackInitialize(owner common.Address, initCallers []common.Address) ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("initialize", owner, initCallers) +} + +// PackIsCaller is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x7ac07dcc. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function isCaller(address caller) view returns(bool) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackIsCaller(caller common.Address) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("isCaller", caller) + if err != nil { + panic(err) + } + return enc +} + +// TryPackIsCaller is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x7ac07dcc. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function isCaller(address caller) view returns(bool) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackIsCaller(caller common.Address) ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("isCaller", caller) +} + +// UnpackIsCaller is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x7ac07dcc. +// +// Solidity: function isCaller(address caller) view returns(bool) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackIsCaller(data []byte) (bool, error) { + out, err := liquidLaneLifiExecutor.abi.Unpack("isCaller", data) + if err != nil { + return *new(bool), err + } + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + return out0, nil +} + +// PackIsValidSignature is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x1626ba7e. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function isValidSignature(bytes32 hash, bytes signature) view returns(bytes4) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackIsValidSignature(hash [32]byte, signature []byte) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("isValidSignature", hash, signature) + if err != nil { + panic(err) + } + return enc +} + +// TryPackIsValidSignature is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x1626ba7e. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function isValidSignature(bytes32 hash, bytes signature) view returns(bytes4) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackIsValidSignature(hash [32]byte, signature []byte) ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("isValidSignature", hash, signature) +} + +// UnpackIsValidSignature is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x1626ba7e. +// +// Solidity: function isValidSignature(bytes32 hash, bytes signature) view returns(bytes4) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackIsValidSignature(data []byte) ([4]byte, error) { + out, err := liquidLaneLifiExecutor.abi.Unpack("isValidSignature", data) + if err != nil { + return *new([4]byte), err + } + out0 := *abi.ConvertType(out[0], new([4]byte)).(*[4]byte) + return out0, nil +} + +// PackLifiRegistrationDigest is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x1ce5298e. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function lifiRegistrationDigest(bytes32 messageHash) view returns(bytes32) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackLifiRegistrationDigest(messageHash [32]byte) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("lifiRegistrationDigest", messageHash) + if err != nil { + panic(err) + } + return enc +} + +// TryPackLifiRegistrationDigest is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x1ce5298e. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function lifiRegistrationDigest(bytes32 messageHash) view returns(bytes32) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackLifiRegistrationDigest(messageHash [32]byte) ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("lifiRegistrationDigest", messageHash) +} + +// UnpackLifiRegistrationDigest is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x1ce5298e. +// +// Solidity: function lifiRegistrationDigest(bytes32 messageHash) view returns(bytes32) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackLifiRegistrationDigest(data []byte) ([32]byte, error) { + out, err := liquidLaneLifiExecutor.abi.Unpack("lifiRegistrationDigest", data) + if err != nil { + return *new([32]byte), err + } + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + return out0, nil +} + +// PackOrderFinalised is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x73e57c27. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function orderFinalised(uint256[2][] inputs, bytes executionData) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackOrderFinalised(inputs [][2]*big.Int, executionData []byte) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("orderFinalised", inputs, executionData) + if err != nil { + panic(err) + } + return enc +} + +// TryPackOrderFinalised is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x73e57c27. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function orderFinalised(uint256[2][] inputs, bytes executionData) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackOrderFinalised(inputs [][2]*big.Int, executionData []byte) ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("orderFinalised", inputs, executionData) +} + +// PackOwner is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x8da5cb5b. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function owner() view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackOwner() []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("owner") + if err != nil { + panic(err) + } + return enc +} + +// TryPackOwner is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x8da5cb5b. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function owner() view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackOwner() ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("owner") +} + +// UnpackOwner is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackOwner(data []byte) (common.Address, error) { + out, err := liquidLaneLifiExecutor.abi.Unpack("owner", data) + if err != nil { + return *new(common.Address), err + } + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + return out0, nil +} + +// PackRenounceOwnership is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x715018a6. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function renounceOwnership() returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackRenounceOwnership() []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("renounceOwnership") + if err != nil { + panic(err) + } + return enc +} + +// TryPackRenounceOwnership is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x715018a6. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function renounceOwnership() returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackRenounceOwnership() ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("renounceOwnership") +} + +// PackSetCallers is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x43ded848. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function setCallers(address[] newCallers) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackSetCallers(newCallers []common.Address) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("setCallers", newCallers) + if err != nil { + panic(err) + } + return enc +} + +// TryPackSetCallers is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x43ded848. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function setCallers(address[] newCallers) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackSetCallers(newCallers []common.Address) ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("setCallers", newCallers) +} + +// PackTransferOwnership is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xf2fde38b. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) PackTransferOwnership(newOwner common.Address) []byte { + enc, err := liquidLaneLifiExecutor.abi.Pack("transferOwnership", newOwner) + if err != nil { + panic(err) + } + return enc +} + +// TryPackTransferOwnership is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xf2fde38b. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) TryPackTransferOwnership(newOwner common.Address) ([]byte, error) { + return liquidLaneLifiExecutor.abi.Pack("transferOwnership", newOwner) +} + +// LiquidLaneLifiExecutorEIP712DomainChanged represents a EIP712DomainChanged event raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorEIP712DomainChanged struct { + Raw *types.Log // Blockchain specific contextual infos +} + +const LiquidLaneLifiExecutorEIP712DomainChangedEventName = "EIP712DomainChanged" + +// ContractEventName returns the user-defined event name. +func (LiquidLaneLifiExecutorEIP712DomainChanged) ContractEventName() string { + return LiquidLaneLifiExecutorEIP712DomainChangedEventName +} + +// UnpackEIP712DomainChangedEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event EIP712DomainChanged() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackEIP712DomainChangedEvent(log *types.Log) (*LiquidLaneLifiExecutorEIP712DomainChanged, error) { + event := "EIP712DomainChanged" + if log.Topics[0] != liquidLaneLifiExecutor.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(LiquidLaneLifiExecutorEIP712DomainChanged) + if len(log.Data) > 0 { + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range liquidLaneLifiExecutor.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// LiquidLaneLifiExecutorInitialized represents a Initialized event raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorInitialized struct { + Version uint64 + Raw *types.Log // Blockchain specific contextual infos +} + +const LiquidLaneLifiExecutorInitializedEventName = "Initialized" + +// ContractEventName returns the user-defined event name. +func (LiquidLaneLifiExecutorInitialized) ContractEventName() string { + return LiquidLaneLifiExecutorInitializedEventName +} + +// UnpackInitializedEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event Initialized(uint64 version) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackInitializedEvent(log *types.Log) (*LiquidLaneLifiExecutorInitialized, error) { + event := "Initialized" + if log.Topics[0] != liquidLaneLifiExecutor.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(LiquidLaneLifiExecutorInitialized) + if len(log.Data) > 0 { + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range liquidLaneLifiExecutor.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// LiquidLaneLifiExecutorOwnershipTransferred represents a OwnershipTransferred event raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorOwnershipTransferred struct { + PreviousOwner common.Address + NewOwner common.Address + Raw *types.Log // Blockchain specific contextual infos +} + +const LiquidLaneLifiExecutorOwnershipTransferredEventName = "OwnershipTransferred" + +// ContractEventName returns the user-defined event name. +func (LiquidLaneLifiExecutorOwnershipTransferred) ContractEventName() string { + return LiquidLaneLifiExecutorOwnershipTransferredEventName +} + +// UnpackOwnershipTransferredEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackOwnershipTransferredEvent(log *types.Log) (*LiquidLaneLifiExecutorOwnershipTransferred, error) { + event := "OwnershipTransferred" + if log.Topics[0] != liquidLaneLifiExecutor.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(LiquidLaneLifiExecutorOwnershipTransferred) + if len(log.Data) > 0 { + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range liquidLaneLifiExecutor.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// LiquidLaneLifiExecutorSetCallers represents a SetCallers event raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorSetCallers struct { + NewCallers []common.Address + Raw *types.Log // Blockchain specific contextual infos +} + +const LiquidLaneLifiExecutorSetCallersEventName = "SetCallers" + +// ContractEventName returns the user-defined event name. +func (LiquidLaneLifiExecutorSetCallers) ContractEventName() string { + return LiquidLaneLifiExecutorSetCallersEventName +} + +// UnpackSetCallersEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event SetCallers(address[] newCallers) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackSetCallersEvent(log *types.Log) (*LiquidLaneLifiExecutorSetCallers, error) { + event := "SetCallers" + if log.Topics[0] != liquidLaneLifiExecutor.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(LiquidLaneLifiExecutorSetCallers) + if len(log.Data) > 0 { + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range liquidLaneLifiExecutor.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// UnpackError attempts to decode the provided error data using user-defined +// error definitions. +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackError(raw []byte) (any, error) { + if bytes.Equal(raw[:4], liquidLaneLifiExecutor.abi.Errors["InvalidInitialization"].ID.Bytes()[:4]) { + return liquidLaneLifiExecutor.UnpackInvalidInitializationError(raw[4:]) + } + if bytes.Equal(raw[:4], liquidLaneLifiExecutor.abi.Errors["NotCaller"].ID.Bytes()[:4]) { + return liquidLaneLifiExecutor.UnpackNotCallerError(raw[4:]) + } + if bytes.Equal(raw[:4], liquidLaneLifiExecutor.abi.Errors["NotInitializing"].ID.Bytes()[:4]) { + return liquidLaneLifiExecutor.UnpackNotInitializingError(raw[4:]) + } + if bytes.Equal(raw[:4], liquidLaneLifiExecutor.abi.Errors["NotInputSettler"].ID.Bytes()[:4]) { + return liquidLaneLifiExecutor.UnpackNotInputSettlerError(raw[4:]) + } + if bytes.Equal(raw[:4], liquidLaneLifiExecutor.abi.Errors["OwnableInvalidOwner"].ID.Bytes()[:4]) { + return liquidLaneLifiExecutor.UnpackOwnableInvalidOwnerError(raw[4:]) + } + if bytes.Equal(raw[:4], liquidLaneLifiExecutor.abi.Errors["OwnableUnauthorizedAccount"].ID.Bytes()[:4]) { + return liquidLaneLifiExecutor.UnpackOwnableUnauthorizedAccountError(raw[4:]) + } + if bytes.Equal(raw[:4], liquidLaneLifiExecutor.abi.Errors["SafeERC20FailedOperation"].ID.Bytes()[:4]) { + return liquidLaneLifiExecutor.UnpackSafeERC20FailedOperationError(raw[4:]) + } + return nil, errors.New("Unknown error") +} + +// LiquidLaneLifiExecutorInvalidInitialization represents a InvalidInitialization error raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorInvalidInitialization struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error InvalidInitialization() +func LiquidLaneLifiExecutorInvalidInitializationErrorID() common.Hash { + return common.HexToHash("0xf92ee8a957075833165f68c320933b1a1294aafc84ee6e0dd3fb178008f9aaf5") +} + +// UnpackInvalidInitializationError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error InvalidInitialization() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackInvalidInitializationError(raw []byte) (*LiquidLaneLifiExecutorInvalidInitialization, error) { + out := new(LiquidLaneLifiExecutorInvalidInitialization) + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, "InvalidInitialization", raw); err != nil { + return nil, err + } + return out, nil +} + +// LiquidLaneLifiExecutorNotCaller represents a NotCaller error raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorNotCaller struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error NotCaller() +func LiquidLaneLifiExecutorNotCallerErrorID() common.Hash { + return common.HexToHash("0x16c618d80989492b64dbf0ed90935e3959f670b9b9d57385b45d00c0d1cdedf9") +} + +// UnpackNotCallerError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error NotCaller() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackNotCallerError(raw []byte) (*LiquidLaneLifiExecutorNotCaller, error) { + out := new(LiquidLaneLifiExecutorNotCaller) + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, "NotCaller", raw); err != nil { + return nil, err + } + return out, nil +} + +// LiquidLaneLifiExecutorNotInitializing represents a NotInitializing error raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorNotInitializing struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error NotInitializing() +func LiquidLaneLifiExecutorNotInitializingErrorID() common.Hash { + return common.HexToHash("0xd7e6bcf8597daa127dc9f0048d2f08d5ef140a2cb659feabd700beff1f7a8302") +} + +// UnpackNotInitializingError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error NotInitializing() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackNotInitializingError(raw []byte) (*LiquidLaneLifiExecutorNotInitializing, error) { + out := new(LiquidLaneLifiExecutorNotInitializing) + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, "NotInitializing", raw); err != nil { + return nil, err + } + return out, nil +} + +// LiquidLaneLifiExecutorNotInputSettler represents a NotInputSettler error raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorNotInputSettler struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error NotInputSettler() +func LiquidLaneLifiExecutorNotInputSettlerErrorID() common.Hash { + return common.HexToHash("0xde89f63ea338ef13c2e1dd13cfee098f9c2ac145dbd7f1e315fcaffdc099d30a") +} + +// UnpackNotInputSettlerError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error NotInputSettler() +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackNotInputSettlerError(raw []byte) (*LiquidLaneLifiExecutorNotInputSettler, error) { + out := new(LiquidLaneLifiExecutorNotInputSettler) + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, "NotInputSettler", raw); err != nil { + return nil, err + } + return out, nil +} + +// LiquidLaneLifiExecutorOwnableInvalidOwner represents a OwnableInvalidOwner error raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorOwnableInvalidOwner struct { + Owner common.Address +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error OwnableInvalidOwner(address owner) +func LiquidLaneLifiExecutorOwnableInvalidOwnerErrorID() common.Hash { + return common.HexToHash("0x1e4fbdf7f3ef8bcaa855599e3abf48b232380f183f08f6f813d9ffa5bd585188") +} + +// UnpackOwnableInvalidOwnerError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error OwnableInvalidOwner(address owner) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackOwnableInvalidOwnerError(raw []byte) (*LiquidLaneLifiExecutorOwnableInvalidOwner, error) { + out := new(LiquidLaneLifiExecutorOwnableInvalidOwner) + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, "OwnableInvalidOwner", raw); err != nil { + return nil, err + } + return out, nil +} + +// LiquidLaneLifiExecutorOwnableUnauthorizedAccount represents a OwnableUnauthorizedAccount error raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorOwnableUnauthorizedAccount struct { + Account common.Address +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error OwnableUnauthorizedAccount(address account) +func LiquidLaneLifiExecutorOwnableUnauthorizedAccountErrorID() common.Hash { + return common.HexToHash("0x118cdaa7a341953d1887a2245fd6665d741c67c8c50581daa59e1d03373fa188") +} + +// UnpackOwnableUnauthorizedAccountError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error OwnableUnauthorizedAccount(address account) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackOwnableUnauthorizedAccountError(raw []byte) (*LiquidLaneLifiExecutorOwnableUnauthorizedAccount, error) { + out := new(LiquidLaneLifiExecutorOwnableUnauthorizedAccount) + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, "OwnableUnauthorizedAccount", raw); err != nil { + return nil, err + } + return out, nil +} + +// LiquidLaneLifiExecutorSafeERC20FailedOperation represents a SafeERC20FailedOperation error raised by the LiquidLaneLifiExecutor contract. +type LiquidLaneLifiExecutorSafeERC20FailedOperation struct { + Token common.Address +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error SafeERC20FailedOperation(address token) +func LiquidLaneLifiExecutorSafeERC20FailedOperationErrorID() common.Hash { + return common.HexToHash("0x5274afe73c98b4749fc91ffae6b7b574e7842cb2144a159e9377a5f20b32edf9") +} + +// UnpackSafeERC20FailedOperationError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error SafeERC20FailedOperation(address token) +func (liquidLaneLifiExecutor *LiquidLaneLifiExecutor) UnpackSafeERC20FailedOperationError(raw []byte) (*LiquidLaneLifiExecutorSafeERC20FailedOperation, error) { + out := new(LiquidLaneLifiExecutorSafeERC20FailedOperation) + if err := liquidLaneLifiExecutor.abi.UnpackIntoInterface(out, "SafeERC20FailedOperation", raw); err != nil { + return nil, err + } + return out, nil +} diff --git a/api/bindings/lifi/inputsettler/ILifiInputSettler.go b/api/bindings/lifi/inputsettler/ILifiInputSettler.go new file mode 100644 index 00000000..013fb0a5 --- /dev/null +++ b/api/bindings/lifi/inputsettler/ILifiInputSettler.go @@ -0,0 +1,2043 @@ +// Code generated via abigen V2 - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package inputsettler + +import ( + "bytes" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind/v2" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = bytes.Equal + _ = errors.New + _ = big.NewInt + _ = common.Big1 + _ = types.BloomLookup + _ = abi.ConvertType +) + +// InputSettlerBaseSolveParams is an auto generated low-level Go binding around an user-defined struct. +type InputSettlerBaseSolveParams struct { + Timestamp uint32 + Solver [32]byte +} + +// MandateOutput is an auto generated low-level Go binding around an user-defined struct. +type MandateOutput struct { + Oracle [32]byte + Settler [32]byte + ChainId *big.Int + Token [32]byte + Amount *big.Int + Recipient [32]byte + CallbackData []byte + Context []byte +} + +// OrderPurchase is an auto generated low-level Go binding around an user-defined struct. +type OrderPurchase struct { + OrderId [32]byte + Destination common.Address + CallData []byte + Discount uint64 + TimeToBuy uint32 +} + +// StandardOrder is an auto generated low-level Go binding around an user-defined struct. +type StandardOrder struct { + User common.Address + Nonce *big.Int + OriginChainId *big.Int + Expires uint32 + FillDeadline uint32 + InputOracle common.Address + Inputs [][2]*big.Int + Outputs []MandateOutput +} + +// ILifiInputSettlerMetaData contains all meta data concerning the ILifiInputSettler contract. +var ILifiInputSettlerMetaData = bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"initialOwner\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"AlreadyInitialized\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"AlreadyPurchased\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"CallOutOfRange\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"CodeSize0\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ContextOutOfRange\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"Expired\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"}],\"name\":\"FillDeadlineAfterExpiry\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"expected\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"actual\",\"type\":\"uint32\"}],\"name\":\"FilledTooLate\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"GovernanceFeeChangeNotReady\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"GovernanceFeeTooHigh\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"HasDirtyBits\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidOrderStatus\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidPurchaser\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidShortString\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidSigner\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidTimestampLength\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NewOwnerIsZeroAddress\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NoDestination\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NoHandoverRequest\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NotOrderOwner\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"provided\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"computed\",\"type\":\"bytes32\"}],\"name\":\"OrderIdMismatch\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ReentrancyDetected\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"SafeERC20FailedOperation\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SignatureAndInputsNotEqual\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes1\",\"name\":\"\",\"type\":\"bytes1\"}],\"name\":\"SignatureNotSupported\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"str\",\"type\":\"string\"}],\"name\":\"StringTooLong\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TimestampNotPassed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"TimestampPassed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"Unauthorized\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"expected\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"actual\",\"type\":\"uint256\"}],\"name\":\"WrongChain\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"EIP712DomainChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"orderId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"solver\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"destination\",\"type\":\"bytes32\"}],\"name\":\"Finalised\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"oldGovernanceFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"newGovernanceFee\",\"type\":\"uint64\"}],\"name\":\"GovernanceFeeChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"nextGovernanceFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"nextGovernanceFeeTime\",\"type\":\"uint64\"}],\"name\":\"NextGovernanceFee\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"orderId\",\"type\":\"bytes32\"}],\"name\":\"Open\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"orderId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"originChainId\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"inputOracle\",\"type\":\"address\"},{\"internalType\":\"uint256[2][]\",\"name\":\"inputs\",\"type\":\"uint256[2][]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"oracle\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"settler\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"token\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"recipient\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"callbackData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"internalType\":\"structMandateOutput[]\",\"name\":\"outputs\",\"type\":\"tuple[]\"}],\"indexed\":false,\"internalType\":\"structStandardOrder\",\"name\":\"order\",\"type\":\"tuple\"}],\"name\":\"Open\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"orderId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"solver\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"purchaser\",\"type\":\"bytes32\"}],\"name\":\"OrderPurchased\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"pendingOwner\",\"type\":\"address\"}],\"name\":\"OwnershipHandoverCanceled\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"pendingOwner\",\"type\":\"address\"}],\"name\":\"OwnershipHandoverRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"orderId\",\"type\":\"bytes32\"}],\"name\":\"Refunded\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"DOMAIN_SEPARATOR\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"applyGovernanceFee\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"cancelOwnershipHandover\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"pendingOwner\",\"type\":\"address\"}],\"name\":\"completeOwnershipHandover\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"eip712Domain\",\"outputs\":[{\"internalType\":\"bytes1\",\"name\":\"fields\",\"type\":\"bytes1\"},{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"version\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"verifyingContract\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"},{\"internalType\":\"uint256[]\",\"name\":\"extensions\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"originChainId\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"inputOracle\",\"type\":\"address\"},{\"internalType\":\"uint256[2][]\",\"name\":\"inputs\",\"type\":\"uint256[2][]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"oracle\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"settler\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"token\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"recipient\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"callbackData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"internalType\":\"structMandateOutput[]\",\"name\":\"outputs\",\"type\":\"tuple[]\"}],\"internalType\":\"structStandardOrder\",\"name\":\"order\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"timestamp\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"solver\",\"type\":\"bytes32\"}],\"internalType\":\"structInputSettlerBase.SolveParams[]\",\"name\":\"solveParams\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes32\",\"name\":\"destination\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"call\",\"type\":\"bytes\"}],\"name\":\"finalise\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"originChainId\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"inputOracle\",\"type\":\"address\"},{\"internalType\":\"uint256[2][]\",\"name\":\"inputs\",\"type\":\"uint256[2][]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"oracle\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"settler\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"token\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"recipient\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"callbackData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"internalType\":\"structMandateOutput[]\",\"name\":\"outputs\",\"type\":\"tuple[]\"}],\"internalType\":\"structStandardOrder\",\"name\":\"order\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"timestamp\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"solver\",\"type\":\"bytes32\"}],\"internalType\":\"structInputSettlerBase.SolveParams[]\",\"name\":\"solveParams\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes32\",\"name\":\"destination\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"call\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"orderOwnerSignature\",\"type\":\"bytes\"}],\"name\":\"finaliseWithSignature\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"governanceFee\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"nextGovernanceFee\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"nextGovernanceFeeTime\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"originChainId\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"inputOracle\",\"type\":\"address\"},{\"internalType\":\"uint256[2][]\",\"name\":\"inputs\",\"type\":\"uint256[2][]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"oracle\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"settler\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"token\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"recipient\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"callbackData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"internalType\":\"structMandateOutput[]\",\"name\":\"outputs\",\"type\":\"tuple[]\"}],\"internalType\":\"structStandardOrder\",\"name\":\"order\",\"type\":\"tuple\"}],\"name\":\"open\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"originChainId\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"inputOracle\",\"type\":\"address\"},{\"internalType\":\"uint256[2][]\",\"name\":\"inputs\",\"type\":\"uint256[2][]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"oracle\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"settler\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"token\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"recipient\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"callbackData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"internalType\":\"structMandateOutput[]\",\"name\":\"outputs\",\"type\":\"tuple[]\"}],\"internalType\":\"structStandardOrder\",\"name\":\"order\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"sponsor\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"}],\"name\":\"openFor\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"originChainId\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"inputOracle\",\"type\":\"address\"},{\"internalType\":\"uint256[2][]\",\"name\":\"inputs\",\"type\":\"uint256[2][]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"oracle\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"settler\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"token\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"recipient\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"callbackData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"internalType\":\"structMandateOutput[]\",\"name\":\"outputs\",\"type\":\"tuple[]\"}],\"internalType\":\"structStandardOrder\",\"name\":\"order\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"sponsor\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"signature\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"call\",\"type\":\"bytes\"}],\"name\":\"openForAndFinalise\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"originChainId\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"inputOracle\",\"type\":\"address\"},{\"internalType\":\"uint256[2][]\",\"name\":\"inputs\",\"type\":\"uint256[2][]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"oracle\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"settler\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"token\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"recipient\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"callbackData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"internalType\":\"structMandateOutput[]\",\"name\":\"outputs\",\"type\":\"tuple[]\"}],\"internalType\":\"structStandardOrder\",\"name\":\"order\",\"type\":\"tuple\"}],\"name\":\"orderIdentifier\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"orderId\",\"type\":\"bytes32\"}],\"name\":\"orderStatus\",\"outputs\":[{\"internalType\":\"enumInputSettlerEscrow.OrderStatus\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"result\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"pendingOwner\",\"type\":\"address\"}],\"name\":\"ownershipHandoverExpiresAt\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"result\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"orderId\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"},{\"internalType\":\"uint64\",\"name\":\"discount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"timeToBuy\",\"type\":\"uint32\"}],\"internalType\":\"structOrderPurchase\",\"name\":\"orderPurchase\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"originChainId\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"inputOracle\",\"type\":\"address\"},{\"internalType\":\"uint256[2][]\",\"name\":\"inputs\",\"type\":\"uint256[2][]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"oracle\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"settler\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"token\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"recipient\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"callbackData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"internalType\":\"structMandateOutput[]\",\"name\":\"outputs\",\"type\":\"tuple[]\"}],\"internalType\":\"structStandardOrder\",\"name\":\"order\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"orderSolvedByIdentifier\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"purchaser\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"expiryTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"solverSignature\",\"type\":\"bytes\"}],\"name\":\"purchaseOrder\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"solver\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"orderId\",\"type\":\"bytes32\"}],\"name\":\"purchasedOrders\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"lastOrderTimestamp\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"purchaser\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"user\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"originChainId\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"expires\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"fillDeadline\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"inputOracle\",\"type\":\"address\"},{\"internalType\":\"uint256[2][]\",\"name\":\"inputs\",\"type\":\"uint256[2][]\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"oracle\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"settler\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"token\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"recipient\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"callbackData\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"context\",\"type\":\"bytes\"}],\"internalType\":\"structMandateOutput[]\",\"name\":\"outputs\",\"type\":\"tuple[]\"}],\"internalType\":\"structStandardOrder\",\"name\":\"order\",\"type\":\"tuple\"}],\"name\":\"refund\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"requestOwnershipHandover\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"_nextGovernanceFee\",\"type\":\"uint64\"}],\"name\":\"setGovernanceFee\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}]", + ID: "ILifiInputSettler", +} + +// ILifiInputSettler is an auto generated Go binding around an Ethereum contract. +type ILifiInputSettler struct { + abi abi.ABI +} + +// NewILifiInputSettler creates a new instance of ILifiInputSettler. +func NewILifiInputSettler() *ILifiInputSettler { + parsed, err := ILifiInputSettlerMetaData.ParseABI() + if err != nil { + panic(errors.New("invalid ABI: " + err.Error())) + } + return &ILifiInputSettler{abi: *parsed} +} + +// Instance creates a wrapper for a deployed contract instance at the given address. +// Use this to create the instance object passed to abigen v2 library functions Call, Transact, etc. +func (c *ILifiInputSettler) Instance(backend bind.ContractBackend, addr common.Address) *bind.BoundContract { + return bind.NewBoundContract(addr, c.abi, backend, backend, backend) +} + +// PackConstructor is the Go binding used to pack the parameters required for +// contract deployment. +// +// Solidity: constructor(address initialOwner) returns() +func (iLifiInputSettler *ILifiInputSettler) PackConstructor(initialOwner common.Address) []byte { + enc, err := iLifiInputSettler.abi.Pack("", initialOwner) + if err != nil { + panic(err) + } + return enc +} + +// PackDOMAINSEPARATOR is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x3644e515. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function DOMAIN_SEPARATOR() view returns(bytes32) +func (iLifiInputSettler *ILifiInputSettler) PackDOMAINSEPARATOR() []byte { + enc, err := iLifiInputSettler.abi.Pack("DOMAIN_SEPARATOR") + if err != nil { + panic(err) + } + return enc +} + +// TryPackDOMAINSEPARATOR is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x3644e515. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function DOMAIN_SEPARATOR() view returns(bytes32) +func (iLifiInputSettler *ILifiInputSettler) TryPackDOMAINSEPARATOR() ([]byte, error) { + return iLifiInputSettler.abi.Pack("DOMAIN_SEPARATOR") +} + +// UnpackDOMAINSEPARATOR is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x3644e515. +// +// Solidity: function DOMAIN_SEPARATOR() view returns(bytes32) +func (iLifiInputSettler *ILifiInputSettler) UnpackDOMAINSEPARATOR(data []byte) ([32]byte, error) { + out, err := iLifiInputSettler.abi.Unpack("DOMAIN_SEPARATOR", data) + if err != nil { + return *new([32]byte), err + } + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + return out0, nil +} + +// PackApplyGovernanceFee is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x8198db87. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function applyGovernanceFee() returns() +func (iLifiInputSettler *ILifiInputSettler) PackApplyGovernanceFee() []byte { + enc, err := iLifiInputSettler.abi.Pack("applyGovernanceFee") + if err != nil { + panic(err) + } + return enc +} + +// TryPackApplyGovernanceFee is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x8198db87. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function applyGovernanceFee() returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackApplyGovernanceFee() ([]byte, error) { + return iLifiInputSettler.abi.Pack("applyGovernanceFee") +} + +// PackCancelOwnershipHandover is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x54d1f13d. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function cancelOwnershipHandover() payable returns() +func (iLifiInputSettler *ILifiInputSettler) PackCancelOwnershipHandover() []byte { + enc, err := iLifiInputSettler.abi.Pack("cancelOwnershipHandover") + if err != nil { + panic(err) + } + return enc +} + +// TryPackCancelOwnershipHandover is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x54d1f13d. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function cancelOwnershipHandover() payable returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackCancelOwnershipHandover() ([]byte, error) { + return iLifiInputSettler.abi.Pack("cancelOwnershipHandover") +} + +// PackCompleteOwnershipHandover is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xf04e283e. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function completeOwnershipHandover(address pendingOwner) payable returns() +func (iLifiInputSettler *ILifiInputSettler) PackCompleteOwnershipHandover(pendingOwner common.Address) []byte { + enc, err := iLifiInputSettler.abi.Pack("completeOwnershipHandover", pendingOwner) + if err != nil { + panic(err) + } + return enc +} + +// TryPackCompleteOwnershipHandover is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xf04e283e. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function completeOwnershipHandover(address pendingOwner) payable returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackCompleteOwnershipHandover(pendingOwner common.Address) ([]byte, error) { + return iLifiInputSettler.abi.Pack("completeOwnershipHandover", pendingOwner) +} + +// PackEip712Domain is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x84b0196e. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (iLifiInputSettler *ILifiInputSettler) PackEip712Domain() []byte { + enc, err := iLifiInputSettler.abi.Pack("eip712Domain") + if err != nil { + panic(err) + } + return enc +} + +// TryPackEip712Domain is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x84b0196e. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (iLifiInputSettler *ILifiInputSettler) TryPackEip712Domain() ([]byte, error) { + return iLifiInputSettler.abi.Pack("eip712Domain") +} + +// Eip712DomainOutput serves as a container for the return parameters of contract +// method Eip712Domain. +type Eip712DomainOutput struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +} + +// UnpackEip712Domain is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (iLifiInputSettler *ILifiInputSettler) UnpackEip712Domain(data []byte) (Eip712DomainOutput, error) { + out, err := iLifiInputSettler.abi.Unpack("eip712Domain", data) + outstruct := new(Eip712DomainOutput) + if err != nil { + return *outstruct, err + } + outstruct.Fields = *abi.ConvertType(out[0], new([1]byte)).(*[1]byte) + outstruct.Name = *abi.ConvertType(out[1], new(string)).(*string) + outstruct.Version = *abi.ConvertType(out[2], new(string)).(*string) + outstruct.ChainId = abi.ConvertType(out[3], new(big.Int)).(*big.Int) + outstruct.VerifyingContract = *abi.ConvertType(out[4], new(common.Address)).(*common.Address) + outstruct.Salt = *abi.ConvertType(out[5], new([32]byte)).(*[32]byte) + outstruct.Extensions = *abi.ConvertType(out[6], new([]*big.Int)).(*[]*big.Int) + return *outstruct, nil +} + +// PackFinalise is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xbab36441. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function finalise((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, (uint32,bytes32)[] solveParams, bytes32 destination, bytes call) returns() +func (iLifiInputSettler *ILifiInputSettler) PackFinalise(order StandardOrder, solveParams []InputSettlerBaseSolveParams, destination [32]byte, call []byte) []byte { + enc, err := iLifiInputSettler.abi.Pack("finalise", order, solveParams, destination, call) + if err != nil { + panic(err) + } + return enc +} + +// TryPackFinalise is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xbab36441. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function finalise((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, (uint32,bytes32)[] solveParams, bytes32 destination, bytes call) returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackFinalise(order StandardOrder, solveParams []InputSettlerBaseSolveParams, destination [32]byte, call []byte) ([]byte, error) { + return iLifiInputSettler.abi.Pack("finalise", order, solveParams, destination, call) +} + +// PackFinaliseWithSignature is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x73ce1aaa. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function finaliseWithSignature((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, (uint32,bytes32)[] solveParams, bytes32 destination, bytes call, bytes orderOwnerSignature) returns() +func (iLifiInputSettler *ILifiInputSettler) PackFinaliseWithSignature(order StandardOrder, solveParams []InputSettlerBaseSolveParams, destination [32]byte, call []byte, orderOwnerSignature []byte) []byte { + enc, err := iLifiInputSettler.abi.Pack("finaliseWithSignature", order, solveParams, destination, call, orderOwnerSignature) + if err != nil { + panic(err) + } + return enc +} + +// TryPackFinaliseWithSignature is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x73ce1aaa. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function finaliseWithSignature((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, (uint32,bytes32)[] solveParams, bytes32 destination, bytes call, bytes orderOwnerSignature) returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackFinaliseWithSignature(order StandardOrder, solveParams []InputSettlerBaseSolveParams, destination [32]byte, call []byte, orderOwnerSignature []byte) ([]byte, error) { + return iLifiInputSettler.abi.Pack("finaliseWithSignature", order, solveParams, destination, call, orderOwnerSignature) +} + +// PackGovernanceFee is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x0ea90a12. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function governanceFee() view returns(uint64) +func (iLifiInputSettler *ILifiInputSettler) PackGovernanceFee() []byte { + enc, err := iLifiInputSettler.abi.Pack("governanceFee") + if err != nil { + panic(err) + } + return enc +} + +// TryPackGovernanceFee is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x0ea90a12. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function governanceFee() view returns(uint64) +func (iLifiInputSettler *ILifiInputSettler) TryPackGovernanceFee() ([]byte, error) { + return iLifiInputSettler.abi.Pack("governanceFee") +} + +// UnpackGovernanceFee is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x0ea90a12. +// +// Solidity: function governanceFee() view returns(uint64) +func (iLifiInputSettler *ILifiInputSettler) UnpackGovernanceFee(data []byte) (uint64, error) { + out, err := iLifiInputSettler.abi.Unpack("governanceFee", data) + if err != nil { + return *new(uint64), err + } + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + return out0, nil +} + +// PackNextGovernanceFee is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xc0e31352. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function nextGovernanceFee() view returns(uint64) +func (iLifiInputSettler *ILifiInputSettler) PackNextGovernanceFee() []byte { + enc, err := iLifiInputSettler.abi.Pack("nextGovernanceFee") + if err != nil { + panic(err) + } + return enc +} + +// TryPackNextGovernanceFee is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xc0e31352. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function nextGovernanceFee() view returns(uint64) +func (iLifiInputSettler *ILifiInputSettler) TryPackNextGovernanceFee() ([]byte, error) { + return iLifiInputSettler.abi.Pack("nextGovernanceFee") +} + +// UnpackNextGovernanceFee is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0xc0e31352. +// +// Solidity: function nextGovernanceFee() view returns(uint64) +func (iLifiInputSettler *ILifiInputSettler) UnpackNextGovernanceFee(data []byte) (uint64, error) { + out, err := iLifiInputSettler.abi.Unpack("nextGovernanceFee", data) + if err != nil { + return *new(uint64), err + } + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + return out0, nil +} + +// PackNextGovernanceFeeTime is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x5791edc0. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function nextGovernanceFeeTime() view returns(uint64) +func (iLifiInputSettler *ILifiInputSettler) PackNextGovernanceFeeTime() []byte { + enc, err := iLifiInputSettler.abi.Pack("nextGovernanceFeeTime") + if err != nil { + panic(err) + } + return enc +} + +// TryPackNextGovernanceFeeTime is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x5791edc0. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function nextGovernanceFeeTime() view returns(uint64) +func (iLifiInputSettler *ILifiInputSettler) TryPackNextGovernanceFeeTime() ([]byte, error) { + return iLifiInputSettler.abi.Pack("nextGovernanceFeeTime") +} + +// UnpackNextGovernanceFeeTime is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x5791edc0. +// +// Solidity: function nextGovernanceFeeTime() view returns(uint64) +func (iLifiInputSettler *ILifiInputSettler) UnpackNextGovernanceFeeTime(data []byte) (uint64, error) { + out, err := iLifiInputSettler.abi.Unpack("nextGovernanceFeeTime", data) + if err != nil { + return *new(uint64), err + } + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + return out0, nil +} + +// PackOpen is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x7515fd56. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function open((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order) returns() +func (iLifiInputSettler *ILifiInputSettler) PackOpen(order StandardOrder) []byte { + enc, err := iLifiInputSettler.abi.Pack("open", order) + if err != nil { + panic(err) + } + return enc +} + +// TryPackOpen is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x7515fd56. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function open((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order) returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackOpen(order StandardOrder) ([]byte, error) { + return iLifiInputSettler.abi.Pack("open", order) +} + +// PackOpenFor is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x49927074. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function openFor((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, address sponsor, bytes signature) returns() +func (iLifiInputSettler *ILifiInputSettler) PackOpenFor(order StandardOrder, sponsor common.Address, signature []byte) []byte { + enc, err := iLifiInputSettler.abi.Pack("openFor", order, sponsor, signature) + if err != nil { + panic(err) + } + return enc +} + +// TryPackOpenFor is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x49927074. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function openFor((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, address sponsor, bytes signature) returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackOpenFor(order StandardOrder, sponsor common.Address, signature []byte) ([]byte, error) { + return iLifiInputSettler.abi.Pack("openFor", order, sponsor, signature) +} + +// PackOpenForAndFinalise is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xafe55c7e. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function openForAndFinalise((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, address sponsor, bytes signature, address destination, bytes call) returns() +func (iLifiInputSettler *ILifiInputSettler) PackOpenForAndFinalise(order StandardOrder, sponsor common.Address, signature []byte, destination common.Address, call []byte) []byte { + enc, err := iLifiInputSettler.abi.Pack("openForAndFinalise", order, sponsor, signature, destination, call) + if err != nil { + panic(err) + } + return enc +} + +// TryPackOpenForAndFinalise is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xafe55c7e. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function openForAndFinalise((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, address sponsor, bytes signature, address destination, bytes call) returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackOpenForAndFinalise(order StandardOrder, sponsor common.Address, signature []byte, destination common.Address, call []byte) ([]byte, error) { + return iLifiInputSettler.abi.Pack("openForAndFinalise", order, sponsor, signature, destination, call) +} + +// PackOrderIdentifier is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x609dbfa0. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function orderIdentifier((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order) view returns(bytes32) +func (iLifiInputSettler *ILifiInputSettler) PackOrderIdentifier(order StandardOrder) []byte { + enc, err := iLifiInputSettler.abi.Pack("orderIdentifier", order) + if err != nil { + panic(err) + } + return enc +} + +// TryPackOrderIdentifier is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x609dbfa0. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function orderIdentifier((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order) view returns(bytes32) +func (iLifiInputSettler *ILifiInputSettler) TryPackOrderIdentifier(order StandardOrder) ([]byte, error) { + return iLifiInputSettler.abi.Pack("orderIdentifier", order) +} + +// UnpackOrderIdentifier is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x609dbfa0. +// +// Solidity: function orderIdentifier((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order) view returns(bytes32) +func (iLifiInputSettler *ILifiInputSettler) UnpackOrderIdentifier(data []byte) ([32]byte, error) { + out, err := iLifiInputSettler.abi.Unpack("orderIdentifier", data) + if err != nil { + return *new([32]byte), err + } + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + return out0, nil +} + +// PackOrderStatus is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x2dff692d. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function orderStatus(bytes32 orderId) view returns(uint8) +func (iLifiInputSettler *ILifiInputSettler) PackOrderStatus(orderId [32]byte) []byte { + enc, err := iLifiInputSettler.abi.Pack("orderStatus", orderId) + if err != nil { + panic(err) + } + return enc +} + +// TryPackOrderStatus is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x2dff692d. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function orderStatus(bytes32 orderId) view returns(uint8) +func (iLifiInputSettler *ILifiInputSettler) TryPackOrderStatus(orderId [32]byte) ([]byte, error) { + return iLifiInputSettler.abi.Pack("orderStatus", orderId) +} + +// UnpackOrderStatus is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x2dff692d. +// +// Solidity: function orderStatus(bytes32 orderId) view returns(uint8) +func (iLifiInputSettler *ILifiInputSettler) UnpackOrderStatus(data []byte) (uint8, error) { + out, err := iLifiInputSettler.abi.Unpack("orderStatus", data) + if err != nil { + return *new(uint8), err + } + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + return out0, nil +} + +// PackOwner is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x8da5cb5b. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function owner() view returns(address result) +func (iLifiInputSettler *ILifiInputSettler) PackOwner() []byte { + enc, err := iLifiInputSettler.abi.Pack("owner") + if err != nil { + panic(err) + } + return enc +} + +// TryPackOwner is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x8da5cb5b. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function owner() view returns(address result) +func (iLifiInputSettler *ILifiInputSettler) TryPackOwner() ([]byte, error) { + return iLifiInputSettler.abi.Pack("owner") +} + +// UnpackOwner is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x8da5cb5b. +// +// Solidity: function owner() view returns(address result) +func (iLifiInputSettler *ILifiInputSettler) UnpackOwner(data []byte) (common.Address, error) { + out, err := iLifiInputSettler.abi.Unpack("owner", data) + if err != nil { + return *new(common.Address), err + } + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + return out0, nil +} + +// PackOwnershipHandoverExpiresAt is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xfee81cf4. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function ownershipHandoverExpiresAt(address pendingOwner) view returns(uint256 result) +func (iLifiInputSettler *ILifiInputSettler) PackOwnershipHandoverExpiresAt(pendingOwner common.Address) []byte { + enc, err := iLifiInputSettler.abi.Pack("ownershipHandoverExpiresAt", pendingOwner) + if err != nil { + panic(err) + } + return enc +} + +// TryPackOwnershipHandoverExpiresAt is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xfee81cf4. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function ownershipHandoverExpiresAt(address pendingOwner) view returns(uint256 result) +func (iLifiInputSettler *ILifiInputSettler) TryPackOwnershipHandoverExpiresAt(pendingOwner common.Address) ([]byte, error) { + return iLifiInputSettler.abi.Pack("ownershipHandoverExpiresAt", pendingOwner) +} + +// UnpackOwnershipHandoverExpiresAt is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0xfee81cf4. +// +// Solidity: function ownershipHandoverExpiresAt(address pendingOwner) view returns(uint256 result) +func (iLifiInputSettler *ILifiInputSettler) UnpackOwnershipHandoverExpiresAt(data []byte) (*big.Int, error) { + out, err := iLifiInputSettler.abi.Unpack("ownershipHandoverExpiresAt", data) + if err != nil { + return new(big.Int), err + } + out0 := abi.ConvertType(out[0], new(big.Int)).(*big.Int) + return out0, nil +} + +// PackPurchaseOrder is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x72903ef8. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function purchaseOrder((bytes32,address,bytes,uint64,uint32) orderPurchase, (address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, bytes32 orderSolvedByIdentifier, bytes32 purchaser, uint256 expiryTimestamp, bytes solverSignature) returns() +func (iLifiInputSettler *ILifiInputSettler) PackPurchaseOrder(orderPurchase OrderPurchase, order StandardOrder, orderSolvedByIdentifier [32]byte, purchaser [32]byte, expiryTimestamp *big.Int, solverSignature []byte) []byte { + enc, err := iLifiInputSettler.abi.Pack("purchaseOrder", orderPurchase, order, orderSolvedByIdentifier, purchaser, expiryTimestamp, solverSignature) + if err != nil { + panic(err) + } + return enc +} + +// TryPackPurchaseOrder is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x72903ef8. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function purchaseOrder((bytes32,address,bytes,uint64,uint32) orderPurchase, (address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order, bytes32 orderSolvedByIdentifier, bytes32 purchaser, uint256 expiryTimestamp, bytes solverSignature) returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackPurchaseOrder(orderPurchase OrderPurchase, order StandardOrder, orderSolvedByIdentifier [32]byte, purchaser [32]byte, expiryTimestamp *big.Int, solverSignature []byte) ([]byte, error) { + return iLifiInputSettler.abi.Pack("purchaseOrder", orderPurchase, order, orderSolvedByIdentifier, purchaser, expiryTimestamp, solverSignature) +} + +// PackPurchasedOrders is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x9efa6120. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function purchasedOrders(bytes32 solver, bytes32 orderId) view returns(uint32 lastOrderTimestamp, bytes32 purchaser) +func (iLifiInputSettler *ILifiInputSettler) PackPurchasedOrders(solver [32]byte, orderId [32]byte) []byte { + enc, err := iLifiInputSettler.abi.Pack("purchasedOrders", solver, orderId) + if err != nil { + panic(err) + } + return enc +} + +// TryPackPurchasedOrders is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x9efa6120. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function purchasedOrders(bytes32 solver, bytes32 orderId) view returns(uint32 lastOrderTimestamp, bytes32 purchaser) +func (iLifiInputSettler *ILifiInputSettler) TryPackPurchasedOrders(solver [32]byte, orderId [32]byte) ([]byte, error) { + return iLifiInputSettler.abi.Pack("purchasedOrders", solver, orderId) +} + +// PurchasedOrdersOutput serves as a container for the return parameters of contract +// method PurchasedOrders. +type PurchasedOrdersOutput struct { + LastOrderTimestamp uint32 + Purchaser [32]byte +} + +// UnpackPurchasedOrders is the Go binding that unpacks the parameters returned +// from invoking the contract method with ID 0x9efa6120. +// +// Solidity: function purchasedOrders(bytes32 solver, bytes32 orderId) view returns(uint32 lastOrderTimestamp, bytes32 purchaser) +func (iLifiInputSettler *ILifiInputSettler) UnpackPurchasedOrders(data []byte) (PurchasedOrdersOutput, error) { + out, err := iLifiInputSettler.abi.Unpack("purchasedOrders", data) + outstruct := new(PurchasedOrdersOutput) + if err != nil { + return *outstruct, err + } + outstruct.LastOrderTimestamp = *abi.ConvertType(out[0], new(uint32)).(*uint32) + outstruct.Purchaser = *abi.ConvertType(out[1], new([32]byte)).(*[32]byte) + return *outstruct, nil +} + +// PackRefund is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x48f49eaf. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function refund((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order) returns() +func (iLifiInputSettler *ILifiInputSettler) PackRefund(order StandardOrder) []byte { + enc, err := iLifiInputSettler.abi.Pack("refund", order) + if err != nil { + panic(err) + } + return enc +} + +// TryPackRefund is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x48f49eaf. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function refund((address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order) returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackRefund(order StandardOrder) ([]byte, error) { + return iLifiInputSettler.abi.Pack("refund", order) +} + +// PackRenounceOwnership is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x715018a6. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function renounceOwnership() payable returns() +func (iLifiInputSettler *ILifiInputSettler) PackRenounceOwnership() []byte { + enc, err := iLifiInputSettler.abi.Pack("renounceOwnership") + if err != nil { + panic(err) + } + return enc +} + +// TryPackRenounceOwnership is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x715018a6. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function renounceOwnership() payable returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackRenounceOwnership() ([]byte, error) { + return iLifiInputSettler.abi.Pack("renounceOwnership") +} + +// PackRequestOwnershipHandover is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x25692962. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function requestOwnershipHandover() payable returns() +func (iLifiInputSettler *ILifiInputSettler) PackRequestOwnershipHandover() []byte { + enc, err := iLifiInputSettler.abi.Pack("requestOwnershipHandover") + if err != nil { + panic(err) + } + return enc +} + +// TryPackRequestOwnershipHandover is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x25692962. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function requestOwnershipHandover() payable returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackRequestOwnershipHandover() ([]byte, error) { + return iLifiInputSettler.abi.Pack("requestOwnershipHandover") +} + +// PackSetGovernanceFee is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x586f9800. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function setGovernanceFee(uint64 _nextGovernanceFee) returns() +func (iLifiInputSettler *ILifiInputSettler) PackSetGovernanceFee(nextGovernanceFee uint64) []byte { + enc, err := iLifiInputSettler.abi.Pack("setGovernanceFee", nextGovernanceFee) + if err != nil { + panic(err) + } + return enc +} + +// TryPackSetGovernanceFee is the Go binding used to pack the parameters required for calling +// the contract method with ID 0x586f9800. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function setGovernanceFee(uint64 _nextGovernanceFee) returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackSetGovernanceFee(nextGovernanceFee uint64) ([]byte, error) { + return iLifiInputSettler.abi.Pack("setGovernanceFee", nextGovernanceFee) +} + +// PackTransferOwnership is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xf2fde38b. This method will panic if any +// invalid/nil inputs are passed. +// +// Solidity: function transferOwnership(address newOwner) payable returns() +func (iLifiInputSettler *ILifiInputSettler) PackTransferOwnership(newOwner common.Address) []byte { + enc, err := iLifiInputSettler.abi.Pack("transferOwnership", newOwner) + if err != nil { + panic(err) + } + return enc +} + +// TryPackTransferOwnership is the Go binding used to pack the parameters required for calling +// the contract method with ID 0xf2fde38b. This method will return an error +// if any inputs are invalid/nil. +// +// Solidity: function transferOwnership(address newOwner) payable returns() +func (iLifiInputSettler *ILifiInputSettler) TryPackTransferOwnership(newOwner common.Address) ([]byte, error) { + return iLifiInputSettler.abi.Pack("transferOwnership", newOwner) +} + +// ILifiInputSettlerEIP712DomainChanged represents a EIP712DomainChanged event raised by the ILifiInputSettler contract. +type ILifiInputSettlerEIP712DomainChanged struct { + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerEIP712DomainChangedEventName = "EIP712DomainChanged" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerEIP712DomainChanged) ContractEventName() string { + return ILifiInputSettlerEIP712DomainChangedEventName +} + +// UnpackEIP712DomainChangedEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event EIP712DomainChanged() +func (iLifiInputSettler *ILifiInputSettler) UnpackEIP712DomainChangedEvent(log *types.Log) (*ILifiInputSettlerEIP712DomainChanged, error) { + event := "EIP712DomainChanged" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerEIP712DomainChanged) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerFinalised represents a Finalised event raised by the ILifiInputSettler contract. +type ILifiInputSettlerFinalised struct { + OrderId [32]byte + Solver [32]byte + Destination [32]byte + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerFinalisedEventName = "Finalised" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerFinalised) ContractEventName() string { + return ILifiInputSettlerFinalisedEventName +} + +// UnpackFinalisedEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event Finalised(bytes32 indexed orderId, bytes32 solver, bytes32 destination) +func (iLifiInputSettler *ILifiInputSettler) UnpackFinalisedEvent(log *types.Log) (*ILifiInputSettlerFinalised, error) { + event := "Finalised" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerFinalised) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerGovernanceFeeChanged represents a GovernanceFeeChanged event raised by the ILifiInputSettler contract. +type ILifiInputSettlerGovernanceFeeChanged struct { + OldGovernanceFee uint64 + NewGovernanceFee uint64 + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerGovernanceFeeChangedEventName = "GovernanceFeeChanged" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerGovernanceFeeChanged) ContractEventName() string { + return ILifiInputSettlerGovernanceFeeChangedEventName +} + +// UnpackGovernanceFeeChangedEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event GovernanceFeeChanged(uint64 oldGovernanceFee, uint64 newGovernanceFee) +func (iLifiInputSettler *ILifiInputSettler) UnpackGovernanceFeeChangedEvent(log *types.Log) (*ILifiInputSettlerGovernanceFeeChanged, error) { + event := "GovernanceFeeChanged" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerGovernanceFeeChanged) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerNextGovernanceFee represents a NextGovernanceFee event raised by the ILifiInputSettler contract. +type ILifiInputSettlerNextGovernanceFee struct { + NextGovernanceFee uint64 + NextGovernanceFeeTime uint64 + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerNextGovernanceFeeEventName = "NextGovernanceFee" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerNextGovernanceFee) ContractEventName() string { + return ILifiInputSettlerNextGovernanceFeeEventName +} + +// UnpackNextGovernanceFeeEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event NextGovernanceFee(uint64 nextGovernanceFee, uint64 nextGovernanceFeeTime) +func (iLifiInputSettler *ILifiInputSettler) UnpackNextGovernanceFeeEvent(log *types.Log) (*ILifiInputSettlerNextGovernanceFee, error) { + event := "NextGovernanceFee" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerNextGovernanceFee) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerOpen represents a Open event raised by the ILifiInputSettler contract. +type ILifiInputSettlerOpen struct { + OrderId [32]byte + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerOpenEventName = "Open" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerOpen) ContractEventName() string { + return ILifiInputSettlerOpenEventName +} + +// UnpackOpenEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event Open(bytes32 indexed orderId) +func (iLifiInputSettler *ILifiInputSettler) UnpackOpenEvent(log *types.Log) (*ILifiInputSettlerOpen, error) { + event := "Open" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerOpen) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerOpen0 represents a Open0 event raised by the ILifiInputSettler contract. +type ILifiInputSettlerOpen0 struct { + OrderId [32]byte + Order StandardOrder + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerOpen0EventName = "Open0" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerOpen0) ContractEventName() string { + return ILifiInputSettlerOpen0EventName +} + +// UnpackOpen0Event is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event Open(bytes32 indexed orderId, (address,uint256,uint256,uint32,uint32,address,uint256[2][],(bytes32,bytes32,uint256,bytes32,uint256,bytes32,bytes,bytes)[]) order) +func (iLifiInputSettler *ILifiInputSettler) UnpackOpen0Event(log *types.Log) (*ILifiInputSettlerOpen0, error) { + event := "Open0" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerOpen0) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerOrderPurchased represents a OrderPurchased event raised by the ILifiInputSettler contract. +type ILifiInputSettlerOrderPurchased struct { + OrderId [32]byte + Solver [32]byte + Purchaser [32]byte + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerOrderPurchasedEventName = "OrderPurchased" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerOrderPurchased) ContractEventName() string { + return ILifiInputSettlerOrderPurchasedEventName +} + +// UnpackOrderPurchasedEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event OrderPurchased(bytes32 indexed orderId, bytes32 solver, bytes32 purchaser) +func (iLifiInputSettler *ILifiInputSettler) UnpackOrderPurchasedEvent(log *types.Log) (*ILifiInputSettlerOrderPurchased, error) { + event := "OrderPurchased" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerOrderPurchased) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerOwnershipHandoverCanceled represents a OwnershipHandoverCanceled event raised by the ILifiInputSettler contract. +type ILifiInputSettlerOwnershipHandoverCanceled struct { + PendingOwner common.Address + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerOwnershipHandoverCanceledEventName = "OwnershipHandoverCanceled" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerOwnershipHandoverCanceled) ContractEventName() string { + return ILifiInputSettlerOwnershipHandoverCanceledEventName +} + +// UnpackOwnershipHandoverCanceledEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event OwnershipHandoverCanceled(address indexed pendingOwner) +func (iLifiInputSettler *ILifiInputSettler) UnpackOwnershipHandoverCanceledEvent(log *types.Log) (*ILifiInputSettlerOwnershipHandoverCanceled, error) { + event := "OwnershipHandoverCanceled" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerOwnershipHandoverCanceled) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerOwnershipHandoverRequested represents a OwnershipHandoverRequested event raised by the ILifiInputSettler contract. +type ILifiInputSettlerOwnershipHandoverRequested struct { + PendingOwner common.Address + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerOwnershipHandoverRequestedEventName = "OwnershipHandoverRequested" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerOwnershipHandoverRequested) ContractEventName() string { + return ILifiInputSettlerOwnershipHandoverRequestedEventName +} + +// UnpackOwnershipHandoverRequestedEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event OwnershipHandoverRequested(address indexed pendingOwner) +func (iLifiInputSettler *ILifiInputSettler) UnpackOwnershipHandoverRequestedEvent(log *types.Log) (*ILifiInputSettlerOwnershipHandoverRequested, error) { + event := "OwnershipHandoverRequested" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerOwnershipHandoverRequested) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerOwnershipTransferred represents a OwnershipTransferred event raised by the ILifiInputSettler contract. +type ILifiInputSettlerOwnershipTransferred struct { + OldOwner common.Address + NewOwner common.Address + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerOwnershipTransferredEventName = "OwnershipTransferred" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerOwnershipTransferred) ContractEventName() string { + return ILifiInputSettlerOwnershipTransferredEventName +} + +// UnpackOwnershipTransferredEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event OwnershipTransferred(address indexed oldOwner, address indexed newOwner) +func (iLifiInputSettler *ILifiInputSettler) UnpackOwnershipTransferredEvent(log *types.Log) (*ILifiInputSettlerOwnershipTransferred, error) { + event := "OwnershipTransferred" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerOwnershipTransferred) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// ILifiInputSettlerRefunded represents a Refunded event raised by the ILifiInputSettler contract. +type ILifiInputSettlerRefunded struct { + OrderId [32]byte + Raw *types.Log // Blockchain specific contextual infos +} + +const ILifiInputSettlerRefundedEventName = "Refunded" + +// ContractEventName returns the user-defined event name. +func (ILifiInputSettlerRefunded) ContractEventName() string { + return ILifiInputSettlerRefundedEventName +} + +// UnpackRefundedEvent is the Go binding that unpacks the event data emitted +// by contract. +// +// Solidity: event Refunded(bytes32 indexed orderId) +func (iLifiInputSettler *ILifiInputSettler) UnpackRefundedEvent(log *types.Log) (*ILifiInputSettlerRefunded, error) { + event := "Refunded" + if log.Topics[0] != iLifiInputSettler.abi.Events[event].ID { + return nil, errors.New("event signature mismatch") + } + out := new(ILifiInputSettlerRefunded) + if len(log.Data) > 0 { + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, event, log.Data); err != nil { + return nil, err + } + } + var indexed abi.Arguments + for _, arg := range iLifiInputSettler.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + if err := abi.ParseTopics(out, indexed, log.Topics[1:]); err != nil { + return nil, err + } + out.Raw = log + return out, nil +} + +// UnpackError attempts to decode the provided error data using user-defined +// error definitions. +func (iLifiInputSettler *ILifiInputSettler) UnpackError(raw []byte) (any, error) { + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["AlreadyInitialized"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackAlreadyInitializedError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["AlreadyPurchased"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackAlreadyPurchasedError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["CallOutOfRange"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackCallOutOfRangeError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["CodeSize0"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackCodeSize0Error(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["ContextOutOfRange"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackContextOutOfRangeError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["Expired"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackExpiredError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["FillDeadlineAfterExpiry"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackFillDeadlineAfterExpiryError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["FilledTooLate"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackFilledTooLateError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["GovernanceFeeChangeNotReady"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackGovernanceFeeChangeNotReadyError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["GovernanceFeeTooHigh"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackGovernanceFeeTooHighError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["HasDirtyBits"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackHasDirtyBitsError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["InvalidOrderStatus"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackInvalidOrderStatusError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["InvalidPurchaser"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackInvalidPurchaserError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["InvalidShortString"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackInvalidShortStringError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["InvalidSigner"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackInvalidSignerError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["InvalidTimestampLength"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackInvalidTimestampLengthError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["NewOwnerIsZeroAddress"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackNewOwnerIsZeroAddressError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["NoDestination"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackNoDestinationError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["NoHandoverRequest"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackNoHandoverRequestError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["NotOrderOwner"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackNotOrderOwnerError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["OrderIdMismatch"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackOrderIdMismatchError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["ReentrancyDetected"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackReentrancyDetectedError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["SafeERC20FailedOperation"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackSafeERC20FailedOperationError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["SignatureAndInputsNotEqual"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackSignatureAndInputsNotEqualError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["SignatureNotSupported"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackSignatureNotSupportedError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["StringTooLong"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackStringTooLongError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["TimestampNotPassed"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackTimestampNotPassedError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["TimestampPassed"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackTimestampPassedError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["Unauthorized"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackUnauthorizedError(raw[4:]) + } + if bytes.Equal(raw[:4], iLifiInputSettler.abi.Errors["WrongChain"].ID.Bytes()[:4]) { + return iLifiInputSettler.UnpackWrongChainError(raw[4:]) + } + return nil, errors.New("Unknown error") +} + +// ILifiInputSettlerAlreadyInitialized represents a AlreadyInitialized error raised by the ILifiInputSettler contract. +type ILifiInputSettlerAlreadyInitialized struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error AlreadyInitialized() +func ILifiInputSettlerAlreadyInitializedErrorID() common.Hash { + return common.HexToHash("0x0dc149f07762891dbcea3fe72770f3d63a1863fc54b2f084e8c59ec476996927") +} + +// UnpackAlreadyInitializedError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error AlreadyInitialized() +func (iLifiInputSettler *ILifiInputSettler) UnpackAlreadyInitializedError(raw []byte) (*ILifiInputSettlerAlreadyInitialized, error) { + out := new(ILifiInputSettlerAlreadyInitialized) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "AlreadyInitialized", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerAlreadyPurchased represents a AlreadyPurchased error raised by the ILifiInputSettler contract. +type ILifiInputSettlerAlreadyPurchased struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error AlreadyPurchased() +func ILifiInputSettlerAlreadyPurchasedErrorID() common.Hash { + return common.HexToHash("0x3367b554dccf0f6b7e731388e7b58cf6b61aa57a5d2d9b20798abf1e9a9eb9d9") +} + +// UnpackAlreadyPurchasedError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error AlreadyPurchased() +func (iLifiInputSettler *ILifiInputSettler) UnpackAlreadyPurchasedError(raw []byte) (*ILifiInputSettlerAlreadyPurchased, error) { + out := new(ILifiInputSettlerAlreadyPurchased) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "AlreadyPurchased", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerCallOutOfRange represents a CallOutOfRange error raised by the ILifiInputSettler contract. +type ILifiInputSettlerCallOutOfRange struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error CallOutOfRange() +func ILifiInputSettlerCallOutOfRangeErrorID() common.Hash { + return common.HexToHash("0x4fe9ad238b0efcfdcc07e41ff080de6477c45b0a2b23e6a1710bf7a4561340e9") +} + +// UnpackCallOutOfRangeError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error CallOutOfRange() +func (iLifiInputSettler *ILifiInputSettler) UnpackCallOutOfRangeError(raw []byte) (*ILifiInputSettlerCallOutOfRange, error) { + out := new(ILifiInputSettlerCallOutOfRange) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "CallOutOfRange", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerCodeSize0 represents a CodeSize0 error raised by the ILifiInputSettler contract. +type ILifiInputSettlerCodeSize0 struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error CodeSize0() +func ILifiInputSettlerCodeSize0ErrorID() common.Hash { + return common.HexToHash("0xfbc1d8e2c3f2772770ee2062b2b56e4b23e4e91332347f7656f0f8aafbb9cb0c") +} + +// UnpackCodeSize0Error is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error CodeSize0() +func (iLifiInputSettler *ILifiInputSettler) UnpackCodeSize0Error(raw []byte) (*ILifiInputSettlerCodeSize0, error) { + out := new(ILifiInputSettlerCodeSize0) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "CodeSize0", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerContextOutOfRange represents a ContextOutOfRange error raised by the ILifiInputSettler contract. +type ILifiInputSettlerContextOutOfRange struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error ContextOutOfRange() +func ILifiInputSettlerContextOutOfRangeErrorID() common.Hash { + return common.HexToHash("0xd94d6ce6aedb93cc32ffa64d0fd16f10262e85593f5410fe9a0c38743fb09af7") +} + +// UnpackContextOutOfRangeError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error ContextOutOfRange() +func (iLifiInputSettler *ILifiInputSettler) UnpackContextOutOfRangeError(raw []byte) (*ILifiInputSettlerContextOutOfRange, error) { + out := new(ILifiInputSettlerContextOutOfRange) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "ContextOutOfRange", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerExpired represents a Expired error raised by the ILifiInputSettler contract. +type ILifiInputSettlerExpired struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error Expired() +func ILifiInputSettlerExpiredErrorID() common.Hash { + return common.HexToHash("0x203d82d8d99f63bfecc8335216735e0271df4249ea752b030f9ab305b94e5afe") +} + +// UnpackExpiredError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error Expired() +func (iLifiInputSettler *ILifiInputSettler) UnpackExpiredError(raw []byte) (*ILifiInputSettlerExpired, error) { + out := new(ILifiInputSettlerExpired) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "Expired", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerFillDeadlineAfterExpiry represents a FillDeadlineAfterExpiry error raised by the ILifiInputSettler contract. +type ILifiInputSettlerFillDeadlineAfterExpiry struct { + FillDeadline uint32 + Expires uint32 +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error FillDeadlineAfterExpiry(uint32 fillDeadline, uint32 expires) +func ILifiInputSettlerFillDeadlineAfterExpiryErrorID() common.Hash { + return common.HexToHash("0xf31549efc20c86d21b99f1bedbff489d9bf9d83f68b5771ceeb0cadd440a8415") +} + +// UnpackFillDeadlineAfterExpiryError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error FillDeadlineAfterExpiry(uint32 fillDeadline, uint32 expires) +func (iLifiInputSettler *ILifiInputSettler) UnpackFillDeadlineAfterExpiryError(raw []byte) (*ILifiInputSettlerFillDeadlineAfterExpiry, error) { + out := new(ILifiInputSettlerFillDeadlineAfterExpiry) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "FillDeadlineAfterExpiry", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerFilledTooLate represents a FilledTooLate error raised by the ILifiInputSettler contract. +type ILifiInputSettlerFilledTooLate struct { + Expected uint32 + Actual uint32 +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error FilledTooLate(uint32 expected, uint32 actual) +func ILifiInputSettlerFilledTooLateErrorID() common.Hash { + return common.HexToHash("0x0ad67c09a1e19240ccd1a72ebab6667d70cc8087485302bc38f12238e5e9d074") +} + +// UnpackFilledTooLateError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error FilledTooLate(uint32 expected, uint32 actual) +func (iLifiInputSettler *ILifiInputSettler) UnpackFilledTooLateError(raw []byte) (*ILifiInputSettlerFilledTooLate, error) { + out := new(ILifiInputSettlerFilledTooLate) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "FilledTooLate", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerGovernanceFeeChangeNotReady represents a GovernanceFeeChangeNotReady error raised by the ILifiInputSettler contract. +type ILifiInputSettlerGovernanceFeeChangeNotReady struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error GovernanceFeeChangeNotReady() +func ILifiInputSettlerGovernanceFeeChangeNotReadyErrorID() common.Hash { + return common.HexToHash("0x6f4cfed1c34a227615bf9d3fb4f3149b79498b8ff3c30e5c7dba10fc2c31e408") +} + +// UnpackGovernanceFeeChangeNotReadyError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error GovernanceFeeChangeNotReady() +func (iLifiInputSettler *ILifiInputSettler) UnpackGovernanceFeeChangeNotReadyError(raw []byte) (*ILifiInputSettlerGovernanceFeeChangeNotReady, error) { + out := new(ILifiInputSettlerGovernanceFeeChangeNotReady) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "GovernanceFeeChangeNotReady", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerGovernanceFeeTooHigh represents a GovernanceFeeTooHigh error raised by the ILifiInputSettler contract. +type ILifiInputSettlerGovernanceFeeTooHigh struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error GovernanceFeeTooHigh() +func ILifiInputSettlerGovernanceFeeTooHighErrorID() common.Hash { + return common.HexToHash("0x0f4820d8a6b3e19893860b79e29977fda9aa6ef4b2e1a7d09c8e8955b69be56c") +} + +// UnpackGovernanceFeeTooHighError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error GovernanceFeeTooHigh() +func (iLifiInputSettler *ILifiInputSettler) UnpackGovernanceFeeTooHighError(raw []byte) (*ILifiInputSettlerGovernanceFeeTooHigh, error) { + out := new(ILifiInputSettlerGovernanceFeeTooHigh) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "GovernanceFeeTooHigh", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerHasDirtyBits represents a HasDirtyBits error raised by the ILifiInputSettler contract. +type ILifiInputSettlerHasDirtyBits struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error HasDirtyBits() +func ILifiInputSettlerHasDirtyBitsErrorID() common.Hash { + return common.HexToHash("0x5f3d6d4f57bdccabacd05058457a7e7ae88d95331a81a9def1d147b62fdf9eab") +} + +// UnpackHasDirtyBitsError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error HasDirtyBits() +func (iLifiInputSettler *ILifiInputSettler) UnpackHasDirtyBitsError(raw []byte) (*ILifiInputSettlerHasDirtyBits, error) { + out := new(ILifiInputSettlerHasDirtyBits) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "HasDirtyBits", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerInvalidOrderStatus represents a InvalidOrderStatus error raised by the ILifiInputSettler contract. +type ILifiInputSettlerInvalidOrderStatus struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error InvalidOrderStatus() +func ILifiInputSettlerInvalidOrderStatusErrorID() common.Hash { + return common.HexToHash("0x2916ae33cf4ed00872aaf269c86d13a12e9ad47f836db89ea191297fecc7a2e7") +} + +// UnpackInvalidOrderStatusError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error InvalidOrderStatus() +func (iLifiInputSettler *ILifiInputSettler) UnpackInvalidOrderStatusError(raw []byte) (*ILifiInputSettlerInvalidOrderStatus, error) { + out := new(ILifiInputSettlerInvalidOrderStatus) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "InvalidOrderStatus", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerInvalidPurchaser represents a InvalidPurchaser error raised by the ILifiInputSettler contract. +type ILifiInputSettlerInvalidPurchaser struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error InvalidPurchaser() +func ILifiInputSettlerInvalidPurchaserErrorID() common.Hash { + return common.HexToHash("0xcf7899a1ea308d1129fe0e01fbd4fdca283f8c93391f8c697f69a9b2d02d339e") +} + +// UnpackInvalidPurchaserError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error InvalidPurchaser() +func (iLifiInputSettler *ILifiInputSettler) UnpackInvalidPurchaserError(raw []byte) (*ILifiInputSettlerInvalidPurchaser, error) { + out := new(ILifiInputSettlerInvalidPurchaser) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "InvalidPurchaser", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerInvalidShortString represents a InvalidShortString error raised by the ILifiInputSettler contract. +type ILifiInputSettlerInvalidShortString struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error InvalidShortString() +func ILifiInputSettlerInvalidShortStringErrorID() common.Hash { + return common.HexToHash("0xb3512b0c6163e5f0bafab72bb631b9d58cd7a731b082f910338aa21c83d5c274") +} + +// UnpackInvalidShortStringError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error InvalidShortString() +func (iLifiInputSettler *ILifiInputSettler) UnpackInvalidShortStringError(raw []byte) (*ILifiInputSettlerInvalidShortString, error) { + out := new(ILifiInputSettlerInvalidShortString) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "InvalidShortString", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerInvalidSigner represents a InvalidSigner error raised by the ILifiInputSettler contract. +type ILifiInputSettlerInvalidSigner struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error InvalidSigner() +func ILifiInputSettlerInvalidSignerErrorID() common.Hash { + return common.HexToHash("0x815e1d64efb74fbe314c20a2b8a2335d18bce12a19165e447fa36bcb35959528") +} + +// UnpackInvalidSignerError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error InvalidSigner() +func (iLifiInputSettler *ILifiInputSettler) UnpackInvalidSignerError(raw []byte) (*ILifiInputSettlerInvalidSigner, error) { + out := new(ILifiInputSettlerInvalidSigner) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "InvalidSigner", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerInvalidTimestampLength represents a InvalidTimestampLength error raised by the ILifiInputSettler contract. +type ILifiInputSettlerInvalidTimestampLength struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error InvalidTimestampLength() +func ILifiInputSettlerInvalidTimestampLengthErrorID() common.Hash { + return common.HexToHash("0x12d486097b64be32f9dcb600781aa0b64747f2a80f9865544107941f4921cea0") +} + +// UnpackInvalidTimestampLengthError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error InvalidTimestampLength() +func (iLifiInputSettler *ILifiInputSettler) UnpackInvalidTimestampLengthError(raw []byte) (*ILifiInputSettlerInvalidTimestampLength, error) { + out := new(ILifiInputSettlerInvalidTimestampLength) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "InvalidTimestampLength", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerNewOwnerIsZeroAddress represents a NewOwnerIsZeroAddress error raised by the ILifiInputSettler contract. +type ILifiInputSettlerNewOwnerIsZeroAddress struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error NewOwnerIsZeroAddress() +func ILifiInputSettlerNewOwnerIsZeroAddressErrorID() common.Hash { + return common.HexToHash("0x7448fbae245b5163a637f61fac94c5376c3e155928452ce47ee52d8c1b99587a") +} + +// UnpackNewOwnerIsZeroAddressError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error NewOwnerIsZeroAddress() +func (iLifiInputSettler *ILifiInputSettler) UnpackNewOwnerIsZeroAddressError(raw []byte) (*ILifiInputSettlerNewOwnerIsZeroAddress, error) { + out := new(ILifiInputSettlerNewOwnerIsZeroAddress) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "NewOwnerIsZeroAddress", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerNoDestination represents a NoDestination error raised by the ILifiInputSettler contract. +type ILifiInputSettlerNoDestination struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error NoDestination() +func ILifiInputSettlerNoDestinationErrorID() common.Hash { + return common.HexToHash("0xb8e78e8013c2b18060a5e1d1d47e7c487b3f4c9e26fe84ba199887e6c88abda5") +} + +// UnpackNoDestinationError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error NoDestination() +func (iLifiInputSettler *ILifiInputSettler) UnpackNoDestinationError(raw []byte) (*ILifiInputSettlerNoDestination, error) { + out := new(ILifiInputSettlerNoDestination) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "NoDestination", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerNoHandoverRequest represents a NoHandoverRequest error raised by the ILifiInputSettler contract. +type ILifiInputSettlerNoHandoverRequest struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error NoHandoverRequest() +func ILifiInputSettlerNoHandoverRequestErrorID() common.Hash { + return common.HexToHash("0x6f5e8818469c73d5be4a0d17c371cde64695907022629c1d064c895f98d466a6") +} + +// UnpackNoHandoverRequestError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error NoHandoverRequest() +func (iLifiInputSettler *ILifiInputSettler) UnpackNoHandoverRequestError(raw []byte) (*ILifiInputSettlerNoHandoverRequest, error) { + out := new(ILifiInputSettlerNoHandoverRequest) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "NoHandoverRequest", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerNotOrderOwner represents a NotOrderOwner error raised by the ILifiInputSettler contract. +type ILifiInputSettlerNotOrderOwner struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error NotOrderOwner() +func ILifiInputSettlerNotOrderOwnerErrorID() common.Hash { + return common.HexToHash("0xf6412b5a9f98f861af79c1937e4ad40c98a45a023657259dd5775a8de7ecca15") +} + +// UnpackNotOrderOwnerError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error NotOrderOwner() +func (iLifiInputSettler *ILifiInputSettler) UnpackNotOrderOwnerError(raw []byte) (*ILifiInputSettlerNotOrderOwner, error) { + out := new(ILifiInputSettlerNotOrderOwner) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "NotOrderOwner", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerOrderIdMismatch represents a OrderIdMismatch error raised by the ILifiInputSettler contract. +type ILifiInputSettlerOrderIdMismatch struct { + Provided [32]byte + Computed [32]byte +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error OrderIdMismatch(bytes32 provided, bytes32 computed) +func ILifiInputSettlerOrderIdMismatchErrorID() common.Hash { + return common.HexToHash("0x0517adf9c87f4f5cb24c4c43e313f684b98703db5b126fdd4f5ac47cc02267d5") +} + +// UnpackOrderIdMismatchError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error OrderIdMismatch(bytes32 provided, bytes32 computed) +func (iLifiInputSettler *ILifiInputSettler) UnpackOrderIdMismatchError(raw []byte) (*ILifiInputSettlerOrderIdMismatch, error) { + out := new(ILifiInputSettlerOrderIdMismatch) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "OrderIdMismatch", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerReentrancyDetected represents a ReentrancyDetected error raised by the ILifiInputSettler contract. +type ILifiInputSettlerReentrancyDetected struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error ReentrancyDetected() +func ILifiInputSettlerReentrancyDetectedErrorID() common.Hash { + return common.HexToHash("0xc5f2be51ec4ec0ad8a7972d497da993a6fcbb89cf72c05f97d654ed81ce53492") +} + +// UnpackReentrancyDetectedError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error ReentrancyDetected() +func (iLifiInputSettler *ILifiInputSettler) UnpackReentrancyDetectedError(raw []byte) (*ILifiInputSettlerReentrancyDetected, error) { + out := new(ILifiInputSettlerReentrancyDetected) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "ReentrancyDetected", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerSafeERC20FailedOperation represents a SafeERC20FailedOperation error raised by the ILifiInputSettler contract. +type ILifiInputSettlerSafeERC20FailedOperation struct { + Token common.Address +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error SafeERC20FailedOperation(address token) +func ILifiInputSettlerSafeERC20FailedOperationErrorID() common.Hash { + return common.HexToHash("0x5274afe73c98b4749fc91ffae6b7b574e7842cb2144a159e9377a5f20b32edf9") +} + +// UnpackSafeERC20FailedOperationError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error SafeERC20FailedOperation(address token) +func (iLifiInputSettler *ILifiInputSettler) UnpackSafeERC20FailedOperationError(raw []byte) (*ILifiInputSettlerSafeERC20FailedOperation, error) { + out := new(ILifiInputSettlerSafeERC20FailedOperation) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "SafeERC20FailedOperation", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerSignatureAndInputsNotEqual represents a SignatureAndInputsNotEqual error raised by the ILifiInputSettler contract. +type ILifiInputSettlerSignatureAndInputsNotEqual struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error SignatureAndInputsNotEqual() +func ILifiInputSettlerSignatureAndInputsNotEqualErrorID() common.Hash { + return common.HexToHash("0x06f68b62ffd2436fe64050449d9d38b1823a747a993c088a72845ae5994b9883") +} + +// UnpackSignatureAndInputsNotEqualError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error SignatureAndInputsNotEqual() +func (iLifiInputSettler *ILifiInputSettler) UnpackSignatureAndInputsNotEqualError(raw []byte) (*ILifiInputSettlerSignatureAndInputsNotEqual, error) { + out := new(ILifiInputSettlerSignatureAndInputsNotEqual) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "SignatureAndInputsNotEqual", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerSignatureNotSupported represents a SignatureNotSupported error raised by the ILifiInputSettler contract. +type ILifiInputSettlerSignatureNotSupported struct { + Arg0 [1]byte +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error SignatureNotSupported(bytes1 arg0) +func ILifiInputSettlerSignatureNotSupportedErrorID() common.Hash { + return common.HexToHash("0x5d0b6f18a8b247272db8eeca2dbe086a9850e7bfd217b19bd636e0a15fbd7861") +} + +// UnpackSignatureNotSupportedError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error SignatureNotSupported(bytes1 arg0) +func (iLifiInputSettler *ILifiInputSettler) UnpackSignatureNotSupportedError(raw []byte) (*ILifiInputSettlerSignatureNotSupported, error) { + out := new(ILifiInputSettlerSignatureNotSupported) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "SignatureNotSupported", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerStringTooLong represents a StringTooLong error raised by the ILifiInputSettler contract. +type ILifiInputSettlerStringTooLong struct { + Str string +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error StringTooLong(string str) +func ILifiInputSettlerStringTooLongErrorID() common.Hash { + return common.HexToHash("0x305a27a93f8e33b7392df0a0f91d6fc63847395853c45991eec52dbf24d72381") +} + +// UnpackStringTooLongError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error StringTooLong(string str) +func (iLifiInputSettler *ILifiInputSettler) UnpackStringTooLongError(raw []byte) (*ILifiInputSettlerStringTooLong, error) { + out := new(ILifiInputSettlerStringTooLong) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "StringTooLong", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerTimestampNotPassed represents a TimestampNotPassed error raised by the ILifiInputSettler contract. +type ILifiInputSettlerTimestampNotPassed struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error TimestampNotPassed() +func ILifiInputSettlerTimestampNotPassedErrorID() common.Hash { + return common.HexToHash("0xeb21afbdfbff45b8884b33197c58f4fdd57aeee3ef678ac1e61248dc84fa5ac0") +} + +// UnpackTimestampNotPassedError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error TimestampNotPassed() +func (iLifiInputSettler *ILifiInputSettler) UnpackTimestampNotPassedError(raw []byte) (*ILifiInputSettlerTimestampNotPassed, error) { + out := new(ILifiInputSettlerTimestampNotPassed) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "TimestampNotPassed", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerTimestampPassed represents a TimestampPassed error raised by the ILifiInputSettler contract. +type ILifiInputSettlerTimestampPassed struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error TimestampPassed() +func ILifiInputSettlerTimestampPassedErrorID() common.Hash { + return common.HexToHash("0x4a313c2dac3291054a75303df1d71c904dff49517ea33f42a82307b8ddca441a") +} + +// UnpackTimestampPassedError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error TimestampPassed() +func (iLifiInputSettler *ILifiInputSettler) UnpackTimestampPassedError(raw []byte) (*ILifiInputSettlerTimestampPassed, error) { + out := new(ILifiInputSettlerTimestampPassed) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "TimestampPassed", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerUnauthorized represents a Unauthorized error raised by the ILifiInputSettler contract. +type ILifiInputSettlerUnauthorized struct { +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error Unauthorized() +func ILifiInputSettlerUnauthorizedErrorID() common.Hash { + return common.HexToHash("0x82b4290015f7ec7256ca2a6247d3c2a89c4865c0e791456df195f40ad0a81367") +} + +// UnpackUnauthorizedError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error Unauthorized() +func (iLifiInputSettler *ILifiInputSettler) UnpackUnauthorizedError(raw []byte) (*ILifiInputSettlerUnauthorized, error) { + out := new(ILifiInputSettlerUnauthorized) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "Unauthorized", raw); err != nil { + return nil, err + } + return out, nil +} + +// ILifiInputSettlerWrongChain represents a WrongChain error raised by the ILifiInputSettler contract. +type ILifiInputSettlerWrongChain struct { + Expected *big.Int + Actual *big.Int +} + +// ErrorID returns the hash of canonical representation of the error's signature. +// +// Solidity: error WrongChain(uint256 expected, uint256 actual) +func ILifiInputSettlerWrongChainErrorID() common.Hash { + return common.HexToHash("0x24497bc308635bccbc06f4997297d2158da178be2267eeead419bfdb19d42d4b") +} + +// UnpackWrongChainError is the Go binding used to decode the provided +// error data into the corresponding Go error struct. +// +// Solidity: error WrongChain(uint256 expected, uint256 actual) +func (iLifiInputSettler *ILifiInputSettler) UnpackWrongChainError(raw []byte) (*ILifiInputSettlerWrongChain, error) { + out := new(ILifiInputSettlerWrongChain) + if err := iLifiInputSettler.abi.UnpackIntoInterface(out, "WrongChain", raw); err != nil { + return nil, err + } + return out, nil +} diff --git a/api/lifiorder/api_bridge_api.go b/api/lifiorder/api_bridge_api.go index ea3edb37..892a31e4 100644 --- a/api/lifiorder/api_bridge_api.go +++ b/api/lifiorder/api_bridge_api.go @@ -126,13 +126,13 @@ type ApiOrdersControllerGetOrderStatusRequest struct { catalystOrderId *string } -// On chain order id propagated in the logs/events. +// On chain order id propagated in the logs/events. At least one of `onChainOrderId` or `catalystOrderId` must be provided. func (r ApiOrdersControllerGetOrderStatusRequest) OnChainOrderId(onChainOrderId string) ApiOrdersControllerGetOrderStatusRequest { r.onChainOrderId = &onChainOrderId return r } -// Internal order id returned by Lifi Intents API +// Internal order id returned by Lifi Intents API. At least one of `onChainOrderId` or `catalystOrderId` must be provided. func (r ApiOrdersControllerGetOrderStatusRequest) CatalystOrderId(catalystOrderId string) ApiOrdersControllerGetOrderStatusRequest { r.catalystOrderId = &catalystOrderId return r @@ -628,7 +628,7 @@ func (r ApiQuotesControllerRequestQuoteRequest) OifQuoteRequestDto(oifQuoteReque return r } -// Raw integrator API key (obtained from integrator onboarding). When provided, integrator-specific quotes become eligible in addition to open-market quotes. +// Raw integrator key. Use a high-entropy random string. When provided, integrator-specific quotes become eligible in addition to open-market quotes. func (r ApiQuotesControllerRequestQuoteRequest) XIntegratorKey(xIntegratorKey string) ApiQuotesControllerRequestQuoteRequest { r.xIntegratorKey = &xIntegratorKey return r diff --git a/api/lifiorder/api_bridge_apiv1.go b/api/lifiorder/api_bridge_apiv1.go index d3dfdc27..2140b21a 100644 --- a/api/lifiorder/api_bridge_apiv1.go +++ b/api/lifiorder/api_bridge_apiv1.go @@ -33,7 +33,7 @@ func (r ApiQuotesControllerV1GetQuoteRequest) QuoteRequestDto(quoteRequestDto Qu return r } -// Raw integrator API key (obtained from integrator onboarding). When provided, integrator-specific quotes become eligible in addition to open-market quotes. +// Raw integrator key. Use a high-entropy random string. When provided, integrator-specific quotes become eligible in addition to open-market quotes. func (r ApiQuotesControllerV1GetQuoteRequest) XIntegratorKey(xIntegratorKey string) ApiQuotesControllerV1GetQuoteRequest { r.xIntegratorKey = &xIntegratorKey return r diff --git a/api/lifiorder/configuration.go b/api/lifiorder/configuration.go index 8f172df9..5c4335f3 100644 --- a/api/lifiorder/configuration.go +++ b/api/lifiorder/configuration.go @@ -93,8 +93,12 @@ func NewConfiguration() *Configuration { Debug: false, Servers: ServerConfigurations{ { - URL: "", - Description: "No description provided", + URL: "https://order.li.fi", + Description: "Production", + }, + { + URL: "https://order-dev.li.fi", + Description: "Development", }, }, OperationServers: map[string]ServerConfigurations{}, diff --git a/api/lifiorder/model_allowance_check_dto.go b/api/lifiorder/model_allowance_check_dto.go new file mode 100644 index 00000000..e34050f0 --- /dev/null +++ b/api/lifiorder/model_allowance_check_dto.go @@ -0,0 +1,273 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the AllowanceCheckDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &AllowanceCheckDto{} + +// AllowanceCheckDto struct for AllowanceCheckDto +type AllowanceCheckDto struct { + // CAIP-2 chain identifier for this allowance check (e.g., \"eip155:1\") + Chain string `json:"chain"` + // Native token address + Token string `json:"token"` + // Native user address + User string `json:"user"` + // Native spender address - InputSettlerEscrowLIFI + Spender string `json:"spender"` + // Required allowance amount as string-encoded integer + Required string `json:"required"` +} + +type _AllowanceCheckDto AllowanceCheckDto + +// NewAllowanceCheckDto instantiates a new AllowanceCheckDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewAllowanceCheckDto(chain string, token string, user string, spender string, required string) *AllowanceCheckDto { + this := AllowanceCheckDto{} + this.Chain = chain + this.Token = token + this.User = user + this.Spender = spender + this.Required = required + return &this +} + +// NewAllowanceCheckDtoWithDefaults instantiates a new AllowanceCheckDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewAllowanceCheckDtoWithDefaults() *AllowanceCheckDto { + this := AllowanceCheckDto{} + return &this +} + +// GetChain returns the Chain field value +func (o *AllowanceCheckDto) GetChain() string { + if o == nil { + var ret string + return ret + } + + return o.Chain +} + +// GetChainOk returns a tuple with the Chain field value +// and a boolean to check if the value has been set. +func (o *AllowanceCheckDto) GetChainOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Chain, true +} + +// SetChain sets field value +func (o *AllowanceCheckDto) SetChain(v string) { + o.Chain = v +} + +// GetToken returns the Token field value +func (o *AllowanceCheckDto) GetToken() string { + if o == nil { + var ret string + return ret + } + + return o.Token +} + +// GetTokenOk returns a tuple with the Token field value +// and a boolean to check if the value has been set. +func (o *AllowanceCheckDto) GetTokenOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Token, true +} + +// SetToken sets field value +func (o *AllowanceCheckDto) SetToken(v string) { + o.Token = v +} + +// GetUser returns the User field value +func (o *AllowanceCheckDto) GetUser() string { + if o == nil { + var ret string + return ret + } + + return o.User +} + +// GetUserOk returns a tuple with the User field value +// and a boolean to check if the value has been set. +func (o *AllowanceCheckDto) GetUserOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.User, true +} + +// SetUser sets field value +func (o *AllowanceCheckDto) SetUser(v string) { + o.User = v +} + +// GetSpender returns the Spender field value +func (o *AllowanceCheckDto) GetSpender() string { + if o == nil { + var ret string + return ret + } + + return o.Spender +} + +// GetSpenderOk returns a tuple with the Spender field value +// and a boolean to check if the value has been set. +func (o *AllowanceCheckDto) GetSpenderOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Spender, true +} + +// SetSpender sets field value +func (o *AllowanceCheckDto) SetSpender(v string) { + o.Spender = v +} + +// GetRequired returns the Required field value +func (o *AllowanceCheckDto) GetRequired() string { + if o == nil { + var ret string + return ret + } + + return o.Required +} + +// GetRequiredOk returns a tuple with the Required field value +// and a boolean to check if the value has been set. +func (o *AllowanceCheckDto) GetRequiredOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Required, true +} + +// SetRequired sets field value +func (o *AllowanceCheckDto) SetRequired(v string) { + o.Required = v +} + +func (o AllowanceCheckDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o AllowanceCheckDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["chain"] = o.Chain + toSerialize["token"] = o.Token + toSerialize["user"] = o.User + toSerialize["spender"] = o.Spender + toSerialize["required"] = o.Required + return toSerialize, nil +} + +func (o *AllowanceCheckDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "chain", + "token", + "user", + "spender", + "required", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varAllowanceCheckDto := _AllowanceCheckDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varAllowanceCheckDto) + + if err != nil { + return err + } + + *o = AllowanceCheckDto(varAllowanceCheckDto) + + return err +} + +type NullableAllowanceCheckDto struct { + value *AllowanceCheckDto + isSet bool +} + +func (v NullableAllowanceCheckDto) Get() *AllowanceCheckDto { + return v.value +} + +func (v *NullableAllowanceCheckDto) Set(val *AllowanceCheckDto) { + v.value = val + v.isSet = true +} + +func (v NullableAllowanceCheckDto) IsSet() bool { + return v.isSet +} + +func (v *NullableAllowanceCheckDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAllowanceCheckDto(val *AllowanceCheckDto) *NullableAllowanceCheckDto { + return &NullableAllowanceCheckDto{value: val, isSet: true} +} + +func (v NullableAllowanceCheckDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAllowanceCheckDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_checks_dto.go b/api/lifiorder/model_checks_dto.go new file mode 100644 index 00000000..140ed5a4 --- /dev/null +++ b/api/lifiorder/model_checks_dto.go @@ -0,0 +1,157 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the ChecksDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &ChecksDto{} + +// ChecksDto struct for ChecksDto +type ChecksDto struct { + // Required allowances and balances. Each item asserts that user has at least required balance and allowance for spender on token. + Allowances []AllowanceCheckDto `json:"allowances"` +} + +type _ChecksDto ChecksDto + +// NewChecksDto instantiates a new ChecksDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewChecksDto(allowances []AllowanceCheckDto) *ChecksDto { + this := ChecksDto{} + this.Allowances = allowances + return &this +} + +// NewChecksDtoWithDefaults instantiates a new ChecksDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewChecksDtoWithDefaults() *ChecksDto { + this := ChecksDto{} + return &this +} + +// GetAllowances returns the Allowances field value +func (o *ChecksDto) GetAllowances() []AllowanceCheckDto { + if o == nil { + var ret []AllowanceCheckDto + return ret + } + + return o.Allowances +} + +// GetAllowancesOk returns a tuple with the Allowances field value +// and a boolean to check if the value has been set. +func (o *ChecksDto) GetAllowancesOk() ([]AllowanceCheckDto, bool) { + if o == nil { + return nil, false + } + return o.Allowances, true +} + +// SetAllowances sets field value +func (o *ChecksDto) SetAllowances(v []AllowanceCheckDto) { + o.Allowances = v +} + +func (o ChecksDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o ChecksDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["allowances"] = o.Allowances + return toSerialize, nil +} + +func (o *ChecksDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "allowances", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varChecksDto := _ChecksDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varChecksDto) + + if err != nil { + return err + } + + *o = ChecksDto(varChecksDto) + + return err +} + +type NullableChecksDto struct { + value *ChecksDto + isSet bool +} + +func (v NullableChecksDto) Get() *ChecksDto { + return v.value +} + +func (v *NullableChecksDto) Set(val *ChecksDto) { + v.value = val + v.isSet = true +} + +func (v NullableChecksDto) IsSet() bool { + return v.isSet +} + +func (v *NullableChecksDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableChecksDto(val *ChecksDto) *NullableChecksDto { + return &NullableChecksDto{value: val, isSet: true} +} + +func (v NullableChecksDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableChecksDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_eip712_payload_dto.go b/api/lifiorder/model_eip712_payload_dto.go new file mode 100644 index 00000000..a7a4e17d --- /dev/null +++ b/api/lifiorder/model_eip712_payload_dto.go @@ -0,0 +1,273 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the Eip712PayloadDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Eip712PayloadDto{} + +// Eip712PayloadDto struct for Eip712PayloadDto +type Eip712PayloadDto struct { + // Signature type indicator + SignatureType string `json:"signatureType"` + // EIP-712 domain separator + Domain map[string]interface{} `json:"domain"` + // Primary type name + PrimaryType string `json:"primaryType"` + // The message object + Message map[string]interface{} `json:"message"` + // EIP-712 types used to construct the digest + Types map[string]interface{} `json:"types"` +} + +type _Eip712PayloadDto Eip712PayloadDto + +// NewEip712PayloadDto instantiates a new Eip712PayloadDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewEip712PayloadDto(signatureType string, domain map[string]interface{}, primaryType string, message map[string]interface{}, types map[string]interface{}) *Eip712PayloadDto { + this := Eip712PayloadDto{} + this.SignatureType = signatureType + this.Domain = domain + this.PrimaryType = primaryType + this.Message = message + this.Types = types + return &this +} + +// NewEip712PayloadDtoWithDefaults instantiates a new Eip712PayloadDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewEip712PayloadDtoWithDefaults() *Eip712PayloadDto { + this := Eip712PayloadDto{} + return &this +} + +// GetSignatureType returns the SignatureType field value +func (o *Eip712PayloadDto) GetSignatureType() string { + if o == nil { + var ret string + return ret + } + + return o.SignatureType +} + +// GetSignatureTypeOk returns a tuple with the SignatureType field value +// and a boolean to check if the value has been set. +func (o *Eip712PayloadDto) GetSignatureTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SignatureType, true +} + +// SetSignatureType sets field value +func (o *Eip712PayloadDto) SetSignatureType(v string) { + o.SignatureType = v +} + +// GetDomain returns the Domain field value +func (o *Eip712PayloadDto) GetDomain() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Domain +} + +// GetDomainOk returns a tuple with the Domain field value +// and a boolean to check if the value has been set. +func (o *Eip712PayloadDto) GetDomainOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Domain, true +} + +// SetDomain sets field value +func (o *Eip712PayloadDto) SetDomain(v map[string]interface{}) { + o.Domain = v +} + +// GetPrimaryType returns the PrimaryType field value +func (o *Eip712PayloadDto) GetPrimaryType() string { + if o == nil { + var ret string + return ret + } + + return o.PrimaryType +} + +// GetPrimaryTypeOk returns a tuple with the PrimaryType field value +// and a boolean to check if the value has been set. +func (o *Eip712PayloadDto) GetPrimaryTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.PrimaryType, true +} + +// SetPrimaryType sets field value +func (o *Eip712PayloadDto) SetPrimaryType(v string) { + o.PrimaryType = v +} + +// GetMessage returns the Message field value +func (o *Eip712PayloadDto) GetMessage() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Message +} + +// GetMessageOk returns a tuple with the Message field value +// and a boolean to check if the value has been set. +func (o *Eip712PayloadDto) GetMessageOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Message, true +} + +// SetMessage sets field value +func (o *Eip712PayloadDto) SetMessage(v map[string]interface{}) { + o.Message = v +} + +// GetTypes returns the Types field value +func (o *Eip712PayloadDto) GetTypes() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Types +} + +// GetTypesOk returns a tuple with the Types field value +// and a boolean to check if the value has been set. +func (o *Eip712PayloadDto) GetTypesOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Types, true +} + +// SetTypes sets field value +func (o *Eip712PayloadDto) SetTypes(v map[string]interface{}) { + o.Types = v +} + +func (o Eip712PayloadDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Eip712PayloadDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["signatureType"] = o.SignatureType + toSerialize["domain"] = o.Domain + toSerialize["primaryType"] = o.PrimaryType + toSerialize["message"] = o.Message + toSerialize["types"] = o.Types + return toSerialize, nil +} + +func (o *Eip712PayloadDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "signatureType", + "domain", + "primaryType", + "message", + "types", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varEip712PayloadDto := _Eip712PayloadDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varEip712PayloadDto) + + if err != nil { + return err + } + + *o = Eip712PayloadDto(varEip712PayloadDto) + + return err +} + +type NullableEip712PayloadDto struct { + value *Eip712PayloadDto + isSet bool +} + +func (v NullableEip712PayloadDto) Get() *Eip712PayloadDto { + return v.value +} + +func (v *NullableEip712PayloadDto) Set(val *Eip712PayloadDto) { + v.value = val + v.isSet = true +} + +func (v NullableEip712PayloadDto) IsSet() bool { + return v.isSet +} + +func (v *NullableEip712PayloadDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableEip712PayloadDto(val *Eip712PayloadDto) *NullableEip712PayloadDto { + return &NullableEip712PayloadDto{value: val, isSet: true} +} + +func (v NullableEip712PayloadDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableEip712PayloadDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_input_dto.go b/api/lifiorder/model_input_dto.go new file mode 100644 index 00000000..cd0febda --- /dev/null +++ b/api/lifiorder/model_input_dto.go @@ -0,0 +1,299 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the InputDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &InputDto{} + +// InputDto struct for InputDto +type InputDto struct { + // CAIP-2 chain identifier for this input (e.g., \"eip155:1\"). Applies to both user and asset. + Chain string `json:"chain"` + // Native address of the user providing the input assets + User string `json:"user"` + // Native address of the token/asset being provided as input + Asset string `json:"asset"` + Amount NullableString `json:"amount,omitempty"` + // Optional lock reference for securing the input assets. Shape: { kind: \"the-compact\" | \"rhinestone\", params: object }. Currently ignored by the quote decoder. + Lock map[string]interface{} `json:"lock,omitempty"` +} + +type _InputDto InputDto + +// NewInputDto instantiates a new InputDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewInputDto(chain string, user string, asset string) *InputDto { + this := InputDto{} + this.Chain = chain + this.User = user + this.Asset = asset + return &this +} + +// NewInputDtoWithDefaults instantiates a new InputDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewInputDtoWithDefaults() *InputDto { + this := InputDto{} + return &this +} + +// GetChain returns the Chain field value +func (o *InputDto) GetChain() string { + if o == nil { + var ret string + return ret + } + + return o.Chain +} + +// GetChainOk returns a tuple with the Chain field value +// and a boolean to check if the value has been set. +func (o *InputDto) GetChainOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Chain, true +} + +// SetChain sets field value +func (o *InputDto) SetChain(v string) { + o.Chain = v +} + +// GetUser returns the User field value +func (o *InputDto) GetUser() string { + if o == nil { + var ret string + return ret + } + + return o.User +} + +// GetUserOk returns a tuple with the User field value +// and a boolean to check if the value has been set. +func (o *InputDto) GetUserOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.User, true +} + +// SetUser sets field value +func (o *InputDto) SetUser(v string) { + o.User = v +} + +// GetAsset returns the Asset field value +func (o *InputDto) GetAsset() string { + if o == nil { + var ret string + return ret + } + + return o.Asset +} + +// GetAssetOk returns a tuple with the Asset field value +// and a boolean to check if the value has been set. +func (o *InputDto) GetAssetOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Asset, true +} + +// SetAsset sets field value +func (o *InputDto) SetAsset(v string) { + o.Asset = v +} + +// GetAmount returns the Amount field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *InputDto) GetAmount() string { + if o == nil || IsNil(o.Amount.Get()) { + var ret string + return ret + } + return *o.Amount.Get() +} + +// GetAmountOk returns a tuple with the Amount field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *InputDto) GetAmountOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.Amount.Get(), o.Amount.IsSet() +} + +// HasAmount returns a boolean if a field has been set. +func (o *InputDto) HasAmount() bool { + if o != nil && o.Amount.IsSet() { + return true + } + + return false +} + +// SetAmount gets a reference to the given NullableString and assigns it to the Amount field. +func (o *InputDto) SetAmount(v string) { + o.Amount.Set(&v) +} + +// SetAmountNil sets the value for Amount to be an explicit nil +func (o *InputDto) SetAmountNil() { + o.Amount.Set(nil) +} + +// UnsetAmount ensures that no value is present for Amount, not even an explicit nil +func (o *InputDto) UnsetAmount() { + o.Amount.Unset() +} + +// GetLock returns the Lock field value if set, zero value otherwise. +func (o *InputDto) GetLock() map[string]interface{} { + if o == nil || IsNil(o.Lock) { + var ret map[string]interface{} + return ret + } + return o.Lock +} + +// GetLockOk returns a tuple with the Lock field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *InputDto) GetLockOk() (map[string]interface{}, bool) { + if o == nil || IsNil(o.Lock) { + return map[string]interface{}{}, false + } + return o.Lock, true +} + +// HasLock returns a boolean if a field has been set. +func (o *InputDto) HasLock() bool { + if o != nil && !IsNil(o.Lock) { + return true + } + + return false +} + +// SetLock gets a reference to the given map[string]interface{} and assigns it to the Lock field. +func (o *InputDto) SetLock(v map[string]interface{}) { + o.Lock = v +} + +func (o InputDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o InputDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["chain"] = o.Chain + toSerialize["user"] = o.User + toSerialize["asset"] = o.Asset + if o.Amount.IsSet() { + toSerialize["amount"] = o.Amount.Get() + } + if !IsNil(o.Lock) { + toSerialize["lock"] = o.Lock + } + return toSerialize, nil +} + +func (o *InputDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "chain", + "user", + "asset", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varInputDto := _InputDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varInputDto) + + if err != nil { + return err + } + + *o = InputDto(varInputDto) + + return err +} + +type NullableInputDto struct { + value *InputDto + isSet bool +} + +func (v NullableInputDto) Get() *InputDto { + return v.value +} + +func (v *NullableInputDto) Set(val *InputDto) { + v.value = val + v.isSet = true +} + +func (v NullableInputDto) IsSet() bool { + return v.isSet +} + +func (v *NullableInputDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInputDto(val *InputDto) *NullableInputDto { + return &NullableInputDto{value: val, isSet: true} +} + +func (v NullableInputDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInputDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_oif3009_order_dto.go b/api/lifiorder/model_oif3009_order_dto.go new file mode 100644 index 00000000..9125a257 --- /dev/null +++ b/api/lifiorder/model_oif3009_order_dto.go @@ -0,0 +1,215 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the Oif3009OrderDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Oif3009OrderDto{} + +// Oif3009OrderDto struct for Oif3009OrderDto +type Oif3009OrderDto struct { + // Order type identifier for EIP-3009 transfers + Type string `json:"type"` + // EIP-3009 Transfer With Authorization typed data + Payload Eip712PayloadDto `json:"payload"` + // Additional metadata for nonce verification and order tracking + Metadata map[string]interface{} `json:"metadata"` +} + +type _Oif3009OrderDto Oif3009OrderDto + +// NewOif3009OrderDto instantiates a new Oif3009OrderDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewOif3009OrderDto(type_ string, payload Eip712PayloadDto, metadata map[string]interface{}) *Oif3009OrderDto { + this := Oif3009OrderDto{} + this.Type = type_ + this.Payload = payload + this.Metadata = metadata + return &this +} + +// NewOif3009OrderDtoWithDefaults instantiates a new Oif3009OrderDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewOif3009OrderDtoWithDefaults() *Oif3009OrderDto { + this := Oif3009OrderDto{} + return &this +} + +// GetType returns the Type field value +func (o *Oif3009OrderDto) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *Oif3009OrderDto) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *Oif3009OrderDto) SetType(v string) { + o.Type = v +} + +// GetPayload returns the Payload field value +func (o *Oif3009OrderDto) GetPayload() Eip712PayloadDto { + if o == nil { + var ret Eip712PayloadDto + return ret + } + + return o.Payload +} + +// GetPayloadOk returns a tuple with the Payload field value +// and a boolean to check if the value has been set. +func (o *Oif3009OrderDto) GetPayloadOk() (*Eip712PayloadDto, bool) { + if o == nil { + return nil, false + } + return &o.Payload, true +} + +// SetPayload sets field value +func (o *Oif3009OrderDto) SetPayload(v Eip712PayloadDto) { + o.Payload = v +} + +// GetMetadata returns the Metadata field value +func (o *Oif3009OrderDto) GetMetadata() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.Metadata +} + +// GetMetadataOk returns a tuple with the Metadata field value +// and a boolean to check if the value has been set. +func (o *Oif3009OrderDto) GetMetadataOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.Metadata, true +} + +// SetMetadata sets field value +func (o *Oif3009OrderDto) SetMetadata(v map[string]interface{}) { + o.Metadata = v +} + +func (o Oif3009OrderDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Oif3009OrderDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["type"] = o.Type + toSerialize["payload"] = o.Payload + toSerialize["metadata"] = o.Metadata + return toSerialize, nil +} + +func (o *Oif3009OrderDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "type", + "payload", + "metadata", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varOif3009OrderDto := _Oif3009OrderDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOif3009OrderDto) + + if err != nil { + return err + } + + *o = Oif3009OrderDto(varOif3009OrderDto) + + return err +} + +type NullableOif3009OrderDto struct { + value *Oif3009OrderDto + isSet bool +} + +func (v NullableOif3009OrderDto) Get() *Oif3009OrderDto { + return v.value +} + +func (v *NullableOif3009OrderDto) Set(val *Oif3009OrderDto) { + v.value = val + v.isSet = true +} + +func (v NullableOif3009OrderDto) IsSet() bool { + return v.isSet +} + +func (v *NullableOif3009OrderDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOif3009OrderDto(val *Oif3009OrderDto) *NullableOif3009OrderDto { + return &NullableOif3009OrderDto{value: val, isSet: true} +} + +func (v NullableOif3009OrderDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOif3009OrderDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_oif_escrow_order_dto.go b/api/lifiorder/model_oif_escrow_order_dto.go new file mode 100644 index 00000000..3a0c7b7b --- /dev/null +++ b/api/lifiorder/model_oif_escrow_order_dto.go @@ -0,0 +1,186 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the OifEscrowOrderDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OifEscrowOrderDto{} + +// OifEscrowOrderDto struct for OifEscrowOrderDto +type OifEscrowOrderDto struct { + // Order type identifier for escrow-based execution + Type string `json:"type"` + // EIP-712 payload for escrow order + Payload Eip712PayloadDto `json:"payload"` +} + +type _OifEscrowOrderDto OifEscrowOrderDto + +// NewOifEscrowOrderDto instantiates a new OifEscrowOrderDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewOifEscrowOrderDto(type_ string, payload Eip712PayloadDto) *OifEscrowOrderDto { + this := OifEscrowOrderDto{} + this.Type = type_ + this.Payload = payload + return &this +} + +// NewOifEscrowOrderDtoWithDefaults instantiates a new OifEscrowOrderDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewOifEscrowOrderDtoWithDefaults() *OifEscrowOrderDto { + this := OifEscrowOrderDto{} + return &this +} + +// GetType returns the Type field value +func (o *OifEscrowOrderDto) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *OifEscrowOrderDto) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *OifEscrowOrderDto) SetType(v string) { + o.Type = v +} + +// GetPayload returns the Payload field value +func (o *OifEscrowOrderDto) GetPayload() Eip712PayloadDto { + if o == nil { + var ret Eip712PayloadDto + return ret + } + + return o.Payload +} + +// GetPayloadOk returns a tuple with the Payload field value +// and a boolean to check if the value has been set. +func (o *OifEscrowOrderDto) GetPayloadOk() (*Eip712PayloadDto, bool) { + if o == nil { + return nil, false + } + return &o.Payload, true +} + +// SetPayload sets field value +func (o *OifEscrowOrderDto) SetPayload(v Eip712PayloadDto) { + o.Payload = v +} + +func (o OifEscrowOrderDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OifEscrowOrderDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["type"] = o.Type + toSerialize["payload"] = o.Payload + return toSerialize, nil +} + +func (o *OifEscrowOrderDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "type", + "payload", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varOifEscrowOrderDto := _OifEscrowOrderDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOifEscrowOrderDto) + + if err != nil { + return err + } + + *o = OifEscrowOrderDto(varOifEscrowOrderDto) + + return err +} + +type NullableOifEscrowOrderDto struct { + value *OifEscrowOrderDto + isSet bool +} + +func (v NullableOifEscrowOrderDto) Get() *OifEscrowOrderDto { + return v.value +} + +func (v *NullableOifEscrowOrderDto) Set(val *OifEscrowOrderDto) { + v.value = val + v.isSet = true +} + +func (v NullableOifEscrowOrderDto) IsSet() bool { + return v.isSet +} + +func (v *NullableOifEscrowOrderDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOifEscrowOrderDto(val *OifEscrowOrderDto) *NullableOifEscrowOrderDto { + return &NullableOifEscrowOrderDto{value: val, isSet: true} +} + +func (v NullableOifEscrowOrderDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOifEscrowOrderDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_oif_quote_dto.go b/api/lifiorder/model_oif_quote_dto.go index 1c0ca638..fcb3eef4 100644 --- a/api/lifiorder/model_oif_quote_dto.go +++ b/api/lifiorder/model_oif_quote_dto.go @@ -21,7 +21,7 @@ var _ MappedNullable = &OifQuoteDto{} // OifQuoteDto struct for OifQuoteDto type OifQuoteDto struct { - // Order details (null for quote requests) + // Order details; null for quote requests, provider-specific structure when populated Order map[string]interface{} `json:"order,omitempty"` // Quote validity timestamp in seconds ValidUntil *float32 `json:"validUntil,omitempty"` @@ -32,7 +32,7 @@ type OifQuoteDto struct { // Provider identifier Provider *string `json:"provider,omitempty"` // Informational amounts for UX/display - Preview QuotePreviewDto `json:"preview"` + Preview OifQuotePreviewDto `json:"preview"` // Failure handling policy for execution FailureHandling string `json:"failureHandling"` // Whether the quote supports partial fills @@ -47,7 +47,7 @@ type _OifQuoteDto OifQuoteDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewOifQuoteDto(preview QuotePreviewDto, failureHandling string, partialFill bool, metadata OifQuoteMetadataDto) *OifQuoteDto { +func NewOifQuoteDto(preview OifQuotePreviewDto, failureHandling string, partialFill bool, metadata OifQuoteMetadataDto) *OifQuoteDto { this := OifQuoteDto{} this.Preview = preview this.FailureHandling = failureHandling @@ -226,9 +226,9 @@ func (o *OifQuoteDto) SetProvider(v string) { } // GetPreview returns the Preview field value -func (o *OifQuoteDto) GetPreview() QuotePreviewDto { +func (o *OifQuoteDto) GetPreview() OifQuotePreviewDto { if o == nil { - var ret QuotePreviewDto + var ret OifQuotePreviewDto return ret } @@ -237,7 +237,7 @@ func (o *OifQuoteDto) GetPreview() QuotePreviewDto { // GetPreviewOk returns a tuple with the Preview field value // and a boolean to check if the value has been set. -func (o *OifQuoteDto) GetPreviewOk() (*QuotePreviewDto, bool) { +func (o *OifQuoteDto) GetPreviewOk() (*OifQuotePreviewDto, bool) { if o == nil { return nil, false } @@ -245,7 +245,7 @@ func (o *OifQuoteDto) GetPreviewOk() (*QuotePreviewDto, bool) { } // SetPreview sets field value -func (o *OifQuoteDto) SetPreview(v QuotePreviewDto) { +func (o *OifQuoteDto) SetPreview(v OifQuotePreviewDto) { o.Preview = v } diff --git a/api/lifiorder/model_oif_quote_metadata_dto.go b/api/lifiorder/model_oif_quote_metadata_dto.go index ec4c2df3..45c0d0cd 100644 --- a/api/lifiorder/model_oif_quote_metadata_dto.go +++ b/api/lifiorder/model_oif_quote_metadata_dto.go @@ -22,7 +22,7 @@ var _ MappedNullable = &OifQuoteMetadataDto{} // OifQuoteMetadataDto struct for OifQuoteMetadataDto type OifQuoteMetadataDto struct { // Solver address with exclusivity on this quote, or null when no solver is exclusive - ExclusiveFor map[string]interface{} `json:"exclusiveFor"` + ExclusiveFor NullableString `json:"exclusiveFor"` } type _OifQuoteMetadataDto OifQuoteMetadataDto @@ -31,7 +31,7 @@ type _OifQuoteMetadataDto OifQuoteMetadataDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewOifQuoteMetadataDto(exclusiveFor map[string]interface{}) *OifQuoteMetadataDto { +func NewOifQuoteMetadataDto(exclusiveFor NullableString) *OifQuoteMetadataDto { this := OifQuoteMetadataDto{} this.ExclusiveFor = exclusiveFor return &this @@ -46,29 +46,29 @@ func NewOifQuoteMetadataDtoWithDefaults() *OifQuoteMetadataDto { } // GetExclusiveFor returns the ExclusiveFor field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OifQuoteMetadataDto) GetExclusiveFor() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OifQuoteMetadataDto) GetExclusiveFor() string { + if o == nil || o.ExclusiveFor.Get() == nil { + var ret string return ret } - return o.ExclusiveFor + return *o.ExclusiveFor.Get() } // GetExclusiveForOk returns a tuple with the ExclusiveFor field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OifQuoteMetadataDto) GetExclusiveForOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.ExclusiveFor) { - return map[string]interface{}{}, false +func (o *OifQuoteMetadataDto) GetExclusiveForOk() (*string, bool) { + if o == nil { + return nil, false } - return o.ExclusiveFor, true + return o.ExclusiveFor.Get(), o.ExclusiveFor.IsSet() } // SetExclusiveFor sets field value -func (o *OifQuoteMetadataDto) SetExclusiveFor(v map[string]interface{}) { - o.ExclusiveFor = v +func (o *OifQuoteMetadataDto) SetExclusiveFor(v string) { + o.ExclusiveFor.Set(&v) } func (o OifQuoteMetadataDto) MarshalJSON() ([]byte, error) { @@ -81,9 +81,7 @@ func (o OifQuoteMetadataDto) MarshalJSON() ([]byte, error) { func (o OifQuoteMetadataDto) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} - if o.ExclusiveFor != nil { - toSerialize["exclusiveFor"] = o.ExclusiveFor - } + toSerialize["exclusiveFor"] = o.ExclusiveFor.Get() return toSerialize, nil } diff --git a/api/lifiorder/model_oif_quote_preview_dto.go b/api/lifiorder/model_oif_quote_preview_dto.go new file mode 100644 index 00000000..fc6a185c --- /dev/null +++ b/api/lifiorder/model_oif_quote_preview_dto.go @@ -0,0 +1,186 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the OifQuotePreviewDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OifQuotePreviewDto{} + +// OifQuotePreviewDto struct for OifQuotePreviewDto +type OifQuotePreviewDto struct { + // Inputs for the preview + Inputs []OifQuotePreviewDtoInputsInner `json:"inputs"` + // Outputs for the preview + Outputs []OifQuotePreviewDtoOutputsInner `json:"outputs"` +} + +type _OifQuotePreviewDto OifQuotePreviewDto + +// NewOifQuotePreviewDto instantiates a new OifQuotePreviewDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewOifQuotePreviewDto(inputs []OifQuotePreviewDtoInputsInner, outputs []OifQuotePreviewDtoOutputsInner) *OifQuotePreviewDto { + this := OifQuotePreviewDto{} + this.Inputs = inputs + this.Outputs = outputs + return &this +} + +// NewOifQuotePreviewDtoWithDefaults instantiates a new OifQuotePreviewDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewOifQuotePreviewDtoWithDefaults() *OifQuotePreviewDto { + this := OifQuotePreviewDto{} + return &this +} + +// GetInputs returns the Inputs field value +func (o *OifQuotePreviewDto) GetInputs() []OifQuotePreviewDtoInputsInner { + if o == nil { + var ret []OifQuotePreviewDtoInputsInner + return ret + } + + return o.Inputs +} + +// GetInputsOk returns a tuple with the Inputs field value +// and a boolean to check if the value has been set. +func (o *OifQuotePreviewDto) GetInputsOk() ([]OifQuotePreviewDtoInputsInner, bool) { + if o == nil { + return nil, false + } + return o.Inputs, true +} + +// SetInputs sets field value +func (o *OifQuotePreviewDto) SetInputs(v []OifQuotePreviewDtoInputsInner) { + o.Inputs = v +} + +// GetOutputs returns the Outputs field value +func (o *OifQuotePreviewDto) GetOutputs() []OifQuotePreviewDtoOutputsInner { + if o == nil { + var ret []OifQuotePreviewDtoOutputsInner + return ret + } + + return o.Outputs +} + +// GetOutputsOk returns a tuple with the Outputs field value +// and a boolean to check if the value has been set. +func (o *OifQuotePreviewDto) GetOutputsOk() ([]OifQuotePreviewDtoOutputsInner, bool) { + if o == nil { + return nil, false + } + return o.Outputs, true +} + +// SetOutputs sets field value +func (o *OifQuotePreviewDto) SetOutputs(v []OifQuotePreviewDtoOutputsInner) { + o.Outputs = v +} + +func (o OifQuotePreviewDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OifQuotePreviewDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["inputs"] = o.Inputs + toSerialize["outputs"] = o.Outputs + return toSerialize, nil +} + +func (o *OifQuotePreviewDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "inputs", + "outputs", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varOifQuotePreviewDto := _OifQuotePreviewDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOifQuotePreviewDto) + + if err != nil { + return err + } + + *o = OifQuotePreviewDto(varOifQuotePreviewDto) + + return err +} + +type NullableOifQuotePreviewDto struct { + value *OifQuotePreviewDto + isSet bool +} + +func (v NullableOifQuotePreviewDto) Get() *OifQuotePreviewDto { + return v.value +} + +func (v *NullableOifQuotePreviewDto) Set(val *OifQuotePreviewDto) { + v.value = val + v.isSet = true +} + +func (v NullableOifQuotePreviewDto) IsSet() bool { + return v.isSet +} + +func (v *NullableOifQuotePreviewDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOifQuotePreviewDto(val *OifQuotePreviewDto) *NullableOifQuotePreviewDto { + return &NullableOifQuotePreviewDto{value: val, isSet: true} +} + +func (v NullableOifQuotePreviewDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOifQuotePreviewDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_quote_preview_dto_inputs_inner.go b/api/lifiorder/model_oif_quote_preview_dto_inputs_inner.go similarity index 57% rename from api/lifiorder/model_quote_preview_dto_inputs_inner.go rename to api/lifiorder/model_oif_quote_preview_dto_inputs_inner.go index ba265001..3d4d33a9 100644 --- a/api/lifiorder/model_quote_preview_dto_inputs_inner.go +++ b/api/lifiorder/model_oif_quote_preview_dto_inputs_inner.go @@ -14,35 +14,35 @@ import ( "encoding/json" ) -// checks if the QuotePreviewDtoInputsInner type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &QuotePreviewDtoInputsInner{} +// checks if the OifQuotePreviewDtoInputsInner type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OifQuotePreviewDtoInputsInner{} -// QuotePreviewDtoInputsInner struct for QuotePreviewDtoInputsInner -type QuotePreviewDtoInputsInner struct { +// OifQuotePreviewDtoInputsInner struct for OifQuotePreviewDtoInputsInner +type OifQuotePreviewDtoInputsInner struct { User *string `json:"user,omitempty"` Asset *string `json:"asset,omitempty"` Amount *string `json:"amount,omitempty"` } -// NewQuotePreviewDtoInputsInner instantiates a new QuotePreviewDtoInputsInner object +// NewOifQuotePreviewDtoInputsInner instantiates a new OifQuotePreviewDtoInputsInner object // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewQuotePreviewDtoInputsInner() *QuotePreviewDtoInputsInner { - this := QuotePreviewDtoInputsInner{} +func NewOifQuotePreviewDtoInputsInner() *OifQuotePreviewDtoInputsInner { + this := OifQuotePreviewDtoInputsInner{} return &this } -// NewQuotePreviewDtoInputsInnerWithDefaults instantiates a new QuotePreviewDtoInputsInner object +// NewOifQuotePreviewDtoInputsInnerWithDefaults instantiates a new OifQuotePreviewDtoInputsInner object // This constructor will only assign default values to properties that have it defined, // but it doesn't guarantee that properties required by API are set -func NewQuotePreviewDtoInputsInnerWithDefaults() *QuotePreviewDtoInputsInner { - this := QuotePreviewDtoInputsInner{} +func NewOifQuotePreviewDtoInputsInnerWithDefaults() *OifQuotePreviewDtoInputsInner { + this := OifQuotePreviewDtoInputsInner{} return &this } // GetUser returns the User field value if set, zero value otherwise. -func (o *QuotePreviewDtoInputsInner) GetUser() string { +func (o *OifQuotePreviewDtoInputsInner) GetUser() string { if o == nil || IsNil(o.User) { var ret string return ret @@ -52,7 +52,7 @@ func (o *QuotePreviewDtoInputsInner) GetUser() string { // GetUserOk returns a tuple with the User field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *QuotePreviewDtoInputsInner) GetUserOk() (*string, bool) { +func (o *OifQuotePreviewDtoInputsInner) GetUserOk() (*string, bool) { if o == nil || IsNil(o.User) { return nil, false } @@ -60,7 +60,7 @@ func (o *QuotePreviewDtoInputsInner) GetUserOk() (*string, bool) { } // HasUser returns a boolean if a field has been set. -func (o *QuotePreviewDtoInputsInner) HasUser() bool { +func (o *OifQuotePreviewDtoInputsInner) HasUser() bool { if o != nil && !IsNil(o.User) { return true } @@ -69,12 +69,12 @@ func (o *QuotePreviewDtoInputsInner) HasUser() bool { } // SetUser gets a reference to the given string and assigns it to the User field. -func (o *QuotePreviewDtoInputsInner) SetUser(v string) { +func (o *OifQuotePreviewDtoInputsInner) SetUser(v string) { o.User = &v } // GetAsset returns the Asset field value if set, zero value otherwise. -func (o *QuotePreviewDtoInputsInner) GetAsset() string { +func (o *OifQuotePreviewDtoInputsInner) GetAsset() string { if o == nil || IsNil(o.Asset) { var ret string return ret @@ -84,7 +84,7 @@ func (o *QuotePreviewDtoInputsInner) GetAsset() string { // GetAssetOk returns a tuple with the Asset field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *QuotePreviewDtoInputsInner) GetAssetOk() (*string, bool) { +func (o *OifQuotePreviewDtoInputsInner) GetAssetOk() (*string, bool) { if o == nil || IsNil(o.Asset) { return nil, false } @@ -92,7 +92,7 @@ func (o *QuotePreviewDtoInputsInner) GetAssetOk() (*string, bool) { } // HasAsset returns a boolean if a field has been set. -func (o *QuotePreviewDtoInputsInner) HasAsset() bool { +func (o *OifQuotePreviewDtoInputsInner) HasAsset() bool { if o != nil && !IsNil(o.Asset) { return true } @@ -101,12 +101,12 @@ func (o *QuotePreviewDtoInputsInner) HasAsset() bool { } // SetAsset gets a reference to the given string and assigns it to the Asset field. -func (o *QuotePreviewDtoInputsInner) SetAsset(v string) { +func (o *OifQuotePreviewDtoInputsInner) SetAsset(v string) { o.Asset = &v } // GetAmount returns the Amount field value if set, zero value otherwise. -func (o *QuotePreviewDtoInputsInner) GetAmount() string { +func (o *OifQuotePreviewDtoInputsInner) GetAmount() string { if o == nil || IsNil(o.Amount) { var ret string return ret @@ -116,7 +116,7 @@ func (o *QuotePreviewDtoInputsInner) GetAmount() string { // GetAmountOk returns a tuple with the Amount field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *QuotePreviewDtoInputsInner) GetAmountOk() (*string, bool) { +func (o *OifQuotePreviewDtoInputsInner) GetAmountOk() (*string, bool) { if o == nil || IsNil(o.Amount) { return nil, false } @@ -124,7 +124,7 @@ func (o *QuotePreviewDtoInputsInner) GetAmountOk() (*string, bool) { } // HasAmount returns a boolean if a field has been set. -func (o *QuotePreviewDtoInputsInner) HasAmount() bool { +func (o *OifQuotePreviewDtoInputsInner) HasAmount() bool { if o != nil && !IsNil(o.Amount) { return true } @@ -133,11 +133,11 @@ func (o *QuotePreviewDtoInputsInner) HasAmount() bool { } // SetAmount gets a reference to the given string and assigns it to the Amount field. -func (o *QuotePreviewDtoInputsInner) SetAmount(v string) { +func (o *OifQuotePreviewDtoInputsInner) SetAmount(v string) { o.Amount = &v } -func (o QuotePreviewDtoInputsInner) MarshalJSON() ([]byte, error) { +func (o OifQuotePreviewDtoInputsInner) MarshalJSON() ([]byte, error) { toSerialize, err := o.ToMap() if err != nil { return []byte{}, err @@ -145,7 +145,7 @@ func (o QuotePreviewDtoInputsInner) MarshalJSON() ([]byte, error) { return json.Marshal(toSerialize) } -func (o QuotePreviewDtoInputsInner) ToMap() (map[string]interface{}, error) { +func (o OifQuotePreviewDtoInputsInner) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} if !IsNil(o.User) { toSerialize["user"] = o.User @@ -159,38 +159,38 @@ func (o QuotePreviewDtoInputsInner) ToMap() (map[string]interface{}, error) { return toSerialize, nil } -type NullableQuotePreviewDtoInputsInner struct { - value *QuotePreviewDtoInputsInner +type NullableOifQuotePreviewDtoInputsInner struct { + value *OifQuotePreviewDtoInputsInner isSet bool } -func (v NullableQuotePreviewDtoInputsInner) Get() *QuotePreviewDtoInputsInner { +func (v NullableOifQuotePreviewDtoInputsInner) Get() *OifQuotePreviewDtoInputsInner { return v.value } -func (v *NullableQuotePreviewDtoInputsInner) Set(val *QuotePreviewDtoInputsInner) { +func (v *NullableOifQuotePreviewDtoInputsInner) Set(val *OifQuotePreviewDtoInputsInner) { v.value = val v.isSet = true } -func (v NullableQuotePreviewDtoInputsInner) IsSet() bool { +func (v NullableOifQuotePreviewDtoInputsInner) IsSet() bool { return v.isSet } -func (v *NullableQuotePreviewDtoInputsInner) Unset() { +func (v *NullableOifQuotePreviewDtoInputsInner) Unset() { v.value = nil v.isSet = false } -func NewNullableQuotePreviewDtoInputsInner(val *QuotePreviewDtoInputsInner) *NullableQuotePreviewDtoInputsInner { - return &NullableQuotePreviewDtoInputsInner{value: val, isSet: true} +func NewNullableOifQuotePreviewDtoInputsInner(val *OifQuotePreviewDtoInputsInner) *NullableOifQuotePreviewDtoInputsInner { + return &NullableOifQuotePreviewDtoInputsInner{value: val, isSet: true} } -func (v NullableQuotePreviewDtoInputsInner) MarshalJSON() ([]byte, error) { +func (v NullableOifQuotePreviewDtoInputsInner) MarshalJSON() ([]byte, error) { return json.Marshal(v.value) } -func (v *NullableQuotePreviewDtoInputsInner) UnmarshalJSON(src []byte) error { +func (v *NullableOifQuotePreviewDtoInputsInner) UnmarshalJSON(src []byte) error { v.isSet = true return json.Unmarshal(src, &v.value) } diff --git a/api/lifiorder/model_quote_preview_dto_outputs_inner.go b/api/lifiorder/model_oif_quote_preview_dto_outputs_inner.go similarity index 57% rename from api/lifiorder/model_quote_preview_dto_outputs_inner.go rename to api/lifiorder/model_oif_quote_preview_dto_outputs_inner.go index b5f2ecd0..f986d5fc 100644 --- a/api/lifiorder/model_quote_preview_dto_outputs_inner.go +++ b/api/lifiorder/model_oif_quote_preview_dto_outputs_inner.go @@ -14,35 +14,35 @@ import ( "encoding/json" ) -// checks if the QuotePreviewDtoOutputsInner type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &QuotePreviewDtoOutputsInner{} +// checks if the OifQuotePreviewDtoOutputsInner type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OifQuotePreviewDtoOutputsInner{} -// QuotePreviewDtoOutputsInner struct for QuotePreviewDtoOutputsInner -type QuotePreviewDtoOutputsInner struct { +// OifQuotePreviewDtoOutputsInner struct for OifQuotePreviewDtoOutputsInner +type OifQuotePreviewDtoOutputsInner struct { Receiver *string `json:"receiver,omitempty"` Asset *string `json:"asset,omitempty"` Amount *string `json:"amount,omitempty"` } -// NewQuotePreviewDtoOutputsInner instantiates a new QuotePreviewDtoOutputsInner object +// NewOifQuotePreviewDtoOutputsInner instantiates a new OifQuotePreviewDtoOutputsInner object // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewQuotePreviewDtoOutputsInner() *QuotePreviewDtoOutputsInner { - this := QuotePreviewDtoOutputsInner{} +func NewOifQuotePreviewDtoOutputsInner() *OifQuotePreviewDtoOutputsInner { + this := OifQuotePreviewDtoOutputsInner{} return &this } -// NewQuotePreviewDtoOutputsInnerWithDefaults instantiates a new QuotePreviewDtoOutputsInner object +// NewOifQuotePreviewDtoOutputsInnerWithDefaults instantiates a new OifQuotePreviewDtoOutputsInner object // This constructor will only assign default values to properties that have it defined, // but it doesn't guarantee that properties required by API are set -func NewQuotePreviewDtoOutputsInnerWithDefaults() *QuotePreviewDtoOutputsInner { - this := QuotePreviewDtoOutputsInner{} +func NewOifQuotePreviewDtoOutputsInnerWithDefaults() *OifQuotePreviewDtoOutputsInner { + this := OifQuotePreviewDtoOutputsInner{} return &this } // GetReceiver returns the Receiver field value if set, zero value otherwise. -func (o *QuotePreviewDtoOutputsInner) GetReceiver() string { +func (o *OifQuotePreviewDtoOutputsInner) GetReceiver() string { if o == nil || IsNil(o.Receiver) { var ret string return ret @@ -52,7 +52,7 @@ func (o *QuotePreviewDtoOutputsInner) GetReceiver() string { // GetReceiverOk returns a tuple with the Receiver field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *QuotePreviewDtoOutputsInner) GetReceiverOk() (*string, bool) { +func (o *OifQuotePreviewDtoOutputsInner) GetReceiverOk() (*string, bool) { if o == nil || IsNil(o.Receiver) { return nil, false } @@ -60,7 +60,7 @@ func (o *QuotePreviewDtoOutputsInner) GetReceiverOk() (*string, bool) { } // HasReceiver returns a boolean if a field has been set. -func (o *QuotePreviewDtoOutputsInner) HasReceiver() bool { +func (o *OifQuotePreviewDtoOutputsInner) HasReceiver() bool { if o != nil && !IsNil(o.Receiver) { return true } @@ -69,12 +69,12 @@ func (o *QuotePreviewDtoOutputsInner) HasReceiver() bool { } // SetReceiver gets a reference to the given string and assigns it to the Receiver field. -func (o *QuotePreviewDtoOutputsInner) SetReceiver(v string) { +func (o *OifQuotePreviewDtoOutputsInner) SetReceiver(v string) { o.Receiver = &v } // GetAsset returns the Asset field value if set, zero value otherwise. -func (o *QuotePreviewDtoOutputsInner) GetAsset() string { +func (o *OifQuotePreviewDtoOutputsInner) GetAsset() string { if o == nil || IsNil(o.Asset) { var ret string return ret @@ -84,7 +84,7 @@ func (o *QuotePreviewDtoOutputsInner) GetAsset() string { // GetAssetOk returns a tuple with the Asset field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *QuotePreviewDtoOutputsInner) GetAssetOk() (*string, bool) { +func (o *OifQuotePreviewDtoOutputsInner) GetAssetOk() (*string, bool) { if o == nil || IsNil(o.Asset) { return nil, false } @@ -92,7 +92,7 @@ func (o *QuotePreviewDtoOutputsInner) GetAssetOk() (*string, bool) { } // HasAsset returns a boolean if a field has been set. -func (o *QuotePreviewDtoOutputsInner) HasAsset() bool { +func (o *OifQuotePreviewDtoOutputsInner) HasAsset() bool { if o != nil && !IsNil(o.Asset) { return true } @@ -101,12 +101,12 @@ func (o *QuotePreviewDtoOutputsInner) HasAsset() bool { } // SetAsset gets a reference to the given string and assigns it to the Asset field. -func (o *QuotePreviewDtoOutputsInner) SetAsset(v string) { +func (o *OifQuotePreviewDtoOutputsInner) SetAsset(v string) { o.Asset = &v } // GetAmount returns the Amount field value if set, zero value otherwise. -func (o *QuotePreviewDtoOutputsInner) GetAmount() string { +func (o *OifQuotePreviewDtoOutputsInner) GetAmount() string { if o == nil || IsNil(o.Amount) { var ret string return ret @@ -116,7 +116,7 @@ func (o *QuotePreviewDtoOutputsInner) GetAmount() string { // GetAmountOk returns a tuple with the Amount field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *QuotePreviewDtoOutputsInner) GetAmountOk() (*string, bool) { +func (o *OifQuotePreviewDtoOutputsInner) GetAmountOk() (*string, bool) { if o == nil || IsNil(o.Amount) { return nil, false } @@ -124,7 +124,7 @@ func (o *QuotePreviewDtoOutputsInner) GetAmountOk() (*string, bool) { } // HasAmount returns a boolean if a field has been set. -func (o *QuotePreviewDtoOutputsInner) HasAmount() bool { +func (o *OifQuotePreviewDtoOutputsInner) HasAmount() bool { if o != nil && !IsNil(o.Amount) { return true } @@ -133,11 +133,11 @@ func (o *QuotePreviewDtoOutputsInner) HasAmount() bool { } // SetAmount gets a reference to the given string and assigns it to the Amount field. -func (o *QuotePreviewDtoOutputsInner) SetAmount(v string) { +func (o *OifQuotePreviewDtoOutputsInner) SetAmount(v string) { o.Amount = &v } -func (o QuotePreviewDtoOutputsInner) MarshalJSON() ([]byte, error) { +func (o OifQuotePreviewDtoOutputsInner) MarshalJSON() ([]byte, error) { toSerialize, err := o.ToMap() if err != nil { return []byte{}, err @@ -145,7 +145,7 @@ func (o QuotePreviewDtoOutputsInner) MarshalJSON() ([]byte, error) { return json.Marshal(toSerialize) } -func (o QuotePreviewDtoOutputsInner) ToMap() (map[string]interface{}, error) { +func (o OifQuotePreviewDtoOutputsInner) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} if !IsNil(o.Receiver) { toSerialize["receiver"] = o.Receiver @@ -159,38 +159,38 @@ func (o QuotePreviewDtoOutputsInner) ToMap() (map[string]interface{}, error) { return toSerialize, nil } -type NullableQuotePreviewDtoOutputsInner struct { - value *QuotePreviewDtoOutputsInner +type NullableOifQuotePreviewDtoOutputsInner struct { + value *OifQuotePreviewDtoOutputsInner isSet bool } -func (v NullableQuotePreviewDtoOutputsInner) Get() *QuotePreviewDtoOutputsInner { +func (v NullableOifQuotePreviewDtoOutputsInner) Get() *OifQuotePreviewDtoOutputsInner { return v.value } -func (v *NullableQuotePreviewDtoOutputsInner) Set(val *QuotePreviewDtoOutputsInner) { +func (v *NullableOifQuotePreviewDtoOutputsInner) Set(val *OifQuotePreviewDtoOutputsInner) { v.value = val v.isSet = true } -func (v NullableQuotePreviewDtoOutputsInner) IsSet() bool { +func (v NullableOifQuotePreviewDtoOutputsInner) IsSet() bool { return v.isSet } -func (v *NullableQuotePreviewDtoOutputsInner) Unset() { +func (v *NullableOifQuotePreviewDtoOutputsInner) Unset() { v.value = nil v.isSet = false } -func NewNullableQuotePreviewDtoOutputsInner(val *QuotePreviewDtoOutputsInner) *NullableQuotePreviewDtoOutputsInner { - return &NullableQuotePreviewDtoOutputsInner{value: val, isSet: true} +func NewNullableOifQuotePreviewDtoOutputsInner(val *OifQuotePreviewDtoOutputsInner) *NullableOifQuotePreviewDtoOutputsInner { + return &NullableOifQuotePreviewDtoOutputsInner{value: val, isSet: true} } -func (v NullableQuotePreviewDtoOutputsInner) MarshalJSON() ([]byte, error) { +func (v NullableOifQuotePreviewDtoOutputsInner) MarshalJSON() ([]byte, error) { return json.Marshal(v.value) } -func (v *NullableQuotePreviewDtoOutputsInner) UnmarshalJSON(src []byte) error { +func (v *NullableOifQuotePreviewDtoOutputsInner) UnmarshalJSON(src []byte) error { v.isSet = true return json.Unmarshal(src, &v.value) } diff --git a/api/lifiorder/model_oif_user_open_intent_order_dto.go b/api/lifiorder/model_oif_user_open_intent_order_dto.go new file mode 100644 index 00000000..34f14107 --- /dev/null +++ b/api/lifiorder/model_oif_user_open_intent_order_dto.go @@ -0,0 +1,214 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the OifUserOpenIntentOrderDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OifUserOpenIntentOrderDto{} + +// OifUserOpenIntentOrderDto struct for OifUserOpenIntentOrderDto +type OifUserOpenIntentOrderDto struct { + // Order type identifier for user open intent execution + Type string `json:"type"` + OpenIntentTx OifUserOpenIntentOrderDtoOpenIntentTx `json:"openIntentTx"` + // Allowance and balance checks that must hold prior to execution. For Solana origins this array is empty; SPL transfers happen inside the open instruction. + Checks ChecksDto `json:"checks"` +} + +type _OifUserOpenIntentOrderDto OifUserOpenIntentOrderDto + +// NewOifUserOpenIntentOrderDto instantiates a new OifUserOpenIntentOrderDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewOifUserOpenIntentOrderDto(type_ string, openIntentTx OifUserOpenIntentOrderDtoOpenIntentTx, checks ChecksDto) *OifUserOpenIntentOrderDto { + this := OifUserOpenIntentOrderDto{} + this.Type = type_ + this.OpenIntentTx = openIntentTx + this.Checks = checks + return &this +} + +// NewOifUserOpenIntentOrderDtoWithDefaults instantiates a new OifUserOpenIntentOrderDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewOifUserOpenIntentOrderDtoWithDefaults() *OifUserOpenIntentOrderDto { + this := OifUserOpenIntentOrderDto{} + return &this +} + +// GetType returns the Type field value +func (o *OifUserOpenIntentOrderDto) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *OifUserOpenIntentOrderDto) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *OifUserOpenIntentOrderDto) SetType(v string) { + o.Type = v +} + +// GetOpenIntentTx returns the OpenIntentTx field value +func (o *OifUserOpenIntentOrderDto) GetOpenIntentTx() OifUserOpenIntentOrderDtoOpenIntentTx { + if o == nil { + var ret OifUserOpenIntentOrderDtoOpenIntentTx + return ret + } + + return o.OpenIntentTx +} + +// GetOpenIntentTxOk returns a tuple with the OpenIntentTx field value +// and a boolean to check if the value has been set. +func (o *OifUserOpenIntentOrderDto) GetOpenIntentTxOk() (*OifUserOpenIntentOrderDtoOpenIntentTx, bool) { + if o == nil { + return nil, false + } + return &o.OpenIntentTx, true +} + +// SetOpenIntentTx sets field value +func (o *OifUserOpenIntentOrderDto) SetOpenIntentTx(v OifUserOpenIntentOrderDtoOpenIntentTx) { + o.OpenIntentTx = v +} + +// GetChecks returns the Checks field value +func (o *OifUserOpenIntentOrderDto) GetChecks() ChecksDto { + if o == nil { + var ret ChecksDto + return ret + } + + return o.Checks +} + +// GetChecksOk returns a tuple with the Checks field value +// and a boolean to check if the value has been set. +func (o *OifUserOpenIntentOrderDto) GetChecksOk() (*ChecksDto, bool) { + if o == nil { + return nil, false + } + return &o.Checks, true +} + +// SetChecks sets field value +func (o *OifUserOpenIntentOrderDto) SetChecks(v ChecksDto) { + o.Checks = v +} + +func (o OifUserOpenIntentOrderDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OifUserOpenIntentOrderDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["type"] = o.Type + toSerialize["openIntentTx"] = o.OpenIntentTx + toSerialize["checks"] = o.Checks + return toSerialize, nil +} + +func (o *OifUserOpenIntentOrderDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "type", + "openIntentTx", + "checks", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varOifUserOpenIntentOrderDto := _OifUserOpenIntentOrderDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOifUserOpenIntentOrderDto) + + if err != nil { + return err + } + + *o = OifUserOpenIntentOrderDto(varOifUserOpenIntentOrderDto) + + return err +} + +type NullableOifUserOpenIntentOrderDto struct { + value *OifUserOpenIntentOrderDto + isSet bool +} + +func (v NullableOifUserOpenIntentOrderDto) Get() *OifUserOpenIntentOrderDto { + return v.value +} + +func (v *NullableOifUserOpenIntentOrderDto) Set(val *OifUserOpenIntentOrderDto) { + v.value = val + v.isSet = true +} + +func (v NullableOifUserOpenIntentOrderDto) IsSet() bool { + return v.isSet +} + +func (v *NullableOifUserOpenIntentOrderDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOifUserOpenIntentOrderDto(val *OifUserOpenIntentOrderDto) *NullableOifUserOpenIntentOrderDto { + return &NullableOifUserOpenIntentOrderDto{value: val, isSet: true} +} + +func (v NullableOifUserOpenIntentOrderDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOifUserOpenIntentOrderDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_oif_user_open_intent_order_dto_open_intent_tx.go b/api/lifiorder/model_oif_user_open_intent_order_dto_open_intent_tx.go new file mode 100644 index 00000000..edc666d5 --- /dev/null +++ b/api/lifiorder/model_oif_user_open_intent_order_dto_open_intent_tx.go @@ -0,0 +1,206 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "encoding/json" + "fmt" + "gopkg.in/validator.v2" +) + +// OifUserOpenIntentOrderDtoOpenIntentTx - Open intent transaction. EVM produces hex calldata; Solana produces a base58 serialized VersionedTransaction whose recentBlockhash must be overwritten before signing; Tron produces hex calldata the client wraps in a TriggerSmartContract envelope. +type OifUserOpenIntentOrderDtoOpenIntentTx struct { + OpenIntentEvmTxDto *OpenIntentEvmTxDto + OpenIntentSvmTxDto *OpenIntentSvmTxDto + OpenIntentTronTxDto *OpenIntentTronTxDto +} + +// OpenIntentEvmTxDtoAsOifUserOpenIntentOrderDtoOpenIntentTx is a convenience function that returns OpenIntentEvmTxDto wrapped in OifUserOpenIntentOrderDtoOpenIntentTx +func OpenIntentEvmTxDtoAsOifUserOpenIntentOrderDtoOpenIntentTx(v *OpenIntentEvmTxDto) OifUserOpenIntentOrderDtoOpenIntentTx { + return OifUserOpenIntentOrderDtoOpenIntentTx{ + OpenIntentEvmTxDto: v, + } +} + +// OpenIntentSvmTxDtoAsOifUserOpenIntentOrderDtoOpenIntentTx is a convenience function that returns OpenIntentSvmTxDto wrapped in OifUserOpenIntentOrderDtoOpenIntentTx +func OpenIntentSvmTxDtoAsOifUserOpenIntentOrderDtoOpenIntentTx(v *OpenIntentSvmTxDto) OifUserOpenIntentOrderDtoOpenIntentTx { + return OifUserOpenIntentOrderDtoOpenIntentTx{ + OpenIntentSvmTxDto: v, + } +} + +// OpenIntentTronTxDtoAsOifUserOpenIntentOrderDtoOpenIntentTx is a convenience function that returns OpenIntentTronTxDto wrapped in OifUserOpenIntentOrderDtoOpenIntentTx +func OpenIntentTronTxDtoAsOifUserOpenIntentOrderDtoOpenIntentTx(v *OpenIntentTronTxDto) OifUserOpenIntentOrderDtoOpenIntentTx { + return OifUserOpenIntentOrderDtoOpenIntentTx{ + OpenIntentTronTxDto: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *OifUserOpenIntentOrderDtoOpenIntentTx) UnmarshalJSON(data []byte) error { + var err error + match := 0 + // try to unmarshal data into OpenIntentEvmTxDto + err = newStrictDecoder(data).Decode(&dst.OpenIntentEvmTxDto) + if err == nil { + jsonOpenIntentEvmTxDto, _ := json.Marshal(dst.OpenIntentEvmTxDto) + if string(jsonOpenIntentEvmTxDto) == "{}" { // empty struct + dst.OpenIntentEvmTxDto = nil + } else { + if err = validator.Validate(dst.OpenIntentEvmTxDto); err != nil { + dst.OpenIntentEvmTxDto = nil + } else { + match++ + } + } + } else { + dst.OpenIntentEvmTxDto = nil + } + + // try to unmarshal data into OpenIntentSvmTxDto + err = newStrictDecoder(data).Decode(&dst.OpenIntentSvmTxDto) + if err == nil { + jsonOpenIntentSvmTxDto, _ := json.Marshal(dst.OpenIntentSvmTxDto) + if string(jsonOpenIntentSvmTxDto) == "{}" { // empty struct + dst.OpenIntentSvmTxDto = nil + } else { + if err = validator.Validate(dst.OpenIntentSvmTxDto); err != nil { + dst.OpenIntentSvmTxDto = nil + } else { + match++ + } + } + } else { + dst.OpenIntentSvmTxDto = nil + } + + // try to unmarshal data into OpenIntentTronTxDto + err = newStrictDecoder(data).Decode(&dst.OpenIntentTronTxDto) + if err == nil { + jsonOpenIntentTronTxDto, _ := json.Marshal(dst.OpenIntentTronTxDto) + if string(jsonOpenIntentTronTxDto) == "{}" { // empty struct + dst.OpenIntentTronTxDto = nil + } else { + if err = validator.Validate(dst.OpenIntentTronTxDto); err != nil { + dst.OpenIntentTronTxDto = nil + } else { + match++ + } + } + } else { + dst.OpenIntentTronTxDto = nil + } + + if match > 1 { // more than 1 match + // reset to nil + dst.OpenIntentEvmTxDto = nil + dst.OpenIntentSvmTxDto = nil + dst.OpenIntentTronTxDto = nil + + return fmt.Errorf("data matches more than one schema in oneOf(OifUserOpenIntentOrderDtoOpenIntentTx)") + } else if match == 1 { + return nil // exactly one match + } else { // no match + return fmt.Errorf("data failed to match schemas in oneOf(OifUserOpenIntentOrderDtoOpenIntentTx)") + } +} + +// Marshal data from the first non-nil pointers in the struct to JSON +func (src OifUserOpenIntentOrderDtoOpenIntentTx) MarshalJSON() ([]byte, error) { + if src.OpenIntentEvmTxDto != nil { + return json.Marshal(&src.OpenIntentEvmTxDto) + } + + if src.OpenIntentSvmTxDto != nil { + return json.Marshal(&src.OpenIntentSvmTxDto) + } + + if src.OpenIntentTronTxDto != nil { + return json.Marshal(&src.OpenIntentTronTxDto) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *OifUserOpenIntentOrderDtoOpenIntentTx) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.OpenIntentEvmTxDto != nil { + return obj.OpenIntentEvmTxDto + } + + if obj.OpenIntentSvmTxDto != nil { + return obj.OpenIntentSvmTxDto + } + + if obj.OpenIntentTronTxDto != nil { + return obj.OpenIntentTronTxDto + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj OifUserOpenIntentOrderDtoOpenIntentTx) GetActualInstanceValue() interface{} { + if obj.OpenIntentEvmTxDto != nil { + return *obj.OpenIntentEvmTxDto + } + + if obj.OpenIntentSvmTxDto != nil { + return *obj.OpenIntentSvmTxDto + } + + if obj.OpenIntentTronTxDto != nil { + return *obj.OpenIntentTronTxDto + } + + // all schemas are nil + return nil +} + +type NullableOifUserOpenIntentOrderDtoOpenIntentTx struct { + value *OifUserOpenIntentOrderDtoOpenIntentTx + isSet bool +} + +func (v NullableOifUserOpenIntentOrderDtoOpenIntentTx) Get() *OifUserOpenIntentOrderDtoOpenIntentTx { + return v.value +} + +func (v *NullableOifUserOpenIntentOrderDtoOpenIntentTx) Set(val *OifUserOpenIntentOrderDtoOpenIntentTx) { + v.value = val + v.isSet = true +} + +func (v NullableOifUserOpenIntentOrderDtoOpenIntentTx) IsSet() bool { + return v.isSet +} + +func (v *NullableOifUserOpenIntentOrderDtoOpenIntentTx) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOifUserOpenIntentOrderDtoOpenIntentTx(val *OifUserOpenIntentOrderDtoOpenIntentTx) *NullableOifUserOpenIntentOrderDtoOpenIntentTx { + return &NullableOifUserOpenIntentOrderDtoOpenIntentTx{value: val, isSet: true} +} + +func (v NullableOifUserOpenIntentOrderDtoOpenIntentTx) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOifUserOpenIntentOrderDtoOpenIntentTx) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_open_intent_evm_tx_dto.go b/api/lifiorder/model_open_intent_evm_tx_dto.go new file mode 100644 index 00000000..9fec31fb --- /dev/null +++ b/api/lifiorder/model_open_intent_evm_tx_dto.go @@ -0,0 +1,244 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the OpenIntentEvmTxDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OpenIntentEvmTxDto{} + +// OpenIntentEvmTxDto struct for OpenIntentEvmTxDto +type OpenIntentEvmTxDto struct { + // CAIP-2 chain identifier for the destination contract + Chain string `json:"chain"` + // Destination contract address (checksummed hex) + To string `json:"to"` + // Transaction calldata as hex string + Data string `json:"data"` + // Gas required for execution as a decimal string + GasRequired string `json:"gasRequired"` +} + +type _OpenIntentEvmTxDto OpenIntentEvmTxDto + +// NewOpenIntentEvmTxDto instantiates a new OpenIntentEvmTxDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewOpenIntentEvmTxDto(chain string, to string, data string, gasRequired string) *OpenIntentEvmTxDto { + this := OpenIntentEvmTxDto{} + this.Chain = chain + this.To = to + this.Data = data + this.GasRequired = gasRequired + return &this +} + +// NewOpenIntentEvmTxDtoWithDefaults instantiates a new OpenIntentEvmTxDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewOpenIntentEvmTxDtoWithDefaults() *OpenIntentEvmTxDto { + this := OpenIntentEvmTxDto{} + return &this +} + +// GetChain returns the Chain field value +func (o *OpenIntentEvmTxDto) GetChain() string { + if o == nil { + var ret string + return ret + } + + return o.Chain +} + +// GetChainOk returns a tuple with the Chain field value +// and a boolean to check if the value has been set. +func (o *OpenIntentEvmTxDto) GetChainOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Chain, true +} + +// SetChain sets field value +func (o *OpenIntentEvmTxDto) SetChain(v string) { + o.Chain = v +} + +// GetTo returns the To field value +func (o *OpenIntentEvmTxDto) GetTo() string { + if o == nil { + var ret string + return ret + } + + return o.To +} + +// GetToOk returns a tuple with the To field value +// and a boolean to check if the value has been set. +func (o *OpenIntentEvmTxDto) GetToOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.To, true +} + +// SetTo sets field value +func (o *OpenIntentEvmTxDto) SetTo(v string) { + o.To = v +} + +// GetData returns the Data field value +func (o *OpenIntentEvmTxDto) GetData() string { + if o == nil { + var ret string + return ret + } + + return o.Data +} + +// GetDataOk returns a tuple with the Data field value +// and a boolean to check if the value has been set. +func (o *OpenIntentEvmTxDto) GetDataOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Data, true +} + +// SetData sets field value +func (o *OpenIntentEvmTxDto) SetData(v string) { + o.Data = v +} + +// GetGasRequired returns the GasRequired field value +func (o *OpenIntentEvmTxDto) GetGasRequired() string { + if o == nil { + var ret string + return ret + } + + return o.GasRequired +} + +// GetGasRequiredOk returns a tuple with the GasRequired field value +// and a boolean to check if the value has been set. +func (o *OpenIntentEvmTxDto) GetGasRequiredOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.GasRequired, true +} + +// SetGasRequired sets field value +func (o *OpenIntentEvmTxDto) SetGasRequired(v string) { + o.GasRequired = v +} + +func (o OpenIntentEvmTxDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OpenIntentEvmTxDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["chain"] = o.Chain + toSerialize["to"] = o.To + toSerialize["data"] = o.Data + toSerialize["gasRequired"] = o.GasRequired + return toSerialize, nil +} + +func (o *OpenIntentEvmTxDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "chain", + "to", + "data", + "gasRequired", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varOpenIntentEvmTxDto := _OpenIntentEvmTxDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOpenIntentEvmTxDto) + + if err != nil { + return err + } + + *o = OpenIntentEvmTxDto(varOpenIntentEvmTxDto) + + return err +} + +type NullableOpenIntentEvmTxDto struct { + value *OpenIntentEvmTxDto + isSet bool +} + +func (v NullableOpenIntentEvmTxDto) Get() *OpenIntentEvmTxDto { + return v.value +} + +func (v *NullableOpenIntentEvmTxDto) Set(val *OpenIntentEvmTxDto) { + v.value = val + v.isSet = true +} + +func (v NullableOpenIntentEvmTxDto) IsSet() bool { + return v.isSet +} + +func (v *NullableOpenIntentEvmTxDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOpenIntentEvmTxDto(val *OpenIntentEvmTxDto) *NullableOpenIntentEvmTxDto { + return &NullableOpenIntentEvmTxDto{value: val, isSet: true} +} + +func (v NullableOpenIntentEvmTxDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOpenIntentEvmTxDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_open_intent_svm_tx_dto.go b/api/lifiorder/model_open_intent_svm_tx_dto.go new file mode 100644 index 00000000..b0bd2fc2 --- /dev/null +++ b/api/lifiorder/model_open_intent_svm_tx_dto.go @@ -0,0 +1,244 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the OpenIntentSvmTxDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OpenIntentSvmTxDto{} + +// OpenIntentSvmTxDto struct for OpenIntentSvmTxDto +type OpenIntentSvmTxDto struct { + // CAIP-2 chain identifier (Solana namespace) + Chain string `json:"chain"` + // Input settler program ID (base58) + To string `json:"to"` + // Base58-encoded serialized VersionedTransaction. The dummy all-zeros recentBlockhash must be replaced with a fresh blockhash before signing. + Data string `json:"data"` + // Estimated compute units (decimal string) + ComputeUnitsRequired string `json:"computeUnitsRequired"` +} + +type _OpenIntentSvmTxDto OpenIntentSvmTxDto + +// NewOpenIntentSvmTxDto instantiates a new OpenIntentSvmTxDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewOpenIntentSvmTxDto(chain string, to string, data string, computeUnitsRequired string) *OpenIntentSvmTxDto { + this := OpenIntentSvmTxDto{} + this.Chain = chain + this.To = to + this.Data = data + this.ComputeUnitsRequired = computeUnitsRequired + return &this +} + +// NewOpenIntentSvmTxDtoWithDefaults instantiates a new OpenIntentSvmTxDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewOpenIntentSvmTxDtoWithDefaults() *OpenIntentSvmTxDto { + this := OpenIntentSvmTxDto{} + return &this +} + +// GetChain returns the Chain field value +func (o *OpenIntentSvmTxDto) GetChain() string { + if o == nil { + var ret string + return ret + } + + return o.Chain +} + +// GetChainOk returns a tuple with the Chain field value +// and a boolean to check if the value has been set. +func (o *OpenIntentSvmTxDto) GetChainOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Chain, true +} + +// SetChain sets field value +func (o *OpenIntentSvmTxDto) SetChain(v string) { + o.Chain = v +} + +// GetTo returns the To field value +func (o *OpenIntentSvmTxDto) GetTo() string { + if o == nil { + var ret string + return ret + } + + return o.To +} + +// GetToOk returns a tuple with the To field value +// and a boolean to check if the value has been set. +func (o *OpenIntentSvmTxDto) GetToOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.To, true +} + +// SetTo sets field value +func (o *OpenIntentSvmTxDto) SetTo(v string) { + o.To = v +} + +// GetData returns the Data field value +func (o *OpenIntentSvmTxDto) GetData() string { + if o == nil { + var ret string + return ret + } + + return o.Data +} + +// GetDataOk returns a tuple with the Data field value +// and a boolean to check if the value has been set. +func (o *OpenIntentSvmTxDto) GetDataOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Data, true +} + +// SetData sets field value +func (o *OpenIntentSvmTxDto) SetData(v string) { + o.Data = v +} + +// GetComputeUnitsRequired returns the ComputeUnitsRequired field value +func (o *OpenIntentSvmTxDto) GetComputeUnitsRequired() string { + if o == nil { + var ret string + return ret + } + + return o.ComputeUnitsRequired +} + +// GetComputeUnitsRequiredOk returns a tuple with the ComputeUnitsRequired field value +// and a boolean to check if the value has been set. +func (o *OpenIntentSvmTxDto) GetComputeUnitsRequiredOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.ComputeUnitsRequired, true +} + +// SetComputeUnitsRequired sets field value +func (o *OpenIntentSvmTxDto) SetComputeUnitsRequired(v string) { + o.ComputeUnitsRequired = v +} + +func (o OpenIntentSvmTxDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OpenIntentSvmTxDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["chain"] = o.Chain + toSerialize["to"] = o.To + toSerialize["data"] = o.Data + toSerialize["computeUnitsRequired"] = o.ComputeUnitsRequired + return toSerialize, nil +} + +func (o *OpenIntentSvmTxDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "chain", + "to", + "data", + "computeUnitsRequired", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varOpenIntentSvmTxDto := _OpenIntentSvmTxDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOpenIntentSvmTxDto) + + if err != nil { + return err + } + + *o = OpenIntentSvmTxDto(varOpenIntentSvmTxDto) + + return err +} + +type NullableOpenIntentSvmTxDto struct { + value *OpenIntentSvmTxDto + isSet bool +} + +func (v NullableOpenIntentSvmTxDto) Get() *OpenIntentSvmTxDto { + return v.value +} + +func (v *NullableOpenIntentSvmTxDto) Set(val *OpenIntentSvmTxDto) { + v.value = val + v.isSet = true +} + +func (v NullableOpenIntentSvmTxDto) IsSet() bool { + return v.isSet +} + +func (v *NullableOpenIntentSvmTxDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOpenIntentSvmTxDto(val *OpenIntentSvmTxDto) *NullableOpenIntentSvmTxDto { + return &NullableOpenIntentSvmTxDto{value: val, isSet: true} +} + +func (v NullableOpenIntentSvmTxDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOpenIntentSvmTxDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_open_intent_tron_tx_dto.go b/api/lifiorder/model_open_intent_tron_tx_dto.go new file mode 100644 index 00000000..a13ef04b --- /dev/null +++ b/api/lifiorder/model_open_intent_tron_tx_dto.go @@ -0,0 +1,244 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the OpenIntentTronTxDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OpenIntentTronTxDto{} + +// OpenIntentTronTxDto struct for OpenIntentTronTxDto +type OpenIntentTronTxDto struct { + // CAIP-2 chain identifier (Tron namespace) + Chain string `json:"chain"` + // Input settler contract address (base58check) + To string `json:"to"` + // Full ABI calldata (selector + args) as a 0x-prefixed hex string. Pass it as `data` (without the 0x prefix) to the fullnode HTTP endpoint wallet/triggersmartcontract, or with tronweb 6.x as `triggerSmartContract(to, \"\", { feeLimit, input: data }, [], owner)`. + Data string `json:"data"` + // Suggested fee_limit in SUN as a decimal string. A cap on energy spend, not an estimate. + FeeLimit string `json:"feeLimit"` +} + +type _OpenIntentTronTxDto OpenIntentTronTxDto + +// NewOpenIntentTronTxDto instantiates a new OpenIntentTronTxDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewOpenIntentTronTxDto(chain string, to string, data string, feeLimit string) *OpenIntentTronTxDto { + this := OpenIntentTronTxDto{} + this.Chain = chain + this.To = to + this.Data = data + this.FeeLimit = feeLimit + return &this +} + +// NewOpenIntentTronTxDtoWithDefaults instantiates a new OpenIntentTronTxDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewOpenIntentTronTxDtoWithDefaults() *OpenIntentTronTxDto { + this := OpenIntentTronTxDto{} + return &this +} + +// GetChain returns the Chain field value +func (o *OpenIntentTronTxDto) GetChain() string { + if o == nil { + var ret string + return ret + } + + return o.Chain +} + +// GetChainOk returns a tuple with the Chain field value +// and a boolean to check if the value has been set. +func (o *OpenIntentTronTxDto) GetChainOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Chain, true +} + +// SetChain sets field value +func (o *OpenIntentTronTxDto) SetChain(v string) { + o.Chain = v +} + +// GetTo returns the To field value +func (o *OpenIntentTronTxDto) GetTo() string { + if o == nil { + var ret string + return ret + } + + return o.To +} + +// GetToOk returns a tuple with the To field value +// and a boolean to check if the value has been set. +func (o *OpenIntentTronTxDto) GetToOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.To, true +} + +// SetTo sets field value +func (o *OpenIntentTronTxDto) SetTo(v string) { + o.To = v +} + +// GetData returns the Data field value +func (o *OpenIntentTronTxDto) GetData() string { + if o == nil { + var ret string + return ret + } + + return o.Data +} + +// GetDataOk returns a tuple with the Data field value +// and a boolean to check if the value has been set. +func (o *OpenIntentTronTxDto) GetDataOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Data, true +} + +// SetData sets field value +func (o *OpenIntentTronTxDto) SetData(v string) { + o.Data = v +} + +// GetFeeLimit returns the FeeLimit field value +func (o *OpenIntentTronTxDto) GetFeeLimit() string { + if o == nil { + var ret string + return ret + } + + return o.FeeLimit +} + +// GetFeeLimitOk returns a tuple with the FeeLimit field value +// and a boolean to check if the value has been set. +func (o *OpenIntentTronTxDto) GetFeeLimitOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.FeeLimit, true +} + +// SetFeeLimit sets field value +func (o *OpenIntentTronTxDto) SetFeeLimit(v string) { + o.FeeLimit = v +} + +func (o OpenIntentTronTxDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OpenIntentTronTxDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["chain"] = o.Chain + toSerialize["to"] = o.To + toSerialize["data"] = o.Data + toSerialize["feeLimit"] = o.FeeLimit + return toSerialize, nil +} + +func (o *OpenIntentTronTxDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "chain", + "to", + "data", + "feeLimit", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varOpenIntentTronTxDto := _OpenIntentTronTxDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOpenIntentTronTxDto) + + if err != nil { + return err + } + + *o = OpenIntentTronTxDto(varOpenIntentTronTxDto) + + return err +} + +type NullableOpenIntentTronTxDto struct { + value *OpenIntentTronTxDto + isSet bool +} + +func (v NullableOpenIntentTronTxDto) Get() *OpenIntentTronTxDto { + return v.value +} + +func (v *NullableOpenIntentTronTxDto) Set(val *OpenIntentTronTxDto) { + v.value = val + v.isSet = true +} + +func (v NullableOpenIntentTronTxDto) IsSet() bool { + return v.isSet +} + +func (v *NullableOpenIntentTronTxDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOpenIntentTronTxDto(val *OpenIntentTronTxDto) *NullableOpenIntentTronTxDto { + return &NullableOpenIntentTronTxDto{value: val, isSet: true} +} + +func (v NullableOpenIntentTronTxDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOpenIntentTronTxDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_order_meta_dto.go b/api/lifiorder/model_order_meta_dto.go index cccf0d99..44c4e335 100644 --- a/api/lifiorder/model_order_meta_dto.go +++ b/api/lifiorder/model_order_meta_dto.go @@ -32,31 +32,31 @@ type OrderMetaDto struct { // Parsed destination address of the order DestinationAddress string `json:"destinationAddress"` // Transaction hash when order was initiated (on-chain order) [eg: Open escrow event] - OrderInitiatedTxHash map[string]interface{} `json:"orderInitiatedTxHash"` + OrderInitiatedTxHash NullableString `json:"orderInitiatedTxHash"` // Transaction hash of the OutputFilled event - OrderDeliveredTxHash map[string]interface{} `json:"orderDeliveredTxHash"` + OrderDeliveredTxHash NullableString `json:"orderDeliveredTxHash"` // Transaction hash of the OutputProven event - OrderVerifiedTxHash map[string]interface{} `json:"orderVerifiedTxHash"` + OrderVerifiedTxHash NullableString `json:"orderVerifiedTxHash"` // Transaction hash of the Finalised event - OrderSettledTxHash map[string]interface{} `json:"orderSettledTxHash"` + OrderSettledTxHash NullableString `json:"orderSettledTxHash"` // Transaction hash of the Refunded event - RefundTxHash map[string]interface{} `json:"refundTxHash"` + RefundTxHash NullableString `json:"refundTxHash"` // Date when the order was signed - SignedAt map[string]interface{} `json:"signedAt"` + SignedAt NullableString `json:"signedAt"` // Date when the order expires - ExpiredAt map[string]interface{} `json:"expiredAt"` + ExpiredAt NullableString `json:"expiredAt"` // Date when the order was delivered - DeliveredAt map[string]interface{} `json:"deliveredAt"` + DeliveredAt NullableString `json:"deliveredAt"` // Date when the order was settled - SettledAt map[string]interface{} `json:"settledAt"` + SettledAt NullableString `json:"settledAt"` // Date when the order was refunded - RefundedAt map[string]interface{} `json:"refundedAt"` + RefundedAt NullableString `json:"refundedAt"` // Last compact deposit block number - LastCompactDepositBlockNumber map[string]interface{} `json:"lastCompactDepositBlockNumber"` + LastCompactDepositBlockNumber NullableString `json:"lastCompactDepositBlockNumber"` // Quote ID associated with the order - QuoteId map[string]interface{} `json:"quoteId"` + QuoteId NullableString `json:"quoteId"` // Solver address that filled the order - SolverAddress map[string]interface{} `json:"solverAddress,omitempty"` + SolverAddress NullableString `json:"solverAddress,omitempty"` // Integrator key hash identifying the integrator this order belongs to IntegratorKeyHash *string `json:"integratorKeyHash,omitempty"` } @@ -67,7 +67,7 @@ type _OrderMetaDto OrderMetaDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewOrderMetaDto(submitTime float32, orderStatus string, orderIdentifier string, onChainOrderId string, destinationAddress string, orderInitiatedTxHash map[string]interface{}, orderDeliveredTxHash map[string]interface{}, orderVerifiedTxHash map[string]interface{}, orderSettledTxHash map[string]interface{}, refundTxHash map[string]interface{}, signedAt map[string]interface{}, expiredAt map[string]interface{}, deliveredAt map[string]interface{}, settledAt map[string]interface{}, refundedAt map[string]interface{}, lastCompactDepositBlockNumber map[string]interface{}, quoteId map[string]interface{}) *OrderMetaDto { +func NewOrderMetaDto(submitTime float32, orderStatus string, orderIdentifier string, onChainOrderId string, destinationAddress string, orderInitiatedTxHash NullableString, orderDeliveredTxHash NullableString, orderVerifiedTxHash NullableString, orderSettledTxHash NullableString, refundTxHash NullableString, signedAt NullableString, expiredAt NullableString, deliveredAt NullableString, settledAt NullableString, refundedAt NullableString, lastCompactDepositBlockNumber NullableString, quoteId NullableString) *OrderMetaDto { this := OrderMetaDto{} this.SubmitTime = submitTime this.OrderStatus = orderStatus @@ -218,348 +218,358 @@ func (o *OrderMetaDto) SetDestinationAddress(v string) { } // GetOrderInitiatedTxHash returns the OrderInitiatedTxHash field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetOrderInitiatedTxHash() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetOrderInitiatedTxHash() string { + if o == nil || o.OrderInitiatedTxHash.Get() == nil { + var ret string return ret } - return o.OrderInitiatedTxHash + return *o.OrderInitiatedTxHash.Get() } // GetOrderInitiatedTxHashOk returns a tuple with the OrderInitiatedTxHash field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetOrderInitiatedTxHashOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.OrderInitiatedTxHash) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetOrderInitiatedTxHashOk() (*string, bool) { + if o == nil { + return nil, false } - return o.OrderInitiatedTxHash, true + return o.OrderInitiatedTxHash.Get(), o.OrderInitiatedTxHash.IsSet() } // SetOrderInitiatedTxHash sets field value -func (o *OrderMetaDto) SetOrderInitiatedTxHash(v map[string]interface{}) { - o.OrderInitiatedTxHash = v +func (o *OrderMetaDto) SetOrderInitiatedTxHash(v string) { + o.OrderInitiatedTxHash.Set(&v) } // GetOrderDeliveredTxHash returns the OrderDeliveredTxHash field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetOrderDeliveredTxHash() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetOrderDeliveredTxHash() string { + if o == nil || o.OrderDeliveredTxHash.Get() == nil { + var ret string return ret } - return o.OrderDeliveredTxHash + return *o.OrderDeliveredTxHash.Get() } // GetOrderDeliveredTxHashOk returns a tuple with the OrderDeliveredTxHash field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetOrderDeliveredTxHashOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.OrderDeliveredTxHash) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetOrderDeliveredTxHashOk() (*string, bool) { + if o == nil { + return nil, false } - return o.OrderDeliveredTxHash, true + return o.OrderDeliveredTxHash.Get(), o.OrderDeliveredTxHash.IsSet() } // SetOrderDeliveredTxHash sets field value -func (o *OrderMetaDto) SetOrderDeliveredTxHash(v map[string]interface{}) { - o.OrderDeliveredTxHash = v +func (o *OrderMetaDto) SetOrderDeliveredTxHash(v string) { + o.OrderDeliveredTxHash.Set(&v) } // GetOrderVerifiedTxHash returns the OrderVerifiedTxHash field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetOrderVerifiedTxHash() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetOrderVerifiedTxHash() string { + if o == nil || o.OrderVerifiedTxHash.Get() == nil { + var ret string return ret } - return o.OrderVerifiedTxHash + return *o.OrderVerifiedTxHash.Get() } // GetOrderVerifiedTxHashOk returns a tuple with the OrderVerifiedTxHash field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetOrderVerifiedTxHashOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.OrderVerifiedTxHash) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetOrderVerifiedTxHashOk() (*string, bool) { + if o == nil { + return nil, false } - return o.OrderVerifiedTxHash, true + return o.OrderVerifiedTxHash.Get(), o.OrderVerifiedTxHash.IsSet() } // SetOrderVerifiedTxHash sets field value -func (o *OrderMetaDto) SetOrderVerifiedTxHash(v map[string]interface{}) { - o.OrderVerifiedTxHash = v +func (o *OrderMetaDto) SetOrderVerifiedTxHash(v string) { + o.OrderVerifiedTxHash.Set(&v) } // GetOrderSettledTxHash returns the OrderSettledTxHash field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetOrderSettledTxHash() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetOrderSettledTxHash() string { + if o == nil || o.OrderSettledTxHash.Get() == nil { + var ret string return ret } - return o.OrderSettledTxHash + return *o.OrderSettledTxHash.Get() } // GetOrderSettledTxHashOk returns a tuple with the OrderSettledTxHash field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetOrderSettledTxHashOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.OrderSettledTxHash) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetOrderSettledTxHashOk() (*string, bool) { + if o == nil { + return nil, false } - return o.OrderSettledTxHash, true + return o.OrderSettledTxHash.Get(), o.OrderSettledTxHash.IsSet() } // SetOrderSettledTxHash sets field value -func (o *OrderMetaDto) SetOrderSettledTxHash(v map[string]interface{}) { - o.OrderSettledTxHash = v +func (o *OrderMetaDto) SetOrderSettledTxHash(v string) { + o.OrderSettledTxHash.Set(&v) } // GetRefundTxHash returns the RefundTxHash field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetRefundTxHash() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetRefundTxHash() string { + if o == nil || o.RefundTxHash.Get() == nil { + var ret string return ret } - return o.RefundTxHash + return *o.RefundTxHash.Get() } // GetRefundTxHashOk returns a tuple with the RefundTxHash field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetRefundTxHashOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.RefundTxHash) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetRefundTxHashOk() (*string, bool) { + if o == nil { + return nil, false } - return o.RefundTxHash, true + return o.RefundTxHash.Get(), o.RefundTxHash.IsSet() } // SetRefundTxHash sets field value -func (o *OrderMetaDto) SetRefundTxHash(v map[string]interface{}) { - o.RefundTxHash = v +func (o *OrderMetaDto) SetRefundTxHash(v string) { + o.RefundTxHash.Set(&v) } // GetSignedAt returns the SignedAt field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetSignedAt() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetSignedAt() string { + if o == nil || o.SignedAt.Get() == nil { + var ret string return ret } - return o.SignedAt + return *o.SignedAt.Get() } // GetSignedAtOk returns a tuple with the SignedAt field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetSignedAtOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.SignedAt) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetSignedAtOk() (*string, bool) { + if o == nil { + return nil, false } - return o.SignedAt, true + return o.SignedAt.Get(), o.SignedAt.IsSet() } // SetSignedAt sets field value -func (o *OrderMetaDto) SetSignedAt(v map[string]interface{}) { - o.SignedAt = v +func (o *OrderMetaDto) SetSignedAt(v string) { + o.SignedAt.Set(&v) } // GetExpiredAt returns the ExpiredAt field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetExpiredAt() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetExpiredAt() string { + if o == nil || o.ExpiredAt.Get() == nil { + var ret string return ret } - return o.ExpiredAt + return *o.ExpiredAt.Get() } // GetExpiredAtOk returns a tuple with the ExpiredAt field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetExpiredAtOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.ExpiredAt) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetExpiredAtOk() (*string, bool) { + if o == nil { + return nil, false } - return o.ExpiredAt, true + return o.ExpiredAt.Get(), o.ExpiredAt.IsSet() } // SetExpiredAt sets field value -func (o *OrderMetaDto) SetExpiredAt(v map[string]interface{}) { - o.ExpiredAt = v +func (o *OrderMetaDto) SetExpiredAt(v string) { + o.ExpiredAt.Set(&v) } // GetDeliveredAt returns the DeliveredAt field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetDeliveredAt() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetDeliveredAt() string { + if o == nil || o.DeliveredAt.Get() == nil { + var ret string return ret } - return o.DeliveredAt + return *o.DeliveredAt.Get() } // GetDeliveredAtOk returns a tuple with the DeliveredAt field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetDeliveredAtOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.DeliveredAt) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetDeliveredAtOk() (*string, bool) { + if o == nil { + return nil, false } - return o.DeliveredAt, true + return o.DeliveredAt.Get(), o.DeliveredAt.IsSet() } // SetDeliveredAt sets field value -func (o *OrderMetaDto) SetDeliveredAt(v map[string]interface{}) { - o.DeliveredAt = v +func (o *OrderMetaDto) SetDeliveredAt(v string) { + o.DeliveredAt.Set(&v) } // GetSettledAt returns the SettledAt field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetSettledAt() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetSettledAt() string { + if o == nil || o.SettledAt.Get() == nil { + var ret string return ret } - return o.SettledAt + return *o.SettledAt.Get() } // GetSettledAtOk returns a tuple with the SettledAt field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetSettledAtOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.SettledAt) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetSettledAtOk() (*string, bool) { + if o == nil { + return nil, false } - return o.SettledAt, true + return o.SettledAt.Get(), o.SettledAt.IsSet() } // SetSettledAt sets field value -func (o *OrderMetaDto) SetSettledAt(v map[string]interface{}) { - o.SettledAt = v +func (o *OrderMetaDto) SetSettledAt(v string) { + o.SettledAt.Set(&v) } // GetRefundedAt returns the RefundedAt field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetRefundedAt() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetRefundedAt() string { + if o == nil || o.RefundedAt.Get() == nil { + var ret string return ret } - return o.RefundedAt + return *o.RefundedAt.Get() } // GetRefundedAtOk returns a tuple with the RefundedAt field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetRefundedAtOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.RefundedAt) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetRefundedAtOk() (*string, bool) { + if o == nil { + return nil, false } - return o.RefundedAt, true + return o.RefundedAt.Get(), o.RefundedAt.IsSet() } // SetRefundedAt sets field value -func (o *OrderMetaDto) SetRefundedAt(v map[string]interface{}) { - o.RefundedAt = v +func (o *OrderMetaDto) SetRefundedAt(v string) { + o.RefundedAt.Set(&v) } // GetLastCompactDepositBlockNumber returns the LastCompactDepositBlockNumber field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetLastCompactDepositBlockNumber() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetLastCompactDepositBlockNumber() string { + if o == nil || o.LastCompactDepositBlockNumber.Get() == nil { + var ret string return ret } - return o.LastCompactDepositBlockNumber + return *o.LastCompactDepositBlockNumber.Get() } // GetLastCompactDepositBlockNumberOk returns a tuple with the LastCompactDepositBlockNumber field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetLastCompactDepositBlockNumberOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.LastCompactDepositBlockNumber) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetLastCompactDepositBlockNumberOk() (*string, bool) { + if o == nil { + return nil, false } - return o.LastCompactDepositBlockNumber, true + return o.LastCompactDepositBlockNumber.Get(), o.LastCompactDepositBlockNumber.IsSet() } // SetLastCompactDepositBlockNumber sets field value -func (o *OrderMetaDto) SetLastCompactDepositBlockNumber(v map[string]interface{}) { - o.LastCompactDepositBlockNumber = v +func (o *OrderMetaDto) SetLastCompactDepositBlockNumber(v string) { + o.LastCompactDepositBlockNumber.Set(&v) } // GetQuoteId returns the QuoteId field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *OrderMetaDto) GetQuoteId() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *OrderMetaDto) GetQuoteId() string { + if o == nil || o.QuoteId.Get() == nil { + var ret string return ret } - return o.QuoteId + return *o.QuoteId.Get() } // GetQuoteIdOk returns a tuple with the QuoteId field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetQuoteIdOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.QuoteId) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetQuoteIdOk() (*string, bool) { + if o == nil { + return nil, false } - return o.QuoteId, true + return o.QuoteId.Get(), o.QuoteId.IsSet() } // SetQuoteId sets field value -func (o *OrderMetaDto) SetQuoteId(v map[string]interface{}) { - o.QuoteId = v +func (o *OrderMetaDto) SetQuoteId(v string) { + o.QuoteId.Set(&v) } // GetSolverAddress returns the SolverAddress field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *OrderMetaDto) GetSolverAddress() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +func (o *OrderMetaDto) GetSolverAddress() string { + if o == nil || IsNil(o.SolverAddress.Get()) { + var ret string return ret } - return o.SolverAddress + return *o.SolverAddress.Get() } // GetSolverAddressOk returns a tuple with the SolverAddress field value if set, nil otherwise // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *OrderMetaDto) GetSolverAddressOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.SolverAddress) { - return map[string]interface{}{}, false +func (o *OrderMetaDto) GetSolverAddressOk() (*string, bool) { + if o == nil { + return nil, false } - return o.SolverAddress, true + return o.SolverAddress.Get(), o.SolverAddress.IsSet() } // HasSolverAddress returns a boolean if a field has been set. func (o *OrderMetaDto) HasSolverAddress() bool { - if o != nil && !IsNil(o.SolverAddress) { + if o != nil && o.SolverAddress.IsSet() { return true } return false } -// SetSolverAddress gets a reference to the given map[string]interface{} and assigns it to the SolverAddress field. -func (o *OrderMetaDto) SetSolverAddress(v map[string]interface{}) { - o.SolverAddress = v +// SetSolverAddress gets a reference to the given NullableString and assigns it to the SolverAddress field. +func (o *OrderMetaDto) SetSolverAddress(v string) { + o.SolverAddress.Set(&v) +} + +// SetSolverAddressNil sets the value for SolverAddress to be an explicit nil +func (o *OrderMetaDto) SetSolverAddressNil() { + o.SolverAddress.Set(nil) +} + +// UnsetSolverAddress ensures that no value is present for SolverAddress, not even an explicit nil +func (o *OrderMetaDto) UnsetSolverAddress() { + o.SolverAddress.Unset() } // GetIntegratorKeyHash returns the IntegratorKeyHash field value if set, zero value otherwise. @@ -609,44 +619,20 @@ func (o OrderMetaDto) ToMap() (map[string]interface{}, error) { toSerialize["orderIdentifier"] = o.OrderIdentifier toSerialize["onChainOrderId"] = o.OnChainOrderId toSerialize["destinationAddress"] = o.DestinationAddress - if o.OrderInitiatedTxHash != nil { - toSerialize["orderInitiatedTxHash"] = o.OrderInitiatedTxHash - } - if o.OrderDeliveredTxHash != nil { - toSerialize["orderDeliveredTxHash"] = o.OrderDeliveredTxHash - } - if o.OrderVerifiedTxHash != nil { - toSerialize["orderVerifiedTxHash"] = o.OrderVerifiedTxHash - } - if o.OrderSettledTxHash != nil { - toSerialize["orderSettledTxHash"] = o.OrderSettledTxHash - } - if o.RefundTxHash != nil { - toSerialize["refundTxHash"] = o.RefundTxHash - } - if o.SignedAt != nil { - toSerialize["signedAt"] = o.SignedAt - } - if o.ExpiredAt != nil { - toSerialize["expiredAt"] = o.ExpiredAt - } - if o.DeliveredAt != nil { - toSerialize["deliveredAt"] = o.DeliveredAt - } - if o.SettledAt != nil { - toSerialize["settledAt"] = o.SettledAt - } - if o.RefundedAt != nil { - toSerialize["refundedAt"] = o.RefundedAt - } - if o.LastCompactDepositBlockNumber != nil { - toSerialize["lastCompactDepositBlockNumber"] = o.LastCompactDepositBlockNumber - } - if o.QuoteId != nil { - toSerialize["quoteId"] = o.QuoteId - } - if o.SolverAddress != nil { - toSerialize["solverAddress"] = o.SolverAddress + toSerialize["orderInitiatedTxHash"] = o.OrderInitiatedTxHash.Get() + toSerialize["orderDeliveredTxHash"] = o.OrderDeliveredTxHash.Get() + toSerialize["orderVerifiedTxHash"] = o.OrderVerifiedTxHash.Get() + toSerialize["orderSettledTxHash"] = o.OrderSettledTxHash.Get() + toSerialize["refundTxHash"] = o.RefundTxHash.Get() + toSerialize["signedAt"] = o.SignedAt.Get() + toSerialize["expiredAt"] = o.ExpiredAt.Get() + toSerialize["deliveredAt"] = o.DeliveredAt.Get() + toSerialize["settledAt"] = o.SettledAt.Get() + toSerialize["refundedAt"] = o.RefundedAt.Get() + toSerialize["lastCompactDepositBlockNumber"] = o.LastCompactDepositBlockNumber.Get() + toSerialize["quoteId"] = o.QuoteId.Get() + if o.SolverAddress.IsSet() { + toSerialize["solverAddress"] = o.SolverAddress.Get() } if !IsNil(o.IntegratorKeyHash) { toSerialize["integratorKeyHash"] = o.IntegratorKeyHash diff --git a/api/lifiorder/model_output_dto.go b/api/lifiorder/model_output_dto.go new file mode 100644 index 00000000..63625852 --- /dev/null +++ b/api/lifiorder/model_output_dto.go @@ -0,0 +1,299 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the OutputDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &OutputDto{} + +// OutputDto struct for OutputDto +type OutputDto struct { + // CAIP-2 chain identifier for this output (e.g., \"eip155:1\"). Applies to both receiver and asset. + Chain string `json:"chain"` + // Native address that will receive the output assets + Receiver string `json:"receiver"` + // Native address of the token/asset to be received as output + Asset string `json:"asset"` + Amount NullableString `json:"amount,omitempty"` + // Optional calldata describing how the receiver will consume the output. Enables composability with other protocols + Calldata *string `json:"calldata,omitempty"` +} + +type _OutputDto OutputDto + +// NewOutputDto instantiates a new OutputDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewOutputDto(chain string, receiver string, asset string) *OutputDto { + this := OutputDto{} + this.Chain = chain + this.Receiver = receiver + this.Asset = asset + return &this +} + +// NewOutputDtoWithDefaults instantiates a new OutputDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewOutputDtoWithDefaults() *OutputDto { + this := OutputDto{} + return &this +} + +// GetChain returns the Chain field value +func (o *OutputDto) GetChain() string { + if o == nil { + var ret string + return ret + } + + return o.Chain +} + +// GetChainOk returns a tuple with the Chain field value +// and a boolean to check if the value has been set. +func (o *OutputDto) GetChainOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Chain, true +} + +// SetChain sets field value +func (o *OutputDto) SetChain(v string) { + o.Chain = v +} + +// GetReceiver returns the Receiver field value +func (o *OutputDto) GetReceiver() string { + if o == nil { + var ret string + return ret + } + + return o.Receiver +} + +// GetReceiverOk returns a tuple with the Receiver field value +// and a boolean to check if the value has been set. +func (o *OutputDto) GetReceiverOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Receiver, true +} + +// SetReceiver sets field value +func (o *OutputDto) SetReceiver(v string) { + o.Receiver = v +} + +// GetAsset returns the Asset field value +func (o *OutputDto) GetAsset() string { + if o == nil { + var ret string + return ret + } + + return o.Asset +} + +// GetAssetOk returns a tuple with the Asset field value +// and a boolean to check if the value has been set. +func (o *OutputDto) GetAssetOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Asset, true +} + +// SetAsset sets field value +func (o *OutputDto) SetAsset(v string) { + o.Asset = v +} + +// GetAmount returns the Amount field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *OutputDto) GetAmount() string { + if o == nil || IsNil(o.Amount.Get()) { + var ret string + return ret + } + return *o.Amount.Get() +} + +// GetAmountOk returns a tuple with the Amount field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *OutputDto) GetAmountOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.Amount.Get(), o.Amount.IsSet() +} + +// HasAmount returns a boolean if a field has been set. +func (o *OutputDto) HasAmount() bool { + if o != nil && o.Amount.IsSet() { + return true + } + + return false +} + +// SetAmount gets a reference to the given NullableString and assigns it to the Amount field. +func (o *OutputDto) SetAmount(v string) { + o.Amount.Set(&v) +} + +// SetAmountNil sets the value for Amount to be an explicit nil +func (o *OutputDto) SetAmountNil() { + o.Amount.Set(nil) +} + +// UnsetAmount ensures that no value is present for Amount, not even an explicit nil +func (o *OutputDto) UnsetAmount() { + o.Amount.Unset() +} + +// GetCalldata returns the Calldata field value if set, zero value otherwise. +func (o *OutputDto) GetCalldata() string { + if o == nil || IsNil(o.Calldata) { + var ret string + return ret + } + return *o.Calldata +} + +// GetCalldataOk returns a tuple with the Calldata field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *OutputDto) GetCalldataOk() (*string, bool) { + if o == nil || IsNil(o.Calldata) { + return nil, false + } + return o.Calldata, true +} + +// HasCalldata returns a boolean if a field has been set. +func (o *OutputDto) HasCalldata() bool { + if o != nil && !IsNil(o.Calldata) { + return true + } + + return false +} + +// SetCalldata gets a reference to the given string and assigns it to the Calldata field. +func (o *OutputDto) SetCalldata(v string) { + o.Calldata = &v +} + +func (o OutputDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o OutputDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["chain"] = o.Chain + toSerialize["receiver"] = o.Receiver + toSerialize["asset"] = o.Asset + if o.Amount.IsSet() { + toSerialize["amount"] = o.Amount.Get() + } + if !IsNil(o.Calldata) { + toSerialize["calldata"] = o.Calldata + } + return toSerialize, nil +} + +func (o *OutputDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "chain", + "receiver", + "asset", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varOutputDto := _OutputDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varOutputDto) + + if err != nil { + return err + } + + *o = OutputDto(varOutputDto) + + return err +} + +type NullableOutputDto struct { + value *OutputDto + isSet bool +} + +func (v NullableOutputDto) Get() *OutputDto { + return v.value +} + +func (v *NullableOutputDto) Set(val *OutputDto) { + v.value = val + v.isSet = true +} + +func (v NullableOutputDto) IsSet() bool { + return v.isSet +} + +func (v *NullableOutputDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableOutputDto(val *OutputDto) *NullableOutputDto { + return &NullableOutputDto{value: val, isSet: true} +} + +func (v NullableOutputDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableOutputDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_put_supported_contracts_dto.go b/api/lifiorder/model_put_supported_contracts_dto.go index e20f188f..1587ae9e 100644 --- a/api/lifiorder/model_put_supported_contracts_dto.go +++ b/api/lifiorder/model_put_supported_contracts_dto.go @@ -19,9 +19,9 @@ var _ MappedNullable = &PutSupportedContractsDto{} // PutSupportedContractsDto struct for PutSupportedContractsDto type PutSupportedContractsDto struct { - Oracle []PutSupportedContractsDtoOracleInner `json:"oracle,omitempty"` - InputSettler []PutSupportedContractsDtoOracleInner `json:"inputSettler,omitempty"` - OutputSettler []PutSupportedContractsDtoOracleInner `json:"outputSettler,omitempty"` + Oracle []QuoteRequestDtoIntentMetadataOracleInner `json:"oracle,omitempty"` + InputSettler []QuoteRequestDtoIntentMetadataOracleInner `json:"inputSettler,omitempty"` + OutputSettler []QuoteRequestDtoIntentMetadataOracleInner `json:"outputSettler,omitempty"` } // NewPutSupportedContractsDto instantiates a new PutSupportedContractsDto object @@ -42,9 +42,9 @@ func NewPutSupportedContractsDtoWithDefaults() *PutSupportedContractsDto { } // GetOracle returns the Oracle field value if set, zero value otherwise. -func (o *PutSupportedContractsDto) GetOracle() []PutSupportedContractsDtoOracleInner { +func (o *PutSupportedContractsDto) GetOracle() []QuoteRequestDtoIntentMetadataOracleInner { if o == nil || IsNil(o.Oracle) { - var ret []PutSupportedContractsDtoOracleInner + var ret []QuoteRequestDtoIntentMetadataOracleInner return ret } return o.Oracle @@ -52,7 +52,7 @@ func (o *PutSupportedContractsDto) GetOracle() []PutSupportedContractsDtoOracleI // GetOracleOk returns a tuple with the Oracle field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *PutSupportedContractsDto) GetOracleOk() ([]PutSupportedContractsDtoOracleInner, bool) { +func (o *PutSupportedContractsDto) GetOracleOk() ([]QuoteRequestDtoIntentMetadataOracleInner, bool) { if o == nil || IsNil(o.Oracle) { return nil, false } @@ -68,15 +68,15 @@ func (o *PutSupportedContractsDto) HasOracle() bool { return false } -// SetOracle gets a reference to the given []PutSupportedContractsDtoOracleInner and assigns it to the Oracle field. -func (o *PutSupportedContractsDto) SetOracle(v []PutSupportedContractsDtoOracleInner) { +// SetOracle gets a reference to the given []QuoteRequestDtoIntentMetadataOracleInner and assigns it to the Oracle field. +func (o *PutSupportedContractsDto) SetOracle(v []QuoteRequestDtoIntentMetadataOracleInner) { o.Oracle = v } // GetInputSettler returns the InputSettler field value if set, zero value otherwise. -func (o *PutSupportedContractsDto) GetInputSettler() []PutSupportedContractsDtoOracleInner { +func (o *PutSupportedContractsDto) GetInputSettler() []QuoteRequestDtoIntentMetadataOracleInner { if o == nil || IsNil(o.InputSettler) { - var ret []PutSupportedContractsDtoOracleInner + var ret []QuoteRequestDtoIntentMetadataOracleInner return ret } return o.InputSettler @@ -84,7 +84,7 @@ func (o *PutSupportedContractsDto) GetInputSettler() []PutSupportedContractsDtoO // GetInputSettlerOk returns a tuple with the InputSettler field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *PutSupportedContractsDto) GetInputSettlerOk() ([]PutSupportedContractsDtoOracleInner, bool) { +func (o *PutSupportedContractsDto) GetInputSettlerOk() ([]QuoteRequestDtoIntentMetadataOracleInner, bool) { if o == nil || IsNil(o.InputSettler) { return nil, false } @@ -100,15 +100,15 @@ func (o *PutSupportedContractsDto) HasInputSettler() bool { return false } -// SetInputSettler gets a reference to the given []PutSupportedContractsDtoOracleInner and assigns it to the InputSettler field. -func (o *PutSupportedContractsDto) SetInputSettler(v []PutSupportedContractsDtoOracleInner) { +// SetInputSettler gets a reference to the given []QuoteRequestDtoIntentMetadataOracleInner and assigns it to the InputSettler field. +func (o *PutSupportedContractsDto) SetInputSettler(v []QuoteRequestDtoIntentMetadataOracleInner) { o.InputSettler = v } // GetOutputSettler returns the OutputSettler field value if set, zero value otherwise. -func (o *PutSupportedContractsDto) GetOutputSettler() []PutSupportedContractsDtoOracleInner { +func (o *PutSupportedContractsDto) GetOutputSettler() []QuoteRequestDtoIntentMetadataOracleInner { if o == nil || IsNil(o.OutputSettler) { - var ret []PutSupportedContractsDtoOracleInner + var ret []QuoteRequestDtoIntentMetadataOracleInner return ret } return o.OutputSettler @@ -116,7 +116,7 @@ func (o *PutSupportedContractsDto) GetOutputSettler() []PutSupportedContractsDto // GetOutputSettlerOk returns a tuple with the OutputSettler field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *PutSupportedContractsDto) GetOutputSettlerOk() ([]PutSupportedContractsDtoOracleInner, bool) { +func (o *PutSupportedContractsDto) GetOutputSettlerOk() ([]QuoteRequestDtoIntentMetadataOracleInner, bool) { if o == nil || IsNil(o.OutputSettler) { return nil, false } @@ -132,8 +132,8 @@ func (o *PutSupportedContractsDto) HasOutputSettler() bool { return false } -// SetOutputSettler gets a reference to the given []PutSupportedContractsDtoOracleInner and assigns it to the OutputSettler field. -func (o *PutSupportedContractsDto) SetOutputSettler(v []PutSupportedContractsDtoOracleInner) { +// SetOutputSettler gets a reference to the given []QuoteRequestDtoIntentMetadataOracleInner and assigns it to the OutputSettler field. +func (o *PutSupportedContractsDto) SetOutputSettler(v []QuoteRequestDtoIntentMetadataOracleInner) { o.OutputSettler = v } diff --git a/api/lifiorder/model_put_supported_contracts_dto_oracle_inner.go b/api/lifiorder/model_put_supported_contracts_dto_oracle_inner.go deleted file mode 100644 index 51773bbc..00000000 --- a/api/lifiorder/model_put_supported_contracts_dto_oracle_inner.go +++ /dev/null @@ -1,186 +0,0 @@ -/* -Lifi Intents API Reference - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: 0.0.19 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package lifiorder - -import ( - "bytes" - "encoding/json" - "fmt" -) - -// checks if the PutSupportedContractsDtoOracleInner type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &PutSupportedContractsDtoOracleInner{} - -// PutSupportedContractsDtoOracleInner struct for PutSupportedContractsDtoOracleInner -type PutSupportedContractsDtoOracleInner struct { - // CAIP-2 chain identifier, e.g. \"eip155:1\" - Chain string `json:"chain"` - // Native contract address for the chain - Address string `json:"address"` -} - -type _PutSupportedContractsDtoOracleInner PutSupportedContractsDtoOracleInner - -// NewPutSupportedContractsDtoOracleInner instantiates a new PutSupportedContractsDtoOracleInner object -// This constructor will assign default values to properties that have it defined, -// and makes sure properties required by API are set, but the set of arguments -// will change when the set of required properties is changed -func NewPutSupportedContractsDtoOracleInner(chain string, address string) *PutSupportedContractsDtoOracleInner { - this := PutSupportedContractsDtoOracleInner{} - this.Chain = chain - this.Address = address - return &this -} - -// NewPutSupportedContractsDtoOracleInnerWithDefaults instantiates a new PutSupportedContractsDtoOracleInner object -// This constructor will only assign default values to properties that have it defined, -// but it doesn't guarantee that properties required by API are set -func NewPutSupportedContractsDtoOracleInnerWithDefaults() *PutSupportedContractsDtoOracleInner { - this := PutSupportedContractsDtoOracleInner{} - return &this -} - -// GetChain returns the Chain field value -func (o *PutSupportedContractsDtoOracleInner) GetChain() string { - if o == nil { - var ret string - return ret - } - - return o.Chain -} - -// GetChainOk returns a tuple with the Chain field value -// and a boolean to check if the value has been set. -func (o *PutSupportedContractsDtoOracleInner) GetChainOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Chain, true -} - -// SetChain sets field value -func (o *PutSupportedContractsDtoOracleInner) SetChain(v string) { - o.Chain = v -} - -// GetAddress returns the Address field value -func (o *PutSupportedContractsDtoOracleInner) GetAddress() string { - if o == nil { - var ret string - return ret - } - - return o.Address -} - -// GetAddressOk returns a tuple with the Address field value -// and a boolean to check if the value has been set. -func (o *PutSupportedContractsDtoOracleInner) GetAddressOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Address, true -} - -// SetAddress sets field value -func (o *PutSupportedContractsDtoOracleInner) SetAddress(v string) { - o.Address = v -} - -func (o PutSupportedContractsDtoOracleInner) MarshalJSON() ([]byte, error) { - toSerialize, err := o.ToMap() - if err != nil { - return []byte{}, err - } - return json.Marshal(toSerialize) -} - -func (o PutSupportedContractsDtoOracleInner) ToMap() (map[string]interface{}, error) { - toSerialize := map[string]interface{}{} - toSerialize["chain"] = o.Chain - toSerialize["address"] = o.Address - return toSerialize, nil -} - -func (o *PutSupportedContractsDtoOracleInner) UnmarshalJSON(data []byte) (err error) { - // This validates that all required properties are included in the JSON object - // by unmarshalling the object into a generic map with string keys and checking - // that every required field exists as a key in the generic map. - requiredProperties := []string{ - "chain", - "address", - } - - allProperties := make(map[string]interface{}) - - err = json.Unmarshal(data, &allProperties) - - if err != nil { - return err - } - - for _, requiredProperty := range requiredProperties { - if _, exists := allProperties[requiredProperty]; !exists { - return fmt.Errorf("no value given for required property %v", requiredProperty) - } - } - - varPutSupportedContractsDtoOracleInner := _PutSupportedContractsDtoOracleInner{} - - decoder := json.NewDecoder(bytes.NewReader(data)) - decoder.DisallowUnknownFields() - err = decoder.Decode(&varPutSupportedContractsDtoOracleInner) - - if err != nil { - return err - } - - *o = PutSupportedContractsDtoOracleInner(varPutSupportedContractsDtoOracleInner) - - return err -} - -type NullablePutSupportedContractsDtoOracleInner struct { - value *PutSupportedContractsDtoOracleInner - isSet bool -} - -func (v NullablePutSupportedContractsDtoOracleInner) Get() *PutSupportedContractsDtoOracleInner { - return v.value -} - -func (v *NullablePutSupportedContractsDtoOracleInner) Set(val *PutSupportedContractsDtoOracleInner) { - v.value = val - v.isSet = true -} - -func (v NullablePutSupportedContractsDtoOracleInner) IsSet() bool { - return v.isSet -} - -func (v *NullablePutSupportedContractsDtoOracleInner) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullablePutSupportedContractsDtoOracleInner(val *PutSupportedContractsDtoOracleInner) *NullablePutSupportedContractsDtoOracleInner { - return &NullablePutSupportedContractsDtoOracleInner{value: val, isSet: true} -} - -func (v NullablePutSupportedContractsDtoOracleInner) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullablePutSupportedContractsDtoOracleInner) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} diff --git a/api/lifiorder/model_quote_dto.go b/api/lifiorder/model_quote_dto.go index d9fd3ed9..19f93413 100644 --- a/api/lifiorder/model_quote_dto.go +++ b/api/lifiorder/model_quote_dto.go @@ -21,8 +21,7 @@ var _ MappedNullable = &QuoteDto{} // QuoteDto struct for QuoteDto type QuoteDto struct { - // Order details - Order map[string]interface{} `json:"order"` + Order QuoteDtoOrder `json:"order"` // Quote validity timestamp in unix timestamp (seconds) ValidUntil *float32 `json:"validUntil,omitempty"` // Estimated time of arrival in seconds @@ -47,7 +46,7 @@ type _QuoteDto QuoteDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewQuoteDto(order map[string]interface{}, quoteId string, provider string, preview QuotePreviewDto, failureHandling string, partialFill bool, metadata QuoteMetadataDto) *QuoteDto { +func NewQuoteDto(order QuoteDtoOrder, quoteId string, provider string, preview QuotePreviewDto, failureHandling string, partialFill bool, metadata QuoteMetadataDto) *QuoteDto { this := QuoteDto{} this.Order = order this.QuoteId = quoteId @@ -68,9 +67,9 @@ func NewQuoteDtoWithDefaults() *QuoteDto { } // GetOrder returns the Order field value -func (o *QuoteDto) GetOrder() map[string]interface{} { +func (o *QuoteDto) GetOrder() QuoteDtoOrder { if o == nil { - var ret map[string]interface{} + var ret QuoteDtoOrder return ret } @@ -79,15 +78,15 @@ func (o *QuoteDto) GetOrder() map[string]interface{} { // GetOrderOk returns a tuple with the Order field value // and a boolean to check if the value has been set. -func (o *QuoteDto) GetOrderOk() (map[string]interface{}, bool) { +func (o *QuoteDto) GetOrderOk() (*QuoteDtoOrder, bool) { if o == nil { - return map[string]interface{}{}, false + return nil, false } - return o.Order, true + return &o.Order, true } // SetOrder sets field value -func (o *QuoteDto) SetOrder(v map[string]interface{}) { +func (o *QuoteDto) SetOrder(v QuoteDtoOrder) { o.Order = v } diff --git a/api/lifiorder/model_quote_dto_order.go b/api/lifiorder/model_quote_dto_order.go new file mode 100644 index 00000000..cf1da7b2 --- /dev/null +++ b/api/lifiorder/model_quote_dto_order.go @@ -0,0 +1,206 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "encoding/json" + "fmt" + "gopkg.in/validator.v2" +) + +// QuoteDtoOrder - Order details +type QuoteDtoOrder struct { + Oif3009OrderDto *Oif3009OrderDto + OifEscrowOrderDto *OifEscrowOrderDto + OifUserOpenIntentOrderDto *OifUserOpenIntentOrderDto +} + +// Oif3009OrderDtoAsQuoteDtoOrder is a convenience function that returns Oif3009OrderDto wrapped in QuoteDtoOrder +func Oif3009OrderDtoAsQuoteDtoOrder(v *Oif3009OrderDto) QuoteDtoOrder { + return QuoteDtoOrder{ + Oif3009OrderDto: v, + } +} + +// OifEscrowOrderDtoAsQuoteDtoOrder is a convenience function that returns OifEscrowOrderDto wrapped in QuoteDtoOrder +func OifEscrowOrderDtoAsQuoteDtoOrder(v *OifEscrowOrderDto) QuoteDtoOrder { + return QuoteDtoOrder{ + OifEscrowOrderDto: v, + } +} + +// OifUserOpenIntentOrderDtoAsQuoteDtoOrder is a convenience function that returns OifUserOpenIntentOrderDto wrapped in QuoteDtoOrder +func OifUserOpenIntentOrderDtoAsQuoteDtoOrder(v *OifUserOpenIntentOrderDto) QuoteDtoOrder { + return QuoteDtoOrder{ + OifUserOpenIntentOrderDto: v, + } +} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *QuoteDtoOrder) UnmarshalJSON(data []byte) error { + var err error + match := 0 + // try to unmarshal data into Oif3009OrderDto + err = newStrictDecoder(data).Decode(&dst.Oif3009OrderDto) + if err == nil { + jsonOif3009OrderDto, _ := json.Marshal(dst.Oif3009OrderDto) + if string(jsonOif3009OrderDto) == "{}" { // empty struct + dst.Oif3009OrderDto = nil + } else { + if err = validator.Validate(dst.Oif3009OrderDto); err != nil { + dst.Oif3009OrderDto = nil + } else { + match++ + } + } + } else { + dst.Oif3009OrderDto = nil + } + + // try to unmarshal data into OifEscrowOrderDto + err = newStrictDecoder(data).Decode(&dst.OifEscrowOrderDto) + if err == nil { + jsonOifEscrowOrderDto, _ := json.Marshal(dst.OifEscrowOrderDto) + if string(jsonOifEscrowOrderDto) == "{}" { // empty struct + dst.OifEscrowOrderDto = nil + } else { + if err = validator.Validate(dst.OifEscrowOrderDto); err != nil { + dst.OifEscrowOrderDto = nil + } else { + match++ + } + } + } else { + dst.OifEscrowOrderDto = nil + } + + // try to unmarshal data into OifUserOpenIntentOrderDto + err = newStrictDecoder(data).Decode(&dst.OifUserOpenIntentOrderDto) + if err == nil { + jsonOifUserOpenIntentOrderDto, _ := json.Marshal(dst.OifUserOpenIntentOrderDto) + if string(jsonOifUserOpenIntentOrderDto) == "{}" { // empty struct + dst.OifUserOpenIntentOrderDto = nil + } else { + if err = validator.Validate(dst.OifUserOpenIntentOrderDto); err != nil { + dst.OifUserOpenIntentOrderDto = nil + } else { + match++ + } + } + } else { + dst.OifUserOpenIntentOrderDto = nil + } + + if match > 1 { // more than 1 match + // reset to nil + dst.Oif3009OrderDto = nil + dst.OifEscrowOrderDto = nil + dst.OifUserOpenIntentOrderDto = nil + + return fmt.Errorf("data matches more than one schema in oneOf(QuoteDtoOrder)") + } else if match == 1 { + return nil // exactly one match + } else { // no match + return fmt.Errorf("data failed to match schemas in oneOf(QuoteDtoOrder)") + } +} + +// Marshal data from the first non-nil pointers in the struct to JSON +func (src QuoteDtoOrder) MarshalJSON() ([]byte, error) { + if src.Oif3009OrderDto != nil { + return json.Marshal(&src.Oif3009OrderDto) + } + + if src.OifEscrowOrderDto != nil { + return json.Marshal(&src.OifEscrowOrderDto) + } + + if src.OifUserOpenIntentOrderDto != nil { + return json.Marshal(&src.OifUserOpenIntentOrderDto) + } + + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *QuoteDtoOrder) GetActualInstance() interface{} { + if obj == nil { + return nil + } + if obj.Oif3009OrderDto != nil { + return obj.Oif3009OrderDto + } + + if obj.OifEscrowOrderDto != nil { + return obj.OifEscrowOrderDto + } + + if obj.OifUserOpenIntentOrderDto != nil { + return obj.OifUserOpenIntentOrderDto + } + + // all schemas are nil + return nil +} + +// Get the actual instance value +func (obj QuoteDtoOrder) GetActualInstanceValue() interface{} { + if obj.Oif3009OrderDto != nil { + return *obj.Oif3009OrderDto + } + + if obj.OifEscrowOrderDto != nil { + return *obj.OifEscrowOrderDto + } + + if obj.OifUserOpenIntentOrderDto != nil { + return *obj.OifUserOpenIntentOrderDto + } + + // all schemas are nil + return nil +} + +type NullableQuoteDtoOrder struct { + value *QuoteDtoOrder + isSet bool +} + +func (v NullableQuoteDtoOrder) Get() *QuoteDtoOrder { + return v.value +} + +func (v *NullableQuoteDtoOrder) Set(val *QuoteDtoOrder) { + v.value = val + v.isSet = true +} + +func (v NullableQuoteDtoOrder) IsSet() bool { + return v.isSet +} + +func (v *NullableQuoteDtoOrder) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableQuoteDtoOrder(val *QuoteDtoOrder) *NullableQuoteDtoOrder { + return &NullableQuoteDtoOrder{value: val, isSet: true} +} + +func (v NullableQuoteDtoOrder) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableQuoteDtoOrder) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_quote_metadata_dto.go b/api/lifiorder/model_quote_metadata_dto.go index 1ff55166..d31fbc6b 100644 --- a/api/lifiorder/model_quote_metadata_dto.go +++ b/api/lifiorder/model_quote_metadata_dto.go @@ -22,7 +22,7 @@ var _ MappedNullable = &QuoteMetadataDto{} // QuoteMetadataDto struct for QuoteMetadataDto type QuoteMetadataDto struct { // Exclusive for address (hex32) - solver address that can fill this quote, or null - ExclusiveFor map[string]interface{} `json:"exclusiveFor"` + ExclusiveFor NullableString `json:"exclusiveFor"` } type _QuoteMetadataDto QuoteMetadataDto @@ -31,7 +31,7 @@ type _QuoteMetadataDto QuoteMetadataDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewQuoteMetadataDto(exclusiveFor map[string]interface{}) *QuoteMetadataDto { +func NewQuoteMetadataDto(exclusiveFor NullableString) *QuoteMetadataDto { this := QuoteMetadataDto{} this.ExclusiveFor = exclusiveFor return &this @@ -46,29 +46,29 @@ func NewQuoteMetadataDtoWithDefaults() *QuoteMetadataDto { } // GetExclusiveFor returns the ExclusiveFor field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *QuoteMetadataDto) GetExclusiveFor() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *QuoteMetadataDto) GetExclusiveFor() string { + if o == nil || o.ExclusiveFor.Get() == nil { + var ret string return ret } - return o.ExclusiveFor + return *o.ExclusiveFor.Get() } // GetExclusiveForOk returns a tuple with the ExclusiveFor field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *QuoteMetadataDto) GetExclusiveForOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.ExclusiveFor) { - return map[string]interface{}{}, false +func (o *QuoteMetadataDto) GetExclusiveForOk() (*string, bool) { + if o == nil { + return nil, false } - return o.ExclusiveFor, true + return o.ExclusiveFor.Get(), o.ExclusiveFor.IsSet() } // SetExclusiveFor sets field value -func (o *QuoteMetadataDto) SetExclusiveFor(v map[string]interface{}) { - o.ExclusiveFor = v +func (o *QuoteMetadataDto) SetExclusiveFor(v string) { + o.ExclusiveFor.Set(&v) } func (o QuoteMetadataDto) MarshalJSON() ([]byte, error) { @@ -81,9 +81,7 @@ func (o QuoteMetadataDto) MarshalJSON() ([]byte, error) { func (o QuoteMetadataDto) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} - if o.ExclusiveFor != nil { - toSerialize["exclusiveFor"] = o.ExclusiveFor - } + toSerialize["exclusiveFor"] = o.ExclusiveFor.Get() return toSerialize, nil } diff --git a/api/lifiorder/model_quote_preview_dto.go b/api/lifiorder/model_quote_preview_dto.go index ebde0bdd..55300168 100644 --- a/api/lifiorder/model_quote_preview_dto.go +++ b/api/lifiorder/model_quote_preview_dto.go @@ -22,9 +22,9 @@ var _ MappedNullable = &QuotePreviewDto{} // QuotePreviewDto struct for QuotePreviewDto type QuotePreviewDto struct { // Inputs for the preview - Inputs []QuotePreviewDtoInputsInner `json:"inputs"` + Inputs []InputDto `json:"inputs"` // Outputs for the preview - Outputs []QuotePreviewDtoOutputsInner `json:"outputs"` + Outputs []OutputDto `json:"outputs"` } type _QuotePreviewDto QuotePreviewDto @@ -33,7 +33,7 @@ type _QuotePreviewDto QuotePreviewDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewQuotePreviewDto(inputs []QuotePreviewDtoInputsInner, outputs []QuotePreviewDtoOutputsInner) *QuotePreviewDto { +func NewQuotePreviewDto(inputs []InputDto, outputs []OutputDto) *QuotePreviewDto { this := QuotePreviewDto{} this.Inputs = inputs this.Outputs = outputs @@ -49,9 +49,9 @@ func NewQuotePreviewDtoWithDefaults() *QuotePreviewDto { } // GetInputs returns the Inputs field value -func (o *QuotePreviewDto) GetInputs() []QuotePreviewDtoInputsInner { +func (o *QuotePreviewDto) GetInputs() []InputDto { if o == nil { - var ret []QuotePreviewDtoInputsInner + var ret []InputDto return ret } @@ -60,7 +60,7 @@ func (o *QuotePreviewDto) GetInputs() []QuotePreviewDtoInputsInner { // GetInputsOk returns a tuple with the Inputs field value // and a boolean to check if the value has been set. -func (o *QuotePreviewDto) GetInputsOk() ([]QuotePreviewDtoInputsInner, bool) { +func (o *QuotePreviewDto) GetInputsOk() ([]InputDto, bool) { if o == nil { return nil, false } @@ -68,14 +68,14 @@ func (o *QuotePreviewDto) GetInputsOk() ([]QuotePreviewDtoInputsInner, bool) { } // SetInputs sets field value -func (o *QuotePreviewDto) SetInputs(v []QuotePreviewDtoInputsInner) { +func (o *QuotePreviewDto) SetInputs(v []InputDto) { o.Inputs = v } // GetOutputs returns the Outputs field value -func (o *QuotePreviewDto) GetOutputs() []QuotePreviewDtoOutputsInner { +func (o *QuotePreviewDto) GetOutputs() []OutputDto { if o == nil { - var ret []QuotePreviewDtoOutputsInner + var ret []OutputDto return ret } @@ -84,7 +84,7 @@ func (o *QuotePreviewDto) GetOutputs() []QuotePreviewDtoOutputsInner { // GetOutputsOk returns a tuple with the Outputs field value // and a boolean to check if the value has been set. -func (o *QuotePreviewDto) GetOutputsOk() ([]QuotePreviewDtoOutputsInner, bool) { +func (o *QuotePreviewDto) GetOutputsOk() ([]OutputDto, bool) { if o == nil { return nil, false } @@ -92,7 +92,7 @@ func (o *QuotePreviewDto) GetOutputsOk() ([]QuotePreviewDtoOutputsInner, bool) { } // SetOutputs sets field value -func (o *QuotePreviewDto) SetOutputs(v []QuotePreviewDtoOutputsInner) { +func (o *QuotePreviewDto) SetOutputs(v []OutputDto) { o.Outputs = v } diff --git a/api/lifiorder/model_quote_request_dto_intent.go b/api/lifiorder/model_quote_request_dto_intent.go index e007380e..84691201 100644 --- a/api/lifiorder/model_quote_request_dto_intent.go +++ b/api/lifiorder/model_quote_request_dto_intent.go @@ -32,8 +32,7 @@ type QuoteRequestDtoIntent struct { // Minimum validity timestamp in unix timestamp (seconds). Only select solver quotes with longer TTL. MinValidUntil *float32 `json:"minValidUntil,omitempty"` // Quote preference (unsupported, ignored if provided) - Preference *string `json:"preference,omitempty"` - // Explicit preference for submission responsibility and acceptable auth schemes. Shape: { mode: \"user\" | \"protocol\", auth?: string[] }. Unsupported, ignored for now - needs gasless feature. + Preference *string `json:"preference,omitempty"` OriginSubmission interface{} `json:"originSubmission,omitempty"` // Failure handling policy for execution that the integrator supports (unsupported, ignored) FailureHandling []string `json:"failureHandling,omitempty"` diff --git a/api/lifiorder/model_quote_request_dto_intent_inputs_inner.go b/api/lifiorder/model_quote_request_dto_intent_inputs_inner.go index f968ac6b..f4ef7ec1 100644 --- a/api/lifiorder/model_quote_request_dto_intent_inputs_inner.go +++ b/api/lifiorder/model_quote_request_dto_intent_inputs_inner.go @@ -26,11 +26,10 @@ type QuoteRequestDtoIntentInputsInner struct { // Native address of the user providing the input assets User string `json:"user"` // Native address of the token/asset being provided as input - Asset string `json:"asset"` - // Amount available. For exact-input: exact amount user will provide and is used for quoting. For exact-output: ignored by the quote decoder and not used for quoting + Asset string `json:"asset"` Amount NullableString `json:"amount,omitempty"` // Optional lock reference for securing the input assets. Shape: { kind: \"the-compact\" | \"rhinestone\", params: object }. Currently ignored by the quote decoder. - Lock interface{} `json:"lock,omitempty"` + Lock map[string]interface{} `json:"lock,omitempty"` } type _QuoteRequestDtoIntentInputsInner QuoteRequestDtoIntentInputsInner @@ -170,10 +169,10 @@ func (o *QuoteRequestDtoIntentInputsInner) UnsetAmount() { o.Amount.Unset() } -// GetLock returns the Lock field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *QuoteRequestDtoIntentInputsInner) GetLock() interface{} { - if o == nil { - var ret interface{} +// GetLock returns the Lock field value if set, zero value otherwise. +func (o *QuoteRequestDtoIntentInputsInner) GetLock() map[string]interface{} { + if o == nil || IsNil(o.Lock) { + var ret map[string]interface{} return ret } return o.Lock @@ -181,12 +180,11 @@ func (o *QuoteRequestDtoIntentInputsInner) GetLock() interface{} { // GetLockOk returns a tuple with the Lock field value if set, nil otherwise // and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *QuoteRequestDtoIntentInputsInner) GetLockOk() (*interface{}, bool) { +func (o *QuoteRequestDtoIntentInputsInner) GetLockOk() (map[string]interface{}, bool) { if o == nil || IsNil(o.Lock) { - return nil, false + return map[string]interface{}{}, false } - return &o.Lock, true + return o.Lock, true } // HasLock returns a boolean if a field has been set. @@ -198,8 +196,8 @@ func (o *QuoteRequestDtoIntentInputsInner) HasLock() bool { return false } -// SetLock gets a reference to the given interface{} and assigns it to the Lock field. -func (o *QuoteRequestDtoIntentInputsInner) SetLock(v interface{}) { +// SetLock gets a reference to the given map[string]interface{} and assigns it to the Lock field. +func (o *QuoteRequestDtoIntentInputsInner) SetLock(v map[string]interface{}) { o.Lock = v } @@ -219,7 +217,7 @@ func (o QuoteRequestDtoIntentInputsInner) ToMap() (map[string]interface{}, error if o.Amount.IsSet() { toSerialize["amount"] = o.Amount.Get() } - if o.Lock != nil { + if !IsNil(o.Lock) { toSerialize["lock"] = o.Lock } return toSerialize, nil diff --git a/api/lifiorder/model_quote_request_dto_intent_metadata.go b/api/lifiorder/model_quote_request_dto_intent_metadata.go index efa12441..f9f185dc 100644 --- a/api/lifiorder/model_quote_request_dto_intent_metadata.go +++ b/api/lifiorder/model_quote_request_dto_intent_metadata.go @@ -20,6 +20,12 @@ var _ MappedNullable = &QuoteRequestDtoIntentMetadata{} // QuoteRequestDtoIntentMetadata Metadata for the order, never required, potentially contains provider specific data type QuoteRequestDtoIntentMetadata struct { ExclusiveFor *QuoteRequestDtoIntentMetadataExclusiveFor `json:"exclusiveFor,omitempty"` + // Accepted cross-chain verifier (oracle) contracts, each a { chain, address } object. When provided, only solvers that support one of these oracles can answer, and the returned order is built to settle against an accepted oracle. Omitted or empty means any oracle is acceptable. Ignored for same-chain swaps. + Oracle []QuoteRequestDtoIntentMetadataOracleInner `json:"oracle,omitempty"` + // Accepted input settler contracts, each a { chain, address } object. When provided, a quote is only returned if the order is built with one of these input settlers and the winning solver supports it. Omitted or empty means any input settler is acceptable. + InputSettler []QuoteRequestDtoIntentMetadataOracleInner `json:"inputSettler,omitempty"` + // Accepted output settler contracts, each a { chain, address } object. When provided, a quote is only returned if the order is built with one of these output settlers and the winning solver supports it. Omitted or empty means any output settler is acceptable. + OutputSettler []QuoteRequestDtoIntentMetadataOracleInner `json:"outputSettler,omitempty"` } // NewQuoteRequestDtoIntentMetadata instantiates a new QuoteRequestDtoIntentMetadata object @@ -71,6 +77,102 @@ func (o *QuoteRequestDtoIntentMetadata) SetExclusiveFor(v QuoteRequestDtoIntentM o.ExclusiveFor = &v } +// GetOracle returns the Oracle field value if set, zero value otherwise. +func (o *QuoteRequestDtoIntentMetadata) GetOracle() []QuoteRequestDtoIntentMetadataOracleInner { + if o == nil || IsNil(o.Oracle) { + var ret []QuoteRequestDtoIntentMetadataOracleInner + return ret + } + return o.Oracle +} + +// GetOracleOk returns a tuple with the Oracle field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *QuoteRequestDtoIntentMetadata) GetOracleOk() ([]QuoteRequestDtoIntentMetadataOracleInner, bool) { + if o == nil || IsNil(o.Oracle) { + return nil, false + } + return o.Oracle, true +} + +// HasOracle returns a boolean if a field has been set. +func (o *QuoteRequestDtoIntentMetadata) HasOracle() bool { + if o != nil && !IsNil(o.Oracle) { + return true + } + + return false +} + +// SetOracle gets a reference to the given []QuoteRequestDtoIntentMetadataOracleInner and assigns it to the Oracle field. +func (o *QuoteRequestDtoIntentMetadata) SetOracle(v []QuoteRequestDtoIntentMetadataOracleInner) { + o.Oracle = v +} + +// GetInputSettler returns the InputSettler field value if set, zero value otherwise. +func (o *QuoteRequestDtoIntentMetadata) GetInputSettler() []QuoteRequestDtoIntentMetadataOracleInner { + if o == nil || IsNil(o.InputSettler) { + var ret []QuoteRequestDtoIntentMetadataOracleInner + return ret + } + return o.InputSettler +} + +// GetInputSettlerOk returns a tuple with the InputSettler field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *QuoteRequestDtoIntentMetadata) GetInputSettlerOk() ([]QuoteRequestDtoIntentMetadataOracleInner, bool) { + if o == nil || IsNil(o.InputSettler) { + return nil, false + } + return o.InputSettler, true +} + +// HasInputSettler returns a boolean if a field has been set. +func (o *QuoteRequestDtoIntentMetadata) HasInputSettler() bool { + if o != nil && !IsNil(o.InputSettler) { + return true + } + + return false +} + +// SetInputSettler gets a reference to the given []QuoteRequestDtoIntentMetadataOracleInner and assigns it to the InputSettler field. +func (o *QuoteRequestDtoIntentMetadata) SetInputSettler(v []QuoteRequestDtoIntentMetadataOracleInner) { + o.InputSettler = v +} + +// GetOutputSettler returns the OutputSettler field value if set, zero value otherwise. +func (o *QuoteRequestDtoIntentMetadata) GetOutputSettler() []QuoteRequestDtoIntentMetadataOracleInner { + if o == nil || IsNil(o.OutputSettler) { + var ret []QuoteRequestDtoIntentMetadataOracleInner + return ret + } + return o.OutputSettler +} + +// GetOutputSettlerOk returns a tuple with the OutputSettler field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *QuoteRequestDtoIntentMetadata) GetOutputSettlerOk() ([]QuoteRequestDtoIntentMetadataOracleInner, bool) { + if o == nil || IsNil(o.OutputSettler) { + return nil, false + } + return o.OutputSettler, true +} + +// HasOutputSettler returns a boolean if a field has been set. +func (o *QuoteRequestDtoIntentMetadata) HasOutputSettler() bool { + if o != nil && !IsNil(o.OutputSettler) { + return true + } + + return false +} + +// SetOutputSettler gets a reference to the given []QuoteRequestDtoIntentMetadataOracleInner and assigns it to the OutputSettler field. +func (o *QuoteRequestDtoIntentMetadata) SetOutputSettler(v []QuoteRequestDtoIntentMetadataOracleInner) { + o.OutputSettler = v +} + func (o QuoteRequestDtoIntentMetadata) MarshalJSON() ([]byte, error) { toSerialize, err := o.ToMap() if err != nil { @@ -84,6 +186,15 @@ func (o QuoteRequestDtoIntentMetadata) ToMap() (map[string]interface{}, error) { if !IsNil(o.ExclusiveFor) { toSerialize["exclusiveFor"] = o.ExclusiveFor } + if !IsNil(o.Oracle) { + toSerialize["oracle"] = o.Oracle + } + if !IsNil(o.InputSettler) { + toSerialize["inputSettler"] = o.InputSettler + } + if !IsNil(o.OutputSettler) { + toSerialize["outputSettler"] = o.OutputSettler + } return toSerialize, nil } diff --git a/api/lifiorder/model_quote_request_dto_intent_metadata_oracle_inner.go b/api/lifiorder/model_quote_request_dto_intent_metadata_oracle_inner.go new file mode 100644 index 00000000..9915f8b0 --- /dev/null +++ b/api/lifiorder/model_quote_request_dto_intent_metadata_oracle_inner.go @@ -0,0 +1,186 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the QuoteRequestDtoIntentMetadataOracleInner type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &QuoteRequestDtoIntentMetadataOracleInner{} + +// QuoteRequestDtoIntentMetadataOracleInner struct for QuoteRequestDtoIntentMetadataOracleInner +type QuoteRequestDtoIntentMetadataOracleInner struct { + // CAIP-2 chain identifier, e.g. \"eip155:1\" + Chain string `json:"chain"` + // Native contract address for the chain + Address string `json:"address"` +} + +type _QuoteRequestDtoIntentMetadataOracleInner QuoteRequestDtoIntentMetadataOracleInner + +// NewQuoteRequestDtoIntentMetadataOracleInner instantiates a new QuoteRequestDtoIntentMetadataOracleInner object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewQuoteRequestDtoIntentMetadataOracleInner(chain string, address string) *QuoteRequestDtoIntentMetadataOracleInner { + this := QuoteRequestDtoIntentMetadataOracleInner{} + this.Chain = chain + this.Address = address + return &this +} + +// NewQuoteRequestDtoIntentMetadataOracleInnerWithDefaults instantiates a new QuoteRequestDtoIntentMetadataOracleInner object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewQuoteRequestDtoIntentMetadataOracleInnerWithDefaults() *QuoteRequestDtoIntentMetadataOracleInner { + this := QuoteRequestDtoIntentMetadataOracleInner{} + return &this +} + +// GetChain returns the Chain field value +func (o *QuoteRequestDtoIntentMetadataOracleInner) GetChain() string { + if o == nil { + var ret string + return ret + } + + return o.Chain +} + +// GetChainOk returns a tuple with the Chain field value +// and a boolean to check if the value has been set. +func (o *QuoteRequestDtoIntentMetadataOracleInner) GetChainOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Chain, true +} + +// SetChain sets field value +func (o *QuoteRequestDtoIntentMetadataOracleInner) SetChain(v string) { + o.Chain = v +} + +// GetAddress returns the Address field value +func (o *QuoteRequestDtoIntentMetadataOracleInner) GetAddress() string { + if o == nil { + var ret string + return ret + } + + return o.Address +} + +// GetAddressOk returns a tuple with the Address field value +// and a boolean to check if the value has been set. +func (o *QuoteRequestDtoIntentMetadataOracleInner) GetAddressOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Address, true +} + +// SetAddress sets field value +func (o *QuoteRequestDtoIntentMetadataOracleInner) SetAddress(v string) { + o.Address = v +} + +func (o QuoteRequestDtoIntentMetadataOracleInner) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o QuoteRequestDtoIntentMetadataOracleInner) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["chain"] = o.Chain + toSerialize["address"] = o.Address + return toSerialize, nil +} + +func (o *QuoteRequestDtoIntentMetadataOracleInner) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "chain", + "address", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varQuoteRequestDtoIntentMetadataOracleInner := _QuoteRequestDtoIntentMetadataOracleInner{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varQuoteRequestDtoIntentMetadataOracleInner) + + if err != nil { + return err + } + + *o = QuoteRequestDtoIntentMetadataOracleInner(varQuoteRequestDtoIntentMetadataOracleInner) + + return err +} + +type NullableQuoteRequestDtoIntentMetadataOracleInner struct { + value *QuoteRequestDtoIntentMetadataOracleInner + isSet bool +} + +func (v NullableQuoteRequestDtoIntentMetadataOracleInner) Get() *QuoteRequestDtoIntentMetadataOracleInner { + return v.value +} + +func (v *NullableQuoteRequestDtoIntentMetadataOracleInner) Set(val *QuoteRequestDtoIntentMetadataOracleInner) { + v.value = val + v.isSet = true +} + +func (v NullableQuoteRequestDtoIntentMetadataOracleInner) IsSet() bool { + return v.isSet +} + +func (v *NullableQuoteRequestDtoIntentMetadataOracleInner) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableQuoteRequestDtoIntentMetadataOracleInner(val *QuoteRequestDtoIntentMetadataOracleInner) *NullableQuoteRequestDtoIntentMetadataOracleInner { + return &NullableQuoteRequestDtoIntentMetadataOracleInner{value: val, isSet: true} +} + +func (v NullableQuoteRequestDtoIntentMetadataOracleInner) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableQuoteRequestDtoIntentMetadataOracleInner) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_quote_request_dto_intent_outputs_inner.go b/api/lifiorder/model_quote_request_dto_intent_outputs_inner.go index 4607dc0b..e5105f7c 100644 --- a/api/lifiorder/model_quote_request_dto_intent_outputs_inner.go +++ b/api/lifiorder/model_quote_request_dto_intent_outputs_inner.go @@ -26,8 +26,7 @@ type QuoteRequestDtoIntentOutputsInner struct { // Native address that will receive the output assets Receiver string `json:"receiver"` // Native address of the token/asset to be received as output - Asset string `json:"asset"` - // For exact-input: ignored by the quote decoder and not used for quoting. For exact-output: exact amount user wants to receive and is used for quoting + Asset string `json:"asset"` Amount NullableString `json:"amount,omitempty"` // Optional calldata describing how the receiver will consume the output. Enables composability with other protocols Calldata *string `json:"calldata,omitempty"` diff --git a/api/lifiorder/model_solver_quote_dto.go b/api/lifiorder/model_solver_quote_dto.go index 0f54897e..4a814ba2 100644 --- a/api/lifiorder/model_solver_quote_dto.go +++ b/api/lifiorder/model_solver_quote_dto.go @@ -52,19 +52,19 @@ type SolverQuoteDto struct { // Maximum amount for this quote range MaxAmount string `json:"maxAmount"` // Exclusive for address - ExclusiveFor map[string]interface{} `json:"exclusiveFor"` + ExclusiveFor NullableString `json:"exclusiveFor"` // Source asset record ID - FromAssetRecordId map[string]interface{} `json:"fromAssetRecordId"` + FromAssetRecordId NullableFloat32 `json:"fromAssetRecordId"` // Destination asset record ID - ToAssetRecordId map[string]interface{} `json:"toAssetRecordId"` + ToAssetRecordId NullableFloat32 `json:"toAssetRecordId"` // Source chain record ID - FromChainRecordId map[string]interface{} `json:"fromChainRecordId"` + FromChainRecordId NullableFloat32 `json:"fromChainRecordId"` // Destination chain record ID - ToChainRecordId map[string]interface{} `json:"toChainRecordId"` + ToChainRecordId NullableFloat32 `json:"toChainRecordId"` // Associated solver ID SolverId float32 `json:"solverId"` // Integrator key hash this quote is tagged for, or null for open-market quotes - IntegratorKeyHash map[string]interface{} `json:"integratorKeyHash,omitempty"` + IntegratorKeyHash NullableString `json:"integratorKeyHash,omitempty"` } type _SolverQuoteDto SolverQuoteDto @@ -73,7 +73,7 @@ type _SolverQuoteDto SolverQuoteDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewSolverQuoteDto(id string, createdAt string, updatedAt string, fromChainNetworkId string, toChainNetworkId string, fromAssetAddress string, toAssetAddress string, fromAssetDecimals float32, toAssetDecimals float32, fromDecimals float32, toDecimals float32, expiry string, quote string, minAmount string, maxAmount string, exclusiveFor map[string]interface{}, fromAssetRecordId map[string]interface{}, toAssetRecordId map[string]interface{}, fromChainRecordId map[string]interface{}, toChainRecordId map[string]interface{}, solverId float32) *SolverQuoteDto { +func NewSolverQuoteDto(id string, createdAt string, updatedAt string, fromChainNetworkId string, toChainNetworkId string, fromAssetAddress string, toAssetAddress string, fromAssetDecimals float32, toAssetDecimals float32, fromDecimals float32, toDecimals float32, expiry string, quote string, minAmount string, maxAmount string, exclusiveFor NullableString, fromAssetRecordId NullableFloat32, toAssetRecordId NullableFloat32, fromChainRecordId NullableFloat32, toChainRecordId NullableFloat32, solverId float32) *SolverQuoteDto { this := SolverQuoteDto{} this.Id = id this.CreatedAt = createdAt @@ -468,133 +468,133 @@ func (o *SolverQuoteDto) SetMaxAmount(v string) { } // GetExclusiveFor returns the ExclusiveFor field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *SolverQuoteDto) GetExclusiveFor() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *SolverQuoteDto) GetExclusiveFor() string { + if o == nil || o.ExclusiveFor.Get() == nil { + var ret string return ret } - return o.ExclusiveFor + return *o.ExclusiveFor.Get() } // GetExclusiveForOk returns a tuple with the ExclusiveFor field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SolverQuoteDto) GetExclusiveForOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.ExclusiveFor) { - return map[string]interface{}{}, false +func (o *SolverQuoteDto) GetExclusiveForOk() (*string, bool) { + if o == nil { + return nil, false } - return o.ExclusiveFor, true + return o.ExclusiveFor.Get(), o.ExclusiveFor.IsSet() } // SetExclusiveFor sets field value -func (o *SolverQuoteDto) SetExclusiveFor(v map[string]interface{}) { - o.ExclusiveFor = v +func (o *SolverQuoteDto) SetExclusiveFor(v string) { + o.ExclusiveFor.Set(&v) } // GetFromAssetRecordId returns the FromAssetRecordId field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *SolverQuoteDto) GetFromAssetRecordId() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for float32 will be returned +func (o *SolverQuoteDto) GetFromAssetRecordId() float32 { + if o == nil || o.FromAssetRecordId.Get() == nil { + var ret float32 return ret } - return o.FromAssetRecordId + return *o.FromAssetRecordId.Get() } // GetFromAssetRecordIdOk returns a tuple with the FromAssetRecordId field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SolverQuoteDto) GetFromAssetRecordIdOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.FromAssetRecordId) { - return map[string]interface{}{}, false +func (o *SolverQuoteDto) GetFromAssetRecordIdOk() (*float32, bool) { + if o == nil { + return nil, false } - return o.FromAssetRecordId, true + return o.FromAssetRecordId.Get(), o.FromAssetRecordId.IsSet() } // SetFromAssetRecordId sets field value -func (o *SolverQuoteDto) SetFromAssetRecordId(v map[string]interface{}) { - o.FromAssetRecordId = v +func (o *SolverQuoteDto) SetFromAssetRecordId(v float32) { + o.FromAssetRecordId.Set(&v) } // GetToAssetRecordId returns the ToAssetRecordId field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *SolverQuoteDto) GetToAssetRecordId() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for float32 will be returned +func (o *SolverQuoteDto) GetToAssetRecordId() float32 { + if o == nil || o.ToAssetRecordId.Get() == nil { + var ret float32 return ret } - return o.ToAssetRecordId + return *o.ToAssetRecordId.Get() } // GetToAssetRecordIdOk returns a tuple with the ToAssetRecordId field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SolverQuoteDto) GetToAssetRecordIdOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.ToAssetRecordId) { - return map[string]interface{}{}, false +func (o *SolverQuoteDto) GetToAssetRecordIdOk() (*float32, bool) { + if o == nil { + return nil, false } - return o.ToAssetRecordId, true + return o.ToAssetRecordId.Get(), o.ToAssetRecordId.IsSet() } // SetToAssetRecordId sets field value -func (o *SolverQuoteDto) SetToAssetRecordId(v map[string]interface{}) { - o.ToAssetRecordId = v +func (o *SolverQuoteDto) SetToAssetRecordId(v float32) { + o.ToAssetRecordId.Set(&v) } // GetFromChainRecordId returns the FromChainRecordId field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *SolverQuoteDto) GetFromChainRecordId() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for float32 will be returned +func (o *SolverQuoteDto) GetFromChainRecordId() float32 { + if o == nil || o.FromChainRecordId.Get() == nil { + var ret float32 return ret } - return o.FromChainRecordId + return *o.FromChainRecordId.Get() } // GetFromChainRecordIdOk returns a tuple with the FromChainRecordId field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SolverQuoteDto) GetFromChainRecordIdOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.FromChainRecordId) { - return map[string]interface{}{}, false +func (o *SolverQuoteDto) GetFromChainRecordIdOk() (*float32, bool) { + if o == nil { + return nil, false } - return o.FromChainRecordId, true + return o.FromChainRecordId.Get(), o.FromChainRecordId.IsSet() } // SetFromChainRecordId sets field value -func (o *SolverQuoteDto) SetFromChainRecordId(v map[string]interface{}) { - o.FromChainRecordId = v +func (o *SolverQuoteDto) SetFromChainRecordId(v float32) { + o.FromChainRecordId.Set(&v) } // GetToChainRecordId returns the ToChainRecordId field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *SolverQuoteDto) GetToChainRecordId() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for float32 will be returned +func (o *SolverQuoteDto) GetToChainRecordId() float32 { + if o == nil || o.ToChainRecordId.Get() == nil { + var ret float32 return ret } - return o.ToChainRecordId + return *o.ToChainRecordId.Get() } // GetToChainRecordIdOk returns a tuple with the ToChainRecordId field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SolverQuoteDto) GetToChainRecordIdOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.ToChainRecordId) { - return map[string]interface{}{}, false +func (o *SolverQuoteDto) GetToChainRecordIdOk() (*float32, bool) { + if o == nil { + return nil, false } - return o.ToChainRecordId, true + return o.ToChainRecordId.Get(), o.ToChainRecordId.IsSet() } // SetToChainRecordId sets field value -func (o *SolverQuoteDto) SetToChainRecordId(v map[string]interface{}) { - o.ToChainRecordId = v +func (o *SolverQuoteDto) SetToChainRecordId(v float32) { + o.ToChainRecordId.Set(&v) } // GetSolverId returns the SolverId field value @@ -622,36 +622,46 @@ func (o *SolverQuoteDto) SetSolverId(v float32) { } // GetIntegratorKeyHash returns the IntegratorKeyHash field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *SolverQuoteDto) GetIntegratorKeyHash() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +func (o *SolverQuoteDto) GetIntegratorKeyHash() string { + if o == nil || IsNil(o.IntegratorKeyHash.Get()) { + var ret string return ret } - return o.IntegratorKeyHash + return *o.IntegratorKeyHash.Get() } // GetIntegratorKeyHashOk returns a tuple with the IntegratorKeyHash field value if set, nil otherwise // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SolverQuoteDto) GetIntegratorKeyHashOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.IntegratorKeyHash) { - return map[string]interface{}{}, false +func (o *SolverQuoteDto) GetIntegratorKeyHashOk() (*string, bool) { + if o == nil { + return nil, false } - return o.IntegratorKeyHash, true + return o.IntegratorKeyHash.Get(), o.IntegratorKeyHash.IsSet() } // HasIntegratorKeyHash returns a boolean if a field has been set. func (o *SolverQuoteDto) HasIntegratorKeyHash() bool { - if o != nil && !IsNil(o.IntegratorKeyHash) { + if o != nil && o.IntegratorKeyHash.IsSet() { return true } return false } -// SetIntegratorKeyHash gets a reference to the given map[string]interface{} and assigns it to the IntegratorKeyHash field. -func (o *SolverQuoteDto) SetIntegratorKeyHash(v map[string]interface{}) { - o.IntegratorKeyHash = v +// SetIntegratorKeyHash gets a reference to the given NullableString and assigns it to the IntegratorKeyHash field. +func (o *SolverQuoteDto) SetIntegratorKeyHash(v string) { + o.IntegratorKeyHash.Set(&v) +} + +// SetIntegratorKeyHashNil sets the value for IntegratorKeyHash to be an explicit nil +func (o *SolverQuoteDto) SetIntegratorKeyHashNil() { + o.IntegratorKeyHash.Set(nil) +} + +// UnsetIntegratorKeyHash ensures that no value is present for IntegratorKeyHash, not even an explicit nil +func (o *SolverQuoteDto) UnsetIntegratorKeyHash() { + o.IntegratorKeyHash.Unset() } func (o SolverQuoteDto) MarshalJSON() ([]byte, error) { @@ -679,24 +689,14 @@ func (o SolverQuoteDto) ToMap() (map[string]interface{}, error) { toSerialize["quote"] = o.Quote toSerialize["minAmount"] = o.MinAmount toSerialize["maxAmount"] = o.MaxAmount - if o.ExclusiveFor != nil { - toSerialize["exclusiveFor"] = o.ExclusiveFor - } - if o.FromAssetRecordId != nil { - toSerialize["fromAssetRecordId"] = o.FromAssetRecordId - } - if o.ToAssetRecordId != nil { - toSerialize["toAssetRecordId"] = o.ToAssetRecordId - } - if o.FromChainRecordId != nil { - toSerialize["fromChainRecordId"] = o.FromChainRecordId - } - if o.ToChainRecordId != nil { - toSerialize["toChainRecordId"] = o.ToChainRecordId - } + toSerialize["exclusiveFor"] = o.ExclusiveFor.Get() + toSerialize["fromAssetRecordId"] = o.FromAssetRecordId.Get() + toSerialize["toAssetRecordId"] = o.ToAssetRecordId.Get() + toSerialize["fromChainRecordId"] = o.FromChainRecordId.Get() + toSerialize["toChainRecordId"] = o.ToChainRecordId.Get() toSerialize["solverId"] = o.SolverId - if o.IntegratorKeyHash != nil { - toSerialize["integratorKeyHash"] = o.IntegratorKeyHash + if o.IntegratorKeyHash.IsSet() { + toSerialize["integratorKeyHash"] = o.IntegratorKeyHash.Get() } return toSerialize, nil } diff --git a/api/lifiorder/model_submit_order_dto_order.go b/api/lifiorder/model_submit_order_dto_order.go index afa4b984..8b12ed86 100644 --- a/api/lifiorder/model_submit_order_dto_order.go +++ b/api/lifiorder/model_submit_order_dto_order.go @@ -24,17 +24,17 @@ type SubmitOrderDtoOrder struct { // User address on source chain (initiator of the intent) User string `json:"user"` // Nonce value of the intent - Nonce *string `json:"nonce,omitempty"` + Nonce string `json:"nonce"` // Origin chain ID (network id) - OriginChainId *string `json:"originChainId,omitempty"` + OriginChainId string `json:"originChainId"` // Fill deadline of the intent in seconds - FillDeadline *string `json:"fillDeadline,omitempty"` + FillDeadline string `json:"fillDeadline"` // Expiry timestamp of the intent in seconds - Expires *string `json:"expires,omitempty"` + Expires string `json:"expires"` // The local oracle address InputOracle string `json:"inputOracle"` // Input token amounts as [tokenId, amount] pairs - Inputs [][]string `json:"inputs"` + Inputs [][]interface{} `json:"inputs"` // Array of output objects Outputs []SubmitOrderDtoOrderOutputsInner `json:"outputs"` } @@ -45,9 +45,13 @@ type _SubmitOrderDtoOrder SubmitOrderDtoOrder // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewSubmitOrderDtoOrder(user string, inputOracle string, inputs [][]string, outputs []SubmitOrderDtoOrderOutputsInner) *SubmitOrderDtoOrder { +func NewSubmitOrderDtoOrder(user string, nonce string, originChainId string, fillDeadline string, expires string, inputOracle string, inputs [][]interface{}, outputs []SubmitOrderDtoOrderOutputsInner) *SubmitOrderDtoOrder { this := SubmitOrderDtoOrder{} this.User = user + this.Nonce = nonce + this.OriginChainId = originChainId + this.FillDeadline = fillDeadline + this.Expires = expires this.InputOracle = inputOracle this.Inputs = inputs this.Outputs = outputs @@ -86,132 +90,100 @@ func (o *SubmitOrderDtoOrder) SetUser(v string) { o.User = v } -// GetNonce returns the Nonce field value if set, zero value otherwise. +// GetNonce returns the Nonce field value func (o *SubmitOrderDtoOrder) GetNonce() string { - if o == nil || IsNil(o.Nonce) { + if o == nil { var ret string return ret } - return *o.Nonce + + return o.Nonce } -// GetNonceOk returns a tuple with the Nonce field value if set, nil otherwise +// GetNonceOk returns a tuple with the Nonce field value // and a boolean to check if the value has been set. func (o *SubmitOrderDtoOrder) GetNonceOk() (*string, bool) { - if o == nil || IsNil(o.Nonce) { + if o == nil { return nil, false } - return o.Nonce, true -} - -// HasNonce returns a boolean if a field has been set. -func (o *SubmitOrderDtoOrder) HasNonce() bool { - if o != nil && !IsNil(o.Nonce) { - return true - } - - return false + return &o.Nonce, true } -// SetNonce gets a reference to the given string and assigns it to the Nonce field. +// SetNonce sets field value func (o *SubmitOrderDtoOrder) SetNonce(v string) { - o.Nonce = &v + o.Nonce = v } -// GetOriginChainId returns the OriginChainId field value if set, zero value otherwise. +// GetOriginChainId returns the OriginChainId field value func (o *SubmitOrderDtoOrder) GetOriginChainId() string { - if o == nil || IsNil(o.OriginChainId) { + if o == nil { var ret string return ret } - return *o.OriginChainId + + return o.OriginChainId } -// GetOriginChainIdOk returns a tuple with the OriginChainId field value if set, nil otherwise +// GetOriginChainIdOk returns a tuple with the OriginChainId field value // and a boolean to check if the value has been set. func (o *SubmitOrderDtoOrder) GetOriginChainIdOk() (*string, bool) { - if o == nil || IsNil(o.OriginChainId) { + if o == nil { return nil, false } - return o.OriginChainId, true -} - -// HasOriginChainId returns a boolean if a field has been set. -func (o *SubmitOrderDtoOrder) HasOriginChainId() bool { - if o != nil && !IsNil(o.OriginChainId) { - return true - } - - return false + return &o.OriginChainId, true } -// SetOriginChainId gets a reference to the given string and assigns it to the OriginChainId field. +// SetOriginChainId sets field value func (o *SubmitOrderDtoOrder) SetOriginChainId(v string) { - o.OriginChainId = &v + o.OriginChainId = v } -// GetFillDeadline returns the FillDeadline field value if set, zero value otherwise. +// GetFillDeadline returns the FillDeadline field value func (o *SubmitOrderDtoOrder) GetFillDeadline() string { - if o == nil || IsNil(o.FillDeadline) { + if o == nil { var ret string return ret } - return *o.FillDeadline + + return o.FillDeadline } -// GetFillDeadlineOk returns a tuple with the FillDeadline field value if set, nil otherwise +// GetFillDeadlineOk returns a tuple with the FillDeadline field value // and a boolean to check if the value has been set. func (o *SubmitOrderDtoOrder) GetFillDeadlineOk() (*string, bool) { - if o == nil || IsNil(o.FillDeadline) { + if o == nil { return nil, false } - return o.FillDeadline, true -} - -// HasFillDeadline returns a boolean if a field has been set. -func (o *SubmitOrderDtoOrder) HasFillDeadline() bool { - if o != nil && !IsNil(o.FillDeadline) { - return true - } - - return false + return &o.FillDeadline, true } -// SetFillDeadline gets a reference to the given string and assigns it to the FillDeadline field. +// SetFillDeadline sets field value func (o *SubmitOrderDtoOrder) SetFillDeadline(v string) { - o.FillDeadline = &v + o.FillDeadline = v } -// GetExpires returns the Expires field value if set, zero value otherwise. +// GetExpires returns the Expires field value func (o *SubmitOrderDtoOrder) GetExpires() string { - if o == nil || IsNil(o.Expires) { + if o == nil { var ret string return ret } - return *o.Expires + + return o.Expires } -// GetExpiresOk returns a tuple with the Expires field value if set, nil otherwise +// GetExpiresOk returns a tuple with the Expires field value // and a boolean to check if the value has been set. func (o *SubmitOrderDtoOrder) GetExpiresOk() (*string, bool) { - if o == nil || IsNil(o.Expires) { + if o == nil { return nil, false } - return o.Expires, true + return &o.Expires, true } -// HasExpires returns a boolean if a field has been set. -func (o *SubmitOrderDtoOrder) HasExpires() bool { - if o != nil && !IsNil(o.Expires) { - return true - } - - return false -} - -// SetExpires gets a reference to the given string and assigns it to the Expires field. +// SetExpires sets field value func (o *SubmitOrderDtoOrder) SetExpires(v string) { - o.Expires = &v + o.Expires = v } // GetInputOracle returns the InputOracle field value @@ -239,9 +211,9 @@ func (o *SubmitOrderDtoOrder) SetInputOracle(v string) { } // GetInputs returns the Inputs field value -func (o *SubmitOrderDtoOrder) GetInputs() [][]string { +func (o *SubmitOrderDtoOrder) GetInputs() [][]interface{} { if o == nil { - var ret [][]string + var ret [][]interface{} return ret } @@ -250,7 +222,7 @@ func (o *SubmitOrderDtoOrder) GetInputs() [][]string { // GetInputsOk returns a tuple with the Inputs field value // and a boolean to check if the value has been set. -func (o *SubmitOrderDtoOrder) GetInputsOk() ([][]string, bool) { +func (o *SubmitOrderDtoOrder) GetInputsOk() ([][]interface{}, bool) { if o == nil { return nil, false } @@ -258,7 +230,7 @@ func (o *SubmitOrderDtoOrder) GetInputsOk() ([][]string, bool) { } // SetInputs sets field value -func (o *SubmitOrderDtoOrder) SetInputs(v [][]string) { +func (o *SubmitOrderDtoOrder) SetInputs(v [][]interface{}) { o.Inputs = v } @@ -297,18 +269,10 @@ func (o SubmitOrderDtoOrder) MarshalJSON() ([]byte, error) { func (o SubmitOrderDtoOrder) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} toSerialize["user"] = o.User - if !IsNil(o.Nonce) { - toSerialize["nonce"] = o.Nonce - } - if !IsNil(o.OriginChainId) { - toSerialize["originChainId"] = o.OriginChainId - } - if !IsNil(o.FillDeadline) { - toSerialize["fillDeadline"] = o.FillDeadline - } - if !IsNil(o.Expires) { - toSerialize["expires"] = o.Expires - } + toSerialize["nonce"] = o.Nonce + toSerialize["originChainId"] = o.OriginChainId + toSerialize["fillDeadline"] = o.FillDeadline + toSerialize["expires"] = o.Expires toSerialize["inputOracle"] = o.InputOracle toSerialize["inputs"] = o.Inputs toSerialize["outputs"] = o.Outputs @@ -321,6 +285,10 @@ func (o *SubmitOrderDtoOrder) UnmarshalJSON(data []byte) (err error) { // that every required field exists as a key in the generic map. requiredProperties := []string{ "user", + "nonce", + "originChainId", + "fillDeadline", + "expires", "inputOracle", "inputs", "outputs", diff --git a/api/lifiorder/model_submit_order_dto_order_outputs_inner.go b/api/lifiorder/model_submit_order_dto_order_outputs_inner.go index 4b90fdad..e6596f52 100644 --- a/api/lifiorder/model_submit_order_dto_order_outputs_inner.go +++ b/api/lifiorder/model_submit_order_dto_order_outputs_inner.go @@ -28,15 +28,13 @@ type SubmitOrderDtoOrderOutputsInner struct { // The token identifier Token string `json:"token"` // The amount of tokens - Amount *string `json:"amount,omitempty"` + Amount string `json:"amount"` // The recipient address Recipient string `json:"recipient"` // The chain ID - ChainId *string `json:"chainId,omitempty"` - // The remote call data + ChainId string `json:"chainId"` CallbackData NullableString `json:"callbackData,omitempty"` - // The fulfillment context - Context NullableString `json:"context,omitempty"` + Context NullableString `json:"context,omitempty"` } type _SubmitOrderDtoOrderOutputsInner SubmitOrderDtoOrderOutputsInner @@ -45,12 +43,14 @@ type _SubmitOrderDtoOrderOutputsInner SubmitOrderDtoOrderOutputsInner // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewSubmitOrderDtoOrderOutputsInner(oracle string, settler string, token string, recipient string) *SubmitOrderDtoOrderOutputsInner { +func NewSubmitOrderDtoOrderOutputsInner(oracle string, settler string, token string, amount string, recipient string, chainId string) *SubmitOrderDtoOrderOutputsInner { this := SubmitOrderDtoOrderOutputsInner{} this.Oracle = oracle this.Settler = settler this.Token = token + this.Amount = amount this.Recipient = recipient + this.ChainId = chainId return &this } @@ -134,36 +134,28 @@ func (o *SubmitOrderDtoOrderOutputsInner) SetToken(v string) { o.Token = v } -// GetAmount returns the Amount field value if set, zero value otherwise. +// GetAmount returns the Amount field value func (o *SubmitOrderDtoOrderOutputsInner) GetAmount() string { - if o == nil || IsNil(o.Amount) { + if o == nil { var ret string return ret } - return *o.Amount + + return o.Amount } -// GetAmountOk returns a tuple with the Amount field value if set, nil otherwise +// GetAmountOk returns a tuple with the Amount field value // and a boolean to check if the value has been set. func (o *SubmitOrderDtoOrderOutputsInner) GetAmountOk() (*string, bool) { - if o == nil || IsNil(o.Amount) { + if o == nil { return nil, false } - return o.Amount, true -} - -// HasAmount returns a boolean if a field has been set. -func (o *SubmitOrderDtoOrderOutputsInner) HasAmount() bool { - if o != nil && !IsNil(o.Amount) { - return true - } - - return false + return &o.Amount, true } -// SetAmount gets a reference to the given string and assigns it to the Amount field. +// SetAmount sets field value func (o *SubmitOrderDtoOrderOutputsInner) SetAmount(v string) { - o.Amount = &v + o.Amount = v } // GetRecipient returns the Recipient field value @@ -190,36 +182,28 @@ func (o *SubmitOrderDtoOrderOutputsInner) SetRecipient(v string) { o.Recipient = v } -// GetChainId returns the ChainId field value if set, zero value otherwise. +// GetChainId returns the ChainId field value func (o *SubmitOrderDtoOrderOutputsInner) GetChainId() string { - if o == nil || IsNil(o.ChainId) { + if o == nil { var ret string return ret } - return *o.ChainId + + return o.ChainId } -// GetChainIdOk returns a tuple with the ChainId field value if set, nil otherwise +// GetChainIdOk returns a tuple with the ChainId field value // and a boolean to check if the value has been set. func (o *SubmitOrderDtoOrderOutputsInner) GetChainIdOk() (*string, bool) { - if o == nil || IsNil(o.ChainId) { + if o == nil { return nil, false } - return o.ChainId, true -} - -// HasChainId returns a boolean if a field has been set. -func (o *SubmitOrderDtoOrderOutputsInner) HasChainId() bool { - if o != nil && !IsNil(o.ChainId) { - return true - } - - return false + return &o.ChainId, true } -// SetChainId gets a reference to the given string and assigns it to the ChainId field. +// SetChainId sets field value func (o *SubmitOrderDtoOrderOutputsInner) SetChainId(v string) { - o.ChainId = &v + o.ChainId = v } // GetCallbackData returns the CallbackData field value if set, zero value otherwise (both if not set or set to explicit null). @@ -321,13 +305,9 @@ func (o SubmitOrderDtoOrderOutputsInner) ToMap() (map[string]interface{}, error) toSerialize["oracle"] = o.Oracle toSerialize["settler"] = o.Settler toSerialize["token"] = o.Token - if !IsNil(o.Amount) { - toSerialize["amount"] = o.Amount - } + toSerialize["amount"] = o.Amount toSerialize["recipient"] = o.Recipient - if !IsNil(o.ChainId) { - toSerialize["chainId"] = o.ChainId - } + toSerialize["chainId"] = o.ChainId if o.CallbackData.IsSet() { toSerialize["callbackData"] = o.CallbackData.Get() } @@ -345,7 +325,9 @@ func (o *SubmitOrderDtoOrderOutputsInner) UnmarshalJSON(data []byte) (err error) "oracle", "settler", "token", + "amount", "recipient", + "chainId", } allProperties := make(map[string]interface{}) diff --git a/api/lifiorder/model_submit_order_response_dto.go b/api/lifiorder/model_submit_order_response_dto.go index ffde9d3c..ee254711 100644 --- a/api/lifiorder/model_submit_order_response_dto.go +++ b/api/lifiorder/model_submit_order_response_dto.go @@ -22,13 +22,12 @@ var _ MappedNullable = &SubmitOrderResponseDto{} // SubmitOrderResponseDto struct for SubmitOrderResponseDto type SubmitOrderResponseDto struct { // The order details - Order CompactOrderResponseDto `json:"order"` - // The quote details - Quote NullableQuoteResponseDto `json:"quote"` + Order CompactOrderResponseDto `json:"order"` + Quote NullableSubmittedOrderQuoteDto `json:"quote"` // Sponsor signature - SponsorSignature map[string]interface{} `json:"sponsorSignature,omitempty"` + SponsorSignature NullableString `json:"sponsorSignature,omitempty"` // Allocator signature - AllocatorSignature map[string]interface{} `json:"allocatorSignature,omitempty"` + AllocatorSignature NullableString `json:"allocatorSignature,omitempty"` // Input settler address InputSettler string `json:"inputSettler"` // Order metadata @@ -41,7 +40,7 @@ type _SubmitOrderResponseDto SubmitOrderResponseDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewSubmitOrderResponseDto(order CompactOrderResponseDto, quote NullableQuoteResponseDto, inputSettler string, meta OrderMetaDto) *SubmitOrderResponseDto { +func NewSubmitOrderResponseDto(order CompactOrderResponseDto, quote NullableSubmittedOrderQuoteDto, inputSettler string, meta OrderMetaDto) *SubmitOrderResponseDto { this := SubmitOrderResponseDto{} this.Order = order this.Quote = quote @@ -83,10 +82,10 @@ func (o *SubmitOrderResponseDto) SetOrder(v CompactOrderResponseDto) { } // GetQuote returns the Quote field value -// If the value is explicit nil, the zero value for QuoteResponseDto will be returned -func (o *SubmitOrderResponseDto) GetQuote() QuoteResponseDto { +// If the value is explicit nil, the zero value for SubmittedOrderQuoteDto will be returned +func (o *SubmitOrderResponseDto) GetQuote() SubmittedOrderQuoteDto { if o == nil || o.Quote.Get() == nil { - var ret QuoteResponseDto + var ret SubmittedOrderQuoteDto return ret } @@ -96,7 +95,7 @@ func (o *SubmitOrderResponseDto) GetQuote() QuoteResponseDto { // GetQuoteOk returns a tuple with the Quote field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SubmitOrderResponseDto) GetQuoteOk() (*QuoteResponseDto, bool) { +func (o *SubmitOrderResponseDto) GetQuoteOk() (*SubmittedOrderQuoteDto, bool) { if o == nil { return nil, false } @@ -104,74 +103,94 @@ func (o *SubmitOrderResponseDto) GetQuoteOk() (*QuoteResponseDto, bool) { } // SetQuote sets field value -func (o *SubmitOrderResponseDto) SetQuote(v QuoteResponseDto) { +func (o *SubmitOrderResponseDto) SetQuote(v SubmittedOrderQuoteDto) { o.Quote.Set(&v) } // GetSponsorSignature returns the SponsorSignature field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *SubmitOrderResponseDto) GetSponsorSignature() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +func (o *SubmitOrderResponseDto) GetSponsorSignature() string { + if o == nil || IsNil(o.SponsorSignature.Get()) { + var ret string return ret } - return o.SponsorSignature + return *o.SponsorSignature.Get() } // GetSponsorSignatureOk returns a tuple with the SponsorSignature field value if set, nil otherwise // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SubmitOrderResponseDto) GetSponsorSignatureOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.SponsorSignature) { - return map[string]interface{}{}, false +func (o *SubmitOrderResponseDto) GetSponsorSignatureOk() (*string, bool) { + if o == nil { + return nil, false } - return o.SponsorSignature, true + return o.SponsorSignature.Get(), o.SponsorSignature.IsSet() } // HasSponsorSignature returns a boolean if a field has been set. func (o *SubmitOrderResponseDto) HasSponsorSignature() bool { - if o != nil && !IsNil(o.SponsorSignature) { + if o != nil && o.SponsorSignature.IsSet() { return true } return false } -// SetSponsorSignature gets a reference to the given map[string]interface{} and assigns it to the SponsorSignature field. -func (o *SubmitOrderResponseDto) SetSponsorSignature(v map[string]interface{}) { - o.SponsorSignature = v +// SetSponsorSignature gets a reference to the given NullableString and assigns it to the SponsorSignature field. +func (o *SubmitOrderResponseDto) SetSponsorSignature(v string) { + o.SponsorSignature.Set(&v) +} + +// SetSponsorSignatureNil sets the value for SponsorSignature to be an explicit nil +func (o *SubmitOrderResponseDto) SetSponsorSignatureNil() { + o.SponsorSignature.Set(nil) +} + +// UnsetSponsorSignature ensures that no value is present for SponsorSignature, not even an explicit nil +func (o *SubmitOrderResponseDto) UnsetSponsorSignature() { + o.SponsorSignature.Unset() } // GetAllocatorSignature returns the AllocatorSignature field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *SubmitOrderResponseDto) GetAllocatorSignature() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +func (o *SubmitOrderResponseDto) GetAllocatorSignature() string { + if o == nil || IsNil(o.AllocatorSignature.Get()) { + var ret string return ret } - return o.AllocatorSignature + return *o.AllocatorSignature.Get() } // GetAllocatorSignatureOk returns a tuple with the AllocatorSignature field value if set, nil otherwise // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SubmitOrderResponseDto) GetAllocatorSignatureOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.AllocatorSignature) { - return map[string]interface{}{}, false +func (o *SubmitOrderResponseDto) GetAllocatorSignatureOk() (*string, bool) { + if o == nil { + return nil, false } - return o.AllocatorSignature, true + return o.AllocatorSignature.Get(), o.AllocatorSignature.IsSet() } // HasAllocatorSignature returns a boolean if a field has been set. func (o *SubmitOrderResponseDto) HasAllocatorSignature() bool { - if o != nil && !IsNil(o.AllocatorSignature) { + if o != nil && o.AllocatorSignature.IsSet() { return true } return false } -// SetAllocatorSignature gets a reference to the given map[string]interface{} and assigns it to the AllocatorSignature field. -func (o *SubmitOrderResponseDto) SetAllocatorSignature(v map[string]interface{}) { - o.AllocatorSignature = v +// SetAllocatorSignature gets a reference to the given NullableString and assigns it to the AllocatorSignature field. +func (o *SubmitOrderResponseDto) SetAllocatorSignature(v string) { + o.AllocatorSignature.Set(&v) +} + +// SetAllocatorSignatureNil sets the value for AllocatorSignature to be an explicit nil +func (o *SubmitOrderResponseDto) SetAllocatorSignatureNil() { + o.AllocatorSignature.Set(nil) +} + +// UnsetAllocatorSignature ensures that no value is present for AllocatorSignature, not even an explicit nil +func (o *SubmitOrderResponseDto) UnsetAllocatorSignature() { + o.AllocatorSignature.Unset() } // GetInputSettler returns the InputSettler field value @@ -234,11 +253,11 @@ func (o SubmitOrderResponseDto) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} toSerialize["order"] = o.Order toSerialize["quote"] = o.Quote.Get() - if o.SponsorSignature != nil { - toSerialize["sponsorSignature"] = o.SponsorSignature + if o.SponsorSignature.IsSet() { + toSerialize["sponsorSignature"] = o.SponsorSignature.Get() } - if o.AllocatorSignature != nil { - toSerialize["allocatorSignature"] = o.AllocatorSignature + if o.AllocatorSignature.IsSet() { + toSerialize["allocatorSignature"] = o.AllocatorSignature.Get() } toSerialize["inputSettler"] = o.InputSettler toSerialize["meta"] = o.Meta diff --git a/api/lifiorder/model_submit_quotes_dto_quotes_inner.go b/api/lifiorder/model_submit_quotes_dto_quotes_inner.go index acdd3f2c..84ef77a4 100644 --- a/api/lifiorder/model_submit_quotes_dto_quotes_inner.go +++ b/api/lifiorder/model_submit_quotes_dto_quotes_inner.go @@ -33,13 +33,13 @@ type SubmitQuotesDtoQuotesInner struct { FromDecimals int32 `json:"fromDecimals"` // Decimals of the destination token ToDecimals int32 `json:"toDecimals"` - // Array of quote ranges with different price tiers + // Array of quote ranges with different price tiers. At most 1000 ranges per quote. Ranges []SubmitQuotesDtoQuotesInnerRangesInner `json:"ranges"` // Expiry timestamp of the quote in seconds Expiry int32 `json:"expiry"` // Exclusive solver address allowed to fill this quote. EVM (eip155): 0x-prefixed 40-char hex. Solana: 32–44 char base58. Tron: base58check, T-prefixed, 34 chars. ExclusiveFor *string `json:"exclusiveFor,omitempty"` - // Integrator key hash identifying the integrator this quote is tagged for. If provided, the quote will only be served to the specific integrator. If omitted, the quote is treated as an open-market quote available to all integrators. + // Integrator key hash this quote is tagged for. If provided, the quote will only be served to the specific integrator. If omitted, the quote is treated as an open-market quote available to all integrators. IntegratorKeyHash *string `json:"integratorKeyHash,omitempty" validate:"regexp=^[a-f0-9]{64}$"` } diff --git a/api/lifiorder/model_submitted_order_quote_dto.go b/api/lifiorder/model_submitted_order_quote_dto.go new file mode 100644 index 00000000..b9778102 --- /dev/null +++ b/api/lifiorder/model_submitted_order_quote_dto.go @@ -0,0 +1,654 @@ +/* +Lifi Intents API Reference + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +API version: 0.0.19 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package lifiorder + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// checks if the SubmittedOrderQuoteDto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SubmittedOrderQuoteDto{} + +// SubmittedOrderQuoteDto struct for SubmittedOrderQuoteDto +type SubmittedOrderQuoteDto struct { + // Quote ID + Id string `json:"id"` + // Quote creation timestamp + CreatedAt string `json:"createdAt"` + // Quote last update timestamp + UpdatedAt string `json:"updatedAt"` + // Unique quote identifier + QuoteId string `json:"quoteId"` + // Source chain network ID + FromChainNetworkId string `json:"fromChainNetworkId"` + // Destination chain network ID + ToChainNetworkId string `json:"toChainNetworkId"` + // Source asset address + FromAssetAddress string `json:"fromAssetAddress"` + // Destination asset address + ToAssetAddress string `json:"toAssetAddress"` + // Source asset decimals + FromAssetDecimals float32 `json:"fromAssetDecimals"` + // Destination asset decimals + ToAssetDecimals float32 `json:"toAssetDecimals"` + // Quote rate + Quote string `json:"quote"` + // Input amount + InputAmount string `json:"inputAmount"` + // Output amount + OutputAmount string `json:"outputAmount"` + // Quote expiry timestamp + Expiry string `json:"expiry"` + // Exclusive for address + ExclusiveFor NullableString `json:"exclusiveFor"` + // Quote owner address + User string `json:"user"` + // Associated order ID + OrderId NullableFloat32 `json:"orderId"` + // Solver ID + SolverId float32 `json:"solverId"` +} + +type _SubmittedOrderQuoteDto SubmittedOrderQuoteDto + +// NewSubmittedOrderQuoteDto instantiates a new SubmittedOrderQuoteDto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSubmittedOrderQuoteDto(id string, createdAt string, updatedAt string, quoteId string, fromChainNetworkId string, toChainNetworkId string, fromAssetAddress string, toAssetAddress string, fromAssetDecimals float32, toAssetDecimals float32, quote string, inputAmount string, outputAmount string, expiry string, exclusiveFor NullableString, user string, orderId NullableFloat32, solverId float32) *SubmittedOrderQuoteDto { + this := SubmittedOrderQuoteDto{} + this.Id = id + this.CreatedAt = createdAt + this.UpdatedAt = updatedAt + this.QuoteId = quoteId + this.FromChainNetworkId = fromChainNetworkId + this.ToChainNetworkId = toChainNetworkId + this.FromAssetAddress = fromAssetAddress + this.ToAssetAddress = toAssetAddress + this.FromAssetDecimals = fromAssetDecimals + this.ToAssetDecimals = toAssetDecimals + this.Quote = quote + this.InputAmount = inputAmount + this.OutputAmount = outputAmount + this.Expiry = expiry + this.ExclusiveFor = exclusiveFor + this.User = user + this.OrderId = orderId + this.SolverId = solverId + return &this +} + +// NewSubmittedOrderQuoteDtoWithDefaults instantiates a new SubmittedOrderQuoteDto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSubmittedOrderQuoteDtoWithDefaults() *SubmittedOrderQuoteDto { + this := SubmittedOrderQuoteDto{} + return &this +} + +// GetId returns the Id field value +func (o *SubmittedOrderQuoteDto) GetId() string { + if o == nil { + var ret string + return ret + } + + return o.Id +} + +// GetIdOk returns a tuple with the Id field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Id, true +} + +// SetId sets field value +func (o *SubmittedOrderQuoteDto) SetId(v string) { + o.Id = v +} + +// GetCreatedAt returns the CreatedAt field value +func (o *SubmittedOrderQuoteDto) GetCreatedAt() string { + if o == nil { + var ret string + return ret + } + + return o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetCreatedAtOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.CreatedAt, true +} + +// SetCreatedAt sets field value +func (o *SubmittedOrderQuoteDto) SetCreatedAt(v string) { + o.CreatedAt = v +} + +// GetUpdatedAt returns the UpdatedAt field value +func (o *SubmittedOrderQuoteDto) GetUpdatedAt() string { + if o == nil { + var ret string + return ret + } + + return o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetUpdatedAtOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.UpdatedAt, true +} + +// SetUpdatedAt sets field value +func (o *SubmittedOrderQuoteDto) SetUpdatedAt(v string) { + o.UpdatedAt = v +} + +// GetQuoteId returns the QuoteId field value +func (o *SubmittedOrderQuoteDto) GetQuoteId() string { + if o == nil { + var ret string + return ret + } + + return o.QuoteId +} + +// GetQuoteIdOk returns a tuple with the QuoteId field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetQuoteIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.QuoteId, true +} + +// SetQuoteId sets field value +func (o *SubmittedOrderQuoteDto) SetQuoteId(v string) { + o.QuoteId = v +} + +// GetFromChainNetworkId returns the FromChainNetworkId field value +func (o *SubmittedOrderQuoteDto) GetFromChainNetworkId() string { + if o == nil { + var ret string + return ret + } + + return o.FromChainNetworkId +} + +// GetFromChainNetworkIdOk returns a tuple with the FromChainNetworkId field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetFromChainNetworkIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.FromChainNetworkId, true +} + +// SetFromChainNetworkId sets field value +func (o *SubmittedOrderQuoteDto) SetFromChainNetworkId(v string) { + o.FromChainNetworkId = v +} + +// GetToChainNetworkId returns the ToChainNetworkId field value +func (o *SubmittedOrderQuoteDto) GetToChainNetworkId() string { + if o == nil { + var ret string + return ret + } + + return o.ToChainNetworkId +} + +// GetToChainNetworkIdOk returns a tuple with the ToChainNetworkId field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetToChainNetworkIdOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.ToChainNetworkId, true +} + +// SetToChainNetworkId sets field value +func (o *SubmittedOrderQuoteDto) SetToChainNetworkId(v string) { + o.ToChainNetworkId = v +} + +// GetFromAssetAddress returns the FromAssetAddress field value +func (o *SubmittedOrderQuoteDto) GetFromAssetAddress() string { + if o == nil { + var ret string + return ret + } + + return o.FromAssetAddress +} + +// GetFromAssetAddressOk returns a tuple with the FromAssetAddress field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetFromAssetAddressOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.FromAssetAddress, true +} + +// SetFromAssetAddress sets field value +func (o *SubmittedOrderQuoteDto) SetFromAssetAddress(v string) { + o.FromAssetAddress = v +} + +// GetToAssetAddress returns the ToAssetAddress field value +func (o *SubmittedOrderQuoteDto) GetToAssetAddress() string { + if o == nil { + var ret string + return ret + } + + return o.ToAssetAddress +} + +// GetToAssetAddressOk returns a tuple with the ToAssetAddress field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetToAssetAddressOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.ToAssetAddress, true +} + +// SetToAssetAddress sets field value +func (o *SubmittedOrderQuoteDto) SetToAssetAddress(v string) { + o.ToAssetAddress = v +} + +// GetFromAssetDecimals returns the FromAssetDecimals field value +func (o *SubmittedOrderQuoteDto) GetFromAssetDecimals() float32 { + if o == nil { + var ret float32 + return ret + } + + return o.FromAssetDecimals +} + +// GetFromAssetDecimalsOk returns a tuple with the FromAssetDecimals field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetFromAssetDecimalsOk() (*float32, bool) { + if o == nil { + return nil, false + } + return &o.FromAssetDecimals, true +} + +// SetFromAssetDecimals sets field value +func (o *SubmittedOrderQuoteDto) SetFromAssetDecimals(v float32) { + o.FromAssetDecimals = v +} + +// GetToAssetDecimals returns the ToAssetDecimals field value +func (o *SubmittedOrderQuoteDto) GetToAssetDecimals() float32 { + if o == nil { + var ret float32 + return ret + } + + return o.ToAssetDecimals +} + +// GetToAssetDecimalsOk returns a tuple with the ToAssetDecimals field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetToAssetDecimalsOk() (*float32, bool) { + if o == nil { + return nil, false + } + return &o.ToAssetDecimals, true +} + +// SetToAssetDecimals sets field value +func (o *SubmittedOrderQuoteDto) SetToAssetDecimals(v float32) { + o.ToAssetDecimals = v +} + +// GetQuote returns the Quote field value +func (o *SubmittedOrderQuoteDto) GetQuote() string { + if o == nil { + var ret string + return ret + } + + return o.Quote +} + +// GetQuoteOk returns a tuple with the Quote field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetQuoteOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Quote, true +} + +// SetQuote sets field value +func (o *SubmittedOrderQuoteDto) SetQuote(v string) { + o.Quote = v +} + +// GetInputAmount returns the InputAmount field value +func (o *SubmittedOrderQuoteDto) GetInputAmount() string { + if o == nil { + var ret string + return ret + } + + return o.InputAmount +} + +// GetInputAmountOk returns a tuple with the InputAmount field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetInputAmountOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.InputAmount, true +} + +// SetInputAmount sets field value +func (o *SubmittedOrderQuoteDto) SetInputAmount(v string) { + o.InputAmount = v +} + +// GetOutputAmount returns the OutputAmount field value +func (o *SubmittedOrderQuoteDto) GetOutputAmount() string { + if o == nil { + var ret string + return ret + } + + return o.OutputAmount +} + +// GetOutputAmountOk returns a tuple with the OutputAmount field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetOutputAmountOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.OutputAmount, true +} + +// SetOutputAmount sets field value +func (o *SubmittedOrderQuoteDto) SetOutputAmount(v string) { + o.OutputAmount = v +} + +// GetExpiry returns the Expiry field value +func (o *SubmittedOrderQuoteDto) GetExpiry() string { + if o == nil { + var ret string + return ret + } + + return o.Expiry +} + +// GetExpiryOk returns a tuple with the Expiry field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetExpiryOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Expiry, true +} + +// SetExpiry sets field value +func (o *SubmittedOrderQuoteDto) SetExpiry(v string) { + o.Expiry = v +} + +// GetExclusiveFor returns the ExclusiveFor field value +// If the value is explicit nil, the zero value for string will be returned +func (o *SubmittedOrderQuoteDto) GetExclusiveFor() string { + if o == nil || o.ExclusiveFor.Get() == nil { + var ret string + return ret + } + + return *o.ExclusiveFor.Get() +} + +// GetExclusiveForOk returns a tuple with the ExclusiveFor field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *SubmittedOrderQuoteDto) GetExclusiveForOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.ExclusiveFor.Get(), o.ExclusiveFor.IsSet() +} + +// SetExclusiveFor sets field value +func (o *SubmittedOrderQuoteDto) SetExclusiveFor(v string) { + o.ExclusiveFor.Set(&v) +} + +// GetUser returns the User field value +func (o *SubmittedOrderQuoteDto) GetUser() string { + if o == nil { + var ret string + return ret + } + + return o.User +} + +// GetUserOk returns a tuple with the User field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetUserOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.User, true +} + +// SetUser sets field value +func (o *SubmittedOrderQuoteDto) SetUser(v string) { + o.User = v +} + +// GetOrderId returns the OrderId field value +// If the value is explicit nil, the zero value for float32 will be returned +func (o *SubmittedOrderQuoteDto) GetOrderId() float32 { + if o == nil || o.OrderId.Get() == nil { + var ret float32 + return ret + } + + return *o.OrderId.Get() +} + +// GetOrderIdOk returns a tuple with the OrderId field value +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *SubmittedOrderQuoteDto) GetOrderIdOk() (*float32, bool) { + if o == nil { + return nil, false + } + return o.OrderId.Get(), o.OrderId.IsSet() +} + +// SetOrderId sets field value +func (o *SubmittedOrderQuoteDto) SetOrderId(v float32) { + o.OrderId.Set(&v) +} + +// GetSolverId returns the SolverId field value +func (o *SubmittedOrderQuoteDto) GetSolverId() float32 { + if o == nil { + var ret float32 + return ret + } + + return o.SolverId +} + +// GetSolverIdOk returns a tuple with the SolverId field value +// and a boolean to check if the value has been set. +func (o *SubmittedOrderQuoteDto) GetSolverIdOk() (*float32, bool) { + if o == nil { + return nil, false + } + return &o.SolverId, true +} + +// SetSolverId sets field value +func (o *SubmittedOrderQuoteDto) SetSolverId(v float32) { + o.SolverId = v +} + +func (o SubmittedOrderQuoteDto) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SubmittedOrderQuoteDto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["id"] = o.Id + toSerialize["createdAt"] = o.CreatedAt + toSerialize["updatedAt"] = o.UpdatedAt + toSerialize["quoteId"] = o.QuoteId + toSerialize["fromChainNetworkId"] = o.FromChainNetworkId + toSerialize["toChainNetworkId"] = o.ToChainNetworkId + toSerialize["fromAssetAddress"] = o.FromAssetAddress + toSerialize["toAssetAddress"] = o.ToAssetAddress + toSerialize["fromAssetDecimals"] = o.FromAssetDecimals + toSerialize["toAssetDecimals"] = o.ToAssetDecimals + toSerialize["quote"] = o.Quote + toSerialize["inputAmount"] = o.InputAmount + toSerialize["outputAmount"] = o.OutputAmount + toSerialize["expiry"] = o.Expiry + toSerialize["exclusiveFor"] = o.ExclusiveFor.Get() + toSerialize["user"] = o.User + toSerialize["orderId"] = o.OrderId.Get() + toSerialize["solverId"] = o.SolverId + return toSerialize, nil +} + +func (o *SubmittedOrderQuoteDto) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "id", + "createdAt", + "updatedAt", + "quoteId", + "fromChainNetworkId", + "toChainNetworkId", + "fromAssetAddress", + "toAssetAddress", + "fromAssetDecimals", + "toAssetDecimals", + "quote", + "inputAmount", + "outputAmount", + "expiry", + "exclusiveFor", + "user", + "orderId", + "solverId", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSubmittedOrderQuoteDto := _SubmittedOrderQuoteDto{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varSubmittedOrderQuoteDto) + + if err != nil { + return err + } + + *o = SubmittedOrderQuoteDto(varSubmittedOrderQuoteDto) + + return err +} + +type NullableSubmittedOrderQuoteDto struct { + value *SubmittedOrderQuoteDto + isSet bool +} + +func (v NullableSubmittedOrderQuoteDto) Get() *SubmittedOrderQuoteDto { + return v.value +} + +func (v *NullableSubmittedOrderQuoteDto) Set(val *SubmittedOrderQuoteDto) { + v.value = val + v.isSet = true +} + +func (v NullableSubmittedOrderQuoteDto) IsSet() bool { + return v.isSet +} + +func (v *NullableSubmittedOrderQuoteDto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSubmittedOrderQuoteDto(val *SubmittedOrderQuoteDto) *NullableSubmittedOrderQuoteDto { + return &NullableSubmittedOrderQuoteDto{value: val, isSet: true} +} + +func (v NullableSubmittedOrderQuoteDto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSubmittedOrderQuoteDto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/api/lifiorder/model_supported_route_dto.go b/api/lifiorder/model_supported_route_dto.go index 3ae9ed79..a1c34d23 100644 --- a/api/lifiorder/model_supported_route_dto.go +++ b/api/lifiorder/model_supported_route_dto.go @@ -36,19 +36,17 @@ type SupportedRouteDto struct { // Gas fee for the route (in token units) GasFee float32 `json:"gasFee"` // Source chain record ID - FromChainRecordId map[string]interface{} `json:"fromChainRecordId"` + FromChainRecordId NullableFloat32 `json:"fromChainRecordId"` // Destination chain record ID - ToChainRecordId map[string]interface{} `json:"toChainRecordId"` + ToChainRecordId NullableFloat32 `json:"toChainRecordId"` // Source token record ID - FromTokenId map[string]interface{} `json:"fromTokenId"` + FromTokenId NullableFloat32 `json:"fromTokenId"` // Destination token record ID - ToTokenId map[string]interface{} `json:"toTokenId"` + ToTokenId NullableFloat32 `json:"toTokenId"` // Whether the route is currently active - IsActive bool `json:"isActive"` - // Source chain information + IsActive bool `json:"isActive"` FromChain NullableRouteChainInfoDto `json:"fromChain"` - // Destination chain information - ToChain NullableRouteChainInfoDto `json:"toChain"` + ToChain NullableRouteChainInfoDto `json:"toChain"` // Source token information for this route FromToken TokenInfoDto `json:"fromToken"` // Destination token information for this route @@ -61,7 +59,7 @@ type _SupportedRouteDto SupportedRouteDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewSupportedRouteDto(id string, createdAt string, updatedAt string, minAmount float32, maxAmount float32, fee float32, gasFee float32, fromChainRecordId map[string]interface{}, toChainRecordId map[string]interface{}, fromTokenId map[string]interface{}, toTokenId map[string]interface{}, isActive bool, fromChain NullableRouteChainInfoDto, toChain NullableRouteChainInfoDto, fromToken TokenInfoDto, toToken TokenInfoDto) *SupportedRouteDto { +func NewSupportedRouteDto(id string, createdAt string, updatedAt string, minAmount float32, maxAmount float32, fee float32, gasFee float32, fromChainRecordId NullableFloat32, toChainRecordId NullableFloat32, fromTokenId NullableFloat32, toTokenId NullableFloat32, isActive bool, fromChain NullableRouteChainInfoDto, toChain NullableRouteChainInfoDto, fromToken TokenInfoDto, toToken TokenInfoDto) *SupportedRouteDto { this := SupportedRouteDto{} this.Id = id this.CreatedAt = createdAt @@ -259,107 +257,107 @@ func (o *SupportedRouteDto) SetGasFee(v float32) { } // GetFromChainRecordId returns the FromChainRecordId field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *SupportedRouteDto) GetFromChainRecordId() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for float32 will be returned +func (o *SupportedRouteDto) GetFromChainRecordId() float32 { + if o == nil || o.FromChainRecordId.Get() == nil { + var ret float32 return ret } - return o.FromChainRecordId + return *o.FromChainRecordId.Get() } // GetFromChainRecordIdOk returns a tuple with the FromChainRecordId field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SupportedRouteDto) GetFromChainRecordIdOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.FromChainRecordId) { - return map[string]interface{}{}, false +func (o *SupportedRouteDto) GetFromChainRecordIdOk() (*float32, bool) { + if o == nil { + return nil, false } - return o.FromChainRecordId, true + return o.FromChainRecordId.Get(), o.FromChainRecordId.IsSet() } // SetFromChainRecordId sets field value -func (o *SupportedRouteDto) SetFromChainRecordId(v map[string]interface{}) { - o.FromChainRecordId = v +func (o *SupportedRouteDto) SetFromChainRecordId(v float32) { + o.FromChainRecordId.Set(&v) } // GetToChainRecordId returns the ToChainRecordId field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *SupportedRouteDto) GetToChainRecordId() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for float32 will be returned +func (o *SupportedRouteDto) GetToChainRecordId() float32 { + if o == nil || o.ToChainRecordId.Get() == nil { + var ret float32 return ret } - return o.ToChainRecordId + return *o.ToChainRecordId.Get() } // GetToChainRecordIdOk returns a tuple with the ToChainRecordId field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SupportedRouteDto) GetToChainRecordIdOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.ToChainRecordId) { - return map[string]interface{}{}, false +func (o *SupportedRouteDto) GetToChainRecordIdOk() (*float32, bool) { + if o == nil { + return nil, false } - return o.ToChainRecordId, true + return o.ToChainRecordId.Get(), o.ToChainRecordId.IsSet() } // SetToChainRecordId sets field value -func (o *SupportedRouteDto) SetToChainRecordId(v map[string]interface{}) { - o.ToChainRecordId = v +func (o *SupportedRouteDto) SetToChainRecordId(v float32) { + o.ToChainRecordId.Set(&v) } // GetFromTokenId returns the FromTokenId field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *SupportedRouteDto) GetFromTokenId() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for float32 will be returned +func (o *SupportedRouteDto) GetFromTokenId() float32 { + if o == nil || o.FromTokenId.Get() == nil { + var ret float32 return ret } - return o.FromTokenId + return *o.FromTokenId.Get() } // GetFromTokenIdOk returns a tuple with the FromTokenId field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SupportedRouteDto) GetFromTokenIdOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.FromTokenId) { - return map[string]interface{}{}, false +func (o *SupportedRouteDto) GetFromTokenIdOk() (*float32, bool) { + if o == nil { + return nil, false } - return o.FromTokenId, true + return o.FromTokenId.Get(), o.FromTokenId.IsSet() } // SetFromTokenId sets field value -func (o *SupportedRouteDto) SetFromTokenId(v map[string]interface{}) { - o.FromTokenId = v +func (o *SupportedRouteDto) SetFromTokenId(v float32) { + o.FromTokenId.Set(&v) } // GetToTokenId returns the ToTokenId field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *SupportedRouteDto) GetToTokenId() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for float32 will be returned +func (o *SupportedRouteDto) GetToTokenId() float32 { + if o == nil || o.ToTokenId.Get() == nil { + var ret float32 return ret } - return o.ToTokenId + return *o.ToTokenId.Get() } // GetToTokenIdOk returns a tuple with the ToTokenId field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *SupportedRouteDto) GetToTokenIdOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.ToTokenId) { - return map[string]interface{}{}, false +func (o *SupportedRouteDto) GetToTokenIdOk() (*float32, bool) { + if o == nil { + return nil, false } - return o.ToTokenId, true + return o.ToTokenId.Get(), o.ToTokenId.IsSet() } // SetToTokenId sets field value -func (o *SupportedRouteDto) SetToTokenId(v map[string]interface{}) { - o.ToTokenId = v +func (o *SupportedRouteDto) SetToTokenId(v float32) { + o.ToTokenId.Set(&v) } // GetIsActive returns the IsActive field value @@ -503,18 +501,10 @@ func (o SupportedRouteDto) ToMap() (map[string]interface{}, error) { toSerialize["maxAmount"] = o.MaxAmount toSerialize["fee"] = o.Fee toSerialize["gasFee"] = o.GasFee - if o.FromChainRecordId != nil { - toSerialize["fromChainRecordId"] = o.FromChainRecordId - } - if o.ToChainRecordId != nil { - toSerialize["toChainRecordId"] = o.ToChainRecordId - } - if o.FromTokenId != nil { - toSerialize["fromTokenId"] = o.FromTokenId - } - if o.ToTokenId != nil { - toSerialize["toTokenId"] = o.ToTokenId - } + toSerialize["fromChainRecordId"] = o.FromChainRecordId.Get() + toSerialize["toChainRecordId"] = o.ToChainRecordId.Get() + toSerialize["fromTokenId"] = o.FromTokenId.Get() + toSerialize["toTokenId"] = o.ToTokenId.Get() toSerialize["isActive"] = o.IsActive toSerialize["fromChain"] = o.FromChain.Get() toSerialize["toChain"] = o.ToChain.Get() diff --git a/api/lifiorder/model_token_info_dto.go b/api/lifiorder/model_token_info_dto.go index f3c5f48b..a7067a38 100644 --- a/api/lifiorder/model_token_info_dto.go +++ b/api/lifiorder/model_token_info_dto.go @@ -22,9 +22,9 @@ var _ MappedNullable = &TokenInfoDto{} // TokenInfoDto struct for TokenInfoDto type TokenInfoDto struct { // Token symbol (null if token not registered in system) - Symbol map[string]interface{} `json:"symbol"` + Symbol NullableString `json:"symbol"` // Token name (null if token not registered in system) - Name map[string]interface{} `json:"name"` + Name NullableString `json:"name"` // Token contract address Address string `json:"address"` // Token decimals @@ -37,7 +37,7 @@ type _TokenInfoDto TokenInfoDto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewTokenInfoDto(symbol map[string]interface{}, name map[string]interface{}, address string, decimals float32) *TokenInfoDto { +func NewTokenInfoDto(symbol NullableString, name NullableString, address string, decimals float32) *TokenInfoDto { this := TokenInfoDto{} this.Symbol = symbol this.Name = name @@ -55,55 +55,55 @@ func NewTokenInfoDtoWithDefaults() *TokenInfoDto { } // GetSymbol returns the Symbol field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *TokenInfoDto) GetSymbol() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *TokenInfoDto) GetSymbol() string { + if o == nil || o.Symbol.Get() == nil { + var ret string return ret } - return o.Symbol + return *o.Symbol.Get() } // GetSymbolOk returns a tuple with the Symbol field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *TokenInfoDto) GetSymbolOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.Symbol) { - return map[string]interface{}{}, false +func (o *TokenInfoDto) GetSymbolOk() (*string, bool) { + if o == nil { + return nil, false } - return o.Symbol, true + return o.Symbol.Get(), o.Symbol.IsSet() } // SetSymbol sets field value -func (o *TokenInfoDto) SetSymbol(v map[string]interface{}) { - o.Symbol = v +func (o *TokenInfoDto) SetSymbol(v string) { + o.Symbol.Set(&v) } // GetName returns the Name field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *TokenInfoDto) GetName() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// If the value is explicit nil, the zero value for string will be returned +func (o *TokenInfoDto) GetName() string { + if o == nil || o.Name.Get() == nil { + var ret string return ret } - return o.Name + return *o.Name.Get() } // GetNameOk returns a tuple with the Name field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *TokenInfoDto) GetNameOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.Name) { - return map[string]interface{}{}, false +func (o *TokenInfoDto) GetNameOk() (*string, bool) { + if o == nil { + return nil, false } - return o.Name, true + return o.Name.Get(), o.Name.IsSet() } // SetName sets field value -func (o *TokenInfoDto) SetName(v map[string]interface{}) { - o.Name = v +func (o *TokenInfoDto) SetName(v string) { + o.Name.Set(&v) } // GetAddress returns the Address field value @@ -164,12 +164,8 @@ func (o TokenInfoDto) MarshalJSON() ([]byte, error) { func (o TokenInfoDto) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} - if o.Symbol != nil { - toSerialize["symbol"] = o.Symbol - } - if o.Name != nil { - toSerialize["name"] = o.Name - } + toSerialize["symbol"] = o.Symbol.Get() + toSerialize["name"] = o.Name.Get() toSerialize["address"] = o.Address toSerialize["decimals"] = o.Decimals return toSerialize, nil diff --git a/api/lifiorder/model_token_info_v1_dto.go b/api/lifiorder/model_token_info_v1_dto.go index e82733d3..dc1feddd 100644 --- a/api/lifiorder/model_token_info_v1_dto.go +++ b/api/lifiorder/model_token_info_v1_dto.go @@ -21,14 +21,14 @@ var _ MappedNullable = &TokenInfoV1Dto{} // TokenInfoV1Dto struct for TokenInfoV1Dto type TokenInfoV1Dto struct { + // Token symbol (null if token not registered in system) + Symbol NullableString `json:"symbol"` + // Token name (null if token not registered in system) + Name NullableString `json:"name"` // Token contract address Address string `json:"address"` - // Token symbol (null if token not registered in system) - Symbol map[string]interface{} `json:"symbol"` // Token decimals Decimals float32 `json:"decimals"` - // Token name (null if token not registered in system) - Name map[string]interface{} `json:"name"` } type _TokenInfoV1Dto TokenInfoV1Dto @@ -37,12 +37,12 @@ type _TokenInfoV1Dto TokenInfoV1Dto // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewTokenInfoV1Dto(address string, symbol map[string]interface{}, decimals float32, name map[string]interface{}) *TokenInfoV1Dto { +func NewTokenInfoV1Dto(symbol NullableString, name NullableString, address string, decimals float32) *TokenInfoV1Dto { this := TokenInfoV1Dto{} - this.Address = address this.Symbol = symbol - this.Decimals = decimals this.Name = name + this.Address = address + this.Decimals = decimals return &this } @@ -54,104 +54,104 @@ func NewTokenInfoV1DtoWithDefaults() *TokenInfoV1Dto { return &this } -// GetAddress returns the Address field value -func (o *TokenInfoV1Dto) GetAddress() string { - if o == nil { +// GetSymbol returns the Symbol field value +// If the value is explicit nil, the zero value for string will be returned +func (o *TokenInfoV1Dto) GetSymbol() string { + if o == nil || o.Symbol.Get() == nil { var ret string return ret } - return o.Address + return *o.Symbol.Get() } -// GetAddressOk returns a tuple with the Address field value +// GetSymbolOk returns a tuple with the Symbol field value // and a boolean to check if the value has been set. -func (o *TokenInfoV1Dto) GetAddressOk() (*string, bool) { +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *TokenInfoV1Dto) GetSymbolOk() (*string, bool) { if o == nil { return nil, false } - return &o.Address, true + return o.Symbol.Get(), o.Symbol.IsSet() } -// SetAddress sets field value -func (o *TokenInfoV1Dto) SetAddress(v string) { - o.Address = v +// SetSymbol sets field value +func (o *TokenInfoV1Dto) SetSymbol(v string) { + o.Symbol.Set(&v) } -// GetSymbol returns the Symbol field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *TokenInfoV1Dto) GetSymbol() map[string]interface{} { - if o == nil { - var ret map[string]interface{} +// GetName returns the Name field value +// If the value is explicit nil, the zero value for string will be returned +func (o *TokenInfoV1Dto) GetName() string { + if o == nil || o.Name.Get() == nil { + var ret string return ret } - return o.Symbol + return *o.Name.Get() } -// GetSymbolOk returns a tuple with the Symbol field value +// GetNameOk returns a tuple with the Name field value // and a boolean to check if the value has been set. // NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *TokenInfoV1Dto) GetSymbolOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.Symbol) { - return map[string]interface{}{}, false +func (o *TokenInfoV1Dto) GetNameOk() (*string, bool) { + if o == nil { + return nil, false } - return o.Symbol, true + return o.Name.Get(), o.Name.IsSet() } -// SetSymbol sets field value -func (o *TokenInfoV1Dto) SetSymbol(v map[string]interface{}) { - o.Symbol = v +// SetName sets field value +func (o *TokenInfoV1Dto) SetName(v string) { + o.Name.Set(&v) } -// GetDecimals returns the Decimals field value -func (o *TokenInfoV1Dto) GetDecimals() float32 { +// GetAddress returns the Address field value +func (o *TokenInfoV1Dto) GetAddress() string { if o == nil { - var ret float32 + var ret string return ret } - return o.Decimals + return o.Address } -// GetDecimalsOk returns a tuple with the Decimals field value +// GetAddressOk returns a tuple with the Address field value // and a boolean to check if the value has been set. -func (o *TokenInfoV1Dto) GetDecimalsOk() (*float32, bool) { +func (o *TokenInfoV1Dto) GetAddressOk() (*string, bool) { if o == nil { return nil, false } - return &o.Decimals, true + return &o.Address, true } -// SetDecimals sets field value -func (o *TokenInfoV1Dto) SetDecimals(v float32) { - o.Decimals = v +// SetAddress sets field value +func (o *TokenInfoV1Dto) SetAddress(v string) { + o.Address = v } -// GetName returns the Name field value -// If the value is explicit nil, the zero value for map[string]interface{} will be returned -func (o *TokenInfoV1Dto) GetName() map[string]interface{} { +// GetDecimals returns the Decimals field value +func (o *TokenInfoV1Dto) GetDecimals() float32 { if o == nil { - var ret map[string]interface{} + var ret float32 return ret } - return o.Name + return o.Decimals } -// GetNameOk returns a tuple with the Name field value +// GetDecimalsOk returns a tuple with the Decimals field value // and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *TokenInfoV1Dto) GetNameOk() (map[string]interface{}, bool) { - if o == nil || IsNil(o.Name) { - return map[string]interface{}{}, false +func (o *TokenInfoV1Dto) GetDecimalsOk() (*float32, bool) { + if o == nil { + return nil, false } - return o.Name, true + return &o.Decimals, true } -// SetName sets field value -func (o *TokenInfoV1Dto) SetName(v map[string]interface{}) { - o.Name = v +// SetDecimals sets field value +func (o *TokenInfoV1Dto) SetDecimals(v float32) { + o.Decimals = v } func (o TokenInfoV1Dto) MarshalJSON() ([]byte, error) { @@ -164,14 +164,10 @@ func (o TokenInfoV1Dto) MarshalJSON() ([]byte, error) { func (o TokenInfoV1Dto) ToMap() (map[string]interface{}, error) { toSerialize := map[string]interface{}{} + toSerialize["symbol"] = o.Symbol.Get() + toSerialize["name"] = o.Name.Get() toSerialize["address"] = o.Address - if o.Symbol != nil { - toSerialize["symbol"] = o.Symbol - } toSerialize["decimals"] = o.Decimals - if o.Name != nil { - toSerialize["name"] = o.Name - } return toSerialize, nil } @@ -180,10 +176,10 @@ func (o *TokenInfoV1Dto) UnmarshalJSON(data []byte) (err error) { // by unmarshalling the object into a generic map with string keys and checking // that every required field exists as a key in the generic map. requiredProperties := []string{ - "address", "symbol", - "decimals", "name", + "address", + "decimals", } allProperties := make(map[string]interface{}) diff --git a/cmd/vault-solver/root.go b/cmd/vault-solver/root.go index 6bdcd7b0..edc82bab 100644 --- a/cmd/vault-solver/root.go +++ b/cmd/vault-solver/root.go @@ -6,6 +6,7 @@ import ( // Solver implementations self-register via init(); these blank imports are the only references to // concrete solvers. Adding another solver is an import here plus a config switch. _ "github.com/symbioticfi/vault-solver/internal/solvers/bridgefacilitator" + _ "github.com/symbioticfi/vault-solver/internal/solvers/lifi" _ "github.com/symbioticfi/vault-solver/internal/solvers/redstoneoev" _ "github.com/symbioticfi/vault-solver/internal/solvers/rfq" ) diff --git a/cmd/vault-solver/run.go b/cmd/vault-solver/run.go index f01c9cd3..a6e019e7 100644 --- a/cmd/vault-solver/run.go +++ b/cmd/vault-solver/run.go @@ -2,6 +2,7 @@ package main import ( "context" + "time" "github.com/go-errors/errors" "github.com/spf13/cobra" @@ -92,9 +93,11 @@ func runBot(ctx context.Context, configPath string, debugFlag, debugFlagSet bool // Shared, nonce-serialized transaction sender. txm := txmanager.New(chainClient, sgnr, chainClient.ChainID(), txmanager.Config{ - Confirmations: cfg.TxManager.Confirmations, - MaxFeeGwei: cfg.TxManager.MaxFeeGwei, - TipGwei: cfg.TxManager.TipGwei, + Confirmations: cfg.TxManager.Confirmations, + MaxFeeGwei: cfg.TxManager.MaxFeeGwei, + TipGwei: cfg.TxManager.TipGwei, + ReplacementInterval: time.Duration(cfg.TxManager.ReplacementIntervalMs) * time.Millisecond, + PendingTimeout: time.Duration(cfg.TxManager.PendingTimeoutMs) * time.Millisecond, }, log) go txm.Start(ctx) diff --git a/config/3f.example.yaml b/config/3f.example.yaml index fb968b5e..0096f6be 100644 --- a/config/3f.example.yaml +++ b/config/3f.example.yaml @@ -27,7 +27,9 @@ signer: txManager: confirmations: 2 # blocks to wait past inclusion before treating a tx as final (default 2) - # maxFeeGwei: 50 # cap on max fee per gas; omit to derive from base fee + maxFeeGwei: 50 # required absolute ceiling; normal sends reserve one bump for cancellation + replacementIntervalMs: 30000 # fee-bump pending transactions every 30s + pendingTimeoutMs: 300000 # cancel the lowest blocked nonce after 5m # tipGwei: 1 # priority fee; omit to use the node's suggestion observability: diff --git a/config/lifi.example.yaml b/config/lifi.example.yaml new file mode 100644 index 00000000..ced278ae --- /dev/null +++ b/config/lifi.example.yaml @@ -0,0 +1,92 @@ +# vault-solver — LI.FI same-chain intent solver (`lifi-samechain`), annotated example. +# +# Publishes standing quotes to the LI.FI Intents order server for LiquidLane-backed same-chain +# RWA -> underlying routes, then listens for matched escrow orders over the LI.FI WebSocket feed. +# +# Secrets are referenced by env-var NAME and read at point of use; ${VAR} fields are expanded from the +# environment at load time. Never commit a real key or production endpoint. + +chain: + rpcUrl: ${ETH_RPC_URL_SEPOLIA} + chainId: 11155111 + # writeRpcUrl: ${WRITE_RPC_URL} + # rpcFallbackUrls: + # - ${ETH_RPC_URL_SEPOLIA_BACKUP} + +signer: + # Runtime caller and tx sender. The executor owner adds it through setCallers(); startup checks isCaller(). + keyEnv: SOLVER_PRIVATE_KEY + +txManager: + confirmations: 2 + maxFeeGwei: 50 # required absolute ceiling; normal sends reserve one bump for cancellation + replacementIntervalMs: 30000 # replace a pending call with higher fees every 30s + pendingTimeoutMs: 300000 # after 5m, cancel the lowest blocked nonce + # Each LI.FI fill also pins its own decision-time fee cap; txmanager clamps fee and tip to that + # budget and drops only when it no longer covers base fee. + # tipGwei: 1 + +observability: + addr: ":9090" + debug: false + +solvers: + - name: lifi-samechain + config: + strategy: + name: default + config: + priceBufferBps: 20 # one rate-move buffer, also reserves upward private-discount output movement + inventoryReserveBps: 500 # never advertise the final 5% of getMaxAssets + minAmount: "1000000" # tokenIn floor; choose it high enough to cover gas and rounding + rangeCount: 8 # target number of exact-input ranges across available capacity + executionDeadlineBuffer: 12s # one Ethereum block left for order and private signatures + + # Gas conversion is a solver fact shared by every strategy. Configure one Chainlink USD feed for + # every distinct adapter vault asset (tokenOut); startup fails if any resolved route is uncovered. + gas: + nativeUsdFeed: ${ETH_USD_FEED} + nativeMaxAge: 2h # actual heartbeat plus testnet publication slack + tokenUsdFeeds: + - token: "0x468BB3245BF520a0CD030BDE029c98aCEAF84C9d" # TLOAN (6 decimals) + feed: ${TLOAN_USD_FEED} + maxAge: 24h # set independently for every token/USD feed + + # External strategy alternative: + # strategy: + # name: webhook + # config: + # url: https://strategy.example/lifi + # timeout: 5s + + orderServer: + baseUrl: https://order-dev.li.fi + wsUrl: wss://order-dev.li.fi + # Deployment convention: one key per executor; all processes using it share its reputation. + apiKeyEnv: LIFI_SOLVER_API_KEY + # httpTimeout: 10s + + # Mirrors RFQ deployment profiles. "external" (default) uses only direct filler-authorized + # adapters. "internal" also uses the shared private-discounts API; the URL is required then. + solverMode: external + # solverMode: internal + # privateDiscountsUrl: ${RFQ_BACKEND_URL} + inputSettler: "0x000025c3226C00B2Cdc200005a1600509f4e00C0" # LI.FI InputSettlerEscrowLIFI + outputSettler: "0x0000000000eC36B683C2E6AC89e9A75989C22a2e" # LI.FI OutputSettler + executor: "0x0000000000000000000000000000000000000000" # registered EIP-1271 LI.FI solver + + # LiquidLane adapter instances this solver serves. Each adapter's vault/asset and tokenToRedeem + # list are resolved on-chain at startup; this config is the executor's route scope. + # Direct filler authorization is required in external mode; signed discounts authorize internal fills. + adapters: + - "0xB5951fecFc34f56a6Ffbd62A2c61cE328E9De70b" # Sepolia TCOL -> TLOAN testbed adapter + + # Scope input tokens against permissionedTokens: "all" (default), "permissioned" (only listed), + # or "permissionless" (only unlisted). Permissioned scope also requires one physical route. + # tokensToQuote: all + # permissionedTokens: + # - "0x..." + + quoteIntervalMs: 1000 # block poll interval; quotes are recalculated only on a new block + quoteTtl: 36s # rolling expiry, about three Ethereum blocks + quoteRefreshMode: block # "block" (default) | "interval" diff --git a/config/rfq.example.yaml b/config/rfq.example.yaml index 4263fc9c..126aeffb 100644 --- a/config/rfq.example.yaml +++ b/config/rfq.example.yaml @@ -25,7 +25,9 @@ signer: txManager: confirmations: 2 # blocks to wait past inclusion before treating a fill as final (default 2) - # maxFeeGwei: 50 # cap on max fee per gas; omit to derive from base fee + maxFeeGwei: 50 # required absolute ceiling; normal sends reserve one bump for cancellation + replacementIntervalMs: 30000 # fee-bump pending transactions every 30s + pendingTimeoutMs: 300000 # cancel the lowest blocked nonce after 5m # tipGwei: 1 # priority fee; omit to use the node's suggestion observability: diff --git a/docs/LIFI-PLAN.md b/docs/LIFI-PLAN.md index bff3c011..cf823ea0 100644 --- a/docs/LIFI-PLAN.md +++ b/docs/LIFI-PLAN.md @@ -1,37 +1,49 @@ # vault-solver — LI.FI / Catalyst same-chain intent filler (plan) -Adding a **`lifi`** solver to `vault-solver` that fills **same-chain** LI.FI Intents (Open Intents -Framework / Catalyst) by redeeming the intent's input RWA through a Symbiotic **LiquidLane adapter** to -produce the output — **atomically, with no held inventory**. Follows the framework boundary and +The **`lifi-samechain`** solver fills **same-chain on-chain** LI.FI Intents (Open Intents Framework / +Catalyst). The executor contract is the registered LI.FI solver identity. Its owner +authorizes runtime callers; a caller submits the selected `FillRoute[]`, and the executor uses the input +settler's direct finalise path, +receives the claimed input RWA in the callback, redeems it through a Symbiotic +**LiquidLane adapter**, then fills and attests the output in one transaction. +Follows the framework boundary and conventions in [`../CLAUDE.md`](../CLAUDE.md); the strategy layer follows [`strategy-plan.md`](strategy-plan.md). -> **Status:** planned (design). Spans two repos: an on-chain executor contract in the sibling `rfq` -> repo, and the off-chain Go solver here. +> **Status:** the on-chain-order path is implemented and has settled a real Sepolia order end to end. +> The solver parses matched escrow orders from the WebSocket feed, takes a fresh LiquidLane fill snapshot, +> runs the strategy decision, builds `LiquidLaneLifiExecutor.finaliseWithCurrentTimestamp(...)` calldata, +> confirms `InputSettlerEscrowLIFI.orderStatus(orderId) == Deposited`, and submits through the shared +> `txmanager`. Gasless opening is explicitly out of scope. The executor is registered once through EIP-1271 +> and the framework signer is an authorized runtime caller; no per-fill solver signature is required. The +> latest ERC-1271-enabled executor ABI still requires the +> redeploy and authorization step tracked in §10 before the next live run. --- ## 1. What it does -A user signs an intent: "here is X of RWA token `tokenIn`; pay me ≥ Y of `tokenOut` (the redeemed -underlying)." The LI.FI order server matches that intent to our standing quote and pushes us the -**signed `StandardOrder`**. We settle it on-chain in **one atomic transaction** via the LI.FI escrow -settler's `openForAndFinalise`, which: +A user opens/funds an intent on-chain: "here is X of RWA token `tokenIn`; pay me ≥ Y of `tokenOut` +(the redeemed underlying)." The LI.FI order server is still used for quote discovery, status tracking, +and matched-order delivery; it pushes the `StandardOrder` to us over the solver WebSocket. We settle +that already-opened order in **one atomic transaction**: -1. pulls the user's RWA input (via their permit2/ERC-3009 signature) and hands it to **our executor - contract** (`destination`), -2. calls back into our executor (`orderFinalised`), where — with the RWA already in hand — the - executor **redeems it through the LiquidLane adapter** to produce `tokenOut`, pays the user via the - OutputSettler, and self-attests, -3. verifies the fill and reverts the whole tx if anything fell short. +1. call `LiquidLaneLifiExecutor.finaliseWithCurrentTimestamp(order, routes)` with the matched + `StandardOrder` and selected LiquidLane routes, +2. the executor calls `InputSettler.finalise(...)` with `solver = destination = address(this)`; the input + settler releases the opened order input to the executor and calls `orderFinalised(inputs, FillCall)` with + the callback payload constructed by the executor, +3. inside the callback the executor redeems the received RWA through the LiquidLane adapter, then fills + and attests the output. -Because settlement is atomic and the output is produced from the just-received input, the solver holds -**zero output inventory** and carries **no float / FX / rebalancing** risk. Profit is the redemption -surplus: `adapter.getAmountOut(RWA, X) − Y`, retained in the executor and swept by its owner. +Because input redemption and output fill are in one transaction, the executor does **not** need prefunded +output inventory for this path. The economic surplus is aggregate redeemed output minus the resolved order +output after the strategy's gas-aware checks. It remains in the executor; the current ABI has no sweep +entrypoint, so recovery requires the proxy administration path described in §7. This is the same-chain specialization of the cross-chain OIF flow. Same-chain is strictly simpler: -`inputOracle == OutputSettler` (the settler is its own oracle — no cross-chain proof relay), and -open + fill + finalise happen in one tx. +`inputOracle == OutputSettler` (the settler is its own oracle — no cross-chain proof relay). The +user/order creator is responsible for the on-chain open step before the solver sees the order. --- @@ -40,101 +52,131 @@ open + fill + finalise happen in one tx. A new self-contained `internal/solvers/lifi/` implementing `solver.Solver` — no framework edits (CLAUDE.md modularity rule). Reused as-is: -- **`Run(ctx)`** connects to the LI.FI order server (WebSocket order feed), refreshes standing quotes - on an interval, and drives the fill loop; blocks until ctx cancels. -- **Fills go through the shared `txmanager`** — the solver builds the `openForAndFinalise` calldata; +- **`Run(ctx)`** connects to the LI.FI order server (WebSocket order feed), refreshes standing quotes, + and evaluates every admitted order once for immediate execution; blocks until ctx cancels. +- **Fills go through the shared `txmanager`** — the solver builds the executor finalise calldata; txmanager owns the nonce, send, and receipt/revert. Same nonce-serialized EOA as every other solver. -- **On-chain reads use `chain.Multicall`** — adapter `getAmountOut` / `getMaxAssets` / `getMaxRate` - batched per quote/price refresh. -- **Signer** — the framework EOA is the registered LI.FI **solver address** and the tx sender. It is - *not* an on-chain signer for the intent (the user signs that); it only sends `openForAndFinalise`. +- **On-chain reads use `chain.Multicall`** — adapter `getAmountOut` / `minDiscount` / `getMaxAssets` / + `getMaxRate`, executor immutables/caller authorization, and filler authorization are batched where appropriate. +- **Signer/caller** — the framework EOA is the tx sender and must be authorized through + `executor.setCallers(...)`. The registered LI.FI solver address is the executor contract itself. - **Config, secrets** — order-server URL + `apiKeyEnv`, settler/executor/adapter addresses via - `solver.config`; the LI.FI API key via `*Env` indirection. + `solver.config`; the LI.FI API key via `*Env` indirection. `solverMode` mirrors RFQ: `external` is + direct-only, while `internal` enables the shared private-discounts backend. - **Pluggable strategy** — both the standing-quote curve and the fill decision are a strategy - (`DecideQuotes` + `DecideFill`; `default` in-process, `webhook` optional later), per + (`DecideQuotes` + `DecideFill`; `default` in-process or `webhook` external), per [`strategy-plan.md`](strategy-plan.md). See §5.2. ### Component / repo map | Piece | Where | Responsibility | |---|---|---| -| `LiquidLaneLifiExecutor` (Solidity) | `../rfq/src/lifi/` | OIF `IInputCallback` callback: redeem input via adapter → `fill` → `setAttestation`. Contract-of-record. | +| `LiquidLaneLifiExecutor` (Solidity) | `../rfq/src/lifi/` | Caller-gated solver/callback contract; `finaliseWithCurrentTimestamp(...)` calls `InputSettler.finalise`; `orderFinalised(..., FillCall)` redeems claimed input via LiquidLane, fills output, and attests; ERC-1271 validates domain-separated registration signatures against the current callers. | | Vendored OIF interfaces/structs | `../rfq/src/lifi/interfaces/` | `IInputCallback`, `MandateOutput`, `StandardOrder`, OutputSettler `fill`/`setAttestation` surface. | -| `lifi` solver (Go) | `internal/solvers/lifi/` | Pricing, decision, `openForAndFinalise` calldata, submit. | +| `lifi` solver (Go) | `internal/solvers/lifi/` | Pricing, decision, finalise calldata with typed `FillRoute[]`, submit. | | Order-server client (Go, generated) | `api/lifiorder/` ← `openapi/lifi-order.openapi.json` | Typed HTTP client for register / `quotes/submit` / `orders` (vendor→generate→commit, like `api/rfqbackend`). The WebSocket order feed is a thin hand-written client. | -| `strategies/{default,webhook}` (Go) | `internal/solvers/lifi/strategies/` | Quote curve + fill decision (`DecideQuotes` + `DecideFill`). | +| LI.FI strategies (Go) | `internal/solvers/lifi/strategies/` | `default` owns local quote/fill policy; `webhook` delegates to `/decide-quotes` and `/decide-fill` and validates returned route references. | | LI.FI order server | external | Discovery: standing quotes + matched-order WS feed. | | OIF settlers | on-chain (LI.FI-owned) | Order lifecycle; **we do not deploy these**. | --- -## 3. On-chain executor — `LiquidLaneLifiExecutor` (contract-of-record) +## 3. On-chain contract — `LiquidLaneLifiExecutor` -New contract in `../rfq/src/lifi/`, modeled on `src/oev/SymbioticOevSolver.sol` (a self-contained -callback for an external protocol that routes through a LiquidLane adapter). It does **not** reuse the -RFQ `Reactor` — the OIF settlers already own the order/signature/nonce/settlement lifecycle. +Contracts live in `../rfq/src/lifi/`. `LiquidLaneLifiExecutor` is a self-contained finalise + callback +executor for LI.FI opened orders. It does **not** reuse the RFQ `Reactor` — the OIF settlers already +own the order/nonce/settlement lifecycle. `LiquidLaneLifiExecutor.finaliseWithCurrentTimestamp` is the +tx entrypoint the Go solver calls. ### Interface ```solidity +// Caller-gated runtime entrypoint. The executor derives settler, solver, and destination itself. +function finaliseWithCurrentTimestamp(StandardOrder calldata order, FillRoute[] calldata routes) external; + // IInputCallback (vendored from OIF) — the settler calls this on `destination`. function orderFinalised(uint256[2][] calldata inputs, bytes calldata call) external; + +// Hashes the LI.FI message hash into the executor's EIP-712 registration domain. +function lifiRegistrationDigest(bytes32 messageHash) external view returns (bytes32); + +// EIP-1271 registration only; accepts a domain-separated signature from any current caller. +function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4); ``` -`inputs` are the RWA amounts already delivered to the executor. `call` is the ABI payload our Go bot -builds. Proposed encoding: +The current contract also exposes `initialize`, `callers`/`setCallers`/`isCaller` plus standard Ownable +`owner()`. It is deployed behind a transparent proxy: settler addresses are implementation immutables; +owner, caller list, and EIP-712 state are initialized in proxy storage. The owner manages caller authorization. +ERC-1271 uses the same caller set but is not used by the Go fill path. + +`inputs` are the RWA amounts delivered to the executor during finalise. The solver submits only `FillRoute[]`. +The executor constructs the callback `FillCall` from the canonical order and those routes: ```solidity struct FillCall { - address adapter; // the LiquidLane adapter to redeem through (must be allowlisted) - address outputSettler; // OIF OutputSettler to fill + attest on - bytes32 orderId; // OIF order id - MandateOutput output; // the single output to satisfy (token, amount, recipient, ...) - uint48 fillDeadline; // from the order - bytes32 solver; // our registered solver identifier (fillerData / attestation) + bytes32 orderId; // OIF order id + MandateOutput output; // the single output to satisfy (token, amount, recipient, ...) + uint32 fillDeadline; // from the order + FillRoute[] routes; // atomic LiquidLane execution legs +} +struct FillRoute { + address adapter; + uint256 amountIn; + uint256 amountOut; // requested direct-swap output; unused by a discount route + FillDiscount discount; // discountId == 0 means direct swap +} +struct FillDiscount { + bytes32 discountId; + ILiquidLaneAdapter.DiscountSwap discountSwap; + bytes protocolSignature; } ``` -### `orderFinalised` flow (inside the atomic tx) - -1. `require(INPUT_SETTLER == msg.sender)` — only the OIF escrow settler may call. -2. Decode `call`; `require(_isAllowedAdapter(fc.adapter))` and `require(fc.outputSettler == OUTPUT_SETTLER)`. -3. Transfer the input RWA to the adapter (`ILiquidLaneAdapter.swap` "assumes tokenIn already - transferred to the adapter") — `SafeERC20.safeTransfer(tokenIn, fc.adapter, amountIn)`. -4. `fc.adapter.swap(Swap{recipient: address(this), tokenIn, amountIn, amountOut: fc.output.amount})` — - the redeemed underlying lands in the executor. (`Swap{address recipient; address tokenIn; uint256 - amountIn; uint256 amountOut;}`.) -5. `require(IERC20(outputToken).balanceOf(self) >= fc.output.amount)` — the redemption covered the - output (belt-and-suspenders; the adapter should deliver `amountOut`). -6. `forceApprove(outputToken, OUTPUT_SETTLER, fc.output.amount)`. -7. `OUTPUT_SETTLER.fill(fc.orderId, fc.output, fc.fillDeadline, abi.encode(fc.solver))` — pays the user - (`transferFrom(executor → recipient)`). -8. `OUTPUT_SETTLER.setAttestation(fc.orderId, fc.solver, uint32(block.timestamp), fc.output)` — writes - the local attestation the settler's `_validateFillsNow` reads (same-chain oracle == settler). -9. Surplus (`redeemed − fc.output.amount`) stays in the executor. +### Execution flow + +1. `finaliseWithCurrentTimestamp(order, routes)` requires an authorized executor caller, computes the order id, + and constructs callback data from `order.outputs[0]`, `order.fillDeadline`, and the supplied routes. +2. It calls `InputSettler.finalise(order, solveParams, bytes32(address(this)), call)` with + `solveParams[0].solver = bytes32(address(this))`. The settler's direct path accepts this because its caller + is the canonical solver contract. +3. After the input is claimed, `orderFinalised` accepts calls only from the immutable input settler, transfers + each route's input to its adapter, and calls direct `swap` or signed + `discountSwap`. The canonical adapter verifies discount signer/protocol signatures and terms. +4. The OutputSettler resolves the accepted limit or exclusive-limit context authoritatively and pulls the + amount it is owed; a shortfall or invalid context reverts the transaction. Dutch contexts are rejected by + the solver before planning. +5. The executor calls `setAttestation(...)`. Any produced surplus stays in the executor. ### Authorization & safety -- **`INPUT_SETTLER`, `OUTPUT_SETTLER` immutable** (constructor); adapters via an **allowlist** - (`setAdapters`, owner-only) or an adapter-factory `isEntity` membership check. -- **`onlyOwner` sweep** for accumulated surplus (`sweep(token, to)`); the executor holds no funds - between txs otherwise. -- The executor must be a **registered filler on each LiquidLane adapter** (adapter `marketMaker` / - `owner` / delegated `isFiller` == executor) — an onboarding prerequisite, exactly like the RFQ - `Executor`. `adapter.swap` reverts `InvalidCaller` otherwise. -- Attack surface is bounded: `openForAndFinalise` requires the **user's signature** to open at all, and - `_validateFillsNow` reverts the whole tx unless the output was paid — so a griefer with a signed - order can at worst make a valid fill on our behalf (paying gas), never redirect the surplus (it stays - in the executor, owner-swept). +- **`INPUT_SETTLER`, `OUTPUT_SETTLER` immutable** (constructor). The Go solver verifies both at startup. +- **Zero governance fee** — this implementation intentionally does not model input deductions. Startup reads + `InputSettler.governanceFee()` and fails unless it is exactly zero. Every admitted order repeats that read + before order identification or planning; a non-zero or unreadable result skips the order and emits an error + log while the process remains available for later orders. With that invariant, the Go solver also requires + the calldata route-input sum to equal the gross order input. +- **Caller runtime gate** — only addresses installed by the owner through `setCallers` can call finalise. + Startup verifies the framework signer through `isCaller`. The configured adapter list is the trusted route + scope; the current executor intentionally has no second adapter allowlist. +- **ERC-1271** is used only for LI.FI account registration. It wraps LI.FI's message hash in the + `LiquidLaneLifiExecutor` version `1` EIP-712 domain for the current chain and executor address, then accepts + a signature from any current caller. It is not used on each fill. +- In `external` mode the executor must be a **registered direct filler** on every configured adapter. + In `internal` mode direct candidates still require that authorization, but signed discount candidates + do not: the adapter authorizes those through the discount signer and protocol cosign. The executor + configured route scope remains mandatory in both modes. +- Attack surface is bounded: the solver only finalises orders that were already opened/funded on-chain, + and the executor/output settler revert the whole tx unless redemption, fill, and attestation all + succeed. A bad order can at worst cost a reverted fill attempt; it cannot redirect output or surplus. ### Placement & house style -`src/lifi/LiquidLaneLifiExecutor.sol` + `src/lifi/interfaces/ILiquidLaneLifiExecutor.sol` + vendored -`src/lifi/interfaces/{IInputCallback,IOutputSettler,...}.sol` (MIT, mirroring `src/oev/interfaces/`). +`src/lifi/LiquidLaneLifiExecutor.sol` + vendored `src/lifi/interfaces/*` (mirroring the RFQ/OEV +contract style). solc `0.8.28`, BUSL-1.1 header, `forge fmt` (120-col, tabs, double quotes, `int_types=long`), I-prefixed interface with full NatSpec, section separators, `callers`/`setCallers`-style patterns. Tests: -`test/lifi/LiquidLaneLifiExecutor.t.sol` (unit, inline mocks à la `test/Reactor.t.sol`) + -`test/lifi/LiquidLaneLifiIntegration.t.sol` (end-to-end same-chain, modeled on +`test/lifi/LiquidLaneLifiExecutor.t.sol` style unit tests + +an on-chain-order E2E script/test (modeled on `catalystsystem/lifi-intent/test/integration/InputSettler7683LIFI.samechain.t.sol` and `test/CoreMirrorIntegration.t.sol`), aiming for 100% line/branch coverage. @@ -148,16 +190,20 @@ uint32 fillDeadline; address inputOracle; uint256[2][] inputs; MandateOutput[] o **`MandateOutput`** (OIF): `{ bytes32 oracle; bytes32 settler; uint256 chainId; bytes32 token; uint256 amount; bytes32 recipient; bytes callbackData; bytes context; }`. Same-chain: `oracle == settler == -OutputSettler`, `chainId == block.chainid`, empty `callbackData`/`context`, and `order.inputOracle == -OutputSettler`. - -**Entrypoint:** `InputSettlerEscrowLIFI.openForAndFinalise(StandardOrder order, address sponsor, bytes -signature, address destination, bytes call)` — `sponsor == order.user`; `signature` = `b1 sigType (0x00 -permit2 / 0x01 3009) || sig`; `destination` = our executor (receives inputs, is the solver identity); -`call` = the `FillCall` payload above. Emits `Open(orderId)` then `Finalised(...)`. +OutputSettler`, `chainId == block.chainid`, empty `callbackData`, and `order.inputOracle == +OutputSettler`. `context` is the OutputSettlerSimple pricing/access payload: empty or `0x00` = limit +amount (`output.amount`), `0x01` = Dutch amount, `0xe0` = exclusive limit, `0xe1` = exclusive Dutch. +The solver supports only limit and exclusive-limit contexts. It discards both Dutch variants at WebSocket +admission and logs the order identifiers and unsupported context type. + +**Entrypoint:** the bot calls +`LiquidLaneLifiExecutor.finaliseWithCurrentTimestamp(order, routes)`. The executor calls the LI.FI +opened-order direct finalise path with the current `block.timestamp`; both canonical solver and destination +are the executor itself, and it constructs the callback `FillCall` internally. Gasless +`openForAndFinalise` is not supported. **Deployed addresses** (LI.FI-owned; integrate against these — do **not** deploy): -- `InputSettlerEscrowLIFI` (has `openForAndFinalise`): `0x000025c3226C00B2Cdc200005a1600509f4e00C0` +- `InputSettlerEscrowLIFI` / opened-order input settler: `0x000025c3226C00B2Cdc200005a1600509f4e00C0` - OutputSettler (LIFI): `0x0000000000eC36B683C2E6AC89e9A75989C22a2e` - (bare OIF reference set: `InputSettlerEscrow 0x1CC9260E285C2C8AC8D2E7102F3978056Ec1d0a8`, `OutputSettlerSimple 0x52602D7cc3D833F5d28ee6D01C7F82C9b2322e10` — deployed at identical addresses on @@ -171,76 +217,206 @@ permit2 / 0x01 3009) || sig`; `destination` = our executor (receives inputs, is ### 5.1 Discovery (LI.FI order server) The REST surface is the **generated `api/lifiorder` client** (from the vendored -`openapi/lifi-order.openapi.json`); all calls carry the `api-key` header (`LIFI_SOLVER_API_KEY`). Wire +`openapi/lifi-order.openapi.json`); all calls carry the `x-api-key` header (`LIFI_SOLVER_API_KEY`). Wire shapes below are verified against the live `order-dev.li.fi` OpenAPI. -**One-time onboarding** (self-serve, no KYC): -1. Create a solver identity + API key in the solver UI (prod `intents.li.fi`, testnet `devintents.li.fi`). -2. Register the framework EOA: `POST /solver-api/account/register` with `{ address, message, signature, - chainId? }` (sign the server-issued message; `chainId` only for EIP-1271). One address ↔ one API key. -3. **Opt into the escrow callback path:** `PUT /api/v1/solver/supported-contracts` with - `{ inputSettler:[{chain, address}], outputSettler:[…], oracle:[…] }` (CAIP-2 chains) listing the - **escrow** `InputSettlerEscrowLIFI` + OutputSettler + oracle (§4 addresses). This is how the order - server routes us escrow orders; our executor is never registered here — it's the `openForAndFinalise` - `destination`. - -**Standing quotes** — every `quoteRefresh`, compute a price curve per configured RWA→underlying route -from `adapter.getMaxRate` / `getAmountOut` / `getMaxAssets`, and `POST /quotes/submit`: +Account, chain, contract, and route prerequisites are the onboarding runbook in §8.1. LI.FI registers the +**executor contract** as the solver account through EIP-1271. On startup the solver verifies that the API +key's registered identities include the configured executor, then checks +`GET /api/v1/solver/supported-contracts` and, when needed, +merges the configured escrow InputSettler and OutputSettler into the complete list with `PUT`. The endpoint +has replace semantics, so the solver preserves existing entries and registers the OutputSettler in both the +`outputSettler` and `oracle` lists. This opts the solver into opened escrow delivery over WebSocket; the same +executor is the on-chain solver identity and callback destination. + +#### Identity, API key, and reputation + +Our deployment convention is **one LI.FI API key ↔ one registered executor contract**. LI.FI supports +multiple registered accounts under one key, but this deployment deliberately keeps each executor on its own +key and reputation. All logical solvers or processes operating through one executor share its +`LIFI_SOLVER_API_KEY`, quotes, matched orders, and status. + +The LI.FI API key, executor owner key, and authorized caller transaction key are distinct credentials. +Sharing the LI.FI identity does not make uncoordinated active-active processes safe: they would observe the +same order flow, and every authorized caller can submit the same fill. Multiple instances must use a single +active sender or shared order coordination; active/standby replicas are the simple supported deployment. + +**Standing quotes** — on each `quoteIntervalMs` tick, or once per new block when +`quoteRefreshMode: block` (block mode polls at `quoteIntervalMs`, default 1s), compute one non-overlapping +price curve per RWA→underlying pair from `adapter.getMaxRate` / `getMaxAssets`, and +`POST /quotes/submit`: ``` { quotes: [{ fromChain, toChain, // toChain == fromChain for our same-chain routes fromAsset, toAsset, fromDecimals, toDecimals, ranges: [{ minAmount, maxAmount, quote }], // quote = toAsset per 1 fromAsset, decimal string - expiry, exclusiveFor }] } // exclusiveFor = our solver address → matched orders route only to us + expiry, exclusiveFor }] } // exclusiveFor = executor address → matched orders route only to us ``` +`fromChain` and `toChain` are transport fields only: `orderClient` initializes both once from the solver's +configured runtime chain. Strategy outputs and quote-state keys contain only the local token pair, so a +same-chain solver cannot accidentally publish a mixed-chain curve. + +There are two independent exclusivity layers. Quote `exclusiveFor = executor` tells the order server which +registered solver should receive a match. Supported on-chain exclusivity is encoded as an `0xe0` exclusive +limit context: before its start time only the encoded `exclusiveFor` address may fill; afterwards any allowed +solver may fill. The strategy resolves that context at decision time and skips an order that is not executable +by this executor now. Exclusive Dutch (`0xe1`) is unsupported and discarded on receipt. `quoteId` is optional +correlation metadata only: it is not an authorization input, is not used by the contract, and may be absent in +the WS event. **Order feed** — subscribe to the WebSocket `user:vm-order-submit` event (respond to `ping` with -`pong`; dedup on `orderId`). Each message is a `SubmitOrderDto`: +`pong`). Each accepted message is enqueued once in an in-memory FIFO and evaluated once; it is neither +persisted nor retried. It is a `SubmitOrderDto`: ``` -{ orderType, quoteId, - inputSettler, // escrow-vs-Compact discriminator — must be the ESCROW settler for our callback path - sponsorSignature, // user's permit2/3009 signature — required by openForAndFinalise +{ orderType?, quoteId, + inputSettler, // escrow-vs-Compact discriminator — must be the opened ESCROW settler order: StandardOrder, meta: { orderStatus: Signed|Delivered|Settled, onChainOrderId, ... } } ``` -The callback (no-inventory) path requires `inputSettler` = the escrow settler, `orderStatus: Signed` -(unopened), and a permit2/3009 `sponsorSignature` (see §10). Registering our executor as the callback -destination is likely `PUT /api/v1/solver/supported-contracts` — confirmed in P1. +We do **not** listen to on-chain events for discovery. The order must arrive via the LI.FI WebSocket. +The fill path requires `inputSettler` = the configured escrow input settler and a live, not-yet-settled +status (`Signed`/`Delivered` today). LI.FI's opened-order WS message currently omits `orderType`, so an +absent value is accepted; an explicitly supplied value is fail-closed to the opened on-chain shapes we +know (`OnChainOrder` / `oif-user-open-v0`). A missing type is inferred only when +`meta.onChainOrderId` and `inputSettler` are present, and is not trusted by itself: the full +`StandardOrder`, configured escrow identity, canonical order ID, and `Deposited` on-chain status are still +required. It does +not require a gasless permit/3009 signature or backend `sponsorSignature`. ### 5.2 The strategy — owns both decisions All pricing lives in a pluggable strategy (per [`strategy-plan.md`](strategy-plan.md)): the solver supplies **raw facts** (adapter reads) and **executes** (publish quotes, send the tx); the strategy is the brain for **both** decision points — the standing-quote curve *and* the fill decision — mirroring -rfq's `DecideQuote`/`BuildFillPlan`. +rfq's `DecideQuote`/`BuildFillPlan`. LiquidLane route/inventory/fill-quote terminology follows +[`LIQUIDLANE-CONVENTIONS.md`](LIQUIDLANE-CONVENTIONS.md), so LIFI-specific route snapshots should map +from shared `Route`, `Inventory`, and `FillQuote` facts rather than defining a third LiquidLane shape. ```go type Strategy interface { // §5.1 standing-quote curve, from configured routes + live adapter facts. - DecideQuotes(ctx, QuoteInput) (QuoteOutput, error) // → per-route ranges[] {minAmount,maxAmount,quote} - // A matched WS order + fresh adapter reads → fill-or-skip and the FillCall params. + DecideQuotes(ctx, QuoteInput) (QuoteOutput, error) // → per-pair ranges[] {minAmount,maxAmount,quote} + // A matched WS order + fresh adapter reads → immediate fill or skip. DecideFill(ctx, FillInput) (*FillPlan, error) } ``` -- **`QuoteInput`** = the configured routes plus, per route, the adapter facts the solver read - (`getMaxRate`, `getAmountOut` at tier points, `getMaxAssets`, token decimals). `DecideQuotes` returns - the `ranges[]` curve the solver POSTs to `/quotes/submit`; the `default` sets - `quote = adapterRate × (1 − minMarginBps)` and caps `maxAmount` at `getMaxAssets`. -- **`FillInput`** = the matched signed `StandardOrder` plus a fresh `getAmountOut(tokenIn, amountIn)` / - `getMaxAssets(tokenIn)` read. `DecideFill` returns a `*FillPlan` (fill) or `nil` (skip). The `default` - fills iff `redeemed ≥ output.amount + minMargin`, `amountIn ≤ getMaxAssets`, the adapter asset matches - `output.token`, and the order is within `fillDeadline`/`expires`. - -The solver then executes the result — publish the curve, or build + send -`openForAndFinalise(destination = executor)` from the `FillPlan`'s `FillCall`. `default` = in-process; -`webhook` = external decider — same trusted-strategy model as rfq/3f/oev, so swapping the pricing brain -never touches the solver skeleton. +- **`QuoteInput`** = shared `[]liquidlane.Inventory`, latest LiquidLane gas snapshot + (adapter-local owner/market-maker `acquireBalance` and vault-level shared `freeAssets`/`withdrawable`), vault-level in-flight capacity + reservations, chain time, server wall time, solver-owned quote expiry, and raw current + `txmanager.MaxFeePerGas`. The shared LiquidLane predictor derives every adapter swap route as + acquire/allocate/deallocate/unknown. The solver reads Chainlink native/USD and token/USD feeds at the + latest state and passes a `tokenOut per native` snapshot to the strategy. Every distinct resolved + adapter `tokenOut` must have a configured feed; missing coverage fails startup and stale/invalid rounds + fail closed for that decision. Gas units are code-owned conservative constants: 250k fixed LI.FI + settlement, shared LiquidLane route units, and 75k for each private route. + `DecideQuotes` applies `inventoryReserveBps` to the gas snapshot before classifying each route, then + orders physical routes greedily by executable rate and capacity into one pair-level ladder capped at + three routes. For each route it keeps at most one best direct and one best private candidate; lower-rate + or wider private alternatives are deliberately ignored. One local `solveExactInputQuote(amount)` distributes + a concrete input greedily. A candidate must cover the whole leg assigned to its physical route, so a + narrow private candidate is used for small legs while a wider direct candidate handles larger legs. + The planner does not backtrack into another route ordering. The strategy targets `rangeCount` + geometrically distributed ranges (default eight, maximum sixteen); there is no profitability binary + search. Each range runs the same exact-input operation at its endpoints and immediately before and after + every possible route/alternative capacity transition inside it. The strategy converts the largest sampled + transaction gas cost into `tokenOut` and deducts two price-movement + stages (quote→decision and decision→inclusion). There is no minimum-profit setting. If the configured + `minAmount` boundary is not economically positive, that whole range is omitted. Every sampled amount uses + the largest sampled gas cost, so route and gas transitions inside the interval cannot overquote. Every range + uses a rate guaranteed across its full amount interval; lower-rate + later adapters cannot make a blended range overquote. Solver-level token admission uses the shared + `internal/tokenpolicy` policy also used by RFQ: `all` serves every input, `permissioned` serves only + `permissionedTokens`, and `permissionless` serves only inputs outside that set. Only the + `permissioned` scope is single-route: the solver passes that constraint into each strategy decision, + the curve uses one physical route, and the solver rejects any fill plan that does not contain exactly + one route. Direct and private candidates for one route are alternatives, never additive, and may own different + non-overlapping ranges. Routes sharing a vault share one conservative `CapacityID`; reserve is applied + before in-flight amounts are subtracted. In internal mode, advertised discount inventory is bounded + by current on-chain `getMaxAssets`/`getMaxRate` and its deadline. The backend discount and its already-net + `maxRate` are validated together; the strategy must not apply the ppm discount to that rate again. + Quote lifetime + belongs to the solver cadence: by default the head is polled every second, quotes are recalculated once per + new block, at most three physical routes are used, and `quoteTtl` is 36 seconds. Unchanged quotes are renewed + when at most `max(quoteInterval, quoteTtl / 3)` remains, even when no new block is observed or the head poll + fails. The strategy + may only shorten that expiry to `discount deadline - executionDeadlineBuffer`. +- **`FillInput`** = the matched signed `StandardOrder` output facts (`output.amount`, raw + `output.context`) plus fresh `getAmountOut`, `minDiscount`, `getMaxAssets`, pending fill reservations + by shared `CapacityID`, and the same latest LiquidLane gas facts. Direct candidates require current + filler authorization. Internal discount candidates are resolved again through the + backend, validated against the advertised ID/adapter/token/deadlines and adapter minimum, then priced + as `getAmountOut * (1 - signedDiscount)`. + `DecideFill` returns an immediate `*FillPlan` or `nil`; the solver does not retain or retry skipped orders. + The `default` resolves the supported OutputSettlerSimple contexts: limit and exclusive limit both use + `output.amount`, while an exclusive order for another solver before `startTime` is declined. Dutch and + exclusive Dutch orders never reach the strategy because WebSocket admission discards them. It fills + only when aggregate fresh output covers resolved amount + one execution price buffer + gas for + every selected leg, the adapter asset matches `output.token`, and `fillDeadline`/`expires` plus + private-signature deadlines have at least `executionDeadlineBuffer` remaining. The plan commits a target + after downward `priceBufferBps` and an aggregate internal `minAmountOut = resolvedAmount + gas`. For direct + routes, the calldata `amountOut` is the buffered target and the adapter either produces it or reverts. A + private-discount swap uses its signed terms instead of calldata `amountOut`, so + the strategy requires its full current output plus upward `priceBufferBps` to fit reserved capacity. + The current adapter minimum is checked directly; there is no separate discount-headroom policy. + Permissionless tokens may split the order across independent capacity domains. Selection keeps at most + the best direct and best private candidate per physical route, then greedily assigns each remaining leg + to the highest-rate candidate that can cover that route's full available share. Shared-vault capacity is + reserved as each leg is selected. There is no capacity-first retry or plan comparison: once the complete + allocation is built, full route-aware gas is charged and the plan is either executed or skipped. + Routes sharing a vault consume one aggregate capacity and gas-liquidity budget. Direct and + private candidates for the same route remain mutually exclusive. + +The order worker owns pending fills and their capacity reservations. It reserves each direct route's +target output and each private route's upward-buffered output against its shared `CapacityID` while an +accepted fill tx is in flight, passes the aggregate reservation snapshot to every later fill decision, +and releases it when that send completes. The quote coordinator receives the same reservation changes +and subtracts them from published capacity. On startup, when any economic payload changes, or when expiry enters the renewal +window, it submits the replacement curve directly; LI.FI overwrites the old quote for the pair. When a pair +stops quoting, it submits the last curve with an expiry in the past, which overwrites and immediately expires +the old server-side quote. An unchanged pair is not reposted on every calculation tick. + +The solver then executes the result — publish the curve, or send one +`finaliseWithCurrentTimestamp(order, routes)` tx from the +`FillPlan`. `default` is in-process. `webhook` posts the same raw snapshots to `/decide-quotes` and +`/decide-fill`; its response is a `FillPlan` or `null`. The solver normalizes adapters/capacity IDs from +trusted candidates and rejects unknown, oversized, duplicated, input-mismatched, capacity-conflicting, +or gas-negative routes before calldata construction. LI.FI settlement and private-payload gas constants +live in the strategy package and are shared by both validators. ### 5.3 Build & submit -Encode the `FillCall` payload → build `InputSettlerEscrowLIFI.openForAndFinalise(order, order.user, -signature, EXECUTOR, call)` via generated bindings → submit through `txmanager`. One tx per fill; an -on-chain revert (e.g. someone else filled, or price moved) marks the attempt failed and it's dropped -(the order is gone). +Convert the strategy plan into typed executor `FillRoute[]` → require the route input sum to equal the gross +order input → pack `LiquidLaneLifiExecutor.finaliseWithCurrentTimestamp(order, routes)` via generated +bindings → read `InputSettlerEscrowLIFI.orderStatus(orderId)` again → submit only when the status is +`Deposited`. +The executor derives the solver identifier from `address(this)`. The WS handler +places parsed orders into an in-memory FIFO without blocking the socket reader, so ping/pong and later messages +continue while one planner evaluates accepted orders in arrival order. It reads +fresh state and gas, asks the strategy, builds calldata, and immediately submits the result. No `FillPlan`, +gas cap, adapter snapshot, discount resolution, or calldata waits in a second queue. The solver has no local +in-flight limit: every accepted order is handed to the shared txmanager as soon as planning finishes. +An admitted order first verifies `governanceFee() == 0`, then derives the canonical ID and verifies +`orderStatus == Deposited` before expensive route reads. It selects only configured routes matching both +order tokens. For private candidates it resolves the +signatures under one order-server timeout, then re-reads latest-state LiquidLane inventory and current block +time before each strategy decision. That decision-time max fee is passed as a hard per-request cap to `txmanager`. +Before broadcast, txmanager clamps its fee cap and tip to that budget and drops the fill only if the current base +fee itself no longer fits. It verifies `Deposited` again immediately before async submission. The shared txmanager +serializes fee selection, signing, nonce assignment, +and broadcast, but waits for receipts independently, allowing consecutive nonces to be pending together. Pending +calls are fee-bumped within their decision cap. After the shared pending timeout, txmanager cancels only the +lowest unresolved nonce with a same-nonce self-transfer; this cancellation is outside the fill's profitability +cap but remains bounded by the operator's required global `txManager.maxFeeGwei`. Normal sends reserve one +replacement bump below that global ceiling so cancellation still has fee headroom. LI.FI +requests complete at inclusion/revert rather than waiting for the txmanager's extra confirmation depth; the +planner then releases that fill's reservation. Every later fill decision subtracts aggregate pending +capacity before route allocation. At inclusion, the LiquidLane adapter and OutputSettler enforce the requested +swap and resolved output; stale state therefore reverts atomically rather than being repriced by the executor. +There is no solver-level pending plan, timer, future-auction scheduling, or new fill attempt. The txmanager +may replace the same pending nonce as described above; that is fee management for one submission, not order +retry. +For a selected private candidate, the solver commits the fresh signed terms and both signatures inside +the selected `FillRoute`; a missing or mismatched resolution aborts before submission. Those two signatures +authorize the private LiquidLane route and are unrelated to LI.FI account or fill authorization. ### 5.4 Config block (sketch) @@ -248,19 +424,38 @@ on-chain revert (e.g. someone else filled, or price moved) marks the attempt fai solvers: - name: lifi-samechain config: - strategy: { name: default, config: {} } + strategy: + name: default + config: + priceBufferBps: 20 + inventoryReserveBps: 500 + minAmount: "1000000" # tokenIn floor sized to cover gas and rounding + rangeCount: 8 # target exact-input ranges across available capacity + executionDeadlineBuffer: 12s + gas: + nativeUsdFeed: "0x…" + nativeMaxAge: 1h # native/USD feed heartbeat + tokenUsdFeeds: + - token: "0x…" # every resolved adapter tokenOut + feed: "0x…" # token/USD Chainlink feed + maxAge: 24h # this token/USD feed's heartbeat orderServer: baseUrl: https://order-dev.li.fi # order.li.fi in prod - wsUrl: wss://order-dev.li.fi/... # confirm exact WS path in P1 + wsUrl: wss://order-dev.li.fi apiKeyEnv: LIFI_SOLVER_API_KEY - solverAddress: "0x…" # our registered solver EOA (== signer) + solverMode: internal + privateDiscountsUrl: ${RFQ_BACKEND_URL} inputSettler: "0x000025c3226C00B2Cdc200005a1600509f4e00C0" outputSettler: "0x0000000000eC36B683C2E6AC89e9A75989C22a2e" - executor: "0x…" # our deployed LiquidLaneLifiExecutor + executor: "0x…" # registered EIP-1271 LiquidLaneLifiExecutor adapters: # LiquidLane adapters (RWA→underlying); vault+asset resolved on-chain - "0x…" - minMarginBps: 10 # required surplus over the order's output - intervals: { quoteRefresh: 30s, statePoll: 10s } + tokensToQuote: permissioned # all (default) | permissioned | permissionless + permissionedTokens: # membership set; single-route only in permissioned scope + - "0x…" + quoteIntervalMs: 1000 # block poll interval; default is 1000ms + quoteTtl: 36s # rolling expiry, about three Ethereum blocks + quoteRefreshMode: block # block (default) | interval ``` --- @@ -268,20 +463,19 @@ solvers: ## 6. Data flow (end to end) ``` -LI.FI order server ──(WS: signed StandardOrder)──▶ lifi solver - price: adapter.getAmountOut(RWA, X) → redeemed ; adapter.getMaxAssets → cap - decide: redeemed ≥ output.amount + margin && X ≤ cap ? ── no ─▶ skip +LI.FI order server ──(WS: opened/funded StandardOrder)──▶ lifi solver + price: fresh direct getAmountOut or signed-discount output; getMaxAssets → reserved cap + decide: buffered target ≥ resolved output + gas, deadlines buffered ? ── no ─▶ skip │ yes - build FillCall + openForAndFinalise(order, user, sig, EXECUTOR, call) + build FillRoute[]; require Σ amountIn == order input │ - txmanager ─▶ InputSettlerEscrowLIFI.openForAndFinalise(...) - ├─ pull user's RWA (permit2) → EXECUTOR - ├─ EXECUTOR.orderFinalised(inputs, call): - │ RWA → adapter ; adapter.swap(→ underlying to EXECUTOR) - │ OUTPUT_SETTLER.fill(orderId, output, deadline, solver) // pays user - │ OUTPUT_SETTLER.setAttestation(orderId, solver, ts, output) - └─ _validateFillsNow ✓ (atomic; reverts all if unfilled) - surplus (redeemed − output.amount) accrues in EXECUTOR → owner sweeps + txmanager ─▶ LiquidLaneLifiExecutor.finaliseWithCurrentTimestamp(order, routes) + └▶ InputSettlerEscrowLIFI.finalise(... solver=destination=EXECUTOR ...) + ├─ deliver opened order RWA → EXECUTOR + └─ EXECUTOR.orderFinalised(inputs, FillCall): + direct swap(amountOut) or signed discount swap → EXECUTOR + OUTPUT_SETTLER.fill + setAttestation + surplus (redeemed - resolved output) remains in EXECUTOR ``` --- @@ -289,16 +483,42 @@ LI.FI order server ──(WS: signed StandardOrder)──▶ lifi solver ## 7. Error handling & safety - **Atomic revert-safety** is the backbone: if the redemption under-delivers, the adapter reverts, or - the output isn't paid, `_validateFillsNow` reverts the entire tx — no partial state, no stuck funds. -- **Pre-submit skips** (never send a doomed tx): unprofitable (`redeemed < output.amount + margin`), - over-capacity (`amountIn > getMaxAssets`), asset mismatch, past deadline/expiry, adapter paused. -- **Staleness** — price/capacity reads are refreshed on `statePoll`; a matched order is priced against - a fresh read at decision time, not the quote-time curve. + the output fill/attestation fails, the entire tx reverts — no partial state, no stuck funds. +- **Pre-submit skips** (never send a doomed tx): insufficient output + (aggregate buffered target below output amount + selected-leg gas), buffered private output above reserved + capacity, invalid current private discount bounds, asset mismatch, deadline/expiry inside the execution + buffer, or adapter paused. +- **Inclusion-time enforcement** — direct routes ask the adapter for the buffered target; private routes use + the signed terms. If current adapter state cannot execute the request or the OutputSettler cannot pull the + accepted order amount, the whole transaction reverts. +- **Gas-aware quotes** — the solver supplies the live txmanager fee cap, latest LiquidLane gas state, and + Chainlink-derived token/native conversion as raw facts. Code-owned fixed settlement/private units combine + with shared route prediction; there is no separate gas padding knob. Operators set `minAmount` high enough + to cover gas, both quote-time price windows, and rounding. + The strategy charges complete-plan gas after route allocation and omits any capacity range whose lower + boundary is not economically positive. +- **Capacity safety** — routes sharing a vault share one conservative capacity domain. Both quote and + fill planning subtract in-flight buffered outputs before allocating that shared capacity. Each fill + still uses a fresh chain snapshot and the adapter enforces execution at inclusion. An economic change removes + old server ranges before replacement. +- **Authorization safety** — startup validates executor immutables and requires the framework signer to be + authorized by `executor.isCaller`. Startup and every admitted order also require + `InputSettler.governanceFee() == 0`; fee-bearing input settlement is intentionally unsupported. External mode + additionally requires direct `owner/marketMaker/isFiller` authorization for every route. Internal mode + checks direct authorization dynamically and otherwise requires fresh adapter-verified discount signatures. +- **Staleness** — a matched order is priced against a fresh read at decision time, not the quote-time + curve. Private-signature resolution is bounded by one timeout and followed by another adapter/block-time + read, so network latency cannot silently preserve the pre-resolution capacity snapshot. - **Competition** — same-chain fills are winner-take-all on-chain; `exclusiveFor` on our quotes routes matched orders to us, but a late/again-priced fill can still revert (already filled) → drop. -- **No inventory / callback-balance risk** (unlike OEV): nothing is fronted; the only capital at risk - per tx is gas, and reverts cost only gas. -- **Executor surplus** is the sole standing balance; owner-swept, never user-redirectable. +- **Private discounts** — internal mode uses shared `internal/liquidlane/discounts` parsing. Advertised terms + may shape standing quotes, but execution always resolves fresh signatures, recomputes output from the + current adapter oracle, and commits the selected discount ID and typed payload in `FillRoute`. +- **No prefunded working inventory is required** (unlike OEV): the opened input funds each atomic + redemption. A reverting fill spends gas only; accumulated executor surplus is a separate standing balance + governed by the deployment and zero-fee invariant. +- **Executor surplus** may remain as a standing balance. The current PR #18 executor ABI has no sweep entrypoint; + recovery therefore requires the deployment's proxy-upgrade administration rather than the runtime solver. --- @@ -308,38 +528,145 @@ LI.FI order server ──(WS: signed StandardOrder)──▶ lifi solver LiquidLane adapter on a public testnet, so the dev environment matches production one-for-one. The local foundry loop (§8.3) is kept only for fast contract-unit iteration, not for integration. -### 8.1 Onboarding (verified, self-serve) -No KYC / approval gate. Testnet UI `devintents.li.fi` + order server `order-dev.li.fi` (both live; prod -is `intents.li.fi` / `order.li.fi`). Create a solver identity → API key → sign a registration message -and `POST /solver-api/account/register` the solver EOA. Secret via `LIFI_SOLVER_API_KEY` env. +### 8.1 Onboarding and prerequisites + +The current dev onboarding is self-serve. Testnet uses `devintents.li.fi` and `order-dev.li.fi`; production +uses `intents.li.fi` and `order.li.fi`. Treat dev and production as separate environments: create and +register the identity in the target environment and do not assume a dev API key or registration is valid in +production. + +#### Supported scope + +| Supported | Rejected / out of scope | +|---|---| +| One configured EVM chain; same-chain input and output. | Cross-chain orders or a chain different from runtime config. | +| Already-opened `InputSettlerEscrowLIFI` order delivered over the LI.FI WebSocket. | Compact, Permit2, ERC-3009, gasless submit, and `openForAndFinalise`. | +| One ERC-20 input, one output, full fill. | Native input, multiple inputs/outputs, and partial fills. | +| Configured OutputSettler as input oracle, output oracle, and output settler. | Unknown settlers/oracles and non-empty output callback data. | +| The default strategy handles limit and exclusive-limit output contexts. | Dutch and exclusive Dutch are ignored globally. The default strategy rejects unknown or malformed contexts; a webhook strategy must decline every non-Dutch context it cannot resolve. | +| Immediate decide-and-send using current time and state. | Retaining or scheduling a future exclusive-limit order for later retry. | +| WebSocket discovery with an on-chain `Deposited` check before send. | On-chain event discovery or trusting WS status without the chain check. | + +#### Ownership map + +| Owner | Must provide | +|---|---| +| LI.FI | Solver identity/API key, executor-account registration, order server + WebSocket access, canonical OIF settler addresses, and support for the target chain. | +| Solver operator (us) | The executor owner EOA, an authorized caller EOA with gas funds, RPC access, a deployed EIP-1271 `LiquidLaneLifiExecutor`, YAML config, and monitoring. | +| LiquidLane adapter owner | A live adapter/route with capacity and rate data, plus direct filler authorization for our executor when direct execution is required. | +| Test order creator | A separate user EOA, input-token balance and approval, and the ability to open/fund an escrow order through `open`/`openFor`. The order server indexes that on-chain order. | + +#### Required before startup + +1. **Choose one chain and route.** The chain must be returned by the target order server's + `/chains/supported` endpoint and host the LiquidLane adapter, input token, output token, and canonical + LI.FI escrow InputSettler/OutputSettler. This solver is same-chain only; `fromChain == toChain`. +2. **Use the canonical settlers.** Put the target chain's opened-order `InputSettlerEscrowLIFI` and + OutputSettler addresses in config. We do not deploy these contracts and we do not support Compact, + Permit2/3009, gasless submit, or `openForAndFinalise` orders. +3. **Deploy our executor.** Deploy the ERC-1271-enabled `LiquidLaneLifiExecutor` implementation with immutable + input/output settlers matching config, then a transparent proxy initialized with the owner EOA and an initial + caller list containing the EOA from `signer.keyEnv`. Keep the proxy-admin owner separate and recorded. The + proxy address is the configured and registered LI.FI solver account; the owner manages callers and callers + can finalise. Configured adapters are the solver's trusted route scope. +4. **Create the API key and register the deployed executor.** Create the target-environment API key and fetch + the server-issued message from `GET /api/v1/solver/register/message`. Compute its standard EVM + `hashMessage(message)`, then sign the EIP-712 `LifiRegistration(bytes32 messageHash)` value with any current + caller using domain `{ name: "LiquidLaneLifiExecutor", version: "1", chainId, verifyingContract: executor }`. + Submit `POST /api/v1/solver/register` with `{ message, signature, account: executor, + chain: "eip155:" }`. LI.FI passes its message hash and the signature to + `executor.isValidSignature`. Keep the key only in the environment named by `orderServer.apiKeyEnv` + (normally `LIFI_SOLVER_API_KEY`). Under our deployment convention all processes using this executor + share the key and reputation; use another executor and key for an independent deployment. +5. **Authorize LiquidLane execution.** For every configured adapter, verify its vault, output asset, + redeemable input-token list, current capacity, and rate. Grant `setFiller(executor, true)` for direct + routes. In `internal` mode a signed private-discount leg has its own authorization, but any direct + fallback still needs filler authorization. +6. **Configure gas conversion.** Provide one native/USD Chainlink feed and one token/USD feed for every + distinct adapter output asset. Set `gas.nativeMaxAge` and every `gas.tokenUsdFeeds[].maxAge` + from the feed's heartbeat plus realistic publication slack; stale, non-positive, missing, or materially + future-dated rounds fail the quote/fill decision closed. +7. **Optional private discounts.** `solverMode: external` needs no discount backend and serves only + direct-authorized routes. `solverMode: internal` additionally requires a reachable + `privateDiscountsUrl` and active signer/protocol policies for the configured adapters. + +#### Deployment preparation + +Before each testnet or production deployment, record enough information in the operator's normal release +process to reproduce and audit it: executor source revision and compiler settings, implementation constructor +arguments, proxy initializer arguments and proxy-admin owner, target chain, expected owner and settler addresses, +implementation/proxy addresses and transactions, verified runtime bytecode, LI.FI registration result, and +every adapter filler-authorization transaction. These values are +deployment-specific and are intentionally not pinned in this repository. + +The minimum operator config is [`../config/lifi.example.yaml`](../config/lifi.example.yaml). Before +starting, replace every zero/placeholder address and provide these secrets without putting them in YAML: + +| Environment | Config reference | Purpose | +|---|---|---| +| `SOLVER_PRIVATE_KEY` | `signer.keyEnv` | Authorized executor caller and tx sender; it may be separate from the owner. | +| `LIFI_SOLVER_API_KEY` | `orderServer.apiKeyEnv` | REST quote/supported-contract calls and WebSocket authentication. | +| RPC URL variables | `chain.rpcUrl` / optional write and fallback URLs | Current-state reads and transaction submission. | +| Chainlink feed variables | `gas.nativeUsdFeed`, `gas.tokenUsdFeeds[]` | Native gas cost converted into each output token; every feed has its own required max age. | +| `RFQ_BACKEND_URL` | `privateDiscountsUrl` | Required only for `solverMode: internal`. | + +#### Startup preflight performed by the solver + +Startup fails before quote publication when config is invalid, any configured adapter's `vault()`, vault +`asset()`, token list, or token decimals cannot be resolved, no adapter routes resolve, an output token has +no configured gas oracle, executor settler immutables do not match, the signer is not returned by +`executor.isCaller`, `InputSettler.governanceFee()` is non-zero or unreadable, the API +key does not list the executor as a registered solver identity, or external mode lacks direct filler +authorization. After those checks the solver reads `GET /api/v1/solver/supported-contracts`; if needed it +preserves the current lists and adds the configured escrow InputSettler plus the OutputSettler in both the +`outputSettler` and `oracle` lists with one replacement `PUT`. + +#### Onboarding acceptance check + +Onboarding is complete only when all of the following are observed in the target environment: + +1. The process starts without route, oracle, executor, authorization, or supported-contract errors. +2. The order server accepts a non-empty same-chain quote whose `exclusiveFor` is the registered executor. +3. A user calls the canonical `open`/`openFor` path. The order server indexes the transaction without + `POST /orders/submit` and delivers `user:vm-order-submit` with a full `StandardOrder` and + `meta.onChainOrderId`; `quoteId` may be absent and no on-chain event listener is involved. +4. Immediately before submission the canonical order ID matches and on-chain status is `Deposited`. +5. The executor transaction succeeds atomically: input claim -> LiquidLane redemption -> OutputSettler + fill/attestation. The user receives the required output and backend status becomes `Settled`. +6. Receipt gas is consistent with the conservative settlement constants and the submitted fee remains within + the decision and global fee caps; any surplus is held by the executor. ### 8.2 Testnet dev environment (primary loop) -Target **Ethereum Sepolia** (chainId 11155111) — the intersection of: LI.FI `order-dev` support, the -canonical OIF settlers (deployed there, §4 addresses), and an existing Symbiotic **LiquidLane adapter** -(the redstone-oev / rfq work already runs on Sepolia LiquidLane adapters). Confirm one adapter that -redeems a testnet RWA → its underlying, or point at/deploy one (an open item, §10). +Target **Ethereum Sepolia** (chainId 11155111) — the intersection of LI.FI `order-dev` support, the +canonical OIF settlers (deployed there, §4 addresses), and the existing Symbiotic **LiquidLane adapter** +used by the redstone-oev / RFQ testbed. The v1 route is **TCOL → TLOAN** (redstone-oev testbed): adapter `0xB5951fec…70b`, TCOL (RWA) `0x17e892…A4D3`, TLOAN (underlying) `0x468BB3…4C9d`. One-time setup: -1. **Executor** — deploy `LiquidLaneLifiExecutor` to Sepolia (`INPUT_SETTLER`/`OUTPUT_SETTLER` = the - LI.FI addresses in §4; adapter allowlist = `0xB5951fec…70b`). -2. **Filler auth** — the testbed owner `0x8124…7309` registers our executor as a filler on the adapter - (`marketMaker`/`owner`/`isFiller` == executor). -3. **Solver identity + opt-in** — register the framework EOA on `devintents.li.fi` + `POST - /solver-api/account/register`, then `PUT /api/v1/solver/supported-contracts` listing the escrow - settler + OutputSettler + oracle for `eip155:11155111`; fund the EOA with Sepolia ETH for gas. -4. **Config** — a `config/lifi.sepolia.example.yaml` pointing `orderServer` at `order-dev.li.fi`, the §4 - settler addresses, our deployed executor, and the TCOL→TLOAN adapter above. +1. **Contracts** — deploy the ERC-1271-enabled `LiquidLaneLifiExecutor` implementation and transparent proxy + to Sepolia (`INPUT_SETTLER`/`OUTPUT_SETTLER` implementation immutables = the LI.FI addresses in §4; + proxy initializer owner = admin EOA; initial callers include the framework signer). +2. **Solver identity** — register that deployed executor on `devintents.li.fi` through the V1 EIP-1271 + flow, and fund the framework caller EOA with Sepolia ETH. +3. **Filler auth** — the testbed owner `0x8124…7309` registers our executor as a filler on the adapter + (`setFiller(executor, true)` / equivalent owner path). +4. **Config** — copy `config/lifi.example.yaml` into an operator-local config, point `orderServer` at + `order-dev.li.fi`, and set the §4 settlers, deployed executor, TCOL->TLOAN adapter, RPC, and gas feeds. + On first startup the solver preserves existing supported contracts and adds the escrow InputSettler plus + the OutputSettler as both output settler and oracle for `eip155:11155111`. The loop, on every change: -1. Run the bot → it submits an **exclusive** standing quote (`exclusiveFor = our solver addr`) for the +1. Run the bot → it submits an **exclusive** standing quote (`exclusiveFor = executor`) for the RWA→underlying route to `order-dev.li.fi`. -2. Create a matching **test order** from a second (user) key — easiest via the `lintent.org` reference - UI in **"Escrow" mode**, or a small script that signs a `StandardOrder` + permit2 and submits it. +2. From a second user key, select the quote and call the canonical escrow `open`/`openFor` path. Do not call + `POST /orders/submit`: the order server detects the on-chain order and delivers the full `StandardOrder` + over WebSocket. For a manual run, use a `quoteTtl` long enough to complete quote selection and opening; + keep the short rolling TTL in automated or production flows. 3. The order server matches it to our exclusive quote and pushes it over the WS feed → the bot prices - it, builds `openForAndFinalise`, and settles it atomically on Sepolia. + it, calls `finaliseWithCurrentTimestamp`, and the executor finalises/redeems/fills it on Sepolia in + the same tx. 4. Inspect the tx (redeem → fill → attest), the user's received output, and the executor's accrued surplus. Iterate. @@ -347,15 +674,16 @@ This exercises the full real path — order server, WS, settlers, adapter, txman ### 8.3 Local contract loop (fast iteration only) For quick Solidity iteration without a network: foundry/anvil, self-deploy the OIF settlers + a -real/mock adapter, and drive `openForAndFinalise` — the shape of Catalyst's +real/mock adapter, open/fund an order, and drive the opened-order finalise path — the shape of Catalyst's `InputSettler7683LIFI.samechain.t.sol`. This is the `forge test` unit/integration coverage of the executor, **not** the integration loop (§8.2 is). The Go side is unit-tested against an `httptest` order-server mock + a simulated/forked chain backend. ### 8.4 Mainnet deployment - **We do not deploy the settlers** — LI.FI/OIF canonical deployments at fixed addresses. -- **Per chain we deploy** `LiquidLaneLifiExecutor` (+ register it as a filler on each target LiquidLane - adapter), register the solver EOA, fund gas, and run the bot — the same steps as §8.2 but against +- **Per chain we deploy** `LiquidLaneLifiExecutor` (+ register the executor as a filler on each target + LiquidLane adapter), register that executor with LI.FI through EIP-1271, fund its runtime caller, and run + the bot — the same steps as §8.2 but against `order.li.fi`. LI.FI is live on Ethereum, Base, Optimism, Arbitrum, Polygon, BSC, Katana, MegaETH, etc. (`order.li.fi/chains/supported` authoritative); v1 targets the chain(s) hosting the LiquidLane RWA adapters we serve. @@ -366,59 +694,83 @@ order-server mock + a simulated/forked chain backend. ## 9. Build phases -Testnet-first: the executor is on Sepolia from P0 so every later phase integrates against the live -`order-dev.li.fi` + real settlers + real adapter (§8.2). - -0. **Contract + Sepolia deploy** — vendor OIF interfaces into `../rfq/src/lifi/interfaces/`; write - `LiquidLaneLifiExecutor` + foundry unit/integration tests (self-deployed settlers + adapter, the - §8.3 local loop). Then **deploy to Ethereum Sepolia and register the executor as an adapter filler**. - CGO-free rfq build stays green; `forge fmt`/`forge test`/coverage pass. -1. **Order-server client** — the vendored `openapi/lifi-order.openapi.json` + generated `api/lifiorder` +Testnet-first: the executor is developed from P0 so every later phase integrates against the live +`order-dev.li.fi` + real settlers + real adapter (§8.2). The opened-order callback flow was proven through +a settled Sepolia order using the previous deployed executor; the latest `FillRoute` ABI still requires +the redeploy in phase 0. + +0. **Done locally; Sepolia redeploy required** — `LiquidLaneLifiExecutor` implements domain-separated ERC-1271 + registration through its caller set and caller-gated runtime authorization in §3. Its Foundry unit suite + and the real-settler Sepolia fork test pass. Deploy to Ethereum + Sepolia, register it with LI.FI through EIP-1271, and register it as an adapter filler. + The vendored ABI and Go binding are generated from the contract artifact at + [symbioticfi/rfq#18](https://github.com/symbioticfi/rfq/pull/18) head `53bf165`. +1. **Done locally** Order-server client — the vendored `openapi/lifi-order.openapi.json` + generated `api/lifiorder` client (register / `quotes/submit` / `orders`) plus a thin hand-written WS client for - `user:vm-order-submit`, wired to the live `order-dev.li.fi`; register the solver EOA; config parsing + `user:vm-order-submit`, wired to the live `order-dev.li.fi`; register the executor account; config parsing + framework wiring (`solver.Register`, blank-import). `httptest`-backed unit tests, validated live. - (The spec + generated client land in the plan PR; P1 wires them into the solver.) -2. **Pricing + decision + tx build** — `default` strategy (getAmountOut/getMaxAssets, margin, asset - match); `FillCall` encoding + `openForAndFinalise` calldata via generated bindings; txmanager submit. - Validated end-to-end on Sepolia by self-filling a `lintent.org` order matched to our exclusive quote. +2. **Done locally; previous ABI live Sepolia happy path proven** Pricing + decision + tx build — `default` strategy + (direct executable getMaxRate for quotes; getAmountOut/minDiscount/getMaxAssets for fills; + Chainlink gas conversion snapshots, code-owned settlement/private gas constants, pair-level route + ladders, one direct plus one private alternative per route, shared capacity, + asset match, immediate OutputSettlerSimple context resolution + for limit and exclusive-limit outputs, with Dutch contexts rejected at WebSocket admission); + executor-as-solver typed `FillRoute[]` direct-finalise calldata; + early/final `orderStatus == Deposited` checks; latest-state snapshots; raw live txmanager fee input; dynamic + ranges; quote reconciliation; immediate unbounded fill handoff, sequential nonce broadcast, + pending-capacity-aware one-shot planning, inclusion-time reservation release, and fresh state for every admitted order. + Unit-tested through the solver-level submit path and validated end-to-end on Sepolia with a + WebSocket-delivered on-chain order matched to our exclusive quote: open tx + `0xd3f619048a745fb896c2f6c8b4e3b42a65b104eb3035b2bb9c20cf9593623480`, fill tx + `0x338aef70060093f6341538cd633fd4b5cfecc2fe2a10d77458946bf0e84fe960`, backend status `Settled`. + The new executor asks direct adapters for the buffered `amountOut`, executes private routes from signed + terms, and delegates context resolution and output sufficiency to the OutputSettler. If the order is not + executable at decision time, it is dropped. 3. **Harden** — staleness/skip edge cases, revert handling, metrics on the shared observability server; a repeatable green E2E on Sepolia (`order-dev`). -4. **Mainnet** — deploy the executor per target chain, point config at `order.li.fi`, register the - solver, and run. +4. **Mainnet** — deploy the executor per target chain, point config at `order.li.fi`, register each + executor account through EIP-1271, and run. --- -## 10. Open items / prerequisites - -- **Callback flow: CONFIRMED supported; only the opt-in wiring remains for P1.** The inventory-free - path our design uses is a documented, first-class LI.FI same-chain flow — the **"Same Chain Intent - with callback"** diagram in [`architecture/overview`](https://docs.li.fi/lifi-intents/architecture/overview) - and `for-solvers/settlement` state that the solver **receives inputs before delivering outputs** via - `orderFinalised(uint256[2][] inputs, bytes call)` and must "fill and setAttestations for the intent - outputs within the callback." That is exactly our `openForAndFinalise(destination = executor)` design. +## 10. Open items + +- **Gas calibration: direct-finalise rerun required.** The previous signature-based executor's 51-test + suite measured a maximum `finaliseWithCurrentTimestamp` call of 478,838 gas. Re-run Foundry gas reports + after the direct-finalise contract cutover and compare the first Sepolia receipts before changing the + conservative Go settlement constants. + The first acquire-route budgets are 550k direct and 625k private from + `250k fixed + LiquidLane route units (+75k private)`. Multi-route callback behavior was also exercised. + Compare the first Sepolia receipts against these constants before mainnet rollout. +- **Executor redeploy** — deploy the [rfq#18](https://github.com/symbioticfi/rfq/pull/18) implementation plus + transparent proxy, initialize the runtime signer as a caller, register the proxy address with LI.FI, update + config, and grant the proxy adapter filler authorization before E2E. Confirm the canonical InputSettler + reports `governanceFee() == 0`; startup fails closed otherwise, and every admitted order rechecks it. +- **Opened-order callback flow: previously confirmed on Sepolia; contract-identity rerun required.** The + executor uses the same opened-order callback path, but `finaliseWithCurrentTimestamp` now calls + `InputSettler.finalise` as the registered solver contract, then receives/redeems inputs and + fills/attests output via `orderFinalised(uint256[2][] inputs, bytes call)` in the same transaction. It is **opt-in** ("your solver has to support `orderFinalised`"). **Opt-in mechanism = resolved:** we - register the **escrow `InputSettlerEscrowLIFI`** (plus the OutputSettler + oracle) via - `PUT /api/v1/solver/supported-contracts` — the vendored spec's `PutSupportedContractsDto` takes - `{ oracle[], inputSettler[], outputSettler[] }` keyed by CAIP-2 chain, i.e. the settler/oracle set the - solver supports. Our executor is **not** registered with the order server; it is only the `destination` - argument we pass to `openForAndFinalise` at settlement. **Residual (empirical, P1 spike on `order-dev`):** - confirm that supporting the escrow settler yields matched orders delivered **escrow-typed** - (`inputSettler` = escrow), **unopened** (`orderStatus: Signed`), carrying the user's permit2/3009 - `sponsorSignature`. Fallback if a given order arrives Compact/pre-opened: a small revolving inventory - buffer (fill from buffer, then replenish by redeeming the claimed input) — Plan-B only. -- **Wire schemas: RESOLVED** — the order-server OpenAPI is vendored (`openapi/lifi-order.openapi.json`) - and the Go client generated (`api/lifiorder`); the quote / order / register shapes are in §5.1. The - WebSocket `user:vm-order-submit` event is a socket event (not in the OpenAPI); its payload is captured - in §5.1 from the reference client — confirm the exact WS URL/handshake on `order-dev` in P1. -- **Upstream OpenAPI defects (report to LI.FI)** — the `order-dev` spec is mislabelled `3.0.0` but uses - 3.1 constructs, has 3 dangling `oneOf` `$ref`s (`Oif{3009,Escrow,UserOpenIntent}OrderDto` are - referenced but never defined), and 2 multi-tag operations (`/quote/request`, `/quotes/submit`) — so - the raw spec does not generate a compiling Go client. `make refresh-lifi-client` applies a documented - normalization shim (`hack/lifi-openapi-normalize.py`: passthrough the undefined order schema, - single-tag the ops) to generate `api/lifiorder`. Report the defects upstream and drop the shim once - fixed. The vendored spec stays raw (contract of record); the shim runs only at codegen time. + ensure the **escrow `InputSettlerEscrowLIFI`** plus the OutputSettler via + `GET /api/v1/solver/supported-contracts`, then conditional `PUT /api/v1/solver/supported-contracts` + when missing. The solver merges its configured escrow InputSettler and OutputSettler into the complete + current list before replacement, preserving other chains and registering the OutputSettler in both + `outputSettler[]` and `oracle[]`. The executor is the registered solver, finalise caller, and callback + destination. The previous EOA-identity build proved the remainder of the live path: `order-dev` + delivered an already-opened/funded escrow order over `user:vm-order-submit`, and the resulting Sepolia + fill reached backend status `Settled`. Repeat that E2E after deploying and registering the new executor. + The live feed may omit `orderType`; admission + therefore requires `meta.onChainOrderId` and relies on the full escrow order plus canonical on-chain + `Deposited` status. We do not support gasless + Compact or permit2/3009 opening. +- **Wire schemas: RESOLVED** — the live order-server OpenAPI is valid 3.1, vendored at + `openapi/lifi-order.openapi.json`, and generates `api/lifiorder` directly without a normalization shim. + The WebSocket `user:vm-order-submit` event is outside the OpenAPI; the confirmed dev connection uses + `wss://order-dev.li.fi`, `x-api-key`, and application-level `ping`/`pong`. Its opened-order payload and + optional `quoteId` behavior are captured in §5.1. - **Adapter filler registration** — our executor must be granted filler rights on each LiquidLane - adapter (`marketMaker`/`owner`/`isFiller`), by the adapter's vault creator. Onboarding prereq. + adapter (`setFiller(executor, true)` / equivalent owner path), by the adapter's vault creator. + Onboarding prereq. - **Sepolia testnet adapter — resolved (v1 dev route).** The redstone-oev testbed provides a usable Sepolia LiquidLane adapter: `0xB5951fecFc34f56a6Ffbd62A2c61cE328E9De70b` (vault `0xb99F1FeA50f40Bb7C5E568c2De6D79dd0b61EB3A`), redeeming **TCOL** `0x17e892d4E802B01d7DA49Ca3542560f6851AA4D3` @@ -432,3 +784,6 @@ Testnet-first: the executor is on Sepolia from P0 so every later phase integrate undocumented; `exclusiveFor` on our own quotes should make this moot for v1. - **`getMaxAssets` is non-view** (mutates) — read via a call, not a static-call, in the pricing path (mirror how the RFQ solver handles it). +- **Private-discount deployment config** — internal mode needs the reachable RFQ/private-discounts + backend URL and live signer/protocol policies for the configured adapters. The code path is complete; + Sepolia E2E still needs a real advertised discount and newly deployed executor ABI. diff --git a/docs/LIQUIDLANE-CONVENTIONS.md b/docs/LIQUIDLANE-CONVENTIONS.md new file mode 100644 index 00000000..c741fc5e --- /dev/null +++ b/docs/LIQUIDLANE-CONVENTIONS.md @@ -0,0 +1,167 @@ +# LiquidLane conventions + +LiquidLane is shared liquidity infrastructure, not a solver. This document is the compact standard for +RFQ, LI.FI, OEV, future UniswapX, and any new solver that consumes `LiquidLaneAdapter` state. + +## Ownership + +| Package | Owns | +|---|---| +| `internal/liquidlane` | adapter/vault/route types, latest-state reads, direct authorization, ids, and rate math | +| `internal/liquidlane/gas` | neutral acquire/allocate/deallocate/unknown route prediction from current adapter/vault facts | +| `internal/liquidlane/discounts` | signed-discount HTTP client, wire parsing, and validation | +| `internal/solvers/` | cadence, caches, strategy inputs, economics, protocol messages, calldata, and execution | + +The generic framework must not know about LiquidLane. Execution is never shared: RFQ, LI.FI, OEV, and +UniswapX use different contracts, signatures, status models, and callbacks. + +## Canonical model + +Every route is one-way: + +```text +tokenIn -> LiquidLaneAdapter -> tokenOut +``` + +`tokenIn` is a member of `tokensToRedeem`; `tokenOut` is `adapter.vault().asset()`. External APIs may +use `asset`, `collateral`, or other names, but adapters must map them to `tokenIn`/`tokenOut` before the +facts reach a strategy. + +| Type | Meaning | +|---|---| +| `Adapter` | stable adapter, vault, output token, and output decimals | +| `Route` | stable adapter + `tokenIn` + `tokenOut` direction and decimals | +| `Inventory` | latest executable capacity/rate for one route | +| `FillQuote` | latest executable output for one concrete `amountIn` | +| `Auth` | direct caller authorization facts | +| `gas.Snapshot` | adapter-local owner/market-maker acquire balances plus vault-level shared free/withdrawable liquidity | + +Core field rules: + +- `MaxAssets` is the current output cap in `tokenOut` units. +- Direct inventory `MaxRate` is `getMaxRate(tokenIn)` and already includes `minDiscount`. A `FillQuote` + derives the same conservative fixed-point fact from `MaxAmountOut / AmountIn`, so fill-time private + offers are bounded without another RPC call. +- Discount `MaxRate` comes from the discounts backend and already includes its advertised discount. +- `GrossAmountOut` is raw `getAmountOut`; `MaxAmountOut` is the executable amount after discount. +- `MinDiscount` is the adapter's current lower bound for a fill. +- `ValidUntil` is an external offer deadline. Inventory does not carry a duplicate read timestamp; + solvers pass current chain/server time separately with each strategy decision. +- Shared values are copied at constructors and treated as immutable after entering a cache or strategy. + +Stable ids are lowercase and content-derived: + +```text +route:::: +capacity::: +candidate: +candidate::discount: +``` + +`CapacityID`, not `RouteID`, is the accounting boundary. Routes backed by the same vault/output pool are +not independent liquidity. + +## Reads and freshness + +LiquidLane reads always target RPC `latest` through ordinary `chain.Multicall`. + +- Do not use historical block tags or require archive-capable RPCs. +- Batch related calls once per logical read. A single Multicall is internally coherent enough for current + quoting; separate protocol reads may naturally observe adjacent heads. +- Do not attach an exact block number to latest inventory. Use decision-time chain state, TTLs, and + protocol deadlines. +- A latest snapshot is an estimate until transaction inclusion. Strategies apply reserve, two-sided price + movement, minimum profit, gas, and deadline padding. Execution contracts revalidate current rate and + capacity; where the protocol permits, they clamp to a signed economic floor before reverting atomically. +- Stable metadata (`vault`, asset, decimals, redeemable tokens) may be cached. Mutable inventory is + refreshed according to solver cadence or immediately before a fill. + +The shared reader exposes facts: + +```go +ResolveRoutes(ctx, adapters) ([]Route, error) +ReadInventory(ctx, routes) ([]Inventory, error) +ReadFillQuotes(ctx, routes, tokenIn, amountIn) ([]FillQuote, error) +ReadGasSnapshot(ctx, routes) (*gas.Snapshot, error) +ReadAdapterSnapshot(ctx, adapter, filler) (AdapterSnapshot, error) +ReadAuth(ctx, adapters, filler) ([]Auth, error) +FilterAuthorized(ctx, inventory, filler) ([]Inventory, error) +FilterAuthorizedRoutes(ctx, routes, filler) ([]Route, error) +``` + +Implementation rules: + +- Use generated `PackXxx`/`UnpackXxx` helpers and Multicall batches. +- Bound `tokensToRedeem`; reject invalid addresses, decimals, rates, caps, and discounts. +- Fail startup when a configured adapter's stable `vault`, output asset, output decimals, + `tokensToRedeem` list, or input-token decimals cannot be resolved. Silently running with a partial + configured adapter or route set is not allowed. +- Treat an unreadable `paused` or authorization result as unavailable. +- Treat an unreadable adapter-local `acquireBalance` as zero for gas prediction. This deliberately + selects an allocate/deallocate/unknown route with an equal or higher gas budget. +- Skip a bad route without hiding a batch transport error. +- Direct authorization is `filler == marketMaker || filler == owner || isFiller(marketMaker, filler)`. + +Block polling is allowed as a refresh trigger. Receipt confirmations and protocol epochs may also use block +numbers. The restriction is specifically against historical state calls and exact-block LiquidLane reads. + +## Strategy boundary + +The solver normally reads LiquidLane and passes immutable `Inventory` or `FillQuote` facts into the +strategy. The solver owns refresh cadence, cache replacement, reservations, and transaction submission. +Runtime values such as current block time, the txmanager fee cap, and `gas.Snapshot` are also facts. The +shared predictor owns only adapter swap route units. Conversion into token-denominated gas cost, +solver-specific settlement/private-payload units, margin, buffers, ranges, and route selection belong to +the strategy. A transaction-level strategy calculation charges settlement gas once, consumes acquire +balances per adapter, and consumes free/withdrawable liquidity once per shared vault. A strategy may reduce +all three budgets by its existing inventory reserve before route classification so a near-boundary plan is +priced as the next more expensive route. It must not add a standalone full-tx gas estimate for every route. + +If the set of reads is itself strategy-dependent, inject a narrow read-only capability such as `Pricing` +or `LiquidLaneState`. Do not inject a signer, tx manager, or unrestricted chain client into the strategy. +The capability must accept `context.Context`, batch calls, return typed facts, and be replaceable by a fake. + +Webhook strategies should receive the same facts in their request. A remote strategy may own its own RPC +only when that deployment deliberately accepts different freshness and availability from the local path. + +## Discounts and capacity + +Direct and signed-discount inventory for the same route are alternative ways to use the same capacity. +Never sum them. Pick one candidate per route, then reserve against the shared `CapacityID`. + +For signed discounts: + +1. List and validate advertised offers for quote construction. +2. Never apply `discount` to backend `maxRate` a second time. +3. Resolve signatures again immediately before fill. +4. Recheck id, adapter, tokens, current discount bounds, and deadlines. +5. Reserve capacity for upward price movement: discount swaps release their full computed output and + cannot be reduced to a requested amount. +6. Pass a discount candidate only when the solver's executor can settle `discountSwap` atomically. + +Discount discovery is shared; discount-to-route matching and execution calldata remain solver-local. + +## Solver profiles + +| Solver | LiquidLane usage | Solver-local responsibility | +|---|---|---| +| RFQ | amount-specific pricing and recovery inventory; narrow pricing capability may read during strategy evaluation | quote cache, RFQ order lifecycle, Reactor/Executor calldata | +| LI.FI | latest inventory on tick/block refresh; fresh `FillQuote` for each received order | range curves, gas/margin policy, OIF contexts, exclusivity, executor finalise, immediate fill | +| OEV | background latest inventory stored by Morpho market id | Morpho discovery, auction pricing, safety haircut, liquidation sizing, bundle/gas/deposit accounting | +| UniswapX | latest inventory either on request or from a short-lived background cache | Reactor orders, Permit2/cosigner rules, auction curve, order status and fill calldata | + +3F does not use LiquidLane and should not be forced through these types. Shared code is justified by the +protocol dependency, not by making every solver look identical. + +## Adding another solver + +1. Resolve configured adapters into shared routes. +2. Choose latest-state refresh cadence and a stale-data policy. +3. Map shared facts into a small solver-specific strategy input. +4. Account capacity by `CapacityID` and in-flight execution. +5. Keep gas, margin, price movement, auctions, and exclusivity in the strategy. +6. Re-read amount-specific state and protocol status before sending funds-moving calldata. +7. Keep execution, signatures, wire DTOs, and failure state machines in the solver package. + +The shared package provides current LiquidLane facts. Solvers decide when those facts are sufficient and +contracts enforce the final executable truth. diff --git a/docs/OEV-PLAN.md b/docs/OEV-PLAN.md index 605a08f8..1fca363a 100644 --- a/docs/OEV-PLAN.md +++ b/docs/OEV-PLAN.md @@ -167,7 +167,7 @@ adapter is OEV-local because it parses directly into the OEV monitor snapshot. | `strategies/default/monitor.go` | Morpho API snapshot, atomic hot-path state, and adapter-scoped market filtering | | `strategies/default/morphoapi.go` | OEV-local adapter over generated Morpho GraphQL operations: `markets` returns adapter-scoped market state (§3.4), `marketPositions` returns at-risk position state capped at `maxTrackedPositions` (§3.2) | | `api/morphographql` | generated Morpho GraphQL binding from vendored schema + explicit operation documents | -| `chainreader.go` | solver-owned on-chain reads: Executor accounting and adapter snapshot | +| `chainreader.go` | solver-owned Executor accounting plus mapping from the shared `liquidlane.Reader` snapshot into OEV strategy types | | `reservations.go` | in-flight auction reservation + pending-auction snapshot + auction-id de-dup (`seenAuctions`) | | `wsclient.go` | resilient WS client: reconnect/backoff/jitter, ~7 h rotation, heartbeat, subscribe replay | | `wsmessages.go` | wire types pinned to RedStone's zod + a captured auction frame, including auction identity/key hashing | diff --git a/docs/RFQ-PLAN.md b/docs/RFQ-PLAN.md index 6577a5de..d2df43fa 100644 --- a/docs/RFQ-PLAN.md +++ b/docs/RFQ-PLAN.md @@ -25,12 +25,14 @@ push path; orders are found exclusively by polling the backend. leg's **adapter** address. - **State** — in-memory only: `strategies` (by `quoteId`), `orders` (state machine), `attempts`. -The `/quote` request inventory (`adapters[]`) and the strategy use **adapter/asset** terminology -(`adapter`, `asset`, `assetDecimals`, `maxAssets`, `maxRate`, `discountId`) — a 1:1 match for the TS -`solverQuoteRequestSchema`. Pricing leg types: **direct** (`discountId == null`, public adapter rate) -and **discount** (`discountId != null`, a signature-gated private rate negotiated off-chain via the -backend `/discounts` flow). Both are in scope for full parity — discount legs are built in **P3** (§4), -after the direct path is solid; they are sequenced last, not dropped. +The `/quote` request inventory (`adapters[]`) still matches the TS `solverQuoteRequestSchema`, but the +solver maps that boundary shape into the shared LiquidLane terms from +[`LIQUIDLANE-CONVENTIONS.md`](LIQUIDLANE-CONVENTIONS.md): `Inventory` is +`adapter + tokenIn + tokenOut + maxAssets + maxRate`, and RFQ's external `asset` field is the shared +`tokenOut`. Pricing leg types are **direct** (`discountId == null`, public adapter rate) and +**discount** (`discountId != null`, a signature-gated private rate negotiated off-chain via the backend +`/discounts` flow). Both are in scope for full parity — discount legs are built in **P3** (§4), after +the direct path is solid; they are sequenced last, not dropped. --- @@ -86,7 +88,7 @@ A new self-contained `internal/solvers/rfq/` implementing `solver.Solver` — no | `quote.ts` + `strategy.ts` | `quote.go` + `strategy.go` (quote-server wiring) + `strategies/` (the pluggable decision layer: `default` = pricing/discount/leg selection, `webhook` = external decider) | | `execution.ts` | `execution.go` (poll loop, order state machine, fill; fill-plan production/recovery lives in the strategy) | | `executor.ts` + `reactor`/`contracts.ts` | `order.go` (encode/decode reactor order, `fill` calldata) | -| `backend.ts` + `discounts.ts` | `backend.go` (thin adapter over the generated `api/rfqbackend` client: `/orders`, `/discounts`) | +| `backend.ts` + `discounts.ts` | `backend.go` (thin adapter over the generated `api/rfqbackend` client for `/orders`) + shared `internal/liquidlane/discounts` (`/discounts`) | | `contracts.ts` + `inventories.ts` | `chainreader.go` (multicall adapter/vault reads) + shared `chain` | | `domain.ts` | `store.go` types + `strategies/types` (strategy input/output, fill plan, legs, candidates) | | `config/env.ts` + deployment manifests | `config.go` (typed `solver.config`) | @@ -122,10 +124,16 @@ not exactly one, so webhook and post-restart recovery fail closed at the same bo The generic strategy pattern and trust model (solver provides raw facts; the trusted strategy is the brain; the solver only enforces its own structural and safety constraints) are documented once in -[`strategy-plan.md`](strategy-plan.md), shared with every solver. The concrete RFQ input/output types -(`QuoteInput`/`QuoteOutput`, `FillInput`/`FillPlan`, `QuoteCandidate`) live in +[`strategy-plan.md`](strategy-plan.md), shared with every solver. Shared LiquidLane fact conventions +are documented in [`LIQUIDLANE-CONVENTIONS.md`](LIQUIDLANE-CONVENTIONS.md). The concrete RFQ +input/output types (`QuoteInput`/`QuoteOutput`, `FillInput`/`FillPlan`, `QuoteCandidate`) live in `internal/solvers/rfq/strategies/types`. +Signed-discount HTTP transport and validation are shared through `internal/liquidlane/discounts`. +Advertised offers are parsed once into typed addresses, ids, amounts, decimals, and deadlines before +they become `liquidlane.Inventory`; expired or malformed offers fail closed. RFQ keeps only its +executor-specific `discountSwap` calldata mapping. + --- ## 3. Configuration (env-agnostic: local / hoodi / mainnet) @@ -281,9 +289,10 @@ unbounded). A few **intentional, non-fund-moving divergences** remain, by design each deployment's `backendUrl` accordingly (mismatch ⇒ 404 on every backend call). - **Internal discounts path** — the discounts API is internal-only and served under `/api-internal/v1` (orders stay on `/api/v1`). Rather than regenerate the client for a routing detail, - `internalDiscountTransport` (`backend.go`) rewrites the generated `/api/v1/discount(s)` requests to - `/api-internal/v1/...` at the transport layer; orders pass through unchanged. Covered by the - `backend_test.go` httptest assertions. + the shared discounts client rewrites generated `/api/v1/discount(s)` requests to + `/api-internal/v1/...` at its transport boundary; orders pass through unchanged. RFQ uses it through + `internal/liquidlane/discounts`; LIFI reuses the same client and validation for its discount-backed + fills. Covered by httptest assertions. - The `{adapter, tokenToRedeem}` discount-resolve selector exists in TS types but is unused by execution (both sides resolve by `discountId`); Go omits it. Cosmetic. diff --git a/docs/UNISWAPX-PLAN.md b/docs/UNISWAPX-PLAN.md index 65c4e128..f30758ca 100644 --- a/docs/UNISWAPX-PLAN.md +++ b/docs/UNISWAPX-PLAN.md @@ -41,7 +41,7 @@ almost wholesale, plus a UniswapX protocol adapter. | Order version | **V2 first (mainnet); codec abstracted for V3** | "Goal is mainnet" (V2). Tempo + most L2s are V3 — slots in behind the same interface later. | | Pricing v1 | **Redemption rate − fixed haircut**, gas-aware floor | Ship fast, tune later (matches how `3f`/`rfq` shipped). | | On-chain executor | **UniswapX-specific** `UniswapXExecutor.sol` | Smallest, auditable surface; no multi-venue abstraction yet. | -| Code organization | **Sibling solver reusing rfq's `default` + `webhook` strategies** — the strategy layer (contract, registry, strategies) is promoted to a shared package on second use, alongside `internal/liquidlanemath` + `internal/webhook` | The strategies are protocol-neutral (verified, §2.1): selection/validation/caching/recovery all transfer; only candidate construction and pricing policy are solver-side. | +| Code organization | **Sibling solver reusing rfq's `default` + `webhook` strategies** — the strategy layer (contract, registry, strategies) is promoted to a shared package on second use, alongside `internal/liquidlane` + `internal/webhook` | The strategies are protocol-neutral (verified, §2.1): selection/validation/caching/recovery all transfer; only candidate construction and pricing policy are solver-side. | **Directionality (structural constraint):** `LiquidLaneAdapter`s are one-way — they consume a token-to-redeem and pay out the vault asset. So the only fillable orders are **RWA-in → vault-asset-out**: @@ -59,14 +59,15 @@ economics, and quoting any pair our vaults can't settle. A new self-contained `internal/solvers/uniswapx/` implementing `solver.Solver` — **no framework edits** (CLAUDE.md modularity rule). Code organization follows the repo's **solver-local strategy architecture** -(see `docs/strategy-plan.md`). §2.5 is the consolidated reuse-vs-delta implementation checklist. +(see `docs/strategy-plan.md`) and the shared LiquidLane read/type conventions +(`docs/LIQUIDLANE-CONVENTIONS.md`). §2.5 is the consolidated reuse-vs-delta implementation checklist. ### 2.1 Shared strategy logic — reusing the rfq strategy layer > Supersedes this plan's earlier `internal/symbiotic/` shared-tier proposal, in favor of the repo's > **solver-local strategy architecture** (`docs/strategy-plan.md`): the framework never parses/routes > strategy configs; each solver defines its own **strategy contract and registry**; and the genuinely -> cross-solver pieces live in **`internal/liquidlanemath/`** (the LiquidLane fixed-point rate math: +> cross-solver pieces live in **`internal/liquidlane/`** (LiquidLane facts, reads, auth, ids, and fixed-point rate math: > `AmountOutForRate`, `MaxAmountInForRate`, `MinAmountInForAmountOut`, `RateForAmountOut`, `RATE_SCALE` > 1e18) and **`internal/webhook/`** (a neutral HTTP-decider transport client — timeouts, body caps, > env-backed headers, strict decode). @@ -74,7 +75,7 @@ A new self-contained `internal/solvers/uniswapx/` implementing `solver.Solver` **Verified against the rfq strategy implementation (2026-07-06): the `default` and `webhook` strategies are protocol-neutral and reusable by `uniswapx` as-is.** Everything the `default` strategy does — candidate matching by `tokenOut`, oracle pricing through the `Pricing` seam, greedy rate-sorted leg -selection on `liquidlanemath`, the validation/replay rules (legs reference input candidates, no +selection on `liquidlane`, the validation/replay rules (legs reference input candidates, no duplicates, ≤ `maxAssets`, achievable under `maxRate`, sums reconcile), the TTL fill-plan cache keyed by `quoteId`, and `BuildFillPlan` recovery with the `RequiredAmountOut` gate — operates purely on neutral types (addresses, big.Ints, candidates). The same holds for the `webhook` strategy and its JSON wire @@ -91,7 +92,7 @@ the contract, registry, and the two strategy implementations. ``` internal/ {config,chain,signer,txmanager,solver,observability}/ # framework — unchanged, protocol-agnostic - liquidlanemath/ # SHARED: rate math, reused verbatim + liquidlane/ # SHARED: LiquidLane facts, reads, auth, ids, and rate math webhook/ # SHARED: remote-strategy transport, reused verbatim llstrategy/ # PROMOTED from internal/solvers/rfq/ when uniswapx lands (naming TBD) types/ # Strategy{DecideQuote, BuildFillPlan}, Pricing seam, QuoteInput/FillPlan + wire JSON @@ -102,7 +103,7 @@ internal/ uniswapx/ # consumes the promoted layer; chain candidate construction + UniswapX plumbing here ``` -- **Reused as-is:** `internal/liquidlanemath/`, `internal/webhook/`, and the promoted strategy layer — +- **Reused as-is:** `internal/liquidlane/`, `internal/webhook/`, and the promoted strategy layer — contract, registry, `default` + `webhook` strategies with their selection/validation/caching/recovery logic unchanged. The discount branch is simply never taken (uniswapx candidates carry no `DiscountID`). - **Two things that need no strategy changes at all** (they compose from outside): @@ -214,7 +215,7 @@ code 2026-07-06 — see §2.1 for the extraction evidence.) **Reused as-is:** -- `internal/liquidlanemath/` — the LiquidLane fixed-point rate math (`AmountOutForRate`, +- `internal/liquidlane/` — LiquidLane facts plus fixed-point rate math (`AmountOutForRate`, `MaxAmountInForRate`, `MinAmountInForAmountOut`, `RateForAmountOut`, `RATE_SCALE` 1e18), verbatim. - `internal/webhook/` — the neutral remote-decider transport client, verbatim (backs the optional `webhook` strategy). @@ -457,7 +458,7 @@ On the ≤500ms path, mirroring `rfq`'s "one multicall, decimals cached" discipl reads + **one Multicall3 `getAmountOut`** across candidate adapters for the matching asset (served to the strategy through its `Pricing` seam). 3. `Strategy.DecideQuote` (the shared `default` strategy): greedy direct-leg selection on - `internal/liquidlanemath`. Then the **solver-side policy** (§2.1): `quote = planOutput − haircutBps`, + `internal/liquidlane`. Then the **solver-side policy** (§2.1): `quote = planOutput − haircutBps`, then a **gas-aware min-profit floor** (est. fill gas × gas price, netted at the configured loan/ETH rate). Below floor ⇒ **decline**. 4. Persist the strategy by `quoteId` (TTL-swept), return `200` with `amountOut` (or `amountIn` for @@ -466,7 +467,7 @@ On the ≤500ms path, mirroring `rfq`'s "one multicall, decimals cached" discipl `EXACT_OUTPUT`: the shared `QuoteInput` contract is exact-input-only, so **v1 declines `EXACT_OUTPUT` by default** (§2.1). If flow data says it matters, the additive path is an optional trade-type/amount-out field on the shared contract plus an output-driven loop in `default` walking the same rate-sorted legs on -`liquidlanemath.MinAmountInForAmountOut` — `rfq` is unaffected either way (it only ever sends +`liquidlane.MinAmountInForAmountOut` — `rfq` is unaffected either way (it only ever sends exact-input). Pricing is intentionally naive for v1; a competitive/win-rate controller (modeling `exclusivityOverrideBps`, time-in-auction, competing fillers) is a later follow-up — the pricing policy function is the seam to extend. @@ -693,7 +694,7 @@ Tracked operational and onboarding steps — **update as items start/finish/drop ### Internal (this monorepo) - Sibling solver template — `vault-solver/internal/solvers/rfq/` + [`RFQ-PLAN.md`](RFQ-PLAN.md) - Strategy architecture (solver-local `strategies/` registry (package `strategies`, `registry.go`) + `strategies/types` + `strategies/{default,webhook}`, shared - `internal/liquidlanemath` + `internal/webhook`) — [`strategy-plan.md`](strategy-plan.md) + `internal/liquidlane` + `internal/webhook`) — [`strategy-plan.md`](strategy-plan.md) - Framework conventions — [`../CLAUDE.md`](../CLAUDE.md) - On-chain adapters/executor live in the sibling `rfq` repo (consumed via `api/bindings/`) diff --git a/docs/strategy-plan.md b/docs/strategy-plan.md index ec96a60c..f1581e81 100644 --- a/docs/strategy-plan.md +++ b/docs/strategy-plan.md @@ -60,6 +60,11 @@ exposes a quote decision and a fill decision; a bidding solver a single bid deci types are documented in each solver's plan (`docs/3F-PLAN.md`, `docs/RFQ-PLAN.md`, …) and defined in its `strategies/types` package — this document intentionally does not restate them. +Solvers that use LiquidLane liquidity also follow +[`LIQUIDLANE-CONVENTIONS.md`](LIQUIDLANE-CONVENTIONS.md): shared LiquidLane packages define +read-side facts (`Route`, `Inventory`, `FillQuote`, authorization, ids, freshness), while each solver +keeps its own strategy interface and execution plan. + ## Selection and configuration Strategy selection is solver-local: the generic framework does not parse, validate, or route strategy @@ -104,9 +109,9 @@ Two strategy kinds are conventional across solvers: Both plug into the same trusted boundary: the solver executes their output the same way, so a solver is never coupled to which strategy is loaded. A solver may still enforce solver-owned structural or -safety constraints before publishing or executing a plan. For example, RFQ marks inputs as -single-route when `tokensToQuote` is `permissioned` and rejects any strategy output that does not -contain exactly one leg; route selection and economics remain strategy-owned. +safety constraints before publishing or executing a plan. RFQ and LI.FI share `internal/tokenpolicy` +for `tokensToQuote` admission. Both mark admitted inputs as single-route only in `permissioned` scope +and reject strategy output that aggregates routes; route selection and economics remain strategy-owned. ## Adding your own strategy diff --git a/hack/lifi-openapi-normalize.py b/hack/lifi-openapi-normalize.py deleted file mode 100644 index 50e09018..00000000 --- a/hack/lifi-openapi-normalize.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -# Normalize the LI.FI order-server OpenAPI spec so the Java openapi-generator emits compiling Go. -# -# The vendored openapi/lifi-order.openapi.json is the RAW contract of record (pulled verbatim from the -# order server's Scalar /docs page) and MUST stay unedited. Two upstream defects in that raw spec make the -# generated Go uncompilable, so `make refresh-lifi-client` pipes the raw spec through this shim first. This -# reads the raw spec on stdin and writes the normalized spec to stdout, applying ONLY these two -# deterministic fixes: -# -# (a) Dangling oneOf $refs. QuoteDto.order is `oneOf: [Oif3009OrderDto, OifEscrowOrderDto, -# OifUserOpenIntentOrderDto]`, but none of those three schemas are defined in components.schemas -# (upstream forgot to register the NestJS DTOs — likely a missing @ApiExtraModels). The generator -# then emits a oneOf wrapper referencing three undefined Go types. Any property/schema whose value is -# a `$ref` (or a oneOf/anyOf/allOf of $refs) pointing at a missing schema is replaced with a -# permissive `{"type": "object", "additionalProperties": true}` passthrough — the solver flow does -# not need the nested order type inside the quote object. -# -# (b) Multi-tag operations. `/quote/request` is tagged ["Quotes","Bridge API"] and `/quotes/submit` -# ["Quotes","Solver API"]. openapi-generator emits each operation's request struct into EVERY tag's -# api_.go file, so a multi-tagged operation yields duplicate package-level types. Each -# operation's `tags` is collapsed to a single entry: prefer a tag ending in "API" (the concrete -# surface, e.g. "Bridge API"), else the first tag. -import json -import sys - - -def _is_ref_to_missing(node: dict, defined: set) -> bool: - ref = node.get("$ref") - return isinstance(ref, str) and ref.startswith("#/components/schemas/") and ref.rsplit("/", 1)[-1] not in defined - - -def _has_dangling_ref(node, defined: set) -> bool: - # A schema node is "dangling" if it is (or is composed via oneOf/anyOf/allOf of) a $ref whose target - # is not defined in components.schemas. - if not isinstance(node, dict): - return False - if _is_ref_to_missing(node, defined): - return True - for kw in ("oneOf", "anyOf", "allOf"): - members = node.get(kw) - if isinstance(members, list) and any(_is_ref_to_missing(m, defined) for m in members if isinstance(m, dict)): - return True - return False - - -PASSTHROUGH = {"type": "object", "additionalProperties": True} - - -def _fix_dangling(node, defined: set): - # Recursively replace any schema node that points at a missing $ref with a permissive passthrough, - # preserving the node's own description/example if present. - if isinstance(node, list): - return [_fix_dangling(v, defined) for v in node] - if not isinstance(node, dict): - return node - if _has_dangling_ref(node, defined): - out = dict(PASSTHROUGH) - for keep in ("description", "example", "title"): - if keep in node: - out[keep] = node[keep] - return out - return {k: _fix_dangling(v, defined) for k, v in node.items()} - - -def _collapse_tags(spec: dict) -> None: - methods = {"get", "put", "post", "delete", "patch", "options", "head", "trace"} - for path_item in spec.get("paths", {}).values(): - if not isinstance(path_item, dict): - continue - for method, op in path_item.items(): - if method.lower() not in methods or not isinstance(op, dict): - continue - tags = op.get("tags") - if isinstance(tags, list) and len(tags) > 1: - api_tags = [t for t in tags if isinstance(t, str) and t.strip().endswith("API")] - op["tags"] = [api_tags[0] if api_tags else tags[0]] - - -def main() -> None: - spec = json.load(sys.stdin) - defined = set(spec.get("components", {}).get("schemas", {}).keys()) - if "components" in spec and "schemas" in spec["components"]: - spec["components"]["schemas"] = _fix_dangling(spec["components"]["schemas"], defined) - if "paths" in spec: - spec["paths"] = _fix_dangling(spec["paths"], defined) - _collapse_tags(spec) - json.dump(spec, sys.stdout, indent=2, ensure_ascii=False) - sys.stdout.write("\n") - - -if __name__ == "__main__": - main() diff --git a/internal/chain/fallback_test.go b/internal/chain/fallback_test.go index 40872930..015b8288 100644 --- a/internal/chain/fallback_test.go +++ b/internal/chain/fallback_test.go @@ -270,6 +270,46 @@ func TestDial_WriteRPCRoutesOnlyBroadcasts(t *testing.T) { } } +func TestMulticallUsesLatestBlockTag(t *testing.T) { + var callParams []json.RawMessage + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params []json.RawMessage `json:"params"` + } + body, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(body, &req) + result := `"0x7a69"` + if req.Method == "eth_call" { + callParams = req.Params + // ABI encoding of an empty aggregate3 Result[] return. + result = `"0x0000000000000000000000000000000000000000000000000000000000000020` + + `0000000000000000000000000000000000000000000000000000000000000000"` + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":` + string(req.ID) + `,"result":` + result + `}`)) + })) + defer server.Close() + + const multicall = "0xcA11bde05977b3631167028862bE2a173976CA11" + c, err := Dial(t.Context(), []string{server.URL}, "", multicall, logr.Discard()) + if err != nil { + t.Fatalf("Dial: %v", err) + } + defer c.Close() + if _, err = c.Multicall(t.Context(), nil); err != nil { + t.Fatalf("Multicall: %v", err) + } + if len(callParams) != 2 { + t.Fatalf("eth_call params = %s", callParams) + } + var blockTag string + if err = json.Unmarshal(callParams[1], &blockTag); err != nil || blockTag != "latest" { + t.Fatalf("eth_call block tag = %q, err=%v", blockTag, err) + } +} + // TestDial_NoWriteRPCReusesPrimary confirms that with no writeRpcUrl, broadcasts fall back to the // primary endpoint (unchanged behaviour). func TestDial_NoWriteRPCReusesPrimary(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index 998b2c9f..ebbba316 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,6 +7,7 @@ package config import ( "bytes" + "math" "os" "github.com/go-errors/errors" @@ -66,10 +67,14 @@ type SignerConfig struct { type TxManagerConfig struct { // Confirmations to wait for before treating a transaction as final. Confirmations uint64 `yaml:"confirmations"` - // MaxFeeGwei caps the EIP-1559 max fee per gas; 0 means "derive from base fee". + // MaxFeeGwei is the required absolute EIP-1559 max fee per gas. MaxFeeGwei float64 `yaml:"maxFeeGwei"` // TipGwei is the EIP-1559 priority fee; 0 means "use the node's suggestion". TipGwei float64 `yaml:"tipGwei"` + // ReplacementIntervalMs is how often a pending transaction is fee-bumped. + ReplacementIntervalMs int `yaml:"replacementIntervalMs"` + // PendingTimeoutMs switches a still-pending call to a same-nonce cancellation. + PendingTimeoutMs int `yaml:"pendingTimeoutMs"` } // SolverConfig names the solver implementation and carries its opaque, deferred config. @@ -82,6 +87,11 @@ type SolverConfig struct { // DefaultConfirmations is used when TxManager.Confirmations is unset. const DefaultConfirmations = 2 +const ( + DefaultReplacementIntervalMs = 30_000 + DefaultPendingTimeoutMs = 300_000 +) + // DefaultObservabilityAddr is used when Observability.Addr is unset. const DefaultObservabilityAddr = ":9090" @@ -121,6 +131,12 @@ func (c *Config) applyDefaults() { if c.TxManager.Confirmations == 0 { c.TxManager.Confirmations = DefaultConfirmations } + if c.TxManager.ReplacementIntervalMs == 0 { + c.TxManager.ReplacementIntervalMs = DefaultReplacementIntervalMs + } + if c.TxManager.PendingTimeoutMs == 0 { + c.TxManager.PendingTimeoutMs = DefaultPendingTimeoutMs + } if c.Observability.Addr == "" { c.Observability.Addr = DefaultObservabilityAddr } @@ -142,6 +158,22 @@ func (c *Config) Validate() error { if c.Chain.ChainID == 0 { return errors.New("chain.chainId is required") } + if c.TxManager.MaxFeeGwei <= 0 || + math.IsNaN(c.TxManager.MaxFeeGwei) || + math.IsInf(c.TxManager.MaxFeeGwei, 0) { + return errors.New("txManager.maxFeeGwei must be finite and positive") + } + if c.TxManager.TipGwei < 0 || + math.IsNaN(c.TxManager.TipGwei) || + math.IsInf(c.TxManager.TipGwei, 0) { + return errors.New("txManager.tipGwei must be finite and non-negative") + } + if c.TxManager.ReplacementIntervalMs <= 0 { + return errors.New("txManager.replacementIntervalMs must be positive") + } + if c.TxManager.PendingTimeoutMs < c.TxManager.ReplacementIntervalMs { + return errors.New("txManager.pendingTimeoutMs must be at least replacementIntervalMs") + } if err := c.Signer.validate(); err != nil { return err } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f49723fe..87b2a668 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -22,6 +22,8 @@ chain: chainId: 11155111 signer: keyEnv: SOLVER_PRIVATE_KEY +txManager: + maxFeeGwei: 100 solvers: - name: 3f-bridge-facilitator config: @@ -37,6 +39,10 @@ func TestLoad_ValidAppliesDefaults(t *testing.T) { if cfg.TxManager.Confirmations != DefaultConfirmations { t.Fatalf("expected default confirmations %d, got %d", DefaultConfirmations, cfg.TxManager.Confirmations) } + if cfg.TxManager.ReplacementIntervalMs != DefaultReplacementIntervalMs || + cfg.TxManager.PendingTimeoutMs != DefaultPendingTimeoutMs { + t.Fatalf("unexpected tx replacement defaults: %+v", cfg.TxManager) + } if cfg.Observability.Addr != DefaultObservabilityAddr { t.Fatalf("expected default addr %q, got %q", DefaultObservabilityAddr, cfg.Observability.Addr) } @@ -48,6 +54,8 @@ chain: chainId: 11155111 signer: keyEnv: SOLVER_PRIVATE_KEY +txManager: + maxFeeGwei: 100 solvers: - name: 3f-bridge-facilitator config: {apiBaseUrl: https://bf.dev.gcp.3f.xyz} @@ -73,6 +81,8 @@ chain: chainId: 11155111 signer: keyEnv: SOLVER_PRIVATE_KEY +txManager: + maxFeeGwei: 100 solvers: - name: rfq-filler config: {} @@ -110,6 +120,8 @@ chain: chainId: 11155111 signer: keyEnv: SOLVER_PRIVATE_KEY +txManager: + maxFeeGwei: 100 solvers: - name: x config: {} @@ -136,6 +148,8 @@ chain: chainId: 1 signer: keyEnv: SOLVER_PRIVATE_KEY +txManager: + maxFeeGwei: 100 solvers: - name: x config: {} @@ -160,6 +174,8 @@ chain: chainId: 1 signer: keyEnv: SOLVER_PRIVATE_KEY +txManager: + maxFeeGwei: 100 solvers: - name: x config: {} @@ -180,6 +196,7 @@ func TestLoad_ExpandsEnvInSolverConfigBlock(t *testing.T) { body := ` chain: {rpcUrl: http://x, chainId: 1} signer: {keyEnv: K} +txManager: {maxFeeGwei: 100} solvers: - name: x config: @@ -230,13 +247,43 @@ solvers: [{name: x}] "missing solver name": ` chain: {rpcUrl: http://x, chainId: 1} signer: {keyEnv: K} +txManager: {maxFeeGwei: 100} solvers: [{}] +`, + "missing max fee cap": ` +chain: {rpcUrl: http://x, chainId: 1} +signer: {keyEnv: K} +solvers: [{name: x}] +`, + "non-finite max fee cap": ` +chain: {rpcUrl: http://x, chainId: 1} +signer: {keyEnv: K} +txManager: {maxFeeGwei: .nan} +solvers: [{name: x}] +`, + "negative tip": ` +chain: {rpcUrl: http://x, chainId: 1} +signer: {keyEnv: K} +txManager: {maxFeeGwei: 100, tipGwei: -1} +solvers: [{name: x}] `, "unknown field": ` chain: {rpcUrl: http://x, chainId: 1} signer: {keyEnv: K} solvers: [{name: x}] bogus: true +`, + "negative replacement interval": ` +chain: {rpcUrl: http://x, chainId: 1} +signer: {keyEnv: K} +txManager: {maxFeeGwei: 100, replacementIntervalMs: -1} +solvers: [{name: x}] +`, + "timeout below replacement interval": ` +chain: {rpcUrl: http://x, chainId: 1} +signer: {keyEnv: K} +txManager: {maxFeeGwei: 100, replacementIntervalMs: 30000, pendingTimeoutMs: 10000} +solvers: [{name: x}] `, } for name, body := range cases { diff --git a/internal/liquidlane/discounts/client.go b/internal/liquidlane/discounts/client.go new file mode 100644 index 00000000..416a54d6 --- /dev/null +++ b/internal/liquidlane/discounts/client.go @@ -0,0 +1,201 @@ +// Package discounts wraps the LiquidLane signed-discounts API shared by solvers. +package discounts + +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/api/rfqbackend" +) + +const ( + defaultTimeout = 10 * time.Second + publicAPIPrefix = "/api/v1" + internalAPIPrefix = "/api-internal/v1" +) + +// Terms is the signed discount the LiquidLane adapter's discountSwap verifies. +// Amounts/nonce stay as wire strings until a solver maps them into its executor-specific calldata. +type Terms struct { + Adapter string + TokenToRedeem string + Discount string + Signer string + Protocol string + Nonce string + Deadline int64 +} + +// Resolved is the fresh signed discount returned at fill time. +type Resolved struct { + RequestID string + DiscountID string + Discount Terms + SignerSignature string + ProtocolDeadline int64 + ProtocolSignature string +} + +// ListItem is one currently advertised private discount. +type ListItem struct { + DiscountID string + Adapter string + TokenToRedeem string + Collateral string + CollateralDecimals int + Discount string + Signer string + Deadline int64 + MaxRate string + MaxAssets string +} + +// List is the GET /discounts response projected into solver-owned types. +type List struct { + RequestID string + Protocol string + Discounts []ListItem +} + +// Client is a small adapter over the generated rfqbackend client for the shared signed-discount +// endpoints. The generated client emits /api/v1/discount(s); rewriteTransport routes only those calls +// to the backend's /api-internal/v1 path. +type Client struct { + api *rfqbackend.APIClient +} + +func NewClient(baseURL string) *Client { + cfg := rfqbackend.NewConfiguration() + cfg.Servers = rfqbackend.ServerConfigurations{{URL: strings.TrimRight(baseURL, "/")}} + cfg.HTTPClient = &http.Client{ + Timeout: defaultTimeout, + Transport: rewriteTransport{base: http.DefaultTransport}, + } + return &Client{api: rfqbackend.NewAPIClient(cfg)} +} + +// rewriteTransport routes private-discount requests to the backend's internal API prefix. Other +// generated-client requests pass through unchanged. +type rewriteTransport struct { + base http.RoundTripper +} + +func (t rewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) { + base := t.base + if base == nil { + base = http.DefaultTransport + } + if index := strings.LastIndex(req.URL.Path, publicAPIPrefix+"/discount"); index >= 0 { + req = req.Clone(req.Context()) + req.URL.Path = req.URL.Path[:index] + + internalAPIPrefix + + strings.TrimPrefix(req.URL.Path[index:], publicAPIPrefix) + req.URL.RawPath = "" + } + return base.RoundTrip(req) +} + +// Resolve fetches a fresh signed discount for discountID. +// +// The backend response is an anyOf union of a single resolved discount and a batch. Solvers resolve one +// discountId at a time, so a batch is accepted only when it has exactly one entry. +func (c *Client) Resolve(ctx context.Context, discountID string) (*Resolved, error) { + body := rfqbackend.NewApiV1DiscountsPostRequest() + body.SetDiscountId(discountID) + resp, httpResp, err := c.api.RFQAPI.ApiV1DiscountsPost(ctx).ApiV1DiscountsPostRequest(*body).Execute() + closeResp(httpResp) + if err != nil { + return nil, errors.Errorf("private discounts: resolve: %w", err) + } + if resp == nil { + return nil, errors.New("private discounts: resolve: empty response") + } + if single := resp.ResolveDiscountResponseAnyOf; single != nil { + return resolvedFromSingle(single), nil + } + if batch := resp.ResolveDiscountResponseAnyOf1; batch != nil { + items := batch.GetDiscounts() + if len(items) != 1 { + return nil, errors.Errorf("private discounts: resolve: expected a single discount, got %d", len(items)) + } + return resolvedFromBatchItem(batch.GetRequestId(), &items[0]), nil + } + return nil, errors.New("private discounts: resolve: response matched neither discount shape") +} + +func resolvedFromSingle(s *rfqbackend.ResolveDiscountResponseAnyOf) *Resolved { + return &Resolved{ + RequestID: s.GetRequestId(), + DiscountID: s.GetDiscountId(), + Discount: termsFromModel(s.GetDiscount()), + SignerSignature: s.GetSignerSignature(), + ProtocolDeadline: int64(s.GetProtocolDeadline()), + ProtocolSignature: s.GetProtocolSignature(), + } +} + +func resolvedFromBatchItem(requestID string, it *rfqbackend.ResolveDiscountResponseAnyOf1DiscountsInner) *Resolved { + return &Resolved{ + RequestID: requestID, + DiscountID: it.GetDiscountId(), + Discount: termsFromModel(it.GetDiscount()), + SignerSignature: it.GetSignerSignature(), + ProtocolDeadline: int64(it.GetProtocolDeadline()), + ProtocolSignature: it.GetProtocolSignature(), + } +} + +func termsFromModel(d rfqbackend.PublishDiscountRequestDiscount) Terms { + return Terms{ + Adapter: d.GetAdapter(), + TokenToRedeem: d.GetTokenToRedeem(), + Discount: d.GetDiscount(), + Signer: d.GetSigner(), + Protocol: d.GetProtocol(), + Nonce: d.GetNonce(), + Deadline: int64(d.GetDeadline()), + } +} + +// ListDiscounts lists currently advertised private discounts. +func (c *Client) ListDiscounts(ctx context.Context) (*List, error) { + resp, httpResp, err := c.api.RFQAPI.ApiV1DiscountsGet(ctx).Execute() + closeResp(httpResp) + if err != nil { + return nil, errors.Errorf("private discounts: list: %w", err) + } + out := &List{} + if resp == nil { + return out, nil + } + out.RequestID = resp.GetRequestId() + out.Protocol = resp.GetProtocol() + gen := resp.GetDiscounts() + out.Discounts = make([]ListItem, 0, len(gen)) + for i := range gen { + d := &gen[i] + out.Discounts = append(out.Discounts, ListItem{ + DiscountID: d.GetDiscountId(), + Adapter: d.GetAdapter(), + TokenToRedeem: d.GetTokenToRedeem(), + Collateral: d.GetCollateral(), + CollateralDecimals: int(d.GetCollateralDecimals()), + Discount: d.GetDiscount(), + Signer: d.GetSigner(), + Deadline: int64(d.GetDeadline()), + MaxRate: d.GetMaxRate(), + MaxAssets: d.GetMaxAssets(), + }) + } + return out, nil +} + +func closeResp(resp *http.Response) { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } +} diff --git a/internal/liquidlane/discounts/client_test.go b/internal/liquidlane/discounts/client_test.go new file mode 100644 index 00000000..a609d9fd --- /dev/null +++ b/internal/liquidlane/discounts/client_test.go @@ -0,0 +1,138 @@ +package discounts + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestClientResolveSingle(t *testing.T) { + var gotPath, gotMethod, gotID string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath, gotMethod = r.URL.Path, r.Method + var body map[string]any + raw, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(raw, &body) + gotID, _ = body["discountId"].(string) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"requestId":"00000000-0000-0000-0000-000000000000",` + + `"discountId":"0x` + hash64 + `",` + + `"discount":{"adapter":"0x0000000000000000000000000000000000000abc",` + + `"tokenToRedeem":"0x0000000000000000000000000000000000000def",` + + `"discount":"123","signer":"0x0000000000000000000000000000000000000aaa",` + + `"protocol":"0x0000000000000000000000000000000000000bbb","nonce":"0x2","deadline":1900000000},` + + `"signerSignature":"0xdead","protocolDeadline":1900000001,"protocolSignature":"0xbeef"}`)) + })) + defer srv.Close() + + id := "0x" + hash64 + res, err := NewClient(srv.URL).Resolve(context.Background(), id) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if gotPath != "/api-internal/v1/discounts" || gotMethod != http.MethodPost || gotID != id { + t.Fatalf("request = path %q method %q id %q", gotPath, gotMethod, gotID) + } + if res.Discount.Adapter != "0x0000000000000000000000000000000000000abc" || + res.Discount.Discount != "123" || res.Discount.Nonce != "0x2" || + res.Discount.Deadline != 1900000000 { + t.Fatalf("discount terms = %+v", res.Discount) + } + if res.SignerSignature != "0xdead" || res.ProtocolSignature != "0xbeef" || res.ProtocolDeadline != 1900000001 { + t.Fatalf("resolved = %+v", res) + } +} + +func TestClientResolveBatchSingleEntryAccepted(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"requestId":"00000000-0000-0000-0000-000000000000",` + + `"discounts":[{"discountId":"0x` + hash64 + `",` + + `"discount":{"adapter":"0x0000000000000000000000000000000000000abc",` + + `"tokenToRedeem":"0x0000000000000000000000000000000000000def",` + + `"discount":"123","signer":"0x0000000000000000000000000000000000000aaa",` + + `"protocol":"0x0000000000000000000000000000000000000bbb","nonce":"0x2","deadline":1900000000},` + + `"signerSignature":"0xdead","protocolDeadline":1900000001,"protocolSignature":"0xbeef"}]}`)) + })) + defer srv.Close() + + res, err := NewClient(srv.URL).Resolve(context.Background(), "0x"+hash64) + if err != nil { + t.Fatalf("Resolve batch: %v", err) + } + if res.Discount.Adapter != "0x0000000000000000000000000000000000000abc" || res.SignerSignature != "0xdead" { + t.Fatalf("resolved from batch = %+v", res) + } +} + +func TestClientResolveBatchMultipleRejected(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + entry := `{"discountId":"0x` + hash64 + `",` + + `"discount":{"adapter":"0x0000000000000000000000000000000000000abc",` + + `"tokenToRedeem":"0x0000000000000000000000000000000000000def",` + + `"discount":"1","signer":"0x0000000000000000000000000000000000000aaa",` + + `"protocol":"0x0000000000000000000000000000000000000bbb","nonce":"0x2","deadline":1900000000},` + + `"signerSignature":"0xdead","protocolDeadline":1900000001,"protocolSignature":"0xbeef"}` + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"requestId":"00000000-0000-0000-0000-000000000000","discounts":[` + + entry + `,` + entry + `]}`)) + })) + defer srv.Close() + + if _, err := NewClient(srv.URL).Resolve(context.Background(), "0x"+hash64); err == nil { + t.Fatalf("expected an error when the backend resolves more than one discount") + } +} + +func TestClientListDiscounts(t *testing.T) { + var gotPath string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"requestId":"00000000-0000-0000-0000-000000000000",` + + `"protocol":"0x0000000000000000000000000000000000000bbb","discounts":[` + + `{"discountId":"0x` + hash64 + `","adapter":"0x0000000000000000000000000000000000000abc",` + + `"tokenToRedeem":"0x0000000000000000000000000000000000000def",` + + `"collateral":"0x0000000000000000000000000000000000000c01","collateralDecimals":6,` + + `"discount":"10","signer":"0x0000000000000000000000000000000000000aaa","deadline":1900000000,` + + `"maxRate":"1000000","maxAssets":"5000"}]}`)) + })) + defer srv.Close() + + resp, err := NewClient(srv.URL).ListDiscounts(context.Background()) + if err != nil { + t.Fatalf("ListDiscounts: %v", err) + } + if gotPath != "/api-internal/v1/discounts" { + t.Fatalf("path = %q", gotPath) + } + if len(resp.Discounts) != 1 || resp.Discounts[0].CollateralDecimals != 6 || + resp.Discounts[0].MaxAssets != "5000" || resp.Discounts[0].Deadline != 1900000000 { + t.Fatalf("discounts = %+v", resp.Discounts) + } +} + +func TestClientPreservesBaseURLPathPrefix(t *testing.T) { + var gotPath string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte( + `{"requestId":"00000000-0000-0000-0000-000000000000",` + + `"protocol":"0x0000000000000000000000000000000000000001","discounts":[]}`, + )) + })) + defer srv.Close() + + if _, err := NewClient(srv.URL + "/backend").ListDiscounts(t.Context()); err != nil { + t.Fatalf("ListDiscounts: %v", err) + } + if gotPath != "/backend/api-internal/v1/discounts" { + t.Fatalf("path = %q, want prefixed internal path", gotPath) + } +} + +const hash64 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" diff --git a/internal/liquidlane/discounts/types.go b/internal/liquidlane/discounts/types.go new file mode 100644 index 00000000..4f2eadf4 --- /dev/null +++ b/internal/liquidlane/discounts/types.go @@ -0,0 +1,199 @@ +package discounts + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" +) + +const maxUint48 = int64(1<<48 - 1) + +// Offer is one validated advertised discount. It is safe to pass into solver candidate construction. +type Offer struct { + DiscountID common.Hash + Adapter common.Address + TokenToRedeem common.Address + Collateral common.Address + CollateralDecimals int + Discount *big.Int + Deadline int64 + // MaxRate is already net of Discount. The backend derives it from the + // adapter oracle output and the advertised discount terms. + MaxRate *big.Int + MaxAssets *big.Int +} + +// Signed is one validated fill-time discount with both signatures decoded. +type Signed struct { + DiscountID common.Hash + Adapter common.Address + Terms SignedTerms + + SignerSignature []byte + ProtocolDeadline *big.Int + ProtocolSignature []byte +} + +type SignedTerms struct { + TokenToRedeem common.Address + Discount *big.Int + Signer common.Address + Protocol common.Address + Nonce *big.Int + Deadline *big.Int +} + +func ParseOffer(item ListItem) (*Offer, error) { + id, err := parseHash(item.DiscountID, "discountId") + if err != nil { + return nil, err + } + adapter, err := parseAddress(item.Adapter, "adapter") + if err != nil { + return nil, err + } + tokenToRedeem, err := parseAddress(item.TokenToRedeem, "tokenToRedeem") + if err != nil { + return nil, err + } + collateral, err := parseAddress(item.Collateral, "collateral") + if err != nil { + return nil, err + } + discount, err := parseNonNegativeDecimal(item.Discount, "discount") + if err != nil { + return nil, err + } + if discount.Cmp(big.NewInt(liquidlane.DiscountPrecision)) > 0 { + return nil, errors.Errorf("discount: must be <= %d", liquidlane.DiscountPrecision) + } + maxRate, err := parsePositiveDecimal(item.MaxRate, "maxRate") + if err != nil { + return nil, err + } + maxAssets, err := parsePositiveDecimal(item.MaxAssets, "maxAssets") + if err != nil { + return nil, err + } + if item.CollateralDecimals < 0 || item.CollateralDecimals > 255 { + return nil, errors.Errorf("collateralDecimals: must be in [0,255], got %d", item.CollateralDecimals) + } + if item.Deadline <= 0 { + return nil, errors.New("deadline: must be positive") + } + return &Offer{ + DiscountID: id, Adapter: adapter, TokenToRedeem: tokenToRedeem, + Collateral: collateral, CollateralDecimals: item.CollateralDecimals, + Discount: discount, Deadline: item.Deadline, + MaxRate: maxRate, MaxAssets: maxAssets, + }, nil +} + +func ParseSigned(resolved *Resolved) (*Signed, error) { + if resolved == nil { + return nil, errors.New("resolved discount is nil") + } + id, err := parseHash(resolved.DiscountID, "discountId") + if err != nil { + return nil, err + } + adapter, err := parseAddress(resolved.Discount.Adapter, "adapter") + if err != nil { + return nil, err + } + tokenToRedeem, err := parseAddress(resolved.Discount.TokenToRedeem, "tokenToRedeem") + if err != nil { + return nil, err + } + discount, err := parseNonNegativeDecimal(resolved.Discount.Discount, "discount") + if err != nil { + return nil, err + } + if discount.Cmp(big.NewInt(liquidlane.DiscountPrecision)) > 0 { + return nil, errors.Errorf("discount: must be <= %d", liquidlane.DiscountPrecision) + } + signer, err := parseAddress(resolved.Discount.Signer, "signer") + if err != nil { + return nil, err + } + protocol, err := parseAddress(resolved.Discount.Protocol, "protocol") + if err != nil { + return nil, err + } + nonce, err := hexutil.DecodeBig(resolved.Discount.Nonce) + if err != nil { + return nil, errors.Errorf("nonce: %w", err) + } + if nonce.Sign() < 0 { + return nil, errors.New("nonce: must be non-negative") + } + signerSignature, err := hexutil.Decode(resolved.SignerSignature) + if err != nil { + return nil, errors.Errorf("signerSignature: %w", err) + } + protocolSignature, err := hexutil.Decode(resolved.ProtocolSignature) + if err != nil { + return nil, errors.Errorf("protocolSignature: %w", err) + } + if len(signerSignature) == 0 || len(protocolSignature) == 0 { + return nil, errors.New("discount signatures must not be empty") + } + if resolved.Discount.Deadline <= 0 || resolved.ProtocolDeadline <= 0 { + return nil, errors.New("discount deadlines must be positive") + } + if resolved.Discount.Deadline > maxUint48 || resolved.ProtocolDeadline > maxUint48 { + return nil, errors.New("discount deadlines exceed uint48") + } + return &Signed{ + DiscountID: id, Adapter: adapter, + Terms: SignedTerms{ + TokenToRedeem: tokenToRedeem, Discount: discount, Signer: signer, Protocol: protocol, + Nonce: nonce, Deadline: big.NewInt(resolved.Discount.Deadline), + }, + SignerSignature: signerSignature, ProtocolDeadline: big.NewInt(resolved.ProtocolDeadline), + ProtocolSignature: protocolSignature, + }, nil +} + +func parseAddress(raw, field string) (common.Address, error) { + if !common.IsHexAddress(raw) { + return common.Address{}, errors.Errorf("%s: invalid address %q", field, raw) + } + address := common.HexToAddress(raw) + if address == (common.Address{}) { + return common.Address{}, errors.Errorf("%s: zero address", field) + } + return address, nil +} + +func parseHash(raw, field string) (common.Hash, error) { + decoded, err := hexutil.Decode(raw) + if err != nil || len(decoded) != common.HashLength { + return common.Hash{}, errors.Errorf("%s: invalid bytes32 %q", field, raw) + } + hash := common.BytesToHash(decoded) + if hash == (common.Hash{}) { + return common.Hash{}, errors.Errorf("%s: zero bytes32", field) + } + return hash, nil +} + +func parsePositiveDecimal(raw, field string) (*big.Int, error) { + out, ok := new(big.Int).SetString(raw, 10) + if !ok || out.Sign() <= 0 { + return nil, errors.Errorf("%s: invalid positive decimal %q", field, raw) + } + return out, nil +} + +func parseNonNegativeDecimal(raw, field string) (*big.Int, error) { + out, ok := new(big.Int).SetString(raw, 10) + if !ok || out.Sign() < 0 { + return nil, errors.Errorf("%s: invalid non-negative decimal %q", field, raw) + } + return out, nil +} diff --git a/internal/liquidlane/discounts/types_test.go b/internal/liquidlane/discounts/types_test.go new file mode 100644 index 00000000..21fc7d82 --- /dev/null +++ b/internal/liquidlane/discounts/types_test.go @@ -0,0 +1,116 @@ +package discounts + +import ( + "strings" + "testing" +) + +func TestParseOffer(t *testing.T) { + offer, err := ParseOffer(ListItem{ + DiscountID: "0x" + hash64, + Adapter: "0x0000000000000000000000000000000000000abc", + TokenToRedeem: "0x0000000000000000000000000000000000000def", + Collateral: "0x0000000000000000000000000000000000000c01", + CollateralDecimals: 6, + Discount: "100000", + Deadline: 1_900_000_000, + MaxRate: "1000000000000000000", + MaxAssets: "5000", + }) + if err != nil { + t.Fatalf("ParseOffer: %v", err) + } + if offer.MaxAssets.String() != "5000" || offer.Discount.String() != "100000" || offer.CollateralDecimals != 6 { + t.Fatalf("offer = %+v", offer) + } +} + +func TestParseOfferRejectsInvalidDiscount(t *testing.T) { + item := ListItem{ + DiscountID: "0x" + hash64, + Adapter: "0x0000000000000000000000000000000000000abc", + TokenToRedeem: "0x0000000000000000000000000000000000000def", + Collateral: "0x0000000000000000000000000000000000000c01", + CollateralDecimals: 6, + Discount: "1000001", + Deadline: 1_900_000_000, + MaxRate: "1000000000000000000", + MaxAssets: "5000", + } + if _, err := ParseOffer(item); err == nil { + t.Fatal("expected out-of-range discount error") + } + item.Discount = "" + if _, err := ParseOffer(item); err == nil { + t.Fatal("expected missing discount error") + } +} + +func TestParseOfferRejectsMalformedIDAndExpiredShape(t *testing.T) { + item := ListItem{ + DiscountID: "0x01", + Adapter: "0x0000000000000000000000000000000000000abc", + TokenToRedeem: "0x0000000000000000000000000000000000000def", + Collateral: "0x0000000000000000000000000000000000000c01", + CollateralDecimals: 6, + Discount: "100000", + Deadline: 1_900_000_000, + MaxRate: "1", + MaxAssets: "1", + } + if _, err := ParseOffer(item); err == nil { + t.Fatal("expected malformed id error") + } + item.DiscountID = "0x" + hash64 + item.Deadline = 0 + if _, err := ParseOffer(item); err == nil { + t.Fatal("expected deadline error") + } + item.Deadline = 1_900_000_000 + item.DiscountID = "0x" + strings.Repeat("0", 64) + if _, err := ParseOffer(item); err == nil { + t.Fatal("expected zero id error") + } +} + +func TestParseSigned(t *testing.T) { + parsed, err := ParseSigned(&Resolved{ + DiscountID: "0x" + hash64, + Discount: Terms{ + Adapter: "0x0000000000000000000000000000000000000abc", + TokenToRedeem: "0x0000000000000000000000000000000000000def", + Discount: "123", + Signer: "0x0000000000000000000000000000000000000aaa", + Protocol: "0x0000000000000000000000000000000000000bbb", + Nonce: "0x2", + Deadline: 1_900_000_000, + }, + SignerSignature: "0xdead", ProtocolDeadline: 1_900_000_001, ProtocolSignature: "0xbeef", + }) + if err != nil { + t.Fatalf("ParseSigned: %v", err) + } + if parsed.Terms.Discount.String() != "123" || parsed.Terms.Nonce.String() != "2" { + t.Fatalf("parsed = %+v", parsed) + } +} + +func TestParseSignedAcceptsZeroAndRejectsOutOfRangeDiscount(t *testing.T) { + resolved := &Resolved{ + DiscountID: "0x" + hash64, + Discount: Terms{ + Adapter: "0x0000000000000000000000000000000000000abc", + TokenToRedeem: "0x0000000000000000000000000000000000000def", + Discount: "0", Signer: "0x0000000000000000000000000000000000000aaa", + Protocol: "0x0000000000000000000000000000000000000bbb", Nonce: "0x2", Deadline: 1_900_000_000, + }, + SignerSignature: "0xdead", ProtocolDeadline: 1_900_000_001, ProtocolSignature: "0xbeef", + } + if _, err := ParseSigned(resolved); err != nil { + t.Fatalf("zero discount: %v", err) + } + resolved.Discount.Discount = "1000001" + if _, err := ParseSigned(resolved); err == nil { + t.Fatal("expected out-of-range discount error") + } +} diff --git a/internal/liquidlane/gas/gas.go b/internal/liquidlane/gas/gas.go index 6cdc8ff6..50635372 100644 --- a/internal/liquidlane/gas/gas.go +++ b/internal/liquidlane/gas/gas.go @@ -1,9 +1,9 @@ -// Package gas predicts gas used by LiquidLane adapter swap routes. +// Package gas provides LiquidLane route gas prediction and Chainlink-backed gas conversion facts. // // It is intentionally limited to LiquidLane adapter swap accounting: callers provide // expected swap demands plus a compact adapter liquidity snapshot, and the package -// returns route labels and route gas units. Solver-specific settlement overhead, -// auction/executor gas limits, price updates, bids, and profitability stay outside. +// returns route labels and route gas units. Solver-specific settlement and payload overhead, +// auction/executor gas limits, price updates, bids, and economics stay outside. package gas const ( diff --git a/internal/liquidlane/gas/gas_test.go b/internal/liquidlane/gas/gas_test.go index c72007be..6554c709 100644 --- a/internal/liquidlane/gas/gas_test.go +++ b/internal/liquidlane/gas/gas_test.go @@ -82,6 +82,66 @@ func TestPredictionConsumesSharedBudgets(t *testing.T) { } } +func TestPredictAdaptersSharesVaultStateAndKeepsFirstSwapTierPerAdapter(t *testing.T) { + adapterA := common.HexToAddress("0x00000000000000000000000000000000000000a1") + adapterB := common.HexToAddress("0x00000000000000000000000000000000000000b1") + vault := common.HexToAddress("0x00000000000000000000000000000000000000f1") + coll := common.HexToAddress("0x00000000000000000000000000000000000000ca") + snapshot := &Snapshot{ + Adapters: map[common.Address]*AdapterState{ + adapterA: {Vault: vault, Acquire: map[common.Address]*big.Int{coll: big.NewInt(100)}}, + adapterB: {Vault: vault, Acquire: map[common.Address]*big.Int{}}, + }, + Vaults: map[common.Address]*VaultState{ + vault: {FreeAssets: big.NewInt(100), Withdrawable: big.NewInt(200)}, + }, + } + prediction := PredictAdapters([]AdapterDemand{ + {Adapter: adapterA, Vault: vault, Demand: Demand{Collateral: coll, AmountOut: big.NewInt(60)}}, + {Adapter: adapterB, Vault: vault, Demand: Demand{Collateral: coll, AmountOut: big.NewInt(60)}}, + {Adapter: adapterA, Vault: vault, Demand: Demand{Collateral: coll, AmountOut: big.NewInt(110)}}, + }, snapshot) + want := UnitsForRouteAt(RouteAcquire, true) + + UnitsForRouteAt(RouteAllocate, true) + + UnitsForRouteAt(RouteDeallocate, false) + if prediction.Units != want { + t.Fatalf("units = %d, want %d", prediction.Units, want) + } + if got := RoutesString(prediction.Routes); got != "acquire,allocate,deallocate" { + t.Fatalf("routes = %q", got) + } + if snapshot.Adapters[adapterA].Acquire[coll].String() != "100" || snapshot.Vaults[vault].FreeAssets.String() != "100" { + t.Fatalf("PredictAdapters mutated input snapshot: %+v", snapshot) + } +} + +func TestWithReserveBpsPricesNextRouteNearBoundary(t *testing.T) { + adapter := common.HexToAddress("0x00000000000000000000000000000000000000a1") + vault := common.HexToAddress("0x00000000000000000000000000000000000000f1") + coll := common.HexToAddress("0x00000000000000000000000000000000000000ca") + snapshot := &Snapshot{ + Adapters: map[common.Address]*AdapterState{ + adapter: {Vault: vault, Acquire: map[common.Address]*big.Int{}}, + }, + Vaults: map[common.Address]*VaultState{ + vault: {FreeAssets: big.NewInt(100), Withdrawable: big.NewInt(200)}, + }, + } + demands := []AdapterDemand{{ + Adapter: adapter, Vault: vault, Demand: Demand{Collateral: coll, AmountOut: big.NewInt(95)}, + }} + if got := RoutesString(PredictAdapters(demands, snapshot).Routes); got != "allocate" { + t.Fatalf("unreserved routes = %q", got) + } + reserved := WithReserveBps(snapshot, 1_000) + if got := RoutesString(PredictAdapters(demands, reserved).Routes); got != "deallocate" { + t.Fatalf("reserved routes = %q", got) + } + if snapshot.Vaults[vault].FreeAssets.String() != "100" { + t.Fatalf("WithReserveBps mutated input snapshot: %+v", snapshot) + } +} + func demandsFor(coll common.Address, outs ...int64) []Demand { demands := make([]Demand, len(outs)) for i, out := range outs { diff --git a/internal/liquidlane/gas/oracle.go b/internal/liquidlane/gas/oracle.go new file mode 100644 index 00000000..a5b95adf --- /dev/null +++ b/internal/liquidlane/gas/oracle.go @@ -0,0 +1,243 @@ +package gas + +import ( + "context" + "encoding/json" + "math/big" + "slices" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/api/bindings/chainlink/aggregator" + "github.com/symbioticfi/vault-solver/internal/chain" +) + +const maxOracleDecimals = 36 + +var chainlinkFeed = aggregator.NewAggregatorV3() + +type OracleConfig struct { + NativeUSDFeed USDFeed + TokenUSDFeeds map[common.Address]USDFeed +} + +type USDFeed struct { + Address common.Address + MaxAge time.Duration +} + +type Token struct { + Address common.Address + Decimals int +} + +type PriceSnapshot struct { + tokenOutPerNative map[common.Address]*big.Int +} + +func NewPriceSnapshot(rates map[common.Address]*big.Int) *PriceSnapshot { + out := make(map[common.Address]*big.Int, len(rates)) + for token, rate := range rates { + if rate != nil { + out[token] = new(big.Int).Set(rate) + } + } + return &PriceSnapshot{tokenOutPerNative: out} +} + +func (s *PriceSnapshot) TokenOutPerNative(token common.Address) *big.Int { + if s == nil || s.tokenOutPerNative[token] == nil { + return nil + } + return new(big.Int).Set(s.tokenOutPerNative[token]) +} + +func (s *PriceSnapshot) MarshalJSON() ([]byte, error) { + rates := map[common.Address]*big.Int(nil) + if s != nil { + rates = s.tokenOutPerNative + } + return json.Marshal(struct { + TokenOutPerNative map[common.Address]*big.Int `json:"tokenOutPerNative"` + }{TokenOutPerNative: rates}) +} + +type multicaller interface { + Multicall(ctx context.Context, calls []chain.Call) ([]chain.CallResult, error) +} + +type OracleReader struct { + chain multicaller + cfg OracleConfig +} + +func NewOracleReader(c multicaller, cfg OracleConfig) (*OracleReader, error) { + if c == nil { + return nil, errors.New("gas oracle: chain client is required") + } + if cfg.NativeUSDFeed.Address == (common.Address{}) { + return nil, errors.New("gas oracle: native USD feed is required") + } + if cfg.NativeUSDFeed.MaxAge <= 0 { + return nil, errors.New("gas oracle: native USD feed max age must be positive") + } + if len(cfg.TokenUSDFeeds) == 0 { + return nil, errors.New("gas oracle: at least one token USD feed is required") + } + feeds := make(map[common.Address]USDFeed, len(cfg.TokenUSDFeeds)) + for token, feed := range cfg.TokenUSDFeeds { + if token == (common.Address{}) || feed.Address == (common.Address{}) { + return nil, errors.New("gas oracle: token and feed addresses must be non-zero") + } + if feed.MaxAge <= 0 { + return nil, errors.Errorf("gas oracle: token %s feed max age must be positive", token.Hex()) + } + feeds[token] = feed + } + cfg.TokenUSDFeeds = feeds + return &OracleReader{chain: c, cfg: cfg}, nil +} + +func (r *OracleReader) ValidateTokens(tokens []Token) error { + decimals := make(map[common.Address]int, len(tokens)) + for _, token := range tokens { + if token.Address == (common.Address{}) { + return errors.New("gas oracle: token address must be non-zero") + } + if current, ok := decimals[token.Address]; ok && current != token.Decimals { + return errors.Errorf("gas oracle: token %s has inconsistent decimals %d and %d", + token.Address.Hex(), current, token.Decimals) + } + decimals[token.Address] = token.Decimals + } + for _, token := range uniqueTokens(tokens) { + if token.Decimals < 0 || token.Decimals > maxOracleDecimals { + return errors.Errorf("gas oracle: token %s decimals %d exceed supported range [0,%d]", + token.Address.Hex(), token.Decimals, maxOracleDecimals) + } + if r.cfg.TokenUSDFeeds[token.Address].Address == (common.Address{}) { + return errors.Errorf("gas oracle: missing USD feed for token %s", token.Address.Hex()) + } + } + return nil +} + +func (r *OracleReader) Read(ctx context.Context, tokens []Token, now time.Time) (*PriceSnapshot, error) { + if err := r.ValidateTokens(tokens); err != nil { + return nil, err + } + tokens = uniqueTokens(tokens) + calls := make([]chain.Call, 0, 2+2*len(tokens)) + calls = appendFeedCalls(calls, r.cfg.NativeUSDFeed.Address) + for _, token := range tokens { + calls = appendFeedCalls(calls, r.cfg.TokenUSDFeeds[token.Address].Address) + } + results, err := r.chain.Multicall(ctx, calls) + if err != nil { + return nil, errors.Errorf("gas oracle: multicall: %w", err) + } + if len(results) != len(calls) { + return nil, errors.Errorf("gas oracle: got %d results, want %d", len(results), len(calls)) + } + native, err := decodeFeed( + results[:2], r.cfg.NativeUSDFeed.Address, now, r.cfg.NativeUSDFeed.MaxAge, + ) + if err != nil { + return nil, err + } + rates := make(map[common.Address]*big.Int, len(tokens)) + for i, token := range tokens { + feed := r.cfg.TokenUSDFeeds[token.Address] + price, decodeErr := decodeFeed(results[2+i*2:4+i*2], feed.Address, now, feed.MaxAge) + if decodeErr != nil { + return nil, decodeErr + } + rate := tokenPerNative(native, price, token.Decimals) + if rate.Sign() <= 0 { + return nil, errors.Errorf("gas oracle: token/native rate for %s rounded to zero", token.Address.Hex()) + } + rates[token.Address] = rate + } + return NewPriceSnapshot(rates), nil +} + +type feedPrice struct { + answer *big.Int + decimals uint8 +} + +func appendFeedCalls(calls []chain.Call, feed common.Address) []chain.Call { + return append(calls, + chain.Call{Target: feed, AllowFailure: true, Data: chainlinkFeed.PackLatestRoundData()}, + chain.Call{Target: feed, AllowFailure: true, Data: chainlinkFeed.PackDecimals()}, + ) +} + +func decodeFeed(results []chain.CallResult, feed common.Address, now time.Time, maxAge time.Duration) (feedPrice, error) { + if len(results) != 2 || !results[0].Success || !results[1].Success { + return feedPrice{}, errors.Errorf("gas oracle: feed %s call failed", feed.Hex()) + } + round, err := chainlinkFeed.UnpackLatestRoundData(results[0].ReturnData) + if err != nil { + return feedPrice{}, errors.Errorf("gas oracle: feed %s latestRoundData: %w", feed.Hex(), err) + } + decimals, err := chainlinkFeed.UnpackDecimals(results[1].ReturnData) + if err != nil { + return feedPrice{}, errors.Errorf("gas oracle: feed %s decimals: %w", feed.Hex(), err) + } + if round.RoundId == nil || round.Answer == nil || round.UpdatedAt == nil { + return feedPrice{}, errors.Errorf("gas oracle: feed %s returned nil round data", feed.Hex()) + } + if round.RoundId.Sign() <= 0 || round.Answer.Sign() <= 0 || + round.UpdatedAt.Sign() <= 0 || !round.UpdatedAt.IsInt64() { + return feedPrice{}, errors.Errorf("gas oracle: feed %s returned invalid round data", feed.Hex()) + } + const maxFutureSkewSeconds = 15 + age := now.Unix() - round.UpdatedAt.Int64() + if age < -maxFutureSkewSeconds { + return feedPrice{}, errors.Errorf( + "gas oracle: feed %s updated %ds in the future", feed.Hex(), -age, + ) + } + // A new Ethereum block can land between the caller's timestamp read and this latest-state + // multicall. Accept only that small race, not arbitrary future timestamps. + age = max(age, 0) + maxAgeSeconds := int64(maxAge / time.Second) + if maxAge%time.Second != 0 { + maxAgeSeconds++ + } + if age > maxAgeSeconds { + return feedPrice{}, errors.Errorf("gas oracle: feed %s is stale: age %ds, max %ds", feed.Hex(), age, maxAgeSeconds) + } + if decimals > maxOracleDecimals { + return feedPrice{}, errors.Errorf("gas oracle: feed %s decimals %d exceed %d", feed.Hex(), decimals, maxOracleDecimals) + } + return feedPrice{answer: new(big.Int).Set(round.Answer), decimals: decimals}, nil +} + +func tokenPerNative(native, token feedPrice, tokenDecimals int) *big.Int { + numerator := new(big.Int).Mul(native.answer, pow10(int(token.decimals)+tokenDecimals)) + denominator := new(big.Int).Mul(token.answer, pow10(int(native.decimals))) + return numerator.Div(numerator, denominator) +} + +func uniqueTokens(tokens []Token) []Token { + byAddress := make(map[common.Address]Token, len(tokens)) + for _, token := range tokens { + if current, ok := byAddress[token.Address]; !ok || token.Decimals > current.Decimals { + byAddress[token.Address] = token + } + } + out := make([]Token, 0, len(byAddress)) + for _, token := range byAddress { + out = append(out, token) + } + slices.SortFunc(out, func(a, b Token) int { return a.Address.Cmp(b.Address) }) + return out +} + +func pow10(decimals int) *big.Int { + return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil) +} diff --git a/internal/liquidlane/gas/oracle_test.go b/internal/liquidlane/gas/oracle_test.go new file mode 100644 index 00000000..a2e8060a --- /dev/null +++ b/internal/liquidlane/gas/oracle_test.go @@ -0,0 +1,185 @@ +package gas + +import ( + "context" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + "github.com/symbioticfi/vault-solver/api/bindings/chainlink/aggregator" + "github.com/symbioticfi/vault-solver/internal/chain" +) + +type oracleMulticaller struct { + results []chain.CallResult +} + +func (f oracleMulticaller) Multicall(context.Context, []chain.Call) ([]chain.CallResult, error) { + return f.results, nil +} + +func TestOracleReaderComposesTokenPerNative(t *testing.T) { + nativeFeed := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenFeed := common.HexToAddress("0x2222222222222222222222222222222222222222") + token := common.HexToAddress("0x3333333333333333333333333333333333333333") + now := time.Unix(1_800_000_000, 0) + reader, err := NewOracleReader(oracleMulticaller{results: []chain.CallResult{ + oracleRoundResult(t, 2000_00000000, now.Unix()), oracleDecimalsResult(t), + oracleRoundResult(t, 2_00000000, now.Unix()), oracleDecimalsResult(t), + }}, OracleConfig{ + NativeUSDFeed: USDFeed{Address: nativeFeed, MaxAge: time.Minute}, + TokenUSDFeeds: map[common.Address]USDFeed{ + token: {Address: tokenFeed, MaxAge: time.Minute}, + }, + }) + if err != nil { + t.Fatalf("NewOracleReader: %v", err) + } + snapshot, err := reader.Read(t.Context(), []Token{{Address: token, Decimals: 6}}, now) + if err != nil { + t.Fatalf("Read: %v", err) + } + if got := snapshot.TokenOutPerNative(token); got == nil || got.String() != "1000000000" { + t.Fatalf("token per native = %v, want 1000000000", got) + } +} + +func TestOracleReaderRejectsMissingAndStaleFeeds(t *testing.T) { + nativeFeed := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenFeed := common.HexToAddress("0x2222222222222222222222222222222222222222") + token := common.HexToAddress("0x3333333333333333333333333333333333333333") + now := time.Unix(1_800_000_000, 0) + reader, err := NewOracleReader(oracleMulticaller{results: []chain.CallResult{ + oracleRoundResult(t, 2000_00000000, now.Add(-2*time.Minute).Unix()), oracleDecimalsResult(t), + oracleRoundResult(t, 2_00000000, now.Unix()), oracleDecimalsResult(t), + }}, OracleConfig{ + NativeUSDFeed: USDFeed{Address: nativeFeed, MaxAge: time.Minute}, + TokenUSDFeeds: map[common.Address]USDFeed{ + token: {Address: tokenFeed, MaxAge: time.Minute}, + }, + }) + if err != nil { + t.Fatalf("NewOracleReader: %v", err) + } + if err := reader.ValidateTokens([]Token{{Address: common.HexToAddress("0x4444444444444444444444444444444444444444"), Decimals: 6}}); err == nil { + t.Fatal("expected missing token feed error") + } + if _, err := reader.Read(t.Context(), []Token{{Address: token, Decimals: 6}}, now); err == nil || + !strings.Contains(err.Error(), "stale") { + t.Fatalf("stale Read error = %v", err) + } +} + +func TestOracleReaderAcceptsFeedUpdatedInNewerBlock(t *testing.T) { + nativeFeed := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenFeed := common.HexToAddress("0x2222222222222222222222222222222222222222") + token := common.HexToAddress("0x3333333333333333333333333333333333333333") + now := time.Unix(1_800_000_000, 0) + reader, err := NewOracleReader(oracleMulticaller{results: []chain.CallResult{ + oracleRoundResult(t, 2000_00000000, now.Add(12*time.Second).Unix()), oracleDecimalsResult(t), + oracleRoundResult(t, 2_00000000, now.Add(12*time.Second).Unix()), oracleDecimalsResult(t), + }}, OracleConfig{ + NativeUSDFeed: USDFeed{Address: nativeFeed, MaxAge: time.Minute}, + TokenUSDFeeds: map[common.Address]USDFeed{ + token: {Address: tokenFeed, MaxAge: time.Minute}, + }, + }) + if err != nil { + t.Fatalf("NewOracleReader: %v", err) + } + if _, err := reader.Read(t.Context(), []Token{{Address: token, Decimals: 6}}, now); err != nil { + t.Fatalf("Read: %v", err) + } +} + +func TestOracleReaderRejectsFeedFarInTheFuture(t *testing.T) { + nativeFeed := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenFeed := common.HexToAddress("0x2222222222222222222222222222222222222222") + token := common.HexToAddress("0x3333333333333333333333333333333333333333") + now := time.Unix(1_800_000_000, 0) + reader, err := NewOracleReader(oracleMulticaller{results: []chain.CallResult{ + oracleRoundResult(t, 2000_00000000, now.Add(time.Minute).Unix()), oracleDecimalsResult(t), + oracleRoundResult(t, 2_00000000, now.Unix()), oracleDecimalsResult(t), + }}, OracleConfig{ + NativeUSDFeed: USDFeed{Address: nativeFeed, MaxAge: time.Minute}, + TokenUSDFeeds: map[common.Address]USDFeed{ + token: {Address: tokenFeed, MaxAge: time.Minute}, + }, + }) + if err != nil { + t.Fatalf("NewOracleReader: %v", err) + } + if _, err := reader.Read(t.Context(), []Token{{Address: token, Decimals: 6}}, now); err == nil || + !strings.Contains(err.Error(), "in the future") { + t.Fatalf("future Read error = %v", err) + } +} + +func TestOracleReaderIgnoresDeprecatedAnsweredInRound(t *testing.T) { + nativeFeed := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenFeed := common.HexToAddress("0x2222222222222222222222222222222222222222") + token := common.HexToAddress("0x3333333333333333333333333333333333333333") + now := time.Unix(1_800_000_000, 0) + reader, err := NewOracleReader(oracleMulticaller{results: []chain.CallResult{ + oracleRoundResultWithAnsweredInRound(t, 2000_00000000, now.Unix(), 0), oracleDecimalsResult(t), + oracleRoundResultWithAnsweredInRound(t, 2_00000000, now.Unix(), 0), oracleDecimalsResult(t), + }}, OracleConfig{ + NativeUSDFeed: USDFeed{Address: nativeFeed, MaxAge: time.Minute}, + TokenUSDFeeds: map[common.Address]USDFeed{ + token: {Address: tokenFeed, MaxAge: time.Minute}, + }, + }) + if err != nil { + t.Fatalf("NewOracleReader: %v", err) + } + if _, err := reader.Read(t.Context(), []Token{{Address: token, Decimals: 6}}, now); err != nil { + t.Fatalf("Read: %v", err) + } +} + +func oracleRoundResult(t *testing.T, answer, updatedAt int64) chain.CallResult { + t.Helper() + return oracleRoundResultWithAnsweredInRound(t, answer, updatedAt, 10) +} + +func oracleRoundResultWithAnsweredInRound( + t *testing.T, + answer, updatedAt, answeredInRound int64, +) chain.CallResult { + t.Helper() + parsed := oracleABI(t) + data, err := parsed.Methods["latestRoundData"].Outputs.Pack( + big.NewInt(10), + big.NewInt(answer), + big.NewInt(updatedAt-1), + big.NewInt(updatedAt), + big.NewInt(answeredInRound), + ) + if err != nil { + t.Fatalf("pack latestRoundData: %v", err) + } + return chain.CallResult{Success: true, ReturnData: data} +} + +func oracleDecimalsResult(t *testing.T) chain.CallResult { + t.Helper() + parsed := oracleABI(t) + data, err := parsed.Methods["decimals"].Outputs.Pack(uint8(8)) + if err != nil { + t.Fatalf("pack decimals: %v", err) + } + return chain.CallResult{Success: true, ReturnData: data} +} + +func oracleABI(t *testing.T) abi.ABI { + t.Helper() + parsed, err := abi.JSON(strings.NewReader(aggregator.AggregatorV3MetaData.ABI)) + if err != nil { + t.Fatalf("parse AggregatorV3 ABI: %v", err) + } + return parsed +} diff --git a/internal/liquidlane/gas/routes.go b/internal/liquidlane/gas/routes.go index 3808188e..e8682f9c 100644 --- a/internal/liquidlane/gas/routes.go +++ b/internal/liquidlane/gas/routes.go @@ -24,12 +24,38 @@ type State struct { Acquire map[common.Address]*big.Int } +// AdapterState contains liquidity owned by one adapter. +type AdapterState struct { + Vault common.Address `json:"vault"` + Acquire map[common.Address]*big.Int `json:"acquire"` +} + +// VaultState contains liquidity shared by every adapter backed by the vault. +type VaultState struct { + FreeAssets *big.Int `json:"freeAssets"` + Withdrawable *big.Int `json:"withdrawable"` +} + +// Snapshot separates adapter-local acquire balances from shared vault liquidity. +type Snapshot struct { + Adapters map[common.Address]*AdapterState `json:"adapters"` + Vaults map[common.Address]*VaultState `json:"vaults"` +} + // Demand is one expected loan-token output from a swap through a LiquidLane adapter. type Demand struct { Collateral common.Address AmountOut *big.Int } +// AdapterDemand is one expected swap output scoped to its LiquidLane adapter and shared vault. +type AdapterDemand struct { + Demand + + Adapter common.Address + Vault common.Address +} + // PredictRoutes estimates the adapter route for each demand in order. func PredictRoutes(demands []Demand, st *State) []Route { if len(demands) == 0 { @@ -54,6 +80,93 @@ func PredictRoutes(demands []Demand, st *State) []Route { return routes } +// PredictAdapters predicts swap routes for a multi-adapter transaction. Acquire balances are consumed +// per adapter while free and withdrawable liquidity is consumed once across adapters sharing a vault. +func PredictAdapters(demands []AdapterDemand, snapshot *Snapshot) Prediction { + if len(demands) == 0 { + return Prediction{} + } + adapters, vaults := cloneSnapshot(snapshot) + seen := make(map[common.Address]bool, len(adapters)) + routes := make([]Route, 0, len(demands)) + var units uint64 + for _, demand := range demands { + route := RouteUnknown + adapterState := adapters[demand.Adapter] + vaultState := vaults[demand.Vault] + if adapterState != nil && adapterState.Vault == demand.Vault && vaultState != nil { + route = predictRoute( + demand.AmountOut, + demand.Collateral, + adapterState.Acquire, + vaultState.FreeAssets, + vaultState.Withdrawable, + ) + } + routes = append(routes, route) + first := !seen[demand.Adapter] + seen[demand.Adapter] = true + units = saturatingAddUint64(units, UnitsForRouteAt(route, first)) + } + return Prediction{Units: units, Routes: routes} +} + +// WithReserveBps returns a conservative copy of snapshot with every mutable liquidity budget reduced. +func WithReserveBps(snapshot *Snapshot, reserveBps int) *Snapshot { + adapters, vaults := cloneSnapshot(snapshot) + if reserveBps <= 0 { + return &Snapshot{Adapters: adapters, Vaults: vaults} + } + if reserveBps > 10_000 { + reserveBps = 10_000 + } + remainingBps := int64(10_000 - reserveBps) + for _, state := range adapters { + for token, amount := range state.Acquire { + state.Acquire[token] = applyBpsDown(amount, remainingBps) + } + } + for _, state := range vaults { + state.FreeAssets = applyBpsDown(state.FreeAssets, remainingBps) + state.Withdrawable = applyBpsDown(state.Withdrawable, remainingBps) + } + return &Snapshot{Adapters: adapters, Vaults: vaults} +} + +func cloneSnapshot(snapshot *Snapshot) (map[common.Address]*AdapterState, map[common.Address]*VaultState) { + if snapshot == nil { + return nil, nil + } + adapters := make(map[common.Address]*AdapterState, len(snapshot.Adapters)) + for address, state := range snapshot.Adapters { + if state == nil { + continue + } + acquire := make(map[common.Address]*big.Int, len(state.Acquire)) + for token, amount := range state.Acquire { + acquire[token] = cloneBig(amount) + } + adapters[address] = &AdapterState{Vault: state.Vault, Acquire: acquire} + } + vaults := make(map[common.Address]*VaultState, len(snapshot.Vaults)) + for address, state := range snapshot.Vaults { + if state == nil || state.FreeAssets == nil || state.Withdrawable == nil { + continue + } + vaults[address] = &VaultState{ + FreeAssets: cloneBig(state.FreeAssets), Withdrawable: cloneBig(state.Withdrawable), + } + } + return adapters, vaults +} + +func applyBpsDown(amount *big.Int, bps int64) *big.Int { + if amount == nil || amount.Sign() <= 0 || bps <= 0 { + return new(big.Int) + } + return new(big.Int).Div(new(big.Int).Mul(amount, big.NewInt(bps)), big.NewInt(10_000)) +} + func predictRoute(amountOut *big.Int, collateral common.Address, acquire map[common.Address]*big.Int, free, withdrawable *big.Int) Route { if amountOut == nil || amountOut.Sign() <= 0 || free == nil || withdrawable == nil { return RouteUnknown diff --git a/internal/liquidlane/math.go b/internal/liquidlane/math.go new file mode 100644 index 00000000..906b38df --- /dev/null +++ b/internal/liquidlane/math.go @@ -0,0 +1,61 @@ +package liquidlane + +import "math/big" + +var rateScale = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) + +func pow10(n int) *big.Int { + return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(n)), nil) +} + +func AmountOutForRate(amountIn, rate *big.Int, tokenInDecimals, tokenOutDecimals int) *big.Int { + if amountIn == nil || rate == nil || amountIn.Sign() <= 0 || rate.Sign() <= 0 { + return new(big.Int) + } + num := new(big.Int).Mul(amountIn, rate) + num.Mul(num, pow10(tokenOutDecimals)) + den := new(big.Int).Mul(rateScale, pow10(tokenInDecimals)) + return num.Div(num, den) +} + +func MaxAmountInForRate(maxAssets, rate *big.Int, tokenInDecimals, tokenOutDecimals int) *big.Int { + if maxAssets == nil || rate == nil || maxAssets.Sign() <= 0 || rate.Sign() <= 0 { + return new(big.Int) + } + den := new(big.Int).Mul(rate, pow10(tokenOutDecimals)) + num := new(big.Int).Mul(maxAssets, rateScale) + num.Mul(num, pow10(tokenInDecimals)) + return num.Div(num, den) +} + +func MinAmountInForAmountOut(amountOut, rate *big.Int, tokenInDecimals, tokenOutDecimals int) *big.Int { + if amountOut == nil || rate == nil || amountOut.Sign() <= 0 || rate.Sign() <= 0 { + return new(big.Int) + } + den := new(big.Int).Mul(rate, pow10(tokenOutDecimals)) + num := new(big.Int).Mul(amountOut, rateScale) + num.Mul(num, pow10(tokenInDecimals)) + num.Add(num, new(big.Int).Sub(den, big.NewInt(1))) + return num.Div(num, den) +} + +func RateForAmountOut(amountOut, amountIn *big.Int, tokenInDecimals, tokenOutDecimals int) *big.Int { + if amountOut == nil || amountIn == nil || amountOut.Sign() <= 0 || amountIn.Sign() <= 0 { + return new(big.Int) + } + num := new(big.Int).Mul(amountOut, rateScale) + num.Mul(num, pow10(tokenInDecimals)) + den := new(big.Int).Mul(amountIn, pow10(tokenOutDecimals)) + return num.Div(num, den) +} + +// AmountOutAfterDiscount applies a LiquidLane ppm discount, rounding down. +func AmountOutAfterDiscount(grossAmountOut, discount *big.Int) *big.Int { + precision := big.NewInt(DiscountPrecision) + if grossAmountOut == nil || grossAmountOut.Sign() <= 0 || discount == nil || discount.Sign() < 0 || + discount.Cmp(precision) > 0 { + return new(big.Int) + } + multiplier := new(big.Int).Sub(precision, discount) + return new(big.Int).Div(new(big.Int).Mul(grossAmountOut, multiplier), big.NewInt(DiscountPrecision)) +} diff --git a/internal/liquidlane/math_test.go b/internal/liquidlane/math_test.go new file mode 100644 index 00000000..8ca44676 --- /dev/null +++ b/internal/liquidlane/math_test.go @@ -0,0 +1,71 @@ +package liquidlane + +import ( + "math/big" + "testing" +) + +func mustBig(t *testing.T, raw string) *big.Int { + t.Helper() + n, ok := new(big.Int).SetString(raw, 10) + if !ok { + t.Fatalf("invalid integer %q", raw) + } + return n +} + +func TestRateMathAcrossDecimals(t *testing.T) { + rate := mustBig(t, "1000000000000000000") + amountIn := mustBig(t, "1000000000000000000") + amountOut := AmountOutForRate(amountIn, rate, 18, 6) + if amountOut.String() != "1000000" { + t.Fatalf("amountOut = %s", amountOut) + } + if got := RateForAmountOut(amountOut, amountIn, 18, 6); got.Cmp(rate) != 0 { + t.Fatalf("rate = %s", got) + } + if got := MaxAmountInForRate(amountOut, rate, 18, 6); got.Cmp(amountIn) != 0 { + t.Fatalf("max amountIn = %s", got) + } +} + +func TestMinAmountInForAmountOutRoundsUp(t *testing.T) { + got := MinAmountInForAmountOut( + big.NewInt(1), + mustBig(t, "3000000000000000000"), + 18, + 6, + ) + if got.String() != "333333333334" { + t.Fatalf("min amountIn = %s", got) + } +} + +func TestRateMathRejectsInvalidInput(t *testing.T) { + if AmountOutForRate(nil, big.NewInt(1), 18, 6).Sign() != 0 { + t.Fatal("nil amount must produce zero") + } + if RateForAmountOut(big.NewInt(1), big.NewInt(0), 18, 6).Sign() != 0 { + t.Fatal("zero input must produce zero") + } +} + +func TestAmountOutAfterDiscount(t *testing.T) { + tests := map[string]struct { + gross *big.Int + discount *big.Int + want string + }{ + "zero": {gross: big.NewInt(1_000), discount: big.NewInt(0), want: "1000"}, + "ten percent": {gross: big.NewInt(1_000), discount: big.NewInt(100_000), want: "900"}, + "full discount": {gross: big.NewInt(1_000), discount: big.NewInt(DiscountPrecision), want: "0"}, + "invalid": {gross: big.NewInt(1_000), discount: big.NewInt(DiscountPrecision + 1), want: "0"}, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + if got := AmountOutAfterDiscount(tt.gross, tt.discount).String(); got != tt.want { + t.Fatalf("AmountOutAfterDiscount() = %s, want %s", got, tt.want) + } + }) + } +} diff --git a/internal/liquidlane/reader.go b/internal/liquidlane/reader.go new file mode 100644 index 00000000..8985e2c6 --- /dev/null +++ b/internal/liquidlane/reader.go @@ -0,0 +1,906 @@ +package liquidlane + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + "github.com/go-logr/logr" + + "github.com/symbioticfi/vault-solver/api/bindings/erc4626" + "github.com/symbioticfi/vault-solver/api/bindings/liquidlane/adapter" + "github.com/symbioticfi/vault-solver/api/bindings/vaultv2" + "github.com/symbioticfi/vault-solver/internal/chain" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" +) + +const ( + DefaultMaxTokensPerAdapter = 64 + inventoryReadsPerRoute = 3 + fillReadsPerRoute = 4 +) + +var ( + llAdapter = adapter.NewLiquidLaneAdapter() + erc4626b = erc4626.NewIERC4626() + vaultV2b = vaultv2.NewIVaultV2() +) + +type Reader struct { + chain liquidLaneBackend + log logr.Logger + dec decimalsReader + + chainID int64 + maxTokensPerAdapter int +} + +type gasAdapterState struct { + owner common.Address + marketMaker common.Address + state *liquidlanegas.AdapterState +} + +type liquidLaneBackend interface { + ChainID() *big.Int + Multicall(ctx context.Context, calls []chain.Call) ([]chain.CallResult, error) +} + +type decimalsReader interface { + Get(ctx context.Context, token common.Address) (int, error) +} + +func NewReader(c *chain.Client, log logr.Logger) *Reader { + return &Reader{ + chain: c, + log: log, + dec: chain.NewDecimals(c), + chainID: c.ChainID().Int64(), + maxTokensPerAdapter: DefaultMaxTokensPerAdapter, + } +} + +func (r *Reader) ResolveAdapters(ctx context.Context, adapters []common.Address) ([]Adapter, error) { + adapters = dedupeAddresses(adapters) + if len(adapters) == 0 { + return nil, nil + } + + vaultCalls := make([]chain.Call, len(adapters)) + for i, a := range adapters { + vaultCalls[i] = chain.Call{Target: a, AllowFailure: true, Data: llAdapter.PackVault()} + } + vaultResults, err := r.chain.Multicall(ctx, vaultCalls) + if err != nil { + return nil, err + } + if len(vaultResults) != len(vaultCalls) { + return nil, errors.Errorf( + "liquidlane: vault multicall: got %d results, want %d", + len(vaultResults), + len(vaultCalls), + ) + } + + out := make([]Adapter, len(adapters)) + assetCalls := make([]chain.Call, len(adapters)) + for i := range adapters { + out[i].Adapter = adapters[i] + if !vaultResults[i].Success { + return nil, errors.Errorf("liquidlane: resolve adapter %s vault: call failed", adapters[i].Hex()) + } + vault, unpackErr := llAdapter.UnpackVault(vaultResults[i].ReturnData) + if unpackErr != nil { + return nil, errors.Errorf("liquidlane: resolve adapter %s vault: %w", adapters[i].Hex(), unpackErr) + } + if vault == (common.Address{}) { + return nil, errors.Errorf("liquidlane: resolve adapter %s vault: zero address", adapters[i].Hex()) + } + out[i].Vault = vault + assetCalls[i] = chain.Call{Target: out[i].Vault, AllowFailure: true, Data: erc4626b.PackAsset()} + } + assetResults, err := r.chain.Multicall(ctx, assetCalls) + if err != nil { + return nil, err + } + if len(assetResults) != len(assetCalls) { + return nil, errors.Errorf( + "liquidlane: asset multicall: got %d results, want %d", + len(assetResults), + len(assetCalls), + ) + } + + for i := range out { + if !assetResults[i].Success { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s vault %s asset: call failed", + out[i].Adapter.Hex(), + out[i].Vault.Hex(), + ) + } + asset, unpackErr := erc4626b.UnpackAsset(assetResults[i].ReturnData) + if unpackErr != nil { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s vault %s asset: %w", + out[i].Adapter.Hex(), + out[i].Vault.Hex(), + unpackErr, + ) + } + if asset == (common.Address{}) { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s vault %s asset: zero address", + out[i].Adapter.Hex(), + out[i].Vault.Hex(), + ) + } + out[i].TokenOut = asset + decimals, decimalsErr := r.dec.Get(ctx, asset) + if decimalsErr != nil { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s tokenOut %s decimals: %w", + out[i].Adapter.Hex(), + asset.Hex(), + decimalsErr, + ) + } + out[i].TokenOutDecimals = decimals + } + return out, nil +} + +func (r *Reader) ResolveRoutes(ctx context.Context, adapters []common.Address) ([]Route, error) { + resolved, err := r.ResolveAdapters(ctx, adapters) + if err != nil { + return nil, err + } + lengths, err := r.readTokenCounts(ctx, resolved) + if err != nil { + return nil, err + } + + type tokenReq struct { + adapterIndex int + tokenIndex int + } + var reqs []tokenReq + var tokenCalls []chain.Call + for i, n := range lengths { + for j := range n { + reqs = append(reqs, tokenReq{adapterIndex: i, tokenIndex: j}) + tokenCalls = append(tokenCalls, chain.Call{ + Target: resolved[i].Adapter, + AllowFailure: true, + Data: llAdapter.PackTokensToRedeem(big.NewInt(int64(j))), + }) + } + } + if len(tokenCalls) == 0 { + return nil, nil + } + res, err := r.chain.Multicall(ctx, tokenCalls) + if err != nil { + return nil, err + } + if len(res) != len(tokenCalls) { + return nil, errors.Errorf("liquidlane: tokensToRedeem multicall: got %d results, want %d", len(res), len(tokenCalls)) + } + + routes := make([]Route, 0, len(res)) + for i, call := range res { + req := reqs[i] + resolvedAdapter := resolved[req.adapterIndex] + if !call.Success { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s tokensToRedeem[%d]: call failed", + resolvedAdapter.Adapter.Hex(), + req.tokenIndex, + ) + } + tokenIn, unpackErr := llAdapter.UnpackTokensToRedeem(call.ReturnData) + if unpackErr != nil { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s tokensToRedeem[%d]: %w", + resolvedAdapter.Adapter.Hex(), + req.tokenIndex, + unpackErr, + ) + } + if tokenIn == (common.Address{}) { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s tokensToRedeem[%d]: zero address", + resolvedAdapter.Adapter.Hex(), + req.tokenIndex, + ) + } + route, routeErr := r.resolveRouteForToken(ctx, resolvedAdapter, tokenIn) + if routeErr != nil { + return nil, routeErr + } + routes = append(routes, route) + } + return compactRoutes(routes), nil +} + +func (r *Reader) RoutesForToken(ctx context.Context, adapters []Adapter, tokenIn common.Address) []Route { + out := make([]Route, 0, len(adapters)) + for _, a := range dedupeAdapters(adapters) { + route, err := r.resolveRouteForToken(ctx, a, tokenIn) + if err != nil { + r.log.Error(err, "liquidlane: route unresolved", + "adapter", a.Adapter.Hex(), + "tokenIn", tokenIn.Hex(), + ) + continue + } + out = append(out, route) + } + return compactRoutes(out) +} + +// readPaused returns current pause state for each successfully decoded adapter. +func (r *Reader) readPaused(ctx context.Context, adapters []common.Address) (map[common.Address]bool, error) { + adapters = dedupeAddresses(adapters) + if len(adapters) == 0 { + return nil, nil + } + calls := make([]chain.Call, len(adapters)) + for i, address := range adapters { + calls[i] = chain.Call{Target: address, AllowFailure: true, Data: llAdapter.PackPaused()} + } + results, err := r.chain.Multicall(ctx, calls) + if err != nil { + return nil, err + } + if len(results) != len(calls) { + return nil, errors.Errorf("liquidlane: paused multicall: got %d results, want %d", len(results), len(calls)) + } + out := make(map[common.Address]bool, len(adapters)) + for i, result := range results { + if !result.Success { + continue + } + paused, unpackErr := llAdapter.UnpackPaused(result.ReturnData) + if unpackErr == nil { + out[adapters[i]] = paused + } + } + return out, nil +} + +func (r *Reader) ReadInventory(ctx context.Context, routes []Route) ([]Inventory, error) { + return r.readInventory(ctx, routes, false) +} + +func (r *Reader) readInventory(ctx context.Context, routes []Route, keepZero bool) ([]Inventory, error) { + routes = compactRoutes(routes) + if len(routes) == 0 { + return nil, nil + } + calls := make([]chain.Call, 0, len(routes)*inventoryReadsPerRoute) + for _, route := range routes { + calls = append(calls, + chain.Call{Target: route.Adapter, AllowFailure: true, Data: llAdapter.PackPaused()}, + chain.Call{Target: route.Adapter, AllowFailure: true, Data: llAdapter.PackGetMaxAssets(route.TokenIn)}, + chain.Call{Target: route.Adapter, AllowFailure: true, Data: llAdapter.PackGetMaxRate(route.TokenIn)}, + ) + } + res, err := r.chain.Multicall(ctx, calls) + if err != nil { + return nil, err + } + if len(res) != len(calls) { + return nil, errors.Errorf("liquidlane: inventory multicall: got %d results, want %d", len(res), len(calls)) + } + out := make([]Inventory, 0, len(routes)) + for i, route := range routes { + base := i * inventoryReadsPerRoute + paused, maxAssetsRes, maxRateRes := res[base], res[base+1], res[base+2] + if !unpaused(paused) { + continue + } + if !maxAssetsRes.Success || !maxRateRes.Success { + continue + } + maxAssets, aerr := llAdapter.UnpackGetMaxAssets(maxAssetsRes.ReturnData) + maxRate, rerr := llAdapter.UnpackGetMaxRate(maxRateRes.ReturnData) + if aerr != nil || rerr != nil || maxAssets == nil || maxRate == nil { + continue + } + if !keepZero && (maxAssets.Sign() <= 0 || maxRate.Sign() <= 0) { + continue + } + out = append(out, DirectInventory(route, maxAssets, maxRate)) + } + return out, nil +} + +// ReadGasSnapshot returns the latest adapter-local acquire balances and shared vault liquidity needed +// to predict LiquidLane swap gas. Partially unread state remains absent and is priced as RouteUnknown. +func (r *Reader) ReadGasSnapshot(ctx context.Context, routes []Route) (*liquidlanegas.Snapshot, error) { + routes = compactRoutes(routes) + if len(routes) == 0 { + return nil, nil + } + type adapterRoutes struct { + adapter common.Address + vault common.Address + routes []Route + } + byAdapter := make(map[common.Address]*adapterRoutes, len(routes)) + ordered := make([]*adapterRoutes, 0, len(routes)) + for _, route := range routes { + entry := byAdapter[route.Adapter] + if entry == nil { + entry = &adapterRoutes{adapter: route.Adapter, vault: route.Vault} + byAdapter[route.Adapter] = entry + ordered = append(ordered, entry) + } + entry.routes = append(entry.routes, route) + } + + headCalls := make([]chain.Call, 0, len(ordered)*2) + for _, entry := range ordered { + headCalls = append(headCalls, + chain.Call{Target: entry.adapter, AllowFailure: true, Data: llAdapter.PackOwner()}, + chain.Call{Target: entry.adapter, AllowFailure: true, Data: llAdapter.PackMarketMaker()}, + ) + } + vaults := make([]common.Address, 0, len(ordered)) + seenVaults := make(map[common.Address]bool, len(ordered)) + for _, entry := range ordered { + if !seenVaults[entry.vault] { + seenVaults[entry.vault] = true + vaults = append(vaults, entry.vault) + } + } + for _, vault := range vaults { + headCalls = append(headCalls, + chain.Call{Target: vault, AllowFailure: true, Data: vaultV2b.PackFreeAssets()}, + chain.Call{Target: vault, AllowFailure: true, Data: vaultV2b.PackWithdrawable()}, + ) + } + headResults, err := r.chain.Multicall(ctx, headCalls) + if err != nil { + return nil, err + } + if len(headResults) != len(headCalls) { + return nil, errors.Errorf("liquidlane: gas state head multicall: got %d results, want %d", len(headResults), len(headCalls)) + } + + states := make(map[common.Address]*gasAdapterState, len(ordered)) + for i, entry := range ordered { + base := i * 2 + ownerRes, makerRes := headResults[base], headResults[base+1] + if !ownerRes.Success || !makerRes.Success { + continue + } + owner, ownerErr := llAdapter.UnpackOwner(ownerRes.ReturnData) + marketMaker, makerErr := llAdapter.UnpackMarketMaker(makerRes.ReturnData) + if ownerErr != nil || makerErr != nil { + continue + } + states[entry.adapter] = &gasAdapterState{ + owner: owner, marketMaker: marketMaker, + state: &liquidlanegas.AdapterState{ + Vault: entry.vault, Acquire: make(map[common.Address]*big.Int, len(entry.routes)), + }, + } + } + vaultStates := make(map[common.Address]*liquidlanegas.VaultState, len(vaults)) + vaultBase := len(ordered) * 2 + for i, vault := range vaults { + base := vaultBase + i*2 + freeRes, withdrawableRes := headResults[base], headResults[base+1] + if !freeRes.Success || !withdrawableRes.Success { + continue + } + freeAssets, freeErr := vaultV2b.UnpackFreeAssets(freeRes.ReturnData) + withdrawable, withdrawableErr := vaultV2b.UnpackWithdrawable(withdrawableRes.ReturnData) + if freeErr != nil || withdrawableErr != nil || freeAssets == nil || withdrawable == nil { + continue + } + vaultStates[vault] = &liquidlanegas.VaultState{ + FreeAssets: new(big.Int).Set(freeAssets), Withdrawable: new(big.Int).Set(withdrawable), + } + } + + type acquireRead struct { + adapter common.Address + token common.Address + holder common.Address + } + acquireCalls := make([]chain.Call, 0, len(routes)*2) + reads := make([]acquireRead, 0, len(routes)*2) + for _, entry := range ordered { + state := states[entry.adapter] + if state == nil { + continue + } + for _, route := range entry.routes { + acquireCalls = append(acquireCalls, chain.Call{ + Target: entry.adapter, AllowFailure: true, Data: llAdapter.PackAcquireBalance(route.TokenIn, state.owner), + }) + reads = append(reads, acquireRead{adapter: entry.adapter, token: route.TokenIn, holder: state.owner}) + if state.marketMaker != (common.Address{}) && state.marketMaker != state.owner { + acquireCalls = append(acquireCalls, chain.Call{ + Target: entry.adapter, AllowFailure: true, + Data: llAdapter.PackAcquireBalance(route.TokenIn, state.marketMaker), + }) + reads = append(reads, acquireRead{ + adapter: entry.adapter, + token: route.TokenIn, + holder: state.marketMaker, + }) + } + } + } + if len(acquireCalls) == 0 { + return gasSnapshot(states, vaultStates), nil + } + acquireResults, err := r.chain.Multicall(ctx, acquireCalls) + if err != nil { + return nil, err + } + if len(acquireResults) != len(acquireCalls) { + return nil, errors.Errorf("liquidlane: gas state acquire multicall: got %d results, want %d", len(acquireResults), len(acquireCalls)) + } + for i, read := range reads { + result := acquireResults[i] + if !result.Success { + continue + } + amount, unpackErr := llAdapter.UnpackAcquireBalance(result.ReturnData) + if unpackErr != nil || amount == nil || amount.Sign() < 0 { + continue + } + state := states[read.adapter] + if state == nil { + return nil, errors.Errorf("liquidlane: missing gas state for adapter %s", read.adapter.Hex()) + } + if state.state.Acquire[read.token] == nil { + state.state.Acquire[read.token] = new(big.Int) + } + state.state.Acquire[read.token].Add(state.state.Acquire[read.token], amount) + } + return gasSnapshot(states, vaultStates), nil +} + +func gasSnapshot( + in map[common.Address]*gasAdapterState, + vaults map[common.Address]*liquidlanegas.VaultState, +) *liquidlanegas.Snapshot { + adapters := make(map[common.Address]*liquidlanegas.AdapterState, len(in)) + for adapter, state := range in { + adapters[adapter] = state.state + } + return &liquidlanegas.Snapshot{Adapters: adapters, Vaults: vaults} +} + +// ReadAdapterSnapshot reads one complete LiquidLane adapter view for solvers that consume all routes. +func (r *Reader) ReadAdapterSnapshot( + ctx context.Context, + adapterAddress common.Address, + filler common.Address, +) (AdapterSnapshot, error) { + routes, err := r.ResolveRoutes(ctx, []common.Address{adapterAddress}) + if err != nil { + return AdapterSnapshot{}, err + } + if len(routes) == 0 { + return AdapterSnapshot{}, errors.New("liquidlane: adapter has no resolved routes") + } + pausedByAdapter, err := r.readPaused(ctx, []common.Address{adapterAddress}) + if err != nil { + return AdapterSnapshot{}, err + } + paused, pausedResolved := pausedByAdapter[adapterAddress] + if !pausedResolved { + return AdapterSnapshot{}, errors.New("liquidlane: adapter pause state unresolved") + } + auth, err := r.ReadAuth(ctx, []common.Address{adapterAddress}, filler) + if err != nil { + return AdapterSnapshot{}, err + } + if len(auth) != 1 || auth[0].Adapter != adapterAddress { + return AdapterSnapshot{}, errors.New("liquidlane: adapter authorization unresolved") + } + gasState, err := r.ReadGasSnapshot(ctx, routes) + if err != nil { + return AdapterSnapshot{}, err + } + adapterState := gasState.Adapters[adapterAddress] + vaultState := gasState.Vaults[routes[0].Vault] + if adapterState == nil || vaultState == nil { + return AdapterSnapshot{}, errors.New("liquidlane: adapter liquidity state unresolved") + } + + inventoryByRoute := make(map[RouteID]Inventory, len(routes)) + if paused { + for _, route := range routes { + inventoryByRoute[route.ID] = DirectInventory(route, new(big.Int), new(big.Int)) + } + } else { + inventory, inventoryErr := r.readInventory(ctx, routes, true) + if inventoryErr != nil { + return AdapterSnapshot{}, inventoryErr + } + for _, item := range inventory { + inventoryByRoute[item.ID] = item + } + if len(inventoryByRoute) != len(routes) { + return AdapterSnapshot{}, errors.New("liquidlane: adapter inventory unresolved") + } + } + + first := routes[0] + out := AdapterSnapshot{ + Adapter: Adapter{ + Adapter: first.Adapter, Vault: first.Vault, + TokenOut: first.TokenOut, TokenOutDecimals: first.TokenOutDecimals, + }, + Paused: paused, Authorized: auth[0].Authorized, + FreeAssets: CloneBig(vaultState.FreeAssets), Withdrawable: CloneBig(vaultState.Withdrawable), + Routes: make([]RouteSnapshot, 0, len(routes)), + } + for _, route := range routes { + item := inventoryByRoute[route.ID] + out.Routes = append(out.Routes, RouteSnapshot{ + Route: route, + MaxAssets: CloneBig(item.MaxAssets), MaxRate: CloneBig(item.MaxRate), + AcquireBalance: CloneBig(adapterState.Acquire[route.TokenIn]), + }) + } + return out, nil +} + +func (r *Reader) ReadFillQuotes( + ctx context.Context, + routes []Route, + tokenIn common.Address, + amountIn *big.Int, +) ([]FillQuote, error) { + if tokenIn == (common.Address{}) || amountIn == nil || amountIn.Sign() <= 0 { + return nil, nil + } + candidates := make([]Route, 0, len(routes)) + for _, route := range compactRoutes(routes) { + if route.TokenIn == tokenIn { + candidates = append(candidates, route) + } + } + if len(candidates) == 0 { + return nil, nil + } + + calls := make([]chain.Call, 0, len(candidates)*fillReadsPerRoute) + for _, route := range candidates { + calls = append(calls, + chain.Call{Target: route.Adapter, AllowFailure: true, Data: llAdapter.PackPaused()}, + chain.Call{Target: route.Adapter, AllowFailure: true, Data: llAdapter.PackGetMaxAssets(route.TokenIn)}, + chain.Call{Target: route.Adapter, AllowFailure: true, Data: llAdapter.PackGetAmountOut(route.TokenIn, amountIn)}, + chain.Call{Target: route.Adapter, AllowFailure: true, Data: llAdapter.PackMinDiscount(route.TokenIn)}, + ) + } + res, err := r.chain.Multicall(ctx, calls) + if err != nil { + return nil, err + } + if len(res) != len(calls) { + return nil, errors.Errorf("liquidlane: fill multicall: got %d results, want %d", len(res), len(calls)) + } + out := make([]FillQuote, 0, len(candidates)) + for i, route := range candidates { + base := i * fillReadsPerRoute + paused, maxAssetsRes, amountOutRes, discountRes := res[base], res[base+1], res[base+2], res[base+3] + if !unpaused(paused) { + continue + } + if !maxAssetsRes.Success || !amountOutRes.Success || !discountRes.Success { + continue + } + maxAssets, aerr := llAdapter.UnpackGetMaxAssets(maxAssetsRes.ReturnData) + grossAmountOut, oerr := llAdapter.UnpackGetAmountOut(amountOutRes.ReturnData) + discount, derr := llAdapter.UnpackMinDiscount(discountRes.ReturnData) + if aerr != nil || oerr != nil || derr != nil || maxAssets.Sign() <= 0 || grossAmountOut.Sign() <= 0 || + discount.Sign() < 0 || discount.Cmp(big.NewInt(DiscountPrecision)) > 0 { + continue + } + maxAmountOut := AmountOutAfterDiscount(grossAmountOut, discount) + if maxAmountOut.Sign() <= 0 { + continue + } + maxRate := RateForAmountOut(maxAmountOut, amountIn, route.TokenInDecimals, route.TokenOutDecimals) + out = append(out, FillQuote{ + Inventory: DirectInventory(route, maxAssets, maxRate), + AmountIn: CloneBig(amountIn), + GrossAmountOut: CloneBig(grossAmountOut), + MaxAmountOut: maxAmountOut, + MinDiscount: CloneBig(discount), + }) + } + return out, nil +} + +func (r *Reader) FilterAuthorized(ctx context.Context, inv []Inventory, filler common.Address) ([]Inventory, error) { + inv = compactInventory(inv) + if len(inv) == 0 { + return nil, nil + } + adapters := make([]common.Address, 0, len(inv)) + for _, item := range inv { + adapters = append(adapters, item.Adapter) + } + authorized, err := r.authorizedAdapters(ctx, adapters, filler) + if err != nil { + return nil, err + } + out := make([]Inventory, 0, len(inv)) + for _, item := range inv { + if authorized[item.Adapter] { + out = append(out, item) + } + } + return out, nil +} + +func (r *Reader) FilterAuthorizedRoutes(ctx context.Context, routes []Route, filler common.Address) ([]Route, error) { + routes = compactRoutes(routes) + if len(routes) == 0 { + return nil, nil + } + adapters := make([]common.Address, 0, len(routes)) + for _, route := range routes { + adapters = append(adapters, route.Adapter) + } + authorized, err := r.authorizedAdapters(ctx, adapters, filler) + if err != nil { + return nil, err + } + out := make([]Route, 0, len(routes)) + for _, route := range routes { + if authorized[route.Adapter] { + out = append(out, route) + } + } + return out, nil +} + +func (r *Reader) authorizedAdapters( + ctx context.Context, + adapters []common.Address, + filler common.Address, +) (map[common.Address]bool, error) { + auth, err := r.ReadAuth(ctx, adapters, filler) + if err != nil { + return nil, err + } + authorized := make(map[common.Address]bool, len(auth)) + for _, item := range auth { + authorized[item.Adapter] = item.Authorized + } + return authorized, nil +} + +func (r *Reader) ReadAuth(ctx context.Context, adapters []common.Address, filler common.Address) ([]Auth, error) { + adapters = dedupeAddresses(adapters) + if len(adapters) == 0 || filler == (common.Address{}) { + return nil, nil + } + calls := make([]chain.Call, 0, len(adapters)*2) + for _, adapterAddr := range adapters { + calls = append(calls, + chain.Call{Target: adapterAddr, AllowFailure: true, Data: llAdapter.PackMarketMaker()}, + chain.Call{Target: adapterAddr, AllowFailure: true, Data: llAdapter.PackOwner()}, + ) + } + res, err := r.chain.Multicall(ctx, calls) + if err != nil { + return nil, err + } + if len(res) != len(calls) { + return nil, errors.Errorf("liquidlane: authorization multicall: got %d results, want %d", len(res), len(calls)) + } + + auths := make([]Auth, len(adapters)) + resolved := make([]bool, len(adapters)) + var fillerChecks []int + for i := range adapters { + mm, ow := res[i*2], res[i*2+1] + if !mm.Success || !ow.Success { + continue + } + marketMaker, e1 := llAdapter.UnpackMarketMaker(mm.ReturnData) + owner, e2 := llAdapter.UnpackOwner(ow.ReturnData) + if e1 != nil || e2 != nil { + continue + } + auths[i] = Auth{Adapter: adapters[i], MarketMaker: marketMaker, Owner: owner} + resolved[i] = true + if marketMaker == filler || owner == filler { + auths[i].Authorized = true + } else if marketMaker != (common.Address{}) { + fillerChecks = append(fillerChecks, i) + } + } + + if len(fillerChecks) > 0 { + fcalls := make([]chain.Call, len(fillerChecks)) + for j, i := range fillerChecks { + fcalls[j] = chain.Call{Target: adapters[i], AllowFailure: true, Data: llAdapter.PackIsFiller(auths[i].MarketMaker, filler)} + } + fres, ferr := r.chain.Multicall(ctx, fcalls) + if ferr != nil { + return nil, ferr + } + if len(fres) != len(fcalls) { + return nil, errors.Errorf("liquidlane: filler authorization multicall: got %d results, want %d", len(fres), len(fcalls)) + } + for j, i := range fillerChecks { + if fres[j].Success { + if ok, derr := llAdapter.UnpackIsFiller(fres[j].ReturnData); derr == nil { + auths[i].IsFiller = ok + auths[i].Authorized = ok + } + } + } + } + + out := make([]Auth, 0, len(auths)) + for i, item := range auths { + if resolved[i] { + out = append(out, item) + } + } + return out, nil +} + +func unpaused(result chain.CallResult) bool { + if !result.Success { + return false + } + paused, err := llAdapter.UnpackPaused(result.ReturnData) + return err == nil && !paused +} + +func (r *Reader) readTokenCounts(ctx context.Context, adapters []Adapter) ([]int, error) { + calls := make([]chain.Call, len(adapters)) + for i, a := range adapters { + calls[i] = chain.Call{Target: a.Adapter, AllowFailure: true, Data: llAdapter.PackGetTokensToRedeemLength()} + } + res, err := r.chain.Multicall(ctx, calls) + if err != nil { + return nil, err + } + if len(res) != len(calls) { + return nil, errors.Errorf("liquidlane: tokensToRedeem length multicall: got %d results, want %d", len(res), len(calls)) + } + out := make([]int, len(adapters)) + for i, call := range res { + if !call.Success { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s tokensToRedeem length: call failed", + adapters[i].Adapter.Hex(), + ) + } + n, unpackErr := llAdapter.UnpackGetTokensToRedeemLength(call.ReturnData) + if unpackErr != nil { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s tokensToRedeem length: %w", + adapters[i].Adapter.Hex(), + unpackErr, + ) + } + if !n.IsInt64() || n.Sign() < 0 { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s tokensToRedeem length: invalid value %s", + adapters[i].Adapter.Hex(), + n, + ) + } + if n.Sign() == 0 { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s: tokensToRedeem is empty", + adapters[i].Adapter.Hex(), + ) + } + if n.Cmp(big.NewInt(int64(r.maxTokensPerAdapter))) > 0 { + return nil, errors.Errorf( + "liquidlane: resolve adapter %s tokensToRedeem length %s exceeds cap %d", + adapters[i].Adapter.Hex(), + n, + r.maxTokensPerAdapter, + ) + } + out[i] = int(n.Int64()) + } + return out, nil +} + +func (r *Reader) resolveRouteForToken(ctx context.Context, adapter Adapter, tokenIn common.Address) (Route, error) { + if tokenIn == (common.Address{}) { + return Route{}, errors.Errorf("liquidlane: resolve adapter %s tokenIn: zero address", adapter.Adapter.Hex()) + } + tokenInDecimals, err := r.dec.Get(ctx, tokenIn) + if err != nil { + return Route{}, errors.Errorf( + "liquidlane: resolve adapter %s tokenIn %s decimals: %w", + adapter.Adapter.Hex(), + tokenIn.Hex(), + err, + ) + } + return NewRoute( + r.chainID, + adapter.Adapter, + adapter.Vault, + tokenIn, + adapter.TokenOut, + tokenInDecimals, + adapter.TokenOutDecimals, + ), nil +} + +func dedupeAddresses(in []common.Address) []common.Address { + seen := make(map[common.Address]bool, len(in)) + out := make([]common.Address, 0, len(in)) + for _, a := range in { + if seen[a] { + continue + } + seen[a] = true + out = append(out, a) + } + return out +} + +func dedupeAdapters(in []Adapter) []Adapter { + seen := make(map[common.Address]bool, len(in)) + out := make([]Adapter, 0, len(in)) + for _, a := range in { + if seen[a.Adapter] { + continue + } + seen[a.Adapter] = true + out = append(out, a) + } + return out +} + +func compactRoutes(in []Route) []Route { + seen := make(map[RouteID]bool, len(in)) + out := make([]Route, 0, len(in)) + for _, route := range in { + if route.Adapter == (common.Address{}) || route.TokenIn == (common.Address{}) || route.TokenOut == (common.Address{}) { + continue + } + if seen[route.ID] { + continue + } + seen[route.ID] = true + out = append(out, route) + } + return out +} + +func compactInventory(in []Inventory) []Inventory { + seen := make(map[CandidateID]bool, len(in)) + out := make([]Inventory, 0, len(in)) + for _, item := range in { + if item.Adapter == (common.Address{}) || item.TokenIn == (common.Address{}) || item.TokenOut == (common.Address{}) { + continue + } + if item.MaxAssets == nil || item.MaxAssets.Sign() <= 0 { + continue + } + id := NewCandidateID(item.Route, item.DiscountID) + if seen[id] { + continue + } + seen[id] = true + out = append(out, item) + } + return out +} diff --git a/internal/liquidlane/reader_test.go b/internal/liquidlane/reader_test.go new file mode 100644 index 00000000..39bfe0e5 --- /dev/null +++ b/internal/liquidlane/reader_test.go @@ -0,0 +1,614 @@ +package liquidlane + +import ( + "context" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + "github.com/go-logr/logr" + + "github.com/symbioticfi/vault-solver/api/bindings/erc4626" + "github.com/symbioticfi/vault-solver/api/bindings/liquidlane/adapter" + "github.com/symbioticfi/vault-solver/api/bindings/vaultv2" + "github.com/symbioticfi/vault-solver/internal/chain" +) + +type scriptedLiquidLaneBackend struct { + latest [][]chain.CallResult +} + +func (b *scriptedLiquidLaneBackend) ChainID() *big.Int { return big.NewInt(11155111) } + +func (b *scriptedLiquidLaneBackend) Multicall(_ context.Context, _ []chain.Call) ([]chain.CallResult, error) { + result := b.latest[0] + b.latest = b.latest[1:] + return result, nil +} + +type fixedDecimals map[common.Address]int + +func (d fixedDecimals) Get(_ context.Context, token common.Address) (int, error) { + return d[token], nil +} + +type failingDecimals struct { + err error +} + +func (d failingDecimals) Get(_ context.Context, _ common.Address) (int, error) { + return 0, d.err +} + +type selectiveDecimals struct { + values fixedDecimals + token common.Address + err error +} + +func (d selectiveDecimals) Get(_ context.Context, token common.Address) (int, error) { + if token == d.token { + return 0, d.err + } + return d.values[token], nil +} + +func TestReaderResolveAdaptersFailsClosedForConfiguredAdapterMetadata(t *testing.T) { + route := testReaderRoute(1) + decimalsErr := errors.New("temporary decimals failure") + tests := map[string]struct { + results [][]chain.CallResult + dec decimalsReader + want string + }{ + "vault call": { + results: [][]chain.CallResult{{{}}}, + dec: fixedDecimals{}, + want: "vault: call failed", + }, + "vault decode": { + results: [][]chain.CallResult{{{Success: true, ReturnData: []byte{0xff}}}}, + dec: fixedDecimals{}, + want: "vault:", + }, + "zero vault": { + results: [][]chain.CallResult{{successOutput(t, "vault", common.Address{})}}, + dec: fixedDecimals{}, + want: "vault: zero address", + }, + "asset call": { + results: [][]chain.CallResult{ + {successOutput(t, "vault", route.Vault)}, + {{}}, + }, + dec: fixedDecimals{}, + want: "asset: call failed", + }, + "asset decode": { + results: [][]chain.CallResult{ + {successOutput(t, "vault", route.Vault)}, + {{Success: true, ReturnData: []byte{0xff}}}, + }, + dec: fixedDecimals{}, + want: "asset:", + }, + "zero asset": { + results: [][]chain.CallResult{ + {successOutput(t, "vault", route.Vault)}, + {successAssetOutput(t, common.Address{})}, + }, + dec: fixedDecimals{}, + want: "asset: zero address", + }, + "decimals": { + results: [][]chain.CallResult{ + {successOutput(t, "vault", route.Vault)}, + {successAssetOutput(t, route.TokenOut)}, + }, + dec: failingDecimals{err: decimalsErr}, + want: "decimals", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + r := &Reader{ + chain: &scriptedLiquidLaneBackend{latest: test.results}, + log: logr.Discard(), + dec: test.dec, + chainID: 11155111, + maxTokensPerAdapter: DefaultMaxTokensPerAdapter, + } + + _, err := r.ResolveAdapters(context.Background(), []common.Address{route.Adapter}) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("ResolveAdapters error = %v, want %q", err, test.want) + } + if name == "decimals" && !errors.Is(err, decimalsErr) { + t.Fatalf("ResolveAdapters error = %v, want wrapped decimals error", err) + } + }) + } +} + +func TestReaderResolveRoutesFailsClosedForConfiguredAdapterRoutes(t *testing.T) { + route := testReaderRoute(1) + decimalsErr := errors.New("temporary token decimals failure") + baseResults := func(extra ...[]chain.CallResult) [][]chain.CallResult { + results := [][]chain.CallResult{ + {successOutput(t, "vault", route.Vault)}, + {successAssetOutput(t, route.TokenOut)}, + } + return append(results, extra...) + } + tests := map[string]struct { + results [][]chain.CallResult + dec decimalsReader + want string + }{ + "length call": { + results: baseResults([]chain.CallResult{{}}), + dec: fixedDecimals{route.TokenOut: route.TokenOutDecimals}, + want: "tokensToRedeem length: call failed", + }, + "length decode": { + results: baseResults([]chain.CallResult{{Success: true, ReturnData: []byte{0xff}}}), + dec: fixedDecimals{route.TokenOut: route.TokenOutDecimals}, + want: "tokensToRedeem length:", + }, + "empty route list": { + results: baseResults([]chain.CallResult{successOutput(t, "getTokensToRedeemLength", new(big.Int))}), + dec: fixedDecimals{route.TokenOut: route.TokenOutDecimals}, + want: "tokensToRedeem is empty", + }, + "route cap": { + results: baseResults([]chain.CallResult{successOutput( + t, + "getTokensToRedeemLength", + big.NewInt(DefaultMaxTokensPerAdapter+1), + )}), + dec: fixedDecimals{route.TokenOut: route.TokenOutDecimals}, + want: "exceeds cap", + }, + "token call": { + results: baseResults( + []chain.CallResult{successOutput(t, "getTokensToRedeemLength", big.NewInt(1))}, + []chain.CallResult{{}}, + ), + dec: fixedDecimals{route.TokenOut: route.TokenOutDecimals}, + want: "tokensToRedeem[0]: call failed", + }, + "token decode": { + results: baseResults( + []chain.CallResult{successOutput(t, "getTokensToRedeemLength", big.NewInt(1))}, + []chain.CallResult{{Success: true, ReturnData: []byte{0xff}}}, + ), + dec: fixedDecimals{route.TokenOut: route.TokenOutDecimals}, + want: "tokensToRedeem[0]:", + }, + "zero token": { + results: baseResults( + []chain.CallResult{successOutput(t, "getTokensToRedeemLength", big.NewInt(1))}, + []chain.CallResult{successOutput(t, "tokensToRedeem", common.Address{})}, + ), + dec: fixedDecimals{route.TokenOut: route.TokenOutDecimals}, + want: "tokensToRedeem[0]: zero address", + }, + "token decimals": { + results: baseResults( + []chain.CallResult{successOutput(t, "getTokensToRedeemLength", big.NewInt(1))}, + []chain.CallResult{successOutput(t, "tokensToRedeem", route.TokenIn)}, + ), + dec: selectiveDecimals{ + values: fixedDecimals{route.TokenOut: route.TokenOutDecimals}, + token: route.TokenIn, + err: decimalsErr, + }, + want: "tokenIn " + route.TokenIn.Hex() + " decimals", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + r := &Reader{ + chain: &scriptedLiquidLaneBackend{latest: test.results}, + log: logr.Discard(), + dec: test.dec, + chainID: 11155111, + maxTokensPerAdapter: DefaultMaxTokensPerAdapter, + } + + _, err := r.ResolveRoutes(t.Context(), []common.Address{route.Adapter}) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("ResolveRoutes error = %v, want %q", err, test.want) + } + if name == "token decimals" && !errors.Is(err, decimalsErr) { + t.Fatalf("ResolveRoutes error = %v, want wrapped decimals error", err) + } + }) + } +} + +func TestReaderReadInventoryUsesLatestAndFailsClosedPerRoute(t *testing.T) { + backend := &scriptedLiquidLaneBackend{ + latest: [][]chain.CallResult{{ + successOutput(t, "paused", false), + successOutput(t, "getMaxAssets", big.NewInt(100)), + successOutput(t, "getMaxRate", big.NewInt(1_000_000_000_000_000_000)), + {Success: true, ReturnData: []byte{0xff}}, + successOutput(t, "getMaxAssets", big.NewInt(200)), + successOutput(t, "getMaxRate", big.NewInt(1_000_000_000_000_000_000)), + }}, + } + r := &Reader{ + chain: backend, log: logr.Discard(), dec: fixedDecimals{}, chainID: 11155111, + maxTokensPerAdapter: DefaultMaxTokensPerAdapter, + } + routes := []Route{testReaderRoute(1), testReaderRoute(2)} + + inventory, err := r.ReadInventory(context.Background(), routes) + if err != nil { + t.Fatalf("ReadInventory: %v", err) + } + if len(inventory) != 1 || inventory[0].ID != routes[0].ID { + t.Fatalf("inventory = %+v", inventory) + } + if inventory[0].MaxRate.String() != "1000000000000000000" { + t.Fatalf("executable max rate = %s", inventory[0].MaxRate) + } +} + +func TestReaderReadFillQuotesFiltersTokenAtLatest(t *testing.T) { + tokenIn := common.HexToAddress("0x0000000000000000000000000000000000000101") + backend := &scriptedLiquidLaneBackend{ + latest: [][]chain.CallResult{{ + successOutput(t, "paused", false), + successOutput(t, "getMaxAssets", big.NewInt(1_000)), + successOutput(t, "getAmountOut", big.NewInt(900)), + successOutput(t, "minDiscount", big.NewInt(100_000)), + }}, + } + r := &Reader{ + chain: backend, log: logr.Discard(), dec: fixedDecimals{}, chainID: 11155111, + maxTokensPerAdapter: DefaultMaxTokensPerAdapter, + } + matching := testReaderRoute(1) + matching.TokenIn = tokenIn + nonMatching := testReaderRoute(2) + amountIn := big.NewInt(500) + + quotes, err := r.ReadFillQuotes(context.Background(), []Route{matching, nonMatching}, tokenIn, amountIn) + if err != nil { + t.Fatalf("ReadFillQuotes: %v", err) + } + if len(quotes) != 1 || quotes[0].GrossAmountOut.String() != "900" || + quotes[0].MaxAmountOut.String() != "810" || quotes[0].MinDiscount.String() != "100000" || + quotes[0].MaxRate.String() != "1620000000000000000000000000000" { + t.Fatalf("quotes = %+v", quotes) + } + amountIn.SetInt64(1) + if quotes[0].AmountIn.String() != "500" { + t.Fatalf("amountIn was not cloned: %s", quotes[0].AmountIn) + } +} + +func TestReaderReadFillQuotesKeepsAmountSpecificFillWhenRateRoundsToZero(t *testing.T) { + tokenIn := common.HexToAddress("0x0000000000000000000000000000000000000101") + backend := &scriptedLiquidLaneBackend{latest: [][]chain.CallResult{{ + successOutput(t, "paused", false), + successOutput(t, "getMaxAssets", big.NewInt(1)), + successOutput(t, "getAmountOut", big.NewInt(1)), + successOutput(t, "minDiscount", big.NewInt(0)), + }}} + r := &Reader{ + chain: backend, log: logr.Discard(), dec: fixedDecimals{}, chainID: 11155111, + maxTokensPerAdapter: DefaultMaxTokensPerAdapter, + } + route := testReaderRoute(1) + route.TokenIn = tokenIn + amountIn := new(big.Int).Exp(big.NewInt(10), big.NewInt(37), nil) + + quotes, err := r.ReadFillQuotes(context.Background(), []Route{route}, tokenIn, amountIn) + if err != nil { + t.Fatalf("ReadFillQuotes: %v", err) + } + if len(quotes) != 1 || quotes[0].MaxAmountOut.String() != "1" || quotes[0].MaxRate.Sign() != 0 { + t.Fatalf("quotes = %+v", quotes) + } +} + +func TestReaderReadGasSnapshotCombinesAcquireAndDeduplicatesVaultState(t *testing.T) { + route := testReaderRoute(1) + secondRoute := testReaderRoute(2) + secondRoute.Vault = route.Vault + secondRoute.CapacityID = route.CapacityID + owner := common.HexToAddress("0x0000000000000000000000000000000000000a11") + marketMaker := common.HexToAddress("0x0000000000000000000000000000000000000b11") + backend := &scriptedLiquidLaneBackend{latest: [][]chain.CallResult{ + { + successOutput(t, "owner", owner), + successOutput(t, "marketMaker", marketMaker), + successOutput(t, "owner", owner), + successOutput(t, "marketMaker", marketMaker), + successVaultOutput(t, "freeAssets", big.NewInt(200)), + successVaultOutput(t, "withdrawable", big.NewInt(150)), + }, + { + successOutput(t, "acquireBalance", big.NewInt(30)), + successOutput(t, "acquireBalance", big.NewInt(70)), + successOutput(t, "acquireBalance", big.NewInt(20)), + successOutput(t, "acquireBalance", big.NewInt(10)), + }, + }} + r := &Reader{chain: backend, log: logr.Discard(), dec: fixedDecimals{}, chainID: 11155111} + snapshot, err := r.ReadGasSnapshot(context.Background(), []Route{route, secondRoute}) + if err != nil { + t.Fatalf("ReadGasSnapshot: %v", err) + } + if len(snapshot.Vaults) != 1 || snapshot.Vaults[route.Vault].FreeAssets.String() != "200" || + snapshot.Vaults[route.Vault].Withdrawable.String() != "150" { + t.Fatalf("gas vault state = %+v", snapshot.Vaults) + } + if snapshot.Adapters[route.Adapter].Acquire[route.TokenIn].String() != "100" || + snapshot.Adapters[secondRoute.Adapter].Acquire[secondRoute.TokenIn].String() != "30" { + t.Fatalf("gas adapter state = %+v", snapshot.Adapters) + } +} + +func TestReaderReadGasSnapshotTreatsInvalidAcquireBalanceAsUnavailable(t *testing.T) { + route := testReaderRoute(1) + owner := common.HexToAddress("0x0000000000000000000000000000000000000a11") + tests := map[string]chain.CallResult{ + "failed call": {}, + "malformed result": {Success: true, ReturnData: []byte{0xff}}, + } + for name, acquireResult := range tests { + t.Run(name, func(t *testing.T) { + backend := &scriptedLiquidLaneBackend{latest: [][]chain.CallResult{ + { + successOutput(t, "owner", owner), + successOutput(t, "marketMaker", owner), + successVaultOutput(t, "freeAssets", big.NewInt(200)), + successVaultOutput(t, "withdrawable", big.NewInt(150)), + }, + {acquireResult}, + }} + r := &Reader{chain: backend, log: logr.Discard(), dec: fixedDecimals{}, chainID: 11155111} + + snapshot, err := r.ReadGasSnapshot(context.Background(), []Route{route}) + if err != nil { + t.Fatalf("ReadGasSnapshot: %v", err) + } + if amount := snapshot.Adapters[route.Adapter].Acquire[route.TokenIn]; amount != nil { + t.Fatalf("acquire balance = %v, want unavailable", amount) + } + }) + } +} + +func TestReaderReadAdapterSnapshotCombinesSharedFacts(t *testing.T) { + route := testReaderRoute(1) + owner := common.HexToAddress("0x0000000000000000000000000000000000000a11") + backend := &scriptedLiquidLaneBackend{latest: [][]chain.CallResult{ + {successOutput(t, "vault", route.Vault)}, + {successAssetOutput(t, route.TokenOut)}, + {successOutput(t, "getTokensToRedeemLength", big.NewInt(1))}, + {successOutput(t, "tokensToRedeem", route.TokenIn)}, + {successOutput(t, "paused", false)}, + {successOutput(t, "marketMaker", owner), successOutput(t, "owner", owner)}, + { + successOutput(t, "owner", owner), successOutput(t, "marketMaker", owner), + successVaultOutput(t, "freeAssets", big.NewInt(200)), + successVaultOutput(t, "withdrawable", big.NewInt(150)), + }, + {successOutput(t, "acquireBalance", big.NewInt(30))}, + { + successOutput(t, "paused", false), + successOutput(t, "getMaxAssets", big.NewInt(120)), + successOutput(t, "getMaxRate", big.NewInt(900)), + }, + }} + r := &Reader{ + chain: backend, log: logr.Discard(), chainID: 11155111, + dec: fixedDecimals{route.TokenIn: route.TokenInDecimals, route.TokenOut: route.TokenOutDecimals}, + maxTokensPerAdapter: DefaultMaxTokensPerAdapter, + } + + snapshot, err := r.ReadAdapterSnapshot(context.Background(), route.Adapter, owner) + if err != nil { + t.Fatalf("ReadAdapterSnapshot: %v", err) + } + if !snapshot.Authorized || snapshot.Paused || snapshot.Vault != route.Vault || snapshot.TokenOut != route.TokenOut { + t.Fatalf("adapter snapshot = %+v", snapshot) + } + if snapshot.FreeAssets.String() != "200" || snapshot.Withdrawable.String() != "150" || len(snapshot.Routes) != 1 { + t.Fatalf("adapter liquidity = %+v", snapshot) + } + gotRoute := snapshot.Routes[0] + if gotRoute.MaxAssets.String() != "120" || gotRoute.MaxRate.String() != "900" || + gotRoute.AcquireBalance.String() != "30" { + t.Fatalf("route snapshot = %+v", gotRoute) + } +} + +func TestReaderReadAdapterSnapshotKeepsZeroCapacityRoutes(t *testing.T) { + first := testReaderRoute(1) + second := testReaderRoute(2) + owner := common.HexToAddress("0x0000000000000000000000000000000000000a11") + backend := &scriptedLiquidLaneBackend{latest: [][]chain.CallResult{ + {successOutput(t, "vault", first.Vault)}, + {successAssetOutput(t, first.TokenOut)}, + {successOutput(t, "getTokensToRedeemLength", big.NewInt(2))}, + { + successOutput(t, "tokensToRedeem", first.TokenIn), + successOutput(t, "tokensToRedeem", second.TokenIn), + }, + {successOutput(t, "paused", false)}, + {successOutput(t, "marketMaker", owner), successOutput(t, "owner", owner)}, + { + successOutput(t, "owner", owner), successOutput(t, "marketMaker", owner), + successVaultOutput(t, "freeAssets", big.NewInt(200)), + successVaultOutput(t, "withdrawable", big.NewInt(150)), + }, + { + successOutput(t, "acquireBalance", big.NewInt(0)), + successOutput(t, "acquireBalance", big.NewInt(30)), + }, + { + successOutput(t, "paused", false), + successOutput(t, "getMaxAssets", big.NewInt(0)), + successOutput(t, "getMaxRate", big.NewInt(900)), + successOutput(t, "paused", false), + successOutput(t, "getMaxAssets", big.NewInt(120)), + successOutput(t, "getMaxRate", big.NewInt(800)), + }, + }} + r := &Reader{ + chain: backend, log: logr.Discard(), chainID: 11155111, + dec: fixedDecimals{ + first.TokenIn: first.TokenInDecimals, second.TokenIn: second.TokenInDecimals, + first.TokenOut: first.TokenOutDecimals, + }, + maxTokensPerAdapter: DefaultMaxTokensPerAdapter, + } + + snapshot, err := r.ReadAdapterSnapshot(context.Background(), first.Adapter, owner) + if err != nil { + t.Fatalf("ReadAdapterSnapshot: %v", err) + } + if len(snapshot.Routes) != 2 { + t.Fatalf("routes = %+v", snapshot.Routes) + } + if snapshot.Routes[0].MaxAssets == nil || snapshot.Routes[0].MaxAssets.Sign() != 0 || + snapshot.Routes[0].MaxRate == nil || snapshot.Routes[0].MaxRate.String() != "900" { + t.Fatalf("zero-cap route = %+v", snapshot.Routes[0]) + } + if snapshot.Routes[1].MaxAssets.String() != "120" || snapshot.Routes[1].MaxRate.String() != "800" { + t.Fatalf("healthy route = %+v", snapshot.Routes[1]) + } +} + +func TestReaderReadAuthUsesDirectRolesAndDelegatedFiller(t *testing.T) { + filler := common.HexToAddress("0x0000000000000000000000000000000000000f11") + marketMaker := common.HexToAddress("0x0000000000000000000000000000000000000a11") + owner := common.HexToAddress("0x0000000000000000000000000000000000000b11") + adapters := []common.Address{ + common.HexToAddress("0x0000000000000000000000000000000000000011"), + common.HexToAddress("0x0000000000000000000000000000000000000012"), + common.HexToAddress("0x0000000000000000000000000000000000000013"), + } + backend := &scriptedLiquidLaneBackend{latest: [][]chain.CallResult{ + { + successOutput(t, "marketMaker", filler), successOutput(t, "owner", owner), + successOutput(t, "marketMaker", marketMaker), successOutput(t, "owner", filler), + successOutput(t, "marketMaker", marketMaker), successOutput(t, "owner", owner), + }, + {successOutput(t, "isFiller", true)}, + }} + r := &Reader{chain: backend, log: logr.Discard(), dec: fixedDecimals{}, chainID: 11155111} + + auth, err := r.ReadAuth(context.Background(), adapters, filler) + if err != nil { + t.Fatalf("ReadAuth: %v", err) + } + if len(auth) != 3 || !auth[0].Authorized || !auth[1].Authorized || !auth[2].Authorized || !auth[2].IsFiller { + t.Fatalf("auth = %+v", auth) + } +} + +func TestReaderReadAuthRejectsIncompleteFillerResults(t *testing.T) { + filler := common.HexToAddress("0x0000000000000000000000000000000000000f11") + marketMaker := common.HexToAddress("0x0000000000000000000000000000000000000a11") + owner := common.HexToAddress("0x0000000000000000000000000000000000000b11") + adapterAddress := common.HexToAddress("0x0000000000000000000000000000000000000011") + backend := &scriptedLiquidLaneBackend{latest: [][]chain.CallResult{ + {successOutput(t, "marketMaker", marketMaker), successOutput(t, "owner", owner)}, + {}, + }} + r := &Reader{chain: backend, log: logr.Discard(), dec: fixedDecimals{}, chainID: 11155111} + + if _, err := r.ReadAuth(context.Background(), []common.Address{adapterAddress}, filler); err == nil { + t.Fatal("expected incomplete filler multicall error") + } +} + +func TestReaderFilterAuthorizedRoutesDropsUnauthorizedAdapters(t *testing.T) { + filler := common.HexToAddress("0x0000000000000000000000000000000000000f11") + owner := common.HexToAddress("0x0000000000000000000000000000000000000b11") + marketMaker := common.HexToAddress("0x0000000000000000000000000000000000000a11") + routes := []Route{testReaderRoute(1), testReaderRoute(2)} + backend := &scriptedLiquidLaneBackend{latest: [][]chain.CallResult{ + { + successOutput(t, "marketMaker", filler), successOutput(t, "owner", owner), + successOutput(t, "marketMaker", marketMaker), successOutput(t, "owner", owner), + }, + {successOutput(t, "isFiller", false)}, + }} + r := &Reader{chain: backend, log: logr.Discard(), dec: fixedDecimals{}, chainID: 11155111} + + got, err := r.FilterAuthorizedRoutes(context.Background(), routes, filler) + if err != nil { + t.Fatalf("FilterAuthorizedRoutes: %v", err) + } + if len(got) != 1 || got[0].ID != routes[0].ID { + t.Fatalf("authorized routes = %+v", got) + } +} + +func testReaderRoute(index byte) Route { + return NewRoute( + 11155111, + common.BytesToAddress([]byte{index}), + common.BytesToAddress([]byte{index + 10}), + common.BytesToAddress([]byte{index + 20}), + common.BytesToAddress([]byte{index + 30}), + 18, + 6, + ) +} + +func successOutput(t *testing.T, method string, values ...any) chain.CallResult { + t.Helper() + parsed, err := adapter.LiquidLaneAdapterMetaData.ParseABI() + if err != nil { + t.Fatalf("parse adapter ABI: %v", err) + } + data, err := packMethodOutput(parsed, method, values...) + if err != nil { + t.Fatalf("pack %s output: %v", method, err) + } + return chain.CallResult{Success: true, ReturnData: data} +} + +func successVaultOutput(t *testing.T, method string, values ...any) chain.CallResult { + t.Helper() + parsed, err := vaultv2.IVaultV2MetaData.ParseABI() + if err != nil { + t.Fatalf("parse vault ABI: %v", err) + } + data, err := packMethodOutput(parsed, method, values...) + if err != nil { + t.Fatalf("pack %s output: %v", method, err) + } + return chain.CallResult{Success: true, ReturnData: data} +} + +func successAssetOutput(t *testing.T, asset common.Address) chain.CallResult { + t.Helper() + parsed, err := erc4626.IERC4626MetaData.ParseABI() + if err != nil { + t.Fatalf("parse ERC4626 ABI: %v", err) + } + data, err := packMethodOutput(parsed, "asset", asset) + if err != nil { + t.Fatalf("pack asset output: %v", err) + } + return chain.CallResult{Success: true, ReturnData: data} +} + +func packMethodOutput(parsed *abi.ABI, method string, values ...any) ([]byte, error) { + return parsed.Methods[method].Outputs.Pack(values...) +} diff --git a/internal/liquidlane/types.go b/internal/liquidlane/types.go new file mode 100644 index 00000000..da027d64 --- /dev/null +++ b/internal/liquidlane/types.go @@ -0,0 +1,177 @@ +package liquidlane + +import ( + "math/big" + "strconv" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +type RouteID string +type CandidateID string +type CapacityID string + +// DiscountPrecision is the LiquidLane parts-per-million denominator. +const DiscountPrecision int64 = 1_000_000 + +// Adapter is adapter-level LiquidLane metadata that is stable after startup. +type Adapter struct { + Adapter common.Address `json:"adapter"` + Vault common.Address `json:"vault"` + + TokenOut common.Address `json:"tokenOut"` + TokenOutDecimals int `json:"tokenOutDecimals"` +} + +// Route is one LiquidLane adapter path: tokenIn -> adapter -> tokenOut. +type Route struct { + ID RouteID `json:"id"` + CapacityID CapacityID `json:"capacityId"` + + Adapter common.Address `json:"adapter"` + Vault common.Address `json:"vault"` + + TokenIn common.Address `json:"tokenIn"` + TokenOut common.Address `json:"tokenOut"` + + TokenInDecimals int `json:"tokenInDecimals"` + TokenOutDecimals int `json:"tokenOutDecimals"` +} + +// Inventory is the current read-side liquidity/cap snapshot for one route. +type Inventory struct { + Route + + MaxAssets *big.Int `json:"maxAssets"` + MaxRate *big.Int `json:"maxRate"` + + DiscountID *common.Hash `json:"discountId"` + + ValidUntil time.Time `json:"validUntil"` +} + +// FillQuote is a current adapter quote for one concrete amountIn. +type FillQuote struct { + Inventory + + AmountIn *big.Int `json:"amountIn"` + GrossAmountOut *big.Int `json:"grossAmountOut"` + MaxAmountOut *big.Int `json:"maxAmountOut"` + MinDiscount *big.Int `json:"minDiscount"` +} + +type Auth struct { + Adapter common.Address + MarketMaker common.Address + Owner common.Address + IsFiller bool + Authorized bool +} + +// AdapterSnapshot is a current, solver-neutral view of one LiquidLane adapter and its routes. +type AdapterSnapshot struct { + Adapter + + Paused bool + Authorized bool + FreeAssets *big.Int + Withdrawable *big.Int + Routes []RouteSnapshot +} + +// RouteSnapshot combines route metadata with current inventory and adapter-local acquire liquidity. +type RouteSnapshot struct { + Route + + MaxAssets *big.Int + MaxRate *big.Int + AcquireBalance *big.Int +} + +func NewRoute( + chainID int64, + adapter common.Address, + vault common.Address, + tokenIn common.Address, + tokenOut common.Address, + tokenInDecimals int, + tokenOutDecimals int, +) Route { + return Route{ + ID: NewRouteID(chainID, adapter, tokenIn, tokenOut), + CapacityID: NewCapacityID(chainID, vault, tokenOut), + Adapter: adapter, + Vault: vault, + TokenIn: tokenIn, + TokenOut: tokenOut, + TokenInDecimals: tokenInDecimals, + TokenOutDecimals: tokenOutDecimals, + } +} + +func NewCapacityID(chainID int64, vault, tokenOut common.Address) CapacityID { + return CapacityID(strings.ToLower( + "capacity:" + strconv.FormatInt(chainID, 10) + ":" + vault.Hex() + ":" + tokenOut.Hex(), + )) +} + +func RouteCapacityID(route Route) CapacityID { + if route.CapacityID != "" { + return route.CapacityID + } + return CapacityID(route.ID) +} + +func NewRouteID(chainID int64, adapter, tokenIn, tokenOut common.Address) RouteID { + return RouteID(strings.ToLower( + "route:" + strconv.FormatInt(chainID, 10) + ":" + adapter.Hex() + ":" + tokenIn.Hex() + ":" + tokenOut.Hex(), + )) +} + +func NewCandidateID(route Route, discountID *common.Hash) CandidateID { + id := "candidate:" + string(route.ID) + if discountID != nil { + id += ":discount:" + discountID.Hex() + } + return CandidateID(strings.ToLower(id)) +} + +func DirectInventory(route Route, maxAssets, maxRate *big.Int) Inventory { + return Inventory{ + Route: route, + MaxAssets: CloneBig(maxAssets), + MaxRate: CloneBig(maxRate), + } +} + +func DiscountInventory( + route Route, + maxAssets, maxRate *big.Int, + discountID common.Hash, + validUntil time.Time, +) Inventory { + return Inventory{ + Route: route, + MaxAssets: CloneBig(maxAssets), + MaxRate: CloneBig(maxRate), + DiscountID: CloneHash(&discountID), + ValidUntil: validUntil, + } +} + +func CloneBig(n *big.Int) *big.Int { + if n == nil { + return nil + } + return new(big.Int).Set(n) +} + +func CloneHash(h *common.Hash) *common.Hash { + if h == nil { + return nil + } + out := *h + return &out +} diff --git a/internal/liquidlane/types_test.go b/internal/liquidlane/types_test.go new file mode 100644 index 00000000..539c5b06 --- /dev/null +++ b/internal/liquidlane/types_test.go @@ -0,0 +1,46 @@ +package liquidlane + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +func TestIDsAreStableLowercase(t *testing.T) { + adapter := common.HexToAddress("0x00000000000000000000000000000000000000AA") + tokenIn := common.HexToAddress("0x00000000000000000000000000000000000000BB") + tokenOut := common.HexToAddress("0x00000000000000000000000000000000000000CC") + discount := common.HexToHash("0xABCDEF0000000000000000000000000000000000000000000000000000000000") + + route := NewRoute(11155111, adapter, common.Address{}, tokenIn, tokenOut, 18, 6) + if got, want := string(route.ID), "route:11155111:0x00000000000000000000000000000000000000aa:0x00000000000000000000000000000000000000bb:0x00000000000000000000000000000000000000cc"; got != want { + t.Fatalf("route id = %q, want %q", got, want) + } + if got, want := string(NewCandidateID(route, &discount)), "candidate:"+string(route.ID)+":discount:"+discount.Hex(); got != want { + t.Fatalf("candidate id = %q, want %q", got, want) + } +} + +func TestInventoryConstructorsCloneMutableValues(t *testing.T) { + route := NewRoute(1, common.HexToAddress("0x1"), common.Address{}, common.HexToAddress("0x2"), common.HexToAddress("0x3"), 18, 6) + maxAssets := big.NewInt(100) + maxRate := big.NewInt(200) + discount := common.HexToHash("0x42") + + validUntil := time.Unix(2, 0) + inv := DiscountInventory(route, maxAssets, maxRate, discount, validUntil) + maxAssets.SetInt64(1) + maxRate.SetInt64(2) + + if inv.MaxAssets.String() != "100" || inv.MaxRate.String() != "200" { + t.Fatalf("inventory did not clone big.Int values: maxAssets=%s maxRate=%s", inv.MaxAssets, inv.MaxRate) + } + if inv.DiscountID == nil || *inv.DiscountID == (common.Hash{}) { + t.Fatalf("inventory did not clone discount id: %v", inv.DiscountID) + } + if !inv.ValidUntil.Equal(validUntil) { + t.Fatalf("valid until = %s", inv.ValidUntil) + } +} diff --git a/internal/liquidlanemath/math.go b/internal/liquidlanemath/math.go deleted file mode 100644 index 321b5002..00000000 --- a/internal/liquidlanemath/math.go +++ /dev/null @@ -1,51 +0,0 @@ -// Package liquidlanemath contains LiquidLane fixed-point rate calculations. -package liquidlanemath - -import "math/big" - -var rateScale = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) - -func pow10(n int) *big.Int { - return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(n)), nil) -} - -func AmountOutForRate(amountIn, rate *big.Int, tokenInDec, assetDec int) *big.Int { - num := new(big.Int).Mul(amountIn, rate) - num.Mul(num, pow10(assetDec)) - den := new(big.Int).Mul(rateScale, pow10(tokenInDec)) - if den.Sign() == 0 { - return new(big.Int) - } - return num.Div(num, den) -} - -func MaxAmountInForRate(maxAssets, rate *big.Int, tokenInDec, assetDec int) *big.Int { - den := new(big.Int).Mul(rate, pow10(assetDec)) - if den.Sign() == 0 { - return new(big.Int) - } - num := new(big.Int).Mul(maxAssets, rateScale) - num.Mul(num, pow10(tokenInDec)) - return num.Div(num, den) -} - -func MinAmountInForAmountOut(amountOut, rate *big.Int, tokenInDec, assetDec int) *big.Int { - den := new(big.Int).Mul(rate, pow10(assetDec)) - if den.Sign() == 0 { - return new(big.Int) - } - num := new(big.Int).Mul(amountOut, rateScale) - num.Mul(num, pow10(tokenInDec)) - num.Add(num, new(big.Int).Sub(den, big.NewInt(1))) - return num.Div(num, den) -} - -func RateForAmountOut(amountOut, amountIn *big.Int, tokenInDec, assetDec int) *big.Int { - if amountIn.Sign() == 0 { - return new(big.Int) - } - num := new(big.Int).Mul(amountOut, rateScale) - num.Mul(num, pow10(tokenInDec)) - den := new(big.Int).Mul(amountIn, pow10(assetDec)) - return num.Div(num, den) -} diff --git a/internal/liquidlanemath/math_test.go b/internal/liquidlanemath/math_test.go deleted file mode 100644 index 88273d76..00000000 --- a/internal/liquidlanemath/math_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package liquidlanemath - -import ( - "math/big" - "testing" -) - -func mustBig(t *testing.T, s string) *big.Int { - t.Helper() - n, ok := new(big.Int).SetString(s, 10) - if !ok { - t.Fatalf("bad big.Int %q", s) - } - return n -} - -func TestAmountOutForRate(t *testing.T) { - got := AmountOutForRate( - mustBig(t, "1000000000000000000"), - mustBig(t, "1000000000000000000"), - 18, - 6, - ) - if got.String() != "1000000" { - t.Fatalf("AmountOutForRate = %s, want 1000000", got) - } -} - -func TestMaxAmountInForRate(t *testing.T) { - got := MaxAmountInForRate( - mustBig(t, "1000000"), - mustBig(t, "1000000000000000000"), - 18, - 6, - ) - if got.String() != "1000000000000000000" { - t.Fatalf("MaxAmountInForRate = %s, want 1000000000000000000", got) - } -} - -func TestMinAmountInForAmountOutRoundsUp(t *testing.T) { - got := MinAmountInForAmountOut( - mustBig(t, "1"), - mustBig(t, "3000000000000000000"), - 18, - 6, - ) - if got.String() != "333333333334" { - t.Fatalf("MinAmountInForAmountOut = %s, want 333333333334", got) - } -} - -func TestRateForAmountOut(t *testing.T) { - got := RateForAmountOut( - mustBig(t, "1000000"), - mustBig(t, "1000000000000000000"), - 18, - 6, - ) - if got.String() != "1000000000000000000" { - t.Fatalf("RateForAmountOut = %s, want 1000000000000000000", got) - } -} diff --git a/internal/solvers/lifi/chainreader.go b/internal/solvers/lifi/chainreader.go new file mode 100644 index 00000000..a2d1df8c --- /dev/null +++ b/internal/solvers/lifi/chainreader.go @@ -0,0 +1,251 @@ +package lifi + +import ( + "context" + "math/big" + "time" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + "github.com/go-logr/logr" + + "github.com/symbioticfi/vault-solver/api/bindings/lifi/inputsettler" + "github.com/symbioticfi/vault-solver/internal/chain" + "github.com/symbioticfi/vault-solver/internal/liquidlane" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" +) + +var ( + lifiInputSettler = inputsettler.NewILifiInputSettler() +) + +type reader struct { + chain *chain.Client + ll *liquidlane.Reader + gasOracle *liquidlanegas.OracleReader +} + +type route = liquidlane.Route + +type quoteSnapshotSet struct { + Direct []liquidlane.Inventory + DiscountBases []liquidlane.Inventory + GasSnapshot *liquidlanegas.Snapshot + GasPrices *liquidlanegas.PriceSnapshot +} + +type fillSnapshotSet struct { + Direct []liquidlane.FillQuote + DiscountBases []liquidlane.FillQuote + GasSnapshot *liquidlanegas.Snapshot + GasPrices *liquidlanegas.PriceSnapshot +} + +func newReader(c *chain.Client, log logr.Logger, gasCfg liquidlanegas.OracleConfig) (*reader, error) { + gasOracle, err := liquidlanegas.NewOracleReader(c, gasCfg) + if err != nil { + return nil, err + } + return &reader{chain: c, ll: liquidlane.NewReader(c, log), gasOracle: gasOracle}, nil +} + +func (r *reader) resolveRoutes(ctx context.Context, adapters []common.Address) ([]route, error) { + return r.ll.ResolveRoutes(ctx, adapters) +} + +func (r *reader) validateGasTokens(routes []route) error { + return r.gasOracle.ValidateTokens(gasTokens(routes)) +} + +func (r *reader) quoteSnapshots( + ctx context.Context, + routes []route, + executorAddr common.Address, + chainTime time.Time, +) (quoteSnapshotSet, error) { + all, err := r.ll.ReadInventory(ctx, routes) + if err != nil { + return quoteSnapshotSet{}, err + } + direct, err := r.ll.FilterAuthorized(ctx, all, executorAddr) + if err != nil { + return quoteSnapshotSet{}, err + } + gasSnapshot, err := r.ll.ReadGasSnapshot(ctx, routes) + if err != nil { + return quoteSnapshotSet{}, err + } + gasPrices, err := r.gasOracle.Read(ctx, gasTokens(routes), chainTime) + if err != nil { + return quoteSnapshotSet{}, err + } + return quoteSnapshotSet{Direct: direct, DiscountBases: all, GasSnapshot: gasSnapshot, GasPrices: gasPrices}, nil +} + +func (r *reader) fillSnapshots( + ctx context.Context, + routes []route, + executorAddr common.Address, + tokenIn common.Address, + amountIn *big.Int, + chainTime time.Time, +) (fillSnapshotSet, error) { + all, err := r.ll.ReadFillQuotes(ctx, routes, tokenIn, amountIn) + if err != nil { + return fillSnapshotSet{}, err + } + authorized, err := r.ll.FilterAuthorizedRoutes(ctx, routes, executorAddr) + if err != nil { + return fillSnapshotSet{}, err + } + directAdapters := make(map[common.Address]bool, len(authorized)) + for _, item := range authorized { + directAdapters[item.Adapter] = true + } + direct := make([]liquidlane.FillQuote, 0, len(all)) + for _, quote := range all { + if directAdapters[quote.Adapter] { + direct = append(direct, quote) + } + } + gasSnapshot, err := r.ll.ReadGasSnapshot(ctx, routes) + if err != nil { + return fillSnapshotSet{}, err + } + gasPrices, err := r.gasOracle.Read(ctx, gasTokens(routes), chainTime) + if err != nil { + return fillSnapshotSet{}, err + } + return fillSnapshotSet{Direct: direct, DiscountBases: all, GasSnapshot: gasSnapshot, GasPrices: gasPrices}, nil +} + +func gasTokens(routes []route) []liquidlanegas.Token { + tokens := make([]liquidlanegas.Token, 0, len(routes)) + for _, route := range routes { + tokens = append(tokens, liquidlanegas.Token{Address: route.TokenOut, Decimals: route.TokenOutDecimals}) + } + return tokens +} + +func (r *reader) validateExecutor( + ctx context.Context, + executorAddr common.Address, + inputSettler common.Address, + outputSettler common.Address, + caller common.Address, +) error { + calls := []chain.Call{ + {Target: executorAddr, Data: lifiExecutor.PackINPUTSETTLER()}, + {Target: executorAddr, Data: lifiExecutor.PackOUTPUTSETTLER()}, + {Target: executorAddr, Data: lifiExecutor.PackIsCaller(caller)}, + } + results, err := r.chain.Multicall(ctx, calls) + if err != nil { + return errors.Errorf("executor configuration: %w", err) + } + if len(results) != len(calls) || !results[0].Success || !results[1].Success || !results[2].Success { + return errors.New("executor configuration: unresolved") + } + gotInput, inputErr := lifiExecutor.UnpackINPUTSETTLER(results[0].ReturnData) + gotOutput, outputErr := lifiExecutor.UnpackOUTPUTSETTLER(results[1].ReturnData) + if inputErr != nil || outputErr != nil { + return errors.New("executor immutables: malformed response") + } + if gotInput != inputSettler || gotOutput != outputSettler { + return errors.Errorf("executor immutables mismatch: input=%s output=%s", gotInput.Hex(), gotOutput.Hex()) + } + allowed, err := lifiExecutor.UnpackIsCaller(results[2].ReturnData) + if err != nil { + return errors.New("executor caller authorization: malformed response") + } + if !allowed { + return errors.Errorf("executor caller %s is not authorized", caller.Hex()) + } + return nil +} + +func (r *reader) validateZeroGovernanceFee(ctx context.Context, inputSettler common.Address) error { + ret, err := r.chain.CallContract(ctx, ethereum.CallMsg{ + To: &inputSettler, + Data: lifiInputSettler.PackGovernanceFee(), + }, nil) + if err != nil { + return errors.Errorf("input settler governance fee: %w", err) + } + fee, err := lifiInputSettler.UnpackGovernanceFee(ret) + if err != nil { + return errors.Errorf("input settler governance fee: malformed response: %w", err) + } + if fee != 0 { + return errors.Errorf("input settler governance fee is %d, expected zero", fee) + } + return nil +} + +func (r *reader) validateDirectAuthorization( + ctx context.Context, + executorAddr common.Address, + routes []route, +) error { + direct, err := r.ll.FilterAuthorizedRoutes(ctx, routes, executorAddr) + if err != nil { + return err + } + if len(direct) != len(routes) { + return errors.Errorf("executor has direct filler authorization for %d of %d configured routes", len(direct), len(routes)) + } + return nil +} + +func (r *reader) orderIdentifier( + ctx context.Context, + inputSettler common.Address, + order inputsettler.StandardOrder, +) (common.Hash, error) { + data, err := lifiInputSettler.TryPackOrderIdentifier(order) + if err != nil { + return common.Hash{}, errors.Errorf("pack orderIdentifier: %w", err) + } + ret, err := r.chain.CallContract(ctx, ethereum.CallMsg{To: &inputSettler, Data: data}, nil) + if err != nil { + return common.Hash{}, errors.Errorf("call orderIdentifier: %w", err) + } + orderID, err := lifiInputSettler.UnpackOrderIdentifier(ret) + if err != nil { + return common.Hash{}, errors.Errorf("unpack orderIdentifier: %w", err) + } + return common.Hash(orderID), nil +} + +func (r *reader) orderStatus(ctx context.Context, inputSettler common.Address, orderID common.Hash) (uint8, error) { + data, err := lifiInputSettler.TryPackOrderStatus(orderID) + if err != nil { + return 0, errors.Errorf("pack orderStatus: %w", err) + } + ret, err := r.chain.CallContract(ctx, ethereum.CallMsg{To: &inputSettler, Data: data}, nil) + if err != nil { + return 0, errors.Errorf("call orderStatus: %w", err) + } + status, err := lifiInputSettler.UnpackOrderStatus(ret) + if err != nil { + return 0, errors.Errorf("unpack orderStatus: %w", err) + } + return status, nil +} + +func (r *reader) latestBlockNumber(ctx context.Context) (uint64, error) { + n, err := r.chain.BlockNumber(ctx) + if err != nil { + return 0, errors.Errorf("block number: %w", err) + } + return n, nil +} + +func (r *reader) latestBlockTime(ctx context.Context) (time.Time, error) { + header, err := r.chain.HeaderByNumber(ctx, nil) + if err != nil { + return time.Time{}, errors.Errorf("latest block header: %w", err) + } + return time.Unix(int64(header.Time), 0), nil +} diff --git a/internal/solvers/lifi/chainreader_test.go b/internal/solvers/lifi/chainreader_test.go new file mode 100644 index 00000000..f8f6a361 --- /dev/null +++ b/internal/solvers/lifi/chainreader_test.go @@ -0,0 +1,72 @@ +package lifi + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-logr/logr" + + "github.com/symbioticfi/vault-solver/internal/chain" +) + +func TestValidateZeroGovernanceFee(t *testing.T) { + tests := []struct { + name string + fee uint64 + wantErr string + }{ + {name: "zero", fee: 0}, + {name: "non-zero", fee: 1, wantErr: "governance fee is 1, expected zero"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := governanceFeeRPC(t, tt.fee) + defer server.Close() + + client, err := chain.Dial(t.Context(), []string{server.URL}, "", common.Address{}.Hex(), logr.Discard()) + if err != nil { + t.Fatalf("chain.Dial: %v", err) + } + defer client.Close() + + err = (&reader{chain: client}).validateZeroGovernanceFee( + t.Context(), common.HexToAddress("0x1111111111111111111111111111111111111111"), + ) + if tt.wantErr == "" && err != nil { + t.Fatalf("validateZeroGovernanceFee: %v", err) + } + if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) { + t.Fatalf("validateZeroGovernanceFee error = %v", err) + } + }) + } +} + +func governanceFeeRPC(t *testing.T, fee uint64) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + var rpcRequest struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + } + body, err := io.ReadAll(request.Body) + if err != nil { + t.Errorf("read RPC request: %v", err) + } + if err := json.Unmarshal(body, &rpcRequest); err != nil { + t.Errorf("decode RPC request: %v", err) + } + result := `"0xaa36a7"` + if rpcRequest.Method == "eth_call" { + result = fmt.Sprintf(`"0x%064x"`, fee) + } + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%s,"result":%s}`, rpcRequest.ID, result) + })) +} diff --git a/internal/solvers/lifi/config.go b/internal/solvers/lifi/config.go new file mode 100644 index 00000000..2b100a52 --- /dev/null +++ b/internal/solvers/lifi/config.go @@ -0,0 +1,275 @@ +package lifi + +import ( + "strconv" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + "gopkg.in/yaml.v3" + + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" + "github.com/symbioticfi/vault-solver/internal/parse" + "github.com/symbioticfi/vault-solver/internal/solver" + "github.com/symbioticfi/vault-solver/internal/tokenpolicy" +) + +type rawConfig struct { + OrderServer rawOrderServerConfig `yaml:"orderServer"` + InputSettler string `yaml:"inputSettler"` + OutputSettler string `yaml:"outputSettler"` + Executor string `yaml:"executor"` + Adapters []string `yaml:"adapters"` + TokensToQuote string `yaml:"tokensToQuote"` + PermissionedTokens []string `yaml:"permissionedTokens"` + QuoteIntervalMs int `yaml:"quoteIntervalMs"` + QuoteTTL string `yaml:"quoteTtl"` + QuoteRefreshMode string `yaml:"quoteRefreshMode"` + SolverMode string `yaml:"solverMode"` + DiscountsURL string `yaml:"privateDiscountsUrl"` + Gas rawGasConfig `yaml:"gas"` + Strategy rawStrategyConfig `yaml:"strategy"` +} + +type rawGasConfig struct { + NativeUSDFeed string `yaml:"nativeUsdFeed"` + NativeMaxAge string `yaml:"nativeMaxAge"` + TokenUSDFeeds []rawTokenUSDFeed `yaml:"tokenUsdFeeds"` +} + +type rawTokenUSDFeed struct { + Token string `yaml:"token"` + Feed string `yaml:"feed"` + MaxAge string `yaml:"maxAge"` +} + +type rawOrderServerConfig struct { + BaseURL string `yaml:"baseUrl"` + WSURL string `yaml:"wsUrl"` + APIKeyEnv string `yaml:"apiKeyEnv"` + HTTPTimeout string `yaml:"httpTimeout"` +} + +type rawStrategyConfig struct { + Name string `yaml:"name"` + Config yaml.Node `yaml:"config"` +} + +type Config struct { + OrderServer OrderServerConfig + InputSettler common.Address + OutputSettler common.Address + Executor common.Address + Adapters []common.Address + TokenPolicy tokenpolicy.Policy + QuoteInterval time.Duration + QuoteTTL time.Duration + QuoteRefreshMode string + SolverMode string + DiscountsURL string + Gas liquidlanegas.OracleConfig + Strategy StrategyConfig +} + +type OrderServerConfig struct { + BaseURL string + WSURL string + APIKeyEnv string + HTTPTimeout time.Duration +} + +type StrategyConfig struct { + Name string + Config yaml.Node +} + +const ( + defaultHTTPTimeout = 10 * time.Second + defaultQuoteInterval = 30 * time.Second + defaultQuoteTTL = 36 * time.Second + defaultBlockPollInterval = time.Second + defaultQuoteRefreshMode = quoteRefreshModeBlock + defaultStrategyName = "default" + defaultSolverMode = solverModeExternal +) + +const ( + quoteRefreshModeInterval = "interval" + quoteRefreshModeBlock = "block" + solverModeExternal = "external" + solverModeInternal = "internal" +) + +func parseConfig(node yaml.Node) (*Config, error) { + var raw rawConfig + if err := solver.DecodeStrict(node, &raw); err != nil { + return nil, err + } + + inputSettler, err := parse.NonZeroAddress(raw.InputSettler, "inputSettler") + if err != nil { + return nil, err + } + outputSettler, err := parse.NonZeroAddress(raw.OutputSettler, "outputSettler") + if err != nil { + return nil, err + } + executor, err := parse.NonZeroAddress(raw.Executor, "executor") + if err != nil { + return nil, err + } + adapters, err := parseAdapters(raw.Adapters) + if err != nil { + return nil, err + } + tokenPolicy, err := tokenpolicy.Parse(raw.TokensToQuote, raw.PermissionedTokens) + if err != nil { + return nil, err + } + httpTimeout, err := parse.Duration(raw.OrderServer.HTTPTimeout, defaultHTTPTimeout, "orderServer.httpTimeout") + if err != nil { + return nil, err + } + quoteRefreshMode := parse.OrDefault(raw.QuoteRefreshMode, defaultQuoteRefreshMode) + if quoteRefreshMode != quoteRefreshModeInterval && quoteRefreshMode != quoteRefreshModeBlock { + return nil, errors.Errorf("quoteRefreshMode: must be %q or %q, got %q", + quoteRefreshModeInterval, quoteRefreshModeBlock, quoteRefreshMode) + } + quoteInterval, err := parseQuoteInterval(raw.QuoteIntervalMs, quoteRefreshMode) + if err != nil { + return nil, err + } + quoteTTL, err := parse.Duration(raw.QuoteTTL, defaultQuoteTTL, "quoteTtl") + if err != nil { + return nil, err + } + if quoteTTL/2 < quoteInterval { + return nil, errors.Errorf("quoteTtl must be at least twice quote interval %s, got %s", quoteInterval, quoteTTL) + } + apiKeyEnv := raw.OrderServer.APIKeyEnv + if apiKeyEnv == "" { + return nil, errors.New("orderServer.apiKeyEnv is required") + } + if raw.OrderServer.BaseURL == "" { + return nil, errors.New("orderServer.baseUrl is required") + } + if raw.OrderServer.WSURL == "" { + return nil, errors.New("orderServer.wsUrl is required") + } + solverMode := parse.OrDefault(raw.SolverMode, defaultSolverMode) + if solverMode != solverModeExternal && solverMode != solverModeInternal { + return nil, errors.Errorf("solverMode: must be %q or %q, got %q", solverModeExternal, solverModeInternal, solverMode) + } + if solverMode == solverModeInternal && raw.DiscountsURL == "" { + return nil, errors.New("privateDiscountsUrl is required in internal solverMode") + } + if solverMode == solverModeExternal && raw.DiscountsURL != "" { + return nil, errors.New("privateDiscountsUrl requires internal solverMode") + } + gas, err := parseGasConfig(raw.Gas) + if err != nil { + return nil, err + } + return &Config{ + OrderServer: OrderServerConfig{ + BaseURL: raw.OrderServer.BaseURL, + WSURL: raw.OrderServer.WSURL, + APIKeyEnv: apiKeyEnv, + HTTPTimeout: httpTimeout, + }, + InputSettler: inputSettler, + OutputSettler: outputSettler, + Executor: executor, + Adapters: adapters, + TokenPolicy: tokenPolicy, + QuoteInterval: quoteInterval, + QuoteTTL: quoteTTL, + QuoteRefreshMode: quoteRefreshMode, + SolverMode: solverMode, + DiscountsURL: raw.DiscountsURL, + Gas: gas, + Strategy: StrategyConfig{ + Name: parse.OrDefault(raw.Strategy.Name, defaultStrategyName), + Config: raw.Strategy.Config, + }, + }, nil +} + +func parseGasConfig(raw rawGasConfig) (liquidlanegas.OracleConfig, error) { + nativeFeed, err := parse.NonZeroAddress(raw.NativeUSDFeed, "gas.nativeUsdFeed") + if err != nil { + return liquidlanegas.OracleConfig{}, err + } + nativeMaxAge, err := parse.Duration(raw.NativeMaxAge, 0, "gas.nativeMaxAge") + if err != nil { + return liquidlanegas.OracleConfig{}, err + } + if nativeMaxAge <= 0 { + return liquidlanegas.OracleConfig{}, errors.New("gas.nativeMaxAge is required") + } + feeds := make(map[common.Address]liquidlanegas.USDFeed, len(raw.TokenUSDFeeds)) + for i, item := range raw.TokenUSDFeeds { + field := "gas.tokenUsdFeeds[" + strconv.Itoa(i) + "]" + token, tokenErr := parse.NonZeroAddress(item.Token, field+".token") + if tokenErr != nil { + return liquidlanegas.OracleConfig{}, tokenErr + } + feed, feedErr := parse.NonZeroAddress(item.Feed, field+".feed") + if feedErr != nil { + return liquidlanegas.OracleConfig{}, feedErr + } + maxAge, ageErr := parse.Duration(item.MaxAge, 0, field+".maxAge") + if ageErr != nil { + return liquidlanegas.OracleConfig{}, ageErr + } + if maxAge <= 0 { + return liquidlanegas.OracleConfig{}, errors.Errorf("%s.maxAge is required", field) + } + if _, duplicate := feeds[token]; duplicate { + return liquidlanegas.OracleConfig{}, errors.Errorf("%s.token: duplicate token %s", field, token.Hex()) + } + feeds[token] = liquidlanegas.USDFeed{Address: feed, MaxAge: maxAge} + } + if len(feeds) == 0 { + return liquidlanegas.OracleConfig{}, errors.New("gas.tokenUsdFeeds must contain at least one token feed") + } + return liquidlanegas.OracleConfig{ + NativeUSDFeed: liquidlanegas.USDFeed{Address: nativeFeed, MaxAge: nativeMaxAge}, + TokenUSDFeeds: feeds, + }, nil +} + +func (c *Config) usesDiscounts() bool { return c.SolverMode == solverModeInternal } + +func parseQuoteInterval(ms int, mode string) (time.Duration, error) { + if ms == 0 { + if mode == quoteRefreshModeBlock { + return defaultBlockPollInterval, nil + } + return defaultQuoteInterval, nil + } + if ms < 0 { + return 0, errors.Errorf("quoteIntervalMs: must be positive, got %d", ms) + } + return time.Duration(ms) * time.Millisecond, nil +} + +func parseAdapters(raw []string) ([]common.Address, error) { + if len(raw) == 0 { + return nil, errors.New("at least one adapters entry is required") + } + out := make([]common.Address, 0, len(raw)) + seen := make(map[common.Address]bool, len(raw)) + for i, a := range raw { + addr, err := parse.NonZeroAddress(a, "adapters["+strconv.Itoa(i)+"]") + if err != nil { + return nil, err + } + if seen[addr] { + return nil, errors.Errorf("adapters[%d]: duplicate adapter %s", i, addr.Hex()) + } + seen[addr] = true + out = append(out, addr) + } + return out, nil +} diff --git a/internal/solvers/lifi/config_test.go b/internal/solvers/lifi/config_test.go new file mode 100644 index 00000000..326a39ab --- /dev/null +++ b/internal/solvers/lifi/config_test.go @@ -0,0 +1,367 @@ +package lifi + +import ( + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-logr/logr" + "github.com/symbioticfi/vault-solver/internal/tokenpolicy" + "gopkg.in/yaml.v3" +) + +func TestParseConfigValid(t *testing.T) { + cfg := parseConfigYAML(t, ` +orderServer: + baseUrl: https://order.example + wsUrl: wss://order.example + apiKeyEnv: LIFI_SOLVER_API_KEY +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" +tokensToQuote: permissioned +permissionedTokens: + - "0x6666666666666666666666666666666666666666" +gas: + nativeUsdFeed: "0x7777777777777777777777777777777777777777" + nativeMaxAge: 30m + tokenUsdFeeds: + - token: "0x6666666666666666666666666666666666666666" + feed: "0x8888888888888888888888888888888888888888" + maxAge: 1h +strategy: + name: default +quoteIntervalMs: 45000 +quoteTtl: 90s +quoteRefreshMode: block +`) + + if cfg.OrderServer.BaseURL != "https://order.example" { + t.Fatalf("baseURL = %q", cfg.OrderServer.BaseURL) + } + if cfg.OrderServer.WSURL != "wss://order.example" { + t.Fatalf("wsURL = %q", cfg.OrderServer.WSURL) + } + if cfg.OrderServer.HTTPTimeout != defaultHTTPTimeout { + t.Fatalf("httpTimeout = %s", cfg.OrderServer.HTTPTimeout) + } + if cfg.QuoteInterval != 45*time.Second { + t.Fatalf("quoteInterval = %s", cfg.QuoteInterval) + } + if cfg.QuoteTTL != 90*time.Second { + t.Fatalf("quoteTTL = %s", cfg.QuoteTTL) + } + if cfg.QuoteRefreshMode != quoteRefreshModeBlock { + t.Fatalf("quoteRefreshMode = %q", cfg.QuoteRefreshMode) + } + if cfg.Strategy.Name != "default" { + t.Fatalf("strategy = %q", cfg.Strategy.Name) + } + permissioned := common.HexToAddress("0x6666666666666666666666666666666666666666") + if cfg.Gas.NativeUSDFeed.MaxAge != 30*time.Minute || + cfg.Gas.TokenUSDFeeds[permissioned].MaxAge != time.Hour { + t.Fatalf("gas oracle config = %+v", cfg.Gas) + } + if cfg.SolverMode != solverModeExternal || cfg.usesDiscounts() { + t.Fatalf("solver mode = %q discounts=%v", cfg.SolverMode, cfg.usesDiscounts()) + } + if !cfg.TokenPolicy.RequiresSingleRoute(permissioned) { + t.Fatalf("token scope = %q", cfg.TokenPolicy.Scope()) + } + if _, err := newStrategy(cfg.Strategy, nil, logr.Discard()); err != nil { + t.Fatalf("newStrategy: %v", err) + } +} + +func TestParseConfigRejectsLegacySolverAddress(t *testing.T) { + _, err := parseConfig(parseYAMLNode(t, `solverAddress: "0x1111111111111111111111111111111111111111"`)) + if err == nil || !strings.Contains(err.Error(), "solverAddress") { + t.Fatalf("err = %v", err) + } +} + +func TestParseConfigTokenScope(t *testing.T) { + const base = ` +orderServer: + baseUrl: https://order.example + wsUrl: wss://order.example + apiKeyEnv: LIFI_SOLVER_API_KEY +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" +permissionedTokens: + - "0x6666666666666666666666666666666666666666" +gas: + nativeUsdFeed: "0x7777777777777777777777777777777777777777" + nativeMaxAge: 30m + tokenUsdFeeds: + - token: "0x6666666666666666666666666666666666666666" + feed: "0x8888888888888888888888888888888888888888" + maxAge: 1h +` + permissioned := common.HexToAddress("0x6666666666666666666666666666666666666666") + all := parseConfigYAML(t, base) + if all.TokenPolicy.Scope() != tokenpolicy.All || all.TokenPolicy.RequiresSingleRoute(permissioned) { + t.Fatalf("default policy = %q", all.TokenPolicy.Scope()) + } + permissionedOnly := parseConfigYAML(t, base+"tokensToQuote: permissioned\n") + if !permissionedOnly.TokenPolicy.Allows(permissioned) || + !permissionedOnly.TokenPolicy.RequiresSingleRoute(permissioned) { + t.Fatal("permissioned scope did not admit and constrain configured token") + } + if _, err := parseConfig(parseYAMLNode(t, base+"tokensToQuote: bogus\n")); err == nil { + t.Fatal("expected invalid tokensToQuote error") + } +} + +func TestParseConfigEnablesPrivateDiscountsOnlyInInternalMode(t *testing.T) { + base := ` +orderServer: + baseUrl: https://order.example + wsUrl: wss://order.example + apiKeyEnv: LIFI_SOLVER_API_KEY +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" +gas: + nativeUsdFeed: "0x7777777777777777777777777777777777777777" + nativeMaxAge: 30m + tokenUsdFeeds: + - token: "0x6666666666666666666666666666666666666666" + feed: "0x8888888888888888888888888888888888888888" + maxAge: 1h +` + cfg := parseConfigYAML(t, base+"solverMode: internal\nprivateDiscountsUrl: https://rfq.example\n") + if !cfg.usesDiscounts() || cfg.DiscountsURL != "https://rfq.example" { + t.Fatalf("config = %+v", cfg) + } + + for _, raw := range []string{ + base + "solverMode: internal\n", + base + "privateDiscountsUrl: https://rfq.example\n", + } { + if _, err := parseConfig(parseYAMLNode(t, raw)); err == nil { + t.Fatalf("expected mode/url validation error for %q", raw) + } + } +} + +func TestParseConfigRejectsMissingAPIKeyEnv(t *testing.T) { + _, err := parseConfig(parseYAMLNode(t, ` +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" +`)) + if err == nil || !strings.Contains(err.Error(), "orderServer.apiKeyEnv is required") { + t.Fatalf("err = %v", err) + } +} + +func TestParseConfigRequiresOrderServerEndpoints(t *testing.T) { + const base = ` +orderServer: + apiKeyEnv: LIFI_SOLVER_API_KEY +%s +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" +` + for _, tc := range []struct { + name string + endpoint string + want string + }{ + {name: "base URL", endpoint: " wsUrl: wss://order.example", want: "orderServer.baseUrl is required"}, + {name: "websocket URL", endpoint: " baseUrl: https://order.example", want: "orderServer.wsUrl is required"}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := parseConfig(parseYAMLNode(t, strings.Replace(base, "%s", tc.endpoint, 1))) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("err = %v", err) + } + }) + } +} + +func TestParseConfigRequiresGasOracleFeeds(t *testing.T) { + _, err := parseConfig(parseYAMLNode(t, ` +orderServer: + baseUrl: https://order.example + wsUrl: wss://order.example + apiKeyEnv: LIFI_SOLVER_API_KEY +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" +`)) + if err == nil || !strings.Contains(err.Error(), "gas.nativeUsdFeed") { + t.Fatalf("err = %v", err) + } +} + +func TestParseGasConfigRequiresPerFeedMaxAge(t *testing.T) { + const address = "0x7777777777777777777777777777777777777777" + for _, tc := range []struct { + name string + raw rawGasConfig + want string + }{ + { + name: "native", + raw: rawGasConfig{ + NativeUSDFeed: address, + TokenUSDFeeds: []rawTokenUSDFeed{{Token: address, Feed: address, MaxAge: "1h"}}, + }, + want: "gas.nativeMaxAge is required", + }, + { + name: "token", + raw: rawGasConfig{ + NativeUSDFeed: address, + NativeMaxAge: "1h", + TokenUSDFeeds: []rawTokenUSDFeed{{Token: address, Feed: address}}, + }, + want: "gas.tokenUsdFeeds[0].maxAge is required", + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := parseGasConfig(tc.raw) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("err = %v", err) + } + }) + } +} + +func TestParseConfigDefaultsToBlockPollingAndShortTTL(t *testing.T) { + cfg := parseConfigYAML(t, ` +orderServer: + baseUrl: https://order.example + wsUrl: wss://order.example + apiKeyEnv: LIFI_SOLVER_API_KEY +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" +gas: + nativeUsdFeed: "0x7777777777777777777777777777777777777777" + nativeMaxAge: 30m + tokenUsdFeeds: + - token: "0x6666666666666666666666666666666666666666" + feed: "0x8888888888888888888888888888888888888888" + maxAge: 1h +`) + if cfg.QuoteInterval != time.Second { + t.Fatalf("quoteInterval = %s", cfg.QuoteInterval) + } + if cfg.QuoteRefreshMode != quoteRefreshModeBlock { + t.Fatalf("quoteRefreshMode = %q", cfg.QuoteRefreshMode) + } + if cfg.QuoteTTL != 36*time.Second { + t.Fatalf("quoteTTL = %s", cfg.QuoteTTL) + } +} + +func TestParseConfigRejectsQuoteTTLBelowTwiceRefreshInterval(t *testing.T) { + _, err := parseConfig(parseYAMLNode(t, ` +orderServer: + baseUrl: https://order.example + wsUrl: wss://order.example + apiKeyEnv: LIFI_SOLVER_API_KEY +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" +quoteIntervalMs: 30000 +quoteTtl: 30s +`)) + if err == nil || !strings.Contains(err.Error(), "quoteTtl must be at least twice quote interval") { + t.Fatalf("err = %v", err) + } +} + +func TestParseConfigRejectsDuplicateAdapters(t *testing.T) { + _, err := parseConfig(parseYAMLNode(t, ` +orderServer: + baseUrl: https://order.example + wsUrl: wss://order.example + apiKeyEnv: LIFI_SOLVER_API_KEY +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" + - "0x5555555555555555555555555555555555555555" +`)) + if err == nil || !strings.Contains(err.Error(), "duplicate adapter") { + t.Fatalf("err = %v", err) + } +} + +func TestParseConfigRejectsInvalidPermissionedTokens(t *testing.T) { + base := ` +orderServer: + baseUrl: https://order.example + wsUrl: wss://order.example + apiKeyEnv: LIFI_SOLVER_API_KEY +inputSettler: "0x2222222222222222222222222222222222222222" +outputSettler: "0x3333333333333333333333333333333333333333" +executor: "0x4444444444444444444444444444444444444444" +adapters: + - "0x5555555555555555555555555555555555555555" +permissionedTokens: +` + for _, entries := range []string{ + ` - "0x0000000000000000000000000000000000000000"`, + ` - "0x6666666666666666666666666666666666666666" + - "0x6666666666666666666666666666666666666666"`, + } { + if _, err := parseConfig(parseYAMLNode(t, base+entries+"\n")); err == nil { + t.Fatalf("expected permissionedTokens validation error for:\n%s", entries) + } + } +} + +func parseConfigYAML(t *testing.T, raw string) *Config { + t.Helper() + cfg, err := parseConfig(parseYAMLNode(t, raw)) + if err != nil { + t.Fatalf("parseConfig: %v", err) + } + return cfg +} + +func parseYAMLNode(t *testing.T, raw string) yaml.Node { + t.Helper() + var node yaml.Node + if err := yaml.Unmarshal([]byte(raw), &node); err != nil { + t.Fatalf("yaml: %v", err) + } + if len(node.Content) != 1 { + t.Fatalf("unexpected yaml document content len %d", len(node.Content)) + } + return *node.Content[0] +} + +func testTokenPolicy(t *testing.T, scope tokenpolicy.Scope, tokens ...common.Address) tokenpolicy.Policy { + t.Helper() + policy, err := tokenpolicy.New(scope, tokens) + if err != nil { + t.Fatalf("tokenpolicy.New: %v", err) + } + return policy +} diff --git a/internal/solvers/lifi/discounts.go b/internal/solvers/lifi/discounts.go new file mode 100644 index 00000000..71b6d092 --- /dev/null +++ b/internal/solvers/lifi/discounts.go @@ -0,0 +1,290 @@ +package lifi + +import ( + "context" + "math/big" + "sort" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + "golang.org/x/sync/errgroup" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/liquidlane/discounts" +) + +const ( + maxPrivateDiscountsPerFill = 16 + maxConcurrentResolutions = 4 +) + +type discountClient interface { + ListDiscounts(ctx context.Context) (*discounts.List, error) + Resolve(ctx context.Context, discountID string) (*discounts.Resolved, error) +} + +type discountRouteKey struct { + adapter common.Address + tokenIn common.Address + tokenOut common.Address +} + +func (s *Solver) quoteDiscountInventories( + ctx context.Context, + bases []liquidlane.Inventory, + now time.Time, +) []liquidlane.Inventory { + if s.discounts == nil { + return nil + } + listed, err := s.discounts.ListDiscounts(ctx) + if err != nil { + s.log.Error(err, "private discounts: list for quote") + return nil + } + return matchingDiscountInventories(listed, bases, now, s.logInvalidDiscount) +} + +func (s *Solver) fillDiscountQuotes( + ctx context.Context, + bases []liquidlane.FillQuote, + now time.Time, +) ([]liquidlane.FillQuote, map[common.Hash]*discounts.Signed) { + if s.discounts == nil || len(bases) == 0 { + return nil, nil + } + inventory := make([]liquidlane.Inventory, 0, len(bases)) + baseByRoute := make(map[liquidlane.RouteID]liquidlane.FillQuote, len(bases)) + for _, quote := range bases { + inventory = append(inventory, quote.Inventory) + baseByRoute[quote.ID] = quote + } + listed, err := s.discounts.ListDiscounts(ctx) + if err != nil { + s.log.Error(err, "private discounts: list for fill") + return nil, nil + } + candidates := matchingDiscountInventories(listed, inventory, now, s.logInvalidDiscount) + sort.Slice(candidates, func(i, j int) bool { + if cmp := candidates[i].MaxRate.Cmp(candidates[j].MaxRate); cmp != 0 { + return cmp > 0 + } + return candidates[i].DiscountID.Hex() < candidates[j].DiscountID.Hex() + }) + if len(candidates) > maxPrivateDiscountsPerFill { + candidates = candidates[:maxPrivateDiscountsPerFill] + } + + type resolution struct { + quote *liquidlane.FillQuote + signed *discounts.Signed + } + resolutions := make([]resolution, len(candidates)) + g, resolveCtx := errgroup.WithContext(ctx) + g.SetLimit(maxConcurrentResolutions) + for i, candidate := range candidates { + g.Go(func() error { + if candidate.DiscountID == nil { + return nil + } + resolved, resolveErr := s.discounts.Resolve(resolveCtx, candidate.DiscountID.Hex()) + if resolveErr != nil { + s.log.Error(resolveErr, "private discounts: resolve", "discountId", candidate.DiscountID.Hex()) + return nil + } + signed, parseErr := discounts.ParseSigned(resolved) + if parseErr != nil { + s.logInvalidDiscount(candidate.DiscountID.Hex(), parseErr) + return nil + } + baseQuote, ok := baseByRoute[candidate.ID] + if !ok { + return nil + } + if validateErr := validateResolvedDiscount(candidate, baseQuote, signed, now); validateErr != nil { + s.logInvalidDiscount(candidate.DiscountID.Hex(), validateErr) + return nil + } + maxAmountOut := liquidlane.AmountOutAfterDiscount(baseQuote.GrossAmountOut, signed.Terms.Discount) + if maxAmountOut.Sign() <= 0 { + return nil + } + candidate.ValidUntil = resolvedDiscountValidUntil(signed) + quote := &liquidlane.FillQuote{ + Inventory: candidate, + AmountIn: liquidlane.CloneBig(baseQuote.AmountIn), + GrossAmountOut: liquidlane.CloneBig(baseQuote.GrossAmountOut), + MaxAmountOut: maxAmountOut, + MinDiscount: liquidlane.CloneBig(baseQuote.MinDiscount), + } + resolutions[i] = resolution{quote: quote, signed: signed} + return nil + }) + } + _ = g.Wait() + quotes := make([]liquidlane.FillQuote, 0, len(candidates)) + resolvedByID := make(map[common.Hash]*discounts.Signed, len(candidates)) + for _, resolution := range resolutions { + if resolution.quote == nil || resolution.signed == nil { + continue + } + quotes = append(quotes, *resolution.quote) + resolvedByID[resolution.signed.DiscountID] = resolution.signed + } + return quotes, resolvedByID +} + +func matchingDiscountInventories( + listed *discounts.List, + bases []liquidlane.Inventory, + now time.Time, + invalid func(string, error), +) []liquidlane.Inventory { + if listed == nil { + return nil + } + byRoute := make(map[discountRouteKey]liquidlane.Inventory, len(bases)) + for _, item := range bases { + byRoute[discountRouteKey{adapter: item.Adapter, tokenIn: item.TokenIn, tokenOut: item.TokenOut}] = item + } + seen := make(map[common.Hash]bool) + out := make([]liquidlane.Inventory, 0, len(listed.Discounts)) + for _, item := range listed.Discounts { + offer, err := discounts.ParseOffer(item) + if err != nil { + if invalid != nil { + invalid(item.DiscountID, err) + } + continue + } + if seen[offer.DiscountID] || offer.Deadline <= now.Unix() { + continue + } + base, ok := byRoute[discountRouteKey{ + adapter: offer.Adapter, tokenIn: offer.TokenToRedeem, tokenOut: offer.Collateral, + }] + if !ok || offer.CollateralDecimals != base.TokenOutDecimals { + continue + } + if base.MaxRate == nil || base.MaxRate.Sign() <= 0 || offer.MaxRate.Cmp(base.MaxRate) > 0 { + if invalid != nil { + invalid(item.DiscountID, errors.New("advertised discount rate exceeds current adapter max rate")) + } + continue + } + maxAssets := new(big.Int).Set(offer.MaxAssets) + if base.MaxAssets == nil || base.MaxAssets.Sign() <= 0 { + continue + } + if maxAssets.Cmp(base.MaxAssets) > 0 { + maxAssets.Set(base.MaxAssets) + } + seen[offer.DiscountID] = true + out = append(out, liquidlane.DiscountInventory( + base.Route, + maxAssets, + offer.MaxRate, + offer.DiscountID, + time.Unix(offer.Deadline, 0), + )) + } + return out +} + +func validateResolvedDiscount( + candidate liquidlane.Inventory, + base liquidlane.FillQuote, + signed *discounts.Signed, + now time.Time, +) error { + if candidate.DiscountID == nil || signed.DiscountID != *candidate.DiscountID { + return errors.New("resolved discount id does not match advertised candidate") + } + if signed.Adapter != candidate.Adapter { + return errors.New("resolved discount adapter does not match advertised candidate") + } + if signed.Terms.TokenToRedeem != candidate.TokenIn { + return errors.New("resolved discount token does not match route") + } + if base.GrossAmountOut == nil || base.GrossAmountOut.Sign() <= 0 || + base.MinDiscount == nil || base.MinDiscount.Sign() < 0 { + return errors.New("fill base is missing discount facts") + } + if signed.Terms.Discount.Sign() < 0 || + signed.Terms.Discount.Cmp(big.NewInt(liquidlane.DiscountPrecision)) > 0 || + signed.Terms.Discount.Cmp(base.MinDiscount) < 0 { + return errors.New("resolved discount is outside adapter bounds") + } + nowUnix := big.NewInt(now.Unix()) + if signed.Terms.Deadline.Cmp(nowUnix) <= 0 || signed.ProtocolDeadline.Cmp(nowUnix) <= 0 { + return errors.New("resolved discount is expired") + } + return nil +} + +func refreshResolvedDiscountQuotes( + candidates []liquidlane.FillQuote, + resolved map[common.Hash]*discounts.Signed, + bases []liquidlane.FillQuote, + now time.Time, + invalid func(string, error), +) []liquidlane.FillQuote { + baseByRoute := make(map[liquidlane.RouteID]liquidlane.FillQuote, len(bases)) + for _, base := range bases { + baseByRoute[base.ID] = base + } + out := make([]liquidlane.FillQuote, 0, len(candidates)) + for _, candidate := range candidates { + if candidate.DiscountID == nil { + continue + } + signed := resolved[*candidate.DiscountID] + base, ok := baseByRoute[candidate.ID] + if signed == nil || !ok { + continue + } + if candidate.MaxRate == nil || base.MaxRate == nil || candidate.MaxRate.Cmp(base.MaxRate) > 0 { + if invalid != nil { + invalid(candidate.DiscountID.Hex(), errors.New("resolved discount rate exceeds refreshed adapter max rate")) + } + continue + } + candidate.MaxAssets = liquidlane.CloneBig(candidate.MaxAssets) + if candidate.MaxAssets == nil || base.MaxAssets == nil || base.MaxAssets.Sign() <= 0 { + continue + } + if candidate.MaxAssets.Cmp(base.MaxAssets) > 0 { + candidate.MaxAssets.Set(base.MaxAssets) + } + if err := validateResolvedDiscount(candidate.Inventory, base, signed, now); err != nil { + if invalid != nil { + invalid(candidate.DiscountID.Hex(), err) + } + continue + } + maxAmountOut := liquidlane.AmountOutAfterDiscount(base.GrossAmountOut, signed.Terms.Discount) + if maxAmountOut.Sign() <= 0 { + continue + } + candidate.AmountIn = liquidlane.CloneBig(base.AmountIn) + candidate.GrossAmountOut = liquidlane.CloneBig(base.GrossAmountOut) + candidate.MaxAmountOut = maxAmountOut + candidate.MinDiscount = liquidlane.CloneBig(base.MinDiscount) + candidate.ValidUntil = resolvedDiscountValidUntil(signed) + out = append(out, candidate) + } + return out +} + +func resolvedDiscountValidUntil(signed *discounts.Signed) time.Time { + deadline := signed.Terms.Deadline + if signed.ProtocolDeadline.Cmp(deadline) < 0 { + deadline = signed.ProtocolDeadline + } + return time.Unix(deadline.Int64(), 0) +} + +func (s *Solver) logInvalidDiscount(discountID string, err error) { + s.log.V(1).Info("private discounts: ignored", "discountId", discountID, "error", err.Error()) +} diff --git a/internal/solvers/lifi/discounts_test.go b/internal/solvers/lifi/discounts_test.go new file mode 100644 index 00000000..c5a115d7 --- /dev/null +++ b/internal/solvers/lifi/discounts_test.go @@ -0,0 +1,233 @@ +package lifi + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-logr/logr" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/liquidlane/discounts" +) + +const testDiscountID = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + +type fakeDiscountClient struct { + listed *discounts.List + resolved *discounts.Resolved + listCalls int + resolveCalls int +} + +func (f *fakeDiscountClient) ListDiscounts(context.Context) (*discounts.List, error) { + f.listCalls++ + return f.listed, nil +} + +func (f *fakeDiscountClient) Resolve(context.Context, string) (*discounts.Resolved, error) { + f.resolveCalls++ + return f.resolved, nil +} + +func TestMatchingDiscountInventoriesScopesAndCapsOffers(t *testing.T) { + now := time.Unix(1_800_000_000, 0) + direct := testDirectDiscountInventory() + listed := &discounts.List{Discounts: []discounts.ListItem{ + testDiscountListItem(direct, 2_000, now.Add(time.Minute)), + { + DiscountID: testDiscountID, + Adapter: common.HexToAddress("0xdead").Hex(), TokenToRedeem: direct.TokenIn.Hex(), + Collateral: direct.TokenOut.Hex(), CollateralDecimals: direct.TokenOutDecimals, + Deadline: now.Add(time.Minute).Unix(), MaxRate: "2000000000000000000", MaxAssets: "2000", + }, + }} + + got := matchingDiscountInventories(listed, []liquidlane.Inventory{direct}, now, func(string, error) {}) + if len(got) != 1 { + t.Fatalf("inventories = %+v", got) + } + if got[0].MaxAssets.String() != "1000" || got[0].MaxRate.Cmp(direct.MaxRate) != 0 { + t.Fatalf("capped inventory = %+v", got[0]) + } + if got[0].DiscountID == nil || got[0].DiscountID.Hex() != testDiscountID { + t.Fatalf("discount id = %v", got[0].DiscountID) + } + if !got[0].ValidUntil.Equal(now.Add(time.Minute)) { + t.Fatalf("valid until = %s", got[0].ValidUntil) + } +} + +func TestMatchingDiscountInventoriesUsesAdvertisedNetRate(t *testing.T) { + now := time.Unix(1_800_000_000, 0) + direct := testDirectDiscountInventory() + netRate := big.NewInt(800_000_000_000_000_000) + item := testDiscountListItem(direct, 1_000, now.Add(time.Minute)) + item.Discount = "200000" + item.MaxRate = netRate.String() + + got := matchingDiscountInventories( + &discounts.List{Discounts: []discounts.ListItem{item}}, + []liquidlane.Inventory{direct}, + now, + nil, + ) + if len(got) != 1 || got[0].MaxRate.Cmp(netRate) != 0 { + t.Fatalf("discount rate = %+v, want backend net rate %s", got, netRate) + } +} + +func TestFillDiscountQuotesUsesFreshSignedTerms(t *testing.T) { + now := time.Unix(1_800_000_000, 0) + directInventory := testDirectDiscountInventory() + direct := liquidlane.FillQuote{ + Inventory: directInventory, AmountIn: big.NewInt(1_000), + GrossAmountOut: big.NewInt(1_000), MaxAmountOut: big.NewInt(900), MinDiscount: big.NewInt(100_000), + } + fake := &fakeDiscountClient{ + listed: &discounts.List{Discounts: []discounts.ListItem{ + testDiscountListItem(directInventory, 1_000, now.Add(time.Minute)), + }}, + resolved: testResolvedDiscount(directInventory, 100_000, now.Add(time.Minute)), + } + s := &Solver{discounts: fake, log: logr.Discard()} + + quotes, resolved := s.fillDiscountQuotes(context.Background(), []liquidlane.FillQuote{direct}, now) + if len(quotes) != 1 || quotes[0].MaxAmountOut.String() != "900" || + !quotes[0].ValidUntil.Equal(now.Add(time.Minute)) { + t.Fatalf("quotes = %+v", quotes) + } + id := common.HexToHash(testDiscountID) + if resolved[id] == nil || fake.listCalls != 1 || fake.resolveCalls != 1 { + t.Fatalf("resolved = %+v calls=%d/%d", resolved, fake.listCalls, fake.resolveCalls) + } +} + +func TestFillDiscountQuotesRejectsResolvedTermsBelowAdapterMinimum(t *testing.T) { + now := time.Unix(1_800_000_000, 0) + directInventory := testDirectDiscountInventory() + fake := &fakeDiscountClient{ + listed: &discounts.List{Discounts: []discounts.ListItem{ + testDiscountListItem(directInventory, 1_000, now.Add(time.Minute)), + }}, + resolved: testResolvedDiscount(directInventory, 50_000, now.Add(time.Minute)), + } + s := &Solver{discounts: fake, log: logr.Discard()} + direct := liquidlane.FillQuote{ + Inventory: directInventory, AmountIn: big.NewInt(1_000), + GrossAmountOut: big.NewInt(1_000), MaxAmountOut: big.NewInt(900), MinDiscount: big.NewInt(100_000), + } + + quotes, resolved := s.fillDiscountQuotes(context.Background(), []liquidlane.FillQuote{direct}, now) + if len(quotes) != 0 || len(resolved) != 0 { + t.Fatalf("unsafe discount survived: quotes=%+v resolved=%+v", quotes, resolved) + } +} + +func TestRefreshResolvedDiscountQuotesUsesFreshAdapterState(t *testing.T) { + now := time.Unix(1_800_000_000, 0) + direct := testDirectDiscountInventory() + id := common.HexToHash(testDiscountID) + candidate := liquidlane.FillQuote{ + Inventory: liquidlane.DiscountInventory( + direct.Route, big.NewInt(1_000), direct.MaxRate, id, now.Add(time.Minute), + ), + AmountIn: big.NewInt(1_000), GrossAmountOut: big.NewInt(1_000), MaxAmountOut: big.NewInt(900), + MinDiscount: big.NewInt(100_000), + } + fresh := liquidlane.FillQuote{ + Inventory: direct, AmountIn: big.NewInt(800), GrossAmountOut: big.NewInt(880), + MaxAmountOut: big.NewInt(792), MinDiscount: big.NewInt(100_000), + } + fresh.MaxAssets = big.NewInt(700) + signed, err := discounts.ParseSigned(testResolvedDiscount(direct, 100_000, now.Add(time.Minute))) + if err != nil { + t.Fatalf("ParseSigned: %v", err) + } + + got := refreshResolvedDiscountQuotes( + []liquidlane.FillQuote{candidate}, map[common.Hash]*discounts.Signed{id: signed}, + []liquidlane.FillQuote{fresh}, now, nil, + ) + if len(got) != 1 { + t.Fatalf("quotes = %+v", got) + } + if got[0].AmountIn.String() != "800" || got[0].GrossAmountOut.String() != "880" || + got[0].MaxAmountOut.String() != "792" || got[0].MaxAssets.String() != "700" { + t.Fatalf("refreshed quote = %+v", got[0]) + } +} + +func TestRefreshResolvedDiscountQuotesRejectsRateAboveFreshAdapterLimit(t *testing.T) { + now := time.Unix(1_800_000_000, 0) + direct := testDirectDiscountInventory() + id := common.HexToHash(testDiscountID) + candidate := liquidlane.FillQuote{ + Inventory: liquidlane.DiscountInventory( + direct.Route, big.NewInt(1_000), direct.MaxRate, id, now.Add(time.Minute), + ), + AmountIn: big.NewInt(1_000), GrossAmountOut: big.NewInt(1_000), MaxAmountOut: big.NewInt(900), + MinDiscount: big.NewInt(100_000), + } + fresh := candidate + fresh.Inventory = direct + fresh.MaxRate = big.NewInt(800_000_000_000_000_000) + signed, err := discounts.ParseSigned(testResolvedDiscount(direct, 100_000, now.Add(time.Minute))) + if err != nil { + t.Fatalf("ParseSigned: %v", err) + } + + got := refreshResolvedDiscountQuotes( + []liquidlane.FillQuote{candidate}, map[common.Hash]*discounts.Signed{id: signed}, + []liquidlane.FillQuote{fresh}, now, nil, + ) + if len(got) != 0 { + t.Fatalf("unsafe quote survived: %+v", got) + } +} + +func testDirectDiscountInventory() liquidlane.Inventory { + routeItem := liquidlane.NewRoute( + 11155111, + common.HexToAddress("0x1111111111111111111111111111111111111111"), + common.HexToAddress("0x2222222222222222222222222222222222222222"), + common.HexToAddress("0x3333333333333333333333333333333333333333"), + common.HexToAddress("0x4444444444444444444444444444444444444444"), + 6, + 6, + ) + return liquidlane.DirectInventory(routeItem, big.NewInt(1_000), big.NewInt(900_000_000_000_000_000)) +} + +func testDiscountListItem( + direct liquidlane.Inventory, + maxAssets int64, + deadline time.Time, +) discounts.ListItem { + return discounts.ListItem{ + DiscountID: testDiscountID, + Adapter: direct.Adapter.Hex(), TokenToRedeem: direct.TokenIn.Hex(), Collateral: direct.TokenOut.Hex(), + CollateralDecimals: direct.TokenOutDecimals, Deadline: deadline.Unix(), + Discount: "100000", + MaxRate: direct.MaxRate.String(), MaxAssets: big.NewInt(maxAssets).String(), + } +} + +func testResolvedDiscount( + direct liquidlane.Inventory, + discount int64, + deadline time.Time, +) *discounts.Resolved { + return &discounts.Resolved{ + DiscountID: testDiscountID, + Discount: discounts.Terms{ + Adapter: direct.Adapter.Hex(), TokenToRedeem: direct.TokenIn.Hex(), Discount: big.NewInt(discount).String(), + Signer: common.HexToAddress("0x5555555555555555555555555555555555555555").Hex(), + Protocol: common.HexToAddress("0x6666666666666666666666666666666666666666").Hex(), + Nonce: "0x1", Deadline: deadline.Unix(), + }, + SignerSignature: "0x1234", ProtocolDeadline: deadline.Unix(), ProtocolSignature: "0x5678", + } +} diff --git a/internal/solvers/lifi/execution.go b/internal/solvers/lifi/execution.go new file mode 100644 index 00000000..33e1f524 --- /dev/null +++ b/internal/solvers/lifi/execution.go @@ -0,0 +1,203 @@ +package lifi + +import ( + "context" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "golang.org/x/sync/errgroup" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/txmanager" +) + +const fillCompletionCapacity = 128 + +type pendingFill struct { + order *submittedOrder + orderID common.Hash + reservationKey string + reservations []quoteReservation + result <-chan txmanager.Result +} + +type fillCompletion struct { + fill *pendingFill + result txmanager.Result +} + +type pendingFillState struct { + byOrder map[string]*pendingFill +} + +// orderInbox keeps the WebSocket reader independent from slower on-chain planning. +// The feed is the only producer and run is the only consumer. +type orderInbox struct { + mu sync.Mutex + orders []*submittedOrder + ready chan struct{} +} + +func newOrderInbox() *orderInbox { + return &orderInbox{ready: make(chan struct{}, 1)} +} + +func (q *orderInbox) enqueue(order *submittedOrder) { + if order == nil { + return + } + q.mu.Lock() + q.orders = append(q.orders, order) + q.mu.Unlock() + select { + case q.ready <- struct{}{}: + default: + } +} + +func (q *orderInbox) run(ctx context.Context, out chan<- *submittedOrder) error { + defer close(out) + for { + q.mu.Lock() + if len(q.orders) == 0 { + q.mu.Unlock() + select { + case <-ctx.Done(): + return ctx.Err() + case <-q.ready: + continue + } + } + order := q.orders[0] + q.orders[0] = nil + q.orders = q.orders[1:] + if len(q.orders) == 0 { + q.orders = nil + } + q.mu.Unlock() + select { + case <-ctx.Done(): + return ctx.Err() + case out <- order: + } + } +} + +func (s *Solver) runOrderFeed(ctx context.Context, routes []route) error { + inbox := newOrderInbox() + orders := make(chan *submittedOrder) + g, gctx := errgroup.WithContext(ctx) + g.Go(func() error { + return s.feed.run(gctx, func(_ context.Context, msg orderMessage) { + order := s.parseOrderMessage(msg) + inbox.enqueue(order) + }) + }) + g.Go(func() error { return inbox.run(gctx, orders) }) + g.Go(func() error { return s.runOrderWorker(gctx, routes, orders) }) + return g.Wait() +} + +func (s *Solver) parseOrderMessage(msg orderMessage) *submittedOrder { + order, err := parseSubmittedOrder(msg.Data, s.cfg, s.chainID) + if err != nil { + s.log.Error(err, "order feed: ignored order", "event", msg.Event) + return nil + } + if isDutchAuctionContext(order.Output.Context) { + s.log.Info("order feed: ignored unsupported Dutch auction", + "event", msg.Event, + "orderId", order.OrderID, + "onChainOrderId", order.OnChainOrderID, + "quoteId", order.QuoteID, + "contextType", hexutil.Encode(order.Output.Context[:1]), + ) + return nil + } + s.log.Info("order received", + "event", msg.Event, + "orderStatus", order.OrderStatus, + "orderId", order.OrderID, + "onChainOrderId", order.OnChainOrderID, + "quoteId", order.QuoteID, + "inputSettler", order.InputSettler.Hex(), + ) + return order +} + +func (s *Solver) runOrderWorker( + ctx context.Context, + routes []route, + orders <-chan *submittedOrder, +) error { + pending := pendingFillState{byOrder: make(map[string]*pendingFill)} + completions := make(chan fillCompletion, fillCompletionCapacity) + for orders != nil || pending.len() > 0 { + select { + case <-ctx.Done(): + return ctx.Err() + case completion := <-completions: + s.completeFill(ctx, &pending, completion) + case order, ok := <-orders: + if !ok { + orders = nil + continue + } + fill := s.processOrderWithPending(ctx, routes, order, &pending) + if fill == nil { + continue + } + pending.add(fill) + s.reserve(ctx, fill.reservationKey, fill.reservations) + go awaitFill(ctx, fill, completions) + } + } + return nil +} + +func awaitFill(ctx context.Context, fill *pendingFill, completions chan<- fillCompletion) { + select { + case result := <-fill.result: + select { + case completions <- fillCompletion{fill: fill, result: result}: + case <-ctx.Done(): + } + case <-ctx.Done(): + } +} + +func (s *pendingFillState) len() int { + if s == nil { + return 0 + } + return len(s.byOrder) +} + +func (s *pendingFillState) contains(key string) bool { + if s == nil { + return false + } + _, ok := s.byOrder[key] + return ok +} + +func (s *pendingFillState) reservedCapacity() map[liquidlane.CapacityID]*big.Int { + reserved := make(map[liquidlane.CapacityID]*big.Int) + if s == nil { + return reserved + } + for _, fill := range s.byOrder { + addReservations(reserved, fill.reservations) + } + return reserved +} + +func (s *pendingFillState) add(fill *pendingFill) { + s.byOrder[fill.reservationKey] = fill +} + +func (s *pendingFillState) remove(key string) { + delete(s.byOrder, key) +} diff --git a/internal/solvers/lifi/execution_test.go b/internal/solvers/lifi/execution_test.go new file mode 100644 index 00000000..b562f656 --- /dev/null +++ b/internal/solvers/lifi/execution_test.go @@ -0,0 +1,88 @@ +package lifi + +import ( + "context" + "encoding/json" + "strconv" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/go-logr/logr/funcr" +) + +func TestOrderInboxDoesNotBlockAndPreservesOrder(t *testing.T) { + inbox := newOrderInbox() + const count = 5_000 + + enqueued := make(chan struct{}) + go func() { + for i := range count { + inbox.enqueue(&submittedOrder{OrderID: strconv.Itoa(i)}) + } + close(enqueued) + }() + select { + case <-enqueued: + case <-time.After(time.Second): + t.Fatal("enqueue blocked without a consumer") + } + + orders := make(chan *submittedOrder) + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan error, 1) + go func() { done <- inbox.run(ctx, orders) }() + for i := range count { + order := <-orders + if order.OrderID != strconv.Itoa(i) { + t.Fatalf("order %d = %s", i, order.OrderID) + } + } + cancel() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("inbox did not stop after cancellation") + } +} + +func TestParseOrderMessageIgnoresDutchAuctions(t *testing.T) { + tests := []byte{dutchAuctionContextType, exclusiveDutchAuctionContextType} + for _, contextType := range tests { + t.Run(hexutil.Encode([]byte{contextType}), func(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal order: %v", err) + } + output := sliceField(t, mapField(t, body, "order"), "outputs")[0].(map[string]any) + output["context"] = hexutil.Encode([]byte{contextType}) + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal order: %v", err) + } + + var logs []string + solver := &Solver{ + cfg: cfg, + chainID: 11155111, + log: funcr.NewJSON(func(entry string) { logs = append(logs, entry) }, funcr.Options{}), + } + if order := solver.parseOrderMessage(orderMessage{Event: orderSubmitEvent, Data: raw}); order != nil { + t.Fatalf("parseOrderMessage() = %+v, want ignored order", order) + } + logged := strings.Join(logs, "\n") + if !strings.Contains(logged, "ignored unsupported Dutch auction") || + !strings.Contains(logged, hexutil.Encode([]byte{contextType})) { + t.Fatalf("unsupported auction log = %s", logged) + } + }) + } +} diff --git a/internal/solvers/lifi/fill.go b/internal/solvers/lifi/fill.go new file mode 100644 index 00000000..85336435 --- /dev/null +++ b/internal/solvers/lifi/fill.go @@ -0,0 +1,157 @@ +package lifi + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/api/bindings/lifi/executor" + "github.com/symbioticfi/vault-solver/api/bindings/lifi/inputsettler" + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/liquidlane/discounts" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +var lifiExecutor = executor.NewLiquidLaneLifiExecutor() + +type fillCalldata struct { + OrderID common.Hash + Finalise []byte +} + +func executorRoutes( + order submittedOrder, + plan *types.FillPlan, + resolvedDiscounts map[common.Hash]*discounts.Signed, +) ([]executor.ILiquidLaneLifiExecutorFillRoute, error) { + if plan == nil || len(plan.Routes) == 0 { + return nil, errors.New("fill plan has no routes") + } + routes := make([]executor.ILiquidLaneLifiExecutorFillRoute, 0, len(plan.Routes)) + totalAmountIn := new(big.Int) + for i, route := range plan.Routes { + if route.Adapter == (common.Address{}) || route.AmountIn == nil || route.AmountIn.Sign() <= 0 || + route.ExpectedAmountOut == nil || route.ExpectedAmountOut.Sign() <= 0 || + route.MinAmountOut == nil || route.MinAmountOut.Sign() <= 0 || + route.MinAmountOut.Cmp(route.ExpectedAmountOut) > 0 { + return nil, errors.Errorf("fill plan route %d is invalid", i) + } + discount, err := executorDiscount(route, order.TokenIn, resolvedDiscounts) + if err != nil { + return nil, errors.Errorf("fill plan route %d discount: %w", i, err) + } + routes = append(routes, executor.ILiquidLaneLifiExecutorFillRoute{ + Adapter: route.Adapter, AmountIn: route.AmountIn, AmountOut: route.ExpectedAmountOut, + Discount: discount, + }) + totalAmountIn.Add(totalAmountIn, route.AmountIn) + } + if totalAmountIn.Cmp(order.AmountIn) != 0 { + return nil, errors.Errorf("fill plan input sum %s does not match order input %s", totalAmountIn, order.AmountIn) + } + return routes, nil +} + +func executorDiscount( + route types.FillRoute, + tokenIn common.Address, + resolvedDiscounts map[common.Hash]*discounts.Signed, +) (executor.ILiquidLaneLifiExecutorFillDiscount, error) { + if route.DiscountID == nil { + return emptyExecutorDiscount(), nil + } + if *route.DiscountID == (common.Hash{}) { + return executor.ILiquidLaneLifiExecutorFillDiscount{}, errors.New("discount id is zero") + } + resolved := resolvedDiscounts[*route.DiscountID] + if resolved == nil { + return executor.ILiquidLaneLifiExecutorFillDiscount{}, errors.New("resolved discount is missing") + } + if resolved.DiscountID != *route.DiscountID { + return executor.ILiquidLaneLifiExecutorFillDiscount{}, errors.New("resolved discount id mismatch") + } + if resolved.Adapter != route.Adapter { + return executor.ILiquidLaneLifiExecutorFillDiscount{}, errors.New("resolved discount adapter mismatch") + } + if resolved.Terms.TokenToRedeem != tokenIn { + return executor.ILiquidLaneLifiExecutorFillDiscount{}, errors.New("resolved discount token mismatch") + } + return executor.ILiquidLaneLifiExecutorFillDiscount{ + DiscountId: [32]byte(resolved.DiscountID), + DiscountSwap: executor.ILiquidLaneAdapterDiscountSwap{ + Discount: executor.ILiquidLaneAdapterDiscount{ + TokenToRedeem: resolved.Terms.TokenToRedeem, + Discount: liquidlane.CloneBig(resolved.Terms.Discount), + Signer: resolved.Terms.Signer, + Protocol: resolved.Terms.Protocol, + Nonce: liquidlane.CloneBig(resolved.Terms.Nonce), + Deadline: liquidlane.CloneBig(resolved.Terms.Deadline), + }, + SignerSignature: append([]byte(nil), resolved.SignerSignature...), + ProtocolDeadline: liquidlane.CloneBig(resolved.ProtocolDeadline), + }, + ProtocolSignature: append([]byte(nil), resolved.ProtocolSignature...), + }, nil +} + +func emptyExecutorDiscount() executor.ILiquidLaneLifiExecutorFillDiscount { + return executor.ILiquidLaneLifiExecutorFillDiscount{ + DiscountSwap: executor.ILiquidLaneAdapterDiscountSwap{ + Discount: executor.ILiquidLaneAdapterDiscount{ + Discount: new(big.Int), Nonce: new(big.Int), Deadline: new(big.Int), + }, + ProtocolDeadline: new(big.Int), + }, + } +} + +func buildFillCalldata( + order submittedOrder, + orderID common.Hash, + plan *types.FillPlan, + resolvedDiscounts map[common.Hash]*discounts.Signed, +) (*fillCalldata, error) { + routes, err := executorRoutes(order, plan, resolvedDiscounts) + if err != nil { + return nil, err + } + finaliseCalldata, err := lifiExecutor.TryPackFinaliseWithCurrentTimestamp( + toExecutorOrder(order.Order), + routes, + ) + if err != nil { + return nil, errors.Errorf("pack finaliseWithCurrentTimestamp: %w", err) + } + return &fillCalldata{OrderID: orderID, Finalise: finaliseCalldata}, nil +} + +func toExecutorOutput(output inputsettler.MandateOutput) executor.MandateOutput { + return executor.MandateOutput{ + Oracle: output.Oracle, + Settler: output.Settler, + ChainId: output.ChainId, + Token: output.Token, + Amount: output.Amount, + Recipient: output.Recipient, + CallbackData: output.CallbackData, + Context: output.Context, + } +} + +func toExecutorOrder(order inputsettler.StandardOrder) executor.IInputSettlerStandardOrder { + outputs := make([]executor.MandateOutput, 0, len(order.Outputs)) + for _, out := range order.Outputs { + outputs = append(outputs, toExecutorOutput(out)) + } + return executor.IInputSettlerStandardOrder{ + User: order.User, + Nonce: order.Nonce, + OriginChainId: order.OriginChainId, + Expires: order.Expires, + FillDeadline: order.FillDeadline, + InputOracle: order.InputOracle, + Inputs: order.Inputs, + Outputs: outputs, + } +} diff --git a/internal/solvers/lifi/fill_test.go b/internal/solvers/lifi/fill_test.go new file mode 100644 index 00000000..f712e573 --- /dev/null +++ b/internal/solvers/lifi/fill_test.go @@ -0,0 +1,224 @@ +package lifi + +import ( + "bytes" + "context" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/symbioticfi/vault-solver/api/bindings/lifi/executor" + "github.com/symbioticfi/vault-solver/api/bindings/lifi/inputsettler" + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/liquidlane/discounts" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +type fakeLifiReader struct { + orderID common.Hash + orderIDFn func(inputsettler.StandardOrder) common.Hash + status uint8 + statusErr error + latestBlock uint64 + latestBlockErr error + fill []liquidlane.FillQuote + fillSet *fillSnapshotSet + fillSetFn func() fillSnapshotSet + fillSnapshotsFn func() []liquidlane.FillQuote + routes []route + directAuthErr error + governanceFeeErr error +} + +func (f fakeLifiReader) resolveRoutes(context.Context, []common.Address) ([]route, error) { + return f.routes, nil +} + +func (f fakeLifiReader) validateExecutor( + context.Context, common.Address, common.Address, common.Address, common.Address, +) error { + return nil +} + +func (f fakeLifiReader) validateZeroGovernanceFee(context.Context, common.Address) error { + return f.governanceFeeErr +} + +func (f fakeLifiReader) validateDirectAuthorization(context.Context, common.Address, []route) error { + return f.directAuthErr +} + +func (f fakeLifiReader) validateGasTokens([]route) error { return nil } + +func (f fakeLifiReader) quoteSnapshots(context.Context, []route, common.Address, time.Time) (quoteSnapshotSet, error) { + return quoteSnapshotSet{}, nil +} + +func (f fakeLifiReader) fillSnapshots( + context.Context, []route, common.Address, common.Address, *big.Int, time.Time, +) (fillSnapshotSet, error) { + if f.fillSetFn != nil { + return withFakeGasPrices(f.fillSetFn()), nil + } + if f.fillSet != nil { + return withFakeGasPrices(*f.fillSet), nil + } + if f.fillSnapshotsFn != nil { + fill := f.fillSnapshotsFn() + return withFakeGasPrices(fillSnapshotSet{Direct: fill, DiscountBases: fill}), nil + } + return withFakeGasPrices(fillSnapshotSet{Direct: f.fill, DiscountBases: f.fill}), nil +} + +func withFakeGasPrices(set fillSnapshotSet) fillSnapshotSet { + rates := make(map[common.Address]*big.Int) + for _, quote := range append(append([]liquidlane.FillQuote(nil), set.Direct...), set.DiscountBases...) { + rates[quote.TokenOut] = big.NewInt(1) + } + set.GasPrices = liquidlanegas.NewPriceSnapshot(rates) + return set +} + +func (f fakeLifiReader) orderIdentifier( + _ context.Context, + _ common.Address, + order inputsettler.StandardOrder, +) (common.Hash, error) { + if f.orderIDFn != nil { + return f.orderIDFn(order), nil + } + return f.orderID, nil +} + +func (f fakeLifiReader) orderStatus(context.Context, common.Address, common.Hash) (uint8, error) { + return f.status, f.statusErr +} + +func (f fakeLifiReader) latestBlockNumber(context.Context) (uint64, error) { + return f.latestBlock, f.latestBlockErr +} + +func (f fakeLifiReader) latestBlockTime(context.Context) (time.Time, error) { + return time.Unix(1_700_000_000, 0), nil +} + +func TestBuildFillCalldata(t *testing.T) { + cfg := testLifiConfig() + tokenIn := common.HexToAddress("0x6666666666666666666666666666666666666666") + tokenOut := common.HexToAddress("0x7777777777777777777777777777777777777777") + submitted, err := parseSubmittedOrder(testOrderJSON(t, cfg, tokenIn, tokenOut), cfg, 11155111) + if err != nil { + t.Fatalf("parseSubmittedOrder: %v", err) + } + orderID := common.HexToHash("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + plan := &types.FillPlan{ + Routes: []types.FillRoute{{ + RouteID: "route-1", CapacityID: "capacity-1", + Adapter: common.HexToAddress("0x9999999999999999999999999999999999999999"), + AmountIn: submitted.AmountIn, ExpectedAmountOut: big.NewInt(1_000_000), + MinAmountOut: big.NewInt(990_000), + }}, + } + + calldata, err := buildFillCalldata(*submitted, orderID, plan, nil) + if err != nil { + t.Fatalf("buildFillCalldata: %v", err) + } + if calldata.OrderID != orderID { + t.Fatalf("order id = %s", calldata.OrderID) + } + + executorABI, err := executor.LiquidLaneLifiExecutorMetaData.ParseABI() + if err != nil { + t.Fatalf("parse executor ABI: %v", err) + } + method := executorABI.Methods["finaliseWithCurrentTimestamp"] + if !bytes.Equal(calldata.Finalise[:4], method.ID) { + t.Fatalf("finalise selector = %s, want %s", hexutil.Encode(calldata.Finalise[:4]), hexutil.Encode(method.ID)) + } + args, err := method.Inputs.Unpack(calldata.Finalise[4:]) + if err != nil { + t.Fatalf("unpack finaliseWithCurrentTimestamp: %v", err) + } + if len(args) != 2 { + t.Fatalf("finalise arguments = %d, want order and routes", len(args)) + } + encodedOrder := *abi.ConvertType( + args[0], new(executor.IInputSettlerStandardOrder), + ).(*executor.IInputSettlerStandardOrder) + if encodedOrder.User != submitted.Order.User || encodedOrder.Nonce.Cmp(submitted.Order.Nonce) != 0 || + len(encodedOrder.Outputs) != 1 || encodedOrder.Outputs[0].Amount.Cmp(submitted.OutputAmount) != 0 { + t.Fatalf("encoded order = %+v", encodedOrder) + } + routes := *abi.ConvertType( + args[1], new([]executor.ILiquidLaneLifiExecutorFillRoute), + ).(*[]executor.ILiquidLaneLifiExecutorFillRoute) + if len(routes) != 1 || routes[0].Adapter != plan.Routes[0].Adapter || + routes[0].AmountIn.Cmp(plan.Routes[0].AmountIn) != 0 || + routes[0].AmountOut.Cmp(plan.Routes[0].ExpectedAmountOut) != 0 { + t.Fatalf("fill routes = %+v", routes) + } +} + +func TestExecutorRoutesRejectsInputMismatch(t *testing.T) { + order := submittedOrder{AmountIn: big.NewInt(100)} + plan := &types.FillPlan{Routes: []types.FillRoute{{ + Adapter: common.HexToAddress("0x9999999999999999999999999999999999999999"), + AmountIn: big.NewInt(99), + ExpectedAmountOut: big.NewInt(90), + MinAmountOut: big.NewInt(80), + }}} + + _, err := executorRoutes(order, plan, nil) + if err == nil || !strings.Contains(err.Error(), "input sum 99 does not match order input 100") { + t.Fatalf("executorRoutes() error = %v", err) + } +} + +func TestExecutorRoutesIncludesResolvedPrivateDiscount(t *testing.T) { + cfg := testLifiConfig() + tokenIn := common.HexToAddress("0x6666666666666666666666666666666666666666") + tokenOut := common.HexToAddress("0x7777777777777777777777777777777777777777") + adapter := common.HexToAddress("0x9999999999999999999999999999999999999999") + submitted, err := parseSubmittedOrder(testOrderJSON(t, cfg, tokenIn, tokenOut), cfg, 11155111) + if err != nil { + t.Fatalf("parseSubmittedOrder: %v", err) + } + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + plan := &types.FillPlan{Routes: []types.FillRoute{{ + RouteID: "route-1", Adapter: adapter, AmountIn: submitted.AmountIn, + ExpectedAmountOut: big.NewInt(900_000), MinAmountOut: big.NewInt(850_000), DiscountID: &discountID, + }}} + resolved := &discounts.Signed{ + DiscountID: discountID, Adapter: adapter, + Terms: discounts.SignedTerms{ + TokenToRedeem: tokenIn, Discount: big.NewInt(100_000), + Signer: common.HexToAddress("0x1111111111111111111111111111111111111111"), + Protocol: common.HexToAddress("0x2222222222222222222222222222222222222222"), + Nonce: big.NewInt(7), Deadline: big.NewInt(1_900_000_000), + }, + SignerSignature: []byte{0x12, 0x34}, ProtocolDeadline: big.NewInt(1_900_000_001), + ProtocolSignature: []byte{0x56, 0x78}, + } + + routes, err := executorRoutes( + *submitted, plan, + map[common.Hash]*discounts.Signed{discountID: resolved}, + ) + if err != nil { + t.Fatalf("executorRoutes: %v", err) + } + discount := routes[0].Discount + if common.Hash(discount.DiscountId) != discountID || + discount.DiscountSwap.Discount.TokenToRedeem != tokenIn || + discount.DiscountSwap.Discount.Discount.Cmp(big.NewInt(100_000)) != 0 || + !bytes.Equal(discount.ProtocolSignature, resolved.ProtocolSignature) { + t.Fatalf("encoded discount = %+v", discount) + } +} diff --git a/internal/solvers/lifi/order.go b/internal/solvers/lifi/order.go new file mode 100644 index 00000000..00d89747 --- /dev/null +++ b/internal/solvers/lifi/order.go @@ -0,0 +1,417 @@ +package lifi + +import ( + "encoding/json" + "math" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/api/bindings/lifi/inputsettler" + "github.com/symbioticfi/vault-solver/api/lifiorder" +) + +const ( + dutchAuctionContextType byte = 0x01 + exclusiveDutchAuctionContextType byte = 0xe1 +) + +type submittedOrderEvent struct { + OrderType string `json:"orderType"` + Order lifiorder.SubmitOrderDtoOrder `json:"order"` + QuoteID *string `json:"quoteId,omitempty"` + InputSettler string `json:"inputSettler"` + Meta submittedOrderEventMeta `json:"meta"` +} + +type submittedOrderEventMeta struct { + OrderStatus string `json:"orderStatus"` + OrderID string `json:"orderIdentifier"` + OnChainOrderID string `json:"onChainOrderId"` + QuoteID json.RawMessage `json:"quoteId"` +} + +type submittedOrder struct { + QuoteID string + OrderStatus string + OrderID string + OnChainOrderID string + + Order inputsettler.StandardOrder + InputSettler common.Address + + TokenIn common.Address + AmountIn *big.Int + TokenOut common.Address + OutputAmount *big.Int + Output inputsettler.MandateOutput +} + +func isDutchAuctionContext(context []byte) bool { + if len(context) == 0 { + return false + } + return context[0] == dutchAuctionContextType || context[0] == exclusiveDutchAuctionContextType +} + +type parsedStandardOrder struct { + order inputsettler.StandardOrder + tokenIn common.Address + amountIn *big.Int + tokenOut common.Address + outputAmount *big.Int + output inputsettler.MandateOutput +} + +type parsedOutput struct { + output inputsettler.MandateOutput + tokenOut common.Address + amount *big.Int +} + +func parseSubmittedOrder(data []byte, cfg *Config, chainID int64) (*submittedOrder, error) { + var event submittedOrderEvent + if err := json.Unmarshal(data, &event); err != nil { + return nil, errors.Errorf("decode submit order dto: %w", err) + } + + if !isFillableOrderStatus(event.Meta.OrderStatus) { + return nil, errors.Errorf("unsupported order status %q", event.Meta.OrderStatus) + } + if !isOnChainOrderEvent(event) { + if event.OrderType == "" { + return nil, errors.New("missing orderType requires onChainOrderId and inputSettler") + } + return nil, errors.Errorf("unsupported non-onchain order type %q", event.OrderType) + } + + inputSettler, err := parseAddress(event.InputSettler, "inputSettler") + if err != nil { + return nil, err + } + if inputSettler != cfg.InputSettler { + return nil, errors.Errorf("inputSettler %s does not match configured %s", inputSettler.Hex(), cfg.InputSettler.Hex()) + } + parsed, err := parseStandardOrder(event.Order, cfg, chainID) + if err != nil { + return nil, err + } + return &submittedOrder{ + QuoteID: eventQuoteID(event), + OrderStatus: event.Meta.OrderStatus, + OrderID: event.Meta.OrderID, + OnChainOrderID: event.Meta.OnChainOrderID, + Order: parsed.order, + InputSettler: inputSettler, + TokenIn: parsed.tokenIn, + AmountIn: parsed.amountIn, + TokenOut: parsed.tokenOut, + OutputAmount: new(big.Int).Set(parsed.outputAmount), + Output: parsed.output, + }, nil +} + +func isFillableOrderStatus(status string) bool { + return status == "Signed" || status == "Delivered" +} + +func isOnChainOrderType(orderType string) bool { + normalized := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(orderType)) + switch normalized { + case "onchainorder", "oifuseropenv0": + return true + default: + return false + } +} + +func isOnChainOrderEvent(event submittedOrderEvent) bool { + if event.OrderType == "" { + return event.Meta.OnChainOrderID != "" && event.InputSettler != "" + } + return isOnChainOrderType(event.OrderType) +} + +func parseStandardOrder( + dto lifiorder.SubmitOrderDtoOrder, + cfg *Config, + chainID int64, +) (*parsedStandardOrder, error) { + user, err := parseAddress(dto.User, "order.user") + if err != nil { + return nil, err + } + inputOracle, err := parseAddress(dto.InputOracle, "order.inputOracle") + if err != nil { + return nil, err + } + if inputOracle != cfg.OutputSettler { + return nil, errors.Errorf("order.inputOracle %s does not match outputSettler %s", inputOracle.Hex(), cfg.OutputSettler.Hex()) + } + if len(dto.Inputs) != 1 { + return nil, errors.Errorf("order.inputs: expected 1 input, got %d", len(dto.Inputs)) + } + if len(dto.Outputs) != 1 { + return nil, errors.Errorf("order.outputs: expected 1 output, got %d", len(dto.Outputs)) + } + + nonce, err := parseUint(dto.Nonce, "order.nonce") + if err != nil { + return nil, err + } + originChainID, err := parseUint(dto.OriginChainId, "order.originChainId") + if err != nil { + return nil, err + } + if originChainID.Cmp(big.NewInt(chainID)) != 0 { + return nil, errors.Errorf("order.originChainId %s does not match chain %d", originChainID, chainID) + } + expires, err := parseUint32(dto.Expires, "order.expires") + if err != nil { + return nil, err + } + fillDeadline, err := parseUint32(dto.FillDeadline, "order.fillDeadline") + if err != nil { + return nil, err + } + + inputPair := dto.Inputs[0] + if len(inputPair) != 2 { + return nil, errors.Errorf("order.inputs[0]: expected [tokenId, amount], got %d values", len(inputPair)) + } + tokenID, err := parseTupleUint(inputPair[0], "order.inputs[0][0]") + if err != nil { + return nil, err + } + tokenIn, err := tokenIDToAddress(tokenID, "order.inputs[0][0]") + if err != nil { + return nil, err + } + amountIn, err := parseTupleUint(inputPair[1], "order.inputs[0][1]") + if err != nil { + return nil, err + } + if amountIn.Sign() <= 0 { + return nil, errors.New("order.inputs[0][1]: must be positive") + } + + output, err := parseOutput(dto.Outputs[0], cfg, chainID) + if err != nil { + return nil, err + } + order := inputsettler.StandardOrder{ + User: user, + Nonce: nonce, + OriginChainId: originChainID, + Expires: expires, + FillDeadline: fillDeadline, + InputOracle: inputOracle, + Inputs: [][2]*big.Int{{new(big.Int).Set(tokenID), new(big.Int).Set(amountIn)}}, + Outputs: []inputsettler.MandateOutput{output.output}, + } + return &parsedStandardOrder{ + order: order, + tokenIn: tokenIn, + amountIn: amountIn, + tokenOut: output.tokenOut, + outputAmount: output.amount, + output: output.output, + }, nil +} + +func parseOutput( + dto lifiorder.SubmitOrderDtoOrderOutputsInner, + cfg *Config, + chainID int64, +) (*parsedOutput, error) { + oracle, err := parseBytes32(dto.Oracle, "order.outputs[0].oracle") + if err != nil { + return nil, err + } + settler, err := parseBytes32(dto.Settler, "order.outputs[0].settler") + if err != nil { + return nil, err + } + wantSettler := addressIdentifier(cfg.OutputSettler) + if oracle != wantSettler { + return nil, errors.New("order.outputs[0].oracle does not match outputSettler") + } + if settler != wantSettler { + return nil, errors.New("order.outputs[0].settler does not match outputSettler") + } + + tokenID, err := parseBytes32(dto.Token, "order.outputs[0].token") + if err != nil { + return nil, err + } + tokenOut, err := identifierAddress(tokenID, "order.outputs[0].token") + if err != nil { + return nil, err + } + recipientID, err := parseBytes32(dto.Recipient, "order.outputs[0].recipient") + if err != nil { + return nil, err + } + if _, err := identifierAddress(recipientID, "order.outputs[0].recipient"); err != nil { + return nil, err + } + + amountOut, err := parseUint(dto.Amount, "order.outputs[0].amount") + if err != nil { + return nil, err + } + if amountOut.Sign() <= 0 { + return nil, errors.New("order.outputs[0].amount: must be positive") + } + outputChainID, err := parseUint(dto.ChainId, "order.outputs[0].chainId") + if err != nil { + return nil, err + } + if outputChainID.Cmp(big.NewInt(chainID)) != 0 { + return nil, errors.Errorf("order.outputs[0].chainId %s does not match chain %d", outputChainID, chainID) + } + + callbackData, err := nullableHexBytes(dto.CallbackData, "order.outputs[0].callbackData") + if err != nil { + return nil, err + } + contextData, err := nullableHexBytes(dto.Context, "order.outputs[0].context") + if err != nil { + return nil, err + } + if len(callbackData) != 0 { + return nil, errors.New("non-empty output callbackData is not supported") + } + + output := inputsettler.MandateOutput{ + Oracle: oracle, + Settler: settler, + ChainId: outputChainID, + Token: tokenID, + Amount: amountOut, + Recipient: recipientID, + CallbackData: callbackData, + Context: contextData, + } + return &parsedOutput{output: output, tokenOut: tokenOut, amount: amountOut}, nil +} + +func eventQuoteID(event submittedOrderEvent) string { + if event.QuoteID != nil && *event.QuoteID != "" { + return *event.QuoteID + } + if len(event.Meta.QuoteID) == 0 { + return "" + } + var s string + if err := json.Unmarshal(event.Meta.QuoteID, &s); err == nil { + return s + } + return "" +} + +func parseAddress(raw, field string) (common.Address, error) { + if !common.IsHexAddress(raw) { + return common.Address{}, errors.Errorf("%s: invalid address %q", field, raw) + } + addr := common.HexToAddress(raw) + if addr == (common.Address{}) { + return common.Address{}, errors.Errorf("%s: zero address", field) + } + return addr, nil +} + +func parseUint32(raw, field string) (uint32, error) { + n, err := parseUint(raw, field) + if err != nil { + return 0, err + } + if !n.IsUint64() || n.Uint64() > math.MaxUint32 { + return 0, errors.Errorf("%s: overflows uint32", field) + } + return uint32(n.Uint64()), nil +} + +func parseTupleUint(raw any, field string) (*big.Int, error) { + value, ok := raw.(string) + if !ok { + return nil, errors.Errorf("%s: expected decimal string, got %T", field, raw) + } + return parseUint(value, field) +} + +func parseUint(raw, field string) (*big.Int, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, errors.Errorf("%s: empty integer", field) + } + n, ok := new(big.Int).SetString(raw, 10) + if !ok || n.Sign() < 0 { + return nil, errors.Errorf("%s: invalid uint %q", field, raw) + } + return n, nil +} + +func parseBytes32(raw, field string) ([32]byte, error) { + b, err := decodeHexBytes(raw, field) + if err != nil { + return [32]byte{}, err + } + if len(b) != 32 { + return [32]byte{}, errors.Errorf("%s: expected 32 bytes, got %d", field, len(b)) + } + var out [32]byte + copy(out[:], b) + return out, nil +} + +func decodeHexBytes(raw, field string) ([]byte, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, errors.Errorf("%s: empty hex", field) + } + if !strings.HasPrefix(raw, "0x") && !strings.HasPrefix(raw, "0X") { + raw = "0x" + raw + } + out, err := hexutil.Decode(raw) + if err != nil { + return nil, errors.Errorf("%s: invalid hex: %w", field, err) + } + return out, nil +} + +func nullableHexBytes(value lifiorder.NullableString, field string) ([]byte, error) { + if !value.IsSet() || value.Get() == nil || *value.Get() == "" { + return nil, nil + } + return decodeHexBytes(*value.Get(), field) +} + +func tokenIDToAddress(n *big.Int, field string) (common.Address, error) { + addr := common.BytesToAddress(n.Bytes()) + roundTrip := new(big.Int).SetBytes(addr.Bytes()) + if addr == (common.Address{}) || roundTrip.Cmp(n) != 0 { + return common.Address{}, errors.Errorf("%s: not a clean address identifier", field) + } + return addr, nil +} + +func addressIdentifier(addr common.Address) [32]byte { + var out [32]byte + copy(out[12:], addr.Bytes()) + return out +} + +func identifierAddress(id [32]byte, field string) (common.Address, error) { + addr := common.BytesToAddress(id[12:]) + if addr == (common.Address{}) { + return common.Address{}, errors.Errorf("%s: zero address identifier", field) + } + if addressIdentifier(addr) != id { + return common.Address{}, errors.Errorf("%s: not a clean address identifier", field) + } + return addr, nil +} diff --git a/internal/solvers/lifi/order_test.go b/internal/solvers/lifi/order_test.go new file mode 100644 index 00000000..5b3d49c5 --- /dev/null +++ b/internal/solvers/lifi/order_test.go @@ -0,0 +1,354 @@ +package lifi + +import ( + "encoding/json" + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +func TestParseSubmittedOrder(t *testing.T) { + cfg := testLifiConfig() + tokenIn := common.HexToAddress("0x6666666666666666666666666666666666666666") + tokenOut := common.HexToAddress("0x7777777777777777777777777777777777777777") + + order, err := parseSubmittedOrder(testOrderJSON(t, cfg, tokenIn, tokenOut), cfg, 11155111) + if err != nil { + t.Fatalf("parseSubmittedOrder: %v", err) + } + if order.QuoteID != "quote-1" { + t.Fatalf("quote id = %q", order.QuoteID) + } + if order.TokenIn != tokenIn || order.TokenOut != tokenOut { + t.Fatalf("tokens = %s/%s", order.TokenIn, order.TokenOut) + } + if got := order.AmountIn.String(); got != "1000000" { + t.Fatalf("amount in = %s", got) + } + if got := order.OutputAmount.String(); got != "990000" { + t.Fatalf("amount out = %s", got) + } + if order.Output.Oracle != addressIdentifier(cfg.OutputSettler) { + t.Fatal("output oracle was not parsed as output settler identifier") + } + order.OutputAmount.SetInt64(1) + if order.Output.Amount.String() != "990000" { + t.Fatalf("output amount aliases mandate output: %s", order.Output.Amount) + } +} + +func TestParseSubmittedOrderRejectsNonStringInputTuple(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + inputs := sliceField(t, mapField(t, body, "order"), "inputs") + inputs[0].([]any)[1] = float64(1_000_000) + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + _, err = parseSubmittedOrder(raw, cfg, 11155111) + if err == nil || !strings.Contains(err.Error(), "expected decimal string") { + t.Fatalf("err = %v", err) + } +} + +func TestParseSubmittedOrderAllowsMissingQuoteID(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + delete(body, "quoteId") + mapField(t, body, "meta")["quoteId"] = nil + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + order, err := parseSubmittedOrder(raw, cfg, 11155111) + if err != nil { + t.Fatalf("parseSubmittedOrder: %v", err) + } + if order.QuoteID != "" { + t.Fatalf("quote id = %q", order.QuoteID) + } +} + +func TestParseSubmittedOrderInfersOnChainOrderWhenTypeMissing(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + delete(body, "orderType") + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + order, err := parseSubmittedOrder(raw, cfg, 11155111) + if err != nil { + t.Fatalf("parseSubmittedOrder: %v", err) + } + if order.OnChainOrderID == "" { + t.Fatal("on-chain order id was not parsed") + } +} + +func TestParseSubmittedOrderRejectsMissingTypeWithoutOnChainMetadata(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + delete(body, "orderType") + meta := body["meta"].(map[string]any) + delete(meta, "onChainOrderId") + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + if _, err = parseSubmittedOrder(raw, cfg, 11155111); err == nil || + !strings.Contains(err.Error(), "missing orderType requires onChainOrderId") { + t.Fatalf("err = %v", err) + } +} + +func TestParseSubmittedOrderPreservesOutputContext(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + outputs := sliceField(t, mapField(t, body, "order"), "outputs") + output := outputs[0].(map[string]any) + output["context"] = "0x01000000010000000200000000000000000000000000000000000000000000000000000000000003" + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + order, err := parseSubmittedOrder(raw, cfg, 11155111) + if err != nil { + t.Fatalf("parseSubmittedOrder: %v", err) + } + if got := hexutil.Encode(order.Output.Context); got != output["context"] { + t.Fatalf("context = %s", got) + } +} + +func TestParseSubmittedOrderRejectsDirtyOutputIdentifier(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + outputs := sliceField(t, mapField(t, body, "order"), "outputs") + output, ok := outputs[0].(map[string]any) + if !ok { + t.Fatalf("output type = %T", outputs[0]) + } + dirty := addressIdentifier(common.HexToAddress("0x7777777777777777777777777777777777777777")) + dirty[0] = 1 + output["token"] = hexutil.Encode(dirty[:]) + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + _, err = parseSubmittedOrder(raw, cfg, 11155111) + if err == nil || !strings.Contains(err.Error(), "clean address identifier") { + t.Fatalf("err = %v", err) + } +} + +func TestParseSubmittedOrderRejectsNonOnChainOrderType(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + body["orderType"] = "GaslessCrosschainOrder" + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + _, err = parseSubmittedOrder(raw, cfg, 11155111) + if err == nil || !strings.Contains(err.Error(), "unsupported non-onchain order type") { + t.Fatalf("err = %v", err) + } + + body["orderType"] = "NonOnChainOrder" + raw, err = json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + _, err = parseSubmittedOrder(raw, cfg, 11155111) + if err == nil || !strings.Contains(err.Error(), "unsupported non-onchain order type") { + t.Fatalf("err = %v", err) + } +} + +func TestParseSubmittedOrderAcceptsOIFUserOpenOrderType(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + body["orderType"] = "oif-user-open-v0" + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + if _, err = parseSubmittedOrder(raw, cfg, 11155111); err != nil { + t.Fatalf("parseSubmittedOrder: %v", err) + } +} + +func TestParseSubmittedOrderRejectsMissingOrderStatus(t *testing.T) { + cfg := testLifiConfig() + var body map[string]any + if err := json.Unmarshal(testOrderJSON( + t, + cfg, + common.HexToAddress("0x6666666666666666666666666666666666666666"), + common.HexToAddress("0x7777777777777777777777777777777777777777"), + ), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + delete(mapField(t, body, "meta"), "orderStatus") + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + _, err = parseSubmittedOrder(raw, cfg, 11155111) + if err == nil || !strings.Contains(err.Error(), "unsupported order status") { + t.Fatalf("err = %v", err) + } +} + +func testLifiConfig() *Config { + return &Config{ + InputSettler: common.HexToAddress("0x2222222222222222222222222222222222222222"), + OutputSettler: common.HexToAddress("0x3333333333333333333333333333333333333333"), + Executor: common.HexToAddress("0x4444444444444444444444444444444444444444"), + } +} + +func testOrderJSON(t *testing.T, cfg *Config, tokenIn, tokenOut common.Address) []byte { + t.Helper() + user := common.HexToAddress("0x1111111111111111111111111111111111111111") + recipient := common.HexToAddress("0x8888888888888888888888888888888888888888") + body := map[string]any{ + "orderType": "OnChainOrder", + "quoteId": "quote-1", + "inputSettler": cfg.InputSettler.Hex(), + "order": map[string]any{ + "user": user.Hex(), + "nonce": "7", + "originChainId": "11155111", + "expires": "1800000000", + "fillDeadline": "1800000300", + "inputOracle": cfg.OutputSettler.Hex(), + "inputs": [][]string{ + {new(big.Int).SetBytes(tokenIn.Bytes()).String(), "1000000"}, + }, + "outputs": []map[string]any{{ + "oracle": hexID(cfg.OutputSettler), + "settler": hexID(cfg.OutputSettler), + "chainId": "11155111", + "token": hexID(tokenOut), + "amount": "990000", + "recipient": hexID(recipient), + "callbackData": "0x", + "context": "0x", + }}, + }, + "meta": map[string]any{ + "orderStatus": "Signed", + "orderIdentifier": "intent-1", + "onChainOrderId": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "quoteId": "quote-from-meta", + }, + } + raw, err := json.Marshal(body) + if err != nil { + t.Fatalf("marshal order: %v", err) + } + return raw +} + +func hexID(addr common.Address) string { + id := addressIdentifier(addr) + return hexutil.Encode(id[:]) +} + +func mapField(t *testing.T, m map[string]any, field string) map[string]any { + t.Helper() + out, ok := m[field].(map[string]any) + if !ok { + t.Fatalf("%s type = %T", field, m[field]) + } + return out +} + +func sliceField(t *testing.T, m map[string]any, field string) []any { + t.Helper() + out, ok := m[field].([]any) + if !ok { + t.Fatalf("%s type = %T", field, m[field]) + } + return out +} diff --git a/internal/solvers/lifi/orderclient.go b/internal/solvers/lifi/orderclient.go new file mode 100644 index 00000000..ac83094d --- /dev/null +++ b/internal/solvers/lifi/orderclient.go @@ -0,0 +1,240 @@ +package lifi + +import ( + "context" + "math" + "net/http" + "strconv" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/api/lifiorder" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +type orderClient struct { + api *lifiorder.APIClient + apiKey string + chain string +} + +func newOrderClient(baseURL, apiKey string, timeout time.Duration, chainID int64) *orderClient { + cfg := lifiorder.NewConfiguration() + cfg.Servers = lifiorder.ServerConfigurations{{URL: strings.TrimRight(baseURL, "/")}} + cfg.HTTPClient = &http.Client{Timeout: timeout} + return &orderClient{api: lifiorder.NewAPIClient(cfg), apiKey: apiKey, chain: strconv.FormatInt(chainID, 10)} +} + +func (c *orderClient) withAuth(ctx context.Context) context.Context { + return context.WithValue(ctx, lifiorder.ContextAPIKeys, map[string]lifiorder.APIKey{ + "api-key": {Key: c.apiKey}, + }) +} + +func (c *orderClient) validateExecutorRegistration(ctx context.Context, executor common.Address) error { + identities, httpResp, err := c.api.SolverAPIAPI. + SolverApiV0ControllerGetSolverIdentities(c.withAuth(ctx)). + Execute() + closeResp(httpResp) + if err != nil { + return apiErr("get solver identities", httpResp, err) + } + if identities != nil { + for _, identity := range identities.Data { + if strings.EqualFold(identity.Address, executor.Hex()) { + return nil + } + } + } + return errors.Errorf("lifi order server: executor %s is not registered for this API key", executor.Hex()) +} + +func (c *orderClient) replaceSupportedContracts( + ctx context.Context, dto lifiorder.PutSupportedContractsDto, +) error { + _, httpResp, err := c.api.SolverAPIV1API. + SupportedContractsControllerReplaceSupportedContracts(c.withAuth(ctx)). + PutSupportedContractsDto(dto). + Execute() + closeResp(httpResp) + if err != nil { + return apiErr("put supported contracts", httpResp, err) + } + return nil +} + +func (c *orderClient) ensureSupportedContracts( + ctx context.Context, chainID int64, inputSettler, outputSettler common.Address, +) error { + chain := chainRef(chainID) + current, httpResp, err := c.api.SolverAPIV1API. + SupportedContractsControllerGetSupportedContracts(c.withAuth(ctx)). + Execute() + closeResp(httpResp) + if err != nil { + return apiErr("get supported contracts", httpResp, err) + } + if current != nil && supportsConfiguredContracts(current.Data, chain, inputSettler, outputSettler) { + return nil + } + contracts := lifiorder.ContractsByKindDto{} + if current != nil { + contracts = current.Data + } + return c.replaceSupportedContracts(ctx, supportedContractsDTO(contracts, chain, inputSettler, outputSettler)) +} + +func chainRef(chainID int64) string { + return "eip155:" + strconv.FormatInt(chainID, 10) +} + +func supportedContractsDTO( + current lifiorder.ContractsByKindDto, + chain string, + inputSettler, outputSettler common.Address, +) lifiorder.PutSupportedContractsDto { + dto := lifiorder.PutSupportedContractsDto{ + Oracle: supportedContractEntries(current.Oracle), + InputSettler: supportedContractEntries(current.InputSettler), + OutputSettler: supportedContractEntries(current.OutputSettler), + } + dto.InputSettler = appendSupportedContract(dto.InputSettler, chain, inputSettler) + dto.OutputSettler = appendSupportedContract(dto.OutputSettler, chain, outputSettler) + dto.Oracle = appendSupportedContract(dto.Oracle, chain, outputSettler) + return dto +} + +func supportedContractEntries(items []lifiorder.ChainAddressDto) []lifiorder.QuoteRequestDtoIntentMetadataOracleInner { + if len(items) == 0 { + return nil + } + out := make([]lifiorder.QuoteRequestDtoIntentMetadataOracleInner, len(items)) + for i, item := range items { + out[i] = lifiorder.QuoteRequestDtoIntentMetadataOracleInner(item) + } + return out +} + +func appendSupportedContract( + items []lifiorder.QuoteRequestDtoIntentMetadataOracleInner, + chain string, + address common.Address, +) []lifiorder.QuoteRequestDtoIntentMetadataOracleInner { + for _, item := range items { + if item.Chain == chain && strings.EqualFold(item.Address, address.Hex()) { + return items + } + } + return append(items, lifiorder.QuoteRequestDtoIntentMetadataOracleInner{Chain: chain, Address: address.Hex()}) +} + +func supportsConfiguredContracts( + contracts lifiorder.ContractsByKindDto, + chain string, + inputSettler, outputSettler common.Address, +) bool { + return hasChainAddress(contracts.InputSettler, chain, inputSettler) && + hasChainAddress(contracts.OutputSettler, chain, outputSettler) && + hasChainAddress(contracts.Oracle, chain, outputSettler) +} + +func hasChainAddress(items []lifiorder.ChainAddressDto, chain string, address common.Address) bool { + for _, item := range items { + if item.Chain == chain && strings.EqualFold(item.Address, address.Hex()) { + return true + } + } + return false +} + +func (c *orderClient) submitQuotes(ctx context.Context, quotes []types.Quote) error { + dtoQuotes := make([]lifiorder.SubmitQuotesDtoQuotesInner, 0, len(quotes)) + for i, quote := range quotes { + dto, err := submitQuoteDTO(c.chain, quote, i) + if err != nil { + return err + } + dtoQuotes = append(dtoQuotes, dto) + } + + _, httpResp, err := c.api.SolverAPIAPI. + QuotesControllerSubmitQuotes(c.withAuth(ctx)). + SubmitQuotesDto(lifiorder.SubmitQuotesDto{Quotes: dtoQuotes}). + Execute() + closeResp(httpResp) + if err != nil { + return apiErr("submit quotes", httpResp, err) + } + return nil +} + +func submitQuoteDTO(chain string, quote types.Quote, index int) (lifiorder.SubmitQuotesDtoQuotesInner, error) { + field := "quotes[" + strconv.Itoa(index) + "]" + expiry, err := int32Checked(quote.Expiry, field+".expiry") + if err != nil { + return lifiorder.SubmitQuotesDtoQuotesInner{}, err + } + fromDecimals, err := int32Checked(int64(quote.FromDecimals), field+".fromDecimals") + if err != nil { + return lifiorder.SubmitQuotesDtoQuotesInner{}, err + } + toDecimals, err := int32Checked(int64(quote.ToDecimals), field+".toDecimals") + if err != nil { + return lifiorder.SubmitQuotesDtoQuotesInner{}, err + } + ranges := make([]lifiorder.SubmitQuotesDtoQuotesInnerRangesInner, 0, len(quote.Ranges)) + for i, quoteRange := range quote.Ranges { + if quoteRange.MinAmount == nil || quoteRange.MaxAmount == nil || quoteRange.Quote == "" { + return lifiorder.SubmitQuotesDtoQuotesInner{}, errors.Errorf("%s.ranges[%d]: incomplete range", field, i) + } + ranges = append(ranges, lifiorder.SubmitQuotesDtoQuotesInnerRangesInner{ + MinAmount: quoteRange.MinAmount.String(), + MaxAmount: quoteRange.MaxAmount.String(), + Quote: quoteRange.Quote, + }) + } + dto := lifiorder.SubmitQuotesDtoQuotesInner{ + FromChain: chain, ToChain: chain, + FromAsset: quote.FromAsset.Hex(), ToAsset: quote.ToAsset.Hex(), + FromDecimals: fromDecimals, ToDecimals: toDecimals, + Ranges: ranges, Expiry: expiry, + } + if quote.ExclusiveFor != (common.Address{}) { + exclusiveFor := quote.ExclusiveFor.Hex() + dto.ExclusiveFor = &exclusiveFor + } + return dto, nil +} + +func closeResp(resp *http.Response) { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } +} + +func apiErr(what string, resp *http.Response, err error) error { + var genErr *lifiorder.GenericOpenAPIError + if errors.As(err, &genErr) { + if body := strings.TrimSpace(string(genErr.Body())); body != "" { + return errors.Errorf("lifi order server: %s: %s: %s: %w", what, statusOf(resp), body, err) + } + } + return errors.Errorf("lifi order server: %s: %s: %w", what, statusOf(resp), err) +} + +func statusOf(resp *http.Response) string { + if resp == nil { + return "no response" + } + return resp.Status +} + +func int32Checked(v int64, field string) (int32, error) { + if v < math.MinInt32 || v > math.MaxInt32 { + return 0, errors.Errorf("%s: %d overflows int32", field, v) + } + return int32(v), nil +} diff --git a/internal/solvers/lifi/orderclient_test.go b/internal/solvers/lifi/orderclient_test.go new file mode 100644 index 00000000..36e8942e --- /dev/null +++ b/internal/solvers/lifi/orderclient_test.go @@ -0,0 +1,243 @@ +package lifi + +import ( + "context" + "encoding/json" + "math/big" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/symbioticfi/vault-solver/api/lifiorder" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +func TestOrderClientSubmitQuotes(t *testing.T) { + var gotHeader string + var gotBody map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/quotes/submit" { + t.Fatalf("path = %s", r.URL.Path) + } + gotHeader = r.Header.Get("x-api-key") + if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { + t.Fatalf("decode body: %v", err) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"status":"success","quotesAdded":1}`)) + })) + defer srv.Close() + + client := newOrderClient(srv.URL, "test-key", time.Second, 11155111) + err := client.submitQuotes(context.Background(), []types.Quote{{ + FromAsset: common.HexToAddress("0x1111111111111111111111111111111111111111"), + ToAsset: common.HexToAddress("0x2222222222222222222222222222222222222222"), + FromDecimals: 6, + ToDecimals: 18, + Expiry: 1_800_000_000, + ExclusiveFor: common.HexToAddress("0x3333333333333333333333333333333333333333"), + Ranges: []types.QuoteRange{{ + MinAmount: big.NewInt(1), + MaxAmount: big.NewInt(1_000_000), + Quote: "0.99", + }}, + }}) + if err != nil { + t.Fatalf("submitQuotes: %v", err) + } + if gotHeader != "test-key" { + t.Fatalf("x-api-key = %q", gotHeader) + } + + quotes := gotBody["quotes"].([]any) + q := quotes[0].(map[string]any) + if q["fromChain"] != "11155111" || q["toChain"] != "11155111" { + t.Fatalf("chains = %v/%v", q["fromChain"], q["toChain"]) + } + if q["fromAsset"] != "0x1111111111111111111111111111111111111111" { + t.Fatalf("fromAsset = %v", q["fromAsset"]) + } + if q["fromDecimals"] != float64(6) || q["toDecimals"] != float64(18) { + t.Fatalf("decimals = %v/%v", q["fromDecimals"], q["toDecimals"]) + } + if q["exclusiveFor"] != "0x3333333333333333333333333333333333333333" { + t.Fatalf("exclusiveFor = %v", q["exclusiveFor"]) + } + ranges := q["ranges"].([]any) + rng := ranges[0].(map[string]any) + if rng["minAmount"] != "1" || rng["maxAmount"] != "1000000" || rng["quote"] != "0.99" { + t.Fatalf("range = %#v", rng) + } +} + +func TestOrderClientValidateExecutorRegistration(t *testing.T) { + executor := common.HexToAddress("0x4444444444444444444444444444444444444444") + for _, tc := range []struct { + name string + address string + wantErr bool + }{ + {name: "registered", address: executor.Hex()}, + {name: "missing", address: "0x5555555555555555555555555555555555555555", wantErr: true}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/solver-api/solver/identities" { + t.Fatalf("%s %s", r.Method, r.URL.Path) + } + if got := r.Header.Get("x-api-key"); got != "test-key" { + t.Fatalf("x-api-key = %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":[{"id":1,"createdAt":"now","updatedAt":"now","address":"` + + tc.address + `","solverId":1}]}`)) + })) + defer srv.Close() + + client := newOrderClient(srv.URL, "test-key", time.Second, 11155111) + err := client.validateExecutorRegistration(context.Background(), executor) + if (err != nil) != tc.wantErr { + t.Fatalf("validateExecutorRegistration() error = %v, wantErr %v", err, tc.wantErr) + } + }) + } +} + +func TestOrderClientReplaceSupportedContracts(t *testing.T) { + var gotBody map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut || r.URL.Path != "/api/v1/solver/supported-contracts" { + t.Fatalf("%s %s", r.Method, r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { + t.Fatalf("decode body: %v", err) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":{"oracle":[],"inputSettler":[],"outputSettler":[]}}`)) + })) + defer srv.Close() + + client := newOrderClient(srv.URL, "test-key", time.Second, 11155111) + err := client.replaceSupportedContracts( + context.Background(), + supportedContractsDTO( + lifiorder.ContractsByKindDto{}, + chainRef(11155111), + common.HexToAddress("0x1111111111111111111111111111111111111111"), + common.HexToAddress("0x2222222222222222222222222222222222222222"), + ), + ) + if err != nil { + t.Fatalf("replaceSupportedContracts: %v", err) + } + if got := gotBody["inputSettler"].([]any)[0].(map[string]any)["chain"]; got != "eip155:11155111" { + t.Fatalf("chain = %v", got) + } + if got := gotBody["oracle"].([]any)[0].(map[string]any)["address"]; got != "0x2222222222222222222222222222222222222222" { + t.Fatalf("oracle address = %v", got) + } +} + +func TestOrderClientEnsureSupportedContractsSkipsPutWhenPresent(t *testing.T) { + var putCalled bool + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/solver/supported-contracts" { + t.Fatalf("path = %s", r.URL.Path) + } + if r.Method == http.MethodPut { + putCalled = true + t.Fatal("unexpected PUT") + } + if r.Method != http.MethodGet { + t.Fatalf("method = %s", r.Method) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":{"oracle":[{"chain":"eip155:11155111","address":"0x2222222222222222222222222222222222222222"}],"inputSettler":[{"chain":"eip155:11155111","address":"0x1111111111111111111111111111111111111111"}],"outputSettler":[{"chain":"eip155:11155111","address":"0x2222222222222222222222222222222222222222"}]}}`)) + })) + defer srv.Close() + + client := newOrderClient(srv.URL, "test-key", time.Second, 11155111) + err := client.ensureSupportedContracts( + context.Background(), + 11155111, + common.HexToAddress("0x1111111111111111111111111111111111111111"), + common.HexToAddress("0x2222222222222222222222222222222222222222"), + ) + if err != nil { + t.Fatalf("ensureSupportedContracts: %v", err) + } + if putCalled { + t.Fatal("PUT was called") + } +} + +func TestOrderClientEnsureSupportedContractsPutsWhenMissing(t *testing.T) { + var methods []string + var gotBody map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/solver/supported-contracts" { + t.Fatalf("path = %s", r.URL.Path) + } + methods = append(methods, r.Method) + w.Header().Set("Content-Type", "application/json") + switch r.Method { + case http.MethodGet: + _, _ = w.Write([]byte(`{"data":{"oracle":[{"chain":"eip155:1","address":"0x3333333333333333333333333333333333333333"}],"inputSettler":[{"chain":"eip155:1","address":"0x4444444444444444444444444444444444444444"}],"outputSettler":[{"chain":"eip155:1","address":"0x5555555555555555555555555555555555555555"}]}}`)) + case http.MethodPut: + if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { + t.Fatalf("decode body: %v", err) + } + _, _ = w.Write([]byte(`{"data":{"oracle":[],"inputSettler":[],"outputSettler":[]}}`)) + default: + t.Fatalf("method = %s", r.Method) + } + })) + defer srv.Close() + + client := newOrderClient(srv.URL, "test-key", time.Second, 11155111) + err := client.ensureSupportedContracts( + context.Background(), + 11155111, + common.HexToAddress("0x1111111111111111111111111111111111111111"), + common.HexToAddress("0x2222222222222222222222222222222222222222"), + ) + if err != nil { + t.Fatalf("ensureSupportedContracts: %v", err) + } + if len(methods) != 2 || methods[0] != http.MethodGet || methods[1] != http.MethodPut { + t.Fatalf("methods = %v", methods) + } + inputSettlers := gotBody["inputSettler"].([]any) + if got := len(inputSettlers); got != 2 { + t.Fatalf("inputSettler count = %d", got) + } + if got := inputSettlers[0].(map[string]any)["address"]; got != "0x4444444444444444444444444444444444444444" { + t.Fatalf("preserved inputSettler address = %v", got) + } + if got := inputSettlers[1].(map[string]any)["address"]; got != "0x1111111111111111111111111111111111111111" { + t.Fatalf("configured inputSettler address = %v", got) + } + outputSettlers := gotBody["outputSettler"].([]any) + if got := len(outputSettlers); got != 2 { + t.Fatalf("outputSettler count = %d", got) + } + if got := outputSettlers[0].(map[string]any)["address"]; got != "0x5555555555555555555555555555555555555555" { + t.Fatalf("preserved outputSettler address = %v", got) + } + if got := outputSettlers[1].(map[string]any)["address"]; got != "0x2222222222222222222222222222222222222222" { + t.Fatalf("configured outputSettler address = %v", got) + } + oracles := gotBody["oracle"].([]any) + if got := len(oracles); got != 2 { + t.Fatalf("oracle count = %d", got) + } + if got := oracles[0].(map[string]any)["address"]; got != "0x3333333333333333333333333333333333333333" { + t.Fatalf("preserved oracle address = %v", got) + } + if got := oracles[1].(map[string]any)["address"]; got != "0x2222222222222222222222222222222222222222" { + t.Fatalf("configured oracle address = %v", got) + } +} diff --git a/internal/solvers/lifi/planning.go b/internal/solvers/lifi/planning.go new file mode 100644 index 00000000..88c7515e --- /dev/null +++ b/internal/solvers/lifi/planning.go @@ -0,0 +1,266 @@ +package lifi + +import ( + "context" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/liquidlane/discounts" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +type fillState struct { + snapshots fillSnapshotSet + discountQuotes []liquidlane.FillQuote + signedDiscounts map[common.Hash]*discounts.Signed + chainTime time.Time +} + +type preparedFill struct { + input types.FillInput + signedDiscounts map[common.Hash]*discounts.Signed +} + +func (s *Solver) processOrder(ctx context.Context, routes []route, order *submittedOrder) { + s.processOrderWithPending(ctx, routes, order, nil) +} + +func (s *Solver) processOrderWithPending( + ctx context.Context, + routes []route, + order *submittedOrder, + pending *pendingFillState, +) *pendingFill { + if !s.cfg.TokenPolicy.Allows(order.TokenIn) { + s.log.V(1).Info("order skipped: input token out of scope", + "orderId", order.OrderID, "quoteId", order.QuoteID, + "tokenIn", order.TokenIn.Hex(), "scope", s.cfg.TokenPolicy.Scope()) + return nil + } + if err := s.reader.validateZeroGovernanceFee(ctx, s.cfg.InputSettler); err != nil { + s.log.Error(err, "order skipped: governance fee invariant failed", + "orderId", order.OrderID, "quoteId", order.QuoteID, + "inputSettler", s.cfg.InputSettler.Hex()) + return nil + } + orderID, ok := s.openedOrderID(ctx, order) + if !ok { + return nil + } + reservationKey := orderID.Hex() + if pending != nil && pending.contains(reservationKey) { + s.log.V(1).Info("order skipped: already pending", "orderId", order.OrderID, + "onChainOrderId", orderID.Hex(), "quoteId", order.QuoteID) + return nil + } + prepared := s.prepareFill(ctx, routes, order, pending.reservedCapacity()) + if prepared == nil { + return nil + } + plan, err := s.strategy.DecideFill(ctx, prepared.input) + if err != nil { + s.log.Error(err, "order fill: strategy", "orderId", order.OrderID, "quoteId", order.QuoteID) + return nil + } + if plan == nil { + s.log.V(1).Info("order skipped: no immediate fill plan", "orderId", order.OrderID, + "quoteId", order.QuoteID, "routes", len(prepared.input.Quotes)) + return nil + } + if prepared.input.RequireSingleRoute && len(plan.Routes) != 1 { + s.log.Error(errors.New("strategy returned invalid multi-route fill for permissioned token"), + "order fill: reject strategy plan", "orderId", order.OrderID, "quoteId", order.QuoteID, + "tokenIn", order.TokenIn.Hex(), "routes", len(plan.Routes)) + return nil + } + calldata, err := buildFillCalldata(*order, orderID, plan, prepared.signedDiscounts) + if err != nil { + s.log.Error(err, "order fill: build calldata", "orderId", order.OrderID, "quoteId", order.QuoteID) + return nil + } + return s.submitFill(ctx, order, plan, calldata, prepared.input.MaxFeePerGas) +} + +func (s *Solver) openedOrderID(ctx context.Context, order *submittedOrder) (common.Hash, bool) { + orderID, err := s.reader.orderIdentifier(ctx, s.cfg.InputSettler, order.Order) + if err != nil { + s.log.Error(err, "order fill: identify order", "orderId", order.OrderID, "quoteId", order.QuoteID) + return common.Hash{}, false + } + status, err := s.reader.orderStatus(ctx, s.cfg.InputSettler, orderID) + if err != nil { + s.log.Error(err, "order fill: read initial order status", "orderId", order.OrderID, + "onChainOrderId", orderID.Hex(), "quoteId", order.QuoteID) + return common.Hash{}, false + } + if status != lifiOrderStatusDeposited { + s.log.Info("order skipped: on-chain order is not deposited", "orderId", order.OrderID, + "onChainOrderId", orderID.Hex(), "quoteId", order.QuoteID, "status", status) + return common.Hash{}, false + } + return orderID, true +} + +func (s *Solver) prepareFill( + ctx context.Context, + routes []route, + order *submittedOrder, + reservations map[liquidlane.CapacityID]*big.Int, +) *preparedFill { + pairRoutes := routesForPair(routes, order.TokenIn, order.TokenOut) + if len(pairRoutes) == 0 { + s.log.V(1).Info("order skipped: no configured route for pair", "orderId", order.OrderID, + "quoteId", order.QuoteID, "tokenIn", order.TokenIn.Hex(), "tokenOut", order.TokenOut.Hex()) + return nil + } + state, err := s.loadFillState(ctx, pairRoutes, order) + if err != nil { + s.log.Error(err, "order fill: prepare current state", "orderId", order.OrderID, "quoteId", order.QuoteID) + return nil + } + if state == nil { + return nil + } + maxFeePerGas, err := s.readMaxFeePerGas(ctx) + if err != nil { + s.log.Error(err, "order fill: read max fee per gas", "orderId", order.OrderID, "quoteId", order.QuoteID) + return nil + } + quotes := append([]liquidlane.FillQuote(nil), state.snapshots.Direct...) + quotes = append(quotes, state.discountQuotes...) + return &preparedFill{ + input: types.FillInput{ + OrderID: order.OrderID, + QuoteID: order.QuoteID, + Solver: s.cfg.Executor, + TokenIn: order.TokenIn, + TokenOut: order.TokenOut, + AmountIn: order.AmountIn, + OutputAmount: order.OutputAmount, + OutputContext: order.Output.Context, + Expires: order.Order.Expires, + FillDeadline: order.Order.FillDeadline, + RequireSingleRoute: s.cfg.TokenPolicy.RequiresSingleRoute(order.TokenIn), + Quotes: quotes, + Reservations: reservations, + GasSnapshot: state.snapshots.GasSnapshot, + GasPrices: state.snapshots.GasPrices, + MaxFeePerGas: maxFeePerGas, + ChainTime: state.chainTime, + }, + signedDiscounts: state.signedDiscounts, + } +} + +func (s *Solver) loadFillState( + ctx context.Context, + routes []route, + order *submittedOrder, +) (*fillState, error) { + snapshots, chainTime, err := s.readFillSnapshot(ctx, routes, order) + if err != nil { + return nil, err + } + if s.skipExpiredOrder(order, chainTime) { + return nil, nil + } + state := &fillState{snapshots: snapshots, chainTime: chainTime} + if s.discounts == nil || len(snapshots.DiscountBases) == 0 { + return state, nil + } + + resolveCtx, cancel := context.WithTimeout(ctx, s.cfg.OrderServer.HTTPTimeout) + state.discountQuotes, state.signedDiscounts = s.fillDiscountQuotes( + resolveCtx, snapshots.DiscountBases, chainTime, + ) + cancel() + + state.snapshots, state.chainTime, err = s.readFillSnapshot(ctx, routes, order) + if err != nil { + return nil, errors.Errorf("refresh after private discount resolution: %w", err) + } + if s.skipExpiredOrder(order, state.chainTime) { + return nil, nil + } + state.discountQuotes = refreshResolvedDiscountQuotes( + state.discountQuotes, + state.signedDiscounts, + state.snapshots.DiscountBases, + state.chainTime, + s.logInvalidDiscount, + ) + return state, nil +} + +func (s *Solver) readFillSnapshot( + ctx context.Context, + routes []route, + order *submittedOrder, +) (fillSnapshotSet, time.Time, error) { + chainTime, err := s.now(ctx) + if err != nil { + return fillSnapshotSet{}, time.Time{}, errors.Errorf("read latest block time: %w", err) + } + snapshots, err := s.reader.fillSnapshots(ctx, routes, s.cfg.Executor, order.TokenIn, order.AmountIn, chainTime) + if err != nil { + return fillSnapshotSet{}, time.Time{}, errors.Errorf("read routes: %w", err) + } + return snapshots, chainTime, nil +} + +func (s *Solver) skipExpiredOrder(order *submittedOrder, chainTime time.Time) bool { + if !orderExpired(order, chainTime) { + return false + } + s.log.Info("order skipped: expired", "orderId", order.OrderID, "quoteId", order.QuoteID, + "chainTime", uint32Unix(chainTime), "expires", order.Order.Expires, + "fillDeadline", order.Order.FillDeadline) + return true +} + +func routesForPair(routes []route, tokenIn, tokenOut common.Address) []route { + out := make([]route, 0, len(routes)) + for _, candidate := range routes { + if candidate.TokenIn == tokenIn && candidate.TokenOut == tokenOut { + out = append(out, candidate) + } + } + return out +} + +func (s *Solver) readMaxFeePerGas(ctx context.Context) (*big.Int, error) { + if s.maxFeePerGas == nil { + return nil, errors.New("max fee per gas reader is not configured") + } + maxFee, err := s.maxFeePerGas(ctx) + if err != nil { + return nil, errors.Errorf("max fee per gas: %w", err) + } + if maxFee == nil || maxFee.Sign() <= 0 { + return nil, errors.New("max fee per gas must be positive") + } + return new(big.Int).Set(maxFee), nil +} + +func uint32Unix(t time.Time) uint32 { + unix := t.Unix() + if unix <= 0 { + return 0 + } + if unix > int64(^uint32(0)) { + return ^uint32(0) + } + return uint32(unix) +} + +func orderExpired(order *submittedOrder, now time.Time) bool { + chainTime := uint32Unix(now) + if order.Order.Expires != 0 && chainTime >= order.Order.Expires { + return true + } + return order.Order.FillDeadline != 0 && chainTime >= order.Order.FillDeadline +} diff --git a/internal/solvers/lifi/quotes.go b/internal/solvers/lifi/quotes.go new file mode 100644 index 00000000..605f2db3 --- /dev/null +++ b/internal/solvers/lifi/quotes.go @@ -0,0 +1,344 @@ +package lifi + +import ( + "context" + "math/big" + "sort" + "strconv" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" + "github.com/symbioticfi/vault-solver/internal/tokenpolicy" +) + +type quoteSubmitter interface { + submitQuotes(ctx context.Context, quotes []types.Quote) error +} + +type quoteEvent struct { + orderKey string + reservations []quoteReservation + release bool +} + +type quoteReservation struct { + capacityID liquidlane.CapacityID + amountOut *big.Int +} + +type quotePairKey struct { + fromAsset common.Address + toAsset common.Address + fromDecimals int + toDecimals int +} + +type quotePairState struct { + fingerprint string + expiry int64 + quotes []types.Quote +} + +type quoteState struct { + active map[quotePairKey]quotePairState + reservations map[string][]quoteReservation + renewBefore time.Duration +} + +func (s *Solver) quoteLoop(ctx context.Context, routes []route, events <-chan quoteEvent) error { + ticker := time.NewTicker(s.cfg.QuoteInterval) + defer ticker.Stop() + + state := newQuoteState(max(s.cfg.QuoteInterval, s.cfg.QuoteTTL/3)) + s.refreshQuotes(ctx, routes, state) + var lastBlock uint64 + for { + select { + case <-ctx.Done(): + return ctx.Err() + case event := <-events: + if state.apply(event) { + s.refreshQuotes(ctx, routes, state) + } + case <-ticker.C: + if s.shouldRefreshQuotes(ctx, state, &lastBlock) { + s.refreshQuotes(ctx, routes, state) + } + } + } +} + +func (s *Solver) shouldRefreshQuotes(ctx context.Context, state *quoteState, lastBlock *uint64) bool { + if s.cfg.QuoteRefreshMode != quoteRefreshModeBlock { + return true + } + needsRenewal := state.needsRenewal(s.wallNow()) + block, err := s.reader.latestBlockNumber(ctx) + if err != nil { + s.log.Error(err, "quote refresh: read latest block") + return needsRenewal + } + if block == *lastBlock && !needsRenewal { + return false + } + *lastBlock = block + return true +} + +func (s *Solver) refreshQuotes(ctx context.Context, routes []route, state *quoteState) { + chainTime, err := s.now(ctx) + if err != nil { + s.log.Error(err, "quote refresh: read latest block time") + return + } + snapshotSet, err := s.reader.quoteSnapshots(ctx, routes, s.cfg.Executor, chainTime) + if err != nil { + s.log.Error(err, "quote refresh: read routes") + return + } + maxFeePerGas, err := s.readMaxFeePerGas(ctx) + if err != nil { + s.log.Error(err, "quote refresh: read max fee per gas") + return + } + direct := filterQuoteInventory(snapshotSet.Direct, s.cfg.TokenPolicy) + discountBases := filterQuoteInventory(snapshotSet.DiscountBases, s.cfg.TokenPolicy) + inventory := append([]liquidlane.Inventory(nil), direct...) + inventory = append(inventory, s.quoteDiscountInventories(ctx, discountBases, chainTime)...) + serverTime := s.wallNow() + out, err := s.strategy.DecideQuotes(ctx, types.QuoteInput{ + Solver: s.cfg.Executor, + Inventory: inventory, + Reservations: state.reservedCapacity(), + SingleRouteTokens: s.cfg.TokenPolicy.SingleRouteTokens(), + GasSnapshot: snapshotSet.GasSnapshot, + GasPrices: snapshotSet.GasPrices, + MaxFeePerGas: maxFeePerGas, + ChainTime: chainTime, + ServerTime: serverTime, + QuoteExpiresAt: serverTime.Add(s.cfg.QuoteTTL), + }) + if err != nil { + s.log.Error(err, "quote refresh: strategy") + return + } + if len(out.Quotes) == 0 { + s.log.V(1).Info("quote refresh: strategy produced no quotes", "routes", len(inventory)) + } + removed, err := state.reconcile(ctx, s.orders, out.Quotes, serverTime) + if err != nil { + s.log.Error(err, "quote refresh: submit quotes", "quotes", len(out.Quotes)) + return + } + s.log.Info("quotes reconciled", "quotes", len(out.Quotes), "removedPairs", removed, "routes", len(inventory)) +} + +func filterQuoteInventory(inventory []liquidlane.Inventory, policy tokenpolicy.Policy) []liquidlane.Inventory { + filtered := make([]liquidlane.Inventory, 0, len(inventory)) + for _, item := range inventory { + if policy.Allows(item.TokenIn) { + filtered = append(filtered, item) + } + } + return filtered +} + +func newQuoteState(renewBefore time.Duration) *quoteState { + return "eState{ + active: make(map[quotePairKey]quotePairState), reservations: make(map[string][]quoteReservation), + renewBefore: renewBefore, + } +} + +func (s *quoteState) needsRenewal(now time.Time) bool { + deadline := now.Add(s.renewBefore).Unix() + for _, pair := range s.active { + if pair.expiry <= deadline { + return true + } + } + return false +} + +func (s *quoteState) apply(event quoteEvent) bool { + if event.orderKey == "" { + return false + } + if event.release { + if _, ok := s.reservations[event.orderKey]; !ok { + return false + } + delete(s.reservations, event.orderKey) + return true + } + if len(event.reservations) == 0 { + return false + } + reservations := make([]quoteReservation, 0, len(event.reservations)) + for _, reservation := range event.reservations { + if reservation.capacityID == "" || reservation.amountOut == nil || reservation.amountOut.Sign() <= 0 { + return false + } + reservations = append(reservations, quoteReservation{ + capacityID: reservation.capacityID, amountOut: liquidlane.CloneBig(reservation.amountOut), + }) + } + if current, ok := s.reservations[event.orderKey]; ok && equalReservations(current, reservations) { + return false + } + s.reservations[event.orderKey] = reservations + return true +} + +func equalReservations(a, b []quoteReservation) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].capacityID != b[i].capacityID || a[i].amountOut.Cmp(b[i].amountOut) != 0 { + return false + } + } + return true +} + +func (s *quoteState) reservedCapacity() map[liquidlane.CapacityID]*big.Int { + reserved := make(map[liquidlane.CapacityID]*big.Int) + for _, orderReservations := range s.reservations { + addReservations(reserved, orderReservations) + } + return reserved +} + +func addReservations( + total map[liquidlane.CapacityID]*big.Int, + reservations []quoteReservation, +) { + for _, reservation := range reservations { + if total[reservation.capacityID] == nil { + total[reservation.capacityID] = new(big.Int) + } + total[reservation.capacityID].Add(total[reservation.capacityID], reservation.amountOut) + } +} + +func (s *quoteState) reconcile( + ctx context.Context, + submitter quoteSubmitter, + quotes []types.Quote, + now time.Time, +) (int, error) { + next := indexQuotePairs(quotes) + expire := make([]quotePairKey, 0) + publish := make(map[quotePairKey]bool, len(next)) + for key, current := range s.active { + upcoming, ok := next[key] + if !ok { + expire = append(expire, key) + continue + } + if shouldReplaceQuotePair(current, upcoming, now, s.renewBefore) { + publish[key] = true + } + } + for key := range next { + if _, ok := s.active[key]; !ok { + publish[key] = true + } + } + publishKeys := make([]quotePairKey, 0, len(publish)) + for key, enabled := range publish { + if enabled { + publishKeys = append(publishKeys, key) + } + } + sort.Slice(publishKeys, func(i, j int) bool { + return quotePairKeyString(publishKeys[i]) < quotePairKeyString(publishKeys[j]) + }) + sort.Slice(expire, func(i, j int) bool { return quotePairKeyString(expire[i]) < quotePairKeyString(expire[j]) }) + toPublish := make([]types.Quote, 0, len(quotes)+len(expire)) + for _, key := range expire { + for _, quote := range s.active[key].quotes { + quote.Expiry = now.Add(-time.Second).Unix() + toPublish = append(toPublish, quote) + } + } + for _, key := range publishKeys { + toPublish = append(toPublish, next[key].quotes...) + } + if len(toPublish) != 0 { + if err := submitter.submitQuotes(ctx, toPublish); err != nil { + return len(expire), err + } + } + for _, key := range expire { + delete(s.active, key) + } + for _, key := range publishKeys { + s.active[key] = next[key] + } + return len(expire), nil +} + +func shouldReplaceQuotePair(current, upcoming quotePairState, now time.Time, renewBefore time.Duration) bool { + if current.fingerprint != upcoming.fingerprint || upcoming.expiry < current.expiry { + return true + } + return current.expiry <= now.Add(renewBefore).Unix() +} + +func indexQuotePairs(quotes []types.Quote) map[quotePairKey]quotePairState { + grouped := make(map[quotePairKey][]types.Quote) + for _, quote := range quotes { + key := pairKey(quote) + grouped[key] = append(grouped[key], quote) + } + + out := make(map[quotePairKey]quotePairState, len(grouped)) + for key, pairQuotes := range grouped { + fingerprints := make([]string, 0, len(pairQuotes)) + expiry := int64(0) + for _, quote := range pairQuotes { + ranges := make([]string, 0, len(quote.Ranges)) + for _, r := range quote.Ranges { + ranges = append(ranges, bigString(r.MinAmount)+":"+bigString(r.MaxAmount)+":"+r.Quote) + } + fingerprints = append(fingerprints, strings.ToLower(quote.ExclusiveFor.Hex())+":"+strings.Join(ranges, ",")) + if expiry == 0 || quote.Expiry < expiry { + expiry = quote.Expiry + } + } + sort.Strings(fingerprints) + out[key] = quotePairState{ + fingerprint: strings.Join(fingerprints, "|"), + expiry: expiry, + quotes: append([]types.Quote(nil), pairQuotes...), + } + } + return out +} + +func pairKey(quote types.Quote) quotePairKey { + return quotePairKey{ + fromAsset: quote.FromAsset, toAsset: quote.ToAsset, + fromDecimals: quote.FromDecimals, toDecimals: quote.ToDecimals, + } +} + +func quotePairKeyString(key quotePairKey) string { + return strings.Join([]string{ + strings.ToLower(key.fromAsset.Hex()), strings.ToLower(key.toAsset.Hex()), + strconv.Itoa(key.fromDecimals), strconv.Itoa(key.toDecimals), + }, ":") +} + +func bigString(n *big.Int) string { + if n == nil { + return "" + } + return n.String() +} diff --git a/internal/solvers/lifi/quotes_test.go b/internal/solvers/lifi/quotes_test.go new file mode 100644 index 00000000..6961f484 --- /dev/null +++ b/internal/solvers/lifi/quotes_test.go @@ -0,0 +1,217 @@ +package lifi + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + "github.com/go-logr/logr" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" + "github.com/symbioticfi/vault-solver/internal/tokenpolicy" +) + +type fakeQuoteSubmitter struct { + calls [][]types.Quote +} + +func TestFilterQuoteInventoryAppliesTokenScope(t *testing.T) { + permissioned := common.HexToAddress("0x1111111111111111111111111111111111111111") + permissionless := common.HexToAddress("0x2222222222222222222222222222222222222222") + inventory := []liquidlane.Inventory{ + {Route: liquidlane.Route{TokenIn: permissioned}}, + {Route: liquidlane.Route{TokenIn: permissionless}}, + } + + tests := []struct { + name string + scope tokenpolicy.Scope + want common.Address + }{ + {"permissioned", tokenpolicy.Permissioned, permissioned}, + {"permissionless", tokenpolicy.Permissionless, permissionless}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filtered := filterQuoteInventory(inventory, testTokenPolicy(t, tt.scope, permissioned)) + if len(filtered) != 1 || filtered[0].TokenIn != tt.want { + t.Fatalf("filtered inventory = %+v", filtered) + } + }) + } +} + +func (f *fakeQuoteSubmitter) submitQuotes(_ context.Context, quotes []types.Quote) error { + copyOfQuotes := append([]types.Quote(nil), quotes...) + f.calls = append(f.calls, copyOfQuotes) + return nil +} + +func TestQuoteStatePublishesAndReplacesChangedTopology(t *testing.T) { + routeItem := testQuoteRoute() + state := newQuoteState(30 * time.Second) + submitter := &fakeQuoteSubmitter{} + now := time.Unix(1_800_000_000, 0) + + first := testStandingQuote(routeItem, 1_000) + removed, err := state.reconcile(context.Background(), submitter, []types.Quote{first}, now) + if err != nil { + t.Fatalf("first reconcile: %v", err) + } + if removed != 0 || len(submitter.calls) != 1 || len(submitter.calls[0][0].Ranges) == 0 { + t.Fatalf("initial reconcile: removed=%d calls=%#v", removed, submitter.calls) + } + + second := testStandingQuote(routeItem, 1_000) + second.Ranges[0].Quote = "0.98" + if _, err := state.reconcile(context.Background(), submitter, []types.Quote{second}, now); err != nil { + t.Fatalf("same topology reconcile: %v", err) + } + if len(submitter.calls) != 2 || submitter.calls[1][0].Ranges[0].Quote != "0.98" { + t.Fatalf("changed price calls = %#v", submitter.calls) + } + + changed := testStandingQuote(routeItem, 2_000) + removed, err = state.reconcile(context.Background(), submitter, []types.Quote{changed}, now) + if err != nil { + t.Fatalf("changed topology reconcile: %v", err) + } + if removed != 0 || len(submitter.calls) != 3 || submitter.calls[2][0].Ranges[0].MaxAmount.String() != "2000" { + t.Fatalf("changed topology: removed=%d calls=%#v", removed, submitter.calls) + } +} + +func TestQuoteStateSkipsUnchangedPairUntilRenewalWindow(t *testing.T) { + routeItem := testQuoteRoute() + state := newQuoteState(30 * time.Second) + submitter := &fakeQuoteSubmitter{} + now := time.Unix(1_800_000_000, 0) + quote := testStandingQuote(routeItem, 1_000) + + if _, err := state.reconcile(context.Background(), submitter, []types.Quote{quote}, now); err != nil { + t.Fatalf("publish: %v", err) + } + refreshed := testStandingQuote(routeItem, 1_000) + refreshed.Expiry += 60 + if _, err := state.reconcile(context.Background(), submitter, []types.Quote{refreshed}, now.Add(30*time.Second)); err != nil { + t.Fatalf("unchanged: %v", err) + } + if len(submitter.calls) != 1 { + t.Fatalf("unchanged pair was republished: calls=%d", len(submitter.calls)) + } + if _, err := state.reconcile(context.Background(), submitter, []types.Quote{refreshed}, now.Add(90*time.Second)); err != nil { + t.Fatalf("renew: %v", err) + } + if len(submitter.calls) != 2 || submitter.calls[1][0].Ranges[0].MaxAmount.String() != "1000" { + t.Fatalf("renew calls = %#v", submitter.calls) + } +} + +func TestQuoteStateNeedsRenewalWithoutNewBlock(t *testing.T) { + now := time.Unix(1_800_000_000, 0) + state := newQuoteState(30 * time.Second) + state.active[quotePairKey{}] = quotePairState{expiry: now.Add(time.Minute).Unix()} + + if state.needsRenewal(now) { + t.Fatal("quote entered renewal window too early") + } + if !state.needsRenewal(now.Add(30 * time.Second)) { + t.Fatal("quote should renew by wall clock even without a new block") + } +} + +func TestShouldRefreshQuotesInBlockMode(t *testing.T) { + now := time.Unix(1_800_000_000, 0) + state := newQuoteState(30 * time.Second) + state.active[quotePairKey{}] = quotePairState{expiry: now.Add(time.Minute).Unix()} + solver := &Solver{ + cfg: &Config{QuoteRefreshMode: quoteRefreshModeBlock}, + reader: fakeLifiReader{latestBlock: 10}, + wallNow: func() time.Time { return now }, + log: logr.Discard(), + } + lastBlock := uint64(10) + + if solver.shouldRefreshQuotes(context.Background(), state, &lastBlock) { + t.Fatal("unchanged block outside renewal window should not refresh") + } + solver.reader = fakeLifiReader{latestBlock: 11} + if !solver.shouldRefreshQuotes(context.Background(), state, &lastBlock) || lastBlock != 11 { + t.Fatalf("new block was not observed: lastBlock=%d", lastBlock) + } + state.active[quotePairKey{}] = quotePairState{expiry: now.Add(30 * time.Second).Unix()} + solver.reader = fakeLifiReader{latestBlockErr: errors.New("rpc unavailable")} + if !solver.shouldRefreshQuotes(context.Background(), state, &lastBlock) { + t.Fatal("renewal should proceed when the block-number read fails") + } +} + +func TestQuoteStateRemovesPairWhenStrategyStopsQuoting(t *testing.T) { + routeItem := testQuoteRoute() + state := newQuoteState(30 * time.Second) + submitter := &fakeQuoteSubmitter{} + now := time.Unix(1_800_000_000, 0) + if _, err := state.reconcile(context.Background(), submitter, []types.Quote{testStandingQuote(routeItem, 1_000)}, now); err != nil { + t.Fatalf("publish: %v", err) + } + submitter.calls = nil + + removed, err := state.reconcile(context.Background(), submitter, nil, now) + if err != nil { + t.Fatalf("remove: %v", err) + } + if removed != 1 || len(submitter.calls) != 1 || len(submitter.calls[0]) != 1 || + len(submitter.calls[0][0].Ranges) == 0 || submitter.calls[0][0].Expiry >= now.Unix() { + t.Fatalf("remove: removed=%d calls=%#v", removed, submitter.calls) + } +} + +func TestQuoteStateSubtractsAndReleasesReservations(t *testing.T) { + routeItem := testQuoteRoute() + state := newQuoteState(30 * time.Second) + reservation := quoteReservation{capacityID: routeItem.CapacityID, amountOut: big.NewInt(250)} + + if !state.apply(quoteEvent{orderKey: "order-1", reservations: []quoteReservation{reservation}}) { + t.Fatal("expected reservation change") + } + reserved := state.reservedCapacity() + if reserved[routeItem.CapacityID].String() != "250" { + t.Fatalf("reserved = %#v", reserved) + } + if state.apply(quoteEvent{orderKey: "order-1", reservations: []quoteReservation{reservation}}) { + t.Fatal("identical reservation should not trigger refresh") + } + if !state.apply(quoteEvent{orderKey: "order-1", release: true}) { + t.Fatal("expected release change") + } + if len(state.reservedCapacity()) != 0 { + t.Fatalf("released reservations = %#v", state.reservedCapacity()) + } +} + +func testQuoteRoute() route { + return liquidlane.NewRoute( + 11155111, + common.HexToAddress("0x1111111111111111111111111111111111111111"), + common.HexToAddress("0x2222222222222222222222222222222222222222"), + common.HexToAddress("0x3333333333333333333333333333333333333333"), + common.HexToAddress("0x4444444444444444444444444444444444444444"), + 6, + 6, + ) +} + +func testStandingQuote(route route, maxAmount int64) types.Quote { + return types.Quote{ + FromAsset: route.TokenIn, ToAsset: route.TokenOut, + FromDecimals: route.TokenInDecimals, ToDecimals: route.TokenOutDecimals, + Ranges: []types.QuoteRange{{ + MinAmount: big.NewInt(100), MaxAmount: big.NewInt(maxAmount), Quote: "0.99", + }}, + Expiry: 1_800_000_120, + } +} diff --git a/internal/solvers/lifi/solver.go b/internal/solvers/lifi/solver.go new file mode 100644 index 00000000..31f37b27 --- /dev/null +++ b/internal/solvers/lifi/solver.go @@ -0,0 +1,172 @@ +// Package lifi implements the LI.FI same-chain intent solver. It publishes LiquidLane-backed +// standing quotes to the LI.FI order server and listens for matched escrow orders. +package lifi + +import ( + "context" + "math/big" + "os" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + "github.com/go-logr/logr" + "golang.org/x/sync/errgroup" + "gopkg.in/yaml.v3" + + "github.com/symbioticfi/vault-solver/api/bindings/lifi/inputsettler" + "github.com/symbioticfi/vault-solver/internal/liquidlane/discounts" + "github.com/symbioticfi/vault-solver/internal/solver" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" + "github.com/symbioticfi/vault-solver/internal/txmanager" +) + +const Name = "lifi-samechain" + +const lifiOrderStatusDeposited uint8 = 1 + +const quoteEventCapacity = 128 + +//nolint:gochecknoinits // self-registration with the solver framework is the intended plugin pattern. +func init() { + solver.Register(Name, factory) +} + +type Solver struct { + cfg *Config + chainID int64 + reader chainReader + strategy types.Strategy + caller common.Address + orders *orderClient + feed *orderFeed + txm txSender + log logr.Logger + now func(context.Context) (time.Time, error) + maxFeePerGas func(context.Context) (*big.Int, error) + wallNow func() time.Time + quoteEvents chan quoteEvent + discounts discountClient +} + +type chainReader interface { + resolveRoutes(ctx context.Context, adapters []common.Address) ([]route, error) + validateExecutor( + ctx context.Context, + executor, inputSettler, outputSettler, caller common.Address, + ) error + validateZeroGovernanceFee(ctx context.Context, inputSettler common.Address) error + validateDirectAuthorization(ctx context.Context, executor common.Address, routes []route) error + validateGasTokens(routes []route) error + quoteSnapshots(ctx context.Context, routes []route, executor common.Address, chainTime time.Time) (quoteSnapshotSet, error) + fillSnapshots( + ctx context.Context, routes []route, executor, tokenIn common.Address, amountIn *big.Int, chainTime time.Time, + ) (fillSnapshotSet, error) + orderIdentifier(ctx context.Context, inputSettler common.Address, order inputsettler.StandardOrder) (common.Hash, error) + orderStatus(ctx context.Context, inputSettler common.Address, orderID common.Hash) (uint8, error) + latestBlockNumber(ctx context.Context) (uint64, error) + latestBlockTime(ctx context.Context) (time.Time, error) +} + +type txSender interface { + SendAsync(ctx context.Context, req txmanager.Request) (<-chan txmanager.Result, bool) +} + +func factory(raw yaml.Node, deps solver.Deps) (solver.Solver, error) { + cfg, err := parseConfig(raw) + if err != nil { + return nil, err + } + apiKey := os.Getenv(cfg.OrderServer.APIKeyEnv) + if apiKey == "" { + return nil, errors.Errorf("%s: order server api key env %q is empty", Name, cfg.OrderServer.APIKeyEnv) + } + + log := deps.Log.WithName(Name) + chainID := deps.Chain.ChainID().Int64() + strategy, err := newStrategy(cfg.Strategy, deps.Chain, log) + if err != nil { + return nil, err + } + reader, err := newReader(deps.Chain, log, cfg.Gas) + if err != nil { + return nil, err + } + result := &Solver{ + cfg: cfg, + chainID: chainID, + reader: reader, + strategy: strategy, + caller: deps.Signer.Address(), + orders: newOrderClient(cfg.OrderServer.BaseURL, apiKey, cfg.OrderServer.HTTPTimeout, chainID), + feed: newOrderFeed(cfg.OrderServer.WSURL, apiKey, log), + txm: deps.TxManager, + log: log, + now: reader.latestBlockTime, + maxFeePerGas: deps.TxManager.MaxFeePerGas, + wallNow: time.Now, + } + if cfg.usesDiscounts() { + result.discounts = discounts.NewClient(cfg.DiscountsURL) + } + return result, nil +} + +func (s *Solver) Name() string { return Name } + +func (s *Solver) Run(ctx context.Context) error { + routes, err := s.reader.resolveRoutes(ctx, s.cfg.Adapters) + if err != nil { + return errors.Errorf("lifi: resolve routes: %w", err) + } + if len(routes) == 0 { + return errors.New("lifi: no quoteable routes resolved from configured adapters") + } + if err := s.reader.validateGasTokens(routes); err != nil { + return errors.Errorf("lifi: validate gas oracles: %w", err) + } + if err := s.reader.validateExecutor( + ctx, s.cfg.Executor, s.cfg.InputSettler, s.cfg.OutputSettler, s.caller, + ); err != nil { + return errors.Errorf("lifi: validate executor: %w", err) + } + if err := s.reader.validateZeroGovernanceFee(ctx, s.cfg.InputSettler); err != nil { + return errors.Errorf("lifi: validate governance fee: %w", err) + } + if !s.cfg.usesDiscounts() { + if err := s.reader.validateDirectAuthorization(ctx, s.cfg.Executor, routes); err != nil { + startupErr := errors.Errorf("lifi: validate direct authorization: %w", err) + s.log.Error(startupErr, "external adapter authorization failed", + "solverMode", s.cfg.SolverMode, + "executor", s.cfg.Executor.Hex(), + "adapters", s.cfg.Adapters, + ) + return startupErr + } + } + if err := s.orders.validateExecutorRegistration(ctx, s.cfg.Executor); err != nil { + return err + } + if err := s.orders.ensureSupportedContracts(ctx, s.chainID, s.cfg.InputSettler, s.cfg.OutputSettler); err != nil { + return err + } + + s.log.Info("starting", + "routes", len(routes), + "baseUrl", s.cfg.OrderServer.BaseURL, + "wsUrl", s.cfg.OrderServer.WSURL, + "quoteRefreshMode", s.cfg.QuoteRefreshMode, + "quoteInterval", s.cfg.QuoteInterval.String(), + "quoteTTL", s.cfg.QuoteTTL.String(), + "solverMode", s.cfg.SolverMode, + "tokensToQuote", s.cfg.TokenPolicy.Scope(), + "executor", s.cfg.Executor.Hex(), + "caller", s.caller.Hex(), + ) + + s.quoteEvents = make(chan quoteEvent, quoteEventCapacity) + g, gctx := errgroup.WithContext(ctx) + g.Go(func() error { return s.quoteLoop(gctx, routes, s.quoteEvents) }) + g.Go(func() error { return s.runOrderFeed(gctx, routes) }) + return g.Wait() +} diff --git a/internal/solvers/lifi/solver_test.go b/internal/solvers/lifi/solver_test.go new file mode 100644 index 00000000..ff6dbaea --- /dev/null +++ b/internal/solvers/lifi/solver_test.go @@ -0,0 +1,662 @@ +package lifi + +import ( + "context" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/go-errors/errors" + "github.com/go-logr/logr" + "github.com/go-logr/logr/funcr" + + "github.com/symbioticfi/vault-solver/api/bindings/lifi/inputsettler" + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/liquidlane/discounts" + defaultstrategy "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/default" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" + "github.com/symbioticfi/vault-solver/internal/tokenpolicy" + "github.com/symbioticfi/vault-solver/internal/txmanager" +) + +func TestRunLogsExternalAdapterAuthorizationFailure(t *testing.T) { + adapter := common.HexToAddress("0x1111111111111111111111111111111111111111") + executor := common.HexToAddress("0x2222222222222222222222222222222222222222") + var logs []string + s := &Solver{ + cfg: &Config{ + SolverMode: solverModeExternal, + Adapters: []common.Address{adapter}, + Executor: executor, + }, + reader: fakeLifiReader{ + routes: []route{{Adapter: adapter}}, + directAuthErr: errors.New("executor is not an authorized filler"), + }, + log: funcr.NewJSON(func(entry string) { logs = append(logs, entry) }, funcr.Options{}), + } + + err := s.Run(t.Context()) + if err == nil || !strings.Contains(err.Error(), "validate direct authorization") { + t.Fatalf("Run() error = %v", err) + } + logged := strings.Join(logs, "\n") + if !strings.Contains(logged, "external adapter authorization failed") || + !strings.Contains(logged, executor.Hex()) || + !strings.Contains(logged, `"error"`) { + t.Fatalf("authorization failure was not logged as an error: %s", logged) + } +} + +func TestRunRejectsNonZeroGovernanceFee(t *testing.T) { + adapter := common.HexToAddress("0x1111111111111111111111111111111111111111") + s := &Solver{ + cfg: &Config{Adapters: []common.Address{adapter}}, + reader: fakeLifiReader{ + routes: []route{{Adapter: adapter}}, + governanceFeeErr: errors.New("input settler governance fee is 1, expected zero"), + }, + log: logr.Discard(), + } + + err := s.Run(t.Context()) + if err == nil || !strings.Contains(err.Error(), "validate governance fee") { + t.Fatalf("Run() error = %v", err) + } +} + +type fakeLifiTxSender struct { + reqs []txmanager.Request + results []chan txmanager.Result + result txmanager.Result + reject bool + hold bool + onSend func(int, chan<- txmanager.Result) +} + +func (f *fakeLifiTxSender) SendAsync( + _ context.Context, + req txmanager.Request, +) (<-chan txmanager.Result, bool) { + if f.reject { + return nil, false + } + f.reqs = append(f.reqs, req) + result := make(chan txmanager.Result, 1) + f.results = append(f.results, result) + if f.onSend != nil { + f.onSend(len(f.reqs), result) + } + if !f.hold { + result <- f.fillResult() + } + return result, true +} + +func (f *fakeLifiTxSender) fillResult() txmanager.Result { + if f.result.Err != nil || f.result.Receipt != nil || f.result.Hash != (common.Hash{}) { + return f.result + } + return txmanager.Result{ + Hash: common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + Receipt: ðtypes.Receipt{Status: ethtypes.ReceiptStatusSuccessful}, + } +} + +type fixedFillStrategy struct { + plan *types.FillPlan +} + +func (s fixedFillStrategy) DecideQuotes(context.Context, types.QuoteInput) (types.QuoteOutput, error) { + return types.QuoteOutput{}, nil +} + +func (s fixedFillStrategy) DecideFill(context.Context, types.FillInput) (*types.FillPlan, error) { + return s.plan, nil +} + +type reservationAwareFillStrategy struct { + plan *types.FillPlan + inputs chan types.FillInput +} + +func (s reservationAwareFillStrategy) DecideQuotes( + context.Context, + types.QuoteInput, +) (types.QuoteOutput, error) { + return types.QuoteOutput{}, nil +} + +func (s reservationAwareFillStrategy) DecideFill( + _ context.Context, + input types.FillInput, +) (*types.FillPlan, error) { + s.inputs <- input + if len(input.Reservations) != 0 { + return nil, nil + } + return s.plan, nil +} + +func TestProcessOrderSubmitsImmediateFill(t *testing.T) { + fixture := immediateTestSetup(t) + strategy, err := defaultstrategy.New(defaultstrategy.Config{}) + if err != nil { + t.Fatalf("New strategy: %v", err) + } + txm := &fakeLifiTxSender{} + s := newProcessTestSolver(fixture.cfg, fixture.caller, txm, strategy, fixture.tokenIn, fixture.tokenOut, fixture.adapter, lifiOrderStatusDeposited) + + s.processOrder(context.Background(), testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut)) + if len(txm.reqs) != 1 { + t.Fatalf("txmanager.Send calls = %d, want 1", len(txm.reqs)) + } + if txm.reqs[0].To != fixture.cfg.Executor || txm.reqs[0].Label != "lifi-fill" || len(txm.reqs[0].Data) == 0 { + t.Fatalf("bad fill request: %+v", txm.reqs[0]) + } + if txm.reqs[0].MaxFeePerGas == nil || txm.reqs[0].MaxFeePerGas.Cmp(big.NewInt(1)) != 0 { + t.Fatalf("fill max fee per gas = %v, want 1", txm.reqs[0].MaxFeePerGas) + } +} + +func TestProcessOrderSkipsInputTokenOutsideScopeBeforeChainReads(t *testing.T) { + fixture := immediateTestSetup(t) + otherToken := common.HexToAddress("0x9999999999999999999999999999999999999999") + fixture.cfg.TokenPolicy = testTokenPolicy(t, tokenpolicy.Permissioned, otherToken) + txm := &fakeLifiTxSender{} + s := newProcessTestSolver( + fixture.cfg, fixture.caller, txm, fixedFillStrategy{}, + fixture.tokenIn, fixture.tokenOut, fixture.adapter, lifiOrderStatusDeposited, + ) + orderIDReads := 0 + s.reader = fakeLifiReader{orderIDFn: func(inputsettler.StandardOrder) common.Hash { + orderIDReads++ + return common.Hash{} + }} + + s.processOrder( + context.Background(), testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), + testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut), + ) + if orderIDReads != 0 || len(txm.reqs) != 0 { + t.Fatalf("out-of-scope order: orderID reads=%d txs=%d", orderIDReads, len(txm.reqs)) + } +} + +func TestProcessOrderSkipsWhenGovernanceFeeInvariantFails(t *testing.T) { + fixture := immediateTestSetup(t) + txm := &fakeLifiTxSender{} + s := newProcessTestSolver( + fixture.cfg, fixture.caller, txm, fixedFillStrategy{}, + fixture.tokenIn, fixture.tokenOut, fixture.adapter, lifiOrderStatusDeposited, + ) + orderIDReads := 0 + s.reader = fakeLifiReader{ + governanceFeeErr: errors.New("input settler governance fee is 1, expected zero"), + orderIDFn: func(inputsettler.StandardOrder) common.Hash { + orderIDReads++ + return common.Hash{} + }, + } + var logs []string + s.log = funcr.NewJSON(func(entry string) { logs = append(logs, entry) }, funcr.Options{}) + + s.processOrder( + context.Background(), testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), + testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut), + ) + if orderIDReads != 0 || len(txm.reqs) != 0 { + t.Fatalf("fee-bearing order: orderID reads=%d txs=%d", orderIDReads, len(txm.reqs)) + } + logged := strings.Join(logs, "\n") + if !strings.Contains(logged, "governance fee invariant failed") || !strings.Contains(logged, `"error"`) { + t.Fatalf("governance fee failure was not logged as an error: %s", logged) + } +} + +func TestProcessOrderFillsThroughPrivateDiscountWithoutDirectAuthorization(t *testing.T) { + fixture := immediateTestSetup(t) + now := time.Unix(1_700_000_000, 0) + strategy, err := defaultstrategy.New(defaultstrategy.Config{}) + if err != nil { + t.Fatalf("New strategy: %v", err) + } + routeItem := testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter)[0] + base := liquidlane.FillQuote{ + Inventory: liquidlane.DirectInventory(routeItem, big.NewInt(2_000_000), big.NewInt(1_000_000_000_000_000_000)), + AmountIn: big.NewInt(1_000_000), GrossAmountOut: big.NewInt(1_100_100), + MaxAmountOut: big.NewInt(990_090), MinDiscount: big.NewInt(100_000), + } + discounts := &fakeDiscountClient{ + listed: &discounts.List{Discounts: []discounts.ListItem{ + testDiscountListItem(base.Inventory, 2_000_000, now.Add(time.Minute)), + }}, + resolved: testResolvedDiscount(base.Inventory, 100_000, now.Add(time.Minute)), + } + txm := &fakeLifiTxSender{} + s := newProcessTestSolver( + fixture.cfg, fixture.caller, txm, strategy, + fixture.tokenIn, fixture.tokenOut, fixture.adapter, lifiOrderStatusDeposited, + ) + s.discounts = discounts + fillReads := 0 + s.reader = fakeLifiReader{ + orderID: common.HexToHash("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + status: lifiOrderStatusDeposited, + fillSetFn: func() fillSnapshotSet { + fillReads++ + return fillSnapshotSet{DiscountBases: []liquidlane.FillQuote{base}} + }, + } + s.now = func(context.Context) (time.Time, error) { return now, nil } + + s.processOrder( + context.Background(), []route{routeItem}, + testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut), + ) + if len(txm.reqs) != 1 || discounts.listCalls != 1 || discounts.resolveCalls != 1 || fillReads != 2 { + t.Fatalf( + "txs=%d discount calls=%d/%d fill reads=%d", + len(txm.reqs), discounts.listCalls, discounts.resolveCalls, fillReads, + ) + } +} + +func TestProcessOrderRejectsMultiRoutePlanForPermissionedToken(t *testing.T) { + fixture := immediateTestSetup(t) + fixture.cfg.TokenPolicy = testTokenPolicy(t, tokenpolicy.Permissioned, fixture.tokenIn) + txm := &fakeLifiTxSender{} + plan := &types.FillPlan{Routes: []types.FillRoute{ + {RouteID: "route-1", Adapter: fixture.adapter, AmountIn: big.NewInt(500_000)}, + {RouteID: "route-2", Adapter: fixture.adapter, AmountIn: big.NewInt(500_000)}, + }} + s := newProcessTestSolver( + fixture.cfg, fixture.caller, txm, fixedFillStrategy{plan: plan}, + fixture.tokenIn, fixture.tokenOut, fixture.adapter, lifiOrderStatusDeposited, + ) + + s.processOrder( + context.Background(), testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), + testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut), + ) + if len(txm.reqs) != 0 { + t.Fatalf("permissioned multi-route plan submitted %d transactions", len(txm.reqs)) + } +} + +func TestRoutesForPairUsesBothTokens(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + other := common.HexToAddress("0x3333333333333333333333333333333333333333") + routes := []route{ + {ID: "exact", TokenIn: tokenIn, TokenOut: tokenOut}, + {ID: "wrong-output", TokenIn: tokenIn, TokenOut: other}, + {ID: "wrong-input", TokenIn: other, TokenOut: tokenOut}, + } + + got := routesForPair(routes, tokenIn, tokenOut) + if len(got) != 1 || got[0].ID != "exact" { + t.Fatalf("routes = %+v", got) + } +} + +func TestProcessOrderChecksOnChainStatusBeforeSend(t *testing.T) { + fixture := immediateTestSetup(t) + strategy, err := defaultstrategy.New(defaultstrategy.Config{}) + if err != nil { + t.Fatalf("New strategy: %v", err) + } + txm := &fakeLifiTxSender{} + s := newProcessTestSolver(fixture.cfg, fixture.caller, txm, strategy, fixture.tokenIn, fixture.tokenOut, fixture.adapter, 2) + fillReads := 0 + s.reader = fakeLifiReader{ + orderID: common.HexToHash("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + status: 2, + fillSnapshotsFn: func() []liquidlane.FillQuote { + fillReads++ + return profitableFillSnapshots(fixture.tokenIn, fixture.tokenOut, fixture.adapter, 1_000_000) + }, + } + + s.processOrder(context.Background(), testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut)) + if len(txm.reqs) != 0 || fillReads != 0 { + t.Fatalf("closed order txs = %d fillReads = %d", len(txm.reqs), fillReads) + } +} + +func TestProcessOrderDoesNotRetryFailedSend(t *testing.T) { + fixture := immediateTestSetup(t) + strategy, err := defaultstrategy.New(defaultstrategy.Config{}) + if err != nil { + t.Fatalf("New strategy: %v", err) + } + txm := &fakeLifiTxSender{result: txmanager.Result{Err: errors.New("send failed")}} + s := newProcessTestSolver(fixture.cfg, fixture.caller, txm, strategy, fixture.tokenIn, fixture.tokenOut, fixture.adapter, lifiOrderStatusDeposited) + + s.processOrder(context.Background(), testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut)) + if len(txm.reqs) != 1 { + t.Fatalf("failed send attempts = %d, want 1", len(txm.reqs)) + } +} + +func TestProcessOrderDropsWhenTransactionSubmissionIsRejected(t *testing.T) { + fixture := immediateTestSetup(t) + strategy, err := defaultstrategy.New(defaultstrategy.Config{}) + if err != nil { + t.Fatalf("New strategy: %v", err) + } + txm := &fakeLifiTxSender{reject: true} + s := newProcessTestSolver( + fixture.cfg, fixture.caller, txm, strategy, + fixture.tokenIn, fixture.tokenOut, fixture.adapter, lifiOrderStatusDeposited, + ) + + s.processOrder( + context.Background(), testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), + testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut), + ) + if len(txm.reqs) != 0 { + t.Fatalf("busy sender accepted %d requests", len(txm.reqs)) + } +} + +func TestOrderWorkerReplansQueuedOrderBeforeSend(t *testing.T) { + fixture := immediateTestSetup(t) + strategy, err := defaultstrategy.New(defaultstrategy.Config{}) + if err != nil { + t.Fatalf("New strategy: %v", err) + } + availableOutput := int64(1_000_000) + maxFeePerGas := big.NewInt(1) + txm := &fakeLifiTxSender{onSend: func(attempt int, _ chan<- txmanager.Result) { + if attempt == 1 { + availableOutput = 980_000 + maxFeePerGas = big.NewInt(2) + } + }} + s := newProcessTestSolver( + fixture.cfg, fixture.caller, txm, strategy, + fixture.tokenIn, fixture.tokenOut, fixture.adapter, lifiOrderStatusDeposited, + ) + fillReads := 0 + feeReads := 0 + s.reader = fakeLifiReader{ + status: lifiOrderStatusDeposited, + orderIDFn: func(order inputsettler.StandardOrder) common.Hash { + return common.BigToHash(order.Nonce) + }, + fillSnapshotsFn: func() []liquidlane.FillQuote { + fillReads++ + return profitableFillSnapshots( + fixture.tokenIn, fixture.tokenOut, fixture.adapter, availableOutput, + ) + }, + } + s.maxFeePerGas = func(context.Context) (*big.Int, error) { + feeReads++ + return new(big.Int).Set(maxFeePerGas), nil + } + first := testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut) + secondValue := *first + secondValue.OrderID = "order-2" + secondValue.OnChainOrderID = "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + secondValue.Order.Nonce = new(big.Int).Add(first.Order.Nonce, big.NewInt(1)) + orders := make(chan *submittedOrder, 2) + orders <- first + orders <- &secondValue + close(orders) + + if err := s.runOrderWorker( + context.Background(), testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), orders, + ); err != nil { + t.Fatalf("runOrderWorker: %v", err) + } + if fillReads != 2 || feeReads != 2 { + t.Fatalf("fresh state reads: fills=%d fees=%d, want 2/2", fillReads, feeReads) + } + if len(txm.reqs) != 1 { + t.Fatalf("fill attempts = %d, want 1 after second order becomes unprofitable", len(txm.reqs)) + } +} + +func TestOrderWorkerSubmitsAllFillsWithoutWaitingForReceipts(t *testing.T) { + fixture := immediateTestSetup(t) + strategy, err := defaultstrategy.New(defaultstrategy.Config{}) + if err != nil { + t.Fatalf("New strategy: %v", err) + } + submitted := make(chan chan<- txmanager.Result, 5) + txm := &fakeLifiTxSender{ + hold: true, + onSend: func(_ int, result chan<- txmanager.Result) { + submitted <- result + }, + } + s := newProcessTestSolver( + fixture.cfg, fixture.caller, txm, strategy, + fixture.tokenIn, fixture.tokenOut, fixture.adapter, lifiOrderStatusDeposited, + ) + fillReads := 0 + feeReads := 0 + s.reader = fakeLifiReader{ + status: lifiOrderStatusDeposited, + orderIDFn: func(order inputsettler.StandardOrder) common.Hash { + return common.BigToHash(order.Nonce) + }, + fillSnapshotsFn: func() []liquidlane.FillQuote { + fillReads++ + fills := profitableFillSnapshots(fixture.tokenIn, fixture.tokenOut, fixture.adapter, 1_000_000) + fills[0].MaxAssets = big.NewInt(10_000_000) + return fills + }, + } + s.maxFeePerGas = func(context.Context) (*big.Int, error) { + feeReads++ + return big.NewInt(1), nil + } + + base := testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut) + orders := make(chan *submittedOrder, 5) + for i := int64(0); i < 5; i++ { + order := *base + order.OrderID = "order-" + big.NewInt(i+1).String() + order.Order.Nonce = new(big.Int).Add(base.Order.Nonce, big.NewInt(i)) + orders <- &order + } + close(orders) + done := make(chan error, 1) + go func() { + done <- s.runOrderWorker( + context.Background(), testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), orders, + ) + }() + + results := make([]chan<- txmanager.Result, 0, 5) + for range 5 { + results = append(results, receiveFillSubmission(t, submitted)) + } + for _, result := range results { + result <- txm.fillResult() + } + select { + case err := <-done: + if err != nil { + t.Fatalf("runOrderWorker: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("worker did not finish after receipts") + } + if fillReads != 5 || feeReads != 5 || len(txm.reqs) != 5 { + t.Fatalf("fills=%d fees=%d submissions=%d, want 5/5/5", fillReads, feeReads, len(txm.reqs)) + } + for i, req := range txm.reqs { + if req.Confirmations == nil || *req.Confirmations != 0 { + t.Fatalf("request %d confirmations = %v, want inclusion receipt", i, req.Confirmations) + } + } +} + +func TestOrderWorkerPassesPendingReservationsToNextFillDecision(t *testing.T) { + fixture := immediateTestSetup(t) + inputs := make(chan types.FillInput, 2) + plan := &types.FillPlan{Routes: []types.FillRoute{{ + RouteID: "route-1", + CapacityID: "capacity-1", + Adapter: fixture.adapter, + AmountIn: big.NewInt(1_000_000), + ExpectedAmountOut: big.NewInt(1_000_000), + MinAmountOut: big.NewInt(990_000), + ReservedAmountOut: big.NewInt(1_000_000), + }}} + txm := &fakeLifiTxSender{hold: true} + s := newProcessTestSolver( + fixture.cfg, + fixture.caller, + txm, + reservationAwareFillStrategy{plan: plan, inputs: inputs}, + fixture.tokenIn, + fixture.tokenOut, + fixture.adapter, + lifiOrderStatusDeposited, + ) + s.reader = fakeLifiReader{ + status: lifiOrderStatusDeposited, + orderIDFn: func(order inputsettler.StandardOrder) common.Hash { + return common.BigToHash(order.Nonce) + }, + fillSnapshotsFn: func() []liquidlane.FillQuote { + return profitableFillSnapshots(fixture.tokenIn, fixture.tokenOut, fixture.adapter, 1_000_000) + }, + } + + first := testSubmittedOrder(t, fixture.cfg, fixture.tokenIn, fixture.tokenOut) + secondValue := *first + secondValue.OrderID = "order-2" + secondValue.Order.Nonce = new(big.Int).Add(first.Order.Nonce, big.NewInt(1)) + orders := make(chan *submittedOrder, 2) + orders <- first + orders <- &secondValue + close(orders) + done := make(chan error, 1) + go func() { + done <- s.runOrderWorker( + t.Context(), + testResolvedRoutes(fixture.tokenIn, fixture.tokenOut, fixture.adapter), + orders, + ) + }() + + firstInput := receiveFillInput(t, inputs) + if firstInput.Solver != fixture.cfg.Executor { + t.Fatalf("solver = %s, want executor %s", firstInput.Solver.Hex(), fixture.cfg.Executor.Hex()) + } + if len(firstInput.Reservations) != 0 { + t.Fatalf("first fill reservations = %v, want none", firstInput.Reservations) + } + secondInput := receiveFillInput(t, inputs) + if got := secondInput.Reservations["capacity-1"]; got == nil || got.Cmp(big.NewInt(1_000_000)) != 0 { + t.Fatalf("second fill reservations = %v, want capacity-1=1000000", secondInput.Reservations) + } + if len(txm.reqs) != 1 { + t.Fatalf("submitted fills = %d, want 1", len(txm.reqs)) + } + txm.results[0] <- txm.fillResult() + select { + case err := <-done: + if err != nil { + t.Fatalf("runOrderWorker: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("worker did not finish after receipt") + } +} + +func receiveFillInput(t *testing.T, inputs <-chan types.FillInput) types.FillInput { + t.Helper() + select { + case input := <-inputs: + return input + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for fill decision") + return types.FillInput{} + } +} + +func receiveFillSubmission(t *testing.T, submitted <-chan chan<- txmanager.Result) chan<- txmanager.Result { + t.Helper() + select { + case result := <-submitted: + return result + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for fill submission") + return nil + } +} + +type processTestFixture struct { + cfg *Config + caller common.Address + tokenIn common.Address + tokenOut common.Address + adapter common.Address +} + +func immediateTestSetup(t *testing.T) processTestFixture { + t.Helper() + cfg := testLifiConfig() + caller := common.HexToAddress("0x5555555555555555555555555555555555555555") + tokenIn := common.HexToAddress("0x6666666666666666666666666666666666666666") + tokenOut := common.HexToAddress("0x7777777777777777777777777777777777777777") + adapter := common.HexToAddress("0x9999999999999999999999999999999999999999") + return processTestFixture{cfg: cfg, caller: caller, tokenIn: tokenIn, tokenOut: tokenOut, adapter: adapter} +} + +func newProcessTestSolver( + cfg *Config, + caller common.Address, + txm *fakeLifiTxSender, + strategy types.Strategy, + tokenIn, tokenOut, adapter common.Address, + status uint8, +) *Solver { + return &Solver{ + cfg: cfg, chainID: 11155111, + reader: fakeLifiReader{ + orderID: common.HexToHash("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + status: status, fill: profitableFillSnapshots(tokenIn, tokenOut, adapter, 1_000_000), + }, + strategy: strategy, caller: caller, txm: txm, log: logr.Discard(), + now: func(context.Context) (time.Time, error) { return time.Unix(1_700_000_000, 0), nil }, + maxFeePerGas: func(context.Context) (*big.Int, error) { return big.NewInt(1), nil }, + } +} + +func profitableFillSnapshots(tokenIn, tokenOut, adapter common.Address, amountOut int64) []liquidlane.FillQuote { + return []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ID: "route-1", Adapter: adapter, TokenIn: tokenIn, TokenOut: tokenOut}, + MaxAssets: big.NewInt(2_000_000), + }, + AmountIn: big.NewInt(1_000_000), + MaxAmountOut: big.NewInt(amountOut), + }} +} + +func testResolvedRoutes(tokenIn, tokenOut, adapter common.Address) []route { + return []route{{ + ID: "route-1", CapacityID: "capacity-1", Adapter: adapter, TokenIn: tokenIn, TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }} +} + +func testSubmittedOrder(t *testing.T, cfg *Config, tokenIn, tokenOut common.Address) *submittedOrder { + t.Helper() + order, err := parseSubmittedOrder(testOrderJSON(t, cfg, tokenIn, tokenOut), cfg, 11155111) + if err != nil { + t.Fatalf("parseSubmittedOrder: %v", err) + } + return order +} diff --git a/internal/solvers/lifi/strategies/default/capacity.go b/internal/solvers/lifi/strategies/default/capacity.go new file mode 100644 index 00000000..edb1601c --- /dev/null +++ b/internal/solvers/lifi/strategies/default/capacity.go @@ -0,0 +1,104 @@ +package defaultstrategy + +import ( + "math/big" + "slices" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" +) + +func (s *Strategy) availableCapacity(maxAssets *big.Int) *big.Int { + return applyBpsDown(maxAssets, bpsDenominator-s.cfg.InventoryReserveBps) +} + +func (s *Strategy) quoteCapacity(route liquidlane.Inventory) *big.Int { + if route.DiscountID == nil { + return liquidlane.CloneBig(route.MaxAssets) + } + return applyBpsDown(route.MaxAssets, bpsDenominator-s.cfg.PriceBufferBps) +} + +func (s *Strategy) allocateQuoteCapacity( + inventory []liquidlane.Inventory, + reservations map[liquidlane.CapacityID]*big.Int, +) []liquidlane.Inventory { + groups := make(map[liquidlane.CapacityID]map[liquidlane.RouteID][]liquidlane.Inventory) + for _, item := range inventory { + if item.MaxAssets == nil || item.MaxAssets.Sign() <= 0 { + continue + } + capacityID := liquidlane.RouteCapacityID(item.Route) + if groups[capacityID] == nil { + groups[capacityID] = make(map[liquidlane.RouteID][]liquidlane.Inventory) + } + groups[capacityID][item.ID] = append(groups[capacityID][item.ID], item) + } + + capacityIDs := make([]liquidlane.CapacityID, 0, len(groups)) + for capacityID := range groups { + capacityIDs = append(capacityIDs, capacityID) + } + slices.Sort(capacityIDs) + + out := make([]liquidlane.Inventory, 0, len(inventory)) + for _, capacityID := range capacityIDs { + routes := groups[capacityID] + routeIDs := make([]liquidlane.RouteID, 0, len(routes)) + for routeID := range routes { + routeIDs = append(routeIDs, routeID) + } + slices.Sort(routeIDs) + domainMax := new(big.Int) + for _, items := range routes { + for _, item := range items { + if item.MaxAssets.Cmp(domainMax) > 0 { + domainMax.Set(item.MaxAssets) + } + } + } + remaining := s.availableCapacity(domainMax) + if reserved := reservations[capacityID]; reserved != nil && reserved.Sign() > 0 { + remaining.Sub(remaining, reserved) + } + if remaining.Sign() <= 0 { + continue + } + + for i, routeID := range routeIDs { + items := routes[routeID] + count := int64(len(routeIDs) - i) + share := mulDivUp(remaining, big.NewInt(1), big.NewInt(count)) + routeCap := new(big.Int) + for _, item := range items { + itemCap := s.availableCapacity(item.MaxAssets) + if itemCap.Cmp(routeCap) > 0 { + routeCap.Set(itemCap) + } + } + if share.Cmp(routeCap) > 0 { + share.Set(routeCap) + } + if share.Sign() <= 0 { + continue + } + for _, item := range items { + itemCap := s.availableCapacity(item.MaxAssets) + if itemCap.Cmp(share) > 0 { + itemCap.Set(share) + } + if itemCap.Sign() <= 0 { + continue + } + item.MaxAssets = itemCap + item.MaxRate = liquidlane.CloneBig(item.MaxRate) + item.DiscountID = liquidlane.CloneHash(item.DiscountID) + out = append(out, item) + } + remaining.Sub(remaining, share) + if remaining.Sign() <= 0 { + break + } + } + } + return out +} diff --git a/internal/solvers/lifi/strategies/default/fill.go b/internal/solvers/lifi/strategies/default/fill.go new file mode 100644 index 00000000..a3df6fef --- /dev/null +++ b/internal/solvers/lifi/strategies/default/fill.go @@ -0,0 +1,50 @@ +package defaultstrategy + +import ( + "context" + + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +func (s *Strategy) DecideFill(_ context.Context, input types.FillInput) (*types.FillPlan, error) { + if input.AmountIn == nil || input.AmountIn.Sign() <= 0 { + return nil, errors.New("amountIn: must be positive") + } + if input.OutputAmount == nil || input.OutputAmount.Sign() <= 0 { + return nil, errors.New("outputAmount: must be positive") + } + if input.AmountIn.Cmp(s.minAmount) < 0 { + return nil, nil + } + validAfter := input.ChainTime.Add(s.executionBuffer) + deadlineCutoff := uint32Time(validAfter) + if input.Expires != 0 && input.Expires <= deadlineCutoff { + return nil, nil + } + if input.FillDeadline != 0 && input.FillDeadline <= deadlineCutoff { + return nil, nil + } + output, err := parseOutputContext(input.OutputAmount, input.OutputContext) + if err != nil { + return nil, err + } + maxRoutes := types.MaxRoutes + if input.RequireSingleRoute { + maxRoutes = 1 + } + solution, err := s.solveGreedyFill(input, validAfter, maxRoutes) + if err != nil || solution == nil { + return nil, err + } + requiredAmountOut, ok := output.fill(input.Solver, input.ChainTime, solution.maxAmountOut) + if !ok { + return nil, nil + } + routes := solution.buildRoutes(requiredAmountOut) + if len(routes) == 0 { + return nil, nil + } + return &types.FillPlan{Routes: routes}, nil +} diff --git a/internal/solvers/lifi/strategies/default/fill_greedy.go b/internal/solvers/lifi/strategies/default/fill_greedy.go new file mode 100644 index 00000000..bb9d175e --- /dev/null +++ b/internal/solvers/lifi/strategies/default/fill_greedy.go @@ -0,0 +1,437 @@ +package defaultstrategy + +import ( + "math/big" + "sort" + "time" + + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +type fillSolution struct { + allocations []fillAllocation + gasAmount *big.Int + maxAmountOut *big.Int +} + +func (solution *fillSolution) buildRoutes(requiredAmountOut *big.Int) []types.FillRoute { + if solution == nil || requiredAmountOut == nil || requiredAmountOut.Sign() <= 0 || + requiredAmountOut.Cmp(solution.maxAmountOut) > 0 { + return nil + } + minimumTotal := new(big.Int).Add(requiredAmountOut, solution.gasAmount) + targets := make([]*big.Int, len(solution.allocations)) + for index := range solution.allocations { + targets[index] = solution.allocations[index].targetOutput + } + minimums := distributeMinimums(targets, minimumTotal) + if minimums == nil { + return nil + } + routes := make([]types.FillRoute, len(solution.allocations)) + for index, leg := range solution.allocations { + routes[index] = types.FillRoute{ + RouteID: leg.candidate.quote.ID, + CapacityID: liquidlane.RouteCapacityID(leg.candidate.quote.Route), + Adapter: leg.candidate.quote.Adapter, + AmountIn: liquidlane.CloneBig(leg.amountIn), + ExpectedAmountOut: liquidlane.CloneBig(leg.targetOutput), + MinAmountOut: minimums[index], + ReservedAmountOut: liquidlane.CloneBig(leg.reservedOutput), + DiscountID: liquidlane.CloneHash(leg.candidate.quote.DiscountID), + } + } + return routes +} + +func (s *Strategy) solveGreedyFill( + input types.FillInput, + validAfter time.Time, + maxRoutes int, +) (*fillSolution, error) { + candidates, err := s.buildFillCandidates(input, validAfter) + if err != nil || len(candidates) == 0 { + return nil, err + } + allocation := s.greedyFillAllocation( + candidates, + input.AmountIn, + min(maxRoutes, len(candidates)), + ) + if len(allocation) == 0 { + return nil, nil + } + pricing, err := newGasPricing( + input.MaxFeePerGas, + input.TokenOut, + input.GasPrices, + input.GasSnapshot, + s.cfg.InventoryReserveBps, + ) + if err != nil { + return nil, err + } + targetTotal := new(big.Int) + legs := make([]gasLeg, 0, len(allocation)) + for index, leg := range allocation { + allocation[index].targetOutput = new(big.Int).Sub( + leg.executableOutput, + applyBpsUp(leg.executableOutput, s.cfg.PriceBufferBps), + ) + if allocation[index].targetOutput.Sign() <= 0 { + return nil, nil + } + targetTotal.Add(targetTotal, allocation[index].targetOutput) + legs = append(legs, gasLeg{ + route: leg.candidate.quote.Route, + amountOut: leg.executableOutput, + private: leg.candidate.quote.DiscountID != nil, + }) + } + gasAmount := pricing.cost(legs) + maxAmountOut := new(big.Int).Sub(targetTotal, gasAmount) + if maxAmountOut.Sign() <= 0 { + return nil, nil + } + return &fillSolution{ + allocations: allocation, gasAmount: gasAmount, maxAmountOut: maxAmountOut, + }, nil +} + +type fillCandidate struct { + quote liquidlane.FillQuote + capacity *big.Int + maxInput *big.Int +} + +type fillRoute struct { + id liquidlane.RouteID + alternatives []fillCandidate +} + +type fillAllocation struct { + candidate fillCandidate + amountIn *big.Int + executableOutput *big.Int + reservedOutput *big.Int + targetOutput *big.Int +} + +func (candidate fillCandidate) id() liquidlane.CandidateID { + return liquidlane.NewCandidateID(candidate.quote.Route, candidate.quote.DiscountID) +} + +func (s *Strategy) buildFillCandidates(input types.FillInput, validAfter time.Time) ([]fillCandidate, error) { + seen := make(map[liquidlane.CandidateID]bool, len(input.Quotes)) + candidates := make([]fillCandidate, 0, len(input.Quotes)) + for _, quote := range input.Quotes { + if quote.TokenIn != input.TokenIn || quote.TokenOut != input.TokenOut { + continue + } + if !quote.ValidUntil.IsZero() && !quote.ValidUntil.After(validAfter) { + continue + } + if quote.AmountIn == nil || quote.AmountIn.Cmp(input.AmountIn) != 0 { + return nil, errors.Errorf("fill quote %s amountIn does not match order", quote.ID) + } + if quote.MaxAssets == nil || quote.MaxAssets.Sign() <= 0 || + quote.MaxAmountOut == nil || quote.MaxAmountOut.Sign() <= 0 { + continue + } + capacityID := liquidlane.RouteCapacityID(quote.Route) + capacity := s.availableCapacity(quote.MaxAssets) + if reserved := input.Reservations[capacityID]; reserved != nil && reserved.Sign() > 0 { + capacity.Sub(capacity, reserved) + } + if capacity.Sign() <= 0 { + continue + } + candidate := fillCandidate{quote: quote, capacity: capacity} + candidate.maxInput = s.maxInputWithinCapacity( + candidate, input.AmountIn, capacity, + ) + candidateID := candidate.id() + if candidate.maxInput.Sign() <= 0 || seen[candidateID] { + continue + } + seen[candidateID] = true + candidates = append(candidates, candidate) + } + return candidates, nil +} + +func (s *Strategy) greedyFillAllocation( + candidates []fillCandidate, + amountIn *big.Int, + maxRoutes int, +) []fillAllocation { + routes := buildFillRoutes(candidates) + capacityLimits := fillCapacityLimits(candidates) + capacityUsed := make(map[liquidlane.CapacityID]*big.Int, len(capacityLimits)) + usedRoutes := make(map[liquidlane.RouteID]bool, maxRoutes) + remaining := liquidlane.CloneBig(amountIn) + allocation := make([]fillAllocation, 0, maxRoutes) + + for remaining.Sign() > 0 && len(allocation) < maxRoutes { + var best *fillAllocation + lastRoute := len(allocation) == maxRoutes-1 + for _, route := range routes { + if usedRoutes[route.id] { + continue + } + choice := s.fillRouteChoice( + route, remaining, capacityLimits, capacityUsed, + ) + if choice != nil && lastRoute && choice.amountIn.Cmp(remaining) < 0 { + continue + } + if choice != nil && (best == nil || fillAllocationBetter(*choice, *best)) { + best = choice + } + } + if best == nil { + break + } + allocation = append(allocation, *best) + usedRoutes[best.candidate.quote.ID] = true + capacityID := liquidlane.RouteCapacityID(best.candidate.quote.Route) + if capacityUsed[capacityID] == nil { + capacityUsed[capacityID] = new(big.Int) + } + capacityUsed[capacityID].Add(capacityUsed[capacityID], best.reservedOutput) + remaining.Sub(remaining, best.amountIn) + } + if remaining.Sign() > 0 { + return nil + } + return allocation +} + +func buildFillRoutes(candidates []fillCandidate) []fillRoute { + byRoute := make(map[liquidlane.RouteID][]fillCandidate) + for _, candidate := range candidates { + byRoute[candidate.quote.ID] = append(byRoute[candidate.quote.ID], candidate) + } + routes := make([]fillRoute, 0, len(byRoute)) + for routeID, alternatives := range byRoute { + routes = append(routes, fillRoute{id: routeID, alternatives: bestFillAlternatives(alternatives)}) + } + sort.Slice(routes, func(i, j int) bool { return routes[i].id < routes[j].id }) + return routes +} + +func bestFillAlternatives(candidates []fillCandidate) []fillCandidate { + var direct, private fillCandidate + var hasDirect, hasPrivate bool + for _, candidate := range candidates { + if candidate.quote.DiscountID == nil { + if !hasDirect || fillCandidateBetter(candidate, direct) { + direct, hasDirect = candidate, true + } + } else if !hasPrivate || fillCandidateBetter(candidate, private) { + private, hasPrivate = candidate, true + } + } + alternatives := make([]fillCandidate, 0, 2) + if hasDirect { + alternatives = append(alternatives, direct) + } + if hasPrivate { + alternatives = append(alternatives, private) + } + return alternatives +} + +func fillCandidateBetter(left, right fillCandidate) bool { + if comparison := compareFillRate(left.quote, right.quote); comparison != 0 { + return comparison > 0 + } + if comparison := left.maxInput.Cmp(right.maxInput); comparison != 0 { + return comparison > 0 + } + leftDirect := left.quote.DiscountID == nil + rightDirect := right.quote.DiscountID == nil + if leftDirect != rightDirect { + return leftDirect + } + return left.id() < right.id() +} + +func fillAllocationBetter(left, right fillAllocation) bool { + if comparison := compareFillRate(left.candidate.quote, right.candidate.quote); comparison != 0 { + return comparison > 0 + } + if comparison := left.amountIn.Cmp(right.amountIn); comparison != 0 { + return comparison > 0 + } + leftDirect := left.candidate.quote.DiscountID == nil + rightDirect := right.candidate.quote.DiscountID == nil + if leftDirect != rightDirect { + return leftDirect + } + return left.candidate.id() < right.candidate.id() +} + +func compareFillRate(left, right liquidlane.FillQuote) int { + leftRate := new(big.Int).Mul(left.MaxAmountOut, right.AmountIn) + rightRate := new(big.Int).Mul(right.MaxAmountOut, left.AmountIn) + return leftRate.Cmp(rightRate) +} + +func fillCapacityLimits(candidates []fillCandidate) map[liquidlane.CapacityID]*big.Int { + limits := make(map[liquidlane.CapacityID]*big.Int) + for _, candidate := range candidates { + capacityID := liquidlane.RouteCapacityID(candidate.quote.Route) + if limit := limits[capacityID]; limit == nil || candidate.capacity.Cmp(limit) > 0 { + limits[capacityID] = liquidlane.CloneBig(candidate.capacity) + } + } + return limits +} + +func (s *Strategy) fillRouteChoice( + route fillRoute, + remaining *big.Int, + capacityLimits map[liquidlane.CapacityID]*big.Int, + capacityUsed map[liquidlane.CapacityID]*big.Int, +) *fillAllocation { + capacityID := liquidlane.RouteCapacityID(route.alternatives[0].quote.Route) + capacityLeft := liquidlane.CloneBig(capacityLimits[capacityID]) + if used := capacityUsed[capacityID]; used != nil { + capacityLeft.Sub(capacityLeft, used) + } + if capacityLeft.Sign() <= 0 { + return nil + } + + available := make([]*big.Int, len(route.alternatives)) + legAmount := new(big.Int) + for index, candidate := range route.alternatives { + candidateCapacity := minBig(capacityLeft, candidate.capacity) + amount := s.maxInputWithinCapacity(candidate, remaining, candidateCapacity) + if amount.Cmp(candidate.maxInput) > 0 { + amount.Set(candidate.maxInput) + } + available[index] = amount + if amount.Cmp(legAmount) > 0 { + legAmount.Set(amount) + } + } + if legAmount.Sign() <= 0 { + return nil + } + + var best *fillCandidate + for index, candidate := range route.alternatives { + if available[index].Cmp(legAmount) < 0 { + continue + } + if best == nil || fillCandidateBetter(candidate, *best) { + selected := candidate + best = &selected + } + } + if best == nil { + return nil + } + return &fillAllocation{ + candidate: *best, + amountIn: legAmount, + executableOutput: scaledFillOutput(best.quote, legAmount), + reservedOutput: s.reservedCapacityOutput(*best, legAmount), + } +} + +func (s *Strategy) maxInputWithinCapacity( + candidate fillCandidate, + inputLimit *big.Int, + capacity *big.Int, +) *big.Int { + quote := candidate.quote + if inputLimit == nil || inputLimit.Sign() <= 0 || capacity == nil || capacity.Sign() <= 0 || + quote.AmountIn == nil || quote.AmountIn.Sign() <= 0 || + quote.MaxAmountOut == nil || quote.MaxAmountOut.Sign() <= 0 { + return new(big.Int) + } + precision := big.NewInt(bpsDenominator) + buffer := big.NewInt(int64(s.cfg.PriceBufferBps)) + maxOutput := new(big.Int) + if quote.DiscountID != nil { + maxOutput.Mul(capacity, precision) + maxOutput.Div(maxOutput, new(big.Int).Add(precision, buffer)) + } else { + // reserved = floor(output * (1 - buffer)); invert the floor exactly. + maxOutput.Add(capacity, big.NewInt(1)) + maxOutput.Mul(maxOutput, precision) + maxOutput.Sub(maxOutput, big.NewInt(1)) + maxOutput.Div(maxOutput, new(big.Int).Sub(precision, buffer)) + } + maxInput := new(big.Int).Add(maxOutput, big.NewInt(1)) + maxInput.Mul(maxInput, quote.AmountIn) + maxInput.Sub(maxInput, big.NewInt(1)) + maxInput.Div(maxInput, quote.MaxAmountOut) + if maxInput.Cmp(inputLimit) > 0 { + maxInput.Set(inputLimit) + } + return maxInput +} + +func (s *Strategy) reservedCapacityOutput(candidate fillCandidate, amountIn *big.Int) *big.Int { + amountOut := scaledFillOutput(candidate.quote, amountIn) + buffer := applyBpsUp(amountOut, s.cfg.PriceBufferBps) + if candidate.quote.DiscountID != nil { + return amountOut.Add(amountOut, buffer) + } + return amountOut.Sub(amountOut, buffer) +} + +func distributeMinimums(targets []*big.Int, total *big.Int) []*big.Int { + if len(targets) == 0 || total == nil || total.Sign() <= 0 || + total.Cmp(big.NewInt(int64(len(targets)))) < 0 { + return nil + } + capacity := new(big.Int) + for _, target := range targets { + if target == nil || target.Sign() <= 0 { + return nil + } + capacity.Add(capacity, target) + } + if total.Cmp(capacity) > 0 { + return nil + } + remaining := new(big.Int).Sub(total, big.NewInt(int64(len(targets)))) + remainingCapacity := new(big.Int).Sub(capacity, big.NewInt(int64(len(targets)))) + minimums := make([]*big.Int, len(targets)) + for index, target := range targets { + available := new(big.Int).Sub(target, big.NewInt(1)) + allocation := new(big.Int) + if index == len(targets)-1 { + allocation.Set(remaining) + } else if remainingCapacity.Sign() > 0 { + allocation.Mul(remaining, available) + allocation.Div(allocation, remainingCapacity) + } + if allocation.Cmp(available) > 0 { + return nil + } + minimums[index] = allocation.Add(allocation, big.NewInt(1)) + remaining.Sub(remaining, new(big.Int).Sub(minimums[index], big.NewInt(1))) + remainingCapacity.Sub(remainingCapacity, available) + } + if remaining.Sign() != 0 { + return nil + } + return minimums +} + +func scaledFillOutput(quote liquidlane.FillQuote, amountIn *big.Int) *big.Int { + if quote.AmountIn == nil || quote.AmountIn.Sign() <= 0 || + quote.MaxAmountOut == nil || amountIn == nil { + return new(big.Int) + } + return new(big.Int).Div(new(big.Int).Mul(quote.MaxAmountOut, amountIn), quote.AmountIn) +} diff --git a/internal/solvers/lifi/strategies/default/fill_test.go b/internal/solvers/lifi/strategies/default/fill_test.go new file mode 100644 index 00000000..9d90f61e --- /dev/null +++ b/internal/solvers/lifi/strategies/default/fill_test.go @@ -0,0 +1,131 @@ +package defaultstrategy + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +func TestSolveFillChoosesBestCompleteRoutes(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + quotes := make([]liquidlane.FillQuote, 3) + for index := range quotes { + quotes[index] = testFillQuote( + liquidlane.RouteID(string(rune('a'+index))), + liquidlane.CapacityID(string(rune('A'+index))), + tokenIn, + tokenOut, + 2, + int64(6+2*index), + int64(3+index), + nil, + ) + } + solution := solveTestFill(t, tokenIn, tokenOut, big.NewInt(2), quotes, nil, 3) + if solution == nil { + t.Fatal("expected fill solution") + } + routes := solution.buildRoutes(solution.maxAmountOut) + if len(routes) != 2 || routes[0].RouteID != "c" || routes[1].RouteID != "b" { + t.Fatalf("routes = %+v, want c,b", routes) + } +} + +func TestSolveFillUsesDirectWhenPrivateCannotCoverLeg(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + direct := testFillQuote("route", "capacity", tokenIn, tokenOut, 100, 100, 100, nil) + private := testFillQuote("route", "capacity", tokenIn, tokenOut, 100, 200, 100, &discountID) + solution := solveTestFill( + t, tokenIn, tokenOut, big.NewInt(100), []liquidlane.FillQuote{private, direct}, nil, 1, + ) + if solution == nil { + t.Fatal("expected fill solution") + } + routes := solution.buildRoutes(big.NewInt(100)) + if len(routes) != 1 || routes[0].DiscountID != nil { + t.Fatalf("routes = %+v, want direct fallback", routes) + } +} + +func TestSolveFillDoesNotOverbookSharedCapacity(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + high := testFillQuote("high", "shared", tokenIn, tokenOut, 75, 150, 100, nil) + wide := testFillQuote("wide", "shared", tokenIn, tokenOut, 75, 75, 60, nil) + solution := solveTestFill( + t, tokenIn, tokenOut, big.NewInt(75), []liquidlane.FillQuote{high, wide}, nil, 2, + ) + if solution != nil { + t.Fatalf("solution = %+v, want shared-capacity rejection", solution) + } +} + +func TestMaxInputWithinCapacityIsExact(t *testing.T) { + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + for _, discount := range []*common.Hash{nil, &discountID} { + strategy := &Strategy{cfg: Config{PriceBufferBps: 1234}} + candidate := fillCandidate{quote: liquidlane.FillQuote{ + Inventory: liquidlane.Inventory{DiscountID: discount}, + AmountIn: big.NewInt(137), MaxAmountOut: big.NewInt(233), + }} + for capacity := int64(1); capacity <= 233; capacity++ { + limit := big.NewInt(137) + got := strategy.maxInputWithinCapacity(candidate, limit, big.NewInt(capacity)) + if strategy.reservedCapacityOutput(candidate, got).Cmp(big.NewInt(capacity)) > 0 { + t.Fatalf("discount=%v capacity=%d input=%s exceeds capacity", discount != nil, capacity, got) + } + if got.Cmp(limit) < 0 { + next := new(big.Int).Add(got, big.NewInt(1)) + if strategy.reservedCapacityOutput(candidate, next).Cmp(big.NewInt(capacity)) <= 0 { + t.Fatalf("discount=%v capacity=%d input=%s is not maximal", discount != nil, capacity, got) + } + } + } + } +} + +func solveTestFill( + t *testing.T, + tokenIn, tokenOut common.Address, + amountIn *big.Int, + quotes []liquidlane.FillQuote, + reservations map[liquidlane.CapacityID]*big.Int, + maxRoutes int, +) *fillSolution { + t.Helper() + strategy := &Strategy{} + solution, err := strategy.solveGreedyFill(types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: amountIn, + Quotes: quotes, Reservations: reservations, MaxFeePerGas: new(big.Int), + }, time.Time{}, maxRoutes) + if err != nil { + t.Fatalf("solveGreedyFill: %v", err) + } + return solution +} + +func testFillQuote( + routeID liquidlane.RouteID, + capacityID liquidlane.CapacityID, + tokenIn, tokenOut common.Address, + amountIn, amountOut, maxAssets int64, + discountID *common.Hash, +) liquidlane.FillQuote { + return liquidlane.FillQuote{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: routeID, CapacityID: capacityID, TokenIn: tokenIn, TokenOut: tokenOut, + }, + MaxAssets: big.NewInt(maxAssets), DiscountID: discountID, + }, + AmountIn: big.NewInt(amountIn), MaxAmountOut: big.NewInt(amountOut), + } +} diff --git a/internal/solvers/lifi/strategies/default/gas.go b/internal/solvers/lifi/strategies/default/gas.go new file mode 100644 index 00000000..801478cc --- /dev/null +++ b/internal/solvers/lifi/strategies/default/gas.go @@ -0,0 +1,57 @@ +package defaultstrategy + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies" +) + +type gasLeg struct { + route liquidlane.Route + amountOut *big.Int + private bool +} + +type gasPricing struct { + feePerGas *big.Int + tokenOutPerNative *big.Int + snapshot *liquidlanegas.Snapshot +} + +func newGasPricing( + maxFeePerGas *big.Int, + tokenOut common.Address, + prices *liquidlanegas.PriceSnapshot, + snapshot *liquidlanegas.Snapshot, + reserveBps int, +) (gasPricing, error) { + if maxFeePerGas == nil || maxFeePerGas.Sign() < 0 { + return gasPricing{}, errors.New("max fee per gas must be non-negative") + } + rate := prices.TokenOutPerNative(tokenOut) + if maxFeePerGas.Sign() > 0 && (rate == nil || rate.Sign() <= 0) { + return gasPricing{}, errors.Errorf("gas oracle: missing tokenOut rate for %s", tokenOut.Hex()) + } + if rate == nil { + rate = new(big.Int) + } + return gasPricing{ + feePerGas: new(big.Int).Set(maxFeePerGas), tokenOutPerNative: rate, + snapshot: liquidlanegas.WithReserveBps(snapshot, reserveBps), + }, nil +} + +func (p gasPricing) cost(legs []gasLeg) *big.Int { + shared := make([]strategies.GasLeg, 0, len(legs)) + for _, leg := range legs { + shared = append(shared, strategies.GasLeg{ + Route: leg.route, AmountOut: leg.amountOut, Private: leg.private, + }) + } + return strategies.FillGasCostAtRate(p.feePerGas, p.tokenOutPerNative, p.snapshot, shared) +} diff --git a/internal/solvers/lifi/strategies/default/gas_test.go b/internal/solvers/lifi/strategies/default/gas_test.go new file mode 100644 index 00000000..042878b0 --- /dev/null +++ b/internal/solvers/lifi/strategies/default/gas_test.go @@ -0,0 +1,121 @@ +package defaultstrategy + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" +) + +func TestGasPricingUsesSharedRoutePrediction(t *testing.T) { + adapter := common.HexToAddress("0x1111111111111111111111111111111111111111") + vault := common.HexToAddress("0x1313131313131313131313131313131313131313") + tokenIn := common.HexToAddress("0x1212121212121212121212121212121212121212") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + pricing := testGasPricing(t, tokenOut, big.NewInt(3), &liquidlanegas.Snapshot{ + Adapters: map[common.Address]*liquidlanegas.AdapterState{ + adapter: {Vault: vault, Acquire: map[common.Address]*big.Int{}}, + }, + Vaults: map[common.Address]*liquidlanegas.VaultState{ + vault: {FreeAssets: big.NewInt(100), Withdrawable: big.NewInt(100)}, + }, + }, 0) + leg := gasLeg{ + route: liquidlane.Route{Adapter: adapter, Vault: vault, TokenIn: tokenIn}, amountOut: big.NewInt(10), + } + direct := pricing.cost([]gasLeg{leg}) + wantUnits := uint64(250_000) + liquidlanegas.UnitsForRouteAt(liquidlanegas.RouteAllocate, true) + wantDirect := new(big.Int).Mul(new(big.Int).SetUint64(wantUnits), big.NewInt(3)) + if direct.Cmp(wantDirect) != 0 { + t.Fatalf("direct cost = %s, want %s", direct, wantDirect) + } + leg.private = true + private := pricing.cost([]gasLeg{leg}) + wantPrivate := new(big.Int).Add(wantDirect, + new(big.Int).Mul(new(big.Int).SetUint64(75_000), big.NewInt(3))) + if private.Cmp(wantPrivate) != 0 { + t.Fatalf("private cost = %s, want %s", private, wantPrivate) + } +} + +func TestGasPricingChargesSettlementOnceForMultiAdapterPlan(t *testing.T) { + adapterA := common.HexToAddress("0x1111111111111111111111111111111111111111") + adapterB := common.HexToAddress("0x2222222222222222222222222222222222222222") + vaultA := common.HexToAddress("0x1414141414141414141414141414141414141414") + vaultB := common.HexToAddress("0x1515151515151515151515151515151515151515") + tokenIn := common.HexToAddress("0x1212121212121212121212121212121212121212") + tokenOut := common.HexToAddress("0x3333333333333333333333333333333333333333") + snapshot := &liquidlanegas.Snapshot{ + Adapters: map[common.Address]*liquidlanegas.AdapterState{ + adapterA: {Vault: vaultA, Acquire: map[common.Address]*big.Int{}}, + adapterB: {Vault: vaultB, Acquire: map[common.Address]*big.Int{}}, + }, + Vaults: map[common.Address]*liquidlanegas.VaultState{ + vaultA: {FreeAssets: big.NewInt(100), Withdrawable: big.NewInt(100)}, + vaultB: {FreeAssets: big.NewInt(100), Withdrawable: big.NewInt(100)}, + }, + } + pricing := testGasPricing(t, tokenOut, big.NewInt(3), snapshot, 0) + cost := pricing.cost([]gasLeg{ + {route: liquidlane.Route{Adapter: adapterA, Vault: vaultA, TokenIn: tokenIn}, amountOut: big.NewInt(10)}, + {route: liquidlane.Route{Adapter: adapterB, Vault: vaultB, TokenIn: tokenIn}, amountOut: big.NewInt(10)}, + }) + wantUnits := uint64(250_000) + + liquidlanegas.UnitsForRouteAt(liquidlanegas.RouteAllocate, true) + + liquidlanegas.UnitsForRouteAt(liquidlanegas.RouteAllocate, true) + want := new(big.Int).Mul(new(big.Int).SetUint64(wantUnits), big.NewInt(3)) + if cost.Cmp(want) != 0 { + t.Fatalf("multi-adapter cost = %s, want %s", cost, want) + } +} + +func TestGasPricingAppliesInventoryReserveBeforeRoutePrediction(t *testing.T) { + adapter := common.HexToAddress("0x1111111111111111111111111111111111111111") + vault := common.HexToAddress("0x1414141414141414141414141414141414141414") + tokenIn := common.HexToAddress("0x1212121212121212121212121212121212121212") + tokenOut := common.HexToAddress("0x3333333333333333333333333333333333333333") + snapshot := &liquidlanegas.Snapshot{ + Adapters: map[common.Address]*liquidlanegas.AdapterState{ + adapter: {Vault: vault, Acquire: map[common.Address]*big.Int{}}, + }, + Vaults: map[common.Address]*liquidlanegas.VaultState{ + vault: {FreeAssets: big.NewInt(100), Withdrawable: big.NewInt(200)}, + }, + } + pricing := testGasPricing(t, tokenOut, big.NewInt(1), snapshot, 1_000) + cost := pricing.cost([]gasLeg{{ + route: liquidlane.Route{Adapter: adapter, Vault: vault, TokenIn: tokenIn}, amountOut: big.NewInt(95), + }}) + want := new(big.Int).SetUint64(250_000 + liquidlanegas.UnitsForRouteAt(liquidlanegas.RouteDeallocate, true)) + if cost.Cmp(want) != 0 { + t.Fatalf("reserved route cost = %s, want %s", cost, want) + } +} + +func TestGasPricingRejectsMissingOracleRate(t *testing.T) { + _, err := newGasPricing(big.NewInt(1), common.HexToAddress("0x2222222222222222222222222222222222222222"), nil, nil, 0) + if err == nil { + t.Fatal("expected missing token rate error") + } +} + +func testGasPricing( + t *testing.T, + tokenOut common.Address, + fee *big.Int, + snapshot *liquidlanegas.Snapshot, + reserveBps int, +) gasPricing { + t.Helper() + prices := liquidlanegas.NewPriceSnapshot(map[common.Address]*big.Int{ + tokenOut: big.NewInt(1_000_000_000_000_000_000), + }) + pricing, err := newGasPricing(fee, tokenOut, prices, snapshot, reserveBps) + if err != nil { + t.Fatalf("newGasPricing: %v", err) + } + return pricing +} diff --git a/internal/solvers/lifi/strategies/default/math.go b/internal/solvers/lifi/strategies/default/math.go new file mode 100644 index 00000000..2342c3b0 --- /dev/null +++ b/internal/solvers/lifi/strategies/default/math.go @@ -0,0 +1,58 @@ +package defaultstrategy + +import ( + "math/big" + "strings" +) + +func applyBpsDown(amount *big.Int, bps int) *big.Int { + if amount == nil || amount.Sign() <= 0 || bps <= 0 { + return new(big.Int) + } + out := new(big.Int).Mul(amount, big.NewInt(int64(bps))) + return out.Div(out, big.NewInt(bpsDenominator)) +} + +func applyBpsUp(amount *big.Int, bps int) *big.Int { + if amount == nil || amount.Sign() <= 0 || bps <= 0 { + return new(big.Int) + } + return mulDivUp(amount, big.NewInt(int64(bps)), big.NewInt(bpsDenominator)) +} + +func mulDivUp(a, b, denominator *big.Int) *big.Int { + if a == nil || b == nil || denominator == nil || a.Sign() <= 0 || b.Sign() <= 0 || denominator.Sign() <= 0 { + return new(big.Int) + } + numerator := new(big.Int).Mul(a, b) + quotient, remainder := new(big.Int).QuoRem(numerator, denominator, new(big.Int)) + if remainder.Sign() != 0 { + quotient.Add(quotient, big.NewInt(1)) + } + return quotient +} + +func minBig(left, right *big.Int) *big.Int { + if left.Cmp(right) < 0 { + return new(big.Int).Set(left) + } + return new(big.Int).Set(right) +} + +func fixedPointDecimal(n *big.Int, scale int) string { + if n.Sign() == 0 { + return "0" + } + unit := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(scale)), nil) + intPart := new(big.Int).Div(new(big.Int).Set(n), unit) + fracPart := new(big.Int).Mod(new(big.Int).Set(n), unit) + if fracPart.Sign() == 0 { + return intPart.String() + } + frac := fracPart.String() + if len(frac) < scale { + frac = strings.Repeat("0", scale-len(frac)) + frac + } + frac = strings.TrimRight(frac, "0") + return intPart.String() + "." + frac +} diff --git a/internal/solvers/lifi/strategies/default/output.go b/internal/solvers/lifi/strategies/default/output.go new file mode 100644 index 00000000..b0cc207a --- /dev/null +++ b/internal/solvers/lifi/strategies/default/output.go @@ -0,0 +1,86 @@ +package defaultstrategy + +import ( + "encoding/binary" + "math" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" +) + +const ( + limitOrderContextType = 0x00 + dutchAuctionContextType = 0x01 + exclusiveLimitOrderContextType = 0xe0 + exclusiveDutchAuctionContextType = 0xe1 +) + +type outputPricing struct { + amount *big.Int + + startTime uint32 + + exclusive bool + exclusiveFor [32]byte +} + +func parseOutputContext(outputAmount *big.Int, outputContext []byte) (*outputPricing, error) { + out := &outputPricing{amount: new(big.Int).Set(outputAmount)} + if len(outputContext) == 0 { + return out, nil + } + switch outputContext[0] { + case limitOrderContextType: + if len(outputContext) != 1 { + return nil, errors.Errorf("outputContext: limit order length must be 1, got %d", len(outputContext)) + } + return out, nil + case dutchAuctionContextType: + return nil, errors.New("outputContext: Dutch auctions are not supported") + case exclusiveLimitOrderContextType: + if len(outputContext) != 37 { + return nil, errors.Errorf("outputContext: exclusive limit length must be 37, got %d", len(outputContext)) + } + out.exclusive = true + copy(out.exclusiveFor[:], outputContext[1:33]) + out.startTime = binary.BigEndian.Uint32(outputContext[33:37]) + return out, nil + case exclusiveDutchAuctionContextType: + return nil, errors.New("outputContext: Dutch auctions are not supported") + default: + return nil, errors.Errorf("outputContext: unsupported type 0x%02x", outputContext[0]) + } +} + +func (o *outputPricing) fill(solver common.Address, now time.Time, acceptableAmount *big.Int) (*big.Int, bool) { + currentTime := uint32Time(now) + if o.exclusive && currentTime < o.startTime { + solverID := solverIdentifier(solver) + if o.exclusiveFor != solverID { + return nil, false + } + } + if o.amount.Cmp(acceptableAmount) > 0 { + return nil, false + } + return new(big.Int).Set(o.amount), true +} + +func uint32Time(t time.Time) uint32 { + unix := t.Unix() + if unix <= 0 { + return 0 + } + if unix > math.MaxUint32 { + return math.MaxUint32 + } + return uint32(unix) +} + +func solverIdentifier(addr common.Address) [32]byte { + var out [32]byte + copy(out[12:], addr.Bytes()) + return out +} diff --git a/internal/solvers/lifi/strategies/default/quote.go b/internal/solvers/lifi/strategies/default/quote.go new file mode 100644 index 00000000..7a423bbc --- /dev/null +++ b/internal/solvers/lifi/strategies/default/quote.go @@ -0,0 +1,220 @@ +package defaultstrategy + +import ( + "context" + "math/big" + "sort" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +type quoteCandidate struct { + liquidlane.Inventory + + maxInput *big.Int +} + +func (c quoteCandidate) id() liquidlane.CandidateID { + return liquidlane.NewCandidateID(c.Route, c.DiscountID) +} + +type quoteRoute struct { + id liquidlane.RouteID + alternatives []quoteCandidate + maxInput *big.Int + bestRate *big.Int +} + +type strategyPairKey struct { + tokenIn common.Address + tokenOut common.Address + inputDecimals int + outputDecimals int +} + +func (s *Strategy) DecideQuotes(_ context.Context, input types.QuoteInput) (types.QuoteOutput, error) { + if !input.QuoteExpiresAt.After(input.ServerTime) { + return types.QuoteOutput{}, errors.New("quoteExpiresAt must be after serverTime") + } + inventory := s.allocateQuoteCapacity( + filterQuoteInventory(input.Inventory, input.ChainTime.Add(s.executionBuffer)), + input.Reservations, + ) + groups := make(map[strategyPairKey][]quoteCandidate) + for _, item := range inventory { + candidate := s.newQuoteCandidate(item) + if candidate == nil { + continue + } + key := strategyPairKey{ + tokenIn: item.TokenIn, tokenOut: item.TokenOut, + inputDecimals: item.TokenInDecimals, outputDecimals: item.TokenOutDecimals, + } + groups[key] = append(groups[key], *candidate) + } + + keys := make([]strategyPairKey, 0, len(groups)) + for key := range groups { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { return pairLess(keys[i], keys[j]) }) + out := types.QuoteOutput{Quotes: make([]types.Quote, 0, len(keys))} + for _, key := range keys { + pricing, err := newGasPricing( + input.MaxFeePerGas, key.tokenOut, input.GasPrices, input.GasSnapshot, s.cfg.InventoryReserveBps, + ) + if err != nil { + return types.QuoteOutput{}, err + } + ladder := buildQuoteLadder(groups[key]) + routeLimit := types.MaxRoutes + if input.SingleRouteTokens[key.tokenIn] { + routeLimit = 1 + } + if len(ladder) > routeLimit { + ladder = ladder[:routeLimit] + } + ranges, used := s.buildQuoteRanges(ladder, pricing) + if len(ranges) == 0 { + continue + } + expiry := quoteExpiry(input.QuoteExpiresAt, s.executionBuffer, used) + if expiry <= input.ServerTime.Unix() { + continue + } + out.Quotes = append(out.Quotes, types.Quote{ + FromAsset: key.tokenIn, ToAsset: key.tokenOut, + FromDecimals: key.inputDecimals, ToDecimals: key.outputDecimals, + Ranges: ranges, Expiry: expiry, ExclusiveFor: input.Solver, + }) + } + return out, nil +} + +func filterQuoteInventory(inventory []liquidlane.Inventory, validAfter time.Time) []liquidlane.Inventory { + seen := make(map[liquidlane.CandidateID]bool, len(inventory)) + out := make([]liquidlane.Inventory, 0, len(inventory)) + for _, candidate := range inventory { + id := liquidlane.NewCandidateID(candidate.Route, candidate.DiscountID) + if (!candidate.ValidUntil.IsZero() && !candidate.ValidUntil.After(validAfter)) || seen[id] { + continue + } + seen[id] = true + out = append(out, candidate) + } + return out +} + +func (s *Strategy) newQuoteCandidate(item liquidlane.Inventory) *quoteCandidate { + if item.MaxAssets == nil || item.MaxAssets.Sign() <= 0 || item.MaxRate == nil || item.MaxRate.Sign() <= 0 { + return nil + } + maxInput := liquidlane.MaxAmountInForRate( + s.quoteCapacity(item), item.MaxRate, item.TokenInDecimals, item.TokenOutDecimals, + ) + if maxInput.Sign() <= 0 || + liquidlane.AmountOutForRate(maxInput, item.MaxRate, item.TokenInDecimals, item.TokenOutDecimals).Sign() <= 0 { + return nil + } + return "eCandidate{Inventory: item, maxInput: maxInput} +} + +func buildQuoteLadder(candidates []quoteCandidate) []quoteRoute { + byRoute := make(map[liquidlane.RouteID][]quoteCandidate) + for _, candidate := range candidates { + byRoute[candidate.ID] = append(byRoute[candidate.ID], candidate) + } + ladder := make([]quoteRoute, 0, len(byRoute)) + for routeID, candidates := range byRoute { + alternatives := bestQuoteAlternatives(candidates) + route := quoteRoute{ + id: routeID, alternatives: alternatives, maxInput: new(big.Int), bestRate: new(big.Int), + } + for _, candidate := range alternatives { + if candidate.maxInput.Cmp(route.maxInput) > 0 { + route.maxInput.Set(candidate.maxInput) + } + if candidate.MaxRate.Cmp(route.bestRate) > 0 { + route.bestRate.Set(candidate.MaxRate) + } + } + ladder = append(ladder, route) + } + sort.Slice(ladder, func(i, j int) bool { + if cmp := ladder[i].bestRate.Cmp(ladder[j].bestRate); cmp != 0 { + return cmp > 0 + } + if cmp := ladder[i].maxInput.Cmp(ladder[j].maxInput); cmp != 0 { + return cmp > 0 + } + return ladder[i].id < ladder[j].id + }) + return ladder +} + +// bestQuoteAlternatives keeps one direct fallback and one private alternative per physical route. +func bestQuoteAlternatives(candidates []quoteCandidate) []quoteCandidate { + var direct, private quoteCandidate + var hasDirect, hasPrivate bool + for i := range candidates { + candidate := candidates[i] + if candidate.DiscountID == nil { + if !hasDirect || quoteCandidateBetter(candidate, direct) { + direct, hasDirect = candidate, true + } + } else if !hasPrivate || quoteCandidateBetter(candidate, private) { + private, hasPrivate = candidate, true + } + } + alternatives := make([]quoteCandidate, 0, 2) + if hasDirect { + alternatives = append(alternatives, direct) + } + if hasPrivate { + alternatives = append(alternatives, private) + } + return alternatives +} + +func quoteCandidateBetter(left, right quoteCandidate) bool { + if cmp := left.MaxRate.Cmp(right.MaxRate); cmp != 0 { + return cmp > 0 + } + if cmp := left.maxInput.Cmp(right.maxInput); cmp != 0 { + return cmp > 0 + } + return preferQuoteCandidate(left, right) +} + +func preferQuoteCandidate(left, right quoteCandidate) bool { + leftDirect := left.DiscountID == nil + rightDirect := right.DiscountID == nil + if leftDirect != rightDirect { + return leftDirect + } + if left.ValidUntil.IsZero() != right.ValidUntil.IsZero() { + return left.ValidUntil.IsZero() + } + if !left.ValidUntil.Equal(right.ValidUntil) { + return left.ValidUntil.After(right.ValidUntil) + } + return left.id() < right.id() +} + +func pairLess(left, right strategyPairKey) bool { + if cmp := left.tokenIn.Cmp(right.tokenIn); cmp != 0 { + return cmp < 0 + } + if cmp := left.tokenOut.Cmp(right.tokenOut); cmp != 0 { + return cmp < 0 + } + if left.inputDecimals != right.inputDecimals { + return left.inputDecimals < right.inputDecimals + } + return left.outputDecimals < right.outputDecimals +} diff --git a/internal/solvers/lifi/strategies/default/quote_exact_input.go b/internal/solvers/lifi/strategies/default/quote_exact_input.go new file mode 100644 index 00000000..cb4556e7 --- /dev/null +++ b/internal/solvers/lifi/strategies/default/quote_exact_input.go @@ -0,0 +1,83 @@ +package defaultstrategy + +import ( + "math/big" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" +) + +type exactInputQuote struct { + grossAmountOut *big.Int + gasCost *big.Int + candidates []quoteCandidate +} + +// solveExactInputQuote greedily prices one concrete input amount across physical routes. +func solveExactInputQuote(ladder []quoteRoute, amountIn *big.Int, pricing gasPricing) (exactInputQuote, bool) { + if amountIn == nil || amountIn.Sign() <= 0 { + return exactInputQuote{}, false + } + + remaining := new(big.Int).Set(amountIn) + usedRoutes := make(map[liquidlane.RouteID]bool, len(ladder)) + candidates := make([]quoteCandidate, 0, len(ladder)) + gasLegs := make([]gasLeg, 0, len(ladder)) + grossAmountOut := new(big.Int) + + for remaining.Sign() > 0 { + candidate, legAmount, ok := bestQuoteLeg(ladder, usedRoutes, remaining) + if !ok { + return exactInputQuote{}, false + } + amountOut := liquidlane.AmountOutForRate( + legAmount, + candidate.MaxRate, + candidate.TokenInDecimals, + candidate.TokenOutDecimals, + ) + if amountOut.Sign() <= 0 { + return exactInputQuote{}, false + } + + candidates = append(candidates, candidate) + gasLegs = append(gasLegs, gasLeg{ + route: candidate.Route, amountOut: amountOut, private: candidate.DiscountID != nil, + }) + grossAmountOut.Add(grossAmountOut, amountOut) + usedRoutes[candidate.ID] = true + remaining.Sub(remaining, legAmount) + } + + return exactInputQuote{ + grossAmountOut: grossAmountOut, + gasCost: pricing.cost(gasLegs), + candidates: candidates, + }, true +} + +func bestQuoteLeg( + ladder []quoteRoute, + used map[liquidlane.RouteID]bool, + remaining *big.Int, +) (quoteCandidate, *big.Int, bool) { + var best quoteCandidate + var bestAmount *big.Int + found := false + for _, route := range ladder { + if used[route.id] { + continue + } + legAmount := minBig(remaining, route.maxInput) + for _, candidate := range route.alternatives { + if candidate.maxInput.Cmp(legAmount) < 0 { + continue + } + if !found || quoteCandidateBetter(candidate, best) { + best = candidate + bestAmount = legAmount + found = true + } + } + } + return best, bestAmount, found +} diff --git a/internal/solvers/lifi/strategies/default/quote_ranges.go b/internal/solvers/lifi/strategies/default/quote_ranges.go new file mode 100644 index 00000000..a383f1dd --- /dev/null +++ b/internal/solvers/lifi/strategies/default/quote_ranges.go @@ -0,0 +1,197 @@ +package defaultstrategy + +import ( + "math/big" + "sort" + "time" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +func (s *Strategy) buildQuoteRanges( + ladder []quoteRoute, + pricing gasPricing, +) ([]types.QuoteRange, map[liquidlane.CandidateID]quoteCandidate) { + used := make(map[liquidlane.CandidateID]quoteCandidate) + transitions := quoteTransitionPoints(ladder) + if len(transitions) == 0 { + return nil, used + } + breakpoints := quoteBreakpoints(transitions[len(transitions)-1], s.minAmount, s.rangeCount) + ranges := make([]types.QuoteRange, 0, len(breakpoints)) + lower := new(big.Int).Set(s.minAmount) + for _, upper := range breakpoints { + if upper.Cmp(lower) < 0 { + continue + } + quoteRange, candidates, ok := s.priceQuoteRange(ladder, lower, upper, transitions, pricing) + if ok { + ranges = append(ranges, quoteRange) + for _, candidate := range candidates { + used[candidate.id()] = candidate + } + } + lower = new(big.Int).Add(upper, big.NewInt(1)) + } + return ranges, used +} + +func (s *Strategy) priceQuoteRange( + ladder []quoteRoute, + lower *big.Int, + upper *big.Int, + transitions []*big.Int, + pricing gasPricing, +) (types.QuoteRange, []quoteCandidate, bool) { + amounts := quoteRangePoints(lower, upper, transitions) + quotes := make([]exactInputQuote, len(amounts)) + gasCost := new(big.Int) + for index, amount := range amounts { + quote, ok := solveExactInputQuote(ladder, amount, pricing) + if !ok { + return types.QuoteRange{}, nil, false + } + quotes[index] = quote + if quote.gasCost.Cmp(gasCost) > 0 { + gasCost.Set(quote.gasCost) + } + } + + var rate *big.Int + candidates := make([]quoteCandidate, 0, len(amounts)*len(ladder)) + for index, quote := range quotes { + decimals := quote.candidates[0] + pointRate := s.guaranteedQuoteRate( + quote.grossAmountOut, amounts[index], gasCost, + decimals.TokenInDecimals, decimals.TokenOutDecimals, + ) + if rate == nil || pointRate.Cmp(rate) < 0 { + rate = pointRate + } + candidates = append(candidates, quote.candidates...) + } + if rate == nil || rate.Sign() <= 0 { + return types.QuoteRange{}, nil, false + } + return types.QuoteRange{ + MinAmount: new(big.Int).Set(lower), MaxAmount: new(big.Int).Set(upper), + Quote: fixedPointDecimal(rate, rateScaleDigits), + }, candidates, true +} + +func quoteBreakpoints(maximum, minimum *big.Int, targetCount int) []*big.Int { + if maximum == nil || maximum.Cmp(minimum) < 0 || targetCount <= 0 { + return nil + } + selected := map[string]*big.Int{maximum.String(): new(big.Int).Set(maximum)} + for len(selected) < targetCount { + points := sortedAmounts(selected) + var bestMid, bestLow, bestHigh *big.Int + low := new(big.Int).Set(minimum) + for _, high := range points { + mid := geometricMidpoint(low, high) + if mid != nil && (bestMid == nil || + new(big.Int).Mul(high, bestLow).Cmp(new(big.Int).Mul(bestHigh, low)) > 0) { + bestMid, bestLow, bestHigh = mid, new(big.Int).Set(low), new(big.Int).Set(high) + } + low = high + } + if bestMid == nil { + break + } + selected[bestMid.String()] = bestMid + } + return sortedAmounts(selected) +} + +func quoteTransitionPoints(ladder []quoteRoute) []*big.Int { + selected := make(map[string]*big.Int) + // A candidate can become ineligible after any subset of other routes has filled to capacity. + for mask := 0; mask < 1< 0 { + selected[prefix.String()] = prefix + } + } + return sortedAmounts(selected) +} + +func quoteRangePoints(lower, upper *big.Int, transitions []*big.Int) []*big.Int { + selected := map[string]*big.Int{ + lower.String(): new(big.Int).Set(lower), + upper.String(): new(big.Int).Set(upper), + } + for _, transition := range transitions { + if transition.Cmp(lower) >= 0 && transition.Cmp(upper) <= 0 { + selected[transition.String()] = new(big.Int).Set(transition) + } + after := new(big.Int).Add(transition, big.NewInt(1)) + if after.Cmp(lower) >= 0 && after.Cmp(upper) <= 0 { + selected[after.String()] = after + } + } + return sortedAmounts(selected) +} + +func sortedAmounts(amounts map[string]*big.Int) []*big.Int { + out := make([]*big.Int, 0, len(amounts)) + for _, amount := range amounts { + out = append(out, amount) + } + sort.Slice(out, func(i, j int) bool { return out[i].Cmp(out[j]) < 0 }) + return out +} + +func geometricMidpoint(lower, upper *big.Int) *big.Int { + if lower.Sign() <= 0 || upper.Cmp(lower) <= 0 { + return nil + } + mid := new(big.Int).Sqrt(new(big.Int).Mul(lower, upper)) + if mid.Cmp(lower) <= 0 { + mid.Add(lower, big.NewInt(1)) + } + if mid.Cmp(upper) >= 0 { + return nil + } + return mid +} + +func (s *Strategy) guaranteedQuoteRate( + grossAmountOut *big.Int, + amountIn *big.Int, + cost *big.Int, + inDecimals int, + outDecimals int, +) *big.Int { + available := applyBpsDown(grossAmountOut, bpsDenominator-2*s.cfg.PriceBufferBps) + available.Sub(available, cost) + if available.Sign() <= 0 { + return new(big.Int) + } + return liquidlane.RateForAmountOut(available, amountIn, inDecimals, outDecimals) +} + +func quoteExpiry(deadline time.Time, buffer time.Duration, used map[liquidlane.CandidateID]quoteCandidate) int64 { + expiry := deadline.Unix() + for _, candidate := range used { + if !candidate.ValidUntil.IsZero() { + expiry = min(expiry, candidate.ValidUntil.Add(-buffer).Unix()) + } + } + return expiry +} diff --git a/internal/solvers/lifi/strategies/default/strategy.go b/internal/solvers/lifi/strategies/default/strategy.go new file mode 100644 index 00000000..bbef2a09 --- /dev/null +++ b/internal/solvers/lifi/strategies/default/strategy.go @@ -0,0 +1,100 @@ +package defaultstrategy + +import ( + "math/big" + "time" + + "github.com/go-errors/errors" + "gopkg.in/yaml.v3" + + "github.com/symbioticfi/vault-solver/internal/parse" + "github.com/symbioticfi/vault-solver/internal/solver" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +const Name = "default" + +const ( + bpsDenominator = 10_000 + rateScaleDigits = 18 + defaultRangeCount = 8 + defaultExecutionBuffer = 12 * time.Second +) + +var defaultMinAmount = big.NewInt(1) + +type Config struct { + PriceBufferBps int `yaml:"priceBufferBps"` + MinAmount string `yaml:"minAmount"` + RangeCount int `yaml:"rangeCount"` + InventoryReserveBps int `yaml:"inventoryReserveBps"` + ExecutionDeadlineBuffer string `yaml:"executionDeadlineBuffer"` +} + +type Strategy struct { + cfg Config + + minAmount *big.Int + rangeCount int + executionBuffer time.Duration +} + +//nolint:gochecknoinits // solver-local strategy self-registration mirrors solver registration. +func init() { + strategies.Register(Name, NewFromConfig) +} + +func NewFromConfig(raw yaml.Node, _ strategies.Deps) (types.Strategy, error) { + var cfg Config + if err := decodeConfig(raw, &cfg); err != nil { + return nil, err + } + return New(cfg) +} + +func New(cfg Config) (*Strategy, error) { + if cfg.PriceBufferBps < 0 || cfg.PriceBufferBps >= bpsDenominator { + return nil, errors.Errorf("priceBufferBps: must be in [0,%d), got %d", bpsDenominator, cfg.PriceBufferBps) + } + if 2*cfg.PriceBufferBps >= bpsDenominator { + return nil, errors.Errorf("2 * priceBufferBps: must be < %d", bpsDenominator) + } + if cfg.InventoryReserveBps < 0 || cfg.InventoryReserveBps >= bpsDenominator { + return nil, errors.Errorf("inventoryReserveBps: must be in [0,%d), got %d", bpsDenominator, cfg.InventoryReserveBps) + } + rangeCount := cfg.RangeCount + if rangeCount == 0 { + rangeCount = defaultRangeCount + } + if rangeCount < 1 || rangeCount > types.MaxQuoteRanges { + return nil, errors.Errorf("rangeCount: must be in [1,%d], got %d", types.MaxQuoteRanges, cfg.RangeCount) + } + minAmount := new(big.Int).Set(defaultMinAmount) + if cfg.MinAmount != "" { + var err error + minAmount, err = parse.Big(cfg.MinAmount, "minAmount") + if err != nil { + return nil, err + } + if minAmount.Sign() <= 0 { + return nil, errors.New("minAmount: must be positive") + } + } + executionBuffer, err := parse.Duration( + cfg.ExecutionDeadlineBuffer, defaultExecutionBuffer, "executionDeadlineBuffer", + ) + if err != nil { + return nil, err + } + return &Strategy{ + cfg: cfg, minAmount: minAmount, rangeCount: rangeCount, executionBuffer: executionBuffer, + }, nil +} + +func decodeConfig(node yaml.Node, out any) error { + if node.Kind == 0 { + node = yaml.Node{Kind: yaml.MappingNode} + } + return solver.DecodeStrict(node, out) +} diff --git a/internal/solvers/lifi/strategies/default/strategy_test.go b/internal/solvers/lifi/strategies/default/strategy_test.go new file mode 100644 index 00000000..fab931de --- /dev/null +++ b/internal/solvers/lifi/strategies/default/strategy_test.go @@ -0,0 +1,1600 @@ +package defaultstrategy + +import ( + "context" + "encoding/binary" + "math/big" + "strconv" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +func TestDecideQuotesRequiresSolverExpiry(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + if _, err = strategy.DecideQuotes(context.Background(), types.QuoteInput{ + ChainTime: time.Unix(1_800_000_000, 0), + }); err == nil { + t.Fatal("expected missing quote expiry error") + } +} + +func TestDefaultExecutionBufferIsOneBlock(t *testing.T) { + strategy, err := New(Config{}) + if err != nil { + t.Fatalf("New: %v", err) + } + if strategy.executionBuffer != 12*time.Second { + t.Fatalf("execution buffer = %s", strategy.executionBuffer) + } +} + +func TestDefaultQuoteRangeCount(t *testing.T) { + strategy, err := New(Config{}) + if err != nil { + t.Fatalf("New: %v", err) + } + if strategy.rangeCount != defaultRangeCount { + t.Fatalf("range count = %d, want %d", strategy.rangeCount, defaultRangeCount) + } +} + +func TestQuoteRangeCountValidation(t *testing.T) { + for _, value := range []int{-1, types.MaxQuoteRanges + 1} { + if _, err := New(Config{RangeCount: value}); err == nil { + t.Fatalf("rangeCount %d: expected error", value) + } + } +} + +func TestDecideQuotesAppliesBuffersAndCapacity(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{PriceBufferBps: 100, MinAmount: "1000"})) + if err != nil { + t.Fatalf("New: %v", err) + } + + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Solver: common.HexToAddress("0x1111111111111111111111111111111111111111"), + ChainTime: time.Unix(1_800_000_000, 0), + QuoteExpiresAt: time.Unix(1_800_000_300, 0), + MaxFeePerGas: big.NewInt(0), + Inventory: []liquidlane.Inventory{{ + Route: liquidlane.Route{ + ID: "route-1", + Adapter: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenIn: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenOut: common.HexToAddress("0x4444444444444444444444444444444444444444"), + TokenInDecimals: 6, + TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(990_000_000), + MaxRate: big.NewInt(1_000_000_000_000_000_000), + }}, + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes len = %d", len(out.Quotes)) + } + q := out.Quotes[0] + if q.Expiry != 1_800_000_300 { + t.Fatalf("expiry = %d", q.Expiry) + } + if got, ok := new(big.Rat).SetString(q.Ranges[0].Quote); !ok || + got.Sign() <= 0 || got.Cmp(big.NewRat(98, 100)) > 0 { + t.Fatalf("quote = %q, want positive rate no greater than buffered 0.98", q.Ranges[0].Quote) + } + if got := q.Ranges[0].MinAmount.String(); got != "1000" { + t.Fatalf("minAmount = %s", got) + } + if got := q.Ranges[len(q.Ranges)-1].MaxAmount.String(); got != "990000000" { + t.Fatalf("maxAmount = %s", got) + } +} + +func TestDecideQuotesChargesGasAfterBuildingRange(t *testing.T) { + cfg := testStrategyConfig(Config{MinAmount: "1000"}) + strategy, err := New(cfg) + if err != nil { + t.Fatalf("New: %v", err) + } + + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Solver: common.HexToAddress("0x1111111111111111111111111111111111111111"), + ChainTime: time.Unix(1_800_000_000, 0), + QuoteExpiresAt: time.Unix(1_800_000_090, 0), + MaxFeePerGas: big.NewInt(100), + GasPrices: testGasPrices(common.HexToAddress("0x4444444444444444444444444444444444444444"), 1_000_000_000_000), + Inventory: []liquidlane.Inventory{{ + Route: liquidlane.Route{ + ID: "route-1", + Adapter: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenIn: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenOut: common.HexToAddress("0x4444444444444444444444444444444444444444"), + TokenInDecimals: 6, + TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(20_000), + MaxRate: big.NewInt(1_000_000_000_000_000_000), + }}, + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes len = %d", len(out.Quotes)) + } + ranges := out.Quotes[0].Ranges + if len(ranges) <= 1 { + t.Fatalf("expected dynamic ranges, got %d", len(ranges)) + } + if got := ranges[0].MinAmount.String(); got != "1000" { + t.Fatalf("range[0].min = %s", got) + } + if got := ranges[len(ranges)-1].MaxAmount.String(); got != "20000" { + t.Fatalf("last range max = %s", got) + } + for _, quoteRange := range ranges { + rate, ok := new(big.Rat).SetString(quoteRange.Quote) + if !ok || rate.Sign() <= 0 || rate.Cmp(big.NewRat(1, 1)) >= 0 { + t.Fatalf("quote should deduct complete-plan gas: %#v", ranges) + } + } +} + +func TestDecideQuotesAllowsBreakEvenMinimum(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "1"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: []liquidlane.Inventory{{ + Route: liquidlane.Route{ + ID: "route-1", + TokenIn: common.HexToAddress("0x1111111111111111111111111111111111111111"), TokenOut: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(100), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }}, + MaxFeePerGas: big.NewInt(0), ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(time.Minute), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 || out.Quotes[0].Ranges[0].Quote != "1" { + t.Fatalf("quotes = %+v, want break-even range", out.Quotes) + } +} + +func TestDecideQuotesRaisesMinimumAboveGasBreakEven(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "1"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: []liquidlane.Inventory{{ + Route: liquidlane.Route{ + ID: "route-1", + TokenIn: common.HexToAddress("0x1111111111111111111111111111111111111111"), TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(10_000_000), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }}, + GasPrices: testGasPrices(tokenOut, 1_000_000_000_000_000_000), MaxFeePerGas: big.NewInt(1), + ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(time.Minute), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 || len(out.Quotes[0].Ranges) == 0 { + t.Fatalf("quotes = %+v, want gas-aware range", out.Quotes) + } + if out.Quotes[0].Ranges[0].MinAmount.Cmp(big.NewInt(1)) <= 0 { + t.Fatalf("minAmount = %s, want amount above gas break-even", out.Quotes[0].Ranges[0].MinAmount) + } +} + +func TestDecideQuotesBoundsGasTransitionInsideRange(t *testing.T) { + cfg := testStrategyConfig(Config{MinAmount: "900"}) + strategy, err := New(cfg) + if err != nil { + t.Fatalf("New: %v", err) + } + + now := time.Unix(1_800_000_000, 0) + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x4444444444444444444444444444444444444444") + adapter := common.HexToAddress("0x2222222222222222222222222222222222222222") + vault := common.HexToAddress("0x3333333333333333333333333333333333333333") + route := liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", Adapter: adapter, Vault: vault, + TokenIn: tokenIn, TokenOut: tokenOut, TokenInDecimals: 6, TokenOutDecimals: 6, + } + gasSnapshot := &liquidlanegas.Snapshot{ + Adapters: map[common.Address]*liquidlanegas.AdapterState{ + adapter: {Vault: vault, Acquire: map[common.Address]*big.Int{tokenIn: big.NewInt(1_000)}}, + }, + Vaults: map[common.Address]*liquidlanegas.VaultState{ + vault: {FreeAssets: big.NewInt(10_000), Withdrawable: big.NewInt(10_000)}, + }, + } + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: []liquidlane.Inventory{{ + Route: route, MaxAssets: big.NewInt(2_000), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }}, + GasSnapshot: gasSnapshot, GasPrices: testGasPrices(tokenOut, 1_000_000), MaxFeePerGas: big.NewInt(1_000_000_000), + ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(time.Minute), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 || len(out.Quotes[0].Ranges) == 0 { + t.Fatalf("quotes = %+v", out.Quotes) + } + pricing, err := newGasPricing( + big.NewInt(1_000_000_000), tokenOut, testGasPrices(tokenOut, 1_000_000), gasSnapshot, 0, + ) + if err != nil { + t.Fatalf("pricing: %v", err) + } + for _, quoteRange := range out.Quotes[0].Ranges { + rate, ok := new(big.Rat).SetString(quoteRange.Quote) + if !ok { + t.Fatalf("invalid quote rate %q", quoteRange.Quote) + } + for amount := quoteRange.MinAmount.Int64(); amount <= quoteRange.MaxAmount.Int64(); amount++ { + actual := new(big.Int).Sub(big.NewInt(amount), pricing.cost([]gasLeg{{ + route: route, amountOut: big.NewInt(amount), + }})) + quoted := new(big.Int).Mul(big.NewInt(amount), rate.Num()) + quoted.Div(quoted, rate.Denom()) + if quoted.Cmp(actual) > 0 { + t.Fatalf("amount %d quoted %s above executable %s in range %+v", amount, quoted, actual, quoteRange) + } + } + } +} + +func TestDecideQuotesUsesConfiguredRangeCount(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "100", RangeCount: 4})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: []liquidlane.Inventory{{ + Route: liquidlane.Route{ + ID: "route-1", TokenIn: tokenIn, TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(1_000), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }}, + MaxFeePerGas: big.NewInt(0), ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(time.Minute), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes = %+v", out.Quotes) + } + ranges := out.Quotes[0].Ranges + if len(ranges) != 4 { + t.Fatalf("ranges = %+v, want four configured ranges", ranges) + } + for i := range ranges { + if i > 0 { + wantMin := new(big.Int).Add(ranges[i-1].MaxAmount, big.NewInt(1)) + if ranges[i].MinAmount.Cmp(wantMin) != 0 { + t.Fatalf("range[%d].min = %s, want %s", i, ranges[i].MinAmount, wantMin) + } + } + } + if got := ranges[len(ranges)-1].MaxAmount.String(); got != "1000" { + t.Fatalf("last maxAmount = %s, want 1000", got) + } +} + +func TestDecideQuotesUsesAtMostThreePhysicalRoutes(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "2"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + inventory := make([]liquidlane.Inventory, 4) + for i := range inventory { + inventory[i] = liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: liquidlane.RouteID("route-" + strconv.Itoa(i+1)), + CapacityID: liquidlane.CapacityID("capacity-" + strconv.Itoa(i+1)), + Adapter: common.BytesToAddress([]byte{byte(i + 1)}), + TokenIn: tokenIn, TokenOut: tokenOut, TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(100), MaxRate: big.NewInt(1_000_000_000_000_000_000), + } + } + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: inventory, MaxFeePerGas: big.NewInt(0), + ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(time.Minute), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes = %+v", out.Quotes) + } + ranges := out.Quotes[0].Ranges + if got := ranges[len(ranges)-1].MaxAmount.String(); got != "300" { + t.Fatalf("maxAmount = %s, want three-route capacity 300", got) + } +} + +func TestDecideQuotesAggregatesIndependentRoutesIntoOnePairCurve(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "2"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + inventory := []liquidlane.Inventory{ + { + Route: liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", TokenIn: tokenIn, TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(500), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }, + { + Route: liquidlane.Route{ + ID: "route-2", CapacityID: "capacity-2", TokenIn: tokenIn, TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(500), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }, + } + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: inventory, + MaxFeePerGas: big.NewInt(0), + ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(90 * time.Second), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes = %+v", out.Quotes) + } + ranges := out.Quotes[0].Ranges + if got := ranges[len(ranges)-1].MaxAmount.String(); got != "1000" { + t.Fatalf("aggregate maxAmount = %s, want 1000", got) + } +} + +func TestDecideQuotesPermissionedTokenUsesOneRoute(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + strategy, err := New(testStrategyConfig(Config{MinAmount: "2"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + inventory := []liquidlane.Inventory{ + { + Route: liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", TokenIn: tokenIn, TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(500), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }, + { + Route: liquidlane.Route{ + ID: "route-2", CapacityID: "capacity-2", TokenIn: tokenIn, TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(500), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }, + } + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: inventory, SingleRouteTokens: map[common.Address]bool{tokenIn: true}, + MaxFeePerGas: big.NewInt(0), + ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(90 * time.Second), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes = %+v", out.Quotes) + } + ranges := out.Quotes[0].Ranges + if got := ranges[len(ranges)-1].MaxAmount.String(); got != "500" { + t.Fatalf("permissioned maxAmount = %s, want 500", got) + } +} + +func TestDecideQuotesNeverOverquotesBlendedRouteRange(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "2"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + inventory := []liquidlane.Inventory{ + { + Route: liquidlane.Route{ + ID: "route-fast", CapacityID: "capacity-fast", TokenIn: tokenIn, TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(200), MaxRate: big.NewInt(2_000_000_000_000_000_000), + }, + { + Route: liquidlane.Route{ + ID: "route-slow", CapacityID: "capacity-slow", TokenIn: tokenIn, TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(100), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }, + } + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: inventory, MaxFeePerGas: big.NewInt(0), + ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(time.Minute), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes = %+v", out.Quotes) + } + for _, quoteRange := range out.Quotes[0].Ranges { + rate, ok := new(big.Rat).SetString(quoteRange.Quote) + if !ok { + t.Fatalf("invalid quote rate %q", quoteRange.Quote) + } + for amount := quoteRange.MinAmount.Int64(); amount <= quoteRange.MaxAmount.Int64(); amount++ { + fastInput := min(amount, int64(100)) + actualOut := 2*fastInput + max(amount-fastInput, int64(0)) + quoted := new(big.Int).Mul(big.NewInt(amount), rate.Num()) + quoted.Div(quoted, rate.Denom()) + if quoted.Cmp(big.NewInt(actualOut)) > 0 { + t.Fatalf("amount %d quoted %s above executable %d in range %+v", amount, quoted, actualOut, quoteRange) + } + } + } +} + +func TestDecideQuotesUsesPrivateAlternativeBeforeDirectFallback(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "100"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + route := liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", TokenIn: tokenIn, TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + } + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + inventory := []liquidlane.Inventory{ + liquidlane.DirectInventory(route, big.NewInt(1_000), big.NewInt(900_000_000_000_000_000)), + liquidlane.DiscountInventory( + route, big.NewInt(1_000), big.NewInt(1_000_000_000_000_000_000), + discountID, now.Add(time.Minute), + ), + } + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: inventory, MaxFeePerGas: big.NewInt(0), + ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(90 * time.Second), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 || out.Quotes[0].Expiry != now.Add(48*time.Second).Unix() { + t.Fatalf("quotes = %+v", out.Quotes) + } + usedPrivateRate := false + for _, quoteRange := range out.Quotes[0].Ranges { + rate, ok := new(big.Rat).SetString(quoteRange.Quote) + if ok && rate.Cmp(big.NewRat(9, 10)) > 0 { + usedPrivateRate = true + break + } + } + if !usedPrivateRate { + t.Fatalf("private alternative did not improve any range: %+v", out.Quotes[0].Ranges) + } +} + +func TestPriceBufferCoversQuoteToFillAndExecutionWindows(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{PriceBufferBps: 100, MinAmount: "10000"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + route := liquidlane.Route{ + ID: "route-1", Adapter: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenIn: tokenIn, TokenOut: tokenOut, TokenInDecimals: 6, TokenOutDecimals: 6, + } + quotes, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: []liquidlane.Inventory{{ + Route: route, MaxAssets: big.NewInt(20_000), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }}, + MaxFeePerGas: big.NewInt(0), ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(time.Minute), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(quotes.Quotes) != 1 { + t.Fatalf("quote = %+v", quotes.Quotes) + } + quoteRate, ok := new(big.Rat).SetString(quotes.Quotes[0].Ranges[0].Quote) + if !ok || quoteRate.Sign() <= 0 || quoteRate.Cmp(big.NewRat(98, 100)) > 0 { + t.Fatalf("quote rate = %q, want positive rate no greater than 0.98", quotes.Quotes[0].Ranges[0].Quote) + } + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, + AmountIn: big.NewInt(10_000), OutputAmount: big.NewInt(9_800), ChainTime: now, + MaxFeePerGas: big.NewInt(0), + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{Route: route, MaxAssets: big.NewInt(20_000)}, + AmountIn: big.NewInt(10_000), MaxAmountOut: big.NewInt(9_900), + }}, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan == nil { + t.Fatal("expected fill after one price-buffer adverse move") + } +} + +func TestDecideFillBuildsMultiRoutePlan(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + quotes := make([]liquidlane.FillQuote, 0, 2) + for i, routeID := range []liquidlane.RouteID{"route-1", "route-2"} { + quotes = append(quotes, liquidlane.FillQuote{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: routeID, CapacityID: liquidlane.CapacityID("capacity-" + strconv.Itoa(i+1)), + Adapter: common.BytesToAddress([]byte{byte(i + 1)}), TokenIn: tokenIn, TokenOut: tokenOut, + }, + MaxAssets: big.NewInt(500), + }, + AmountIn: big.NewInt(1_000), MaxAmountOut: big.NewInt(1_000), + }) + } + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, + AmountIn: big.NewInt(1_000), OutputAmount: big.NewInt(900), ChainTime: time.Unix(1_800_000_000, 0), + MaxFeePerGas: big.NewInt(0), Quotes: quotes, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan == nil || len(plan.Routes) != 2 { + t.Fatalf("plan = %+v", plan) + } + amountIn := new(big.Int) + minimumOut := new(big.Int) + for _, route := range plan.Routes { + amountIn.Add(amountIn, route.AmountIn) + minimumOut.Add(minimumOut, route.MinAmountOut) + } + if amountIn.String() != "1000" || minimumOut.String() != "900" { + t.Fatalf("amountIn=%s minimumOut=%s", amountIn, minimumOut) + } +} + +func TestDecideFillDoesNotDoubleSpendSharedVaultCapacity(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + quotes := make([]liquidlane.FillQuote, 0, 2) + for i, routeID := range []liquidlane.RouteID{"route-1", "route-2"} { + quotes = append(quotes, liquidlane.FillQuote{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: routeID, CapacityID: "shared-capacity", + Adapter: common.BytesToAddress([]byte{byte(i + 1)}), TokenIn: tokenIn, TokenOut: tokenOut, + }, + MaxAssets: big.NewInt(600), + }, + AmountIn: big.NewInt(1_000), MaxAmountOut: big.NewInt(1_000), + }) + } + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, + AmountIn: big.NewInt(1_000), OutputAmount: big.NewInt(900), ChainTime: time.Unix(1_800_000_000, 0), + MaxFeePerGas: big.NewInt(0), Quotes: quotes, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan != nil { + t.Fatalf("shared capacity was double counted: %+v", plan) + } +} + +func TestDecideQuotesUsesPrivateDiscountWithoutDirectCandidateAndClipsExpiry(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "1000"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + route := liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", + TokenIn: common.HexToAddress("0x1111111111111111111111111111111111111111"), + TokenOut: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenInDecimals: 6, TokenOutDecimals: 6, + } + discount := liquidlane.DiscountInventory( + route, big.NewInt(900), big.NewInt(800_000_000_000_000_000), discountID, now.Add(time.Minute), + ) + + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: []liquidlane.Inventory{discount}, + MaxFeePerGas: big.NewInt(0), ChainTime: now, QuoteExpiresAt: now.Add(90 * time.Second), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes = %+v", out.Quotes) + } + quote := out.Quotes[0] + last := quote.Ranges[len(quote.Ranges)-1] + lastRate, ok := new(big.Rat).SetString(last.Quote) + if quote.Expiry != now.Add(48*time.Second).Unix() || last.MaxAmount.String() != "1125" || + !ok || lastRate.Sign() <= 0 || lastRate.Cmp(big.NewRat(8, 10)) > 0 { + t.Fatalf("discount quote = %+v", quote) + } +} + +func TestDecideQuotesPublishesOnePairForDirectAndDiscount(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + route := liquidlane.Route{ + ID: "route-1", TokenIn: common.HexToAddress("0x1111111111111111111111111111111111111111"), + TokenOut: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenInDecimals: 6, TokenOutDecimals: 6, + } + direct := liquidlane.DirectInventory( + route, big.NewInt(1_000), big.NewInt(900_000_000_000_000_000), + ) + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + discount := liquidlane.DiscountInventory( + route, big.NewInt(1_000), big.NewInt(800_000_000_000_000_000), discountID, now.Add(time.Minute), + ) + + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: []liquidlane.Inventory{direct, discount}, MaxFeePerGas: big.NewInt(0), + ChainTime: now, ServerTime: now, QuoteExpiresAt: now.Add(time.Minute), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes = %+v", out.Quotes) + } +} + +func TestDecideQuotesSkipsBelowMinAmount(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "1000001"})) + if err != nil { + t.Fatalf("New: %v", err) + } + + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Solver: common.HexToAddress("0x1111111111111111111111111111111111111111"), + ChainTime: time.Unix(1_800_000_000, 0), + QuoteExpiresAt: time.Unix(1_800_000_090, 0), + MaxFeePerGas: big.NewInt(0), + Inventory: []liquidlane.Inventory{{ + Route: liquidlane.Route{ + ID: "route-1", + TokenIn: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenOut: common.HexToAddress("0x4444444444444444444444444444444444444444"), + TokenInDecimals: 6, + TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(1_000_000), + MaxRate: big.NewInt(1_000_000_000_000_000_000), + }}, + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 0 { + t.Fatalf("quotes len = %d", len(out.Quotes)) + } +} + +func TestDecideFillSelectsProfitableRoute(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "1000"})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + adapter := common.HexToAddress("0x3333333333333333333333333333333333333333") + + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: big.NewInt(1_000_000), + OutputAmount: big.NewInt(990_000), + ChainTime: time.Unix(1_800_000_000, 0), + MaxFeePerGas: big.NewInt(0), + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ID: "route-1", Adapter: adapter, TokenIn: tokenIn, TokenOut: tokenOut}, + MaxAssets: big.NewInt(2_000_000), + }, + AmountIn: big.NewInt(1_000_000), MaxAmountOut: big.NewInt(1_000_000), + }}, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan == nil { + t.Fatal("expected fill plan") + } + if len(plan.Routes) != 1 || plan.Routes[0].Adapter != adapter { + t.Fatalf("routes = %+v", plan.Routes) + } + if plan.Routes[0].ExpectedAmountOut.String() != "1000000" { + t.Fatalf("plan = %+v", plan) + } +} + +func TestDecideFillPermissionedTokenNeverAggregatesRoutes(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + quotes := []liquidlane.FillQuote{ + { + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", + TokenIn: tokenIn, TokenOut: tokenOut, + }, + MaxAssets: big.NewInt(500), + }, + AmountIn: big.NewInt(1_000), MaxAmountOut: big.NewInt(1_000), + }, + { + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: "route-2", CapacityID: "capacity-2", + TokenIn: tokenIn, TokenOut: tokenOut, + }, + MaxAssets: big.NewInt(500), + }, + AmountIn: big.NewInt(1_000), MaxAmountOut: big.NewInt(1_000), + }, + } + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: big.NewInt(1_000), OutputAmount: big.NewInt(900), + RequireSingleRoute: true, + ChainTime: time.Unix(1_800_000_000, 0), MaxFeePerGas: big.NewInt(0), Quotes: quotes, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan != nil { + t.Fatalf("permissioned token must not aggregate routes, got %+v", plan) + } +} + +func TestDecideFillSelectsBestRouteInsteadOfConfigOrder(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + firstAdapter := common.HexToAddress("0x3333333333333333333333333333333333333333") + bestAdapter := common.HexToAddress("0x4444444444444444444444444444444444444444") + + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, + AmountIn: big.NewInt(1_000_000), OutputAmount: big.NewInt(990_000), + ChainTime: time.Unix(1_800_000_000, 0), + MaxFeePerGas: big.NewInt(0), + Quotes: []liquidlane.FillQuote{ + { + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ID: "route-1", Adapter: firstAdapter, TokenIn: tokenIn, TokenOut: tokenOut}, + MaxAssets: big.NewInt(2_000_000), + }, + AmountIn: big.NewInt(1_000_000), MaxAmountOut: big.NewInt(1_000_000), + }, + { + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ID: "route-2", Adapter: bestAdapter, TokenIn: tokenIn, TokenOut: tokenOut}, + MaxAssets: big.NewInt(2_000_000), + }, + AmountIn: big.NewInt(1_000_000), MaxAmountOut: big.NewInt(1_100_000), + }, + }, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan == nil || len(plan.Routes) != 1 || plan.Routes[0].Adapter != bestAdapter { + t.Fatalf("plan = %+v, want adapter %s", plan, bestAdapter) + } +} + +func TestDecideFillCommitsSelectedPrivateDiscount(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + adapter := common.HexToAddress("0x3333333333333333333333333333333333333333") + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + route := liquidlane.Route{ID: "route-1", Adapter: adapter, TokenIn: tokenIn, TokenOut: tokenOut} + + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, + AmountIn: big.NewInt(1_000), OutputAmount: big.NewInt(850), ChainTime: time.Unix(1_800_000_000, 0), + MaxFeePerGas: big.NewInt(0), + Quotes: []liquidlane.FillQuote{ + {Inventory: liquidlane.Inventory{Route: route, MaxAssets: big.NewInt(1_000)}, AmountIn: big.NewInt(1_000), MaxAmountOut: big.NewInt(900)}, + { + Inventory: liquidlane.Inventory{ + Route: route, MaxAssets: big.NewInt(1_000), + DiscountID: &discountID, + }, + AmountIn: big.NewInt(1_000), MaxAmountOut: big.NewInt(950), MinDiscount: big.NewInt(100_000), + }, + }, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan == nil || len(plan.Routes) != 1 || plan.Routes[0].DiscountID == nil || + *plan.Routes[0].DiscountID != discountID { + t.Fatalf("plan = %+v", plan) + } +} + +func TestDecideFillChargesPrivateExecutionGasAfterGreedySelection(t *testing.T) { + cfg := testStrategyConfig(Config{}) + strategy, err := New(cfg) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + adapter := common.HexToAddress("0x3333333333333333333333333333333333333333") + vault := common.HexToAddress("0x4444444444444444444444444444444444444444") + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + route := liquidlane.Route{ID: "route-1", Adapter: adapter, Vault: vault, TokenIn: tokenIn, TokenOut: tokenOut} + + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, + AmountIn: big.NewInt(1_000), OutputAmount: big.NewInt(900_000), ChainTime: time.Unix(1_800_000_000, 0), + MaxFeePerGas: big.NewInt(1), + GasPrices: testGasPrices(tokenOut, 1_000_000_000_000_000_000), + GasSnapshot: &liquidlanegas.Snapshot{ + Adapters: map[common.Address]*liquidlanegas.AdapterState{ + adapter: {Vault: vault, Acquire: map[common.Address]*big.Int{tokenIn: big.NewInt(3_000_000)}}, + }, + Vaults: map[common.Address]*liquidlanegas.VaultState{ + vault: {FreeAssets: new(big.Int), Withdrawable: new(big.Int)}, + }, + }, + Quotes: []liquidlane.FillQuote{ + { + Inventory: liquidlane.Inventory{Route: route, MaxAssets: big.NewInt(3_000_000)}, + AmountIn: big.NewInt(1_000), MaxAmountOut: big.NewInt(1_800_000), + }, + { + Inventory: liquidlane.Inventory{ + Route: route, MaxAssets: big.NewInt(3_000_000), + DiscountID: &discountID, + }, + AmountIn: big.NewInt(1_000), MaxAmountOut: big.NewInt(1_800_050), + }, + }, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan == nil || len(plan.Routes) != 1 || plan.Routes[0].DiscountID == nil { + t.Fatalf("plan = %+v, want higher-rate private route", plan) + } + if plan.Routes[0].MinAmountOut.Cmp(big.NewInt(900_000)) <= 0 { + t.Fatalf("minAmountOut = %s, want order output plus complete-plan gas", plan.Routes[0].MinAmountOut) + } +} + +func TestDecideFillPrivateCapacityIncludesUpwardPriceBuffer(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{PriceBufferBps: 100})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + + for _, tt := range []struct { + name string + maxAmountOut int64 + wantFill bool + }{ + {name: "buffer fits", maxAmountOut: 9_900, wantFill: true}, + {name: "buffer exceeds capacity", maxAmountOut: 9_901, wantFill: false}, + } { + t.Run(tt.name, func(t *testing.T) { + plan, fillErr := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, + AmountIn: big.NewInt(10_000), OutputAmount: big.NewInt(9_500), ChainTime: now, + MaxFeePerGas: big.NewInt(0), + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: "route-1", Adapter: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenIn: tokenIn, TokenOut: tokenOut, + }, + MaxAssets: big.NewInt(10_000), + DiscountID: &discountID, ValidUntil: now.Add(time.Minute), + }, + AmountIn: big.NewInt(10_000), + MaxAmountOut: big.NewInt(tt.maxAmountOut), + MinDiscount: big.NewInt(100_000), + }}, + }) + if fillErr != nil { + t.Fatalf("DecideFill: %v", fillErr) + } + if (plan != nil) != tt.wantFill { + t.Fatalf("plan = %+v, wantFill = %v", plan, tt.wantFill) + } + if tt.wantFill && plan.Routes[0].ReservedAmountOut.String() != "9999" { + t.Fatalf("private reservation = %s, want 9999", plan.Routes[0].ReservedAmountOut) + } + }) + } +} + +func TestDecideFillSubtractsPendingCapacityReservations(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + route := liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", + Adapter: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenIn: tokenIn, + TokenOut: tokenOut, + } + + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: big.NewInt(100), + OutputAmount: big.NewInt(90), + MaxFeePerGas: big.NewInt(0), + Reservations: map[liquidlane.CapacityID]*big.Int{"capacity-1": big.NewInt(60)}, + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{Route: route, MaxAssets: big.NewInt(100)}, + AmountIn: big.NewInt(100), + MaxAmountOut: big.NewInt(100), + }}, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan != nil { + t.Fatalf("plan = %+v, want pending reservation to leave insufficient capacity", plan) + } +} + +func TestDecideFillRequiresExecutionDeadlineBuffer(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{ExecutionDeadlineBuffer: "30s"})) + if err != nil { + t.Fatalf("New: %v", err) + } + now := time.Unix(1_800_000_000, 0) + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, + AmountIn: big.NewInt(1_000), OutputAmount: big.NewInt(900), ChainTime: now, + Expires: uint32(now.Add(30 * time.Second).Unix()), FillDeadline: uint32(now.Add(time.Minute).Unix()), + MaxFeePerGas: big.NewInt(0), Quotes: profitableFillQuotes(tokenIn, tokenOut), + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan != nil { + t.Fatalf("expected near-expiry order to be skipped, got %+v", plan) + } +} + +func TestDecideQuotesKeepsInventoryReserve(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{InventoryReserveBps: 1_000, MinAmount: "2"})) + if err != nil { + t.Fatalf("New: %v", err) + } + routeItem := liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: "route-1", TokenIn: common.HexToAddress("0x1111111111111111111111111111111111111111"), + TokenOut: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(1_000), MaxRate: big.NewInt(1_000_000_000_000_000_000), + } + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: []liquidlane.Inventory{routeItem}, MaxFeePerGas: big.NewInt(0), + ChainTime: time.Unix(1_800_000_000, 0), QuoteExpiresAt: time.Unix(1_800_000_090, 0), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes = %d", len(out.Quotes)) + } + ranges := out.Quotes[0].Ranges + if got := ranges[len(ranges)-1].MaxAmount.String(); got != "900" { + t.Fatalf("reserved maxAmount = %s, want 900", got) + } +} + +func TestDecideQuotesAppliesReserveBeforeInFlightReservations(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{InventoryReserveBps: 1_000, MinAmount: "2"})) + if err != nil { + t.Fatalf("New: %v", err) + } + routeItem := liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", + TokenIn: common.HexToAddress("0x1111111111111111111111111111111111111111"), + TokenOut: common.HexToAddress("0x2222222222222222222222222222222222222222"), + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(1_000), MaxRate: big.NewInt(1_000_000_000_000_000_000), + } + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: []liquidlane.Inventory{routeItem}, MaxFeePerGas: big.NewInt(0), + Reservations: map[liquidlane.CapacityID]*big.Int{"capacity-1": big.NewInt(800)}, + ChainTime: time.Unix(1_800_000_000, 0), + QuoteExpiresAt: time.Unix(1_800_000_090, 0), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 1 { + t.Fatalf("quotes = %d", len(out.Quotes)) + } + if got := out.Quotes[0].Ranges[len(out.Quotes[0].Ranges)-1].MaxAmount.String(); got != "100" { + t.Fatalf("maxAmount = %s, want reserve-first capacity 100", got) + } +} + +func TestDecideQuotesSharesOneCapacityDomainAcrossRoutes(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{MinAmount: "2"})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + inventory := []liquidlane.Inventory{ + { + Route: liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", + TokenIn: common.HexToAddress("0x1111111111111111111111111111111111111111"), TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(1_000), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }, + { + Route: liquidlane.Route{ + ID: "route-2", CapacityID: "capacity-1", + TokenIn: common.HexToAddress("0x3333333333333333333333333333333333333333"), TokenOut: tokenOut, + TokenInDecimals: 6, TokenOutDecimals: 6, + }, + MaxAssets: big.NewInt(1_000), MaxRate: big.NewInt(1_000_000_000_000_000_000), + }, + } + out, err := strategy.DecideQuotes(context.Background(), types.QuoteInput{ + Inventory: inventory, + MaxFeePerGas: big.NewInt(0), + ChainTime: time.Unix(1_800_000_000, 0), + QuoteExpiresAt: time.Unix(1_800_000_090, 0), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(out.Quotes) != 2 { + t.Fatalf("quotes = %d", len(out.Quotes)) + } + total := new(big.Int) + for _, quote := range out.Quotes { + total.Add(total, quote.Ranges[len(quote.Ranges)-1].MaxAmount) + } + if total.String() != "1000" { + t.Fatalf("total quoted capacity = %s, want 1000", total) + } +} + +func TestDecideFillSeparatesBufferedTargetFromEconomicFloor(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{PriceBufferBps: 100})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, + AmountIn: big.NewInt(10_000), OutputAmount: big.NewInt(9_600), + MaxFeePerGas: big.NewInt(100), + GasPrices: testGasPrices(tokenOut, 1), + ChainTime: time.Unix(1_800_000_000, 0), + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", + Adapter: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenIn: tokenIn, TokenOut: tokenOut, + }, + MaxAssets: big.NewInt(20_000), + }, + AmountIn: big.NewInt(10_000), MaxAmountOut: big.NewInt(10_000), + }}, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan == nil || len(plan.Routes) != 1 { + t.Fatalf("plan = %+v", plan) + } + if plan.Routes[0].ExpectedAmountOut.String() != "9900" || + plan.Routes[0].MinAmountOut.String() != "9601" { + t.Fatalf("plan = %+v", plan) + } +} + +func TestDecideFillRejectsDutchAuctionContext(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + for _, outputContext := range [][]byte{{dutchAuctionContextType}, {exclusiveDutchAuctionContextType}} { + plan, decideErr := strategy.DecideFill(context.Background(), types.FillInput{ + AmountIn: big.NewInt(1_000_000), + OutputAmount: big.NewInt(990_000), + OutputContext: outputContext, + }) + if decideErr == nil || !strings.Contains(decideErr.Error(), "Dutch auctions are not supported") { + t.Fatalf("DecideFill(context=%x) error = %v", outputContext, decideErr) + } + if plan != nil { + t.Fatalf("DecideFill(context=%x) plan = %+v", outputContext, plan) + } + } +} + +func TestDecideFillRespectsExclusiveWindow(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + solver := common.HexToAddress("0x5555555555555555555555555555555555555555") + otherSolver := common.HexToAddress("0x6666666666666666666666666666666666666666") + + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + Solver: solver, + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: big.NewInt(1_000_000), + OutputAmount: big.NewInt(990_000), + OutputContext: exclusiveLimitContext(otherSolver), + ChainTime: time.Unix(1_800_000_000, 0), + MaxFeePerGas: big.NewInt(0), + Quotes: profitableFillQuotes(tokenIn, tokenOut), + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan != nil { + t.Fatalf("expected skip during another solver's exclusive window, got %+v", plan) + } + + plan, err = strategy.DecideFill(context.Background(), types.FillInput{ + Solver: solver, + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: big.NewInt(1_000_000), + OutputAmount: big.NewInt(990_000), + OutputContext: exclusiveLimitContext(otherSolver), + ChainTime: time.Unix(1_800_000_011, 0), + MaxFeePerGas: big.NewInt(0), + Quotes: profitableFillQuotes(tokenIn, tokenOut), + }) + if err != nil { + t.Fatalf("DecideFill after window: %v", err) + } + if plan == nil { + t.Fatal("expected fill after exclusive window") + } +} + +func TestOutputPricingSupportsOutputSettlerSimpleContexts(t *testing.T) { + solver := common.HexToAddress("0x5555555555555555555555555555555555555555") + base := big.NewInt(990_000) + now := time.Unix(1_800_000_005, 0) + + tests := map[string]struct { + context []byte + want string + fill bool + wantErr bool + }{ + "empty limit": { + context: nil, + want: "990000", + fill: true, + }, + "typed limit": { + context: []byte{limitOrderContextType}, + want: "990000", + fill: true, + }, + "dutch": { + context: []byte{dutchAuctionContextType}, + wantErr: true, + }, + "exclusive limit for solver": { + context: exclusiveLimitContext(solver), + want: "990000", + fill: true, + }, + "exclusive limit for another solver": { + context: exclusiveLimitContext(common.HexToAddress("0x6666666666666666666666666666666666666666")), + fill: false, + }, + "exclusive dutch": { + context: []byte{exclusiveDutchAuctionContextType}, + wantErr: true, + }, + "invalid type": { + context: []byte{0x02}, + wantErr: true, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + pricing, err := parseOutputContext(base, tt.context) + if tt.wantErr { + if err == nil { + t.Fatal("expected error") + } + return + } + if err != nil { + t.Fatalf("parseOutputContext: %v", err) + } + got, ok := pricing.fill(solver, now, big.NewInt(1_000_000)) + if ok != tt.fill { + t.Fatalf("fill = %v", ok) + } + if !ok { + return + } + if got.String() != tt.want { + t.Fatalf("amount = %s, want %s", got, tt.want) + } + }) + } +} + +func TestDistributeMinimumsPreservesTotalAndRouteBounds(t *testing.T) { + tests := []struct { + name string + targets []*big.Int + total *big.Int + want []string + }{ + {name: "equal", targets: []*big.Int{big.NewInt(500), big.NewInt(500)}, total: big.NewInt(900), want: []string{"450", "450"}}, + {name: "uneven", targets: []*big.Int{big.NewInt(200), big.NewInt(800)}, total: big.NewInt(503), want: []string{"100", "403"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := distributeMinimums(tt.targets, tt.total) + if len(got) != len(tt.want) { + t.Fatalf("minimums = %v", got) + } + total := new(big.Int) + for i := range got { + if got[i].String() != tt.want[i] || got[i].Sign() <= 0 || got[i].Cmp(tt.targets[i]) > 0 { + t.Fatalf("minimum[%d] = %s, want %s", i, got[i], tt.want[i]) + } + total.Add(total, got[i]) + } + if total.Cmp(tt.total) != 0 { + t.Fatalf("sum = %s, want %s", total, tt.total) + } + }) + } +} + +func TestDecideFillDoesNotRequireProfitMargin(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: big.NewInt(1_000_000), + OutputAmount: big.NewInt(990_000), + MaxFeePerGas: big.NewInt(0), + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ID: "route-1", Adapter: common.HexToAddress("0x3333333333333333333333333333333333333333"), TokenIn: tokenIn, TokenOut: tokenOut}, + MaxAssets: big.NewInt(2_000_000), + }, + AmountIn: big.NewInt(1_000_000), MaxAmountOut: big.NewInt(999_000), + }}, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan == nil { + t.Fatal("expected fill without an explicit profit margin") + } +} + +func TestDecideFillSkipsExpiredOrder(t *testing.T) { + strategy, err := New(testStrategyConfig(Config{})) + if err != nil { + t.Fatalf("New: %v", err) + } + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + + plan, err := strategy.DecideFill(context.Background(), types.FillInput{ + TokenIn: tokenIn, + TokenOut: tokenOut, + AmountIn: big.NewInt(1_000_000), + OutputAmount: big.NewInt(990_000), + Expires: 1_700_000_000, + FillDeadline: 1_700_000_100, + ChainTime: time.Unix(1_700_000_000, 0), + MaxFeePerGas: big.NewInt(0), + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ID: "route-1", Adapter: common.HexToAddress("0x3333333333333333333333333333333333333333"), TokenIn: tokenIn, TokenOut: tokenOut}, + MaxAssets: big.NewInt(2_000_000), + }, + AmountIn: big.NewInt(1_000_000), MaxAmountOut: big.NewInt(1_000_000), + }}, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan != nil { + t.Fatalf("expected expired skip, got %#v", plan) + } +} + +func exclusiveLimitContext(exclusiveFor common.Address) []byte { + out := make([]byte, 37) + out[0] = exclusiveLimitOrderContextType + solverID := solverIdentifier(exclusiveFor) + copy(out[1:33], solverID[:]) + binary.BigEndian.PutUint32(out[33:37], 1_800_000_010) + return out +} + +func profitableFillQuotes(tokenIn, tokenOut common.Address) []liquidlane.FillQuote { + return []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{ + Route: liquidlane.Route{ + ID: "route-1", Adapter: common.HexToAddress("0x3333333333333333333333333333333333333333"), + TokenIn: tokenIn, TokenOut: tokenOut, + }, + MaxAssets: big.NewInt(2_000_000), + }, + AmountIn: big.NewInt(1_000_000), + MaxAmountOut: big.NewInt(1_000_000), + }} +} + +func testStrategyConfig(cfg Config) Config { + return cfg +} + +func testGasPrices(token common.Address, amount int64) *liquidlanegas.PriceSnapshot { + return liquidlanegas.NewPriceSnapshot(map[common.Address]*big.Int{token: big.NewInt(amount)}) +} + +func TestFixedPointDecimal(t *testing.T) { + tests := map[string]string{ + "0": "0", + "990000000000000000": "0.99", + "1000000000000000000": "1", + "1234500000000000000": "1.2345", + "1000000000000000000000": "1000", + } + for raw, want := range tests { + n, ok := new(big.Int).SetString(raw, 10) + if !ok { + t.Fatalf("bad test int %s", raw) + } + if got := fixedPointDecimal(n, 18); got != want { + t.Fatalf("fixedPointDecimal(%s) = %q, want %q", raw, got, want) + } + } +} + +func TestQuoteLadderKeepsBestDirectAndPrivate(t *testing.T) { + route := liquidlane.Route{ID: "route-1"} + bestDiscount := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + worseDiscount := common.HexToHash("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + ladder := buildQuoteLadder([]quoteCandidate{ + { + Inventory: liquidlane.Inventory{Route: route, MaxRate: big.NewInt(90)}, + maxInput: big.NewInt(100), + }, + { + Inventory: liquidlane.Inventory{Route: route, MaxRate: big.NewInt(100), DiscountID: &bestDiscount}, + maxInput: big.NewInt(100), + }, + { + Inventory: liquidlane.Inventory{Route: route, MaxRate: big.NewInt(95), DiscountID: &worseDiscount}, + maxInput: big.NewInt(1_000), + }, + }) + if len(ladder) != 1 || len(ladder[0].alternatives) != 2 { + t.Fatalf("ladder = %+v", ladder) + } + foundDirect, foundBestPrivate := false, false + for _, candidate := range ladder[0].alternatives { + foundDirect = foundDirect || candidate.DiscountID == nil + foundBestPrivate = foundBestPrivate || + (candidate.DiscountID != nil && *candidate.DiscountID == bestDiscount) + } + if !foundDirect || !foundBestPrivate { + t.Fatalf("alternatives = %+v", ladder[0].alternatives) + } +} + +func TestSolveExactInputQuoteUsesAlternativeThatCoversWholeLeg(t *testing.T) { + discountID := common.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + routeA := liquidlane.Route{ID: "route-a"} + routeB := liquidlane.Route{ID: "route-b"} + ladder := buildQuoteLadder([]quoteCandidate{ + {Inventory: liquidlane.Inventory{Route: routeA, MaxRate: big.NewInt(500_000_000_000_000_000)}, maxInput: big.NewInt(1_000)}, + {Inventory: liquidlane.Inventory{Route: routeA, MaxRate: big.NewInt(2_000_000_000_000_000_000), DiscountID: &discountID}, maxInput: big.NewInt(1)}, + {Inventory: liquidlane.Inventory{Route: routeB, MaxRate: big.NewInt(1_000_000_000_000_000_000)}, maxInput: big.NewInt(1_000)}, + }) + + pricing := gasPricing{feePerGas: new(big.Int), tokenOutPerNative: new(big.Int)} + quote, ok := solveExactInputQuote(ladder, big.NewInt(1_000), pricing) + if !ok || len(quote.candidates) != 1 { + t.Fatalf("quote = %+v, ok = %v", quote, ok) + } + if quote.candidates[0].ID != routeB.ID || quote.candidates[0].DiscountID != nil { + t.Fatalf("candidates = %+v", quote.candidates) + } + if quote.grossAmountOut.Cmp(big.NewInt(1_000)) != 0 { + t.Fatalf("output = %s, want 1000", quote.grossAmountOut) + } + + small, ok := solveExactInputQuote(ladder, big.NewInt(1), pricing) + if !ok || len(small.candidates) != 1 || small.candidates[0].DiscountID == nil { + t.Fatalf("small quote = %+v, ok = %v", small, ok) + } +} + +func TestQuoteRangesCoverEveryInteriorAmountAndCandidate(t *testing.T) { + strategy := &Strategy{minAmount: big.NewInt(1), rangeCount: 8} + pricing := gasPricing{feePerGas: new(big.Int), tokenOutPerNative: new(big.Int)} + rateUnit := new(big.Int).Exp(big.NewInt(10), big.NewInt(rateScaleDigits), nil) + + for scenario := 1; scenario <= 200; scenario++ { + candidates := make([]quoteCandidate, 0, 6) + for routeIndex := 0; routeIndex < 3; routeIndex++ { + route := liquidlane.Route{ID: liquidlane.RouteID("route-" + strconv.Itoa(routeIndex))} + directInput := big.NewInt(int64(1 + (scenario*(routeIndex+3))%15)) + directRate := new(big.Int).Mul( + rateUnit, big.NewInt(int64(1+(scenario*(routeIndex+5))%7)), + ) + candidates = append(candidates, quoteCandidate{ + Inventory: liquidlane.Inventory{Route: route, MaxRate: directRate}, + maxInput: directInput, + }) + + discountID := common.BigToHash(big.NewInt(int64(scenario*10 + routeIndex + 1))) + privateInput := big.NewInt(int64(1 + (scenario*(routeIndex+7))%15)) + privateRate := new(big.Int).Mul( + rateUnit, big.NewInt(int64(1+(scenario*(routeIndex+11))%9)), + ) + candidates = append(candidates, quoteCandidate{ + Inventory: liquidlane.Inventory{ + Route: route, MaxRate: privateRate, DiscountID: &discountID, + }, + maxInput: privateInput, + }) + } + + ladder := buildQuoteLadder(candidates) + ranges, sampledCandidates := strategy.buildQuoteRanges(ladder, pricing) + for _, quoteRange := range ranges { + rate, parsed := new(big.Rat).SetString(quoteRange.Quote) + if !parsed { + t.Fatalf("scenario %d: invalid rate %q", scenario, quoteRange.Quote) + } + for amount := quoteRange.MinAmount.Int64(); amount <= quoteRange.MaxAmount.Int64(); amount++ { + solution, solved := solveExactInputQuote(ladder, big.NewInt(amount), pricing) + if !solved { + t.Fatalf("scenario %d amount %d: published without a solution", scenario, amount) + } + quoted := new(big.Int).Mul(big.NewInt(amount), rate.Num()) + quoted.Div(quoted, rate.Denom()) + if quoted.Cmp(solution.grossAmountOut) > 0 { + t.Fatalf( + "scenario %d amount %d: quoted %s above output %s in range %+v", + scenario, amount, quoted, solution.grossAmountOut, quoteRange, + ) + } + for _, candidate := range solution.candidates { + if _, found := sampledCandidates[candidate.id()]; !found { + t.Fatalf( + "scenario %d amount %d: interior candidate %s missing from expiry set", + scenario, amount, candidate.id(), + ) + } + } + } + } + } +} diff --git a/internal/solvers/lifi/strategies/gas.go b/internal/solvers/lifi/strategies/gas.go new file mode 100644 index 00000000..9dbc3437 --- /dev/null +++ b/internal/solvers/lifi/strategies/gas.go @@ -0,0 +1,97 @@ +package strategies + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" +) + +const ( + nativeUnit = 1_000_000_000_000_000_000 + // Foundry measured the full mocked LI.FI direct settlement at <=478,838 gas. The shared + // LiquidLane predictor already charges at least 300,000 for the first swap. + settlementGasUnits uint64 = 250_000 + // Private routes additionally decode and verify signed discount payloads. + privateRouteGasUnits uint64 = 75_000 +) + +// GasLeg describes one LiquidLane swap included in a LI.FI settlement. +type GasLeg struct { + Route liquidlane.Route + AmountOut *big.Int + Private bool +} + +// FillGasCost predicts one LI.FI settlement and converts its native gas cost into tokenOut. +func FillGasCost( + maxFeePerGas *big.Int, + tokenOut common.Address, + prices *liquidlanegas.PriceSnapshot, + snapshot *liquidlanegas.Snapshot, + legs []GasLeg, +) (*big.Int, error) { + if maxFeePerGas == nil || maxFeePerGas.Sign() == 0 || len(legs) == 0 { + return new(big.Int), nil + } + if maxFeePerGas.Sign() < 0 { + return nil, errors.New("max fee per gas must be non-negative") + } + tokenOutPerNative := prices.TokenOutPerNative(tokenOut) + if tokenOutPerNative == nil || tokenOutPerNative.Sign() <= 0 { + return nil, errors.Errorf("gas oracle: missing tokenOut rate for %s", tokenOut.Hex()) + } + return FillGasCostAtRate(maxFeePerGas, tokenOutPerNative, snapshot, legs), nil +} + +// FillGasCostAtRate predicts one LI.FI settlement using an already-validated token/native rate. +func FillGasCostAtRate( + maxFeePerGas, tokenOutPerNative *big.Int, + snapshot *liquidlanegas.Snapshot, + legs []GasLeg, +) *big.Int { + if maxFeePerGas == nil || maxFeePerGas.Sign() <= 0 || tokenOutPerNative == nil || + tokenOutPerNative.Sign() <= 0 || len(legs) == 0 { + return new(big.Int) + } + demands := make([]liquidlanegas.AdapterDemand, 0, len(legs)) + units := settlementGasUnits + for _, leg := range legs { + demands = append(demands, liquidlanegas.AdapterDemand{ + Adapter: leg.Route.Adapter, + Vault: leg.Route.Vault, + Demand: liquidlanegas.Demand{ + Collateral: leg.Route.TokenIn, + AmountOut: liquidlane.CloneBig(leg.AmountOut), + }, + }) + if leg.Private { + units = saturatingAdd(units, privateRouteGasUnits) + } + } + units = saturatingAdd(units, liquidlanegas.PredictAdapters(demands, snapshot).Units) + nativeCost := new(big.Int).Mul(maxFeePerGas, new(big.Int).SetUint64(units)) + return mulDivUp(nativeCost, tokenOutPerNative, big.NewInt(nativeUnit)) +} + +func mulDivUp(x, y, denominator *big.Int) *big.Int { + if x == nil || y == nil || denominator == nil || denominator.Sign() <= 0 { + return new(big.Int) + } + numerator := new(big.Int).Mul(x, y) + quotient, remainder := new(big.Int).QuoRem(numerator, denominator, new(big.Int)) + if remainder.Sign() > 0 { + quotient.Add(quotient, big.NewInt(1)) + } + return quotient +} + +func saturatingAdd(a, b uint64) uint64 { + if b > ^uint64(0)-a { + return ^uint64(0) + } + return a + b +} diff --git a/internal/solvers/lifi/strategies/gas_test.go b/internal/solvers/lifi/strategies/gas_test.go new file mode 100644 index 00000000..bfaa52c6 --- /dev/null +++ b/internal/solvers/lifi/strategies/gas_test.go @@ -0,0 +1,72 @@ +package strategies + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" +) + +func TestFillGasCostIncludesSettlementRouteAndPrivateOverhead(t *testing.T) { + adapter := common.HexToAddress("0x1111111111111111111111111111111111111111") + vault := common.HexToAddress("0x2222222222222222222222222222222222222222") + tokenIn := common.HexToAddress("0x3333333333333333333333333333333333333333") + tokenOut := common.HexToAddress("0x4444444444444444444444444444444444444444") + snapshot := &liquidlanegas.Snapshot{ + Adapters: map[common.Address]*liquidlanegas.AdapterState{ + adapter: {Vault: vault, Acquire: map[common.Address]*big.Int{}}, + }, + Vaults: map[common.Address]*liquidlanegas.VaultState{ + vault: {FreeAssets: big.NewInt(100), Withdrawable: big.NewInt(100)}, + }, + } + prices := liquidlanegas.NewPriceSnapshot(map[common.Address]*big.Int{ + tokenOut: big.NewInt(nativeUnit), + }) + leg := GasLeg{ + Route: liquidlane.Route{Adapter: adapter, Vault: vault, TokenIn: tokenIn}, + AmountOut: big.NewInt(10), + } + + direct, err := FillGasCost(big.NewInt(3), tokenOut, prices, snapshot, []GasLeg{leg}) + if err != nil { + t.Fatalf("direct FillGasCost: %v", err) + } + wantDirect := new(big.Int).SetUint64( + settlementGasUnits + liquidlanegas.UnitsForRouteAt(liquidlanegas.RouteAllocate, true), + ) + wantDirect.Mul(wantDirect, big.NewInt(3)) + if direct.Cmp(wantDirect) != 0 { + t.Fatalf("direct gas cost = %s, want %s", direct, wantDirect) + } + + leg.Private = true + private, err := FillGasCost(big.NewInt(3), tokenOut, prices, snapshot, []GasLeg{leg}) + if err != nil { + t.Fatalf("private FillGasCost: %v", err) + } + wantPrivate := new(big.Int).Add( + wantDirect, + new(big.Int).Mul(new(big.Int).SetUint64(privateRouteGasUnits), big.NewInt(3)), + ) + if private.Cmp(wantPrivate) != 0 { + t.Fatalf("private gas cost = %s, want %s", private, wantPrivate) + } +} + +func TestFillGasCostRejectsMissingTokenRate(t *testing.T) { + tokenOut := common.HexToAddress("0x4444444444444444444444444444444444444444") + _, err := FillGasCost( + big.NewInt(1), + tokenOut, + nil, + nil, + []GasLeg{{AmountOut: big.NewInt(1)}}, + ) + if err == nil { + t.Fatal("expected missing token rate error") + } +} diff --git a/internal/solvers/lifi/strategies/registry.go b/internal/solvers/lifi/strategies/registry.go new file mode 100644 index 00000000..ab05d401 --- /dev/null +++ b/internal/solvers/lifi/strategies/registry.go @@ -0,0 +1,61 @@ +package strategies + +import ( + "sort" + "sync" + + "github.com/go-errors/errors" + "github.com/go-logr/logr" + "gopkg.in/yaml.v3" + + "github.com/symbioticfi/vault-solver/internal/chain" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" +) + +type Deps struct { + Chain *chain.Client + Log logr.Logger +} + +type Factory func(raw yaml.Node, deps Deps) (types.Strategy, error) + +var ( + mu sync.RWMutex + registry = map[string]Factory{} +) + +func Register(name string, f Factory) { + mu.Lock() + defer mu.Unlock() + if name == "" { + panic("lifi strategy: Register called with empty name") + } + if f == nil { + panic("lifi strategy: Register called with nil factory for " + name) + } + if _, dup := registry[name]; dup { + panic("lifi strategy: duplicate registration for " + name) + } + registry[name] = f +} + +func New(name string, raw yaml.Node, deps Deps) (types.Strategy, error) { + mu.RLock() + f, ok := registry[name] + mu.RUnlock() + if !ok { + return nil, errors.Errorf("unknown LI.FI strategy %q (registered: %v)", name, Registered()) + } + return f(raw, deps) +} + +func Registered() []string { + mu.RLock() + defer mu.RUnlock() + names := make([]string, 0, len(registry)) + for name := range registry { + names = append(names, name) + } + sort.Strings(names) + return names +} diff --git a/internal/solvers/lifi/strategies/types/types.go b/internal/solvers/lifi/strategies/types/types.go new file mode 100644 index 00000000..bee95a31 --- /dev/null +++ b/internal/solvers/lifi/strategies/types/types.go @@ -0,0 +1,97 @@ +// Package types defines the LI.FI same-chain solver strategy contract. +package types + +import ( + "context" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" +) + +const ( + // MaxRoutes bounds the physical LiquidLane routes used by one quote or fill. + MaxRoutes = 3 + // MaxQuoteRanges bounds the amount ranges published for one token pair. + MaxQuoteRanges = 16 +) + +type Strategy interface { + DecideQuotes(ctx context.Context, input QuoteInput) (QuoteOutput, error) + DecideFill(ctx context.Context, input FillInput) (*FillPlan, error) +} + +type QuoteInput struct { + Solver common.Address `json:"solver"` + Inventory []liquidlane.Inventory `json:"inventory"` + Reservations map[liquidlane.CapacityID]*big.Int `json:"reservations"` + SingleRouteTokens map[common.Address]bool `json:"singleRouteTokens"` + GasSnapshot *liquidlanegas.Snapshot `json:"gasSnapshot"` + GasPrices *liquidlanegas.PriceSnapshot `json:"gasPrices"` + MaxFeePerGas *big.Int `json:"maxFeePerGas"` + ChainTime time.Time `json:"chainTime"` + ServerTime time.Time `json:"serverTime"` + QuoteExpiresAt time.Time `json:"quoteExpiresAt"` +} + +type QuoteOutput struct { + Quotes []Quote `json:"quotes"` +} + +type Quote struct { + FromAsset common.Address `json:"fromAsset"` + ToAsset common.Address `json:"toAsset"` + + FromDecimals int `json:"fromDecimals"` + ToDecimals int `json:"toDecimals"` + + Ranges []QuoteRange `json:"ranges"` + Expiry int64 `json:"expiry"` + ExclusiveFor common.Address `json:"exclusiveFor"` +} + +type QuoteRange struct { + MinAmount *big.Int `json:"minAmount"` + MaxAmount *big.Int `json:"maxAmount"` + Quote string `json:"quote"` +} + +type FillInput struct { + OrderID string `json:"orderId"` + QuoteID string `json:"quoteId"` + Solver common.Address `json:"solver"` + + TokenIn common.Address `json:"tokenIn"` + TokenOut common.Address `json:"tokenOut"` + AmountIn *big.Int `json:"amountIn"` + OutputAmount *big.Int `json:"outputAmount"` + OutputContext []byte `json:"outputContext"` + Expires uint32 `json:"expires"` + FillDeadline uint32 `json:"fillDeadline"` + RequireSingleRoute bool `json:"requireSingleRoute"` + + Quotes []liquidlane.FillQuote `json:"quotes"` + Reservations map[liquidlane.CapacityID]*big.Int `json:"reservations"` + GasSnapshot *liquidlanegas.Snapshot `json:"gasSnapshot"` + GasPrices *liquidlanegas.PriceSnapshot `json:"gasPrices"` + MaxFeePerGas *big.Int `json:"maxFeePerGas"` + ChainTime time.Time `json:"chainTime"` +} + +type FillPlan struct { + Routes []FillRoute `json:"routes"` +} + +type FillRoute struct { + RouteID liquidlane.RouteID `json:"routeId"` + CapacityID liquidlane.CapacityID `json:"capacityId"` + Adapter common.Address `json:"adapter"` + AmountIn *big.Int `json:"amountIn"` + ExpectedAmountOut *big.Int `json:"expectedAmountOut"` + MinAmountOut *big.Int `json:"minAmountOut"` + ReservedAmountOut *big.Int `json:"reservedAmountOut"` + DiscountID *common.Hash `json:"discountId"` +} diff --git a/internal/solvers/lifi/strategies/webhook/strategy.go b/internal/solvers/lifi/strategies/webhook/strategy.go new file mode 100644 index 00000000..d6664c50 --- /dev/null +++ b/internal/solvers/lifi/strategies/webhook/strategy.go @@ -0,0 +1,223 @@ +package webhookstrategy + +import ( + "context" + "math/big" + "net/http" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + "gopkg.in/yaml.v3" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" + "github.com/symbioticfi/vault-solver/internal/webhook" +) + +const ( + Name = "webhook" + decideQuotesRoute = "/decide-quotes" + decideFillRoute = "/decide-fill" +) + +type Strategy struct { + client *webhook.Client +} + +//nolint:gochecknoinits // solver-local strategy self-registration mirrors solver registration. +func init() { + strategies.Register(Name, NewFromConfig) +} + +func NewFromConfig(raw yaml.Node, _ strategies.Deps) (types.Strategy, error) { + cfg, err := webhook.ParseConfig(raw) + if err != nil { + return nil, err + } + client, err := webhook.NewClient(cfg) + if err != nil { + return nil, err + } + return New(client), nil +} + +func New(client *webhook.Client) *Strategy { + return &Strategy{client: client} +} + +func (s *Strategy) DecideQuotes(ctx context.Context, input types.QuoteInput) (types.QuoteOutput, error) { + var out types.QuoteOutput + if err := s.client.DoJSON(ctx, http.MethodPost, decideQuotesRoute, input, &out); err != nil { + return types.QuoteOutput{}, err + } + if err := validateQuotes(input, &out); err != nil { + return types.QuoteOutput{}, err + } + return out, nil +} + +func (s *Strategy) DecideFill(ctx context.Context, input types.FillInput) (*types.FillPlan, error) { + var out *types.FillPlan + if err := s.client.DoJSON(ctx, http.MethodPost, decideFillRoute, input, &out); err != nil { + return nil, err + } + if out == nil { + return nil, nil + } + if err := validateFill(input, out); err != nil { + return nil, err + } + return out, nil +} + +type quotePair struct { + from, to common.Address + fromDec, toDec int +} + +func validateQuotes(input types.QuoteInput, out *types.QuoteOutput) error { + pairs := make(map[quotePair]bool) + seen := make(map[quotePair]bool) + for _, candidate := range input.Inventory { + pairs[quotePair{ + from: candidate.TokenIn, to: candidate.TokenOut, + fromDec: candidate.TokenInDecimals, toDec: candidate.TokenOutDecimals, + }] = true + } + for i := range out.Quotes { + quote := &out.Quotes[i] + pair := quotePair{quote.FromAsset, quote.ToAsset, quote.FromDecimals, quote.ToDecimals} + if !pairs[pair] { + return errors.Errorf("webhook quote %d uses unknown token pair", i) + } + if seen[pair] { + return errors.Errorf("webhook quote %d repeats token pair", i) + } + seen[pair] = true + if quote.Expiry <= input.ServerTime.Unix() || quote.Expiry > input.QuoteExpiresAt.Unix() { + return errors.Errorf("webhook quote %d expiry is outside the solver window", i) + } + if len(quote.Ranges) == 0 || len(quote.Ranges) > types.MaxQuoteRanges { + return errors.Errorf("webhook quote %d has %d ranges, allowed [1,%d]", i, len(quote.Ranges), types.MaxQuoteRanges) + } + for j, priceRange := range quote.Ranges { + rate, rateOK := new(big.Rat).SetString(priceRange.Quote) + if priceRange.MinAmount == nil || priceRange.MaxAmount == nil || priceRange.MinAmount.Sign() <= 0 || + priceRange.MinAmount.Cmp(priceRange.MaxAmount) > 0 || priceRange.Quote == "" { + return errors.Errorf("webhook quote %d range %d is invalid", i, j) + } + if !rateOK || rate.Sign() <= 0 { + return errors.Errorf("webhook quote %d range %d rate is invalid", i, j) + } + } + sort.Slice(quote.Ranges, func(i, j int) bool { return quote.Ranges[i].MinAmount.Cmp(quote.Ranges[j].MinAmount) < 0 }) + for j, priceRange := range quote.Ranges { + if j > 0 && quote.Ranges[j-1].MaxAmount.Cmp(priceRange.MinAmount) >= 0 { + return errors.Errorf("webhook quote %d ranges overlap", i) + } + } + quote.ExclusiveFor = input.Solver + } + return nil +} + +func validateFill(input types.FillInput, plan *types.FillPlan) error { + if input.AmountIn == nil || input.AmountIn.Sign() <= 0 { + return errors.New("webhook fill input amount is invalid") + } + if len(plan.Routes) == 0 || len(plan.Routes) > types.MaxRoutes { + return errors.Errorf("webhook fill has %d routes, allowed [1,%d]", len(plan.Routes), types.MaxRoutes) + } + candidates := make(map[liquidlane.CandidateID]liquidlane.FillQuote, len(input.Quotes)) + for _, candidate := range input.Quotes { + candidates[liquidlane.NewCandidateID(candidate.Route, candidate.DiscountID)] = candidate + } + usedRoutes := make(map[liquidlane.RouteID]bool, len(plan.Routes)) + capacityLimits := make(map[liquidlane.CapacityID]*big.Int, len(plan.Routes)) + capacityUsed := make(map[liquidlane.CapacityID]*big.Int, len(plan.Routes)) + totalInput := new(big.Int) + totalMinimumOutput := new(big.Int) + gasLegs := make([]strategies.GasLeg, 0, len(plan.Routes)) + for i := range plan.Routes { + route := &plan.Routes[i] + id := liquidlane.NewCandidateID(liquidlane.Route{ID: route.RouteID}, route.DiscountID) + candidate, ok := candidates[id] + if !ok { + return errors.Errorf("webhook fill route %d uses unknown candidate %s", i, id) + } + if usedRoutes[candidate.ID] { + return errors.Errorf("webhook fill repeats physical route %s", candidate.ID) + } + usedRoutes[candidate.ID] = true + if route.AmountIn == nil || route.AmountIn.Sign() <= 0 || route.ExpectedAmountOut == nil || + route.ExpectedAmountOut.Sign() <= 0 || route.MinAmountOut == nil || route.MinAmountOut.Sign() <= 0 || + route.MinAmountOut.Cmp(route.ExpectedAmountOut) > 0 || route.ReservedAmountOut == nil || + route.ReservedAmountOut.Sign() <= 0 { + return errors.Errorf("webhook fill route %d has invalid amounts", i) + } + if candidate.MaxAssets == nil || candidate.MaxAssets.Sign() <= 0 { + return errors.Errorf("webhook fill route %d candidate capacity is invalid", i) + } + available := scaledOutput(candidate, route.AmountIn) + if route.ExpectedAmountOut.Cmp(available) > 0 || + route.ReservedAmountOut.Cmp(route.ExpectedAmountOut) < 0 || + route.ReservedAmountOut.Cmp(candidate.MaxAssets) > 0 { + return errors.Errorf("webhook fill route %d exceeds current candidate output or capacity", i) + } + route.RouteID = candidate.ID + route.CapacityID = liquidlane.RouteCapacityID(candidate.Route) + route.Adapter = candidate.Adapter + route.DiscountID = liquidlane.CloneHash(candidate.DiscountID) + totalInput.Add(totalInput, route.AmountIn) + totalMinimumOutput.Add(totalMinimumOutput, route.MinAmountOut) + if limit := capacityLimits[route.CapacityID]; limit == nil || candidate.MaxAssets.Cmp(limit) > 0 { + capacityLimits[route.CapacityID] = liquidlane.CloneBig(candidate.MaxAssets) + } + if capacityUsed[route.CapacityID] == nil { + capacityUsed[route.CapacityID] = new(big.Int) + } + capacityUsed[route.CapacityID].Add(capacityUsed[route.CapacityID], route.ReservedAmountOut) + gasLegs = append(gasLegs, strategies.GasLeg{ + Route: candidate.Route, AmountOut: available, Private: candidate.DiscountID != nil, + }) + } + if totalInput.Cmp(input.AmountIn) != 0 { + return errors.Errorf("webhook fill input sum %s does not match order %s", totalInput, input.AmountIn) + } + if input.RequireSingleRoute && len(plan.Routes) != 1 { + return errors.New("webhook fill aggregates a permissioned token") + } + if input.OutputAmount == nil || input.OutputAmount.Sign() <= 0 { + return errors.New("webhook fill output amount is invalid") + } + gasCost, err := strategies.FillGasCost( + input.MaxFeePerGas, input.TokenOut, input.GasPrices, input.GasSnapshot, gasLegs, + ) + if err != nil { + return errors.Errorf("webhook fill gas cost: %w", err) + } + requiredOutput := new(big.Int).Add(input.OutputAmount, gasCost) + if totalMinimumOutput.Cmp(requiredOutput) < 0 { + return errors.New("webhook fill minimum output does not cover the order") + } + for capacityID, used := range capacityUsed { + if reserved := input.Reservations[capacityID]; reserved != nil && reserved.Sign() > 0 { + used.Add(used, reserved) + } + if used.Cmp(capacityLimits[capacityID]) > 0 { + return errors.Errorf("webhook fill exceeds shared capacity %s", capacityID) + } + } + return nil +} + +func scaledOutput(candidate liquidlane.FillQuote, amountIn *big.Int) *big.Int { + if candidate.AmountIn == nil || candidate.AmountIn.Sign() <= 0 || candidate.MaxAmountOut == nil { + return new(big.Int) + } + return new(big.Int).Div(new(big.Int).Mul(candidate.MaxAmountOut, amountIn), candidate.AmountIn) +} + +var _ types.Strategy = (*Strategy)(nil) diff --git a/internal/solvers/lifi/strategies/webhook/strategy_test.go b/internal/solvers/lifi/strategies/webhook/strategy_test.go new file mode 100644 index 00000000..d99aa982 --- /dev/null +++ b/internal/solvers/lifi/strategies/webhook/strategy_test.go @@ -0,0 +1,227 @@ +package webhookstrategy + +import ( + "encoding/json" + "math/big" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + liquidlanegas "github.com/symbioticfi/vault-solver/internal/liquidlane/gas" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" + "github.com/symbioticfi/vault-solver/internal/webhook" +) + +func TestWebhookStrategyDelegatesQuotesAndFill(t *testing.T) { + now := time.Unix(1_800_000_000, 0) + solver := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenIn := common.HexToAddress("0x2222222222222222222222222222222222222222") + tokenOut := common.HexToAddress("0x3333333333333333333333333333333333333333") + adapter := common.HexToAddress("0x4444444444444444444444444444444444444444") + route := liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", Adapter: adapter, + TokenIn: tokenIn, TokenOut: tokenOut, TokenInDecimals: 6, TokenOutDecimals: 6, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case decideQuotesRoute: + _ = json.NewEncoder(w).Encode(types.QuoteOutput{Quotes: []types.Quote{{ + FromAsset: tokenIn, ToAsset: tokenOut, FromDecimals: 6, ToDecimals: 6, + Ranges: []types.QuoteRange{{MinAmount: big.NewInt(1), MaxAmount: big.NewInt(100), Quote: "1"}}, + Expiry: now.Add(30 * time.Second).Unix(), + }}}) + case decideFillRoute: + _ = json.NewEncoder(w).Encode(types.FillPlan{Routes: []types.FillRoute{{ + RouteID: "route-1", AmountIn: big.NewInt(100), ExpectedAmountOut: big.NewInt(100), + MinAmountOut: big.NewInt(90), ReservedAmountOut: big.NewInt(100), + }}}) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + client, err := webhook.NewClient(webhook.Config{URL: server.URL, Timeout: time.Second}) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + strategy := New(client) + inventory := liquidlane.Inventory{Route: route, MaxAssets: big.NewInt(100), MaxRate: big.NewInt(1)} + quotes, err := strategy.DecideQuotes(t.Context(), types.QuoteInput{ + Solver: solver, Inventory: []liquidlane.Inventory{inventory}, ServerTime: now, QuoteExpiresAt: now.Add(time.Minute), + }) + if err != nil { + t.Fatalf("DecideQuotes: %v", err) + } + if len(quotes.Quotes) != 1 || quotes.Quotes[0].ExclusiveFor != solver { + t.Fatalf("quotes = %+v", quotes.Quotes) + } + plan, err := strategy.DecideFill(t.Context(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: big.NewInt(100), OutputAmount: big.NewInt(90), + Quotes: []liquidlane.FillQuote{{ + Inventory: inventory, AmountIn: big.NewInt(100), MaxAmountOut: big.NewInt(100), + }}, + }) + if err != nil { + t.Fatalf("DecideFill: %v", err) + } + if plan == nil || len(plan.Routes) != 1 || plan.Routes[0].Adapter != adapter || + plan.Routes[0].CapacityID != route.CapacityID { + t.Fatalf("plan = %+v", plan) + } +} + +func TestWebhookStrategyRejectsSharedCapacityOverspend(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(types.FillPlan{Routes: []types.FillRoute{ + {RouteID: "route-1", AmountIn: big.NewInt(50), ExpectedAmountOut: big.NewInt(50), + MinAmountOut: big.NewInt(45), ReservedAmountOut: big.NewInt(60)}, + {RouteID: "route-2", AmountIn: big.NewInt(50), ExpectedAmountOut: big.NewInt(50), + MinAmountOut: big.NewInt(45), ReservedAmountOut: big.NewInt(60)}, + }}) + })) + defer server.Close() + client, err := webhook.NewClient(webhook.Config{URL: server.URL, Timeout: time.Second}) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + quotes := make([]liquidlane.FillQuote, 0, 2) + for _, routeID := range []liquidlane.RouteID{"route-1", "route-2"} { + quotes = append(quotes, liquidlane.FillQuote{ + Inventory: liquidlane.Inventory{Route: liquidlane.Route{ + ID: routeID, CapacityID: "shared", TokenIn: tokenIn, TokenOut: tokenOut, + }, MaxAssets: big.NewInt(100)}, + AmountIn: big.NewInt(100), MaxAmountOut: big.NewInt(100), + }) + } + _, err = New(client).DecideFill(t.Context(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: big.NewInt(100), OutputAmount: big.NewInt(90), Quotes: quotes, + }) + if err == nil { + t.Fatal("expected shared capacity error") + } +} + +func TestWebhookStrategyRejectsPendingCapacityOverspend(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(types.FillPlan{Routes: []types.FillRoute{{ + RouteID: "route-1", AmountIn: big.NewInt(100), ExpectedAmountOut: big.NewInt(70), + MinAmountOut: big.NewInt(70), ReservedAmountOut: big.NewInt(70), + }}}) + })) + defer server.Close() + client, err := webhook.NewClient(webhook.Config{URL: server.URL, Timeout: time.Second}) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + _, err = New(client).DecideFill(t.Context(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: big.NewInt(100), OutputAmount: big.NewInt(70), + Reservations: map[liquidlane.CapacityID]*big.Int{"shared": big.NewInt(40)}, + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{Route: liquidlane.Route{ + ID: "route-1", CapacityID: "shared", TokenIn: tokenIn, TokenOut: tokenOut, + }, MaxAssets: big.NewInt(100)}, + AmountIn: big.NewInt(100), MaxAmountOut: big.NewInt(100), + }}, + }) + if err == nil { + t.Fatal("expected pending reservation capacity error") + } +} + +func TestWebhookStrategyRejectsUnderReservedFill(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(types.FillPlan{Routes: []types.FillRoute{{ + RouteID: "route-1", AmountIn: big.NewInt(100), ExpectedAmountOut: big.NewInt(100), + MinAmountOut: big.NewInt(90), ReservedAmountOut: big.NewInt(99), + }}}) + })) + defer server.Close() + client, err := webhook.NewClient(webhook.Config{URL: server.URL, Timeout: time.Second}) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + _, err = New(client).DecideFill(t.Context(), types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: big.NewInt(100), OutputAmount: big.NewInt(90), + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{Route: liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", TokenIn: tokenIn, TokenOut: tokenOut, + }, MaxAssets: big.NewInt(100)}, + AmountIn: big.NewInt(100), MaxAmountOut: big.NewInt(100), + }}, + }) + if err == nil { + t.Fatal("expected under-reservation error") + } +} + +func TestWebhookStrategyRejectsFillThatDoesNotCoverGas(t *testing.T) { + tokenIn := common.HexToAddress("0x1111111111111111111111111111111111111111") + tokenOut := common.HexToAddress("0x2222222222222222222222222222222222222222") + adapter := common.HexToAddress("0x3333333333333333333333333333333333333333") + vault := common.HexToAddress("0x4444444444444444444444444444444444444444") + route := liquidlane.Route{ + ID: "route-1", CapacityID: "capacity-1", Adapter: adapter, Vault: vault, + TokenIn: tokenIn, TokenOut: tokenOut, + } + plan := &types.FillPlan{Routes: []types.FillRoute{{ + RouteID: "route-1", AmountIn: big.NewInt(1_000_000), ExpectedAmountOut: big.NewInt(1_000_000), + MinAmountOut: big.NewInt(900_000), ReservedAmountOut: big.NewInt(1_000_000), + }}} + input := types.FillInput{ + TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: big.NewInt(1_000_000), OutputAmount: big.NewInt(500_000), + Quotes: []liquidlane.FillQuote{{ + Inventory: liquidlane.Inventory{Route: route, MaxAssets: big.NewInt(1_000_000)}, + AmountIn: big.NewInt(1_000_000), MaxAmountOut: big.NewInt(1_000_000), + }}, + MaxFeePerGas: big.NewInt(1), + GasPrices: liquidlanegas.NewPriceSnapshot(map[common.Address]*big.Int{ + tokenOut: big.NewInt(1_000_000_000_000_000_000), + }), + GasSnapshot: &liquidlanegas.Snapshot{ + Adapters: map[common.Address]*liquidlanegas.AdapterState{ + adapter: {Vault: vault, Acquire: map[common.Address]*big.Int{}}, + }, + Vaults: map[common.Address]*liquidlanegas.VaultState{ + vault: {FreeAssets: big.NewInt(1_000_000), Withdrawable: big.NewInt(1_000_000)}, + }, + }, + } + + if err := validateFill(input, plan); err == nil { + t.Fatal("expected gas-negative fill to be rejected") + } +} + +func TestWebhookStrategyRejectsUnknownFillCandidate(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(types.FillPlan{Routes: []types.FillRoute{{ + RouteID: "unknown", AmountIn: big.NewInt(1), ExpectedAmountOut: big.NewInt(1), + MinAmountOut: big.NewInt(1), ReservedAmountOut: big.NewInt(1), + }}}) + })) + defer server.Close() + client, err := webhook.NewClient(webhook.Config{URL: server.URL, Timeout: time.Second}) + if err != nil { + t.Fatalf("NewClient: %v", err) + } + _, err = New(client).DecideFill(t.Context(), types.FillInput{AmountIn: big.NewInt(1)}) + if err == nil { + t.Fatal("expected unknown candidate error") + } +} diff --git a/internal/solvers/lifi/strategy.go b/internal/solvers/lifi/strategy.go new file mode 100644 index 00000000..8c844f4f --- /dev/null +++ b/internal/solvers/lifi/strategy.go @@ -0,0 +1,19 @@ +package lifi + +import ( + "github.com/go-logr/logr" + + "github.com/symbioticfi/vault-solver/internal/chain" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies" + _ "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/default" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" + _ "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/webhook" +) + +func newStrategy(spec StrategyConfig, chainClient *chain.Client, log logr.Logger) (types.Strategy, error) { + name := spec.Name + if name == "" { + name = defaultStrategyName + } + return strategies.New(name, spec.Config, strategies.Deps{Chain: chainClient, Log: log}) +} diff --git a/internal/solvers/lifi/submission.go b/internal/solvers/lifi/submission.go new file mode 100644 index 00000000..84ab9fe3 --- /dev/null +++ b/internal/solvers/lifi/submission.go @@ -0,0 +1,106 @@ +package lifi + +import ( + "context" + "math/big" + + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/solvers/lifi/strategies/types" + "github.com/symbioticfi/vault-solver/internal/txmanager" +) + +func (s *Solver) submitFill( + ctx context.Context, + order *submittedOrder, + plan *types.FillPlan, + calldata *fillCalldata, + maxFeePerGas *big.Int, +) *pendingFill { + reservations, ok := fillPlanReservations(plan) + if !ok { + s.log.Error(errors.New("strategy returned invalid capacity reservations"), + "order fill: reject strategy plan", "orderId", order.OrderID, "quoteId", order.QuoteID) + return nil + } + status, err := s.reader.orderStatus(ctx, s.cfg.InputSettler, calldata.OrderID) + if err != nil { + s.log.Error(err, "order fill: read order status", "orderId", order.OrderID, + "onChainOrderId", calldata.OrderID.Hex(), "quoteId", order.QuoteID) + return nil + } + if status != lifiOrderStatusDeposited { + s.log.Info("order skipped: on-chain order is not deposited", "orderId", order.OrderID, + "onChainOrderId", calldata.OrderID.Hex(), "quoteId", order.QuoteID, "status", status) + return nil + } + reservationKey := calldata.OrderID.Hex() + confirmations := uint64(0) + result, accepted := s.txm.SendAsync(ctx, txmanager.Request{ + To: s.cfg.Executor, Data: calldata.Finalise, MaxFeePerGas: new(big.Int).Set(maxFeePerGas), + Confirmations: &confirmations, Label: "lifi-fill", + }) + if !accepted { + s.log.Info("order skipped: transaction submission canceled", "orderId", order.OrderID, + "onChainOrderId", calldata.OrderID.Hex(), "quoteId", order.QuoteID) + return nil + } + return &pendingFill{ + order: order, orderID: calldata.OrderID, reservationKey: reservationKey, + reservations: reservations, result: result, + } +} + +func (s *Solver) completeFill(ctx context.Context, pending *pendingFillState, completion fillCompletion) { + fill := completion.fill + pending.remove(fill.reservationKey) + s.releaseReservation(ctx, fill.reservationKey) + if completion.result.Err == nil { + s.log.Info("order filled", "orderId", fill.order.OrderID, "onChainOrderId", fill.orderID.Hex(), + "quoteId", fill.order.QuoteID, "tx", completion.result.Hash.Hex()) + return + } + s.log.Error(completion.result.Err, "order fill failed", + "orderId", fill.order.OrderID, + "onChainOrderId", fill.orderID.Hex(), + "quoteId", fill.order.QuoteID, + "tx", completion.result.Hash.Hex(), + ) +} + +func fillPlanReservations(plan *types.FillPlan) ([]quoteReservation, bool) { + if plan == nil || len(plan.Routes) == 0 { + return nil, false + } + reservations := make([]quoteReservation, 0, len(plan.Routes)) + for _, route := range plan.Routes { + if route.CapacityID == "" || route.ReservedAmountOut == nil || route.ReservedAmountOut.Sign() <= 0 { + return nil, false + } + reservations = append(reservations, quoteReservation{ + capacityID: route.CapacityID, amountOut: liquidlane.CloneBig(route.ReservedAmountOut), + }) + } + return reservations, true +} + +func (s *Solver) reserve(ctx context.Context, orderKey string, reservations []quoteReservation) { + if s.quoteEvents == nil || len(reservations) == 0 { + return + } + select { + case s.quoteEvents <- quoteEvent{orderKey: orderKey, reservations: reservations}: + case <-ctx.Done(): + } +} + +func (s *Solver) releaseReservation(ctx context.Context, orderKey string) { + if s.quoteEvents == nil { + return + } + select { + case s.quoteEvents <- quoteEvent{orderKey: orderKey, release: true}: + case <-ctx.Done(): + } +} diff --git a/internal/solvers/lifi/wsclient.go b/internal/solvers/lifi/wsclient.go new file mode 100644 index 00000000..083e99c5 --- /dev/null +++ b/internal/solvers/lifi/wsclient.go @@ -0,0 +1,135 @@ +package lifi + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "strings" + "time" + + "github.com/go-errors/errors" + "github.com/go-logr/logr" + "github.com/gorilla/websocket" +) + +const ( + orderSubmitEvent = "user:vm-order-submit" + initialWSBackoff = time.Second + maxWSBackoff = 30 * time.Second +) + +type orderMessage struct { + Event string `json:"event"` + Data json.RawMessage `json:"data"` +} + +type orderFeed struct { + url string + apiKey string + log logr.Logger +} + +func newOrderFeed(url, apiKey string, log logr.Logger) *orderFeed { + return &orderFeed{url: url, apiKey: apiKey, log: log} +} + +func (f *orderFeed) run(ctx context.Context, handle func(context.Context, orderMessage)) error { + backoff := initialWSBackoff + for { + connected, err := f.watchOnce(ctx, handle) + if ctx.Err() != nil { + return ctx.Err() + } + if connected { + backoff = initialWSBackoff + } + f.log.Error(err, "order feed disconnected; reconnecting", "backoff", backoff.String()) + timer := time.NewTimer(backoff) + select { + case <-ctx.Done(): + timer.Stop() + return ctx.Err() + case <-timer.C: + } + backoff *= 2 + if backoff > maxWSBackoff { + backoff = maxWSBackoff + } + } +} + +func (f *orderFeed) watchOnce( + ctx context.Context, + handle func(context.Context, orderMessage), +) (bool, error) { + headers := http.Header{} + if f.apiKey != "" { + headers.Set("x-api-key", f.apiKey) + } + conn, resp, err := websocket.DefaultDialer.DialContext(ctx, f.url, headers) + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + if err != nil { + if resp != nil { + return false, errors.Errorf("dial websocket: %w (status %s)", err, resp.Status) + } + return false, errors.Errorf("dial websocket: %w", err) + } + done := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + _ = conn.Close() + case <-done: + } + }() + defer close(done) + defer conn.Close() + + f.log.Info("order feed connected", "url", f.url) + for { + messageType, msg, err := conn.ReadMessage() + if err != nil { + return true, errors.Errorf("read websocket: %w", err) + } + if messageType != websocket.TextMessage { + continue + } + if pong, ok := pongFor(msg); ok { + if err := conn.WriteMessage(websocket.TextMessage, pong); err != nil { + return true, errors.Errorf("write websocket pong: %w", err) + } + continue + } + + var envelope orderMessage + if err := json.Unmarshal(msg, &envelope); err != nil { + f.log.V(1).Info("order feed: non-json message ignored") + continue + } + if envelope.Event != orderSubmitEvent { + f.log.V(1).Info("order feed event ignored", "event", envelope.Event) + continue + } + handle(ctx, envelope) + } +} + +func pongFor(msg []byte) ([]byte, bool) { + trimmed := bytes.TrimSpace(msg) + if strings.EqualFold(string(trimmed), "ping") { + return []byte("pong"), true + } + var envelope struct { + Event string `json:"event"` + } + if err := json.Unmarshal(trimmed, &envelope); err != nil { + return nil, false + } + if strings.EqualFold(envelope.Event, "ping") { + return []byte(`{"event":"pong"}`), true + } + return nil, false +} diff --git a/internal/solvers/lifi/wsclient_test.go b/internal/solvers/lifi/wsclient_test.go new file mode 100644 index 00000000..4ef6a1b2 --- /dev/null +++ b/internal/solvers/lifi/wsclient_test.go @@ -0,0 +1,58 @@ +package lifi + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-logr/logr" + "github.com/gorilla/websocket" +) + +func TestPongFor(t *testing.T) { + tests := []struct { + name string + in string + want string + ok bool + }{ + {name: "plain", in: "ping", want: "pong", ok: true}, + {name: "json", in: `{"event":"ping"}`, want: `{"event":"pong"}`, ok: true}, + {name: "other", in: `{"event":"user:vm-order-submit"}`, ok: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := pongFor([]byte(tt.in)) + if ok != tt.ok { + t.Fatalf("ok = %v", ok) + } + if string(got) != tt.want { + t.Fatalf("pong = %q", got) + } + }) + } +} + +func TestWatchOnceReportsEstablishedConnection(t *testing.T) { + upgrader := websocket.Upgrader{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + t.Errorf("upgrade: %v", err) + return + } + _ = conn.Close() + })) + defer server.Close() + + feed := newOrderFeed("ws"+strings.TrimPrefix(server.URL, "http"), "", logr.Discard()) + connected, err := feed.watchOnce(context.Background(), func(context.Context, orderMessage) {}) + if !connected { + t.Fatal("connection was not reported as established") + } + if err == nil { + t.Fatal("expected read error after server closed the connection") + } +} diff --git a/internal/solvers/redstoneoev/chainreader.go b/internal/solvers/redstoneoev/chainreader.go index 4bb09658..a43c67a5 100644 --- a/internal/solvers/redstoneoev/chainreader.go +++ b/internal/solvers/redstoneoev/chainreader.go @@ -3,44 +3,27 @@ package redstoneoev import ( "context" "math/big" - "slices" - "sync" "github.com/ethereum/go-ethereum/common" "github.com/go-errors/errors" "github.com/go-logr/logr" - "github.com/symbioticfi/vault-solver/api/bindings/erc4626" - "github.com/symbioticfi/vault-solver/api/bindings/liquidlane/adapter" "github.com/symbioticfi/vault-solver/api/bindings/oev/executor" - "github.com/symbioticfi/vault-solver/api/bindings/vaultv2" "github.com/symbioticfi/vault-solver/internal/chain" + "github.com/symbioticfi/vault-solver/internal/liquidlane" "github.com/symbioticfi/vault-solver/internal/solvers/redstoneoev/strategies/types" ) -var ( - executorB = executor.NewRedStoneExecutor() - liquidLaneRead = adapter.NewLiquidLaneAdapter() - erc4626Read = erc4626.NewIERC4626() - vaultV2Read = vaultv2.NewIVaultV2() -) +var executorB = executor.NewRedStoneExecutor() -// reader performs solver-owned on-chain reads. Strategy-owned reads live in the strategy package. +// reader owns RedStone Executor reads and maps shared LiquidLane facts into OEV strategy input. type reader struct { - chain *chain.Client - log logr.Logger - decimals *chain.Decimals - mu sync.Mutex - redeemColl map[common.Address][]common.Address + chain *chain.Client + ll *liquidlane.Reader } func newReader(c *chain.Client, log logr.Logger) *reader { - return &reader{ - chain: c, - log: log, - decimals: chain.NewDecimals(c), - redeemColl: map[common.Address][]common.Address{}, - } + return &reader{chain: c, ll: liquidlane.NewReader(c, log)} } // ExecutorState is the signer's accounting on the RedStone Executor. @@ -63,309 +46,38 @@ func (r *reader) ReadExecutorState(ctx context.Context, executorAddr, signer com if !allSuccess(res, 3) { return ExecutorState{}, errors.New("executor state read reverted") } - nonce, e1 := executorB.UnpackNonces(res[0].ReturnData) - deposit, e2 := executorB.UnpackDeposits(res[1].ReturnData) - locked, e3 := executorB.UnpackLocked(res[2].ReturnData) - if e1 != nil || e2 != nil || e3 != nil { + nonce, nonceErr := executorB.UnpackNonces(res[0].ReturnData) + deposit, depositErr := executorB.UnpackDeposits(res[1].ReturnData) + locked, lockedErr := executorB.UnpackLocked(res[2].ReturnData) + if nonceErr != nil || depositErr != nil || lockedErr != nil { return ExecutorState{}, errors.New("executor state decode failed") } return ExecutorState{Nonce: nonce, Deposit: deposit, Locked: locked}, nil } -// ReadAdapterSnapshot reads the configured LiquidLane adapter context passed to every strategy. -func (r *reader) ReadAdapterSnapshot(ctx context.Context, adapterAddr, callback common.Address) (types.AdapterSnapshot, error) { - head, err := r.readAdapterHead(ctx, adapterAddr) - if err != nil { - return types.AdapterSnapshot{}, err - } - state, err := r.readAdapterVaultState(ctx, head.Vault) - if err != nil { - return types.AdapterSnapshot{}, err - } - if state.Loan == (common.Address{}) { - return types.AdapterSnapshot{}, errors.New("adapter loan token unresolved") - } - redeemable, err := r.readRedeemable(ctx, adapterAddr, head.Owner, head.MarketMaker) - if err != nil { - return types.AdapterSnapshot{}, err - } - if len(redeemable) == 0 { - return types.AdapterSnapshot{}, errors.New("adapter redeemable collateral unresolved") - } - loanDecimals, err := r.fillRedeemableDecimals(ctx, state.Loan, redeemable) +// ReadAdapterSnapshot maps the shared LiquidLane snapshot to the stable OEV strategy contract. +func (r *reader) ReadAdapterSnapshot( + ctx context.Context, + adapterAddress common.Address, + callback common.Address, +) (types.AdapterSnapshot, error) { + snapshot, err := r.ll.ReadAdapterSnapshot(ctx, adapterAddress, callback) if err != nil { return types.AdapterSnapshot{}, err } - filler, err := r.readCallbackAuthorization(ctx, adapterAddr, head, callback) - if err != nil { - return types.AdapterSnapshot{}, err + redeemable := make([]types.RedeemableSnapshot, 0, len(snapshot.Routes)) + for _, route := range snapshot.Routes { + redeemable = append(redeemable, types.RedeemableSnapshot{ + Asset: route.TokenIn, Decimals: route.TokenInDecimals, + MaxRate: liquidlane.CloneBig(route.MaxRate), MaxAssets: liquidlane.CloneBig(route.MaxAssets), + AcquireBalance: liquidlane.CloneBig(route.AcquireBalance), + }) } return types.AdapterSnapshot{ - Address: adapterAddr, - Vault: head.Vault, - Loan: state.Loan, - LoanDecimals: loanDecimals, - Paused: head.Paused, - FreeAssets: state.FreeAssets, - Withdrawable: state.Withdrawable, - Redeemable: redeemable, - Filler: filler, - }, nil -} - -type adapterHead struct { - Vault common.Address - Owner common.Address - MarketMaker common.Address - Paused bool -} - -type adapterVaultState struct { - Loan common.Address - FreeAssets *big.Int - Withdrawable *big.Int -} - -func (r *reader) readAdapterHead(ctx context.Context, adapterAddr common.Address) (adapterHead, error) { - res, err := r.chain.Multicall(ctx, []chain.Call{ - {Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackVault()}, - {Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackOwner()}, - {Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackMarketMaker()}, - {Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackPaused()}, - }) - if err != nil { - return adapterHead{}, err - } - if !allSuccess(res, 4) { - return adapterHead{}, errors.New("adapter head read reverted") - } - vault, e1 := liquidLaneRead.UnpackVault(res[0].ReturnData) - owner, e2 := liquidLaneRead.UnpackOwner(res[1].ReturnData) - marketMaker, e3 := liquidLaneRead.UnpackMarketMaker(res[2].ReturnData) - paused, e4 := liquidLaneRead.UnpackPaused(res[3].ReturnData) - if e1 != nil || e2 != nil || e3 != nil || e4 != nil || vault == (common.Address{}) { - return adapterHead{}, errors.New("adapter head decode failed") - } - return adapterHead{ - Vault: vault, - Owner: owner, - MarketMaker: marketMaker, - Paused: paused, - }, nil -} - -func (r *reader) readAdapterVaultState(ctx context.Context, vault common.Address) (adapterVaultState, error) { - res, err := r.chain.Multicall(ctx, []chain.Call{ - {Target: vault, AllowFailure: true, Data: erc4626Read.PackAsset()}, - {Target: vault, AllowFailure: true, Data: vaultV2Read.PackFreeAssets()}, - {Target: vault, AllowFailure: true, Data: vaultV2Read.PackWithdrawable()}, - }) - if err != nil { - return adapterVaultState{}, err - } - if !allSuccess(res, 3) { - return adapterVaultState{}, errors.New("adapter vault state read reverted") - } - loan, e1 := erc4626Read.UnpackAsset(res[0].ReturnData) - free, e2 := vaultV2Read.UnpackFreeAssets(res[1].ReturnData) - withdrawable, e3 := vaultV2Read.UnpackWithdrawable(res[2].ReturnData) - if e1 != nil || e2 != nil || e3 != nil || loan == (common.Address{}) || free == nil || withdrawable == nil { - return adapterVaultState{}, errors.New("adapter vault state decode failed") - } - return adapterVaultState{ - Loan: loan, - FreeAssets: free, - Withdrawable: withdrawable, + Address: snapshot.Adapter.Adapter, Vault: snapshot.Vault, + Loan: snapshot.TokenOut, LoanDecimals: snapshot.TokenOutDecimals, + Paused: snapshot.Paused, + FreeAssets: liquidlane.CloneBig(snapshot.FreeAssets), Withdrawable: liquidlane.CloneBig(snapshot.Withdrawable), + Redeemable: redeemable, Filler: snapshot.Authorized, }, nil } - -func (r *reader) readRedeemable(ctx context.Context, adapterAddr, owner, marketMaker common.Address) ([]types.RedeemableSnapshot, error) { - collaterals, err := r.readRedeemableCollaterals(ctx, adapterAddr) - if err != nil { - return nil, err - } - collaterals = dedupeNonZeroAddresses(collaterals) - out := make([]types.RedeemableSnapshot, 0, len(collaterals)) - for _, coll := range collaterals { - snap, err := r.readRedeemableSnapshot(ctx, adapterAddr, coll, owner, marketMaker) - if err != nil { - return nil, err - } - out = append(out, snap) - } - return out, nil -} - -func (r *reader) readRedeemableSnapshot(ctx context.Context, adapterAddr, coll, owner, marketMaker common.Address) (types.RedeemableSnapshot, error) { - calls := []chain.Call{ - {Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackGetMaxRate(coll)}, - {Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackGetMaxAssets(coll)}, - {Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackAcquireBalance(coll, owner)}, - } - readMarketMakerAcquire := marketMaker != (common.Address{}) && marketMaker != owner - if readMarketMakerAcquire { - calls = append(calls, chain.Call{Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackAcquireBalance(coll, marketMaker)}) - } - res, err := r.chain.Multicall(ctx, calls) - if err != nil { - return types.RedeemableSnapshot{}, err - } - if !allSuccess(res, len(calls)) { - return types.RedeemableSnapshot{}, errors.Errorf("redeemable %s read reverted", coll.Hex()) - } - maxRate, e1 := liquidLaneRead.UnpackGetMaxRate(res[0].ReturnData) - maxAssets, e2 := liquidLaneRead.UnpackGetMaxAssets(res[1].ReturnData) - acquire, e3 := liquidLaneRead.UnpackAcquireBalance(res[2].ReturnData) - if e1 != nil || e2 != nil || e3 != nil || maxRate == nil || maxAssets == nil || acquire == nil { - return types.RedeemableSnapshot{}, errors.Errorf("redeemable %s decode failed", coll.Hex()) - } - if readMarketMakerAcquire { - mmAcquire, merr := liquidLaneRead.UnpackAcquireBalance(res[3].ReturnData) - if merr != nil || mmAcquire == nil { - return types.RedeemableSnapshot{}, errors.Errorf("redeemable %s market-maker acquire decode failed", coll.Hex()) - } - acquire = new(big.Int).Add(acquire, mmAcquire) - } - return types.RedeemableSnapshot{ - Asset: coll, - MaxRate: maxRate, - MaxAssets: maxAssets, - AcquireBalance: acquire, - }, nil -} - -func (r *reader) fillRedeemableDecimals(ctx context.Context, loan common.Address, redeemable []types.RedeemableSnapshot) (int, error) { - tokens := make([]common.Address, 0, 1+len(redeemable)) - tokens = append(tokens, loan) - for _, item := range redeemable { - tokens = append(tokens, item.Asset) - } - decimals, err := r.decimals.GetMany(ctx, tokens) - if err != nil { - return 0, err - } - loanDecimals, ok := decimals[loan] - if !ok { - return 0, errors.Errorf("erc20.decimals() missing for loan token %s", loan.Hex()) - } - for i := range redeemable { - dec, hasDecimals := decimals[redeemable[i].Asset] - if !hasDecimals { - return 0, errors.Errorf("erc20.decimals() missing for redeemable token %s", redeemable[i].Asset.Hex()) - } - redeemable[i].Decimals = dec - } - return loanDecimals, nil -} - -func (r *reader) readRedeemableCollaterals(ctx context.Context, adapterAddr common.Address) ([]common.Address, error) { - r.mu.Lock() - c, ok := r.redeemColl[adapterAddr] - r.mu.Unlock() - if ok { - return slices.Clone(c), nil - } - lenRes, err := r.chain.Multicall(ctx, []chain.Call{ - {Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackGetTokensToRedeemLength()}, - }) - if err != nil { - return nil, err - } - count, ok := decodeRedeemCount(lenRes) - if !ok { - return nil, nil - } - if count == 0 { - r.mu.Lock() - r.redeemColl[adapterAddr] = nil - r.mu.Unlock() - return nil, nil - } - calls := make([]chain.Call, count) - for i := range count { - calls[i] = chain.Call{Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackTokensToRedeem(big.NewInt(int64(i)))} - } - res, err := r.chain.Multicall(ctx, calls) - if err != nil { - return nil, err - } - toks, ok := decodeRedeemTokens(res, count) - if !ok { - return nil, nil - } - r.mu.Lock() - r.redeemColl[adapterAddr] = slices.Clone(toks) - r.mu.Unlock() - return toks, nil -} - -func (r *reader) readCallbackAuthorization(ctx context.Context, adapterAddr common.Address, head adapterHead, callback common.Address) (bool, error) { - if callback == (common.Address{}) { - return false, nil - } - if callback == head.Owner || callback == head.MarketMaker { - return true, nil - } - if head.MarketMaker == (common.Address{}) { - return false, nil - } - res, err := r.chain.Multicall(ctx, []chain.Call{ - {Target: adapterAddr, AllowFailure: true, Data: liquidLaneRead.PackIsFiller(head.MarketMaker, callback)}, - }) - if err != nil { - return false, err - } - if len(res) != 1 || !res[0].Success { - return false, nil - } - filler, err := liquidLaneRead.UnpackIsFiller(res[0].ReturnData) - if err != nil { - return false, errors.Errorf("adapter filler status decode failed: %w", err) - } - return filler, nil -} - -func decodeRedeemCount(res []chain.CallResult) (int, bool) { - if len(res) != 1 || !res[0].Success { - return 0, false - } - n, err := liquidLaneRead.UnpackGetTokensToRedeemLength(res[0].ReturnData) - if err != nil || n == nil || n.Sign() < 0 || !n.IsInt64() { - return 0, false - } - return int(n.Int64()), true -} - -func decodeRedeemTokens(res []chain.CallResult, count int) ([]common.Address, bool) { - if len(res) != count { - return nil, false - } - out := make([]common.Address, 0, count) - for i := range res { - if !res[i].Success { - return nil, false - } - tok, err := liquidLaneRead.UnpackTokensToRedeem(res[i].ReturnData) - if err != nil || tok == (common.Address{}) { - return nil, false - } - out = append(out, tok) - } - return out, true -} - -func dedupeNonZeroAddresses(in []common.Address) []common.Address { - seen := make(map[common.Address]struct{}, len(in)) - out := make([]common.Address, 0, len(in)) - for _, addr := range in { - if addr == (common.Address{}) { - continue - } - if _, ok := seen[addr]; ok { - continue - } - seen[addr] = struct{}{} - out = append(out, addr) - } - return out -} diff --git a/internal/solvers/redstoneoev/strategies/default/chainreader.go b/internal/solvers/redstoneoev/strategies/default/chainreader.go index 98ed7a0a..e16c51ee 100644 --- a/internal/solvers/redstoneoev/strategies/default/chainreader.go +++ b/internal/solvers/redstoneoev/strategies/default/chainreader.go @@ -10,7 +10,7 @@ import ( "github.com/go-errors/errors" "github.com/go-logr/logr" - "github.com/symbioticfi/vault-solver/api/bindings/oev/aggregator" + "github.com/symbioticfi/vault-solver/api/bindings/chainlink/aggregator" "github.com/symbioticfi/vault-solver/api/bindings/oev/callback" morphobinding "github.com/symbioticfi/vault-solver/api/bindings/oev/morpho" "github.com/symbioticfi/vault-solver/api/bindings/oev/oracle" diff --git a/internal/solvers/redstoneoev/strategies/default/chainreader_test.go b/internal/solvers/redstoneoev/strategies/default/chainreader_test.go index dfcd235b..73e358d9 100644 --- a/internal/solvers/redstoneoev/strategies/default/chainreader_test.go +++ b/internal/solvers/redstoneoev/strategies/default/chainreader_test.go @@ -8,7 +8,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/symbioticfi/vault-solver/api/bindings/oev/aggregator" + "github.com/symbioticfi/vault-solver/api/bindings/chainlink/aggregator" ) func mustParseABI(j string) abi.ABI { diff --git a/internal/solvers/rfq/apitypes.go b/internal/solvers/rfq/apitypes.go index 6419301d..dfeca818 100644 --- a/internal/solvers/rfq/apitypes.go +++ b/internal/solvers/rfq/apitypes.go @@ -3,10 +3,12 @@ package rfq import ( "math/big" "strconv" + "time" "github.com/ethereum/go-ethereum/common" "github.com/go-errors/errors" + "github.com/symbioticfi/vault-solver/internal/liquidlane" "github.com/symbioticfi/vault-solver/internal/parse" ) @@ -96,7 +98,7 @@ func (q *quoteRequest) toStrategy(chainID int64) (*parsedQuote, error) { } inv := make([]solverInventory, 0, len(q.Adapters)) for i := range q.Adapters { - entry, perr := q.Adapters[i].parse(i) + entry, perr := q.Adapters[i].parse(i, q.TokenInChainID, tokenIn) if perr != nil { return nil, perr } @@ -119,7 +121,7 @@ func (q *quoteRequest) toStrategy(chainID int64) (*parsedQuote, error) { }, nil } -func (v *quoteAdapter) parse(index int) (solverInventory, error) { +func (v *quoteAdapter) parse(index int, chainID int64, tokenIn common.Address) (solverInventory, error) { adapter, err := parse.Address(v.Adapter, idxField(index, "adapter")) if err != nil { return solverInventory{}, err @@ -144,14 +146,11 @@ func (v *quoteAdapter) parse(index int) (solverInventory, error) { h := common.HexToHash(*v.DiscountID) discountID = &h } - return solverInventory{ - Adapter: adapter, - Asset: asset, - AssetDecimals: v.AssetDecimals, - MaxAssets: maxAssets, - MaxRate: maxRate, - DiscountID: discountID, - }, nil + route := liquidlane.NewRoute(chainID, adapter, common.Address{}, tokenIn, asset, 0, v.AssetDecimals) + if discountID != nil { + return liquidlane.DiscountInventory(route, maxAssets, maxRate, *discountID, time.Time{}), nil + } + return liquidlane.DirectInventory(route, maxAssets, maxRate), nil } // parseUint256 parses a base-10 non-negative integer string into a big.Int. diff --git a/internal/solvers/rfq/backend.go b/internal/solvers/rfq/backend.go index 94a837fb..adf1a595 100644 --- a/internal/solvers/rfq/backend.go +++ b/internal/solvers/rfq/backend.go @@ -9,6 +9,7 @@ import ( "github.com/go-errors/errors" "github.com/symbioticfi/vault-solver/api/rfqbackend" + "github.com/symbioticfi/vault-solver/internal/liquidlane/discounts" ) // backendOrder is one order row from the RFQ backend (GET /orders), projected from the generated @@ -43,11 +44,11 @@ type backendOut struct { Recipient string } -// backendClient is a thin adapter over the generated rfqbackend client for the filler-facing order -// and discount endpoints. It owns no transport state of its own beyond the generated APIClient, whose -// HTTPClient carries the request timeout. Used from the single execution goroutine. +// backendClient is a thin adapter over the generated rfqbackend client for filler-facing orders plus +// the shared private-discounts client. Used from the single execution goroutine. type backendClient struct { - api *rfqbackend.APIClient + api *rfqbackend.APIClient + discounts *discounts.Client } // newBackendClient builds a backend client rooted at baseURL. The generated client carries the @@ -58,30 +59,9 @@ func newBackendClient(baseURL string) *backendClient { cfg := rfqbackend.NewConfiguration() cfg.Servers = rfqbackend.ServerConfigurations{{URL: strings.TrimRight(baseURL, "/")}} cfg.HTTPClient = &http.Client{ - Timeout: 10 * time.Second, - Transport: internalDiscountTransport{base: http.DefaultTransport}, + Timeout: 10 * time.Second, } - return &backendClient{api: rfqbackend.NewAPIClient(cfg)} -} - -const ( - publicAPIPrefix = "/api/v1" // the spec's prefix, baked into the generated client - internalAPIPrefix = "/api-internal/v1" // where the backend serves the internal-only discounts API -) - -// internalDiscountTransport routes discount requests to the backend's internal API prefix. The discounts -// API is internal-only, but the generated client emits the public /api/v1/discount(s) paths from the -// spec; rather than regenerate the client for a deployment routing detail, we rewrite just those paths to -// /api-internal/v1/... at the transport layer. Orders and everything else pass through unchanged. -type internalDiscountTransport struct{ base http.RoundTripper } - -func (t internalDiscountTransport) RoundTrip(req *http.Request) (*http.Response, error) { - if strings.HasPrefix(req.URL.Path, publicAPIPrefix+"/discount") { - req = req.Clone(req.Context()) // RoundTrippers must not mutate the caller's request - req.URL.Path = internalAPIPrefix + strings.TrimPrefix(req.URL.Path, publicAPIPrefix) - req.URL.RawPath = "" // drop any cached encoding so the URL re-encodes from Path - } - return t.base.RoundTrip(req) + return &backendClient{api: rfqbackend.NewAPIClient(cfg), discounts: discounts.NewClient(baseURL)} } // closeResp drains and closes the HTTP response body. The generated client already reads the body @@ -200,50 +180,10 @@ func first(orders []backendOrder) *backendOrder { return &orders[0] } -/* ───────── discounts (P3) ───────── */ - -// discountTerms is the signed discount the adapter's discount-swap verifies. Amounts/nonce are -// numeric/hex strings on the wire. -type discountTerms struct { - Adapter string - TokenToRedeem string - Discount string - Signer string - Protocol string - Nonce string - Deadline int64 -} - -// resolveDiscountResponse is the fresh, signed discount the backend issues at fill time (the single -// shape of the backend's ResolveDiscountResponse anyOf union; see resolveDiscount). -type resolveDiscountResponse struct { - RequestID string - DiscountID string - Discount discountTerms - SignerSignature string - ProtocolDeadline int64 - ProtocolSignature string -} - -// discountListItem is one offered discount (GET /discounts), used during strategy recovery. -type discountListItem struct { - DiscountID string - Adapter string - TokenToRedeem string - Collateral string - CollateralDecimals int - Discount string - Signer string - Deadline int64 - MaxRate string - MaxAssets string -} - -type discountsResponse struct { - RequestID string - Protocol string - Discounts []discountListItem -} +type discountTerms = discounts.Terms +type resolveDiscountResponse = discounts.Resolved +type discountListItem = discounts.ListItem +type discountsResponse = discounts.List // resolveDiscount fetches the fresh signed discount for a discountId (POST /discounts). // @@ -253,92 +193,10 @@ type discountsResponse struct { // accepted (it carries the same signed fields); anything else (neither shape, or a batch with ≠1 // entries) is rejected so we never fill on an ambiguous resolution. func (c *backendClient) resolveDiscount(ctx context.Context, discountID string) (*resolveDiscountResponse, error) { - body := rfqbackend.NewApiV1DiscountsPostRequest() - body.SetDiscountId(discountID) - resp, httpResp, err := c.api.RFQAPI.ApiV1DiscountsPost(ctx).ApiV1DiscountsPostRequest(*body).Execute() - closeResp(httpResp) - if err != nil { - return nil, errors.Errorf("backend: resolve discount: %w", err) - } - if resp == nil { - return nil, errors.New("backend: resolve discount: empty response") - } - if single := resp.ResolveDiscountResponseAnyOf; single != nil { - return resolvedFromSingle(single), nil - } - if batch := resp.ResolveDiscountResponseAnyOf1; batch != nil { - items := batch.GetDiscounts() - if len(items) != 1 { - return nil, errors.Errorf("backend: resolve discount: expected a single discount, got %d", len(items)) - } - return resolvedFromBatchItem(batch.GetRequestId(), &items[0]), nil - } - return nil, errors.New("backend: resolve discount: response matched neither discount shape") -} - -func resolvedFromSingle(s *rfqbackend.ResolveDiscountResponseAnyOf) *resolveDiscountResponse { - return &resolveDiscountResponse{ - RequestID: s.GetRequestId(), - DiscountID: s.GetDiscountId(), - Discount: termsFromModel(s.GetDiscount()), - SignerSignature: s.GetSignerSignature(), - ProtocolDeadline: int64(s.GetProtocolDeadline()), - ProtocolSignature: s.GetProtocolSignature(), - } -} - -func resolvedFromBatchItem(requestID string, it *rfqbackend.ResolveDiscountResponseAnyOf1DiscountsInner) *resolveDiscountResponse { - return &resolveDiscountResponse{ - RequestID: requestID, - DiscountID: it.GetDiscountId(), - Discount: termsFromModel(it.GetDiscount()), - SignerSignature: it.GetSignerSignature(), - ProtocolDeadline: int64(it.GetProtocolDeadline()), - ProtocolSignature: it.GetProtocolSignature(), - } -} - -func termsFromModel(d rfqbackend.PublishDiscountRequestDiscount) discountTerms { - return discountTerms{ - Adapter: d.GetAdapter(), - TokenToRedeem: d.GetTokenToRedeem(), - Discount: d.GetDiscount(), - Signer: d.GetSigner(), - Protocol: d.GetProtocol(), - Nonce: d.GetNonce(), - Deadline: int64(d.GetDeadline()), - } + return c.discounts.Resolve(ctx, discountID) } // listDiscounts lists currently-offered discounts (GET /discounts). func (c *backendClient) listDiscounts(ctx context.Context) (*discountsResponse, error) { - resp, httpResp, err := c.api.RFQAPI.ApiV1DiscountsGet(ctx).Execute() - closeResp(httpResp) - if err != nil { - return nil, errors.Errorf("backend: list discounts: %w", err) - } - out := &discountsResponse{} - if resp == nil { - return out, nil - } - out.RequestID = resp.GetRequestId() - out.Protocol = resp.GetProtocol() - gen := resp.GetDiscounts() - out.Discounts = make([]discountListItem, 0, len(gen)) - for i := range gen { - d := &gen[i] - out.Discounts = append(out.Discounts, discountListItem{ - DiscountID: d.GetDiscountId(), - Adapter: d.GetAdapter(), - TokenToRedeem: d.GetTokenToRedeem(), - Collateral: d.GetCollateral(), - CollateralDecimals: int(d.GetCollateralDecimals()), - Discount: d.GetDiscount(), - Signer: d.GetSigner(), - Deadline: int64(d.GetDeadline()), - MaxRate: d.GetMaxRate(), - MaxAssets: d.GetMaxAssets(), - }) - } - return out, nil + return c.discounts.ListDiscounts(ctx) } diff --git a/internal/solvers/rfq/backend_test.go b/internal/solvers/rfq/backend_test.go index 05df6dce..cfddf238 100644 --- a/internal/solvers/rfq/backend_test.go +++ b/internal/solvers/rfq/backend_test.go @@ -10,7 +10,7 @@ import ( ) // The generated rfqbackend client carries the spec's `/api/v1` prefix, so the backend client rooted at -// the httptest server URL hits `/api/v1/orders`; the discount transport rewrite (internalDiscountTransport) +// the httptest server URL hits `/api/v1/orders`; the shared private-discounts client transport rewrite // sends discount calls to `/api-internal/v1/discounts` instead (orders unchanged). func TestBackendClient_ListOpenOrders(t *testing.T) { diff --git a/internal/solvers/rfq/chainreader.go b/internal/solvers/rfq/chainreader.go index dab40009..4ed90b05 100644 --- a/internal/solvers/rfq/chainreader.go +++ b/internal/solvers/rfq/chainreader.go @@ -4,54 +4,27 @@ import ( "context" "github.com/ethereum/go-ethereum/common" - "github.com/go-errors/errors" "github.com/go-logr/logr" - "github.com/symbioticfi/vault-solver/api/bindings/erc4626" - "github.com/symbioticfi/vault-solver/api/bindings/liquidlane/adapter" "github.com/symbioticfi/vault-solver/internal/chain" + "github.com/symbioticfi/vault-solver/internal/liquidlane" ) -// Contract bindings (abigen --v2): typed Pack/Unpack helpers for the Multicall3 sub-calls below, so an -// ABI change fails at compile time (see CLAUDE.md "Code generation"). -var ( - llAdapter = adapter.NewLiquidLaneAdapter() - // erc4626b serves the vault's asset(): a method's selector/return shape is fixed by the ABI - // regardless of target. (Token decimals go through the shared chain.Decimals helper.) - erc4626b = erc4626.NewIERC4626() -) - -// readsPerAdapter is the number of Multicall3 sub-calls readVaultInventories issues per adapter -// (paused, getMaxAssets, getMaxRate). vault() and the vault's asset() are resolved once at startup -// (see resolveVaults), not re-read here. -const readsPerAdapter = 3 - -// reader performs the on-chain reads, batching via Multicall3. Token decimals are resolved + cached by -// the shared chain.Decimals helper (its own mutex), so concurrent quote requests stay safe. +// reader is the RFQ adapter over the shared LiquidLane read surface. type reader struct { chain *chain.Client - log logr.Logger - dec *chain.Decimals + ll *liquidlane.Reader } func newReader(c *chain.Client, log logr.Logger) *reader { - return &reader{chain: c, log: log, dec: chain.NewDecimals(c)} + return &reader{chain: c, ll: liquidlane.NewReader(c, log)} } // recoveryVault is one configured LiquidLane adapter plus the Vault and Asset derived from it. Config // carries only Adapter; Vault (adapter.vault()) and Asset (vault.asset()) are resolved on-chain at // startup (see resolveVaults) and are fixed for the adapter's lifetime. The entries double as the // adapter whitelist source (see buildAdapterWhitelist) and the fill-plan recovery candidate universe. -type recoveryVault struct { - Adapter common.Address - Vault common.Address - Asset common.Address -} - -// tokenDecimals returns the ERC-20 decimals for token (cached). Delegates to the shared chain.Decimals. -func (r *reader) tokenDecimals(ctx context.Context, token common.Address) (int, error) { - return r.dec.Get(ctx, token) -} +type recoveryVault = liquidlane.Adapter // readVaultInventories reads each adapter's fill-time views (paused, getMaxAssets(tokenIn), // getMaxRate(tokenIn)) in one multicall, using the startup-resolved Vault/Asset (decimals cached). Used @@ -60,54 +33,10 @@ func (r *reader) tokenDecimals(ctx context.Context, token common.Address) (int, func (r *reader) readVaultInventories( ctx context.Context, tokenIn common.Address, vaults []recoveryVault, ) ([]solverInventory, error) { - vaults = dedupeVaultsByAdapter(vaults) if len(vaults) == 0 { return nil, nil } - calls := make([]chain.Call, 0, len(vaults)*readsPerAdapter) - for _, v := range vaults { - calls = append(calls, - chain.Call{Target: v.Adapter, AllowFailure: true, Data: llAdapter.PackPaused()}, - chain.Call{Target: v.Adapter, AllowFailure: true, Data: llAdapter.PackGetMaxAssets(tokenIn)}, - chain.Call{Target: v.Adapter, AllowFailure: true, Data: llAdapter.PackGetMaxRate(tokenIn)}, - ) - } - res, err := r.chain.Multicall(ctx, calls) - if err != nil { - return nil, err - } - if len(res) != len(calls) { - return nil, errors.Errorf("inventory multicall: got %d results, want %d", len(res), len(calls)) - } - - out := make([]solverInventory, 0, len(vaults)) - for i, v := range vaults { - base := i * readsPerAdapter - paused, maxA, mr := res[base], res[base+1], res[base+2] - if !maxA.Success || !mr.Success { - continue - } - if p, perr := llAdapter.UnpackPaused(paused.ReturnData); paused.Success && perr == nil && p { - continue - } - maxAssets, e1 := llAdapter.UnpackGetMaxAssets(maxA.ReturnData) - maxRate, e2 := llAdapter.UnpackGetMaxRate(mr.ReturnData) - if e1 != nil || e2 != nil { - continue - } - if maxAssets.Sign() <= 0 || maxRate.Sign() <= 0 { - continue - } - decimals, derr := r.tokenDecimals(ctx, v.Asset) - if derr != nil { - continue - } - out = append(out, solverInventory{ - Adapter: v.Adapter, Asset: v.Asset, AssetDecimals: decimals, - MaxAssets: maxAssets, MaxRate: maxRate, DiscountID: nil, - }) - } - return out, nil + return r.ll.ReadInventory(ctx, r.ll.RoutesForToken(ctx, vaults, tokenIn)) } // resolveVaults returns a copy of the configured entries with each Vault (adapter.vault()) and Asset @@ -117,47 +46,11 @@ func (r *reader) readVaultInventories( // vault(), then those vaults' asset()); an entry whose reads revert is left zero and skipped by // fill-time reads (readVaultInventories needs a non-zero Asset). Errors only on a multicall transport failure. func (r *reader) resolveVaults(ctx context.Context, vaults []recoveryVault) ([]recoveryVault, error) { - out := make([]recoveryVault, len(vaults)) + adapters := make([]common.Address, len(vaults)) for i := range vaults { - out[i].Adapter = vaults[i].Adapter - } - if len(out) == 0 { - return out, nil - } - vcalls := make([]chain.Call, len(out)) - for i := range out { - vcalls[i] = chain.Call{Target: out[i].Adapter, AllowFailure: true, Data: llAdapter.PackVault()} - } - vres, err := r.chain.Multicall(ctx, vcalls) - if err != nil { - return nil, err - } - acalls := make([]chain.Call, len(out)) - for i := range out { - if i < len(vres) && vres[i].Success { - if vault, verr := llAdapter.UnpackVault(vres[i].ReturnData); verr == nil { - out[i].Vault = vault - } - } - // asset() reads the resolved vault; a zero target reverts (AllowFailure) and is skipped. - acalls[i] = chain.Call{Target: out[i].Vault, AllowFailure: true, Data: erc4626b.PackAsset()} - } - ares, err := r.chain.Multicall(ctx, acalls) - if err != nil { - return nil, err + adapters[i] = vaults[i].Adapter } - for i := range out { - if i < len(ares) && ares[i].Success { - if asset, aerr := erc4626b.UnpackAsset(ares[i].ReturnData); aerr == nil { - out[i].Asset = asset - } - } - if out[i].Vault == (common.Address{}) || out[i].Asset == (common.Address{}) { - r.log.Error(errors.New("adapter vault/asset unresolved"), "recovery entry skipped until restart", - "adapter", out[i].Adapter.Hex()) - } - } - return out, nil + return r.ll.ResolveAdapters(ctx, adapters) } // readPermissionedVaultInventories returns the subset of readVaultInventories the executor is @@ -172,86 +65,5 @@ func (r *reader) readPermissionedVaultInventories( if err != nil || len(base) == 0 { return base, err } - - // 1) marketMaker() + owner() for each candidate adapter, in one multicall. - calls := make([]chain.Call, 0, len(base)*2) - for _, inv := range base { - calls = append(calls, - chain.Call{Target: inv.Adapter, AllowFailure: true, Data: llAdapter.PackMarketMaker()}, - chain.Call{Target: inv.Adapter, AllowFailure: true, Data: llAdapter.PackOwner()}, - ) - } - res, err := r.chain.Multicall(ctx, calls) - if err != nil { - return nil, err - } - if len(res) != len(calls) { - return nil, errors.Errorf("authorization multicall: got %d results, want %d", len(res), len(calls)) - } - - type authz struct { - marketMaker, owner common.Address - resolved bool - } - auths := make([]authz, len(base)) - var fillerChecks []int // base indices needing an isFiller delegation check - for i := range base { - mm, ow := res[i*2], res[i*2+1] - if !mm.Success || !ow.Success { - continue - } - marketMaker, e1 := llAdapter.UnpackMarketMaker(mm.ReturnData) - owner, e2 := llAdapter.UnpackOwner(ow.ReturnData) - if e1 != nil || e2 != nil { - continue - } - auths[i] = authz{marketMaker: marketMaker, owner: owner, resolved: true} - if marketMaker != executor && owner != executor { - fillerChecks = append(fillerChecks, i) - } - } - - // 2) isFiller(marketMaker, executor) for the adapters not directly owned, in one multicall. - delegated := make(map[int]bool, len(fillerChecks)) - if len(fillerChecks) > 0 { - fcalls := make([]chain.Call, len(fillerChecks)) - for j, i := range fillerChecks { - fcalls[j] = chain.Call{Target: base[i].Adapter, AllowFailure: true, Data: llAdapter.PackIsFiller(auths[i].marketMaker, executor)} - } - fres, ferr := r.chain.Multicall(ctx, fcalls) - if ferr != nil { - return nil, ferr - } - for j, i := range fillerChecks { - if j < len(fres) && fres[j].Success { - if ok, derr := llAdapter.UnpackIsFiller(fres[j].ReturnData); derr == nil && ok { - delegated[i] = true - } - } - } - } - - out := make([]solverInventory, 0, len(base)) - for i, inv := range base { - a := auths[i] - if a.resolved && (a.marketMaker == executor || a.owner == executor || delegated[i]) { - out = append(out, inv) - } - } - return out, nil -} - -// dedupeByAdapter keeps the first recovery vault per distinct adapter, matching the de-dup in -// readAdapterInventories (keyed by adapter). -func dedupeVaultsByAdapter(in []recoveryVault) []recoveryVault { - seen := make(map[common.Address]bool, len(in)) - out := make([]recoveryVault, 0, len(in)) - for _, v := range in { - if seen[v.Adapter] { - continue - } - seen[v.Adapter] = true - out = append(out, v) - } - return out + return r.ll.FilterAuthorized(ctx, base, executor) } diff --git a/internal/solvers/rfq/config.go b/internal/solvers/rfq/config.go index 3a1c58de..376cf0c9 100644 --- a/internal/solvers/rfq/config.go +++ b/internal/solvers/rfq/config.go @@ -10,6 +10,7 @@ import ( "github.com/symbioticfi/vault-solver/internal/parse" "github.com/symbioticfi/vault-solver/internal/solver" + "github.com/symbioticfi/vault-solver/internal/tokenpolicy" ) // rawConfig mirrors the YAML shape; strings are parsed into typed values in parseConfig. @@ -57,14 +58,8 @@ type Config struct { // - internal: uses public discounts; adapters (optional) scope the QUOTE path only, while filling stays // unrestricted so discount-driven recovery legs through any advertised adapter still execute. SolverMode string - // TokensToQuote scopes which input tokens this filler quotes by class: "all" (default) quotes any, - // "permissioned" quotes only tokens in PermissionedTokens, "permissionless" quotes only tokens NOT in - // PermissionedTokens. Typically set per instance via env (e.g. tokensToQuote: ${TOKENS_TO_QUOTE}). - TokensToQuote string - // PermissionedTokens is the local set of input-token addresses treated as permissioned. The - // TokensToQuote scope is evaluated against it. When TokensToQuote is "permissioned", admitted - // inputs must use exactly one route. Empty means no input token is permissioned. - PermissionedTokens map[common.Address]bool + // TokenPolicy scopes quoted input tokens and enforces single-route fills in permissioned mode. + TokenPolicy tokenpolicy.Policy // Adapters is the configured LiquidLane adapter universe: in external mode the set quoting/filling is // scoped to, and the candidate universe used to rebuild a fill plan when the quote-time plan isn't // cached (e.g. after a restart). Config carries only adapter addresses; @@ -85,14 +80,6 @@ const ( solverModeInternal = "internal" // public discounts API on top of all advertised adapters ) -// Input-token quote scopes (see Config.TokensToQuote): "all" quotes any input token, "permissioned" -// quotes only tokens in PermissionedTokens, "permissionless" quotes only tokens not in it. -const ( - tokensToQuoteAll = "all" - tokensToQuotePermissioned = "permissioned" - tokensToQuotePermissionless = "permissionless" -) - // Defaults applied when a field is unset. const ( defaultListenAddr = ":42073" @@ -122,10 +109,9 @@ func parseConfig(node yaml.Node) (*Config, error) { if mode != solverModeExternal && mode != solverModeInternal { return nil, errors.Errorf("solverMode: must be %q or %q, got %q", solverModeExternal, solverModeInternal, mode) } - scope := parse.OrDefault(raw.TokensToQuote, tokensToQuoteAll) - if scope != tokensToQuoteAll && scope != tokensToQuotePermissioned && scope != tokensToQuotePermissionless { - return nil, errors.Errorf("tokensToQuote: must be %q, %q or %q, got %q", - tokensToQuoteAll, tokensToQuotePermissioned, tokensToQuotePermissionless, scope) + tokenPolicy, err := tokenpolicy.Parse(raw.TokensToQuote, raw.PermissionedTokens) + if err != nil { + return nil, err } cfg := &Config{ @@ -136,22 +122,12 @@ func parseConfig(node yaml.Node) (*Config, error) { PollInterval: defaultPollInterval, OrderLimit: defaultOrderLimit, SolverMode: mode, - TokensToQuote: scope, + TokenPolicy: tokenPolicy, Strategy: StrategyConfig{ Name: parse.OrDefault(raw.Strategy.Name, defaultStrategyName), Config: raw.Strategy.Config, }, } - for i, t := range raw.PermissionedTokens { - addr, terr := parse.NonZeroAddress(t, "permissionedTokens["+strconv.Itoa(i)+"]") - if terr != nil { - return nil, terr - } - if cfg.PermissionedTokens == nil { - cfg.PermissionedTokens = make(map[common.Address]bool, len(raw.PermissionedTokens)) - } - cfg.PermissionedTokens[addr] = true - } if raw.PollIntervalMs > 0 { cfg.PollInterval = time.Duration(raw.PollIntervalMs) * time.Millisecond } diff --git a/internal/solvers/rfq/config_test.go b/internal/solvers/rfq/config_test.go index 36c33fb4..ec6f165b 100644 --- a/internal/solvers/rfq/config_test.go +++ b/internal/solvers/rfq/config_test.go @@ -186,7 +186,7 @@ func TestParseConfig_Adapters(t *testing.T) { if v.Adapter != common.HexToAddress("0x0000000000000000000000000000000000000042") { t.Fatalf("adapter entry not parsed: %+v", v) } - if v.Vault != (common.Address{}) || v.Asset != (common.Address{}) { + if v.Vault != (common.Address{}) || v.TokenOut != (common.Address{}) { t.Fatalf("vault/asset should be unresolved before startup: %+v", v) } } diff --git a/internal/solvers/rfq/discounts_disabled_test.go b/internal/solvers/rfq/discounts_disabled_test.go index e2c423f3..250e7b9c 100644 --- a/internal/solvers/rfq/discounts_disabled_test.go +++ b/internal/solvers/rfq/discounts_disabled_test.go @@ -21,7 +21,7 @@ func TestExecution_DiscountsDisabled_RecoverySkipsListDiscounts(t *testing.T) { be.discounts = &discountsResponse{Discounts: []discountListItem{{ DiscountID: "0x00000000000000000000000000000000000000000000000000000000000000ab", Adapter: vlt.Hex(), TokenToRedeem: tIn.Hex(), Collateral: tOut.Hex(), CollateralDecimals: 6, - MaxAssets: "10000000", MaxRate: "1000000000000000000", + Discount: "500", MaxAssets: "10000000", MaxRate: "1000000000000000000", }}} txm := &fakeTxm{result: txmanager.Result{Hash: common.HexToHash("0xdead")}} e := newExec(t, st, be, txm) diff --git a/internal/solvers/rfq/execution.go b/internal/solvers/rfq/execution.go index 6e0ed374..1b2e1c0c 100644 --- a/internal/solvers/rfq/execution.go +++ b/internal/solvers/rfq/execution.go @@ -10,7 +10,10 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/go-errors/errors" "github.com/go-logr/logr" + "github.com/symbioticfi/vault-solver/internal/liquidlane" + "github.com/symbioticfi/vault-solver/internal/liquidlane/discounts" "github.com/symbioticfi/vault-solver/internal/solvers/rfq/strategies/types" + "github.com/symbioticfi/vault-solver/internal/tokenpolicy" "github.com/symbioticfi/vault-solver/api/bindings/rfq/executor" "github.com/symbioticfi/vault-solver/internal/txmanager" @@ -45,21 +48,20 @@ type executable struct { // own goroutine; per-order work is guarded by an in-flight set so overlapping poll cycles never // double-submit the same order. type executionService struct { - chainID int64 - executor common.Address - orderLimit int - vaults []recoveryVault - whitelist adapterWhitelist // nil disables adapter filtering - tokensToQuote string - permissionedTokens map[common.Address]bool - discountsEnabled bool // false (external solver) skips the backend discounts API entirely - backend orderBackend - store *store - reader recoveryReader - strategy types.Strategy - txm txSender - log logr.Logger - now func() time.Time + chainID int64 + executor common.Address + orderLimit int + vaults []recoveryVault + whitelist adapterWhitelist // nil disables adapter filtering + tokenPolicy tokenpolicy.Policy + discountsEnabled bool // false (external solver) skips the backend discounts API entirely + backend orderBackend + store *store + reader recoveryReader + strategy types.Strategy + txm txSender + log logr.Logger + now func() time.Time inflightMu sync.Mutex inflight map[string]bool @@ -281,7 +283,7 @@ func (e *executionService) buildFillPlan( RequestID: exec.quoteID, QuoteID: exec.quoteID, TokenIn: order.Request.TokenIn, TokenOut: outputToken, Amount: order.Request.AmountIn, } - requireSingleRoute := requiresSingleRoute(e.tokensToQuote, e.permissionedTokens, req.TokenIn) + requireSingleRoute := e.tokenPolicy.RequiresSingleRoute(req.TokenIn) input := newFillInput(e.chainID, e.executor, req, inv, required, requireSingleRoute, e.now()) plan, err := e.strategy.BuildFillPlan(ctx, input) if err != nil || plan == nil { @@ -336,31 +338,30 @@ func (e *executionService) discountInventories( for _, d := range direct { seen[d.Adapter] = true } + now := e.now() var out []solverInventory for _, d := range resp.Discounts { - if !common.IsHexAddress(d.Adapter) || !common.IsHexAddress(d.TokenToRedeem) || !common.IsHexAddress(d.Collateral) { + offer, parseErr := discounts.ParseOffer(d) + if parseErr != nil { + e.log.V(1).Info("recover: skip invalid discount", "discountId", d.DiscountID, "error", parseErr.Error()) continue } - if common.HexToAddress(d.TokenToRedeem) != tokenIn { + if offer.Deadline <= now.Unix() || offer.TokenToRedeem != tokenIn { continue } - adapter := common.HexToAddress(d.Adapter) + adapter := offer.Adapter if !e.whitelist.allows(adapter) { continue } if seen[adapter] { continue } - maxOut, ok1 := new(big.Int).SetString(d.MaxAssets, 10) - maxRate, ok2 := new(big.Int).SetString(d.MaxRate, 10) - if !ok1 || !ok2 { - continue - } - h := common.HexToHash(d.DiscountID) - out = append(out, solverInventory{ - Adapter: adapter, Asset: common.HexToAddress(d.Collateral), AssetDecimals: d.CollateralDecimals, - MaxAssets: maxOut, MaxRate: maxRate, DiscountID: &h, - }) + route := liquidlane.NewRoute( + e.chainID, adapter, common.Address{}, tokenIn, offer.Collateral, 0, offer.CollateralDecimals, + ) + out = append(out, liquidlane.DiscountInventory( + route, offer.MaxAssets, offer.MaxRate, offer.DiscountID, time.Unix(offer.Deadline, 0), + )) } return out } @@ -378,47 +379,29 @@ var errDiscountsDisabled = errors.New("discount leg present but discounts are di func toDiscountSwapInput( r *resolveDiscountResponse, leg fillLeg, recipient common.Address, ) (executor.IReactorDiscountSwapInput, error) { - d := r.Discount - for _, a := range []string{d.Adapter, d.TokenToRedeem, d.Signer, d.Protocol} { - if !common.IsHexAddress(a) { - return executor.IReactorDiscountSwapInput{}, errors.Errorf("discount: invalid address %q", a) - } - } - if common.HexToAddress(d.Adapter) != leg.Adapter { - return executor.IReactorDiscountSwapInput{}, errors.Errorf( - "%w: resolved %s, leg %s", errDiscountAdapterMismatch, d.Adapter, leg.Adapter.Hex()) - } - discount, ok := new(big.Int).SetString(d.Discount, 10) - if !ok { - return executor.IReactorDiscountSwapInput{}, errors.Errorf("discount: invalid amount %q", d.Discount) - } - nonce, err := hexutil.DecodeBig(d.Nonce) - if err != nil { - return executor.IReactorDiscountSwapInput{}, errors.Errorf("discount: invalid nonce %q: %w", d.Nonce, err) - } - signerSig, err := hexutil.Decode(r.SignerSignature) + parsed, err := discounts.ParseSigned(r) if err != nil { - return executor.IReactorDiscountSwapInput{}, errors.Errorf("discount: signerSignature: %w", err) + return executor.IReactorDiscountSwapInput{}, errors.Errorf("discount: %w", err) } - protocolSig, err := hexutil.Decode(r.ProtocolSignature) - if err != nil { - return executor.IReactorDiscountSwapInput{}, errors.Errorf("discount: protocolSignature: %w", err) + if parsed.Adapter != leg.Adapter { + return executor.IReactorDiscountSwapInput{}, errors.Errorf( + "%w: resolved %s, leg %s", errDiscountAdapterMismatch, parsed.Adapter.Hex(), leg.Adapter.Hex()) } // Mirrors buildDiscountSwapInputs in discounts.ts: the outer adapter comes from the resolved // discount's adapter, the inner Discount no longer carries the vault field, and the input drops // amountOut. return executor.IReactorDiscountSwapInput{ - Adapter: common.HexToAddress(d.Adapter), + Adapter: parsed.Adapter, DiscountSwap: executor.ILiquidLaneAdapterDiscountSwap{ Discount: executor.ILiquidLaneAdapterDiscount{ - TokenToRedeem: common.HexToAddress(d.TokenToRedeem), - Discount: discount, Signer: common.HexToAddress(d.Signer), Protocol: common.HexToAddress(d.Protocol), - Nonce: nonce, Deadline: big.NewInt(d.Deadline), + TokenToRedeem: parsed.Terms.TokenToRedeem, + Discount: parsed.Terms.Discount, Signer: parsed.Terms.Signer, Protocol: parsed.Terms.Protocol, + Nonce: parsed.Terms.Nonce, Deadline: parsed.Terms.Deadline, }, - SignerSignature: signerSig, - ProtocolDeadline: big.NewInt(r.ProtocolDeadline), + SignerSignature: parsed.SignerSignature, + ProtocolDeadline: parsed.ProtocolDeadline, }, - ProtocolSignature: protocolSig, + ProtocolSignature: parsed.ProtocolSignature, Recipient: recipient, AmountIn: new(big.Int).Set(leg.AmountIn), }, nil diff --git a/internal/solvers/rfq/execution_test.go b/internal/solvers/rfq/execution_test.go index bd2b61f1..8db0fc5a 100644 --- a/internal/solvers/rfq/execution_test.go +++ b/internal/solvers/rfq/execution_test.go @@ -184,6 +184,7 @@ func TestExecution_DiscountFill(t *testing.T) { st, be := fillFixtures(t) h := common.HexToHash("0x00000000000000000000000000000000000000000000000000000000000000ab") be.discount = &resolveDiscountResponse{ + DiscountID: h.Hex(), Discount: discountTerms{ Adapter: vlt.Hex(), TokenToRedeem: tIn.Hex(), Discount: "500", Signer: "0x00000000000000000000000000000000000000a1", @@ -222,9 +223,11 @@ func TestExecution_DiscountOnlyRecovery_EmptyVaults(t *testing.T) { be.discounts = &discountsResponse{Discounts: []discountListItem{{ DiscountID: h.Hex(), Adapter: vlt.Hex(), TokenToRedeem: tIn.Hex(), Collateral: tOut.Hex(), CollateralDecimals: 6, + Discount: "500", Deadline: 4_102_444_800, MaxAssets: "10000000", MaxRate: "1000000000000000000", // 1e7 liquidity, rate 1.0 → 1000000 out ≥ 900000 required }}} be.discount = &resolveDiscountResponse{ + DiscountID: h.Hex(), Discount: discountTerms{ Adapter: vlt.Hex(), TokenToRedeem: tIn.Hex(), Discount: "500", Signer: "0x00000000000000000000000000000000000000a1", @@ -259,6 +262,7 @@ func TestExecution_DiscountAdapterMismatchFails(t *testing.T) { // different adapter — the fill must be aborted without a tx. h := common.HexToHash("0x00000000000000000000000000000000000000000000000000000000000000ab") be.discount = &resolveDiscountResponse{ + DiscountID: h.Hex(), Discount: discountTerms{ Adapter: "0x00000000000000000000000000000000000000aa", // not the quoted leg's adapter TokenToRedeem: tIn.Hex(), Discount: "500", @@ -305,9 +309,11 @@ func TestExecution_DiscountInventoriesWhitelist(t *testing.T) { rogue := common.HexToAddress("0x00000000000000000000000000000000000000aa") be := &fakeBackend{discounts: &discountsResponse{Discounts: []discountListItem{ {DiscountID: listedID, Adapter: vlt.Hex(), TokenToRedeem: tIn.Hex(), Collateral: tOut.Hex(), - CollateralDecimals: 6, MaxRate: "1000000000000000000", MaxAssets: "10000000"}, + CollateralDecimals: 6, Discount: "500", Deadline: 4_102_444_800, + MaxRate: "1000000000000000000", MaxAssets: "10000000"}, {DiscountID: rogueID, Adapter: rogue.Hex(), TokenToRedeem: tIn.Hex(), Collateral: tOut.Hex(), - CollateralDecimals: 6, MaxRate: "2000000000000000000", MaxAssets: "10000000"}, + CollateralDecimals: 6, Discount: "500", Deadline: 4_102_444_800, + MaxRate: "2000000000000000000", MaxAssets: "10000000"}, }}} st := newStore(func() time.Time { return time.Unix(0, 0) }) @@ -326,6 +332,22 @@ func TestExecution_DiscountInventoriesWhitelist(t *testing.T) { } } +func TestExecution_DiscountInventoriesSkipsExpired(t *testing.T) { + be := &fakeBackend{discounts: &discountsResponse{Discounts: []discountListItem{{ + DiscountID: "0x00000000000000000000000000000000000000000000000000000000000000a1", + Adapter: vlt.Hex(), TokenToRedeem: tIn.Hex(), Collateral: tOut.Hex(), + CollateralDecimals: 6, Discount: "500", Deadline: 1, + MaxRate: "1000000000000000000", MaxAssets: "10000000", + }}}} + st := newStore(func() time.Time { return time.Unix(2, 0) }) + e := newExec(t, st, be, &fakeTxm{}) + e.now = func() time.Time { return time.Unix(2, 0) } + e.whitelist = buildAdapterWhitelist(true, []recoveryVault{{Adapter: vlt}}) + if out := e.discountInventories(context.Background(), tIn, nil); len(out) != 0 { + t.Fatalf("expired discount inventories = %+v", out) + } +} + func TestExecution_MissingFillPlanFails(t *testing.T) { _, be := fillFixtures(t) st := newStore(func() time.Time { return time.Unix(0, 0) }) @@ -347,8 +369,7 @@ func TestExecutionRecoveryMarksPermissionedScopeAsSingleRoute(t *testing.T) { strategy := &inputRecordingStrategy{fillPlan: baseFillPlan()} e := newExec(t, newStore(func() time.Time { return time.Unix(0, 0) }), &fakeBackend{}, &fakeTxm{}) e.discountsEnabled = false - e.tokensToQuote = tokensToQuotePermissioned - e.permissionedTokens = map[common.Address]bool{tIn: true} + e.tokenPolicy = testPermissionedPolicy(t, tIn) e.strategy = strategy plan, err := e.buildFillPlan( @@ -378,8 +399,7 @@ func TestExecutionRejectsPermissionedScopeMultiLegFillPlan(t *testing.T) { } e := newExec(t, newStore(func() time.Time { return time.Unix(0, 0) }), &fakeBackend{}, &fakeTxm{}) e.discountsEnabled = false - e.tokensToQuote = tokensToQuotePermissioned - e.permissionedTokens = map[common.Address]bool{tIn: true} + e.tokenPolicy = testPermissionedPolicy(t, tIn) e.strategy = fixedFillStrategy{plan: plan} got, err := e.buildFillPlan( diff --git a/internal/solvers/rfq/gating_test.go b/internal/solvers/rfq/gating_test.go index 8016ded2..36fe571a 100644 --- a/internal/solvers/rfq/gating_test.go +++ b/internal/solvers/rfq/gating_test.go @@ -7,59 +7,11 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/symbioticfi/vault-solver/internal/solvers/rfq/strategies/types" + "github.com/symbioticfi/vault-solver/internal/tokenpolicy" ) -// mGLOBAL (permissioned) and mF-ONE (permissionless) Hoodi addresses, used as token fixtures. -var ( - permissionedToken = common.HexToAddress("0x2Ee6f1A395Bce7a7c5bF1D07bAaF9F8A0828A8d3") - permissionlessToken = common.HexToAddress("0xA684911e92b8E4Dd27046331B849Bbd6dbca0fA2") -) - -func TestQuotesTokenInScope(t *testing.T) { - perm := map[common.Address]bool{permissionedToken: true} - - cases := []struct { - scope string - token common.Address - want bool - }{ - {tokensToQuoteAll, permissionedToken, true}, - {tokensToQuoteAll, permissionlessToken, true}, - {tokensToQuotePermissioned, permissionedToken, true}, - {tokensToQuotePermissioned, permissionlessToken, false}, - {tokensToQuotePermissionless, permissionedToken, false}, - {tokensToQuotePermissionless, permissionlessToken, true}, - {"", permissionlessToken, true}, // unset scope behaves like "all" - } - for _, c := range cases { - qs := "eService{tokensToQuote: c.scope, permissionedTokens: perm} - if got := qs.quotesTokenIn(c.token); got != c.want { - t.Errorf("scope=%q token=%s: got %v, want %v", c.scope, c.token.Hex(), got, c.want) - } - } -} - -func TestRequiresSingleRoute(t *testing.T) { - permissioned := map[common.Address]bool{permissionedToken: true} - tests := []struct { - name string - scope string - token common.Address - want bool - }{ - {"permissioned scope and token", tokensToQuotePermissioned, permissionedToken, true}, - {"permissioned scope but permissionless token", tokensToQuotePermissioned, permissionlessToken, false}, - {"all scope", tokensToQuoteAll, permissionedToken, false}, - {"permissionless scope", tokensToQuotePermissionless, permissionedToken, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := requiresSingleRoute(tt.scope, permissioned, tt.token); got != tt.want { - t.Fatalf("requiresSingleRoute() = %v, want %v", got, tt.want) - } - }) - } -} +// mGLOBAL Hoodi address, used as a permissioned-token fixture. +var permissionedToken = common.HexToAddress("0x2Ee6f1A395Bce7a7c5bF1D07bAaF9F8A0828A8d3") func TestParseConfigTokenScope(t *testing.T) { const base = ` @@ -77,19 +29,19 @@ permissionedTokens: if err != nil { t.Fatalf("parse: %v", err) } - if cfg.TokensToQuote != tokensToQuotePermissioned { - t.Errorf("TokensToQuote = %q, want %q", cfg.TokensToQuote, tokensToQuotePermissioned) + if cfg.TokenPolicy.Scope() != tokenpolicy.Permissioned { + t.Errorf("TokenPolicy.Scope() = %q, want %q", cfg.TokenPolicy.Scope(), tokenpolicy.Permissioned) } - if !cfg.PermissionedTokens[permissionedToken] { - t.Errorf("expected mGLOBAL in PermissionedTokens") + if !cfg.TokenPolicy.RequiresSingleRoute(permissionedToken) { + t.Errorf("expected mGLOBAL to require one route") } def, err := parseCfg(t, base) if err != nil { t.Fatalf("parse default: %v", err) } - if def.TokensToQuote != tokensToQuoteAll { - t.Errorf("default TokensToQuote = %q, want %q", def.TokensToQuote, tokensToQuoteAll) + if def.TokenPolicy.Scope() != tokenpolicy.All { + t.Errorf("default token scope = %q, want %q", def.TokenPolicy.Scope(), tokenpolicy.All) } if _, err := parseCfg(t, base+"tokensToQuote: bogus\n"); err == nil { @@ -131,8 +83,7 @@ func TestQuoteMarksPermissionedScopeAsSingleRoute(t *testing.T) { }}, }} srv := testServer() - srv.quotes.tokensToQuote = tokensToQuotePermissioned - srv.quotes.permissionedTokens = map[common.Address]bool{permissionedToken: true} + srv.quotes.tokenPolicy = testPermissionedPolicy(t, permissionedToken) srv.quotes.strategy = strategy request := validQuoteBody() request.TokenIn = permissionedToken.Hex() @@ -148,3 +99,12 @@ func TestQuoteMarksPermissionedScopeAsSingleRoute(t *testing.T) { t.Fatal("permissioned quote input did not require a single route") } } + +func testPermissionedPolicy(t *testing.T, tokens ...common.Address) tokenpolicy.Policy { + t.Helper() + policy, err := tokenpolicy.New(tokenpolicy.Permissioned, tokens) + if err != nil { + t.Fatalf("tokenpolicy.New: %v", err) + } + return policy +} diff --git a/internal/solvers/rfq/quote.go b/internal/solvers/rfq/quote.go index df2092c1..8d5246a5 100644 --- a/internal/solvers/rfq/quote.go +++ b/internal/solvers/rfq/quote.go @@ -9,22 +9,20 @@ import ( "github.com/go-errors/errors" "github.com/go-logr/logr" "github.com/symbioticfi/vault-solver/internal/solvers/rfq/strategies/types" + "github.com/symbioticfi/vault-solver/internal/tokenpolicy" ) // quoteService prices backend RFQ requests by handing filtered candidates to the strategy. It is safe // for concurrent use (the HTTP server serves quotes in parallel): its dependencies are individually // synchronized, and it holds no mutable state itself. type quoteService struct { - chainID int64 - executor common.Address - whitelist adapterWhitelist // nil disables adapter filtering - // tokensToQuote scopes which input tokens are quotable: "all" (default), "permissioned", or - // "permissionless" (see Config.TokensToQuote); evaluated against permissionedTokens. - tokensToQuote string - permissionedTokens map[common.Address]bool - strategy types.Strategy - log logr.Logger - now func() time.Time + chainID int64 + executor common.Address + whitelist adapterWhitelist // nil disables adapter filtering + tokenPolicy tokenpolicy.Policy + strategy types.Strategy + log logr.Logger + now func() time.Time } // quote returns a priced quote, or nil (→ HTTP 204) when the request is well-formed but this filler @@ -39,9 +37,9 @@ func (qs *quoteService) quote(ctx context.Context, q *quoteRequest) (*quoteRespo qs.log.V(1).Info("declining quote: not quotable", "quoteId", q.QuoteID, "type", q.Type) return nil, nil } - if !qs.quotesTokenIn(parsed.req.TokenIn) { + if !qs.tokenPolicy.Allows(parsed.req.TokenIn) { qs.log.V(1).Info("declining quote: input token out of scope", - "quoteId", q.QuoteID, "tokenIn", lowerAddr(parsed.req.TokenIn), "scope", qs.tokensToQuote) + "quoteId", q.QuoteID, "tokenIn", lowerAddr(parsed.req.TokenIn), "scope", qs.tokenPolicy.Scope()) return nil, nil } req, inv := parsed.req, qs.whitelist.filter(parsed.inv) @@ -50,7 +48,7 @@ func (qs *quoteService) quote(ctx context.Context, q *quoteRequest) (*quoteRespo return nil, nil } - requireSingleRoute := requiresSingleRoute(qs.tokensToQuote, qs.permissionedTokens, req.TokenIn) + requireSingleRoute := qs.tokenPolicy.RequiresSingleRoute(req.TokenIn) input := newQuoteInput(qs.chainID, qs.executor, req, inv, nil, requireSingleRoute, qs.now()) out, err := qs.strategy.DecideQuote(ctx, input) if err != nil { @@ -86,17 +84,3 @@ func (qs *quoteService) quote(ctx context.Context, q *quoteRequest) (*quoteRespo // lowerAddr renders an address as lowercase hex; RFQ backend payloads use lowercase addresses. func lowerAddr(a common.Address) string { return strings.ToLower(a.Hex()) } - -// quotesTokenIn reports whether this filler's TokensToQuote scope admits the request's input token: -// "permissioned" admits only tokens in permissionedTokens, "permissionless" admits only those not in -// it, and "all" (or any unset value, for hand-built services) admits every token. -func (qs *quoteService) quotesTokenIn(tokenIn common.Address) bool { - switch qs.tokensToQuote { - case tokensToQuotePermissioned: - return qs.permissionedTokens[tokenIn] - case tokensToQuotePermissionless: - return !qs.permissionedTokens[tokenIn] - default: - return true - } -} diff --git a/internal/solvers/rfq/solver.go b/internal/solvers/rfq/solver.go index 7788659c..33e44e1a 100644 --- a/internal/solvers/rfq/solver.go +++ b/internal/solvers/rfq/solver.go @@ -90,32 +90,30 @@ func buildServices( execWhitelist := buildAdapterWhitelist(cfg.restrictsToAdapters(), cfg.Adapters) quotes := "eService{ - chainID: chainID, - executor: cfg.Executor, - whitelist: quoteWhitelist, - tokensToQuote: cfg.TokensToQuote, - permissionedTokens: cfg.PermissionedTokens, - strategy: quoteStrategy, - log: log, - now: time.Now, + chainID: chainID, + executor: cfg.Executor, + whitelist: quoteWhitelist, + tokenPolicy: cfg.TokenPolicy, + strategy: quoteStrategy, + log: log, + now: time.Now, } exec := &executionService{ - chainID: chainID, - executor: cfg.Executor, - orderLimit: cfg.OrderLimit, - vaults: cfg.Adapters, - whitelist: execWhitelist, - tokensToQuote: cfg.TokensToQuote, - permissionedTokens: cfg.PermissionedTokens, - discountsEnabled: cfg.usesDiscounts(), - backend: newBackendClient(cfg.BackendURL), - store: st, - reader: rdr, - strategy: quoteStrategy, - txm: txm, - log: log, - now: time.Now, - inflight: make(map[string]bool), + chainID: chainID, + executor: cfg.Executor, + orderLimit: cfg.OrderLimit, + vaults: cfg.Adapters, + whitelist: execWhitelist, + tokenPolicy: cfg.TokenPolicy, + discountsEnabled: cfg.usesDiscounts(), + backend: newBackendClient(cfg.BackendURL), + store: st, + reader: rdr, + strategy: quoteStrategy, + txm: txm, + log: log, + now: time.Now, + inflight: make(map[string]bool), } return quotes, exec } diff --git a/internal/solvers/rfq/solver_test.go b/internal/solvers/rfq/solver_test.go index 1726c785..c7dca1bb 100644 --- a/internal/solvers/rfq/solver_test.go +++ b/internal/solvers/rfq/solver_test.go @@ -18,11 +18,10 @@ func TestBuildServices_WhitelistWiring(t *testing.T) { listed := common.HexToAddress("0x0000000000000000000000000000000000000042") rogue := common.HexToAddress("0x00000000000000000000000000000000000000aa") cfg := &Config{ - BackendURL: "https://rfq-backend.example", - Executor: common.HexToAddress("0x0000000000000000000000000000000000000010"), - Adapters: []recoveryVault{{Adapter: listed}}, - TokensToQuote: tokensToQuotePermissioned, - PermissionedTokens: map[common.Address]bool{permissionedToken: true}, + BackendURL: "https://rfq-backend.example", + Executor: common.HexToAddress("0x0000000000000000000000000000000000000010"), + Adapters: []recoveryVault{{Adapter: listed}}, + TokenPolicy: testPermissionedPolicy(t, permissionedToken), } st := newStore(func() time.Time { return time.Unix(0, 0) }) @@ -42,11 +41,9 @@ func TestBuildServices_WhitelistWiring(t *testing.T) { quotes, exec := buildServices(cfg, 1, st, nil, nil, nil, logr.Discard()) scopedToConfigured(t, "quote", quotes.whitelist) scopedToConfigured(t, "execution", exec.whitelist) - if !quotes.permissionedTokens[permissionedToken] || !exec.permissionedTokens[permissionedToken] { - t.Fatal("permissionedTokens were not wired to both quote and execution services") - } - if exec.tokensToQuote != cfg.TokensToQuote { - t.Fatalf("execution tokensToQuote = %q, want %q", exec.tokensToQuote, cfg.TokensToQuote) + if !quotes.tokenPolicy.RequiresSingleRoute(permissionedToken) || + !exec.tokenPolicy.RequiresSingleRoute(permissionedToken) { + t.Fatal("token policy was not wired to both quote and execution services") } // Internal + configured adapters ⇒ the QUOTE path scopes to the configured adapters, but execution diff --git a/internal/solvers/rfq/strategies/default/strategy.go b/internal/solvers/rfq/strategies/default/strategy.go index 8bb7f771..95c9848d 100644 --- a/internal/solvers/rfq/strategies/default/strategy.go +++ b/internal/solvers/rfq/strategies/default/strategy.go @@ -13,7 +13,7 @@ import ( "github.com/symbioticfi/vault-solver/internal/solvers/rfq/strategies/types" "gopkg.in/yaml.v3" - "github.com/symbioticfi/vault-solver/internal/liquidlanemath" + "github.com/symbioticfi/vault-solver/internal/liquidlane" "github.com/symbioticfi/vault-solver/internal/solver" ) @@ -175,7 +175,7 @@ func evaluateGroup( return nil } - privateRate := liquidlanemath.RateForAmountOut(oracleAmountOut, input.AmountIn, tokenInDecimals, assetDecimals) + privateRate := liquidlane.RateForAmountOut(oracleAmountOut, input.AmountIn, tokenInDecimals, assetDecimals) eligible := make([]eligibleLeg, 0, len(group)) for _, c := range group { @@ -217,7 +217,7 @@ func evaluateGroup( break } c := e.candidate - maxAmountIn := liquidlanemath.MaxAmountInForRate(c.MaxAssets, e.rate, tokenInDecimals, c.AssetDecimals) + maxAmountIn := liquidlane.MaxAmountInForRate(c.MaxAssets, e.rate, tokenInDecimals, c.AssetDecimals) if maxAmountIn.Sign() == 0 { continue } @@ -225,11 +225,11 @@ func evaluateGroup( var amountIn, amountOut *big.Int if saturated { - amountIn = liquidlanemath.MinAmountInForAmountOut(c.MaxAssets, e.rate, tokenInDecimals, c.AssetDecimals) + amountIn = liquidlane.MinAmountInForAmountOut(c.MaxAssets, e.rate, tokenInDecimals, c.AssetDecimals) amountOut = new(big.Int).Set(c.MaxAssets) } else { amountIn = new(big.Int).Set(remainingIn) - amountOut = liquidlanemath.AmountOutForRate(amountIn, e.rate, tokenInDecimals, c.AssetDecimals) + amountOut = liquidlane.AmountOutForRate(amountIn, e.rate, tokenInDecimals, c.AssetDecimals) } if amountOut.Sign() == 0 { continue @@ -267,7 +267,7 @@ func evaluateSingleRoute( var best *types.QuoteOutput for _, e := range eligible { c := e.candidate - amountOut := liquidlanemath.AmountOutForRate(input.AmountIn, e.rate, tokenInDecimals, c.AssetDecimals) + amountOut := liquidlane.AmountOutForRate(input.AmountIn, e.rate, tokenInDecimals, c.AssetDecimals) if amountOut.Sign() <= 0 || amountOut.Cmp(c.MaxAssets) > 0 { continue } @@ -361,7 +361,7 @@ func (s *Strategy) fillPlanFromQuote( if c.MaxRate == nil || c.MaxRate.Sign() <= 0 { return nil, errors.Errorf("candidate %q has invalid maxRate", leg.CandidateID) } - maxAmountOut := liquidlanemath.AmountOutForRate(leg.AmountIn, c.MaxRate, tokenInDecimals, c.AssetDecimals) + maxAmountOut := liquidlane.AmountOutForRate(leg.AmountIn, c.MaxRate, tokenInDecimals, c.AssetDecimals) if leg.AmountOut.Cmp(maxAmountOut) > 0 { return nil, errors.Errorf("leg %d exceeds candidate maxRate", i) } diff --git a/internal/solvers/rfq/strategy.go b/internal/solvers/rfq/strategy.go index 02fa322c..c189761f 100644 --- a/internal/solvers/rfq/strategy.go +++ b/internal/solvers/rfq/strategy.go @@ -2,12 +2,12 @@ package rfq import ( "math/big" - "strconv" "time" "github.com/ethereum/go-ethereum/common" "github.com/go-errors/errors" "github.com/go-logr/logr" + "github.com/symbioticfi/vault-solver/internal/liquidlane" "github.com/symbioticfi/vault-solver/internal/solvers/rfq/strategies" "github.com/symbioticfi/vault-solver/internal/solvers/rfq/strategies/types" @@ -22,18 +22,9 @@ func newStrategy(spec StrategyConfig, chainClient *chain.Client, log logr.Logger return strategies.New(name, spec.Config, strategies.Deps{Chain: chainClient, Log: log}) } -// solverInventory is one candidate adapter leg, taken from the backend quote request's snapshot -// (the filler does not re-read maxAssets/maxRate/decimals on-chain in the quote path). "adapter" is -// the address that fills (placed in the on-chain Swap's vault slot); "asset" is the output token. -type solverInventory struct { - ID string - Adapter common.Address - Asset common.Address - AssetDecimals int - MaxAssets *big.Int - MaxRate *big.Int - DiscountID *common.Hash // nil for a direct leg; set for a discount leg -} +// solverInventory is one LiquidLane candidate leg; RFQ maps backend adapter snapshots and fill-time +// recovery reads into the shared LiquidLane inventory shape. +type solverInventory = liquidlane.Inventory type fillLeg = types.FillLeg type fillPlan = types.FillPlan @@ -57,19 +48,16 @@ func newQuoteInput( now time.Time, ) types.QuoteInput { candidates := make([]types.QuoteCandidate, 0, len(inv)) - for i, v := range inv { - id := v.ID - if id == "" { - id = "candidate-" + strconv.Itoa(i) - } + for _, v := range inv { + id := string(liquidlane.NewCandidateID(v.Route, v.DiscountID)) candidates = append(candidates, types.QuoteCandidate{ ID: id, Adapter: v.Adapter, - Asset: v.Asset, - AssetDecimals: v.AssetDecimals, - MaxAssets: cloneBig(v.MaxAssets), - MaxRate: cloneBig(v.MaxRate), - DiscountID: cloneHash(v.DiscountID), + Asset: v.TokenOut, + AssetDecimals: v.TokenOutDecimals, + MaxAssets: liquidlane.CloneBig(v.MaxAssets), + MaxRate: liquidlane.CloneBig(v.MaxRate), + DiscountID: liquidlane.CloneHash(v.DiscountID), }) } return types.QuoteInput{ @@ -79,8 +67,8 @@ func newQuoteInput( Executor: executor, TokenIn: req.TokenIn, TokenOut: req.TokenOut, - AmountIn: cloneBig(req.Amount), - RequiredAmountOut: cloneBig(required), + AmountIn: liquidlane.CloneBig(req.Amount), + RequiredAmountOut: liquidlane.CloneBig(required), RequireSingleRoute: requireSingleRoute, Candidates: candidates, Now: now, @@ -99,33 +87,9 @@ func newFillInput( q := newQuoteInput(chainID, executor, req, inv, required, requireSingleRoute, now) return types.FillInput(q) } - func validateSingleRoute(requireSingleRoute bool, legCount int) error { if requireSingleRoute && legCount != 1 { return errors.Errorf("single-route input requires exactly one leg, got %d", legCount) } return nil } - -func requiresSingleRoute( - tokensToQuote string, - permissionedTokens map[common.Address]bool, - tokenIn common.Address, -) bool { - return tokensToQuote == tokensToQuotePermissioned && permissionedTokens[tokenIn] -} - -func cloneBig(n *big.Int) *big.Int { - if n == nil { - return nil - } - return new(big.Int).Set(n) -} - -func cloneHash(h *common.Hash) *common.Hash { - if h == nil { - return nil - } - out := *h - return &out -} diff --git a/internal/solvers/rfq/strategy_test.go b/internal/solvers/rfq/strategy_test.go index b375bf49..94f3c632 100644 --- a/internal/solvers/rfq/strategy_test.go +++ b/internal/solvers/rfq/strategy_test.go @@ -150,8 +150,7 @@ func TestQuoteRejectsWebhookMultiLegPlanForPermissionedScope(t *testing.T) { t.Fatalf("NewClient: %v", err) } quoteServer := testServer() - quoteServer.quotes.tokensToQuote = tokensToQuotePermissioned - quoteServer.quotes.permissionedTokens = map[common.Address]bool{tIn: true} + quoteServer.quotes.tokenPolicy = testPermissionedPolicy(t, tIn) quoteServer.quotes.strategy = webhookstrategy.New(client) request := validQuoteBody() request.Adapters = append(request.Adapters, quoteAdapter{ diff --git a/internal/solvers/rfq/test_helpers_test.go b/internal/solvers/rfq/test_helpers_test.go index 2e35d66b..e8831347 100644 --- a/internal/solvers/rfq/test_helpers_test.go +++ b/internal/solvers/rfq/test_helpers_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/symbioticfi/vault-solver/internal/liquidlane" "github.com/symbioticfi/vault-solver/internal/solvers/rfq/strategies/types" defaultstrategy "github.com/symbioticfi/vault-solver/internal/solvers/rfq/strategies/default" @@ -49,3 +50,8 @@ func (f *fakeStrategyPricing) AmountsOut( func newDefaultTestStrategy(decimals int, out map[common.Address]*big.Int) types.Strategy { return defaultstrategy.New(&fakeStrategyPricing{decimals: decimals, out: out}) } + +func testInventory(adapter, tokenIn, tokenOut common.Address, maxAssets, maxRate *big.Int) solverInventory { + route := liquidlane.NewRoute(1, adapter, common.Address{}, tokenIn, tokenOut, 18, 6) + return liquidlane.DirectInventory(route, maxAssets, maxRate) +} diff --git a/internal/solvers/rfq/whitelist_test.go b/internal/solvers/rfq/whitelist_test.go index b378dfe1..265620cd 100644 --- a/internal/solvers/rfq/whitelist_test.go +++ b/internal/solvers/rfq/whitelist_test.go @@ -38,8 +38,8 @@ func TestAdapterWhitelist_Filter(t *testing.T) { listed := common.HexToAddress("0x0000000000000000000000000000000000000042") rogue := common.HexToAddress("0x00000000000000000000000000000000000000aa") inv := []solverInventory{ - {Adapter: listed, Asset: tOut, MaxAssets: big.NewInt(1), MaxRate: big.NewInt(1)}, - {Adapter: rogue, Asset: tOut, MaxAssets: big.NewInt(1), MaxRate: big.NewInt(1)}, + testInventory(listed, tIn, tOut, big.NewInt(1), big.NewInt(1)), + testInventory(rogue, tIn, tOut, big.NewInt(1), big.NewInt(1)), } wl := buildAdapterWhitelist(true, []recoveryVault{{Adapter: listed}}) diff --git a/internal/tokenpolicy/policy.go b/internal/tokenpolicy/policy.go new file mode 100644 index 00000000..67c891e5 --- /dev/null +++ b/internal/tokenpolicy/policy.go @@ -0,0 +1,112 @@ +// Package tokenpolicy defines the shared input-token admission policy used by solvers. +package tokenpolicy + +import ( + "strconv" + + "github.com/ethereum/go-ethereum/common" + "github.com/go-errors/errors" + + "github.com/symbioticfi/vault-solver/internal/parse" +) + +// Scope selects which input-token class a solver serves. +type Scope string + +const ( + All Scope = "all" + Permissioned Scope = "permissioned" + Permissionless Scope = "permissionless" +) + +// Policy admits input tokens and marks the permissioned-only scope as single-route. +// Its zero value is the unrestricted "all" policy. +type Policy struct { + scope Scope + permissioned map[common.Address]bool +} + +// Parse validates the YAML-facing scope and permissioned-token list. +func Parse(rawScope string, rawTokens []string) (Policy, error) { + scope := Scope(parse.OrDefault(rawScope, string(All))) + if !validScope(scope) { + return Policy{}, errors.Errorf( + "tokensToQuote: must be %q, %q or %q, got %q", + All, Permissioned, Permissionless, rawScope, + ) + } + + tokens := make([]common.Address, 0, len(rawTokens)) + for i, value := range rawTokens { + token, err := parse.NonZeroAddress(value, "permissionedTokens["+strconv.Itoa(i)+"]") + if err != nil { + return Policy{}, err + } + tokens = append(tokens, token) + } + return New(scope, tokens) +} + +// New constructs a policy from typed values. +func New(scope Scope, tokens []common.Address) (Policy, error) { + if scope == "" { + scope = All + } + if !validScope(scope) { + return Policy{}, errors.Errorf("invalid token scope %q", scope) + } + + policy := Policy{scope: scope, permissioned: make(map[common.Address]bool, len(tokens))} + for i, token := range tokens { + if token == (common.Address{}) { + return Policy{}, errors.Errorf("permissionedTokens[%d]: zero address", i) + } + if policy.permissioned[token] { + return Policy{}, errors.Errorf("permissionedTokens[%d]: duplicate token %s", i, token.Hex()) + } + policy.permissioned[token] = true + } + return policy, nil +} + +func validScope(scope Scope) bool { + return scope == All || scope == Permissioned || scope == Permissionless +} + +// Scope returns the normalized configured scope. +func (p Policy) Scope() Scope { + if p.scope == "" { + return All + } + return p.scope +} + +// Allows reports whether token is in this solver's input-token scope. +func (p Policy) Allows(token common.Address) bool { + switch p.Scope() { + case Permissioned: + return p.permissioned[token] + case Permissionless: + return !p.permissioned[token] + case All: + return true + } + return false +} + +// RequiresSingleRoute reports whether an admitted token must use one physical route. +func (p Policy) RequiresSingleRoute(token common.Address) bool { + return p.Scope() == Permissioned && p.permissioned[token] +} + +// SingleRouteTokens returns the strategy-facing single-route token set. +func (p Policy) SingleRouteTokens() map[common.Address]bool { + if p.Scope() != Permissioned || len(p.permissioned) == 0 { + return nil + } + tokens := make(map[common.Address]bool, len(p.permissioned)) + for token := range p.permissioned { + tokens[token] = true + } + return tokens +} diff --git a/internal/tokenpolicy/policy_test.go b/internal/tokenpolicy/policy_test.go new file mode 100644 index 00000000..641ce313 --- /dev/null +++ b/internal/tokenpolicy/policy_test.go @@ -0,0 +1,93 @@ +package tokenpolicy + +import ( + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +var ( + permissionedToken = common.HexToAddress("0x1111111111111111111111111111111111111111") + permissionlessToken = common.HexToAddress("0x2222222222222222222222222222222222222222") +) + +func TestPolicyScopes(t *testing.T) { + tests := []struct { + name string + scope Scope + token common.Address + wantAllowed bool + wantSingleRoute bool + wantSingleRouteSet bool + }{ + {"zero defaults to all", "", permissionedToken, true, false, false}, + {"all admits permissioned", All, permissionedToken, true, false, false}, + {"all admits permissionless", All, permissionlessToken, true, false, false}, + {"permissioned admits member", Permissioned, permissionedToken, true, true, true}, + {"permissioned rejects non-member", Permissioned, permissionlessToken, false, false, true}, + {"permissionless rejects member", Permissionless, permissionedToken, false, false, false}, + {"permissionless admits non-member", Permissionless, permissionlessToken, true, false, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + policy, err := New(tt.scope, []common.Address{permissionedToken}) + if err != nil { + t.Fatalf("New: %v", err) + } + if got := policy.Allows(tt.token); got != tt.wantAllowed { + t.Fatalf("Allows() = %v, want %v", got, tt.wantAllowed) + } + if got := policy.RequiresSingleRoute(tt.token); got != tt.wantSingleRoute { + t.Fatalf("RequiresSingleRoute() = %v, want %v", got, tt.wantSingleRoute) + } + _, gotSingleRouteSet := policy.SingleRouteTokens()[permissionedToken] + if gotSingleRouteSet != tt.wantSingleRouteSet { + t.Fatalf("SingleRouteTokens() contains permissioned token = %v, want %v", gotSingleRouteSet, tt.wantSingleRouteSet) + } + }) + } +} + +func TestParseValidatesConfig(t *testing.T) { + address := permissionedToken.Hex() + policy, err := Parse("", []string{address}) + if err != nil { + t.Fatalf("Parse: %v", err) + } + if policy.Scope() != All || !policy.Allows(permissionlessToken) { + t.Fatalf("default policy = %q", policy.Scope()) + } + + tests := []struct { + name string + scope string + tokens []string + match string + }{ + {"invalid scope", "private", nil, "tokensToQuote"}, + {"invalid address", "all", []string{"bad"}, "invalid address"}, + {"zero address", "all", []string{common.Address{}.Hex()}, "zero address"}, + {"duplicate", "all", []string{address, address}, "duplicate token"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Parse(tt.scope, tt.tokens) + if err == nil || !strings.Contains(err.Error(), tt.match) { + t.Fatalf("Parse() error = %v, want match %q", err, tt.match) + } + }) + } +} + +func TestSingleRouteTokensReturnsCopy(t *testing.T) { + policy, err := New(Permissioned, []common.Address{permissionedToken}) + if err != nil { + t.Fatalf("New: %v", err) + } + tokens := policy.SingleRouteTokens() + delete(tokens, permissionedToken) + if !policy.RequiresSingleRoute(permissionedToken) { + t.Fatal("caller mutated policy through SingleRouteTokens") + } +} diff --git a/internal/txmanager/txmanager.go b/internal/txmanager/txmanager.go index abf892f5..55bc4b6d 100644 --- a/internal/txmanager/txmanager.go +++ b/internal/txmanager/txmanager.go @@ -1,6 +1,6 @@ // Package txmanager owns the on-chain sending account and serializes all transactions through a // single worker goroutine, so multiple solvers can never race on the account nonce. Solvers build -// calldata and hand it over via Send; they never sign or broadcast directly. +// calldata and hand it over via Send, TrySend, or SendAsync; they never sign or broadcast directly. package txmanager import ( @@ -34,28 +34,52 @@ type Backend interface { // Config tunes fee selection and confirmation behavior. type Config struct { - Confirmations uint64 // blocks to wait past inclusion before returning - MaxFeeGwei float64 // cap on max fee per gas; 0 => derive from base fee - TipGwei float64 // priority fee; 0 => use the node's suggestion - PollInterval time.Duration // receipt/confirmation poll cadence; 0 => 2s + Confirmations uint64 // blocks to wait past inclusion before returning + MaxFeeGwei float64 // absolute max fee per gas; app config requires a positive value + TipGwei float64 // priority fee; 0 => use the node's suggestion + PollInterval time.Duration // receipt/confirmation poll cadence; 0 => 2s + ReplacementInterval time.Duration // pending tx fee-bump cadence; 0 => 30s + PendingTimeout time.Duration // switch from replacing the call to cancelling its nonce; 0 => 5m } // Request is a transaction to send. Value nil means 0; GasLimit 0 means "estimate". type Request struct { - To common.Address - Data []byte - Value *big.Int - GasLimit uint64 - Label string // for logs/metrics, e.g. "redeem" + To common.Address + Data []byte + Value *big.Int + GasLimit uint64 + MaxFeePerGas *big.Int // optional hard EIP-1559 fee ceiling; fees are clamped to it or rejected below base fee + Confirmations *uint64 // optional wait override; nil uses Config.Confirmations + Label string // for logs/metrics, e.g. "redeem" } -// Result carries the outcome of a Send. +// Result carries the outcome of one transaction request. type Result struct { Hash common.Hash Receipt *types.Receipt Err error } +type feeQuote struct { + baseFee *big.Int + tip *big.Int + maxFee *big.Int +} + +type pendingTransaction struct { + req Request + nonce uint64 + gas uint64 + value *big.Int + fees feeQuote + attempts []txAttempt +} + +type txAttempt struct { + hash common.Hash + cancellation bool +} + // Manager is the single-writer transaction sender. type Manager struct { backend Backend @@ -64,11 +88,15 @@ type Manager struct { cfg Config log logr.Logger - queue chan job + queue chan job + blockingSlot chan struct{} mu sync.Mutex // guards the local nonce nonce uint64 nonceInit bool + + unminedMu sync.Mutex + unminedNonces map[uint64]struct{} } type job struct { @@ -77,8 +105,13 @@ type job struct { } const ( - defaultPollInterval = 2 * time.Second - maxNonceResyncs = 1 + defaultPollInterval = 2 * time.Second + defaultReplacementInterval = 30 * time.Second + defaultPendingTimeout = 5 * time.Minute + replacementBumpNumerator = 9 + replacementBumpDenominator = 8 + cancellationGasLimit = 21_000 + maxNonceResyncs = 1 ) // New constructs a Manager. Call Start to launch its worker. @@ -86,13 +119,21 @@ func New(backend Backend, s signer.Signer, chainID *big.Int, cfg Config, log log if cfg.PollInterval <= 0 { cfg.PollInterval = defaultPollInterval } + if cfg.ReplacementInterval <= 0 { + cfg.ReplacementInterval = defaultReplacementInterval + } + if cfg.PendingTimeout <= 0 { + cfg.PendingTimeout = defaultPendingTimeout + } return &Manager{ - backend: backend, - signer: s, - chainID: chainID, - cfg: cfg, - log: log.WithName("txmanager"), - queue: make(chan job), + backend: backend, + signer: s, + chainID: chainID, + cfg: cfg, + log: log.WithName("txmanager"), + queue: make(chan job), + blockingSlot: make(chan struct{}, 1), + unminedNonces: make(map[uint64]struct{}), } } @@ -105,7 +146,13 @@ func (m *Manager) Start(ctx context.Context) { m.log.Info("stopped", "reason", ctx.Err().Error()) return case j := <-m.queue: - j.res <- m.execute(ctx, j.req) + pending, err := m.broadcast(ctx, j.req) + if err != nil { + j.res <- Result{Err: err} + continue + } + m.addUnminedNonce(pending.nonce) + go m.complete(ctx, pending, j.res) } } } @@ -118,30 +165,92 @@ func (m *Manager) Start(ctx context.Context) { // context, so Send waits for and returns that real outcome — it must not report a cancellation while // the transaction still lands on-chain, which a caller would read as "not sent" (the caller's ctx is // typically an errgroup child that cancels the instant any sibling solver errors, well before -// shutdown). The worker always delivers exactly one Result, so this wait cannot hang. +// shutdown). The worker owns fee replacement and same-nonce cancellation until it can deliver the +// real receipt or the manager context ends. func (m *Manager) Send(ctx context.Context, req Request) Result { - res := make(chan Result, 1) select { - case m.queue <- job{req: req, res: res}: + case m.blockingSlot <- struct{}{}: + defer func() { <-m.blockingSlot }() case <-ctx.Done(): return Result{Err: ctx.Err()} } - return <-res + return m.sendAccepted(ctx, req) +} + +// TrySend submits only when no blocking Send or TrySend call owns the exclusive slot. Async +// transactions do not hold this slot; every accepted broadcast still receives a serialized nonce. +func (m *Manager) TrySend(ctx context.Context, req Request) (Result, bool) { + select { + case m.blockingSlot <- struct{}{}: + defer func() { <-m.blockingSlot }() + default: + return Result{}, false + } + return m.sendAccepted(ctx, req), true } -// execute runs on the worker goroutine only, so nonce access is single-threaded here; the mutex -// guards against concurrent reads from a future status API. -func (m *Manager) execute(ctx context.Context, req Request) Result { - tip, maxFee, err := m.fees(ctx) +func (m *Manager) sendAccepted(ctx context.Context, req Request) Result { + result, accepted := m.SendAsync(ctx, req) + if !accepted { + return Result{Err: ctx.Err()} + } + return <-result +} + +// SendAsync enqueues one transaction for nonce-serialized broadcast and returns its eventual +// receipt result without waiting for it. Once accepted, the manager's long-lived context owns the +// broadcast and receipt wait, matching Send's cancellation contract. +func (m *Manager) SendAsync(ctx context.Context, req Request) (<-chan Result, bool) { + res := make(chan Result, 1) + select { + case m.queue <- job{req: cloneRequest(req), res: res}: + case <-ctx.Done(): + return nil, false + } + return res, true +} + +// MaxFeePerGas returns the conservative per-gas fee cap that the next transaction would use. Solvers +// use it only for profitability calculations; Send recomputes fees immediately before signing. +func (m *Manager) MaxFeePerGas(ctx context.Context) (*big.Int, error) { + fees, err := m.currentFees(ctx) if err != nil { - return Result{Err: err} + return nil, err + } + return fees.maxFee, nil +} + +// broadcast runs on the worker goroutine only, so fee selection, signing, and nonce assignment stay +// serialized even while earlier transactions wait for receipts concurrently. +func (m *Manager) broadcast(ctx context.Context, req Request) (*pendingTransaction, error) { + fees, err := m.currentFees(ctx) + if err != nil { + return nil, err + } + if req.MaxFeePerGas != nil { + feeCap := new(big.Int).Set(req.MaxFeePerGas) + if feeCap.Sign() <= 0 { + return nil, errors.Errorf("send %q: request max fee per gas must be positive", req.Label) + } + if feeCap.Cmp(fees.baseFee) < 0 { + return nil, errors.Errorf( + "send %q: current base fee per gas %s exceeds request cap %s", req.Label, fees.baseFee, feeCap, + ) + } + if fees.maxFee.Cmp(feeCap) > 0 { + fees.maxFee.Set(feeCap) + } + maxTip := new(big.Int).Sub(feeCap, fees.baseFee) + if fees.tip.Cmp(maxTip) > 0 { + fees.tip.Set(maxTip) + } } gas := req.GasLimit if gas == 0 { gas, err = m.estimateGas(ctx, req) if err != nil { - return Result{Err: err} + return nil, err } } @@ -154,74 +263,288 @@ func (m *Manager) execute(ctx context.Context, req Request) Result { for attempt := 0; attempt <= maxNonceResyncs; attempt++ { nonce, nErr := m.nextNonce(ctx, attempt > 0) if nErr != nil { - return Result{Err: nErr} - } - - tx := types.NewTx(&types.DynamicFeeTx{ - ChainID: m.chainID, - Nonce: nonce, - GasTipCap: tip, - GasFeeCap: maxFee, - Gas: gas, - To: &req.To, - Value: value, - Data: req.Data, - }) - - signed, sErr := m.signer.SignTx(tx, m.chainID) - if sErr != nil { - return Result{Err: sErr} + return nil, nErr } - if sendErr := m.backend.SendTransaction(ctx, signed); sendErr != nil { + hash, sendErr := m.signAndSend( + ctx, nonce, req.To, req.Data, value, gas, fees, + ) + if sendErr != nil { lastErr = sendErr if isNonceTooLow(sendErr) { m.log.Info("nonce too low; resyncing", "label", req.Label, "nonce", nonce) continue // retry with a freshly-synced nonce } - return Result{Err: errors.Errorf("send %q: %w", req.Label, sendErr)} + return nil, errors.Errorf("send %q: %w", req.Label, sendErr) } m.commitNonce(nonce) - hash := signed.Hash() m.log.Info("sent", "label", req.Label, "hash", hash.Hex(), "nonce", nonce) + return &pendingTransaction{ + req: req, + nonce: nonce, + gas: gas, + value: new(big.Int).Set(value), + fees: cloneFeeQuote(fees), + attempts: []txAttempt{{hash: hash}}, + }, nil + } + return nil, errors.Errorf("send %q: exhausted nonce resyncs: %w", req.Label, lastErr) +} + +func (m *Manager) complete(ctx context.Context, pending *pendingTransaction, result chan<- Result) { + defer m.removeUnminedNonce(pending.nonce) + result <- m.waitForPendingTransaction(ctx, pending) +} + +func (m *Manager) confirmations(req Request) uint64 { + if req.Confirmations != nil { + return *req.Confirmations + } + return m.cfg.Confirmations +} + +func (m *Manager) waitForPendingTransaction(ctx context.Context, pending *pendingTransaction) Result { + poll := time.NewTicker(m.cfg.PollInterval) + defer poll.Stop() + replace := time.NewTicker(m.cfg.ReplacementInterval) + defer replace.Stop() + timeout := time.NewTimer(m.cfg.PendingTimeout) + defer timeout.Stop() + + cancelling := false + for { + if receiptResult, done := m.receiptResult(ctx, pending); done { + return receiptResult + } + select { + case <-ctx.Done(): + return Result{Hash: pending.attempts[0].hash, Err: ctx.Err()} + case <-poll.C: + case <-replace.C: + m.tryReplace(ctx, pending, cancelling) + case <-timeout.C: + if !m.isLowestUnminedNonce(pending.nonce) { + m.log.Info("pending timeout deferred behind lower nonce", + "label", pending.req.Label, + "nonce", pending.nonce, + ) + timeout.Reset(m.cfg.PendingTimeout) + continue + } + cancelling = true + m.log.Info("pending transaction timed out; cancelling nonce", + "label", pending.req.Label, + "nonce", pending.nonce, + "timeout", m.cfg.PendingTimeout.String(), + ) + m.tryReplace(ctx, pending, true) + } + } +} + +func (m *Manager) receiptResult(ctx context.Context, pending *pendingTransaction) (Result, bool) { + for i := len(pending.attempts) - 1; i >= 0; i-- { + attempt := pending.attempts[i] + receipt, err := m.backend.TransactionReceipt(ctx, attempt.hash) + if errors.Is(err, ethereum.NotFound) { + continue + } + if err != nil { + m.log.Error(err, "pending transaction receipt unavailable", + "label", pending.req.Label, + "hash", attempt.hash.Hex(), + "nonce", pending.nonce, + ) + continue + } + m.removeUnminedNonce(pending.nonce) + if receipt.Status == types.ReceiptStatusFailed { + return Result{ + Hash: attempt.hash, + Receipt: receipt, + Err: errors.Errorf("tx %s reverted on-chain", attempt.hash.Hex()), + }, true + } + if err := m.waitForConfirmations(ctx, receipt, m.confirmations(pending.req)); err != nil { + return Result{Hash: attempt.hash, Receipt: receipt, Err: err}, true + } + if attempt.cancellation { + return Result{ + Hash: attempt.hash, + Receipt: receipt, + Err: errors.Errorf( + "send %q: pending transaction cancelled at nonce %d after %s", + pending.req.Label, pending.nonce, m.cfg.PendingTimeout, + ), + }, true + } + return Result{Hash: attempt.hash, Receipt: receipt}, true + } + return Result{}, false +} + +func (m *Manager) tryReplace(ctx context.Context, pending *pendingTransaction, cancellation bool) { + limit := m.normalFeeLimit(pending.req) + if cancellation { + limit = m.globalFeeLimit() + } + fees, err := m.nextReplacementFees(ctx, pending.fees, limit) + if err != nil { + m.log.Error(err, "cannot replace pending transaction", + "label", pending.req.Label, + "nonce", pending.nonce, + "cancellation", cancellation, + ) + return + } + to := pending.req.To + data := pending.req.Data + value := pending.value + gas := pending.gas + if cancellation { + to = m.signer.Address() + data = nil + value = new(big.Int) + gas = cancellationGasLimit + } + hash, err := m.signAndSend(ctx, pending.nonce, to, data, value, gas, fees) + if err != nil { + m.log.Error(err, "pending transaction replacement failed", + "label", pending.req.Label, + "nonce", pending.nonce, + "cancellation", cancellation, + ) + return + } + pending.fees = cloneFeeQuote(fees) + pending.attempts = append(pending.attempts, txAttempt{hash: hash, cancellation: cancellation}) + m.log.Info("pending transaction replaced", + "label", pending.req.Label, + "hash", hash.Hex(), + "nonce", pending.nonce, + "cancellation", cancellation, + "maxFeePerGas", fees.maxFee.String(), + "maxPriorityFeePerGas", fees.tip.String(), + ) +} - receipt, wErr := m.waitForReceipt(ctx, hash) - return Result{Hash: hash, Receipt: receipt, Err: wErr} +func (m *Manager) nextReplacementFees( + ctx context.Context, + previous feeQuote, + limit *big.Int, +) (feeQuote, error) { + current, err := m.currentFees(ctx) + if err != nil { + return feeQuote{}, err + } + next := feeQuote{ + baseFee: current.baseFee, + tip: maxBig(current.tip, bumpFee(previous.tip)), + maxFee: maxBig(current.maxFee, bumpFee(previous.maxFee)), } - return Result{Err: errors.Errorf("send %q: exhausted nonce resyncs: %w", req.Label, lastErr)} + if limit != nil && next.maxFee.Cmp(limit) > 0 { + next.maxFee.Set(limit) + } + maxTip := new(big.Int).Sub(next.maxFee, next.baseFee) + if maxTip.Sign() < 0 { + return feeQuote{}, errors.Errorf( + "replacement base fee %s exceeds fee limit %s", next.baseFee, next.maxFee, + ) + } + if next.tip.Cmp(maxTip) > 0 { + next.tip.Set(maxTip) + } + if next.maxFee.Cmp(previous.maxFee) <= 0 || next.tip.Cmp(previous.tip) <= 0 { + return feeQuote{}, errors.Errorf( + "replacement fee limit reached: previous max fee %s tip %s, limit %s", + previous.maxFee, previous.tip, feeLimitString(limit), + ) + } + return next, nil } -// fees computes the EIP-1559 tip and max-fee-per-gas. -func (m *Manager) fees(ctx context.Context) (tip, maxFee *big.Int, err error) { +func (m *Manager) normalFeeLimit(req Request) *big.Int { + limit := reserveCancellationBump(m.globalFeeLimit()) + if req.MaxFeePerGas != nil && (limit == nil || req.MaxFeePerGas.Cmp(limit) < 0) { + limit = new(big.Int).Set(req.MaxFeePerGas) + } + return limit +} + +func (m *Manager) globalFeeLimit() *big.Int { + if m.cfg.MaxFeeGwei <= 0 { + return nil + } + return gweiToWei(m.cfg.MaxFeeGwei) +} + +func (m *Manager) addUnminedNonce(nonce uint64) { + m.unminedMu.Lock() + defer m.unminedMu.Unlock() + m.unminedNonces[nonce] = struct{}{} +} + +func (m *Manager) removeUnminedNonce(nonce uint64) { + m.unminedMu.Lock() + defer m.unminedMu.Unlock() + delete(m.unminedNonces, nonce) +} + +func (m *Manager) isLowestUnminedNonce(nonce uint64) bool { + m.unminedMu.Lock() + defer m.unminedMu.Unlock() + for unmined := range m.unminedNonces { + if unmined < nonce { + return false + } + } + return true +} + +// currentFees computes the current EIP-1559 base fee, tip, and normal-send fee cap. +func (m *Manager) currentFees(ctx context.Context) (feeQuote, error) { + var tip *big.Int if m.cfg.TipGwei > 0 { tip = gweiToWei(m.cfg.TipGwei) } else { + var err error tip, err = m.backend.SuggestGasTipCap(ctx) if err != nil { - return nil, nil, errors.Errorf("suggest gas tip: %w", err) + return feeQuote{}, errors.Errorf("suggest gas tip: %w", err) } } + if tip == nil || tip.Sign() < 0 { + return feeQuote{}, errors.New("gas tip must be non-negative") + } + tip = new(big.Int).Set(tip) head, err := m.backend.HeaderByNumber(ctx, nil) if err != nil { - return nil, nil, errors.Errorf("header by number: %w", err) + return feeQuote{}, errors.Errorf("header by number: %w", err) } - baseFee := head.BaseFee - if baseFee == nil { + var baseFee *big.Int + if head.BaseFee == nil { baseFee = new(big.Int) + } else { + baseFee = new(big.Int).Set(head.BaseFee) } - if m.cfg.MaxFeeGwei > 0 { - maxFee = gweiToWei(m.cfg.MaxFeeGwei) - } else { - // 2*baseFee + tip leaves headroom for one base-fee doubling between now and inclusion. - maxFee = new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), tip) + // 2*baseFee + tip leaves headroom for one base-fee doubling between now and inclusion. + maxFee := new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), tip) + if limit := m.normalFeeLimit(Request{}); limit != nil { + if maxFee.Cmp(limit) > 0 { + maxFee.Set(limit) + } } - if maxFee.Cmp(tip) < 0 { - maxFee = new(big.Int).Set(tip) + maxTip := new(big.Int).Sub(maxFee, baseFee) + if maxTip.Sign() < 0 { + return feeQuote{}, errors.Errorf("current base fee %s exceeds tx manager max fee %s", baseFee, maxFee) } - return tip, maxFee, nil + if tip.Cmp(maxTip) > 0 { + tip.Set(maxTip) + } + return feeQuote{baseFee: baseFee, tip: tip, maxFee: maxFee}, nil } func (m *Manager) estimateGas(ctx context.Context, req Request) (uint64, error) { @@ -238,6 +561,35 @@ func (m *Manager) estimateGas(ctx context.Context, req Request) (uint64, error) return gas + gas/5, nil } +func (m *Manager) signAndSend( + ctx context.Context, + nonce uint64, + to common.Address, + data []byte, + value *big.Int, + gas uint64, + fees feeQuote, +) (common.Hash, error) { + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: m.chainID, + Nonce: nonce, + GasTipCap: fees.tip, + GasFeeCap: fees.maxFee, + Gas: gas, + To: &to, + Value: value, + Data: data, + }) + signed, err := m.signer.SignTx(tx, m.chainID) + if err != nil { + return common.Hash{}, errors.Errorf("sign transaction: %w", err) + } + if err := m.backend.SendTransaction(ctx, signed); err != nil { + return common.Hash{}, err + } + return signed.Hash(), nil +} + // nextNonce returns the nonce to use, seeding or resyncing from the pending nonce when needed. func (m *Manager) nextNonce(ctx context.Context, resync bool) (uint64, error) { m.mu.Lock() @@ -261,41 +613,92 @@ func (m *Manager) commitNonce(used uint64) { } } -func (m *Manager) waitForReceipt(ctx context.Context, hash common.Hash) (*types.Receipt, error) { +func (m *Manager) waitForConfirmations( + ctx context.Context, + receipt *types.Receipt, + confirmations uint64, +) error { + if receipt == nil || receipt.BlockNumber == nil { + return errors.New("receipt block number is required") + } + if confirmations == 0 { + return nil + } ticker := time.NewTicker(m.cfg.PollInterval) defer ticker.Stop() - var receipt *types.Receipt for { - if receipt == nil { - r, err := m.backend.TransactionReceipt(ctx, hash) - if err == nil { - receipt = r - if receipt.Status == types.ReceiptStatusFailed { - return receipt, errors.Errorf("tx %s reverted on-chain", hash.Hex()) - } - } else if !errors.Is(err, ethereum.NotFound) { - return nil, errors.Errorf("receipt %s: %w", hash.Hex(), err) - } + head, err := m.backend.BlockNumber(ctx) + if err != nil { + return errors.Errorf("block number: %w", err) } - if receipt != nil { - head, err := m.backend.BlockNumber(ctx) - if err != nil { - return nil, errors.Errorf("block number: %w", err) - } - confirmed := receipt.BlockNumber.Uint64() + m.cfg.Confirmations - if head >= confirmed { - return receipt, nil - } + confirmed := receipt.BlockNumber.Uint64() + confirmations + if head >= confirmed { + return nil } select { case <-ctx.Done(): - return nil, ctx.Err() + return ctx.Err() case <-ticker.C: } } } +func cloneRequest(req Request) Request { + req.Data = append([]byte(nil), req.Data...) + if req.Value != nil { + req.Value = new(big.Int).Set(req.Value) + } + if req.MaxFeePerGas != nil { + req.MaxFeePerGas = new(big.Int).Set(req.MaxFeePerGas) + } + if req.Confirmations != nil { + confirmations := *req.Confirmations + req.Confirmations = &confirmations + } + return req +} + +func cloneFeeQuote(fees feeQuote) feeQuote { + return feeQuote{ + baseFee: new(big.Int).Set(fees.baseFee), + tip: new(big.Int).Set(fees.tip), + maxFee: new(big.Int).Set(fees.maxFee), + } +} + +func bumpFee(value *big.Int) *big.Int { + numerator := new(big.Int).Mul(value, big.NewInt(replacementBumpNumerator)) + numerator.Add(numerator, big.NewInt(replacementBumpDenominator-1)) + bumped := numerator.Div(numerator, big.NewInt(replacementBumpDenominator)) + if bumped.Cmp(value) <= 0 { + bumped.Add(value, big.NewInt(1)) + } + return bumped +} + +func reserveCancellationBump(limit *big.Int) *big.Int { + if limit == nil { + return nil + } + reserved := new(big.Int).Mul(limit, big.NewInt(replacementBumpDenominator)) + return reserved.Div(reserved, big.NewInt(replacementBumpNumerator)) +} + +func maxBig(a, b *big.Int) *big.Int { + if a.Cmp(b) >= 0 { + return new(big.Int).Set(a) + } + return new(big.Int).Set(b) +} + +func feeLimitString(limit *big.Int) string { + if limit == nil { + return "unbounded" + } + return limit.String() +} + func gweiToWei(gwei float64) *big.Int { wei, _ := new(big.Float).Mul(big.NewFloat(gwei), big.NewFloat(params.GWei)).Int(nil) return wei diff --git a/internal/txmanager/txmanager_anvil_test.go b/internal/txmanager/txmanager_anvil_test.go new file mode 100644 index 00000000..06466c5b --- /dev/null +++ b/internal/txmanager/txmanager_anvil_test.go @@ -0,0 +1,294 @@ +//go:build integration + +package txmanager + +import ( + "bytes" + "context" + "math/big" + "net" + "os/exec" + "strconv" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + "github.com/go-logr/logr" + + "github.com/symbioticfi/vault-solver/internal/signer" +) + +func TestAnvilTxManagerPendingLifecycle(t *testing.T) { + t.Run("fee bump replacement", testAnvilReplacement) + t.Run("timeout cancellation unblocks later nonce", testAnvilCancellation) +} + +func testAnvilReplacement(t *testing.T) { + rpcClient, ethClient := startAnvilWithoutMining(t) + sgnr := anvilSigner(t) + manager := New( + ethClient, + sgnr, + big.NewInt(31337), + Config{ + MaxFeeGwei: 100, + PollInterval: 20 * time.Millisecond, + ReplacementInterval: 200 * time.Millisecond, + PendingTimeout: 5 * time.Second, + }, + logr.Discard(), + ) + go manager.Start(t.Context()) + + result, accepted := manager.SendAsync(t.Context(), Request{ + To: common.HexToAddress("0x000000000000000000000000000000000000dEaD"), GasLimit: 21_000, Label: "replace", + }) + if !accepted { + t.Fatal("transaction was not accepted") + } + initial := waitForPoolTransaction(t, rpcClient, sgnr.Address(), 0, func(poolTransaction) bool { return true }) + replacement := waitForPoolTransaction(t, rpcClient, sgnr.Address(), 0, func(tx poolTransaction) bool { + return tx.Hash != initial.Hash + }) + if compareHexQuantity(replacement.MaxFeePerGas, initial.MaxFeePerGas) <= 0 || + compareHexQuantity(replacement.MaxPriorityFeePerGas, initial.MaxPriorityFeePerGas) <= 0 { + t.Fatalf( + "replacement fees did not increase: first=%s/%s replacement=%s/%s", + initial.MaxFeePerGas, + initial.MaxPriorityFeePerGas, + replacement.MaxFeePerGas, + replacement.MaxPriorityFeePerGas, + ) + } + + mineAnvilBlock(t, rpcClient) + got := waitForTxResult(t, result) + if got.Err != nil { + t.Fatalf("replacement result: %v", got.Err) + } + if !strings.EqualFold(got.Hash.Hex(), replacement.Hash) { + t.Fatalf("mined hash = %s, want replacement %s", got.Hash.Hex(), replacement.Hash) + } +} + +func testAnvilCancellation(t *testing.T) { + rpcClient, ethClient := startAnvilWithoutMining(t) + sgnr := anvilSigner(t) + manager := New( + ethClient, + sgnr, + big.NewInt(31337), + Config{ + MaxFeeGwei: 100, + PollInterval: 20 * time.Millisecond, + ReplacementInterval: 5 * time.Second, + PendingTimeout: 300 * time.Millisecond, + }, + logr.Discard(), + ) + go manager.Start(t.Context()) + + first, accepted := manager.SendAsync(t.Context(), Request{ + To: common.HexToAddress("0x000000000000000000000000000000000000dEaD"), + GasLimit: 21_000, + MaxFeePerGas: big.NewInt(3_000_000_000), + Label: "blocked", + }) + if !accepted { + t.Fatal("first transaction was not accepted") + } + second, accepted := manager.SendAsync(t.Context(), Request{ + To: common.HexToAddress("0x000000000000000000000000000000000000bEEF"), + GasLimit: 21_000, + Label: "later", + }) + if !accepted { + t.Fatal("second transaction was not accepted") + } + + waitForPoolTransaction(t, rpcClient, sgnr.Address(), 1, func(poolTransaction) bool { return true }) + cancellation := waitForPoolTransaction(t, rpcClient, sgnr.Address(), 0, func(tx poolTransaction) bool { + return strings.EqualFold(tx.To, sgnr.Address().Hex()) && tx.Input == "0x" && tx.Value == "0x0" + }) + if cancellation.Gas != "0x5208" { + t.Fatalf("cancellation gas = %s, want 0x5208", cancellation.Gas) + } + + mineAnvilBlock(t, rpcClient) + firstResult := waitForTxResult(t, first) + if firstResult.Err == nil || !strings.Contains(firstResult.Err.Error(), "cancelled at nonce 0") { + t.Fatalf("first result = %+v, want cancellation", firstResult) + } + if secondResult := waitForTxResult(t, second); secondResult.Err != nil { + t.Fatalf("later transaction remained blocked: %v", secondResult.Err) + } +} + +type poolTransaction struct { + Hash string `json:"hash"` + To string `json:"to"` + Value string `json:"value"` + Input string `json:"input"` + Gas string `json:"gas"` + MaxFeePerGas string `json:"maxFeePerGas"` + MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas"` +} + +type txPoolContent struct { + Pending map[string]map[string]poolTransaction `json:"pending"` + Queued map[string]map[string]poolTransaction `json:"queued"` +} + +func waitForPoolTransaction( + t *testing.T, + client *rpc.Client, + sender common.Address, + nonce uint64, + accept func(poolTransaction) bool, +) poolTransaction { + t.Helper() + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + tx, ok, err := poolTransactionAt(t.Context(), client, sender, nonce) + if err != nil { + t.Fatalf("txpool_content: %v", err) + } + if ok && accept(tx) { + return tx + } + time.Sleep(10 * time.Millisecond) + } + t.Fatalf("timed out waiting for sender %s nonce %d in txpool", sender.Hex(), nonce) + return poolTransaction{} +} + +func poolTransactionAt( + ctx context.Context, + client *rpc.Client, + sender common.Address, + nonce uint64, +) (poolTransaction, bool, error) { + var content txPoolContent + if err := client.CallContext(ctx, &content, "txpool_content"); err != nil { + return poolTransaction{}, false, err + } + nonceKey := strconv.FormatUint(nonce, 10) + for _, pool := range []map[string]map[string]poolTransaction{content.Pending, content.Queued} { + for address, transactions := range pool { + if !strings.EqualFold(address, sender.Hex()) { + continue + } + tx, ok := transactions[nonceKey] + return tx, ok, nil + } + } + return poolTransaction{}, false, nil +} + +func compareHexQuantity(left, right string) int { + leftValue, leftErr := hexutil.DecodeBig(left) + rightValue, rightErr := hexutil.DecodeBig(right) + if leftErr != nil || rightErr != nil { + return 0 + } + return leftValue.Cmp(rightValue) +} + +func mineAnvilBlock(t *testing.T, client *rpc.Client) { + t.Helper() + if err := client.CallContext(t.Context(), nil, "anvil_mine", 1); err != nil { + t.Fatalf("anvil_mine: %v", err) + } +} + +func waitForTxResult(t *testing.T, result <-chan Result) Result { + t.Helper() + select { + case got := <-result: + return got + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for transaction result") + return Result{} + } +} + +func anvilSigner(t *testing.T) signer.Signer { + t.Helper() + sgnr, err := signer.NewFromHexKey(testKey) + if err != nil { + t.Fatalf("signer: %v", err) + } + return sgnr +} + +func startAnvilWithoutMining(t *testing.T) (*rpc.Client, *ethclient.Client) { + t.Helper() + anvil, err := exec.LookPath("anvil") + if err != nil { + t.Skip("anvil is not installed") + } + listener, err := new(net.ListenConfig).Listen(t.Context(), "tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("reserve anvil port: %v", err) + } + port := listener.Addr().(*net.TCPAddr).Port + if err := listener.Close(); err != nil { + t.Fatalf("release anvil port: %v", err) + } + + var output bytes.Buffer + + cmd := exec.CommandContext( + t.Context(), + anvil, + "--no-mining", + "--silent", + "--chain-id", + "31337", + "--port", + strconv.Itoa(port), + ) + cmd.Stdout = &output + cmd.Stderr = &output + if err := cmd.Start(); err != nil { + t.Fatalf("start anvil: %v", err) + } + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + t.Cleanup(func() { + if cmd.Process != nil { + _ = cmd.Process.Kill() + } + select { + case <-done: + case <-time.After(time.Second): + } + }) + + url := "http://127.0.0.1:" + strconv.Itoa(port) + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + client, dialErr := rpc.DialContext(t.Context(), url) + if dialErr == nil { + var chainID string + if callErr := client.CallContext(t.Context(), &chainID, "eth_chainId"); callErr == nil { + ethClient := ethclient.NewClient(client) + t.Cleanup(ethClient.Close) + return client, ethClient + } + client.Close() + } + select { + case exitErr := <-done: + t.Fatalf("anvil exited during startup: %v\n%s", exitErr, output.String()) + default: + } + time.Sleep(20 * time.Millisecond) + } + t.Fatalf("anvil did not become ready:\n%s", output.String()) + return nil, nil +} diff --git a/internal/txmanager/txmanager_test.go b/internal/txmanager/txmanager_test.go index 497b0b9f..5358da1b 100644 --- a/internal/txmanager/txmanager_test.go +++ b/internal/txmanager/txmanager_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "math/big" + "strings" "sync" "testing" "time" @@ -148,6 +149,113 @@ func TestSend_HappyPath(t *testing.T) { } } +func TestMaxFeePerGasMatchesSendFeePolicy(t *testing.T) { + b := newMockBackend() + m, cancel := newTestManager(t, b) + defer cancel() + + fee, err := m.MaxFeePerGas(context.Background()) + if err != nil { + t.Fatalf("MaxFeePerGas: %v", err) + } + if fee.String() != "41000000000" { + t.Fatalf("max fee = %s, want 41000000000", fee) + } +} + +func TestMaxFeeGweiCapsDerivedFeeWithoutConsumingReplacementHeadroom(t *testing.T) { + b := newMockBackend() + m := New( + b, mustSigner(t), big.NewInt(11155111), + Config{MaxFeeGwei: 100, PollInterval: time.Millisecond}, + logr.Discard(), + ) + fee, err := m.MaxFeePerGas(t.Context()) + if err != nil { + t.Fatalf("MaxFeePerGas: %v", err) + } + if fee.String() != "41000000000" { + t.Fatalf("max fee = %s, want derived 41000000000 below the 100 gwei cap", fee) + } +} + +func TestMaxFeeGweiRejectsCurrentBaseFeeAboveCap(t *testing.T) { + b := newMockBackend() + m := New( + b, mustSigner(t), big.NewInt(11155111), + Config{MaxFeeGwei: 10, PollInterval: time.Millisecond}, + logr.Discard(), + ) + if _, err := m.MaxFeePerGas(t.Context()); err == nil { + t.Fatal("expected max fee cap below current base fee to fail") + } +} + +func TestSend_ClampsFeeToRequestCap(t *testing.T) { + b := newMockBackend() + m, cancel := newTestManager(t, b) + defer cancel() + + res := m.Send(context.Background(), Request{ + To: common.HexToAddress("0xabc"), GasLimit: 21_000, Label: "capped", + MaxFeePerGas: big.NewInt(40_000_000_000), + }) + if res.Err != nil { + t.Fatalf("send: %v", res.Err) + } + tx := b.lastSent() + if tx == nil { + t.Fatal("no transaction sent") + } + if tx.GasFeeCap().Cmp(big.NewInt(40_000_000_000)) != 0 { + t.Fatalf("gas fee cap = %s, want 40000000000", tx.GasFeeCap()) + } + if tx.GasTipCap().Cmp(big.NewInt(1_000_000_000)) != 0 { + t.Fatalf("gas tip cap = %s, want 1000000000", tx.GasTipCap()) + } +} + +func TestSend_ClampsTipToFitRequestCap(t *testing.T) { + b := newMockBackend() + m, cancel := newTestManager(t, b) + defer cancel() + + res := m.Send(context.Background(), Request{ + To: common.HexToAddress("0xabc"), GasLimit: 21_000, Label: "capped", + MaxFeePerGas: big.NewInt(20_500_000_000), + }) + if res.Err != nil { + t.Fatalf("send: %v", res.Err) + } + tx := b.lastSent() + if tx == nil { + t.Fatal("no transaction sent") + } + if tx.GasFeeCap().Cmp(big.NewInt(20_500_000_000)) != 0 { + t.Fatalf("gas fee cap = %s, want 20500000000", tx.GasFeeCap()) + } + if tx.GasTipCap().Cmp(big.NewInt(500_000_000)) != 0 { + t.Fatalf("gas tip cap = %s, want 500000000", tx.GasTipCap()) + } +} + +func TestSend_RejectsRequestCapBelowCurrentBaseFee(t *testing.T) { + b := newMockBackend() + m, cancel := newTestManager(t, b) + defer cancel() + + res := m.Send(context.Background(), Request{ + To: common.HexToAddress("0xabc"), GasLimit: 21_000, Label: "capped", + MaxFeePerGas: big.NewInt(19_000_000_000), + }) + if res.Err == nil { + t.Fatal("expected base-fee rejection") + } + if tx := b.lastSent(); tx != nil { + t.Fatalf("underpriced request sent transaction %s", tx.Hash()) + } +} + func TestSend_SequentialNoncesMonotonic(t *testing.T) { b := newMockBackend() m, cancel := newTestManager(t, b) @@ -164,6 +272,415 @@ func TestSend_SequentialNoncesMonotonic(t *testing.T) { } } +func TestSendAsyncBroadcastsSequentialNoncesBeforeConfirmations(t *testing.T) { + b := newMockBackend() + m := New( + b, mustSigner(t), big.NewInt(11155111), + Config{Confirmations: 2, PollInterval: time.Millisecond}, logr.Discard(), + ) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go m.Start(ctx) + + results := make([]<-chan Result, 0, 3) + for range 3 { + result, accepted := m.SendAsync( + context.Background(), Request{To: common.HexToAddress("0xabc"), GasLimit: 21_000, Label: "pipeline"}, + ) + if !accepted { + t.Fatal("SendAsync was not accepted") + } + results = append(results, result) + } + waitForSentTransactions(t, b, 3) + b.mu.Lock() + for i, tx := range b.sent { + if want := uint64(7 + i); tx.Nonce() != want { + b.mu.Unlock() + t.Fatalf("transaction %d nonce = %d, want %d", i, tx.Nonce(), want) + } + } + b.head = 102 + b.mu.Unlock() + for i, result := range results { + select { + case got := <-result: + if got.Err != nil { + t.Fatalf("result %d: %v", i, got.Err) + } + case <-time.After(time.Second): + t.Fatalf("timed out waiting for result %d", i) + } + } +} + +func TestSendAsyncCanCompleteAtInclusion(t *testing.T) { + b := newMockBackend() + m := New( + b, mustSigner(t), big.NewInt(11155111), + Config{Confirmations: 2, PollInterval: time.Millisecond}, logr.Discard(), + ) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go m.Start(ctx) + confirmations := uint64(0) + result, accepted := m.SendAsync(context.Background(), Request{ + To: common.HexToAddress("0xabc"), GasLimit: 21_000, Confirmations: &confirmations, Label: "inclusion", + }) + if !accepted { + t.Fatal("SendAsync was not accepted") + } + select { + case got := <-result: + if got.Err != nil || got.Receipt == nil || got.Receipt.BlockNumber.Uint64() != 100 { + t.Fatalf("result = %+v", got) + } + case <-time.After(time.Second): + t.Fatal("request did not complete at inclusion") + } +} + +func TestSendAsyncReplacesPendingTransactionWithHigherFees(t *testing.T) { + b := &replacementBackend{ + mockBackend: newMockBackend(), + receiptOnSameNonce: 2, + } + m := New( + b, mustSigner(t), big.NewInt(11155111), + Config{ + PollInterval: time.Millisecond, + ReplacementInterval: 2 * time.Millisecond, + PendingTimeout: time.Second, + }, + logr.Discard(), + ) + go m.Start(t.Context()) + + result, accepted := m.SendAsync(t.Context(), Request{ + To: common.HexToAddress("0xabc"), Data: []byte{0x01}, GasLimit: 21_000, Label: "replace", + }) + if !accepted { + t.Fatal("SendAsync was not accepted") + } + select { + case got := <-result: + if got.Err != nil { + t.Fatalf("replacement result: %v", got.Err) + } + case <-time.After(time.Second): + t.Fatal("replacement did not complete") + } + + b.mu.Lock() + defer b.mu.Unlock() + if len(b.sent) < 2 { + t.Fatalf("sent transactions = %d, want at least 2", len(b.sent)) + } + first, replacement := b.sent[0], b.sent[1] + if replacement.Nonce() != first.Nonce() || string(replacement.Data()) != string(first.Data()) { + t.Fatalf("replacement changed transaction: first=%+v replacement=%+v", first, replacement) + } + if replacement.GasFeeCapCmp(first) <= 0 || replacement.GasTipCapCmp(first) <= 0 { + t.Fatalf( + "replacement fees did not increase: first=%s/%s replacement=%s/%s", + first.GasFeeCap(), first.GasTipCap(), replacement.GasFeeCap(), replacement.GasTipCap(), + ) + } +} + +func TestFailedReplacementDoesNotAdvanceFeeState(t *testing.T) { + b := newMockBackend() + b.sendErrs = []error{errors.New("temporary broadcast failure")} + m := New( + b, mustSigner(t), big.NewInt(11155111), + Config{MaxFeeGwei: 100, PollInterval: time.Millisecond}, + logr.Discard(), + ) + original := feeQuote{ + baseFee: big.NewInt(20_000_000_000), + tip: big.NewInt(1_000_000_000), + maxFee: big.NewInt(41_000_000_000), + } + pending := &pendingTransaction{ + req: Request{To: common.HexToAddress("0xabc"), Label: "replace"}, + nonce: 7, + gas: 21_000, + value: new(big.Int), + fees: cloneFeeQuote(original), + } + + m.tryReplace(t.Context(), pending, false) + if pending.fees.maxFee.Cmp(original.maxFee) != 0 || pending.fees.tip.Cmp(original.tip) != 0 { + t.Fatalf("failed replacement advanced fees to %+v", pending.fees) + } + if len(pending.attempts) != 0 { + t.Fatalf("failed replacement attempts = %+v", pending.attempts) + } + + m.tryReplace(t.Context(), pending, false) + if len(pending.attempts) != 1 { + t.Fatalf("successful retry attempts = %+v", pending.attempts) + } + wantMaxFee := bumpFee(original.maxFee) + if pending.fees.maxFee.Cmp(wantMaxFee) != 0 { + t.Fatalf("successful retry max fee = %s, want first bump %s", pending.fees.maxFee, wantMaxFee) + } +} + +func TestNormalFeeLimitReservesOneCancellationBump(t *testing.T) { + b := newMockBackend() + b.baseFee = big.NewInt(30_000_000_000) + m := New( + b, mustSigner(t), big.NewInt(11155111), + Config{MaxFeeGwei: 50, PollInterval: time.Millisecond}, + logr.Discard(), + ) + + fees, err := m.currentFees(t.Context()) + if err != nil { + t.Fatalf("fees: %v", err) + } + normalLimit := reserveCancellationBump(gweiToWei(50)) + if fees.maxFee.Cmp(normalLimit) != 0 { + t.Fatalf("normal max fee = %s, want reserved limit %s", fees.maxFee, normalLimit) + } + cancellationFees, err := m.nextReplacementFees( + t.Context(), + feeQuote{baseFee: fees.baseFee, tip: fees.tip, maxFee: normalLimit}, + m.globalFeeLimit(), + ) + if err != nil { + t.Fatalf("cancellation fees: %v", err) + } + if cancellationFees.maxFee.Cmp(gweiToWei(50)) != 0 { + t.Fatalf("cancellation max fee = %s, want global cap %s", cancellationFees.maxFee, gweiToWei(50)) + } +} + +func TestPendingTimeoutCancelsBlockedNonceAndUnblocksLaterTransaction(t *testing.T) { + sgnr := mustSigner(t) + b := &replacementBackend{mockBackend: newMockBackend(), cancellationTo: sgnr.Address()} + m := New( + b, sgnr, big.NewInt(11155111), + Config{ + PollInterval: time.Millisecond, + ReplacementInterval: 2 * time.Millisecond, + PendingTimeout: 8 * time.Millisecond, + }, + logr.Discard(), + ) + go m.Start(t.Context()) + + first, accepted := m.SendAsync(t.Context(), Request{ + To: common.HexToAddress("0xabc"), + Data: []byte{0x01}, + GasLimit: 21_000, + MaxFeePerGas: big.NewInt(42_000_000_000), + Label: "blocked", + }) + if !accepted { + t.Fatal("first SendAsync was not accepted") + } + second, accepted := m.SendAsync(t.Context(), Request{ + To: common.HexToAddress("0xdef"), Data: []byte{0x02}, GasLimit: 21_000, Label: "later", + }) + if !accepted { + t.Fatal("second SendAsync was not accepted") + } + + select { + case got := <-first: + if got.Err == nil || !strings.Contains(got.Err.Error(), "cancelled at nonce 7") { + t.Fatalf("first result = %+v", got) + } + case <-time.After(time.Second): + t.Fatal("blocked transaction was not cancelled") + } + select { + case got := <-second: + if got.Err != nil { + t.Fatalf("later transaction result: %v", got.Err) + } + case <-time.After(time.Second): + t.Fatal("later nonce remained wedged") + } + cancellation := b.cancellationTransaction() + if cancellation == nil { + t.Fatal("same-nonce cancellation was not sent") + } + if cancellation.GasFeeCap().Cmp(big.NewInt(42_000_000_000)) <= 0 { + t.Fatalf("cancellation fee %s did not escape the fill profitability cap", cancellation.GasFeeCap()) + } +} + +func TestIncludedNonceDoesNotBlockLaterCancellationWhileConfirming(t *testing.T) { + b := newMockBackend() + m := New( + b, mustSigner(t), big.NewInt(11155111), + Config{ + Confirmations: 2, + MaxFeeGwei: 100, + PollInterval: time.Millisecond, + ReplacementInterval: time.Second, + PendingTimeout: time.Second, + }, + logr.Discard(), + ) + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: big.NewInt(11155111), Nonce: 7, GasTipCap: big.NewInt(1), GasFeeCap: big.NewInt(2), + Gas: 21_000, To: ptr(common.HexToAddress("0xabc")), + }) + b.receipts[tx.Hash()] = successfulReceipt(tx, b.head) + pending := &pendingTransaction{ + req: Request{Label: "confirming"}, + nonce: 7, + fees: feeQuote{baseFee: big.NewInt(1), tip: big.NewInt(1), maxFee: big.NewInt(2)}, + attempts: []txAttempt{{hash: tx.Hash()}}, + } + m.addUnminedNonce(7) + m.addUnminedNonce(8) + + result := make(chan Result, 1) + go func() { result <- m.waitForPendingTransaction(t.Context(), pending) }() + eventually(t, func() bool { return m.isLowestUnminedNonce(8) }) + select { + case got := <-result: + t.Fatalf("transaction completed before confirmations: %+v", got) + default: + } + + b.mu.Lock() + b.head = 102 + b.mu.Unlock() + if got := <-result; got.Err != nil { + t.Fatalf("confirmed result: %v", got.Err) + } +} + +func TestTransientReceiptErrorKeepsTrackingPendingTransaction(t *testing.T) { + b := &receiptErrorBackend{mockBackend: newMockBackend(), failures: 1} + m := New( + b, mustSigner(t), big.NewInt(11155111), + Config{ + MaxFeeGwei: 100, + PollInterval: time.Millisecond, + ReplacementInterval: time.Second, + PendingTimeout: time.Second, + }, + logr.Discard(), + ) + go m.Start(t.Context()) + + result, accepted := m.SendAsync(t.Context(), Request{ + To: common.HexToAddress("0xabc"), GasLimit: 21_000, Label: "receipt retry", + }) + if !accepted { + t.Fatal("transaction was not accepted") + } + if got := <-result; got.Err != nil { + t.Fatalf("receipt retry result: %v", got.Err) + } +} + +func waitForSentTransactions(t *testing.T, b *mockBackend, count int) { + t.Helper() + deadline := time.Now().Add(time.Second) + for time.Now().Before(deadline) { + b.mu.Lock() + sent := len(b.sent) + b.mu.Unlock() + if sent >= count { + return + } + time.Sleep(time.Millisecond) + } + t.Fatalf("timed out waiting for %d broadcasts", count) +} + +type receiptErrorBackend struct { + *mockBackend + + receiptMu sync.Mutex + failures int +} + +func (b *receiptErrorBackend) TransactionReceipt(ctx context.Context, hash common.Hash) (*types.Receipt, error) { + b.receiptMu.Lock() + if b.failures > 0 { + b.failures-- + b.receiptMu.Unlock() + return nil, errors.New("temporary receipt failure") + } + b.receiptMu.Unlock() + return b.mockBackend.TransactionReceipt(ctx, hash) +} + +type replacementBackend struct { + *mockBackend + + receiptOnSameNonce int + cancellationTo common.Address + sameNonceSends int + cancelled bool +} + +func (b *replacementBackend) SendTransaction(_ context.Context, tx *types.Transaction) error { + b.mu.Lock() + defer b.mu.Unlock() + b.sendCalls++ + b.sent = append(b.sent, tx) + + if b.isCancellation(tx) { + b.cancelled = true + b.receipts[tx.Hash()] = successfulReceipt(tx, b.head) + for _, sent := range b.sent { + if sent.Nonce() > tx.Nonce() { + b.receipts[sent.Hash()] = successfulReceipt(sent, b.head) + } + } + return nil + } + if tx.Nonce() == b.pendingNonce { + b.sameNonceSends++ + if b.receiptOnSameNonce > 0 && b.sameNonceSends >= b.receiptOnSameNonce { + b.receipts[tx.Hash()] = successfulReceipt(tx, b.head) + } + return nil + } + if b.cancelled { + b.receipts[tx.Hash()] = successfulReceipt(tx, b.head) + } + return nil +} + +func (b *replacementBackend) cancellationTransaction() *types.Transaction { + b.mu.Lock() + defer b.mu.Unlock() + for _, tx := range b.sent { + if b.isCancellation(tx) { + return tx + } + } + return nil +} + +func (b *replacementBackend) isCancellation(tx *types.Transaction) bool { + return b.cancellationTo != (common.Address{}) && + tx.To() != nil && + *tx.To() == b.cancellationTo && + len(tx.Data()) == 0 && + tx.Value().Sign() == 0 && + tx.Gas() == cancellationGasLimit +} + +func successfulReceipt(tx *types.Transaction, block uint64) *types.Receipt { + return &types.Receipt{ + Status: types.ReceiptStatusSuccessful, + TxHash: tx.Hash(), + BlockNumber: new(big.Int).SetUint64(block), + } +} + func TestSend_NonceTooLowResyncsAndRetries(t *testing.T) { b := newMockBackend() b.sendErrs = []error{errors.New("nonce too low")} // first send fails, second succeeds @@ -268,6 +785,36 @@ func TestSend_CallerCancelAfterEnqueueStillReturnsResult(t *testing.T) { } } +func TestTrySendRejectsWhileTransactionIsActive(t *testing.T) { + bb := &blockingBackend{mockBackend: newMockBackend(), entered: make(chan struct{}), release: make(chan struct{})} + m := New(bb, mustSigner(t), big.NewInt(11155111), Config{PollInterval: time.Millisecond}, logr.Discard()) + go m.Start(t.Context()) + + type tryResult struct { + result Result + accepted bool + } + first := make(chan tryResult, 1) + go func() { + result, accepted := m.TrySend( + context.Background(), Request{To: common.HexToAddress("0xabc"), GasLimit: 21_000, Label: "first"}, + ) + first <- tryResult{result: result, accepted: accepted} + }() + + <-bb.entered + if result, accepted := m.TrySend( + context.Background(), Request{To: common.HexToAddress("0xdef"), GasLimit: 21_000, Label: "second"}, + ); accepted || result.Err != nil { + t.Fatalf("busy TrySend = (%+v, %v), want not accepted", result, accepted) + } + close(bb.release) + got := <-first + if !got.accepted || got.result.Err != nil { + t.Fatalf("first TrySend = (%+v, %v)", got.result, got.accepted) + } +} + func mustSigner(t *testing.T) signer.Signer { t.Helper() s, err := signer.NewFromHexKey(testKey) @@ -276,3 +823,19 @@ func mustSigner(t *testing.T) signer.Signer { } return s } + +func ptr[T any](value T) *T { + return &value +} + +func eventually(t *testing.T, condition func() bool) { + t.Helper() + deadline := time.Now().Add(time.Second) + for time.Now().Before(deadline) { + if condition() { + return + } + time.Sleep(time.Millisecond) + } + t.Fatal("condition was not met") +} diff --git a/openapi/lifi-order.openapi.json b/openapi/lifi-order.openapi.json index 63afa11c..2bf7ef53 100644 --- a/openapi/lifi-order.openapi.json +++ b/openapi/lifi-order.openapi.json @@ -1,5 +1,5 @@ { - "openapi": "3.0.0", + "openapi": "3.1.0", "paths": { "/quote/request": { "post": { @@ -7,9 +7,9 @@ "parameters": [ { "name": "X-Integrator-Key", - "in": "header", - "description": "Raw integrator API key (obtained from integrator onboarding). When provided, integrator-specific quotes become eligible in addition to open-market quotes.", "required": false, + "in": "header", + "description": "Raw integrator key. Use a high-entropy random string. When provided, integrator-specific quotes become eligible in addition to open-market quotes.", "schema": { "type": "string" } @@ -101,7 +101,7 @@ "quotes": [ { "order": null, - "validUntil": 1777941876, + "validUntil": 1900000000, "quoteId": "quote_yCyE5aWW4NILo2UdM-8ETpia05TCLv", "preview": { "inputs": [ @@ -153,9 +153,9 @@ }, "summary": "Request quote", "tags": [ - "Quotes", "Bridge API" - ] + ], + "security": [] } }, "/quotes/submit": { @@ -183,7 +183,7 @@ "fromDecimals": 6, "toDecimals": 6, "exclusiveFor": "0x841F63697cFa0e3B54c4D42b3d679F07F7F2485f", - "expiry": 1777941876, + "expiry": 1900000000, "ranges": [ { "minAmount": "300010", @@ -212,7 +212,7 @@ "fromDecimals": 6, "toDecimals": 6, "exclusiveFor": "0x841F63697cFa0e3B54c4D42b3d679F07F7F2485f", - "expiry": 1777941876, + "expiry": 1900000000, "ranges": [ { "minAmount": "300010", @@ -297,7 +297,7 @@ "fromDecimals": 6, "toDecimals": 6, "exclusiveFor": "4Nd1mY9XzN6vfFh1Cm9wHQXgVxqQ8j8s8MoREvkLqJ7G", - "expiry": 1777941876, + "expiry": 1900000000, "ranges": [ { "minAmount": "1000000", @@ -366,7 +366,6 @@ ], "summary": "Submit quotes", "tags": [ - "Quotes", "Solver API" ] } @@ -413,7 +412,8 @@ "summary": "Get supported chains", "tags": [ "Bridge API" - ] + ], + "security": [] } }, "/api/v1/integrator/quote/request": { @@ -422,9 +422,9 @@ "parameters": [ { "name": "X-Integrator-Key", - "in": "header", - "description": "Raw integrator API key (obtained from integrator onboarding). When provided, integrator-specific quotes become eligible in addition to open-market quotes.", "required": false, + "in": "header", + "description": "Raw integrator key. Use a high-entropy random string. When provided, integrator-specific quotes become eligible in addition to open-market quotes.", "schema": { "type": "string" } @@ -558,7 +558,8 @@ "summary": "Request quote", "tags": [ "Bridge API v1" - ] + ], + "security": [] } }, "/orders/submit": { @@ -580,11 +581,11 @@ "inputSettler": "0x000001bf3F3175BD007f3889b50000c7006E72c0", "quoteId": "quote_kQAD6-AIP5AdHKTwPUlz-Ha6VYN31n", "order": { - "expires": 1942819670, + "expires": "1942819670", "user": "0x9773DAcbc46CAFb4e055060565e319922B48607D", "nonce": "1004", "originChainId": "84532", - "fillDeadline": 1942819670, + "fillDeadline": "1942819670", "inputOracle": "0xada1de62bE4F386346453A5b6F005BCdBE4515A1", "inputs": [ [ @@ -612,11 +613,11 @@ "orderType": "CatalystCompactOrder", "inputSettler": "0x000001bf3F3175BD007f3889b50000c7006E72c0", "order": { - "expires": 1942819670, + "expires": "1942819670", "user": "0x9773DAcbc46CAFb4e055060565e319922B48607D", "nonce": "1004", "originChainId": "84532", - "fillDeadline": 1942819670, + "fillDeadline": "1942819670", "inputOracle": "0xada1de62bE4F386346453A5b6F005BCdBE4515A1", "inputs": [ [ @@ -749,7 +750,8 @@ "summary": "Submit order", "tags": [ "Bridge API" - ] + ], + "security": [] } }, "/orders": { @@ -1036,7 +1038,8 @@ "summary": "Get orders", "tags": [ "Bridge API" - ] + ], + "security": [] } }, "/orders/status": { @@ -1048,7 +1051,7 @@ "name": "onChainOrderId", "required": false, "in": "query", - "description": "On chain order id propagated in the logs/events.", + "description": "On chain order id propagated in the logs/events. At least one of `onChainOrderId` or `catalystOrderId` must be provided.", "schema": { "example": "0xc7b44934463285434d1acc3405a6514e6677adfaae180ec042204d3cfc218d81", "type": "string" @@ -1058,7 +1061,7 @@ "name": "catalystOrderId", "required": false, "in": "query", - "description": "Internal order id returned by Lifi Intents API", + "description": "Internal order id returned by Lifi Intents API. At least one of `onChainOrderId` or `catalystOrderId` must be provided.", "schema": { "example": "intent_iU_VePNSu8ED3Y2WxEcQmA0GKxmJsE", "type": "string" @@ -1171,7 +1174,8 @@ "summary": "Get order status", "tags": [ "Bridge API" - ] + ], + "security": [] } }, "/solver-api/solver/identities": { @@ -2691,7 +2695,8 @@ "summary": "Get supported routes", "tags": [ "Bridge API" - ] + ], + "security": [] } }, "/api/v1/integrator/routes": { @@ -2821,7 +2826,8 @@ "summary": "Get supported routes", "tags": [ "Bridge API v1" - ] + ], + "security": [] } } }, @@ -2832,7 +2838,16 @@ "contact": {} }, "tags": [], - "servers": [], + "servers": [ + { + "url": "https://order.li.fi", + "description": "Production" + }, + { + "url": "https://order-dev.li.fi", + "description": "Development" + } + ], "components": { "securitySchemes": { "api-key": { @@ -2856,11 +2871,9 @@ "properties": { "intentType": { "type": "string", + "const": "oif-swap", "description": "Intent type", - "example": "oif-swap", - "enum": [ - "oif-swap" - ] + "example": "oif-swap" }, "inputs": { "type": "array", @@ -2965,7 +2978,7 @@ }, "minValidUntil": { "description": "Minimum validity timestamp in seconds", - "example": 1700000000, + "example": 1785154905, "type": "number" }, "preference": { @@ -3073,7 +3086,7 @@ "supportedTypes" ] }, - "QuotePreviewDto": { + "OifQuotePreviewDto": { "type": "object", "properties": { "inputs": { @@ -3128,10 +3141,12 @@ "type": "object", "properties": { "exclusiveFor": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Solver address with exclusivity on this quote, or null when no solver is exclusive", - "example": "0x1234567890123456789012345678901234567890", - "nullable": true + "example": "0x1234567890123456789012345678901234567890" } }, "required": [ @@ -3142,15 +3157,17 @@ "type": "object", "properties": { "order": { - "type": "object", - "description": "Order details (null for quote requests)", - "nullable": true, + "type": [ + "object", + "null" + ], + "description": "Order details; null for quote requests, provider-specific structure when populated", "example": null }, "validUntil": { "type": "number", "description": "Quote validity timestamp in seconds", - "example": 1700000000 + "example": 1900000000 }, "eta": { "type": "number", @@ -3171,7 +3188,7 @@ "description": "Informational amounts for UX/display", "allOf": [ { - "$ref": "#/components/schemas/QuotePreviewDto" + "$ref": "#/components/schemas/OifQuotePreviewDto" } ] }, @@ -3300,6 +3317,7 @@ "example": 6 }, "ranges": { + "maxItems": 1000, "type": "array", "items": { "type": "object", @@ -3339,14 +3357,14 @@ "quote" ] }, - "description": "Array of quote ranges with different price tiers" + "description": "Array of quote ranges with different price tiers. At most 1000 ranges per quote." }, "expiry": { "type": "integer", "minimum": 1000000000, "maximum": 4102444800, "description": "Expiry timestamp of the quote in seconds", - "example": 1672531200 + "example": 1900000000 }, "exclusiveFor": { "description": "Exclusive solver address allowed to fill this quote. EVM (eip155): 0x-prefixed 40-char hex. Solana: 32–44 char base58. Tron: base58check, T-prefixed, 34 chars.", @@ -3354,7 +3372,7 @@ "type": "string" }, "integratorKeyHash": { - "description": "Integrator key hash identifying the integrator this quote is tagged for. If provided, the quote will only be served to the specific integrator. If omitted, the quote is treated as an open-market quote available to all integrators.", + "description": "Integrator key hash this quote is tagged for. If provided, the quote will only be served to the specific integrator. If omitted, the quote is treated as an open-market quote available to all integrators.", "example": "a1b2c3d4e5f60000000000000000000000000000000000000000000000000000", "type": "string", "pattern": "^[a-f0-9]{64}$" @@ -3463,11 +3481,9 @@ "properties": { "intentType": { "type": "string", + "const": "oif-swap", "description": "Intent type (otherwise return quotes: [])", - "example": "oif-swap", - "enum": [ - "oif-swap" - ] + "example": "oif-swap" }, "inputs": { "type": "array", @@ -3493,13 +3509,20 @@ "example": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, "amount": { - "type": "string", "description": "Amount available. For exact-input: exact amount user will provide and is used for quoting. For exact-output: ignored by the quote decoder and not used for quoting", "example": "4000000000", - "nullable": true + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "lock": { - "description": "Optional lock reference for securing the input assets. Shape: { kind: \"the-compact\" | \"rhinestone\", params: object }. Currently ignored by the quote decoder." + "description": "Optional lock reference for securing the input assets. Shape: { kind: \"the-compact\" | \"rhinestone\", params: object }. Currently ignored by the quote decoder.", + "type": "object" } }, "required": [ @@ -3534,10 +3557,16 @@ "example": "0xdAC17F958D2ee523a2206206994597C13D831ec7" }, "amount": { - "type": "string", "description": "For exact-input: ignored by the quote decoder and not used for quoting. For exact-output: exact amount user wants to receive and is used for quoting", "example": "2000000000000000000", - "nullable": true + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "calldata": { "description": "Optional calldata describing how the receiver will consume the output. Enables composability with other protocols", @@ -3565,7 +3594,7 @@ }, "minValidUntil": { "description": "Minimum validity timestamp in unix timestamp (seconds). Only select solver quotes with longer TTL.", - "example": 1700000000, + "example": 1785154905, "type": "number" }, "preference": { @@ -3618,6 +3647,102 @@ } } ] + }, + "oracle": { + "description": "Accepted cross-chain verifier (oracle) contracts, each a { chain, address } object. When provided, only solvers that support one of these oracles can answer, and the returned order is built to settle against an accepted oracle. Omitted or empty means any oracle is acceptable. Ignored for same-chain swaps.", + "example": [ + { + "chain": "eip155:1", + "address": "0x0000003E06000007A224AeE90052fA6bb46d43C9" + } + ], + "maxItems": 50, + "type": "array", + "items": { + "type": "object", + "properties": { + "chain": { + "type": "string", + "minLength": 1, + "description": "CAIP-2 chain identifier, e.g. \"eip155:1\"", + "example": "eip155:1" + }, + "address": { + "type": "string", + "minLength": 1, + "description": "Native contract address for the chain", + "example": "0x0000003e06000007a224aee90052fa6bb46d43c9" + } + }, + "required": [ + "chain", + "address" + ] + } + }, + "inputSettler": { + "description": "Accepted input settler contracts, each a { chain, address } object. When provided, a quote is only returned if the order is built with one of these input settlers and the winning solver supports it. Omitted or empty means any input settler is acceptable.", + "example": [ + { + "chain": "eip155:1", + "address": "0x000025c3226C00B2Cdc200005a1600509f4e00C0" + } + ], + "maxItems": 50, + "type": "array", + "items": { + "type": "object", + "properties": { + "chain": { + "type": "string", + "minLength": 1, + "description": "CAIP-2 chain identifier, e.g. \"eip155:1\"", + "example": "eip155:1" + }, + "address": { + "type": "string", + "minLength": 1, + "description": "Native contract address for the chain", + "example": "0x0000003e06000007a224aee90052fa6bb46d43c9" + } + }, + "required": [ + "chain", + "address" + ] + } + }, + "outputSettler": { + "description": "Accepted output settler contracts, each a { chain, address } object. When provided, a quote is only returned if the order is built with one of these output settlers and the winning solver supports it. Omitted or empty means any output settler is acceptable.", + "example": [ + { + "chain": "eip155:1", + "address": "0x0000000000eC36B683C2E6AC89e9A75989C22a2e" + } + ], + "maxItems": 50, + "type": "array", + "items": { + "type": "object", + "properties": { + "chain": { + "type": "string", + "minLength": 1, + "description": "CAIP-2 chain identifier, e.g. \"eip155:1\"", + "example": "eip155:1" + }, + "address": { + "type": "string", + "minLength": 1, + "description": "Native contract address for the chain", + "example": "0x0000003e06000007a224aee90052fa6bb46d43c9" + } + }, + "required": [ + "chain", + "address" + ] + } } } } @@ -3646,143 +3771,562 @@ "supportedTypes" ] }, - "QuoteMetadataDto": { + "OpenIntentEvmTxDto": { "type": "object", "properties": { - "exclusiveFor": { - "type": "object", - "description": "Exclusive for address (hex32) - solver address that can fill this quote, or null", - "example": "0x1234567890123456789012345678901234567890", - "nullable": true + "chain": { + "type": "string", + "description": "CAIP-2 chain identifier for the destination contract", + "example": "eip155:1" + }, + "to": { + "type": "string", + "description": "Destination contract address (checksummed hex)", + "example": "0x1234567890123456789012345678901234567890" + }, + "data": { + "type": "string", + "description": "Transaction calldata as hex string", + "example": "0x095ea7b3000000000000000000000000..." + }, + "gasRequired": { + "type": "string", + "description": "Gas required for execution as a decimal string", + "example": "120000" } }, "required": [ - "exclusiveFor" + "chain", + "to", + "data", + "gasRequired" ] }, - "QuoteDto": { + "OpenIntentSvmTxDto": { "type": "object", "properties": { - "order": { - "description": "Order details", - "oneOf": [ - { - "$ref": "#/components/schemas/OifUserOpenIntentOrderDto" - }, - { - "$ref": "#/components/schemas/OifEscrowOrderDto" - }, - { - "$ref": "#/components/schemas/Oif3009OrderDto" - } - ] + "chain": { + "type": "string", + "description": "CAIP-2 chain identifier (Solana namespace)", + "example": "solana:1151111081099710" }, - "validUntil": { - "type": "number", - "description": "Quote validity timestamp in unix timestamp (seconds)", - "example": 1700000000 + "to": { + "type": "string", + "description": "Input settler program ID (base58)", + "example": "Amx9xngT2J5156cf1iMdF1BfJFwoDu8Je6Qv2zH14V5c" }, - "eta": { - "type": "number", - "description": "Estimated time of arrival in seconds", - "example": 8 + "data": { + "type": "string", + "description": "Base58-encoded serialized VersionedTransaction. The dummy all-zeros recentBlockhash must be replaced with a fresh blockhash before signing.", + "example": "Base58SerializedVersionedTx..." }, - "quoteId": { + "computeUnitsRequired": { "type": "string", - "description": "Unique quote identifier", - "example": "quote-123-abc" + "description": "Estimated compute units (decimal string)", + "example": "1200000" + } + }, + "required": [ + "chain", + "to", + "data", + "computeUnitsRequired" + ] + }, + "OpenIntentTronTxDto": { + "type": "object", + "properties": { + "chain": { + "type": "string", + "description": "CAIP-2 chain identifier (Tron namespace)", + "example": "tron:728126428" }, - "provider": { + "to": { "type": "string", - "description": "Provider identifier", - "enum": [ - "LI.FI Intent" - ], - "example": "LI.FI Intent" + "description": "Input settler contract address (base58check)", + "example": "TXabfeeRfzpZiK6wABb2KDB3Rbzte87x3o" }, - "preview": { - "description": "Informational amounts for UX/display, must be verified against the order", - "allOf": [ - { - "$ref": "#/components/schemas/QuotePreviewDto" - } - ] + "data": { + "type": "string", + "description": "Full ABI calldata (selector + args) as a 0x-prefixed hex string. Pass it as `data` (without the 0x prefix) to the fullnode HTTP endpoint wallet/triggersmartcontract, or with tronweb 6.x as `triggerSmartContract(to, \"\", { feeLimit, input: data }, [], owner)`.", + "example": "0x7515fd56000000000000000000000000..." }, - "failureHandling": { + "feeLimit": { "type": "string", - "description": "Failure handling policy for execution", - "enum": [ - "refund-automatic" - ], - "example": "refund-automatic" + "description": "Suggested fee_limit in SUN as a decimal string. A cap on energy spend, not an estimate.", + "example": "150000000" + } + }, + "required": [ + "chain", + "to", + "data", + "feeLimit" + ] + }, + "AllowanceCheckDto": { + "type": "object", + "properties": { + "chain": { + "type": "string", + "description": "CAIP-2 chain identifier for this allowance check (e.g., \"eip155:1\")", + "example": "eip155:1" }, - "partialFill": { - "type": "boolean", - "description": "Whether the quote supports partial fills", - "example": false + "token": { + "type": "string", + "description": "Native token address", + "example": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, - "metadata": { - "description": "Metadata for the order, potentially contains provider specific data", - "allOf": [ - { - "$ref": "#/components/schemas/QuoteMetadataDto" - } - ] + "user": { + "type": "string", + "description": "Native user address", + "example": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8" + }, + "spender": { + "type": "string", + "description": "Native spender address - InputSettlerEscrowLIFI", + "example": "0x1234567890123456789012345678901234567890" + }, + "required": { + "type": "string", + "description": "Required allowance amount as string-encoded integer", + "example": "1000000000" } }, "required": [ - "order", - "quoteId", - "provider", - "preview", - "failureHandling", - "partialFill", - "metadata" + "chain", + "token", + "user", + "spender", + "required" ] }, - "QuoteResponseDto": { + "ChecksDto": { "type": "object", "properties": { - "quotes": { - "description": "Array of generated quotes. List of available quotes, may be empty if no quotes are available", + "allowances": { + "description": "Required allowances and balances. Each item asserts that user has at least required balance and allowance for spender on token.", "type": "array", "items": { - "$ref": "#/components/schemas/QuoteDto" + "$ref": "#/components/schemas/AllowanceCheckDto" } } }, "required": [ - "quotes" + "allowances" ] }, - "SubmitOrderDto": { + "OifUserOpenIntentOrderDto": { "type": "object", "properties": { - "orderType": { - "default": "CatalystCompactOrder", - "description": "The type of the order", + "type": { "type": "string", + "description": "Order type identifier for user open intent execution", "enum": [ - "CatalystCompactOrder" - ] + "oif-user-open-v0" + ], + "example": "oif-user-open-v0" }, - "order": { - "type": "object", - "properties": { - "user": { - "description": "User address on source chain (initiator of the intent)", - "example": "0x", - "type": "string" - }, - "nonce": { - "description": "Nonce value of the intent", - "type": "string" + "openIntentTx": { + "description": "Open intent transaction. EVM produces hex calldata; Solana produces a base58 serialized VersionedTransaction whose recentBlockhash must be overwritten before signing; Tron produces hex calldata the client wraps in a TriggerSmartContract envelope.", + "oneOf": [ + { + "$ref": "#/components/schemas/OpenIntentEvmTxDto" }, - "originChainId": { - "description": "Origin chain ID (network id)", - "type": "string" + { + "$ref": "#/components/schemas/OpenIntentSvmTxDto" }, - "fillDeadline": { + { + "$ref": "#/components/schemas/OpenIntentTronTxDto" + } + ] + }, + "checks": { + "description": "Allowance and balance checks that must hold prior to execution. For Solana origins this array is empty; SPL transfers happen inside the open instruction.", + "allOf": [ + { + "$ref": "#/components/schemas/ChecksDto" + } + ] + } + }, + "required": [ + "type", + "openIntentTx", + "checks" + ] + }, + "Eip712PayloadDto": { + "type": "object", + "properties": { + "signatureType": { + "type": "string", + "description": "Signature type indicator", + "enum": [ + "eip712" + ], + "example": "eip712" + }, + "domain": { + "type": "object", + "description": "EIP-712 domain separator", + "example": { + "name": "Permit2", + "version": "1", + "chainId": 1, + "verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3" + } + }, + "primaryType": { + "type": "string", + "description": "Primary type name", + "example": "PermitBatchWitnessTransferFrom" + }, + "message": { + "type": "object", + "description": "The message object", + "example": {} + }, + "types": { + "type": "object", + "description": "EIP-712 types used to construct the digest", + "example": { + "EIP712Domain": [ + { + "name": "name", + "type": "string" + }, + { + "name": "version", + "type": "string" + }, + { + "name": "chainId", + "type": "uint256" + }, + { + "name": "verifyingContract", + "type": "address" + } + ] + } + } + }, + "required": [ + "signatureType", + "domain", + "primaryType", + "message", + "types" + ] + }, + "OifEscrowOrderDto": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Order type identifier for escrow-based execution", + "enum": [ + "oif-escrow-v0" + ], + "example": "oif-escrow-v0" + }, + "payload": { + "description": "EIP-712 payload for escrow order", + "allOf": [ + { + "$ref": "#/components/schemas/Eip712PayloadDto" + } + ] + } + }, + "required": [ + "type", + "payload" + ] + }, + "Oif3009OrderDto": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Order type identifier for EIP-3009 transfers", + "enum": [ + "oif-3009-v0" + ], + "example": "oif-3009-v0" + }, + "payload": { + "description": "EIP-3009 Transfer With Authorization typed data", + "allOf": [ + { + "$ref": "#/components/schemas/Eip712PayloadDto" + } + ] + }, + "metadata": { + "type": "object", + "description": "Additional metadata for nonce verification and order tracking", + "example": {} + } + }, + "required": [ + "type", + "payload", + "metadata" + ] + }, + "InputDto": { + "type": "object", + "properties": { + "chain": { + "type": "string", + "minLength": 1, + "description": "CAIP-2 chain identifier for this input (e.g., \"eip155:1\"). Applies to both user and asset.", + "example": "eip155:1" + }, + "user": { + "type": "string", + "minLength": 1, + "description": "Native address of the user providing the input assets", + "example": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8" + }, + "asset": { + "type": "string", + "minLength": 1, + "description": "Native address of the token/asset being provided as input", + "example": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + "amount": { + "description": "Amount available. For exact-input: exact amount user will provide and is used for quoting. For exact-output: ignored by the quote decoder and not used for quoting", + "example": "4000000000", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "lock": { + "description": "Optional lock reference for securing the input assets. Shape: { kind: \"the-compact\" | \"rhinestone\", params: object }. Currently ignored by the quote decoder.", + "type": "object" + } + }, + "required": [ + "chain", + "user", + "asset" + ] + }, + "OutputDto": { + "type": "object", + "properties": { + "chain": { + "type": "string", + "minLength": 1, + "description": "CAIP-2 chain identifier for this output (e.g., \"eip155:1\"). Applies to both receiver and asset.", + "example": "eip155:1" + }, + "receiver": { + "type": "string", + "minLength": 1, + "description": "Native address that will receive the output assets", + "example": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8" + }, + "asset": { + "type": "string", + "minLength": 1, + "description": "Native address of the token/asset to be received as output", + "example": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + "amount": { + "description": "For exact-input: ignored by the quote decoder and not used for quoting. For exact-output: exact amount user wants to receive and is used for quoting", + "example": "2000000000000000000", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "calldata": { + "description": "Optional calldata describing how the receiver will consume the output. Enables composability with other protocols", + "example": "0x095ea7b3...", + "type": "string" + } + }, + "required": [ + "chain", + "receiver", + "asset" + ] + }, + "QuotePreviewDto": { + "type": "object", + "properties": { + "inputs": { + "description": "Inputs for the preview", + "type": "array", + "items": { + "$ref": "#/components/schemas/InputDto" + } + }, + "outputs": { + "description": "Outputs for the preview", + "type": "array", + "items": { + "$ref": "#/components/schemas/OutputDto" + } + } + }, + "required": [ + "inputs", + "outputs" + ] + }, + "QuoteMetadataDto": { + "type": "object", + "properties": { + "exclusiveFor": { + "type": [ + "string", + "null" + ], + "description": "Exclusive for address (hex32) - solver address that can fill this quote, or null", + "example": "0x1234567890123456789012345678901234567890" + } + }, + "required": [ + "exclusiveFor" + ] + }, + "QuoteDto": { + "type": "object", + "properties": { + "order": { + "description": "Order details", + "oneOf": [ + { + "$ref": "#/components/schemas/OifUserOpenIntentOrderDto" + }, + { + "$ref": "#/components/schemas/OifEscrowOrderDto" + }, + { + "$ref": "#/components/schemas/Oif3009OrderDto" + } + ] + }, + "validUntil": { + "type": "number", + "description": "Quote validity timestamp in unix timestamp (seconds)", + "example": 1900000000 + }, + "eta": { + "type": "number", + "description": "Estimated time of arrival in seconds", + "example": 8 + }, + "quoteId": { + "type": "string", + "description": "Unique quote identifier", + "example": "quote-123-abc" + }, + "provider": { + "type": "string", + "description": "Provider identifier", + "enum": [ + "LI.FI Intent" + ], + "example": "LI.FI Intent" + }, + "preview": { + "description": "Informational amounts for UX/display, must be verified against the order", + "allOf": [ + { + "$ref": "#/components/schemas/QuotePreviewDto" + } + ] + }, + "failureHandling": { + "type": "string", + "description": "Failure handling policy for execution", + "enum": [ + "refund-automatic" + ], + "example": "refund-automatic" + }, + "partialFill": { + "type": "boolean", + "description": "Whether the quote supports partial fills", + "example": false + }, + "metadata": { + "description": "Metadata for the order, potentially contains provider specific data", + "allOf": [ + { + "$ref": "#/components/schemas/QuoteMetadataDto" + } + ] + } + }, + "required": [ + "order", + "quoteId", + "provider", + "preview", + "failureHandling", + "partialFill", + "metadata" + ] + }, + "QuoteResponseDto": { + "type": "object", + "properties": { + "quotes": { + "description": "Array of generated quotes. List of available quotes, may be empty if no quotes are available", + "type": "array", + "items": { + "$ref": "#/components/schemas/QuoteDto" + } + } + }, + "required": [ + "quotes" + ] + }, + "SubmitOrderDto": { + "type": "object", + "properties": { + "orderType": { + "default": "CatalystCompactOrder", + "description": "The type of the order", + "type": "string", + "enum": [ + "CatalystCompactOrder" + ] + }, + "order": { + "type": "object", + "properties": { + "user": { + "description": "User address on source chain (initiator of the intent)", + "example": "0x742d35cc6634c0532925a3b8d0c0e1c4c5c5c5c5", + "type": "string" + }, + "nonce": { + "description": "Nonce value of the intent", + "type": "string" + }, + "originChainId": { + "description": "Origin chain ID (network id)", + "type": "string" + }, + "fillDeadline": { "description": "Fill deadline of the intent in seconds", "type": "string" }, @@ -3841,21 +4385,35 @@ "type": "string" }, "callbackData": { - "type": "string", "description": "The remote call data", - "nullable": true + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "context": { - "type": "string", "description": "The fulfillment context", - "nullable": true + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, "required": [ "oracle", "settler", "token", - "recipient" + "amount", + "recipient", + "chainId" ] }, "description": "Array of output objects" @@ -3863,6 +4421,10 @@ }, "required": [ "user", + "nonce", + "originChainId", + "fillDeadline", + "expires", "inputOracle", "inputs", "outputs" @@ -3876,7 +4438,7 @@ }, "inputSettler": { "description": "Input settler address on source chain. Used to determine the type of order [escrow, compact] (mandatory for all orders)", - "example": "0x", + "example": "0x00000000000000447f2a2544c4c1d8c0e2a3b1c9", "type": "string" }, "sponsorSignature": { @@ -4002,6 +4564,127 @@ "outputs" ] }, + "SubmittedOrderQuoteDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Quote ID", + "example": "51" + }, + "createdAt": { + "type": "string", + "description": "Quote creation timestamp", + "example": "2025-10-16T11:51:59.426Z" + }, + "updatedAt": { + "type": "string", + "description": "Quote last update timestamp", + "example": "2025-10-16T11:57:14.716Z" + }, + "quoteId": { + "type": "string", + "description": "Unique quote identifier", + "example": "quote_kQAD6-AIP5AdHKTwPUlz-Ha6VYN31n" + }, + "fromChainNetworkId": { + "type": "string", + "description": "Source chain network ID", + "example": "84532" + }, + "toChainNetworkId": { + "type": "string", + "description": "Destination chain network ID", + "example": "11155111" + }, + "fromAssetAddress": { + "type": "string", + "description": "Source asset address", + "example": "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + }, + "toAssetAddress": { + "type": "string", + "description": "Destination asset address", + "example": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" + }, + "fromAssetDecimals": { + "type": "number", + "description": "Source asset decimals", + "example": 6 + }, + "toAssetDecimals": { + "type": "number", + "description": "Destination asset decimals", + "example": 6 + }, + "quote": { + "type": "string", + "description": "Quote rate", + "example": "0.98548764945417963" + }, + "inputAmount": { + "type": "string", + "description": "Input amount", + "example": "30380900" + }, + "outputAmount": { + "type": "string", + "description": "Output amount", + "example": "29940002" + }, + "expiry": { + "type": "string", + "description": "Quote expiry timestamp", + "example": "2026-05-05T00:44:36.000Z" + }, + "exclusiveFor": { + "type": [ + "string", + "null" + ], + "description": "Exclusive for address", + "example": "0x841F63697cFa0e3B54c4D42b3d679F07F7F2485f" + }, + "user": { + "type": "string", + "description": "Quote owner address", + "example": "0x9773DAcbc46CAFb4e055060565e319922B48607D" + }, + "orderId": { + "type": [ + "number", + "null" + ], + "description": "Associated order ID", + "example": 13 + }, + "solverId": { + "type": "number", + "description": "Solver ID", + "example": 1 + } + }, + "required": [ + "id", + "createdAt", + "updatedAt", + "quoteId", + "fromChainNetworkId", + "toChainNetworkId", + "fromAssetAddress", + "toAssetAddress", + "fromAssetDecimals", + "toAssetDecimals", + "quote", + "inputAmount", + "outputAmount", + "expiry", + "exclusiveFor", + "user", + "orderId", + "solverId" + ] + }, "OrderMetaDto": { "type": "object", "properties": { @@ -4039,82 +4722,108 @@ "example": "0x742d35Cc6634C0532925a3b8D0C0E1C4C5C5C5C5" }, "orderInitiatedTxHash": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Transaction hash when order was initiated (on-chain order) [eg: Open escrow event]", - "example": "0x1234567890abcdef1234567890abcdef12345678", - "nullable": true + "example": "0x1234567890abcdef1234567890abcdef12345678" }, "orderDeliveredTxHash": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Transaction hash of the OutputFilled event", - "example": "0x1234567890abcdef1234567890abcdef12345678", - "nullable": true + "example": "0x1234567890abcdef1234567890abcdef12345678" }, "orderVerifiedTxHash": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Transaction hash of the OutputProven event", - "example": "0x1234567890abcdef1234567890abcdef12345678", - "nullable": true + "example": "0x1234567890abcdef1234567890abcdef12345678" }, "orderSettledTxHash": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Transaction hash of the Finalised event", - "example": "0x1234567890abcdef1234567890abcdef12345678", - "nullable": true + "example": "0x1234567890abcdef1234567890abcdef12345678" }, "refundTxHash": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Transaction hash of the Refunded event", - "example": "0x1234567890abcdef1234567890abcdef12345678", - "nullable": true + "example": "0x1234567890abcdef1234567890abcdef12345678" }, "signedAt": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Date when the order was signed", - "example": "2024-01-01T00:00:00.000Z", - "nullable": true + "example": "2024-01-01T00:00:00.000Z" }, "expiredAt": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Date when the order expires", - "example": "2024-01-02T00:00:00.000Z", - "nullable": true + "example": "2024-01-02T00:00:00.000Z" }, "deliveredAt": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Date when the order was delivered", - "example": "2024-01-01T12:00:00.000Z", - "nullable": true + "example": "2024-01-01T12:00:00.000Z" }, "settledAt": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Date when the order was settled", - "example": "2024-01-01T18:00:00.000Z", - "nullable": true + "example": "2024-01-01T18:00:00.000Z" }, "refundedAt": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Date when the order was refunded", - "example": "2024-01-01T18:00:00.000Z", - "nullable": true + "example": "2024-01-01T18:00:00.000Z" }, "lastCompactDepositBlockNumber": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Last compact deposit block number", - "example": "12345678", - "nullable": true + "example": "12345678" }, "quoteId": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Quote ID associated with the order", - "example": "quote-123456", - "nullable": true + "example": "quote-123456" }, "solverAddress": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Solver address that filled the order", - "example": "0x742d35Cc6634C0532925a3b8D0C0E1C4C5C5C5C5", - "nullable": true + "example": "0x742d35Cc6634C0532925a3b8D0C0E1C4C5C5C5C5" }, "integratorKeyHash": { "type": "string", @@ -4155,25 +4864,34 @@ }, "quote": { "description": "The quote details", - "nullable": true, - "type": "object", - "allOf": [ + "anyOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/SubmittedOrderQuoteDto" + } + ] + }, { - "$ref": "#/components/schemas/QuoteResponseDto" + "type": "null" } ] }, "sponsorSignature": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Sponsor signature", - "example": null, - "nullable": true + "example": null }, "allocatorSignature": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Allocator signature", - "example": null, - "nullable": true + "example": null }, "inputSettler": { "type": "string", @@ -4379,34 +5097,44 @@ "example": "8965673" }, "exclusiveFor": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Exclusive for address", - "example": "0x841F63697cFa0e3B54c4D42b3d679F07F7F2485f", - "nullable": true + "example": "0x841F63697cFa0e3B54c4D42b3d679F07F7F2485f" }, "fromAssetRecordId": { - "type": "object", + "type": [ + "number", + "null" + ], "description": "Source asset record ID", - "example": 7, - "nullable": true + "example": 7 }, "toAssetRecordId": { - "type": "object", + "type": [ + "number", + "null" + ], "description": "Destination asset record ID", - "example": 8, - "nullable": true + "example": 8 }, "fromChainRecordId": { - "type": "object", + "type": [ + "number", + "null" + ], "description": "Source chain record ID", - "example": 11, - "nullable": true + "example": 11 }, "toChainRecordId": { - "type": "object", + "type": [ + "number", + "null" + ], "description": "Destination chain record ID", - "example": 12, - "nullable": true + "example": 12 }, "solverId": { "type": "number", @@ -4414,10 +5142,12 @@ "example": 2 }, "integratorKeyHash": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Integrator key hash this quote is tagged for, or null for open-market quotes", - "example": "a1b2c3d4e5f6...", - "nullable": true + "example": "a1b2c3d4e5f6..." } }, "required": [ @@ -4824,16 +5554,20 @@ "type": "object", "properties": { "symbol": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Token symbol (null if token not registered in system)", - "example": "USDC", - "nullable": true + "example": "USDC" }, "name": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Token name (null if token not registered in system)", - "example": "USDC", - "nullable": true + "example": "USDC" }, "address": { "type": "string", @@ -4892,28 +5626,36 @@ "example": 0 }, "fromChainRecordId": { - "type": "object", + "type": [ + "number", + "null" + ], "description": "Source chain record ID", - "example": 3, - "nullable": true + "example": 3 }, "toChainRecordId": { - "type": "object", + "type": [ + "number", + "null" + ], "description": "Destination chain record ID", - "example": 3, - "nullable": true + "example": 3 }, "fromTokenId": { - "type": "object", + "type": [ + "number", + "null" + ], "description": "Source token record ID", - "example": 5, - "nullable": true + "example": 5 }, "toTokenId": { - "type": "object", + "type": [ + "number", + "null" + ], "description": "Destination token record ID", - "example": 6, - "nullable": true + "example": 6 }, "isActive": { "type": "boolean", @@ -4922,21 +5664,31 @@ }, "fromChain": { "description": "Source chain information", - "nullable": true, - "type": "object", - "allOf": [ + "anyOf": [ { - "$ref": "#/components/schemas/RouteChainInfoDto" + "allOf": [ + { + "$ref": "#/components/schemas/RouteChainInfoDto" + } + ] + }, + { + "type": "null" } ] }, "toChain": { "description": "Destination chain information", - "nullable": true, - "type": "object", - "allOf": [ + "anyOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/RouteChainInfoDto" + } + ] + }, { - "$ref": "#/components/schemas/RouteChainInfoDto" + "type": "null" } ] }, @@ -5000,10 +5752,12 @@ "example": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" }, "symbol": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Token symbol (null if token not registered in system)", - "example": "USDC", - "nullable": true + "example": "USDC" }, "decimals": { "type": "number", @@ -5011,10 +5765,12 @@ "example": 6 }, "name": { - "type": "object", + "type": [ + "string", + "null" + ], "description": "Token name (null if token not registered in system)", - "example": "USDC", - "nullable": true + "example": "USDC" } }, "required": [