Skip to content

Commit 3df8c01

Browse files
feat(BA-2555): Set primary flow for EOAs in Web (#2615)
* Update registration flow for SCWs for ensip 19 * first stab at EOA updates for registration flow * Add contract addresses * works with EOA for regular registration and set-primary * some fixes after rebase * review comments * second round review comments * initial cursor stab at including setReverseRecord stuff * remove unused import * use utility for constructing signature digest * clean up * fix rendering on success * set error for potential downstream use if errors during setPrimary flow * remove unused refetchPrimaryUsername * fix build * add spinner to primary badge * simplify spinner * remove context and use state for primary spinner * simplify spinner logic --------- Co-authored-by: Arjun Dureja <arjun.dureja@coinbase.com>
1 parent 7f18f38 commit 3df8c01

File tree

3 files changed

+74
-41
lines changed

3 files changed

+74
-41
lines changed

apps/web/src/components/Basenames/ManageNames/NameDisplay.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ export default function NameDisplay({
5959
const expirationText = formatDistanceToNow(parseISO(expiresAt), { addSuffix: true });
6060
const name = domain.split('.')[0];
6161

62-
const { setPrimaryUsername } = useUpdatePrimaryName(domain as Basename);
62+
const { removeNameFromUI } = useRemoveNameFromUI();
63+
const { setPrimaryUsername, isPending } = useUpdatePrimaryName(domain as Basename);
6364

6465
// Transfer state and callbacks
6566
const [isTransferModalOpen, setIsTransferModalOpen] = useState<boolean>(false);
6667
const openTransferModal = useCallback(() => setIsTransferModalOpen(true), []);
6768
const closeTransferModal = useCallback(() => setIsTransferModalOpen(false), []);
68-
const { removeNameFromUI } = useRemoveNameFromUI(domain as Basename);
6969

7070
// Renewal state and callbacks
7171
const [isRenewalModalOpen, setIsRenewalModalOpen] = useState<boolean>(false);
@@ -102,7 +102,12 @@ export default function NameDisplay({
102102
</Link>
103103
<div className="flex items-center gap-2">
104104
{isPrimary && (
105-
<span className="rounded-full bg-white px-2 py-1 text-sm text-black">Primary</span>
105+
<span className="flex items-center gap-2 rounded-full bg-white px-2 py-1 text-sm text-black">
106+
{isPending ? (
107+
<Icon name="spinner" color="currentColor" width="12px" height="12px" />
108+
) : null}
109+
<span>Primary</span>
110+
</span>
106111
)}
107112
<Dropdown>
108113
<DropdownToggle>

apps/web/src/components/Basenames/ManageNames/hooks.tsx

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export function useNameList() {
4343
}
4444
},
4545
enabled: !!address,
46+
placeholderData: (prev) => prev,
4647
});
4748

4849
// Navigation functions
@@ -102,21 +103,16 @@ export function useNameList() {
102103
};
103104
}
104105

105-
export function useRemoveNameFromUI(domain: Basename) {
106+
export function useRemoveNameFromUI() {
106107
const { address } = useAccount();
107108
const chainId = useChainId();
108109

109110
const network = chainId === 8453 ? 'base-mainnet' : 'base-sepolia';
110111
const queryClient = useQueryClient();
111112

112113
const removeNameFromUI = useCallback(() => {
113-
queryClient.setQueryData(
114-
['usernames', address, network],
115-
(prevData: ManagedAddressesResponse) => {
116-
return { ...prevData, data: prevData.data.filter((name) => name.domain !== domain) };
117-
},
118-
);
119-
}, [address, domain, network, queryClient]);
114+
void queryClient.invalidateQueries({ queryKey: ['usernames', address, network] });
115+
}, [address, network, queryClient]);
120116

121117
return { removeNameFromUI };
122118
}
@@ -131,38 +127,25 @@ export function useUpdatePrimaryName(domain: Basename) {
131127
const network = chainId === 8453 ? 'base-mainnet' : 'base-sepolia';
132128

133129
// Hook to update primary name
134-
const { setPrimaryName, transactionIsSuccess } = useSetPrimaryBasename({
130+
const { setPrimaryName, transactionIsSuccess, transactionPending } = useSetPrimaryBasename({
135131
secondaryUsername: domain,
136132
});
137133

138134
const setPrimaryUsername = useCallback(async () => {
139135
try {
140136
await setPrimaryName();
137+
void queryClient.invalidateQueries({ queryKey: ['usernames', address, network] });
141138
} catch (error) {
142139
logError(error, 'Failed to update primary name');
143140
throw error;
144141
}
145-
}, [logError, setPrimaryName]);
142+
}, [address, network, logError, queryClient, setPrimaryName]);
146143

147144
useEffect(() => {
148145
if (transactionIsSuccess) {
149-
queryClient.setQueryData(
150-
['usernames', address, network],
151-
(prevData: ManagedAddressesResponse) => {
152-
return {
153-
...prevData,
154-
data: prevData.data.map((name) =>
155-
name.domain === domain
156-
? { ...name, is_primary: true }
157-
: name.is_primary
158-
? { ...name, is_primary: false }
159-
: name,
160-
),
161-
};
162-
},
163-
);
146+
void queryClient.invalidateQueries({ queryKey: ['usernames', address, network] });
164147
}
165-
}, [transactionIsSuccess, address, domain, network, queryClient]);
148+
}, [transactionIsSuccess, address, network, queryClient]);
166149

