Skip to content

Commit 99249db

Browse files
committed
fix(bindgen): port to spend-groups/leaves ABI; gate whole workspace in CI
Rewrite the IR and TS/Go emitters around AbiFunctionGroup{arkade, leaves[]} instead of the removed serverVariant/witnessSchema shape; regenerate fixtures from the compiler; per-leaf witness types and buildWitness(group, leaf, w). CI now builds/clippies/tests --workspace so sub-crates can't silently break.
1 parent 2c7f241 commit 99249db

14 files changed

Lines changed: 519 additions & 685 deletions

File tree

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ jobs:
1919
steps:
2020
- uses: actions/checkout@v4
2121
- name: Build
22-
run: cargo build --verbose
22+
run: cargo build --workspace --verbose
2323
- name: Clippy
24-
run: cargo clippy --all-targets -- -D warnings
24+
run: cargo clippy --workspace --all-targets -- -D warnings
2525
- name: Run tests
26-
run: cargo test --verbose
26+
run: cargo test --workspace --verbose
2727

2828
check-format:
2929

arkade-bindgen/src/ir.rs

Lines changed: 84 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::collections::BTreeMap;
2-
31
use arkade_compiler::ContractJson;
42

53
/// Wire-encoding descriptor, matching the compiler's `WitnessElement.encoding` field.
@@ -41,8 +39,8 @@ impl Encoding {
4139
}
4240
}
4341

