Skip to content

Commit 55bfeb6

Browse files
committed
feat(transaction_approve): implement instruction, test and sdk
1 parent 581e260 commit 55bfeb6

File tree

16 files changed

+826
-57
lines changed

16 files changed

+826
-57
lines changed

programs/multisig/src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,12 @@ pub enum MultisigError {
2020
NotAMember,
2121
#[msg("TransactionMessage is malformed.")]
2222
InvalidTransactionMessage,
23+
#[msg("Transaction is stale.")]
24+
StaleTransaction,
25+
#[msg("Invalid transaction status.")]
26+
InvalidTransactionStatus,
27+
#[msg("Transaction does not belong to the multisig.")]
28+
TransactionNotForMultisig,
29+
#[msg("Member already approved the transaction.")]
30+
AlreadyApproved,
2331
}

programs/multisig/src/events.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,15 @@ pub struct TransactionCreatedEvent {
4444
/// Memo that was added by the creator.
4545
pub memo: Option<String>,
4646
}
47+
48+
/// New multisig transaction account is created.
49+
#[event]
50+
pub struct TransactionApprovedEvent {
51+
/// The multisig account.
52+
pub multisig: Pubkey,
53+
/// The transaction account.
54+
pub transaction: Pubkey,
55+
#[index]
56+
/// Memo that was added by the creator.
57+
pub memo: Option<String>,
58+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
pub use multisig_config::*;
22
pub use multisig_create::*;
33
pub use transaction_create::*;
4+
pub use transaction_vote::*;
45

56
mod multisig_config;
67
mod multisig_create;
78
mod transaction_create;
9+
mod transaction_vote;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use anchor_lang::prelude::*;
2+
3+
use crate::errors::*;
4+
use crate::events::*;
5+
use crate::state::*;
6+
7+
#[derive(AnchorSerialize, AnchorDeserialize)]
8+
pub struct TransactionVoteArgs {
9+
pub memo: Option<String>,
10+
}
11+
12+
#[derive(Accounts)]
13+
pub struct TransactionVote<'info> {
14+
#[account(
15+
seeds = [SEED_PREFIX, multisig.create_key.as_ref(), SEED_MULTISIG],
16+
bump = multisig.bump,
17+
)]
18+
pub multisig: Account<'info, Multisig>,
19+
20+
#[account(
21+
mut,
22+
seeds = [
23+
SEED_PREFIX,
24+
multisig.key().as_ref(),
25+
&transaction.transaction_index.to_le_bytes(),
26+
b"transaction"
27+
],
28+
bump = transaction.bump,
29+
constraint = transaction.status == TransactionStatus::Active @ MultisigError::InvalidTransactionStatus,
30+
constraint = transaction.transaction_index > multisig.stale_transaction_index @ MultisigError::StaleTransaction,
31+
constraint = transaction.multisig == multisig.key() @ MultisigError::TransactionNotForMultisig
32+
)]
33+
pub transaction: Account<'info, MultisigTransaction>,
34+
35+
#[account(
36+
mut,
37+
constraint = multisig.is_member(member.key()).is_some() @ MultisigError::NotAMember,
38+
constraint = multisig.member_has_permission(member.key(), Permission::Vote) @MultisigError::Unauthorized,
39+
)]
40+
pub member: Signer<'info>,
41+
}
42+
43+
impl TransactionVote<'_> {
44+
/// Approve the transaction on behalf of the `member`.
45+
/// The transaction must be `Active`.
46+
pub fn transaction_approve(ctx: Context<Self>, args: TransactionVoteArgs) -> Result<()> {
47+
let multisig = &mut ctx.accounts.multisig;
48+
let transaction = &mut ctx.accounts.transaction;
49+
let member = &mut ctx.accounts.member;
50+
51+
require!(
52+
transaction.has_voted_approve(member.key()).is_none(),
53+
MultisigError::AlreadyApproved
54+
);
55+
56+
// If `member` has previously voted to reject, remove that vote.
57+
if let Some(vote_index) = transaction.has_voted_reject(member.key()) {
58+
transaction.remove_rejection_vote(vote_index);
59+
}
60+
61+
transaction.approve(member.key());
62+
63+
// If current number of approvals reaches threshold, mark the transaction as `ExecuteReady`.
64+
if transaction.approved.len() >= usize::from(multisig.threshold) {
65+
transaction.status = TransactionStatus::ExecuteReady;
66+
}
67+
68+
emit!(TransactionApprovedEvent {
69+
multisig: multisig.key(),
70+
transaction: transaction.key(),
71+
memo: args.memo,
72+
});
73+
74+
Ok(())
75+
}
76+
}