167-
return { setPrimaryUsername };
150+
return { setPrimaryUsername, isPending: transactionPending, transactionIsSuccess };
168151
}

apps/web/src/hooks/useSetPrimaryBasename.ts

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,20 @@ import {
55
USERNAME_REVERSE_REGISTRAR_ADDRESSES,
66
} from 'apps/web/src/addresses/usernames';
77
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
8-
import { useCallback, useEffect } from 'react';
8+
import { useCallback, useEffect, useState } from 'react';
99
import { Basename } from '@coinbase/onchainkit/identity';
10-
import { useAccount } from 'wagmi';
10+
import { useAccount, useSignMessage } from 'wagmi';
1111
import useBaseEnsName from 'apps/web/src/hooks/useBaseEnsName';
1212
import { useErrors } from 'apps/web/contexts/Errors';
1313
import useWriteContractWithReceipt from 'apps/web/src/hooks/useWriteContractWithReceipt';
1414
import { useUsernameProfile } from 'apps/web/src/components/Basenames/UsernameProfileContext';
1515
import useWriteContractsWithLogs from 'apps/web/src/hooks/useWriteContractsWithLogs';
1616
import useCapabilitiesSafe from 'apps/web/src/hooks/useCapabilitiesSafe';
1717
import L2ReverseRegistrarAbi from 'apps/web/src/abis/L2ReverseRegistrarAbi';
18+
import UpgradeableRegistrarControllerAbi from 'apps/web/src/abis/UpgradeableRegistrarControllerAbi';
19+
import { UPGRADEABLE_REGISTRAR_CONTROLLER_ADDRESSES } from 'apps/web/src/addresses/usernames';
20+
import { type AbiFunction } from 'viem';
21+
import { buildReverseRegistrarSignatureDigest } from 'apps/web/src/utils/usernames';
1822

