Skip to content

Commit 91db70a

Browse files
fix(sdk): masternodeVote takes the ProTxHash as an Identifier and returns the vote (#5138)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 99207ac commit 91db70a

4 files changed

Lines changed: 91 additions & 65 deletions

File tree

‎packages/js-evo-sdk/src/voting/facade.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class VotingFacade {
5353
return w.getVotePollsByEndDateWithProofInfo(query);
5454
}
5555

56-
async masternodeVote(options: wasm.MasternodeVoteOptions): Promise<void> {
56+
async masternodeVote(options: wasm.MasternodeVoteOptions): Promise<wasm.Vote> {
5757
const w = await this.sdk.getWasmSdkConnected();
5858
return w.masternodeVote(options);
5959
}

‎packages/js-evo-sdk/tests/unit/facades/voting.spec.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe('VotingFacade', () => {
2121
let getVotePollsByEndDateStub: SinonStub;
2222
let getVotePollsByEndDateWithProofInfoStub: SinonStub;
2323
let masternodeVoteStub: SinonStub;
24+
let recordedVote: wasmSDKPackage.Vote;
2425

2526
beforeEach(async function setup() {
2627
await init();
@@ -60,9 +61,8 @@ describe('VotingFacade', () => {
6061
});
6162

6263
// Stub transition method
63-
masternodeVoteStub = this.sinon.stub(wasmSdk, 'masternodeVote').resolves({
64-
success: true,
65-
});
64+
recordedVote = Object.create(wasmSDKPackage.Vote.prototype);
65+
masternodeVoteStub = this.sinon.stub(wasmSdk, 'masternodeVote').resolves(recordedVote);
6666
});
6767

6868
describe('contestedResourceVoteState()', () => {
@@ -167,7 +167,7 @@ describe('VotingFacade', () => {
167167
});
168168

169169
describe('masternodeVote()', () => {
170-
it('should cast a vote on a contested resource', async () => {
170+
it('should cast a vote on a contested resource and return the recorded vote', async () => {
171171
const options = {
172172
masternodeProTxHash,
173173
contractId: dataContractId,
@@ -178,9 +178,10 @@ describe('VotingFacade', () => {
178178
signer,
179179
};
180180

181-
await client.voting.masternodeVote(options);
181+
const vote = await client.voting.masternodeVote(options);
182182

183183
expect(masternodeVoteStub).to.be.calledOnceWithExactly(options);
184+
expect(vote).to.equal(recordedVote);
184185
});
185186

186187
it('should support abstain vote choice', async () => {

‎packages/wasm-sdk/src/state_transitions/identity.rs‎

Lines changed: 34 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@ use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicK
1212
use dash_sdk::dpp::identity::signer::Signer;
1313
use dash_sdk::dpp::identity::{Identity, IdentityPublicKey, KeyType, Purpose, SecurityLevel};
1414
use dash_sdk::dpp::platform_value::Identifier;
15+
use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice;
16+
use dash_sdk::dpp::voting::vote_polls::VotePoll;
17+
use dash_sdk::dpp::voting::votes::resource_vote::v0::ResourceVoteV0;
18+
use dash_sdk::dpp::voting::votes::resource_vote::ResourceVote;
19+
use dash_sdk::dpp::voting::votes::Vote;
1520
use dash_sdk::platform::transition::broadcast::BroadcastStateTransition;
1621
use dash_sdk::platform::transition::put_identity::PutIdentity;
1722
use dash_sdk::platform::transition::top_up_identity::TopUpIdentity;
1823
use dash_sdk::platform::transition::update_identity_key_limits::{
1924
raised_key_limits, UpdateIdentityKeyLimits,
2025
};
26+
use dash_sdk::platform::transition::vote::PutVote;
2127
use js_sys::BigInt;
2228
use serde::Deserialize;
2329
use wasm_bindgen::prelude::*;
@@ -28,6 +34,9 @@ use wasm_dpp2::utils::{
2834
try_from_options_optional, try_from_options_optional_with, try_from_options_with, try_to_array,
2935
try_to_u64, IntoWasm,
3036
};
37+
use wasm_dpp2::voting::resource_vote_choice::ResourceVoteChoiceWasm;
38+
use wasm_dpp2::voting::vote::VoteWasm;
39+
use wasm_dpp2::voting::vote_poll::VotePollWasm;
3140
use wasm_dpp2::PrivateKeyWasm;
3241
use wasm_dpp2::{IdentityPublicKeyInCreationWasm, IdentitySignerWasm, IdentityWasm};
3342

@@ -714,9 +723,10 @@ const MASTERNODE_VOTE_OPTIONS_TS: &'static str = r#"
714723
*/
715724
export interface MasternodeVoteOptions {
716725
/**
717-
* The ProTxHash of the masternode.
726+
* The ProTxHash of the masternode: an Identifier, its 32 bytes, the hex Core's RPC shows,
727+
* or base58.
718728
*/
719-
masternodeProTxHash: Identifier;
729+
masternodeProTxHash: IdentifierLike;
720730
721731
/**
722732
* The vote poll to vote on.
@@ -756,84 +766,49 @@ extern "C" {
756766
pub type MasternodeVoteOptionsJs;
757767
}
758768

759-
/// Main input struct for masternode vote options.
760-
#[derive(Deserialize)]
761-
#[serde(rename_all = "camelCase")]
762-
struct MasternodeVoteOptionsInput {
763-
masternode_pro_tx_hash: IdentifierWasm,
764-
}
765-
766-
fn deserialize_masternode_vote_options(
767-
options: JsValue,
768-
) -> Result<MasternodeVoteOptionsInput, WasmSdkError> {
769-
deserialize_required_query(
770-
options,
771-
"Options object is required",
772-
"masternode vote options",
773-
)
774-
}
775-
776769
#[wasm_bindgen]
777770
impl WasmSdk {
778771
/// Submit a masternode vote for a contested resource.
779772
///
780773
/// This method handles the complete voting flow:
781-
/// 1. Creates the voting public key from the signer
782-
/// 2. Builds and signs the vote transition
783-
/// 3. Broadcasts and waits for confirmation
774+
/// 1. Builds and signs the vote transition with the voting key
775+
/// 2. Broadcasts it and waits for the block that records it
776+
/// 3. Verifies the proof of the recorded vote
784777
///
785-
/// @param options - Vote options including masternode ID, vote poll, choice, and signer
786-
/// @returns Promise that resolves when the vote is submitted
778+
/// @param options - Vote options including masternode ProTxHash, vote poll, choice, and signer
779+
/// @returns The vote as Platform recorded it
787780
#[wasm_bindgen(js_name = "masternodeVote")]
788781
pub async fn masternode_vote(
789782
&self,
790783
options: MasternodeVoteOptionsJs,
791-
) -> Result<(), WasmSdkError> {
792-
use wasm_dpp2::voting::resource_vote_choice::ResourceVoteChoiceWasm;
793-
use wasm_dpp2::voting::vote_poll::VotePollWasm;
794-
795-
// Extract complex types first (borrows &options)
796-
let vote_poll: dash_sdk::dpp::voting::vote_polls::VotePoll =
797-
VotePollWasm::try_from_options(&options, "votePoll")?.into();
798-
let resource_vote_choice: dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice =
784+
) -> Result<VoteWasm, WasmSdkError> {
785+
let pro_tx_hash: Identifier =
786+
IdentifierWasm::try_from_options(&options, "masternodeProTxHash")?.into();
787+
let vote_poll: VotePoll = VotePollWasm::try_from_options(&options, "votePoll")?.into();
788+
let resource_vote_choice: ResourceVoteChoice =
799789
ResourceVoteChoiceWasm::try_from_options(&options, "voteChoice")?.into();
800790
let voting_public_key: IdentityPublicKey =
801791
IdentityPublicKeyWasm::try_from_options(&options, "votingKey")?.into();
802792
let signer = IdentitySignerWasm::try_from_options(&options, "signer")?;
803793
let settings =
804794
try_from_options_optional::<PutSettingsInput>(&options, "settings")?.map(Into::into);
805795

806-
// Deserialize simple fields last (consumes options)
807-
let parsed = deserialize_masternode_vote_options(options.into())?;
808-
809-
// Convert ProTxHash
810-
let pro_tx_hash: Identifier = parsed.masternode_pro_tx_hash.into();
811-
812-
// Create the resource vote
813-
use dash_sdk::dpp::voting::votes::resource_vote::v0::ResourceVoteV0;
814-
use dash_sdk::dpp::voting::votes::resource_vote::ResourceVote;
815-
let resource_vote = ResourceVote::V0(ResourceVoteV0 {
796+
let vote = Vote::ResourceVote(ResourceVote::V0(ResourceVoteV0 {
816797
vote_poll,
817798
resource_vote_choice,
818-
});
799+
}));
819800

820-
// Create the vote
821-
use dash_sdk::dpp::voting::votes::Vote;
822-
let vote = Vote::ResourceVote(resource_vote);
823-
824-
// Submit the vote using PutVote trait
825-
use dash_sdk::platform::transition::vote::PutVote;
826-
827-
vote.put_to_platform(
828-
pro_tx_hash,
829-
&voting_public_key,
830-
self.inner_sdk(),
831-
&signer,
832-
settings,
833-
)
834-
.await?;
801+
let recorded_vote = vote
802+
.put_to_platform_and_wait_for_response(
803+
pro_tx_hash,
804+
&voting_public_key,
805+
self.inner_sdk(),
806+
&signer,
807+
settings,
808+
)
809+
.await?;
835810

836-
Ok(())
811+
Ok(recorded_vote.into())
837812
}
838813
}
839814

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* `masternodeVote` takes the masternode's ProTxHash as an `IdentifierLike` and reads it before
3+
* the other options, so a call whose only fault is its missing vote poll fails on the vote poll:
4+
* its ProTxHash was accepted, and nothing reached the network.
5+
*/
6+
import { expect } from './helpers/chai.ts';
7+
import init, * as sdk from '../../dist/sdk.compressed.js';
8+
9+
const proTxHashHex = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2';
10+
11+
async function rejectionOf(promise: Promise<unknown>): Promise<Error> {
12+
try {
13+
await promise;
14+
} catch (e) {
15+
return e as Error;
16+
}
17+
throw new Error('expected the vote to be refused');
18+
}
19+
20+
describe('masternodeVote()', () => {
21+
let client: sdk.WasmSdk;
22+
23+
before(async () => {
24+
await init();
25+
client = await sdk.WasmSdkBuilder.testnet().build();
26+
});
27+
28+
const accepted: Array<[string, () => unknown]> = [
29+
['an Identifier', () => new sdk.Identifier(proTxHashHex)],
30+
['its 32 bytes', () => new sdk.Identifier(proTxHashHex).toBytes()],
31+
['the hex Core shows', () => proTxHashHex],
32+
['base58', () => new sdk.Identifier(proTxHashHex).toBase58()],
33+
];
34+
35+
accepted.forEach(([form, proTxHash]) => {
36+
it(`should accept the ProTxHash as ${form}`, async () => {
37+
const error = await rejectionOf(client.masternodeVote({
38+
masternodeProTxHash: proTxHash(),
39+
} as never));
40+
expect(error.message).to.match(/'votePoll' is required/);
41+
});
42+
});
43+
44+
it('should refuse a ProTxHash that is not an identifier before the vote poll', async () => {
45+
const error = await rejectionOf(client.masternodeVote({
46+
masternodeProTxHash: 42,
47+
} as never));
48+
expect(error.message).to.not.match(/votePoll/);
49+
});
50+
});

0 commit comments

Comments
 (0)