diff --git a/src/types/index.ts b/src/types/index.ts index ce89595..0d49e93 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,6 +2,7 @@ export * as blurTypes from "./blur"; export * as looksrareTypes from "./looksrare"; export * as seaportTypes from "./seaport"; export * as visualizer from "./visualizer"; +export * as oneinchFusionTypes from "./oneinch-fusion"; export enum ASSET_TYPE { NATIVE = "NATIVE", diff --git a/src/types/oneinch-fusion.ts b/src/types/oneinch-fusion.ts new file mode 100644 index 0000000..a1db5e4 --- /dev/null +++ b/src/types/oneinch-fusion.ts @@ -0,0 +1,15 @@ +// see +// https://github.com/1inch/fusion-sdk/blob/4b76e9c232276742f879f3495e452dfc667b5a3a/src/limit-order/types.ts#L29-L41 +// https://github.com/1inch/fusion-sdk/blob/4b76e9c232276742f879f3495e452dfc667b5a3a/src/limit-order/eip712/order-typed-data-builder.ts#L23-L37 +export type OneinchFusionOrder = { + salt: string; + makerAsset: string; + takerAsset: string; + maker: string; + receiver: string; + allowedSender: string; + makingAmount: string; + takingAmount: string; + offsets: string; + 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/const.ts b/src/visualizer/oneinch-fusion/const.ts new file mode 100644 index 0000000..fbec9ad --- /dev/null +++ b/src/visualizer/oneinch-fusion/const.ts @@ -0,0 +1,14 @@ +// EIP-712 domain +export const ONEINCH_FUSION_VERIFYING_CONTRACT = + "0x1111111254eeb25477b68fb85ed929f73a960582"; + +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 +]; diff --git a/src/visualizer/oneinch-fusion/getAuctionTime.ts b/src/visualizer/oneinch-fusion/getAuctionTime.ts new file mode 100644 index 0000000..2e3989c --- /dev/null +++ b/src/visualizer/oneinch-fusion/getAuctionTime.ts @@ -0,0 +1,28 @@ +// The bit constants refers to fusion-sdk code. +// @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); + +// 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..6f6bab1 --- /dev/null +++ b/src/visualizer/oneinch-fusion/index.ts @@ -0,0 +1,79 @@ +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; + +// @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 +]; + +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, + to: endTime, + }, + approvals: [], + }; +}; + +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..18b905f --- /dev/null +++ b/test/visualizer/oneinch-fusion/data.ts @@ -0,0 +1,21 @@ +import { oneinchFusionTypes } from "../../../src/types"; + +// Test data refers to fusion-sdk code. +// 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..563cd73 --- /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: 1673548149, + to: 1673548329, + }, + approvals: [], + }); + }); +});