programs/multisig/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ pub mod multisig {
3535
TransactionCreate::transaction_create(ctx, args)
3636
}
3737

38+
/// Approve the transaction on behalf of the `member`.
39+
/// The transaction must be `Active`.
40+
pub fn transaction_approve(
41+
ctx: Context<TransactionVote>,
42+
args: TransactionVoteArgs,
43+
) -> Result<()> {
44+
TransactionVote::transaction_approve(ctx, args)
45+
}
46+
3847
// // instruction to remove a member/key from the multisig
3948
// pub fn remove_member(ctx: Context<MsAuth>, old_member: Pubkey) -> Result<()> {
4049
// // if there is only one key in this multisig, reject the removal

programs/multisig/src/state.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -189,24 +189,21 @@ impl MultisigTransaction {
189189
}
190190

191191
/// Approve the transaction.
192-
pub fn approve(&mut self, member: Pubkey) -> Result<()> {
192+
pub fn approve(&mut self, member: Pubkey) {
193193
self.approved.push(member);
194194
self.approved.sort();
195-
Ok(())
196195
}
197196

198197
/// Reject the transaction.
199-
pub fn reject(&mut self, member: Pubkey) -> Result<()> {
198+
pub fn reject(&mut self, member: Pubkey) {
200199
self.rejected.push(member);
201200
self.rejected.sort();
202-
Ok(())
203201
}
204202

205203
/// Cancel the transaction if execute_ready.
206-
pub fn cancel(&mut self, member: Pubkey) -> Result<()> {
204+
pub fn cancel(&mut self, member: Pubkey) {
207205
self.cancelled.push(member);
208206
self.cancelled.sort();
209-
Ok(())
210207
}
211208

212209
/// Check if the member has already voted.
@@ -217,30 +214,31 @@ impl MultisigTransaction {
217214
}
218215

219216
/// Check if the member approved the transaction.
217+
/// Returns `Some(index)` if `member` has approved the transaction, with `index` into the `approved` vec.
220218
pub fn has_voted_approve(&self, member: Pubkey) -> Option<usize> {
221219
self.approved.binary_search(&member).ok()
222220
}
223221

224222
/// Check if the member rejected the transaction.
223+
/// Returns `Some(index)` if `member` has rejected the transaction, with `index` into the `rejected` vec.
225224
pub fn has_voted_reject(&self, member: Pubkey) -> Option<usize> {
226225
self.rejected.binary_search(&member).ok()
227226
}
228227

229228
/// Check if a user has signed to cancel
229+
/// Returns `Some(index)` if `member` has cancelled the transaction, with `index` into the `cancelled` vec.
230230
pub fn has_cancelled(&self, member: Pubkey) -> Option<usize> {
231231
self.cancelled.binary_search(&member).ok()
232232
}
233233

234234
/// Delete the vote of rejection at the `index`.
235-
pub fn remove_rejection_vote(&mut self, index: usize) -> Result<()> {
235+
pub fn remove_rejection_vote(&mut self, index: usize) {
236236
self.rejected.remove(index);
237-
Ok(())
238237
}
239238

240239
/// Delete the vote of approval at the `index`.
241-
pub fn remove_approval_vote(&mut self, index: usize) -> Result<()> {
240+
pub fn remove_approval_vote(&mut self, index: usize) {
242241
self.approved.remove(index);
243-
Ok(())
244242
}
245243
}
246244

programs/multisig/src/utils/small_vec.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ mod test {
6262
0x09, // vec[1]
6363
][..];
6464

65-
let small_vec: SmallVec<u8, u8> = AnchorDeserialize::deserialize(&mut input).unwrap();
65+
let small_vec: SmallVec<u8, u8> = SmallVec::deserialize(&mut input).unwrap();
6666

6767
assert_eq!(small_vec.0, vec![5, 9]);
6868
}
@@ -75,7 +75,7 @@ mod test {
7575
0x09, 0x00, 0x00, 0x00, // vec[1]
7676
][..];
7777

78-
let small_vec: SmallVec<u8, u32> = AnchorDeserialize::deserialize(&mut input).unwrap();
78+
let small_vec: SmallVec<u8, u32> = SmallVec::deserialize(&mut input).unwrap();
7979

8080
assert_eq!(small_vec.0, vec![5, 9]);
8181
}
@@ -91,7 +91,7 @@ mod test {
9191
]
9292
.concat()[..];
9393

94-
let small_vec: SmallVec<u8, Pubkey> = AnchorDeserialize::deserialize(&mut input).unwrap();
94+
let small_vec: SmallVec<u8, Pubkey> = SmallVec::deserialize(&mut input).unwrap();
9595

9696
assert_eq!(small_vec.0, vec![pubkey1, pubkey2]);
9797
}
@@ -104,7 +104,7 @@ mod test {
104104
0x09, // vec[1]
105105
][..];
106106

107-
let small_vec: SmallVec<u16, u8> = AnchorDeserialize::deserialize(&mut input).unwrap();
107+
let small_vec: SmallVec<u16, u8> = SmallVec::deserialize(&mut input).unwrap();
108108

109109
assert_eq!(small_vec.0, vec![5, 9]);
110110
}
@@ -120,7 +120,7 @@ mod test {
120120
]
121121
.concat()[..];
122122

