Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a5b5b73
refactor: audit-driven surgical cleanup (dead code, React keys, CI, s…
jinglescode May 1, 2026
5777084
chore(ci): gate typecheck/test/build; add dependabot
jinglescode May 10, 2026
ed321fe
fix(build): lazy-init mainnet provider; remove global sideEffects:false
jinglescode May 10, 2026
074ac9a
feat(server): AuditLog table, DB indexes, security hardening, observa…
jinglescode May 10, 2026
749c593
refactor(wallet-flow): stable signerIds for React keys; tripwire test
jinglescode May 10, 2026
c9fb61b
chore(ui): a11y skip-link, useMemo transaction parse, cleanup deletions
jinglescode May 10, 2026
a634525
fix(wallet-assets): show actual IPFS NFT image instead of hardcoded CID
jinglescode May 10, 2026
f6d9cfe
chore(proxy): remove three write-only useState declarations
jinglescode May 10, 2026
9e4eed1
Merge pull request #236 from MeshJS/fix/build-ssr-provider
jinglescode May 10, 2026
56ed3b4
Merge pull request #237 from MeshJS/feat/server-hardening-and-audit-log
jinglescode May 10, 2026
8303f89
Merge pull request #235 from MeshJS/chore/ci-and-deps
jinglescode May 10, 2026
94cd08e
Merge pull request #247 from MeshJS/refactor/wallet-flow-shared
jinglescode May 10, 2026
c376851
Merge pull request #249 from MeshJS/chore/ui-cleanup-and-hooks
jinglescode May 10, 2026
4ff1d0a
Merge pull request #240 from MeshJS/fix/wallet-assets-ipfs-image
jinglescode May 10, 2026
2d8b5ef
Merge pull request #241 from MeshJS/chore/remove-dead-proxy-state
jinglescode May 10, 2026
39a0c9b
chore: remove Nostr chat system
jinglescode May 10, 2026
80fbe91
Merge pull request #253 from MeshJS/chore/remove-chat-system
jinglescode May 10, 2026
6e92e56
Merge remote-tracking branch 'origin/main' into HEAD
QSchlegel May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ A comprehensive, enterprise-grade multi-signature wallet solution built on Carda
- Secure multi-sig staking operations

### Collaboration
- Real-time Nostr-based chat
- Discord integration for notifications
- Signer verification via message signing
- Automated transaction alerts
Expand Down Expand Up @@ -188,11 +187,11 @@ graph TD
### Database Schema
```prisma
model User {
id String @id @default(cuid())
address String @unique
stakeAddress String @unique
nostrKey String @unique
discordId String @default("")
id String @id @default(cuid())
address String @unique
stakeAddress String @unique
nostrKey String? @unique
discordId String @default("")
}

model Wallet {
Expand Down
27 changes: 24 additions & 3 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,16 @@ const config = {
layers: true,
};

// Optimize tree-shaking by ensuring proper module resolution
// Optimize tree-shaking by ensuring proper module resolution.
// Note: do NOT set `sideEffects: false` globally — it tells webpack that
// every file is side-effect-free, which silently strips CSS imports,
// polyfills, and other modules that exist purely for their side effects.
// Per-package sideEffects flags in package.json are the correct surface.
config.optimization = {
...config.optimization,
usedExports: true,
sideEffects: false,
};

// Handle CommonJS modules that don't support named exports
config.resolve = {
...config.resolve,
Expand All @@ -75,6 +78,24 @@ const config = {

// External packages for server components to avoid bundling issues
serverExternalPackages: ["@fabianbormann/cardano-peer-connect"],

// Basic security headers applied to all routes.
// NOTE: Content-Security-Policy and Strict-Transport-Security are intentionally
// omitted — CSP would break inline scripts/styles and HSTS locks browsers to
// HTTPS for max-age and should only be enabled after team review.
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
],
},
];
},
};

// Bundle analyzer - only enable when ANALYZE env var is set
Expand Down
59 changes: 44 additions & 15 deletions src/__tests__/pendingTransactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,73 @@ import type { NextApiRequest, NextApiResponse } from 'next';
const addCorsCacheBustingHeadersMock = jest.fn<(res: NextApiResponse) => void>();
const corsMock = jest.fn<(req: NextApiRequest, res: NextApiResponse) => Promise<void>>();

jest.mock(
jest.unstable_mockModule(
'@/lib/cors',
() => ({
__esModule: true,
addCorsCacheBustingHeaders: addCorsCacheBustingHeadersMock,
cors: corsMock,
}),
{ virtual: true },
);

const verifyJwtMock = jest.fn<(token: string | undefined) => { address: string } | null>();
type JwtPayloadLike = { address: string; botId?: string; type?: string };
const verifyJwtMock = jest.fn<(token: string | undefined) => JwtPayloadLike | null>();
const isBotJwtMock = jest.fn<(payload: JwtPayloadLike) => boolean>(
(payload) => Boolean(payload && (payload as JwtPayloadLike).type === "bot"),
);

jest.mock(
jest.unstable_mockModule(
'@/lib/verifyJwt',
() => ({
__esModule: true,
verifyJwt: verifyJwtMock,
isBotJwt: isBotJwtMock,
}),
);

const applyRateLimitMock = jest.fn<
(req: NextApiRequest, res: NextApiResponse, options?: unknown) => boolean
>(() => true);
const applyBotRateLimitMock = jest.fn<
(req: NextApiRequest, res: NextApiResponse, botId: string, maxRequests?: number) => boolean
>(() => true);
const applyStrictRateLimitMock = jest.fn<
(req: NextApiRequest, res: NextApiResponse, options?: unknown) => boolean
>(() => true);
const enforceBodySizeMock = jest.fn<
(req: NextApiRequest, res: NextApiResponse, maxBytes: number) => boolean
>(() => true);

jest.unstable_mockModule(
'@/lib/security/requestGuards',
() => ({
__esModule: true,
applyRateLimit: applyRateLimitMock,
applyBotRateLimit: applyBotRateLimitMock,
applyStrictRateLimit: applyStrictRateLimitMock,
enforceBodySize: enforceBodySizeMock,
isBodyTooLarge: jest.fn(() => false),
}),
{ virtual: true },
);

const createCallerMock = jest.fn();

jest.mock(
jest.unstable_mockModule(
'@/server/api/root',
() => ({
__esModule: true,
createCaller: createCallerMock,
}),
{ virtual: true },
);

const dbMock = { __type: 'dbMock' };

jest.mock(
jest.unstable_mockModule(
'@/server/db',
() => ({
__esModule: true,
db: dbMock,
}),
{ virtual: true },
);

type ResponseMock = NextApiResponse & { statusCode?: number };
Expand Down Expand Up @@ -164,13 +190,16 @@ describe('pendingTransactions API route', () => {
expect(addCorsCacheBustingHeadersMock).toHaveBeenCalledWith(res);
expect(corsMock).toHaveBeenCalledWith(req, res);
expect(verifyJwtMock).toHaveBeenCalledWith(token);
expect(createCallerMock).toHaveBeenCalledWith({
db: dbMock,
session: expect.objectContaining({
user: { id: address },
expires: expect.any(String),
expect(createCallerMock).toHaveBeenCalledWith(
expect.objectContaining({
db: dbMock,
session: expect.objectContaining({
user: { id: address },
expires: expect.any(String),
}),
sessionAddress: address,
}),
});
);
expect(walletGetWalletMock).toHaveBeenCalledWith({ walletId, address });
expect(transactionGetPendingTransactionsMock).toHaveBeenCalledWith({ walletId });
expect(res.status).toHaveBeenCalledWith(200);
Expand Down
33 changes: 32 additions & 1 deletion src/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Test setup file for Jest
import '@jest/globals';
import { jest, beforeEach, afterEach } from '@jest/globals';

// Mock console methods to reduce noise in tests
global.console = {
Expand All @@ -14,3 +14,34 @@ global.console = {

// Global test timeout
jest.setTimeout(10000);

// Determinism: freeze the wall clock (`Date.now` / `new Date()`) so tests are
// byte-identical across runs. Timer APIs (setTimeout/setInterval/etc) stay
// real — many tests in this suite hit tRPC's timing middleware and other
// real-async paths that hang under faked timers. Tests that specifically
// exercise timer behavior can opt in via `jest.useFakeTimers()` in `beforeAll`.
beforeEach(() => {
jest.useFakeTimers({
now: new Date('2026-01-01T00:00:00Z'),
doNotFake: [
'nextTick',
'setImmediate',
'clearImmediate',
'queueMicrotask',
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'requestAnimationFrame',
'cancelAnimationFrame',
'requestIdleCallback',
'cancelIdleCallback',
'hrtime',
'performance',
],
});
});

afterEach(() => {
jest.useRealTimers();
});
1 change: 1 addition & 0 deletions src/components/common/ImgDragAndDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export default function ImgDragAndDrop({ onImageUpload, initialUrl }: ImgDragAnd
</div>
<Input
type="url"
aria-label="Image URL"
className="w-full rounded-md border border-gray-300 p-2"
placeholder="Enter your own image URL..."
value={filePath}
Expand Down
2 changes: 0 additions & 2 deletions src/components/common/MeshProviderClient.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client";

import "@meshsdk/react/styles.css";
import { MeshProvider } from "@meshsdk/react";

Expand Down
6 changes: 1 addition & 5 deletions src/components/common/cardano-objects/connect-wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
} from "@meshsdk/core";
import useUTXOS from "@/hooks/useUTXOS";
import { api } from "@/utils/api";
import { useNostrChat } from "@jinglescode/nostr-chat-plugin";
import { useWalletContext, WalletState } from "@/hooks/useWalletContext";
import { useToast } from "@/hooks/use-toast";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -103,7 +102,6 @@ function ConnectWalletContent({
(state) => state.setUserAssetMetadata,
);
const { user, isLoading: isUserLoading } = useUser();
const { generateNsec } = useNostrChat();
const userAddress = useUserStore((state) => state.userAddress);
const setUserAddress = useUserStore((state) => state.setUserAddress);
const { toast } = useToast();
Expand Down Expand Up @@ -472,12 +470,10 @@ function ConnectWalletContent({

// 4) Create or update user (same as normal wallet)
if (!isUserLoading) {
const nostrKey = generateNsec();
createUser({
address,
stakeAddress,
drepKeyHash,
nostrKey: JSON.stringify(nostrKey),
});
}

Expand All @@ -487,7 +483,7 @@ function ConnectWalletContent({
utxosInitializedRef.current = false;
}
})();
}, [isUtxosEnabled, utxosWallet, isUserLoading, createUser, generateNsec, setUserAddress, netId]);
}, [isUtxosEnabled, utxosWallet, isUserLoading, createUser, setUserAddress, netId]);

// Handle UTXOS wallet assets and network
useEffect(() => {
Expand Down
12 changes: 9 additions & 3 deletions src/components/common/cardano-objects/resolve-adahandle.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { toast } from "@/hooks/use-toast";
import { getProvider } from "@/utils/get-provider";

//AdaHandle look up provider only supports mainnnet
const provider = getProvider(1)
// AdaHandle lookup is mainnet-only. Lazy-init the provider so the module
// can be imported during SSR / page-data collection without requiring
// NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET to be defined at module-load time.
let cachedProvider: ReturnType<typeof getProvider> | null = null;
const getMainnetProvider = () => {
if (!cachedProvider) cachedProvider = getProvider(1);
return cachedProvider;
};

export const resolveAdaHandle = async (
setAdaHandle: (value: string) => void,
Expand All @@ -18,7 +24,7 @@ export const resolveAdaHandle = async (
return;
}

const address = await provider.fetchHandleAddress(handleName);
const address = await getMainnetProvider().fetchHandleAddress(handleName);

if (address) {
const newAddresses = [...recipientAddresses];
Expand Down
67 changes: 13 additions & 54 deletions src/components/common/overall-layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,56 +93,6 @@ class WalletErrorBoundary extends Component<
}
}

// Component to track layout content changes
function LayoutContentTracker({
children,
router,
pageIsPublic,
userAddress
}: {
children: ReactNode;
router: ReturnType<typeof useRouter>;
pageIsPublic: boolean;
userAddress: string | undefined;
}) {
const prevPathRef = useRef<string>(router.pathname);
const prevQueryRef = useRef<string>(JSON.stringify(router.query));

useEffect(() => {
const handleRouteChangeStart = (url: string) => {
// Route change started
};

const handleRouteChangeComplete = (url: string) => {
prevPathRef.current = router.pathname;
prevQueryRef.current = JSON.stringify(router.query);
};

const handleRouteChangeError = (err: Error, url: string) => {
// Route change error
};

router.events.on('routeChangeStart', handleRouteChangeStart);
router.events.on('routeChangeComplete', handleRouteChangeComplete);
router.events.on('routeChangeError', handleRouteChangeError);

return () => {
router.events.off('routeChangeStart', handleRouteChangeStart);
router.events.off('routeChangeComplete', handleRouteChangeComplete);
router.events.off('routeChangeError', handleRouteChangeError);
};
}, [router]);

useEffect(() => {
if (router.pathname !== prevPathRef.current || JSON.stringify(router.query) !== prevQueryRef.current) {
prevPathRef.current = router.pathname;
prevQueryRef.current = JSON.stringify(router.query);
}
}, [router.pathname, router.query]);

return <>{children}</>;
}

export default function RootLayout({
children,
}: {
Expand Down Expand Up @@ -547,6 +497,13 @@ export default function RootLayout({

return (
<div className="flex h-screen w-screen flex-col overflow-hidden">
{/* Skip link for keyboard users */}
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[200] focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground focus:shadow-lg focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
Skip to main content
</a>
{(shouldShowBackgroundLoading || showPostAuthLoading) && (
<div className="fixed inset-0 z-50 transition-opacity duration-300 ease-in-out opacity-100">
<Loading />
Expand Down Expand Up @@ -672,7 +629,11 @@ export default function RootLayout({
)}

{/* Main content */}
<main className="relative flex flex-1 flex-col gap-4 overflow-y-auto overflow-x-hidden p-4 md:p-8">
<main
id="main-content"
tabIndex={-1}
className="relative flex flex-1 flex-col gap-4 overflow-y-auto overflow-x-hidden p-4 md:p-8 focus:outline-none"
>
<WalletErrorBoundary
fallback={
<div className="flex flex-col items-center justify-center h-full min-h-[400px] px-4">
Expand Down Expand Up @@ -707,9 +668,7 @@ export default function RootLayout({
</div>
}
>
<LayoutContentTracker router={router} pageIsPublic={pageIsPublic} userAddress={userAddress}>
{pageIsPublic || userAddress ? children : <PageHomepage />}
</LayoutContentTracker>
{pageIsPublic || userAddress ? children : <PageHomepage />}
</WalletErrorBoundary>
</main>
</div>
Expand Down
Loading
Loading