- {multisigWallet && multisigWallet.stakingEnabled() ? (
+ {multisigWallet && appWallet.capabilities?.canStake ? (
) : (
diff --git a/src/components/pages/wallet/info/inspect-script.tsx b/src/components/pages/wallet/info/inspect-script.tsx
index 9a0afb19..e449f5f6 100644
--- a/src/components/pages/wallet/info/inspect-script.tsx
+++ b/src/components/pages/wallet/info/inspect-script.tsx
@@ -100,7 +100,7 @@ export function NativeScriptSection({ appWallet }: { appWallet: Wallet }) {
)}
- {isImportedWallet && appWallet.stakeScriptCbor && (
+ {appWallet.capabilities?.canStake && appWallet.stakeScriptCbor && (
Stake Script CBOR
diff --git a/src/components/pages/wallet/staking/StakingActions/stake.tsx b/src/components/pages/wallet/staking/StakingActions/stake.tsx
index b3d96709..71578bc0 100644
--- a/src/components/pages/wallet/staking/StakingActions/stake.tsx
+++ b/src/components/pages/wallet/staking/StakingActions/stake.tsx
@@ -99,13 +99,12 @@ export default function StakeButton({
setLoading(true);
try {
if (!mWallet) throw new Error("Multisig Wallet could not be built.");
-
- const rewardAddress = mWallet.getStakeAddress();
+
+ const rewardAddress = appWallet.capabilities?.stakeAddress;
if (!rewardAddress) throw new Error("Reward Address could not be built.");
- // For wallets with rawImportBodies, use stored stake script
- // Otherwise, derive from MultisigWallet
- const stakingScript = appWallet.stakeScriptCbor || mWallet.getStakingScript();
+ // For wallets with rawImportBodies or SDK, use stored stake script or derived
+ const stakingScript = appWallet.stakeScriptCbor || (mWallet ? mWallet.getStakingScript() : undefined);
if (!stakingScript) throw new Error("Staking Script could not be built.");
const txBuilder = getTxBuilder(network);
diff --git a/src/hooks/useAppWallet.ts b/src/hooks/useAppWallet.ts
index 36248bc2..4ab295c9 100644
--- a/src/hooks/useAppWallet.ts
+++ b/src/hooks/useAppWallet.ts
@@ -4,7 +4,7 @@ import { buildWallet } from "@/utils/common";
import { useSiteStore } from "@/lib/zustand/site";
import { useRouter } from "next/router";
import { useWalletsStore } from "@/lib/zustand/wallets";
-import { DbWalletWithLegacy } from "@/types/wallet";
+import { DbWalletWithLegacy, Wallet } from "@/types/wallet";
export default function useAppWallet() {
const router = useRouter();
@@ -22,7 +22,7 @@ export default function useAppWallet() {
);
if (wallet) {
- return { appWallet: buildWallet(wallet as DbWalletWithLegacy, network, walletsUtxos[walletId]), isLoading };
+ return { appWallet: buildWallet(wallet as DbWalletWithLegacy, network, walletsUtxos[walletId]) as Wallet, isLoading };
}
return { appWallet: undefined, isLoading };
diff --git a/src/hooks/useMultisigWallet.ts b/src/hooks/useMultisigWallet.ts
index 5e05a2f5..db4791e2 100644
--- a/src/hooks/useMultisigWallet.ts
+++ b/src/hooks/useMultisigWallet.ts
@@ -4,7 +4,7 @@ import { api } from "@/utils/api";
import { useSiteStore } from "@/lib/zustand/site";
import { useUserStore } from "@/lib/zustand/user";
import { buildMultisigWallet } from "@/utils/common";
-import { DbWalletWithLegacy } from "@/types/wallet";
+import { DbWalletWithLegacy, Wallet } from "@/types/wallet";
export default function useMultisigWallet() {
const router = useRouter();
@@ -20,7 +20,7 @@ export default function useMultisigWallet() {
},
);
if (wallet) {
- return { multisigWallet: buildMultisigWallet(wallet as DbWalletWithLegacy, network), wallet, isLoading };
+ return { multisigWallet: buildMultisigWallet(wallet as DbWalletWithLegacy, network), wallet: wallet as Wallet, isLoading };
}
return { multisigWallet: undefined, wallet: undefined, isLoading };
diff --git a/src/hooks/useWalletBalances.ts b/src/hooks/useWalletBalances.ts
index 376a14c8..e1299e06 100644
--- a/src/hooks/useWalletBalances.ts
+++ b/src/hooks/useWalletBalances.ts
@@ -60,7 +60,7 @@ export default function useWalletBalances(
const setBalance = useWalletBalancesStore((state) => state.setBalance);
const getCachedBalance = useWalletBalancesStore((state) => state.getCachedBalance);
const clearExpiredBalances = useWalletBalancesStore((state) => state.clearExpiredBalances);
-
+
const [balances, setBalances] = useState>({});
const [loadingStates, setLoadingStates] = useState<
Record
@@ -95,63 +95,9 @@ export default function useWalletBalances(
const getCanonicalWalletAddress = useCallback(
(wallet: Wallet): string => {
- // Goal: get the address we should query Blockfrost with, without throwing for
- // legacy/summon wallets (which do not have an SDK MultisigWallet).
- try {
- const walletType = getWalletType(wallet);
-
- // Prefer deriving network from the best available address.
- const fallbackAddress =
- wallet.rawImportBodies?.multisig?.address ||
- wallet.signersAddresses?.find((a) => !!a) ||
- wallet.address;
- const walletNetwork = fallbackAddress
- ? addressToNetwork(fallbackAddress)
- : network;
-
- if (walletType === "sdk") {
- const mWallet = buildMultisigWallet(wallet, walletNetwork);
- return mWallet?.getScript().address || wallet.address;
- }
-
- if (walletType === "summon") {
- const importedAddress =
- wallet.rawImportBodies?.multisig?.address || wallet.address;
- const importedPaymentCbor =
- wallet.rawImportBodies?.multisig?.payment_script;
- const summonWallet = buildWallet(wallet, walletNetwork);
-
- // Build payment CBOR from the wallet's native script and compare hashes
- // with imported payment CBOR to ensure we are checking the same script.
- const builtPaymentCbor = serializeNativeScript(
- summonWallet.nativeScript,
- undefined,
- walletNetwork,
- ).scriptCbor;
- const importedPaymentHash = scriptHashFromCbor(importedPaymentCbor);
- const builtPaymentHash = scriptHashFromCbor(builtPaymentCbor);
-
- if (
- importedPaymentHash &&
- builtPaymentHash &&
- importedPaymentHash !== builtPaymentHash
- ) {
- console.warn(
- `[useWalletBalances] Summon payment script mismatch for wallet ${wallet.id}: importedHash=${importedPaymentHash}, builtHash=${builtPaymentHash}`,
- );
- return importedAddress || summonWallet.address;
- }
-
- return summonWallet.address || importedAddress;
- }
-
- // legacy
- return buildWallet(wallet, walletNetwork).address;
- } catch {
- return wallet.address;
- }
+ return wallet.capabilities!.address;
},
- [network],
+ [],
);
const fetchWalletBalance = useCallback(
@@ -205,10 +151,10 @@ export default function useWalletBalances(
// Update local state
setBalances((prev) => ({ ...prev, [wallet.id]: balance }));
setLoadingStates((prev) => ({ ...prev, [wallet.id]: "loaded" }));
-
+
// Cache the balance in Zustand store (including successful fetches)
setBalance(wallet.id, balance, walletAddress);
-
+
fetchedWalletsRef.current.add(wallet.id);
} catch (error: unknown) {
// 404 is expected for never-used addresses.
@@ -324,7 +270,7 @@ export default function useWalletBalances(
fetchedWalletsRef.current.add(wallet.id);
}
});
-
+
if (Object.keys(cached).length > 0) {
setBalances((prev) => ({ ...prev, ...cached }));
}
diff --git a/src/pages/api/v1/freeUtxos.ts b/src/pages/api/v1/freeUtxos.ts
index 856332a6..a7ea2572 100644
--- a/src/pages/api/v1/freeUtxos.ts
+++ b/src/pages/api/v1/freeUtxos.ts
@@ -4,7 +4,7 @@ import { cors, addCorsCacheBustingHeaders } from "@/lib/cors";
//remove all wallet input utxos found in pending txs from the whole pool of txs.
import type { Wallet as DbWallet } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
-import { buildMultisigWallet } from "@/utils/common";
+import { buildWallet } from "@/utils/common";
import { getProvider } from "@/utils/get-provider";
import { addressToNetwork } from "@/utils/multisigSDK";
import type { UTxO } from "@meshsdk/core";
@@ -107,11 +107,11 @@ export default async function handler(
if (!walletFetch) {
return res.status(404).json({ error: "Wallet not found" });
}
- const mWallet = buildMultisigWallet(walletFetch as DbWalletWithLegacy);
- if (!mWallet) {
+ const walletInfo = buildWallet(walletFetch as DbWalletWithLegacy, addressToNetwork(address as string));
+ const addr = walletInfo.capabilities?.address ?? walletInfo.address;
+ if (!addr) {
return res.status(500).json({ error: "Wallet could not be constructed" });
}
- const addr = mWallet.getScript().address;
const network = addressToNetwork(addr);
const blockchainProvider = getProvider(network);
diff --git a/src/pages/api/v1/stats/run-snapshots-batch.ts b/src/pages/api/v1/stats/run-snapshots-batch.ts
index 942695b1..43e5d1f5 100644
--- a/src/pages/api/v1/stats/run-snapshots-batch.ts
+++ b/src/pages/api/v1/stats/run-snapshots-batch.ts
@@ -272,43 +272,10 @@ export default async function handler(
}
}
- // Build wallet conditionally: use MultisigSDK ordering if signersStakeKeys exist
let walletAddress: string;
try {
- const hasStakeKeys = !!(wallet.signersStakeKeys && wallet.signersStakeKeys.length > 0);
- if (hasStakeKeys) {
- // Build MultisigSDK wallet with ordered keys
- const keys: MultisigKey[] = [];
- wallet.signersAddresses.forEach((addr: string, i: number) => {
- if (!addr) return;
- try {
- keys.push({ keyHash: resolvePaymentKeyHash(addr), role: 0, name: wallet.signersDescriptions[i] || "" });
- } catch {}
- });
- wallet.signersStakeKeys?.forEach((stakeKey: string, i: number) => {
- if (!stakeKey) return;
- try {
- keys.push({ keyHash: resolveStakeKeyHash(stakeKey), role: 2, name: wallet.signersDescriptions[i] || "" });
- } catch {}
- });
- if (keys.length === 0 && !wallet.stakeCredentialHash) {
- throw new Error("No valid keys or stakeCredentialHash provided");
- }
- const mWallet = new MultisigWallet(
- wallet.name,
- keys,
- wallet.description ?? "",
- wallet.numRequiredSigners ?? 1,
- network,
- wallet.stakeCredentialHash as undefined | string,
- (wallet.type as any) || "atLeast"
- );
- walletAddress = mWallet.getScript().address;
- } else {
- // Fallback: build the wallet without enforcing key ordering (legacy payment-script build)
- const builtWallet = buildWallet(wallet as DbWalletWithLegacy, network);
- walletAddress = builtWallet.address;
- }
+ const builtWallet = buildWallet(wallet as DbWalletWithLegacy, network);
+ walletAddress = builtWallet.capabilities?.address ?? builtWallet.address;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown wallet build error';
console.error(`Failed to build wallet for ${wallet.id.slice(0, 8)}...:`, errorMessage);
diff --git a/src/server/api/routers/transactions.ts b/src/server/api/routers/transactions.ts
index aef26e7b..7f9128ba 100644
--- a/src/server/api/routers/transactions.ts
+++ b/src/server/api/routers/transactions.ts
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { csl, calculateTxHash } from "@meshsdk/core-csl";
import { resolvePaymentKeyHash } from "@meshsdk/core";
-import { buildMultisigWallet } from "@/utils/common";
+import { buildWallet } from "@/utils/common";
import { getProvider } from "@/utils/get-provider";
import { addressToNetwork } from "@/utils/multisigSDK";
@@ -262,15 +262,15 @@ export const transactionRouter = createTRPCRouter({
? addressToNetwork(wallet.signersAddresses[0]!)
: 0; // Default to preprod/testnet
- const mWallet = buildMultisigWallet(wallet as any, network);
- if (!mWallet) {
+ const walletInfo = buildWallet(wallet as any, network);
+ if (!walletInfo || (!walletInfo.capabilities?.address && !walletInfo.address)) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to build wallet script",
});
}
- const walletScriptAddress = mWallet.getScript().address;
+ const walletScriptAddress = walletInfo.capabilities?.address ?? walletInfo.address;
const blockchainProvider = getProvider(network);
// Convert transaction body to txJson format
diff --git a/src/types/wallet.ts b/src/types/wallet.ts
index 11a74f5d..8a70d73d 100644
--- a/src/types/wallet.ts
+++ b/src/types/wallet.ts
@@ -39,6 +39,14 @@ export interface RawImportBodies {
[key: string]: unknown;
}
+export interface WalletCapabilities {
+ canStake: boolean;
+ canVote: boolean;
+ address: string;
+ stakeAddress?: string;
+ dRepId?: string;
+}
+
export type DbWalletWithLegacy = DbWallet & {
rawImportBodies?: RawImportBodies | null;
};
@@ -48,5 +56,6 @@ export type Wallet = DbWalletWithLegacy & {
address: string;
dRepId: string;
stakeScriptCbor?: string;
+ capabilities?: WalletCapabilities;
};
diff --git a/src/utils/common.ts b/src/utils/common.ts
index 1be645a3..ba2c2903 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -1,4 +1,4 @@
-import { DbWalletWithLegacy, Wallet } from "@/types/wallet";
+import { DbWalletWithLegacy, Wallet } from "../types/wallet";
import {
deserializeAddress,
NativeScript,
@@ -7,17 +7,18 @@ import {
resolveScriptHashDRepId,
resolveStakeKeyHash,
serializeNativeScript,
+ serializeRewardAddress,
UTxO,
} from "@meshsdk/core";
import { getDRepIds } from "@meshsdk/core-cst";
-import { MultisigKey, MultisigWallet } from "@/utils/multisigSDK";
+import { MultisigKey, MultisigWallet } from "./multisigSDK";
import {
decodeNativeScriptFromCbor,
buildPaymentSigScriptsFromAddresses,
decodedToNativeScript,
normalizeHex,
scriptHashFromCbor,
-} from "@/utils/nativeScriptUtils";
+} from "./nativeScriptUtils";
function addressToNetwork(address: string): number {
return address.includes("test") ? 0 : 1;
@@ -131,13 +132,13 @@ export type WalletType = 'legacy' | 'sdk' | 'summon';
export function getWalletType(wallet: DbWalletWithLegacy): WalletType {
if (wallet.rawImportBodies?.multisig) return 'summon';
-
+
// Legacy: only payment keys (no stake keys, no DRep keys)
// External stake credential hash doesn't make it SDK - it's still legacy if only payment keys
const hasStakeKeys = wallet.signersStakeKeys && wallet.signersStakeKeys.length > 0;
const hasDRepKeys = wallet.signersDRepKeys && wallet.signersDRepKeys.length > 0;
if (!hasStakeKeys && !hasDRepKeys) return 'legacy';
-
+
return 'sdk';
}
@@ -150,7 +151,7 @@ export function buildMultisigWallet(
network?: number,
): MultisigWallet | undefined {
const walletType = getWalletType(wallet);
-
+
// Only build MultisigWallet for SDK wallets
if (walletType !== 'sdk') {
return undefined;
@@ -158,7 +159,7 @@ export function buildMultisigWallet(
const keys: MultisigKey[] = [];
const resolvedNetwork = resolveWalletNetwork(wallet, network);
-
+
// Add payment keys (role 0)
if (wallet.signersAddresses.length > 0) {
wallet.signersAddresses.forEach((addr, i) => {
@@ -178,7 +179,7 @@ export function buildMultisigWallet(
}
});
}
-
+
// Add staking keys (role 2)
if (wallet.signersStakeKeys && wallet.signersStakeKeys.length > 0) {
wallet.signersStakeKeys.forEach((stakeKey, i) => {
@@ -196,7 +197,7 @@ export function buildMultisigWallet(
}
});
}
-
+
// Add DRep keys (role 3)
if (wallet.signersDRepKeys && wallet.signersDRepKeys.length > 0) {
wallet.signersDRepKeys.forEach((dRepKey, i) => {
@@ -259,7 +260,7 @@ export function buildWallet(
if (!multisig) {
throw new Error("rawImportBodies.multisig is required for Summon wallets");
}
-
+
// Always use stored address from rawImportBodies
const address = multisig.address;
if (!address) {
@@ -272,7 +273,10 @@ export function buildWallet(
stakeScript: multisig.stake_script,
});
- // Always use the script that matches the address payment credential hash
+ // Always use the scriptCbor from metadata if available. This is CRITICAL
+ // for backward compatibility with wallets created with "unordered CBOR lists".
+ // Re-deriving the address from a canonicalized script would change the hash
+ // and break existing wallets.
const scriptCbor = paymentScriptCbor;
if (!scriptCbor) {
throw new Error("A valid payment script is required in rawImportBodies.multisig");
@@ -289,20 +293,32 @@ export function buildWallet(
// Fallback to placeholder if decoding fails
nativeScript = scriptType === "atLeast"
? {
- type: "atLeast",
- required: wallet.numRequiredSigners ?? 1,
- scripts: [],
- }
+ type: "atLeast",
+ required: wallet.numRequiredSigners ?? 1,
+ scripts: [],
+ }
: {
- type: scriptType,
- scripts: [],
- };
+ type: scriptType,
+ scripts: [],
+ };
}
// For rawImportBodies wallets, dRepId cannot be easily derived from stored CBOR
// Set to empty string - it can be derived later if needed from the actual script
const dRepId = "";
+ // Capability logic for Summon
+ // Staking is enabled if a stake script is present
+ const canStake = !!stakeScriptCbor;
+ const stakeScriptHash = stakeScriptCbor ? scriptHashFromCbor(stakeScriptCbor) : undefined;
+ const stakeAddress = stakeScriptHash
+ ? serializeRewardAddress(
+ stakeScriptHash,
+ true,
+ network as 0 | 1
+ )
+ : undefined;
+
return {
...wallet,
scriptCbor,
@@ -310,6 +326,13 @@ export function buildWallet(
address,
dRepId,
stakeScriptCbor,
+ capabilities: {
+ canStake,
+ canVote: false, // Summon does not support DRep yet
+ address,
+ stakeAddress,
+ dRepId: dRepId || undefined,
+ },
} as Wallet;
}
@@ -339,6 +362,12 @@ export function buildWallet(
nativeScript,
address,
dRepId: dRepIdCip129,
+ capabilities: {
+ canStake: false,
+ canVote: false,
+ address,
+ dRepId: dRepIdCip129,
+ },
} as Wallet;
}
@@ -383,5 +412,12 @@ export function buildWallet(
nativeScript,
address,
dRepId: dRepIdCip129,
+ capabilities: {
+ canStake: mWallet.stakingEnabled(),
+ canVote: mWallet.drepEnabled(),
+ address,
+ stakeAddress: mWallet.getStakeAddress(),
+ dRepId: mWallet.getDRepId(),
+ },
} as Wallet;
}
diff --git a/src/utils/nativeScriptUtils.ts b/src/utils/nativeScriptUtils.ts
index 7af0877a..0f47a3c9 100644
--- a/src/utils/nativeScriptUtils.ts
+++ b/src/utils/nativeScriptUtils.ts
@@ -121,15 +121,15 @@ export function decodeNativeScriptFromCsl(
return { type: "any", scripts };
}
- const sn = ns.as_script_n_of_k();
- if (sn) {
- const list = sn.native_scripts();
+ const saNOfK = ns.as_script_n_of_k();
+ if (saNOfK) {
+ const n = saNOfK.n();
+ const list = saNOfK.native_scripts();
const scripts: DecodedNativeScript[] = [];
for (let i = 0; i < list.len(); i++) {
const child = list.get(i);
scripts.push(decodeNativeScriptFromCsl(child));
}
- const n = sn.n();
const required =
typeof n === "number"
? n