44-
/// Infer encoding from an Arkade Script type string.
45-
/// Used as fallback when `witnessSchema` is absent.
42+
/// Infer encoding from an Arkade Script type string. Constructor parameters
43+
/// and covenant inputs carry only a type, not an explicit encoding.
4644
pub fn from_ark_type(type_str: &str) -> Self {
4745
match type_str {
4846
"pubkey" => Encoding::Compressed33,
@@ -72,7 +70,7 @@ impl Encoding {
7270
}
7371
}
7472

75-
/// A typed field in the IR (constructor param or witness element).
73+
/// A typed field in the IR (constructor param, covenant input, or witness element).
7674
#[derive(Debug, Clone)]
7775
pub struct Field {
7876
/// Field name as it appears in the artifact (camelCase).
@@ -81,34 +79,49 @@ pub struct Field {
8179
pub ark_type: String,
8280
/// Wire encoding descriptor.
8381
pub encoding: Encoding,
84-
/// True if this field is injected by the Arkade server (e.g., "serverSig").
85-
pub is_server_injected: bool,
82+
/// True when Arkade infrastructure supplies this field (e.g., co-signer sigs).
83+
pub is_injected: bool,
8684
}
8785

88-
/// One variant of a function (cooperative or exit).
86+
/// One L1 tapleaf spend path within a group.
8987
#[derive(Debug, Clone)]
90-
pub struct VariantIR {
91-
/// Fields the caller must supply (excludes server-injected).
92-
pub user_fields: Vec<Field>,
93-
/// All fields including server-injected.
94-
pub all_fields: Vec<Field>,
95-
/// Raw assembly instructions.
88+
pub struct LeafIR {
89+
/// Leaf name (source tapscript name, or covenant name for a synthesized default).
90+
pub name: String,
91+
/// Witness stack the caller supplies, in order. Injected fields are marked.
92+
pub witness_fields: Vec<Field>,
93+
/// Tapleaf assembly.
9694
pub asm: Vec<String>,
97-
/// Human-readable requirement descriptions for doc comments.
98-
pub requirements: Vec<String>,
99-
/// Whether this is an N-of-N multisig fallback exit path.
100-
pub is_nofn_fallback: bool,
10195
}
10296

103-
/// A paired function with cooperative and exit variants.
97+
impl LeafIR {
98+
/// Fields the caller must supply (excludes infrastructure-injected ones).
99+
pub fn user_fields(&self) -> Vec<&Field> {
100+
self.witness_fields
101+
.iter()
102+
.filter(|f| !f.is_injected)
103+
.collect()
104+
}
105+
}
106+
107+
/// The emulator-run covenant attached to a function-backed group.
104108
#[derive(Debug, Clone)]
105-
pub struct FunctionIR {
106-
/// Function name as it appears in the artifact.
109+
pub struct CovenantIR {
110+
/// Covenant inputs (function parameters, array-expanded).
111+
pub inputs: Vec<Field>,
112+
/// Covenant assembly.
113+
pub asm: Vec<String>,
114+
}
115+
116+
/// A spend group: an optional covenant plus its L1 leaves.
117+
#[derive(Debug, Clone)]
118+
pub struct GroupIR {
119+
/// Group name (covenant function name, or a standalone leaf's own name).
107120
pub name: String,
108-
/// Cooperative path (serverVariant: true).
109-
pub cooperative: VariantIR,
110-
/// Exit path (serverVariant: false).
111-
pub exit: VariantIR,
121+
/// Emulator covenant; absent for groups of standalone leaves.
122+
pub covenant: Option<CovenantIR>,
123+
/// L1 tapleaves grouped under this entry.
124+
pub leaves: Vec<LeafIR>,
112125
}
113126

114127
/// The complete intermediate representation of a contract.
@@ -118,164 +131,70 @@ pub struct ContractIR {
118131
pub name: String,
119132
/// Typed constructor parameters.
120133
pub constructor_fields: Vec<Field>,
121-
/// Paired functions (cooperative + exit).
122-
pub functions: Vec<FunctionIR>,
134+
/// Spend groups in artifact order.
135+
pub groups: Vec<GroupIR>,
123136
/// Original .ark source code, if embedded.
124137
pub source: Option<String>,
125138
/// Compiler version string.
126139
pub compiler_version: Option<String>,
127140
}
128141

142+
fn field_from_ark_type(name: &str, ark_type: &str) -> Field {
143+
Field {
144+
name: name.to_string(),
145+
ark_type: ark_type.to_string(),
146+
encoding: Encoding::from_ark_type(ark_type),
147+
is_injected: false,
148+
}
149+
}
150+
129151
/// Build an IR from a compiled contract artifact.
130152
pub fn build_ir(artifact: &ContractJson) -> Result<ContractIR, String> {
131153
let constructor_fields = artifact
132154
.parameters
133155
.iter()
134-
.map(|p| Field {
135-
name: p.name.clone(),
136-
ark_type: p.param_type.clone(),
137-
encoding: Encoding::from_ark_type(&p.param_type),
138-
is_server_injected: false,
139-
})
156+
.map(|p| field_from_ark_type(&p.name, &p.param_type))
140157
.collect();
141158

142-
let functions = pair_functions(artifact)?;
159+
let groups = artifact
160+
.functions
161+
.iter()
162+
.map(|group| GroupIR {
163+
name: group.name.clone(),
164+
covenant: group.arkade.as_ref().map(|cov| CovenantIR {
165+
inputs: cov
166+
.inputs
167+
.iter()
168+
.map(|i| field_from_ark_type(&i.name, &i.param_type))
169+
.collect(),
170+
asm: cov.asm.clone(),
171+
}),
172+
leaves: group
173+
.leaves
174+
.iter()
175+
.map(|leaf| LeafIR {
176+
name: leaf.name.clone(),
177+
witness_fields: leaf
178+
.witness
179+
.iter()
180+
.map(|w| Field {
181+
name: w.name.clone(),
182+
ark_type: w.elem_type.clone(),
183+
encoding: Encoding::parse(&w.encoding),
184+
is_injected: w.injected,
185+
})
186+
.collect(),
187+
asm: leaf.asm.clone(),
188+
})
189+
.collect(),
190+
})
191+
.collect();
143192

144193
Ok(ContractIR {
145194
name: artifact.name.clone(),
146195
constructor_fields,
147-
functions,
196+
groups,
148197
source: artifact.source.clone(),
149198
compiler_version: artifact.compiler.as_ref().map(|c| c.version.clone()),
150199
})
151200
}
152-
153-
/// Group artifact functions by name and pair cooperative + exit variants.
154-
fn pair_functions(artifact: &ContractJson) -> Result<Vec<FunctionIR>, String> {
155-
// Use BTreeMap to preserve insertion order (sorted, but stable).
156-
// We'll re-sort by first-appearance order afterwards.
157-
let mut groups: BTreeMap<String, (Option<usize>, Vec<&arkade_compiler::models::AbiFunction>)> =
158-
BTreeMap::new();
159-
160-
for (idx, func) in artifact.functions.iter().enumerate() {
161-
let entry = groups
162-
.entry(func.name.clone())
163-
.or_insert_with(|| (Some(idx), Vec::new()));
164-
entry.1.push(func);
165-
}
166-
167-
// Sort by first appearance order
168-
let mut ordered: Vec<_> = groups.into_iter().collect();
169-
ordered.sort_by_key(|(_, (first_idx, _))| first_idx.unwrap_or(usize::MAX));
170-
171-
let mut result = Vec::new();
172-
173-
for (name, (_, variants)) in ordered {
174-
let mut cooperative = None;
175-
let mut exit = None;
176-
177-
for func in &variants {
178-
if func.server_variant {
179-
if cooperative.is_some() {
180-
return Err(format!(
181-
"Function '{}' has multiple cooperative variants",
182-
name
183-
));
184-
}
185-
cooperative = Some(*func);
186-
} else {
187-
if exit.is_some() {
188-
return Err(format!("Function '{}' has multiple exit variants", name));
189-
}
190-
exit = Some(*func);
191-
}
192-
}
193-
194-
let cooperative_func = cooperative
195-
.ok_or_else(|| format!("Function '{}' missing cooperative variant", name))?;
196-
let exit_func = exit.ok_or_else(|| format!("Function '{}' missing exit variant", name))?;
197-
198-
result.push(FunctionIR {
199-
name: name.clone(),
200-
cooperative: build_variant(cooperative_func),
201-
exit: build_variant(exit_func),
202-
});
203-
}
204-
205-
Ok(result)
206-
}
207-
208-
/// Build a VariantIR from a single AbiFunction.
209-
fn build_variant(func: &arkade_compiler::models::AbiFunction) -> VariantIR {
210-
let has_witness_schema = !func.witness_schema.is_empty();
211-
212-
// Check if "serverSig" is a user-declared input (not protocol-injected)
213-
let user_declared_server_sig = func.function_inputs.iter().any(|fi| fi.name == "serverSig");
214-
215-
let all_fields: Vec<Field> = if has_witness_schema {
216-
// Primary path: use witnessSchema with explicit encodings
217-
func.witness_schema
218-
.iter()
219-
.map(|w| Field {
220-
name: w.name.clone(),
221-
ark_type: w.elem_type.clone(),
222-
encoding: Encoding::parse(&w.encoding),
223-
is_server_injected: func.server_variant
224-
&& w.name == "serverSig"
225-
&& !user_declared_server_sig,
226-
})
227-
.collect()
228-
} else {
229-
// Fallback: infer encodings from functionInputs type strings
230-
let mut fields: Vec<Field> = func
231-
.function_inputs
232-
.iter()
233-
.map(|fi| Field {
234-
name: fi.name.clone(),
235-
ark_type: fi.param_type.clone(),
236-
encoding: Encoding::from_ark_type(&fi.param_type),
237-
is_server_injected: false,
238-
})
239-
.collect();
240-
241-
// If this is a cooperative variant and serverSig isn't user-declared, add it
242-
if func.server_variant && !user_declared_server_sig {
243-
fields.push(Field {
244-
name: "serverSig".to_string(),
245-
ark_type: "signature".to_string(),
246-
encoding: Encoding::Schnorr64,
247-
is_server_injected: true,
248-
});
249-
}
250-
251-
fields
252-
};
253-
254-
let user_fields = all_fields
255-
.iter()
256-
.filter(|f| !f.is_server_injected)
257-
.cloned()
258-
.collect();
259-
260-
let requirements = func
261-
.require
262-
.iter()
263-
.map(|r| {
264-
if let Some(ref msg) = r.message {
265-
format!("{}: {}", r.req_type, msg)
266-
} else {
267-
r.req_type.clone()
268-
}
269-
})
270-
.collect();
271-
272-
let is_nofn_fallback = func.require.iter().any(|r| r.req_type == "nOfNMultisig");
273-
274-
VariantIR {
275-
user_fields,
276-
all_fields,
277-
asm: func.asm.clone(),
278-
requirements,
279-
is_nofn_fallback,
280-
}
281-
}

arkade-bindgen/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub enum Target {
1616
}
1717

1818
impl Target {
19+
#[allow(clippy::should_implement_trait)]
1920
pub fn from_str(s: &str) -> Option<Self> {
2021
match s {
2122
"typescript" | "ts" => Some(Target::TypeScript),

arkade-bindgen/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ fn collect_artifacts(input: &Path) -> Vec<PathBuf> {
151151
.filter_map(|entry| {
152152
let entry = entry.ok()?;
153153
let path = entry.path();
154-
if path.extension().map_or(false, |ext| ext == "json") {
154+
if path.extension().is_some_and(|ext| ext == "json") {
155155
Some(path)
156156
} else {
157157
None

arkade-bindgen/src/naming.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ fn split_words(s: &str) -> Vec<String> {
9191
let prev_is_upper = current
9292
.chars()
9393
.last()
94-
.map_or(false, |c| c.is_ascii_uppercase());
94+
.is_some_and(|c| c.is_ascii_uppercase());
9595
if prev_is_upper {
9696
// Continue the caps run
9797
current.push(ch);

0 commit comments

Comments
 (0)