use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::error::{ProtocolContractError, ProtocolResult};
use super::intent::{AccountReadResultV1, FeePlacement};
use super::model::PROTOCOL_HANDLER_ABI_V1;
use super::release::decode_base64;
use super::shape::*;
pub const MAX_OPERATION_TRANSACTIONS: usize = 8;
pub const MAX_TRANSACTION_INSTRUCTIONS: usize = 64;
pub const MAX_INSTRUCTION_ACCOUNTS: usize = 256;
pub const MAX_INSTRUCTION_DATA_BYTES: usize = 10 * 1024;
pub const MAX_HANDLER_RESULT_BYTES: usize = 2 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PrepareInputV1 {
pub abi: String,
pub step: String,
pub args: Value,
pub resources: BTreeMap<String, Value>,
pub params: Value,
pub instance: Option<InstanceInputV1>,
pub participant: Option<ParticipantInputV1>,
pub reads: BTreeMap<String, ReadInputV1>,
pub wallet: String,
pub execution_id: String,
pub ephemeral: BTreeMap<String, String>,
pub programs: BTreeMap<String, String>,
pub fee: Option<FeeContextV1>,
pub now: i64,
pub deployment: DeploymentInputV1,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DeploymentInputV1 {
pub id: String,
pub attestation: String,
pub squads_proposer: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct InstanceInputV1 {
pub id: String,
pub state: String,
pub accounts: BTreeMap<String, String>,
pub roles: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ParticipantInputV1 {
pub id: String,
pub wallet: String,
pub state: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReadInputV1 {
pub account: AccountReadResultV1,
pub decoded: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FeeContextV1 {
pub gross: String,
pub fee: String,
pub net: String,
pub asset: String,
pub placement: FeePlacement,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PreparedOperationV1 {
pub abi: String,
pub transactions: Vec<PreparedTransactionV1>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PreparedTransactionV1 {
pub name: String,
pub instructions: Vec<PortableInstructionV1>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PortableInstructionV1 {
pub program_id: String,
pub keys: Vec<PortableAccountMetaV1>,
pub data_base64: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PortableAccountMetaV1 {
pub pubkey: String,
pub is_signer: bool,
pub is_writable: bool,
}
impl PortableInstructionV1 {
pub fn data(&self) -> Vec<u8> {
decode_base64(&self.data_base64).unwrap_or_default()
}
}
pub fn parse_prepared_operation_v1(bytes: &[u8]) -> ProtocolResult<PreparedOperationV1> {
if bytes.len() > MAX_HANDLER_RESULT_BYTES {
return Err(ProtocolContractError::new(
"string-too-long",
"",
format!("handler results are at most {MAX_HANDLER_RESULT_BYTES} bytes"),
));
}
let value = super::parse_contract_json(bytes)?;
validate_prepared_operation_value(&value)
}
pub fn validate_prepared_operation_value(value: &Value) -> ProtocolResult<PreparedOperationV1> {
reject_floats(value, "")?;
let root = Obj::new(value, "", &["abi", "transactions"])?;
schema_tag(root.req("abi")?, "/abi", PROTOCOL_HANDLER_ABI_V1)?;
let transactions = as_array(
root.req("transactions")?,
"/transactions",
MAX_OPERATION_TRANSACTIONS,
)?;
if transactions.is_empty() {
return Err(err(
"invalid-value",
"/transactions",
"an operation has at least one transaction",
));
}
for (i, transaction) in transactions.iter().enumerate() {
let path = index("/transactions", i);
let transaction = Obj::new(transaction, &path, &["name", "instructions"])?;
identifier_value(transaction.req("name")?, &transaction.path_of("name"))?;
let instructions_path = transaction.path_of("instructions");
let instructions = as_array(
transaction.req("instructions")?,
&instructions_path,
MAX_TRANSACTION_INSTRUCTIONS,
)?;
if instructions.is_empty() {
return Err(err(
"invalid-value",
&instructions_path,
"a transaction has at least one instruction",
));
}
for (j, instruction) in instructions.iter().enumerate() {
let path = index(&instructions_path, j);
let instruction = Obj::new(instruction, &path, &["programId", "keys", "dataBase64"])?;
pubkey(
instruction.req("programId")?,
&instruction.path_of("programId"),
)?;
let keys_path = instruction.path_of("keys");
for (k, key) in as_array(
instruction.req("keys")?,
&keys_path,
MAX_INSTRUCTION_ACCOUNTS,
)?
.iter()
.enumerate()
{
let key = Obj::new(
key,
&index(&keys_path, k),
&["pubkey", "isSigner", "isWritable"],
)?;
pubkey(key.req("pubkey")?, &key.path_of("pubkey"))?;
as_bool(key.req("isSigner")?, &key.path_of("isSigner"))?;
as_bool(key.req("isWritable")?, &key.path_of("isWritable"))?;
}
let data_path = instruction.path_of("dataBase64");
let data = as_str(instruction.req("dataBase64")?, &data_path)?;
match decode_base64(data) {
Some(bytes) if bytes.len() <= MAX_INSTRUCTION_DATA_BYTES => {}
Some(_) => {
return Err(err(
"string-too-long",
&data_path,
"instruction data is too large",
))
}
None => {
return Err(err(
"invalid-value",
&data_path,
"expected canonical base64",
))
}
}
}
}
serde_json::from_value(value.clone()).map_err(|error| {
ProtocolContractError::new("invalid-value", "", format!("model mismatch: {error}"))
})
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn input(deployment: Value) -> Value {
json!({
"abi": PROTOCOL_HANDLER_ABI_V1, "step": "create", "args": {}, "resources": {},
"params": {}, "instance": null, "participant": null, "reads": {},
"wallet": "11111111111111111111111111111111",
"executionId": "00000000-0000-4000-8000-000000000001",
"ephemeral": {}, "programs": {}, "fee": null, "now": 1_789_000_000,
"deployment": deployment,
})
}
#[test]
fn prepare_input_carries_the_deployment_keys() {
let parsed: PrepareInputV1 = serde_json::from_value(input(json!({
"id": "00000000-0000-4000-8000-0000000000d1",
"attestation": "Vote111111111111111111111111111111111111111",
"squadsProposer": null,
})))
.expect("valid input");
assert_eq!(parsed.deployment.squads_proposer, None);
let wire = serde_json::to_value(&parsed).expect("serializes");
assert_eq!(wire["deployment"]["squadsProposer"], Value::Null);
assert_eq!(
wire["deployment"]["attestation"],
"Vote111111111111111111111111111111111111111"
);
let mut missing = input(Value::Null);
missing
.as_object_mut()
.expect("object")
.remove("deployment");
assert!(serde_json::from_value::<PrepareInputV1>(missing).is_err());
let extra = input(json!({
"id": "x", "attestation": "y", "squadsProposer": null, "other": 1,
}));
assert!(serde_json::from_value::<PrepareInputV1>(extra).is_err());
}
}