feat: add invocation tree utilities for context rule ID resolution#9
feat: add invocation tree utilities for context rule ID resolution#9brozorec wants to merge 3 commits into
Conversation
Add pure utility functions for inspecting Soroban invocation trees and resolving context_rule_ids for multi-contract operations (e.g. deposit that internally calls transfer). Includes kit.hintContextRuleIds() and kit.resolveContextRuleIds() convenience methods, comprehensive tests, and public exports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Nice work on this — the problem it solves (manually building What I like:
Things to address:
Looking forward to seeing this land after the WASM hash fix. |
| } | ||
| } else if (ct.tag === "CreateContract") { | ||
| if (!node.contractAddress) { | ||
| matchingRules.push({ |
There was a problem hiding this comment.
CreateContract matching — WASM hash comparison is missing
In hintContextRuleIds, the CallContract branch correctly compares the rule's address against the node's address:
if (node.contractAddress && ct.values[0] === node.contractAddress) {But the CreateContract branch only checks !node.contractAddress — it never compares the rule's WASM hash (ct.values[0]) against the node's wasmHash:
} else if (ct.tag === "CreateContract") {
if (!node.contractAddress) { // ← no hash comparison
matchingRules.push({ ... });
}
}This means any CreateContract rule will match any create-contract invocation, regardless of which WASM is being deployed. If a smart account has multiple CreateContract rules scoped to different contracts, the hint will suggest the first one found rather than the correct one.
Something like this would fix it:
} else if (ct.tag === "CreateContract") {
if (node.wasmHash && ct.values[0] && Buffer.from(ct.values[0]).equals(node.wasmHash)) {
matchingRules.push({
ruleId: rule.id,
ruleName: rule.name,
contextType: "CreateContract",
reason: "CreateContract rule (matching WASM hash)",
});
} else if (!node.contractAddress && !node.wasmHash) {
// Non-contract invocation without a hash — still a candidate
matchingRules.push({ ... });
}
}The exact comparison depends on how ct.values[0] is typed (Buffer vs string), but the key point is it should be checked against node.wasmHash. You'd want a corresponding test case for a scenario with two different CreateContract rules and two different WASM hashes to verify the right one is selected.
| invocation: xdr.SorobanAuthorizedInvocation | ||
| ): number { | ||
| return walkInvocationTree(invocation).length; | ||
| } |
There was a problem hiding this comment.
Minor: countAuthContexts builds the entire InvocationNode[] array (with string allocations for addresses, function names, etc.) just to return .length. Since this is exported as a public utility, callers might use it in hot paths where they only care about the count.
A lightweight recursive counter would avoid those allocations:
export function countAuthContexts(
invocation: xdr.SorobanAuthorizedInvocation
): number {
let count = 1;
for (const sub of invocation.subInvocations()) {
count += countAuthContexts(sub);
}
return count;
}Not a blocker — just a nice-to-have if you're touching this anyway.
Summary
src/kit/invocation-utils.ts— pure utility functions for inspecting Soroban invocation trees and resolving context rule IDskit.hintContextRuleIds()andkit.resolveContextRuleIds()convenience methods that auto-fetch rules and return per-node suggestionsbuildInvocationContextTypesincontext-rules.tsnow delegates towalkInvocationTree, eliminating a duplicate depth-first traversalextractCreateContractWasmHashto also check thewasmHashaccessor (current SDK useswasmHash(), notwasm())Motivation
When a smart account executes a multi-contract operation (e.g. a DeFi deposit that internally calls transfer),
AuthPayload.context_rule_idsmust have one entry per node in the invocation tree. Building this array manually requires understanding the depth-first traversal order and matching each node to the correct rule.These utilities automate that: