Skip to content

Commit

Permalink
Merge pull request #295 from Uniswap/bump-sdk
Browse files Browse the repository at this point in the history
feat: hard-quote integration tests
  • Loading branch information
ConjunctiveNormalForm authored Apr 2, 2024
2 parents d7e487b + ef5315e commit 5cd359b
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 15 deletions.
16 changes: 13 additions & 3 deletions bin/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class APIPipeline extends Stack {

const betaUsEast2AppStage = pipeline.addStage(betaUsEast2Stage);

this.addIntegTests(code, betaUsEast2Stage, betaUsEast2AppStage);
this.addIntegTests(code, betaUsEast2Stage, betaUsEast2AppStage, STAGE.BETA);

// Prod us-east-2
const prodUsEast2Stage = new APIStage(this, 'prod-us-east-2', {
Expand All @@ -152,7 +152,7 @@ export class APIPipeline extends Stack {

const prodUsEast2AppStage = pipeline.addStage(prodUsEast2Stage);

this.addIntegTests(code, prodUsEast2Stage, prodUsEast2AppStage);
this.addIntegTests(code, prodUsEast2Stage, prodUsEast2AppStage, STAGE.PROD);

pipeline.buildPipeline();

Expand All @@ -170,8 +170,10 @@ export class APIPipeline extends Stack {
private addIntegTests(
sourceArtifact: cdk.pipelines.CodePipelineSource,
apiStage: APIStage,
applicationStage: cdk.pipelines.StageDeployment
applicationStage: cdk.pipelines.StageDeployment,
stage: STAGE
) {
const cosignerSecret = `param-api/${stage}/cosignerAddress`;
const testAction = new CodeBuildStep(`${SERVICE_NAME}-IntegTests-${apiStage.stageName}`, {
projectName: `${SERVICE_NAME}-IntegTests-${apiStage.stageName}`,
input: sourceArtifact,
Expand All @@ -198,6 +200,14 @@ export class APIPipeline extends Stack {
value: '--max-old-space-size=8192',
type: BuildEnvironmentVariableType.PLAINTEXT,
},
INTEG_TEST_PK: {
value: 'param-api/integ-test-pk',
type: BuildEnvironmentVariableType.SECRETS_MANAGER,
},
COSIGNER_ADDR: {
value: cosignerSecret,
type: BuildEnvironmentVariableType.SECRETS_MANAGER,
},
},
},
commands: [
Expand Down
2 changes: 1 addition & 1 deletion lib/config/chains.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { ChainId } from '../util/chains';

export const SUPPORTED_CHAINS: ChainId[] = [ChainId.MAINNET, ChainId.GÖRLI, ChainId.POLYGON];
export const SUPPORTED_CHAINS: ChainId[] = [ChainId.MAINNET, ChainId.GÖRLI, ChainId.POLYGON, ChainId.SEPOLIA];
43 changes: 37 additions & 6 deletions lib/handlers/hard-quote/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MetricLoggerUnit } from '@uniswap/smart-order-router';
import { CosignedV2DutchOrder, CosignerData } from '@uniswap/uniswapx-sdk';
import { BigNumber, ethers } from 'ethers';
import Joi from 'joi';
import { v4 as uuidv4 } from 'uuid';

import { HardQuoteRequest, HardQuoteResponse, Metric, QuoteResponse } from '../../entities';
import { NoQuotesAvailable, OrderPostError, UnknownOrderCosignerError } from '../../util/errors';
Expand Down Expand Up @@ -44,7 +45,7 @@ export class QuoteHandler extends APIGLambdaHandler<

// we dont have access to the cosigner key, throw
if (request.order.info.cosigner !== cosignerAddress) {
log.error({ cosigner: request.order.info.cosigner }, 'Unknown cosigner');
log.error({ cosignerInReq: request.order.info.cosigner, expected: cosignerAddress }, 'Unknown cosigner');
throw new UnknownOrderCosignerError();
}

Expand All @@ -70,20 +71,29 @@ export class QuoteHandler extends APIGLambdaHandler<
});

const bestQuote = await getBestQuote(quoters, request.toQuoteRequest(), log, metric, 'HardResponse');
if (!bestQuote) {
if (!bestQuote && !requestBody.allowNoQuote) {
metric.putMetric(Metric.HARD_QUOTE_404, 1, MetricLoggerUnit.Count);
throw new NoQuotesAvailable();
if (!requestBody.allowNoQuote) {
throw new NoQuotesAvailable();
}
}

log.info({ bestQuote: bestQuote }, 'bestQuote');
let cosignerData: CosignerData;
if (bestQuote) {
cosignerData = getCosignerData(request, bestQuote);
log.info({ bestQuote: bestQuote }, 'bestQuote');
} else {
cosignerData = getDefaultCosignerData(request);
log.info({ cosignerData: cosignerData }, 'open order with default cosignerData');
}

// TODO: use server key to cosign instead of local wallet
const cosignerData = getCosignerData(request, bestQuote);
const cosignature = await cosigner.signDigest(request.order.cosignatureHash(cosignerData));
const cosignedOrder = CosignedV2DutchOrder.fromUnsignedOrder(request.order, cosignerData, cosignature);

try {
await orderServiceProvider.postOrder(cosignedOrder, request.innerSig, bestQuote.quoteId);
// if no quote and creating open order, create random new quoteId
await orderServiceProvider.postOrder(cosignedOrder, request.innerSig, bestQuote?.quoteId ?? uuidv4());
} catch (e) {
metric.putMetric(Metric.HARD_QUOTE_400, 1, MetricLoggerUnit.Count);
throw new OrderPostError();
Expand Down Expand Up @@ -148,6 +158,27 @@ export function getCosignerData(request: HardQuoteRequest, quote: QuoteResponse)
};
}

export function getDefaultCosignerData(request: HardQuoteRequest): CosignerData {
const decayStartTime = getDecayStartTime(request.tokenInChainId);
const filler = ethers.constants.AddressZero;
let inputOverride = BigNumber.from(0);
const outputOverrides = request.order.info.outputs.map(() => BigNumber.from(0));
if (request.type === TradeType.EXACT_INPUT) {
outputOverrides[0] = request.totalOutputAmountStart;
} else {
inputOverride = request.totalInputAmountStart;
}

return {
decayStartTime: decayStartTime,
decayEndTime: getDecayEndTime(request.tokenInChainId, decayStartTime),
exclusiveFiller: filler,
exclusivityOverrideBps: DEFAULT_EXCLUSIVITY_OVERRIDE_BPS,
inputOverride: inputOverride,
outputOverrides: outputOverrides,
};
}

function getDecayStartTime(chainId: number): number {
const nowTimestamp = Math.floor(Date.now() / 1000);
switch (chainId) {
Expand Down
1 change: 1 addition & 0 deletions lib/handlers/hard-quote/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export class QuoteInjector extends ApiInjector<ContainerInjected, RequestInjecte
const quoters: Quoter[] = [
new WebhookQuoter(log, firehose, webhookProvider, circuitBreakerProvider, fillerComplianceProvider),
];
log.info({ cosignerAddress }, 'Cosigner address from KMS Signer');
return {
quoters: quoters,
firehose: firehose,
Expand Down
2 changes: 2 additions & 0 deletions lib/handlers/hard-quote/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const HardQuoteRequestBodyJoi = Joi.object({
innerSig: FieldValidator.rawSignature.required(),
tokenInChainId: FieldValidator.chainId.required(),
tokenOutChainId: Joi.number().integer().valid(Joi.ref('tokenInChainId')).required(),
allowNoQuote: Joi.boolean().optional(),
});

export type HardQuoteRequestBody = {
Expand All @@ -19,6 +20,7 @@ export type HardQuoteRequestBody = {
innerSig: string;
tokenInChainId: number;
tokenOutChainId: number;
allowNoQuote?: boolean;
};

export const HardQuoteResponseDataJoi = Joi.object({
Expand Down
4 changes: 4 additions & 0 deletions lib/util/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ export enum ChainId {
MAINNET = 1,
GÖRLI = 5,
POLYGON = 137,
SEPOLIA = 11155111,
}

export enum ChainName {
// ChainNames match infura network strings
MAINNET = 'mainnet',
GÖRLI = 'goerli',
POLYGON = 'polygon',
SEPOLIA = 'sepolia',
}

export const ID_TO_NETWORK_NAME = (id: number): ChainName => {
Expand All @@ -19,6 +21,8 @@ export const ID_TO_NETWORK_NAME = (id: number): ChainName => {
return ChainName.GÖRLI;
case 137:
return ChainName.POLYGON;
case 11155111:
return ChainName.SEPOLIA;
default:
throw new Error(`Unknown chain id: ${id}`);
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"@uniswap/signer": "0.0.3-beta.4",
"@uniswap/smart-order-router": "^3.3.0",
"@uniswap/token-lists": "^1.0.0-beta.31",
"@uniswap/uniswapx-sdk": "2.0.1-alpha.3",
"@uniswap/uniswapx-sdk": "2.0.1-alpha.7",
"@uniswap/v3-sdk": "^3.9.0",
"aws-cdk-lib": "2.85.0",
"aws-embedded-metrics": "^4.1.0",
Expand Down
163 changes: 163 additions & 0 deletions test/integ/hard-quote.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { V2DutchOrderBuilder } from '@uniswap/uniswapx-sdk';
import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import chaiSubset from 'chai-subset';
import { BigNumber, ethers } from 'ethers';
import { v4 as uuidv4 } from 'uuid';

import { HardQuoteRequestBody } from '../../lib/handlers/hard-quote';
import { checkDefined } from '../../lib/preconditions/preconditions';
import AxiosUtils from '../util/axios';

chai.use(chaiAsPromised);
chai.use(chaiSubset);

const COSIGNER_ADDR = checkDefined(
process.env.COSIGNER_ADDR,
'Must set COSIGNER_ADDR env variable for integ tests. See README'
);
const INTEG_TEST_PK = checkDefined(
process.env.INTEG_TEST_PK,
'Must set INTEG_TEST_PK env variable for integ tests. See README'
);
// PARAM_API base URL
const UNISWAP_API = checkDefined(
process.env.UNISWAP_API,
'Must set UNISWAP_API env variable for integ tests. See README'
);

const SEPOLIA = 11155111;
const PARAM_API = `${UNISWAP_API}hard-quote`;

const REQUEST_ID = uuidv4();
const now = Math.floor(Date.now() / 1000);
const swapper = new ethers.Wallet(INTEG_TEST_PK);
const SWAPPER_ADDRESS = swapper.address;
const TOKEN_IN = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'; // USDC on Sepolia
const TOKEN_OUT = '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14'; // WETH on Sepolia
const AMOUNT = BigNumber.from('1');

let builder: V2DutchOrderBuilder;

describe('Hard Quote endpoint integration test', function () {
beforeEach(() => {
builder = new V2DutchOrderBuilder(SEPOLIA);
});

describe('Invalid requests', async () => {
it('missing signature', async () => {
const v2Order = builder
.input({ token: TOKEN_IN, startAmount: AMOUNT, endAmount: AMOUNT })
.output({ token: TOKEN_OUT, startAmount: AMOUNT, endAmount: AMOUNT, recipient: SWAPPER_ADDRESS })
.nonce(BigNumber.from(100))
.cosigner(ethers.constants.AddressZero)
.deadline(now + 1000)
.swapper(SWAPPER_ADDRESS)
.buildPartial();

const quoteReq = {
requestId: REQUEST_ID,
encodedInnerOrder: v2Order.serialize(),
tokenInChainId: SEPOLIA,
tokenOutChainId: SEPOLIA,
};

const { data, status } = await AxiosUtils.callPassThroughFail('POST', PARAM_API, quoteReq);
expect(data.detail).to.equal('"innerSig" is required');
expect(status).to.equal(400);
});

it('missing encodedInnerOrder', async () => {
const quoteReq = {
requestId: REQUEST_ID,
innerSig: '0x',
tokenInChainId: SEPOLIA,
tokenOutChainId: SEPOLIA,
};

const { data, status } = await AxiosUtils.callPassThroughFail('POST', PARAM_API, quoteReq);
expect(data.detail).to.equal('"encodedInnerOrder" is required');
expect(status).to.equal(400);
});

it('missing requestId', async () => {
const v2Order = builder
.input({ token: TOKEN_IN, startAmount: AMOUNT, endAmount: AMOUNT })
.output({ token: TOKEN_OUT, startAmount: AMOUNT, endAmount: AMOUNT, recipient: SWAPPER_ADDRESS })
.nonce(BigNumber.from(100))
.cosigner(ethers.constants.AddressZero)
.deadline(now + 1000)
.swapper(SWAPPER_ADDRESS)
.buildPartial();
const { domain, types, values } = v2Order.permitData();
const signature = await swapper._signTypedData(domain, types, values);

const quoteReq = {
encodedInnerOrder: v2Order.serialize(),
innerSig: signature,
tokenInChainId: SEPOLIA,
tokenOutChainId: SEPOLIA,
};

const { data, status } = await AxiosUtils.callPassThroughFail('POST', PARAM_API, quoteReq);
expect(data.detail).to.equal('"requestId" is required');
expect(status).to.equal(400);
});

it('unknown cosigner', async () => {
const v2Order = builder
.input({ token: TOKEN_IN, startAmount: AMOUNT, endAmount: AMOUNT })
.output({ token: TOKEN_OUT, startAmount: AMOUNT, endAmount: AMOUNT, recipient: SWAPPER_ADDRESS })
.nonce(BigNumber.from(100))
.cosigner(ethers.constants.AddressZero)
.deadline(now + 1000)
.swapper(SWAPPER_ADDRESS)
.buildPartial();
const { domain, types, values } = v2Order.permitData();
const signature = await swapper._signTypedData(domain, types, values);

const quoteReq: HardQuoteRequestBody = {
requestId: REQUEST_ID,
encodedInnerOrder: v2Order.serialize(),
innerSig: signature,
tokenInChainId: SEPOLIA,
tokenOutChainId: SEPOLIA,
};

const { data, status } = await AxiosUtils.callPassThroughFail('POST', PARAM_API, quoteReq);
expect(data.detail).to.equal('Unknown cosigner');
expect(status).to.equal(400);
});
});

describe('Valid requests', async () => {
it('successfully posts to order service', async () => {
const prebuildOrder = builder
.input({ token: TOKEN_IN, startAmount: AMOUNT, endAmount: AMOUNT })
.output({ token: TOKEN_OUT, startAmount: AMOUNT, endAmount: AMOUNT, recipient: SWAPPER_ADDRESS })
.nonce(BigNumber.from(100))
.cosigner(COSIGNER_ADDR)
.deadline(now + 1000)
.swapper(SWAPPER_ADDRESS);

const v2Order = prebuildOrder.buildPartial();
const { domain, types, values } = v2Order.permitData();
const signature = await swapper._signTypedData(domain, types, values);

const quoteReq: HardQuoteRequestBody = {
requestId: REQUEST_ID,
encodedInnerOrder: v2Order.serialize(),
innerSig: signature,
tokenInChainId: SEPOLIA,
tokenOutChainId: SEPOLIA,
allowNoQuote: true,
};

const { data, status } = await AxiosUtils.callPassThroughFail('POST', PARAM_API, quoteReq);
console.log(data);
expect(status).to.equal(200);
expect(data.chainId).to.equal(SEPOLIA);
expect(data.orderHash).to.match(/0x[0-9a-fA-F]{64}/);
});
});
});
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5304,10 +5304,10 @@
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.33.tgz#966ba96c9ccc8f0e9e09809890b438203f2b1911"
integrity sha512-JQkXcpRI3jFG8y3/CGC4TS8NkDgcxXaOQuYW8Qdvd6DcDiIyg2vVYCG9igFEzF0G6UvxgHkBKC7cWCgzZNYvQg==

"@uniswap/[email protected].3":
version "2.0.1-alpha.3"
resolved "https://registry.npmjs.org/@uniswap/uniswapx-sdk/-/uniswapx-sdk-2.0.1-alpha.3.tgz#09992069e7de258504185a85512fc2a5b763713b"
integrity sha512-0KUqmscaRPOL2QVguvZZq/573sQmNoohp7pRyxxuEqz0BSr91jxWe5j6UiSCL4SkACuI0Ti/hvyZAFQgue46YA==
"@uniswap/[email protected].7":
version "2.0.1-alpha.7"
resolved "https://registry.npmjs.org/@uniswap/uniswapx-sdk/-/uniswapx-sdk-2.0.1-alpha.7.tgz#64c3283f78c6a8814d44d8cbe6c1b3ac400f34eb"
integrity sha512-i4ie/NGK42AFDsoCbDYfvKKrJHy7UzWulCPj26USVbDq1goq9s0bIfxniMHEDL2nph47zSDLUzAYEtYbvqRKvw==
dependencies:
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/providers" "^5.7.0"
Expand Down

0 comments on commit 5cd359b

Please sign in to comment.