Skip to content

Commit

Permalink
feat(): migrate to ethers (#14)
Browse files Browse the repository at this point in the history
* feat(): listen eip6963 events

* feat(): add component to render state depends on provider connection

* feat(): add connection components

* feat(): support parsing v3 orders as a demo

* feat(): support checking signature for v3 orders

* feat(): migrate from web3 to ethers
  • Loading branch information
limitofzero committed Mar 30, 2024
1 parent dcf347d commit 99d158b
Show file tree
Hide file tree
Showing 17 changed files with 1,949 additions and 175 deletions.
13 changes: 13 additions & 0 deletions app/components/connect-wallet-btn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useProvider } from "@/app/providers/providers-context";

export default function ConnectWalletBtn() {
const { setActiveProvider } = useProvider();

return (
<button className='bg-1inch-bg-3 rounded-2xl p-4 text-1inch-text-2 flex hover:bg-btn-active-color-2'>
<img src='https://app.1inch.io/assets/images/icons/connect.svg' alt='Connect wallet' className='mr-2'/>
<span>Connect wallet</span>
</button>
)
}

7 changes: 3 additions & 4 deletions app/components/inch-button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ButtonHTMLAttributes, HTMLAttributes } from "react";
import React, { ButtonHTMLAttributes } from "react";

