Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8b553da
refactor: remove unused CreateIntentConfigurationWithTimedRefundSapient
ScreamingHawk Jul 27, 2026
195627c
feat: add optional payload gate leaf to CreateIntentTree/CreateIntent…
ScreamingHawk Jul 27, 2026
4b4377d
feat: gate sapient signer leaves behind the payload gate too
ScreamingHawk Jul 29, 2026
100e352
fix: skip the calls gate when there are no call batches
ScreamingHawk Jul 29, 2026
26a73e4
fix: cap payloadGateLeaf to weight 1 in wrapPayloadGate
ScreamingHawk Jul 29, 2026
1f1286b
refactor: merge calls and sapient behind a single payload gate
ScreamingHawk Jul 29, 2026
80db92b
fix: reject WalletConfigTreeNestedLeaf in leafWeight
ScreamingHawk Jul 30, 2026
680c9dd
fix: match full signer identity in BuildIntentConfigurationSignature
ScreamingHawk Jul 30, 2026
ed9047d
feat: thread payloadGateLeafNode through GetIntentConfigurationSignature
ScreamingHawk Jul 30, 2026
df27a83
fix: embed supplied signatures deterministically in intent config sig…
ScreamingHawk Jul 30, 2026
6237ebe
refactor: replace optional leaf params with functional options
ScreamingHawk Jul 30, 2026
9e09633
fix: ignore nil IntentConfigOption values
ScreamingHawk Jul 30, 2026
52d9fc6
fix: reject payload gate signer among gated leaves
ScreamingHawk Jul 31, 2026
5560a42
fix: reject payload-matching leaves as payload gate
ScreamingHawk Jul 31, 2026
4cde50e
fix: reject typed-nil payload gate leaves
ScreamingHawk Jul 31, 2026
18cf2c0
refactor: rename payloadGate to gate
ScreamingHawk Jul 31, 2026
a512cd1
fix: reject gate signer at any depth in gated subtree
ScreamingHawk Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 104 additions & 18 deletions intent_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,47 +186,125 @@ func CreateAnyAddressSubdigestTree(calls []*v3.CallsPayload) ([]v3.WalletConfigT
return leaves, nil
}

