diff --git a/.gitignore b/.gitignore index a5226171..ee33215a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ yarn-debug.log* yarn-error.log* lerna-debug.log* +cdk.out/ +cache/ + # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/README.md b/README.md index 84d74786..4466b660 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,13 @@ To test your changes you must redeploy your service. The dev cycle is thus: 1. Make code changes. Make sure all env variables are present in the .env file: ``` -WEB3_RPC_TENDERLY=<> +FIREHOSE_ARN_LOCAL=<> +RPC_12341234=<> + +# Only need these if testing against custom contract deployments REACTOR_TENDERLY=<> QUOTER_TENDERLY=<> PERMIT_TENDERLY=<> -FIREHOSE_ARN_LOCAL=<> ``` 1. `yarn build && cdk deploy GoudaServiceStack` diff --git a/bin/stacks/lambda-stack.ts b/bin/stacks/lambda-stack.ts index 63230bb1..b643314f 100644 --- a/bin/stacks/lambda-stack.ts +++ b/bin/stacks/lambda-stack.ts @@ -116,6 +116,7 @@ export class LambdaStack extends cdk.NestedStack { }, environment: { ...props.envVars, + stage: props.stage as STAGE, VERSION: '2', NODE_OPTIONS: '--enable-source-maps', STATE_MACHINE_ARN: sfnStack.statusTrackingStateMachine.attrArn, diff --git a/lib/handlers/check-order-status/handler.ts b/lib/handlers/check-order-status/handler.ts index 6bc32b5e..806b99ec 100644 --- a/lib/handlers/check-order-status/handler.ts +++ b/lib/handlers/check-order-status/handler.ts @@ -1,4 +1,4 @@ -import { DutchLimitOrder, OrderValidation } from '@uniswap/gouda-sdk' +import { DutchOrder, OrderValidation } from '@uniswap/gouda-sdk' import { default as Logger } from 'bunyan' import { ethers } from 'ethers' import Joi from 'joi' @@ -35,7 +35,7 @@ export class CheckOrderStatusHandler extends SfnLambdaHandler onchainValidatorByChainId[Number(chainId)].orderQuoterAddress + ), + }, + 'onchain validators' + ) + let decodedOrder: DutchOrder try { - decodedOrder = DutchLimitOrder.parse(encodedOrder, chainId) as DutchLimitOrder + decodedOrder = DutchOrder.parse(encodedOrder, chainId) as DutchOrder } catch (e: unknown) { log.error(e, 'Failed to parse order') return { @@ -74,13 +81,7 @@ export class PostOrderHandler extends APIGLambdaHandler< } } - const order: OrderEntity = formatOrderEntity( - decodedOrder, - signature, - OrderType.DutchLimit, - ORDER_STATUS.OPEN, - quoteId - ) + const order: OrderEntity = formatOrderEntity(decodedOrder, signature, OrderType.Dutch, ORDER_STATUS.OPEN, quoteId) const id = order.orderHash try { diff --git a/lib/handlers/post-order/injector.ts b/lib/handlers/post-order/injector.ts index 9cec6842..12b16c3e 100644 --- a/lib/handlers/post-order/injector.ts +++ b/lib/handlers/post-order/injector.ts @@ -20,7 +20,6 @@ export class PostOrderInjector extends ApiInjector { const onchainValidatorByChainId: { [chainId: number]: OnchainValidator } = {} SUPPORTED_CHAINS.forEach((chainId) => { - // TODO: remove when we bring back tenderly if (typeof chainId === 'number') { const rpc = process.env[`RPC_${chainId}`] if (rpc) { diff --git a/lib/util/chain.ts b/lib/util/chain.ts index 68daafc3..276b7a98 100644 --- a/lib/util/chain.ts +++ b/lib/util/chain.ts @@ -4,7 +4,7 @@ export enum ChainId { OPTIMISM = 10, ARBITRUM_ONE = 42161, POLYGON = 137, - TENDERLY = 'TENDERLY', + TENDERLY = 12341234, } -export const SUPPORTED_CHAINS = [ChainId.MAINNET, ChainId.TENDERLY, ChainId.POLYGON] +export const SUPPORTED_CHAINS = [ChainId.MAINNET, ChainId.POLYGON, ChainId.TENDERLY] diff --git a/lib/util/field-validator.ts b/lib/util/field-validator.ts index 171e627b..ddfdb683 100644 --- a/lib/util/field-validator.ts +++ b/lib/util/field-validator.ts @@ -1,3 +1,4 @@ +// import { OrderType } from '@uniswap/gouda-sdk' import { OrderType } from '@uniswap/gouda-sdk' import { BigNumber, ethers } from 'ethers' import Joi, { CustomHelpers, NumberSchema, StringSchema } from 'joi' @@ -38,7 +39,7 @@ export default class FieldValidator { ) private static readonly SORT_KEY_JOI = Joi.string().valid(SORT_FIELDS.CREATED_AT) private static readonly SORT_JOI = Joi.string().regex(SORT_REGEX) - private static readonly ORDER_TYPE_JOI = Joi.string().valid(OrderType.DutchLimit) + private static readonly ORDER_TYPE_JOI = Joi.string().valid(OrderType.Dutch) private static readonly ETH_ADDRESS_JOI = Joi.string().custom((value: string, helpers: CustomHelpers) => { if (!ethers.utils.isAddress(value)) { diff --git a/lib/util/order-validator.ts b/lib/util/order-validator.ts index de214484..a903d06a 100644 --- a/lib/util/order-validator.ts +++ b/lib/util/order-validator.ts @@ -1,4 +1,4 @@ -import { DutchLimitOrder, DutchOutput } from '@uniswap/gouda-sdk' +import { DutchOrder, DutchOutput } from '@uniswap/gouda-sdk' import { BigNumber } from 'ethers' import FieldValidator from './field-validator' @@ -12,7 +12,7 @@ const THIRTY_MINUTES_IN_SECONDS = 60 * 30 export class OrderValidator { constructor(private readonly getCurrentTime: () => number) {} - validate(order: DutchLimitOrder): OrderValidationResponse { + validate(order: DutchOrder): OrderValidationResponse { const chainIdValidation = this.validateChainId(order.chainId) if (!chainIdValidation.valid) { return chainIdValidation diff --git a/lib/util/order.ts b/lib/util/order.ts index ebea4017..31695cfb 100644 --- a/lib/util/order.ts +++ b/lib/util/order.ts @@ -1,4 +1,4 @@ -import { DutchLimitOrder, OrderType } from '@uniswap/gouda-sdk' +import { DutchOrder, OrderType } from '@uniswap/gouda-sdk' import { DynamoDBRecord } from 'aws-lambda' import { OrderEntity, ORDER_STATUS } from '../entities' @@ -38,7 +38,7 @@ export const eventRecordToOrder = (record: DynamoDBRecord): ParsedOrder => { } export const formatOrderEntity = ( - decodedOrder: DutchLimitOrder, + decodedOrder: DutchOrder, signature: string, orderType: OrderType, orderStatus: ORDER_STATUS, diff --git a/package.json b/package.json index cce44832..049c08bc 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@aws-sdk/client-s3": "^3.341.0", "@aws-sdk/client-sfn": "^3.341.0", "@types/sinon": "^10.0.13", - "@uniswap/gouda-sdk": "0.9.3", + "@uniswap/gouda-sdk": "^1.0.0-alpha.2", "aws-cdk-lib": "2.43.1", "aws-sdk": "^2.1238.0", "axios": "^1.2.1", diff --git a/swagger.json b/swagger.json index def29555..d9171a02 100644 --- a/swagger.json +++ b/swagger.json @@ -277,7 +277,7 @@ }, "OrderType": { "type": "string", - "enum": ["DutchLimit"] + "enum": ["Dutch"] }, "OrderEntity": { "type": "object", diff --git a/test/abis/erc20.json b/test/abis/erc20.json new file mode 100644 index 00000000..bdf541d4 --- /dev/null +++ b/test/abis/erc20.json @@ -0,0 +1,308 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ERC20", + "sourceName": "solmate/src/tokens/ERC20.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "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": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/test/handlers/check-order-status.test.ts b/test/handlers/check-order-status.test.ts index 9d01a26c..97a5e7b1 100644 --- a/test/handlers/check-order-status.test.ts +++ b/test/handlers/check-order-status.test.ts @@ -15,7 +15,7 @@ const MOCK_ORDER_ENTITY: OrderEntity = { orderHash: MOCK_ORDER_HASH, offerer: '0xofferer', orderStatus: ORDER_STATUS.OPEN, - type: OrderType.DutchLimit, + type: OrderType.Dutch, chainId: 1, reactor: '0x1', startTime: 1, @@ -117,7 +117,7 @@ describe('Testing check order status handler', () => { orderStatus: ORDER_STATUS.OPEN as string, chainId: 2022, } as any) - ).rejects.toThrowError('"chainId" must be one of [1, TENDERLY, 137]') + ).rejects.toThrowError(`"chainId" must be one of [1, 137, 12341234]`) }) }) diff --git a/test/handlers/get-orders.test.ts b/test/handlers/get-orders.test.ts index 1278ce71..6d237902 100644 --- a/test/handlers/get-orders.test.ts +++ b/test/handlers/get-orders.test.ts @@ -12,7 +12,7 @@ describe('Testing get orders handler.', () => { offerer: '0x11E4857Bb9993a50c685A79AFad4E6F65D518DDa', createdAt: 1667276283251, encodedOrder: '0xencoded000order', - type: OrderType.DutchLimit, + type: OrderType.Dutch, chainId: 1, input: { token: '0x0000000000000000000000000000000000000000', @@ -121,7 +121,7 @@ describe('Testing get orders handler.', () => { [{ sort: 'foo(bar)' }, '"foo(bar)\\" fails to match the required pattern'], [{ cursor: 1 }, 'must be a string'], [{ sort: 'gt(4)' }, '{"detail":"\\"sortKey\\" is required","errorCode":"VALIDATION_ERROR"}'], - [{ chainId: 420 }, '{"detail":"\\"chainId\\" must be one of [1, TENDERLY, 137]","errorCode":"VALIDATION_ERROR"}'], + [{ chainId: 420 }, '{"detail":"\\"chainId\\" must be one of [1, 137, 12341234]","errorCode":"VALIDATION_ERROR"}'], [{ desc: true }, '{"detail":"\\"sortKey\\" is required","errorCode":"VALIDATION_ERROR"}'], [ { desc: 'yes', sortKey: 'createdAt', orderStatus: 'expired' }, diff --git a/test/handlers/post-order.test.ts b/test/handlers/post-order.test.ts index 7fd9abae..e69f50db 100644 --- a/test/handlers/post-order.test.ts +++ b/test/handlers/post-order.test.ts @@ -1,7 +1,7 @@ var parserMock = jest.fn() import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn' -import { DutchLimitOrderInfo, OrderValidation } from '@uniswap/gouda-sdk' +import { DutchOrderInfo, OrderValidation } from '@uniswap/gouda-sdk' import { mockClient } from 'aws-sdk-client-mock' import { BigNumber } from 'ethers' import { ORDER_STATUS } from '../../lib/entities' @@ -24,7 +24,7 @@ mockSfnClient }) .resolves({}) -const ORDER_INFO: DutchLimitOrderInfo = { +const ORDER_INFO: DutchOrderInfo = { deadline: 10, offerer: '0x0000000000000000000000000000000000000001', reactor: '0x0000000000000000000000000000000000000002', @@ -61,10 +61,10 @@ jest.mock('@uniswap/gouda-sdk', () => { const originalSdk = jest.requireActual('@uniswap/gouda-sdk') return { ...originalSdk, - DutchLimitOrder: { - parse: parserMock, + DutchOrder: { + parse: parserMock }, - OrderType: { DutchLimit: 'DutchLimit' }, + OrderType: { Dutch: 'Dutch' }, } }) @@ -108,7 +108,7 @@ describe('Testing post order handler.', () => { endTime: 10, deadline: 10, quoteId: '55e2cfca-5521-4a0a-b597-7bfb569032d7', - type: 'DutchLimit', + type: 'Dutch', input: { endAmount: '30', startAmount: '30', @@ -215,7 +215,7 @@ describe('Testing post order handler.', () => { { signature: '0xbad_signature' }, '{"detail":"\\"signature\\" with value \\"0xbad_signature\\" fails to match the required pattern: /^0x[0-9,a-z,A-Z]{130}$/","errorCode":"VALIDATION_ERROR"}', ], - [{ chainId: 0 }, '{"detail":"\\"chainId\\" must be one of [1, TENDERLY, 137]","errorCode":"VALIDATION_ERROR"}'], + [{ chainId: 0 }, `{"detail":"\\"chainId\\" must be one of [1, 137, 12341234]","errorCode":"VALIDATION_ERROR"}`], [{ quoteId: 'not_UUIDV4' }, '{"detail":"\\"quoteId\\" must be a valid GUID","errorCode":"VALIDATION_ERROR"}'], ])('Throws 400 with invalid field %p', async (invalidBodyField, bodyMsg) => { const invalidEvent = { diff --git a/test/integ/constants.ts b/test/integ/constants.ts new file mode 100644 index 00000000..dbb44026 --- /dev/null +++ b/test/integ/constants.ts @@ -0,0 +1,5 @@ +export const ANVIL_TEST_WALLET_PK = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' +export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +export const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' +export const UNI = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' diff --git a/test/integ/nonce.test.ts b/test/integ/nonce.test.ts index f63c0405..f31af644 100644 --- a/test/integ/nonce.test.ts +++ b/test/integ/nonce.test.ts @@ -1,12 +1,11 @@ -import { DutchLimitOrderBuilder } from '@uniswap/gouda-sdk' +import { DutchOrderBuilder } from '@uniswap/gouda-sdk' import axios from 'axios' import dotenv from 'dotenv' import { BigNumber, ethers } from 'ethers' import { checkDefined } from '../../lib/preconditions/preconditions' +import { ANVIL_TEST_WALLET_PK, ZERO_ADDRESS } from './constants' dotenv.config() -const ANVIL_TEST_WALLET_PK = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const URL = checkDefined(process.env.GOUDA_SERVICE_URL, 'GOUDA_SERVICE_URL must be defined') const wallet = new ethers.Wallet(ANVIL_TEST_WALLET_PK) @@ -16,14 +15,14 @@ axios.defaults.timeout = 10000 describe('get nonce', () => { it('should get current nonce for address, and increment it by one after the address posts an order', async () => { - const address = await (await wallet.getAddress()).toLowerCase() + const address = (await wallet.getAddress()).toLowerCase() const getResponse = await axios.get(`${URL}dutch-auction/nonce?address=${address}`) expect(getResponse.status).toEqual(200) const nonce = BigNumber.from(getResponse.data.nonce) expect(nonce.lt(ethers.constants.MaxUint256)).toBeTruthy() const deadline = Math.round(new Date().getTime() / 1000) + 10 - const order = new DutchLimitOrderBuilder(1) + const order = new DutchOrderBuilder(1) .deadline(deadline) .endTime(deadline) .startTime(deadline - 5) diff --git a/test/integ/order.test.ts b/test/integ/order.test.ts new file mode 100644 index 00000000..60431dc9 --- /dev/null +++ b/test/integ/order.test.ts @@ -0,0 +1,233 @@ +import { DutchOrderBuilder } from '@uniswap/gouda-sdk' +import axios from 'axios' +import dotenv from 'dotenv' +import { BigNumber, Contract, ethers, Wallet } from 'ethers' +import { UNI, WETH, ZERO_ADDRESS } from './constants' + +import { GetOrdersResponse } from '../../lib/handlers/get-orders/schema' +import { ChainId } from '../../lib/util/chain' +import * as ERC20_ABI from '../abis/erc20.json' +const { abi } = ERC20_ABI + +dotenv.config() + +const PERMIT2 = '0x000000000022d473030f116ddee9f6b43ac78ba3' + +describe('/dutch-auction/order', () => { + jest.setTimeout(60 * 1000) + let wallet: Wallet + let provider: ethers.providers.JsonRpcProvider + let aliceAddress: string + let nonce: BigNumber + let URL: string + // Token contracts + let weth: Contract + let uni: Contract + // Fork management + let snap: null | string = null + + beforeAll(async () => { + if (!process.env.GOUDA_SERVICE_URL) { + throw new Error('GOUDA_SERVICE_URL not set') + } + if (!process.env.RPC_12341234) { + throw new Error('RPC_12341234 not set') + } + URL = process.env.GOUDA_SERVICE_URL + + provider = new ethers.providers.JsonRpcProvider(process.env.RPC_12341234) + + wallet = ethers.Wallet.createRandom().connect(provider) + aliceAddress = (await wallet.getAddress()).toLowerCase() + + weth = new Contract(WETH, abi, provider) + uni = new Contract(UNI, abi, provider) + + // Set alice's balance to 10 ETH + await provider.send('tenderly_setBalance', [ + [aliceAddress], + ethers.utils.hexValue(ethers.utils.parseUnits('10', 'ether').toHexString()), + ]) + + // Ensure alice has some WETH and UNI + await provider.send('tenderly_setStorageAt', [ + UNI, + ethers.utils.keccak256( + ethers.utils.concat([ + ethers.utils.hexZeroPad(aliceAddress, 32), + ethers.utils.hexZeroPad('0x04', 32), // the balances slot is 4th in the UNI contract + ]) + ), + ethers.utils.hexZeroPad(ethers.utils.parseEther('20').toHexString(), 32), + ]) + const uniBalance = (await uni.balanceOf(wallet.address)) as BigNumber + expect(uniBalance).toEqual(ethers.utils.parseEther('20')) + + await provider.send('tenderly_setStorageAt', [ + WETH, + ethers.utils.keccak256( + ethers.utils.concat([ethers.utils.hexZeroPad(aliceAddress, 32), ethers.utils.hexZeroPad('0x03', 32)]) + ), + ethers.utils.hexZeroPad(ethers.utils.parseEther('20').toHexString(), 32), + ]) + const wethBalance = (await weth.balanceOf(wallet.address)) as BigNumber + expect(wethBalance).toEqual(ethers.utils.parseEther('20')) + + // approve P2 + await weth.connect(wallet).approve(PERMIT2, ethers.constants.MaxUint256) + await uni.connect(wallet).approve(PERMIT2, ethers.constants.MaxUint256) + + const getResponse = await axios.get(`${URL}dutch-auction/nonce?address=${aliceAddress}`) + expect(getResponse.status).toEqual(200) + nonce = BigNumber.from(getResponse.data.nonce) + expect(nonce.lt(ethers.constants.MaxUint256)).toBeTruthy() + }) + + beforeEach(async () => { + snap = await provider.send("evm_snapshot", []); + }); + + afterEach(async () => { + await provider.send("evm_revert", [snap]); + }) + + async function expectOrdersToBeOpen(orderHashes: string[]) { + // check that orders are open, retrying if status is unverified, with exponential backoff + for (let i = 0; i < 5; i++) { + const promises = orderHashes.map((orderHash) => + axios.get(`${URL}dutch-auction/orders?orderHash=${orderHash}`) + ) + const responses = await Promise.all(promises) + expect(responses.every((resp) => resp.status === 200)) + const orders = responses.map((resp) => resp.data.orders[0]) + expect(orders.length).toEqual(orderHashes.length) + const orderStatuses = orders.map((order) => order!.orderStatus) + if (orderStatuses.every((status) => status === 'open')) { + return true + } + await new Promise((resolve) => setTimeout(resolve, 2 ** i * 1000)) + } + return false + } + + async function waitAndGetOrderStatus(orderHash: string, deadlineSeconds: number) { + /// @dev testing expiry of the order via the step function is very finicky + /// we fast forward the fork's timestamp by the deadline and then mine a block to get the changes included + /// However, we have to wait for the sfn to fire again, so we wait a bit, and as long as the order's expiry is longer than that time period, + /// we can be sure that the order correctly expired based on the block.timestamp + const params = [ + ethers.utils.hexValue(deadlineSeconds), // hex encoded number of seconds + ] + const blockNumber = (await provider.getBlock('latest')).number + + await provider.send('evm_increaseTime', params) + const blocksToMine = 1 + await provider.send('evm_increaseBlocks', [ + ethers.utils.hexValue(blocksToMine) + ]) + expect((await provider.getBlock('latest')).number).toEqual(blockNumber + blocksToMine + 1) + // Wait a bit for sfn to fire again + await new Promise((resolve) => setTimeout(resolve, 15_000)) + + const resp = await axios.get(`${URL}dutch-auction/orders?orderHash=${orderHash}`) + expect(resp.status).toEqual(200) + expect(resp.data.orders.length).toEqual(1) + const order = resp.data.orders[0] + expect(order).toBeDefined() + expect(order!.orderHash).toEqual(orderHash) + return order!.orderStatus + } + + const buildAndSubmitOrder = async ( + offerer: string, + amount: BigNumber, + deadlineSeconds: number, + inputToken: string, + outputToken: string + ) => { + const deadline = Math.round(new Date().getTime() / 1000) + deadlineSeconds + const startTime = Math.round(new Date().getTime() / 1000) + const nextNonce = nonce.add(1) + const order = new DutchOrderBuilder(ChainId.MAINNET) + .deadline(deadline) + .endTime(deadline) + .startTime(startTime) + .offerer(offerer) + .nonce(nextNonce) + .input({ + token: inputToken, + startAmount: amount, + endAmount: amount, + }) + .output({ + token: outputToken, + startAmount: amount, + endAmount: amount, + recipient: offerer, + }) + .build() + + const { domain, types, values } = order.permitData() + const signature = await wallet._signTypedData(domain, types, values) + const encodedOrder = order.serialize() + + try { + const postResponse = await axios.post( + `${URL}dutch-auction/order`, + { + encodedOrder, + signature, + chainId: ChainId.TENDERLY, + }, + { + headers: { + accept: 'application/json, text/plain, */*', + 'content-type': 'application/json', + }, + } + ) + + expect(postResponse.status).toEqual(201) + const newGetResponse = await axios.get(`${URL}dutch-auction/nonce?address=${aliceAddress}`) + expect(newGetResponse.status).toEqual(200) + const newNonce = BigNumber.from(newGetResponse.data.nonce) + expect(newNonce.eq(nonce.add(1))).toBeTruthy() + + return postResponse.data.hash + } + catch(err: any) { + console.log(err) + throw err + } + } + + describe('checking expiry', () => { + it('erc20 to erc20', async () => { + const amount = ethers.utils.parseEther('1') + const orderHash = await buildAndSubmitOrder(aliceAddress, amount, 1000, WETH, UNI) + expect(await expectOrdersToBeOpen([orderHash])).toBeTruthy() + expect(await waitAndGetOrderStatus(orderHash, 1001)).toBe('expired') + }) + + it('erc20 to eth', async () => { + const amount = ethers.utils.parseEther('1') + const orderHash = await buildAndSubmitOrder(aliceAddress, amount, 1000, UNI, ZERO_ADDRESS) + expect(await expectOrdersToBeOpen([orderHash])).toBeTruthy() + expect(await waitAndGetOrderStatus(orderHash, 1001)).toBe('expired') + }) + + it('does not expire order before deadline', async () => { + const amount = ethers.utils.parseEther('1') + const orderHash = await buildAndSubmitOrder(aliceAddress, amount, 1000, UNI, ZERO_ADDRESS) + expect(await expectOrdersToBeOpen([orderHash])).toBeTruthy() + expect(await waitAndGetOrderStatus(orderHash, 900)).toBe('open') + }) + }) + + it('allows same offerer to post multiple orders', async () => { + const amount = ethers.utils.parseEther('1') + const orderHash1 = await buildAndSubmitOrder(aliceAddress, amount, 5, WETH, UNI) + const orderHash2 = await buildAndSubmitOrder(aliceAddress, amount, 5, UNI, ZERO_ADDRESS) + expect(await expectOrdersToBeOpen([orderHash1, orderHash2])).toBeTruthy() + }) +}) diff --git a/test/integ/smoke.test.ts b/test/integ/smoke.test.ts deleted file mode 100644 index 37d8b88f..00000000 --- a/test/integ/smoke.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('Smoke test', () => { - it('smoke test should pass', async () => { - expect(1).toEqual(1) - }) -}) diff --git a/test/util/field-validator.test.ts b/test/util/field-validator.test.ts index 758ad8cf..8452398a 100644 --- a/test/util/field-validator.test.ts +++ b/test/util/field-validator.test.ts @@ -114,13 +114,13 @@ describe('Testing each field on the FieldValidator class.', () => { const chainId = 'MAINNET' const validatedField = FieldValidator.isValidChainId().validate(chainId) expect(validatedField.error).toBeTruthy() - expect(validatedField.error?.details[0].message).toEqual('"value" must be one of [1, TENDERLY, 137]') + expect(validatedField.error?.details[0].message).toEqual(`"value" must be one of [1, 137, 12341234]`) }) it('should invalidate unsupported chain.', async () => { const chainId = ChainId.ARBITRUM_ONE const validatedField = FieldValidator.isValidChainId().validate(chainId) expect(validatedField.error).toBeTruthy() - expect(validatedField.error?.details[0].message).toEqual('"value" must be one of [1, TENDERLY, 137]') + expect(validatedField.error?.details[0].message).toEqual(`"value" must be one of [1, 137, 12341234]`) }) }) describe('Testing nonce field.', () => { diff --git a/test/util/order-validator.test.ts b/test/util/order-validator.test.ts index 57f73098..297532c8 100644 --- a/test/util/order-validator.test.ts +++ b/test/util/order-validator.test.ts @@ -1,4 +1,4 @@ -import { DutchLimitOrder } from '@uniswap/gouda-sdk' +import { DutchOrder } from '@uniswap/gouda-sdk' import { BigNumber } from 'ethers' import { OrderValidator } from '../../lib/util/order-validator' @@ -34,8 +34,8 @@ function newOrder({ chainId = 1, validationContract = VALIDATION_CONTRACT_ADDRESS, validationData = VALIDATION_DATA, -}): DutchLimitOrder { - return new DutchLimitOrder( +}): DutchOrder { + return new DutchOrder( { startTime, endTime: deadline, diff --git a/tsconfig.json b/tsconfig.json index 769f84f1..e404cf64 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,7 +28,8 @@ "strictPropertyInitialization": true, "outDir": "dist", "allowJs": true, - "typeRoots": ["./node_modules/@types"] + "typeRoots": ["./node_modules/@types"], + "types": ["node", "jest"] }, "exclude": ["cdk.out", "./dist/**/*"] } diff --git a/yarn.lock b/yarn.lock index 0b4a9a5a..872003a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3056,10 +3056,10 @@ "@typescript-eslint/types" "5.41.0" eslint-visitor-keys "^3.3.0" -"@uniswap/gouda-sdk@0.9.3": - version "0.9.3" - resolved "https://registry.yarnpkg.com/@uniswap/gouda-sdk/-/gouda-sdk-0.9.3.tgz#95ac441ecd27d1cbb25aa9ffe3d46e0adfb18657" - integrity sha512-mf2hk3el/+SML4NUktDtE8M/+f8qgJl/1JVqVq6ABKOB/rK3Qs2WODb3ZHp7gr4/aNZjOvunR3ov88T+D3dn2g== +"@uniswap/gouda-sdk@^1.0.0-alpha.2": + version "1.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/@uniswap/gouda-sdk/-/gouda-sdk-1.0.0-alpha.2.tgz#761c3e4fcdfd1db07c5724b498db6918783cc70a" + integrity sha512-Qt6xuC1Xe6xe/hNOwPTnAKXZb7l6ld1t4+Lxltg87vd3R1sYhk7mnDkTRKM30epxBbicecHQUfoxiUU5SRN8XQ== dependencies: "@ethersproject/bytes" "^5.7.0" "@ethersproject/providers" "^5.7.0"