export default function InchButton(
{
Expand All @@ -7,10 +7,9 @@ export default function InchButton(
...props
}: {
children: React.ReactNode
} & HTMLAttributes<HTMLElement>) {
} & ButtonHTMLAttributes<HTMLElement>) {
return (
<button type={'submit'}
className={`${className} bg-btn-color p-4 rounded-2xl hover:bg-btn-active-color`} {...props}>
<button className={`${className} bg-btn-color p-4 rounded-2xl hover:bg-btn-active-color`} {...props}>
{children}
</button>
)
Expand Down
31 changes: 31 additions & 0 deletions app/components/render-if-wallet-is-connected.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Provider, { useProvider } from "@/app/providers/providers-context";
import { ReactNode } from "react";

export default function RenderIfWalletIsConnected(props: {
ifConnected: ReactNode,
ifNotConnected: ReactNode,
}) {

return (
<Provider>
<Main {...props}/>
</Provider>
);
}

function Main({
ifConnected,
ifNotConnected
}: {
ifConnected: ReactNode,
ifNotConnected: ReactNode,
}) {
const { activeProvider } = useProvider();
return (<>
{
activeProvider
? ifConnected
: ifNotConnected
}
</>)
}
2 changes: 1 addition & 1 deletion app/components/string-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function StringField<
{
formInstance,
name,
label
label,
}: { formInstance: FieldType<Form, TContext, TTransformedValues>, name: Path<Form>, label: string }
) {
return (
Expand Down
49 changes: 49 additions & 0 deletions app/helpers/ethers-provider-connector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { AbiDecodeResult, AbiItem, EIP712TypedData, ProviderConnector } from "@1inch/limit-order-protocol-utils";
import { AbiCoder, Interface, } from "ethers";
import { Web3Provider } from "@ethersproject/providers";
import { AbiOutput } from "web3-utils";
import { JsonRpcSigner } from "@ethersproject/providers/src.ts/json-rpc-provider";

export class EthersProviderConnector implements ProviderConnector {
#orderContract: Interface | null = null;

readonly #abiCoder = new AbiCoder();

readonly #web3Provider: Web3Provider;

readonly #signer: JsonRpcSigner;

constructor(provider: Web3Provider) {
this.#web3Provider = provider;
this.#signer = this.#web3Provider.getSigner();
}

contractEncodeABI(
abi: AbiItem[],
address: string | null,
methodName: string,
methodParams: unknown[]
): string {
if (this.#orderContract === null) {
this.#orderContract = new Interface(abi);
}

return this.#orderContract.encodeFunctionData(methodName, methodParams);
}

decodeABICallParameters(types: Array<AbiOutput | string>, callData: string): AbiDecodeResult {
return this.#abiCoder.decode(types as Array<string>, callData);
}

decodeABIParameter<T>(type: AbiOutput | string, hex: string): T {
return this.#abiCoder.decode([type as string], hex)[0] as T;
}

ethCall(contractAddress: string, callData: string): Promise<string> {
return this.#web3Provider.call({ to: contractAddress, data: callData });
}

signTypedData(walletAddress: string, typedData: EIP712TypedData, typedDataHash: string): Promise<string> {
return this.#signer._signTypedData(typedData.domain, typedData.types, typedData.message);
}
}
71 changes: 54 additions & 17 deletions app/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import Web3 from "web3";
import {
EIP712TypedData,
LimitOrderBuilder,
LimitOrderProtocolFacade,
LimitOrderProtocolFacade, LimitOrderProtocolV3Facade, LimitOrderV3Builder,
ParsedMakerTraits,
PROTOCOL_NAME,
PROTOCOL_VERSION,
ProviderConnector,
Web3ProviderConnector
} from "@1inch/limit-order-protocol-utils";
import { contractAddresses } from "@/app/helpers/contracts";
import { contractAddresses, EthChainId } from "@/app/helpers/contracts";
import { Web3Provider } from "@ethersproject/providers";
import { throws } from "assert";
import { Contract } from "ethers";
import { EthersProviderConnector } from "@/app/helpers/ethers-provider-connector";

export type FormattedMakerTraits = Omit<ParsedMakerTraits, 'nonce' | 'series'> & { nonce: number, series: number };

let web3: Web3 | null = null;
let web3: Web3Provider | null = null;
let facade: LimitOrderProtocolFacade | null = null;
let facadeV3: LimitOrderProtocolV3Facade | null = null;
export async function connectWeb3() {
if (web3) {
return;
}

if (typeof (window as any).ethereum !== 'undefined') {
web3 = new Web3((window as any).ethereum);
web3 = new Web3Provider((window as any).ethereum);
try {
await (window as any).ethereum.enable();
} catch (error) {
Expand All @@ -33,16 +37,22 @@ export async function connectWeb3() {
}

export async function getWeb3Data() {
await connectWeb3();
const networkId = await web3?.eth.net.getId()!;
const contractAddress = contractAddresses.get(networkId ?? 1)!;
const currentAddress = await web3?.eth.getAccounts();
await connectWeb3();
const network = await web3?.getNetwork();
const networkId = network?.chainId;
if (networkId === undefined) {
throw new Error('Network id is not defined');
}
const contractAddress = contractAddresses.get(networkId as EthChainId)!;
const contractAddressV3 = '0x1111111254EEB25477B68fb85Ed929f73A960582';
const currentAddress = await web3?.listAccounts()

return {
networkId,
contractAddress,
maker: currentAddress?.[0],
}
return {
networkId,
contractAddress,
contractAddressV3,
maker: currentAddress?.[0],
}
}

export function createProviderConnector(): ProviderConnector {
Expand All @@ -65,18 +75,33 @@ export function createProviderConnector(): ProviderConnector {
}

export function getLimitOrderBuilder() {
const builder = new LimitOrderBuilder(
return new LimitOrderBuilder(
createProviderConnector(),
{
domainName: PROTOCOL_NAME,
version: PROTOCOL_VERSION,
}
);
return builder;
}

export function getLimitOrderBuilderV3() {
return new LimitOrderV3Builder(
createProviderConnector(),
{
version: '1inch Aggregation Router',
domainName: '5',
}
)
}

export function getProvideConnector() {
return new Web3ProviderConnector(web3 as any)
const ethereum = web3;

if (!ethereum) {
throw new Error('Please connect a wallet first');
}

return new EthersProviderConnector(ethereum);
}

export async function getLimitOrderFacade() {
Expand All @@ -91,3 +116,15 @@ export async function getLimitOrderFacade() {
);
}

export async function getLimitOrderFacadeV3() {
if (facadeV3) {
return facadeV3;
}

const { networkId, contractAddressV3 } = await getWeb3Data();
const connector = getProvideConnector();
return new LimitOrderProtocolV3Facade(
contractAddressV3, networkId, connector
);
}

5 changes: 3 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './globals.css'
import type { Metadata } from 'next'
import Link from "next/link";
import React from "react";
import { ReactNode } from "react";

export const metadata: Metadata = {
title: 'Limit order parser',
Expand All @@ -10,7 +10,7 @@ export const metadata: Metadata = {
export default function RootLayout({
children,
}: {
children: React.ReactNode
children: ReactNode
}) {
return (
<html lang="en">
Expand All @@ -20,6 +20,7 @@ export default function RootLayout({
<ul className='flex flex-row'>
{/*<li><Link href="/builder">Builder</Link></li>*/}
<li className='ml-5'><Link href="/">Parser</Link></li>
<li className='ml-5'><Link href="/parser-v3">Parser v3 (demo)</Link></li>
</ul>
</nav>
</header>
Expand Down
37 changes: 33 additions & 4 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
'use client'
import React from "react";
import React, { useEffect } from "react";
import Parser from "@/app/parser/parser";
import { EIP6963AnnounceProviderEvent } from "@/app/types/eip6963";
import Provider, { useProvider } from "@/app/providers/providers-context";

export default function Home() {
return (
<>
<Parser></Parser>
</>
<Provider>
<Main></Main>
</Provider>
)
}

function Main() {
const { addProvider } = useProvider();

useEffect(() => {
const eip6963EventName = 'eip6963:announceProvider';
const handleEvent = (event: any) => {
const eventAsEIP6963AnnounceProviderEvent = event as EIP6963AnnounceProviderEvent;
addProvider(event);
};

window.addEventListener(
eip6963EventName,
handleEvent,
);

window.dispatchEvent(new Event("eip6963:requestProvider"));

return () => window.removeEventListener(eip6963EventName, handleEvent);
}, []);

return (
<>
<Parser></Parser>
</>
)
}
74 changes: 74 additions & 0 deletions app/parser-v3/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client'

import RenderIfWalletIsConnected from "@/app/components/render-if-wallet-is-connected";
import InchButton from "@/app/components/inch-button";
import React from "react";
import { FieldValues, useForm } from "react-hook-form";
import { LimitOrderLegacy } from "@1inch/limit-order-protocol-utils";
import { getLimitOrderFacadeV3 } from "@/app/helpers/helpers";
import { recoverAddress } from "ethers";
import StringField from "@/app/components/string-field";

const orderMock = {
allowedSender: '0xa88800cd213da5ae406ce248380802bd53b47647',
interactions: '0xbfa75143000000000000000000000000000000000000000000000000000000a800000024000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a863592c2b0000000000000000000000000000000000000000000000000000000065f82eb3bf15fcd80000000000000000000000005e92d4021e49f9a2967b4ea1d20213b3a1c7c912000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000040202470800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b8a49d816cc709b6eadb09498030ae3416b66dc00000000874d26f8f5dd55ee1a4167c49965b991c1c9530a00000000d1742b3c4fbb096990c8950fa635aec75b30781a00000000ad3b67bca8935cb510c8d18bd45f0b94f54a968f000000008571c129f335832f6bbc76d49414ad2b8371a42200000000f14f17989790a2116fc0a59ca88d5813e693528f00000000d14699b6b02e900a5c2338700d5181a674fdb9a2ffffffff38',
maker: '0x6edfb29e8bc75cbf448f2c557153dbcd979123b2',
makerAsset: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
makingAmount: '2000000000',
offsets: '0x12400000124000001240000012400000000000000000000000000000000',
receiver: '0x0000000000000000000000000000000000000000',
salt: '46122092693148306155654862778940756091791315691212717845485035582808298072652',
takerAsset: '0x111111111117dc0aa78b770fa6a738034120c302',
takingAmount: '3336299920270844680073'
}

const signature = '0x7badb3296e2bb4b3dccb874fdb153cc80572d146d7b770570a22585bbbc643a45a401c215528f00c6a505dfc489b3ea55f6efb3ab9bff66436e26b4970b52c4e1b';

export default function Page() {
const orderForm = useForm<{ order: string, signature: string }>({
defaultValues: {
order: JSON.stringify(orderMock, null, 2),
signature: signature
}
});

const parseOrder = async (data: FieldValues) => {
let order: LimitOrderLegacy & { signature: string } | null = null;
try {
order = JSON.parse(data.order);
} catch (e) {
alert(`Can't parse order`);
}

if (!order) {
return;
}
const facade = await getLimitOrderFacadeV3();
const orderHash = await facade.callHashOrder(order);
const checkSignature = recoverAddress(orderHash, signature);

alert(`signature is valid: ${checkSignature === order.maker}`);
}

return (
<>
<form className="grid grid-cols-1 gap-1" onSubmit={orderForm.handleSubmit((data) => parseOrder(data))}>
<div className="flex flex-col gap-10">
<textarea className="bg-1inch-bg-1 p-4 rounded-2xl w-100%"
style={{height: '370px'}}
{...orderForm.register('order')}
placeholder="Put order structure here"
></textarea>

<StringField formInstance={orderForm}
name='signature' label='Signature'></StringField>
<div className='flex justify-center flex-1'>
<RenderIfWalletIsConnected
ifConnected={<InchButton className='w-1/2'>Validate signature</InchButton>}
ifNotConnected={<InchButton className='w-1/2'>Validate signature</InchButton>}/>
</div>
</div>
</form>
</>
)
}
Loading

0 comments on commit 99d158b

Please sign in to comment.