diff --git a/src/types/index.ts b/src/types/index.ts index ce89595..4c0dd89 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,7 @@ export * as blurTypes from "./blur"; export * as looksrareTypes from "./looksrare"; export * as seaportTypes from "./seaport"; +export * as oneinchFusionTypes from "./oneinch-fusion"; export * as visualizer from "./visualizer"; export enum ASSET_TYPE { diff --git a/src/types/oneinch-fusion.ts b/src/types/oneinch-fusion.ts new file mode 100644 index 0000000..3b31720 --- /dev/null +++ b/src/types/oneinch-fusion.ts @@ -0,0 +1,28 @@ +/** + * @dev The order type used by 1inch Fusion sdks. + * @see https://portal.1inch.dev/documentation/fusion/fusion-sdk/for-resolvers/auction-calculator + * @see https://github.com/1inch/fusion-sdk/blob/4b76e9c232276742f879f3495e452dfc667b5a3a/src/limit-order/types.ts#L29-L41 + * @see https://github.com/1inch/fusion-sdk/blob/4b76e9c232276742f879f3495e452dfc667b5a3a/src/limit-order/eip712/order-typed-data-builder.ts#L34 + */ +export type OneinchFusionOrder = { + // this includes the auction's start time, duration. + salt: string; + // the address of the asset user want to sell (address of a token contract) + makerAsset: string; + // the address of the asset user want to buy (address of a token contract) + takerAsset: string; + // the address of the maker + maker: string; + // If it contains a zero address, which means that taker asset will be sent to the address of the creator of the order. If user set any other value, then taker asset will be sent to the specified address + receiver: string; + // If it contains a zero address, which means that a limit order is available for everyone to fill. If user set any other value, then the limit order will be available for execution only for the specified address (private limit order) + allowedSender: string; + // amount of the token to sell + makingAmount: string; + // amount of taker asset + takingAmount: string; + // every 32's bytes represents offset of the n'ths interaction + offsets: string; + // used to encode fusion specific data. But it's not used in the wizard-sdk. + interactions: string; +}; diff --git a/src/visualizer/index.ts b/src/visualizer/index.ts index 384e7e0..dbf5229 100644 --- a/src/visualizer/index.ts +++ b/src/visualizer/index.ts @@ -1,13 +1,15 @@ import { PermitMessage } from "../types"; -import { SeaPortPayload } from "../types/seaport"; import { BlurIoOrder } from "../types/blur"; import { LooksrareMakerOrderWithEncodedParams } from "../types/looksrare"; +import { OneinchFusionOrder } from "../types/oneinch-fusion"; +import { SeaPortPayload } from "../types/seaport"; import blurIo from "./blur-io"; import erc20Permit from "./erc20-permit"; import looksrare from "./looksrare"; import looksrareV2 from "./looksrare-v2"; +import oneinchFusion from "./oneinch-fusion"; import seaport from "./seaport"; import { Domain, VisualizationResult } from "../types/visualizer"; import { WizardError } from "../utils"; @@ -19,6 +21,7 @@ export enum PROTOCOL_ID { LOOKSRARE_EXCHANGE_V2 = "LOOKSRARE_EXCHANGE_V2", BLUR_IO_MARKETPLACE = "BLUR_IO_MARKETPLACE", ERC20_PERMIT = "ERC20_PERMIT", + ONEINCH_FUSION = "ONEINCH_FUSION", } export const getProtocolId = (domain: Domain): PROTOCOL_ID | undefined => { @@ -26,6 +29,7 @@ export const getProtocolId = (domain: Domain): PROTOCOL_ID | undefined => { if (blurIo.isCorrectDomain(domain)) return PROTOCOL_ID.BLUR_IO_MARKETPLACE; if (looksrareV2.isCorrectDomain(domain)) return PROTOCOL_ID.LOOKSRARE_EXCHANGE_V2; if (looksrare.isCorrectDomain(domain)) return PROTOCOL_ID.LOOKSRARE_EXCHANGE; + if (oneinchFusion.isCorrectDomain(domain)) return PROTOCOL_ID.ONEINCH_FUSION; return; }; @@ -55,6 +59,9 @@ export default async function visualize( case PROTOCOL_ID.BLUR_IO_MARKETPLACE: return blurIo.visualize(message as BlurIoOrder, domain); + case PROTOCOL_ID.ONEINCH_FUSION: + return oneinchFusion.visualize(message as OneinchFusionOrder, domain); + default: if (erc20Permit.isERC20Permit(message)) { return erc20Permit.visualize(message as PermitMessage, domain); diff --git a/src/visualizer/oneinch-fusion/getAuctionTime.ts b/src/visualizer/oneinch-fusion/getAuctionTime.ts new file mode 100644 index 0000000..c37e7a7 --- /dev/null +++ b/src/visualizer/oneinch-fusion/getAuctionTime.ts @@ -0,0 +1,32 @@ +/** + * @dev 1inch fusion auction salt mask constants + * @see https://github.com/1inch/fusion-sdk/blob/4b76e9c232276742f879f3495e452dfc667b5a3a/src/auction-salt/parser/constants.ts + */ +const START_TIME_MASK = BigInt( + "0xFFFFFFFF00000000000000000000000000000000000000000000000000000000" +); +const DURATION_MASK = BigInt( + "0x00000000FFFFFF00000000000000000000000000000000000000000000000000" +); +const START_TIME_SHIFT = BigInt(224); +const DURATION_SHIFT = BigInt(200); + +/** + * @dev get the auction start and end time from the salt + * @see https://github.com/1inch/fusion-sdk/blob/4b76e9c232276742f879f3495e452dfc667b5a3a/src/auction-salt/parser/parser.ts#L15-L27 + */ +export const getAuctionTime = (salt: string): { startTime: number; endTime: number } => { + const startTime = getAuctionStartTime(salt); + const duration = getAuctionDuration(salt); + + return { + startTime: Number(startTime), + endTime: Number(startTime + duration), + }; +}; + +const getAuctionStartTime = (salt: string) => + (BigInt(salt) & START_TIME_MASK) >> START_TIME_SHIFT; + +const getAuctionDuration = (salt: string) => + (BigInt(salt) & DURATION_MASK) >> DURATION_SHIFT; diff --git a/src/visualizer/oneinch-fusion/index.ts b/src/visualizer/oneinch-fusion/index.ts new file mode 100644 index 0000000..8988ea2 --- /dev/null +++ b/src/visualizer/oneinch-fusion/index.ts @@ -0,0 +1,83 @@ +import { Domain, EIP712Protocol, VisualizationResult } from "../../types/visualizer"; +import { OneinchFusionOrder } from "../../types/oneinch-fusion"; +import { PROTOCOL_ID } from ".."; +import { ASSET_TYPE, AssetInOut } from "../../types"; +import { ZERO_ADDRESS, getPaymentAssetType } from "../../utils"; +import { getAuctionTime } from "./getAuctionTime"; + +const { NATIVE } = ASSET_TYPE; + +export const isCorrectDomain = (domain: Domain) => { + return ( + ONEINCH_FUSION_SUPPORTED_CHAINS.includes(Number(domain.chainId)) && + addressesBook.includes(domain.verifyingContract.toLocaleLowerCase()) + ); +}; + +export const visualize = ( + message: OneinchFusionOrder, + domain: Domain +): VisualizationResult => { + if (!isCorrectDomain(domain)) throw new Error("wrong 1inch-fusion domain"); + + const makerAssetType = getPaymentAssetType(message.makerAsset); + const assetsIn: AssetInOut[] = [ + { + address: makerAssetType === NATIVE ? ZERO_ADDRESS : message.makerAsset, + type: makerAssetType, + amounts: [message.makingAmount], + }, + ]; + + const takerAssetType = getPaymentAssetType(message.takerAsset); + const assetsOut: AssetInOut[] = [ + { + address: takerAssetType === NATIVE ? ZERO_ADDRESS : message.takerAsset, + type: takerAssetType, + amounts: [message.takingAmount], + }, + ]; + + const { startTime, endTime } = getAuctionTime(message.salt); + + return { + protocol: PROTOCOL_ID.ONEINCH_FUSION, + assetsIn, + assetsOut, + liveness: { + from: startTime * 1000, + to: endTime * 1000, + }, + approvals: [], + }; +}; + +/** + * @see https://github.com/1inch/fusion-sdk/blob/4b76e9c232276742f879f3495e452dfc667b5a3a/src/constants.ts#L50 + */ +export const ONEINCH_FUSION_VERIFYING_CONTRACT = + "0x1111111254eeb25477b68fb85ed929f73a960582"; + +/** + * @see https://github.com/1inch/fusion-sdk/blob/4b76e9c232276742f879f3495e452dfc667b5a3a/src/constants.ts#L6-L15 + */ +export const ONEINCH_FUSION_SUPPORTED_CHAINS = [ + 1, //Ethereum + 137, //Polygon + 56, //Binance + 42161, //Arbitrum One + 43114, //Avalanche C-Chain + 10, //Optimism + 250, //Fantom + 100, //Gnosis +]; + +const addressesBook = [ONEINCH_FUSION_VERIFYING_CONTRACT].map((e) => + e.toLocaleLowerCase() +); + +const oneinchFusion: EIP712Protocol = { + isCorrectDomain, + visualize, +}; +export default oneinchFusion; diff --git a/test/visualizer/oneinch-fusion/data.ts b/test/visualizer/oneinch-fusion/data.ts new file mode 100644 index 0000000..83d9760 --- /dev/null +++ b/test/visualizer/oneinch-fusion/data.ts @@ -0,0 +1,22 @@ +import { oneinchFusionTypes } from "../../../src/types"; + +/** + * @dev test data for 1inch fusion successful order + * @see https://github.com/1inch/fusion-sdk/blob/4b76e9c232276742f879f3495e452dfc667b5a3a/src/fusion-order/fusion-order.spec.ts#L12-L61 + */ +const oneinchFusionOrder: oneinchFusionTypes.OneinchFusionOrder = { + salt: "45118768841948961586167738353692277076075522015101619148498725069326976558864", + makerAsset: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + takerAsset: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maker: "0x00000000219ab540356cbb839cbe05303d7705fa", + receiver: "0x0000000000000000000000000000000000000000", + allowedSender: "0xa88800cd213da5ae406ce248380802bd53b47647", + makingAmount: "1000000000000000000", + takingAmount: "1420000000", + offsets: "0", + interactions: "0x000c004e200000000000000000219ab540356cbb839cbe05303d7705faf486570009", +}; + +Object.freeze(oneinchFusionOrder); + +export { oneinchFusionOrder }; diff --git a/test/visualizer/oneinch-fusion/index.test.ts b/test/visualizer/oneinch-fusion/index.test.ts new file mode 100644 index 0000000..4df2c1c --- /dev/null +++ b/test/visualizer/oneinch-fusion/index.test.ts @@ -0,0 +1,65 @@ +import { Domain } from "../../../src/types/visualizer"; +import visualize from "../../../src/visualizer"; +import oneinchFusion from "../../../src/visualizer/oneinch-fusion"; + +import { oneinchFusionOrder } from "./data"; + +describe("oneinch-fusion", () => { + const oneinchFusionDomain: Domain = { + chainId: "1", + name: "1inch Aggregation Router", + verifyingContract: "0x1111111254eeb25477b68fb85ed929f73a960582", + version: "5", + }; + + it("should revert if domain is not recognized by SDK entry", async () => { + await expect( + visualize(oneinchFusionOrder, { ...oneinchFusionDomain, chainId: "-1" }) + ).rejects.toThrowError("Unrecognized/Unsupported EIP712Protocol Domain"); + }); + + it("should revert at oneinch-fusion module level if the domain verifyingContract is wrong", () => { + expect(() => { + oneinchFusion.visualize(oneinchFusionOrder, { + ...oneinchFusionDomain, + verifyingContract: "0x0", + }); + }).toThrow("wrong 1inch-fusion domain"); + }); + + it("should revert at oneinch-fusion module level if the domain chainId is wrong", () => { + expect(() => { + oneinchFusion.visualize(oneinchFusionOrder, { + ...oneinchFusionDomain, + chainId: "-1", + }); + }).toThrow("wrong 1inch-fusion domain"); + }); + + it("should successfully visualize oneinch-fusion order", async () => { + const result = await visualize(oneinchFusionOrder, oneinchFusionDomain); + + expect(result).toEqual({ + protocol: "ONEINCH_FUSION", + assetsIn: [ + { + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + amounts: ["1000000000000000000"], + type: "ERC20", + }, + ], + assetsOut: [ + { + address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + amounts: ["1420000000"], + type: "ERC20", + }, + ], + liveness: { + from: 1673548149000, + to: 1673548329000, + }, + approvals: [], + }); + }); +});