Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add 1inch-fusion support #34

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
28 changes: 28 additions & 0 deletions src/types/oneinch-fusion.ts
Original file line number Diff line number Diff line change
@@ -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;
};
9 changes: 8 additions & 1 deletion src/visualizer/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -19,13 +21,15 @@ 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 => {
if (seaport.isCorrectDomain(domain)) return PROTOCOL_ID.OPENSEA_SEAPORT;
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;
};
Expand Down Expand Up @@ -55,6 +59,9 @@ export default async function visualize<T extends object>(
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);
Expand Down
32 changes: 32 additions & 0 deletions src/visualizer/oneinch-fusion/getAuctionTime.ts
Original file line number Diff line number Diff line change
@@ -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;
83 changes: 83 additions & 0 deletions src/visualizer/oneinch-fusion/index.ts
Original file line number Diff line number Diff line change
@@ -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<OneinchFusionOrder> = {
isCorrectDomain,
visualize,
};
export default oneinchFusion;
22 changes: 22 additions & 0 deletions test/visualizer/oneinch-fusion/data.ts
Original file line number Diff line number Diff line change
@@ -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 };
65 changes: 65 additions & 0 deletions test/visualizer/oneinch-fusion/index.test.ts
Original file line number Diff line number Diff line change
@@ -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: [],
});
});
});