Skip to content

Commit 3eb3fb2

Browse files
kanyukupierrepeach
authored andcommitted
feat: handle Summon wallets with capability-based metadata
- Update Wallet type to include capabilities\n- Modify buildWallet to compute capabilities for Summon, SDK, and Legacy wallets\n- Update hooks (useAppWallet, useMultisigWallet, useWalletBalances) to leverage capabilities\n- Update UI components (CardWallet, ShowInfo) to use capability-driven rendering\n- Add unit tests for Summon wallet capabilities
1 parent dfb87d2 commit 3eb3fb2

File tree

10 files changed

+203
-180
lines changed

10 files changed

+203
-180
lines changed

src/__tests__/summonWallet.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { buildWallet } from "../utils/common";
2+
import { Wallet as DbWallet } from "@prisma/client";
3+
import { RawImportBodies } from "../types/wallet";
4+
5+
describe("Summon Wallet Capabilities", () => {
6+
const network = 0; // Testnet
7+
8+
const mockSummonWallet: DbWallet & { rawImportBodies: RawImportBodies } = {
9+
id: "test-summon-uuid",
10+
name: "Test Summon Wallet",
11+
description: "A test summon wallet",
12+
address: "addr_test1wpnlxv2xv988tvv9z06m6pax76r98slymr6uzy958tclv6sgp98k8",
13+
type: "atLeast",
14+
numRequiredSigners: 2,
15+
signersAddresses: ["addr_test1vpu5vl76u73su6p0657cw6q0657cw6q0657cw6q0657cw6q0657cw"],
16+
signersStakeKeys: [],
17+
signersDRepKeys: [],
18+
scriptCbor: "8201828200581caf000000000000000000000000000000000000000000000000000000008200581cb0000000000000000000000000000000000000000000000000000000",
19+
isArchived: false,
20+
createdAt: new Date(),
21+
updatedAt: new Date(),
22+
profileImageIpfsUrl: null,
23+
stakeCredentialHash: null,
24+
dRepId: "",
25+
rawImportBodies: {
26+
multisig: {
27+
address: "addr_test1wpnlxv2xv988tvv9z06m6pax76r98slymr6uzy958tclv6sgp98k8",
28+
payment_script: "8200581c00000000000000000000000000000000000000000000000000000000",
29+
stake_script: "8200581c11111111111111111111111111111111111111111111111111111111",
30+
}
31+
}
32+
} as any;
33+
34+
it("should correctly populate capabilities for a Summon wallet with staking", () => {
35+
const wallet = buildWallet(mockSummonWallet, network);
36+
37+
expect(wallet.capabilities).toBeDefined();
38+
expect(wallet.capabilities!.canStake).toBe(true);
39+
expect(wallet.capabilities!.canVote).toBe(false);
40+
expect(wallet.capabilities!.address).toBe(mockSummonWallet.rawImportBodies.multisig!.address);
41+
expect(wallet.capabilities!.stakeAddress).toBeDefined();
42+
expect(wallet.capabilities!.stakeAddress).toMatch(/^stake_test/);
43+
});
44+
45+
it("should correctly populate capabilities for a Summon wallet without staking", () => {
46+
const mockNoStake = {
47+
...mockSummonWallet,
48+
rawImportBodies: {
49+
multisig: {
50+
...mockSummonWallet.rawImportBodies.multisig,
51+
stake_script: undefined
52+
}
53+
}
54+
};
55+
const wallet = buildWallet(mockNoStake, network);
56+
57+
expect(wallet.capabilities!.canStake).toBe(false);
58+
expect(wallet.capabilities!.stakeAddress).toBeUndefined();
59+
});
60+
});

