-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathABI.lean
More file actions
184 lines (167 loc) · 7.31 KB
/
ABI.lean
File metadata and controls
184 lines (167 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import Std
import Compiler.CompilationModel
import Compiler.Json
namespace Compiler.ABI
open Compiler.CompilationModel
open Compiler.Json
private def joinJsonFields (fields : List String) : String :=
String.intercalate ", " fields
private def abiTypeString : ParamType → String
| .uint256 => "uint256"
| .int256 => "int256"
| .uint8 => "uint8"
| .address => "address"
| .bool => "bool"
| .bytes32 => "bytes32"
| .string => "string"
| .bytes => "bytes"
| .tuple _ => "tuple"
| .array t => abiTypeString t ++ "[]"
| .fixedArray t n => abiTypeString t ++ "[" ++ toString n ++ "]"
| .adt _ _ => "tuple" -- ADTs are ABI-encoded as static tuples
| .newtypeOf _ baseType => abiTypeString baseType -- Erased to base type
-- Uses `fieldTypeToParamType` from CompilationModel (shared, not duplicated).
-- Uses `isInteropEntrypointName` from CompilationModel for consistent filtering.
private def functionOutputs (fn : FunctionSpec) : List ParamType :=
if !fn.returns.isEmpty then
fn.returns
else
match fn.returnType with
| some ty => [fieldTypeToParamType ty]
| none => []
mutual
private partial def abiComponents? : ParamType → Option String
| .tuple elems =>
let rendered := elems.map (fun ty => renderParam "" ty none)
some ("[" ++ String.intercalate ", " rendered ++ "]")
| .adt _ maxFields =>
-- ADTs encode as (uint8 tag, uint256 field₀, ..., uint256 fieldₙ)
let tagComponent := renderParam "tag" .uint8 none
let fieldComponents := List.range maxFields |>.map fun i =>
renderParam s!"field{i}" .uint256 none
some ("[" ++ String.intercalate ", " (tagComponent :: fieldComponents) ++ "]")
| .newtypeOf _ baseType => abiComponents? baseType
| .array t => abiComponents? t
| .fixedArray t _ => abiComponents? t
| _ => none
private partial def renderParam (name : String) (ty : ParamType) (indexed : Option Bool) : String :=
let base := [
s!"\"name\": {jsonString name}",
s!"\"type\": {jsonString (abiTypeString ty)}"
]
let withComponents :=
match abiComponents? ty with
| some components => base ++ [s!"\"components\": {components}"]
| none => base
let allFields :=
match indexed with
| some isIndexed => withComponents ++ [s!"\"indexed\": {(if isIndexed then "true" else "false")}"]
| none => withComponents
"{" ++ joinJsonFields allFields ++ "}"
end
private def renderFunctionEntry (fn : FunctionSpec) : String :=
let inputs := fn.params.map (fun p => renderParam p.name p.ty none)
let outputs := (functionOutputs fn).map (fun ty => renderParam "" ty none)
let stateMutability := functionStateMutability fn
"{" ++ joinJsonFields [
"\"type\": \"function\"",
s!"\"name\": {jsonString fn.name}",
s!"\"inputs\": [" ++ String.intercalate ", " inputs ++ "]",
s!"\"outputs\": [" ++ String.intercalate ", " outputs ++ "]",
s!"\"stateMutability\": {jsonString stateMutability}"
] ++ "}"
private def renderEventEntry (ev : EventDef) : String :=
let inputs := ev.params.map (fun p => renderParam p.name p.ty (some (p.kind == .indexed)))
"{" ++ joinJsonFields [
"\"type\": \"event\"",
s!"\"name\": {jsonString ev.name}",
s!"\"inputs\": [" ++ String.intercalate ", " inputs ++ "]",
"\"anonymous\": false"
] ++ "}"
private def renderErrorEntry (err : ErrorDef) : String :=
let inputs := err.params.map (fun ty => renderParam "" ty none)
"{" ++ joinJsonFields [
"\"type\": \"error\"",
s!"\"name\": {jsonString err.name}",
s!"\"inputs\": [" ++ String.intercalate ", " inputs ++ "]"
] ++ "}"
private def renderConstructorEntry (ctor : ConstructorSpec) : String :=
let inputs := ctor.params.map (fun p => renderParam p.name p.ty none)
let stateMutability := if ctor.isPayable then "payable" else "nonpayable"
"{" ++ joinJsonFields [
"\"type\": \"constructor\"",
s!"\"inputs\": [" ++ String.intercalate ", " inputs ++ "]",
s!"\"stateMutability\": {jsonString stateMutability}"
] ++ "}"
/-- Render an ABI entry for a special entrypoint (fallback/receive).
Uses `isInteropEntrypointName` so this stays in sync with selector filtering.
Returns `some` for special entrypoints, `none` otherwise (defensive guard
even though the caller pre-filters via `isInteropEntrypointName`). -/
private def renderSpecialEntry (fn : FunctionSpec) : Option String :=
if !isInteropEntrypointName fn.name then
none
else
some ("{" ++ joinJsonFields [
s!"\"type\": {jsonString fn.name}",
s!"\"stateMutability\": {jsonString (functionStateMutability fn)}"
] ++ "}")
def emitContractABIJson (spec : CompilationModel) : String :=
let ctorEntries :=
match spec.constructor with
| some ctor => [renderConstructorEntry ctor]
| none => []
let functionEntries := spec.functions.filter (fun fn => !fn.isInternal && !isInteropEntrypointName fn.name) |>.map renderFunctionEntry
let specialEntries := (spec.functions.filter (fun fn => !fn.isInternal && isInteropEntrypointName fn.name)).filterMap renderSpecialEntry
let eventEntries := spec.events.map renderEventEntry
let errorEntries := spec.errors.map renderErrorEntry
let entries := ctorEntries ++ functionEntries ++ eventEntries ++ errorEntries ++ specialEntries
"[\n " ++ String.intercalate ",\n " entries ++ "\n]\n"
def writeContractABIFile (outDir : String) (spec : CompilationModel) : IO Unit := do
IO.FS.createDirAll outDir
let path := s!"{outDir}/{spec.name}.abi.json"
IO.FS.writeFile path (emitContractABIJson spec)
/-- Render the storage layout for a contract as a JSON object.
Includes EIP-7201 namespace when present (#1730, Axis 4 Step 4d).
The output is a JSON object with `"contract"`, `"storageNamespace"`,
and `"fields"` keys. -/
def emitContractStorageLayoutJson (spec : CompilationModel) : String :=
let nsTerm := match spec.storageNamespace with
| some ns => jsonString (toString ns)
| none => "null"
let fieldEntries := renderFields spec.fields 0
"{" ++ joinJsonFields [
s!"\"contract\": {jsonString spec.name}",
s!"\"storageNamespace\": {nsTerm}",
s!"\"fields\": [{String.intercalate ", " fieldEntries}]"
] ++ "}\n"
where
renderFieldType : FieldType → String
| .uint256 => "uint256"
| .address => "address"
| .adt name maxFields => s!"adt({name},{maxFields})"
| .dynamicArray elemType => renderStorageArrayElemType elemType ++ "[]"
| .mappingTyped _ => "mapping"
| .mappingStruct _ _ => "mapping"
| .mappingStruct2 _ _ _ => "mapping"
renderStorageArrayElemType : StorageArrayElemType → String
| .uint256 => "uint256"
| .address => "address"
| .bool => "bool"
| .uint8 => "uint8"
| .bytes32 => "bytes32"
renderFields (fields : List Field) (idx : Nat) : List String :=
match fields with
| [] => []
| f :: rest =>
let slot := f.slot.getD idx
let entry := "{" ++ joinJsonFields [
s!"\"name\": {jsonString f.name}",
s!"\"slot\": {jsonString (toString slot)}",
s!"\"type\": {jsonString (renderFieldType f.ty)}"
] ++ "}"
entry :: renderFields rest (idx + 1)
def writeContractStorageLayoutFile (outDir : String) (spec : CompilationModel) : IO Unit := do
IO.FS.createDirAll outDir
let path := s!"{outDir}/{spec.name}.storage.json"
IO.FS.writeFile path (emitContractStorageLayoutJson spec)
end Compiler.ABI