11import { spawnSync } from "node:child_process" ;
22import { existsSync , readFileSync } from "node:fs" ;
33import { join } from "node:path" ;
4+ import { parseAbiItem } from "abitype" ;
45import open from "open" ;
56import ora , { type Ora } from "ora" ;
67import prompts from "prompts" ;
@@ -16,12 +17,6 @@ export async function publishStylus(secretKey?: string) {
1617
1718 checkPrerequisites ( spinner , "cargo" , [ "--version" ] , "Rust (cargo)" ) ;
1819 checkPrerequisites ( spinner , "rustc" , [ "--version" ] , "Rust compiler (rustc)" ) ;
19- checkPrerequisites (
20- spinner ,
21- "solc" ,
22- [ "--version" ] ,
23- "Solidity compiler (solc)" ,
24- ) ;
2520
2621 const uri = await buildStylus ( spinner , secretKey ) ;
2722
@@ -35,12 +30,6 @@ export async function deployStylus(secretKey?: string) {
3530
3631 checkPrerequisites ( spinner , "cargo" , [ "--version" ] , "Rust (cargo)" ) ;
3732 checkPrerequisites ( spinner , "rustc" , [ "--version" ] , "Rust compiler (rustc)" ) ;
38- checkPrerequisites (
39- spinner ,
40- "solc" ,
41- [ "--version" ] ,
42- "Solidity compiler (solc)" ,
43- ) ;
4433
4534 const uri = await buildStylus ( spinner , secretKey ) ;
4635
@@ -99,25 +88,31 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
9988 }
10089 spinner . succeed ( "Initcode generated." ) ;
10190
102- // Step 3: Run stylus command to generate abi
91+ // Step 3: Run stylus command to generate abi (plain Solidity, no solc needed)
10392 spinner . start ( "Generating ABI..." ) ;
104- const abiResult = spawnSync ( "cargo" , [ "stylus" , "export-abi" , "--json" ] , {
93+ const abiResult = spawnSync ( "cargo" , [ "stylus" , "export-abi" ] , {
10594 encoding : "utf-8" ,
10695 } ) ;
10796 if ( abiResult . status !== 0 ) {
10897 spinner . fail ( "Failed to generate ABI." ) ;
10998 process . exit ( 1 ) ;
11099 }
111100
112- const abiContent = abiResult . stdout . trim ( ) ;
113- if ( ! abiContent ) {
101+ const solidityOutput = abiResult . stdout . trim ( ) ;
102+ if ( ! solidityOutput ) {
114103 spinner . fail ( "Failed to generate ABI." ) ;
115104 process . exit ( 1 ) ;
116105 }
106+
107+ const interfaces = parseSolidityInterfaces ( solidityOutput ) ;
108+ if ( interfaces . length === 0 ) {
109+ spinner . fail ( "No interfaces found in ABI output." ) ;
110+ process . exit ( 1 ) ;
111+ }
117112 spinner . succeed ( "ABI generated." ) ;
118113
119114 // Step 3.5: detect the constructor
120- spinner . start ( "Detecting constructor… " ) ;
115+ spinner . start ( "Detecting constructor\u2026 " ) ;
121116 const constructorResult = spawnSync ( "cargo" , [ "stylus" , "constructor" ] , {
122117 encoding : "utf-8" ,
123118 } ) ;
@@ -127,70 +122,48 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
127122 process . exit ( 1 ) ;
128123 }
129124
130- const constructorSigRaw = constructorResult . stdout . trim ( ) ; // e.g. "constructor(address owner)"
125+ const constructorSigRaw = constructorResult . stdout . trim ( ) ;
131126 spinner . succeed ( `Constructor found: ${ constructorSigRaw || "none" } ` ) ;
132127
133128 // Step 4: Process the output
134- const parts = abiContent . split ( / = = = = = = = < s t d i n > : / g) . filter ( Boolean ) ;
135- const contractNames = extractContractNamesFromExportAbi ( abiContent ) ;
136-
137- let selectedContractName : string | undefined ;
138- let selectedAbiContent : string | undefined ;
129+ let selectedIndex = 0 ;
139130
140- if ( contractNames . length === 1 ) {
141- selectedContractName = contractNames [ 0 ] ?. replace ( / ^ I / , "" ) ;
142- selectedAbiContent = parts [ 0 ] ;
143- } else {
131+ if ( interfaces . length > 1 ) {
144132 const response = await prompts ( {
145- choices : contractNames . map ( ( name , idx ) => ( {
146- title : name ,
133+ choices : interfaces . map ( ( iface , idx ) => ( {
134+ title : iface . name ,
147135 value : idx ,
148136 } ) ) ,
149137 message : "Select entrypoint:" ,
150138 name : "contract" ,
151139 type : "select" ,
152140 } ) ;
153141
154- const selectedIndex = response . contract ;
155-
156- if ( typeof selectedIndex !== "number" ) {
142+ if ( typeof response . contract !== "number" ) {
157143 spinner . fail ( "No contract selected." ) ;
158144 process . exit ( 1 ) ;
159145 }
160146
161- selectedContractName = contractNames [ selectedIndex ] ?. replace ( / ^ I / , "" ) ;
162- selectedAbiContent = parts [ selectedIndex ] ;
147+ selectedIndex = response . contract ;
163148 }
164149
165- if ( ! selectedAbiContent ) {
166- throw new Error ( "Entrypoint not found" ) ;
167- }
168-
169- if ( ! selectedContractName ) {
170- spinner . fail ( "Error: Could not determine contract name from ABI output." ) ;
171- process . exit ( 1 ) ;
172- }
173-
174- let cleanedAbi = "" ;
175- try {
176- const jsonMatch = selectedAbiContent . match ( / \[ .* \] / s) ;
177- if ( jsonMatch ) {
178- cleanedAbi = jsonMatch [ 0 ] ;
179- } else {
180- throw new Error ( "No valid JSON ABI found in the file." ) ;
181- }
182- } catch ( error ) {
183- spinner . fail ( "Error: ABI file contains invalid format." ) ;
184- console . error ( error ) ;
150+ const selectedInterface = interfaces [ selectedIndex ] ;
151+ if ( ! selectedInterface ) {
152+ spinner . fail ( "No interface found." ) ;
185153 process . exit ( 1 ) ;
186154 }
187155
188- // biome-ignore lint/suspicious/noExplicitAny: <>
189- const abiArray : any [ ] = JSON . parse ( cleanedAbi ) ;
156+ const selectedContractName = selectedInterface . name . replace ( / ^ I / , "" ) ;
157+ // biome-ignore lint/suspicious/noExplicitAny: ABI is untyped JSON from parseAbiItem
158+ const abiArray : any [ ] = selectedInterface . abi ;
190159
191160 const constructorAbi = constructorSigToAbi ( constructorSigRaw ) ;
192- if ( constructorAbi && ! abiArray . some ( ( e ) => e . type === "constructor" ) ) {
193- abiArray . unshift ( constructorAbi ) ; // put it at the top for readability
161+ if (
162+ constructorAbi &&
163+ // biome-ignore lint/suspicious/noExplicitAny: ABI entries have varying shapes
164+ ! abiArray . some ( ( e : any ) => e . type === "constructor" )
165+ ) {
166+ abiArray . unshift ( constructorAbi ) ;
194167 }
195168
196169 const metadata = {
@@ -256,10 +229,94 @@ async function buildStylus(spinner: Ora, secretKey?: string) {
256229 }
257230}
258231
259- function extractContractNamesFromExportAbi ( abiRawOutput : string ) : string [ ] {
260- return [ ...abiRawOutput . matchAll ( / < s t d i n > : ( I ? [ A - Z a - z 0 - 9 _ ] + ) / g) ]
261- . map ( ( m ) => m [ 1 ] )
262- . filter ( ( name ) : name is string => typeof name === "string" ) ;
232+ // biome-ignore lint/suspicious/noExplicitAny: ABI items from parseAbiItem are untyped
233+ type AbiEntry = any ;
234+ type ParsedInterface = { name : string ; abi : AbiEntry [ ] } ;
235+
236+ function parseSolidityInterfaces ( source : string ) : ParsedInterface [ ] {
237+ const results : ParsedInterface [ ] = [ ] ;
238+
239+ const ifaceRegex = / i n t e r f a c e \s + ( I ? [ A - Z a - z 0 - 9 _ ] + ) \s * \{ ( [ \s \S ] * ?) \n \} / g;
240+ for (
241+ let ifaceMatch = ifaceRegex . exec ( source ) ;
242+ ifaceMatch !== null ;
243+ ifaceMatch = ifaceRegex . exec ( source )
244+ ) {
245+ const name = ifaceMatch [ 1 ] ?? "" ;
246+ const body = ifaceMatch [ 2 ] ?? "" ;
247+ const abi : AbiEntry [ ] = [ ] ;
248+
249+ // Build struct lookup: name -> tuple type string
250+ const structs = new Map < string , string > ( ) ;
251+ const structRegex = / s t r u c t \s + ( \w + ) \s * \{ ( [ ^ } ] * ) \} / g;
252+ for (
253+ let structMatch = structRegex . exec ( body ) ;
254+ structMatch !== null ;
255+ structMatch = structRegex . exec ( body )
256+ ) {
257+ const fields = ( structMatch [ 2 ] ?? "" )
258+ . split ( ";" )
259+ . map ( ( f ) => f . trim ( ) )
260+ . filter ( Boolean )
261+ . map ( ( f ) => f . split ( / \s + / ) [ 0 ] ?? "" ) ;
262+ structs . set ( structMatch [ 1 ] ?? "" , `(${ fields . join ( "," ) } )` ) ;
263+ }
264+
265+ // Resolve struct references in a type string (iterative for nested structs)
266+ const resolveStructs = ( sig : string ) : string => {
267+ let resolved = sig ;
268+ for ( let i = 0 ; i < 10 ; i ++ ) {
269+ let changed = false ;
270+ for ( const [ sName , sTuple ] of structs ) {
271+ const re = new RegExp ( `\\b${ sName } \\b(\\[\\])?` , "g" ) ;
272+ const next = resolved . replace (
273+ re ,
274+ ( _ , arr ) => `${ sTuple } ${ arr ?? "" } ` ,
275+ ) ;
276+ if ( next !== resolved ) {
277+ resolved = next ;
278+ changed = true ;
279+ }
280+ }
281+ if ( ! changed ) break ;
282+ }
283+ return resolved ;
284+ } ;
285+
286+ // Extract each statement (function/error/event) delimited by ;
287+ const statements = body
288+ . split ( ";" )
289+ . map ( ( s ) => s . replace ( / \n / g, " " ) . trim ( ) )
290+ . filter (
291+ ( s ) =>
292+ s . startsWith ( "function " ) ||
293+ s . startsWith ( "error " ) ||
294+ s . startsWith ( "event " ) ,
295+ ) ;
296+
297+ for ( const stmt of statements ) {
298+ // Strip Solidity qualifiers that abitype doesn't expect
299+ let cleaned = stmt
300+ . replace ( / \b ( e x t e r n a l | p u b l i c | i n t e r n a l | p r i v a t e ) \b / g, "" )
301+ . replace ( / \b ( m e m o r y | c a l l d a t a | s t o r a g e ) \b / g, "" )
302+ . replace ( / \s + / g, " " )
303+ . trim ( ) ;
304+
305+ // Resolve struct type names to tuple types
306+ cleaned = resolveStructs ( cleaned ) ;
307+
308+ try {
309+ const parsed = parseAbiItem ( cleaned ) ;
310+ abi . push ( parsed ) ;
311+ } catch {
312+ // Skip unparseable items
313+ }
314+ }
315+
316+ results . push ( { abi, name } ) ;
317+ }
318+
319+ return results ;
263320}
264321
265322function getUrl ( hash : string , command : string ) {
0 commit comments