Skip to content
71 changes: 67 additions & 4 deletions apps/web/src/hooks/useRegisterNameCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ 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 { encodePacked, getFunctionSelector, keccak256, type AbiFunction } from 'viem';
import { usePublicClient, useSignMessage } from 'wagmi';

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

export function useRegisterNameCallback(
Expand All @@ -50,6 +57,8 @@ export function useRegisterNameCallback(
const { paymasterService: paymasterServiceEnabled } = useCapabilitiesSafe({
chainId: basenameChain.id,
});
const publicClient = usePublicClient({ 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,11 @@ export function useRegisterNameCallback(
);

const [reverseRecord, setReverseRecord] = useState<boolean>(!hasExistingBasename);
const [reverseSigPayload, setReverseSigPayload] = useState<{
coinTypes: readonly bigint[];
signatureExpiry: bigint;
signature: `0x${string}`;
} | null>(null);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This useState feels unnecessary since these values are only computed in registerName and don't depend on the react lifecycle

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I switched this to use a ref, which from what I understand helps persist a value across renders though doesn't trigger a re-render if the value changes. Are you thinking we don't even need a ref because the signature will be used immediately upon its creation?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Are you thinking we don't even need a ref because the signature will be used immediately upon its creation?

Yes exactly, we don't need these values to persist across renders since the transaction using the signature is initiated in the same function (registerName)

Also, it's generally safer to re-request the signature every time register is clicked because there's currently no cache invalidation implemented. So a flow like this could happen:

  1. User clicks register
  2. User signs the message
  3. User cancels the transaction
  4. User disconnects and connects a different wallet
  5. User clicks register
  6. This time there's no signature request and the transaction re-uses the wrong signature from the last wallet

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🧠


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

const prepareReverseRecordSignature = useCallback(async () => {
if (!address) throw new Error('No address');
if (!publicClient) throw new Error('No public client');

// Use the L2 Reverse Registrar address configured for this chain.
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 selector = getFunctionSelector(functionAbi);

const signatureExpiry = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);
const coinTypes = [BigInt(convertChainIdToCoinTypeUint(basenameChain.id))] as const;
const fullName = formatBaseEthDomain(name, basenameChain.id);

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

const digest = keccak256(preimage);
const signature = await signMessageAsync({ message: { raw: digest } });

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

// Callback
const registerName = useCallback(async () => {
if (!address) return;
Expand Down Expand Up @@ -114,16 +157,35 @@ export function useRegisterNameCallback(
],
});

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

if (!paymasterServiceEnabled && reverseRecord) {
const payload =
reverseSigPayload ??
(await prepareReverseRecordSignature().catch((e) => {
logError(e, 'Reverse record signature step failed');
return null;
}));
if (!payload) return;
coinTypesForRequest = payload.coinTypes;
signatureExpiryForRequest = payload.signatureExpiry;
signatureForRequest = payload.signature;
}

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 Down Expand Up @@ -191,5 +253,6 @@ export function useRegisterNameCallback(
hasExistingBasename,
batchCallsStatus,
registerNameStatus,
prepareReverseRecordSignature,
};
}
Loading