From 9c0851955e73cfdc64a8d66e9a87624d3001a877 Mon Sep 17 00:00:00 2001
From: "Emmanuel .A"
Date: Wed, 14 Oct 2026 10:44:33 +0100
Subject: [PATCH 1/2] chore: prompt drep delegation while staking works
according to plomin HF
---
package-lock.json | 157 ++++--------------
src/__tests__/tx-builders/stakingCert.test.ts | 62 ++++++-
.../wallet/staking/StakingActions/stake.tsx | 11 +-
.../StakingActions/stakingActionCard.tsx | 6 +
src/components/pages/wallet/staking/index.tsx | 48 +++++-
.../pages/wallet/staking/stakingInfoCard.tsx | 2 +
src/pages/api/v1/README.md | 1 +
src/pages/api/v1/botStakeCertificate.ts | 19 +++
src/utils/stakingCertificates.ts | 61 ++++++-
src/utils/swagger.ts | 3 +-
10 files changed, 233 insertions(+), 137 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 6878e799..4d73d425 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -217,8 +217,9 @@
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.3.tgz",
"integrity": "sha512-jMjY/S0doZnWYNV90x0jmU3B+UcrsfGYnukxYrRbj0CVvGI/MX3JbHsxSrx2d4mbnXaUsqJmAcDfoQWA6r0lOw==",
- "extraneous": true,
"license": "ISC",
+ "optional": true,
+ "peer": true,
"dependencies": {
"@panva/hkdf": "^1.1.1",
"@types/cookie": "0.6.0",
@@ -249,8 +250,9 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
- "extraneous": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -259,8 +261,9 @@
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
- "extraneous": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -269,8 +272,9 @@
"version": "10.11.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
- "extraneous": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -280,8 +284,9 @@
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz",
"integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==",
- "extraneous": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"pretty-format": "^3.8.0"
},
@@ -293,8 +298,9 @@
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
- "extraneous": true,
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/@auth/prisma-adapter": {
"version": "2.11.3",
@@ -2048,9 +2054,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2067,9 +2070,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2086,9 +2086,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2105,9 +2102,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2124,9 +2118,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2143,9 +2134,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2162,9 +2150,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2181,9 +2166,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2200,9 +2182,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2225,9 +2204,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2250,9 +2226,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2275,9 +2248,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2300,9 +2270,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2325,9 +2292,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2350,9 +2314,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2375,9 +2336,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -4511,9 +4469,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -4530,9 +4485,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -4549,9 +4501,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -4568,9 +4517,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -5100,9 +5046,6 @@
"arm"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -5124,9 +5067,6 @@
"arm"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -5148,9 +5088,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -5172,9 +5109,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -5196,9 +5130,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -5220,9 +5151,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -7096,8 +7024,9 @@
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-9.0.1.tgz",
"integrity": "sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==",
- "extraneous": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"@simplewebauthn/types": "^9.0.1"
}
@@ -7107,8 +7036,9 @@
"resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz",
"integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
- "extraneous": true,
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/@sinclair/typebox": {
"version": "0.34.52",
@@ -8154,8 +8084,9 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
- "extraneous": true,
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/@types/cors": {
"version": "2.8.19",
@@ -9022,9 +8953,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9039,9 +8967,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9056,9 +8981,6 @@
"loong64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9073,9 +8995,6 @@
"loong64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9090,9 +9009,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9107,9 +9023,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9124,9 +9037,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9141,9 +9051,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9158,9 +9065,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -9175,9 +9079,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10662,6 +10563,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/bare-ansi-escapes": {
@@ -11250,6 +11152,7 @@
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
"integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -11588,6 +11491,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -11670,6 +11574,7 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
"integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -11826,6 +11731,7 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
"license": "MIT"
},
"node_modules/confbox": {
@@ -14430,6 +14336,7 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "devOptional": true,
"license": "ISC"
},
"node_modules/grammex": {
@@ -14533,6 +14440,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -16436,6 +16344,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
@@ -17948,6 +17857,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -20229,8 +20139,9 @@
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.17.0.tgz",
"integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==",
- "extraneous": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -23322,6 +23233,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -23727,8 +23639,9 @@
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
- "extraneous": true,
"license": "ISC",
+ "optional": true,
+ "peer": true,
"bin": {
"yaml": "bin.mjs"
},
diff --git a/src/__tests__/tx-builders/stakingCert.test.ts b/src/__tests__/tx-builders/stakingCert.test.ts
index 7aa7e4f3..d06f3b50 100644
--- a/src/__tests__/tx-builders/stakingCert.test.ts
+++ b/src/__tests__/tx-builders/stakingCert.test.ts
@@ -13,6 +13,7 @@ type CertCall =
| { type: "register"; address: string }
| { type: "deregister"; address: string }
| { type: "delegate"; address: string; poolHex: string }
+ | { type: "voteDelegation"; address: string; dRep: unknown }
| { type: "withdrawal"; address: string; amount: string };
function createCertBuilderMock() {
@@ -31,6 +32,10 @@ function createCertBuilderMock() {
calls.push({ type: "delegate", address, poolHex });
return builder;
},
+ voteDelegationCertificate: (dRep: unknown, address: string) => {
+ calls.push({ type: "voteDelegation", address, dRep });
+ return builder;
+ },
certificateScript: (_scriptCbor: string) => builder,
withdrawal: (address: string, amount: string) => {
calls.push({ type: "withdrawal", address, amount });
@@ -79,11 +84,12 @@ describe("buildStakingCertificateActions", () => {
rewardAddress: REWARD_ADDRESS,
stakingScript: STAKING_SCRIPT_CBOR,
poolHex: POOL_HEX,
+ dRep: "Always Abstain",
});
actions.delegate.execute();
- expect(calls).toHaveLength(1);
+ expect(calls).toHaveLength(2);
expect(calls[0]).toMatchObject({ type: "delegate", address: REWARD_ADDRESS, poolHex: POOL_HEX });
});
@@ -94,11 +100,12 @@ describe("buildStakingCertificateActions", () => {
rewardAddress: REWARD_ADDRESS,
stakingScript: STAKING_SCRIPT_CBOR,
poolHex: POOL_HEX,
+ dRep: "Always Abstain",
});
actions.register_and_delegate.execute();
- expect(calls).toHaveLength(2);
+ expect(calls).toHaveLength(3);
expect(calls[0]).toMatchObject({ type: "register", address: REWARD_ADDRESS });
expect(calls[1]).toMatchObject({ type: "delegate", address: REWARD_ADDRESS, poolHex: POOL_HEX });
});
@@ -110,6 +117,7 @@ describe("buildStakingCertificateActions", () => {
rewardAddress: REWARD_ADDRESS,
stakingScript: STAKING_SCRIPT_CBOR,
poolHex: "",
+ dRep: "Always Abstain",
});
expect(() => actions.delegate.execute()).not.toThrow();
@@ -157,11 +165,12 @@ describe("buildStakingActionConfigs", () => {
stakingScript: STAKING_SCRIPT_CBOR,
poolHex: POOL_HEX,
rewards: "0",
+ dRep: "Always Abstain",
});
configs.registerAndDelegate.execute();
- expect(calls).toHaveLength(2);
+ expect(calls).toHaveLength(3);
expect(calls[0]).toMatchObject({ type: "register", address: REWARD_ADDRESS });
expect(calls[1]).toMatchObject({ type: "delegate", address: REWARD_ADDRESS, poolHex: POOL_HEX });
});
@@ -184,3 +193,50 @@ describe("buildStakingActionConfigs", () => {
}
});
});
+
+describe("staking requires a DRep", () => {
+ const base = {
+ rewardAddress: REWARD_ADDRESS,
+ stakingScript: STAKING_SCRIPT_CBOR,
+ poolHex: POOL_HEX,
+ };
+
+ it("delegate throws without a DRep", () => {
+ const { builder } = createCertBuilderMock();
+ const actions = buildStakingCertificateActions({ txBuilder: builder as never, ...base });
+ expect(() => actions.delegate.execute()).toThrow(/requires a DRep/);
+ expect(() => actions.register_and_delegate.execute()).toThrow(/requires a DRep/);
+ });
+
+ it("delegate emits pool then vote delegation (abstain / no-confidence)", () => {
+ for (const [dRep, expected] of [
+ ["Always Abstain", { alwaysAbstain: null }],
+ ["Always No Confidence", { alwaysNoConfidence: null }],
+ ] as const) {
+ const { builder, calls } = createCertBuilderMock();
+ buildStakingCertificateActions({ txBuilder: builder as never, ...base, dRep }).delegate.execute();
+ expect(calls.map((c) => c.type)).toEqual(["delegate", "voteDelegation"]);
+ expect(calls[1]).toMatchObject({ dRep: expected });
+ }
+ });
+
+ it("register_and_delegate emits register, delegate, vote delegation", () => {
+ const { builder, calls } = createCertBuilderMock();
+ buildStakingCertificateActions({
+ txBuilder: builder as never,
+ ...base,
+ dRep: "Always Abstain",
+ }).register_and_delegate.execute();
+ expect(calls.map((c) => c.type)).toEqual(["register", "delegate", "voteDelegation"]);
+ });
+
+ it("skips the vote delegation when the stake credential already has a DRep", () => {
+ const { builder, calls } = createCertBuilderMock();
+ buildStakingCertificateActions({
+ txBuilder: builder as never,
+ ...base,
+ dRepAlreadyDelegated: true,
+ }).delegate.execute();
+ expect(calls.map((c) => c.type)).toEqual(["delegate"]);
+ });
+});
diff --git a/src/components/pages/wallet/staking/StakingActions/stake.tsx b/src/components/pages/wallet/staking/StakingActions/stake.tsx
index 944d2122..bab4b4f6 100644
--- a/src/components/pages/wallet/staking/StakingActions/stake.tsx
+++ b/src/components/pages/wallet/staking/StakingActions/stake.tsx
@@ -20,6 +20,7 @@ export default function StakeButton({
utxos,
network,
poolHex,
+ dRep,
action,
}: {
stakingInfo: StakingInfo;
@@ -28,10 +29,12 @@ export default function StakeButton({
utxos: UTxO[];
network: number;
poolHex: string;
+ dRep?: string;
action: StakingAction;
}) {
const { newTransaction } = useTransaction();
const [loading, setLoading] = useState(false);
+ const needsDRep = action === "delegate" || action === "registerAndDelegate";
async function handleStake() {
setLoading(true);
@@ -65,6 +68,8 @@ export default function StakeButton({
stakingScript,
poolHex,
rewards: stakingInfo.rewards,
+ dRep,
+ dRepAlreadyDelegated: !!stakingInfo.dRepId,
});
const actionConfig = actionConfigs[action];
@@ -122,7 +127,11 @@ export default function StakeButton({
}
return (
-
)}
+ {!stakingInfo.dRepId && (
+
+
+
+ {dRepMode === "custom" && (
+
setCustomDRep(e.target.value)}
+ placeholder="drep1..."
+ className="mt-2 w-full rounded border px-3 py-2 text-sm"
+ />
+ )}
+ {dRepMode === "custom" && !dRep && customDRep && (
+
Not a valid DRep id.
+ )}
+
+ )}
{stakingInfo && appWallet && multisigWallet && selectedPoolHex && (
-
+
)}
diff --git a/src/components/pages/wallet/staking/stakingInfoCard.tsx b/src/components/pages/wallet/staking/stakingInfoCard.tsx
index 7c6973f8..0b35cc52 100644
--- a/src/components/pages/wallet/staking/stakingInfoCard.tsx
+++ b/src/components/pages/wallet/staking/stakingInfoCard.tsx
@@ -8,6 +8,8 @@ export interface StakingInfo {
balance: string;
rewards: string;
withdrawals: string;
+ /** Current on-chain vote delegation of the stake credential, if any. */
+ dRepId?: string | null;
}
export default function StakingInfoCard({ stakingInfo }: { stakingInfo: StakingInfo }) {
diff --git a/src/pages/api/v1/README.md b/src/pages/api/v1/README.md
index 11b25ddc..7406a967 100644
--- a/src/pages/api/v1/README.md
+++ b/src/pages/api/v1/README.md
@@ -109,6 +109,7 @@ A comprehensive REST API implementation for the multisig wallet application, pro
- `address`: string (required; must match JWT)
- `action`: `"register"` | `"deregister"` | `"delegate"` | `"register_and_delegate"` (required)
- `poolId`: string (required for `delegate` and `register_and_delegate`; bech32 `pool1...` or 56-character hex pool id)
+ - `dRep`: string (optional; `"Always Abstain"` (default), `"Always No Confidence"` or a `drep1...` id). Delegation always adds a vote delegation to this DRep.
- `utxoRefs`: `{ txHash: string; outputIndex: number }[]` (required)
- `description`: string (optional; defaults to a short label for the action)
- **Response**: Same as `addTransaction` — either a pending `Transaction` row (**201**) when multiple signatures are required, or the immediate **`submitTx`** result when the wallet submits in one step (single signer / `type === "any"`).
diff --git a/src/pages/api/v1/botStakeCertificate.ts b/src/pages/api/v1/botStakeCertificate.ts
index 9ff77542..f40f9b73 100644
--- a/src/pages/api/v1/botStakeCertificate.ts
+++ b/src/pages/api/v1/botStakeCertificate.ts
@@ -13,8 +13,10 @@ import { buildMultisigWallet, buildWallet, getWalletType } from "@/utils/common"
import { getTxBuilder } from "@/utils/get-tx-builder";
import {
buildStakingCertificateActions,
+ isValidStakingDRep,
type StakingActionApi,
} from "@/utils/stakingCertificates";
+import { AUTO_ABSTAIN_DREP } from "@/lib/tx-draft/drep";
import { normalizePoolIdForDelegation } from "@/utils/normalizePoolId";
import { resolveWalletScriptAddress } from "@/lib/server/walletScriptAddress";
import { resolveUtxoRefsFromChain } from "@/lib/server/resolveUtxoRefsFromChain";
@@ -81,6 +83,8 @@ export default async function handler(
address?: string;
action?: string;
poolId?: string;
+ /** "Always Abstain" (default), "Always No Confidence" or drep1... — used when delegating. */
+ dRep?: string;
utxoRefs?: { txHash: string; outputIndex: number }[];
description?: string;
};
@@ -109,6 +113,20 @@ export default async function handler(
) {
return res.status(400).json({ error: "poolId is required for this action" });
}
+ // Optional for backwards compatibility; delegation always pairs a DRep, defaulting to Auto-Abstain.
+ const dRep =
+ typeof body.dRep === "string" && body.dRep.trim()
+ ? body.dRep.trim()
+ : AUTO_ABSTAIN_DREP;
+ if (
+ (action === "delegate" || action === "register_and_delegate") &&
+ !isValidStakingDRep(dRep)
+ ) {
+ return res.status(400).json({
+ error:
+ 'Invalid dRep: use "Always Abstain", "Always No Confidence" or a drep1... id',
+ });
+ }
try {
await authorizeWalletSignerForV1Tx(payload, walletId, address);
@@ -205,6 +223,7 @@ export default async function handler(
rewardAddress,
stakingScript,
poolHex,
+ dRep,
});
certActions[action].execute();
txBuilder.changeAddress(mWallet.getScript().address);
diff --git a/src/utils/stakingCertificates.ts b/src/utils/stakingCertificates.ts
index 2642d650..ec6a9787 100644
--- a/src/utils/stakingCertificates.ts
+++ b/src/utils/stakingCertificates.ts
@@ -1,4 +1,21 @@
import { getTxBuilder } from "@/utils/get-tx-builder";
+import { AUTO_ABSTAIN_DREP, isValidDrepId } from "@/lib/tx-draft/drep";
+
+/** Draft value for the protocol Always-No-Confidence DRep. */
+export const NO_CONFIDENCE_DREP = "Always No Confidence";
+
+/** Auto-Abstain, No-Confidence, or a drep1... id. */
+export type StakingDRepChoice = string;
+
+export function isValidStakingDRep(choice: string): boolean {
+ return choice === NO_CONFIDENCE_DREP || isValidDrepId(choice);
+}
+
+function toMeshDRep(choice: StakingDRepChoice) {
+ if (choice === AUTO_ABSTAIN_DREP) return { alwaysAbstain: null };
+ if (choice === NO_CONFIDENCE_DREP) return { alwaysNoConfidence: null };
+ return { dRepId: choice };
+}
export type StakingActionApi =
| "register"
@@ -26,12 +43,37 @@ export function buildStakingCertificateActions({
rewardAddress,
stakingScript,
poolHex,
+ dRep,
+ dRepAlreadyDelegated,
}: {
txBuilder: Awaited>;
rewardAddress: string;
stakingScript: string;
poolHex: string;
+ /** The stake credential already has an on-chain vote delegation, so none is added. */
+ dRepAlreadyDelegated?: boolean;
+ /** Required for delegate / register_and_delegate: stake never goes to a pool without a DRep. */
+ dRep?: StakingDRepChoice;
}): Record {
+ const delegateWithDRep = () => {
+ if (dRepAlreadyDelegated) {
+ txBuilder
+ .delegateStakeCertificate(rewardAddress, poolHex)
+ .certificateScript(stakingScript);
+ return;
+ }
+ if (!dRep || !isValidStakingDRep(dRep)) {
+ throw new Error(
+ "Staking requires a DRep: choose Auto-Abstain, No-Confidence, or a drep1... id.",
+ );
+ }
+ txBuilder
+ .delegateStakeCertificate(rewardAddress, poolHex)
+ .certificateScript(stakingScript);
+ txBuilder
+ .voteDelegationCertificate(toMeshDRep(dRep), rewardAddress)
+ .certificateScript(stakingScript);
+ };
return {
register: {
execute: () =>
@@ -48,22 +90,17 @@ export function buildStakingCertificateActions({
description: "Deregister stake.",
},
delegate: {
- execute: () =>
- txBuilder
- .delegateStakeCertificate(rewardAddress, poolHex)
- .certificateScript(stakingScript),
- description: "Delegate stake.",
+ execute: delegateWithDRep,
+ description: "Delegate stake and vote.",
},
register_and_delegate: {
execute: () => {
txBuilder
.registerStakeCertificate(rewardAddress)
.certificateScript(stakingScript);
- txBuilder
- .delegateStakeCertificate(rewardAddress, poolHex)
- .certificateScript(stakingScript);
+ delegateWithDRep();
},
- description: "Register & delegate stake.",
+ description: "Register & delegate stake and vote.",
},
};
}
@@ -75,18 +112,24 @@ export function buildStakingActionConfigs({
stakingScript,
poolHex,
rewards,
+ dRep,
+ dRepAlreadyDelegated,
}: {
txBuilder: Awaited>;
rewardAddress: string;
stakingScript: string;
poolHex: string;
rewards: string;
+ dRep?: StakingDRepChoice;
+ dRepAlreadyDelegated?: boolean;
}): Record {
const base = buildStakingCertificateActions({
txBuilder,
rewardAddress,
stakingScript,
poolHex,
+ dRep,
+ dRepAlreadyDelegated,
});
return {
register: {
diff --git a/src/utils/swagger.ts b/src/utils/swagger.ts
index 978f7432..6ed3a96e 100644
--- a/src/utils/swagger.ts
+++ b/src/utils/swagger.ts
@@ -339,7 +339,7 @@ This API uses **Bearer Token** authentication (JWT).
tags: ["V1"],
summary: "Build stake certificate transaction (SDK multisig)",
description:
- "Server builds register/delegate/deregister stake transactions using Mesh (same as UI). Requires wallet signer JWT; bots need cosigner access and multisig:sign scope. Body must include utxoRefs (txHash + outputIndex) resolved from chain; use GET /api/v1/freeUtxos to pick inputs. poolId is required for delegate and register_and_delegate (bech32 pool1... or 56-char hex).",
+ "Server builds register/delegate/deregister stake transactions using Mesh (same as UI). Requires wallet signer JWT; bots need cosigner access and multisig:sign scope. Body must include utxoRefs (txHash + outputIndex) resolved from chain; use GET /api/v1/freeUtxos to pick inputs. poolId is required for delegate and register_and_delegate (bech32 pool1... or 56-char hex). Delegation also adds a vote delegation to dRep (optional: \"Always Abstain\" default, \"Always No Confidence\", or drep1...).",
requestBody: {
required: true,
content: {
@@ -354,6 +354,7 @@ This API uses **Bearer Token** authentication (JWT).
enum: ["register", "deregister", "delegate", "register_and_delegate"],
},
poolId: { type: "string" },
+ dRep: { type: "string" },
utxoRefs: {
type: "array",
items: {
From d77ab392f16c0b92a6754cdd04ee68f67069b490 Mon Sep 17 00:00:00 2001
From: "Emmanuel .A"
Date: Wed, 14 Oct 2026 10:57:57 +0100
Subject: [PATCH 2/2] fix: wallet switch after signin results to signer
verification failure
---
.../wallet/info/signers/card-show-signers.tsx | 36 +++++++++++++------
1 file changed, 26 insertions(+), 10 deletions(-)
diff --git a/src/components/pages/wallet/info/signers/card-show-signers.tsx b/src/components/pages/wallet/info/signers/card-show-signers.tsx
index f6887c2e..ace4355d 100644
--- a/src/components/pages/wallet/info/signers/card-show-signers.tsx
+++ b/src/components/pages/wallet/info/signers/card-show-signers.tsx
@@ -104,12 +104,25 @@ export default function ShowSigners({ appWallet }: ShowSignersProps) {
const { mutate: updateWalletVerifiedList } =
api.wallet.updateWalletVerifiedList.useMutation({
- onSuccess: async () => {
- toast({
- title: "Wallet verified",
- description: "You have successfully verified this wallet",
- duration: 5000,
- });
+ onSuccess: async (updated) => {
+ // The server only ever records the signed-in session address, and
+ // returns success even when that isn't the connected wallet's address
+ // (wallet switched after sign-in). Don't claim success in that case.
+ if (userAddress && !updated.verified.includes(userAddress)) {
+ toast({
+ title: "Verification not recorded",
+ description:
+ "Your signed-in session is for a different address than the connected wallet. Re-authorize with the connected wallet and try again.",
+ variant: "destructive",
+ duration: 8000,
+ });
+ } else {
+ toast({
+ title: "Wallet verified",
+ description: "You have successfully verified this wallet",
+ duration: 5000,
+ });
+ }
void ctx.wallet.getWallet.invalidate({
address: userAddress,
walletId: appWallet.id,
@@ -117,6 +130,12 @@ export default function ShowSigners({ appWallet }: ShowSignersProps) {
},
onError: (e) => {
console.error(e);
+ toast({
+ title: "Verification failed",
+ description: e.message,
+ variant: "destructive",
+ duration: 6000,
+ });
},
});
@@ -146,12 +165,9 @@ export default function ShowSigners({ appWallet }: ShowSignersProps) {
const result = await checkSignature(nonce, signature);
if (result) {
- const _verified = appWallet.verified;
- _verified.push(userAddress);
-
updateWalletVerifiedList({
walletId: appWallet.id,
- verified: _verified,
+ verified: [...appWallet.verified, userAddress],
});
}
}