Skip to content

Commit 6db3dc0

Browse files
authored
Merge pull request #280 from MeshJS/preprod
Release: preprod → main
2 parents 566174b + 131b266 commit 6db3dc0

32 files changed

Lines changed: 1475 additions & 140 deletions

File tree

eslint.config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,23 @@ export default [
3030
},
3131
},
3232
rules: {
33+
// Guardrail: never pull `wallet` out of @meshsdk/react 2.0's useWallet().
34+
// That object is a low-level CIP-30 wallet whose signData(address, payload)
35+
// / signTx(tx, partialSign) signatures differ from the @meshsdk/core 1.9
36+
// IWallet the app is built on — a wrong-order call compiles but signs the
37+
// wrong bytes (caused VESPR CIP-30 InternalError -2 and ballot witness
38+
// divergence). Use useMeshWallet()/useActiveWallet() for any wallet ops;
39+
// useWallet() is fine for connection state only (name/connected/connect/
40+
// disconnect).
41+
"no-restricted-syntax": [
42+
"error",
43+
{
44+
selector:
45+
"VariableDeclarator[init.callee.name='useWallet'] > ObjectPattern > Property[key.name='wallet']",
46+
message:
47+
"Don't destructure `wallet` from @meshsdk/react useWallet() — its signData/signTx args differ from core 1.9 and silently sign wrong bytes. Use useMeshWallet()/useActiveWallet() instead.",
48+
},
49+
],
3350
"@typescript-eslint/array-type": "off",
3451
"@typescript-eslint/consistent-type-definitions": "off",
3552
"@typescript-eslint/consistent-type-imports": [
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, it, expect } from '@jest/globals';
2+
import { normalizeAddressToBech32 } from '../utils/addressCompatibility';
3+
4+
describe('normalizeAddressToBech32', () => {
5+
// 57-byte mainnet base address as returned hex-encoded by some CIP-30
6+
// wallets (mobile in-app browsers) from getChangeAddress/getUsedAddresses.
7+
const mainnetBaseHex =
8+
'01188691447471593ad888086cd3cffcb93833f38225ebd56bb1986476b59d6e7bd1e5ae3ae5ffe52dada5528d868ef67b738687543193df8d';
9+
const mainnetBaseBech32 =
10+
'addr1qyvgdy2yw3c4jwkc3qyxe570ljunsvlnsgj7h4ttkxvxga44n4h8h5094cawtll99kk6255ds680v7mns6r4gvvnm7xscrhvw9';
11+
12+
it('converts hex-encoded mainnet base address bytes to bech32', () => {
13+
expect(normalizeAddressToBech32(mainnetBaseHex)).toBe(mainnetBaseBech32);
14+
});
15+
16+
it('converts hex-encoded testnet base address bytes to addr_test', () => {
17+
const testnetHex = '00' + mainnetBaseHex.slice(2);
18+
expect(normalizeAddressToBech32(testnetHex)).toMatch(/^addr_test1/);
19+
});
20+
21+
it('converts hex-encoded reward address bytes to stake bech32', () => {
22+
const rewardHex = 'e1ad675b9ef479ae3ae5ffe52dada5528d868ef67b738687543193df8d';
23+
expect(normalizeAddressToBech32(rewardHex)).toBe(
24+
'stake1uxkkwku773u6uwh9lljjmtd922xcdrhk0decdp65xxfalrgc9mvct',
25+
);
26+
});
27+
28+
it('returns bech32 addresses unchanged', () => {
29+
expect(normalizeAddressToBech32(mainnetBaseBech32)).toBe(mainnetBaseBech32);
30+
const stake = 'stake1uxkkwku773u6uwh9lljjmtd922xcdrhk0decdp65xxfalrgc9mvct';
31+
expect(normalizeAddressToBech32(stake)).toBe(stake);
32+
});
33+
34+
it('returns non-address input unchanged', () => {
35+
expect(normalizeAddressToBech32('deadbeef')).toBe('deadbeef');
36+
expect(normalizeAddressToBech32('not-an-address')).toBe('not-an-address');
37+
expect(normalizeAddressToBech32('')).toBe('');
38+
});
39+
});

src/components/common/cardano-objects/connect-wallet.tsx

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,8 @@ import React from "react";
1616
import useUser from "@/hooks/useUser";
1717
import { useUserStore } from "@/lib/zustand/user";
1818
import { getProvider } from "@/utils/get-provider";
19-
import {
20-
Asset,
21-
deserializeAddress,
22-
pubKeyAddress,
23-
scriptAddress,
24-
serializeAddressObj,
25-
serializeRewardAddress,
26-
} from "@meshsdk/core";
19+
import { Asset } from "@meshsdk/core";
20+
import { normalizeAddressToBech32 } from "@/utils/addressCompatibility";
2721
import useUTXOS from "@/hooks/useUTXOS";
2822
import { api } from "@/utils/api";
2923
import { useWalletContext, WalletState } from "@/hooks/useWalletContext";
@@ -405,23 +399,7 @@ function ConnectWalletContent({
405399
}
406400

407401
// Normalize possible hex-encoded CIP-30 address bytes to bech32 (addr/addr_test)
408-
try {
409-
if (!address.startsWith("addr1") && !address.startsWith("addr_test1")) {
410-
const d = deserializeAddress(address);
411-
const stakeCredential = d.stakeCredentialHash || d.stakeScriptCredentialHash || "";
412-
const rebuilt =
413-
d.pubKeyHash
414-
? pubKeyAddress(d.pubKeyHash, stakeCredential, !!d.stakeScriptCredentialHash)
415-
: d.scriptHash
416-
? scriptAddress(d.scriptHash, stakeCredential, !!d.stakeScriptCredentialHash)
417-
: null;
418-
if (rebuilt) {
419-
address = serializeAddressObj(rebuilt, netId);
420-
}
421-
}
422-
} catch {
423-
// If normalization fails, keep original (better than dropping the address)
424-
}
402+
address = normalizeAddressToBech32(address);
425403

426404
setUserAddress(address);
427405

@@ -430,24 +408,8 @@ function ConnectWalletContent({
430408
let stakeAddress = stakeAddresses[0];
431409

432410
// Normalize possible hex-encoded reward address bytes to bech32 (stake/stake_test)
433-
try {
434-
if (
435-
stakeAddress &&
436-
!stakeAddress.startsWith("stake1") &&
437-
!stakeAddress.startsWith("stake_test1")
438-
) {
439-
const d = deserializeAddress(stakeAddress);
440-
const stakeHash = d.stakeCredentialHash || d.stakeScriptCredentialHash;
441-
if (stakeHash) {
442-
stakeAddress = serializeRewardAddress(
443-
stakeHash,
444-
!!d.stakeScriptCredentialHash,
445-
netId,
446-
);
447-
}
448-
}
449-
} catch {
450-
// ignore
411+
if (stakeAddress) {
412+
stakeAddress = normalizeAddressToBech32(stakeAddress);
451413
}
452414

453415
if (!stakeAddress || !address) {
@@ -483,7 +445,7 @@ function ConnectWalletContent({
483445
utxosInitializedRef.current = false;
484446
}
485447
})();
486-
}, [isUtxosEnabled, utxosWallet, isUserLoading, createUser, setUserAddress, netId]);
448+
}, [isUtxosEnabled, utxosWallet, isUserLoading, createUser, setUserAddress]);
487449

488450
// Handle UTXOS wallet assets and network
489451
useEffect(() => {

src/components/common/modals/WalletAuthModal.tsx

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { useState, useEffect, useCallback } from "react";
2-
import { useWallet } from "@meshsdk/react";
32
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
43
import { Button } from "@/components/ui/button";
54
import { useToast } from "@/hooks/use-toast";
65
import useUTXOS from "@/hooks/useUTXOS";
7-
import { useSiteStore } from "@/lib/zustand/site";
8-
import { deserializeAddress, pubKeyAddress, scriptAddress, serializeAddressObj } from "@meshsdk/core";
6+
import useMeshWallet from "@/hooks/useMeshWallet";
7+
import { normalizeAddressToBech32 } from "@/utils/addressCompatibility";
98

109
interface WalletAuthModalProps {
1110
address: string; // display label; actual signing address is derived from wallet.getUsedAddresses()
@@ -16,9 +15,16 @@ interface WalletAuthModalProps {
1615
}
1716

1817
export function WalletAuthModal({ address, open, onClose, onAuthorized, autoAuthorize = false }: WalletAuthModalProps) {
19-
const { wallet, connected } = useWallet();
20-
const network = useSiteStore((state) => state.network);
21-
const netId = (network === 1 ? 1 : 0) as 0 | 1;
18+
// Use the Mesh 1.9 BrowserWallet (via useMeshWallet), NOT react-2.0's
19+
// useWallet().wallet. The latter is a low-level CIP-30 wallet whose
20+
// signData(address, payload) argument order is SWAPPED relative to 1.9's
21+
// signData(payload, address). Calling it with our (nonce, address) order
22+
// made wallets (e.g. VESPR) sign with the nonce as the address and throw
23+
// CIP-30 InternalError (-2). The 1.9 wallet matches the (payload, address)
24+
// order used everywhere else in the app, and the UTXOS MeshWallet below is
25+
// also payload-first — so a single signData(nonce, address) call is correct
26+
// for both.
27+
const { wallet: meshWallet, connected } = useMeshWallet();
2228
const { wallet: utxosWallet, isEnabled: isUtxosEnabled } = useUTXOS();
2329
const { toast } = useToast();
2430
const [submitting, setSubmitting] = useState(false);
@@ -27,22 +33,7 @@ export function WalletAuthModal({ address, open, onClose, onAuthorized, autoAuth
2733
const signingWallet =
2834
isUtxosEnabled && utxosWallet?.cardano
2935
? utxosWallet.cardano
30-
: (wallet && connected ? wallet : null);
31-
32-
const normalizePaymentAddress = useCallback((maybeHexOrBech: string): string => {
33-
if (maybeHexOrBech.startsWith("addr1") || maybeHexOrBech.startsWith("addr_test1")) {
34-
return maybeHexOrBech;
35-
}
36-
const d = deserializeAddress(maybeHexOrBech);
37-
const stakeCredential = d.stakeCredentialHash || d.stakeScriptCredentialHash || "";
38-
const rebuilt =
39-
d.pubKeyHash
40-
? pubKeyAddress(d.pubKeyHash, stakeCredential, !!d.stakeScriptCredentialHash)
41-
: d.scriptHash
42-
? scriptAddress(d.scriptHash, stakeCredential, !!d.stakeScriptCredentialHash)
43-
: null;
44-
return rebuilt ? serializeAddressObj(rebuilt, netId) : maybeHexOrBech;
45-
}, [netId]);
36+
: (meshWallet && connected ? meshWallet : null);
4637

4738
const handleAuthorize = useCallback(async () => {
4839
if (!signingWallet) {
@@ -95,7 +86,10 @@ export function WalletAuthModal({ address, open, onClose, onAuthorized, autoAuth
9586
if (!signingAddress) {
9687
throw new Error("No addresses found for wallet");
9788
}
98-
signingAddress = normalizePaymentAddress(signingAddress);
89+
signingAddress = normalizeAddressToBech32(signingAddress);
90+
if (!signingAddress.startsWith("addr1") && !signingAddress.startsWith("addr_test1")) {
91+
throw new Error("Could not read a valid payment address from this wallet.");
92+
}
9993

10094
// 1) Get nonce from existing endpoint
10195
const nonceRes = await fetch(`/api/v1/getNonce?address=${encodeURIComponent(signingAddress)}`);
@@ -113,26 +107,31 @@ export function WalletAuthModal({ address, open, onClose, onAuthorized, autoAuth
113107

114108
let signed: { signature: string; key: string } | undefined;
115109
try {
116-
// Mirror the working Swagger token flow: signData(nonce, address)
110+
// Mesh 1.9 / UTXOS order is signData(payload, address).
117111
signed = (await (signingWallet as any).signData(
118112
nonce,
119113
signingAddress,
120114
)) as { signature: string; key: string };
121115
} catch (error: any) {
122-
if (error instanceof Error) {
123-
const msg = error.message.toLowerCase();
124-
if (
125-
msg.includes("user") ||
126-
msg.includes("cancel") ||
127-
msg.includes("decline") ||
128-
msg.includes("reject")
129-
) {
130-
throw new Error(
131-
"Signing cancelled. Please try again and approve the signing request.",
132-
);
133-
}
134-
}
135-
throw new Error("Failed to sign nonce. Please try again.");
116+
const raw =
117+
error instanceof Error
118+
? error.message
119+
: typeof error === "string"
120+
? error
121+
: (() => {
122+
try {
123+
return JSON.stringify(error);
124+
} catch {
125+
return String(error);
126+
}
127+
})();
128+
// Surface the wallet's real error verbatim — some (e.g. the UTXOS
129+
// smart wallet) fail inside signData with a provider-specific
130+
// message we otherwise lose. The previous cancel/reject heuristic
131+
// false-matched non-cancellation errors that merely contained the
132+
// word "user", hiding the true cause, so always show the raw text.
133+
console.error("[WalletAuthModal] signData failed:", error);
134+
throw new Error(`Failed to sign nonce: ${raw || "unknown wallet error"}`);
136135
}
137136

138137
if (!signed?.signature || !signed?.key) {
@@ -171,7 +170,7 @@ export function WalletAuthModal({ address, open, onClose, onAuthorized, autoAuth
171170
} finally {
172171
setSubmitting(false);
173172
}
174-
}, [signingWallet, toast, onAuthorized, onClose, normalizePaymentAddress]);
173+
}, [signingWallet, toast, onAuthorized, onClose]);
175174

176175
// Auto-authorize when modal opens if autoAuthorize is true (only once)
177176
useEffect(() => {

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect, Component, ReactNode, useMemo, useCallback, useState, useRef } from "react";
22
import { useRouter } from "next/router";
33
import Link from "next/link";
4-
import { useWallet, useAddress } from "@meshsdk/react";
4+
import { useAddress } from "@meshsdk/react";
55
import { publicRoutes } from "@/data/public-routes";
66
import { api } from "@/utils/api";
77
import useUser from "@/hooks/useUser";
@@ -99,7 +99,6 @@ export default function RootLayout({
9999
}: {
100100
children: React.ReactNode;
101101
}) {
102-
const { wallet } = useWallet();
103102
// 1.9 IWallet bridge — used for getDRep(), which the react 2.0 wallet lacks.
104103
const { wallet: meshWallet } = useMeshWallet();
105104
const { state: walletState, connectedWalletInstance } = useWalletContext();
@@ -124,9 +123,9 @@ export default function RootLayout({
124123
const connected = String(walletState) === String(WalletState.CONNECTED);
125124
const anyWalletConnected = connected || isUtxosEnabled;
126125
// Use connectedWalletInstance if available, otherwise fall back to wallet
127-
const activeWallet = connectedWalletInstance && Object.keys(connectedWalletInstance).length > 0
128-
? connectedWalletInstance
129-
: wallet;
126+
const activeWallet = connectedWalletInstance && Object.keys(connectedWalletInstance).length > 0
127+
? connectedWalletInstance
128+
: meshWallet;
130129

131130
// Global error handler for unhandled promise rejections
132131
useEffect(() => {
@@ -289,8 +288,8 @@ export default function RootLayout({
289288
}
290289

291290
async function initializeWallet() {
292-
if (!walletAddress) return;
293-
291+
if (!walletAddress || !activeWallet) return;
292+
294293
try {
295294
// Get stake address
296295
const stakeAddresses = await activeWallet.getRewardAddresses();

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
DropdownMenuTrigger,
99
} from "@/components/ui/dropdown-menu";
1010
import { useWallet } from "@meshsdk/react";
11+
import useMeshWallet from "@/hooks/useMeshWallet";
1112
import { useToast } from "@/hooks/use-toast";
1213
import { useRouter } from "next/router";
1314
import { useUserStore } from "@/lib/zustand/user";
@@ -24,7 +25,10 @@ export default function UserDropDownWrapper({
2425
mode,
2526
onAction
2627
}: UserDropDownWrapperProps) {
27-
const { wallet, connected, disconnect } = useWallet();
28+
// useWallet only for connection state/control; wallet ops go through the
29+
// Mesh 1.9 bridge.
30+
const { connected, disconnect } = useWallet();
31+
const { wallet } = useMeshWallet();
2832
const { wallet: utxosWallet, isEnabled: isUtxosEnabled, disable: disableUtxos } = useUTXOS();
2933
const { toast } = useToast();
3034
const router = useRouter();

src/components/common/overall-layout/user-drop-down.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import {
99
DropdownMenuTrigger,
1010
} from "@/components/ui/dropdown-menu";
1111
import { useWallet } from "@meshsdk/react";
12+
import useMeshWallet from "@/hooks/useMeshWallet";
1213
import { useToast } from "@/hooks/use-toast";
1314
import { useRouter } from "next/router";
1415
import { useUserStore } from "@/lib/zustand/user";
1516
import { api } from "@/utils/api";
1617

1718
export default function UserDropDown() {
18-
const { wallet, disconnect } = useWallet();
19+
// useWallet only for connection control (disconnect); wallet ops go
20+
// through the Mesh 1.9 bridge.
21+
const { disconnect } = useWallet();
22+
const { wallet } = useMeshWallet();
1923
const { toast } = useToast();
2024
const router = useRouter();
2125
const setPastWallet = useUserStore((state) => state.setPastWallet);
@@ -42,6 +46,7 @@ export default function UserDropDown() {
4246

4347
async function unlinkDiscord(): Promise<void> {
4448
try {
49+
if (!wallet) return;
4550
const usedAddresses = await wallet.getUsedAddresses();
4651
const address = usedAddresses[0];
4752
unlinkDiscordMutation.mutate({ address: address ?? "" });
@@ -84,6 +89,7 @@ export default function UserDropDown() {
8489
<DropdownMenuItem
8590
onClick={async () => {
8691
try {
92+
if (!wallet) return;
8793
let userAddress: string | undefined;
8894
try {
8995
const usedAddresses = await wallet.getUsedAddresses();

src/components/pages/homepage/governance/drep/id/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
88
import { Loader } from "lucide-react";
99
import ActiveIndicator from "../activeIndicator";
1010
import ScriptIndicator from "../scriptIndicator";
11-
import { useWallet } from "@meshsdk/react";
11+
import useMeshWallet from "@/hooks/useMeshWallet";
1212
import RowLabelInfo from "@/components/common/row-label-info";
1313
import { extractJsonLdValue } from "@/utils/jsonLdParser";
1414
import { Button } from "@/components/ui/button";
@@ -17,7 +17,7 @@ import DelegateButton from "./delegateButton";
1717
export default function DrepDetailPage() {
1818
const router = useRouter();
1919
const { id } = router.query;
20-
const { wallet, connected } = useWallet();
20+
const { wallet, connected } = useMeshWallet();
2121
const [drepInfo, setDrepInfo] = useState<BlockfrostDrepInfo | null>(null);
2222
const [drepMetadata, setDrepMetadata] =
2323
useState<BlockfrostDrepMetadata | null>(null);

src/components/pages/homepage/governance/drep/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Pagination from "@/components/common/overall-layout/pagination";
44
import { getProvider } from "@/utils/get-provider";
55
import { BlockfrostDrepInfo, BlockfrostDrepMetadata } from "@/types/governance";
66
import Link from "next/link";
7-
import { useWallet } from "@meshsdk/react";
7+
import useMeshWallet from "@/hooks/useMeshWallet";
88
import DelegateButton from "./id/delegateButton";
99
import RowLabelInfo from "@/components/common/row-label-info";
1010
import { TooltipProvider } from "@/components/ui/tooltip";
@@ -16,7 +16,7 @@ export default function DrepOverviewPage() {
1616
Array<{ details: BlockfrostDrepInfo; metadata: BlockfrostDrepMetadata | null }>
1717
>([]);
1818
const [loading, setLoading] = useState<boolean>(true);
19-
const { wallet, connected } = useWallet();
19+
const { wallet, connected } = useMeshWallet();
2020
const [currentPage, setCurrentPage] = useState<number>(1);
2121
const [pageSize, setPageSize] = useState<number>(25);
2222
const [order, setOrder] = useState<"asc" | "desc">("asc");

0 commit comments

Comments
 (0)