1use std::collections::BTreeMap;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use super::error::{ProtocolContractError, ProtocolResult};
14use super::intent::{AccountReadResultV1, FeePlacement};
15use super::model::PROTOCOL_HANDLER_ABI_V1;
16use super::release::decode_base64;
17use super::shape::*;
18
19pub const MAX_OPERATION_TRANSACTIONS: usize = 8;
20pub const MAX_TRANSACTION_INSTRUCTIONS: usize = 64;
21pub const MAX_INSTRUCTION_ACCOUNTS: usize = 256;
22pub const MAX_INSTRUCTION_DATA_BYTES: usize = 10 * 1024;
23pub const MAX_HANDLER_RESULT_BYTES: usize = 2 * 1024 * 1024;
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct PrepareInputV1 {
30 pub abi: String,
31 pub step: String,
32 pub args: Value,
33 pub resources: BTreeMap<String, Value>,
34 pub params: Value,
35 pub instance: Option<InstanceInputV1>,
36 pub participant: Option<ParticipantInputV1>,
37 pub reads: BTreeMap<String, ReadInputV1>,
38 pub wallet: String,
39 pub execution_id: String,
40 pub ephemeral: BTreeMap<String, String>,
41 pub programs: BTreeMap<String, String>,
42 pub fee: Option<FeeContextV1>,
43 pub now: i64,
45 pub deployment: DeploymentInputV1,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct DeploymentInputV1 {
54 pub id: String,
56 pub attestation: String,
58 pub squads_proposer: Option<String>,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase", deny_unknown_fields)]
65pub struct InstanceInputV1 {
66 pub id: String,
67 pub state: String,
68 pub accounts: BTreeMap<String, String>,
69 pub roles: BTreeMap<String, String>,
70}
71
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase", deny_unknown_fields)]
74pub struct ParticipantInputV1 {
75 pub id: String,
76 pub wallet: String,
77 pub state: String,
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct ReadInputV1 {
85 pub account: AccountReadResultV1,
86 pub decoded: Option<Value>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase", deny_unknown_fields)]
91pub struct FeeContextV1 {
92 pub gross: String,
93 pub fee: String,
94 pub net: String,
95 pub asset: String,
97 pub placement: FeePlacement,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase", deny_unknown_fields)]
103pub struct PreparedOperationV1 {
104 pub abi: String,
105 pub transactions: Vec<PreparedTransactionV1>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase", deny_unknown_fields)]
110pub struct PreparedTransactionV1 {
111 pub name: String,
112 pub instructions: Vec<PortableInstructionV1>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117pub struct PortableInstructionV1 {
118 pub program_id: String,
119 pub keys: Vec<PortableAccountMetaV1>,
120 pub data_base64: String,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase", deny_unknown_fields)]
125pub struct PortableAccountMetaV1 {
126 pub pubkey: String,
127 pub is_signer: bool,
128 pub is_writable: bool,
129}
130
131impl PortableInstructionV1 {
132 pub fn data(&self) -> Vec<u8> {
133 decode_base64(&self.data_base64).unwrap_or_default()
134 }
135}
136
137pub fn parse_prepared_operation_v1(bytes: &[u8]) -> ProtocolResult<PreparedOperationV1> {
141 if bytes.len() > MAX_HANDLER_RESULT_BYTES {
142 return Err(ProtocolContractError::new(
143 "string-too-long",
144 "",
145 format!("handler results are at most {MAX_HANDLER_RESULT_BYTES} bytes"),
146 ));
147 }
148 let value = super::parse_contract_json(bytes)?;
149 validate_prepared_operation_value(&value)
150}
151
152pub fn validate_prepared_operation_value(value: &Value) -> ProtocolResult<PreparedOperationV1> {
153 reject_floats(value, "")?;
154 let root = Obj::new(value, "", &["abi", "transactions"])?;
155 schema_tag(root.req("abi")?, "/abi", PROTOCOL_HANDLER_ABI_V1)?;
156 let transactions = as_array(
157 root.req("transactions")?,
158 "/transactions",
159 MAX_OPERATION_TRANSACTIONS,
160 )?;
161 if transactions.is_empty() {
162 return Err(err(
163 "invalid-value",
164 "/transactions",
165 "an operation has at least one transaction",
166 ));
167 }
168 for (i, transaction) in transactions.iter().enumerate() {
169 let path = index("/transactions", i);
170 let transaction = Obj::new(transaction, &path, &["name", "instructions"])?;
171 identifier_value(transaction.req("name")?, &transaction.path_of("name"))?;
172 let instructions_path = transaction.path_of("instructions");
173 let instructions = as_array(
174 transaction.req("instructions")?,
175 &instructions_path,
176 MAX_TRANSACTION_INSTRUCTIONS,
177 )?;
178 if instructions.is_empty() {
179 return Err(err(
180 "invalid-value",
181 &instructions_path,
182 "a transaction has at least one instruction",
183 ));
184 }
185 for (j, instruction) in instructions.iter().enumerate() {
186 let path = index(&instructions_path, j);
187 let instruction = Obj::new(instruction, &path, &["programId", "keys", "dataBase64"])?;
188 pubkey(
189 instruction.req("programId")?,
190 &instruction.path_of("programId"),
191 )?;
192 let keys_path = instruction.path_of("keys");
193 for (k, key) in as_array(
194 instruction.req("keys")?,
195 &keys_path,
196 MAX_INSTRUCTION_ACCOUNTS,
197 )?
198 .iter()
199 .enumerate()
200 {
201 let key = Obj::new(
202 key,
203 &index(&keys_path, k),
204 &["pubkey", "isSigner", "isWritable"],
205 )?;
206 pubkey(key.req("pubkey")?, &key.path_of("pubkey"))?;
207 as_bool(key.req("isSigner")?, &key.path_of("isSigner"))?;
208 as_bool(key.req("isWritable")?, &key.path_of("isWritable"))?;
209 }
210 let data_path = instruction.path_of("dataBase64");
211 let data = as_str(instruction.req("dataBase64")?, &data_path)?;
212 match decode_base64(data) {
213 Some(bytes) if bytes.len() <= MAX_INSTRUCTION_DATA_BYTES => {}
214 Some(_) => {
215 return Err(err(
216 "string-too-long",
217 &data_path,
218 "instruction data is too large",
219 ))
220 }
221 None => {
222 return Err(err(
223 "invalid-value",
224 &data_path,
225 "expected canonical base64",
226 ))
227 }
228 }
229 }
230 }
231 serde_json::from_value(value.clone()).map_err(|error| {
232 ProtocolContractError::new("invalid-value", "", format!("model mismatch: {error}"))
233 })
234}
235
236#[cfg(test)]
237mod tests {
238 use serde_json::json;
239
240 use super::*;
241
242 fn input(deployment: Value) -> Value {
243 json!({
244 "abi": PROTOCOL_HANDLER_ABI_V1, "step": "create", "args": {}, "resources": {},
245 "params": {}, "instance": null, "participant": null, "reads": {},
246 "wallet": "11111111111111111111111111111111",
247 "executionId": "00000000-0000-4000-8000-000000000001",
248 "ephemeral": {}, "programs": {}, "fee": null, "now": 1_789_000_000,
249 "deployment": deployment,
250 })
251 }
252
253 #[test]
254 fn prepare_input_carries_the_deployment_keys() {
255 let parsed: PrepareInputV1 = serde_json::from_value(input(json!({
256 "id": "00000000-0000-4000-8000-0000000000d1",
257 "attestation": "Vote111111111111111111111111111111111111111",
258 "squadsProposer": null,
259 })))
260 .expect("valid input");
261 assert_eq!(parsed.deployment.squads_proposer, None);
262 let wire = serde_json::to_value(&parsed).expect("serializes");
263 assert_eq!(wire["deployment"]["squadsProposer"], Value::Null);
264 assert_eq!(
265 wire["deployment"]["attestation"],
266 "Vote111111111111111111111111111111111111111"
267 );
268
269 let mut missing = input(Value::Null);
270 missing
271 .as_object_mut()
272 .expect("object")
273 .remove("deployment");
274 assert!(serde_json::from_value::<PrepareInputV1>(missing).is_err());
275 let extra = input(json!({
276 "id": "x", "attestation": "y", "squadsProposer": null, "other": 1,
277 }));
278 assert!(serde_json::from_value::<PrepareInputV1>(extra).is_err());
279 }
280}