From 51e9e8067077f04e4a6379bf8587aba71233373f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Diamond?= <32074058+Andre-Diamond@users.noreply.github.com> Date: Sat, 18 Oct 2025 08:30:24 +0200 Subject: [PATCH 01/12] feat(wallet): implement ownership claim for new wallets - Added functionality to allow users to claim ownership of new wallets when the current owner is set to "all" and the user qualifies based on their stake address or signer addresses. - Introduced a new mutation in the API to handle ownership updates, ensuring atomic conditional claims. - Enhanced the wallet invite page to trigger ownership updates automatically when conditions are met. --- .../pages/homepage/wallets/invite/index.tsx | 36 +++++++++++++++++-- src/server/api/routers/wallets.ts | 33 +++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/components/pages/homepage/wallets/invite/index.tsx b/src/components/pages/homepage/wallets/invite/index.tsx index 599dd0c1..20b37323 100644 --- a/src/components/pages/homepage/wallets/invite/index.tsx +++ b/src/components/pages/homepage/wallets/invite/index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { api } from "@/utils/api"; import { useUserStore } from "@/lib/zustand/user"; import { useRouter } from "next/router"; @@ -44,6 +44,14 @@ export default function PageNewWalletInvite() { }, ); + const ownerUpdateTriggered = useRef(false); + + const { mutate: updateNewWalletOwner } = api.wallet.updateNewWalletOwner.useMutation({ + onSuccess: async () => { + void utils.wallet.getNewWallet.invalidate({ walletId: newWalletId! }); + }, + }); + useEffect(() => { if (!newWallet) { setShowNotFound(false); @@ -55,7 +63,31 @@ export default function PageNewWalletInvite() { }, [newWallet]); // Calculate user role once (after newWallet is loaded) - const isOwner = newWallet?.ownerAddress === userAddress; + const isOwner = !!newWallet && ( + newWallet.ownerAddress === userAddress || + (newWallet.ownerAddress === "all" && ( + (user?.stakeAddress ? newWallet.signersStakeKeys?.includes(user.stakeAddress) : false) || + (userAddress ? newWallet.signersAddresses?.includes(userAddress) : false) + )) + ); + + // If owner is set to "all" and the connected user qualifies, claim ownership + useEffect(() => { + if (!newWallet || !userAddress) return; + if (ownerUpdateTriggered.current) return; + + const qualifies = + newWallet.ownerAddress === "all" && ( + (user?.stakeAddress ? newWallet.signersStakeKeys?.includes(user.stakeAddress) : false) || + newWallet.signersAddresses?.includes(userAddress) + ); + + if (qualifies) { + ownerUpdateTriggered.current = true; + updateNewWalletOwner({ walletId: newWallet.id, ownerAddress: userAddress }); + } + }, [newWallet, userAddress, user, updateNewWalletOwner]); + console.log(user, newWallet) const isAlreadySigner = newWallet?.signersAddresses.includes(userAddress || "") || false; diff --git a/src/server/api/routers/wallets.ts b/src/server/api/routers/wallets.ts index 2503cd1e..b8e8c5ba 100644 --- a/src/server/api/routers/wallets.ts +++ b/src/server/api/routers/wallets.ts @@ -268,6 +268,39 @@ export const walletRouter = createTRPCRouter({ }); }), + updateNewWalletOwner: publicProcedure + .input( + z.object({ + walletId: z.string(), + ownerAddress: z.string(), + }), + ) + .mutation(async ({ ctx, input }) => { + // Look up user's stake address for stake-key membership check + const user = await ctx.db.user.findUnique({ where: { address: input.ownerAddress } }); + const stakeAddr = user?.stakeAddress || ""; + + // Atomic conditional claim: only if owner is currently "all" AND caller qualifies + const result = await ctx.db.newWallet.updateMany({ + where: { + id: input.walletId, + ownerAddress: "all", + OR: [ + { signersAddresses: { has: input.ownerAddress } }, + stakeAddr ? { signersStakeKeys: { has: stakeAddr } } : { id: "__never__" }, + ], + }, + data: { ownerAddress: input.ownerAddress }, + }); + + if (result.count === 0) { + // Either already claimed, not eligible, or wallet not found + return ctx.db.newWallet.findUnique({ where: { id: input.walletId } }); + } + + return ctx.db.newWallet.findUnique({ where: { id: input.walletId } }); + }), + deleteNewWallet: publicProcedure .input(z.object({ walletId: z.string() })) .mutation(async ({ ctx, input }) => { From 62274f8f50ab7f837df587d89da47185b442e29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Diamond?= <32074058+Andre-Diamond@users.noreply.github.com> Date: Sat, 18 Oct 2025 10:58:27 +0200 Subject: [PATCH 02/12] feat(wallet): enhance wallet invite functionality with key hash normalization - Added logic to handle user payment and stake key hashes for signers, improving the qualification process for wallet ownership. - Implemented normalization of key hash placeholders to actual addresses, ensuring accurate signer identification. - Enhanced user feedback with toast notifications upon successful updates and error handling for failed operations. - Refactored the component to streamline the update process for new wallet signers and their descriptions. --- .../pages/homepage/wallets/invite/index.tsx | 258 ++++++++++++++++-- 1 file changed, 230 insertions(+), 28 deletions(-) diff --git a/src/components/pages/homepage/wallets/invite/index.tsx b/src/components/pages/homepage/wallets/invite/index.tsx index 20b37323..7b29a081 100644 --- a/src/components/pages/homepage/wallets/invite/index.tsx +++ b/src/components/pages/homepage/wallets/invite/index.tsx @@ -20,6 +20,7 @@ import WalletInfoCard from "./WalletInfoCard"; import JoinAsSignerCard from "./JoinAsSignerCard"; import ManageSignerCard from "./ManageSignerCard"; import { serializeRewardAddress, deserializeAddress } from "@meshsdk/core"; +import { paymentKeyHash, stakeKeyHash } from "@/utils/multisigSDK"; export default function PageNewWalletInvite() { const router = useRouter(); @@ -52,6 +53,33 @@ export default function PageNewWalletInvite() { }, }); + const { mutate: updateNewWalletSigners } = + api.wallet.updateNewWalletSigners.useMutation({ + onSuccess: async () => { + setLoading(false); + // Clear the name input after successful addition + setSignerDescription(""); + toast({ + title: "Success", + description: "You have been added as a signer", + duration: 5000, + }); + // No reload - just refetch the wallet data + void utils.wallet.getNewWallet.invalidate({ walletId: newWalletId! }); + }, + onError: (error) => { + setLoading(false); + toast({ + title: "Page No Longer Available", + variant: "destructive", + duration: 5000, + }); + }, + }); + + // Prevent repeated normalization updates + const normalizationTriggered = useRef(false); + useEffect(() => { if (!newWallet) { setShowNotFound(false); @@ -62,6 +90,22 @@ export default function PageNewWalletInvite() { } }, [newWallet]); + // Helper to detect native script key hash entries (28-byte hex) + const isNativeKeyHash = (value: string | undefined): boolean => + !!value && /^[0-9a-fA-F]{56}$/.test(value); + + // Compare script CBORs if present + const hasBothCbors = !!((newWallet as any)?.paymentCbor && (newWallet as any)?.stakeCbor); + const paymentEqualsStake = !!( + hasBothCbors && (newWallet as any)?.paymentCbor === (newWallet as any)?.stakeCbor + ); + const paymentNotEqualsStake = !!( + hasBothCbors && (newWallet as any)?.paymentCbor !== (newWallet as any)?.stakeCbor + ); + + const userPaymentHash = userAddress ? paymentKeyHash(userAddress) : ""; + const userStakeHash = user?.stakeAddress ? stakeKeyHash(user.stakeAddress) : ""; + // Calculate user role once (after newWallet is loaded) const isOwner = !!newWallet && ( newWallet.ownerAddress === userAddress || @@ -76,10 +120,39 @@ export default function PageNewWalletInvite() { if (!newWallet || !userAddress) return; if (ownerUpdateTriggered.current) return; + const qualifiesByKeyHash = (() => { + // Equal CBORs: only compare payment key hash against signersAddresses + if (paymentEqualsStake) { + if (!userPaymentHash) return false; + if (!newWallet.signersAddresses?.some(isNativeKeyHash)) return false; + return newWallet.signersAddresses.some( + (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), + ); + } + // Not equal CBORs: require BOTH payment hash in signersAddresses AND stake hash in signersStakeKeys + if (paymentNotEqualsStake) { + const paymentMatch = !!( + userPaymentHash && + newWallet.signersAddresses?.some( + (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), + ) + ); + const stakeMatch = !!( + userStakeHash && + newWallet.signersStakeKeys?.some( + (sk) => isNativeKeyHash(sk) && sk.toLowerCase() === userStakeHash.toLowerCase(), + ) + ); + return paymentMatch && stakeMatch; + } + return false; + })(); + const qualifies = newWallet.ownerAddress === "all" && ( (user?.stakeAddress ? newWallet.signersStakeKeys?.includes(user.stakeAddress) : false) || - newWallet.signersAddresses?.includes(userAddress) + newWallet.signersAddresses?.includes(userAddress) || + qualifiesByKeyHash ); if (qualifies) { @@ -88,44 +161,173 @@ export default function PageNewWalletInvite() { } }, [newWallet, userAddress, user, updateNewWalletOwner]); console.log(user, newWallet) - const isAlreadySigner = - newWallet?.signersAddresses.includes(userAddress || "") || false; + const isAlreadySigner = (() => { + if (!newWallet) return false; + if (userAddress && newWallet.signersAddresses.includes(userAddress)) return true; + // If equal CBORs: allow payment key hash match in signersAddresses + if (paymentEqualsStake) { + if (!userPaymentHash) return false; + if (!newWallet.signersAddresses.some(isNativeKeyHash)) return false; + return newWallet.signersAddresses.some( + (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), + ); + } + // If not equal CBORs: require BOTH payment key hash in signersAddresses AND stake key match (direct or hash) + if (paymentNotEqualsStake) { + const paymentMatch = !!( + userPaymentHash && + newWallet.signersAddresses.some( + (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), + ) + ); + const stakeDirectMatch = !!( + user?.stakeAddress && newWallet.signersStakeKeys?.includes(user.stakeAddress) + ); + const stakeHashMatch = !!( + userStakeHash && + newWallet.signersStakeKeys?.some( + (sk) => isNativeKeyHash(sk) && sk.toLowerCase() === userStakeHash.toLowerCase(), + ) + ); + const stakeMatched = stakeDirectMatch || stakeHashMatch; + return paymentMatch && stakeMatched; + } + return false; + })(); + + // Normalize any key-hash placeholders to actual addresses when we can identify the user + useEffect(() => { + if (!newWallet) return; + if (normalizationTriggered.current) return; + if (!userAddress && !user?.stakeAddress) return; + + let nextSignersAddresses = [...newWallet.signersAddresses]; + let nextSignersStakeKeys = [...newWallet.signersStakeKeys]; + let didChangeAddresses = false; + let didChangeStake = false; + + // Debug: compute and log match status before normalization + const nativeAddressKeyHashes = newWallet.signersAddresses?.filter(isNativeKeyHash) || []; + const nativeStakeKeyHashes = newWallet.signersStakeKeys?.filter(isNativeKeyHash) || []; + const paymentAddrKeyHashMatch = !!( + userPaymentHash && nativeAddressKeyHashes.some((h) => h.toLowerCase() === userPaymentHash.toLowerCase()) + ); + const stakeKeyHashMatch = !!( + userStakeHash && nativeStakeKeyHashes.some((h) => h.toLowerCase() === userStakeHash.toLowerCase()) + ); + const stakeDirectMatch = !!( + user?.stakeAddress && newWallet.signersStakeKeys?.includes(user.stakeAddress) + ); + + // If CBORs differ, wait until both payment and stake data are present so we can update both sides together + if (paymentNotEqualsStake) { + if (!userAddress || !user?.stakeAddress || !userPaymentHash || !userStakeHash) { + console.log("[invite] normalize: waiting for both user payment and stake info before updating (CBORs differ)"); + return; + } + } + + // Replace payment keyHash in signersAddresses with userAddress + if (userAddress && userPaymentHash && newWallet.signersAddresses?.some(isNativeKeyHash)) { + const replaced = nextSignersAddresses.map((addr) => + isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase() + ? userAddress + : addr, + ); + if (replaced.some((v, i) => v !== nextSignersAddresses[i])) { + nextSignersAddresses = replaced; + didChangeAddresses = true; + } + } + + // Replace stake keyHash in signersStakeKeys with user's stake address + if (user?.stakeAddress && userStakeHash && newWallet.signersStakeKeys?.some(isNativeKeyHash)) { + const replacedStake = nextSignersStakeKeys.map((sk) => + isNativeKeyHash(sk) && sk.toLowerCase() === userStakeHash.toLowerCase() + ? user.stakeAddress as string + : sk, + ); + if (replacedStake.some((v, i) => v !== nextSignersStakeKeys[i])) { + nextSignersStakeKeys = replacedStake; + didChangeStake = true; + } + } + + // Extra logs to see which replacements will be sent + console.log("[invite] normalize: replacements computed", { + didChangeAddresses, + didChangeStake, + paymentEqualsStake, + paymentNotEqualsStake, + }); + + // If CBORs are equal: allow updating addresses-only. + // If CBORs differ: require both sides to change in the same mutation to keep indices aligned. + if (paymentEqualsStake) { + if (!didChangeAddresses && !didChangeStake) return; + } else if (paymentNotEqualsStake) { + if (!(didChangeAddresses && didChangeStake)) return; + } else { + if (!didChangeAddresses && !didChangeStake) return; + } + + normalizationTriggered.current = true; + updateNewWalletSigners( + { + walletId: newWalletId!, + signersAddresses: nextSignersAddresses, + signersStakeKeys: nextSignersStakeKeys, + signersDRepKeys: newWallet.signersDRepKeys || [], + signersDescriptions: newWallet.signersDescriptions, + }, + { + onSuccess: async () => { + // silent refresh + void utils.wallet.getNewWallet.invalidate({ walletId: newWalletId! }); + }, + onError: () => { + // allow retry on next render if it fails + normalizationTriggered.current = false; + }, + }, + ); + }, [newWallet, userAddress, user?.stakeAddress, userPaymentHash, userStakeHash, updateNewWalletSigners, utils, newWalletId]); // Set initial signer name when wallet data loads useEffect(() => { if (newWallet && userAddress) { - const signerIndex = newWallet.signersAddresses.findIndex( + let signerIndex = newWallet.signersAddresses.findIndex( (addr) => addr === userAddress, ); + // Equal CBORs: fallback to payment hash in signersAddresses + if (signerIndex === -1 && paymentEqualsStake && userPaymentHash) { + signerIndex = newWallet.signersAddresses.findIndex( + (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), + ); + } + // Not equal CBORs: only set name when BOTH indices match and align + if (signerIndex === -1 && paymentNotEqualsStake) { + const addrIdx = userPaymentHash + ? newWallet.signersAddresses.findIndex( + (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), + ) + : -1; + const stakeIdx = userStakeHash + ? newWallet.signersStakeKeys.findIndex( + (sk) => isNativeKeyHash(sk) && sk.toLowerCase() === userStakeHash.toLowerCase(), + ) + : -1; + if (addrIdx !== -1 && stakeIdx !== -1 && addrIdx === stakeIdx) { + signerIndex = addrIdx; + } + } if (signerIndex !== -1) { setLocalSignerName(newWallet.signersDescriptions[signerIndex] || ""); } } - }, [newWallet, userAddress]); + }, [newWallet, userAddress, paymentEqualsStake, paymentNotEqualsStake, userPaymentHash, userStakeHash]); - const { mutate: updateNewWalletSigners } = - api.wallet.updateNewWalletSigners.useMutation({ - onSuccess: async () => { - setLoading(false); - // Clear the name input after successful addition - setSignerDescription(""); - toast({ - title: "Success", - description: "You have been added as a signer", - duration: 5000, - }); - // No reload - just refetch the wallet data - void utils.wallet.getNewWallet.invalidate({ walletId: newWalletId! }); - }, - onError: (error) => { - setLoading(false); - toast({ - title: "Page No Longer Available", - variant: "destructive", - duration: 5000, - }); - }, - }); + const updateNewWalletSignersDescriptionsMutation = api.wallet.updateNewWalletSignersDescriptions.useMutation({ From 50a518b60c3ea5221090cad0a132f6834cdefff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Diamond?= <32074058+Andre-Diamond@users.noreply.github.com> Date: Sun, 19 Oct 2025 10:36:59 +0200 Subject: [PATCH 03/12] feat(wallet): improve wallet invite logic with enhanced payment hash handling - Introduced useMemo for blockchain provider and payment hash matching to optimize performance. - Added logic to fetch and compare derived payment hashes from user's stake address, improving wallet qualification checks. - Refactored conditions for user ownership and signer status to streamline the invite process. - Enhanced error handling for address fetching failures, ensuring better user feedback. --- .../pages/homepage/wallets/invite/index.tsx | 116 +++++++++++------- 1 file changed, 71 insertions(+), 45 deletions(-) diff --git a/src/components/pages/homepage/wallets/invite/index.tsx b/src/components/pages/homepage/wallets/invite/index.tsx index 7b29a081..432ae481 100644 --- a/src/components/pages/homepage/wallets/invite/index.tsx +++ b/src/components/pages/homepage/wallets/invite/index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useMemo } from "react"; import { api } from "@/utils/api"; import { useUserStore } from "@/lib/zustand/user"; import { useRouter } from "next/router"; @@ -21,6 +21,8 @@ import JoinAsSignerCard from "./JoinAsSignerCard"; import ManageSignerCard from "./ManageSignerCard"; import { serializeRewardAddress, deserializeAddress } from "@meshsdk/core"; import { paymentKeyHash, stakeKeyHash } from "@/utils/multisigSDK"; +import { getProvider } from "@/utils/get-provider"; +import { useSiteStore } from "@/lib/zustand/site"; export default function PageNewWalletInvite() { const router = useRouter(); @@ -33,6 +35,9 @@ export default function PageNewWalletInvite() { const { user } = useUser(); const { toast } = useToast(); + const network = useSiteStore((state) => state.network); + const blockchainProvider = useMemo(() => getProvider(network), [network]); + const pathIsNewWallet = router.pathname == "/wallets/invite/[id]"; const newWalletId = pathIsNewWallet ? (router.query.id as string) : undefined; @@ -106,6 +111,65 @@ export default function PageNewWalletInvite() { const userPaymentHash = userAddress ? paymentKeyHash(userAddress) : ""; const userStakeHash = user?.stakeAddress ? stakeKeyHash(user.stakeAddress) : ""; + // Fallback payment key hashes derived from user's stake address payment addresses + const [stakePaymentHashes, setStakePaymentHashes] = useState([]); + const stakeFetchTriggered = useRef(false); + + // If user's own payment hash does not match any signer key-hash entries, + // fetch first few payment addresses for the user's stake address and compare their hashes + useEffect(() => { + if (!user?.stakeAddress) return; + if (!newWallet) return; + if (!newWallet.signersAddresses?.some(isNativeKeyHash)) return; + const userHashMatches = !!( + userPaymentHash && + newWallet.signersAddresses.some( + (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), + ) + ); + if (userHashMatches) return; + if (stakeFetchTriggered.current) return; + + stakeFetchTriggered.current = true; + const stakeAddr = user.stakeAddress; + blockchainProvider + .get(`/accounts/${stakeAddr}/addresses`) + .then((data: any) => { + const addresses: string[] = Array.isArray(data) + ? data + .map((d: any) => (typeof d === "string" ? d : d?.address)) + .filter((a: any) => typeof a === "string") + : []; + const firstFew = addresses.slice(0, 30); + const hashes = firstFew + .map((addr) => { + try { + return paymentKeyHash(addr); + } catch (e) { + return ""; + } + }) + .filter((h) => !!h); + setStakePaymentHashes(hashes); + }) + .catch((err: any) => { + console.error("[invite] failed fetching addresses by stake address", err); + }); + }, [user?.stakeAddress, newWallet, userPaymentHash, blockchainProvider]); + + // Combined check: does any of the user's payment hashes (own or derived) match signer key-hash entries? + const paymentHashMatchedInSigners = useMemo(() => { + if (!newWallet) return false; + const candidateHashes = [ + ...(userPaymentHash ? [userPaymentHash] : []), + ...stakePaymentHashes, + ].map((h) => h.toLowerCase()); + if (candidateHashes.length === 0) return false; + return newWallet.signersAddresses?.some( + (addr) => isNativeKeyHash(addr) && candidateHashes.includes(addr.toLowerCase()), + ); + }, [newWallet, userPaymentHash, stakePaymentHashes]); + // Calculate user role once (after newWallet is loaded) const isOwner = !!newWallet && ( newWallet.ownerAddress === userAddress || @@ -123,20 +187,12 @@ export default function PageNewWalletInvite() { const qualifiesByKeyHash = (() => { // Equal CBORs: only compare payment key hash against signersAddresses if (paymentEqualsStake) { - if (!userPaymentHash) return false; if (!newWallet.signersAddresses?.some(isNativeKeyHash)) return false; - return newWallet.signersAddresses.some( - (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), - ); + return paymentHashMatchedInSigners; } // Not equal CBORs: require BOTH payment hash in signersAddresses AND stake hash in signersStakeKeys if (paymentNotEqualsStake) { - const paymentMatch = !!( - userPaymentHash && - newWallet.signersAddresses?.some( - (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), - ) - ); + const paymentMatch = paymentHashMatchedInSigners; const stakeMatch = !!( userStakeHash && newWallet.signersStakeKeys?.some( @@ -159,27 +215,19 @@ export default function PageNewWalletInvite() { ownerUpdateTriggered.current = true; updateNewWalletOwner({ walletId: newWallet.id, ownerAddress: userAddress }); } - }, [newWallet, userAddress, user, updateNewWalletOwner]); - console.log(user, newWallet) + }, [newWallet, userAddress, user, updateNewWalletOwner, paymentHashMatchedInSigners]); + const isAlreadySigner = (() => { if (!newWallet) return false; if (userAddress && newWallet.signersAddresses.includes(userAddress)) return true; // If equal CBORs: allow payment key hash match in signersAddresses if (paymentEqualsStake) { - if (!userPaymentHash) return false; if (!newWallet.signersAddresses.some(isNativeKeyHash)) return false; - return newWallet.signersAddresses.some( - (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), - ); + return paymentHashMatchedInSigners; } // If not equal CBORs: require BOTH payment key hash in signersAddresses AND stake key match (direct or hash) if (paymentNotEqualsStake) { - const paymentMatch = !!( - userPaymentHash && - newWallet.signersAddresses.some( - (addr) => isNativeKeyHash(addr) && addr.toLowerCase() === userPaymentHash.toLowerCase(), - ) - ); + const paymentMatch = paymentHashMatchedInSigners; const stakeDirectMatch = !!( user?.stakeAddress && newWallet.signersStakeKeys?.includes(user.stakeAddress) ); @@ -206,23 +254,9 @@ export default function PageNewWalletInvite() { let didChangeAddresses = false; let didChangeStake = false; - // Debug: compute and log match status before normalization - const nativeAddressKeyHashes = newWallet.signersAddresses?.filter(isNativeKeyHash) || []; - const nativeStakeKeyHashes = newWallet.signersStakeKeys?.filter(isNativeKeyHash) || []; - const paymentAddrKeyHashMatch = !!( - userPaymentHash && nativeAddressKeyHashes.some((h) => h.toLowerCase() === userPaymentHash.toLowerCase()) - ); - const stakeKeyHashMatch = !!( - userStakeHash && nativeStakeKeyHashes.some((h) => h.toLowerCase() === userStakeHash.toLowerCase()) - ); - const stakeDirectMatch = !!( - user?.stakeAddress && newWallet.signersStakeKeys?.includes(user.stakeAddress) - ); - // If CBORs differ, wait until both payment and stake data are present so we can update both sides together if (paymentNotEqualsStake) { if (!userAddress || !user?.stakeAddress || !userPaymentHash || !userStakeHash) { - console.log("[invite] normalize: waiting for both user payment and stake info before updating (CBORs differ)"); return; } } @@ -253,14 +287,6 @@ export default function PageNewWalletInvite() { } } - // Extra logs to see which replacements will be sent - console.log("[invite] normalize: replacements computed", { - didChangeAddresses, - didChangeStake, - paymentEqualsStake, - paymentNotEqualsStake, - }); - // If CBORs are equal: allow updating addresses-only. // If CBORs differ: require both sides to change in the same mutation to keep indices aligned. if (paymentEqualsStake) { From 819e19310aecd04b101114254d5af456b2347e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Diamond?= <32074058+Andre-Diamond@users.noreply.github.com> Date: Sun, 19 Oct 2025 11:35:22 +0200 Subject: [PATCH 04/12] feat(wallet): enhance wallet invite logic with stake key updates - Introduced logic to update signers' stake keys based on derived payment hashes, improving wallet qualification accuracy. - Added checks to prevent unnecessary updates and ensure alignment with user stake addresses. - Enhanced error handling for potential failures during stake key updates, providing better feedback for users. --- .../pages/homepage/wallets/invite/index.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/components/pages/homepage/wallets/invite/index.tsx b/src/components/pages/homepage/wallets/invite/index.tsx index 432ae481..dab12c5e 100644 --- a/src/components/pages/homepage/wallets/invite/index.tsx +++ b/src/components/pages/homepage/wallets/invite/index.tsx @@ -114,6 +114,7 @@ export default function PageNewWalletInvite() { // Fallback payment key hashes derived from user's stake address payment addresses const [stakePaymentHashes, setStakePaymentHashes] = useState([]); const stakeFetchTriggered = useRef(false); + const stakeKeyUpdateTriggered = useRef(false); // If user's own payment hash does not match any signer key-hash entries, // fetch first few payment addresses for the user's stake address and compare their hashes @@ -151,6 +152,64 @@ export default function PageNewWalletInvite() { }) .filter((h) => !!h); setStakePaymentHashes(hashes); + + // If CBORs are equal and any derived payment hash matches a signer key-hash entry, + // update the corresponding index in signersStakeKeys with user's stake address + try { + if (!newWallet) return; + if (!user?.stakeAddress) return; + if (!paymentEqualsStake) return; // keep indices strictly aligned; skip stake-only updates when CBORs differ + if (stakeKeyUpdateTriggered.current) return; + + const signerAddresses = newWallet.signersAddresses || []; + const existingStakeKeys = newWallet.signersStakeKeys || []; + const lowerDerived = hashes.map((h) => h.toLowerCase()); + + const indicesToUpdate = signerAddresses + .map((addr, idx) => + isNativeKeyHash(addr) && lowerDerived.includes(addr.toLowerCase()) + ? idx + : -1, + ) + .filter((idx) => idx !== -1) + // avoid writing if already set to user's stake address + .filter((idx) => existingStakeKeys[idx] !== user.stakeAddress); + + if (indicesToUpdate.length === 0) return; + + const nextStakeKeys = signerAddresses.map((_, idx) => + indicesToUpdate.includes(idx) + ? (user.stakeAddress as string) + : existingStakeKeys[idx] ?? "", + ); + + stakeKeyUpdateTriggered.current = true; + updateNewWalletSigners( + { + walletId: newWalletId!, + // keep signer key-hash entries unchanged + signersAddresses: signerAddresses, + // write stake address at matched indices + signersStakeKeys: nextStakeKeys, + signersDRepKeys: newWallet.signersDRepKeys || [], + signersDescriptions: newWallet.signersDescriptions, + }, + { + onSuccess: async () => { + void utils.wallet.getNewWallet.invalidate({ walletId: newWalletId! }); + }, + onError: () => { + // allow retry on next render if it fails + stakeKeyUpdateTriggered.current = false; + }, + }, + ); + } catch (err) { + console.error( + "[invite] failed updating signersStakeKeys from derived payment hashes", + err, + ); + } }) .catch((err: any) => { console.error("[invite] failed fetching addresses by stake address", err); From 2844acabf57529839e9551d3483bba74ccb960e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Diamond?= <32074058+Andre-Diamond@users.noreply.github.com> Date: Sun, 19 Oct 2025 12:07:26 +0200 Subject: [PATCH 05/12] fix(wallet): update loading and disabled states for signer actions - Modified the loading state to account for both loading and external stake credential conditions, ensuring accurate UI feedback during signer actions. - Updated the disabled state for the add signer button to reflect the new loading logic, improving user experience by preventing interactions during Summon platform operations. --- src/components/pages/homepage/wallets/invite/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/homepage/wallets/invite/index.tsx b/src/components/pages/homepage/wallets/invite/index.tsx index dab12c5e..1a8a644a 100644 --- a/src/components/pages/homepage/wallets/invite/index.tsx +++ b/src/components/pages/homepage/wallets/invite/index.tsx @@ -644,14 +644,14 @@ export default function PageNewWalletInvite() { signerName={signersDescription} setSignerName={setSignerDescription} onJoin={addSigner} - loading={loading} + loading={loading || hasBothCbors} hasExternalStakeCredential={!!(newWallet as any).stakeCredentialHash} />
{/* Create Button */}