diff --git a/packages/sdk-router/src/abi/FastBridgeV2.json b/packages/sdk-router/src/abi/FastBridgeV2.json new file mode 100644 index 0000000000..12affdfe54 --- /dev/null +++ b/packages/sdk-router/src/abi/FastBridgeV2.json @@ -0,0 +1,1203 @@ +[ + { + "type": "constructor", + "inputs": [ + { "name": "defaultAdmin", "type": "address", "internalType": "address" } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "CANCELER_ROLE", + "inputs": [], + "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DEFAULT_ADMIN_ROLE", + "inputs": [], + "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DEFAULT_CANCEL_DELAY", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "DISPUTE_PERIOD", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "FEE_BPS", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "FEE_RATE_MAX", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "GOVERNOR_ROLE", + "inputs": [], + "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "GUARD_ROLE", + "inputs": [], + "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MAX_ZAP_DATA_LENGTH", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MIN_CANCEL_DELAY", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MIN_DEADLINE_PERIOD", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "NATIVE_GAS_TOKEN", + "inputs": [], + "outputs": [{ "name": "", "type": "address", "internalType": "address" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "PROVER_ROLE", + "inputs": [], + "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "QUOTER_ROLE", + "inputs": [], + "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "bridge", + "inputs": [ + { + "name": "params", + "type": "tuple", + "internalType": "struct IFastBridge.BridgeParams", + "components": [ + { + "name": "dstChainId", + "type": "uint32", + "internalType": "uint32" + }, + { "name": "sender", "type": "address", "internalType": "address" }, + { "name": "to", "type": "address", "internalType": "address" }, + { + "name": "originToken", + "type": "address", + "internalType": "address" + }, + { + "name": "destToken", + "type": "address", + "internalType": "address" + }, + { + "name": "originAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "destAmount", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "sendChainGas", "type": "bool", "internalType": "bool" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "bridgeProofs", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { "name": "timestamp", "type": "uint96", "internalType": "uint96" }, + { "name": "relayer", "type": "address", "internalType": "address" } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "bridgeRelayDetails", + "inputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "outputs": [ + { "name": "blockNumber", "type": "uint48", "internalType": "uint48" }, + { + "name": "blockTimestamp", + "type": "uint48", + "internalType": "uint48" + }, + { "name": "relayer", "type": "address", "internalType": "address" } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "bridgeRelays", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "bridgeStatuses", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "status", + "type": "uint8", + "internalType": "enum IFastBridgeV2.BridgeStatus" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "bridgeTxDetails", + "inputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "outputs": [ + { + "name": "status", + "type": "uint8", + "internalType": "enum IFastBridgeV2.BridgeStatus" + }, + { "name": "destChainId", "type": "uint32", "internalType": "uint32" }, + { + "name": "proofBlockTimestamp", + "type": "uint56", + "internalType": "uint56" + }, + { "name": "proofRelayer", "type": "address", "internalType": "address" } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "bridgeV2", + "inputs": [ + { + "name": "params", + "type": "tuple", + "internalType": "struct IFastBridge.BridgeParams", + "components": [ + { + "name": "dstChainId", + "type": "uint32", + "internalType": "uint32" + }, + { "name": "sender", "type": "address", "internalType": "address" }, + { "name": "to", "type": "address", "internalType": "address" }, + { + "name": "originToken", + "type": "address", + "internalType": "address" + }, + { + "name": "destToken", + "type": "address", + "internalType": "address" + }, + { + "name": "originAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "destAmount", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "sendChainGas", "type": "bool", "internalType": "bool" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" } + ] + }, + { + "name": "paramsV2", + "type": "tuple", + "internalType": "struct IFastBridgeV2.BridgeParamsV2", + "components": [ + { + "name": "quoteRelayer", + "type": "address", + "internalType": "address" + }, + { + "name": "quoteExclusivitySeconds", + "type": "int256", + "internalType": "int256" + }, + { "name": "quoteId", "type": "bytes", "internalType": "bytes" }, + { + "name": "zapNative", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "zapData", "type": "bytes", "internalType": "bytes" } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "canClaim", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "internalType": "bytes32" + }, + { "name": "relayer", "type": "address", "internalType": "address" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "cancelDelay", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "cancelV2", + "inputs": [{ "name": "request", "type": "bytes", "internalType": "bytes" }], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "chainGasAmount", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "claim", + "inputs": [ + { "name": "request", "type": "bytes", "internalType": "bytes" }, + { "name": "to", "type": "address", "internalType": "address" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "claimV2", + "inputs": [{ "name": "request", "type": "bytes", "internalType": "bytes" }], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "deployBlock", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "dispute", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getBridgeTransaction", + "inputs": [{ "name": "request", "type": "bytes", "internalType": "bytes" }], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct IFastBridge.BridgeTransaction", + "components": [ + { + "name": "originChainId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destChainId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "originSender", + "type": "address", + "internalType": "address" + }, + { + "name": "destRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "originToken", + "type": "address", + "internalType": "address" + }, + { + "name": "destToken", + "type": "address", + "internalType": "address" + }, + { + "name": "originAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "destAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "originFeeAmount", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "sendChainGas", "type": "bool", "internalType": "bool" }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "nonce", "type": "uint256", "internalType": "uint256" } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getBridgeTransactionV2", + "inputs": [{ "name": "request", "type": "bytes", "internalType": "bytes" }], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct IFastBridgeV2.BridgeTransactionV2", + "components": [ + { + "name": "originChainId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "destChainId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "originSender", + "type": "address", + "internalType": "address" + }, + { + "name": "destRecipient", + "type": "address", + "internalType": "address" + }, + { + "name": "originToken", + "type": "address", + "internalType": "address" + }, + { + "name": "destToken", + "type": "address", + "internalType": "address" + }, + { + "name": "originAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "destAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "originFeeAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "nonce", "type": "uint256", "internalType": "uint256" }, + { + "name": "exclusivityRelayer", + "type": "address", + "internalType": "address" + }, + { + "name": "exclusivityEndTime", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "zapNative", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "zapData", "type": "bytes", "internalType": "bytes" } + ] + } + ], + "stateMutability": "pure" + }, + { + "type": "function", + "name": "getRoleAdmin", + "inputs": [ + { "name": "role", "type": "bytes32", "internalType": "bytes32" } + ], + "outputs": [{ "name": "", "type": "bytes32", "internalType": "bytes32" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRoleMember", + "inputs": [ + { "name": "role", "type": "bytes32", "internalType": "bytes32" }, + { "name": "index", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "address", "internalType": "address" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getRoleMemberCount", + "inputs": [ + { "name": "role", "type": "bytes32", "internalType": "bytes32" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "grantRole", + "inputs": [ + { "name": "role", "type": "bytes32", "internalType": "bytes32" }, + { "name": "account", "type": "address", "internalType": "address" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "hasRole", + "inputs": [ + { "name": "role", "type": "bytes32", "internalType": "bytes32" }, + { "name": "account", "type": "address", "internalType": "address" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "multicallNoResults", + "inputs": [ + { "name": "data", "type": "bytes[]", "internalType": "bytes[]" }, + { "name": "ignoreReverts", "type": "bool", "internalType": "bool" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "multicallWithResults", + "inputs": [ + { "name": "data", "type": "bytes[]", "internalType": "bytes[]" }, + { "name": "ignoreReverts", "type": "bool", "internalType": "bool" } + ], + "outputs": [ + { + "name": "results", + "type": "tuple[]", + "internalType": "struct IMulticallTarget.Result[]", + "components": [ + { "name": "success", "type": "bool", "internalType": "bool" }, + { "name": "returnData", "type": "bytes", "internalType": "bytes" } + ] + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "nonce", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "protocolFeeRate", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "protocolFees", + "inputs": [{ "name": "", "type": "address", "internalType": "address" }], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "prove", + "inputs": [ + { "name": "request", "type": "bytes", "internalType": "bytes" }, + { "name": "destTxHash", "type": "bytes32", "internalType": "bytes32" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "proveV2", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "internalType": "bytes32" + }, + { "name": "destTxHash", "type": "bytes32", "internalType": "bytes32" }, + { "name": "relayer", "type": "address", "internalType": "address" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "refund", + "inputs": [{ "name": "request", "type": "bytes", "internalType": "bytes" }], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "relay", + "inputs": [{ "name": "request", "type": "bytes", "internalType": "bytes" }], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "relayV2", + "inputs": [ + { "name": "request", "type": "bytes", "internalType": "bytes" }, + { "name": "relayer", "type": "address", "internalType": "address" } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "renounceRole", + "inputs": [ + { "name": "role", "type": "bytes32", "internalType": "bytes32" }, + { + "name": "callerConfirmation", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "revokeRole", + "inputs": [ + { "name": "role", "type": "bytes32", "internalType": "bytes32" }, + { "name": "account", "type": "address", "internalType": "address" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "senderNonces", + "inputs": [{ "name": "", "type": "address", "internalType": "address" }], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setCancelDelay", + "inputs": [ + { + "name": "newCancelDelay", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setProtocolFeeRate", + "inputs": [ + { "name": "newFeeRate", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { "name": "interfaceId", "type": "bytes4", "internalType": "bytes4" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "sweepProtocolFees", + "inputs": [ + { "name": "token", "type": "address", "internalType": "address" }, + { "name": "recipient", "type": "address", "internalType": "address" } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "BridgeDepositClaimed", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "relayer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "BridgeDepositRefunded", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "BridgeProofDisputed", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "relayer", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "BridgeProofProvided", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "relayer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "transactionHash", + "type": "bytes32", + "indexed": false, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "BridgeQuoteDetails", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "quoteId", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "BridgeRelayed", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "relayer", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "originChainId", + "type": "uint32", + "indexed": false, + "internalType": "uint32" + }, + { + "name": "originToken", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "destToken", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "originAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "destAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "chainGasAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "BridgeRequested", + "inputs": [ + { + "name": "transactionId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "request", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + }, + { + "name": "destChainId", + "type": "uint32", + "indexed": false, + "internalType": "uint32" + }, + { + "name": "originToken", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "destToken", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "originAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "destAmount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "sendChainGas", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CancelDelayUpdated", + "inputs": [ + { + "name": "oldCancelDelay", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newCancelDelay", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FeeRateUpdated", + "inputs": [ + { + "name": "oldFeeRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "newFeeRate", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "FeesSwept", + "inputs": [ + { + "name": "token", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "recipient", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleAdminChanged", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "previousAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "newAdminRole", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleGranted", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "RoleRevoked", + "inputs": [ + { + "name": "role", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { "type": "error", "name": "AccessControlBadConfirmation", "inputs": [] }, + { + "type": "error", + "name": "AccessControlUnauthorizedAccount", + "inputs": [ + { "name": "account", "type": "address", "internalType": "address" }, + { "name": "neededRole", "type": "bytes32", "internalType": "bytes32" } + ] + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { "name": "target", "type": "address", "internalType": "address" } + ] + }, + { + "type": "error", + "name": "AddressInsufficientBalance", + "inputs": [ + { "name": "account", "type": "address", "internalType": "address" } + ] + }, + { "type": "error", "name": "AmountIncorrect", "inputs": [] }, + { + "type": "error", + "name": "BridgeTransactionV2__InvalidEncodedTx", + "inputs": [] + }, + { + "type": "error", + "name": "BridgeTransactionV2__UnsupportedVersion", + "inputs": [ + { "name": "version", "type": "uint16", "internalType": "uint16" } + ] + }, + { "type": "error", "name": "CancelDelayBelowMin", "inputs": [] }, + { "type": "error", "name": "ChainIncorrect", "inputs": [] }, + { "type": "error", "name": "DeadlineExceeded", "inputs": [] }, + { "type": "error", "name": "DeadlineNotExceeded", "inputs": [] }, + { "type": "error", "name": "DeadlineTooShort", "inputs": [] }, + { "type": "error", "name": "DisputePeriodNotPassed", "inputs": [] }, + { "type": "error", "name": "DisputePeriodPassed", "inputs": [] }, + { "type": "error", "name": "ExclusivityParamsIncorrect", "inputs": [] }, + { "type": "error", "name": "ExclusivityPeriodNotPassed", "inputs": [] }, + { "type": "error", "name": "FailedInnerCall", "inputs": [] }, + { "type": "error", "name": "FeeRateAboveMax", "inputs": [] }, + { "type": "error", "name": "MsgValueIncorrect", "inputs": [] }, + { + "type": "error", + "name": "MulticallTarget__UndeterminedRevert", + "inputs": [] + }, + { "type": "error", "name": "RecipientIncorrectReturnValue", "inputs": [] }, + { "type": "error", "name": "RecipientNoReturnValue", "inputs": [] }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { "name": "token", "type": "address", "internalType": "address" } + ] + }, + { "type": "error", "name": "SenderIncorrect", "inputs": [] }, + { "type": "error", "name": "StatusIncorrect", "inputs": [] }, + { "type": "error", "name": "TokenNotContract", "inputs": [] }, + { "type": "error", "name": "TransactionRelayed", "inputs": [] }, + { "type": "error", "name": "ZapDataLengthAboveMax", "inputs": [] }, + { "type": "error", "name": "ZapNativeNotSupported", "inputs": [] }, + { "type": "error", "name": "ZeroAddress", "inputs": [] } +] diff --git a/packages/sdk-router/src/abi/IDefaultActions.json b/packages/sdk-router/src/abi/IDefaultActions.json new file mode 100644 index 0000000000..e1727d609f --- /dev/null +++ b/packages/sdk-router/src/abi/IDefaultActions.json @@ -0,0 +1,56 @@ +[ + { + "type": "function", + "name": "addLiquidity", + "inputs": [ + { "name": "amounts", "type": "uint256[]", "internalType": "uint256[]" }, + { "name": "minToMint", "type": "uint256", "internalType": "uint256" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "deposit", + "inputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "removeLiquidityOneToken", + "inputs": [ + { "name": "tokenAmount", "type": "uint256", "internalType": "uint256" }, + { "name": "tokenIndex", "type": "uint8", "internalType": "uint8" }, + { "name": "minAmount", "type": "uint256", "internalType": "uint256" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "swap", + "inputs": [ + { "name": "tokenIndexFrom", "type": "uint8", "internalType": "uint8" }, + { "name": "tokenIndexTo", "type": "uint8", "internalType": "uint8" }, + { "name": "dx", "type": "uint256", "internalType": "uint256" }, + { "name": "minDy", "type": "uint256", "internalType": "uint256" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [ + { "name": "amountOut", "type": "uint256", "internalType": "uint256" } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { "name": "amount", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [], + "stateMutability": "nonpayable" + } +] diff --git a/packages/sdk-router/src/abi/IERC20Metadata.json b/packages/sdk-router/src/abi/IERC20Metadata.json new file mode 100644 index 0000000000..0e255f4ff1 --- /dev/null +++ b/packages/sdk-router/src/abi/IERC20Metadata.json @@ -0,0 +1,130 @@ +[ + { + "type": "function", + "name": "allowance", + "inputs": [ + { "name": "owner", "type": "address", "internalType": "address" }, + { "name": "spender", "type": "address", "internalType": "address" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "approve", + "inputs": [ + { "name": "spender", "type": "address", "internalType": "address" }, + { "name": "value", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { "name": "account", "type": "address", "internalType": "address" } + ], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "decimals", + "inputs": [], + "outputs": [{ "name": "", "type": "uint8", "internalType": "uint8" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [{ "name": "", "type": "string", "internalType": "string" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "symbol", + "inputs": [], + "outputs": [{ "name": "", "type": "string", "internalType": "string" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "totalSupply", + "inputs": [], + "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transfer", + "inputs": [ + { "name": "to", "type": "address", "internalType": "address" }, + { "name": "value", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "transferFrom", + "inputs": [ + { "name": "from", "type": "address", "internalType": "address" }, + { "name": "to", "type": "address", "internalType": "address" }, + { "name": "value", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [{ "name": "", "type": "bool", "internalType": "bool" }], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Approval", + "inputs": [ + { + "name": "owner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "spender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Transfer", + "inputs": [ + { + "name": "from", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + } +] diff --git a/packages/sdk-router/src/abi/SynapseIntentPreviewer.json b/packages/sdk-router/src/abi/SynapseIntentPreviewer.json new file mode 100644 index 0000000000..92dcb08707 --- /dev/null +++ b/packages/sdk-router/src/abi/SynapseIntentPreviewer.json @@ -0,0 +1,57 @@ +[ + { + "type": "function", + "name": "NATIVE_GAS_TOKEN", + "inputs": [], + "outputs": [{ "name": "", "type": "address", "internalType": "address" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "previewIntent", + "inputs": [ + { "name": "swapQuoter", "type": "address", "internalType": "address" }, + { "name": "forwardTo", "type": "address", "internalType": "address" }, + { "name": "slippageWei", "type": "uint256", "internalType": "uint256" }, + { "name": "tokenIn", "type": "address", "internalType": "address" }, + { "name": "tokenOut", "type": "address", "internalType": "address" }, + { "name": "amountIn", "type": "uint256", "internalType": "uint256" } + ], + "outputs": [ + { "name": "amountOut", "type": "uint256", "internalType": "uint256" }, + { + "name": "steps", + "type": "tuple[]", + "internalType": "struct ISynapseIntentRouter.StepParams[]", + "components": [ + { "name": "token", "type": "address", "internalType": "address" }, + { "name": "amount", "type": "uint256", "internalType": "uint256" }, + { + "name": "msgValue", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "zapData", "type": "bytes", "internalType": "bytes" } + ] + } + ], + "stateMutability": "view" + }, + { "type": "error", "name": "SIP__NoOpForwardNotSupported", "inputs": [] }, + { "type": "error", "name": "SIP__PoolTokenMismatch", "inputs": [] }, + { "type": "error", "name": "SIP__PoolZeroAddress", "inputs": [] }, + { "type": "error", "name": "SIP__RawParamsEmpty", "inputs": [] }, + { "type": "error", "name": "SIP__TokenNotNative", "inputs": [] }, + { + "type": "error", + "name": "ZapDataV1__FinalTokenNotSpecified", + "inputs": [] + }, + { "type": "error", "name": "ZapDataV1__InvalidEncoding", "inputs": [] }, + { + "type": "error", + "name": "ZapDataV1__PayloadLengthAboveMax", + "inputs": [] + }, + { "type": "error", "name": "ZapDataV1__TargetZeroAddress", "inputs": [] } +] diff --git a/packages/sdk-router/src/abi/SynapseIntentRouter.json b/packages/sdk-router/src/abi/SynapseIntentRouter.json new file mode 100644 index 0000000000..ddee817997 --- /dev/null +++ b/packages/sdk-router/src/abi/SynapseIntentRouter.json @@ -0,0 +1,98 @@ +[ + { + "type": "function", + "name": "NATIVE_GAS_TOKEN", + "inputs": [], + "outputs": [{ "name": "", "type": "address", "internalType": "address" }], + "stateMutability": "view" + }, + { + "type": "function", + "name": "completeIntent", + "inputs": [ + { + "name": "zapRecipient", + "type": "address", + "internalType": "address" + }, + { "name": "amountIn", "type": "uint256", "internalType": "uint256" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" }, + { + "name": "steps", + "type": "tuple[]", + "internalType": "struct ISynapseIntentRouter.StepParams[]", + "components": [ + { "name": "token", "type": "address", "internalType": "address" }, + { "name": "amount", "type": "uint256", "internalType": "uint256" }, + { + "name": "msgValue", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "zapData", "type": "bytes", "internalType": "bytes" } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "completeIntentWithBalanceChecks", + "inputs": [ + { + "name": "zapRecipient", + "type": "address", + "internalType": "address" + }, + { "name": "amountIn", "type": "uint256", "internalType": "uint256" }, + { "name": "deadline", "type": "uint256", "internalType": "uint256" }, + { + "name": "steps", + "type": "tuple[]", + "internalType": "struct ISynapseIntentRouter.StepParams[]", + "components": [ + { "name": "token", "type": "address", "internalType": "address" }, + { "name": "amount", "type": "uint256", "internalType": "uint256" }, + { + "name": "msgValue", + "type": "uint256", + "internalType": "uint256" + }, + { "name": "zapData", "type": "bytes", "internalType": "bytes" } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "error", + "name": "AddressEmptyCode", + "inputs": [ + { "name": "target", "type": "address", "internalType": "address" } + ] + }, + { + "type": "error", + "name": "AddressInsufficientBalance", + "inputs": [ + { "name": "account", "type": "address", "internalType": "address" } + ] + }, + { "type": "error", "name": "FailedInnerCall", "inputs": [] }, + { "type": "error", "name": "SIR__DeadlineExceeded", "inputs": [] }, + { "type": "error", "name": "SIR__MsgValueIncorrect", "inputs": [] }, + { "type": "error", "name": "SIR__StepsNotProvided", "inputs": [] }, + { "type": "error", "name": "SIR__TokenNotContract", "inputs": [] }, + { "type": "error", "name": "SIR__UnspentFunds", "inputs": [] }, + { "type": "error", "name": "SIR__ZapIncorrectReturnValue", "inputs": [] }, + { "type": "error", "name": "SIR__ZapNoReturnValue", "inputs": [] }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { "name": "token", "type": "address", "internalType": "address" } + ] + } +] diff --git a/packages/sdk-router/src/constants/addresses.ts b/packages/sdk-router/src/constants/addresses.ts index 503b576b5a..ed0f86e734 100644 --- a/packages/sdk-router/src/constants/addresses.ts +++ b/packages/sdk-router/src/constants/addresses.ts @@ -1,6 +1,7 @@ import { CCTP_SUPPORTED_CHAIN_IDS, RFQ_SUPPORTED_CHAIN_IDS, + INTENTS_SUPPORTED_CHAIN_IDS, SUPPORTED_CHAIN_IDS, SupportedChainId, } from './chainIds' @@ -64,3 +65,69 @@ export const FAST_BRIDGE_ROUTER_ADDRESS_MAP: AddressMap = generateAddressMap( FAST_BRIDGE_ROUTER_ADDRESS, FAST_BRIDGE_ROUTER_EXCEPTION_MAP ) + +/** + * FastBridgeV2 contract address for all chains except ones from FAST_BRIDGE_V2_EXCEPTION_MAP. + * TODO: this is a staging FastBridgeV2 deployment, update to the production deployment when ready. + */ +const FAST_BRIDGE_V2_ADDRESS = '0xEb1eb846342274d5d652e068DD189EfCEd256332' +const FAST_BRIDGE_V2_EXCEPTION_MAP: AddressMap = {} +export const FAST_BRIDGE_V2_ADDRESS_MAP: AddressMap = generateAddressMap( + INTENTS_SUPPORTED_CHAIN_IDS, + FAST_BRIDGE_V2_ADDRESS, + FAST_BRIDGE_V2_EXCEPTION_MAP +) + +/** + * TokenZapV1 contract address for all chains except ones from TOKEN_ZAP_V1_EXCEPTION_MAP. + * TODO: this is a staging TokenZapV1 deployment, update to the production deployment when ready. + */ +const TOKEN_ZAP_V1_ADDRESS = '0x6C6FA1cE8160bb680f7a1dd2068c7302bA2a9eaB' +const TOKEN_ZAP_V1_EXCEPTION_MAP: AddressMap = {} +export const TOKEN_ZAP_V1_ADDRESS_MAP: AddressMap = generateAddressMap( + INTENTS_SUPPORTED_CHAIN_IDS, + TOKEN_ZAP_V1_ADDRESS, + TOKEN_ZAP_V1_EXCEPTION_MAP +) + +/** + * SynapseIntentRouter contract address for all chains except ones from SYNAPSE_INTENT_ROUTER_EXCEPTION_MAP. + * TODO: this is a staging SynapseIntentRouter deployment, update to the production deployment when ready. + */ +const SYNAPSE_INTENT_ROUTER_ADDRESS = + '0x018396706193B16F8a1b20B87B2dcC840979D7EA' +const SYNAPSE_INTENT_ROUTER_EXCEPTION_MAP: AddressMap = {} +export const SYNAPSE_INTENT_ROUTER_ADDRESS_MAP: AddressMap = generateAddressMap( + INTENTS_SUPPORTED_CHAIN_IDS, + SYNAPSE_INTENT_ROUTER_ADDRESS, + SYNAPSE_INTENT_ROUTER_EXCEPTION_MAP +) + +/** + * SynapseIntentPreviewer contract address for all chains except ones from SYNAPSE_INTENT_PREVIEWER_EXCEPTION_MAP. + * TODO: this is a staging SynapseIntentPreviewer deployment, update to the production deployment when ready. + */ +const SYNAPSE_INTENT_PREVIEWER_ADDRESS = + '0xc542Df6aA3813b49EE8A5A07f82eA0EaAa006bFa' +const SYNAPSE_INTENT_PREVIEWER_EXCEPTION_MAP: AddressMap = {} +export const SYNAPSE_INTENT_PREVIEWER_ADDRESS_MAP: AddressMap = + generateAddressMap( + INTENTS_SUPPORTED_CHAIN_IDS, + SYNAPSE_INTENT_PREVIEWER_ADDRESS, + SYNAPSE_INTENT_PREVIEWER_EXCEPTION_MAP + ) + +/** + * SwapQuoterV2 contract address on Ethereum. Addresses for other chains are defined in the + * SWAP_QUOTER_V2_EXCEPTION_MAP. + */ +const SWAP_QUOTER_V2_ADDRESS = '0x5682dC851C33adb48F6958a963A5d3Aa31F6f184' +const SWAP_QUOTER_V2_EXCEPTION_MAP: AddressMap = { + [SupportedChainId.ARBITRUM]: '0xE402cC7826dD835FCe5E3cFb61D56703fEbc2642', + [SupportedChainId.OPTIMISM]: '0xd6Bdb96b356F4F51bf491297DF03F25DCd0cBf6D', +} +export const SWAP_QUOTER_V2_ADDRESS_MAP: AddressMap = generateAddressMap( + INTENTS_SUPPORTED_CHAIN_IDS, + SWAP_QUOTER_V2_ADDRESS, + SWAP_QUOTER_V2_EXCEPTION_MAP +) diff --git a/packages/sdk-router/src/constants/chainIds.ts b/packages/sdk-router/src/constants/chainIds.ts index 041705a69e..e422facfe6 100644 --- a/packages/sdk-router/src/constants/chainIds.ts +++ b/packages/sdk-router/src/constants/chainIds.ts @@ -78,6 +78,15 @@ export const RFQ_SUPPORTED_CHAIN_IDS: number[] = [ SupportedChainId.SCROLL, ].filter((chainId) => !PAUSED_CHAIN_IDS.includes(chainId)) +/** + * List of chain ids where SynapseIntentRouter is deployed, ordered by chain id. + * Note: this is currently serving as an entry point for FastBridgeV2, but will support other modules in the future. + */ +export const INTENTS_SUPPORTED_CHAIN_IDS: number[] = [ + SupportedChainId.OPTIMISM, + SupportedChainId.ARBITRUM, +].filter((chainId) => !PAUSED_CHAIN_IDS.includes(chainId)) + /** * List of chain ids where hydrating on constructor is supported , ordered by monke * diff --git a/packages/sdk-router/src/module/query.ts b/packages/sdk-router/src/module/query.ts index 1e382d1154..a4083cb526 100644 --- a/packages/sdk-router/src/module/query.ts +++ b/packages/sdk-router/src/module/query.ts @@ -142,15 +142,24 @@ export const applySlippageToQuery = ( slipNumerator <= slipDenominator, 'Slippage cannot be greater than 1' ) - const slippageAmount = query.minAmountOut - .mul(slipNumerator) - .div(slipDenominator) return { ...query, - minAmountOut: query.minAmountOut.sub(slippageAmount), + minAmountOut: applySlippage( + query.minAmountOut, + slipNumerator, + slipDenominator + ), } } +export const applySlippage = ( + amount: BigNumber, + slipNumerator: number, + slipDenominator: number +): BigNumber => { + return amount.sub(amount.mul(slipNumerator).div(slipDenominator)) +} + /** * Creates a Query object for a no-swap bridge action. * @@ -158,7 +167,10 @@ export const applySlippageToQuery = ( * @param amount - The amount of token to bridge. * @returns The Query object for a no-swap bridge action. */ -export const createNoSwapQuery = (token: string, amount: BigNumber): Query => { +export const createNoSwapQuery = ( + token: string, + amount: BigNumber +): CCTPRouterQuery => { return { routerAdapter: AddressZero, tokenOut: token, diff --git a/packages/sdk-router/src/module/synapseModuleSet.ts b/packages/sdk-router/src/module/synapseModuleSet.ts index 723b7998ee..c3d64b3e9f 100644 --- a/packages/sdk-router/src/module/synapseModuleSet.ts +++ b/packages/sdk-router/src/module/synapseModuleSet.ts @@ -6,6 +6,7 @@ import { BigintIsh } from '../constants' import { BridgeQuote, BridgeRoute, FeeConfig } from './types' import { SynapseModule } from './synapseModule' import { applyOptionalDeadline } from '../utils/deadlines' +import { isSameAddress } from '../utils/addressUtils' import { Query } from './query' export abstract class SynapseModuleSet { @@ -70,10 +71,7 @@ export abstract class SynapseModuleSet { moduleAddress: string ): SynapseModule | undefined { const module = this.getModule(chainId) - if (module?.address.toLowerCase() === moduleAddress.toLowerCase()) { - return module - } - return undefined + return isSameAddress(module?.address, moduleAddress) ? module : undefined } /** @@ -214,7 +212,7 @@ export abstract class SynapseModuleSet { feeAmount, feeConfig, routerAddress: originModule.address, - maxAmountOut: destQuery.minAmountOut, + maxAmountOut: bridgeRoute.destAmountOut, originQuery, destQuery, estimatedTime: this.getEstimatedTime(bridgeRoute.originChainId), diff --git a/packages/sdk-router/src/module/types.ts b/packages/sdk-router/src/module/types.ts index 3a988a5185..4f5cc6cbb7 100644 --- a/packages/sdk-router/src/module/types.ts +++ b/packages/sdk-router/src/module/types.ts @@ -76,7 +76,10 @@ export type BridgeRoute = { originChainId: number destChainId: number originQuery: Query + originAmountOut: BigNumber destQuery: Query + destAmountIn: BigNumber + destAmountOut: BigNumber bridgeToken: BridgeToken bridgeModuleName: string } diff --git a/packages/sdk-router/src/rfq/api.integration.test.ts b/packages/sdk-router/src/rfq/api.integration.test.ts index a041d98b98..9d8c04d221 100644 --- a/packages/sdk-router/src/rfq/api.integration.test.ts +++ b/packages/sdk-router/src/rfq/api.integration.test.ts @@ -1,14 +1,67 @@ -import { getAllQuotes } from './api' +import { parseFixed } from '@ethersproject/bignumber' + +import { getAllQuotes, getBestRelayerQuote, RelayerQuote } from './api' +import { Ticker } from './ticker' +import { ETH_NATIVE_TOKEN_ADDRESS } from '../utils/handleNativeToken' global.fetch = require('node-fetch') // Retry the flaky tests up to 3 times jest.retryTimes(3) -describe('getAllQuotes', () => { - it('Integration test', async () => { +describe('Integration test: getAllQuotes', () => { + it('returns a non-empty array', async () => { const result = await getAllQuotes() // console.log('Current quotes: ' + JSON.stringify(result, null, 2)) expect(result.length).toBeGreaterThan(0) }) }) + +describe('Integration test: getBestRelayerQuote', () => { + const ticker: Ticker = { + originToken: { + chainId: 42161, + token: ETH_NATIVE_TOKEN_ADDRESS, + }, + destToken: { + chainId: 10, + token: ETH_NATIVE_TOKEN_ADDRESS, + }, + } + + describe('Cases where a non-zero quote is returned', () => { + it('ARB ETH -> OP ETH; 0.01 ETH', async () => { + const result = await getBestRelayerQuote(ticker, parseFixed('0.01', 18)) + expect(result?.destAmount.gt(0)).toBe(true) + expect(result?.relayerAddress).toBeDefined() + }) + }) + + describe('Cases where a zero quote is returned', () => { + const quoteZero: RelayerQuote = { + destAmount: parseFixed('0'), + } + + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { + // Do nothing + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('ARB ETH -> OP ETH; 1337 wei', async () => { + const result = await getBestRelayerQuote(ticker, parseFixed('1337', 18)) + expect(result).toEqual(quoteZero) + expect(console.error).toHaveBeenCalled() + }) + + it('ARB ETH -> OP ETH; 10**36 wei', async () => { + const result = await getBestRelayerQuote(ticker, parseFixed('1', 36)) + expect(result).toEqual(quoteZero) + expect(console.error).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/sdk-router/src/rfq/api.test.ts b/packages/sdk-router/src/rfq/api.test.ts index 7960f6f140..e5271c6f7e 100644 --- a/packages/sdk-router/src/rfq/api.test.ts +++ b/packages/sdk-router/src/rfq/api.test.ts @@ -1,18 +1,23 @@ import fetchMock from 'jest-fetch-mock' +import { parseFixed } from '@ethersproject/bignumber' -import { getAllQuotes } from './api' +import { + getAllQuotes, + getBestRelayerQuote, + PutRFQResponseAPI, + RelayerQuote, +} from './api' +import { Ticker } from './ticker' import { FastBridgeQuoteAPI, unmarshallFastBridgeQuote } from './quote' const OK_RESPONSE_TIME = 1900 const SLOW_RESPONSE_TIME = 2100 const delayedAPIPromise = ( - quotes: FastBridgeQuoteAPI[], + body: string, delay: number ): Promise<{ body: string }> => { - return new Promise((resolve) => - setTimeout(() => resolve({ body: JSON.stringify(quotes) }), delay) - ) + return new Promise((resolve) => setTimeout(() => resolve({ body }), delay)) } describe('getAllQuotes', () => { @@ -65,7 +70,7 @@ describe('getAllQuotes', () => { it('when the response takes a long, but reasonable time to return', async () => { fetchMock.mockResponseOnce(() => - delayedAPIPromise(quotesAPI, OK_RESPONSE_TIME) + delayedAPIPromise(JSON.stringify(quotesAPI), OK_RESPONSE_TIME) ) const result = await getAllQuotes() expect(result).toEqual([ @@ -102,7 +107,7 @@ describe('getAllQuotes', () => { it('when the response takes too long to return', async () => { fetchMock.mockResponseOnce(() => - delayedAPIPromise(quotesAPI, SLOW_RESPONSE_TIME) + delayedAPIPromise(JSON.stringify(quotesAPI), SLOW_RESPONSE_TIME) ) const result = await getAllQuotes() expect(result).toEqual([]) @@ -110,3 +115,146 @@ describe('getAllQuotes', () => { }) }) }) + +describe('getBestRelayerQuote', () => { + const bigAmount = parseFixed('1', 24) + const bigAmountStr = '1000000000000000000000000' + const relayerAddress = '0x0000000000000000000000000000000000001337' + const quoteID = 'acbdef-123456' + + const ticker: Ticker = { + originToken: { + chainId: 1, + token: '0x0000000000000000000000000000000000000001', + }, + destToken: { + chainId: 2, + token: '0x0000000000000000000000000000000000000002', + }, + } + + const noQuotesFound: PutRFQResponseAPI = { + success: false, + reason: 'No quotes found', + } + + const quoteFound: PutRFQResponseAPI = { + success: true, + quote_id: quoteID, + dest_amount: bigAmountStr, + relayer_address: relayerAddress, + } + + const quote: RelayerQuote = { + destAmount: bigAmount, + relayerAddress, + quoteID, + } + + const quoteZero: RelayerQuote = { + destAmount: parseFixed('0'), + } + + beforeEach(() => { + fetchMock.enableMocks() + }) + + afterEach(() => { + fetchMock.resetMocks() + }) + + describe('Returns a non-zero quote', () => { + it('when the response is ok', async () => { + fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quote) + }) + + it('when the response takes a long, but reasonable time to return', async () => { + fetchMock.mockResponseOnce(() => + delayedAPIPromise(JSON.stringify(quoteFound), OK_RESPONSE_TIME) + ) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quote) + }) + + it('when the user address is not provided', async () => { + fetchMock.mockResponseOnce(JSON.stringify(quoteFound)) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quote) + }) + + it('when the response does not contain quote ID', async () => { + const responseWithoutID = { ...quoteFound, quote_id: undefined } + const quoteWithoutID = { ...quote, quoteID: undefined } + fetchMock.mockResponseOnce(JSON.stringify(responseWithoutID)) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quoteWithoutID) + }) + }) + + describe('Returns a zero quote', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { + // Do nothing + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('when the response is not ok', async () => { + fetchMock.mockResponseOnce(JSON.stringify(quoteFound), { status: 500 }) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quoteZero) + expect(console.error).toHaveBeenCalled() + }) + + it('when the response success is false', async () => { + fetchMock.mockResponseOnce(JSON.stringify(noQuotesFound)) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quoteZero) + expect(console.error).toHaveBeenCalled() + }) + + it('when the response takes too long to return', async () => { + fetchMock.mockResponseOnce(() => + delayedAPIPromise(JSON.stringify(quoteFound), SLOW_RESPONSE_TIME) + ) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quoteZero) + expect(console.error).toHaveBeenCalled() + }) + + it('when the response does not contain dest amount', async () => { + const responseWithoutDestAmount = { + ...quoteFound, + dest_amount: undefined, + } + fetchMock.mockResponseOnce(JSON.stringify(responseWithoutDestAmount)) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quoteZero) + expect(console.error).toHaveBeenCalled() + }) + + it('when the response does not contain relayer address', async () => { + const responseWithoutRelayerAddress = { + ...quoteFound, + relayer_address: undefined, + } + fetchMock.mockResponseOnce(JSON.stringify(responseWithoutRelayerAddress)) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quoteZero) + expect(console.error).toHaveBeenCalled() + }) + + it('when the response dest amount is zero', async () => { + const responseWithZeroDestAmount = { ...quoteFound, dest_amount: '0' } + fetchMock.mockResponseOnce(JSON.stringify(responseWithZeroDestAmount)) + const result = await getBestRelayerQuote(ticker, bigAmount) + expect(result).toEqual(quoteZero) + expect(console.error).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/sdk-router/src/rfq/api.ts b/packages/sdk-router/src/rfq/api.ts index 1723ab1683..18ae9eb3ea 100644 --- a/packages/sdk-router/src/rfq/api.ts +++ b/packages/sdk-router/src/rfq/api.ts @@ -1,21 +1,152 @@ +import { BigNumber } from '@ethersproject/bignumber' + +import { Ticker } from './ticker' import { FastBridgeQuote, FastBridgeQuoteAPI, unmarshallFastBridgeQuote, } from './quote' +import { logger } from '../utils/logger' -const API_URL = 'https://rfq-api.omnirpc.io' +const API_URL = 'https://rfq-api-stage.omnirpc.io' const API_TIMEOUT = 2000 -const fetchWithTimeout = async ( +/** + * The expiration window for active quotes in milliseconds to be used by the RFQ API. + * Relayers will have to respond with a quote within this time window. + */ +const EXPIRATION_WINDOW = 1000 + +export type PutRFQRequestAPI = { + // TODO: make integrator_id required + integrator_id?: string + quote_types: string[] + data: { + origin_chain_id: number + dest_chain_id: number + origin_token_addr: string + dest_token_addr: string + origin_amount_exact: string + expiration_window: number + origin_sender?: string + dest_recipient?: string + zap_data?: string + zap_native?: string + } +} + +export type PutRFQResponseAPI = { + success: boolean + reason?: string + quote_type?: string + quote_id?: string + dest_amount?: string + relayer_address?: string +} + +export type RelayerQuote = { + destAmount: BigNumber + relayerAddress?: string + quoteID?: string +} + +export type QuoteRequestOptions = { + originSender?: string + destRecipient?: string + zapData?: string + zapNative?: BigNumber +} + +const ZeroQuote: RelayerQuote = { + destAmount: BigNumber.from(0), +} + +export const fetchWithTimeout = async ( + name: string, url: string, - timeout: number -): Promise => { + timeout: number, + params: any = {}, + init?: RequestInit +): Promise => { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) - return fetch(url, { signal: controller.signal }).finally(() => + try { + const response = await fetch(url, { + signal: controller.signal, + ...init, + }) + if (!response.ok) { + const text = await response.text() + logger.info( + { name, url, params, response, text }, + `${name}: response not OK` + ) + return null + } + return response + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + logger.info({ name, url, timeout, params }, `${name}: timed out`) + } else { + logger.error( + { name, url, params, error }, + `${name}: was not able to get a response` + ) + } + return null + } finally { clearTimeout(timeoutId) - ) + } +} + +export const getWithTimeout = async ( + name: string, + url: string, + timeout: number, + params: any, + headers: any = {} +): Promise => { + const urlWithParams = Object.keys(params).length + ? `${url}?${new URLSearchParams(params)}` + : url + return fetchWithTimeout(name, urlWithParams, timeout, params, { + method: 'GET', + headers, + }) +} + +export const postWithTimeout = async ( + name: string, + url: string, + timeout: number, + params: any, + headers: any = {} +): Promise => { + return fetchWithTimeout(name, url, timeout, params, { + method: 'POST', + body: JSON.stringify(params), + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + }) +} + +export const putWithTimeout = async ( + name: string, + url: string, + timeout: number, + params: any, + headers: any = {} +): Promise => { + return fetchWithTimeout(name, url, timeout, params, { + method: 'PUT', + body: JSON.stringify(params), + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + }) } /** @@ -26,9 +157,12 @@ const fetchWithTimeout = async ( */ export const getAllQuotes = async (): Promise => { try { - const response = await fetchWithTimeout(`${API_URL}/quotes`, API_TIMEOUT) - if (!response.ok) { - console.error('Error fetching quotes:', response.statusText) + const response = await fetchWithTimeout( + 'RFQ API', + `${API_URL}/quotes`, + API_TIMEOUT + ) + if (!response) { return [] } // The response is a list of quotes in the FastBridgeQuoteAPI format @@ -38,13 +172,85 @@ export const getAllQuotes = async (): Promise => { try { return unmarshallFastBridgeQuote(quote) } catch (error) { - console.error('Error unmarshalling quote:', error) + logger.error({ quote, error }, 'Could not unmarshall quote') return null } }) .filter((quote): quote is FastBridgeQuote => quote !== null) } catch (error) { - console.error('Error fetching quotes:', error) + logger.error({ error }, 'Failed to fetch all quotes') return [] } } + +/** + * Hits Quoter API /rfq PUT endpoint to get the best quote for a given ticker and origin amount. + * + * @returns A promise that resolves to the best quote. + * Will return a zero quote if the request fails or times out. + */ +export const getBestRelayerQuote = async ( + ticker: Ticker, + originAmount: BigNumber, + options: QuoteRequestOptions = {} +): Promise => { + const rfqRequest: PutRFQRequestAPI = { + quote_types: ['active', 'passive'], + data: { + origin_chain_id: ticker.originToken.chainId, + dest_chain_id: ticker.destToken.chainId, + origin_token_addr: ticker.originToken.token, + dest_token_addr: ticker.destToken.token, + origin_amount_exact: originAmount.toString(), + expiration_window: EXPIRATION_WINDOW, + origin_sender: options.originSender, + dest_recipient: options.destRecipient, + zap_data: options.zapData ?? '0x', + zap_native: options.zapNative?.toString() ?? '0', + }, + } + try { + const response = await putWithTimeout( + 'RFQ API', + `${API_URL}/rfq`, + API_TIMEOUT, + rfqRequest + ) + if (!response) { + return ZeroQuote + } + // Check that response is successful, contains non-zero dest amount, and has a relayer address + const rfqResponse: PutRFQResponseAPI = await response.json() + if (!rfqResponse.success) { + logger.info( + { + rfqRequest, + rfqResponse, + reason: rfqResponse.reason ?? 'Unknown reason', + }, + 'No RFQ quote returned' + ) + return ZeroQuote + } + if (!rfqResponse.dest_amount || !rfqResponse.relayer_address) { + logger.error( + { rfqRequest, rfqResponse }, + 'Error getting RFQ quote: missing dest_amount or relayer_address in response' + ) + return ZeroQuote + } + const destAmount = BigNumber.from(rfqResponse.dest_amount) + if (destAmount.lte(0)) { + logger.info({ rfqRequest, rfqResponse }, 'No RFQ quote returned') + return ZeroQuote + } + return { + destAmount, + relayerAddress: rfqResponse.relayer_address, + quoteID: rfqResponse.quote_id, + } + } catch (error) { + logger.error({ rfqRequest, error }, 'Failed to get RFQ quote') + return ZeroQuote + } +} diff --git a/packages/sdk-router/src/rfq/engine/defaultEngine.ts b/packages/sdk-router/src/rfq/engine/defaultEngine.ts new file mode 100644 index 0000000000..fe3ce0a620 --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/defaultEngine.ts @@ -0,0 +1,122 @@ +import { Interface } from '@ethersproject/abi' +import { Zero } from '@ethersproject/constants' +import { BigNumber, Contract } from 'ethers' +import invariant from 'tiny-invariant' + +import defaultActionsAbi from '../../abi/IDefaultActions.json' +import previewerAbi from '../../abi/SynapseIntentPreviewer.json' +import { + SYNAPSE_INTENT_PREVIEWER_ADDRESS_MAP, + SWAP_QUOTER_V2_ADDRESS_MAP, +} from '../../constants/addresses' +import { ChainProvider } from '../../router' +import { SynapseIntentPreviewer as PreviewerContract } from '../../typechain/SynapseIntentPreviewer' +import { IDefaultActionsInterface } from '../../typechain/IDefaultActions' +import { isSameAddress } from '../../utils/addressUtils' +import { logger, logExecutionTime } from '../../utils/logger' +import { + SwapEngine, + SwapEngineRoute, + EngineID, + toWei, + RouteInput, + SlippageMax, + getEmptyRoute, + getForwardTo, +} from './swapEngine' + +export class DefaultEngine implements SwapEngine { + static defaultActions = new Interface( + defaultActionsAbi + ) as IDefaultActionsInterface + static previewerInterface = new Interface(previewerAbi) + + public readonly id = EngineID.Default + + private contracts: { + [chainId: number]: { + previewer: PreviewerContract + swapQuoter: string + } + } + + constructor(chains: ChainProvider[]) { + invariant(DefaultEngine.defaultActions, 'INTERFACE_UNDEFINED') + invariant(DefaultEngine.previewerInterface, 'INTERFACE_UNDEFINED') + this.contracts = {} + chains.forEach(({ chainId, provider }) => { + const previewerAddress = SYNAPSE_INTENT_PREVIEWER_ADDRESS_MAP[chainId] + // Skip chains without a SynapseIntentPreviewer address + if (!previewerAddress) { + return + } + // Swap Quoter must be defined for chains with SynapseIntentPreviewer + const swapQuoterAddress = SWAP_QUOTER_V2_ADDRESS_MAP[chainId] + invariant( + swapQuoterAddress, + 'Swap Quoter address not found for chain ' + chainId + ) + this.contracts[chainId] = { + previewer: new Contract( + previewerAddress, + DefaultEngine.previewerInterface, + provider + ) as PreviewerContract, + swapQuoter: swapQuoterAddress, + } + }) + } + + @logExecutionTime('DefaultEngine.getQuote') + public async getQuote(input: RouteInput): Promise { + // TODO: timeout + const { chainId, tokenIn, tokenOut, amountIn, finalRecipient } = input + const { previewer, swapQuoter } = this.contracts[chainId] + if ( + !previewer || + !swapQuoter || + isSameAddress(tokenIn, tokenOut) || + BigNumber.from(amountIn).eq(Zero) + ) { + return getEmptyRoute(this.id) + } + // Get the quote + const forwardTo = getForwardTo(finalRecipient) + // Note: restrictComplexity is not supported by the on-chain previewer + const { amountOut, steps: stepsOutput } = await previewer.previewIntent( + swapQuoter, + forwardTo, + // slippage settings are applied when generating the zap data as minFinalAmount + toWei(SlippageMax), + tokenIn, + tokenOut, + amountIn + ) + // Remove extra fields before the encoding + return { + engineID: this.id, + chainId, + expectedAmountOut: amountOut, + steps: stepsOutput.map(({ token, amount, msgValue, zapData }) => ({ + token, + amount, + msgValue, + zapData, + })), + } + } + + @logExecutionTime('DefaultEngine.generateRoute') + public async generateRoute( + _input: RouteInput, + quote: SwapEngineRoute + ): Promise { + if (quote.engineID !== this.id || !quote.steps) { + logger.error({ quote }, 'DefaultEngine: unexpected quote') + return getEmptyRoute(this.id) + } + return quote + } + + // TODO: getQuotes +} diff --git a/packages/sdk-router/src/rfq/engine/engineSet.ts b/packages/sdk-router/src/rfq/engine/engineSet.ts new file mode 100644 index 0000000000..c3fa1e54b6 --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/engineSet.ts @@ -0,0 +1,166 @@ +import { hexlify } from '@ethersproject/bytes' +import { AddressZero, Zero } from '@ethersproject/constants' +import invariant from 'tiny-invariant' + +import { TOKEN_ZAP_V1_ADDRESS_MAP } from '../../constants' +import { ChainProvider } from '../../router' +import { DefaultEngine } from './defaultEngine' +import { NoOpEngine } from './noOpEngine' +import { + SwapEngine, + SwapEngineRoute, + RouteInput, + SwapEngineQuote, + sanitizeMultiStepQuote, + sanitizeMultiStepRoute, +} from './swapEngine' +import { CCTPRouterQuery } from '../../module' +import { encodeStepParams } from '../steps' +import { KyberSwapEngine } from './kyberSwapEngine' +import { decodeZapData, encodeZapData } from '../zapData' + +export enum EngineTimeout { + Short = 1000, + Long = 3000, +} + +type QuoteOptions = { + allowMultiStep: boolean + timeout?: number +} + +type RouteOptions = QuoteOptions & { + useZeroSlippage: boolean +} + +export class EngineSet { + private engines: { + [engineID: number]: SwapEngine + } + + private tokenZaps: { + [chainId: number]: string + } + + constructor(chains: ChainProvider[]) { + this.engines = {} + this._addEngine(new NoOpEngine()) + this._addEngine(new DefaultEngine(chains)) + this._addEngine(new KyberSwapEngine(TOKEN_ZAP_V1_ADDRESS_MAP)) + + this.tokenZaps = {} + chains.forEach(({ chainId }) => { + const tokenZapAddress = TOKEN_ZAP_V1_ADDRESS_MAP[chainId] + // Skip chains without a Token Zap address + if (tokenZapAddress) { + this.tokenZaps[chainId] = tokenZapAddress + } + }) + } + + public async getBestQuote( + input: RouteInput, + options: QuoteOptions + ): Promise { + // Find the quote for each engine. + const allQuotes = await Promise.all( + Object.values(this.engines).map(async (engine) => + this._getQuote(engine, input, options) + ) + ) + // Select the best quote. + const quote = allQuotes.reduce((best, current) => + current.expectedAmountOut.gt(best.expectedAmountOut) ? current : best + ) + return quote.expectedAmountOut.gt(Zero) ? quote : undefined + } + + public async getQuote( + engineID: number, + input: RouteInput, + options: QuoteOptions + ): Promise { + const quote = await this._getQuote( + this._getEngine(engineID), + input, + options + ) + return quote.expectedAmountOut.gt(Zero) ? quote : undefined + } + + public async generateRoute( + input: RouteInput, + quote: SwapEngineQuote, + options: RouteOptions + ): Promise { + // Use longer timeout for route generation by default. + let route = await this._getEngine(quote.engineID).generateRoute( + input, + quote, + options.timeout ?? EngineTimeout.Long + ) + route = options.allowMultiStep ? route : sanitizeMultiStepRoute(route) + if (options.useZeroSlippage && route.steps.length > 0) { + const lastStepIndex = route.steps.length - 1 + const lastStepZapData = decodeZapData( + hexlify(route.steps[lastStepIndex].zapData) + ) + route.steps[lastStepIndex].zapData = encodeZapData({ + ...lastStepZapData, + minFinalAmount: route.expectedAmountOut, + }) + } + return route.expectedAmountOut.gt(Zero) ? route : undefined + } + + public getOriginQuery( + chainId: number, + route: SwapEngineRoute, + tokenOut: string + ): CCTPRouterQuery { + // To preserve consistency with other modules, router adapter is not set for a no-op intent + return { + routerAdapter: + route.steps.length > 0 ? this.getTokenZap(chainId) : AddressZero, + tokenOut, + minAmountOut: route.expectedAmountOut, + // The default deadline will be overridden later in `finalizeBridgeRoute` + deadline: Zero, + rawParams: encodeStepParams(route.steps), + } + } + + public getTokenZap(chainId: number): string { + const tokenZap = this.tokenZaps[chainId] + if (!tokenZap) { + throw new Error('Token Zap address not found for chain ' + chainId) + } + return tokenZap + } + + private _addEngine(engine: SwapEngine) { + invariant(!this.engines[engine.id], 'ENGINE_ALREADY_EXISTS') + this.engines[engine.id] = engine + } + + private _getEngine(engineID: number): SwapEngine { + const engine = this.engines[engineID] + if (!engine) { + throw new Error('ENGINE_NOT_FOUND') + } + return engine + } + + private async _getQuote( + engine: SwapEngine, + input: RouteInput, + options: QuoteOptions + ): Promise { + // Use shorter timeout for quote fetching by default. + const quote = await engine.getQuote( + input, + options.timeout ?? EngineTimeout.Short + ) + return options.allowMultiStep ? quote : sanitizeMultiStepQuote(quote) + } +} diff --git a/packages/sdk-router/src/rfq/engine/index.ts b/packages/sdk-router/src/rfq/engine/index.ts new file mode 100644 index 0000000000..040b09dd9a --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/index.ts @@ -0,0 +1,4 @@ +export * from './defaultEngine' +export * from './engineSet' +export * from './noOpEngine' +export * from './swapEngine' diff --git a/packages/sdk-router/src/rfq/engine/kyberSwapEngine.test.ts b/packages/sdk-router/src/rfq/engine/kyberSwapEngine.test.ts new file mode 100644 index 0000000000..d5ca8c269b --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/kyberSwapEngine.test.ts @@ -0,0 +1,51 @@ +import { ETH_USDC, ETH_USDT } from '../../constants/testValues' +import { USER_SIMULATED_ADDRESS } from './swapEngine' +import { + KyberSwapEngine, + KyberSwapQuoteResponse, + KyberSwapBuildResponse, +} from './kyberSwapEngine' +import { SupportedChainId } from '../../constants' + +global.fetch = require('node-fetch') + +describe('Integration test: KyberSwapEngine', () => { + it('Ethereum USDC -> USDT', async () => { + const kyberSwapEngine = new KyberSwapEngine({}) + let response = await kyberSwapEngine.getQuoteResponse( + SupportedChainId.ETH, + { + tokenIn: ETH_USDC, + tokenOut: ETH_USDT, + amountIn: '1000000000', + gasInclude: true, + }, + 2000 + ) + expect(response).not.toBeNull() + if (!response) { + return + } + const quoteResponse: KyberSwapQuoteResponse = await response.json() + console.log(JSON.stringify(quoteResponse, null, 2)) + + response = await kyberSwapEngine.getBuildResponse( + SupportedChainId.ETH, + { + routeSummary: quoteResponse.data.routeSummary, + sender: USER_SIMULATED_ADDRESS, + recipient: USER_SIMULATED_ADDRESS, + deadline: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, + slippageTolerance: 100, + enableGasEstimation: false, + }, + 2000 + ) + expect(response).not.toBeNull() + if (!response) { + return + } + const buildResponse: KyberSwapBuildResponse = await response.json() + console.log(JSON.stringify(buildResponse, null, 2)) + }) +}) diff --git a/packages/sdk-router/src/rfq/engine/kyberSwapEngine.ts b/packages/sdk-router/src/rfq/engine/kyberSwapEngine.ts new file mode 100644 index 0000000000..726f9950d2 --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/kyberSwapEngine.ts @@ -0,0 +1,225 @@ +import { BigNumber } from 'ethers' +import { Zero } from '@ethersproject/constants' + +import { AddressMap, SupportedChainId } from '../../constants' +import { getWithTimeout, postWithTimeout } from '../api' +import { + SwapEngine, + EngineID, + SwapEngineRoute, + getEmptyRoute, + SwapEngineQuote, + RouteInput, + toBasisPoints, + SlippageMax, +} from './swapEngine' +import { isSameAddress } from '../../utils/addressUtils' +import { ONE_WEEK } from '../../utils/deadlines' +import { logger, logExecutionTime } from '../../utils/logger' +import { generateAPIRoute } from './response' +import { isNativeToken } from '../../utils/handleNativeToken' + +const KYBER_SWAP_API_URL = 'https://aggregator-api.kyberswap.com' + +type KyberSwapQuoteRequest = { + tokenIn: string + tokenOut: string + amountIn: string + gasInclude: boolean + onlySinglePath?: boolean +} + +type KyberSwapRouteSummary = { + amountOut: string +} + +export type KyberSwapQuoteResponse = { + code: number + message: string + data: { + routeSummary: KyberSwapRouteSummary + routerAddress: string + } +} + +type KyberSwapBuildRequest = { + routeSummary: KyberSwapRouteSummary + sender: string + recipient: string + deadline: number + slippageTolerance: number + enableGasEstimation: boolean +} + +export type KyberSwapBuildResponse = { + code: number + message: string + data: { + routerAddress: string + data: string + } +} + +type KyberSwapQuote = SwapEngineQuote & { + routeSummary: KyberSwapRouteSummary +} + +const EmptyKyberSwapQuote: KyberSwapQuote = { + engineID: EngineID.KyberSwap, + chainId: 0, + expectedAmountOut: Zero, + routeSummary: { + amountOut: '0', + }, +} + +const KyberSwapChainMap: Record = { + [SupportedChainId.ETH]: 'ethereum', + [SupportedChainId.OPTIMISM]: 'optimism', + [SupportedChainId.BSC]: 'bsc', + [SupportedChainId.POLYGON]: 'polygon', + [SupportedChainId.FANTOM]: 'fantom', + [SupportedChainId.BASE]: 'base', + [SupportedChainId.ARBITRUM]: 'arbitrum', + [SupportedChainId.AVALANCHE]: 'avalanche', + [SupportedChainId.LINEA]: 'linea', + [SupportedChainId.BLAST]: 'blast', + [SupportedChainId.SCROLL]: 'scroll', +} + +export class KyberSwapEngine implements SwapEngine { + readonly id: EngineID = EngineID.KyberSwap + + private readonly tokenZapAddressMap: AddressMap + + constructor(tokenZapAddressMap: AddressMap) { + this.tokenZapAddressMap = tokenZapAddressMap + } + + public async getQuote( + input: RouteInput, + timeout: number + ): Promise { + const { chainId, tokenIn, tokenOut, amountIn } = input + const tokenZap = this.tokenZapAddressMap[chainId] + if ( + !tokenZap || + isSameAddress(tokenIn, tokenOut) || + BigNumber.from(amountIn).eq(Zero) + ) { + return EmptyKyberSwapQuote + } + const request: KyberSwapQuoteRequest = { + tokenIn, + tokenOut, + amountIn: amountIn.toString(), + gasInclude: true, + onlySinglePath: input.restrictComplexity, + } + const response = await this.getQuoteResponse( + input.chainId, + request, + timeout + ) + if (!response) { + return EmptyKyberSwapQuote + } + const kyberSwapQuoteResponse: KyberSwapQuoteResponse = await response.json() + if ( + kyberSwapQuoteResponse.code !== 0 || + !kyberSwapQuoteResponse.data?.routeSummary + ) { + return EmptyKyberSwapQuote + } + const expectedAmountOut = BigNumber.from( + kyberSwapQuoteResponse.data.routeSummary.amountOut ?? '0' + ) + if (expectedAmountOut.eq(Zero)) { + return EmptyKyberSwapQuote + } + return { + engineID: this.id, + chainId, + expectedAmountOut, + routeSummary: kyberSwapQuoteResponse.data.routeSummary, + } + } + + public async generateRoute( + input: RouteInput, + quote: KyberSwapQuote, + timeout: number + ): Promise { + const chainId = input.chainId + const tokenZap = this.tokenZapAddressMap[chainId] + if (quote.engineID !== this.id || !quote.routeSummary || !tokenZap) { + logger.error({ quote }, 'KyberSwap: unexpected quote') + return getEmptyRoute(this.id) + } + const response = await this.getBuildResponse( + chainId, + { + routeSummary: quote.routeSummary, + sender: tokenZap, + recipient: tokenZap, + deadline: Math.floor(Date.now() / 1000) + ONE_WEEK, + slippageTolerance: toBasisPoints(SlippageMax), + enableGasEstimation: false, + }, + timeout + ) + if (!response) { + return getEmptyRoute(this.id) + } + const kyberSwapBuildResponse: KyberSwapBuildResponse = await response.json() + if (kyberSwapBuildResponse.code !== 0) { + return getEmptyRoute(this.id) + } + return generateAPIRoute(input, this.id, SlippageMax, { + amountOut: quote.expectedAmountOut, + transaction: { + chainId, + from: tokenZap, + to: kyberSwapBuildResponse.data.routerAddress, + value: isNativeToken(input.tokenIn) ? input.amountIn.toString() : '0', + data: kyberSwapBuildResponse.data.data, + }, + }) + } + + @logExecutionTime('KyberSwapEngine.getQuoteResponse') + public async getQuoteResponse( + chainId: number, + params: KyberSwapQuoteRequest, + timeout: number + ): Promise { + const chain = KyberSwapChainMap[chainId] + if (!chain) { + return null + } + const url = `${this.buildBaseURL(chain)}/routes` + return getWithTimeout('KyberSwap', url, timeout, params, { + 'x-client-id': 'SynapseIntentNetwork', + }) + } + + @logExecutionTime('KyberSwapEngine.getBuildResponse') + public async getBuildResponse( + chainId: number, + params: KyberSwapBuildRequest, + timeout: number + ): Promise { + const chain = KyberSwapChainMap[chainId] + if (!chain) { + return null + } + const url = `${this.buildBaseURL(chain)}/route/build` + return postWithTimeout('KyberSwap', url, timeout, params, { + 'x-client-id': 'SynapseIntentNetwork', + }) + } + + private buildBaseURL(chain: string): string { + return `${KYBER_SWAP_API_URL}/${chain}/api/v1` + } +} diff --git a/packages/sdk-router/src/rfq/engine/noOpEngine.ts b/packages/sdk-router/src/rfq/engine/noOpEngine.ts new file mode 100644 index 0000000000..9e16695ad8 --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/noOpEngine.ts @@ -0,0 +1,39 @@ +import { BigNumber } from 'ethers' + +import { isSameAddress } from '../../utils/addressUtils' +import { logger } from '../../utils/logger' +import { + SwapEngine, + SwapEngineRoute, + EngineID, + RouteInput, + getEmptyRoute, +} from './swapEngine' + +export class NoOpEngine implements SwapEngine { + public readonly id: EngineID = EngineID.NoOp + + public async getQuote(input: RouteInput): Promise { + const { chainId, tokenIn, tokenOut, amountIn } = input + if (!isSameAddress(tokenIn, tokenOut)) { + return getEmptyRoute(this.id) + } + return { + engineID: this.id, + chainId, + expectedAmountOut: BigNumber.from(amountIn), + steps: [], + } + } + + public async generateRoute( + _input: RouteInput, + quote: SwapEngineRoute + ): Promise { + if (quote.engineID !== this.id || !quote.steps) { + logger.error({ quote }, 'NoOpEngine: unexpected quote') + return getEmptyRoute(this.id) + } + return quote + } +} diff --git a/packages/sdk-router/src/rfq/engine/odosEngine.test.ts b/packages/sdk-router/src/rfq/engine/odosEngine.test.ts new file mode 100644 index 0000000000..5ed6b05e4d --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/odosEngine.test.ts @@ -0,0 +1,36 @@ +import { ETH_USDC, ETH_USDT } from '../../constants/testValues' +import { USER_SIMULATED_ADDRESS } from './swapEngine' +import { OdosEngine, OdosQuoteResponse } from './odosEngine' + +global.fetch = require('node-fetch') + +describe('Integration test: ParaSwapEngine', () => { + it('Ethereum USDC -> USDT', async () => { + const odosEngine = new OdosEngine({}) + let response = await odosEngine.getQuoteResponse( + { + chainId: 1, + inputTokens: [{ amount: '1000000000', tokenAddress: ETH_USDC }], + outputTokens: [{ proportion: 1, tokenAddress: ETH_USDT }], + userAddr: USER_SIMULATED_ADDRESS, + slippageLimitPercent: 1, + simple: true, + }, + 2000 + ) + expect(response).not.toBeNull() + const quoteResponse: OdosQuoteResponse = await response?.json() + console.log(JSON.stringify(quoteResponse, null, 2)) + + response = await odosEngine.getAssembleResponse( + { + userAddr: USER_SIMULATED_ADDRESS, + pathId: quoteResponse.pathId, + }, + 2000 + ) + expect(response).not.toBeNull() + const assembleResponse = await response?.json() + console.log(JSON.stringify(assembleResponse, null, 2)) + }) +}) diff --git a/packages/sdk-router/src/rfq/engine/odosEngine.ts b/packages/sdk-router/src/rfq/engine/odosEngine.ts new file mode 100644 index 0000000000..e8fcfe51e5 --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/odosEngine.ts @@ -0,0 +1,198 @@ +import { BigNumber } from 'ethers' +import { AddressZero, Zero } from '@ethersproject/constants' + +import { postWithTimeout } from '../api' +import { generateAPIRoute } from './response' +import { + SwapEngine, + EngineID, + SwapEngineRoute, + RouteInput, + getEmptyRoute, + toPercentFloat, + SlippageMax, + SwapEngineQuote, +} from './swapEngine' +import { AddressMap } from '../../constants' +import { isSameAddress } from '../../utils/addressUtils' +import { logger, logExecutionTime } from '../../utils/logger' +import { isNativeToken } from '../../utils/handleNativeToken' + +const ODOS_API_URL = 'https://api.odos.xyz/sor' + +type OdosQuoteRequest = { + chainId: number + inputTokens: { + amount: string + tokenAddress: string + }[] + outputTokens: { + proportion: number + tokenAddress: string + }[] + userAddr: string + slippageLimitPercent: number + referralCode?: number + simple?: boolean +} + +export type OdosQuoteResponse = { + pathId: string + outAmounts: string[] +} + +type OdosAssembleRequest = { + userAddr: string + pathId: string + receiver?: string +} + +type OdosAssembleResponse = { + transaction: { + chainId: number + gas: number + gasPrice: number + value: string + to: string + from: string + data: string + } +} + +type OdosQuote = SwapEngineQuote & { + assembleRequest: OdosAssembleRequest +} + +const EmptyOdosQuote: OdosQuote = { + engineID: EngineID.Odos, + chainId: 0, + expectedAmountOut: Zero, + assembleRequest: { + userAddr: AddressZero, + pathId: '', + }, +} + +export class OdosEngine implements SwapEngine { + readonly id: EngineID = EngineID.Odos + + private readonly tokenZapAddressMap: AddressMap + + constructor(tokenZapAddressMap: AddressMap) { + this.tokenZapAddressMap = tokenZapAddressMap + } + + public async getQuote( + input: RouteInput, + timeout: number + ): Promise { + const { chainId, tokenIn, tokenOut, amountIn } = input + const tokenZap = this.tokenZapAddressMap[chainId] + if ( + !tokenZap || + isSameAddress(tokenIn, tokenOut) || + BigNumber.from(amountIn).eq(Zero) + ) { + return EmptyOdosQuote + } + const request: OdosQuoteRequest = { + chainId, + inputTokens: [ + { + amount: amountIn.toString(), + tokenAddress: this.handleNativeToken(tokenIn), + }, + ], + outputTokens: [ + { + proportion: 1, + tokenAddress: this.handleNativeToken(tokenOut), + }, + ], + userAddr: tokenZap, + // slippage settings are applied when generating the zap data as minFinalAmount + slippageLimitPercent: toPercentFloat(SlippageMax), + simple: input.restrictComplexity, + } + const response = await this.getQuoteResponse(request, timeout) + if (!response) { + return EmptyOdosQuote + } + const odosQuoteResponse: OdosQuoteResponse = await response.json() + if ( + !odosQuoteResponse.outAmounts || + !odosQuoteResponse.pathId || + odosQuoteResponse.outAmounts.length !== 1 + ) { + logger.error( + { request, odosQuoteResponse }, + 'Odos: invalid quote response' + ) + return EmptyOdosQuote + } + const amountOut = odosQuoteResponse.outAmounts[0] + if (amountOut === '0') { + logger.info({ request, odosQuoteResponse }, 'Odos: zero amount out') + return EmptyOdosQuote + } + return { + engineID: this.id, + chainId, + expectedAmountOut: BigNumber.from(amountOut), + assembleRequest: { + userAddr: tokenZap, + pathId: odosQuoteResponse.pathId, + }, + } + } + + public async generateRoute( + input: RouteInput, + quote: OdosQuote, + timeout: number + ): Promise { + if (quote.engineID !== this.id || !quote.assembleRequest) { + logger.error({ quote }, 'Odos: unexpected quote') + return getEmptyRoute(this.id) + } + const response = await this.getAssembleResponse( + quote.assembleRequest, + timeout + ) + if (!response) { + return getEmptyRoute(this.id) + } + const odosAssembleResponse: OdosAssembleResponse = await response.json() + if (!odosAssembleResponse.transaction) { + logger.error( + { request: quote.assembleRequest, response }, + 'Odos: invalid assemble response' + ) + return getEmptyRoute(this.id) + } + return generateAPIRoute(input, this.id, SlippageMax, { + amountOut: quote.expectedAmountOut, + transaction: odosAssembleResponse.transaction, + }) + } + + @logExecutionTime('OdosEngine.getAssembleResponse') + public async getAssembleResponse( + params: OdosAssembleRequest, + timeout: number + ): Promise { + return postWithTimeout('Odos', `${ODOS_API_URL}/assemble`, timeout, params) + } + + @logExecutionTime('OdosEngine.getQuoteResponse') + public async getQuoteResponse( + params: OdosQuoteRequest, + timeout: number + ): Promise { + return postWithTimeout('Odos', `${ODOS_API_URL}/quote/v2`, timeout, params) + } + + private handleNativeToken(token: string): string { + return isNativeToken(token) ? AddressZero : token + } +} diff --git a/packages/sdk-router/src/rfq/engine/response.ts b/packages/sdk-router/src/rfq/engine/response.ts new file mode 100644 index 0000000000..2761d7322a --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/response.ts @@ -0,0 +1,72 @@ +import { BigNumber } from 'ethers' +import { AddressZero, Zero } from '@ethersproject/constants' + +import { + getEmptyRoute, + EngineID, + RouteInput, + SwapEngineRoute, + getForwardTo, + Slippage, + applySlippage, +} from './swapEngine' +import { isSameAddress } from '../../utils/addressUtils' +import { AMOUNT_NOT_PRESENT, encodeZapData } from '../zapData' + +export type SwapAPIResponse = { + amountOut: BigNumber + transaction: { + chainId: number + from: string + to: string + value: string + data: string + } +} + +export const EMPTY_SWAP_API_RESPONSE: SwapAPIResponse = { + amountOut: Zero, + transaction: { + chainId: 0, + from: '', + to: '', + value: '', + data: '', + }, +} + +export const generateAPIRoute = ( + input: RouteInput, + engineID: EngineID, + slippage: Slippage, + response: SwapAPIResponse +): SwapEngineRoute => { + if (isSameAddress(input.finalRecipient.address, AddressZero)) { + throw new Error('Missing recipient address') + } + if (response.amountOut.eq(Zero)) { + return getEmptyRoute(engineID) + } + const zapData = encodeZapData({ + target: response.transaction.to, + payload: response.transaction.data, + amountPosition: AMOUNT_NOT_PRESENT, + finalToken: input.tokenOut, + forwardTo: getForwardTo(input.finalRecipient), + minFinalAmount: applySlippage(response.amountOut, slippage), + }) + + return { + engineID, + chainId: input.chainId, + expectedAmountOut: response.amountOut, + steps: [ + { + token: input.tokenIn, + amount: BigNumber.from(input.amountIn), + msgValue: BigNumber.from(response.transaction.value), + zapData, + }, + ], + } +} diff --git a/packages/sdk-router/src/rfq/engine/swapEngine.test.ts b/packages/sdk-router/src/rfq/engine/swapEngine.test.ts new file mode 100644 index 0000000000..50f66ffe80 --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/swapEngine.test.ts @@ -0,0 +1,17 @@ +import { EngineID, validateEngineID } from './swapEngine' + +describe('EngineID', () => { + it('validates values within enum', () => { + expect(validateEngineID(EngineID.Null)).toBe(true) + expect(validateEngineID(EngineID.NoOp)).toBe(true) + expect(validateEngineID(EngineID.Default)).toBe(true) + expect(validateEngineID(EngineID.Odos)).toBe(true) + expect(validateEngineID(EngineID.KyberSwap)).toBe(true) + }) + + it('does not validate values outside enum', () => { + expect(validateEngineID(EngineID.Odos + 1)).toBe(false) + expect(validateEngineID(10)).toBe(false) + expect(validateEngineID(1000)).toBe(false) + }) +}) diff --git a/packages/sdk-router/src/rfq/engine/swapEngine.ts b/packages/sdk-router/src/rfq/engine/swapEngine.ts new file mode 100644 index 0000000000..d48c61a5e4 --- /dev/null +++ b/packages/sdk-router/src/rfq/engine/swapEngine.ts @@ -0,0 +1,152 @@ +import { BigNumber } from 'ethers' +import { Zero, WeiPerEther, AddressZero } from '@ethersproject/constants' + +import { StepParams } from '../steps' +import { BigintIsh } from '../../constants' + +export enum EngineID { + Null, + NoOp, + Default, + Odos, + KyberSwap, +} + +export type SwapEngineQuote = { + engineID: EngineID + chainId: number + expectedAmountOut: BigNumber + steps?: StepParams[] +} + +export type SwapEngineRoute = Required + +export enum RecipientEntity { + Self, + User, + UserSimulated, +} + +export type Recipient = { + entity: RecipientEntity + address: string +} + +export type Slippage = { + numerator: number + denominator: number +} + +// Max slippage that can be used by the swap engines, 100 bips (1%) +export const SlippageMax: Slippage = { + numerator: 100, + denominator: 10000, +} + +export const USER_SIMULATED_ADDRESS = + '0xFAcefaCEFACefACeFaCefacEFaCeFACEFAceFAcE' + +/** + * Input parameters for generating a swap route. + * + * @property {number} chainId - The chain ID of the route. + * @property {string} tokenIn - The input token address. + * @property {string} tokenOut - The output token address. + * @property {BigintIsh} amountIn - The amount of input token to swap. + * @property {Recipient} finalRecipient - The recipient of the output token. + * @property {boolean} restrictComplexity - Whether to restrict the complexity of the route (no splitting, less steps). + */ +export type RouteInput = { + chainId: number + tokenIn: string + tokenOut: string + amountIn: BigintIsh + finalRecipient: Recipient + restrictComplexity: boolean +} + +export interface SwapEngine { + readonly id: EngineID + + /** + * Gets a swap quote from the engine for the given tokenIn -> tokenOut input. + * Some of the engines may not be able to generate the route steps at the same time, + * use the `generateRoute` method to generate the steps. + */ + getQuote(input: RouteInput, timeout: number): Promise + + /** + * Generates the route steps from the quote obtained from the `getQuote` method. + */ + generateRoute( + input: RouteInput, + quote: SwapEngineQuote, + timeout: number + ): Promise +} + +export const validateEngineID = (engineID: number): engineID is EngineID => { + return Object.values(EngineID).includes(engineID) +} + +export const toBasisPoints = (slippage: Slippage): number => { + return Math.round((slippage.numerator * 10000) / slippage.denominator) +} + +export const toPercentFloat = (slippage: Slippage): number => { + return (slippage.numerator * 100) / slippage.denominator +} + +export const toWei = (slippage: Slippage): BigNumber => { + return BigNumber.from(slippage.numerator) + .mul(WeiPerEther) + .div(slippage.denominator) +} + +export const applySlippage = ( + amount: BigNumber, + slippage: Slippage +): BigNumber => { + return amount.sub(amount.mul(slippage.numerator).div(slippage.denominator)) +} + +export const getEmptyQuote = (engineID: EngineID): SwapEngineQuote => { + return { + engineID, + chainId: 0, + expectedAmountOut: Zero, + } +} + +export const getEmptyRoute = (engineID: EngineID): SwapEngineRoute => { + return { + engineID, + chainId: 0, + expectedAmountOut: Zero, + steps: [], + } +} + +export const sanitizeMultiStepQuote = ( + quote: SwapEngineQuote +): SwapEngineQuote => { + if (!quote.steps || quote.steps.length <= 1) { + return quote + } + return getEmptyQuote(quote.engineID) +} + +export const sanitizeMultiStepRoute = ( + route: SwapEngineRoute +): SwapEngineRoute => { + if (route.steps.length <= 1) { + return route + } + return getEmptyRoute(route.engineID) +} + +export const getForwardTo = (recipient: Recipient): string => { + return recipient.entity === RecipientEntity.Self + ? AddressZero + : recipient.address +} diff --git a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts index 8caee554ed..e5aca3960a 100644 --- a/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts +++ b/packages/sdk-router/src/rfq/fastBridgeRouterSet.ts @@ -20,9 +20,10 @@ import { import { FastBridgeRouter } from './fastBridgeRouter' import { ChainProvider } from '../router' import { ONE_HOUR, TEN_MINUTES } from '../utils/deadlines' -import { FastBridgeQuote, applyQuote } from './quote' -import { marshallTicker } from './ticker' -import { getAllQuotes } from './api' +import { isSameAddress } from '../utils/addressUtils' +import { marshallTicker, Ticker } from './ticker' +import { getAllQuotes, getBestRelayerQuote } from './api' +import { logExecutionTime } from '../utils/logger' export class FastBridgeRouterSet extends SynapseModuleSet { static readonly MAX_QUOTE_AGE_MILLISECONDS = 5 * 60 * 1000 // 5 minutes @@ -88,6 +89,7 @@ export class FastBridgeRouterSet extends SynapseModuleSet { /** * @inheritdoc SynapseModuleSet.getBridgeRoutes */ + @logExecutionTime('SynapseRFQ.getBridgeRoutes') public async getBridgeRoutes( originChainId: number, destChainId: number, @@ -100,47 +102,58 @@ export class FastBridgeRouterSet extends SynapseModuleSet { if (!this.getModule(originChainId) || !this.getModule(destChainId)) { return [] } - // Get all quotes that result in the final token - const allQuotes: FastBridgeQuote[] = await this.getQuotes( + // TODO: revert (temporarily disable this module) + if (this.getModule(originChainId)) { + return [] + } + // Get all tickers that can be used to fulfill the tokenIn -> tokenOut intent via RFQ + const tickers = await this.getAllTickers( originChainId, destChainId, tokenOut ) - // Get queries for swaps on the origin chain into the "RFQ-supported token" - const filteredQuotes = await this.filterOriginQuotes( + // Get queries for swaps on the origin chain from tokenIn into the "RFQ-supported token" + const filteredTickers = await this.filterTickersWithPossibleSwap( originChainId, tokenIn, amountIn, - allQuotes + tickers ) const protocolFeeRate = await this.getFastBridgeRouter( originChainId ).getProtocolFeeRate() - return filteredQuotes - .map(({ quote, originQuery }) => ({ - quote, + const quotes = await Promise.all( + filteredTickers.map(async ({ ticker, originQuery }) => ({ + ticker, originQuery, - // Apply quote to the proceeds of the origin swap with protocol fee applied - // TODO: handle optional gas airdrop pricing - destAmountOut: applyQuote( - quote, - this.applyProtocolFeeRate(originQuery.minAmountOut, protocolFeeRate) + quote: await getBestRelayerQuote( + ticker, + // Get the quote for the proceeds of the origin swap with protocol fee applied + this.applyProtocolFeeRate(originQuery.minAmountOut, protocolFeeRate), + { + originSender: originUserAddress, + } ), })) - .filter(({ destAmountOut }) => destAmountOut.gt(0)) - .map(({ quote, originQuery, destAmountOut }) => ({ + ) + return quotes + .filter(({ quote }) => quote.destAmount.gt(0)) + .map(({ ticker, originQuery, quote }) => ({ originChainId, destChainId, bridgeToken: { - symbol: marshallTicker(quote.ticker), - token: quote.ticker.destToken.token, + symbol: marshallTicker(ticker), + token: ticker.destToken.token, }, originQuery, + originAmountOut: originQuery.minAmountOut, destQuery: FastBridgeRouterSet.createRFQDestQuery( tokenOut, - destAmountOut, + quote.destAmount, originUserAddress ), + destAmountIn: quote.destAmount, + destAmountOut: quote.destAmount, bridgeModuleName: this.bridgeModuleName, })) } @@ -152,11 +165,10 @@ export class FastBridgeRouterSet extends SynapseModuleSet { feeAmount: BigNumber feeConfig: FeeConfig }> { - // Origin Out vs Dest Out is the effective fee + // Origin Out vs Dest In is the effective fee + const feeAmount = bridgeRoute.originAmountOut.sub(bridgeRoute.destAmountIn) return { - feeAmount: bridgeRoute.originQuery.minAmountOut.sub( - bridgeRoute.destQuery.minAmountOut - ), + feeAmount: feeAmount.lt(Zero) ? Zero : feeAmount, feeConfig: { bridgeFee: 0, minFee: BigNumber.from(0), @@ -251,66 +263,76 @@ export class FastBridgeRouterSet extends SynapseModuleSet { } /** - * Filters the list of quotes to only include those that can be used for given amount of input token. - * For every filtered quote, the origin query is returned with the information for tokenIn -> RFQ token swaps. + * Filters the list of tickers to only include those that can be used for given amount of input token. + * For every filtered ticker, the origin query is returned with the information for tokenIn -> ticker swaps. */ - private async filterOriginQuotes( + private async filterTickersWithPossibleSwap( originChainId: number, tokenIn: string, amountIn: BigintIsh, - allQuotes: FastBridgeQuote[] - ): Promise<{ quote: FastBridgeQuote; originQuery: Query }[]> { + tickers: Ticker[] + ): Promise<{ ticker: Ticker; originQuery: Query }[]> { // Get queries for swaps on the origin chain into the "RFQ-supported token" const originQueries = await this.getFastBridgeRouter( originChainId ).getOriginAmountOut( tokenIn, - allQuotes.map((quote) => quote.ticker.originToken.token), + tickers.map((ticker) => ticker.originToken.token), amountIn ) - // Note: allQuotes.length === originQueries.length - // Zip the quotes and queries together, filter out "no path found" queries - return allQuotes - .map((quote, index) => ({ - quote, + // Note: tickers.length === originQueries.length + // Zip the tickers and queries together, filter out "no path found" queries + return tickers + .map((ticker, index) => ({ + ticker, originQuery: originQueries[index], })) .filter(({ originQuery }) => originQuery.minAmountOut.gt(0)) } /** - * Get the list of quotes between two chains for a given final token. + * Get all unique tickers for a given origin chain and a destination token. In other words, + * this is the list of all origin tokens that can be used to create a quote for a + * swap to the given destination token, without duplicates. * * @param originChainId - The ID of the origin chain. * @param destChainId - The ID of the destination chain. * @param tokenOut - The final token of the cross-chain swap. - * @returns A promise that resolves to the list of supported tickers. + * @returns A promise that resolves to the list of tickers. */ - private async getQuotes( + private async getAllTickers( originChainId: number, destChainId: number, tokenOut: string - ): Promise { + ): Promise { const allQuotes = await getAllQuotes() const originFB = await this.getFastBridgeAddress(originChainId) const destFB = await this.getFastBridgeAddress(destChainId) + // First, we filter out quotes for other chainIDs, bridges or destination token. + // Then, we filter out quotes that are too old. + // Finally, we remove the duplicates of the origin token. return allQuotes - .filter( - (quote) => + .filter((quote) => { + const areSameChainsAndToken = quote.ticker.originToken.chainId === originChainId && quote.ticker.destToken.chainId === destChainId && - quote.ticker.destToken.token && - quote.ticker.destToken.token.toLowerCase() === tokenOut.toLowerCase() - ) - .filter( - (quote) => - quote.originFastBridge.toLowerCase() === originFB.toLowerCase() && - quote.destFastBridge.toLowerCase() === destFB.toLowerCase() - ) - .filter((quote) => { + isSameAddress(quote.originFastBridge, originFB) && + isSameAddress(quote.destFastBridge, destFB) && + isSameAddress(quote.ticker.destToken.token, tokenOut) + // TODO: don't filter by age here const age = Date.now() - quote.updatedAt - return 0 <= age && age < FastBridgeRouterSet.MAX_QUOTE_AGE_MILLISECONDS + const isValidAge = + 0 <= age && age < FastBridgeRouterSet.MAX_QUOTE_AGE_MILLISECONDS + return areSameChainsAndToken && isValidAge }) + .map((quote) => quote.ticker) + .filter( + (ticker, index, self) => + index === + self.findIndex((t) => + isSameAddress(t.originToken.token, ticker.originToken.token) + ) + ) } public static createRFQDestQuery( diff --git a/packages/sdk-router/src/rfq/index.ts b/packages/sdk-router/src/rfq/index.ts index 01bea3b1ef..1c503c6e17 100644 --- a/packages/sdk-router/src/rfq/index.ts +++ b/packages/sdk-router/src/rfq/index.ts @@ -1,3 +1,5 @@ export * from './fastBridgeRouter' export * from './fastBridgeRouterSet' export * from './ticker' +export * from './sir' +export * from './sirSet' diff --git a/packages/sdk-router/src/rfq/paramsV2.test.ts b/packages/sdk-router/src/rfq/paramsV2.test.ts new file mode 100644 index 0000000000..5fae87264a --- /dev/null +++ b/packages/sdk-router/src/rfq/paramsV2.test.ts @@ -0,0 +1,99 @@ +import { BigNumber } from 'ethers' +import { WeiPerEther } from '@ethersproject/constants' + +import { + BridgeParamsV2, + SavedParamsV1, + encodeSavedBridgeParams, + decodeSavedBridgeParams, +} from './paramsV2' +import { ETH_USDC, ETH_USDT, ARB_USDC, ARB_USDT } from '../constants/testValues' + +describe('paramsV2', () => { + const paramsV1: SavedParamsV1 = { + originSender: ARB_USDC, + destChainId: 1234, + destEngineID: 4321, + destRelayRecipient: ARB_USDT, + destRelayToken: ETH_USDC, + destRelayAmount: WeiPerEther.mul(2), + } + + const paramsV2: BridgeParamsV2 = { + quoteRelayer: ETH_USDT, + quoteExclusivitySeconds: BigNumber.from(30), + quoteId: '0xdead', + zapNative: WeiPerEther, + zapData: '0xbeef', + } + + const paramsV2QuoteIdEmpty: BridgeParamsV2 = { + quoteRelayer: ETH_USDT, + quoteExclusivitySeconds: BigNumber.from(30), + quoteId: '0x', + zapNative: WeiPerEther, + zapData: '0xdeadbeef', + } + + const paramsV2ZapDataEmpty: BridgeParamsV2 = { + quoteRelayer: ETH_USDT, + quoteExclusivitySeconds: BigNumber.from(30), + quoteId: '0xdeadbeef', + zapNative: WeiPerEther, + zapData: '0x', + } + + const paramsV2AllBytesEmpty: BridgeParamsV2 = { + quoteRelayer: ETH_USDT, + quoteExclusivitySeconds: BigNumber.from(30), + quoteId: '0x', + zapNative: WeiPerEther, + zapData: '0x', + } + + const paramsV2NegativeExclusivity: BridgeParamsV2 = { + quoteRelayer: ETH_USDT, + quoteExclusivitySeconds: BigNumber.from(-30), + quoteId: '0xdeadbeef', + zapNative: WeiPerEther, + zapData: '0x', + } + + it('roundtrip encoding', () => { + const encoded = encodeSavedBridgeParams(paramsV1, paramsV2) + const decoded = decodeSavedBridgeParams(encoded) + expect(decoded.paramsV1).toEqual(paramsV1) + expect(decoded.paramsV2).toEqual(paramsV2) + }) + + it('roundtrip encoding with empty quoteId', () => { + const encoded = encodeSavedBridgeParams(paramsV1, paramsV2QuoteIdEmpty) + const decoded = decodeSavedBridgeParams(encoded) + expect(decoded.paramsV1).toEqual(paramsV1) + expect(decoded.paramsV2).toEqual(paramsV2QuoteIdEmpty) + }) + + it('roundtrip encoding with empty zapData', () => { + const encoded = encodeSavedBridgeParams(paramsV1, paramsV2ZapDataEmpty) + const decoded = decodeSavedBridgeParams(encoded) + expect(decoded.paramsV1).toEqual(paramsV1) + expect(decoded.paramsV2).toEqual(paramsV2ZapDataEmpty) + }) + + it('roundtrip encoding with empty quoteId and zapData', () => { + const encoded = encodeSavedBridgeParams(paramsV1, paramsV2AllBytesEmpty) + const decoded = decodeSavedBridgeParams(encoded) + expect(decoded.paramsV1).toEqual(paramsV1) + expect(decoded.paramsV2).toEqual(paramsV2AllBytesEmpty) + }) + + it('roundtrip encoding with negative exclusivity', () => { + const encoded = encodeSavedBridgeParams( + paramsV1, + paramsV2NegativeExclusivity + ) + const decoded = decodeSavedBridgeParams(encoded) + expect(decoded.paramsV1).toEqual(paramsV1) + expect(decoded.paramsV2).toEqual(paramsV2NegativeExclusivity) + }) +}) diff --git a/packages/sdk-router/src/rfq/paramsV2.ts b/packages/sdk-router/src/rfq/paramsV2.ts new file mode 100644 index 0000000000..9972b639e6 --- /dev/null +++ b/packages/sdk-router/src/rfq/paramsV2.ts @@ -0,0 +1,77 @@ +import { defaultAbiCoder } from '@ethersproject/abi' +import { BigNumber } from 'ethers' + +import { IFastBridgeV2 } from '../typechain/FastBridgeV2' + +export type SavedParamsV1 = { + originSender: string + destChainId: number + destEngineID: number + destRelayRecipient: string + destRelayToken: string + destRelayAmount: BigNumber +} +export type BridgeParamsV2 = IFastBridgeV2.BridgeParamsV2Struct +const savedBridgeParams = [ + 'tuple(address,uint256,uint256,address,address,uint256)', + 'tuple(address,int256,bytes,uint256,bytes)', +] + +export const encodeSavedBridgeParams = ( + paramsV1: SavedParamsV1, + paramsV2: BridgeParamsV2 +) => { + return defaultAbiCoder.encode(savedBridgeParams, [ + [ + paramsV1.originSender, + paramsV1.destChainId, + paramsV1.destEngineID, + paramsV1.destRelayRecipient, + paramsV1.destRelayToken, + paramsV1.destRelayAmount, + ], + [ + paramsV2.quoteRelayer, + paramsV2.quoteExclusivitySeconds, + paramsV2.quoteId, + paramsV2.zapNative, + paramsV2.zapData, + ], + ]) +} + +export const decodeSavedBridgeParams = ( + data: string +): { + paramsV1: SavedParamsV1 + paramsV2: BridgeParamsV2 +} => { + const [ + [ + originSender, + destChainId, + destEngineID, + destRelayRecipient, + destRelayToken, + destRelayAmount, + ], + [quoteRelayer, quoteExclusivitySeconds, quoteId, zapNative, zapData], + ] = defaultAbiCoder.decode(savedBridgeParams, data) + return { + paramsV1: { + originSender, + destChainId: destChainId.toNumber(), + destEngineID: destEngineID.toNumber(), + destRelayRecipient, + destRelayToken, + destRelayAmount, + }, + paramsV2: { + quoteRelayer, + quoteExclusivitySeconds, + quoteId, + zapNative, + zapData, + }, + } +} diff --git a/packages/sdk-router/src/rfq/quote.test.ts b/packages/sdk-router/src/rfq/quote.test.ts index 253df3da5a..07f3a0872c 100644 --- a/packages/sdk-router/src/rfq/quote.test.ts +++ b/packages/sdk-router/src/rfq/quote.test.ts @@ -1,143 +1,12 @@ -import { BigNumber, parseFixed } from '@ethersproject/bignumber' +import { BigNumber } from '@ethersproject/bignumber' import { FastBridgeQuote, FastBridgeQuoteAPI, marshallFastBridgeQuote, unmarshallFastBridgeQuote, - applyQuote, } from './quote' -const createZeroAmountTests = (quote: FastBridgeQuote) => { - describe('Returns zero', () => { - it('If origin amount is zero', () => { - expect(applyQuote(quote, BigNumber.from(0))).toEqual(BigNumber.from(0)) - }) - - it('If origin amount is lower than fixed fee', () => { - expect(applyQuote(quote, quote.fixedFee.sub(1))).toEqual( - BigNumber.from(0) - ) - }) - - it('If origin amount is equal to fixed fee', () => { - expect(applyQuote(quote, quote.fixedFee)).toEqual(BigNumber.from(0)) - }) - - it('If origin amount is greater than max origin amount + fixed fee', () => { - const amount = quote.maxOriginAmount.add(quote.fixedFee).add(1) - expect(applyQuote(quote, amount)).toEqual(BigNumber.from(0)) - }) - }) - - describe('Returns non-zero', () => { - it('If origin amount is equal to max origin amount', () => { - expect(applyQuote(quote, quote.maxOriginAmount)).not.toEqual( - BigNumber.from(0) - ) - }) - - it('If origin amount is 1 wei greater than max origin amount', () => { - const amount = quote.maxOriginAmount.add(1) - expect(applyQuote(quote, amount)).not.toEqual(BigNumber.from(0)) - }) - - it('If origin amount is max origin amount + fixed fee', () => { - const amount = quote.maxOriginAmount.add(quote.fixedFee) - expect(applyQuote(quote, amount)).not.toEqual(BigNumber.from(0)) - }) - }) -} - -const createCorrectAmountTest = ( - quote: FastBridgeQuote, - amount: BigNumber, - expected: BigNumber -) => { - it(`${amount.toString()} -> ${expected.toString()}`, () => { - expect(applyQuote(quote, amount)).toEqual(expected) - }) -} - -const createQuoteTests = ( - quoteTemplate: FastBridgeQuote, - originDecimals: number, - destDecimals: number -) => { - describe(`Origin decimals: ${originDecimals}, dest decimals: ${destDecimals}`, () => { - describe(`origin:destination price 1:1`, () => { - const quote: FastBridgeQuote = { - ...quoteTemplate, - maxOriginAmount: parseFixed('100000', originDecimals), - destAmount: parseFixed('100000', destDecimals), - fixedFee: parseFixed('1', originDecimals), - } - - // 10 origin -> 9 dest - createCorrectAmountTest( - quote, - parseFixed('10', originDecimals), - parseFixed('9', destDecimals) - ) - createZeroAmountTests(quote) - }) - - describe(`origin:destination price 1:1.0001`, () => { - const quote: FastBridgeQuote = { - ...quoteTemplate, - maxOriginAmount: parseFixed('100000', originDecimals), - destAmount: parseFixed('100010', destDecimals), - fixedFee: parseFixed('1', originDecimals), - } - - // 10 origin -> 9.0009 dest - createCorrectAmountTest( - quote, - parseFixed('10', originDecimals), - parseFixed('9.0009', destDecimals) - ) - createZeroAmountTests(quote) - }) - - describe(`origin:destination price 1:0.9999`, () => { - const quote: FastBridgeQuote = { - ...quoteTemplate, - maxOriginAmount: parseFixed('100000', originDecimals), - destAmount: parseFixed('99990', destDecimals), - fixedFee: parseFixed('1', originDecimals), - } - - // 10 origin -> 8.9991 dest - createCorrectAmountTest( - quote, - parseFixed('10', originDecimals), - parseFixed('8.9991', destDecimals) - ) - createZeroAmountTests(quote) - }) - }) -} - -const createRoundDownTest = ( - quoteTemplate: FastBridgeQuote, - maxOriginAmount: BigNumber, - destAmount: BigNumber, - fixedFee: BigNumber, - amountIn: BigNumber, - expected: BigNumber -) => { - describe(`Rounds down with price ${maxOriginAmount.toString()} -> ${destAmount.toString()} and fixed fee ${fixedFee.toString()}`, () => { - const quote: FastBridgeQuote = { - ...quoteTemplate, - maxOriginAmount, - destAmount, - fixedFee, - } - - createCorrectAmountTest(quote, amountIn, expected) - }) -} - describe('quote', () => { const quoteAPI: FastBridgeQuoteAPI = { origin_chain_id: 1, @@ -180,70 +49,4 @@ describe('quote', () => { it('should marshall a quote', () => { expect(marshallFastBridgeQuote(quote)).toEqual(quoteAPI) }) - - describe('applyQuote', () => { - // Equal decimals - createQuoteTests(quote, 18, 18) - createRoundDownTest( - quote, - parseFixed('1234', 18), - parseFixed('2345', 18), - parseFixed('1', 18), - parseFixed('2', 18), - // (2 - 1) * 2345 / 1234 = 1.900324149108589951 - BigNumber.from('1900324149108589951') - ) - - // // Bigger decimals - createQuoteTests(quote, 6, 18) - createRoundDownTest( - quote, - parseFixed('1234', 6), - parseFixed('2345', 18), - parseFixed('1', 6), - parseFixed('2', 6), - // (2 - 1) * 2345 / 1234 = 1.900324149108589951 - BigNumber.from('1900324149108589951') - ) - - // Smaller decimals - createQuoteTests(quote, 18, 6) - createRoundDownTest( - quote, - parseFixed('1234', 18), - parseFixed('2345', 6), - parseFixed('1', 18), - parseFixed('2', 18), - // (2 - 1) * 2345 / 1234 = 1.900324149108589951 - BigNumber.from('1900324') - ) - - it('Returns zero when max origin amount is zero', () => { - const zeroQuote: FastBridgeQuote = { - ...quote, - maxOriginAmount: BigNumber.from(0), - } - const amount = zeroQuote.fixedFee.mul(2) - expect(applyQuote(zeroQuote, amount)).toEqual(BigNumber.from(0)) - }) - - it('Returns zero when dest amount is zero', () => { - const zeroQuote: FastBridgeQuote = { - ...quote, - destAmount: BigNumber.from(0), - } - const amount = zeroQuote.fixedFee.mul(2) - expect(applyQuote(zeroQuote, amount)).toEqual(BigNumber.from(0)) - }) - - it('Returns zero when max origin amount and dest amount are zero', () => { - const zeroQuote: FastBridgeQuote = { - ...quote, - maxOriginAmount: BigNumber.from(0), - destAmount: BigNumber.from(0), - } - const amount = zeroQuote.fixedFee.mul(2) - expect(applyQuote(zeroQuote, amount)).toEqual(BigNumber.from(0)) - }) - }) }) diff --git a/packages/sdk-router/src/rfq/quote.ts b/packages/sdk-router/src/rfq/quote.ts index 5d8816d1f7..714c9bdb29 100644 --- a/packages/sdk-router/src/rfq/quote.ts +++ b/packages/sdk-router/src/rfq/quote.ts @@ -1,5 +1,4 @@ import { BigNumber } from 'ethers' -import { Zero } from '@ethersproject/constants' import { Ticker } from './ticker' @@ -69,21 +68,3 @@ export const marshallFastBridgeQuote = ( updated_at: new Date(quote.updatedAt).toISOString(), } } - -export const applyQuote = ( - quote: FastBridgeQuote, - originAmount: BigNumber -): BigNumber => { - // Check that the origin amount covers the fixed fee - if (originAmount.lte(quote.fixedFee)) { - return Zero - } - // Check that the Relayer is able to process the origin amount (post fixed fee) - const amountAfterFee = originAmount.sub(quote.fixedFee) - if (amountAfterFee.gt(quote.maxOriginAmount)) { - return Zero - } - // After these checks: 0 < amountAfterFee <= quote.maxOriginAmount - // Solve (amountAfterFee -> ?) using (maxOriginAmount -> destAmount) pricing ratio - return amountAfterFee.mul(quote.destAmount).div(quote.maxOriginAmount) -} diff --git a/packages/sdk-router/src/rfq/sir.ts b/packages/sdk-router/src/rfq/sir.ts new file mode 100644 index 0000000000..1ac6852621 --- /dev/null +++ b/packages/sdk-router/src/rfq/sir.ts @@ -0,0 +1,242 @@ +import { Provider } from '@ethersproject/abstract-provider' +import invariant from 'tiny-invariant' +import { Contract, PopulatedTransaction } from '@ethersproject/contracts' +import { Interface } from '@ethersproject/abi' +import { BigNumber } from '@ethersproject/bignumber' +import { hexlify } from '@ethersproject/bytes' +import { AddressZero, MaxUint256, Zero } from '@ethersproject/constants' + +import fastBridgeV2Abi from '../abi/FastBridgeV2.json' +import previewerAbi from '../abi/SynapseIntentPreviewer.json' +import synapseIntentRouterAbi from '../abi/SynapseIntentRouter.json' +import { + FastBridgeV2 as FastBridgeV2Contract, + IFastBridge, +} from '../typechain/FastBridgeV2' +import { SynapseIntentRouter as SIRContract } from '../typechain/SynapseIntentRouter' +import { BigintIsh } from '../constants' +import { SynapseModule, CCTPRouterQuery } from '../module' +import { getMatchingTxLog } from '../utils/logs' +import { adjustValueIfNative, isNativeToken } from '../utils/handleNativeToken' +import { CACHE_TIMES, RouterCache } from '../utils/RouterCache' +import { USER_SIMULATED_ADDRESS } from './engine' +import { decodeSavedBridgeParams } from './paramsV2' +import { StepParams, decodeStepParams } from './steps' +import { decodeZapData, encodeZapData } from './zapData' +import { isSameAddress } from '../utils/addressUtils' +import { logger } from '../utils/logger' + +export class SynapseIntentRouter implements SynapseModule { + static fastBridgeV2Interface = new Interface(fastBridgeV2Abi) + static previewerInterface = new Interface(previewerAbi) + static sirInterface = new Interface(synapseIntentRouterAbi) + + public readonly address: string + public readonly chainId: number + public readonly provider: Provider + + private readonly fastBridgeV2Contract: FastBridgeV2Contract + private readonly sirContract: SIRContract + private readonly tokenZapAddress: string + + // All possible events emitted by the FastBridgeV2 contract in the origin transaction (in alphabetical order) + private readonly originEvents = ['BridgeRequested'] + + constructor( + chainId: number, + provider: Provider, + contracts: { + fastBridgeV2Address: string + previewerAddress: string + sirAddress: string + swapQuoterAddress: string + tokenZapAddress: string + } + ) { + invariant(chainId, 'CHAIN_ID_UNDEFINED') + invariant(provider, 'PROVIDER_UNDEFINED') + invariant(contracts.fastBridgeV2Address, 'ADDRESS_UNDEFINED') + invariant(contracts.previewerAddress, 'ADDRESS_UNDEFINED') + invariant(contracts.sirAddress, 'ADDRESS_UNDEFINED') + invariant(contracts.swapQuoterAddress, 'ADDRESS_UNDEFINED') + invariant(contracts.tokenZapAddress, 'ADDRESS_UNDEFINED') + invariant(SynapseIntentRouter.fastBridgeV2Interface, 'INTERFACE_UNDEFINED') + invariant(SynapseIntentRouter.previewerInterface, 'INTERFACE_UNDEFINED') + invariant(SynapseIntentRouter.sirInterface, 'INTERFACE_UNDEFINED') + this.chainId = chainId + this.provider = provider + this.address = contracts.sirAddress + this.tokenZapAddress = contracts.tokenZapAddress + + this.fastBridgeV2Contract = new Contract( + contracts.fastBridgeV2Address, + fastBridgeV2Abi, + provider + ) as FastBridgeV2Contract + + this.sirContract = new Contract( + contracts.sirAddress, + SynapseIntentRouter.sirInterface, + provider + ) as SIRContract + } + + /** + * @inheritdoc SynapseModule.bridge + */ + public async bridge( + to: string, + destChainId: number, + token: string, + amount: BigintIsh, + originQuery: CCTPRouterQuery, + destQuery: CCTPRouterQuery + ): Promise { + // Merge the preparation and final steps + const steps: StepParams[] = [ + ...this.getPrepSteps(originQuery), + await this.getFinalStep(to, destChainId, originQuery.tokenOut, destQuery), + ] + if (isNativeToken(token)) { + steps[0].msgValue = BigNumber.from(amount) + } + // Get data for the complete intent transaction + const populatedTransaction = + await this.sirContract.populateTransaction.completeIntentWithBalanceChecks( + this.tokenZapAddress, + amount, + originQuery.deadline, + steps + ) + // Adjust the tx.value if the initial token is native + return adjustValueIfNative( + populatedTransaction, + token, + BigNumber.from(amount) + ) + } + + /** + * @inheritdoc SynapseModule.getSynapseTxId + */ + public async getSynapseTxId(txHash: string): Promise { + // TODO: this should support older instances of FastBridge to track legacy txs + const fastBridgeLog = await getMatchingTxLog( + this.provider, + txHash, + this.fastBridgeV2Contract, + this.originEvents + ) + // transactionId always exists in the log as we are using the correct ABI + const parsedLog = + this.fastBridgeV2Contract.interface.parseLog(fastBridgeLog) + return parsedLog.args.transactionId + } + + /** + * @inheritdoc SynapseModule.getBridgeTxStatus + */ + public async getBridgeTxStatus(synapseTxId: string): Promise { + // TODO: this should support older instances of FastBridge to track legacy txs + return this.fastBridgeV2Contract.bridgeRelays(synapseTxId) + } + + // ══════════════════════════════════════════════ FAST BRIDGE V2 ═══════════════════════════════════════════════════ + + /** + * @returns The protocol fee rate, multiplied by 1_000_000 (e.g. 1 basis point = 100). + */ + @RouterCache(CACHE_TIMES.TEN_MINUTES) + public async getProtocolFeeRate(): Promise { + return this.fastBridgeV2Contract.protocolFeeRate() + } + + // ═════════════════════════════════════════════════ SIR TOOLS ═════════════════════════════════════════════════════ + + private getPrepSteps(originQuery: CCTPRouterQuery): StepParams[] { + const prepSteps = decodeStepParams(originQuery.rawParams) + // Check that the minAmountOut matches the last step + if (prepSteps.length > 0) { + const decodedLastStep = decodeZapData( + hexlify(prepSteps[prepSteps.length - 1].zapData) + ) + if ( + !decodedLastStep.minFinalAmount || + !decodedLastStep.minFinalAmount.eq(originQuery.minAmountOut) + ) { + logger.error( + { + decodedLastStep, + originQuery, + }, + 'Mismatch in minAmountOut in last step' + ) + throw new Error('Mismatch in minAmountOut in last step') + } + } + return prepSteps + } + + private async getFinalStep( + to: string, + dstChainId: number, + originToken: string, + dstQuery: CCTPRouterQuery + ): Promise { + // We should have saved neccessary params within dstQuery.rawParams + if (dstQuery.rawParams.length <= 2) { + throw new Error('Missing bridge params for FastBridgeV2') + } + const { paramsV1, paramsV2 } = decodeSavedBridgeParams(dstQuery.rawParams) + const dstZapData = decodeZapData(hexlify(paramsV2.zapData)) + if (paramsV1.destChainId !== dstChainId) { + throw new Error('Wrong destination chain ID for FastBridgeV2') + } + if (paramsV1.originSender === AddressZero) { + throw new Error('Missing sender address for FastBridgeV2') + } + if (paramsV1.destRelayRecipient === AddressZero) { + throw new Error('Missing recipient address for FastBridgeV2') + } + // Override the simulated forward address if it was used. + if (isSameAddress(paramsV1.destRelayRecipient, USER_SIMULATED_ADDRESS)) { + paramsV1.destRelayRecipient = to + } + if (isSameAddress(dstZapData.forwardTo, USER_SIMULATED_ADDRESS)) { + paramsV2.zapData = encodeZapData({ + ...dstZapData, + forwardTo: to, + }) + } + const bridgeParamsV1: IFastBridge.BridgeParamsStruct = { + dstChainId, + sender: paramsV1.originSender, + to: paramsV1.destRelayRecipient, + originToken, + destToken: paramsV1.destRelayToken, + // Will be set in encodeZapData below + originAmount: 0, + destAmount: paramsV1.destRelayAmount, + sendChainGas: false, + deadline: dstQuery.deadline, + } + const fastBridgeV2CallData = + this.fastBridgeV2Contract.interface.encodeFunctionData('bridgeV2', [ + bridgeParamsV1, + paramsV2, + ]) + // Amount is the 6-th parameter within the FastBridgeV2 call + const originZapData = encodeZapData({ + target: this.fastBridgeV2Contract.address, + payload: fastBridgeV2CallData, + amountPosition: 4 + 32 * 5, + }) + return { + token: originToken, + // Use the full balance for the Zap action + amount: MaxUint256, + msgValue: Zero, + zapData: originZapData, + } + } +} diff --git a/packages/sdk-router/src/rfq/sirSet.ts b/packages/sdk-router/src/rfq/sirSet.ts new file mode 100644 index 0000000000..ec25f14a99 --- /dev/null +++ b/packages/sdk-router/src/rfq/sirSet.ts @@ -0,0 +1,695 @@ +import { Provider } from '@ethersproject/abstract-provider' +import { BigNumber } from '@ethersproject/bignumber' +import invariant from 'tiny-invariant' +import { AddressZero, Zero } from '@ethersproject/constants' +import { hexDataLength, hexlify } from '@ethersproject/bytes' +import NodeCache from 'node-cache' + +import { + BigintIsh, + FAST_BRIDGE_V2_ADDRESS_MAP, + SYNAPSE_INTENT_PREVIEWER_ADDRESS_MAP, + SYNAPSE_INTENT_ROUTER_ADDRESS_MAP, + SWAP_QUOTER_V2_ADDRESS_MAP, + TOKEN_ZAP_V1_ADDRESS_MAP, + MEDIAN_TIME_RFQ, +} from '../constants' +import { + BridgeRoute, + FeeConfig, + Query, + SynapseModule, + SynapseModuleSet, + createNoSwapQuery, + applySlippageToQuery, + CCTPRouterQuery, +} from '../module' +import { SynapseIntentRouter } from './sir' +import { ChainProvider } from '../router' +import { ONE_HOUR, TEN_MINUTES } from '../utils/deadlines' +import { logExecutionTime, logger } from '../utils/logger' +import { isSameAddress } from '../utils/addressUtils' +import { marshallTicker, Ticker } from './ticker' +import { + getAllQuotes, + getBestRelayerQuote, + QuoteRequestOptions, + RelayerQuote, +} from './api' +import { + EngineSet, + SwapEngineRoute, + USER_SIMULATED_ADDRESS, + Recipient, + RecipientEntity, + validateEngineID, + Slippage, + applySlippage, + SwapEngineQuote, + RouteInput, + EngineTimeout, +} from './engine' +import { + BridgeParamsV2, + decodeSavedBridgeParams, + encodeSavedBridgeParams, + SavedParamsV1, +} from './paramsV2' +import { modifyMinFinalAmount } from './zapData' +import { + decodeStepParams, + encodeStepParams, + extractSingleZapData, +} from './steps' +import { FastBridgeQuote } from './quote' + +type IntentFragment = { + input: RouteInput + quote: SwapEngineQuote + finalAmount: BigNumber +} + +type Intent = { + ticker: Ticker + originFragment: IntentFragment + destFragment: IntentFragment + relayerQuote: RelayerQuote + originRoute: SwapEngineRoute + destRoute: SwapEngineRoute +} + +type DestQueryData = { + tokenOut: string + minAmountOut: BigNumber + deadline: BigNumber + paramsV1?: SavedParamsV1 + paramsV2: BridgeParamsV2 +} + +export class SynapseIntentRouterSet extends SynapseModuleSet { + public readonly bridgeModuleName = 'SynapseIntents' + public readonly allEvents = ['BridgeRequestedEvent', 'BridgeRelayedEvent'] + + public routers: { + [chainId: number]: SynapseIntentRouter + } + public providers: { + [chainId: number]: Provider + } + + private engineSet: EngineSet + private cache = new NodeCache() + + constructor(chains: ChainProvider[]) { + super() + this.routers = {} + this.providers = {} + chains.forEach(({ chainId, provider }) => { + const sirAddress = SYNAPSE_INTENT_ROUTER_ADDRESS_MAP[chainId] + // Skip chains without a SynapseIntentRouter address + if (sirAddress) { + this.routers[chainId] = new SynapseIntentRouter(chainId, provider, { + fastBridgeV2Address: FAST_BRIDGE_V2_ADDRESS_MAP[chainId], + previewerAddress: SYNAPSE_INTENT_PREVIEWER_ADDRESS_MAP[chainId], + sirAddress, + swapQuoterAddress: SWAP_QUOTER_V2_ADDRESS_MAP[chainId], + tokenZapAddress: TOKEN_ZAP_V1_ADDRESS_MAP[chainId], + }) + this.providers[chainId] = provider + } + }) + this.engineSet = new EngineSet(chains) + } + + /** + * @inheritdoc SynapseModuleSet.getModule + */ + public getModule(chainId: number): SynapseModule | undefined { + return this.routers[chainId] + } + + /** + * @inheritdoc SynapseModuleSet.getEstimatedTime + */ + public getEstimatedTime(chainId: number): number { + const medianTime = MEDIAN_TIME_RFQ[chainId as keyof typeof MEDIAN_TIME_RFQ] + invariant(medianTime, `No estimated time for chain ${chainId}`) + return medianTime + } + + /** + * @inheritdoc SynapseModuleSet.getGasDropAmount + */ + public async getGasDropAmount(): Promise { + return Zero + } + + /** + * @inheritdoc SynapseModuleSet.getBridgeRoutes + */ + @logExecutionTime('SynapseIntents.getBridgeRoutes') + public async getBridgeRoutes( + originChainId: number, + destChainId: number, + tokenIn: string, + tokenOut: string, + amountIn: BigintIsh, + originUserAddress?: string + ): Promise { + // Check that Routers exist on both chains + if (!this.getModule(originChainId) || !this.getModule(destChainId)) { + return [] + } + // Get all tickers that can be used between the two chains + const tickers = await this.getAllTickers(originChainId, destChainId) + const protocolFeeRate = await this.getSynapseIntentRouter( + originChainId + ).getProtocolFeeRate() + const quotes = await Promise.all( + tickers.map(async (ticker) => + this.getTickerQuote( + ticker, + tokenIn, + tokenOut, + amountIn, + protocolFeeRate, + originUserAddress + ) + ) + ) + return quotes + .filter((quote): quote is Intent => !!quote) + .filter(({ destRoute }) => destRoute.expectedAmountOut.gt(Zero)) + .map((quote) => ({ + bridgeModuleName: this.bridgeModuleName, + originChainId, + destChainId, + bridgeToken: { + symbol: marshallTicker(quote.ticker), + token: quote.ticker.destToken.token, + }, + originQuery: this.engineSet.getOriginQuery( + originChainId, + quote.originRoute, + quote.ticker.originToken.token + ), + originAmountOut: quote.originFragment.finalAmount, + destQuery: this.getRFQDestinationQuery({ + tokenOut, + minAmountOut: quote.destRoute.expectedAmountOut, + // The default deadline will be overridden later in `finalizeBridgeRoute` + deadline: Zero, + paramsV1: this.getSavedParamsV1(quote, originUserAddress), + paramsV2: this.getBridgeParamsV2(quote.destRoute), + }), + destAmountIn: quote.relayerQuote.destAmount, + destAmountOut: quote.destRoute.expectedAmountOut, + })) + } + + /** + * @inheritdoc SynapseModuleSet.getFeeData + */ + public async getFeeData(bridgeRoute: BridgeRoute): Promise<{ + feeAmount: BigNumber + feeConfig: FeeConfig + }> { + // Origin Out vs Dest In is the effective fee + const feeAmount = bridgeRoute.originAmountOut.sub(bridgeRoute.destAmountIn) + return { + feeAmount: feeAmount.lt(Zero) ? Zero : feeAmount, + feeConfig: { + bridgeFee: 0, + minFee: BigNumber.from(0), + maxFee: BigNumber.from(0), + }, + } + } + + /** + * @inheritdoc SynapseModuleSet.getDefaultPeriods + */ + public getDefaultPeriods(): { + originPeriod: number + destPeriod: number + } { + return { + originPeriod: TEN_MINUTES, + destPeriod: 2 * ONE_HOUR, + } + } + + /** + * @inheritdoc SynapseModuleSet.applySlippage + */ + public applySlippage( + originQueryPrecise: Query, + destQueryPrecise: Query, + slipNumerator: number, + slipDenominator: number + ): { originQuery: Query; destQuery: Query } { + // We should have saved neccessary params within dstQuery.rawParams + if (hexDataLength(destQueryPrecise.rawParams) === 0) { + logger.warn( + 'No params saved in destQuery.rawParams, slippage is not applied' + ) + return { + originQuery: originQueryPrecise, + destQuery: destQueryPrecise, + } + } + // Find out the quoted destAmount for the RFQ token + const { paramsV1, paramsV2 } = decodeSavedBridgeParams( + destQueryPrecise.rawParams + ) + if ( + isSameAddress(paramsV1.destRelayToken, AddressZero) || + paramsV1.destRelayAmount.eq(0) + ) { + logger.warn( + 'No destToken or destAmount saved in destQuery.rawParams, slippage is not applied' + ) + return { + originQuery: originQueryPrecise, + destQuery: destQueryPrecise, + } + } + const slippage = { + numerator: slipNumerator, + denominator: slipDenominator, + } + return { + originQuery: this.applyOriginSlippage( + originQueryPrecise, + paramsV1.destRelayAmount, + slippage + ), + destQuery: this.applyDestinationSlippage( + destQueryPrecise, + paramsV1, + paramsV2, + slippage + ), + } + } + + private applyOriginSlippage( + originQueryPrecise: Query, + destRelayAmount: BigNumber, + slippage: Slippage + ): Query { + // Do nothing if there are no Zap steps. + if (hexDataLength(originQueryPrecise.rawParams) === 0) { + return originQueryPrecise + } + // Max slippage for the origin swap is 5% of the (destAmount - originAmount). + // Anything over that might lead to quote that the Relayers will not process. + const maxOriginSlippage = originQueryPrecise.minAmountOut + .sub(destRelayAmount) + .div(20) + // TODO: figure out a better way to handle destAmount > originAmount + const minAmountFinalAmount = maxOriginSlippage.isNegative() + ? originQueryPrecise.minAmountOut + : originQueryPrecise.minAmountOut.sub(maxOriginSlippage) + const originQuery = applySlippageToQuery( + originQueryPrecise, + slippage.numerator, + slippage.denominator + ) + if (originQuery.minAmountOut.lt(minAmountFinalAmount)) { + originQuery.minAmountOut = minAmountFinalAmount + } + // Adjust the slippage in the last origin step. + const originSteps = decodeStepParams(originQueryPrecise.rawParams) + if (originSteps.length === 0) { + logger.error({ originQueryPrecise }, 'No steps in originQueryPrecise') + return originQuery + } + originSteps[originSteps.length - 1].zapData = modifyMinFinalAmount( + hexlify(originSteps[originSteps.length - 1].zapData), + originQuery.minAmountOut + ) + originQuery.rawParams = encodeStepParams(originSteps) + return originQuery + } + + private applyDestinationSlippage( + destQueryPrecise: Query, + paramsV1: SavedParamsV1, + paramsV2: BridgeParamsV2, + slippage: Slippage + ): Query { + // Check that engineID is within range + if (!validateEngineID(paramsV1.destEngineID)) { + throw new Error(`Invalid engineID: ${paramsV1.destEngineID}`) + } + const oldZapData = hexlify(paramsV2.zapData) + // Do nothing if there is no Zap on the destination chain. + if (hexDataLength(oldZapData) === 0) { + return destQueryPrecise + } + // Regenarate ZapData with the new minAmountOut + const minAmountOut = applySlippage(destQueryPrecise.minAmountOut, slippage) + const zapData = modifyMinFinalAmount(oldZapData, minAmountOut) + return this.getRFQDestinationQuery({ + tokenOut: destQueryPrecise.tokenOut, + minAmountOut, + deadline: destQueryPrecise.deadline, + paramsV1, + paramsV2: { + ...paramsV2, + zapData, + }, + }) + } + + /** + * Returns the existing SynapseIntentRouter instance for the given chain. + * + * @throws Will throw an error if SynapseIntentRouter is not deployed on the given chain. + */ + public getSynapseIntentRouter(chainId: number): SynapseIntentRouter { + return this.getExistingModule(chainId) as SynapseIntentRouter + } + + /** + * Applies the protocol fee to the amount. + * + * @returns The amount after the fee. + */ + public applyProtocolFeeRate( + amount: BigNumber, + protocolFeeRate: BigNumber + ): BigNumber { + const protocolFee = amount.mul(protocolFeeRate).div(1_000_000) + return amount.sub(protocolFee) + } + + @logExecutionTime('SynapseIntents.getTickerQuote') + private async getTickerQuote( + ticker: Ticker, + tokenIn: string, + tokenOut: string, + amountIn: BigintIsh, + protocolFeeRate: BigNumber, + originUserAddress?: string + ): Promise { + const originFragment = await this.getOriginQuote( + ticker, + tokenIn, + amountIn, + protocolFeeRate + ) + if (!originFragment) { + return + } + const destFragmentSimulated = await this.getDestinationQuote( + ticker, + originFragment, + tokenOut + ) + if (!destFragmentSimulated) { + return + } + const relayerQuote = await this.getRelayerQuote( + ticker, + originFragment, + destFragmentSimulated, + originUserAddress + ) + if (!relayerQuote) { + return + } + const { destFragment, originRoute, destRoute } = await this.generateRoutes( + originFragment, + destFragmentSimulated, + relayerQuote + ) + if (!destFragment || !originRoute || !destRoute) { + return + } + return { + ticker, + originFragment, + destFragment, + relayerQuote, + originRoute, + destRoute, + } + } + + @logExecutionTime('SynapseIntents.getOriginQuote') + private async getOriginQuote( + ticker: Ticker, + tokenIn: string, + amountIn: BigintIsh, + protocolFeeRate: BigNumber + ): Promise { + const finalRecipient: Recipient = { + entity: RecipientEntity.Self, + address: this.engineSet.getTokenZap(ticker.originToken.chainId), + } + // Swap complexity is not restricted on the origin chain, where execution is done by the user at the time of bridging. + const input: RouteInput = { + chainId: ticker.originToken.chainId, + tokenIn, + tokenOut: ticker.originToken.token, + amountIn, + finalRecipient, + restrictComplexity: false, + } + const quote = await this.engineSet.getBestQuote(input, { + allowMultiStep: true, + }) + if (!quote) { + return + } + return { + input, + quote, + finalAmount: this.applyProtocolFeeRate( + quote.expectedAmountOut, + protocolFeeRate + ), + } + } + + @logExecutionTime('SynapseIntents.getDestinationQuote') + private async getDestinationQuote( + ticker: Ticker, + originFragment: IntentFragment, + tokenOut: string + ): Promise { + const finalRecipient: Recipient = { + entity: RecipientEntity.UserSimulated, + address: USER_SIMULATED_ADDRESS, + } + // Swap complexity is restricted on the destination chain, where execution is done by the Relayers with a delay. + const input: RouteInput = { + chainId: ticker.destToken.chainId, + tokenIn: ticker.destToken.token, + tokenOut, + amountIn: originFragment.finalAmount, + finalRecipient, + restrictComplexity: true, + } + const quote = await this.engineSet.getBestQuote(input, { + allowMultiStep: false, + }) + if (!quote) { + return + } + return { + input, + quote, + finalAmount: quote.expectedAmountOut, + } + } + + @logExecutionTime('SynapseIntents.getRelayerQuote') + private async getRelayerQuote( + ticker: Ticker, + originFragment: IntentFragment, + destFragment: IntentFragment, + originUserAddress?: string + ): Promise { + // Note: we leave the default max slippage from `generateRoute` here to ensure that the Relayer simulation + // succeeds even in the even that on-chain price moves. We will overwrite this later in `generateRoutes`. + const destRoute = await this.engineSet.generateRoute( + destFragment.input, + destFragment.quote, + { allowMultiStep: false, useZeroSlippage: false } + ) + if (!destRoute) { + return + } + // intent.destQuote is generated by using `originAmountOut` as the input amount on the destination chain. + // The Relayers will also use `originAmountOut` as the input amount for simulatiion purposes as per RFQ API spec. + return this.apiGetBestRelayerQuote(ticker, originFragment.finalAmount, { + originSender: originUserAddress, + destRecipient: this.getRelayRecipient(destRoute), + zapData: extractSingleZapData(destRoute.steps), + }) + } + + @logExecutionTime('SynapseIntents.generateRoutes') + private async generateRoutes( + originFragment: IntentFragment, + destFragmentSimulated: IntentFragment, + relayerQuote: RelayerQuote + ): Promise<{ + destFragment?: IntentFragment + originRoute?: SwapEngineRoute + destRoute?: SwapEngineRoute + }> { + // Update `destInput` and `destQuote` with the actual values. + const destInput = { + ...destFragmentSimulated.input, + amountIn: relayerQuote.destAmount, + } + // Use longer timeout for finalizing the route. + const destQuote = await this.engineSet.getQuote( + destFragmentSimulated.quote.engineID, + destInput, + { + allowMultiStep: false, + timeout: EngineTimeout.Long, + } + ) + if (!destQuote) { + return {} + } + const destFragment = { + input: destInput, + quote: destQuote, + finalAmount: destQuote.expectedAmountOut, + } + // Final rotures will be returned with a zero slippage by default, and could be then modified + // by the SDK consumer. + const [originRoute, destRoute] = await Promise.all([ + this.engineSet.generateRoute(originFragment.input, originFragment.quote, { + allowMultiStep: true, + useZeroSlippage: true, + }), + this.engineSet.generateRoute(destInput, destQuote, { + allowMultiStep: false, + useZeroSlippage: true, + }), + ]) + return { + destFragment, + originRoute, + destRoute, + } + } + + /** + * Get all unique tickers for a given origin and destination chains. In other words, + * this is the list of all (originToken, destToken) pairs that can be used to create a quote + * for a swap between the two chains, without duplicates. + * + * @param originChainId - The ID of the origin chain. + * @param destChainId - The ID of the destination chain. + * @returns A promise that resolves to the list of tickers. + */ + private async getAllTickers( + originChainId: number, + destChainId: number + ): Promise { + const allQuotes = await this.apiGetAllQuotes() + const originFB = FAST_BRIDGE_V2_ADDRESS_MAP[originChainId] + const destFB = FAST_BRIDGE_V2_ADDRESS_MAP[destChainId] + // First, we filter out quotes for other chainIDs and bridge addresses. + // Finally, we remove the duplicates of the origin token. + return allQuotes + .filter( + (quote) => + quote.ticker.originToken.chainId === originChainId && + quote.ticker.destToken.chainId === destChainId && + isSameAddress(quote.originFastBridge, originFB) && + isSameAddress(quote.destFastBridge, destFB) + ) + .map((quote) => quote.ticker) + .filter( + (ticker, index, self) => + index === + self.findIndex((t) => + isSameAddress(t.originToken.token, ticker.originToken.token) + ) + ) + } + + @logExecutionTime('SynapseIntents.API /quotes') + private async apiGetAllQuotes(): Promise { + // Try getting cached quotes first. + const cacheKey = 'getAllQuotes' + const cachedQuotes = this.cache.get(cacheKey) + if (cachedQuotes) { + return cachedQuotes + } + // If not cached, fetch new quotes and cache them. + const data = await getAllQuotes() + this.cache.set(cacheKey, data, ONE_HOUR) + return data + } + + @logExecutionTime('SynapseIntents.API /rfq') + private async apiGetBestRelayerQuote( + ticker: Ticker, + originAmount: BigNumber, + options: QuoteRequestOptions = {} + ): Promise { + const quote = await getBestRelayerQuote(ticker, originAmount, options) + return quote.destAmount.gt(Zero) ? quote : undefined + } + + private getSavedParamsV1( + quote: Intent, + originUserAddress?: string + ): SavedParamsV1 | undefined { + if (!originUserAddress) { + return + } + return { + originSender: originUserAddress, + destChainId: quote.ticker.destToken.chainId, + destEngineID: quote.destFragment.quote.engineID, + destRelayRecipient: this.getRelayRecipient(quote.destRoute), + destRelayToken: quote.ticker.destToken.token, + destRelayAmount: quote.relayerQuote.destAmount, + } + } + + private getBridgeParamsV2(destRoute: SwapEngineRoute): BridgeParamsV2 { + return { + // TODO: exclusivity + quoteRelayer: AddressZero, + quoteExclusivitySeconds: Zero, + // TODO: quoteId + quoteId: '0x', + zapNative: Zero, + zapData: extractSingleZapData(destRoute.steps), + } + } + + private getRelayRecipient(destRoute: SwapEngineRoute): string { + // FastBridge will use TokenZap as the recipient if there are any Zap steps to perform + return destRoute.steps.length === 0 + ? USER_SIMULATED_ADDRESS + : this.engineSet.getTokenZap(destRoute.chainId) + } + + private getRFQDestinationQuery(data: DestQueryData): CCTPRouterQuery { + // Use no-swap query by default. + const destQuery = createNoSwapQuery(data.tokenOut, data.minAmountOut) + destQuery.deadline = data.deadline + if (!data.paramsV1) { + return destQuery + } + destQuery.routerAdapter = this.engineSet.getTokenZap( + data.paramsV1.destChainId + ) + destQuery.rawParams = encodeSavedBridgeParams(data.paramsV1, data.paramsV2) + return destQuery + } +} diff --git a/packages/sdk-router/src/rfq/steps.test.ts b/packages/sdk-router/src/rfq/steps.test.ts new file mode 100644 index 0000000000..3f1edfda17 --- /dev/null +++ b/packages/sdk-router/src/rfq/steps.test.ts @@ -0,0 +1,80 @@ +import { BigNumber } from 'ethers' + +import { ETH_USDC, ETH_USDT, NATIVE_ADDRESS } from '../constants/testValues' +import { + StepParams, + decodeStepParams, + encodeStepParams, + extractSingleZapData, +} from './steps' + +describe('Steps', () => { + const ether = BigNumber.from(10).pow(18) + + const param0: StepParams = { + token: NATIVE_ADDRESS, + amount: ether.mul(1), + msgValue: ether.mul(2), + zapData: '0x', + } + + const param1: StepParams = { + token: ETH_USDC, + amount: ether.mul(3), + msgValue: ether.mul(4), + zapData: '0xdeadbeef', + } + + const param2: StepParams = { + token: ETH_USDT, + amount: ether.mul(5), + msgValue: ether.mul(6), + zapData: '0x00112233445566778899aabbccddeeff', + } + + it('roundtrip empty', () => { + const data = encodeStepParams([]) + expect(decodeStepParams(data)).toEqual([]) + }) + + it('roundtrip with one step', () => { + const data = encodeStepParams([param0]) + expect(decodeStepParams(data)).toEqual([param0]) + }) + + it('roundtrip with two steps', () => { + const data = encodeStepParams([param0, param1]) + expect(decodeStepParams(data)).toEqual([param0, param1]) + }) + + it('roundtrip with three steps', () => { + const data = encodeStepParams([param0, param1, param2]) + expect(decodeStepParams(data)).toEqual([param0, param1, param2]) + }) + + describe('extractSingleZapData', () => { + it('no steps', () => { + expect(extractSingleZapData([])).toEqual('0x') + }) + + it('single step', () => { + expect(extractSingleZapData([param0])).toEqual('0x') + expect(extractSingleZapData([param1])).toEqual('0xdeadbeef') + expect(extractSingleZapData([param2])).toEqual( + '0x00112233445566778899aabbccddeeff' + ) + }) + + it('multiple steps', () => { + expect(() => extractSingleZapData([param0, param1])).toThrowError( + 'extractSingleZapData: more than one step' + ) + expect(() => extractSingleZapData([param0, param2])).toThrowError( + 'extractSingleZapData: more than one step' + ) + expect(() => extractSingleZapData([param1, param2])).toThrowError( + 'extractSingleZapData: more than one step' + ) + }) + }) +}) diff --git a/packages/sdk-router/src/rfq/steps.ts b/packages/sdk-router/src/rfq/steps.ts new file mode 100644 index 0000000000..4dfc9fe8e4 --- /dev/null +++ b/packages/sdk-router/src/rfq/steps.ts @@ -0,0 +1,48 @@ +import { defaultAbiCoder } from '@ethersproject/abi' +import { BigNumber } from '@ethersproject/bignumber' +import { hexlify } from '@ethersproject/bytes' + +import { ISynapseIntentRouter } from '../typechain/SynapseIntentRouter' + +export type StepParams = ISynapseIntentRouter.StepParamsStruct +const stepParamsArray = ['tuple(address,uint256,uint256,bytes)[]'] + +export const encodeStepParams = (steps: StepParams[]): string => { + // Check if there are any steps + if (steps.length === 0) { + return '0x' + } + // Unwrap every struct into a tuple + return defaultAbiCoder.encode(stepParamsArray, [ + steps.map((step) => [step.token, step.amount, step.msgValue, step.zapData]), + ]) +} + +export const decodeStepParams = (data: string): StepParams[] => { + // Check if there are any steps + if (data === '0x') { + return [] + } + const decoded = defaultAbiCoder.decode(stepParamsArray, data) + // decoded is [[[token0, amount0, msgValue0, zapData0], [token1, amount1, msgValue1, zapData1], ...]] + return decoded[0].map( + ([token, amount, msgValue, zapData]: [ + string, + BigNumber, + BigNumber, + string + ]) => ({ + token, + amount, + msgValue, + zapData, + }) + ) +} + +export const extractSingleZapData = (steps: StepParams[]): string => { + if (steps.length > 1) { + throw new Error('extractSingleZapData: more than one step') + } + return steps.length === 0 ? '0x' : hexlify(steps[0].zapData) +} diff --git a/packages/sdk-router/src/rfq/zapData.test.ts b/packages/sdk-router/src/rfq/zapData.test.ts new file mode 100644 index 0000000000..fbb705dc3b --- /dev/null +++ b/packages/sdk-router/src/rfq/zapData.test.ts @@ -0,0 +1,61 @@ +import { BigNumber } from 'ethers' + +import { ETH_USDC, ETH_USDT, ETH_DAI } from '../constants/testValues' +import { + encodeZapData, + decodeZapData, + modifyMinFinalAmount, + ZapDataV1, + AMOUNT_NOT_PRESENT, +} from './zapData' + +describe('zapData', () => { + const zapData: ZapDataV1 = { + target: ETH_USDC.toLowerCase(), + payload: '0xdeadbeef', + amountPosition: 1, + finalToken: ETH_USDT.toLowerCase(), + forwardTo: ETH_DAI.toLowerCase(), + minFinalAmount: BigNumber.from('1234567890123456789012345678901234567890'), + } + + const zapDataNewFinalAmount: ZapDataV1 = { + target: ETH_USDC.toLowerCase(), + payload: '0xdeadbeef', + amountPosition: 1, + finalToken: ETH_USDT.toLowerCase(), + forwardTo: ETH_DAI.toLowerCase(), + minFinalAmount: BigNumber.from('1234567890'), + } + + const zapDataEmptyPayload: ZapDataV1 = { + target: ETH_USDC.toLowerCase(), + payload: '0x', + amountPosition: AMOUNT_NOT_PRESENT, + finalToken: ETH_USDT.toLowerCase(), + forwardTo: ETH_DAI.toLowerCase(), + minFinalAmount: BigNumber.from('1234567890'), + } + + it('roundtrip encoding', () => { + const encoded = encodeZapData(zapData) + const decoded = decodeZapData(encoded) + expect(decoded).toEqual(zapData) + }) + + it('roundtrip encoding with empty payload', () => { + const encoded = encodeZapData(zapDataEmptyPayload) + const decoded = decodeZapData(encoded) + expect(decoded).toEqual(zapDataEmptyPayload) + }) + + it('roundtrip encoding with final amount modified', () => { + const encoded = encodeZapData(zapData) + const encodedModified = modifyMinFinalAmount( + encoded, + zapDataNewFinalAmount.minFinalAmount + ) + const decoded = decodeZapData(encodedModified) + expect(decoded).toEqual(zapDataNewFinalAmount) + }) +}) diff --git a/packages/sdk-router/src/rfq/zapData.ts b/packages/sdk-router/src/rfq/zapData.ts new file mode 100644 index 0000000000..ee83cbd528 --- /dev/null +++ b/packages/sdk-router/src/rfq/zapData.ts @@ -0,0 +1,114 @@ +import { AddressZero, Zero } from '@ethersproject/constants' +import { + hexConcat, + hexDataSlice, + hexDataLength, + hexZeroPad, +} from '@ethersproject/bytes' +import { BigNumber } from 'ethers' + +export const ZAP_DATA_VERSION = 1 +export const AMOUNT_NOT_PRESENT = 0xffff + +const OFFSET_AMOUNT_POSITION = 2 +const OFFSET_FINAL_TOKEN = 4 +const OFFSET_FORWARD_TO = 24 +const OFFSET_MIN_FWD_AMOUNT = 44 +const OFFSET_TARGET = 76 +const OFFSET_PAYLOAD = 96 + +export type ZapDataV1 = { + target: string + payload: string + amountPosition: number + finalToken: string + forwardTo: string + minFinalAmount: BigNumber +} + +export const encodeZapData = (zapData: Partial): string => { + if (!zapData.target) { + return '0x' + } + const { + target, + payload, + amountPosition, + finalToken, + forwardTo, + minFinalAmount, + } = applyDefaultValues(zapData) + return hexConcat([ + encodeUint16(ZAP_DATA_VERSION), + encodeUint16(amountPosition), + finalToken, + forwardTo, + encodeUint256(minFinalAmount), + target, + payload, + ]) +} + +export const decodeZapData = (zapData: string): Partial => { + if (zapData === '0x') { + return {} + } + if (hexDataLength(zapData) < OFFSET_PAYLOAD) { + throw new Error('decodeZapData: zapData too short') + } + // Offsets of the fields in the packed ZapData struct + // uint16 version [000 .. 002) + // uint16 amountPosition [002 .. 004) + // address finalToken [004 .. 024) + // address forwardTo [024 .. 044) + // uint256 minFinalAmount [044 .. 076) + // address target [076 .. 096) + // bytes payload [096 .. ***) + const version = parseInt(hexDataSlice(zapData, 0, 2), 16) + if (version !== ZAP_DATA_VERSION) { + throw new Error('decodeZapData: unsupported version') + } + return { + amountPosition: parseInt( + hexDataSlice(zapData, OFFSET_AMOUNT_POSITION, OFFSET_FINAL_TOKEN), + 16 + ), + finalToken: hexDataSlice(zapData, OFFSET_FINAL_TOKEN, OFFSET_FORWARD_TO), + forwardTo: hexDataSlice(zapData, OFFSET_FORWARD_TO, OFFSET_MIN_FWD_AMOUNT), + minFinalAmount: BigNumber.from( + hexDataSlice(zapData, OFFSET_MIN_FWD_AMOUNT, OFFSET_TARGET) + ), + target: hexDataSlice(zapData, OFFSET_TARGET, OFFSET_PAYLOAD), + payload: hexDataSlice(zapData, OFFSET_PAYLOAD), + } +} + +export const modifyMinFinalAmount = ( + zapData: string, + newMinFinalAmount: BigNumber +): string => { + const decoded = decodeZapData(zapData) + return encodeZapData({ + ...decoded, + minFinalAmount: newMinFinalAmount, + }) +} + +export const applyDefaultValues = (zapData: Partial): ZapDataV1 => { + return { + target: zapData.target || AddressZero, + payload: zapData.payload || '0x', + amountPosition: zapData.amountPosition || AMOUNT_NOT_PRESENT, + finalToken: zapData.finalToken || AddressZero, + forwardTo: zapData.forwardTo || AddressZero, + minFinalAmount: zapData.minFinalAmount || Zero, + } +} + +const encodeUint16 = (n: number): string => { + return hexZeroPad('0x' + n.toString(16), 2) +} + +const encodeUint256 = (n: BigNumber): string => { + return hexZeroPad(n.toHexString(), 32) +} diff --git a/packages/sdk-router/src/router/router.ts b/packages/sdk-router/src/router/router.ts index 133a0766bf..75b87d8714 100644 --- a/packages/sdk-router/src/router/router.ts +++ b/packages/sdk-router/src/router/router.ts @@ -8,6 +8,7 @@ import { BigintIsh } from '../constants' import { Query } from '../module/query' import { DestRequest } from './types' import { BridgeToken, FeeConfig, SynapseModule } from '../module' +import { logger } from '../utils/logger' /** * Abstract class for a router contract deployed on a chain. @@ -117,9 +118,9 @@ export abstract class Router implements SynapseModule { // Don't filter anything, as the amount of returned queries should match the amount of symbols return this.getOriginAmountOut(tokenIn, tokenSymbols, amountIn) } catch (error) { - console.error( - '[SynapseSDK: Router] Failed to fetch origin queries', - error + logger.error( + { tokenIn, tokenSymbols, amountIn, error }, + '[SynapseSDK: Router] Failed to fetch origin queries' ) throw error } @@ -141,9 +142,9 @@ export abstract class Router implements SynapseModule { // Don't filter anything, as the amount of returned queries should match the amount of requests return this.getDestinationAmountOut(requests, tokenOut) } catch (error) { - console.error( - '[SynapseSDK: Router] Failed to fetch destination queries', - error + logger.error( + { requests, tokenOut, error }, + '[SynapseSDK: Router] Failed to fetch destination queries' ) throw error } diff --git a/packages/sdk-router/src/router/routerSet.ts b/packages/sdk-router/src/router/routerSet.ts index 5155e5056b..9618adf360 100644 --- a/packages/sdk-router/src/router/routerSet.ts +++ b/packages/sdk-router/src/router/routerSet.ts @@ -17,6 +17,7 @@ import { hasComplexBridgeAction, } from '../module/query' import { ONE_WEEK, TEN_MINUTES } from '../utils/deadlines' +import { logger } from '../utils/logger' export type ChainProvider = { chainId: number @@ -130,7 +131,10 @@ export abstract class RouterSet extends SynapseModuleSet { originChainId, destChainId, originQuery: originRoute.originQuery, + originAmountOut: originRoute.originQuery.minAmountOut, destQuery: destQueries[index], + destAmountIn: originRoute.originQuery.minAmountOut, + destAmountOut: destQueries[index].minAmountOut, bridgeToken: originRoute.bridgeToken, bridgeModuleName: this.bridgeModuleName, }) @@ -140,9 +144,17 @@ export abstract class RouterSet extends SynapseModuleSet { bridgeRoute.destQuery.minAmountOut.gt(0) ) } catch (error) { - console.error( - `[SynapseSDK: RouterSet] Error when trying to calculate the best quote with bridge tokens: ${bridgeTokens} `, - error + logger.error( + { + originChainId, + destChainId, + tokenIn, + tokenOut, + amountIn, + bridgeTokens, + error, + }, + '[SynapseSDK: RouterSet] Error when trying to calculate the best quote' ) return [] } diff --git a/packages/sdk-router/src/router/synapseCCTPRouterSet.ts b/packages/sdk-router/src/router/synapseCCTPRouterSet.ts index ac003c0aa9..b0395c11d7 100644 --- a/packages/sdk-router/src/router/synapseCCTPRouterSet.ts +++ b/packages/sdk-router/src/router/synapseCCTPRouterSet.ts @@ -3,8 +3,13 @@ import { BigNumber } from '@ethersproject/bignumber' import { SynapseCCTPRouter } from './synapseCCTPRouter' import { ChainProvider, RouterSet } from './routerSet' -import { CCTP_ROUTER_ADDRESS_MAP, MEDIAN_TIME_CCTP } from '../constants' +import { + BigintIsh, + CCTP_ROUTER_ADDRESS_MAP, + MEDIAN_TIME_CCTP, +} from '../constants' import { BridgeRoute } from '../module' +import { logExecutionTime } from '../utils/logger' /** * Wrapper class for interacting with a SynapseCCTPRouter contracts deployed on multiple chains. @@ -20,6 +25,23 @@ export class SynapseCCTPRouterSet extends RouterSet { super(chains, CCTP_ROUTER_ADDRESS_MAP, SynapseCCTPRouter) } + @logExecutionTime('SynapseCCTP.getBridgeRoutes') + public async getBridgeRoutes( + originChainId: number, + destChainId: number, + tokenIn: string, + tokenOut: string, + amountIn: BigintIsh + ): Promise { + return super.getBridgeRoutes( + originChainId, + destChainId, + tokenIn, + tokenOut, + amountIn + ) + } + /** * @inheritdoc RouterSet.getOriginAmountOut */ diff --git a/packages/sdk-router/src/router/synapseRouterSet.ts b/packages/sdk-router/src/router/synapseRouterSet.ts index 778d0891ab..b577ed265a 100644 --- a/packages/sdk-router/src/router/synapseRouterSet.ts +++ b/packages/sdk-router/src/router/synapseRouterSet.ts @@ -4,8 +4,9 @@ import { Zero } from '@ethersproject/constants' import { BridgeTokenType, SynapseRouter } from './synapseRouter' import { ChainProvider, RouterSet } from './routerSet' -import { MEDIAN_TIME_BRIDGE, ROUTER_ADDRESS_MAP } from '../constants' +import { BigintIsh, MEDIAN_TIME_BRIDGE, ROUTER_ADDRESS_MAP } from '../constants' import { BridgeRoute } from '../module' +import { logExecutionTime } from '../utils/logger' /** * Wrapper class for interacting with a SynapseRouter contracts deployed on multiple chains. @@ -29,6 +30,23 @@ export class SynapseRouterSet extends RouterSet { super(chains, ROUTER_ADDRESS_MAP, SynapseRouter) } + @logExecutionTime('SynapseBridge.getBridgeRoutes') + public async getBridgeRoutes( + originChainId: number, + destChainId: number, + tokenIn: string, + tokenOut: string, + amountIn: BigintIsh + ): Promise { + return super.getBridgeRoutes( + originChainId, + destChainId, + tokenIn, + tokenOut, + amountIn + ) + } + /** * @inheritdoc RouterSet.getOriginAmountOut */ diff --git a/packages/sdk-router/src/sdk.ts b/packages/sdk-router/src/sdk.ts index 9966b57ed3..6f5560a067 100644 --- a/packages/sdk-router/src/sdk.ts +++ b/packages/sdk-router/src/sdk.ts @@ -1,7 +1,7 @@ import { Provider } from '@ethersproject/abstract-provider' import invariant from 'tiny-invariant' -import { FastBridgeRouterSet } from './rfq' +import { FastBridgeRouterSet, SynapseIntentRouterSet } from './rfq' import { SynapseRouterSet, SynapseCCTPRouterSet, @@ -16,6 +16,7 @@ class SynapseSDK { public allModuleSets: SynapseModuleSet[] public synapseRouterSet: SynapseRouterSet public synapseCCTPRouterSet: SynapseCCTPRouterSet + public synapseIntentRouterSet: SynapseIntentRouterSet public fastBridgeRouterSet: FastBridgeRouterSet public providers: { [chainId: number]: Provider } @@ -45,10 +46,12 @@ class SynapseSDK { this.synapseRouterSet = new SynapseRouterSet(chainProviders) this.synapseCCTPRouterSet = new SynapseCCTPRouterSet(chainProviders) this.fastBridgeRouterSet = new FastBridgeRouterSet(chainProviders) + this.synapseIntentRouterSet = new SynapseIntentRouterSet(chainProviders) this.allModuleSets = [ this.synapseRouterSet, this.synapseCCTPRouterSet, this.fastBridgeRouterSet, + this.synapseIntentRouterSet, ] } diff --git a/packages/sdk-router/src/typechain/FastBridgeV2.ts b/packages/sdk-router/src/typechain/FastBridgeV2.ts new file mode 100644 index 0000000000..e7b4393968 --- /dev/null +++ b/packages/sdk-router/src/typechain/FastBridgeV2.ts @@ -0,0 +1,2172 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PayableOverrides, + PopulatedTransaction, + Signer, + utils, +} from 'ethers' +import type { + FunctionFragment, + Result, + EventFragment, +} from '@ethersproject/abi' +import type { Listener, Provider } from '@ethersproject/providers' +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from './common' + +export declare namespace IFastBridge { + export type BridgeParamsStruct = { + dstChainId: BigNumberish + sender: string + to: string + originToken: string + destToken: string + originAmount: BigNumberish + destAmount: BigNumberish + sendChainGas: boolean + deadline: BigNumberish + } + + export type BridgeParamsStructOutput = [ + number, + string, + string, + string, + string, + BigNumber, + BigNumber, + boolean, + BigNumber + ] & { + dstChainId: number + sender: string + to: string + originToken: string + destToken: string + originAmount: BigNumber + destAmount: BigNumber + sendChainGas: boolean + deadline: BigNumber + } + + export type BridgeTransactionStruct = { + originChainId: BigNumberish + destChainId: BigNumberish + originSender: string + destRecipient: string + originToken: string + destToken: string + originAmount: BigNumberish + destAmount: BigNumberish + originFeeAmount: BigNumberish + sendChainGas: boolean + deadline: BigNumberish + nonce: BigNumberish + } + + export type BridgeTransactionStructOutput = [ + number, + number, + string, + string, + string, + string, + BigNumber, + BigNumber, + BigNumber, + boolean, + BigNumber, + BigNumber + ] & { + originChainId: number + destChainId: number + originSender: string + destRecipient: string + originToken: string + destToken: string + originAmount: BigNumber + destAmount: BigNumber + originFeeAmount: BigNumber + sendChainGas: boolean + deadline: BigNumber + nonce: BigNumber + } +} + +export declare namespace IFastBridgeV2 { + export type BridgeParamsV2Struct = { + quoteRelayer: string + quoteExclusivitySeconds: BigNumberish + quoteId: BytesLike + zapNative: BigNumberish + zapData: BytesLike + } + + export type BridgeParamsV2StructOutput = [ + string, + BigNumber, + string, + BigNumber, + string + ] & { + quoteRelayer: string + quoteExclusivitySeconds: BigNumber + quoteId: string + zapNative: BigNumber + zapData: string + } + + export type BridgeTransactionV2Struct = { + originChainId: BigNumberish + destChainId: BigNumberish + originSender: string + destRecipient: string + originToken: string + destToken: string + originAmount: BigNumberish + destAmount: BigNumberish + originFeeAmount: BigNumberish + deadline: BigNumberish + nonce: BigNumberish + exclusivityRelayer: string + exclusivityEndTime: BigNumberish + zapNative: BigNumberish + zapData: BytesLike + } + + export type BridgeTransactionV2StructOutput = [ + number, + number, + string, + string, + string, + string, + BigNumber, + BigNumber, + BigNumber, + BigNumber, + BigNumber, + string, + BigNumber, + BigNumber, + string + ] & { + originChainId: number + destChainId: number + originSender: string + destRecipient: string + originToken: string + destToken: string + originAmount: BigNumber + destAmount: BigNumber + originFeeAmount: BigNumber + deadline: BigNumber + nonce: BigNumber + exclusivityRelayer: string + exclusivityEndTime: BigNumber + zapNative: BigNumber + zapData: string + } +} + +export declare namespace IMulticallTarget { + export type ResultStruct = { success: boolean; returnData: BytesLike } + + export type ResultStructOutput = [boolean, string] & { + success: boolean + returnData: string + } +} + +export interface FastBridgeV2Interface extends utils.Interface { + functions: { + 'CANCELER_ROLE()': FunctionFragment + 'DEFAULT_ADMIN_ROLE()': FunctionFragment + 'DEFAULT_CANCEL_DELAY()': FunctionFragment + 'DISPUTE_PERIOD()': FunctionFragment + 'FEE_BPS()': FunctionFragment + 'FEE_RATE_MAX()': FunctionFragment + 'GOVERNOR_ROLE()': FunctionFragment + 'GUARD_ROLE()': FunctionFragment + 'MAX_ZAP_DATA_LENGTH()': FunctionFragment + 'MIN_CANCEL_DELAY()': FunctionFragment + 'MIN_DEADLINE_PERIOD()': FunctionFragment + 'NATIVE_GAS_TOKEN()': FunctionFragment + 'PROVER_ROLE()': FunctionFragment + 'QUOTER_ROLE()': FunctionFragment + 'bridge((uint32,address,address,address,address,uint256,uint256,bool,uint256))': FunctionFragment + 'bridgeProofs(bytes32)': FunctionFragment + 'bridgeRelayDetails(bytes32)': FunctionFragment + 'bridgeRelays(bytes32)': FunctionFragment + 'bridgeStatuses(bytes32)': FunctionFragment + 'bridgeTxDetails(bytes32)': FunctionFragment + 'bridgeV2((uint32,address,address,address,address,uint256,uint256,bool,uint256),(address,int256,bytes,uint256,bytes))': FunctionFragment + 'canClaim(bytes32,address)': FunctionFragment + 'cancelDelay()': FunctionFragment + 'cancelV2(bytes)': FunctionFragment + 'chainGasAmount()': FunctionFragment + 'claim(bytes,address)': FunctionFragment + 'claimV2(bytes)': FunctionFragment + 'deployBlock()': FunctionFragment + 'dispute(bytes32)': FunctionFragment + 'getBridgeTransaction(bytes)': FunctionFragment + 'getBridgeTransactionV2(bytes)': FunctionFragment + 'getRoleAdmin(bytes32)': FunctionFragment + 'getRoleMember(bytes32,uint256)': FunctionFragment + 'getRoleMemberCount(bytes32)': FunctionFragment + 'grantRole(bytes32,address)': FunctionFragment + 'hasRole(bytes32,address)': FunctionFragment + 'multicallNoResults(bytes[],bool)': FunctionFragment + 'multicallWithResults(bytes[],bool)': FunctionFragment + 'nonce()': FunctionFragment + 'protocolFeeRate()': FunctionFragment + 'protocolFees(address)': FunctionFragment + 'prove(bytes,bytes32)': FunctionFragment + 'proveV2(bytes32,bytes32,address)': FunctionFragment + 'refund(bytes)': FunctionFragment + 'relay(bytes)': FunctionFragment + 'relayV2(bytes,address)': FunctionFragment + 'renounceRole(bytes32,address)': FunctionFragment + 'revokeRole(bytes32,address)': FunctionFragment + 'senderNonces(address)': FunctionFragment + 'setCancelDelay(uint256)': FunctionFragment + 'setProtocolFeeRate(uint256)': FunctionFragment + 'supportsInterface(bytes4)': FunctionFragment + 'sweepProtocolFees(address,address)': FunctionFragment + } + + getFunction( + nameOrSignatureOrTopic: + | 'CANCELER_ROLE' + | 'DEFAULT_ADMIN_ROLE' + | 'DEFAULT_CANCEL_DELAY' + | 'DISPUTE_PERIOD' + | 'FEE_BPS' + | 'FEE_RATE_MAX' + | 'GOVERNOR_ROLE' + | 'GUARD_ROLE' + | 'MAX_ZAP_DATA_LENGTH' + | 'MIN_CANCEL_DELAY' + | 'MIN_DEADLINE_PERIOD' + | 'NATIVE_GAS_TOKEN' + | 'PROVER_ROLE' + | 'QUOTER_ROLE' + | 'bridge' + | 'bridgeProofs' + | 'bridgeRelayDetails' + | 'bridgeRelays' + | 'bridgeStatuses' + | 'bridgeTxDetails' + | 'bridgeV2' + | 'canClaim' + | 'cancelDelay' + | 'cancelV2' + | 'chainGasAmount' + | 'claim' + | 'claimV2' + | 'deployBlock' + | 'dispute' + | 'getBridgeTransaction' + | 'getBridgeTransactionV2' + | 'getRoleAdmin' + | 'getRoleMember' + | 'getRoleMemberCount' + | 'grantRole' + | 'hasRole' + | 'multicallNoResults' + | 'multicallWithResults' + | 'nonce' + | 'protocolFeeRate' + | 'protocolFees' + | 'prove' + | 'proveV2' + | 'refund' + | 'relay' + | 'relayV2' + | 'renounceRole' + | 'revokeRole' + | 'senderNonces' + | 'setCancelDelay' + | 'setProtocolFeeRate' + | 'supportsInterface' + | 'sweepProtocolFees' + ): FunctionFragment + + encodeFunctionData( + functionFragment: 'CANCELER_ROLE', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'DEFAULT_ADMIN_ROLE', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'DEFAULT_CANCEL_DELAY', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'DISPUTE_PERIOD', + values?: undefined + ): string + encodeFunctionData(functionFragment: 'FEE_BPS', values?: undefined): string + encodeFunctionData( + functionFragment: 'FEE_RATE_MAX', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'GOVERNOR_ROLE', + values?: undefined + ): string + encodeFunctionData(functionFragment: 'GUARD_ROLE', values?: undefined): string + encodeFunctionData( + functionFragment: 'MAX_ZAP_DATA_LENGTH', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'MIN_CANCEL_DELAY', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'MIN_DEADLINE_PERIOD', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'NATIVE_GAS_TOKEN', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'PROVER_ROLE', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'QUOTER_ROLE', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'bridge', + values: [IFastBridge.BridgeParamsStruct] + ): string + encodeFunctionData( + functionFragment: 'bridgeProofs', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'bridgeRelayDetails', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'bridgeRelays', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'bridgeStatuses', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'bridgeTxDetails', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'bridgeV2', + values: [IFastBridge.BridgeParamsStruct, IFastBridgeV2.BridgeParamsV2Struct] + ): string + encodeFunctionData( + functionFragment: 'canClaim', + values: [BytesLike, string] + ): string + encodeFunctionData( + functionFragment: 'cancelDelay', + values?: undefined + ): string + encodeFunctionData(functionFragment: 'cancelV2', values: [BytesLike]): string + encodeFunctionData( + functionFragment: 'chainGasAmount', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'claim', + values: [BytesLike, string] + ): string + encodeFunctionData(functionFragment: 'claimV2', values: [BytesLike]): string + encodeFunctionData( + functionFragment: 'deployBlock', + values?: undefined + ): string + encodeFunctionData(functionFragment: 'dispute', values: [BytesLike]): string + encodeFunctionData( + functionFragment: 'getBridgeTransaction', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'getBridgeTransactionV2', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'getRoleAdmin', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'getRoleMember', + values: [BytesLike, BigNumberish] + ): string + encodeFunctionData( + functionFragment: 'getRoleMemberCount', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'grantRole', + values: [BytesLike, string] + ): string + encodeFunctionData( + functionFragment: 'hasRole', + values: [BytesLike, string] + ): string + encodeFunctionData( + functionFragment: 'multicallNoResults', + values: [BytesLike[], boolean] + ): string + encodeFunctionData( + functionFragment: 'multicallWithResults', + values: [BytesLike[], boolean] + ): string + encodeFunctionData(functionFragment: 'nonce', values?: undefined): string + encodeFunctionData( + functionFragment: 'protocolFeeRate', + values?: undefined + ): string + encodeFunctionData(functionFragment: 'protocolFees', values: [string]): string + encodeFunctionData( + functionFragment: 'prove', + values: [BytesLike, BytesLike] + ): string + encodeFunctionData( + functionFragment: 'proveV2', + values: [BytesLike, BytesLike, string] + ): string + encodeFunctionData(functionFragment: 'refund', values: [BytesLike]): string + encodeFunctionData(functionFragment: 'relay', values: [BytesLike]): string + encodeFunctionData( + functionFragment: 'relayV2', + values: [BytesLike, string] + ): string + encodeFunctionData( + functionFragment: 'renounceRole', + values: [BytesLike, string] + ): string + encodeFunctionData( + functionFragment: 'revokeRole', + values: [BytesLike, string] + ): string + encodeFunctionData(functionFragment: 'senderNonces', values: [string]): string + encodeFunctionData( + functionFragment: 'setCancelDelay', + values: [BigNumberish] + ): string + encodeFunctionData( + functionFragment: 'setProtocolFeeRate', + values: [BigNumberish] + ): string + encodeFunctionData( + functionFragment: 'supportsInterface', + values: [BytesLike] + ): string + encodeFunctionData( + functionFragment: 'sweepProtocolFees', + values: [string, string] + ): string + + decodeFunctionResult( + functionFragment: 'CANCELER_ROLE', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'DEFAULT_ADMIN_ROLE', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'DEFAULT_CANCEL_DELAY', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'DISPUTE_PERIOD', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'FEE_BPS', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'FEE_RATE_MAX', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'GOVERNOR_ROLE', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'GUARD_ROLE', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'MAX_ZAP_DATA_LENGTH', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'MIN_CANCEL_DELAY', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'MIN_DEADLINE_PERIOD', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'NATIVE_GAS_TOKEN', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'PROVER_ROLE', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'QUOTER_ROLE', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'bridge', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'bridgeProofs', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'bridgeRelayDetails', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'bridgeRelays', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'bridgeStatuses', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'bridgeTxDetails', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'bridgeV2', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'canClaim', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'cancelDelay', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'cancelV2', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'chainGasAmount', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'claim', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'claimV2', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'deployBlock', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'dispute', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'getBridgeTransaction', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'getBridgeTransactionV2', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'getRoleAdmin', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'getRoleMember', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'getRoleMemberCount', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'grantRole', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'hasRole', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'multicallNoResults', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'multicallWithResults', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'nonce', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'protocolFeeRate', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'protocolFees', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'prove', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'proveV2', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'refund', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'relay', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'relayV2', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'renounceRole', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'revokeRole', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'senderNonces', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'setCancelDelay', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'setProtocolFeeRate', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'supportsInterface', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'sweepProtocolFees', + data: BytesLike + ): Result + + events: { + 'BridgeDepositClaimed(bytes32,address,address,address,uint256)': EventFragment + 'BridgeDepositRefunded(bytes32,address,address,uint256)': EventFragment + 'BridgeProofDisputed(bytes32,address)': EventFragment + 'BridgeProofProvided(bytes32,address,bytes32)': EventFragment + 'BridgeQuoteDetails(bytes32,bytes)': EventFragment + 'BridgeRelayed(bytes32,address,address,uint32,address,address,uint256,uint256,uint256)': EventFragment + 'BridgeRequested(bytes32,address,bytes,uint32,address,address,uint256,uint256,bool)': EventFragment + 'CancelDelayUpdated(uint256,uint256)': EventFragment + 'FeeRateUpdated(uint256,uint256)': EventFragment + 'FeesSwept(address,address,uint256)': EventFragment + 'RoleAdminChanged(bytes32,bytes32,bytes32)': EventFragment + 'RoleGranted(bytes32,address,address)': EventFragment + 'RoleRevoked(bytes32,address,address)': EventFragment + } + + getEvent(nameOrSignatureOrTopic: 'BridgeDepositClaimed'): EventFragment + getEvent(nameOrSignatureOrTopic: 'BridgeDepositRefunded'): EventFragment + getEvent(nameOrSignatureOrTopic: 'BridgeProofDisputed'): EventFragment + getEvent(nameOrSignatureOrTopic: 'BridgeProofProvided'): EventFragment + getEvent(nameOrSignatureOrTopic: 'BridgeQuoteDetails'): EventFragment + getEvent(nameOrSignatureOrTopic: 'BridgeRelayed'): EventFragment + getEvent(nameOrSignatureOrTopic: 'BridgeRequested'): EventFragment + getEvent(nameOrSignatureOrTopic: 'CancelDelayUpdated'): EventFragment + getEvent(nameOrSignatureOrTopic: 'FeeRateUpdated'): EventFragment + getEvent(nameOrSignatureOrTopic: 'FeesSwept'): EventFragment + getEvent(nameOrSignatureOrTopic: 'RoleAdminChanged'): EventFragment + getEvent(nameOrSignatureOrTopic: 'RoleGranted'): EventFragment + getEvent(nameOrSignatureOrTopic: 'RoleRevoked'): EventFragment +} + +export interface BridgeDepositClaimedEventObject { + transactionId: string + relayer: string + to: string + token: string + amount: BigNumber +} +export type BridgeDepositClaimedEvent = TypedEvent< + [string, string, string, string, BigNumber], + BridgeDepositClaimedEventObject +> + +export type BridgeDepositClaimedEventFilter = + TypedEventFilter + +export interface BridgeDepositRefundedEventObject { + transactionId: string + to: string + token: string + amount: BigNumber +} +export type BridgeDepositRefundedEvent = TypedEvent< + [string, string, string, BigNumber], + BridgeDepositRefundedEventObject +> + +export type BridgeDepositRefundedEventFilter = + TypedEventFilter + +export interface BridgeProofDisputedEventObject { + transactionId: string + relayer: string +} +export type BridgeProofDisputedEvent = TypedEvent< + [string, string], + BridgeProofDisputedEventObject +> + +export type BridgeProofDisputedEventFilter = + TypedEventFilter + +export interface BridgeProofProvidedEventObject { + transactionId: string + relayer: string + transactionHash: string +} +export type BridgeProofProvidedEvent = TypedEvent< + [string, string, string], + BridgeProofProvidedEventObject +> + +export type BridgeProofProvidedEventFilter = + TypedEventFilter + +export interface BridgeQuoteDetailsEventObject { + transactionId: string + quoteId: string +} +export type BridgeQuoteDetailsEvent = TypedEvent< + [string, string], + BridgeQuoteDetailsEventObject +> + +export type BridgeQuoteDetailsEventFilter = + TypedEventFilter + +export interface BridgeRelayedEventObject { + transactionId: string + relayer: string + to: string + originChainId: number + originToken: string + destToken: string + originAmount: BigNumber + destAmount: BigNumber + chainGasAmount: BigNumber +} +export type BridgeRelayedEvent = TypedEvent< + [ + string, + string, + string, + number, + string, + string, + BigNumber, + BigNumber, + BigNumber + ], + BridgeRelayedEventObject +> + +export type BridgeRelayedEventFilter = TypedEventFilter + +export interface BridgeRequestedEventObject { + transactionId: string + sender: string + request: string + destChainId: number + originToken: string + destToken: string + originAmount: BigNumber + destAmount: BigNumber + sendChainGas: boolean +} +export type BridgeRequestedEvent = TypedEvent< + [ + string, + string, + string, + number, + string, + string, + BigNumber, + BigNumber, + boolean + ], + BridgeRequestedEventObject +> + +export type BridgeRequestedEventFilter = TypedEventFilter + +export interface CancelDelayUpdatedEventObject { + oldCancelDelay: BigNumber + newCancelDelay: BigNumber +} +export type CancelDelayUpdatedEvent = TypedEvent< + [BigNumber, BigNumber], + CancelDelayUpdatedEventObject +> + +export type CancelDelayUpdatedEventFilter = + TypedEventFilter + +export interface FeeRateUpdatedEventObject { + oldFeeRate: BigNumber + newFeeRate: BigNumber +} +export type FeeRateUpdatedEvent = TypedEvent< + [BigNumber, BigNumber], + FeeRateUpdatedEventObject +> + +export type FeeRateUpdatedEventFilter = TypedEventFilter + +export interface FeesSweptEventObject { + token: string + recipient: string + amount: BigNumber +} +export type FeesSweptEvent = TypedEvent< + [string, string, BigNumber], + FeesSweptEventObject +> + +export type FeesSweptEventFilter = TypedEventFilter + +export interface RoleAdminChangedEventObject { + role: string + previousAdminRole: string + newAdminRole: string +} +export type RoleAdminChangedEvent = TypedEvent< + [string, string, string], + RoleAdminChangedEventObject +> + +export type RoleAdminChangedEventFilter = + TypedEventFilter + +export interface RoleGrantedEventObject { + role: string + account: string + sender: string +} +export type RoleGrantedEvent = TypedEvent< + [string, string, string], + RoleGrantedEventObject +> + +export type RoleGrantedEventFilter = TypedEventFilter + +export interface RoleRevokedEventObject { + role: string + account: string + sender: string +} +export type RoleRevokedEvent = TypedEvent< + [string, string, string], + RoleRevokedEventObject +> + +export type RoleRevokedEventFilter = TypedEventFilter + +export interface FastBridgeV2 extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this + attach(addressOrName: string): this + deployed(): Promise + + interface: FastBridgeV2Interface + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise> + + listeners( + eventFilter?: TypedEventFilter + ): Array> + listeners(eventName?: string): Array + removeAllListeners( + eventFilter: TypedEventFilter + ): this + removeAllListeners(eventName?: string): this + off: OnEvent + on: OnEvent + once: OnEvent + removeListener: OnEvent + + functions: { + CANCELER_ROLE(overrides?: CallOverrides): Promise<[string]> + + DEFAULT_ADMIN_ROLE(overrides?: CallOverrides): Promise<[string]> + + DEFAULT_CANCEL_DELAY(overrides?: CallOverrides): Promise<[BigNumber]> + + DISPUTE_PERIOD(overrides?: CallOverrides): Promise<[BigNumber]> + + FEE_BPS(overrides?: CallOverrides): Promise<[BigNumber]> + + FEE_RATE_MAX(overrides?: CallOverrides): Promise<[BigNumber]> + + GOVERNOR_ROLE(overrides?: CallOverrides): Promise<[string]> + + GUARD_ROLE(overrides?: CallOverrides): Promise<[string]> + + MAX_ZAP_DATA_LENGTH(overrides?: CallOverrides): Promise<[BigNumber]> + + MIN_CANCEL_DELAY(overrides?: CallOverrides): Promise<[BigNumber]> + + MIN_DEADLINE_PERIOD(overrides?: CallOverrides): Promise<[BigNumber]> + + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise<[string]> + + PROVER_ROLE(overrides?: CallOverrides): Promise<[string]> + + QUOTER_ROLE(overrides?: CallOverrides): Promise<[string]> + + bridge( + params: IFastBridge.BridgeParamsStruct, + overrides?: PayableOverrides & { from?: string } + ): Promise + + bridgeProofs( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise<[BigNumber, string] & { timestamp: BigNumber; relayer: string }> + + bridgeRelayDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise< + [number, number, string] & { + blockNumber: number + blockTimestamp: number + relayer: string + } + > + + bridgeRelays( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise<[boolean]> + + bridgeStatuses( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise<[number] & { status: number }> + + bridgeTxDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise< + [number, number, BigNumber, string] & { + status: number + destChainId: number + proofBlockTimestamp: BigNumber + proofRelayer: string + } + > + + bridgeV2( + params: IFastBridge.BridgeParamsStruct, + paramsV2: IFastBridgeV2.BridgeParamsV2Struct, + overrides?: PayableOverrides & { from?: string } + ): Promise + + canClaim( + transactionId: BytesLike, + relayer: string, + overrides?: CallOverrides + ): Promise<[boolean]> + + cancelDelay(overrides?: CallOverrides): Promise<[BigNumber]> + + cancelV2( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + chainGasAmount(overrides?: CallOverrides): Promise<[BigNumber]> + + claim( + request: BytesLike, + to: string, + overrides?: Overrides & { from?: string } + ): Promise + + claimV2( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + deployBlock(overrides?: CallOverrides): Promise<[BigNumber]> + + dispute( + transactionId: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + getBridgeTransaction( + request: BytesLike, + overrides?: CallOverrides + ): Promise<[IFastBridge.BridgeTransactionStructOutput]> + + getBridgeTransactionV2( + request: BytesLike, + overrides?: CallOverrides + ): Promise<[IFastBridgeV2.BridgeTransactionV2StructOutput]> + + getRoleAdmin(role: BytesLike, overrides?: CallOverrides): Promise<[string]> + + getRoleMember( + role: BytesLike, + index: BigNumberish, + overrides?: CallOverrides + ): Promise<[string]> + + getRoleMemberCount( + role: BytesLike, + overrides?: CallOverrides + ): Promise<[BigNumber]> + + grantRole( + role: BytesLike, + account: string, + overrides?: Overrides & { from?: string } + ): Promise + + hasRole( + role: BytesLike, + account: string, + overrides?: CallOverrides + ): Promise<[boolean]> + + multicallNoResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: Overrides & { from?: string } + ): Promise + + multicallWithResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: Overrides & { from?: string } + ): Promise + + nonce(overrides?: CallOverrides): Promise<[BigNumber]> + + protocolFeeRate(overrides?: CallOverrides): Promise<[BigNumber]> + + protocolFees(arg0: string, overrides?: CallOverrides): Promise<[BigNumber]> + + prove( + request: BytesLike, + destTxHash: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + proveV2( + transactionId: BytesLike, + destTxHash: BytesLike, + relayer: string, + overrides?: Overrides & { from?: string } + ): Promise + + refund( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + relay( + request: BytesLike, + overrides?: PayableOverrides & { from?: string } + ): Promise + + relayV2( + request: BytesLike, + relayer: string, + overrides?: PayableOverrides & { from?: string } + ): Promise + + renounceRole( + role: BytesLike, + callerConfirmation: string, + overrides?: Overrides & { from?: string } + ): Promise + + revokeRole( + role: BytesLike, + account: string, + overrides?: Overrides & { from?: string } + ): Promise + + senderNonces(arg0: string, overrides?: CallOverrides): Promise<[BigNumber]> + + setCancelDelay( + newCancelDelay: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + setProtocolFeeRate( + newFeeRate: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise<[boolean]> + + sweepProtocolFees( + token: string, + recipient: string, + overrides?: Overrides & { from?: string } + ): Promise + } + + CANCELER_ROLE(overrides?: CallOverrides): Promise + + DEFAULT_ADMIN_ROLE(overrides?: CallOverrides): Promise + + DEFAULT_CANCEL_DELAY(overrides?: CallOverrides): Promise + + DISPUTE_PERIOD(overrides?: CallOverrides): Promise + + FEE_BPS(overrides?: CallOverrides): Promise + + FEE_RATE_MAX(overrides?: CallOverrides): Promise + + GOVERNOR_ROLE(overrides?: CallOverrides): Promise + + GUARD_ROLE(overrides?: CallOverrides): Promise + + MAX_ZAP_DATA_LENGTH(overrides?: CallOverrides): Promise + + MIN_CANCEL_DELAY(overrides?: CallOverrides): Promise + + MIN_DEADLINE_PERIOD(overrides?: CallOverrides): Promise + + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + PROVER_ROLE(overrides?: CallOverrides): Promise + + QUOTER_ROLE(overrides?: CallOverrides): Promise + + bridge( + params: IFastBridge.BridgeParamsStruct, + overrides?: PayableOverrides & { from?: string } + ): Promise + + bridgeProofs( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise<[BigNumber, string] & { timestamp: BigNumber; relayer: string }> + + bridgeRelayDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise< + [number, number, string] & { + blockNumber: number + blockTimestamp: number + relayer: string + } + > + + bridgeRelays( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeStatuses( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeTxDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise< + [number, number, BigNumber, string] & { + status: number + destChainId: number + proofBlockTimestamp: BigNumber + proofRelayer: string + } + > + + bridgeV2( + params: IFastBridge.BridgeParamsStruct, + paramsV2: IFastBridgeV2.BridgeParamsV2Struct, + overrides?: PayableOverrides & { from?: string } + ): Promise + + canClaim( + transactionId: BytesLike, + relayer: string, + overrides?: CallOverrides + ): Promise + + cancelDelay(overrides?: CallOverrides): Promise + + cancelV2( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + chainGasAmount(overrides?: CallOverrides): Promise + + claim( + request: BytesLike, + to: string, + overrides?: Overrides & { from?: string } + ): Promise + + claimV2( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + deployBlock(overrides?: CallOverrides): Promise + + dispute( + transactionId: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + getBridgeTransaction( + request: BytesLike, + overrides?: CallOverrides + ): Promise + + getBridgeTransactionV2( + request: BytesLike, + overrides?: CallOverrides + ): Promise + + getRoleAdmin(role: BytesLike, overrides?: CallOverrides): Promise + + getRoleMember( + role: BytesLike, + index: BigNumberish, + overrides?: CallOverrides + ): Promise + + getRoleMemberCount( + role: BytesLike, + overrides?: CallOverrides + ): Promise + + grantRole( + role: BytesLike, + account: string, + overrides?: Overrides & { from?: string } + ): Promise + + hasRole( + role: BytesLike, + account: string, + overrides?: CallOverrides + ): Promise + + multicallNoResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: Overrides & { from?: string } + ): Promise + + multicallWithResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: Overrides & { from?: string } + ): Promise + + nonce(overrides?: CallOverrides): Promise + + protocolFeeRate(overrides?: CallOverrides): Promise + + protocolFees(arg0: string, overrides?: CallOverrides): Promise + + prove( + request: BytesLike, + destTxHash: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + proveV2( + transactionId: BytesLike, + destTxHash: BytesLike, + relayer: string, + overrides?: Overrides & { from?: string } + ): Promise + + refund( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + relay( + request: BytesLike, + overrides?: PayableOverrides & { from?: string } + ): Promise + + relayV2( + request: BytesLike, + relayer: string, + overrides?: PayableOverrides & { from?: string } + ): Promise + + renounceRole( + role: BytesLike, + callerConfirmation: string, + overrides?: Overrides & { from?: string } + ): Promise + + revokeRole( + role: BytesLike, + account: string, + overrides?: Overrides & { from?: string } + ): Promise + + senderNonces(arg0: string, overrides?: CallOverrides): Promise + + setCancelDelay( + newCancelDelay: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + setProtocolFeeRate( + newFeeRate: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise + + sweepProtocolFees( + token: string, + recipient: string, + overrides?: Overrides & { from?: string } + ): Promise + + callStatic: { + CANCELER_ROLE(overrides?: CallOverrides): Promise + + DEFAULT_ADMIN_ROLE(overrides?: CallOverrides): Promise + + DEFAULT_CANCEL_DELAY(overrides?: CallOverrides): Promise + + DISPUTE_PERIOD(overrides?: CallOverrides): Promise + + FEE_BPS(overrides?: CallOverrides): Promise + + FEE_RATE_MAX(overrides?: CallOverrides): Promise + + GOVERNOR_ROLE(overrides?: CallOverrides): Promise + + GUARD_ROLE(overrides?: CallOverrides): Promise + + MAX_ZAP_DATA_LENGTH(overrides?: CallOverrides): Promise + + MIN_CANCEL_DELAY(overrides?: CallOverrides): Promise + + MIN_DEADLINE_PERIOD(overrides?: CallOverrides): Promise + + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + PROVER_ROLE(overrides?: CallOverrides): Promise + + QUOTER_ROLE(overrides?: CallOverrides): Promise + + bridge( + params: IFastBridge.BridgeParamsStruct, + overrides?: CallOverrides + ): Promise + + bridgeProofs( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise<[BigNumber, string] & { timestamp: BigNumber; relayer: string }> + + bridgeRelayDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise< + [number, number, string] & { + blockNumber: number + blockTimestamp: number + relayer: string + } + > + + bridgeRelays( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeStatuses( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeTxDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise< + [number, number, BigNumber, string] & { + status: number + destChainId: number + proofBlockTimestamp: BigNumber + proofRelayer: string + } + > + + bridgeV2( + params: IFastBridge.BridgeParamsStruct, + paramsV2: IFastBridgeV2.BridgeParamsV2Struct, + overrides?: CallOverrides + ): Promise + + canClaim( + transactionId: BytesLike, + relayer: string, + overrides?: CallOverrides + ): Promise + + cancelDelay(overrides?: CallOverrides): Promise + + cancelV2(request: BytesLike, overrides?: CallOverrides): Promise + + chainGasAmount(overrides?: CallOverrides): Promise + + claim( + request: BytesLike, + to: string, + overrides?: CallOverrides + ): Promise + + claimV2(request: BytesLike, overrides?: CallOverrides): Promise + + deployBlock(overrides?: CallOverrides): Promise + + dispute(transactionId: BytesLike, overrides?: CallOverrides): Promise + + getBridgeTransaction( + request: BytesLike, + overrides?: CallOverrides + ): Promise + + getBridgeTransactionV2( + request: BytesLike, + overrides?: CallOverrides + ): Promise + + getRoleAdmin(role: BytesLike, overrides?: CallOverrides): Promise + + getRoleMember( + role: BytesLike, + index: BigNumberish, + overrides?: CallOverrides + ): Promise + + getRoleMemberCount( + role: BytesLike, + overrides?: CallOverrides + ): Promise + + grantRole( + role: BytesLike, + account: string, + overrides?: CallOverrides + ): Promise + + hasRole( + role: BytesLike, + account: string, + overrides?: CallOverrides + ): Promise + + multicallNoResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: CallOverrides + ): Promise + + multicallWithResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: CallOverrides + ): Promise + + nonce(overrides?: CallOverrides): Promise + + protocolFeeRate(overrides?: CallOverrides): Promise + + protocolFees(arg0: string, overrides?: CallOverrides): Promise + + prove( + request: BytesLike, + destTxHash: BytesLike, + overrides?: CallOverrides + ): Promise + + proveV2( + transactionId: BytesLike, + destTxHash: BytesLike, + relayer: string, + overrides?: CallOverrides + ): Promise + + refund(request: BytesLike, overrides?: CallOverrides): Promise + + relay(request: BytesLike, overrides?: CallOverrides): Promise + + relayV2( + request: BytesLike, + relayer: string, + overrides?: CallOverrides + ): Promise + + renounceRole( + role: BytesLike, + callerConfirmation: string, + overrides?: CallOverrides + ): Promise + + revokeRole( + role: BytesLike, + account: string, + overrides?: CallOverrides + ): Promise + + senderNonces(arg0: string, overrides?: CallOverrides): Promise + + setCancelDelay( + newCancelDelay: BigNumberish, + overrides?: CallOverrides + ): Promise + + setProtocolFeeRate( + newFeeRate: BigNumberish, + overrides?: CallOverrides + ): Promise + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise + + sweepProtocolFees( + token: string, + recipient: string, + overrides?: CallOverrides + ): Promise + } + + filters: { + 'BridgeDepositClaimed(bytes32,address,address,address,uint256)'( + transactionId?: BytesLike | null, + relayer?: string | null, + to?: string | null, + token?: null, + amount?: null + ): BridgeDepositClaimedEventFilter + BridgeDepositClaimed( + transactionId?: BytesLike | null, + relayer?: string | null, + to?: string | null, + token?: null, + amount?: null + ): BridgeDepositClaimedEventFilter + + 'BridgeDepositRefunded(bytes32,address,address,uint256)'( + transactionId?: BytesLike | null, + to?: string | null, + token?: null, + amount?: null + ): BridgeDepositRefundedEventFilter + BridgeDepositRefunded( + transactionId?: BytesLike | null, + to?: string | null, + token?: null, + amount?: null + ): BridgeDepositRefundedEventFilter + + 'BridgeProofDisputed(bytes32,address)'( + transactionId?: BytesLike | null, + relayer?: string | null + ): BridgeProofDisputedEventFilter + BridgeProofDisputed( + transactionId?: BytesLike | null, + relayer?: string | null + ): BridgeProofDisputedEventFilter + + 'BridgeProofProvided(bytes32,address,bytes32)'( + transactionId?: BytesLike | null, + relayer?: string | null, + transactionHash?: null + ): BridgeProofProvidedEventFilter + BridgeProofProvided( + transactionId?: BytesLike | null, + relayer?: string | null, + transactionHash?: null + ): BridgeProofProvidedEventFilter + + 'BridgeQuoteDetails(bytes32,bytes)'( + transactionId?: BytesLike | null, + quoteId?: null + ): BridgeQuoteDetailsEventFilter + BridgeQuoteDetails( + transactionId?: BytesLike | null, + quoteId?: null + ): BridgeQuoteDetailsEventFilter + + 'BridgeRelayed(bytes32,address,address,uint32,address,address,uint256,uint256,uint256)'( + transactionId?: BytesLike | null, + relayer?: string | null, + to?: string | null, + originChainId?: null, + originToken?: null, + destToken?: null, + originAmount?: null, + destAmount?: null, + chainGasAmount?: null + ): BridgeRelayedEventFilter + BridgeRelayed( + transactionId?: BytesLike | null, + relayer?: string | null, + to?: string | null, + originChainId?: null, + originToken?: null, + destToken?: null, + originAmount?: null, + destAmount?: null, + chainGasAmount?: null + ): BridgeRelayedEventFilter + + 'BridgeRequested(bytes32,address,bytes,uint32,address,address,uint256,uint256,bool)'( + transactionId?: BytesLike | null, + sender?: string | null, + request?: null, + destChainId?: null, + originToken?: null, + destToken?: null, + originAmount?: null, + destAmount?: null, + sendChainGas?: null + ): BridgeRequestedEventFilter + BridgeRequested( + transactionId?: BytesLike | null, + sender?: string | null, + request?: null, + destChainId?: null, + originToken?: null, + destToken?: null, + originAmount?: null, + destAmount?: null, + sendChainGas?: null + ): BridgeRequestedEventFilter + + 'CancelDelayUpdated(uint256,uint256)'( + oldCancelDelay?: null, + newCancelDelay?: null + ): CancelDelayUpdatedEventFilter + CancelDelayUpdated( + oldCancelDelay?: null, + newCancelDelay?: null + ): CancelDelayUpdatedEventFilter + + 'FeeRateUpdated(uint256,uint256)'( + oldFeeRate?: null, + newFeeRate?: null + ): FeeRateUpdatedEventFilter + FeeRateUpdated( + oldFeeRate?: null, + newFeeRate?: null + ): FeeRateUpdatedEventFilter + + 'FeesSwept(address,address,uint256)'( + token?: null, + recipient?: null, + amount?: null + ): FeesSweptEventFilter + FeesSwept( + token?: null, + recipient?: null, + amount?: null + ): FeesSweptEventFilter + + 'RoleAdminChanged(bytes32,bytes32,bytes32)'( + role?: BytesLike | null, + previousAdminRole?: BytesLike | null, + newAdminRole?: BytesLike | null + ): RoleAdminChangedEventFilter + RoleAdminChanged( + role?: BytesLike | null, + previousAdminRole?: BytesLike | null, + newAdminRole?: BytesLike | null + ): RoleAdminChangedEventFilter + + 'RoleGranted(bytes32,address,address)'( + role?: BytesLike | null, + account?: string | null, + sender?: string | null + ): RoleGrantedEventFilter + RoleGranted( + role?: BytesLike | null, + account?: string | null, + sender?: string | null + ): RoleGrantedEventFilter + + 'RoleRevoked(bytes32,address,address)'( + role?: BytesLike | null, + account?: string | null, + sender?: string | null + ): RoleRevokedEventFilter + RoleRevoked( + role?: BytesLike | null, + account?: string | null, + sender?: string | null + ): RoleRevokedEventFilter + } + + estimateGas: { + CANCELER_ROLE(overrides?: CallOverrides): Promise + + DEFAULT_ADMIN_ROLE(overrides?: CallOverrides): Promise + + DEFAULT_CANCEL_DELAY(overrides?: CallOverrides): Promise + + DISPUTE_PERIOD(overrides?: CallOverrides): Promise + + FEE_BPS(overrides?: CallOverrides): Promise + + FEE_RATE_MAX(overrides?: CallOverrides): Promise + + GOVERNOR_ROLE(overrides?: CallOverrides): Promise + + GUARD_ROLE(overrides?: CallOverrides): Promise + + MAX_ZAP_DATA_LENGTH(overrides?: CallOverrides): Promise + + MIN_CANCEL_DELAY(overrides?: CallOverrides): Promise + + MIN_DEADLINE_PERIOD(overrides?: CallOverrides): Promise + + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + PROVER_ROLE(overrides?: CallOverrides): Promise + + QUOTER_ROLE(overrides?: CallOverrides): Promise + + bridge( + params: IFastBridge.BridgeParamsStruct, + overrides?: PayableOverrides & { from?: string } + ): Promise + + bridgeProofs( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeRelayDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeRelays( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeStatuses( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeTxDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeV2( + params: IFastBridge.BridgeParamsStruct, + paramsV2: IFastBridgeV2.BridgeParamsV2Struct, + overrides?: PayableOverrides & { from?: string } + ): Promise + + canClaim( + transactionId: BytesLike, + relayer: string, + overrides?: CallOverrides + ): Promise + + cancelDelay(overrides?: CallOverrides): Promise + + cancelV2( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + chainGasAmount(overrides?: CallOverrides): Promise + + claim( + request: BytesLike, + to: string, + overrides?: Overrides & { from?: string } + ): Promise + + claimV2( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + deployBlock(overrides?: CallOverrides): Promise + + dispute( + transactionId: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + getBridgeTransaction( + request: BytesLike, + overrides?: CallOverrides + ): Promise + + getBridgeTransactionV2( + request: BytesLike, + overrides?: CallOverrides + ): Promise + + getRoleAdmin(role: BytesLike, overrides?: CallOverrides): Promise + + getRoleMember( + role: BytesLike, + index: BigNumberish, + overrides?: CallOverrides + ): Promise + + getRoleMemberCount( + role: BytesLike, + overrides?: CallOverrides + ): Promise + + grantRole( + role: BytesLike, + account: string, + overrides?: Overrides & { from?: string } + ): Promise + + hasRole( + role: BytesLike, + account: string, + overrides?: CallOverrides + ): Promise + + multicallNoResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: Overrides & { from?: string } + ): Promise + + multicallWithResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: Overrides & { from?: string } + ): Promise + + nonce(overrides?: CallOverrides): Promise + + protocolFeeRate(overrides?: CallOverrides): Promise + + protocolFees(arg0: string, overrides?: CallOverrides): Promise + + prove( + request: BytesLike, + destTxHash: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + proveV2( + transactionId: BytesLike, + destTxHash: BytesLike, + relayer: string, + overrides?: Overrides & { from?: string } + ): Promise + + refund( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + relay( + request: BytesLike, + overrides?: PayableOverrides & { from?: string } + ): Promise + + relayV2( + request: BytesLike, + relayer: string, + overrides?: PayableOverrides & { from?: string } + ): Promise + + renounceRole( + role: BytesLike, + callerConfirmation: string, + overrides?: Overrides & { from?: string } + ): Promise + + revokeRole( + role: BytesLike, + account: string, + overrides?: Overrides & { from?: string } + ): Promise + + senderNonces(arg0: string, overrides?: CallOverrides): Promise + + setCancelDelay( + newCancelDelay: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + setProtocolFeeRate( + newFeeRate: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise + + sweepProtocolFees( + token: string, + recipient: string, + overrides?: Overrides & { from?: string } + ): Promise + } + + populateTransaction: { + CANCELER_ROLE(overrides?: CallOverrides): Promise + + DEFAULT_ADMIN_ROLE(overrides?: CallOverrides): Promise + + DEFAULT_CANCEL_DELAY( + overrides?: CallOverrides + ): Promise + + DISPUTE_PERIOD(overrides?: CallOverrides): Promise + + FEE_BPS(overrides?: CallOverrides): Promise + + FEE_RATE_MAX(overrides?: CallOverrides): Promise + + GOVERNOR_ROLE(overrides?: CallOverrides): Promise + + GUARD_ROLE(overrides?: CallOverrides): Promise + + MAX_ZAP_DATA_LENGTH( + overrides?: CallOverrides + ): Promise + + MIN_CANCEL_DELAY(overrides?: CallOverrides): Promise + + MIN_DEADLINE_PERIOD( + overrides?: CallOverrides + ): Promise + + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + PROVER_ROLE(overrides?: CallOverrides): Promise + + QUOTER_ROLE(overrides?: CallOverrides): Promise + + bridge( + params: IFastBridge.BridgeParamsStruct, + overrides?: PayableOverrides & { from?: string } + ): Promise + + bridgeProofs( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeRelayDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeRelays( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeStatuses( + transactionId: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeTxDetails( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise + + bridgeV2( + params: IFastBridge.BridgeParamsStruct, + paramsV2: IFastBridgeV2.BridgeParamsV2Struct, + overrides?: PayableOverrides & { from?: string } + ): Promise + + canClaim( + transactionId: BytesLike, + relayer: string, + overrides?: CallOverrides + ): Promise + + cancelDelay(overrides?: CallOverrides): Promise + + cancelV2( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + chainGasAmount(overrides?: CallOverrides): Promise + + claim( + request: BytesLike, + to: string, + overrides?: Overrides & { from?: string } + ): Promise + + claimV2( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + deployBlock(overrides?: CallOverrides): Promise + + dispute( + transactionId: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + getBridgeTransaction( + request: BytesLike, + overrides?: CallOverrides + ): Promise + + getBridgeTransactionV2( + request: BytesLike, + overrides?: CallOverrides + ): Promise + + getRoleAdmin( + role: BytesLike, + overrides?: CallOverrides + ): Promise + + getRoleMember( + role: BytesLike, + index: BigNumberish, + overrides?: CallOverrides + ): Promise + + getRoleMemberCount( + role: BytesLike, + overrides?: CallOverrides + ): Promise + + grantRole( + role: BytesLike, + account: string, + overrides?: Overrides & { from?: string } + ): Promise + + hasRole( + role: BytesLike, + account: string, + overrides?: CallOverrides + ): Promise + + multicallNoResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: Overrides & { from?: string } + ): Promise + + multicallWithResults( + data: BytesLike[], + ignoreReverts: boolean, + overrides?: Overrides & { from?: string } + ): Promise + + nonce(overrides?: CallOverrides): Promise + + protocolFeeRate(overrides?: CallOverrides): Promise + + protocolFees( + arg0: string, + overrides?: CallOverrides + ): Promise + + prove( + request: BytesLike, + destTxHash: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + proveV2( + transactionId: BytesLike, + destTxHash: BytesLike, + relayer: string, + overrides?: Overrides & { from?: string } + ): Promise + + refund( + request: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise + + relay( + request: BytesLike, + overrides?: PayableOverrides & { from?: string } + ): Promise + + relayV2( + request: BytesLike, + relayer: string, + overrides?: PayableOverrides & { from?: string } + ): Promise + + renounceRole( + role: BytesLike, + callerConfirmation: string, + overrides?: Overrides & { from?: string } + ): Promise + + revokeRole( + role: BytesLike, + account: string, + overrides?: Overrides & { from?: string } + ): Promise + + senderNonces( + arg0: string, + overrides?: CallOverrides + ): Promise + + setCancelDelay( + newCancelDelay: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + setProtocolFeeRate( + newFeeRate: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + supportsInterface( + interfaceId: BytesLike, + overrides?: CallOverrides + ): Promise + + sweepProtocolFees( + token: string, + recipient: string, + overrides?: Overrides & { from?: string } + ): Promise + } +} diff --git a/packages/sdk-router/src/typechain/IDefaultActions.ts b/packages/sdk-router/src/typechain/IDefaultActions.ts new file mode 100644 index 0000000000..e28993ee46 --- /dev/null +++ b/packages/sdk-router/src/typechain/IDefaultActions.ts @@ -0,0 +1,285 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PayableOverrides, + PopulatedTransaction, + Signer, + utils, +} from 'ethers' +import type { FunctionFragment, Result } from '@ethersproject/abi' +import type { Listener, Provider } from '@ethersproject/providers' +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from './common' + +export interface IDefaultActionsInterface extends utils.Interface { + functions: { + 'addLiquidity(uint256[],uint256,uint256)': FunctionFragment + 'deposit(uint256)': FunctionFragment + 'removeLiquidityOneToken(uint256,uint8,uint256,uint256)': FunctionFragment + 'swap(uint8,uint8,uint256,uint256,uint256)': FunctionFragment + 'withdraw(uint256)': FunctionFragment + } + + getFunction( + nameOrSignatureOrTopic: + | 'addLiquidity' + | 'deposit' + | 'removeLiquidityOneToken' + | 'swap' + | 'withdraw' + ): FunctionFragment + + encodeFunctionData( + functionFragment: 'addLiquidity', + values: [BigNumberish[], BigNumberish, BigNumberish] + ): string + encodeFunctionData( + functionFragment: 'deposit', + values: [BigNumberish] + ): string + encodeFunctionData( + functionFragment: 'removeLiquidityOneToken', + values: [BigNumberish, BigNumberish, BigNumberish, BigNumberish] + ): string + encodeFunctionData( + functionFragment: 'swap', + values: [ + BigNumberish, + BigNumberish, + BigNumberish, + BigNumberish, + BigNumberish + ] + ): string + encodeFunctionData( + functionFragment: 'withdraw', + values: [BigNumberish] + ): string + + decodeFunctionResult( + functionFragment: 'addLiquidity', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'deposit', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'removeLiquidityOneToken', + data: BytesLike + ): Result + decodeFunctionResult(functionFragment: 'swap', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'withdraw', data: BytesLike): Result + + events: {} +} + +export interface IDefaultActions extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this + attach(addressOrName: string): this + deployed(): Promise + + interface: IDefaultActionsInterface + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise> + + listeners( + eventFilter?: TypedEventFilter + ): Array> + listeners(eventName?: string): Array + removeAllListeners( + eventFilter: TypedEventFilter + ): this + removeAllListeners(eventName?: string): this + off: OnEvent + on: OnEvent + once: OnEvent + removeListener: OnEvent + + functions: { + addLiquidity( + amounts: BigNumberish[], + minToMint: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + deposit( + arg0: BigNumberish, + overrides?: PayableOverrides & { from?: string } + ): Promise + + removeLiquidityOneToken( + tokenAmount: BigNumberish, + tokenIndex: BigNumberish, + minAmount: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + swap( + tokenIndexFrom: BigNumberish, + tokenIndexTo: BigNumberish, + dx: BigNumberish, + minDy: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + withdraw( + amount: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + } + + addLiquidity( + amounts: BigNumberish[], + minToMint: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + deposit( + arg0: BigNumberish, + overrides?: PayableOverrides & { from?: string } + ): Promise + + removeLiquidityOneToken( + tokenAmount: BigNumberish, + tokenIndex: BigNumberish, + minAmount: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + swap( + tokenIndexFrom: BigNumberish, + tokenIndexTo: BigNumberish, + dx: BigNumberish, + minDy: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + withdraw( + amount: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + callStatic: { + addLiquidity( + amounts: BigNumberish[], + minToMint: BigNumberish, + deadline: BigNumberish, + overrides?: CallOverrides + ): Promise + + deposit(arg0: BigNumberish, overrides?: CallOverrides): Promise + + removeLiquidityOneToken( + tokenAmount: BigNumberish, + tokenIndex: BigNumberish, + minAmount: BigNumberish, + deadline: BigNumberish, + overrides?: CallOverrides + ): Promise + + swap( + tokenIndexFrom: BigNumberish, + tokenIndexTo: BigNumberish, + dx: BigNumberish, + minDy: BigNumberish, + deadline: BigNumberish, + overrides?: CallOverrides + ): Promise + + withdraw(amount: BigNumberish, overrides?: CallOverrides): Promise + } + + filters: {} + + estimateGas: { + addLiquidity( + amounts: BigNumberish[], + minToMint: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + deposit( + arg0: BigNumberish, + overrides?: PayableOverrides & { from?: string } + ): Promise + + removeLiquidityOneToken( + tokenAmount: BigNumberish, + tokenIndex: BigNumberish, + minAmount: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + swap( + tokenIndexFrom: BigNumberish, + tokenIndexTo: BigNumberish, + dx: BigNumberish, + minDy: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + withdraw( + amount: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + } + + populateTransaction: { + addLiquidity( + amounts: BigNumberish[], + minToMint: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + deposit( + arg0: BigNumberish, + overrides?: PayableOverrides & { from?: string } + ): Promise + + removeLiquidityOneToken( + tokenAmount: BigNumberish, + tokenIndex: BigNumberish, + minAmount: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + swap( + tokenIndexFrom: BigNumberish, + tokenIndexTo: BigNumberish, + dx: BigNumberish, + minDy: BigNumberish, + deadline: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + withdraw( + amount: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + } +} diff --git a/packages/sdk-router/src/typechain/IERC20Metadata.ts b/packages/sdk-router/src/typechain/IERC20Metadata.ts new file mode 100644 index 0000000000..5d99c22456 --- /dev/null +++ b/packages/sdk-router/src/typechain/IERC20Metadata.ts @@ -0,0 +1,361 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PopulatedTransaction, + Signer, + utils, +} from 'ethers' +import type { + FunctionFragment, + Result, + EventFragment, +} from '@ethersproject/abi' +import type { Listener, Provider } from '@ethersproject/providers' +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from './common' + +export interface IERC20MetadataInterface extends utils.Interface { + functions: { + 'allowance(address,address)': FunctionFragment + 'approve(address,uint256)': FunctionFragment + 'balanceOf(address)': FunctionFragment + 'decimals()': FunctionFragment + 'name()': FunctionFragment + 'symbol()': FunctionFragment + 'totalSupply()': FunctionFragment + 'transfer(address,uint256)': FunctionFragment + 'transferFrom(address,address,uint256)': FunctionFragment + } + + getFunction( + nameOrSignatureOrTopic: + | 'allowance' + | 'approve' + | 'balanceOf' + | 'decimals' + | 'name' + | 'symbol' + | 'totalSupply' + | 'transfer' + | 'transferFrom' + ): FunctionFragment + + encodeFunctionData( + functionFragment: 'allowance', + values: [string, string] + ): string + encodeFunctionData( + functionFragment: 'approve', + values: [string, BigNumberish] + ): string + encodeFunctionData(functionFragment: 'balanceOf', values: [string]): string + encodeFunctionData(functionFragment: 'decimals', values?: undefined): string + encodeFunctionData(functionFragment: 'name', values?: undefined): string + encodeFunctionData(functionFragment: 'symbol', values?: undefined): string + encodeFunctionData( + functionFragment: 'totalSupply', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'transfer', + values: [string, BigNumberish] + ): string + encodeFunctionData( + functionFragment: 'transferFrom', + values: [string, string, BigNumberish] + ): string + + decodeFunctionResult(functionFragment: 'allowance', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'approve', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'balanceOf', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'decimals', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'name', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'symbol', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'totalSupply', data: BytesLike): Result + decodeFunctionResult(functionFragment: 'transfer', data: BytesLike): Result + decodeFunctionResult( + functionFragment: 'transferFrom', + data: BytesLike + ): Result + + events: { + 'Approval(address,address,uint256)': EventFragment + 'Transfer(address,address,uint256)': EventFragment + } + + getEvent(nameOrSignatureOrTopic: 'Approval'): EventFragment + getEvent(nameOrSignatureOrTopic: 'Transfer'): EventFragment +} + +export interface ApprovalEventObject { + owner: string + spender: string + value: BigNumber +} +export type ApprovalEvent = TypedEvent< + [string, string, BigNumber], + ApprovalEventObject +> + +export type ApprovalEventFilter = TypedEventFilter + +export interface TransferEventObject { + from: string + to: string + value: BigNumber +} +export type TransferEvent = TypedEvent< + [string, string, BigNumber], + TransferEventObject +> + +export type TransferEventFilter = TypedEventFilter + +export interface IERC20Metadata extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this + attach(addressOrName: string): this + deployed(): Promise + + interface: IERC20MetadataInterface + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise> + + listeners( + eventFilter?: TypedEventFilter + ): Array> + listeners(eventName?: string): Array + removeAllListeners( + eventFilter: TypedEventFilter + ): this + removeAllListeners(eventName?: string): this + off: OnEvent + on: OnEvent + once: OnEvent + removeListener: OnEvent + + functions: { + allowance( + owner: string, + spender: string, + overrides?: CallOverrides + ): Promise<[BigNumber]> + + approve( + spender: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + balanceOf(account: string, overrides?: CallOverrides): Promise<[BigNumber]> + + decimals(overrides?: CallOverrides): Promise<[number]> + + name(overrides?: CallOverrides): Promise<[string]> + + symbol(overrides?: CallOverrides): Promise<[string]> + + totalSupply(overrides?: CallOverrides): Promise<[BigNumber]> + + transfer( + to: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + transferFrom( + from: string, + to: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + } + + allowance( + owner: string, + spender: string, + overrides?: CallOverrides + ): Promise + + approve( + spender: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + balanceOf(account: string, overrides?: CallOverrides): Promise + + decimals(overrides?: CallOverrides): Promise + + name(overrides?: CallOverrides): Promise + + symbol(overrides?: CallOverrides): Promise + + totalSupply(overrides?: CallOverrides): Promise + + transfer( + to: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + transferFrom( + from: string, + to: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + callStatic: { + allowance( + owner: string, + spender: string, + overrides?: CallOverrides + ): Promise + + approve( + spender: string, + value: BigNumberish, + overrides?: CallOverrides + ): Promise + + balanceOf(account: string, overrides?: CallOverrides): Promise + + decimals(overrides?: CallOverrides): Promise + + name(overrides?: CallOverrides): Promise + + symbol(overrides?: CallOverrides): Promise + + totalSupply(overrides?: CallOverrides): Promise + + transfer( + to: string, + value: BigNumberish, + overrides?: CallOverrides + ): Promise + + transferFrom( + from: string, + to: string, + value: BigNumberish, + overrides?: CallOverrides + ): Promise + } + + filters: { + 'Approval(address,address,uint256)'( + owner?: string | null, + spender?: string | null, + value?: null + ): ApprovalEventFilter + Approval( + owner?: string | null, + spender?: string | null, + value?: null + ): ApprovalEventFilter + + 'Transfer(address,address,uint256)'( + from?: string | null, + to?: string | null, + value?: null + ): TransferEventFilter + Transfer( + from?: string | null, + to?: string | null, + value?: null + ): TransferEventFilter + } + + estimateGas: { + allowance( + owner: string, + spender: string, + overrides?: CallOverrides + ): Promise + + approve( + spender: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + balanceOf(account: string, overrides?: CallOverrides): Promise + + decimals(overrides?: CallOverrides): Promise + + name(overrides?: CallOverrides): Promise + + symbol(overrides?: CallOverrides): Promise + + totalSupply(overrides?: CallOverrides): Promise + + transfer( + to: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + transferFrom( + from: string, + to: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + } + + populateTransaction: { + allowance( + owner: string, + spender: string, + overrides?: CallOverrides + ): Promise + + approve( + spender: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + balanceOf( + account: string, + overrides?: CallOverrides + ): Promise + + decimals(overrides?: CallOverrides): Promise + + name(overrides?: CallOverrides): Promise + + symbol(overrides?: CallOverrides): Promise + + totalSupply(overrides?: CallOverrides): Promise + + transfer( + to: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + + transferFrom( + from: string, + to: string, + value: BigNumberish, + overrides?: Overrides & { from?: string } + ): Promise + } +} diff --git a/packages/sdk-router/src/typechain/SynapseIntentPreviewer.ts b/packages/sdk-router/src/typechain/SynapseIntentPreviewer.ts new file mode 100644 index 0000000000..36f3c0e5f3 --- /dev/null +++ b/packages/sdk-router/src/typechain/SynapseIntentPreviewer.ts @@ -0,0 +1,185 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + PopulatedTransaction, + Signer, + utils, +} from 'ethers' +import type { FunctionFragment, Result } from '@ethersproject/abi' +import type { Listener, Provider } from '@ethersproject/providers' +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from './common' + +export declare namespace ISynapseIntentRouter { + export type StepParamsStruct = { + token: string + amount: BigNumberish + msgValue: BigNumberish + zapData: BytesLike + } + + export type StepParamsStructOutput = [ + string, + BigNumber, + BigNumber, + string + ] & { + token: string + amount: BigNumber + msgValue: BigNumber + zapData: string + } +} + +export interface SynapseIntentPreviewerInterface extends utils.Interface { + functions: { + 'NATIVE_GAS_TOKEN()': FunctionFragment + 'previewIntent(address,address,uint256,address,address,uint256)': FunctionFragment + } + + getFunction( + nameOrSignatureOrTopic: 'NATIVE_GAS_TOKEN' | 'previewIntent' + ): FunctionFragment + + encodeFunctionData( + functionFragment: 'NATIVE_GAS_TOKEN', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'previewIntent', + values: [string, string, BigNumberish, string, string, BigNumberish] + ): string + + decodeFunctionResult( + functionFragment: 'NATIVE_GAS_TOKEN', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'previewIntent', + data: BytesLike + ): Result + + events: {} +} + +export interface SynapseIntentPreviewer extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this + attach(addressOrName: string): this + deployed(): Promise + + interface: SynapseIntentPreviewerInterface + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise> + + listeners( + eventFilter?: TypedEventFilter + ): Array> + listeners(eventName?: string): Array + removeAllListeners( + eventFilter: TypedEventFilter + ): this + removeAllListeners(eventName?: string): this + off: OnEvent + on: OnEvent + once: OnEvent + removeListener: OnEvent + + functions: { + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise<[string]> + + previewIntent( + swapQuoter: string, + forwardTo: string, + slippageWei: BigNumberish, + tokenIn: string, + tokenOut: string, + amountIn: BigNumberish, + overrides?: CallOverrides + ): Promise< + [BigNumber, ISynapseIntentRouter.StepParamsStructOutput[]] & { + amountOut: BigNumber + steps: ISynapseIntentRouter.StepParamsStructOutput[] + } + > + } + + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + previewIntent( + swapQuoter: string, + forwardTo: string, + slippageWei: BigNumberish, + tokenIn: string, + tokenOut: string, + amountIn: BigNumberish, + overrides?: CallOverrides + ): Promise< + [BigNumber, ISynapseIntentRouter.StepParamsStructOutput[]] & { + amountOut: BigNumber + steps: ISynapseIntentRouter.StepParamsStructOutput[] + } + > + + callStatic: { + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + previewIntent( + swapQuoter: string, + forwardTo: string, + slippageWei: BigNumberish, + tokenIn: string, + tokenOut: string, + amountIn: BigNumberish, + overrides?: CallOverrides + ): Promise< + [BigNumber, ISynapseIntentRouter.StepParamsStructOutput[]] & { + amountOut: BigNumber + steps: ISynapseIntentRouter.StepParamsStructOutput[] + } + > + } + + filters: {} + + estimateGas: { + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + previewIntent( + swapQuoter: string, + forwardTo: string, + slippageWei: BigNumberish, + tokenIn: string, + tokenOut: string, + amountIn: BigNumberish, + overrides?: CallOverrides + ): Promise + } + + populateTransaction: { + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + previewIntent( + swapQuoter: string, + forwardTo: string, + slippageWei: BigNumberish, + tokenIn: string, + tokenOut: string, + amountIn: BigNumberish, + overrides?: CallOverrides + ): Promise + } +} diff --git a/packages/sdk-router/src/typechain/SynapseIntentRouter.ts b/packages/sdk-router/src/typechain/SynapseIntentRouter.ts new file mode 100644 index 0000000000..d4ecd9113d --- /dev/null +++ b/packages/sdk-router/src/typechain/SynapseIntentRouter.ts @@ -0,0 +1,224 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + PayableOverrides, + PopulatedTransaction, + Signer, + utils, +} from 'ethers' +import type { FunctionFragment, Result } from '@ethersproject/abi' +import type { Listener, Provider } from '@ethersproject/providers' +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from './common' + +export declare namespace ISynapseIntentRouter { + export type StepParamsStruct = { + token: string + amount: BigNumberish + msgValue: BigNumberish + zapData: BytesLike + } + + export type StepParamsStructOutput = [ + string, + BigNumber, + BigNumber, + string + ] & { + token: string + amount: BigNumber + msgValue: BigNumber + zapData: string + } +} + +export interface SynapseIntentRouterInterface extends utils.Interface { + functions: { + 'NATIVE_GAS_TOKEN()': FunctionFragment + 'completeIntent(address,uint256,uint256,(address,uint256,uint256,bytes)[])': FunctionFragment + 'completeIntentWithBalanceChecks(address,uint256,uint256,(address,uint256,uint256,bytes)[])': FunctionFragment + } + + getFunction( + nameOrSignatureOrTopic: + | 'NATIVE_GAS_TOKEN' + | 'completeIntent' + | 'completeIntentWithBalanceChecks' + ): FunctionFragment + + encodeFunctionData( + functionFragment: 'NATIVE_GAS_TOKEN', + values?: undefined + ): string + encodeFunctionData( + functionFragment: 'completeIntent', + values: [ + string, + BigNumberish, + BigNumberish, + ISynapseIntentRouter.StepParamsStruct[] + ] + ): string + encodeFunctionData( + functionFragment: 'completeIntentWithBalanceChecks', + values: [ + string, + BigNumberish, + BigNumberish, + ISynapseIntentRouter.StepParamsStruct[] + ] + ): string + + decodeFunctionResult( + functionFragment: 'NATIVE_GAS_TOKEN', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'completeIntent', + data: BytesLike + ): Result + decodeFunctionResult( + functionFragment: 'completeIntentWithBalanceChecks', + data: BytesLike + ): Result + + events: {} +} + +export interface SynapseIntentRouter extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this + attach(addressOrName: string): this + deployed(): Promise + + interface: SynapseIntentRouterInterface + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise> + + listeners( + eventFilter?: TypedEventFilter + ): Array> + listeners(eventName?: string): Array + removeAllListeners( + eventFilter: TypedEventFilter + ): this + removeAllListeners(eventName?: string): this + off: OnEvent + on: OnEvent + once: OnEvent + removeListener: OnEvent + + functions: { + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise<[string]> + + completeIntent( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: PayableOverrides & { from?: string } + ): Promise + + completeIntentWithBalanceChecks( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: PayableOverrides & { from?: string } + ): Promise + } + + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + completeIntent( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: PayableOverrides & { from?: string } + ): Promise + + completeIntentWithBalanceChecks( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: PayableOverrides & { from?: string } + ): Promise + + callStatic: { + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + completeIntent( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: CallOverrides + ): Promise + + completeIntentWithBalanceChecks( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: CallOverrides + ): Promise + } + + filters: {} + + estimateGas: { + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + completeIntent( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: PayableOverrides & { from?: string } + ): Promise + + completeIntentWithBalanceChecks( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: PayableOverrides & { from?: string } + ): Promise + } + + populateTransaction: { + NATIVE_GAS_TOKEN(overrides?: CallOverrides): Promise + + completeIntent( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: PayableOverrides & { from?: string } + ): Promise + + completeIntentWithBalanceChecks( + zapRecipient: string, + amountIn: BigNumberish, + deadline: BigNumberish, + steps: ISynapseIntentRouter.StepParamsStruct[], + overrides?: PayableOverrides & { from?: string } + ): Promise + } +} diff --git a/packages/sdk-router/src/utils/RouterCache.ts b/packages/sdk-router/src/utils/RouterCache.ts index bb2a329a62..e117cd4433 100644 --- a/packages/sdk-router/src/utils/RouterCache.ts +++ b/packages/sdk-router/src/utils/RouterCache.ts @@ -2,6 +2,8 @@ /* @ts-ignore */ import NodeCache from 'node-cache' +import { logger } from './logger' + // export const CACHE_HYDRATION_DELAY = { // RFQ: 0, // CCTP: 0, @@ -56,10 +58,18 @@ export const RouterCache = (maxAge: number) => { cache.set(key, res) return res }) - .catch((e) => { - console.error( - `[SynapseSDK: RouterCache] Error on ChainID ${this.chainId}: `, - e + .catch((error) => { + logger.error( + { + target, + args, + propertyKey, + name: target.constructor.name, + address: this.address, + chainId: this.chainId, + error, + }, + '[SynapseSDK: RouterCache] Error' ) }) return result diff --git a/packages/sdk-router/src/utils/addressUtils.test.ts b/packages/sdk-router/src/utils/addressUtils.test.ts new file mode 100644 index 0000000000..5ceff95d5b --- /dev/null +++ b/packages/sdk-router/src/utils/addressUtils.test.ts @@ -0,0 +1,114 @@ +import { isSameAddress } from './addressUtils' + +describe('isSameAddress', () => { + const lowerCaseAlice = '0x0123456789abcdef0123456789abcdef01234567' + const checkSumdAlice = '0x0123456789abcDEF0123456789abCDef01234567' + const upperCaseAlice = '0x0123456789ABCDEF0123456789ABCDEF01234567' + + const lowerCaseBob = '0x0123456789abcdef0123456789abcdef01234568' + const checkSumdBob = '0x0123456789ABCDeF0123456789aBcdEF01234568' + const upperCaseBob = '0x0123456789ABCDEF0123456789ABCDEF01234568' + + describe('True when the addresses are the same', () => { + it('Both lowercase', () => { + expect(isSameAddress(lowerCaseAlice, lowerCaseAlice)).toBe(true) + expect(isSameAddress(lowerCaseBob, lowerCaseBob)).toBe(true) + }) + + it('Both checksummed', () => { + expect(isSameAddress(checkSumdAlice, checkSumdAlice)).toBe(true) + expect(isSameAddress(checkSumdBob, checkSumdBob)).toBe(true) + }) + + it('Both uppercase', () => { + expect(isSameAddress(upperCaseAlice, upperCaseAlice)).toBe(true) + expect(isSameAddress(upperCaseBob, upperCaseBob)).toBe(true) + }) + + it('Lowercase and checksummed', () => { + expect(isSameAddress(lowerCaseAlice, checkSumdAlice)).toBe(true) + expect(isSameAddress(checkSumdAlice, lowerCaseAlice)).toBe(true) + expect(isSameAddress(lowerCaseBob, checkSumdBob)).toBe(true) + expect(isSameAddress(checkSumdBob, lowerCaseBob)).toBe(true) + }) + + it('Lowercase and uppercase', () => { + expect(isSameAddress(lowerCaseAlice, upperCaseAlice)).toBe(true) + expect(isSameAddress(upperCaseAlice, lowerCaseAlice)).toBe(true) + expect(isSameAddress(lowerCaseBob, upperCaseBob)).toBe(true) + expect(isSameAddress(upperCaseBob, lowerCaseBob)).toBe(true) + }) + + it('Checksummed and uppercase', () => { + expect(isSameAddress(checkSumdAlice, upperCaseAlice)).toBe(true) + expect(isSameAddress(upperCaseAlice, checkSumdAlice)).toBe(true) + expect(isSameAddress(checkSumdBob, upperCaseBob)).toBe(true) + expect(isSameAddress(upperCaseBob, checkSumdBob)).toBe(true) + }) + }) + + describe('False when the addresses are different', () => { + it('Both lowercase', () => { + expect(isSameAddress(lowerCaseAlice, lowerCaseBob)).toBe(false) + expect(isSameAddress(lowerCaseBob, lowerCaseAlice)).toBe(false) + }) + + it('Both checksummed', () => { + expect(isSameAddress(checkSumdAlice, checkSumdBob)).toBe(false) + expect(isSameAddress(checkSumdBob, checkSumdAlice)).toBe(false) + }) + + it('Both uppercase', () => { + expect(isSameAddress(upperCaseAlice, upperCaseBob)).toBe(false) + expect(isSameAddress(upperCaseBob, upperCaseAlice)).toBe(false) + }) + + it('Lowercase and checksummed', () => { + expect(isSameAddress(lowerCaseAlice, checkSumdBob)).toBe(false) + expect(isSameAddress(checkSumdBob, lowerCaseAlice)).toBe(false) + }) + + it('Lowercase and uppercase', () => { + expect(isSameAddress(lowerCaseAlice, upperCaseBob)).toBe(false) + expect(isSameAddress(upperCaseBob, lowerCaseAlice)).toBe(false) + }) + + it('Checksummed and uppercase', () => { + expect(isSameAddress(checkSumdAlice, upperCaseBob)).toBe(false) + expect(isSameAddress(upperCaseBob, checkSumdAlice)).toBe(false) + }) + }) + + describe('False when one of the addresses is undefined', () => { + it('single undefined', () => { + expect(isSameAddress(undefined, lowerCaseAlice)).toBe(false) + expect(isSameAddress(lowerCaseAlice, undefined)).toBe(false) + }) + + it('both undefined', () => { + expect(isSameAddress(undefined, undefined)).toBe(false) + }) + }) + + describe('False when one of the addresses is empty', () => { + it('single empty', () => { + expect(isSameAddress('', lowerCaseAlice)).toBe(false) + expect(isSameAddress(lowerCaseAlice, '')).toBe(false) + }) + + it('both empty', () => { + expect(isSameAddress('', '')).toBe(false) + }) + }) + + describe('False when one of the addresses is null', () => { + it('single null', () => { + expect(isSameAddress(null as any, lowerCaseAlice)).toBe(false) + expect(isSameAddress(lowerCaseAlice, null as any)).toBe(false) + }) + + it('both null', () => { + expect(isSameAddress(null as any, null as any)).toBe(false) + }) + }) +}) diff --git a/packages/sdk-router/src/utils/addressUtils.ts b/packages/sdk-router/src/utils/addressUtils.ts new file mode 100644 index 0000000000..0856ab32c5 --- /dev/null +++ b/packages/sdk-router/src/utils/addressUtils.ts @@ -0,0 +1,3 @@ +export const isSameAddress = (addr1?: string, addr2?: string): boolean => { + return !!addr1 && !!addr2 && addr1.toLowerCase() === addr2.toLowerCase() +} diff --git a/packages/sdk-router/src/utils/handleNativeToken.ts b/packages/sdk-router/src/utils/handleNativeToken.ts index 4a4eec3407..1c093d36d7 100644 --- a/packages/sdk-router/src/utils/handleNativeToken.ts +++ b/packages/sdk-router/src/utils/handleNativeToken.ts @@ -2,6 +2,8 @@ import { AddressZero, Zero } from '@ethersproject/constants' import { BigNumber } from '@ethersproject/bignumber' import { PopulatedTransaction } from '@ethersproject/contracts' +import { isSameAddress } from './addressUtils' + export const ETH_NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' @@ -12,7 +14,7 @@ export const handleNativeToken = (tokenAddr: string) => { } export const isNativeToken = (tokenAddr: string): boolean => { - return tokenAddr.toLowerCase() === ETH_NATIVE_TOKEN_ADDRESS.toLowerCase() + return isSameAddress(tokenAddr, ETH_NATIVE_TOKEN_ADDRESS) } /** diff --git a/packages/sdk-router/src/utils/logger.ts b/packages/sdk-router/src/utils/logger.ts new file mode 100644 index 0000000000..326b0282fc --- /dev/null +++ b/packages/sdk-router/src/utils/logger.ts @@ -0,0 +1,18 @@ +export const logger = console + +// Decorator to log the execution time of a function +export const logExecutionTime = + (functionName: string) => + (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) => { + const originalMethod = descriptor.value + + descriptor.value = async function (...args: any[]) { + const startTime = Date.now() + const result = await originalMethod.apply(this, args) + const elapsedTime = Date.now() - startTime + logger.info({ args }, `${functionName} execution time: ${elapsedTime}ms`) + return result + } + + return descriptor + } diff --git a/packages/sdk-router/src/utils/logs.ts b/packages/sdk-router/src/utils/logs.ts index 2b8c2f1e4e..cc264a3798 100644 --- a/packages/sdk-router/src/utils/logs.ts +++ b/packages/sdk-router/src/utils/logs.ts @@ -2,6 +2,8 @@ import { Log, Provider } from '@ethersproject/abstract-provider' import { Contract } from '@ethersproject/contracts' import { Interface } from '@ethersproject/abi' +import { isSameAddress } from './addressUtils' + /** * Extracts the first log from a transaction receipt that matches * the provided contract and any of the provided event names. @@ -26,7 +28,10 @@ export const getMatchingTxLog = async ( const topics = getEventTopics(contract.interface, eventNames) // Find the log with the correct contract address and topic matching any of the provided topics const matchingLog = txReceipt.logs.find((log) => { - return log.address === contract.address && topics.includes(log.topics[0]) + return ( + isSameAddress(log.address, contract.address) && + topics.includes(log.topics[0]) + ) }) if (!matchingLog) { // Throw an error and include the event names in the message