Skip to content

Commit

Permalink
refactor: cleanup + align swap response formats (#1355)
Browse files Browse the repository at this point in the history
* refactor: use common hash domain separator

* feat: align response format of all swap handlers

* test: add unified swap endpoint script
  • Loading branch information
dohaki authored Jan 7, 2025
1 parent 73b19f8 commit 8593c50
Show file tree
Hide file tree
Showing 16 changed files with 382 additions and 245 deletions.
23 changes: 23 additions & 0 deletions api/_eip712.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { utils } from "ethers";

export function hashDomainSeparator(params: {
name: string;
version: string | number;
chainId: number;
verifyingContract: string;
}): string {
return utils.keccak256(
utils.defaultAbiCoder.encode(
["bytes32", "bytes32", "bytes32", "uint256", "address"],
[
utils.id(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
utils.id(params.name),
utils.id(params.version.toString()),
params.chainId,
params.verifyingContract,
]
)
);
}
21 changes: 7 additions & 14 deletions api/_permit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BigNumberish, ethers } from "ethers";

import { getProvider } from "./_utils";
import { ERC20_PERMIT_ABI } from "./_abis";
import { hashDomainSeparator } from "./_eip712";

export class PermitNotSupportedError extends Error {
constructor(tokenAddress: string, cause?: Error) {
Expand Down Expand Up @@ -129,20 +130,12 @@ export async function getPermitArgsFromContract(params: {
? Number(versionFromContract)
: params.eip712DomainVersion || 1;

const domainSeparatorHash = ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
["bytes32", "bytes32", "bytes32", "uint256", "address"],
[
ethers.utils.id(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
ethers.utils.id(name),
ethers.utils.id(eip712DomainVersion.toString()),
params.chainId,
params.tokenAddress,
]
)
);
const domainSeparatorHash = hashDomainSeparator({
name,
version: eip712DomainVersion,
chainId: params.chainId,
verifyingContract: params.tokenAddress,
});

if (domainSeparator !== domainSeparatorHash) {
throw new PermitDomainSeparatorMismatchError(params.tokenAddress);
Expand Down
24 changes: 1 addition & 23 deletions api/_transfer-with-auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BigNumberish, ethers } from "ethers";
import { getProvider } from "./_utils";
import { ERC_TRANSFER_WITH_AUTH_ABI } from "./_abis";
import { utils } from "ethers";
import { hashDomainSeparator } from "./_eip712";

export class TransferWithAuthNotSupportedError extends Error {
constructor(tokenAddress: string, cause?: Error) {
Expand All @@ -22,28 +22,6 @@ export class TransferWithAuthDomainSeparatorMismatchError extends Error {
}
}

export function hashDomainSeparator(params: {
name: string;
version: string | number;
chainId: number;
verifyingContract: string;
}): string {
return utils.keccak256(
utils.defaultAbiCoder.encode(
["bytes32", "bytes32", "bytes32", "uint256", "address"],
[
utils.id(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
utils.id(params.name),
utils.id(params.version.toString()),
params.chainId,
params.verifyingContract,
]
)
);
}

export async function getReceiveWithAuthTypedData(params: {
tokenAddress: string;
chainId: number;
Expand Down
114 changes: 114 additions & 0 deletions api/swap/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
} from "../_dexes/types";
import { AMOUNT_TYPE } from "../_dexes/utils";
import { encodeApproveCalldata } from "../_multicall-handler";
import { AuthTxPayload } from "./auth/_utils";
import { PermitTxPayload } from "./permit/_utils";

export const BaseSwapQueryParamsSchema = type({
amount: positiveIntStr(),
Expand Down Expand Up @@ -110,6 +112,8 @@ export async function handleBaseSwapQueryParams(
}),
]);

const refundToken = refundOnOrigin ? inputToken : outputToken;

return {
inputToken,
outputToken,
Expand All @@ -124,6 +128,7 @@ export async function handleBaseSwapQueryParams(
recipient,
depositor,
slippageTolerance,
refundToken,
};
}

Expand Down Expand Up @@ -262,3 +267,112 @@ export function stringifyBigNumProps<T extends object | any[]>(value: T): T {
})
) as T;
}

export function buildBaseSwapResponseJson(params: {
inputTokenAddress: string;
originChainId: number;
inputAmount: BigNumber;
allowance: BigNumber;
balance: BigNumber;
approvalTxns?: {
to: string;
data: string;
}[];
originSwapQuote?: SwapQuote;
bridgeQuote: CrossSwapQuotes["bridgeQuote"];
destinationSwapQuote?: SwapQuote;
refundToken: Token;
approvalSwapTx?: {
from: string;
to: string;
data: string;
value?: BigNumber;
gas?: BigNumber;
gasPrice: BigNumber;
};
permitSwapTx?: AuthTxPayload | PermitTxPayload;
}) {
return stringifyBigNumProps({
checks: {
allowance: params.approvalSwapTx
? {
token: params.inputTokenAddress,
spender: params.approvalSwapTx.to,
actual: params.allowance,
expected: params.inputAmount,
}
: // TODO: Handle permit2 required allowance
{
token: params.inputTokenAddress,
spender: constants.AddressZero,
actual: 0,
expected: 0,
},
balance: {
token: params.inputTokenAddress,
actual: params.balance,
expected: params.inputAmount,
},
},
approvalTxns: params.approvalTxns,
steps: {
originSwap: params.originSwapQuote
? {
tokenIn: params.originSwapQuote.tokenIn,
tokenOut: params.originSwapQuote.tokenOut,
inputAmount: params.originSwapQuote.expectedAmountIn,
outputAmount: params.originSwapQuote.expectedAmountOut,
minOutputAmount: params.originSwapQuote.minAmountOut,
maxInputAmount: params.originSwapQuote.maximumAmountIn,
}
: undefined,
bridge: {
inputAmount: params.bridgeQuote.inputAmount,
outputAmount: params.bridgeQuote.outputAmount,
tokenIn: params.bridgeQuote.inputToken,
tokenOut: params.bridgeQuote.outputToken,
},
destinationSwap: params.destinationSwapQuote
? {
tokenIn: params.destinationSwapQuote.tokenIn,
tokenOut: params.destinationSwapQuote.tokenOut,
inputAmount: params.destinationSwapQuote.expectedAmountIn,
maxInputAmount: params.destinationSwapQuote.maximumAmountIn,
outputAmount: params.destinationSwapQuote.expectedAmountOut,
minOutputAmount: params.destinationSwapQuote.minAmountOut,
}
: undefined,
},
refundToken:
params.refundToken.symbol === "ETH"
? {
...params.refundToken,
symbol: "WETH",
}
: params.refundToken,
inputAmount:
params.originSwapQuote?.expectedAmountIn ??
params.bridgeQuote.inputAmount,
expectedOutputAmount:
params.destinationSwapQuote?.expectedAmountOut ??
params.bridgeQuote.outputAmount,
minOutputAmount:
params.destinationSwapQuote?.minAmountOut ??
params.bridgeQuote.outputAmount,
expectedFillTime: params.bridgeQuote.suggestedFees.estimatedFillTimeSec,
swapTx: params.approvalSwapTx
? {
simulationSuccess: !!params.approvalSwapTx.gas,
chainId: params.originChainId,
to: params.approvalSwapTx.to,
data: params.approvalSwapTx.data,
value: params.approvalSwapTx.value,
gas: params.approvalSwapTx.gas,
gasPrice: params.approvalSwapTx.gasPrice,
}
: params.permitSwapTx
? params.permitSwapTx.swapTx
: undefined,
eip712: params.permitSwapTx?.eip712,
});
}
2 changes: 1 addition & 1 deletion api/swap/approval/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export async function buildCrossSwapTxForAllowanceHolder(
return {
from: crossSwapQuotes.crossSwap.depositor,
to: toAddress,
data: integratorId ? tagIntegratorId(integratorId, tx.data!) : tx.data,
data: integratorId ? tagIntegratorId(integratorId, tx.data!) : tx.data!,
value: tx.value,
};
}
90 changes: 17 additions & 73 deletions api/swap/approval/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
handleBaseSwapQueryParams,
BaseSwapQueryParams,
getApprovalTxns,
buildBaseSwapResponseJson,
} from "../_utils";
import { getBalanceAndAllowance } from "../../_erc20";
import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service";
Expand Down Expand Up @@ -56,6 +57,7 @@ const handler = async (
recipient,
depositor,
slippageTolerance,
refundToken,
} = await handleBaseSwapQueryParams(request.query);

const crossSwapQuotes = await getCrossSwapQuotes(
Expand Down Expand Up @@ -134,81 +136,23 @@ const handler = async (
});
}

const refundToken = crossSwap.refundOnOrigin
? bridgeQuote.inputToken
: bridgeQuote.outputToken;

const responseJson = {
// fees: crossSwapQuotes.fees,
checks: {
allowance: {
token: inputTokenAddress,
spender: crossSwapTx.to,
actual: allowance.toString(),
expected: inputAmount.toString(),
},
balance: {
token: inputTokenAddress,
actual: balance.toString(),
expected: inputAmount.toString(),
},
const responseJson = buildBaseSwapResponseJson({
originChainId,
inputTokenAddress,
inputAmount,
approvalSwapTx: {
...crossSwapTx,
gas: originTxGas,
gasPrice: originTxGasPrice,
},
allowance,
balance,
approvalTxns,
steps: {
originSwap: originSwapQuote
? {
tokenIn: originSwapQuote.tokenIn,
tokenOut: originSwapQuote.tokenOut,
inputAmount: originSwapQuote.expectedAmountIn.toString(),
outputAmount: originSwapQuote.expectedAmountOut.toString(),
minOutputAmount: originSwapQuote.minAmountOut.toString(),
maxInputAmount: originSwapQuote.maximumAmountIn.toString(),
}
: undefined,
bridge: {
inputAmount: bridgeQuote.inputAmount.toString(),
outputAmount: bridgeQuote.outputAmount.toString(),
tokenIn: bridgeQuote.inputToken,
tokenOut: bridgeQuote.outputToken,
},
destinationSwap: destinationSwapQuote
? {
tokenIn: destinationSwapQuote.tokenIn,
tokenOut: destinationSwapQuote.tokenOut,
inputAmount: destinationSwapQuote.expectedAmountIn.toString(),
maxInputAmount: destinationSwapQuote.maximumAmountIn.toString(),
outputAmount: destinationSwapQuote.expectedAmountOut.toString(),
minOutputAmount: destinationSwapQuote.minAmountOut.toString(),
}
: undefined,
},
swapTx: {
simulationSuccess: !!originTxGas,
chainId: originChainId,
to: crossSwapTx.to,
data: crossSwapTx.data,
value: crossSwapTx.value?.toString(),
gas: originTxGas?.toString(),
gasPrice: originTxGasPrice?.toString(),
},
refundToken:
refundToken.symbol === "ETH"
? {
...refundToken,
symbol: "WETH",
}
: refundToken,
inputAmount:
originSwapQuote?.expectedAmountIn.toString() ??
bridgeQuote.inputAmount.toString(),
expectedOutputAmount:
destinationSwapQuote?.expectedAmountOut.toString() ??
bridgeQuote.outputAmount.toString(),
minOutputAmount:
destinationSwapQuote?.minAmountOut.toString() ??
bridgeQuote.outputAmount.toString(),
expectedFillTime: bridgeQuote.suggestedFees.estimatedFillTimeSec,
};
originSwapQuote,
bridgeQuote,
destinationSwapQuote,
refundToken,
});
mark.stop();
logger.debug({
at: "Swap/approval",
Expand Down
5 changes: 3 additions & 2 deletions api/swap/auth/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
import { BigNumberish, BytesLike, utils } from "ethers";
import { SpokePoolV3PeripheryInterface } from "../../_typechain/SpokePoolV3Periphery";

export type AuthTxPayload = Awaited<ReturnType<typeof buildAuthTxPayload>>;

export async function buildAuthTxPayload({
crossSwapQuotes,
authDeadline,
Expand Down Expand Up @@ -154,10 +156,9 @@ export async function buildAuthTxPayload({

return {
eip712: {
receiveWithAuthorization: authTypedData.eip712,
permit: authTypedData.eip712,
deposit: depositTypedData.eip712,
},

swapTx: {
chainId: originChainId,
to: entryPointContract.address,
Expand Down
Loading

0 comments on commit 8593c50

Please sign in to comment.