1923
/*
2024
A hook to set a name as primary for resolution.
@@ -38,6 +42,9 @@ export default function useSetPrimaryBasename({ secondaryUsername }: UseSetPrima
3842
const { paymasterService: paymasterServiceEnabled } = useCapabilitiesSafe({
3943
chainId: secondaryUsernameChain.id,
4044
});
45+
const { signMessageAsync } = useSignMessage();
46+
47+
const [signatureError, setSignatureError] = useState<Error | null>(null);
4148

4249
// Get current primary username
4350
// Note: This is sometimes undefined
@@ -65,6 +72,28 @@ export default function useSetPrimaryBasename({ secondaryUsername }: UseSetPrima
6572
eventName: 'update_primary_name',
6673
});
6774

75+
const signMessageForReverseRecord = useCallback(async () => {
76+
if (!address) throw new Error('No address');
77+
78+
const reverseRegistrar = USERNAME_L2_REVERSE_REGISTRAR_ADDRESSES[secondaryUsernameChain.id];
79+
const functionAbi = L2ReverseRegistrarAbi.find(
80+
(f) => f.type === 'function' && f.name === 'setNameForAddrWithSignature',
81+
) as unknown as AbiFunction;
82+
83+
const signatureExpiry = BigInt(Math.floor(Date.now() / 1000) + 5 * 60);
84+
const nameLabel = (secondaryUsername as string).split('.')[0];
85+
const { digest, coinTypes } = buildReverseRegistrarSignatureDigest({
86+
reverseRegistrar,
87+
functionAbi,
88+
address,
89+
chainId: secondaryUsernameChain.id,
90+
name: nameLabel,
91+
signatureExpiry,
92+
});
93+
const signature = await signMessageAsync({ message: { raw: digest } });
94+
return { coinTypes, signatureExpiry, signature } as const;
95+
}, [address, secondaryUsername, secondaryUsernameChain.id, signMessageAsync]);
96+
6897
useEffect(() => {
6998
if (transactionIsSuccess) {
7099
refetchPrimaryUsername()
@@ -82,16 +111,28 @@ export default function useSetPrimaryBasename({ secondaryUsername }: UseSetPrima
82111

83112
try {
84113
if (!paymasterServiceEnabled) {
114+
let payload: {
115+
coinTypes: readonly bigint[];
116+
signatureExpiry: bigint;
117+
signature: `0x${string}`;
118+
};
119+
try {
120+
setSignatureError(null);
121+
payload = await signMessageForReverseRecord();
122+
} catch (e) {
123+
logError(e, 'Reverse record signature step failed');
124+
const msg = e instanceof Error && e.message ? e.message : 'Unknown error';
125+
setSignatureError(new Error(`Could not prepare reverse record signature: ${msg}`));
126+
return undefined;
127+
}
128+
129+
const nameLabel = (secondaryUsername as string).split('.')[0];
130+
85131
await initiateTransaction({
86-
abi: ReverseRegistrarAbi,
87-
address: USERNAME_REVERSE_REGISTRAR_ADDRESSES[secondaryUsernameChain.id],
88-
args: [
89-
address,
90-
address,
91-
USERNAME_L2_RESOLVER_ADDRESSES[secondaryUsernameChain.id],
92-
secondaryUsername,
93-
],
94-
functionName: 'setNameForAddr',
132+
abi: UpgradeableRegistrarControllerAbi,
133+
address: UPGRADEABLE_REGISTRAR_CONTROLLER_ADDRESSES[secondaryUsernameChain.id],
134+
args: [nameLabel, payload.signatureExpiry, payload.coinTypes, payload.signature],
135+
functionName: 'setReverseRecord',
95136
});
96137
} else {
97138
await initiateBatchCalls({
@@ -130,7 +171,9 @@ export default function useSetPrimaryBasename({ secondaryUsername }: UseSetPrima
130171
address,
131172
paymasterServiceEnabled,
132173
initiateTransaction,
174+
secondaryUsernameChain,
133175
secondaryUsernameChain.id,
176+
signMessageForReverseRecord,
134177
initiateBatchCalls,
135178
logError,
136179
]);
@@ -146,5 +189,7 @@ export default function useSetPrimaryBasename({ secondaryUsername }: UseSetPrima
146189
canSetUsernameAsPrimary,
147190
isLoading,
148191
transactionIsSuccess: transactionIsSuccess || batchCallsIsSuccess,
192+
transactionPending: transactionIsLoading || batchCallsIsLoading,
193+
error: signatureError,
149194
};
150195
}

0 commit comments

Comments
 (0)