Skip to content

Commit 8e47933

Browse files
committed
Move path to abicalldata
1 parent 3241ee2 commit 8e47933

11 files changed

Lines changed: 99 additions & 94 deletions

File tree

lib/abicalldata/README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ABI calldata
22

3-
Helpers for **Ethereum ABI-encoded calldata**: head layout, static argument words, and dynamic `bytes`/`string` tails. Also defines **`ByteRange`** and **`Selector`** for locating byte spans inside a v3 **`CallsPayload`**.
3+
Helpers for **Ethereum ABI-encoded calldata**: head layout, static argument words, and dynamic `bytes`/`string` tails. Also defines **`ByteRange`**, **`Selector`**, and a fluent **`Path`** for locating byte spans inside a v3 **`CallsPayload`**.
44

55
## Calldata layout
66

@@ -33,4 +33,35 @@ sel := abicalldata.NewRangeSelector(0, 0x24, 32)
3333
ranges, err := sel.Resolve(payload)
3434
```
3535

36-
**`Selector`** is implemented by types in other packages that walk `CallsPayload` and nested calldata; they typically use the functions above to turn `abi.Method` + argument index into `ByteRange` updates. **`NewRangeSelector`** is the built-in implementation for a fixed call index, offset, and length.
36+
## Path builder (`NewPath`)
37+
38+
**`*Path`** is a **`Selector`**: chain steps to walk from a top-level call into nested packed calls and ABI argument slots, then call **`.AsSelector()`** for APIs that take a **`Selector`**.
39+
40+
Typical steps:
41+
42+
- **`.CallData(i)`** — start from `payload.Calls[i].Data`.
43+
- **`.ABI(contractABI, method)`** — bind the current range to that method (checks the 4-byte selector).
44+
- **`.ArgSlot(name)`** / **`.ArgSlotIndex(i)`** — static argument word(s) in the current frame.
45+
- **`.ArgBytesData(name)`** / **`.ArgBytesDataIndex(i)`** — inner payload of a `bytes`/`string` argument (clears ABI context; rebind with `.ABI` before further arg steps).
46+
- **`.ArgBytesEncoded(name)`** — full ABI-encoded tail for that dynamic argument.
47+
- **`.EncodedCallsPayload()`** — treat the active range as v3 packed calls; clear ABI context.
48+
- **`.EncodedCallData(j)`** — select packed call `j`’s calldata within that layout.
49+
- **`.Slice(offset, size)`** — byte slice within the active range.
50+
51+
```go
52+
sel := abicalldata.NewPath().
53+
CallData(0).
54+
ABI(&outerABI, "hydrateExecute").
55+
ArgBytesData("payload").
56+
EncodedCallsPayload().
57+
EncodedCallData(0).
58+
ABI(&tokenABI, "permit").
59+
ArgSlot("value").
60+
AsSelector()
61+
```
62+
63+
After any step that narrows the range or switches to a new byte frame (including **`.Slice`**, **`.ArgBytesData`**, **`.ArgBytesDataIndex`**, **`.ArgBytesEncoded`**, **`.EncodedCallsPayload`**, **`.EncodedCallData`**), ABI context is cleared. Call **`.ABI(...)`** again before **`.ArgSlot`**, **`.ArgSlotIndex`**, **`.ArgBytesData`**, **`.ArgBytesDataIndex`**, or **`.ArgBytesEncoded`** on the new frame.
64+
65+
## Packed calls layout
66+
67+
**`ParsePackedCalls(packed []byte)`** returns a **`PackedCallsLayout`** describing where each call’s calldata sits inside the encoded packed-calls blob (as produced by `CallsPayload.Encode`). **`CallData`** entries use **`Span`** (`Start`, `Len`; `Start == -1` when that call has no calldata). The path step **`.EncodedCallData(i)`** uses this parser internally.

lib/sapient/malleable/locators_packedcalls.go renamed to lib/abicalldata/packedcalls.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package malleable
1+
package abicalldata
22

33
import (
44
"encoding/binary"

lib/sapient/malleable/locators_packedcalls_test.go renamed to lib/abicalldata/packedcalls_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package malleable
1+
package abicalldata
22

33
import (
44
"math/big"
Lines changed: 43 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package malleable
1+
package abicalldata
22

33
import (
44
"fmt"
@@ -7,8 +7,6 @@ import (
77

88
"github.com/0xsequence/ethkit/go-ethereum/accounts/abi"
99
v3 "github.com/0xsequence/go-sequence/core/v3"
10-
11-
"github.com/0xsequence/go-sequence/lib/abicalldata"
1210
)
1311

1412
type Path struct {
@@ -80,11 +78,11 @@ func (p *Path) EncodedCallData(i int) *Path {
8078
return p
8179
}
8280

83-
func (p *Path) AsSelector() abicalldata.Selector {
81+
func (p *Path) AsSelector() Selector {
8482
return p
8583
}
8684

87-
func (p *Path) Resolve(payload *v3.CallsPayload) ([]abicalldata.ByteRange, error) {
85+
func (p *Path) Resolve(payload *v3.CallsPayload) ([]ByteRange, error) {
8886
state := pathState{}
8987
for _, step := range p.steps {
9088
if err := step.apply(payload, &state); err != nil {
@@ -105,13 +103,13 @@ func (p *Path) String() string {
105103
}
106104

107105
type pathState struct {
108-
ranges []abicalldata.ByteRange
106+
ranges []ByteRange
109107
// method only applies to the current full calldata frame. Any step that
110108
// narrows or reinterprets the active range must clear it.
111109
method *abi.Method
112110
}
113111

114-
func (s *pathState) replaceRanges(ranges []abicalldata.ByteRange) {
112+
func (s *pathState) replaceRanges(ranges []ByteRange) {
115113
s.ranges = ranges
116114
s.method = nil
117115
}
@@ -135,8 +133,8 @@ type pathStep interface {
135133
apply(payload *v3.CallsPayload, state *pathState) error
136134
}
137135

138-
func mapRanges(ranges []abicalldata.ByteRange, fn func(abicalldata.ByteRange) (abicalldata.ByteRange, error)) ([]abicalldata.ByteRange, error) {
139-
out := make([]abicalldata.ByteRange, 0, len(ranges))
136+
func mapRanges(ranges []ByteRange, fn func(ByteRange) (ByteRange, error)) ([]ByteRange, error) {
137+
out := make([]ByteRange, 0, len(ranges))
140138
for _, r := range ranges {
141139
next, err := fn(r)
142140
if err != nil {
@@ -158,7 +156,7 @@ func (s callDataStep) apply(payload *v3.CallsPayload, state *pathState) error {
158156
if s.index < 0 || s.index >= len(payload.Calls) {
159157
return fmt.Errorf("call index out of range: %d", s.index)
160158
}
161-
state.replaceRanges([]abicalldata.ByteRange{{
159+
state.replaceRanges([]ByteRange{{
162160
CallIndex: s.index,
163161
Offset: 0,
164162
Size: len(payload.Calls[s.index].Data),
@@ -175,17 +173,17 @@ func (s sliceStep) apply(payload *v3.CallsPayload, state *pathState) error {
175173
if len(state.ranges) == 0 {
176174
return fmt.Errorf("slice step has no active ranges")
177175
}
178-
out, err := mapRanges(state.ranges, func(r abicalldata.ByteRange) (abicalldata.ByteRange, error) {
176+
out, err := mapRanges(state.ranges, func(r ByteRange) (ByteRange, error) {
179177
if s.offset < 0 || s.size < 0 {
180-
return abicalldata.ByteRange{}, fmt.Errorf("slice offset/size negative: %d, %d", s.offset, s.size)
178+
return ByteRange{}, fmt.Errorf("slice offset/size negative: %d, %d", s.offset, s.size)
181179
}
182180
if s.size > r.Size || s.offset > r.Size-s.size {
183-
return abicalldata.ByteRange{}, fmt.Errorf("slice out of bounds: offset=%d size=%d within %d", s.offset, s.size, r.Size)
181+
return ByteRange{}, fmt.Errorf("slice out of bounds: offset=%d size=%d within %d", s.offset, s.size, r.Size)
184182
}
185183
if s.offset > math.MaxInt-r.Offset {
186-
return abicalldata.ByteRange{}, fmt.Errorf("slice offset overflow with range")
184+
return ByteRange{}, fmt.Errorf("slice offset overflow with range")
187185
}
188-
return abicalldata.ByteRange{
186+
return ByteRange{
189187
CallIndex: r.CallIndex,
190188
Offset: r.Offset + s.offset,
191189
Size: s.size,
@@ -250,15 +248,15 @@ func (s argSlotStep) apply(payload *v3.CallsPayload, state *pathState) error {
250248
if isDynamicType(argType) {
251249
return fmt.Errorf("arg %s is dynamic (%s)", s.name, argType.String())
252250
}
253-
start, length, err := abicalldata.CalldataStaticWord(method, argIndex)
251+
start, length, err := CalldataStaticWord(method, argIndex)
254252
if err != nil {
255253
return err
256254
}
257-
out, err := mapRanges(state.ranges, func(r abicalldata.ByteRange) (abicalldata.ByteRange, error) {
255+
out, err := mapRanges(state.ranges, func(r ByteRange) (ByteRange, error) {
258256
if start+length > r.Size {
259-
return abicalldata.ByteRange{}, fmt.Errorf("arg slot out of bounds for %s", s.name)
257+
return ByteRange{}, fmt.Errorf("arg slot out of bounds for %s", s.name)
260258
}
261-
return abicalldata.ByteRange{
259+
return ByteRange{
262260
CallIndex: r.CallIndex,
263261
Offset: r.Offset + start,
264262
Size: length,
@@ -288,15 +286,15 @@ func (s argSlotIndexStep) apply(payload *v3.CallsPayload, state *pathState) erro
288286
if isDynamicType(argType) {
289287
return fmt.Errorf("arg %d is dynamic (%s)", argIndex, argType.String())
290288
}
291-
start, length, err := abicalldata.CalldataStaticWord(method, argIndex)
289+
start, length, err := CalldataStaticWord(method, argIndex)
292290
if err != nil {
293291
return err
294292
}
295-
out, err := mapRanges(state.ranges, func(r abicalldata.ByteRange) (abicalldata.ByteRange, error) {
293+
out, err := mapRanges(state.ranges, func(r ByteRange) (ByteRange, error) {
296294
if start+length > r.Size {
297-
return abicalldata.ByteRange{}, fmt.Errorf("arg slot out of bounds for %d", argIndex)
295+
return ByteRange{}, fmt.Errorf("arg slot out of bounds for %d", argIndex)
298296
}
299-
return abicalldata.ByteRange{
297+
return ByteRange{
300298
CallIndex: r.CallIndex,
301299
Offset: r.Offset + start,
302300
Size: length,
@@ -326,16 +324,16 @@ func (s argBytesDataStep) apply(payload *v3.CallsPayload, state *pathState) erro
326324
if argType.T != abi.BytesTy && argType.T != abi.StringTy {
327325
return fmt.Errorf("arg %s is not bytes/string (%s)", s.name, argType.String())
328326
}
329-
out, err := mapRanges(state.ranges, func(r abicalldata.ByteRange) (abicalldata.ByteRange, error) {
327+
out, err := mapRanges(state.ranges, func(r ByteRange) (ByteRange, error) {
330328
data, err := r.Slice(payload)
331329
if err != nil {
332-
return abicalldata.ByteRange{}, err
330+
return ByteRange{}, err
333331
}
334-
start, length, err := abicalldata.CalldataBytesContent(data, method, argIndex)
332+
start, length, err := CalldataBytesContent(data, method, argIndex)
335333
if err != nil {
336-
return abicalldata.ByteRange{}, err
334+
return ByteRange{}, err
337335
}
338-
return abicalldata.ByteRange{
336+
return ByteRange{
339337
CallIndex: r.CallIndex,
340338
Offset: r.Offset + start,
341339
Size: length,
@@ -365,16 +363,16 @@ func (s argBytesDataIndexStep) apply(payload *v3.CallsPayload, state *pathState)
365363
if argType.T != abi.BytesTy && argType.T != abi.StringTy {
366364
return fmt.Errorf("arg %d is not bytes/string (%s)", argIndex, argType.String())
367365
}
368-
out, err := mapRanges(state.ranges, func(r abicalldata.ByteRange) (abicalldata.ByteRange, error) {
366+
out, err := mapRanges(state.ranges, func(r ByteRange) (ByteRange, error) {
369367
data, err := r.Slice(payload)
370368
if err != nil {
371-
return abicalldata.ByteRange{}, err
369+
return ByteRange{}, err
372370
}
373-
start, length, err := abicalldata.CalldataBytesContent(data, method, argIndex)
371+
start, length, err := CalldataBytesContent(data, method, argIndex)
374372
if err != nil {
375-
return abicalldata.ByteRange{}, err
373+
return ByteRange{}, err
376374
}
377-
return abicalldata.ByteRange{
375+
return ByteRange{
378376
CallIndex: r.CallIndex,
379377
Offset: r.Offset + start,
380378
Size: length,
@@ -404,16 +402,16 @@ func (s argBytesEncodedStep) apply(payload *v3.CallsPayload, state *pathState) e
404402
if argType.T != abi.BytesTy && argType.T != abi.StringTy {
405403
return fmt.Errorf("arg %s is not bytes/string (%s)", s.name, argType.String())
406404
}
407-
out, err := mapRanges(state.ranges, func(r abicalldata.ByteRange) (abicalldata.ByteRange, error) {
405+
out, err := mapRanges(state.ranges, func(r ByteRange) (ByteRange, error) {
408406
data, err := r.Slice(payload)
409407
if err != nil {
410-
return abicalldata.ByteRange{}, err
408+
return ByteRange{}, err
411409
}
412-
start, length, err := abicalldata.CalldataBytesEncoded(data, method, argIndex)
410+
start, length, err := CalldataBytesEncoded(data, method, argIndex)
413411
if err != nil {
414-
return abicalldata.ByteRange{}, err
412+
return ByteRange{}, err
415413
}
416-
return abicalldata.ByteRange{
414+
return ByteRange{
417415
CallIndex: r.CallIndex,
418416
Offset: r.Offset + start,
419417
Size: length,
@@ -444,26 +442,26 @@ func (s encodedCallDataStep) apply(payload *v3.CallsPayload, state *pathState) e
444442
if len(state.ranges) == 0 {
445443
return fmt.Errorf("encodedCallData step has no active ranges")
446444
}
447-
out, err := mapRanges(state.ranges, func(r abicalldata.ByteRange) (abicalldata.ByteRange, error) {
445+
out, err := mapRanges(state.ranges, func(r ByteRange) (ByteRange, error) {
448446
data, err := r.Slice(payload)
449447
if err != nil {
450-
return abicalldata.ByteRange{}, err
448+
return ByteRange{}, err
451449
}
452450
layout, err := ParsePackedCalls(data)
453451
if err != nil {
454-
return abicalldata.ByteRange{}, err
452+
return ByteRange{}, err
455453
}
456454
if s.index < 0 || s.index >= layout.NumCalls {
457-
return abicalldata.ByteRange{}, fmt.Errorf("packed call index out of range: %d", s.index)
455+
return ByteRange{}, fmt.Errorf("packed call index out of range: %d", s.index)
458456
}
459457
span := layout.CallData[s.index]
460458
if span.Start < 0 || span.Len == 0 {
461-
return abicalldata.ByteRange{}, fmt.Errorf("packed call %d has no calldata", s.index)
459+
return ByteRange{}, fmt.Errorf("packed call %d has no calldata", s.index)
462460
}
463461
if span.Start > math.MaxInt-r.Offset {
464-
return abicalldata.ByteRange{}, fmt.Errorf("packed call %d offset overflow", s.index)
462+
return ByteRange{}, fmt.Errorf("packed call %d offset overflow", s.index)
465463
}
466-
return abicalldata.ByteRange{
464+
return ByteRange{
467465
CallIndex: r.CallIndex,
468466
Offset: r.Offset + span.Start,
469467
Size: span.Len,
@@ -485,24 +483,6 @@ func argIndexByName(method abi.Method, name string) (int, error) {
485483
return -1, fmt.Errorf("arg not found: %s", name)
486484
}
487485

488-
func isDynamicType(t abi.Type) bool {
489-
switch t.T {
490-
case abi.BytesTy, abi.StringTy, abi.SliceTy:
491-
return true
492-
case abi.ArrayTy:
493-
return t.Size == 0 || isDynamicType(*t.Elem)
494-
case abi.TupleTy:
495-
for _, elem := range t.TupleElems {
496-
if isDynamicType(*elem) {
497-
return true
498-
}
499-
}
500-
return false
501-
default:
502-
return false
503-
}
504-
}
505-
506486
func bytesEqual(a, b []byte) bool {
507487
if len(a) != len(b) {
508488
return false
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package malleable
1+
package abicalldata
22

33
import (
44
"math/big"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package malleable
1+
package abicalldata
22

33
type Span struct {
44
Start int

lib/hydrate/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
Build `hydratePayload` bytes for **HydrateProxy** (`hydrateExecute` / `hydrateExecuteAndSweep` in trails-contracts) by resolving patch targets with [`abicalldata`](https://github.com/0xsequence/go-sequence/tree/master/lib/abicalldata) **`Selector`** and **`ByteRange`** instead of hand-counting calldata offsets.
44

5-
`hydrate` depends only on `abicalldata` for selector types. The example below builds an `abicalldata.Selector` using `Path` from `lib/sapient/malleable`; fixed offsets can use `abicalldata.NewRangeSelector` instead.
5+
This package depends on **`abicalldata`** for selector types and for building selectors with **`abicalldata.NewPath()`** (or **`abicalldata.NewRangeSelector`** for fixed offsets).
66

77
## Usage
88

99
```go
1010
payload := v3.NewCallsPayload(...)
1111

