- {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 50e6d7b8..944d2122 100644
--- a/src/components/pages/wallet/staking/StakingActions/stake.tsx
+++ b/src/components/pages/wallet/staking/StakingActions/stake.tsx
@@ -37,13 +37,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 = await getTxBuilder(network);
diff --git a/src/hooks/useAppWallet.ts b/src/hooks/useAppWallet.ts
index 5555181b..42240ab2 100644
--- a/src/hooks/useAppWallet.ts
+++ b/src/hooks/useAppWallet.ts
@@ -5,7 +5,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();
diff --git a/src/hooks/useMultisigWallet.ts b/src/hooks/useMultisigWallet.ts
index 8a1c9d67..c90026f9 100644
--- a/src/hooks/useMultisigWallet.ts
+++ b/src/hooks/useMultisigWallet.ts
@@ -5,7 +5,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();
diff --git a/src/hooks/useWalletBalances.ts b/src/hooks/useWalletBalances.ts
index 376a14c8..89012978 100644
--- a/src/hooks/useWalletBalances.ts
+++ b/src/hooks/useWalletBalances.ts
@@ -1,10 +1,8 @@
import { useEffect, useRef, useState, useCallback } from "react";
-import { serializeNativeScript } from "@meshsdk/core";
import { Wallet } from "@/types/wallet";
import { getProvider } from "@/utils/get-provider";
import { addressToNetwork } from "@/utils/multisigSDK";
-import { buildMultisigWallet, buildWallet, getWalletType } from "@/utils/common";
-import { scriptHashFromCbor } from "@/utils/nativeScriptUtils";
+import { buildWallet } from "@/utils/common";
import { useSiteStore } from "@/lib/zustand/site";
import { useWalletBalancesStore } from "@/lib/zustand/wallet-balances";
@@ -60,7 +58,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
@@ -93,14 +91,15 @@ export default function useWalletBalances(
};
}, []);
+ // The address to query Blockfrost with. `buildWallet()` resolves it once and
+ // caches it on `capabilities`, but `capabilities` is optional on `Wallet` —
+ // rows that reach this hook without going through `buildWallet()` (cached
+ // records, tests, future call sites) would otherwise crash here. Fall back to
+ // resolving it the old way, and to the stored address if even that throws.
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).
+ if (wallet.capabilities?.address) return wallet.capabilities.address;
try {
- const walletType = getWalletType(wallet);
-
- // Prefer deriving network from the best available address.
const fallbackAddress =
wallet.rawImportBodies?.multisig?.address ||
wallet.signersAddresses?.find((a) => !!a) ||
@@ -108,45 +107,7 @@ export default function useWalletBalances(
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;
+ return buildWallet(wallet, walletNetwork).address || wallet.address;
} catch {
return wallet.address;
}
@@ -205,10 +166,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 +285,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/stats/run-snapshots-batch.ts b/src/pages/api/v1/stats/run-snapshots-batch.ts
index b7660d84..bac1acc2 100644
--- a/src/pages/api/v1/stats/run-snapshots-batch.ts
+++ b/src/pages/api/v1/stats/run-snapshots-batch.ts
@@ -2,9 +2,8 @@ import { cors, addCorsCacheBustingHeaders } from "@/lib/cors";
import type { NextApiRequest, NextApiResponse } from "next";
import { db } from "@/server/db";
import { buildWallet } from "@/utils/common";
-import { MultisigWallet, type MultisigKey } from "@/utils/multisigSDK";
import { getProvider } from "@/utils/get-provider";
-import { resolvePaymentKeyHash, resolveStakeKeyHash, type UTxO } from "@meshsdk/core";
+import { type UTxO } from "@meshsdk/core";
import { getBalance } from "@/utils/getBalance";
import { addressToNetwork } from "@/utils/multisigSDK";
import { Prisma, type Wallet as DbWallet } from "@prisma/client";
@@ -271,43 +270,15 @@ export default async function handler(
}
}
- // Build wallet conditionally: use MultisigSDK ordering if signersStakeKeys exist
+ // This used to branch on `signersStakeKeys` and hand-assemble a
+ // MultisigWallet with ordered keys for the stake-key case, falling back
+ // to buildWallet() otherwise. buildWallet() now does that branch itself
+ // (getWalletType -> sdk/legacy/summon) and reports the resolved address
+ // on `capabilities`, so the conditional here is fully subsumed.
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 f6c685fc..7dec7aa6 100644
--- a/src/server/api/routers/transactions.ts
+++ b/src/server/api/routers/transactions.ts
@@ -3,7 +3,7 @@ import { z } from "zod";
import { csl } from "@meshsdk/core-csl";
import { resolveTxHash } from "@meshsdk/core-cst";
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";
@@ -366,15 +366,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 fadbfb68..12bcd961 100644
--- a/src/types/wallet.ts
+++ b/src/types/wallet.ts
@@ -44,6 +44,14 @@ export interface RawImportBodies {
[key: string]: unknown;
}
+export interface WalletCapabilities {
+ canStake: boolean;
+ canVote: boolean;
+ address: string;
+ stakeAddress?: string;
+ dRepId?: string;
+}
+
export type WalletImportProvenance =
| {
origin: "summon";
@@ -93,5 +101,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 9a3555ca..39523748 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -7,6 +7,7 @@ import {
resolveScriptHashDRepId,
resolveStakeKeyHash,
serializeNativeScript,
+ serializeRewardAddress,
UTxO,
} from "@meshsdk/core";
import { getDRepIds } from "@meshsdk/core-cst";
@@ -135,13 +136,13 @@ function hasNonEmptyEntries(values?: string[] | null): boolean {
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 = hasNonEmptyEntries(wallet.signersStakeKeys);
const hasDRepKeys = hasNonEmptyEntries(wallet.signersDRepKeys);
if (!hasStakeKeys && !hasDRepKeys) return 'legacy';
-
+
return 'sdk';
}
@@ -154,7 +155,7 @@ export function buildMultisigWallet(
network?: number,
): MultisigWallet | undefined {
const walletType = getWalletType(wallet);
-
+
// Only build MultisigWallet for SDK wallets
if (walletType !== 'sdk') {
return undefined;
@@ -162,7 +163,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) => {
@@ -182,7 +183,7 @@ export function buildMultisigWallet(
}
});
}
-
+
// Add staking keys (role 2)
if (wallet.signersStakeKeys && wallet.signersStakeKeys.length > 0) {
wallet.signersStakeKeys.forEach((stakeKey, i) => {
@@ -200,7 +201,7 @@ export function buildMultisigWallet(
}
});
}
-
+
// Add DRep keys (role 3)
if (wallet.signersDRepKeys && wallet.signersDRepKeys.length > 0) {
wallet.signersDRepKeys.forEach((dRepKey, i) => {
@@ -263,7 +264,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) {
@@ -276,7 +277,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");
@@ -293,20 +297,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,
@@ -314,6 +330,18 @@ export function buildWallet(
address,
dRepId,
stakeScriptCbor,
+ capabilities: {
+ canStake,
+ // TODO: flip once Summon exports a DRep script. Summon's
+ // rawImportBodies.multisig carries only payment_script and
+ // stake_script, so there is nothing to derive a DRep credential from —
+ // `dRepId` above is empty for the same reason. See the review thread on
+ // https://github.com/MeshJS/multisig/pull/212 before changing this.
+ canVote: false,
+ address,
+ stakeAddress,
+ dRepId: dRepId || undefined,
+ },
} as Wallet;
}
@@ -343,6 +371,12 @@ export function buildWallet(
nativeScript,
address,
dRepId: dRepIdCip129,
+ capabilities: {
+ canStake: false,
+ canVote: false,
+ address,
+ dRepId: dRepIdCip129,
+ },
} as Wallet;
}
@@ -395,5 +429,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