Skip to content

Commit

Permalink
feat: update resolver and publish hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
adamazad committed May 11, 2023
1 parent 1e9d645 commit fdfa5c9
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 81 deletions.
90 changes: 48 additions & 42 deletions src/components/CreateNimi/CreateNimi.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ContractReceipt, ContractTransaction } from '@ethersproject/contracts';

import { encodeContenthash, namehash as ensNameHash } from '@ensdomains/ui';
import { yupResolver } from '@hookform/resolvers/yup';
import { Nimi, NimiImageType, NimiLinkType, NimiWidget, NimiWidgetType } from '@nimi.io/card/types';
import { nimiValidator } from '@nimi.io/card/validators';
Expand All @@ -14,7 +13,8 @@ import { PoapField } from './partials/PoapField';
import { BlockchainAddresses, FormItem, InnerWrapper, MainContent, PageSectionTitle } from './styled';
import { themes } from './themes';
import { usePublishNimiIPNS, useUpdateNimiIPNS } from '../../api/RestAPI/hooks/usePublishNimiIPNS';
import { useENSPublicResolverContract } from '../../hooks/useENSPublicResolverContract';
import { getContentHashStruct, useContenthash } from '../../hooks/useContenthash';
import { useENSNameResolverContract } from '../../hooks/useENSNameResolverContract';
import { useRainbow } from '../../hooks/useRainbow';
import { setENSNameContentHash } from '../../hooks/useSetContentHash';
import {
Expand Down Expand Up @@ -53,7 +53,6 @@ export interface CreateNimiProps {

export function CreateNimi({ ensAddress, ensName, availableThemes, initialNimi, nimiIPNSKey }: CreateNimiProps) {
const [stepsCompleted, setStepsCompleted] = useState<PublishNimiPageStep[]>([]);

const [showPreviewMobile, setShowPreviewMobile] = useState(false);
const { modalOpened, ModalTypes, openModal, closeModal, showSpinner, hideSpinner } = useUserInterface();
const { mutateAsync: publishNimiAsync } = usePublishNimiIPNS();
Expand All @@ -67,10 +66,8 @@ export function CreateNimi({ ensAddress, ensName, availableThemes, initialNimi,
const { chainId } = useRainbow();
const { t } = useTranslation('nimi');
const { signMessageAsync } = useSignMessage();

const publicResolverContract = useENSPublicResolverContract();

debug({ initialNimi });
const { data: ensNameResolverContract } = useENSNameResolverContract(ensName, true);
const { data: currentContenthashData, refetch: refetchCurrentContenthash } = useContenthash(ensName);

const useFormContext = useForm<Nimi>({
resolver: yupResolver(nimiValidator),
Expand Down Expand Up @@ -117,73 +114,80 @@ export function CreateNimi({ ensAddress, ensName, availableThemes, initialNimi,
setSetContentHashReceipt(undefined);
openModal(ModalTypes.PUBLISH_NIMI);

if (!ensNameResolverContract) {
setPublishNimiError(new Error('No ENS name resolver contract'));
return;
}

try {
if (!publicResolverContract) {
throw new Error('ENS Public Resolver contract is not available.');
}
const publishOrUpdateResult = {
cid: '',
ipns: '',
};

// Updating a current Nimi IPNS record
if (nimiIPNSKey) {
debug(`Updating Nimi IPNS record ${nimiIPNSKey}`);
const signature = await signMessageAsync({ message: JSON.stringify(nimi) });
const updateNimiResponse = await updateNimiAsync({
const { cid, ipns } = await updateNimiAsync({
nimi,
chainId: 1, // always mainnet
signature,
});
if (!updateNimiResponse || !updateNimiResponse.cid) {
if (!ipns || !cid) {
throw new Error('No response from updateNimiAsync');
}
setStepsCompleted((stepsCompleted) => [
...stepsCompleted,
PublishNimiPageStep.BUNDLE_NIMI_PAGE,
PublishNimiPageStep.SET_CONTENT_HASH,
]);
setPublishNimiResponseIpfsHash(updateNimiResponse.cid);
setSetContentHashReceipt({ status: 1 } as ContractReceipt);
setIsNimiPublished(true);
setIsPublishingNimi(false);
return;
}

// Publishing a new Nimi IPNS record
const { cid, ipns } = await publishNimiAsync({
nimi,
chainId: chainId as number,
});
publishOrUpdateResult.cid = cid;
publishOrUpdateResult.ipns = ipns;
} else {
// Publishing a new Nimi IPNS record
const { cid, ipns } = await publishNimiAsync({
nimi,
chainId: chainId as number,
});

if (!cid) {
throw new Error('No CID returned from publishNimiViaIPNS');
if (!ipns || !cid) {
throw new Error('No CID returned from publishNimiViaIPNS');
}
publishOrUpdateResult.cid = cid;
publishOrUpdateResult.ipns = ipns;
}

// Compare the current content hash with the new one
const currentContentHashEncoded = await publicResolverContract.contenthash(ensNameHash(ensName));
const contentHash = `ipns://${ipns}`;
const newContentHashEncoded = encodeContenthash(contentHash).encoded as unknown as string;
// At this point, the Nimi Page has been published to IPFS network and we have the IPNS record (or created)
setStepsCompleted((stepsCompleted) => [...stepsCompleted, PublishNimiPageStep.BUNDLE_NIMI_PAGE]);
setPublishNimiResponseIpfsHash(publishOrUpdateResult.cid);

// Get the current contenthash data
const nextContenthashData = getContentHashStruct(publishOrUpdateResult.ipns, 'ipns');
console.log({ currentContenthashData, nextContenthashData });

if (currentContentHashEncoded === newContentHashEncoded) {
// If the current ENS content has is Nimi's IPNS record, we don't need to update it
if (currentContenthashData.contenthash === nextContenthashData.contenthash) {
setSetContentHashReceipt({ status: 1 } as ContractReceipt);
setStepsCompleted((stepsCompleted) => [...stepsCompleted, PublishNimiPageStep.SET_CONTENT_HASH]);
setIsNimiPublished(true);
hideSpinner();
return;
}

setStepsCompleted([PublishNimiPageStep.BUNDLE_NIMI_PAGE]);
setPublishNimiResponseIpfsHash(cid);

// Set the contenthash on the ENS name
const setContentHashTx = await setENSNameContentHash({
contract: publicResolverContract,
contract: ensNameResolverContract,
name: nimi.ensName,
contentHash,
contentHashURI: nextContenthashData.contenthashURI as string,
});
setSetContentHashTransaction(setContentHashTx);
// Wait for the transaction to be mined
const setContentHashTxReceipt = await setContentHashTx.wait();
setSetContentHashReceipt(setContentHashTxReceipt);
setStepsCompleted((stepsCompleted) => [...stepsCompleted, PublishNimiPageStep.SET_CONTENT_HASH]);
setIsNimiPublished(true);
openModal(ModalTypes.PUBLISH_NIMI);
} catch (error) {
debug(error);
setPublishNimiError(error);
} finally {
// Always refetch the current contenthash data
refetchCurrentContenthash();
}
};

Expand All @@ -200,6 +204,8 @@ export function CreateNimi({ ensAddress, ensName, availableThemes, initialNimi,
eventTarget.style.height = `${eventTarget.scrollHeight}px`;
};

debug({ publishNimiError, initialNimi, currentContenthashData });

return (
<FormProvider {...useFormContext}>
<InnerWrapper>
Expand Down
104 changes: 104 additions & 0 deletions src/hooks/useContenthash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { decodeContenthash, encodeContenthash, namehash } from '@ensdomains/ui';
import { useEffect, useState } from 'react';

import { useENSNameResolverContract } from './useENSNameResolverContract';

type ContentHashProtocolType = 'ipns' | 'ipfs';

interface UseContentnHashResults {
isLoading: boolean;
data: ContenthashStruct;
/**
* Refetches the content hash
* @returns void
*/
refetch: () => void;
}

export interface ContenthashStruct {
/**
* Encoded content hash (e.g. 0xe30101701220b9c015918bd73d3e800000000000000)
*/
contenthash?: '0x' | string;
/**
* Decoded content hash (e.g. bafybeib7z3q2)
*/
contenthashCID?: string;
/**
* Decoded content hash with protocol (e.g. ipfs://bafybeib7z3q2)
*/
contenthashURI?: `${ContentHashProtocolType}://${string}`;
/**
* Content hash protocol type (e.g. ipfs)
*/
protocolType?: ContentHashProtocolType;
}

/**
* Returns the content hash of an ENS name
* @param ensName The ENS name
* @returns The content hash of the ENS name
*/
export function useContenthash(ensName: string): UseContentnHashResults {
const [refetchCount, setRefetchCount] = useState(0);
const { data: resolverContract } = useENSNameResolverContract(ensName, true);
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<ContenthashStruct>({});

useEffect(() => {
if (!resolverContract || !ensName) {
return;
}

setIsLoading(true);

resolverContract
.contenthash(namehash(ensName))
.then((encodeContenthash) => {
const { protocolType, decoded } = decodeContenthash(encodeContenthash);

const contenthashURI =
protocolType && decoded ? (`${protocolType}://${decoded}` as ContenthashStruct['contenthashURI']) : undefined;

setData({
contenthash: encodeContenthash,
contenthashCID: decoded,
contenthashURI,
protocolType: protocolType as ContentHashProtocolType,
});
})
.catch((error) => {
console.error(error);
})
.finally(() => {
setIsLoading(false);
});
}, [refetchCount, ensName, resolverContract]);

const refetch = () => {
setRefetchCount((prev) => prev + 1);
};

return {
isLoading,
data,
refetch,
};
}

/**
* Transforms a CID into a content hash struct
* @param cid The CID
* @param protocolType The protocol type (e.g. ipfs)
* @returns The content hash struct
*/
export function getContentHashStruct(cid: string, protocolType: ContentHashProtocolType): ContenthashStruct {
const nextContenthashData: ContenthashStruct = {
contenthashURI: `${protocolType}://${cid}`,
contenthash: encodeContenthash(`${protocolType}://${cid}`).encoded as unknown as string,
contenthashCID: cid,
protocolType,
};

return nextContenthashData;
}
70 changes: 70 additions & 0 deletions src/hooks/useENSNameResolverContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Signer } from '@wagmi/core';
import { useEffect, useMemo, useState } from 'react';
import { useSigner } from 'wagmi';

import { useRainbow } from './useRainbow';
import { PUBLIC_RESOLVER_ADDRESSES } from '../constants';
import { EnsPublicResolver, EnsPublicResolver__factory } from '../generated/contracts';

/**
* Returns the ENS Resolver contract instance for a given ENS name
* @param ensName The ENS name
* @param withSignerIfPossible Whether to use a signer if one is available
* @returns The ENS Resolver contract instance
*/
export function useENSNameResolverContract(ensName?: string, withSignerIfPossible = true) {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<EnsPublicResolver | null>(null);
const { data: signer } = useSigner();
const { provider } = useRainbow();
const signerOrProvider = useMemo(() => {
return withSignerIfPossible === true ? (signer as Signer) : provider;
}, [provider, signer, withSignerIfPossible]);

useEffect(() => {
if (!provider || !ensName) {
setIsLoading(false);
setData(null);
return;
}

provider
.getResolver(ensName)
.then((nextResolver) => {
setData(
nextResolver !== null ? EnsPublicResolver__factory.connect(nextResolver.address, signerOrProvider) : null
);
})
.catch((error) => {
console.error(error);
setData(null);
})
.finally(() => {
setIsLoading(false);
});
}, [ensName, provider, signerOrProvider]);

return {
isLoading,
data,
};
}

/**
* Returns a ENS Public Resolver contract instance
* @param withSignerIfPossible Whether to use a signer if one is available
* @returns The ENS Public Resolver contract instance
*/
export function useENSPublicResolverContract(withSignerIfPossible = true): EnsPublicResolver | null {
const { chainId, provider } = useRainbow();
const { data: signer } = useSigner();

if (chainId && PUBLIC_RESOLVER_ADDRESSES[chainId] !== undefined) {
return EnsPublicResolver__factory.connect(
PUBLIC_RESOLVER_ADDRESSES[chainId],
withSignerIfPossible === true ? (signer as Signer) : provider
);
}

return null;
}
25 changes: 0 additions & 25 deletions src/hooks/useENSPublicResolverContract.ts

This file was deleted.

Loading

0 comments on commit fdfa5c9

Please sign in to comment.