diff --git a/lib/abis/RelayOrderReactor.json b/lib/abis/RelayOrderReactor.json new file mode 100644 index 00000000..22ef5421 --- /dev/null +++ b/lib/abis/RelayOrderReactor.json @@ -0,0 +1,226 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "RelayOrderReactor", + "sourceName": "src/reactors/RelayOrderReactor.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_universalRouter", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "DeadlineBeforeEndTime", + "type": "error" + }, + { + "inputs": [], + "name": "EndTimeBeforeStartTime", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAmounts", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidReactor", + "type": "error" + }, + { + "inputs": [], + "name": "UnsafeCast", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "filler", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "swapper", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + } + ], + "name": "Relay", + "type": "event" + }, + { + "inputs": [], + "name": "PERMIT2", + "outputs": [ + { + "internalType": "contract IPermit2", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "order", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "sig", + "type": "bytes" + } + ], + "internalType": "struct SignedOrder", + "name": "signedOrder", + "type": "tuple" + } + ], + "name": "execute", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "order", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "sig", + "type": "bytes" + } + ], + "internalType": "struct SignedOrder", + "name": "signedOrder", + "type": "tuple" + }, + { + "internalType": "address", + "name": "feeRecipient", + "type": "address" + } + ], + "name": "execute", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "data", + "type": "bytes[]" + } + ], + "name": "multicall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "universalRouter", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x60a03461007057601f611cfa38819003918201601f19168301916001600160401b038311848410176100755780849260209460405283398101031261007057516001600160a01b038116810361007057608052604051611c6e908161008c823960805181818160bf0152610ca10152f35b600080fd5b634e487b7160e01b600052604160045260246000fdfe6080604052600436101561001257600080fd5b6000803560e01c90816335a9e4df1461007a575080633f62192e146100755780636afdd85014610070578063ac9650d81461006b578063d339056d146100665763e956bbdf1461006157600080fd5b610667565b61033a565b610294565b610164565b610109565b346100e857807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e85773ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001660805260206080f35b80fd5b60009103126100f657565b600080fd5b908160409103126100f65790565b346100f65760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f65760043567ffffffffffffffff81116100f65761015b6101629136906004016100fb565b3390610b1c565b005b346100f65760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f65760206040516e22d473030f116ddee9f6b43ac78ba38152f35b60005b8381106101bf5750506000910152565b81810151838201526020016101af565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f60209361020b815180928187528780880191016101ac565b0116010190565b6020808201906020835283518092526040830192602060408460051b8301019501936000915b8483106102485750505050505090565b9091929394958480610284837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187528a516101cf565b9801930193019194939290610238565b346100f65760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f65767ffffffffffffffff6004358181116100f657366023820112156100f65780600401359182116100f6573660248360051b830101116100f65761031891602461030c920161094e565b60405191829182610212565b0390f35b73ffffffffffffffffffffffffffffffffffffffff8116036100f657565b346100f6576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f6576004356103768161031c565b6024356103828161031c565b60443561038e8161031c565b60a43560843560643560ff831683036100f65760c4359360e435956040516020808201917f3644e515000000000000000000000000000000000000000000000000000000008352600481526103e28161072f565b60009283838d829373ffffffffffffffffffffffffffffffffffffffff82169573c02aaa39b223fe8d0a0e5c4f27ead9083c756cc28703610647575b5050505061043a575b5050501561043157005b610162976116f0565b919250907fdbb8cf42e1ecb028be3f3dbc922e1d878b963f411dc388ced501601c60f7c6f7036105bc576040517f7ecebe0000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff84166004820152908290829060249082905afa9182156105b757866105786000948594859161058a575b506040517f8fcbaf0c000000000000000000000000000000000000000000000000000000008582015273ffffffffffffffffffffffffffffffffffffffff80891660248301528916604482015260648101919091526084810192909252600160a483015260ff8a1660c483015260e482018b905261010482018c90528161012481015b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282610783565b905b81519101828c5af1388080610427565b6105aa9150843d86116105b0575b6105a28183610783565b810190610ce0565b386104c9565b503d610598565b610cef565b506040517fd505accf000000000000000000000000000000000000000000000000000000008183015273ffffffffffffffffffffffffffffffffffffffff808416602483015284166044820152606481018590526084810186905260ff871660a482015260c4810188905260e48101899052600091829161064181610104810161054c565b9061057a565b5192945090611388fa823d149351938415151616809390838d388061041e565b346100f65760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f65760043567ffffffffffffffff81116100f6576106b96101629136906004016100fb565b602435906106c68261031c565b610b1c565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6080810190811067ffffffffffffffff82111761071657604052565b6106cb565b67ffffffffffffffff811161071657604052565b6040810190811067ffffffffffffffff82111761071657604052565b6060810190811067ffffffffffffffff82111761071657604052565b60a0810190811067ffffffffffffffff82111761071657604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761071657604052565b604051906107d18261074b565b565b604051906107d1826106fa565b604051906107d18261072f565b67ffffffffffffffff81116107165760051b60200190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1813603018212156100f6570180359067ffffffffffffffff82116100f6576020019181360383136100f657565b908210156108a05761089c9160051b810190610834565b9091565b610805565b908092918237016000815290565b67ffffffffffffffff811161071657601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b3d15610918573d906108fe826108b3565b9161090c6040519384610783565b82523d6000602084013e565b606090565b8051156108a05760200190565b8051600110156108a05760400190565b80518210156108a05760209160051b010190565b91909161095a836107ed565b90604061096a6040519384610783565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0610997866107ed565b0160005b818110610a0f575050829460005b8181106109b7575050505050565b6000806109c5838588610885565b906109d48751809381936108a5565b0390305af46109e16108ed565b9015610a0757906001916109f5828861093a565b52610a00818761093a565b50016109a9565b602081519101fd5b80606060208093880101520161099b565b91908260609103126100f6576040516060810181811067ffffffffffffffff8211176107165760405260408082948035610a598161031c565b845260208101356020850152013591610a718361031c565b0152565b91908260a09103126100f65760405160a0810181811067ffffffffffffffff8211176107165760405260808082948035610aae8161031c565b84526020810135602085015260408101356040850152606081013560608501520135910152565b81601f820112156100f657803590610aec826108b3565b92610afa6040519485610783565b828452602083830101116100f657816000926020809301838601378301015290565b90610b278280610834565b819391019260209384828203126100f657813567ffffffffffffffff928382116100f6570193848203926101a084126100f6576040956080875195610b6b876106fa565b126100f6578651610b7b816106fa565b8135610b868161031c565b815288820135610b958161031c565b898201528782013588820152606082013560608201528552610bba8460808301610a20565b88860152610bcb8460e08301610a75565b878601526101808101359182116100f6578693610c1b92610bec9201610ad5565b9160608501928352610bfd85610cfb565b610c13610c09866110d5565b9689810190610834565b918787611468565b518581519182610c96575b50505073ffffffffffffffffffffffffffffffffffffffff90610c827fbdc51d356193695661ad4ba75c0bfa57f277fdefd271d6267cf74dc93f1c86d393519687015173ffffffffffffffffffffffffffffffffffffffff1690565b9501519351938452909316923392602090a4565b6000935083929101827f00000000000000000000000000000000000000000000000000000000000000005af1610cca6108ed565b9015610cd95780858592610c26565b8481519101fd5b908160209103126100f6575190565b6040513d6000823e3d90fd5b60608151015160806040830151015111610d5957515173ffffffffffffffffffffffffffffffffffffffff163003610d2f57565b60046040517f4ddf4a64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f773a6187000000000000000000000000000000000000000000000000000000008152fd5b6040517f52656c61794f726465722800000000000000000000000000000000000000000060208201527f52656c61794f72646572496e666f20696e666f2c000000000000000000000000602b8201527f496e70757420696e7075742c0000000000000000000000000000000000000000603f8201527f466565457363616c61746f72206665652c000000000000000000000000000000604b8201527f627974657320756e6976657273616c526f7574657243616c6c64617461290000605c820152605a8152610e51816106fa565b90565b6040517f466565457363616c61746f72280000000000000000000000000000000000000060208201527f6164647265737320746f6b656e2c000000000000000000000000000000000000602d8201527f75696e74323536207374617274416d6f756e742c000000000000000000000000603b8201527f75696e7432353620656e64416d6f756e742c0000000000000000000000000000604f8201527f75696e7432353620737461727454696d652c000000000000000000000000000060618201527f75696e7432353620656e6454696d652900000000000000000000000000000000607382015260638152610e5181610767565b6040517f496e70757428000000000000000000000000000000000000000000000000000060208201527f6164647265737320746f6b656e2c00000000000000000000000000000000000060268201527f75696e7432353620616d6f756e742c000000000000000000000000000000000060348201527f6164647265737320726563697069656e74290000000000000000000000000000604382015260358152610e518161074b565b6040517f52656c61794f72646572496e666f28000000000000000000000000000000000060208201527f616464726573732072656163746f722c00000000000000000000000000000000602f8201527f6164647265737320737761707065722c00000000000000000000000000000000603f8201527f75696e74323536206e6f6e63652c000000000000000000000000000000000000604f8201527f75696e7432353620646561646c696e6529000000000000000000000000000000605d820152604e8152610e51816106fa565b906110d1602092828151948592016101ac565b0190565b6110dd610d83565b906111c96110e9610e54565b926110f2610f48565b936111636110fe610ff0565b91604051809360209889938461111d818601998a8151938492016101ac565b8401611131825180938880850191016101ac565b01611144825180938780850191016101ac565b01611157825180938680850191016101ac565b01038084520182610783565b5190209261054c61117484516118b2565b9361118183820151611932565b9060606111916040830151611990565b91015184815191012091604051968795860198899192608093969594919660a084019784526020840152604083015260608201520152565b51902090565b611299610e516111dd610e54565b61054c6111e8610f48565b60336111f2610d83565b916111fb610ff0565b906112d16040519461120c8661074b565b602e86526020927f546f6b656e5065726d697373696f6e73286164647265737320746f6b656e2c75848801527f696e7432353620616d6f756e74290000000000000000000000000000000000006040880152856040519b8c809b7f52656c61794f72646572207769746e6573732900000000000000000000000000888301528781519485930191016101ac565b89016112ad82518093878a850191016101ac565b016112c0825180938689850191016101ac565b0191835193849186850191016101ac565b0101906110be565b90815180825260208080930193019160005b8281106112f9575050505090565b909192938260408261132e60019489516020809173ffffffffffffffffffffffffffffffffffffffff81511684520151910152565b019501939291016112eb565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b9593909796949160c087526101208701988051606060c08a01528051809b5261014089019a60208092019160005b82811061142657505050506113e7610e51999a611418969594936040848c60e0602061140698015191015201516101008c01528a820360208c01526112d9565b73ffffffffffffffffffffffffffffffffffffffff9094166040890152565b606087015285820360808701526101cf565b9260a081850391015261133a565b9091929c8260408f9261145c81600195516020809173ffffffffffffffffffffffffffffffffffffffff81511684520151910152565b019e01939291016113a7565b93909360408051936114798561074b565b6002855260005b8281106115db5750602061151c6115589282860151836114b4825173ffffffffffffffffffffffffffffffffffffffff1690565b9101516114de6114c26107e0565b73ffffffffffffffffffffffffffffffffffffffff9093168352565b848201526114eb8961091d565b526114f58861091d565b5061150285870151611b21565b61150b8961092a565b526115158861092a565b5085611a30565b938051606085820151910151906115316107c4565b988952838901528488015251015173ffffffffffffffffffffffffffffffffffffffff1690565b936115616111cf565b916e22d473030f116ddee9f6b43ac78ba394853b156100f65760009788946115b793519a8b998a9889977ffe8ec1a700000000000000000000000000000000000000000000000000000000895260048901611379565b03925af180156105b7576115c85750565b806115d56107d19261071b565b806100eb565b6020906115e6611a17565b82828901015201611480565b519065ffffffffffff821682036100f657565b908160609103126100f657805161161b8161031c565b91610e51604061162d602085016115f2565b93016115f2565b92917fff00000000000000000000000000000000000000000000000000000000000000916040519460208601526040850152166060830152604182526107d1826106fa565b6040610e5194936101009373ffffffffffffffffffffffffffffffffffffffff8091168452815181815116602086015281602082015116848601526060848201519165ffffffffffff80931682880152015116608085015260208201511660a0840152015160c08201528160e082015201906101cf565b6040517f927da10500000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8381166004830152918216602482018190529184166044820152909791966e22d473030f116ddee9f6b43ac78ba39690959294909391906060826064818b5afa9182156105b75760009261187a575b5061178490611ad6565b61178c6107d3565b73ffffffffffffffffffffffffffffffffffffffff909a168a5273ffffffffffffffffffffffffffffffffffffffff1660208a015265ffffffffffff60408a015265ffffffffffff1660608901526117e26107c4565b97885273ffffffffffffffffffffffffffffffffffffffff166020880152604087015260f81b7fff00000000000000000000000000000000000000000000000000000000000000169061183492611634565b90803b156100f6576115b79360008094604051968795869485937f2b67b57000000000000000000000000000000000000000000000000000000000855260048501611679565b6117849192506118a19060603d6060116118ab575b6118998183610783565b810190611605565b915050919061177a565b503d61188f565b6118ba610ff0565b602081519101209073ffffffffffffffffffffffffffffffffffffffff908181511691602082015116906060604082015191015191604051936020850195865260408501526060840152608083015260a082015260a0815260c0810181811067ffffffffffffffff8211176107165760405251902090565b61193a610f48565b602081519101209073ffffffffffffffffffffffffffffffffffffffff9081815116916040602083015192015116906040519260208401948552604084015260608301526080820152608081526111c981610767565b611998610e54565b602081519101209073ffffffffffffffffffffffffffffffffffffffff8151169060208101519060408101516080606083015192015192604051946020860196875260408601526060850152608084015260a083015260c082015260c0815260e0810181811067ffffffffffffffff8211176107165760405251902090565b60405190611a248261072f565b60006020838281520152565b919060409060405191611a428361074b565b6002835260005b818110611abf57505090611aad611abc92604083966020810151602073ffffffffffffffffffffffffffffffffffffffff8483015116910151611a8d6114c26107e0565b6020820152611a9b8661091d565b52611aa58561091d565b500151611b5e565b611ab68261092a565b5261092a565b50565b602090611aca611a17565b82828701015201611a49565b73ffffffffffffffffffffffffffffffffffffffff90818111611af7571690565b60046040517fc4bd89a9000000000000000000000000000000000000000000000000000000008152fd5b611b29611a17565b50604073ffffffffffffffffffffffffffffffffffffffff82511691015160405191611b548361072f565b8252602082015290565b611b66611a17565b5060208101516040820151916080606082015191015190838311600014611bb15760046040517fd856fc5a000000000000000000000000000000000000000000000000000000008152fd5b80821015611be35760046040517f43133453000000000000000000000000000000000000000000000000000000008152fd5b428211611c01575050505b611bf96114c26107e0565b602082015290565b919290919083428310611c175750505050611bee565b82611c2794039242039103611c2d565b01611bee565b817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0481118202158302156100f65702049056fea164736f6c6343000818000a", + "deployedBytecode": "0x6080604052600436101561001257600080fd5b6000803560e01c90816335a9e4df1461007a575080633f62192e146100755780636afdd85014610070578063ac9650d81461006b578063d339056d146100665763e956bbdf1461006157600080fd5b610667565b61033a565b610294565b610164565b610109565b346100e857807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e85773ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001660805260206080f35b80fd5b60009103126100f657565b600080fd5b908160409103126100f65790565b346100f65760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f65760043567ffffffffffffffff81116100f65761015b6101629136906004016100fb565b3390610b1c565b005b346100f65760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f65760206040516e22d473030f116ddee9f6b43ac78ba38152f35b60005b8381106101bf5750506000910152565b81810151838201526020016101af565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f60209361020b815180928187528780880191016101ac565b0116010190565b6020808201906020835283518092526040830192602060408460051b8301019501936000915b8483106102485750505050505090565b9091929394958480610284837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187528a516101cf565b9801930193019194939290610238565b346100f65760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f65767ffffffffffffffff6004358181116100f657366023820112156100f65780600401359182116100f6573660248360051b830101116100f65761031891602461030c920161094e565b60405191829182610212565b0390f35b73ffffffffffffffffffffffffffffffffffffffff8116036100f657565b346100f6576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f6576004356103768161031c565b6024356103828161031c565b60443561038e8161031c565b60a43560843560643560ff831683036100f65760c4359360e435956040516020808201917f3644e515000000000000000000000000000000000000000000000000000000008352600481526103e28161072f565b60009283838d829373ffffffffffffffffffffffffffffffffffffffff82169573c02aaa39b223fe8d0a0e5c4f27ead9083c756cc28703610647575b5050505061043a575b5050501561043157005b610162976116f0565b919250907fdbb8cf42e1ecb028be3f3dbc922e1d878b963f411dc388ced501601c60f7c6f7036105bc576040517f7ecebe0000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff84166004820152908290829060249082905afa9182156105b757866105786000948594859161058a575b506040517f8fcbaf0c000000000000000000000000000000000000000000000000000000008582015273ffffffffffffffffffffffffffffffffffffffff80891660248301528916604482015260648101919091526084810192909252600160a483015260ff8a1660c483015260e482018b905261010482018c90528161012481015b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282610783565b905b81519101828c5af1388080610427565b6105aa9150843d86116105b0575b6105a28183610783565b810190610ce0565b386104c9565b503d610598565b610cef565b506040517fd505accf000000000000000000000000000000000000000000000000000000008183015273ffffffffffffffffffffffffffffffffffffffff808416602483015284166044820152606481018590526084810186905260ff871660a482015260c4810188905260e48101899052600091829161064181610104810161054c565b9061057a565b5192945090611388fa823d149351938415151616809390838d388061041e565b346100f65760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100f65760043567ffffffffffffffff81116100f6576106b96101629136906004016100fb565b602435906106c68261031c565b610b1c565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6080810190811067ffffffffffffffff82111761071657604052565b6106cb565b67ffffffffffffffff811161071657604052565b6040810190811067ffffffffffffffff82111761071657604052565b6060810190811067ffffffffffffffff82111761071657604052565b60a0810190811067ffffffffffffffff82111761071657604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761071657604052565b604051906107d18261074b565b565b604051906107d1826106fa565b604051906107d18261072f565b67ffffffffffffffff81116107165760051b60200190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1813603018212156100f6570180359067ffffffffffffffff82116100f6576020019181360383136100f657565b908210156108a05761089c9160051b810190610834565b9091565b610805565b908092918237016000815290565b67ffffffffffffffff811161071657601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b3d15610918573d906108fe826108b3565b9161090c6040519384610783565b82523d6000602084013e565b606090565b8051156108a05760200190565b8051600110156108a05760400190565b80518210156108a05760209160051b010190565b91909161095a836107ed565b90604061096a6040519384610783565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0610997866107ed565b0160005b818110610a0f575050829460005b8181106109b7575050505050565b6000806109c5838588610885565b906109d48751809381936108a5565b0390305af46109e16108ed565b9015610a0757906001916109f5828861093a565b52610a00818761093a565b50016109a9565b602081519101fd5b80606060208093880101520161099b565b91908260609103126100f6576040516060810181811067ffffffffffffffff8211176107165760405260408082948035610a598161031c565b845260208101356020850152013591610a718361031c565b0152565b91908260a09103126100f65760405160a0810181811067ffffffffffffffff8211176107165760405260808082948035610aae8161031c565b84526020810135602085015260408101356040850152606081013560608501520135910152565b81601f820112156100f657803590610aec826108b3565b92610afa6040519485610783565b828452602083830101116100f657816000926020809301838601378301015290565b90610b278280610834565b819391019260209384828203126100f657813567ffffffffffffffff928382116100f6570193848203926101a084126100f6576040956080875195610b6b876106fa565b126100f6578651610b7b816106fa565b8135610b868161031c565b815288820135610b958161031c565b898201528782013588820152606082013560608201528552610bba8460808301610a20565b88860152610bcb8460e08301610a75565b878601526101808101359182116100f6578693610c1b92610bec9201610ad5565b9160608501928352610bfd85610cfb565b610c13610c09866110d5565b9689810190610834565b918787611468565b518581519182610c96575b50505073ffffffffffffffffffffffffffffffffffffffff90610c827fbdc51d356193695661ad4ba75c0bfa57f277fdefd271d6267cf74dc93f1c86d393519687015173ffffffffffffffffffffffffffffffffffffffff1690565b9501519351938452909316923392602090a4565b6000935083929101827f00000000000000000000000000000000000000000000000000000000000000005af1610cca6108ed565b9015610cd95780858592610c26565b8481519101fd5b908160209103126100f6575190565b6040513d6000823e3d90fd5b60608151015160806040830151015111610d5957515173ffffffffffffffffffffffffffffffffffffffff163003610d2f57565b60046040517f4ddf4a64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f773a6187000000000000000000000000000000000000000000000000000000008152fd5b6040517f52656c61794f726465722800000000000000000000000000000000000000000060208201527f52656c61794f72646572496e666f20696e666f2c000000000000000000000000602b8201527f496e70757420696e7075742c0000000000000000000000000000000000000000603f8201527f466565457363616c61746f72206665652c000000000000000000000000000000604b8201527f627974657320756e6976657273616c526f7574657243616c6c64617461290000605c820152605a8152610e51816106fa565b90565b6040517f466565457363616c61746f72280000000000000000000000000000000000000060208201527f6164647265737320746f6b656e2c000000000000000000000000000000000000602d8201527f75696e74323536207374617274416d6f756e742c000000000000000000000000603b8201527f75696e7432353620656e64416d6f756e742c0000000000000000000000000000604f8201527f75696e7432353620737461727454696d652c000000000000000000000000000060618201527f75696e7432353620656e6454696d652900000000000000000000000000000000607382015260638152610e5181610767565b6040517f496e70757428000000000000000000000000000000000000000000000000000060208201527f6164647265737320746f6b656e2c00000000000000000000000000000000000060268201527f75696e7432353620616d6f756e742c000000000000000000000000000000000060348201527f6164647265737320726563697069656e74290000000000000000000000000000604382015260358152610e518161074b565b6040517f52656c61794f72646572496e666f28000000000000000000000000000000000060208201527f616464726573732072656163746f722c00000000000000000000000000000000602f8201527f6164647265737320737761707065722c00000000000000000000000000000000603f8201527f75696e74323536206e6f6e63652c000000000000000000000000000000000000604f8201527f75696e7432353620646561646c696e6529000000000000000000000000000000605d820152604e8152610e51816106fa565b906110d1602092828151948592016101ac565b0190565b6110dd610d83565b906111c96110e9610e54565b926110f2610f48565b936111636110fe610ff0565b91604051809360209889938461111d818601998a8151938492016101ac565b8401611131825180938880850191016101ac565b01611144825180938780850191016101ac565b01611157825180938680850191016101ac565b01038084520182610783565b5190209261054c61117484516118b2565b9361118183820151611932565b9060606111916040830151611990565b91015184815191012091604051968795860198899192608093969594919660a084019784526020840152604083015260608201520152565b51902090565b611299610e516111dd610e54565b61054c6111e8610f48565b60336111f2610d83565b916111fb610ff0565b906112d16040519461120c8661074b565b602e86526020927f546f6b656e5065726d697373696f6e73286164647265737320746f6b656e2c75848801527f696e7432353620616d6f756e74290000000000000000000000000000000000006040880152856040519b8c809b7f52656c61794f72646572207769746e6573732900000000000000000000000000888301528781519485930191016101ac565b89016112ad82518093878a850191016101ac565b016112c0825180938689850191016101ac565b0191835193849186850191016101ac565b0101906110be565b90815180825260208080930193019160005b8281106112f9575050505090565b909192938260408261132e60019489516020809173ffffffffffffffffffffffffffffffffffffffff81511684520151910152565b019501939291016112eb565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b9593909796949160c087526101208701988051606060c08a01528051809b5261014089019a60208092019160005b82811061142657505050506113e7610e51999a611418969594936040848c60e0602061140698015191015201516101008c01528a820360208c01526112d9565b73ffffffffffffffffffffffffffffffffffffffff9094166040890152565b606087015285820360808701526101cf565b9260a081850391015261133a565b9091929c8260408f9261145c81600195516020809173ffffffffffffffffffffffffffffffffffffffff81511684520151910152565b019e01939291016113a7565b93909360408051936114798561074b565b6002855260005b8281106115db5750602061151c6115589282860151836114b4825173ffffffffffffffffffffffffffffffffffffffff1690565b9101516114de6114c26107e0565b73ffffffffffffffffffffffffffffffffffffffff9093168352565b848201526114eb8961091d565b526114f58861091d565b5061150285870151611b21565b61150b8961092a565b526115158861092a565b5085611a30565b938051606085820151910151906115316107c4565b988952838901528488015251015173ffffffffffffffffffffffffffffffffffffffff1690565b936115616111cf565b916e22d473030f116ddee9f6b43ac78ba394853b156100f65760009788946115b793519a8b998a9889977ffe8ec1a700000000000000000000000000000000000000000000000000000000895260048901611379565b03925af180156105b7576115c85750565b806115d56107d19261071b565b806100eb565b6020906115e6611a17565b82828901015201611480565b519065ffffffffffff821682036100f657565b908160609103126100f657805161161b8161031c565b91610e51604061162d602085016115f2565b93016115f2565b92917fff00000000000000000000000000000000000000000000000000000000000000916040519460208601526040850152166060830152604182526107d1826106fa565b6040610e5194936101009373ffffffffffffffffffffffffffffffffffffffff8091168452815181815116602086015281602082015116848601526060848201519165ffffffffffff80931682880152015116608085015260208201511660a0840152015160c08201528160e082015201906101cf565b6040517f927da10500000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8381166004830152918216602482018190529184166044820152909791966e22d473030f116ddee9f6b43ac78ba39690959294909391906060826064818b5afa9182156105b75760009261187a575b5061178490611ad6565b61178c6107d3565b73ffffffffffffffffffffffffffffffffffffffff909a168a5273ffffffffffffffffffffffffffffffffffffffff1660208a015265ffffffffffff60408a015265ffffffffffff1660608901526117e26107c4565b97885273ffffffffffffffffffffffffffffffffffffffff166020880152604087015260f81b7fff00000000000000000000000000000000000000000000000000000000000000169061183492611634565b90803b156100f6576115b79360008094604051968795869485937f2b67b57000000000000000000000000000000000000000000000000000000000855260048501611679565b6117849192506118a19060603d6060116118ab575b6118998183610783565b810190611605565b915050919061177a565b503d61188f565b6118ba610ff0565b602081519101209073ffffffffffffffffffffffffffffffffffffffff908181511691602082015116906060604082015191015191604051936020850195865260408501526060840152608083015260a082015260a0815260c0810181811067ffffffffffffffff8211176107165760405251902090565b61193a610f48565b602081519101209073ffffffffffffffffffffffffffffffffffffffff9081815116916040602083015192015116906040519260208401948552604084015260608301526080820152608081526111c981610767565b611998610e54565b602081519101209073ffffffffffffffffffffffffffffffffffffffff8151169060208101519060408101516080606083015192015192604051946020860196875260408601526060850152608084015260a083015260c082015260c0815260e0810181811067ffffffffffffffff8211176107165760405251902090565b60405190611a248261072f565b60006020838281520152565b919060409060405191611a428361074b565b6002835260005b818110611abf57505090611aad611abc92604083966020810151602073ffffffffffffffffffffffffffffffffffffffff8483015116910151611a8d6114c26107e0565b6020820152611a9b8661091d565b52611aa58561091d565b500151611b5e565b611ab68261092a565b5261092a565b50565b602090611aca611a17565b82828701015201611a49565b73ffffffffffffffffffffffffffffffffffffffff90818111611af7571690565b60046040517fc4bd89a9000000000000000000000000000000000000000000000000000000008152fd5b611b29611a17565b50604073ffffffffffffffffffffffffffffffffffffffff82511691015160405191611b548361072f565b8252602082015290565b611b66611a17565b5060208101516040820151916080606082015191015190838311600014611bb15760046040517fd856fc5a000000000000000000000000000000000000000000000000000000008152fd5b80821015611be35760046040517f43133453000000000000000000000000000000000000000000000000000000008152fd5b428211611c01575050505b611bf96114c26107e0565b602082015290565b919290919083428310611c175750505050611bee565b82611c2794039242039103611c2d565b01611bee565b817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0481118202158302156100f65702049056fea164736f6c6343000818000a", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/lib/entities/quote/ClassicQuote.ts b/lib/entities/quote/ClassicQuote.ts index 3390da3e..6d144547 100644 --- a/lib/entities/quote/ClassicQuote.ts +++ b/lib/entities/quote/ClassicQuote.ts @@ -160,7 +160,9 @@ export class ClassicQuote implements IQuote { : BigNumber.from(this.quoteData.amount); } - public get gasUseEstimateGasToken(): BigNumber | undefined { + public get gasUseEstimateGasToken(): BigNumber { + if (!this.quoteData.gasUseEstimateGasToken) throw new Error('No gasUseEstimateGasToken in ClassicQuote'); + return BigNumber.from(this.quoteData.gasUseEstimateGasToken); } diff --git a/lib/entities/request/index.ts b/lib/entities/request/index.ts index e097bbb2..a65a2cc0 100644 --- a/lib/entities/request/index.ts +++ b/lib/entities/request/index.ts @@ -1,3 +1,4 @@ +import { Protocol } from '@uniswap/router-sdk'; import { TradeType } from '@uniswap/sdk-core'; import { BigNumber } from 'ethers'; @@ -144,3 +145,13 @@ export function defaultRequestKey(request: QuoteRequest): string { type: info.type, }); } + +export function parseProtocol(protocol: string): Protocol { + const protocolUpper = protocol.toUpperCase(); + + if (protocolUpper in Protocol) { + return Protocol[protocolUpper as keyof typeof Protocol]; + } + + throw new Error(`Invalid protocol: ${protocol}`); +} diff --git a/lib/handlers/quote/handler.ts b/lib/handlers/quote/handler.ts index 6405bccf..2677dc45 100644 --- a/lib/handlers/quote/handler.ts +++ b/lib/handlers/quote/handler.ts @@ -18,6 +18,7 @@ import { QuoteRequest, QuoteRequestBodyJSON, QuoteRequestInfo, + RelayQuote, RequestSource, } from '../../entities'; import { TokenFetcher } from '../../fetchers/TokenFetcher'; @@ -451,27 +452,57 @@ export async function getBestQuote(quotes: Quote[], uniswapXRequested?: boolean) }, null); } -// compares two quotes of any type and returns the best one based on tradeType -export function compareQuotes(lhs: Quote, rhs: Quote, tradeType: TradeType): boolean { +export const QuoteComparisonOverrides = { + relayAndClassic(lhs: Quote, rhs: Quote): boolean { + return ( + (lhs.routingType === RoutingType.CLASSIC && rhs.routingType === RoutingType.RELAY) || + (rhs.routingType === RoutingType.CLASSIC && lhs.routingType === RoutingType.RELAY) + ); + }, + + breakTie(lhs: Quote, rhs: Quote): boolean { + // Prefer relay over classic if requested together + if (QuoteComparisonOverrides.relayAndClassic(lhs, rhs)) return lhs.routingType === RoutingType.RELAY; + // Otherwise, we default to keeping rhs in the case of a tie + return false; + }, +}; + +// Compare quotes, returning true if lhs is better than rhs +// Applies any overrides before the default comparision logic using quoted amount +export const compareQuotes = (lhs: Quote, rhs: Quote, tradeType: TradeType): boolean => { + if (getQuotedAmount(lhs, tradeType).eq(getQuotedAmount(rhs, tradeType))) { + return QuoteComparisonOverrides.breakTie(lhs, rhs); + } + + // Default comparison if no overrides apply if (tradeType === TradeType.EXACT_INPUT) { return getQuotedAmount(lhs, tradeType).gt(getQuotedAmount(rhs, tradeType)); } else { // EXACT_OUTPUT return getQuotedAmount(lhs, tradeType).lt(getQuotedAmount(rhs, tradeType)); } -} +}; const getQuotedAmount = (quote: Quote, tradeType: TradeType) => { if (tradeType === TradeType.EXACT_INPUT) { if (quote.routingType === RoutingType.CLASSIC) { return (quote as ClassicQuote).amountOutGasAndPortionAdjusted; + } else if (quote.routingType === RoutingType.DUTCH_LIMIT) { + return (quote as DutchQuote).amountOutGasAndPortionAdjusted; + } else if (quote.routingType === RoutingType.RELAY) { + return (quote as RelayQuote).classicQuote.amountOutGasAndPortionAdjusted; } - return (quote as DutchQuote).amountOutGasAndPortionAdjusted; + throw new Error(`Invalid routing type: ${quote}`); } else { if (quote.routingType === RoutingType.CLASSIC) { return (quote as ClassicQuote).amountInGasAndPortionAdjusted; + } else if (quote.routingType === RoutingType.DUTCH_LIMIT) { + return (quote as DutchQuote).amountInGasAndPortionAdjusted; + } else if (quote.routingType === RoutingType.RELAY) { + return (quote as RelayQuote).classicQuote.amountInGasAndPortionAdjusted; } - return (quote as DutchQuote).amountInGasAndPortionAdjusted; + throw new Error(`Invalid routing type: ${quote}`); } }; diff --git a/lib/handlers/quote/schema.ts b/lib/handlers/quote/schema.ts index b128c22e..b894bbc9 100644 --- a/lib/handlers/quote/schema.ts +++ b/lib/handlers/quote/schema.ts @@ -10,7 +10,12 @@ export const PostQuoteRequestBodyJoi = Joi.object({ amount: FieldValidator.amount.required(), type: FieldValidator.tradeType.required(), configs: Joi.array() - .items(FieldValidator.classicConfig, FieldValidator.dutchLimitConfig, FieldValidator.dutchV2Config) + .items( + FieldValidator.classicConfig, + FieldValidator.dutchLimitConfig, + FieldValidator.dutchV2Config, + FieldValidator.relayConfig + ) .unique((a: any, b: any) => { return a.routingType === b.routingType; }) diff --git a/lib/util/validator.ts b/lib/util/validator.ts index f44c2f07..0e859158 100644 --- a/lib/util/validator.ts +++ b/lib/util/validator.ts @@ -38,7 +38,7 @@ export class FieldValidator { public static readonly tradeType = Joi.string().valid('EXACT_INPUT', 'EXACT_OUTPUT'); - public static readonly routingType = Joi.string().valid('CLASSIC', 'DUTCH_LIMIT', 'DUTCH_V2').messages({ + public static readonly routingType = Joi.string().valid('CLASSIC', 'DUTCH_LIMIT', 'DUTCH_V2', 'RELAY').messages({ 'any.only': 'Invalid routingType', }); @@ -110,6 +110,18 @@ export class FieldValidator { useSyntheticQuotes: Joi.boolean().optional(), }); + // extends a classic request config, but requires a gasToken and has optional parameters for the fee auction + public static readonly relayConfig = this.classicConfig.keys({ + routingType: Joi.string().valid('RELAY'), + gasToken: FieldValidator.address.required(), + swapper: FieldValidator.address.optional(), + startTimeBufferSecs: FieldValidator.positiveNumber.optional(), + auctionPeriodSecs: FieldValidator.positiveNumber.optional(), + deadlineBufferSecs: FieldValidator.positiveNumber.optional(), + slippageTolerance: FieldValidator.slippageTolerance.optional(), + amountInGasTokenStartOverride: FieldValidator.amount.optional(), + }); + public static readonly dutchV2Config = Joi.object({ routingType: Joi.string().valid('DUTCH_V2'), swapper: FieldValidator.address.optional(), diff --git a/test/constants.ts b/test/constants.ts index 4aa981be..454caed6 100644 --- a/test/constants.ts +++ b/test/constants.ts @@ -1,3 +1,4 @@ +import { Protocol } from '@uniswap/router-sdk'; import { ChainId, Currency, Ether, WETH9 } from '@uniswap/sdk-core'; import { DAI_MAINNET, USDC_MAINNET, WBTC_MAINNET } from '@uniswap/smart-order-router'; import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'; @@ -49,7 +50,15 @@ export const DUTCH_V2_CONFIG = { export const CLASSIC_CONFIG = { routingType: RoutingType.CLASSIC, - protocols: ['V2', 'V3', 'MIXED'], + protocols: [Protocol.V2, Protocol.V3, Protocol.MIXED], +}; + +export const RELAY_CONFIG = { + routingType: RoutingType.RELAY, + protocols: [Protocol.V2, Protocol.V3, Protocol.MIXED], + swapper: SWAPPER, + auctionPeriodSecs: 60, + gasToken: TOKEN_IN, }; export const PERMIT2_USED = { diff --git a/test/integ/base.test.ts b/test/integ/base.test.ts index 1a9e65a7..5f529d9c 100644 --- a/test/integ/base.test.ts +++ b/test/integ/base.test.ts @@ -11,7 +11,7 @@ import { WBTC_MAINNET, WETH9, } from '@uniswap/smart-order-router'; -import { DutchOrder } from '@uniswap/uniswapx-sdk'; +import { DutchOrder, RelayOrder } from '@uniswap/uniswapx-sdk'; import { PERMIT2_ADDRESS, UNIVERSAL_ROUTER_ADDRESS as UNIVERSAL_ROUTER_ADDRESS_BY_CHAIN, @@ -20,7 +20,7 @@ import { fail } from 'assert'; import axiosStatic, { AxiosRequestConfig, AxiosResponse } from 'axios'; import axiosRetry from 'axios-retry'; import { expect } from 'chai'; -import { BigNumber, providers } from 'ethers'; +import { BigNumber, Contract, ContractFactory, providers } from 'ethers'; import hre from 'hardhat'; import _ from 'lodash'; import NodeCache from 'node-cache'; @@ -29,7 +29,7 @@ import { ClassicQuoteDataJSON } from '../../lib/entities/quote'; import { QuoteRequestBodyJSON } from '../../lib/entities/request'; import { Portion, PortionFetcher } from '../../lib/fetchers/PortionFetcher'; import { QuoteResponseJSON } from '../../lib/handlers/quote/handler'; -import { ExclusiveDutchOrderReactor__factory, Permit2__factory } from '../../lib/types/ext'; +import { ExclusiveDutchOrderReactor__factory, Permit2__factory, RelayOrderReactor__factory } from '../../lib/types/ext'; import { resetAndFundAtBlock } from '../utils/forkAndFund'; import { getBalance, getBalanceAndApprove } from '../utils/getBalanceAndApprove'; import { agEUR_MAINNET, BULLET, getAmount, UNI_MAINNET, XSGD_MAINNET } from '../utils/tokens'; @@ -291,17 +291,65 @@ export class BaseIntegrationTestSuite { }; }; + executeRelaySwap = async ( + swapper: SignerWithAddress, + filler: SignerWithAddress, + order: RelayOrder, + currencyIn: Currency, + currencyGasToken: Currency, + currencyOut: Currency + ): Promise<{ + tokenInAfter: CurrencyAmount; + tokenInBefore: CurrencyAmount; + gasTokenAfter: CurrencyAmount; + gasTokenBefore: CurrencyAmount; + tokenOutAfter: CurrencyAmount; + tokenOutBefore: CurrencyAmount; + }> => { + const reactor = RelayOrderReactor__factory.connect(order.info.reactor, filler); + + // Approve Permit2 for Alice + // Note we pass in currency.wrapped, since reactor does not support native ETH in + const tokenInBefore = await getBalanceAndApprove(swapper, PERMIT2_ADDRESS, currencyIn.wrapped); + const tokenOutBefore = await getBalance(swapper, currencyOut); + const gasTokenBefore = await getBalanceAndApprove(swapper, PERMIT2_ADDRESS, currencyGasToken.wrapped); + + const { domain, types, values } = order.permitData(); + const signature = await swapper._signTypedData(domain, types, values); + + const transactionResponse = await reactor['execute((bytes,bytes),address)']( + { order: order.serialize(), sig: signature }, + filler.address + ); + await transactionResponse.wait(); + + const tokenInAfter = await getBalance(swapper, currencyIn.wrapped); + const tokenOutAfter = await getBalance(swapper, currencyOut); + const gasTokenAfter = await getBalance(swapper, currencyGasToken.wrapped); + + return { + tokenInAfter, + tokenInBefore, + tokenOutAfter, + tokenOutBefore, + gasTokenAfter, + gasTokenBefore, + }; + }; + before = async () => { - const [alice, filler] = await ethers.getSigners(); + let alice: SignerWithAddress; + let filler: SignerWithAddress; + [alice, filler] = await ethers.getSigners(); // Make a dummy call to the API to get a block number to fork from. const quoteReq: QuoteRequestBodyJSON = { requestId: 'id', tokenIn: 'USDC', tokenInChainId: 1, - tokenOut: 'USDT', + tokenOut: 'DAI', tokenOutChainId: 1, - amount: await getAmount(1, 'EXACT_INPUT', 'USDC', 'USDT', '100'), + amount: await getAmount(1, 'EXACT_INPUT', 'USDC', 'DAI', '100'), type: 'EXACT_INPUT', configs: [ { @@ -318,7 +366,7 @@ export class BaseIntegrationTestSuite { this.block = parseInt(blockNumber) - 10; - await resetAndFundAtBlock(alice, this.block, [ + alice = await resetAndFundAtBlock(alice, this.block, [ parseAmount('80000000', USDC_MAINNET), parseAmount('50000000', USDT_MAINNET), parseAmount('100', WBTC_MAINNET), @@ -337,4 +385,10 @@ export class BaseIntegrationTestSuite { return [alice, filler]; }; + + deployContract = async (factory: ContractFactory, args: any[]): Promise => { + const contract = await factory.deploy(...args); + await contract.deployed(); + return contract; + }; } diff --git a/test/integ/quote-relay.test.ts b/test/integ/quote-relay.test.ts new file mode 100644 index 00000000..77050966 --- /dev/null +++ b/test/integ/quote-relay.test.ts @@ -0,0 +1,182 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { CurrencyAmount } from '@uniswap/sdk-core'; +import { DAI_MAINNET, ID_TO_NETWORK_NAME, USDC_MAINNET, USDT_MAINNET } from '@uniswap/smart-order-router'; +import { RelayOrder } from '@uniswap/uniswapx-sdk'; +import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'; +import { AxiosResponse } from 'axios'; +import chai, { expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import chaiSubset from 'chai-subset'; +import _ from 'lodash'; +import { RoutingType } from '../../lib/constants'; +import { QuoteRequestBodyJSON, RelayQuoteDataJSON, RoutingConfigJSON } from '../../lib/entities'; +import { QuoteResponseJSON } from '../../lib/handlers/quote/handler'; +import { RelayOrderReactor__factory } from '../../lib/types/ext'; +import { getAmount } from '../utils/tokens'; +import { BaseIntegrationTestSuite, call } from './base.test'; + +chai.use(chaiAsPromised); +chai.use(chaiSubset); + +const SLIPPAGE = '5'; + +describe('relayQuote', function () { + let baseTest: BaseIntegrationTestSuite; + let reactorAddress: string; + + // Help with test flakiness by retrying. + this.retries(2); + this.timeout(40000); + + let alice: SignerWithAddress; + let filler: SignerWithAddress; + + beforeEach(async function () { + baseTest = new BaseIntegrationTestSuite(); + [alice, filler] = await baseTest.before(); + // deploy reactor + const factory = new RelayOrderReactor__factory(alice); + const reactorContract = await baseTest.deployContract(factory, [UNIVERSAL_ROUTER_ADDRESS(1)]); + reactorAddress = reactorContract.address; + }); + + for (const type of ['EXACT_INPUT', 'EXACT_OUTPUT']) { + describe(`${ID_TO_NETWORK_NAME(1)} ${type} 2xx`, () => { + describe(`+ Execute Swap`, () => { + it(`stable -> stable, gas token == input token`, async () => { + const quoteReq: QuoteRequestBodyJSON = { + requestId: 'id', + tokenIn: USDC_MAINNET.address, + tokenInChainId: 1, + tokenOut: USDT_MAINNET.address, + tokenOutChainId: 1, + amount: await getAmount(1, type, 'USDC', 'USDT', '100'), + type, + slippageTolerance: SLIPPAGE, + configs: [ + { + routingType: RoutingType.RELAY, + protocols: ['V2', 'V3', 'MIXED'], + swapper: alice.address, + gasToken: USDC_MAINNET.address, + }, + ] as RoutingConfigJSON[], + }; + + const response: AxiosResponse = await call(quoteReq); + const { + data: { quote }, + status, + } = response; + + expect(status).to.equal(200); + const order = RelayOrder.parse((quote as RelayQuoteDataJSON).encodedOrder, 1); + order.info.reactor = reactorAddress; + + expect(order.info.swapper).to.equal(alice.address); + expect(order.info.input).to.not.be.undefined; + expect(order.info.fee).to.not.be.undefined; + expect(order.info.universalRouterCalldata).to.not.be.undefined; + + const { tokenInBefore, tokenInAfter, tokenOutBefore, tokenOutAfter } = await baseTest.executeRelaySwap( + alice, + filler, + order, + USDC_MAINNET, // tokenIn + USDC_MAINNET, // gasToken + USDT_MAINNET // outputToken + ); + + const expectedMinAmountOut = CurrencyAmount.fromRawAmount( + USDT_MAINNET, + (quote as RelayQuoteDataJSON).classicQuoteData.quote + ) + .multiply(95) + .divide(100); // apply slippage + + const expectedMaxAmountIn = CurrencyAmount.fromRawAmount( + USDC_MAINNET, + parseInt(order.info.fee.endAmount.toString()) + parseInt(order.info.input.amount.toString()) + ); + // at most netMaxAmountIn of tokenIn should be spent + expect( + tokenInBefore.subtract(tokenInAfter).lessThan(expectedMaxAmountIn) || + tokenInBefore.subtract(tokenInAfter).equalTo(expectedMaxAmountIn) + ).to.be.true; + // user should have received at least expectedAmountOut of tokenOut + expect( + tokenOutAfter.subtract(tokenOutBefore).greaterThan(expectedMinAmountOut) || + tokenOutAfter.subtract(tokenOutBefore).equalTo(expectedMinAmountOut) + ).to.be.true; + }); + + it(`stable -> stable, gas token != input token`, async () => { + const quoteReq: QuoteRequestBodyJSON = { + requestId: 'id', + tokenIn: USDC_MAINNET.address, + tokenInChainId: 1, + tokenOut: USDT_MAINNET.address, + tokenOutChainId: 1, + amount: await getAmount(1, type, 'USDC', 'USDT', '100'), + type, + slippageTolerance: SLIPPAGE, + configs: [ + { + routingType: RoutingType.RELAY, + protocols: ['V2', 'V3', 'MIXED'], + swapper: alice.address, + gasToken: DAI_MAINNET.address, + }, + ] as RoutingConfigJSON[], + }; + + const response: AxiosResponse = await call(quoteReq); + const { + data: { quote }, + status, + } = response; + + const order = new RelayOrder((quote as any).orderInfo, 1); + expect(status).to.equal(200); + + order.info.reactor = reactorAddress; + + expect(order.info.swapper).to.equal(alice.address); + expect(order.info.input).to.not.be.undefined; + expect(order.info.fee).to.not.be.undefined; + expect(order.info.universalRouterCalldata).to.not.be.undefined; + + const { tokenInBefore, tokenInAfter, gasTokenBefore, gasTokenAfter, tokenOutBefore, tokenOutAfter } = + await baseTest.executeRelaySwap(alice, filler, order, USDC_MAINNET, DAI_MAINNET, USDT_MAINNET); + + const expectedMinAmountOut = CurrencyAmount.fromRawAmount( + USDT_MAINNET, + (quote as RelayQuoteDataJSON).classicQuoteData.quote + ) + .multiply(95) + .divide(100); // apply slippage + + const tokenInMaxAmount = CurrencyAmount.fromRawAmount( + USDC_MAINNET, + parseInt(order.info.input.amount.toString()) + ); + const gasMaxAmount = CurrencyAmount.fromRawAmount(DAI_MAINNET, parseInt(order.info.fee.endAmount.toString())); + + expect( + tokenInBefore.subtract(tokenInAfter).lessThan(tokenInMaxAmount) || + tokenInBefore.subtract(tokenInAfter).equalTo(tokenInMaxAmount) + ).to.be.true; + expect( + gasTokenBefore.subtract(gasTokenAfter).lessThan(gasMaxAmount) || + gasTokenBefore.subtract(gasTokenAfter).equalTo(gasMaxAmount) + ).to.be.true; + // user should have received at least expectedAmountOut of tokenOut + expect( + tokenOutAfter.subtract(tokenOutBefore).greaterThan(expectedMinAmountOut) || + tokenOutAfter.subtract(tokenOutBefore).equalTo(expectedMinAmountOut) + ).to.be.true; + }); + }); + }); + } +}); diff --git a/test/unit/lib/entities/context/QuoteContextHandler.test.ts b/test/unit/lib/entities/context/QuoteContextHandler.test.ts index 41da7135..92596a1a 100644 --- a/test/unit/lib/entities/context/QuoteContextHandler.test.ts +++ b/test/unit/lib/entities/context/QuoteContextHandler.test.ts @@ -19,6 +19,9 @@ import { QUOTE_REQUEST_DL_EXACT_OUT, QUOTE_REQUEST_DL_NATIVE_IN, QUOTE_REQUEST_DL_NATIVE_OUT, + QUOTE_REQUEST_RELAY, + QUOTE_REQUEST_RELAY_EXACT_OUT, + RELAY_QUOTE_EXACT_IN_BETTER, } from '../../../../utils/fixtures'; class MockQuoteContext implements QuoteContext { @@ -67,6 +70,14 @@ describe('QuoteContextManager', () => { expect(requests[0]).toMatchObject(QUOTE_REQUEST_CLASSIC); }); + it('returns base request from single relay context', () => { + const context = new MockQuoteContext(QUOTE_REQUEST_RELAY); + const handler = new QuoteContextManager([context]); + const requests = handler.getRequests(); + expect(requests.length).toEqual(1); + expect(requests[0]).toMatchObject(QUOTE_REQUEST_RELAY); + }); + it('returns dependency requests from a single context', () => { const context = new MockQuoteContext(QUOTE_REQUEST_DL); context.setDependencies([QUOTE_REQUEST_CLASSIC, QUOTE_REQUEST_DL_EXACT_OUT]); @@ -78,19 +89,33 @@ describe('QuoteContextManager', () => { expect(requests[2]).toMatchObject(QUOTE_REQUEST_DL_EXACT_OUT); }); + it('returns dependency requests from a single context - relay', () => { + const context = new MockQuoteContext(QUOTE_REQUEST_RELAY); + context.setDependencies([QUOTE_REQUEST_CLASSIC]); + const handler = new QuoteContextManager([context]); + const requests = handler.getRequests(); + expect(requests.length).toEqual(2); + expect(requests[0]).toMatchObject(QUOTE_REQUEST_RELAY); + expect(requests[1]).toMatchObject(QUOTE_REQUEST_CLASSIC); + }); + it('returns dependency requests from multiple contexts in the correct order', () => { const context1 = new MockQuoteContext(QUOTE_REQUEST_DL); context1.setDependencies([QUOTE_REQUEST_DL_EXACT_OUT]); const context2 = new MockQuoteContext(QUOTE_REQUEST_CLASSIC); context2.setDependencies([QUOTE_REQUEST_DL_NATIVE_IN]); - const handler = new QuoteContextManager([context1, context2]); + const context3 = new MockQuoteContext(QUOTE_REQUEST_RELAY); + context3.setDependencies([QUOTE_REQUEST_RELAY_EXACT_OUT]); + const handler = new QuoteContextManager([context1, context2, context3]); const requests = handler.getRequests(); - expect(requests.length).toEqual(4); + expect(requests.length).toEqual(6); // user defined requests go first expect(requests[0]).toMatchObject(QUOTE_REQUEST_DL); expect(requests[1]).toMatchObject(QUOTE_REQUEST_CLASSIC); - expect(requests[2]).toMatchObject(QUOTE_REQUEST_DL_EXACT_OUT); - expect(requests[3]).toMatchObject(QUOTE_REQUEST_DL_NATIVE_IN); + expect(requests[2]).toMatchObject(QUOTE_REQUEST_RELAY); + expect(requests[3]).toMatchObject(QUOTE_REQUEST_DL_EXACT_OUT); + expect(requests[4]).toMatchObject(QUOTE_REQUEST_DL_NATIVE_IN); + expect(requests[5]).toMatchObject(QUOTE_REQUEST_RELAY_EXACT_OUT); }); it('deduplicates quote requests on info / type', () => { @@ -98,12 +123,15 @@ describe('QuoteContextManager', () => { context1.setDependencies([QUOTE_REQUEST_DL_EXACT_OUT]); const context2 = new MockQuoteContext(QUOTE_REQUEST_CLASSIC); context2.setDependencies([QUOTE_REQUEST_DL_EXACT_OUT, QUOTE_REQUEST_DL_EXACT_OUT]); - const handler = new QuoteContextManager([context1, context2]); + const context3 = new MockQuoteContext(QUOTE_REQUEST_RELAY); + context3.setDependencies([QUOTE_REQUEST_CLASSIC]); + const handler = new QuoteContextManager([context1, context2, context3]); const requests = handler.getRequests(); - expect(requests.length).toEqual(3); + expect(requests.length).toEqual(4); expect(requests[0]).toMatchObject(QUOTE_REQUEST_DL); expect(requests[1]).toMatchObject(QUOTE_REQUEST_CLASSIC); - expect(requests[2]).toMatchObject(QUOTE_REQUEST_DL_EXACT_OUT); + expect(requests[2]).toMatchObject(QUOTE_REQUEST_RELAY); + expect(requests[3]).toMatchObject(QUOTE_REQUEST_DL_EXACT_OUT); }); it('deduplicates even with differing configs', () => { @@ -149,6 +177,30 @@ describe('QuoteContextManager', () => { expect(requests[2]).toMatchObject(QUOTE_REQUEST_DL_EXACT_OUT); }); + it('merges gasToken on classic requests', () => { + const context1 = new MockQuoteContext(QUOTE_REQUEST_CLASSIC); + context1.setDependencies([]); + const context2 = new MockQuoteContext(QUOTE_REQUEST_RELAY); + const gasToken = '0x1111111111111111111111111111111111111111'; + context2.setDependencies([ + Object.assign({}, QUOTE_REQUEST_CLASSIC, { + config: { + routingType: RoutingType.CLASSIC, + protocols: ['v3'], + gasToken, + }, + key: QUOTE_REQUEST_CLASSIC.key, + }), + ]); + const handler = new QuoteContextManager([context1, context2]); + const requests = handler.getRequests(); + expect(requests.length).toEqual(2); + expect(requests[0]).toMatchObject(QUOTE_REQUEST_CLASSIC); + // injects simulateFromAddress into the classic request + expect((requests[0].config as ClassicConfig).gasToken).toEqual(gasToken); + expect(requests[1]).toMatchObject(QUOTE_REQUEST_RELAY); + }); + it('deduplicates even with differing requestIds', () => { const context1 = new MockQuoteContext(QUOTE_REQUEST_DL); context1.setDependencies([QUOTE_REQUEST_DL_EXACT_OUT]); @@ -236,11 +288,18 @@ describe('QuoteContextManager', () => { it('passes matching dependencies', async () => { const context = new MockQuoteContext(QUOTE_REQUEST_DL); context.setDependencies([CLASSIC_QUOTE_EXACT_IN_BETTER.request]); - const handler = new QuoteContextManager([context]); - await handler.resolveQuotes([DL_QUOTE_EXACT_IN_BETTER, CLASSIC_QUOTE_EXACT_IN_BETTER]); + const context2 = new MockQuoteContext(QUOTE_REQUEST_RELAY); + context2.setDependencies([RELAY_QUOTE_EXACT_IN_BETTER.request]); + const handler = new QuoteContextManager([context, context2]); + await handler.resolveQuotes([ + DL_QUOTE_EXACT_IN_BETTER, + CLASSIC_QUOTE_EXACT_IN_BETTER, + RELAY_QUOTE_EXACT_IN_BETTER, + ]); expect(context._quoteDependencies).toEqual({ [DL_QUOTE_EXACT_IN_BETTER.request.key()]: DL_QUOTE_EXACT_IN_BETTER, [CLASSIC_QUOTE_EXACT_IN_BETTER.request.key()]: CLASSIC_QUOTE_EXACT_IN_BETTER, + [RELAY_QUOTE_EXACT_IN_BETTER.request.key()]: RELAY_QUOTE_EXACT_IN_BETTER, }); }); @@ -357,6 +416,31 @@ describe('QuoteContextManager', () => { expect(merged.key()).toEqual(QUOTE_REQUEST_CLASSIC.key()); }); + it('keeps gasToken if defined in base', () => { + const baseGasToken = '0x1111111111111111111111111111111111111111'; + const layerGastoken = '0x2222222222222222222222222222222222222222'; + const base = Object.assign({}, QUOTE_REQUEST_CLASSIC, { + config: { + routingType: RoutingType.CLASSIC, + protocols: ['v3'], + gasToken: baseGasToken, + }, + }); + + const layer = Object.assign({}, QUOTE_REQUEST_CLASSIC, { + config: { + routingType: RoutingType.CLASSIC, + protocols: ['v3'], + gasToken: layerGastoken, + }, + }); + + const merged = mergeRequests(base, layer); + expect(merged).toMatchObject(base); + expect((merged.config as ClassicConfig).gasToken).toEqual(baseGasToken); + expect(merged.key()).toEqual(QUOTE_REQUEST_CLASSIC.key()); + }); + it('sets simulateFromAddress if defined in layer', () => { const layerSimulateAddress = '0x2222222222222222222222222222222222222222'; const layer = Object.assign({}, QUOTE_REQUEST_CLASSIC, { @@ -404,5 +488,21 @@ describe('QuoteContextManager', () => { expect((merged.config as ClassicConfig).recipient).toEqual(layerRecipient); expect(merged.key()).toEqual(QUOTE_REQUEST_CLASSIC.key()); }); + + it('sets gasToken if defined in layer', () => { + const layerGasToken = '0x2222222222222222222222222222222222222222'; + const layer = Object.assign({}, QUOTE_REQUEST_CLASSIC, { + config: { + routingType: RoutingType.CLASSIC, + protocols: ['v3'], + gasToken: layerGasToken, + }, + }); + + const merged = mergeRequests(QUOTE_REQUEST_CLASSIC, layer); + expect(merged).toMatchObject(QUOTE_REQUEST_CLASSIC); + expect((merged.config as ClassicConfig).gasToken).toEqual(layerGasToken); + expect(merged.key()).toEqual(QUOTE_REQUEST_CLASSIC.key()); + }); }); }); diff --git a/test/unit/lib/entities/context/RelayQuoteContext.test.ts b/test/unit/lib/entities/context/RelayQuoteContext.test.ts new file mode 100644 index 00000000..d588ad01 --- /dev/null +++ b/test/unit/lib/entities/context/RelayQuoteContext.test.ts @@ -0,0 +1,136 @@ +import Logger from 'bunyan'; + +import { NATIVE_ADDRESS, RoutingType } from '../../../../../lib/constants'; +import { RelayQuoteContext } from '../../../../../lib/entities'; +import { Erc20__factory } from '../../../../../lib/types/ext/factories/Erc20__factory'; +import { AMOUNT } from '../../../../constants'; +import { + createClassicQuote, + createRelayQuote, + DL_QUOTE_EXACT_IN_BETTER, + makeRelayRequest, + QUOTE_REQUEST_RELAY, +} from '../../../../utils/fixtures'; + +describe('RelayQuoteContext', () => { + const logger = Logger.createLogger({ name: 'test' }); + logger.level(Logger.FATAL); + let provider: any; + + beforeAll(() => { + jest.resetModules(); // Most important - it clears the cache + + jest.mock('../../../../../lib/types/ext/factories/Erc20__factory'); + Erc20__factory.connect = jest.fn().mockImplementation(() => { + return { + allowance: () => ({ gte: () => true }), + }; + }); + provider = jest.fn(); + }); + + function makeProviders() { + return { + rpcProvider: provider, + }; + } + + describe('dependencies', () => { + it('returns expected dependencies when output is weth', () => { + const context = new RelayQuoteContext(logger, QUOTE_REQUEST_RELAY, makeProviders()); + const deps = context.dependencies(); + expect(deps.length).toEqual(2); + // first is base + expect(deps[0]).toEqual(QUOTE_REQUEST_RELAY); + // second is classic + expect(deps[1].info).toEqual(QUOTE_REQUEST_RELAY.info); + expect(deps[1].routingType).toEqual(RoutingType.CLASSIC); + }); + + it('returns expected dependencies when output is not weth', () => { + const request = makeRelayRequest({ + tokenOut: '0x1111111111111111111111111111111111111111', + }); + const context = new RelayQuoteContext(logger, request, makeProviders()); + const deps = context.dependencies(); + expect(deps.length).toEqual(2); + // first is base + expect(deps[0]).toEqual(request); + // second is classic + expect(deps[1].info).toEqual(request.info); + expect(deps[1].routingType).toEqual(RoutingType.CLASSIC); + }); + }); + + describe('resolve', () => { + it('returns null if no dependencies given', async () => { + const context = new RelayQuoteContext(logger, QUOTE_REQUEST_RELAY, makeProviders()); + expect(await context.resolve({})).toEqual(null); + }); + + it('returns null if quote key is not set properly', async () => { + const context = new RelayQuoteContext(logger, QUOTE_REQUEST_RELAY, makeProviders()); + expect( + await context.resolve({ + wrong: DL_QUOTE_EXACT_IN_BETTER, + }) + ).toBeNull(); + }); + + it('returns main quote if others are null', async () => { + const context = new RelayQuoteContext(logger, QUOTE_REQUEST_RELAY, makeProviders()); + + const relayQuote = createRelayQuote({ amountOut: AMOUNT }, 'EXACT_INPUT'); + const quote = await context.resolve({ + [QUOTE_REQUEST_RELAY.key()]: relayQuote, + }); + expect(quote).toMatchObject(relayQuote); + }); + + it('reconstructs quote from dependencies if main quote is null', async () => { + const context = new RelayQuoteContext(logger, QUOTE_REQUEST_RELAY, makeProviders()); + + const classicQuote = createClassicQuote( + { quote: '10000000000', quoteGasAdjusted: '9999000000', gasUseEstimateGasToken: '1' }, + { type: 'EXACT_INPUT' } + ); + context.dependencies(); + + const quote = await context.resolve({ + [context.classicKey]: classicQuote, + }); + expect(quote?.routingType).toEqual(RoutingType.RELAY); + }); + + it('returns null if quotes have 0 amountOut', async () => { + const context = new RelayQuoteContext(logger, QUOTE_REQUEST_RELAY, makeProviders()); + + const relayQuote = createRelayQuote({ amountOut: '0' }, 'EXACT_INPUT'); + expect( + await context.resolve({ + [QUOTE_REQUEST_RELAY.key()]: relayQuote, + }) + ).toBe(null); + }); + + it('returns relay quote if tokenIn is NATIVE_ADDRESS', async () => { + const context = new RelayQuoteContext(logger, QUOTE_REQUEST_RELAY, makeProviders()); + const relayQuote = createRelayQuote({ tokenIn: NATIVE_ADDRESS, amountOut: '2' }, 'EXACT_INPUT'); + const quote = await context.resolve({ + [QUOTE_REQUEST_RELAY.key()]: relayQuote, + }); + expect(quote?.routingType).toEqual(RoutingType.RELAY); + expect(quote?.amountOut.toString()).toEqual('2'); + }); + + it('returns relay quote if tokenOut is NATIVE_ADDRESS', async () => { + const context = new RelayQuoteContext(logger, QUOTE_REQUEST_RELAY, makeProviders()); + const relayQuote = createRelayQuote({ tokenOut: NATIVE_ADDRESS, amountOut: '2' }, 'EXACT_INPUT'); + const quote = await context.resolve({ + [QUOTE_REQUEST_RELAY.key()]: relayQuote, + }); + expect(quote?.routingType).toEqual(RoutingType.RELAY); + expect(quote?.amountOut.toString()).toEqual('2'); + }); + }); +}); diff --git a/test/unit/lib/entities/quoteRequest.test.ts b/test/unit/lib/entities/quoteRequest.test.ts index 4b995f6a..4bf1b375 100644 --- a/test/unit/lib/entities/quoteRequest.test.ts +++ b/test/unit/lib/entities/quoteRequest.test.ts @@ -9,6 +9,7 @@ import { parseQuoteRequests, QuoteRequestBodyJSON, } from '../../../../lib/entities'; +import { RelayConfigJSON, RelayRequest } from '../../../../lib/entities/request/RelayRequest'; import { ValidationError } from '../../../../lib/util/errors'; import { AMOUNT, CHAIN_IN_ID, CHAIN_OUT_ID, SWAPPER, TOKEN_IN, TOKEN_OUT } from '../../../constants'; @@ -21,6 +22,14 @@ const MOCK_DL_CONFIG_JSON: DutchConfigJSON = { useSyntheticQuotes: true, }; +const MOCK_RELAY_CONFIG_JSON: RelayConfigJSON = { + routingType: RoutingType.RELAY, + swapper: SWAPPER, + auctionPeriodSecs: 60, + deadlineBufferSecs: 12, + gasToken: TOKEN_IN, +}; + const MOCK_DUTCH_V2_CONFIG_JSON: DutchV2ConfigJSON = { routingType: RoutingType.DUTCH_V2, swapper: SWAPPER, @@ -54,7 +63,7 @@ const EXACT_INPUT_MOCK_REQUEST_JSON: QuoteRequestBodyJSON = { amount: AMOUNT, type: 'EXACT_INPUT', swapper: SWAPPER, - configs: [MOCK_DL_CONFIG_JSON, CLASSIC_CONFIG_JSON], + configs: [MOCK_DL_CONFIG_JSON, MOCK_RELAY_CONFIG_JSON, CLASSIC_CONFIG_JSON], }; const EXACT_OUTPUT_MOCK_REQUEST_JSON: QuoteRequestBodyJSON = { @@ -66,7 +75,7 @@ const EXACT_OUTPUT_MOCK_REQUEST_JSON: QuoteRequestBodyJSON = { amount: AMOUNT, type: 'EXACT_OUTPUT', swapper: SWAPPER, - configs: [MOCK_DL_CONFIG_JSON, CLASSIC_CONFIG_JSON], + configs: [MOCK_DL_CONFIG_JSON, MOCK_RELAY_CONFIG_JSON, CLASSIC_CONFIG_JSON], }; const EXACT_INPUT_V2_MOCK_REQUEST_JSON: QuoteRequestBodyJSON = { @@ -112,9 +121,25 @@ describe('QuoteRequest', () => { expect(config.toJSON()).toEqual(MOCK_DL_CONFIG_JSON); }); + it('parses exactInput relay order config properly', () => { + const { quoteRequests: requests } = parseQuoteRequests(request); + const info = requests[1].info; + + const config = RelayRequest.fromRequestBody(info, MOCK_RELAY_CONFIG_JSON); + expect(config.toJSON()).toEqual(MOCK_RELAY_CONFIG_JSON); + }); + + it('parses exactOutput relay order config properly', () => { + const { quoteRequests: requests } = parseQuoteRequests(request); + const info = requests[1].info; + + const config = RelayRequest.fromRequestBody(info, MOCK_RELAY_CONFIG_JSON); + expect(config.toJSON()).toEqual(MOCK_RELAY_CONFIG_JSON); + }); + it('parses basic classic quote order config properly', () => { const { quoteRequests: requests } = parseQuoteRequests(request); - const info = requests[0].info; + const info = requests[2].info; const config = ClassicRequest.fromRequestBody(info, CLASSIC_CONFIG_JSON); expect(config.toJSON()).toEqual(CLASSIC_CONFIG_JSON); @@ -142,9 +167,17 @@ describe('QuoteRequest', () => { expect(config.info.swapper).toEqual(SWAPPER); }); + it('includes swapper in info for relay', () => { + const { quoteRequests: requests } = parseQuoteRequests(request); + const info = requests[1].info; + const config = RelayRequest.fromRequestBody(info, MOCK_RELAY_CONFIG_JSON); + + expect(config.info.swapper).toEqual(SWAPPER); + }); + it('includes swapper in info for classic', () => { const { quoteRequests: requests } = parseQuoteRequests(request); - const info = requests[0].info; + const info = requests[2].info; const config = ClassicRequest.fromRequestBody(info, CLASSIC_CONFIG_JSON); expect(config.info.swapper).toEqual(SWAPPER); diff --git a/test/unit/lib/entities/quoteResponse.test.ts b/test/unit/lib/entities/quoteResponse.test.ts index 55953d70..d4950485 100644 --- a/test/unit/lib/entities/quoteResponse.test.ts +++ b/test/unit/lib/entities/quoteResponse.test.ts @@ -1,7 +1,16 @@ -import { DutchOrder, parseValidation, ValidationType } from '@uniswap/uniswapx-sdk'; +import { DutchOrder, RelayOrder } from '@uniswap/uniswapx-sdk'; import { BigNumber } from 'ethers'; -import { ClassicQuote, ClassicQuoteDataJSON, DutchQuote, DutchQuoteJSON, DutchRequest } from '../../../../lib/entities'; +import { + ClassicQuote, + ClassicQuoteDataJSON, + DutchQuote, + DutchQuoteJSON, + DutchRequest, + RelayQuote, + RelayQuoteJSON, +} from '../../../../lib/entities'; +import { RelayRequest } from '../../../../lib/entities/request/RelayRequest'; import { AMOUNT, CHAIN_IN_ID, @@ -14,9 +23,11 @@ import { TOKEN_OUT, } from '../../../constants'; import { + CLASSIC_QUOTE_DATA_WITH_ROUTE_AND_GAS_TOKEN, CLASSIC_QUOTE_EXACT_IN_BETTER, CLASSIC_QUOTE_EXACT_OUT_BETTER, QUOTE_REQUEST_DL, + QUOTE_REQUEST_RELAY, } from '../../../utils/fixtures'; const DL_QUOTE_JSON: DutchQuoteJSON = { @@ -43,6 +54,21 @@ const DL_QUOTE_JSON_RFQ: DutchQuoteJSON = { filler: '0x1111111111111111111111111111111111111111', }; +const RELAY_QUOTE_JSON: RelayQuoteJSON = { + chainId: 1, + requestId: 'requestId', + quoteId: 'quoteId', + tokenIn: TOKEN_IN, + amountIn: AMOUNT, + tokenOut: TOKEN_OUT, + amountOut: AMOUNT, + gasToken: TOKEN_IN, + feeAmountStart: AMOUNT, + feeAmountEnd: AMOUNT, + swapper: SWAPPER, + classicQuoteData: CLASSIC_QUOTE_DATA_WITH_ROUTE_AND_GAS_TOKEN.quote, +}; + const CLASSIC_QUOTE_JSON: ClassicQuoteDataJSON = { requestId: '0xrequestId', quoteId: '0xquoteId', @@ -67,13 +93,20 @@ const CLASSIC_QUOTE_JSON: ClassicQuoteDataJSON = { portionRecipient: PORTION_RECIPIENT, }; +const UNIVERSAL_ROUTER_ADDRESS = '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD'; + describe('QuoteResponse', () => { const config: DutchRequest = QUOTE_REQUEST_DL; + const relayConfig: RelayRequest = QUOTE_REQUEST_RELAY; it('parses dutch limit quote from param-api properly', () => { expect(() => DutchQuote.fromResponseBody(config, DL_QUOTE_JSON)).not.toThrow(); }); + it('parses relay quote properly', () => { + expect(() => RelayQuote.fromResponseBody(relayConfig, RELAY_QUOTE_JSON)).not.toThrow(); + }); + it('produces dutch limit order info from param-api response and config', () => { const quote = DutchQuote.fromResponseBody(config, DL_QUOTE_JSON_RFQ); expect(quote.toOrder().toJSON()).toMatchObject({ @@ -99,28 +132,27 @@ describe('QuoteResponse', () => { expect(BigNumber.from(quote.toOrder().toJSON().nonce).gt(0)).toBeTruthy(); }); - it('produces dutch limit order info from param-api response and config without filler', () => { - const quote = DutchQuote.fromResponseBody(config, Object.assign({}, DL_QUOTE_JSON, { filler: undefined })); + it('produces relay order info from quote', () => { + const quote = RelayQuote.fromResponseBody(relayConfig, RELAY_QUOTE_JSON); expect(quote.toOrder().toJSON()).toMatchObject({ swapper: SWAPPER, input: { + token: TOKEN_IN, + amount: AMOUNT, + recipient: UNIVERSAL_ROUTER_ADDRESS, + }, + fee: { token: TOKEN_IN, startAmount: AMOUNT, endAmount: AMOUNT, + startTime: expect.any(Number), + endTime: expect.any(Number), }, - outputs: [ - { - token: TOKEN_OUT, - startAmount: AMOUNT, - endAmount: BigNumber.from(AMOUNT).mul(995).div(1000).toString(), // default 0.5% slippage - recipient: SWAPPER, - }, - ], + universalRouterCalldata: + '0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000100000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000dc46ef164c4a49e00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f984000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', }); - const order = DutchOrder.fromJSON(quote.toOrder().toJSON(), quote.chainId); - const parsedValidation = parseValidation(order.info); - expect(parsedValidation.type).toEqual(ValidationType.None); - expect(BigNumber.from(quote.toOrder().toJSON().nonce).gt(0)).toBeTruthy(); + const order = RelayOrder.fromJSON(quote.toOrder().toJSON(), quote.chainId); + expect(BigNumber.from(order.toJSON().nonce).gt(0)).toBeTruthy(); }); it('parses classic quote exactInput', () => { diff --git a/test/unit/lib/handlers/quote/handler.test.ts b/test/unit/lib/handlers/quote/handler.test.ts index c55be586..b94d367c 100644 --- a/test/unit/lib/handlers/quote/handler.test.ts +++ b/test/unit/lib/handlers/quote/handler.test.ts @@ -5,12 +5,15 @@ import { BASE_REQUEST_INFO_EXACT_IN, BASE_REQUEST_INFO_EXACT_OUT, CLASSIC_QUOTE_EXACT_IN_BETTER, + CLASSIC_QUOTE_EXACT_IN_BETTER_GAS_TOKEN, CLASSIC_QUOTE_EXACT_IN_BETTER_WITH_PORTION, CLASSIC_QUOTE_EXACT_IN_WORSE, + CLASSIC_QUOTE_EXACT_IN_WORSE_GAS_TOKEN, CLASSIC_QUOTE_EXACT_IN_WORSE_WITH_PORTION, CLASSIC_QUOTE_EXACT_OUT_BETTER, CLASSIC_QUOTE_EXACT_OUT_BETTER_WITH_PORTION, CLASSIC_QUOTE_EXACT_OUT_WORSE, + CLASSIC_QUOTE_EXACT_OUT_WORSE_GAS_TOKEN, CLASSIC_QUOTE_EXACT_OUT_WORSE_WITH_PORTION, CLASSIC_REQUEST_BODY, createClassicQuote, @@ -28,18 +31,33 @@ import { QUOTE_REQUEST_CLASSIC, QUOTE_REQUEST_DL, QUOTE_REQUEST_MULTI, + RELAY_QUOTE_EXACT_IN_BETTER, + RELAY_QUOTE_EXACT_IN_WORSE, + RELAY_QUOTE_EXACT_OUT_BETTER, + RELAY_QUOTE_EXACT_OUT_WORSE, + RELAY_QUOTE_NATIVE_EXACT_IN_BETTER, + RELAY_REQUEST_BODY, + RELAY_REQUEST_BODY_EXACT_OUT, + RELAY_REQUEST_WITH_CLASSIC_BODY, } from '../../../../utils/fixtures'; import { PermitDetails } from '@uniswap/permit2-sdk'; -import { DutchOrderInfoJSON } from '@uniswap/uniswapx-sdk'; +import { DutchOrderInfoJSON, RelayOrderInfoJSON } from '@uniswap/uniswapx-sdk'; import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'; import { MetricsLogger } from 'aws-embedded-metrics'; import { APIGatewayProxyEventHeaders } from 'aws-lambda/trigger/api-gateway-proxy'; import { AxiosError } from 'axios'; -import { providers } from 'ethers'; +import { BigNumber, providers } from 'ethers'; import NodeCache from 'node-cache'; import { RoutingType } from '../../../../../lib/constants'; -import { ClassicQuote, ClassicQuoteDataJSON, DutchQuote, Quote, RequestSource } from '../../../../../lib/entities'; +import { + ClassicQuote, + ClassicQuoteDataJSON, + DutchQuote, + Quote, + RelayQuote, + RequestSource, +} from '../../../../../lib/entities'; import { DutchConfigJSON, QuoteRequestBodyJSON } from '../../../../../lib/entities/request/index'; import { Permit2Fetcher } from '../../../../../lib/fetchers/Permit2Fetcher'; import { @@ -63,7 +81,7 @@ import { ErrorCode } from '../../../../../lib/util/errors'; import { setGlobalLogger } from '../../../../../lib/util/log'; import { INELIGIBLE_TOKEN, PERMIT2_USED, PERMIT_DETAILS, SWAPPER, TOKEN_IN, TOKEN_OUT } from '../../../../constants'; -export const LOGGER_MOCK = { +const LOGGER_MOCK = { info: jest.fn(), error: jest.fn(), child: () => ({ @@ -73,11 +91,11 @@ export const LOGGER_MOCK = { }), }; -export const METRICS_MOCK = { +const METRICS_MOCK = { putMetric: jest.fn(), }; -export const requestInjectedMock: Promise = new Promise((resolve) => { +const requestInjectedMock: Promise = new Promise((resolve) => { setGlobalLogger(LOGGER_MOCK as any); resolve({ log: LOGGER_MOCK as unknown as Logger, @@ -113,7 +131,7 @@ const injectorPromiseMock = ( } as unknown as ApiInjector) ); -export const getQuoteHandler = ( +const getQuoteHandler = ( quoters: QuoterByRoutingType, tokenFetcher: TokenFetcher, portionFetcher: PortionFetcher, @@ -125,24 +143,30 @@ export const getQuoteHandler = ( injectorPromiseMock(quoters, tokenFetcher, portionFetcher, permit2Fetcher, syntheticStatusProvider) ); -export const RfqQuoterMock = (dlQuote: DutchQuote): Quoter => { +const RfqQuoterMock = (dlQuote: DutchQuote): Quoter => { return { quote: jest.fn().mockResolvedValue(dlQuote), }; }; -export const RfqQuoterErrorMock = (axiosError: AxiosError): Quoter => { +const RfqQuoterErrorMock = (axiosError: AxiosError): Quoter => { return { quote: jest.fn().mockReturnValue(Promise.reject(axiosError)), }; }; -export const ClassicQuoterMock = (classicQuote: ClassicQuote): Quoter => { +const RelayQuoterMock = (relayQuote: RelayQuote): Quoter => { + return { + quote: jest.fn().mockResolvedValue(relayQuote), + }; +}; + +const ClassicQuoterMock = (classicQuote: ClassicQuote): Quoter => { return { quote: jest.fn().mockResolvedValue(classicQuote), }; }; -export const TokenFetcherMock = (addresses: string[], isError = false): TokenFetcher => { +const TokenFetcherMock = (addresses: string[], isError = false): TokenFetcher => { const fetcher = { resolveTokenBySymbolOrAddress: jest.fn(), getTokenBySymbolOrAddress: (_chainId: number, address: string) => [TOKEN_IN, TOKEN_OUT].includes(address), @@ -158,15 +182,13 @@ export const TokenFetcherMock = (addresses: string[], isError = false): TokenFet } return fetcher as unknown as TokenFetcher; }; - -export const PortionFetcherMock = (portionResponse: GetPortionResponse): PortionFetcher => { +const PortionFetcherMock = (portionResponse: GetPortionResponse): PortionFetcher => { const portionCache = new NodeCache({ stdTTL: 600 }); const portionFetcher = new PortionFetcher('https://portion.uniswap.org/', portionCache); jest.spyOn(portionFetcher, 'getPortion').mockResolvedValue(portionResponse); return portionFetcher; }; - -export const Permit2FetcherMock = (permitDetails: PermitDetails, isError = false): Permit2Fetcher => { +const Permit2FetcherMock = (permitDetails: PermitDetails, isError = false): Permit2Fetcher => { const fetcher = { fetchAllowance: jest.fn(), }; @@ -180,7 +202,7 @@ export const Permit2FetcherMock = (permitDetails: PermitDetails, isError = false return fetcher as unknown as Permit2Fetcher; }; -export const SyntheticStatusProviderMock = (syntheticEnabled: boolean): SyntheticStatusProvider => { +const SyntheticStatusProviderMock = (syntheticEnabled: boolean): SyntheticStatusProvider => { const provider = { getStatus: jest.fn(), }; @@ -188,7 +210,8 @@ export const SyntheticStatusProviderMock = (syntheticEnabled: boolean): Syntheti provider.getStatus.mockResolvedValueOnce({ syntheticEnabled }); return provider as unknown as SyntheticStatusProvider; }; -export const getEvent = (request: QuoteRequestBodyJSON, headers?: APIGatewayProxyEventHeaders): APIGatewayProxyEvent => + +const getEvent = (request: QuoteRequestBodyJSON, headers?: APIGatewayProxyEventHeaders): APIGatewayProxyEvent => ({ body: JSON.stringify(request), ...(headers !== undefined && { headers: headers }), @@ -215,7 +238,7 @@ describe('QuoteHandler', () => { }); describe('handler', () => { - describe('Handler Test', () => { + describe('handler test', () => { it('handles exactIn classic quotes', async () => { const quoters = { [RoutingType.CLASSIC]: ClassicQuoterMock(CLASSIC_QUOTE_EXACT_IN_WORSE) }; const tokenFetcher = TokenFetcherMock([TOKEN_IN, TOKEN_OUT]); @@ -449,6 +472,76 @@ describe('QuoteHandler', () => { expect(quoteJSON.outputs[0].endAmount).toBe(slippageAdjustedAmountOut.toString()); }); + it('handles exactIn relay quotes', async () => { + const quoters = { + [RoutingType.CLASSIC]: ClassicQuoterMock(CLASSIC_QUOTE_EXACT_IN_WORSE_GAS_TOKEN), + [RoutingType.RELAY]: RelayQuoterMock(RELAY_QUOTE_NATIVE_EXACT_IN_BETTER), + }; + const tokenFetcher = TokenFetcherMock([TOKEN_IN, TOKEN_OUT]); + const portionFetcher = PortionFetcherMock(GET_NO_PORTION_RESPONSE); + const permit2Fetcher = Permit2FetcherMock(PERMIT_DETAILS); + const syntheticStatusProvider = SyntheticStatusProviderMock(false); + + const res = await getQuoteHandler( + quoters, + tokenFetcher, + portionFetcher, + permit2Fetcher, + syntheticStatusProvider + ).handler(getEvent(RELAY_REQUEST_BODY), {} as unknown as Context); + const quoteJSON = JSON.parse(res.body).quote.orderInfo as RelayOrderInfoJSON; + expect(quoteJSON.input.amount).toBe(RELAY_QUOTE_EXACT_IN_BETTER.amountIn.toString()); + }); + + it('returns relay quote if both relay and classic are requested', async () => { + const quoters = { + [RoutingType.CLASSIC]: ClassicQuoterMock(CLASSIC_QUOTE_EXACT_IN_BETTER_GAS_TOKEN), + [RoutingType.RELAY]: RelayQuoterMock(RELAY_QUOTE_EXACT_IN_BETTER), + }; + const tokenFetcher = TokenFetcherMock([TOKEN_IN, TOKEN_OUT]); + const portionFetcher = PortionFetcherMock(GET_NO_PORTION_RESPONSE); + const permit2Fetcher = Permit2FetcherMock(PERMIT_DETAILS); + const syntheticStatusProvider = SyntheticStatusProviderMock(false); + + const res = await getQuoteHandler( + quoters, + tokenFetcher, + portionFetcher, + permit2Fetcher, + syntheticStatusProvider + ).handler(getEvent(RELAY_REQUEST_WITH_CLASSIC_BODY), {} as unknown as Context); + const quoteJSON = JSON.parse(res.body).quote.orderInfo as RelayOrderInfoJSON; + // Expect relay to always be preferred over classic if requested together + expect(JSON.parse(res.body).routing).toEqual(RoutingType.RELAY); + expect(quoteJSON.input.amount).toBe(RELAY_QUOTE_EXACT_IN_BETTER.amountIn.toString()); + + const allQuotes = JSON.parse(res.body).allQuotes; + expect(allQuotes.length).toEqual(2); + expect(allQuotes[0].routing).toEqual(RoutingType.RELAY); + expect(allQuotes[1].routing).toEqual(RoutingType.CLASSIC); + }); + + it('handles exactOut relay quotes', async () => { + const quoters = { + [RoutingType.CLASSIC]: ClassicQuoterMock(CLASSIC_QUOTE_EXACT_OUT_WORSE_GAS_TOKEN), + [RoutingType.RELAY]: RelayQuoterMock(RELAY_QUOTE_EXACT_OUT_BETTER), + }; + const tokenFetcher = TokenFetcherMock([TOKEN_IN, TOKEN_OUT]); + const portionFetcher = PortionFetcherMock(GET_NO_PORTION_RESPONSE); + const permit2Fetcher = Permit2FetcherMock(PERMIT_DETAILS); + const syntheticStatusProvider = SyntheticStatusProviderMock(false); + + const res = await getQuoteHandler( + quoters, + tokenFetcher, + portionFetcher, + permit2Fetcher, + syntheticStatusProvider + ).handler(getEvent(RELAY_REQUEST_BODY_EXACT_OUT), {} as unknown as Context); + const quoteJSON = JSON.parse(res.body).quote.orderInfo as RelayOrderInfoJSON; + expect(quoteJSON.input.amount).toBe(RELAY_QUOTE_EXACT_OUT_BETTER.amountIn.toString()); + }); + it('returns allQuotes', async () => { const quoters = { [RoutingType.CLASSIC]: ClassicQuoterMock(CLASSIC_QUOTE_EXACT_IN_WORSE), @@ -544,6 +637,35 @@ describe('QuoteHandler', () => { expect(permit2Fetcher.fetchAllowance).not.toHaveBeenCalled(); }); + it('always returns correct permit for relay', async () => { + const quoters = { + [RoutingType.RELAY]: RelayQuoterMock(RELAY_QUOTE_EXACT_IN_BETTER), + }; + const tokenFetcher = TokenFetcherMock([TOKEN_IN, TOKEN_OUT]); + const portionFetcher = PortionFetcherMock(GET_NO_PORTION_RESPONSE); + const permit2Fetcher = Permit2FetcherMock(PERMIT_DETAILS); + const syntheticStatusProvider = SyntheticStatusProviderMock(false); + + const response = await getQuoteHandler( + quoters, + tokenFetcher, + portionFetcher, + permit2Fetcher, + syntheticStatusProvider + ).handler(getEvent(RELAY_REQUEST_WITH_CLASSIC_BODY), {} as unknown as Context); + + const responseBody = JSON.parse(response.body); + const permitData = responseBody.quote.permitData; + const quote = responseBody.quote.orderInfo as RelayOrderInfoJSON; + expect(permitData.values.permitted[0].token).toBe(quote.input.token); + expect(BigNumber.from(permitData.values.permitted[0].amount).eq(quote.input.amount)).toBe(true); + expect(permitData.values.permitted[1].token).toBe(quote.fee.token); + expect(BigNumber.from(permitData.values.permitted[1].amount).eq(quote.fee.endAmount)).toBe(true); + expect(BigNumber.from(permitData.values.witness.fee.startAmount).eq(quote.fee.startAmount)).toBe(true); + expect(BigNumber.from(permitData.values.witness.fee.endAmount).eq(quote.fee.endAmount)).toBe(true); + expect(permit2Fetcher.fetchAllowance).not.toHaveBeenCalled(); + }); + it('returns permit for Classic with swapper and current permit invalid', async () => { const quoters = { [RoutingType.CLASSIC]: ClassicQuoterMock(CLASSIC_QUOTE_EXACT_IN_WORSE), @@ -693,6 +815,28 @@ describe('QuoteHandler', () => { expect(quote.encodedOrder).not.toBe(null); }); + it('always returns encodedOrder in quote for relay', async () => { + const quoters = { + [RoutingType.RELAY]: RelayQuoterMock(RELAY_QUOTE_EXACT_IN_BETTER), + }; + const tokenFetcher = TokenFetcherMock([TOKEN_IN, TOKEN_OUT]); + const portionFetcher = PortionFetcherMock(GET_NO_PORTION_RESPONSE); + const permit2Fetcher = Permit2FetcherMock(PERMIT_DETAILS); + const syntheticStatusProvider = SyntheticStatusProviderMock(false); + + const response = await getQuoteHandler( + quoters, + tokenFetcher, + portionFetcher, + permit2Fetcher, + syntheticStatusProvider + ).handler(getEvent(RELAY_REQUEST_WITH_CLASSIC_BODY), {} as unknown as Context); + + const responseBody = JSON.parse(response.body); + const quote = responseBody.quote; + expect(quote.encodedOrder).not.toBe(null); + }); + it('returns 500 when quoters 429 and there are no valid DL quotes', async () => { const message = 'Request failed with status code 429'; const axiosResponse = { @@ -936,6 +1080,26 @@ describe('QuoteHandler', () => { expect(res.state).toBe('valid'); }); + it('Succeeds - Relay Quote', async () => { + const quoters = { [RoutingType.RELAY]: RelayQuoterMock(RELAY_QUOTE_EXACT_IN_BETTER) }; + const event = { + body: JSON.stringify(RELAY_REQUEST_BODY), + } as APIGatewayProxyEvent; + const tokenFetcher = TokenFetcherMock([TOKEN_IN, TOKEN_OUT]); + const portionFetcher = PortionFetcherMock(GET_NO_PORTION_RESPONSE); + const permit2Fetcher = Permit2FetcherMock(PERMIT_DETAILS); + const syntheticStatusProvider = SyntheticStatusProviderMock(false); + + const res = await getQuoteHandler( + quoters, + tokenFetcher, + portionFetcher, + permit2Fetcher, + syntheticStatusProvider + ).parseAndValidateRequest(event, LOGGER_MOCK as unknown as Logger); + expect(res.state).toBe('valid'); + }); + it('Succeeds - Bad swapper address', async () => { const quoters = { [RoutingType.CLASSIC]: ClassicQuoterMock(CLASSIC_QUOTE_EXACT_IN_WORSE) }; const event = { @@ -1078,12 +1242,63 @@ describe('QuoteHandler', () => { ).toBe(false); }); + it('returns true if lhs is a better relay quote than rhs', () => { + expect(compareQuotes(RELAY_QUOTE_EXACT_IN_BETTER, RELAY_QUOTE_EXACT_IN_WORSE, TradeType.EXACT_INPUT)).toBe(true); + expect(compareQuotes(RELAY_QUOTE_EXACT_OUT_BETTER, RELAY_QUOTE_EXACT_OUT_WORSE, TradeType.EXACT_OUTPUT)).toBe( + true + ); + }); + + it('returns false if lhs is a worse relay quote than rhs', () => { + expect(compareQuotes(RELAY_QUOTE_EXACT_IN_WORSE, RELAY_QUOTE_EXACT_IN_BETTER, TradeType.EXACT_INPUT)).toBe(false); + expect(compareQuotes(RELAY_QUOTE_EXACT_OUT_WORSE, RELAY_QUOTE_EXACT_OUT_BETTER, TradeType.EXACT_OUTPUT)).toBe( + false + ); + }); + + it('expect relay to always be preferred over classic', () => { + expect(compareQuotes(RELAY_QUOTE_EXACT_IN_BETTER, CLASSIC_QUOTE_EXACT_IN_BETTER, TradeType.EXACT_INPUT)).toBe( + true + ); + // expect to be the same + expect( + RELAY_QUOTE_EXACT_IN_BETTER.classicQuote.amountOutGasAndPortionAdjusted.eq( + CLASSIC_QUOTE_EXACT_IN_BETTER.amountOutGasAndPortionAdjusted + ) + ).toBe(true); + + expect(compareQuotes(RELAY_QUOTE_EXACT_OUT_BETTER, CLASSIC_QUOTE_EXACT_OUT_BETTER, TradeType.EXACT_OUTPUT)).toBe( + true + ); + // expect to be the same + expect( + RELAY_QUOTE_EXACT_OUT_BETTER.classicQuote.amountInGasAndPortionAdjusted.eq( + CLASSIC_QUOTE_EXACT_OUT_BETTER.amountInGasAndPortionAdjusted + ) + ).toBe(true); + }); + + it('returns true if lhs is a better dutch quote than rhs relay', () => { + expect(compareQuotes(DL_QUOTE_EXACT_IN_BETTER, RELAY_QUOTE_EXACT_IN_WORSE, TradeType.EXACT_INPUT)).toBe(true); + expect(compareQuotes(DL_QUOTE_EXACT_OUT_BETTER, RELAY_QUOTE_EXACT_OUT_WORSE, TradeType.EXACT_OUTPUT)).toBe(true); + }); + + it('returns true if lhs is a better relay quote than rhs dutch', () => { + expect(compareQuotes(RELAY_QUOTE_EXACT_IN_BETTER, DL_QUOTE_EXACT_IN_WORSE, TradeType.EXACT_INPUT)).toBe(true); + expect(compareQuotes(RELAY_QUOTE_EXACT_OUT_BETTER, DL_QUOTE_EXACT_OUT_WORSE, TradeType.EXACT_OUTPUT)).toBe(true); + }); + it('returns true if lhs is a better mixed type', () => { expect(compareQuotes(DL_QUOTE_EXACT_IN_BETTER, CLASSIC_QUOTE_EXACT_IN_WORSE, TradeType.EXACT_INPUT)).toBe(true); expect(compareQuotes(CLASSIC_QUOTE_EXACT_IN_BETTER, DL_QUOTE_EXACT_IN_WORSE, TradeType.EXACT_INPUT)).toBe(true); expect(compareQuotes(DL_QUOTE_EXACT_OUT_BETTER, CLASSIC_QUOTE_EXACT_OUT_WORSE, TradeType.EXACT_OUTPUT)).toBe( true ); + expect(compareQuotes(RELAY_QUOTE_EXACT_IN_BETTER, DL_QUOTE_EXACT_IN_WORSE, TradeType.EXACT_INPUT)).toBe(true); + expect(compareQuotes(RELAY_QUOTE_EXACT_IN_BETTER, CLASSIC_QUOTE_EXACT_IN_WORSE, TradeType.EXACT_INPUT)).toBe( + true + ); + expect(compareQuotes(RELAY_QUOTE_EXACT_OUT_BETTER, DL_QUOTE_EXACT_OUT_WORSE, TradeType.EXACT_OUTPUT)).toBe(true); }); it('returns true if lhs is a better mixed type with portion', () => { diff --git a/test/unit/lib/handlers/quote/schema.test.ts b/test/unit/lib/handlers/quote/schema.test.ts index 96b34784..c797cca1 100644 --- a/test/unit/lib/handlers/quote/schema.test.ts +++ b/test/unit/lib/handlers/quote/schema.test.ts @@ -7,6 +7,7 @@ import { CLASSIC_CONFIG, DL_CONFIG, DUTCH_V2_CONFIG, + RELAY_CONFIG, TOKEN_IN, TOKEN_OUT, } from '../../../../constants'; @@ -26,6 +27,11 @@ const CLASSIC_CONFIG_JSON = { routingType: 'CLASSIC', }; +const RELAY_CONFIG_JSON = { + ...RELAY_CONFIG, + routingType: 'RELAY', +}; + const BASE_REQUEST_BODY = { tokenInChainId: CHAIN_IN_ID, tokenOutChainId: CHAIN_OUT_ID, @@ -33,7 +39,7 @@ const BASE_REQUEST_BODY = { tokenOut: TOKEN_OUT, amount: AMOUNT, type: 'EXACT_INPUT', - configs: [DL_CONFIG_JSON, CLASSIC_CONFIG_JSON], + configs: [DL_CONFIG_JSON, RELAY_CONFIG_JSON, CLASSIC_CONFIG_JSON], }; describe('Post quote request validation', () => { @@ -92,6 +98,19 @@ describe('Post quote request validation', () => { expect(error).toBeUndefined(); }); + it('should validate relay config', () => { + const { error } = FieldValidator.relayConfig.validate(RELAY_CONFIG_JSON); + expect(error).toBeUndefined(); + }); + + it('should reject invalid gasToken', () => { + const { error } = FieldValidator.relayConfig.validate({ + ...RELAY_CONFIG_JSON, + gasToken: '0x', + }); + expect(error).toBeDefined(); + }); + it('should reject invalid routingType', () => { const { error } = FieldValidator.classicConfig.validate({ ...CLASSIC_CONFIG_JSON, diff --git a/test/utils/fixtures.ts b/test/utils/fixtures.ts index 7142e308..b038e0bc 100644 --- a/test/utils/fixtures.ts +++ b/test/utils/fixtures.ts @@ -2,13 +2,12 @@ import { ID_TO_CHAIN_ID, WRAPPED_NATIVE_CURRENCY } from '@uniswap/smart-order-ro import { BPS, NATIVE_ADDRESS, RoutingType } from '../../lib/constants'; import { ChainId, WETH9 } from '@uniswap/sdk-core'; -import { PoolType } from '@uniswap/universal-router-sdk'; +import { PoolType, V2PoolInRoute, V3PoolInRoute } from '@uniswap/universal-router-sdk'; import { BigNumber } from 'ethers'; import { ClassicQuoteDataJSON, ClassicRequest, DutchConfig, - DutchConfigJSON, DutchQuoteJSON, DutchRequest, DutchV2Config, @@ -98,17 +97,48 @@ export const QUOTE_REQUEST_BODY_MULTI_SYNTHETIC: QuoteRequestBodyJSON = { ], }; -export const DL_REQUEST_BODY: QuoteRequestBodyJSON & { - configs: DutchConfigJSON[]; -} = { +export const RELAY_REQUEST_BODY: QuoteRequestBodyJSON = { ...BASE_REQUEST_INFO_EXACT_IN, configs: [ { - routingType: RoutingType.DUTCH_LIMIT, + routingType: RoutingType.RELAY, + protocols: ['V3', 'V2', 'MIXED'], swapper: SWAPPER, - exclusivityOverrideBps: 12, auctionPeriodSecs: 60, deadlineBufferSecs: 12, + gasToken: TOKEN_IN, + }, + ], +}; + +export const RELAY_REQUEST_BODY_EXACT_OUT: QuoteRequestBodyJSON = { + ...BASE_REQUEST_INFO_EXACT_OUT, + configs: [ + { + routingType: RoutingType.RELAY, + protocols: ['V3', 'V2', 'MIXED'], + swapper: SWAPPER, + auctionPeriodSecs: 60, + deadlineBufferSecs: 12, + gasToken: TOKEN_IN, + }, + ], +}; + +export const RELAY_REQUEST_WITH_CLASSIC_BODY: QuoteRequestBodyJSON = { + ...BASE_REQUEST_INFO_EXACT_IN, + configs: [ + { + routingType: RoutingType.RELAY, + protocols: ['V3', 'V2', 'MIXED'], + swapper: SWAPPER, + auctionPeriodSecs: 60, + deadlineBufferSecs: 12, + gasToken: TOKEN_IN, + }, + { + routingType: RoutingType.CLASSIC, + protocols: ['V3', 'V2', 'MIXED'], }, ], }; @@ -123,6 +153,19 @@ export const CLASSIC_REQUEST_BODY: QuoteRequestBodyJSON = { ], }; +export const DL_REQUEST_BODY: QuoteRequestBodyJSON = { + ...BASE_REQUEST_INFO_EXACT_IN, + configs: [ + { + routingType: RoutingType.DUTCH_LIMIT, + swapper: SWAPPER, + exclusivityOverrideBps: 12, + auctionPeriodSecs: 60, + deadlineBufferSecs: 12, + }, + ], +}; + export function makeClassicRequest(overrides: Partial): ClassicRequest { const requestInfo = Object.assign({}, BASE_REQUEST_INFO_EXACT_IN, overrides); @@ -382,6 +425,54 @@ export const CLASSIC_QUOTE_DATA_WITH_FOX_TAX = { }, }; +const GET_ROUTE = ( + amountIn: string = AMOUNT, + amountOut: string = AMOUNT, + tokenIn: string = TOKEN_IN, + tokenOut: string = TOKEN_OUT +): Array<(V3PoolInRoute | V2PoolInRoute)[]> => { + return [ + [ + { + type: PoolType.V2Pool, + address: '0x0D0A1767da735F725f41c4315E072c63Dbc6ab3D', + tokenIn: { + chainId: ChainId.MAINNET, + decimals: '18', + address: tokenIn, + symbol: 'UNI', + }, + tokenOut: { + chainId: ChainId.MAINNET, + decimals: '18', + address: tokenOut, + symbol: 'WETH', + }, + reserve0: { + token: { + chainId: ChainId.MAINNET, + decimals: '18', + address: TOKEN_IN, + symbol: 'UNI', + }, + quotient: '100000000000000000000000000000', + }, + reserve1: { + token: { + chainId: ChainId.MAINNET, + decimals: '18', + address: TOKEN_OUT, + symbol: 'WETH', + }, + quotient: '100000000000000000000000000000', + }, + amountIn, + amountOut, + }, + ], + ]; +}; + export const CLASSIC_QUOTE_DATA_WITH_ROUTE_AND_GAS_TOKEN: { routing: RoutingType.CLASSIC; quote: ClassicQuoteDataJSON; @@ -405,46 +496,7 @@ export const CLASSIC_QUOTE_DATA_WITH_ROUTE_AND_GAS_TOKEN: { simulationStatus: 'start', gasPriceWei: '10000', blockNumber: '1234', - route: [ - [ - { - type: PoolType.V2Pool, - address: '0x0D0A1767da735F725f41c4315E072c63Dbc6ab3D', - tokenIn: { - chainId: ChainId.MAINNET, - decimals: '18', - address: TOKEN_IN, - symbol: 'UNI', - }, - tokenOut: { - chainId: ChainId.MAINNET, - decimals: '18', - address: TOKEN_OUT, - symbol: 'WETH', - }, - reserve0: { - token: { - chainId: ChainId.MAINNET, - decimals: '18', - address: TOKEN_IN, - symbol: 'UNI', - }, - quotient: AMOUNT, - }, - reserve1: { - token: { - chainId: ChainId.MAINNET, - decimals: '18', - address: TOKEN_OUT, - symbol: 'WETH', - }, - quotient: AMOUNT, - }, - amountIn: AMOUNT, - amountOut: AMOUNT, - }, - ], - ], + route: GET_ROUTE(), routeString: 'UNI-ETH', tradeType: 'exactIn', slippage: 0.5, @@ -682,10 +734,24 @@ export const CLASSIC_QUOTE_EXACT_IN_BETTER_WITH_PORTION = createClassicQuote( undefined, FLAT_PORTION ); +export const CLASSIC_QUOTE_EXACT_IN_BETTER_GAS_TOKEN = createClassicQuote( + { + quote: AMOUNT_BETTER, + quoteGasAdjusted: AMOUNT_BETTER, + gasUseEstimateGasToken: AMOUNT_BETTER, + route: GET_ROUTE(AMOUNT_BETTER, AMOUNT_BETTER), + }, + { type: 'EXACT_INPUT' } +); export const CLASSIC_QUOTE_EXACT_IN_WORSE = createClassicQuote( { quote: AMOUNT, quoteGasAdjusted: AMOUNT }, { type: 'EXACT_INPUT' } ); +export const CLASSIC_QUOTE_EXACT_IN_WORSE_GAS_TOKEN = createClassicQuote( + { quote: AMOUNT, quoteGasAdjusted: AMOUNT, gasUseEstimateGasToken: AMOUNT, route: GET_ROUTE() }, + { type: 'EXACT_INPUT' } +); + export const CLASSIC_QUOTE_EXACT_IN_WORSE_WITH_PORTION = createClassicQuote( { quote: AMOUNT, @@ -772,6 +838,10 @@ export const CLASSIC_QUOTE_EXACT_OUT_BETTER_WITH_PORTION = createClassicQuote( }, { type: 'EXACT_OUTPUT' } ); +export const CLASSIC_QUOTE_EXACT_OUT_BETTER_GAS_TOKEN = createClassicQuote( + { quote: AMOUNT, quoteGasAdjusted: AMOUNT, gasUseEstimateGasToken: AMOUNT, route: GET_ROUTE() }, + { type: 'EXACT_OUTPUT' } +); export const CLASSIC_QUOTE_EXACT_OUT_WORSE = createClassicQuote( { quote: AMOUNT_BETTER, quoteGasAdjusted: AMOUNT_BETTER }, { type: 'EXACT_OUTPUT' } @@ -788,6 +858,15 @@ export const CLASSIC_QUOTE_EXACT_OUT_WORSE_WITH_PORTION = createClassicQuote( }, { type: 'EXACT_OUTPUT' } ); +export const CLASSIC_QUOTE_EXACT_OUT_WORSE_GAS_TOKEN = createClassicQuote( + { + quote: AMOUNT_BETTER, + quoteGasAdjusted: AMOUNT_BETTER, + gasUseEstimateGasToken: AMOUNT_BETTER, + route: GET_ROUTE(AMOUNT_BETTER, AMOUNT_BETTER), + }, + { type: 'EXACT_OUTPUT' } +); export const CLASSIC_QUOTE_EXACT_OUT_LARGE = createClassicQuote( { amount: AMOUNT_LARGE, quote: AMOUNT_LARGE, quoteGasAdjusted: AMOUNT_LARGE }, { type: 'EXACT_OUTPUT' } @@ -831,7 +910,16 @@ export function createRelayQuoteWithRequest( } export const RELAY_QUOTE_EXACT_IN_BETTER = createRelayQuote( - { amountOut: AMOUNT_BETTER, feeAmountStart: AMOUNT_BETTER, feeAmountEnd: AMOUNT_BETTER }, + { + amountOut: AMOUNT_BETTER, + feeAmountStart: AMOUNT_BETTER, + feeAmountEnd: AMOUNT_BETTER, + classicQuoteData: { + ...CLASSIC_QUOTE_DATA_WITH_ROUTE_AND_GAS_TOKEN.quote, + quote: AMOUNT_BETTER, + quoteGasAndPortionAdjusted: AMOUNT_BETTER, + }, + }, 'EXACT_INPUT' ); export const RELAY_QUOTE_NATIVE_EXACT_IN_BETTER = createRelayQuoteWithRequest( @@ -853,10 +941,28 @@ export const RELAY_QUOTE_EXACT_IN_WORSE = createRelayQuote( 'EXACT_INPUT' ); export const RELAY_QUOTE_EXACT_OUT_BETTER = createRelayQuote( - { amountIn: AMOUNT, feeAmountStart: AMOUNT, feeAmountEnd: AMOUNT }, + { + amountIn: AMOUNT, + feeAmountStart: AMOUNT, + feeAmountEnd: AMOUNT, + classicQuoteData: { + ...CLASSIC_QUOTE_DATA_WITH_ROUTE_AND_GAS_TOKEN.quote, + quote: AMOUNT, + quoteGasAndPortionAdjusted: AMOUNT, + }, + }, 'EXACT_OUTPUT' ); export const RELAY_QUOTE_EXACT_OUT_WORSE = createRelayQuote( - { amountIn: AMOUNT_BETTER, feeAmountStart: AMOUNT_BETTER, feeAmountEnd: AMOUNT_BETTER }, + { + amountIn: AMOUNT_BETTER, + feeAmountStart: AMOUNT_BETTER, + feeAmountEnd: AMOUNT_BETTER, + classicQuoteData: { + ...CLASSIC_QUOTE_DATA_WITH_ROUTE_AND_GAS_TOKEN.quote, + quote: AMOUNT_BETTER, + quoteGasAndPortionAdjusted: AMOUNT_BETTER, + }, + }, 'EXACT_OUTPUT' ); diff --git a/test/utils/quoteResponse.ts b/test/utils/quoteResponse.ts index 99d41c9d..fb328371 100644 --- a/test/utils/quoteResponse.ts +++ b/test/utils/quoteResponse.ts @@ -74,7 +74,7 @@ function parseQuote( // also: is this parsing quote responses even needed outside of testing? return ClassicQuote.fromResponseBody(request, quote as ClassicQuoteDataJSON); case RoutingType.RELAY: - return RelayQuote.fromResponseBody(request as RelayRequest, quote as RelayQuoteJSON); + return RelayQuote.fromResponseBody(request as RelayRequest, quote as RelayQuoteJSON, nonce); default: throw new Error(`Unknown routing type: ${routing}`); }