@@ -5,13 +5,19 @@ import (
55 "flag"
66 "fmt"
77 "log"
8+ "net"
89 "net/http"
910 "os"
11+ "os/exec"
12+ "path/filepath"
13+ "strings"
14+ "time"
1015
1116 "github.com/stackdump/bitwrap-io/dsl"
1217 "github.com/stackdump/bitwrap-io/internal/server"
1318 "github.com/stackdump/bitwrap-io/public"
1419 "github.com/stackdump/bitwrap-io/internal/store"
20+ "github.com/stackdump/bitwrap-io/solidity"
1521)
1622
1723func main () {
@@ -21,6 +27,7 @@ func main() {
2127 noSolgen := flag .Bool ("no-solgen" , false , "Disable Solidity generation endpoints" )
2228 keyDir := flag .String ("key-dir" , "" , "Directory for persistent circuit keys (enables fast restarts)" )
2329 compile := flag .String ("compile" , "" , "Compile a .btw file and output JSON schema to stdout" )
30+ validate := flag .String ("validate" , "" , "Validate a .btw file: compile → generate Solidity → forge build → forge test → deploy" )
2431 flag .Parse ()
2532
2633 if * compile != "" {
@@ -44,6 +51,10 @@ func main() {
4451 return
4552 }
4653
54+ if * validate != "" {
55+ os .Exit (runValidate (* validate ))
56+ }
57+
4758 storage := store .NewFSStore (* dataDir )
4859
4960 publicFS , err := public .FS ()
@@ -70,3 +81,149 @@ func main() {
7081 log .Fatalf ("Server failed: %v" , err )
7182 }
7283}
84+
85+ // runValidate compiles a .btw file through the full pipeline:
86+ // parse → build schema → generate Solidity + tests → forge build → forge test → deploy to anvil
87+ func runValidate (path string ) int {
88+ src , err := os .ReadFile (path )
89+ if err != nil {
90+ fmt .Fprintf (os .Stderr , "FAIL: read %s: %v\n " , path , err )
91+ return 1
92+ }
93+
94+ // Step 1: Parse DSL
95+ fmt .Printf ("● parse %s\n " , path )
96+ ast , err := dsl .Parse (string (src ))
97+ if err != nil {
98+ fmt .Fprintf (os .Stderr , "FAIL: parse: %v\n " , err )
99+ return 1
100+ }
101+
102+ // Step 2: Build metamodel schema
103+ fmt .Printf ("● build schema: %s\n " , ast .Name )
104+ schema , err := dsl .Build (ast )
105+ if err != nil {
106+ fmt .Fprintf (os .Stderr , "FAIL: build: %v\n " , err )
107+ return 1
108+ }
109+ fmt .Printf (" states: %d, actions: %d, arcs: %d\n " ,
110+ len (schema .States ), len (schema .Actions ), len (schema .InputArcs ("" ))+ len (schema .OutputArcs ("" )))
111+
112+ // Step 3: Generate Solidity
113+ contractName := solidity .ContractName (schema .Name )
114+ contractCode := solidity .Generate (schema )
115+ testCode := solidity .GenerateTests (schema )
116+ genesisCode := solidity .GenerateGenesis (schema .Name , solidity.GenesisConfig {}, solidity .DefaultAddresses ())
117+ fmt .Printf ("● generate Solidity: %s.sol (%d bytes)\n " , contractName , len (contractCode ))
118+ fmt .Printf (" tests: %sTest.t.sol (%d bytes)\n " , contractName , len (testCode ))
119+ fmt .Printf (" genesis: %sGenesis.s.sol (%d bytes)\n " , contractName , len (genesisCode ))
120+
121+ // Step 4: Check for forge
122+ if _ , err := exec .LookPath ("forge" ); err != nil {
123+ fmt .Printf ("● forge not installed — skipping compilation and deployment\n " )
124+ fmt .Printf ("PASS (parse + generate only)\n " )
125+ return 0
126+ }
127+
128+ // Step 5: Set up temp Foundry project
129+ dir , err := os .MkdirTemp ("" , "bitwrap-validate-*" )
130+ if err != nil {
131+ fmt .Fprintf (os .Stderr , "FAIL: tmpdir: %v\n " , err )
132+ return 1
133+ }
134+ defer os .RemoveAll (dir )
135+
136+ for _ , sub := range []string {"src" , "test" , "script" } {
137+ os .MkdirAll (filepath .Join (dir , sub ), 0o755 )
138+ }
139+
140+ foundryToml := "[profile.default]\n src = \" src\" \n out = \" out\" \n libs = [\" lib\" ]\n solc_version = \" 0.8.20\" \n "
141+ os .WriteFile (filepath .Join (dir , "foundry.toml" ), []byte (foundryToml ), 0o644 )
142+ os .WriteFile (filepath .Join (dir , "src" , contractName + ".sol" ), []byte (contractCode ), 0o644 )
143+ os .WriteFile (filepath .Join (dir , "test" , contractName + "Test.t.sol" ), []byte (testCode ), 0o644 )
144+ os .WriteFile (filepath .Join (dir , "script" , contractName + "Genesis.s.sol" ), []byte (genesisCode ), 0o644 )
145+
146+ // Step 6: Install forge-std
147+ if ! runStep (dir , "install forge-std" , "git" , "init" ) {
148+ return 1
149+ }
150+ if ! runStep (dir , "install forge-std" , "forge" , "install" , "foundry-rs/forge-std" ) {
151+ return 1
152+ }
153+
154+ // Step 7: Compile
155+ if ! runStep (dir , "forge build" , "forge" , "build" ) {
156+ return 1
157+ }
158+
159+ // Step 8: Test
160+ if ! runStep (dir , "forge test" , "forge" , "test" , "-vv" ) {
161+ return 1
162+ }
163+
164+ // Step 9: Deploy to anvil
165+ if _ , err := exec .LookPath ("anvil" ); err != nil {
166+ fmt .Printf ("● anvil not installed — skipping deployment\n " )
167+ fmt .Printf ("PASS\n " )
168+ return 0
169+ }
170+
171+ listener , err := net .Listen ("tcp" , "127.0.0.1:0" )
172+ if err != nil {
173+ fmt .Fprintf (os .Stderr , "FAIL: find free port: %v\n " , err )
174+ return 1
175+ }
176+ port := listener .Addr ().(* net.TCPAddr ).Port
177+ listener .Close ()
178+
179+ rpcURL := fmt .Sprintf ("http://127.0.0.1:%d" , port )
180+ anvil := exec .Command ("anvil" , "--port" , fmt .Sprintf ("%d" , port ), "--silent" )
181+ if err := anvil .Start (); err != nil {
182+ fmt .Fprintf (os .Stderr , "FAIL: start anvil: %v\n " , err )
183+ return 1
184+ }
185+ defer func () { anvil .Process .Kill (); anvil .Wait () }()
186+
187+ // Wait for anvil
188+ for i := 0 ; i < 50 ; i ++ {
189+ cmd := exec .Command ("cast" , "chain-id" , "--rpc-url" , rpcURL )
190+ if out , err := cmd .CombinedOutput (); err == nil && strings .TrimSpace (string (out )) == "31337" {
191+ break
192+ }
193+ time .Sleep (100 * time .Millisecond )
194+ }
195+
196+ privKey := "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
197+ createArgs := []string {"create" , fmt .Sprintf ("src/%s.sol:%s" , contractName , contractName ),
198+ "--rpc-url" , rpcURL , "--private-key" , privKey , "--broadcast" }
199+
200+ cmd := exec .Command ("forge" , createArgs ... )
201+ cmd .Dir = dir
202+ out , err := cmd .CombinedOutput ()
203+ if err != nil || ! strings .Contains (string (out ), "Deployed to:" ) {
204+ fmt .Fprintf (os .Stderr , "● deploy FAIL\n %s\n " , out )
205+ return 1
206+ }
207+
208+ // Extract address
209+ for _ , line := range strings .Split (string (out ), "\n " ) {
210+ if strings .Contains (line , "Deployed to:" ) {
211+ fmt .Printf ("● deploy: %s\n " , strings .TrimSpace (line ))
212+ }
213+ }
214+
215+ fmt .Printf ("PASS\n " )
216+ return 0
217+ }
218+
219+ func runStep (dir , label , name string , args ... string ) bool {
220+ fmt .Printf ("● %s\n " , label )
221+ cmd := exec .Command (name , args ... )
222+ cmd .Dir = dir
223+ out , err := cmd .CombinedOutput ()
224+ if err != nil {
225+ fmt .Fprintf (os .Stderr , "FAIL: %s\n %s\n " , label , out )
226+ return false
227+ }
228+ return true
229+ }
0 commit comments