123-
let small_vec: SmallVec<u16, Pubkey> = AnchorDeserialize::deserialize(&mut input).unwrap();
123+
let small_vec: SmallVec<u16, Pubkey> = SmallVec::deserialize(&mut input).unwrap();
124124

125125
assert_eq!(small_vec.0, vec![pubkey1, pubkey2]);
126126
}

sdk/multisig/idl/multisig.json

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,38 @@
105105
}
106106
}
107107
]
108+
},
109+
{
110+
"name": "transactionApprove",
111+
"docs": [
112+
"Approve the transaction on behalf of the `member`.",
113+
"The transaction must be `Active`."
114+
],
115+
"accounts": [
116+
{
117+
"name": "multisig",
118+
"isMut": false,
119+
"isSigner": false
120+
},
121+
{
122+
"name": "transaction",
123+
"isMut": true,
124+
"isSigner": false
125+
},
126+
{
127+
"name": "member",
128+
"isMut": true,
129+
"isSigner": true
130+
}
131+
],
132+
"args": [
133+
{
134+
"name": "args",
135+
"type": {
136+
"defined": "TransactionVoteArgs"
137+
}
138+
}
139+
]
108140
}
109141
],
110142
"accounts": [
@@ -397,6 +429,20 @@
397429
]
398430
}
399431
},
432+
{
433+
"name": "TransactionVoteArgs",
434+
"type": {
435+
"kind": "struct",
436+
"fields": [
437+
{
438+
"name": "memo",
439+
"type": {
440+
"option": "string"
441+
}
442+
}
443+
]
444+
}
445+
},
400446
{
401447
"name": "Member",
402448
"type": {
@@ -679,6 +725,28 @@
679725
"index": true
680726
}
681727
]
728+
},
729+
{
730+
"name": "TransactionApprovedEvent",
731+
"fields": [
732+
{
733+
"name": "multisig",
734+
"type": "publicKey",
735+
"index": false
736+
},
737+
{
738+
"name": "transaction",
739+
"type": "publicKey",
740+
"index": false
741+
},
742+
{
743+
"name": "memo",
744+
"type": {
745+
"option": "string"
746+
},
747+
"index": true
748+
}
749+
]
682750
}
683751
],
684752
"errors": [
@@ -726,6 +794,26 @@
726794
"code": 6008,
727795
"name": "InvalidTransactionMessage",
728796
"msg": "TransactionMessage is malformed."
797+
},
798+
{
799+
"code": 6009,
800+
"name": "StaleTransaction",
801+
"msg": "Transaction is stale."
802+
},
803+
{
804+
"code": 6010,
805+
"name": "InvalidTransactionStatus",
806+
"msg": "Invalid transaction status."
807+
},
808+
{
809+
"code": 6011,
810+
"name": "TransactionNotForMultisig",
811+
"msg": "Transaction does not belong to the multisig."
812+
},
813+
{
814+
"code": 6012,
815+
"name": "AlreadyApproved",
816+
"msg": "Member already approved the transaction."
729817
}
730818
],
731819
"metadata": {

0 commit comments

Comments
 (0)