1212
// Selector must resolve to exactly one range on the target call (same index as ForCall).
13-
permitOwner := malleable.NewPath().
13+
permitOwner := abicalldata.NewPath().
1414
CallData(0).
1515
ABI(trailsABI, "hydrateExecute").
1616
ArgBytesData("packedPayload").
@@ -47,7 +47,7 @@ calldata, err := hydrate.PackHydrateExecuteAndSweep(
4747
)
4848
```
4949

50-
If ABI parameters are unnamed, use index-based steps such as `ArgSlotIndex` / `ArgBytesDataIndex` when your path builder provides them.
50+
If ABI parameters are unnamed, use index-based steps such as **`ArgSlotIndex`** / **`ArgBytesDataIndex`** on **`abicalldata.NewPath()`** (see the abicalldata README).
5151

5252
## Call sections and ordering
5353

lib/hydrate/builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func SourceTxOrigin() AddrSource { return AddrSource{kind: DataTx
2727
func SourceAddress(a common.Address) AddrSource { return AddrSource{kind: DataAnyAddress, addr: a} }
2828

2929
// Builder constructs hydratePayload bytes for HydrateProxy using abicalldata.Selector
30-
// (calldata selectors: ABI paths via malleable.NewPath().AsSelector() in app code, fixed
30+
// (calldata selectors: ABI paths via abicalldata.NewPath().AsSelector() in app code, fixed
3131
// ranges via abicalldata.NewRangeSelector, etc.) so call-data offsets are not hand-computed.
3232
//
3333
// Sections are emitted in ascending call index order, which matches how HydrateProxy

lib/hydrate/builder_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/0xsequence/ethkit/go-ethereum/common"
1111
v3 "github.com/0xsequence/go-sequence/core/v3"
1212
"github.com/0xsequence/go-sequence/lib/abicalldata"
13-
"github.com/0xsequence/go-sequence/lib/sapient/malleable"
1413
"github.com/stretchr/testify/require"
1514
)
1615

@@ -158,7 +157,7 @@ func TestBuilder_DataAddress_ArgSlotUsesRightAlignedAddressBytes(t *testing.T) {
158157
big.NewInt(0),
159158
)
160159

161-
ownerSelector := malleable.NewPath().
160+
ownerSelector := abicalldata.NewPath().
162161
CallData(0).
163162
ABI(&ownerABI, "setOwner").
164163
ArgSlot("owner").

0 commit comments

Comments
 (0)