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/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], }); } } 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 ( - diff --git a/src/components/pages/wallet/staking/StakingActions/stakingActionCard.tsx b/src/components/pages/wallet/staking/StakingActions/stakingActionCard.tsx index 2aefdfdf..aa3b259c 100644 --- a/src/components/pages/wallet/staking/StakingActions/stakingActionCard.tsx +++ b/src/components/pages/wallet/staking/StakingActions/stakingActionCard.tsx @@ -13,12 +13,14 @@ export default function StakingActionCard({ mWallet, network, poolHex, + dRep, }: { stakingInfo: StakingInfo; appWallet: Wallet; mWallet: MultisigWallet; network: number; poolHex: string; + dRep?: string; }) { const [selectedUtxos, setSelectedUtxos] = useState([]); const [manualSelected, setManualSelected] = useState(false); @@ -34,6 +36,7 @@ export default function StakingActionCard({ utxos={selectedUtxos} network={network} poolHex={poolHex} + dRep={dRep} action="registerAndDelegate" /> )} @@ -46,6 +49,7 @@ export default function StakingActionCard({ utxos={selectedUtxos} network={network} poolHex={poolHex} + dRep={dRep} action="delegate" /> @@ -67,6 +72,7 @@ export default function StakingActionCard({ utxos={selectedUtxos} network={network} poolHex={poolHex} + dRep={dRep} action="withdrawal" /> )} diff --git a/src/components/pages/wallet/staking/index.tsx b/src/components/pages/wallet/staking/index.tsx index a7bff60f..ff692f91 100644 --- a/src/components/pages/wallet/staking/index.tsx +++ b/src/components/pages/wallet/staking/index.tsx @@ -6,6 +6,10 @@ import { getProvider } from "@/utils/get-provider"; import StakingInfoCard, { StakingInfo } from "./stakingInfoCard"; import PoolSelector from "./poolSelector"; import useAppWallet from "@/hooks/useAppWallet"; +import { AUTO_ABSTAIN_DREP } from "@/lib/tx-draft/drep"; +import { NO_CONFIDENCE_DREP, isValidStakingDRep } from "@/utils/stakingCertificates"; + +type DRepMode = "abstain" | "noConfidence" | "custom"; export default function PageStaking() { const { multisigWallet } = useMultisigWallet(); @@ -19,6 +23,19 @@ export default function PageStaking() { const [stakingInfo, setStakingInfo] = useState(null); const [selectedPoolHex, setSelectedPoolHex] = useState(null); + const [dRepMode, setDRepMode] = useState("abstain"); + const [customDRep, setCustomDRep] = useState(""); + + // Staking always pairs the pool with a DRep; undefined keeps the actions disabled. + const dRep: string | undefined = + dRepMode === "abstain" + ? AUTO_ABSTAIN_DREP + : dRepMode === "noConfidence" + ? NO_CONFIDENCE_DREP + : isValidStakingDRep(customDRep.trim()) + ? customDRep.trim() + : undefined; + const address = multisigWallet?.getStakeAddress(); useEffect(() => { @@ -32,6 +49,7 @@ export default function PageStaking() { balance: data.controlled_amount, rewards: data.rewards_sum, withdrawals: data.withdrawals_sum, + dRepId: data.drep_id ?? null, }); }) .catch((err) => { @@ -74,10 +92,38 @@ export default function PageStaking() {

)} + {!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: {