Skip to content
83 changes: 76 additions & 7 deletions apps/web/src/hooks/useRegisterNameCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ import {
normalizeEnsDomainName,
REGISTER_CONTRACT_ABI,
REGISTER_CONTRACT_ADDRESSES,
buildReverseRegistrarSignatureDigest,
} from 'apps/web/src/utils/usernames';
import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react';
import { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react';
import { encodeFunctionData, namehash } from 'viem';
import { useAccount } from 'wagmi';
import { secondsInYears } from 'apps/web/src/utils/secondsInYears';
import L2ReverseRegistrarAbi from 'apps/web/src/abis/L2ReverseRegistrarAbi';
import { getFunctionSelector, type AbiFunction, type WriteContractParameters } from 'viem';
import { useSignMessage } from 'wagmi';

type UseRegisterNameCallbackReturnType = {
callback: () => Promise<void>;
Expand All @@ -35,6 +38,11 @@ type UseRegisterNameCallbackReturnType = {
hasExistingBasename: boolean;
batchCallsStatus: BatchCallsStatus;
registerNameStatus: WriteTransactionWithReceiptStatus;
signMessageForReverseRecord: () => Promise<{
coinTypes: readonly bigint[];
signatureExpiry: bigint;
signature: `0x${string}`;
}>;
};

export function useRegisterNameCallback(
Expand All @@ -50,6 +58,7 @@ export function useRegisterNameCallback(
const { paymasterService: paymasterServiceEnabled } = useCapabilitiesSafe({
chainId: basenameChain.id,
});
const { signMessageAsync } = useSignMessage();

// If user has a basename, reverse record is set to false
const { data: baseEnsName, isLoading: baseEnsNameIsLoading } = useBaseEnsName({
Expand All @@ -62,6 +71,12 @@ export function useRegisterNameCallback(
);

const [reverseRecord, setReverseRecord] = useState<boolean>(!hasExistingBasename);
const [signatureError, setSignatureError] = useState<Error | null>(null);
const reverseSigPayloadRef = useRef<{
coinTypes: readonly bigint[];
signatureExpiry: bigint;
signature: `0x${string}`;
} | null>(null);

// Transaction with paymaster enabled
const { initiateBatchCalls, batchCallsStatus, batchCallsIsLoading, batchCallsError } =
Expand All @@ -85,9 +100,33 @@ export function useRegisterNameCallback(
const normalizedName = normalizeEnsDomainName(name);
const isDiscounted = Boolean(discountKey && validationData);

const signMessageForReverseRecord = useCallback(async () => {
if (!address) throw new Error('No address');
const reverseRegistrar = USERNAME_L2_REVERSE_REGISTRAR_ADDRESSES[basenameChain.id];

const functionAbi = L2ReverseRegistrarAbi.find(
(f) => f.type === 'function' && f.name === 'setNameForAddrWithSignature',
) as unknown as AbiFunction;
const signatureExpiry = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);
const { digest, coinTypes } = buildReverseRegistrarSignatureDigest({
reverseRegistrar,
functionAbi,
address,
chainId: basenameChain.id,
name,
signatureExpiry,
});
const signature = await signMessageAsync({ message: { raw: digest } });

const payload = { coinTypes, signatureExpiry, signature } as const;
reverseSigPayloadRef.current = payload;
return payload;
}, [address, basenameChain.id, name, signMessageAsync]);

// Callback
const registerName = useCallback(async () => {
if (!address) return;
setSignatureError(null);

const addressData = encodeFunctionData({
abi: L2ResolverAbi,
Expand All @@ -114,16 +153,43 @@ export function useRegisterNameCallback(
],
});

let coinTypesForRequest: readonly bigint[] = [];
let signatureExpiryForRequest: bigint = 0n;
let signatureForRequest: `0x${string}` = '0x';

if (!paymasterServiceEnabled && reverseRecord) {
let payload = reverseSigPayloadRef.current;
if (!payload) {
try {
payload = await signMessageForReverseRecord();
} catch (e) {
logError(e, 'Reverse record signature step failed');
const msg = e instanceof Error && e.message ? e.message : 'Unknown error';
setSignatureError(new Error(`Could not prepare reverse record signature: ${msg}`));
return;
}
}
if (!payload) {
setSignatureError(new Error('Could not prepare reverse record signature'));
return;
}
coinTypesForRequest = payload?.coinTypes ?? [];
signatureExpiryForRequest = payload?.signatureExpiry ?? '0x';
signatureForRequest = payload?.signature ?? '0x';
}

const reverseRecordForRequest = paymasterServiceEnabled ? false : reverseRecord;

const registerRequest = {
name: normalizedName, // The name being registered.
owner: address, // The address of the owner for the name.
duration: secondsInYears(years), // The duration of the registration in seconds.
resolver: UPGRADEABLE_L2_RESOLVER_ADDRESSES[basenameChain.id], // The address of the resolver to set for this name.
data: [addressData, baseCointypeData, nameData], // Multicallable data bytes for setting records in the associated resolver upon registration.
reverseRecord, // Bool to decide whether to set this name as the "primary" name for the `owner`.
coinTypes: [],
signatureExpiry: 0,
signature: '0x',
reverseRecord: reverseRecordForRequest, // When using paymaster (atomic batch), set via separate call instead of signature flow.
coinTypes: coinTypesForRequest,
signatureExpiry: signatureExpiryForRequest,
signature: signatureForRequest,
};

try {
Expand All @@ -134,7 +200,9 @@ export function useRegisterNameCallback(
functionName: isDiscounted ? 'discountedRegister' : 'register',
args: isDiscounted ? [registerRequest, discountKey, validationData] : [registerRequest],
value,
});
chain: basenameChain,
account: address,
} as unknown as WriteContractParameters);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to pass these extra params? also is the type cast needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, this is fixing an existing linter error around value : Object literal may only specify known properties, and 'value' does not exist in type 'ContractFunctionParameters'.ts(2353)

But I'm going to revert it to how it was because it doesn't break the build and isn't explicitly necessary for this change

} else {
await initiateBatchCalls({
contracts: [
Expand Down Expand Up @@ -185,11 +253,12 @@ export function useRegisterNameCallback(
return {
callback: registerName,
isPending: registerNameIsLoading || batchCallsIsLoading,
Copy link
Collaborator

@arjun-dureja arjun-dureja Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also return a loading state from the signature request here. useSignMessage exposes a status param you can use.

const { signMessageAsync, status } = useSignMessage();
const signMessageIsLoading = status === 'pending'
...
isPending: registerNameIsLoading || batchCallsIsLoading || signMessageIsLoading,

error: registerNameError ?? batchCallsError,
error: signatureError ?? registerNameError ?? batchCallsError,
reverseRecord,
setReverseRecord,
hasExistingBasename,
batchCallsStatus,
registerNameStatus,
signMessageForReverseRecord,
};
}
30 changes: 30 additions & 0 deletions apps/web/src/utils/usernames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
createPublicClient,
http,
sha256,
getFunctionSelector,
type AbiFunction,
} from 'viem';
import { normalize } from 'viem/ens';
import L2ResolverAbi from 'apps/web/src/abis/L2Resolver';
Expand Down Expand Up @@ -366,6 +368,34 @@ export const convertChainIdToCoinTypeUint = (chainId: number): number => {
return (0x80000000 | chainId) >>> 0;
};

export function buildReverseRegistrarSignatureDigest({
reverseRegistrar,
functionAbi,
address,
chainId,
name,
signatureExpiry,
}: {
reverseRegistrar: `0x${string}`;
functionAbi: AbiFunction;
address: `0x${string}`;
chainId: number;
name: string;
signatureExpiry: bigint;
}) {
const coinTypes = [BigInt(convertChainIdToCoinTypeUint(chainId))] as const;
const fullName = formatBaseEthDomain(name, chainId);
const selector = getFunctionSelector(functionAbi);

const preimage = encodePacked(
['address', 'bytes4', 'address', 'uint256', 'string', 'uint256[]'],
[reverseRegistrar, selector, address, signatureExpiry, fullName, coinTypes],
);
const digest = keccak256(preimage);

return { digest, coinTypes, fullName } as const;
}

export const convertReverseNodeToBytes = ({
address,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
Loading