Skip to main content

runar_lang/sdk/
types.rs

1//! Core types for the Rúnar deployment SDK.
2
3use serde::Deserialize;
4use std::collections::HashMap;
5use super::anf_interpreter::ANFProgram;
6
7// ---------------------------------------------------------------------------
8// Transaction types
9// ---------------------------------------------------------------------------
10
11/// A parsed Bitcoin transaction (data shape for get_transaction return).
12#[derive(Debug, Clone)]
13pub struct TransactionData {
14    pub txid: String,
15    pub version: u32,
16    pub inputs: Vec<TxInput>,
17    pub outputs: Vec<TxOutput>,
18    pub locktime: u32,
19    pub raw: Option<String>,
20}
21
22/// Backward compatibility alias.
23pub type Transaction = TransactionData;
24
25/// A transaction input.
26#[derive(Debug, Clone)]
27pub struct TxInput {
28    pub txid: String,
29    pub output_index: u32,
30    pub script: String,
31    pub sequence: u32,
32}
33
34/// A transaction output.
35#[derive(Debug, Clone)]
36pub struct TxOutput {
37    pub satoshis: i64,
38    pub script: String,
39}
40
41/// An unspent transaction output.
42#[derive(Debug, Clone)]
43pub struct Utxo {
44    pub txid: String,
45    pub output_index: u32,
46    pub satoshis: i64,
47    pub script: String,
48}
49
50// ---------------------------------------------------------------------------
51// Option types
52// ---------------------------------------------------------------------------
53
54/// Options for deploying a contract.
55#[derive(Debug, Clone)]
56pub struct DeployOptions {
57    pub satoshis: i64,
58    pub change_address: Option<String>,
59}
60
61/// Options for calling a contract method.
62#[derive(Debug, Clone, Default)]
63pub struct CallOptions {
64    /// Satoshis for the next output (stateful contracts).
65    pub satoshis: Option<i64>,
66    pub change_address: Option<String>,
67    /// New state values for the continuation output (stateful contracts).
68    pub new_state: Option<HashMap<String, SdkValue>>,
69    /// Multiple continuation outputs for multi-output methods (e.g., transfer).
70    /// When provided, replaces the single continuation output from `new_state`.
71    pub outputs: Option<Vec<OutputSpec>>,
72    /// Additional contract UTXOs as inputs (e.g., merge, swap).
73    /// Each input is signed with the same method and args as the primary call,
74    /// with OP_PUSH_TX and Sig auto-computed per input.
75    pub additional_contract_inputs: Option<Vec<Utxo>>,
76    /// Per-input args for additional contract inputs. When provided,
77    /// `additional_contract_input_args[i]` overrides args for
78    /// `additional_contract_inputs[i]`. Sig params (Auto) are still auto-computed.
79    pub additional_contract_input_args: Option<Vec<Vec<SdkValue>>>,
80    /// Override the public key used for the change output (hex-encoded).
81    /// Defaults to the signer's public key.
82    pub change_pub_key: Option<String>,
83    /// Terminal outputs for methods that verify exact output structure via
84    /// extractOutputHash(). When set, the transaction is built with ONLY
85    /// the contract UTXO as input (no funding inputs, no change output).
86    /// The fee comes from the contract balance. The contract is considered
87    /// fully spent after this call (currentUtxo becomes None).
88    pub terminal_outputs: Option<Vec<TerminalOutput>>,
89}
90
91/// Specification for an exact output in a terminal method call.
92#[derive(Debug, Clone)]
93pub struct TerminalOutput {
94    pub script_hex: String,
95    pub satoshis: i64,
96}
97
98/// Specification for a single continuation output in multi-output calls.
99#[derive(Debug, Clone)]
100pub struct OutputSpec {
101    pub satoshis: i64,
102    pub state: HashMap<String, SdkValue>,
103}
104
105// ---------------------------------------------------------------------------
106// Artifact types (deserialized from JSON)
107// ---------------------------------------------------------------------------
108
109/// A compiled Rúnar contract artifact.
110#[derive(Debug, Clone, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct RunarArtifact {
113    pub version: String,
114    pub contract_name: String,
115    pub abi: Abi,
116    pub script: String,
117    #[serde(default)]
118    pub state_fields: Option<Vec<StateField>>,
119    #[serde(default)]
120    pub constructor_slots: Option<Vec<ConstructorSlot>>,
121    #[serde(default, rename = "codeSeparatorIndex")]
122    pub code_separator_index: Option<usize>,
123    #[serde(default, rename = "codeSeparatorIndices")]
124    pub code_separator_indices: Option<Vec<usize>>,
125    #[serde(default)]
126    pub anf: Option<ANFProgram>,
127}
128
129/// The ABI (Application Binary Interface) of a contract.
130#[derive(Debug, Clone, Deserialize)]
131pub struct Abi {
132    pub constructor: AbiConstructor,
133    pub methods: Vec<AbiMethod>,
134}
135
136/// The constructor portion of an ABI.
137#[derive(Debug, Clone, Deserialize)]
138pub struct AbiConstructor {
139    pub params: Vec<AbiParam>,
140}
141
142/// A method in the ABI.
143#[derive(Debug, Clone, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct AbiMethod {
146    pub name: String,
147    pub params: Vec<AbiParam>,
148    pub is_public: bool,
149    #[serde(default)]
150    pub is_terminal: Option<bool>,
151}
152
153/// A parameter in the ABI.
154#[derive(Debug, Clone, Deserialize)]
155pub struct AbiParam {
156    pub name: String,
157    #[serde(rename = "type")]
158    pub param_type: String,
159}
160
161/// A state field definition.
162#[derive(Debug, Clone, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct StateField {
165    pub name: String,
166    #[serde(rename = "type")]
167    pub field_type: String,
168    pub index: usize,
169    /// Compile-time default value for properties with initializers.
170    /// When artifacts are loaded via plain JSON.parse (without a BigInt
171    /// reviver), BigInt values appear as strings with an "n" suffix
172    /// (e.g. `"0n"`, `"1000n"`).
173    #[serde(default)]
174    pub initial_value: Option<serde_json::Value>,
175}
176
177/// A constructor slot mapping parameter index to byte offset in the script.
178#[derive(Debug, Clone, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct ConstructorSlot {
181    pub param_index: usize,
182    pub byte_offset: usize,
183}
184
185// ---------------------------------------------------------------------------
186// SDK value type
187// ---------------------------------------------------------------------------
188
189/// A value that can be passed to or read from the SDK.
190#[derive(Debug, Clone, PartialEq)]
191pub enum SdkValue {
192    /// An integer (maps to Bitcoin Script numbers).
193    Int(i64),
194    /// An arbitrary-precision integer for values that exceed i64 range.
195    BigInt(num_bigint::BigInt),
196    /// A boolean value.
197    Bool(bool),
198    /// Hex-encoded byte data.
199    Bytes(String),
200    /// Placeholder for auto-computed Sig or PubKey params.
201    /// Pass this as an arg to `call()` for params of type `Sig` or `PubKey` —
202    /// the SDK will compute the real value from the signer.
203    Auto,
204}
205
206impl SdkValue {
207    /// Convert to i64. Works for Int and BigInt (if within range).
208    /// Panics if the value is not numeric or exceeds i64 range.
209    pub fn as_int(&self) -> i64 {
210        match self {
211            SdkValue::Int(n) => *n,
212            SdkValue::BigInt(n) => {
213                use num_bigint::ToBigInt;
214                let min = i64::MIN.to_bigint().unwrap();
215                let max = i64::MAX.to_bigint().unwrap();
216                if *n >= min && *n <= max {
217                    // Safe to convert: value fits in i64
218                    n.to_string().parse::<i64>().unwrap()
219                } else {
220                    panic!("SdkValue::as_int: BigInt value {} exceeds i64 range", n)
221                }
222            }
223            _ => panic!("SdkValue::as_int called on non-numeric variant"),
224        }
225    }
226
227    /// Convert to bool, panicking if not a Bool variant.
228    pub fn as_bool(&self) -> bool {
229        match self {
230            SdkValue::Bool(b) => *b,
231            _ => panic!("SdkValue::as_bool called on non-Bool variant"),
232        }
233    }
234
235    /// Convert to hex string, panicking if not a Bytes variant.
236    pub fn as_bytes(&self) -> &str {
237        match self {
238            SdkValue::Bytes(s) => s,
239            _ => panic!("SdkValue::as_bytes called on non-Bytes variant"),
240        }
241    }
242}
243
244// ---------------------------------------------------------------------------
245// Tests
246// ---------------------------------------------------------------------------
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    // -----------------------------------------------------------------------
253    // Utxo
254    // -----------------------------------------------------------------------
255
256    #[test]
257    fn utxo_creation_and_field_access() {
258        let utxo = Utxo {
259            txid: "aa".repeat(32),
260            output_index: 0,
261            satoshis: 100_000,
262            script: "76a914".to_string(),
263        };
264        assert_eq!(utxo.txid, "aa".repeat(32));
265        assert_eq!(utxo.output_index, 0);
266        assert_eq!(utxo.satoshis, 100_000);
267        assert_eq!(utxo.script, "76a914");
268    }
269
270    #[test]
271    fn utxo_clone() {
272        let utxo = Utxo {
273            txid: "bb".repeat(32),
274            output_index: 1,
275            satoshis: 50_000,
276            script: "51".to_string(),
277        };
278        let cloned = utxo.clone();
279        assert_eq!(cloned.txid, utxo.txid);
280        assert_eq!(cloned.output_index, utxo.output_index);
281        assert_eq!(cloned.satoshis, utxo.satoshis);
282        assert_eq!(cloned.script, utxo.script);
283    }
284
285    // -----------------------------------------------------------------------
286    // TransactionData
287    // -----------------------------------------------------------------------
288
289    #[test]
290    fn transaction_data_construction_defaults() {
291        let tx = TransactionData {
292            txid: "cc".repeat(32),
293            version: 1,
294            inputs: vec![],
295            outputs: vec![],
296            locktime: 0,
297            raw: None,
298        };
299        assert_eq!(tx.txid, "cc".repeat(32));
300        assert_eq!(tx.version, 1);
301        assert!(tx.inputs.is_empty());
302        assert!(tx.outputs.is_empty());
303        assert_eq!(tx.locktime, 0);
304        assert!(tx.raw.is_none());
305    }
306
307    #[test]
308    fn transaction_data_with_inputs_and_outputs() {
309        let tx = TransactionData {
310            txid: "dd".repeat(32),
311            version: 1,
312            inputs: vec![TxInput {
313                txid: "ee".repeat(32),
314                output_index: 0,
315                script: "00".to_string(),
316                sequence: 0xffffffff,
317            }],
318            outputs: vec![TxOutput {
319                satoshis: 75_000,
320                script: "76a914".to_string(),
321            }],
322            locktime: 500_000,
323            raw: Some("0100000001...".to_string()),
324        };
325        assert_eq!(tx.inputs.len(), 1);
326        assert_eq!(tx.inputs[0].sequence, 0xffffffff);
327        assert_eq!(tx.outputs.len(), 1);
328        assert_eq!(tx.outputs[0].satoshis, 75_000);
329        assert_eq!(tx.locktime, 500_000);
330        assert!(tx.raw.is_some());
331    }
332
333    // -----------------------------------------------------------------------
334    // RunarArtifact deserialization
335    // -----------------------------------------------------------------------
336
337    #[test]
338    fn runar_artifact_deserialize_minimal() {
339        let json = r#"{
340            "version": "0.1.0",
341            "contractName": "TestContract",
342            "abi": {
343                "constructor": { "params": [] },
344                "methods": []
345            },
346            "script": "5151"
347        }"#;
348        let artifact: RunarArtifact = serde_json::from_str(json).unwrap();
349        assert_eq!(artifact.version, "0.1.0");
350        assert_eq!(artifact.contract_name, "TestContract");
351        assert_eq!(artifact.script, "5151");
352        assert!(artifact.abi.constructor.params.is_empty());
353        assert!(artifact.abi.methods.is_empty());
354        assert!(artifact.state_fields.is_none());
355        assert!(artifact.constructor_slots.is_none());
356        assert!(artifact.code_separator_index.is_none());
357    }
358
359    #[test]
360    fn runar_artifact_deserialize_with_methods() {
361        let json = r#"{
362            "version": "0.1.0",
363            "contractName": "Counter",
364            "abi": {
365                "constructor": {
366                    "params": [{ "name": "count", "type": "bigint" }]
367                },
368                "methods": [{
369                    "name": "increment",
370                    "params": [],
371                    "isPublic": true
372                }]
373            },
374            "script": "00ab"
375        }"#;
376        let artifact: RunarArtifact = serde_json::from_str(json).unwrap();
377        assert_eq!(artifact.abi.constructor.params.len(), 1);
378        assert_eq!(artifact.abi.constructor.params[0].name, "count");
379        assert_eq!(artifact.abi.constructor.params[0].param_type, "bigint");
380        assert_eq!(artifact.abi.methods.len(), 1);
381        assert_eq!(artifact.abi.methods[0].name, "increment");
382        assert!(artifact.abi.methods[0].is_public);
383    }
384
385    #[test]
386    fn runar_artifact_deserialize_with_state_fields_and_slots() {
387        let json = r#"{
388            "version": "0.1.0",
389            "contractName": "Stateful",
390            "abi": {
391                "constructor": { "params": [{ "name": "x", "type": "bigint" }] },
392                "methods": [{ "name": "update", "params": [], "isPublic": true }]
393            },
394            "script": "aabb",
395            "stateFields": [{ "name": "x", "type": "bigint", "index": 0 }],
396            "constructorSlots": [{ "paramIndex": 0, "byteOffset": 5 }],
397            "codeSeparatorIndex": 10,
398            "codeSeparatorIndices": [10, 20]
399        }"#;
400        let artifact: RunarArtifact = serde_json::from_str(json).unwrap();
401        let sf = artifact.state_fields.as_ref().unwrap();
402        assert_eq!(sf.len(), 1);
403        assert_eq!(sf[0].name, "x");
404        assert_eq!(sf[0].index, 0);
405        let cs = artifact.constructor_slots.as_ref().unwrap();
406        assert_eq!(cs.len(), 1);
407        assert_eq!(cs[0].param_index, 0);
408        assert_eq!(cs[0].byte_offset, 5);
409        assert_eq!(artifact.code_separator_index, Some(10));
410        assert_eq!(artifact.code_separator_indices, Some(vec![10, 20]));
411    }
412
413    // -----------------------------------------------------------------------
414    // SdkValue
415    // -----------------------------------------------------------------------
416
417    #[test]
418    fn sdk_value_int() {
419        let v = SdkValue::Int(42);
420        assert_eq!(v.as_int(), 42);
421    }
422
423    #[test]
424    fn sdk_value_bool() {
425        let v = SdkValue::Bool(true);
426        assert!(v.as_bool());
427    }
428
429    #[test]
430    fn sdk_value_bytes() {
431        let v = SdkValue::Bytes("deadbeef".to_string());
432        assert_eq!(v.as_bytes(), "deadbeef");
433    }
434
435    #[test]
436    fn sdk_value_auto() {
437        let v = SdkValue::Auto;
438        assert_eq!(v, SdkValue::Auto);
439    }
440
441    #[test]
442    #[should_panic(expected = "non-numeric")]
443    fn sdk_value_as_int_panics_on_bool() {
444        SdkValue::Bool(true).as_int();
445    }
446
447    #[test]
448    #[should_panic(expected = "non-Bool")]
449    fn sdk_value_as_bool_panics_on_int() {
450        SdkValue::Int(1).as_bool();
451    }
452
453    #[test]
454    #[should_panic(expected = "non-Bytes")]
455    fn sdk_value_as_bytes_panics_on_int() {
456        SdkValue::Int(1).as_bytes();
457    }
458
459    #[test]
460    fn sdk_value_equality() {
461        assert_eq!(SdkValue::Int(5), SdkValue::Int(5));
462        assert_ne!(SdkValue::Int(5), SdkValue::Int(6));
463        assert_ne!(SdkValue::Int(1), SdkValue::Bool(true));
464    }
465
466    // -----------------------------------------------------------------------
467    // DeployOptions / CallOptions
468    // -----------------------------------------------------------------------
469
470    #[test]
471    fn deploy_options_construction() {
472        let opts = DeployOptions {
473            satoshis: 1000,
474            change_address: Some("maddr".to_string()),
475        };
476        assert_eq!(opts.satoshis, 1000);
477        assert_eq!(opts.change_address.as_deref(), Some("maddr"));
478    }
479
480    #[test]
481    fn call_options_default() {
482        let opts = CallOptions::default();
483        assert!(opts.satoshis.is_none());
484        assert!(opts.change_address.is_none());
485        assert!(opts.new_state.is_none());
486        assert!(opts.outputs.is_none());
487        assert!(opts.terminal_outputs.is_none());
488    }
489}
490
491// ---------------------------------------------------------------------------
492// PreparedCall — result of prepare_call()
493// ---------------------------------------------------------------------------
494
495/// Result of `prepare_call()` — contains everything needed for external signing
496/// and subsequent `finalize_call()`.
497///
498/// Public fields (`sighash`, `preimage`, `op_push_tx_sig`, `tx_hex`, `sig_indices`)
499/// are for external signer coordination. Other fields are opaque
500/// internals consumed by `finalize_call()`.
501#[derive(Debug, Clone)]
502pub struct PreparedCall {
503    /// BIP-143 sighash (hex) — what external signers ECDSA-sign.
504    pub sighash: String,
505    /// Full BIP-143 preimage (hex).
506    pub preimage: String,
507    /// OP_PUSH_TX DER signature + sighash byte (hex). Empty if not needed.
508    pub op_push_tx_sig: String,
509    /// Built transaction hex (P2PKH funding signed, primary contract input uses placeholder sigs).
510    pub tx_hex: String,
511    /// User-visible arg positions that need external Sig values.
512    pub sig_indices: Vec<usize>,
513
514    // Internal fields — consumed by finalize_call()
515    pub(crate) method_name: String,
516    pub(crate) resolved_args: Vec<SdkValue>,
517    pub(crate) method_selector_hex: String,
518    pub(crate) is_stateful: bool,
519    pub(crate) is_terminal: bool,
520    pub(crate) needs_op_push_tx: bool,
521    pub(crate) method_needs_change: bool,
522    pub(crate) change_pkh_hex: String,
523    pub(crate) change_amount: i64,
524    pub(crate) method_needs_new_amount: bool,
525    pub(crate) new_amount: i64,
526    pub(crate) preimage_index: Option<usize>,
527    pub(crate) contract_utxo: Utxo,
528    pub(crate) new_locking_script: String,
529    pub(crate) new_satoshis: i64,
530    pub(crate) has_multi_output: bool,
531    pub(crate) contract_outputs: Vec<ContractOutputEntry>,
532    pub(crate) code_sep_idx: i64,
533}
534
535/// A contract output entry stored in PreparedCall (script + satoshis).
536#[derive(Debug, Clone)]
537pub struct ContractOutputEntry {
538    pub script: String,
539    pub satoshis: i64,
540}