Skip to content

Commit 9d16067

Browse files
committed
feat(wallet): add DRep key support and enhance wallet management
- Introduced `signersDRepKeys` to the NewWallet model and updated related components to handle DRep keys. - Enhanced wallet creation and management flows to include DRep key handling. - Updated API endpoints to support DRep key storage and retrieval. - Improved UI components to display DRep key information and validation messages. - Refactored error handling in wallet operations to manage account changes effectively.
1 parent 914a4ac commit 9d16067

File tree

32 files changed

+2081
-844
lines changed

32 files changed

+2081
-844
lines changed

package-lock.json

Lines changed: 820 additions & 415 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
"@auth/prisma-adapter": "^1.6.0",
2525
"@hookform/resolvers": "^3.9.0",
2626
"@jinglescode/nostr-chat-plugin": "^0.0.11",
27-
"@meshsdk/core": "^1.9.0-beta.18",
28-
"@meshsdk/core-csl": "^1.9.0-beta.18",
29-
"@meshsdk/core-cst": "^1.9.0-beta.19",
30-
"@meshsdk/react": "^1.9.0-beta.69",
27+
"@meshsdk/core": "^1.9.0-beta.77",
28+
"@meshsdk/core-csl": "^1.9.0-beta.77",
29+
"@meshsdk/core-cst": "^1.9.0-beta.77",
30+
"@meshsdk/react": "^1.9.0-beta.77",
3131
"@octokit/core": "^6.1.2",
3232
"@prisma/client": "^6.4.1",
3333
"@radix-ui/react-accordion": "^1.2.0",
@@ -60,14 +60,14 @@
6060
"clsx": "^2.1.1",
6161
"cors": "^2.8.5",
6262
"dexie": "^4.0.11",
63-
"formidable": "^3.5.2",
63+
"formidable": "^3.5.4",
6464
"framer-motion": "^11.11.9",
6565
"geist": "^1.3.0",
6666
"idb-keyval": "^6.2.1",
6767
"jsonld": "^8.3.3",
6868
"jsonwebtoken": "^9.0.2",
6969
"lucide-react": "^0.439.0",
70-
"next": "^14.2.4",
70+
"next": "^15.2.4",
7171
"next-auth": "^4.24.7",
7272
"papaparse": "^5.5.3",
7373
"react": "^18.3.1",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "NewWallet" ADD COLUMN "signersDRepKeys" TEXT[];

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ model NewWallet {
7272
description String?
7373
signersAddresses String[]
7474
signersStakeKeys String[]
75+
signersDRepKeys String[]
7576
signersDescriptions String[]
7677
numRequiredSigners Int?
7778
ownerAddress String

src/components/common/card-content.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function CardUI({
1717
headerDom?: ReactNode;
1818
}) {
1919
return (
20-
<Card className={`self-start ${cardClassName}`}>
20+
<Card className={`w-full max-w-4xl ${cardClassName}`}>
2121
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
2222
<CardTitle className="text-xl font-medium">{title}</CardTitle>
2323
{headerDom && headerDom}
@@ -33,7 +33,7 @@ export default function CardUI({
3333
</>
3434
)}
3535
</CardHeader>
36-
<CardContent>
36+
<CardContent className="overflow-y-auto max-h-[calc(100vh-200px)]">
3737
<div className="mt-1 flex flex-col gap-2">
3838
{description && (
3939
<p className="text-sm text-muted-foreground">{description}</p>

src/components/common/overall-layout/layout.tsx

Lines changed: 101 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,33 @@ import {
3535
BreadcrumbSeparator,
3636
} from "@/components/ui/breadcrumb";
3737

38-
// Simple error boundary component
39-
class SimpleErrorBoundary extends Component<
38+
// Enhanced error boundary component for wallet errors
39+
class WalletErrorBoundary extends Component<
4040
{ children: ReactNode; fallback: ReactNode },
41-
{ hasError: boolean }
41+
{ hasError: boolean; error: Error | null }
4242
> {
4343
constructor(props: { children: ReactNode; fallback: ReactNode }) {
4444
super(props);
45-
this.state = { hasError: false };
45+
this.state = { hasError: false, error: null };
4646
}
4747

48-
static getDerivedStateFromError() {
49-
return { hasError: true };
48+
static getDerivedStateFromError(error: Error) {
49+
return { hasError: true, error };
5050
}
5151

5252
componentDidCatch(error: Error, errorInfo: any) {
53-
console.error('Error caught by boundary:', error, errorInfo);
53+
console.error('Error caught by wallet boundary:', error, errorInfo);
54+
55+
// Handle specific wallet errors
56+
if (error.message.includes("account changed")) {
57+
console.log("Wallet account changed error caught by boundary, reloading page...");
58+
window.location.reload();
59+
return;
60+
}
5461
}
5562

5663
render() {
57-
if (this.state.hasError) {
64+
if (this.state.hasError && this.state.error && !this.state.error.message.includes("account changed")) {
5865
return this.props.fallback;
5966
}
6067
return this.props.children;
@@ -75,6 +82,30 @@ export default function RootLayout({
7582
const userAddress = useUserStore((state) => state.userAddress);
7683
const setUserAddress = useUserStore((state) => state.setUserAddress);
7784

85+
// Global error handler for unhandled promise rejections
86+
useEffect(() => {
87+
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
88+
console.error('Unhandled promise rejection:', event.reason);
89+
90+
// Handle wallet-related errors specifically
91+
if (event.reason && typeof event.reason === 'object') {
92+
const error = event.reason as Error;
93+
if (error.message && error.message.includes("account changed")) {
94+
console.log("Account changed error caught by global handler, reloading page...");
95+
event.preventDefault(); // Prevent the error from being logged to console
96+
window.location.reload();
97+
return;
98+
}
99+
}
100+
};
101+
102+
window.addEventListener('unhandledrejection', handleUnhandledRejection);
103+
104+
return () => {
105+
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
106+
};
107+
}, []);
108+
78109
const { mutate: createUser } = api.user.createUser.useMutation({
79110
onError: (e) => console.error(e),
80111
});
@@ -87,56 +118,57 @@ export default function RootLayout({
87118
(async () => {
88119
if (!connected || !wallet) return;
89120

90-
// 1) Set user address in store
91-
let address = (await wallet.getUsedAddresses())[0];
92-
if (!address) address = (await wallet.getUnusedAddresses())[0];
93-
if (address) setUserAddress(address);
121+
try {
122+
// 1) Set user address in store
123+
let address = (await wallet.getUsedAddresses())[0];
124+
if (!address) address = (await wallet.getUnusedAddresses())[0];
125+
if (address) setUserAddress(address);
94126

95-
// 2) Get stake address
96-
const stakeAddress = (await wallet.getRewardAddresses())[0];
97-
if (!stakeAddress || !address) {
98-
console.error("No stake address or payment address found");
99-
return;
100-
}
127+
// 2) Get stake address
128+
const stakeAddress = (await wallet.getRewardAddresses())[0];
129+
if (!stakeAddress || !address) {
130+
console.error("No stake address or payment address found");
131+
return;
132+
}
101133

102-
// 3) Get DRep key hash
103-
const dRepKey = await wallet.getDRep();
104-
if (!dRepKey) {
105-
console.error("No DRep key found");
106-
return;
107-
}
108-
const drepKeyHash = dRepKey.publicKeyHash;
109-
if (!drepKeyHash) {
110-
console.error("No DRep key hash found:", drepKeyHash);
111-
return;
112-
}
113-
114-
// 4) If user doesn't exist create it
115-
if (!isLoading && user === null) {
116-
const nostrKey = generateNsec();
117-
createUser({
118-
address,
119-
stakeAddress,
120-
drepKeyHash,
121-
nostrKey: JSON.stringify(nostrKey),
122-
});
123-
}
134+
// 3) Get DRep key hash (optional)
135+
let drepKeyHash = "";
136+
try {
137+
const dRepKey = await wallet.getDRep();
138+
console.log("DRep key:", dRepKey);
139+
if (dRepKey && dRepKey.publicKeyHash) {
140+
drepKeyHash = dRepKey.publicKeyHash;
141+
}
142+
} catch (error) {
143+
// DRep key is optional, so we continue without it
144+
console.log("No DRep key available for this wallet");
145+
}
124146

125-
// 5) If user exists but missing fields, update it
126-
if (
127-
!isLoading &&
128-
user &&
129-
user !== null &&
130-
(user.stakeAddress !== stakeAddress || user.drepKeyHash !== drepKeyHash)
131-
) {
132-
updateUser({
133-
address,
134-
stakeAddress,
135-
drepKeyHash,
136-
});
147+
// 4) Create or update user (upsert pattern handles both cases)
148+
if (!isLoading) {
149+
const nostrKey = generateNsec();
150+
createUser({
151+
address,
152+
stakeAddress,
153+
drepKeyHash,
154+
nostrKey: JSON.stringify(nostrKey),
155+
});
156+
}
157+
} catch (error) {
158+
console.error("Error in wallet initialization effect:", error);
159+
160+
// If we get an "account changed" error, reload the page
161+
if (error instanceof Error && error.message.includes("account changed")) {
162+
console.log("Account changed detected, reloading page...");
163+
window.location.reload();
164+
return;
165+
}
166+
167+
// For other errors, don't throw to prevent app crash
168+
// The user can retry by reconnecting their wallet
137169
}
138170
})();
139-
}, [connected, wallet, user, isLoading, generateNsec, setUserAddress]);
171+
}, [connected, wallet, user, isLoading, createUser, generateNsec, setUserAddress]);
140172

141173
const isWalletPath = router.pathname.includes("/wallets/[wallet]");
142174
const walletPageRoute = router.pathname.split("/wallets/[wallet]/")[1];
@@ -268,7 +300,22 @@ export default function RootLayout({
268300
</header>
269301

270302
<main className="relative flex flex-1 flex-col gap-4 overflow-y-auto overflow-x-hidden p-4 md:p-8">
271-
{pageIsPublic || userAddress ? children : <PageHomepage />}
303+
<WalletErrorBoundary
304+
fallback={
305+
<div className="flex flex-col items-center justify-center h-full">
306+
<h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
307+
<p className="text-gray-600 mb-4">Please try refreshing the page or reconnecting your wallet.</p>
308+
<button
309+
onClick={() => window.location.reload()}
310+
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
311+
>
312+
Refresh Page
313+
</button>
314+
</div>
315+
}
316+
>
317+
{pageIsPublic || userAddress ? children : <PageHomepage />}
318+
</WalletErrorBoundary>
272319
</main>
273320
</div>
274321
</div>

src/components/common/overall-layout/mobile-wrappers/user-dropdown-wrapper.tsx

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,28 +51,77 @@ export default function UserDropDownWrapper({
5151
});
5252

5353
async function unlinkDiscord(): Promise<void> {
54-
const address = (await wallet.getUsedAddresses())[0];
55-
unlinkDiscordMutation.mutate({ address: address ?? "" });
56-
setOpen(false);
54+
try {
55+
const usedAddresses = await wallet.getUsedAddresses();
56+
const address = usedAddresses[0];
57+
unlinkDiscordMutation.mutate({ address: address ?? "" });
58+
setOpen(false);
59+
} catch (error) {
60+
console.error("Error getting wallet address for Discord unlink:", error);
61+
if (error instanceof Error && error.message.includes("account changed")) {
62+
console.log("Account changed during Discord unlink, aborting");
63+
return;
64+
}
65+
// Show error to user
66+
toast({
67+
title: "Error",
68+
description: "Failed to get wallet address",
69+
variant: "destructive",
70+
duration: 3000,
71+
});
72+
}
5773
}
5874

5975
const { data: discordData } = api.user.getUserDiscordId.useQuery({
6076
address: userAddress ?? "",
6177
});
6278

6379
async function handleCopyAddress() {
64-
let userAddress = (await wallet.getUsedAddresses())[0];
65-
if (userAddress === undefined) {
66-
userAddress = (await wallet.getUnusedAddresses())[0];
80+
try {
81+
let userAddress: string | undefined;
82+
try {
83+
const usedAddresses = await wallet.getUsedAddresses();
84+
userAddress = usedAddresses[0];
85+
} catch (error) {
86+
if (error instanceof Error && error.message.includes("account changed")) {
87+
console.log("Account changed during address copy, aborting");
88+
return;
89+
}
90+
throw error;
91+
}
92+
93+
if (userAddress === undefined) {
94+
try {
95+
const unusedAddresses = await wallet.getUnusedAddresses();
96+
userAddress = unusedAddresses[0];
97+
} catch (error) {
98+
if (error instanceof Error && error.message.includes("account changed")) {
99+
console.log("Account changed during unused address retrieval, aborting");
100+
return;
101+
}
102+
throw error;
103+
}
104+
}
105+
106+
if (userAddress) {
107+
navigator.clipboard.writeText(userAddress);
108+
toast({
109+
title: "Copied",
110+
description: "Address copied to clipboard",
111+
duration: 5000,
112+
});
113+
setOpen(false);
114+
if (onAction) onAction();
115+
}
116+
} catch (error) {
117+
console.error("Error copying wallet address:", error);
118+
toast({
119+
title: "Error",
120+
description: "Failed to copy address",
121+
variant: "destructive",
122+
duration: 3000,
123+
});
67124
}
68-
navigator.clipboard.writeText(userAddress!);
69-
toast({
70-
title: "Copied",
71-
description: "Address copied to clipboard",
72-
duration: 5000,
73-
});
74-
setOpen(false);
75-
if (onAction) onAction();
76125
}
77126

78127
function handleLogout() {

0 commit comments

Comments
 (0)