func createIntentTree(mainSigner common.Address, calls []*v3.CallsPayload, additionalLeaves ...v3.WalletConfigTree) (*v3.WalletConfigTree, error) {
// wrapPayloadGate requires payloadGateLeaf's co-signature alongside any one of
// gateableLeaves. An inner threshold-1 nest OR's the groups and contributes weight 1 at
// most, so satisfying many groups still cannot clear the outer threshold without the gate
// leaf. The outer threshold is payloadGateLeaf's weight + 1, so any gate weight is safe by
// construction (the gate alone cannot meet it).
func wrapPayloadGate(payloadGateLeaf v3.WalletConfigTree, gateableLeaves ...v3.WalletConfigTree) (v3.WalletConfigTree, error) {
gateWeight, err := leafWeight(payloadGateLeaf)
if err != nil {
return nil, fmt.Errorf("invalid payloadGateLeafNode: %w", err)
}
if gateWeight == 0 {
return nil, fmt.Errorf("invalid payloadGateLeafNode: weight must be > 0")
}
gateableTree := &v3.WalletConfigTreeNestedLeaf{
Weight: 1,
Threshold: 1,
Tree: v3.WalletConfigTreeNodes(gateableLeaves...),
Comment thread
ScreamingHawk marked this conversation as resolved.
}
return &v3.WalletConfigTreeNestedLeaf{
Weight: 1,
Threshold: uint16(gateWeight) + 1,
Tree: v3.WalletConfigTreeNodes(payloadGateLeaf, gateableTree),
Comment thread
ScreamingHawk marked this conversation as resolved.
Outdated
}, nil
}

// leafWeight returns the contribution weight of a single leaf. Nodes and other types whose
// effective weight can't be read from outside package v3 are rejected.
func leafWeight(tree v3.WalletConfigTree) (uint8, error) {
if tree == nil {
return 0, fmt.Errorf("nil leaf")
}
switch t := tree.(type) {
case *v3.WalletConfigTreeAddressLeaf:
return t.Weight, nil
case *v3.WalletConfigTreeSapientSignerLeaf:
return t.Weight, nil
Comment thread
ScreamingHawk marked this conversation as resolved.
Outdated
case *v3.WalletConfigTreeNestedLeaf:
Comment thread
ScreamingHawk marked this conversation as resolved.
Outdated
return t.Weight, nil
default:
return 0, fmt.Errorf("unsupported leaf type %T", tree)
}
}

func createIntentTree(
mainSigner common.Address,
calls []*v3.CallsPayload,
payloadGateLeafNode v3.WalletConfigTree,
sapientSignerLeafNode v3.WalletConfigTree,
) (*v3.WalletConfigTree, error) {
var leaves []v3.WalletConfigTree

// Create the subdigest leaves from the batched transactions.
leaves, err := CreateAnyAddressSubdigestTree(calls)
gateableLeaves, err := CreateAnyAddressSubdigestTree(calls)
if err != nil {
return nil, err
}

for _, leaf := range additionalLeaves {
if leaf != nil {
leaves = append(leaves, leaf)
// Add the sapient signer leaf to the gateable leaves.
if sapientSignerLeafNode != nil {
gateableLeaves = append(gateableLeaves, sapientSignerLeafNode)
Comment thread
ScreamingHawk marked this conversation as resolved.
}

// If there are any gateable leaves, wrap them in a gate if a payload gate leaf is provided.
if len(gateableLeaves) > 0 {
if payloadGateLeafNode == nil {
// No gate: preserve flat structure so counterfactual addresses stay stable.
leaves = append(leaves, gateableLeaves...)
} else {
// Calls and sapient share one gate; either needs payloadGateLeaf's co-signature.
gate, err := wrapPayloadGate(payloadGateLeafNode, gateableLeaves...)
if err != nil {
return nil, err
}
leaves = append(leaves, gate)
}
}

// Create the main signer leaf (with weight 1).
// Add the main signer leaf to the leaves (ungated).
mainSignerLeaf := &v3.WalletConfigTreeAddressLeaf{
Weight: 1,
Address: mainSigner,
}

// If the length of the leaves is 1
// If the length of the leaves is 1.
if len(leaves) == 1 {
tree := v3.WalletConfigTreeNodes(mainSignerLeaf, leaves[0])
return &tree, nil
}

// Create a tree from the subdigest leaves.
// Create a tree from the (gated) leaves.
tree := v3.WalletConfigTreeNodes(leaves...)

// Construct the new wallet config using:
// Construct the new wallet config.
fullTree := v3.WalletConfigTreeNodes(mainSignerLeaf, tree)

return &fullTree, nil
}

// `CreateIntentTree` creates a tree from a list of intent operations and a main signer address.
func CreateIntentTree(mainSigner common.Address, calls []*v3.CallsPayload, sapientSignerLeafNode v3.WalletConfigTree) (*v3.WalletConfigTree, error) {
return createIntentTree(mainSigner, calls, sapientSignerLeafNode)
// `CreateIntentTree` creates a tree from a list of intent operations and a main signer
// address. When payloadGateLeafNode is set, calls and sapientSignerLeafNode share one gate
// that requires its co-signature (see wrapPayloadGate); mainSigner never is. nil preserves
// the legacy tree shape.
func CreateIntentTree(
mainSigner common.Address,
calls []*v3.CallsPayload,
payloadGateLeafNode v3.WalletConfigTree,
Comment thread
ScreamingHawk marked this conversation as resolved.
Outdated
sapientSignerLeafNode v3.WalletConfigTree,
) (*v3.WalletConfigTree, error) {
return createIntentTree(mainSigner, calls, payloadGateLeafNode, sapientSignerLeafNode)
}

func createIntentConfiguration(mainSigner common.Address, calls []*v3.CallsPayload, checkpoint uint64, additionalLeaves ...v3.WalletConfigTree) (*v3.WalletConfig, error) {
tree, err := createIntentTree(mainSigner, calls, additionalLeaves...)
func createIntentConfiguration(
mainSigner common.Address,
calls []*v3.CallsPayload,
checkpoint uint64,
payloadGateLeafNode v3.WalletConfigTree,
sapientSignerLeafNode v3.WalletConfigTree,
) (*v3.WalletConfig, error) {
tree, err := createIntentTree(mainSigner, calls, payloadGateLeafNode, sapientSignerLeafNode)
if err != nil {
return nil, err
}
Expand All @@ -238,9 +316,17 @@ func createIntentConfiguration(mainSigner common.Address, calls []*v3.CallsPaylo
}, nil
}

// `CreateIntentConfiguration` creates a wallet configuration where the intent's transaction batches are grouped into the initial subdigest.
func CreateIntentConfiguration(mainSigner common.Address, calls []*v3.CallsPayload, checkpoint uint64, sapientSignerLeafNode v3.WalletConfigTree) (*v3.WalletConfig, error) {
return createIntentConfiguration(mainSigner, calls, checkpoint, sapientSignerLeafNode)
// `CreateIntentConfiguration` creates a wallet configuration where the intent's transaction
// batches are grouped into the initial subdigest. See CreateIntentTree for
// payloadGateLeafNode and sapientSignerLeafNode semantics.
func CreateIntentConfiguration(
mainSigner common.Address,
calls []*v3.CallsPayload,
checkpoint uint64,
payloadGateLeafNode v3.WalletConfigTree,
sapientSignerLeafNode v3.WalletConfigTree,
) (*v3.WalletConfig, error) {
return createIntentConfiguration(mainSigner, calls, checkpoint, payloadGateLeafNode, sapientSignerLeafNode)
}

// `BuildIntentConfigurationSignature` creates a signature for an already-built intent configuration
Expand Down Expand Up @@ -287,7 +373,7 @@ func GetIntentConfigurationSignature(
sapientSignerLeafNode v3.WalletConfigTree,
signerSignatures []*core.SignerSignature,
) ([]byte, error) {
config, err := createIntentConfiguration(mainSigner, calls, checkpoint, sapientSignerLeafNode)
config, err := createIntentConfiguration(mainSigner, calls, checkpoint, nil, sapientSignerLeafNode)
Comment thread
ScreamingHawk marked this conversation as resolved.
Outdated
if err != nil {
return nil, err
}
Expand Down
Loading
Loading