src/components/pages/homepage/wallets/index.tsx

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ export default function PageWallets() {
5454
retry: (failureCount, error) => {
5555
// Don't retry on authorization errors (403)
5656
if (error && typeof error === "object") {
57-
const err = error as {
58-
code?: string;
59-
message?: string;
57+
const err = error as {
58+
code?: string;
59+
message?: string;
6060
data?: { code?: string; httpStatus?: number };
6161
shape?: { code?: string; message?: string };
6262
};
@@ -83,9 +83,9 @@ export default function PageWallets() {
8383
retry: (failureCount, error) => {
8484
// Don't retry on authorization errors (403)
8585
if (error && typeof error === "object") {
86-
const err = error as {
87-
code?: string;
88-
message?: string;
86+
const err = error as {
87+
code?: string;
88+
message?: string;
8989
data?: { code?: string; httpStatus?: number };
9090
shape?: { code?: string; message?: string };
9191
};
@@ -108,8 +108,8 @@ export default function PageWallets() {
108108
const walletsForBalance = useMemo(
109109
() =>
110110
wallets?.filter((wallet) => showArchived || !wallet.isArchived) as
111-
| Wallet[]
112-
| undefined,
111+
| Wallet[]
112+
| undefined,
113113
[wallets, showArchived],
114114
);
115115

@@ -257,21 +257,7 @@ function CardWallet({
257257

258258
// Rebuild the multisig wallet to get the correct canonical address for display
259259
// This ensures we show the correct address even if wallet.address was built incorrectly
260-
const displayAddress = useMemo(() => {
261-
try {
262-
const walletNetwork = wallet.signersAddresses.length > 0
263-
? addressToNetwork(wallet.signersAddresses[0]!)
264-
: network;
265-
const mWallet = buildMultisigWallet(wallet, walletNetwork);
266-
if (mWallet) {
267-
return mWallet.getScript().address;
268-
}
269-
} catch (error) {
270-
console.error(`Error building wallet for display: ${wallet.id}`, error);
271-
}
272-
// Fallback to wallet.address if rebuild fails (legacy support)
273-
return wallet.address;
274-
}, [wallet, network]);
260+
const displayAddress = wallet.capabilities?.address || wallet.address;
275261

276262
return (
277263
<Link href={`/wallets/${wallet.id}`}>
@@ -293,8 +279,8 @@ function CardWallet({
293279
}
294280
headerDom={
295281
isSummonWallet ? (
296-
<Badge
297-
variant="outline"
282+
<Badge
283+
variant="outline"
298284
className="text-xs bg-orange-600/10 border-orange-600/30 text-orange-700 dark:text-orange-400"
299285
>
300286
<Archive className="h-3 w-3 mr-1" />

src/components/pages/wallet/info/card-info.tsx

Lines changed: 58 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { getWalletType } from "@/utils/common";
4646

4747
export default function CardInfo({ appWallet }: { appWallet: Wallet }) {
4848
const [showEdit, setShowEdit] = useState(false);
49-
49+
5050
// Check if this is a legacy wallet using the centralized detection
5151
const walletType = getWalletType(appWallet);
5252
const isLegacyWallet = walletType === 'legacy';
@@ -70,8 +70,8 @@ export default function CardInfo({ appWallet }: { appWallet: Wallet }) {
7070
headerDom={
7171
<div className="flex items-center gap-2">
7272
{isLegacyWallet && (
73-
<Badge
74-
variant="outline"
73+
<Badge
74+
variant="outline"
7575
className="text-xs bg-orange-400/10 border-orange-400/30 text-orange-600 dark:text-orange-300"
7676
>
7777
<Archive className="h-3 w-3 mr-1" />
@@ -184,7 +184,7 @@ function EditInfo({
184184
initialUrl={profileImageIpfsUrl}
185185
/>
186186
<p className="text-xs text-muted-foreground">
187-
<strong>Note:</strong> Images will be stored on public IPFS (InterPlanetary File System).
187+
<strong>Note:</strong> Images will be stored on public IPFS (InterPlanetary File System).
188188
Once uploaded, the image will be publicly accessible and cannot be removed from IPFS.
189189
</p>
190190
</div>
@@ -220,17 +220,17 @@ function EditInfo({
220220
onClick={() => editWallet()}
221221
disabled={
222222
loading ||
223-
(appWallet.name === name &&
224-
appWallet.description === description &&
225-
appWallet.isArchived === isArchived &&
226-
appWallet.profileImageIpfsUrl === profileImageIpfsUrl)
223+
(appWallet.name === name &&
224+
appWallet.description === description &&
225+
appWallet.isArchived === isArchived &&
226+
appWallet.profileImageIpfsUrl === profileImageIpfsUrl)
227227
}
228228
className="flex-1 sm:flex-initial"
229229
>
230230
{loading ? "Updating Wallet..." : "Update"}
231231
</Button>
232-
<Button
233-
onClick={() => setShowEdit(false)}
232+
<Button
233+
onClick={() => setShowEdit(false)}
234234
variant="outline"
235235
className="flex-1 sm:flex-initial"
236236
>
@@ -246,7 +246,7 @@ function MultisigScriptSection({ mWallet }: { mWallet: MultisigWallet }) {
246246
const { appWallet } = useAppWallet();
247247
const walletsUtxos = useWalletsStore((state) => state.walletsUtxos);
248248
const [balance, setBalance] = useState<number>(0);
249-
249+
250250
useEffect(() => {
251251
if (!appWallet) return;
252252
const utxos = walletsUtxos[appWallet.id];
@@ -266,7 +266,7 @@ function MultisigScriptSection({ mWallet }: { mWallet: MultisigWallet }) {
266266
<Code className="block text-xs sm:text-sm whitespace-pre">{JSON.stringify(mWallet?.getJsonMetadata(), null, 2)}</Code>
267267
</div>
268268
</div>
269-
269+
270270
{/* Register Wallet Section */}
271271
<div className="pt-3 border-t border-border/30">
272272
<div className="mb-2">
@@ -377,7 +377,7 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) {
377377
const { multisigWallet } = useMultisigWallet();
378378
const walletsUtxos = useWalletsStore((state) => state.walletsUtxos);
379379
const [balance, setBalance] = useState<number>(0);
380-
380+
381381
useEffect(() => {
382382
if (!appWallet) return;
383383
const utxos = walletsUtxos[appWallet.id];
@@ -390,18 +390,12 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) {
390390
// Check if this is a legacy wallet using the centralized detection
391391
const walletType = getWalletType(appWallet);
392392
const isLegacyWallet = walletType === 'legacy';
393-
394-
// For legacy wallets, multisigWallet will be undefined, so use appWallet.address
395-
// For SDK wallets, prefer the address from multisigWallet if staking is enabled
396-
const address = multisigWallet?.getKeysByRole(2) ? multisigWallet?.getScript().address : appWallet.address;
397-
398-
// Get DRep ID from multisig wallet if available (it handles no DRep keys by using payment script),
399-
// otherwise fallback to appWallet (for legacy wallets without multisigWallet)
400-
const dRepId = multisigWallet ? multisigWallet.getDRepId() : appWallet?.dRepId;
401-
402-
// For rawImportBodies wallets, dRepId may not be available
403-
const showDRepId = dRepId && dRepId.length > 0;
404-
393+
394+
// Use capabilities for address and DRep ID (Type 2 Summon, Type 1 SDK, Type 0 Legacy)
395+
const address = appWallet.capabilities?.address || appWallet.address;
396+
const dRepId = appWallet.capabilities?.dRepId || appWallet.dRepId;
397+
const showDRepId = !!appWallet.capabilities?.canVote && !!dRepId;
398+
405399
// Calculate signers info
406400
const signersCount = appWallet.signersAddresses.length;
407401
const requiredSigners = appWallet.numRequiredSigners ?? signersCount;
@@ -414,7 +408,7 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) {
414408
return `${requiredSigners} of ${signersCount} signers`;
415409
}
416410
};
417-
411+
418412
// Get the number of required signers for visualization
419413
const getRequiredCount = () => {
420414
if (appWallet.type === 'all') {
@@ -425,40 +419,39 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) {
425419
return requiredSigners;
426420
}
427421
};
428-
422+
429423
const requiredCount = getRequiredCount();
430-
424+
431425
return (
432426
<div className="space-y-6">
433427
{/* Top Section: Key Info */}
434428
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
435-
{/* Signing Threshold */}
436-
<div className="flex items-center gap-3 p-4 bg-muted/40 rounded-lg border border-border/40">
437-
<div className="flex items-center gap-1.5 flex-shrink-0">
438-
{Array.from({ length: signersCount }).map((_, index) => (
439-
<User
440-
key={index}
441-
className={`h-4 w-4 sm:h-5 sm:w-5 ${
442-
index < requiredCount
443-
? "text-foreground opacity-100"
444-
: "text-muted-foreground opacity-30"
429+
{/* Signing Threshold */}
430+
<div className="flex items-center gap-3 p-4 bg-muted/40 rounded-lg border border-border/40">
431+
<div className="flex items-center gap-1.5 flex-shrink-0">
432+
{Array.from({ length: signersCount }).map((_, index) => (
433+
<User
434+
key={index}
435+
className={`h-4 w-4 sm:h-5 sm:w-5 ${index < requiredCount
436+
? "text-foreground opacity-100"
437+
: "text-muted-foreground opacity-30"
445438
}`}
446-
/>
447-
))}
448-
</div>
449-
<div className="flex-1 min-w-0">
450-
<div className="text-xs font-medium text-muted-foreground mb-0.5">Signing Threshold</div>
451-
<div className="text-sm font-semibold">{getSignersText()}</div>
452-
</div>
439+
/>
440+
))}
453441
</div>
454-
455-
{/* Balance */}
456-
<div className="flex flex-col justify-center p-4 bg-muted/40 rounded-lg border border-border/40">
457-
<div className="text-xs font-medium text-muted-foreground mb-1">Balance</div>
458-
<div className="text-2xl sm:text-3xl font-bold">{balance}</div>
442+
<div className="flex-1 min-w-0">
443+
<div className="text-xs font-medium text-muted-foreground mb-0.5">Signing Threshold</div>
444+
<div className="text-sm font-semibold">{getSignersText()}</div>
459445
</div>
446+
</div>
447+
448+
{/* Balance */}
449+
<div className="flex flex-col justify-center p-4 bg-muted/40 rounded-lg border border-border/40">
450+
<div className="text-xs font-medium text-muted-foreground mb-1">Balance</div>
451+
<div className="text-2xl sm:text-3xl font-bold">{balance}</div>
452+
</div>
460453
</div>
461-
454+
462455
{/* Addresses Section */}
463456
<div className="space-y-3 pt-2 border-t border-border/30">
464457
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">Wallet Details</div>
@@ -470,20 +463,17 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) {
470463
copyString={address}
471464
allowOverflow={false}
472465
/>
473-
474-
{/* Stake Address - Show if staking is enabled */}
475-
{multisigWallet && multisigWallet.stakingEnabled() && (() => {
476-
const stakeAddress = multisigWallet.getStakeAddress();
477-
return stakeAddress ? (
478-
<RowLabelInfo
479-
label="Stake Key"
480-
value={getFirstAndLast(stakeAddress, 20, 15)}
481-
copyString={stakeAddress}
482-
allowOverflow={false}
483-
/>
484-
) : null;
485-
})()}
486-
466+
467+
{/* Stake Address - Show if staking is supported (Summon or SDK) */}
468+
{appWallet.capabilities?.canStake && appWallet.capabilities?.stakeAddress && (
469+
<RowLabelInfo
470+
label="Stake Key"
471+
value={getFirstAndLast(appWallet.capabilities.stakeAddress, 20, 15)}
472+
copyString={appWallet.capabilities.stakeAddress}
473+
allowOverflow={false}
474+
/>
475+
)}
476+
487477
{/* External Stake Key Hash - Always show if available */}
488478
{appWallet?.stakeCredentialHash && (
489479
<RowLabelInfo
@@ -493,7 +483,7 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) {
493483
allowOverflow={false}
494484
/>
495485
)}
496-
486+
497487
{/* DRep ID */}
498488
{showDRepId && dRepId ? (
499489
<RowLabelInfo
@@ -511,10 +501,10 @@ function ShowInfo({ appWallet }: { appWallet: Wallet }) {
511501
) : null}
512502
</div>
513503
</div>
514-
504+
515505
{/* Native Script - Collapsible Pro Feature */}
516506
<div className="pt-2 border-t border-border/30">
517-
{multisigWallet && multisigWallet.stakingEnabled() ? (
507+
{multisigWallet && appWallet.capabilities?.canStake ? (
518508
<MultisigScriptSection mWallet={multisigWallet} />
519509
) : (
520510
<NativeScriptSection appWallet={appWallet} />

src/components/pages/wallet/info/inspect-script.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export function NativeScriptSection({ appWallet }: { appWallet: Wallet }) {
100100
</div>
101101
)}
102102

103-
{isImportedWallet && appWallet.stakeScriptCbor && (
103+
{appWallet.capabilities?.canStake && appWallet.stakeScriptCbor && (
104104
<div className="flex flex-col gap-2 sm:gap-3">
105105
<div className="text-xs sm:text-sm font-medium text-muted-foreground">Stake Script CBOR</div>
106106
<div className="overflow-x-auto -mx-4 sm:-mx-6 px-4 sm:px-6">

src/components/pages/wallet/staking/StakingActions/stake.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,12 @@ export default function StakeButton({
9999
setLoading(true);
100100
try {
101101
if (!mWallet) throw new Error("Multisig Wallet could not be built.");
102-
103-
const rewardAddress = mWallet.getStakeAddress();
102+
103+
const rewardAddress = appWallet.capabilities?.stakeAddress;
104104
if (!rewardAddress) throw new Error("Reward Address could not be built.");
105105

106-
// For wallets with rawImportBodies, use stored stake script
107-
// Otherwise, derive from MultisigWallet
108-
const stakingScript = appWallet.stakeScriptCbor || mWallet.getStakingScript();
106+
// For wallets with rawImportBodies or SDK, use stored stake script or derived
107+
const stakingScript = appWallet.stakeScriptCbor || (mWallet ? mWallet.getStakingScript() : undefined);
109108
if (!stakingScript) throw new Error("Staking Script could not be built.");
110109

111110
const txBuilder = getTxBuilder(network);

0 commit comments

Comments
 (0)