gate-evm-tools 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +6 -2
  2. package/bin/cli.js +3 -0
  3. package/bin/delivery.cjs +11 -6
  4. package/bin/migration.js +38 -0
  5. package/dist/commands/prepare.d.ts.map +1 -1
  6. package/dist/commands/prepare.js +7 -1
  7. package/dist/commands/prepare.js.map +1 -1
  8. package/dist/core/BytecodeChecker.d.ts.map +1 -1
  9. package/dist/core/BytecodeChecker.js +27 -4
  10. package/dist/core/BytecodeChecker.js.map +1 -1
  11. package/dist/core/preparation/artifacts.d.ts.map +1 -1
  12. package/dist/core/preparation/artifacts.js +12 -5
  13. package/dist/core/preparation/artifacts.js.map +1 -1
  14. package/dist/core/preparation/index.d.ts +3 -0
  15. package/dist/core/preparation/index.d.ts.map +1 -1
  16. package/dist/core/preparation/index.js +9 -2
  17. package/dist/core/preparation/index.js.map +1 -1
  18. package/dist/core/preparation/zksync.d.ts.map +1 -1
  19. package/dist/core/preparation/zksync.js +5 -0
  20. package/dist/core/preparation/zksync.js.map +1 -1
  21. package/dist/core/validateAndGenerateCalldata.d.ts.map +1 -1
  22. package/dist/core/validateAndGenerateCalldata.js +3 -0
  23. package/dist/core/validateAndGenerateCalldata.js.map +1 -1
  24. package/dist/deployment-zksync/index.d.ts +40 -0
  25. package/dist/deployment-zksync/index.d.ts.map +1 -0
  26. package/dist/deployment-zksync/index.js +59 -0
  27. package/dist/deployment-zksync/index.js.map +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +7 -12
  30. package/dist/index.js.map +1 -1
  31. package/dist/migration/index.d.ts +76 -0
  32. package/dist/migration/index.d.ts.map +1 -0
  33. package/dist/migration/index.js +284 -0
  34. package/dist/migration/index.js.map +1 -0
  35. package/dist/types.d.ts +1 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist-cjs/core/BytecodeChecker.js +27 -4
  38. package/dist-cjs/core/BytecodeChecker.js.map +1 -1
  39. package/dist-cjs/core/preparation/artifacts.js +11 -4
  40. package/dist-cjs/core/preparation/artifacts.js.map +1 -1
  41. package/dist-cjs/core/preparation/index.js +47 -6
  42. package/dist-cjs/core/preparation/index.js.map +1 -1
  43. package/dist-cjs/core/preparation/zksync.js +5 -0
  44. package/dist-cjs/core/preparation/zksync.js.map +1 -1
  45. package/dist-cjs/deployment-zksync/index.js +67 -0
  46. package/dist-cjs/deployment-zksync/index.js.map +1 -0
  47. package/dist-cjs/migration/index.js +328 -0
  48. package/dist-cjs/migration/index.js.map +1 -0
  49. package/dist-runtime/delivery/aggregate-diagnostics.js +3 -0
  50. package/dist-runtime/delivery/aggregate-diagnostics.js.map +1 -1
  51. package/dist-runtime/delivery/aggregate-inputs.d.ts +32 -6
  52. package/dist-runtime/delivery/aggregate-inputs.js +7 -3
  53. package/dist-runtime/delivery/aggregate-inputs.js.map +1 -1
  54. package/dist-runtime/delivery/aggregate-worker.js +3 -3
  55. package/dist-runtime/delivery/aggregate-worker.js.map +1 -1
  56. package/dist-runtime/delivery/aggregate.js +2 -2
  57. package/dist-runtime/delivery/aggregate.js.map +1 -1
  58. package/dist-runtime/delivery/cli-host.d.ts +17 -0
  59. package/dist-runtime/delivery/cli-host.js +118 -0
  60. package/dist-runtime/delivery/cli-host.js.map +1 -0
  61. package/dist-runtime/delivery/cli.d.ts +114 -0
  62. package/dist-runtime/delivery/cli.js +228 -0
  63. package/dist-runtime/delivery/cli.js.map +1 -0
  64. package/dist-runtime/delivery/compiler-dependency-pins.js +30 -1
  65. package/dist-runtime/delivery/compiler-dependency-pins.js.map +1 -1
  66. package/dist-runtime/delivery/configuration-definition.js +152 -11
  67. package/dist-runtime/delivery/configuration-definition.js.map +1 -1
  68. package/dist-runtime/delivery/configuration-hre.js +1 -1
  69. package/dist-runtime/delivery/configuration-hre.js.map +1 -1
  70. package/dist-runtime/delivery/configuration.js +7 -4
  71. package/dist-runtime/delivery/configuration.js.map +1 -1
  72. package/dist-runtime/delivery/custom-format.d.ts +2 -1
  73. package/dist-runtime/delivery/custom-format.js +15 -2
  74. package/dist-runtime/delivery/custom-format.js.map +1 -1
  75. package/dist-runtime/delivery/custom.d.ts +4 -0
  76. package/dist-runtime/delivery/custom.js +21 -0
  77. package/dist-runtime/delivery/custom.js.map +1 -1
  78. package/dist-runtime/delivery/development-inputs.d.ts +23 -0
  79. package/dist-runtime/delivery/development-inputs.js +117 -0
  80. package/dist-runtime/delivery/development-inputs.js.map +1 -0
  81. package/dist-runtime/delivery/development.d.ts +1 -0
  82. package/dist-runtime/delivery/development.js +7 -1
  83. package/dist-runtime/delivery/development.js.map +1 -1
  84. package/dist-runtime/delivery/execution-records.d.ts +16 -0
  85. package/dist-runtime/delivery/execution-records.js +48 -0
  86. package/dist-runtime/delivery/execution-records.js.map +1 -0
  87. package/dist-runtime/delivery/existing-code.d.ts +3 -0
  88. package/dist-runtime/delivery/existing-code.js +20 -0
  89. package/dist-runtime/delivery/existing-code.js.map +1 -1
  90. package/dist-runtime/delivery/history-package.d.ts +2 -0
  91. package/dist-runtime/delivery/history-package.js +70 -0
  92. package/dist-runtime/delivery/history-package.js.map +1 -0
  93. package/dist-runtime/delivery/input-files.d.ts +2 -0
  94. package/dist-runtime/delivery/input-files.js +5 -0
  95. package/dist-runtime/delivery/input-files.js.map +1 -1
  96. package/dist-runtime/delivery/inputs.js +11 -6
  97. package/dist-runtime/delivery/inputs.js.map +1 -1
  98. package/dist-runtime/delivery/materialization.js +20 -1
  99. package/dist-runtime/delivery/materialization.js.map +1 -1
  100. package/dist-runtime/delivery/planning.d.ts +1 -0
  101. package/dist-runtime/delivery/planning.js +10 -2
  102. package/dist-runtime/delivery/planning.js.map +1 -1
  103. package/dist-runtime/delivery/production-finalization.js +3 -2
  104. package/dist-runtime/delivery/production-finalization.js.map +1 -1
  105. package/dist-runtime/delivery/production.d.ts +2 -0
  106. package/dist-runtime/delivery/production.js +5 -4
  107. package/dist-runtime/delivery/production.js.map +1 -1
  108. package/dist-runtime/delivery/project-files.js +6 -1
  109. package/dist-runtime/delivery/project-files.js.map +1 -1
  110. package/dist-runtime/delivery/project-rpc.d.ts +1 -1
  111. package/dist-runtime/delivery/project-rpc.js +10 -4
  112. package/dist-runtime/delivery/project-rpc.js.map +1 -1
  113. package/dist-runtime/delivery/protocol.d.ts +6 -4
  114. package/dist-runtime/delivery/protocol.js +16 -5
  115. package/dist-runtime/delivery/protocol.js.map +1 -1
  116. package/dist-runtime/delivery/query.d.ts +21 -0
  117. package/dist-runtime/delivery/query.js +58 -0
  118. package/dist-runtime/delivery/query.js.map +1 -0
  119. package/dist-runtime/delivery/release-graph.d.ts +17 -0
  120. package/dist-runtime/delivery/release-graph.js +23 -0
  121. package/dist-runtime/delivery/release-graph.js.map +1 -1
  122. package/dist-runtime/delivery/release-history-capture.js +26 -27
  123. package/dist-runtime/delivery/release-history-capture.js.map +1 -1
  124. package/dist-runtime/delivery/release-history.d.ts +2 -1
  125. package/dist-runtime/delivery/release-history.js +30 -52
  126. package/dist-runtime/delivery/release-history.js.map +1 -1
  127. package/dist-runtime/delivery/release.d.ts +22 -0
  128. package/dist-runtime/delivery/release.js +13 -3
  129. package/dist-runtime/delivery/release.js.map +1 -1
  130. package/dist-runtime/delivery/scripts.d.ts +116 -0
  131. package/dist-runtime/delivery/scripts.js +289 -0
  132. package/dist-runtime/delivery/scripts.js.map +1 -0
  133. package/dist-runtime/delivery/setup.d.ts +1 -1
  134. package/dist-runtime/delivery/setup.js +26 -36
  135. package/dist-runtime/delivery/setup.js.map +1 -1
  136. package/dist-runtime/delivery/source.d.ts +5 -2
  137. package/dist-runtime/delivery/source.js +4 -3
  138. package/dist-runtime/delivery/source.js.map +1 -1
  139. package/dist-runtime/delivery/standalone-source.d.ts +40 -0
  140. package/dist-runtime/delivery/standalone-source.js +54 -0
  141. package/dist-runtime/delivery/standalone-source.js.map +1 -0
  142. package/dist-runtime/runtime-identity.json +2 -2
  143. package/dist-runtime/workflow/lib/calls.js +21 -1
  144. package/dist-runtime/workflow/lib/calls.js.map +1 -1
  145. package/docs/configuration-runtime-observation.md +4 -2
  146. package/docs/delivery-custom-input-closure.md +2 -0
  147. package/docs/delivery-development.md +2 -0
  148. package/docs/delivery-inputs.md +2 -0
  149. package/docs/delivery-release.md +20 -2
  150. package/docs/delivery-setup.md +25 -20
  151. package/docs/delivery-source.md +4 -2
  152. package/docs/migration.md +108 -0
  153. package/docs/script-execution.md +85 -0
  154. package/package.json +42 -19
  155. package/runtime/delivery/aggregate-diagnostics.ts +2 -0
  156. package/runtime/delivery/aggregate-inputs.ts +6 -5
  157. package/runtime/delivery/aggregate-worker.ts +3 -3
  158. package/runtime/delivery/aggregate.ts +2 -2
  159. package/runtime/delivery/cli-host.ts +55 -0
  160. package/runtime/delivery/cli.ts +124 -0
  161. package/runtime/delivery/compiler-dependency-pins.ts +30 -1
  162. package/runtime/delivery/configuration-definition.ts +80 -9
  163. package/runtime/delivery/configuration-hre.ts +1 -1
  164. package/runtime/delivery/configuration.ts +7 -5
  165. package/runtime/delivery/custom-format.ts +11 -3
  166. package/runtime/delivery/custom.ts +13 -0
  167. package/runtime/delivery/development-inputs.ts +73 -0
  168. package/runtime/delivery/development.ts +4 -2
  169. package/runtime/delivery/execution-records.ts +31 -0
  170. package/runtime/delivery/existing-code.ts +12 -1
  171. package/runtime/delivery/history-package.ts +50 -0
  172. package/runtime/delivery/input-files.ts +5 -1
  173. package/runtime/delivery/inputs.ts +10 -6
  174. package/runtime/delivery/materialization.ts +8 -1
  175. package/runtime/delivery/planning.ts +10 -2
  176. package/runtime/delivery/production-finalization.ts +3 -2
  177. package/runtime/delivery/production.ts +5 -4
  178. package/runtime/delivery/project-files.ts +5 -1
  179. package/runtime/delivery/project-rpc.ts +10 -4
  180. package/runtime/delivery/protocol.ts +13 -5
  181. package/runtime/delivery/query.ts +34 -0
  182. package/runtime/delivery/release-graph.ts +17 -0
  183. package/runtime/delivery/release-history-capture.ts +21 -10
  184. package/runtime/delivery/release-history.ts +20 -19
  185. package/runtime/delivery/release.ts +7 -4
  186. package/runtime/delivery/scripts.ts +162 -0
  187. package/runtime/delivery/setup.ts +21 -31
  188. package/runtime/delivery/source.ts +7 -3
  189. package/runtime/delivery/standalone-source.ts +40 -0
  190. package/runtime/workflow/lib/calls.ts +19 -1
  191. package/dist-runtime/delivery/toolchain.d.ts +0 -3
  192. package/dist-runtime/delivery/toolchain.js +0 -52
  193. package/dist-runtime/delivery/toolchain.js.map +0 -1
  194. package/runtime/delivery/toolchain.ts +0 -48
@@ -30,6 +30,17 @@ function data(n:ts.Expression):any {
30
30
  if(ts.isStringLiteral(n))return n.text;
31
31
  if(ts.isNumericLiteral(n)){const v=Number(n.text);if(!Number.isSafeInteger(v))return fail();return v;}
32
32
  if(n.kind===ts.SyntaxKind.TrueKeyword)return true;if(n.kind===ts.SyntaxKind.FalseKeyword)return false;
33
+ if(ts.isPrefixUnaryExpression(n)&&[ts.SyntaxKind.PlusToken,ts.SyntaxKind.MinusToken].includes(n.operator)){
34
+ const operand=data(n.operand);if(typeof operand!=='number')return fail();return n.operator===ts.SyntaxKind.MinusToken?-operand:operand;
35
+ }
36
+ if(ts.isBinaryExpression(n)){
37
+ const left=data(n.left),right=data(n.right);if(typeof left!=='number'||typeof right!=='number')return fail();
38
+ const operations:Record<number,()=>number>={
39
+ [ts.SyntaxKind.PlusToken]:()=>left+right,[ts.SyntaxKind.MinusToken]:()=>left-right,
40
+ [ts.SyntaxKind.AsteriskToken]:()=>left*right,[ts.SyntaxKind.SlashToken]:()=>left/right,[ts.SyntaxKind.PercentToken]:()=>left%right,
41
+ };
42
+ const operation=operations[n.operatorToken.kind];if(!operation)return fail();const value=operation();if(!Number.isSafeInteger(value))return fail();return value;
43
+ }
33
44
  if(ts.isArrayLiteralExpression(n))return n.elements.map(e=>data(e as ts.Expression));
34
45
  return Object.fromEntries([...fields(n)].map(([k,v])=>[k,data(v)]));
35
46
  }
@@ -88,13 +99,18 @@ function accounts(n:ts.Expression,local:boolean,env:NodeJS.ProcessEnv,credential
88
99
  }
89
100
  const common=['chainId','from','gas','gasPrice','gasMultiplier'];
90
101
  const localFields=[...common,'initialBaseFeePerGas','hardfork','mining','accounts','blockGasLimit','minGasPrice','throwOnTransactionFailures','throwOnCallFailures','allowUnlimitedContractSize','allowBlocksWithSameTimestamp','initialDate','loggingEnabled','forking','coinbase','chains','enableTransientStorage','enableRip7212'];
91
- const httpFields=[...common,'url','timeout','httpHeaders','accounts'];
92
- function network(n:ts.Expression,local:boolean,env:NodeJS.ProcessEnv,helper:boolean,credentialResolver:boolean):{value:Data;definition:Data} {
102
+ const httpFields=[...common,'url','timeout','httpHeaders','accounts','zksync','ethNetwork'];
103
+ function network(n:ts.Expression,local:boolean,env:NodeJS.ProcessEnv,helper:boolean,credentialResolver:boolean,aliases:Map<string,ts.Expression>,usedAliases:Set<string>):{value:Data;definition:Data} {
93
104
  const value:Data={},definition:Data={};
94
105
  for(const [key,node] of fields(n)){
95
106
  if(!(local?localFields:httpFields).includes(key))return fail();
96
107
  if(key==='url'){const s=networkUrl(node,env,helper);value[key]=s.value;definition[key]=s.definition;}
97
- else if(key==='accounts'){const a=accounts(node,local,env,credentialResolver);value[key]=a.value;definition[key]=a.definition;}
108
+ else if(key==='accounts'){
109
+ let initializer=node;
110
+ if(ts.isIdentifier(node)){if(!aliases.has(node.text))return fail();usedAliases.add(node.text);initializer=aliases.get(node.text)!;}
111
+ const a=accounts(initializer,local,env,credentialResolver);value[key]=a.value;definition[key]=a.definition;
112
+ }
113
+ else if(key==='ethNetwork'){const s=secret(node,env);value[key]=s.value;definition[key]=s.definition;}
98
114
  else if(key==='httpHeaders'){const h=headers(node,env);value[key]=h.value;definition[key]=h.definition;}
99
115
  else if(key==='forking'){
100
116
  const v:Data={},d:Data={};for(const [k,x] of fields(node)){
@@ -113,7 +129,8 @@ function network(n:ts.Expression,local:boolean,env:NodeJS.ProcessEnv,helper:bool
113
129
  if('hardfork'in value&&!['chainstart','homestead','dao','tangerineWhistle','spuriousDragon','byzantium','constantinople','petersburg','istanbul','muirGlacier','berlin','london','arrowGlacier','grayGlacier','merge','shanghai','cancun','prague'].includes(value.hardfork))return fail();
114
130
  if(value.mining){keys(value.mining,['auto','interval','mempool']);if('auto'in value.mining&&typeof value.mining.auto!=='boolean')return fail();if('interval'in value.mining){const i=value.mining.interval;if(!(Number.isSafeInteger(i)&&i>=0)&&!(Array.isArray(i)&&i.length===2&&i.every(x=>Number.isSafeInteger(x)&&x>=0)&&i[0]<=i[1]))return fail();}if(value.mining.mempool){keys(value.mining.mempool,['order']);if(!['priority','fifo'].includes(value.mining.mempool.order))return fail();}}
115
131
  if(value.chains)for(const [id,c] of Object.entries(value.chains)){if(!/^[1-9]\d*$/.test(id)||!Number.isSafeInteger(Number(id)))return fail();keys(c as Data,['hardforkHistory']);for(const [fork,block] of Object.entries((c as Data).hardforkHistory??{})){if(typeof fork!=='string'||!Number.isSafeInteger(block)||(block as number)<0)return fail();}}
116
- if(!local&&(!('url'in value)||!Number.isSafeInteger(value.chainId)||value.chainId<=0))return fail();
132
+ if('zksync'in value&&typeof value.zksync!=='boolean')return fail();
133
+ if(!local&&!('url'in value))return fail();
117
134
  return {value,definition};
118
135
  }
119
136
  function compiler(config:any):void {
@@ -121,6 +138,47 @@ function compiler(config:any):void {
121
138
  if('version'in config){keys(config,['version','settings']);compiler(config.version);if(config.settings){keys(config.settings,['optimizer','evmVersion','viaIR','outputSelection','metadata','libraries','remappings','debug','stopAfter']);if(config.settings.optimizer){keys(config.settings.optimizer,['enabled','runs','details']);if(config.settings.optimizer.details)keys(config.settings.optimizer.details,['peephole','inliner','jumpdestRemover','orderLiterals','deduplicate','cse','constantOptimizer','yul','yulDetails']);}if(config.settings.metadata)keys(config.settings.metadata,['appendCBOR','useLiteralContent','bytecodeHash']);if(config.settings.debug)keys(config.settings.debug,['revertStrings','debugInfo']);}return;}
122
139
  keys(config,['compilers','overrides']);if(!Array.isArray(config.compilers)||!config.compilers.length)fail();config.compilers.forEach(compiler);if(config.overrides)Object.values(config.overrides).forEach(compiler);
123
140
  }
141
+ const pluginsSupported=new Set(['gate-evm-tools/delivery-hardhat','@nomicfoundation/hardhat-ethers',
142
+ '@nomicfoundation/hardhat-chai-matchers','@openzeppelin/hardhat-upgrades','@OpenZeppelin/hardhat-upgrades',
143
+ '@nomicfoundation/hardhat-foundry','@nomicfoundation/hardhat-toolbox','@nomicfoundation/hardhat-verify',
144
+ '@matterlabs/hardhat-zksync-solc','@matterlabs/hardhat-zksync-deploy',
145
+ 'hardhat-abi-exporter','hardhat-contract-sizer','hardhat-gas-reporter','@typechain/hardhat']);
146
+ const pluginFields:Record<string,string[]>={
147
+ mocha:['timeout','bail','allowUncaught','forbidOnly','forbidPending','fullTrace','retries','slow','parallel','grep','invert'],
148
+ contractSizer:['alphaSort','disambiguatePaths','runOnCompile','strict','only','except','unit','precision'],
149
+ abiExporter:['path','runOnCompile','clear','flat','pretty','except','only','format','spacing'],
150
+ typechain:['outDir','target','alwaysGenerateOverloads','dontOverrideCompile','externalArtifacts','discriminateTypes','tsNocheck'],
151
+ };
152
+ function pluginConfiguration(key:string,node:ts.Expression,env:NodeJS.ProcessEnv):{value:Data;definition:Data} {
153
+ const f=fields(node),value:Data={},definition:Data={};
154
+ if(key==='gasReporter'){
155
+ for(const [name,item] of f){
156
+ if(name==='enabled'){
157
+ const n=unwrap(item);
158
+ if(ts.isPrefixUnaryExpression(n)&&n.operator===ts.SyntaxKind.ExclamationToken&&ts.isPrefixUnaryExpression(n.operand)&&n.operand.operator===ts.SyntaxKind.ExclamationToken){
159
+ const flag=secret(n.operand.operand,env);if(flag.definition.kind!=='env')return fail();value[name]=!!flag.value;definition[name]={kind:'boolean-env',source:flag.definition};
160
+ }else{const flag=data(n);if(typeof flag!=='boolean')return fail();value[name]=definition[name]=flag;}
161
+ }else if(name==='currency'){value[name]=definition[name]=data(item);if(typeof value[name]!=='string')return fail();}
162
+ else return fail();
163
+ }return {value,definition};
164
+ }
165
+ if(key==='etherscan'){
166
+ for(const [name,item] of f){
167
+ if(name==='apiKey'){
168
+ if(ts.isObjectLiteralExpression(unwrap(item))){value[name]={};definition[name]={};for(const [network,keyNode] of fields(item)){const api=secret(keyNode,env);value[name][network]=api.value;definition[name][network]=api.definition;}}
169
+ else{const api=secret(item,env);value[name]=api.value;definition[name]=api.definition;}
170
+ }else if(name==='enabled'){const enabled=data(item);if(typeof enabled!=='boolean')return fail();value[name]=definition[name]=enabled;}
171
+ else return fail();
172
+ }return {value,definition};
173
+ }
174
+ const parsed=data(node);
175
+ if(key==='zksolc'){
176
+ keys(parsed,['version','compilerSource','settings']);compiler(parsed.version);
177
+ if(parsed.compilerSource!==undefined&&!['binary','docker'].includes(parsed.compilerSource))return fail();
178
+ if(parsed.settings){keys(parsed.settings,['optimizer','isSystem','forceEvmla','libraries','enableEraVMExtensions']);if(parsed.settings.optimizer)keys(parsed.settings.optimizer,['enabled','mode','fallback_to_optimizing_for_size']);}
179
+ }else keys(parsed,pluginFields[key]??[]);
180
+ return {value:parsed,definition:parsed};
181
+ }
124
182
  export interface ParsedConfiguration { userConfig:Data; definition:Data; plugins:string[]; explicitDate:boolean }
125
183
  /** Internal only: callers must not expose userConfig, which contains private evaluated capabilities. */
126
184
  export function parseConfigurationDefinition(bytes:Buffer,selected:string,env:NodeJS.ProcessEnv=process.env):ParsedConfiguration {
@@ -129,6 +187,7 @@ export function parseConfigurationDefinition(bytes:Buffer,selected:string,env:No
129
187
  const {file,fields:top,networks}=parseProjectConfig(new TextDecoder('utf-8',{fatal:true}).decode(bytes));
130
188
  const rpc=projectRpcSyntax(file);
131
189
  const plugins:string[]=[];
190
+ const aliases=new Map<string,ts.Expression>(),usedAliases=new Set<string>();
132
191
  let credentialResolver=false;
133
192
  const exported=unwrap(file.statements.filter(ts.isExportAssignment)[0].expression);
134
193
  for(const statement of file.statements){
@@ -143,18 +202,30 @@ export function parseConfigurationDefinition(bytes:Buffer,selected:string,env:No
143
202
  credentialResolver=true;
144
203
  }else if(name!=='hardhat/config'||!clause.isTypeOnly||clause.name||!clause.namedBindings||!ts.isNamedImports(clause.namedBindings)||clause.namedBindings.elements.some(e=>e.name.text!=='HardhatUserConfig'||e.propertyName))fail();
145
204
  }
146
- else {if(!['gate-evm-tools/delivery-hardhat','@nomicfoundation/hardhat-ethers','@nomicfoundation/hardhat-chai-matchers','@openzeppelin/hardhat-upgrades'].includes(name)||plugins.includes(name))fail();plugins.push(name);}
147
- }else if(ts.isVariableStatement(statement)){if(!ts.isIdentifier(exported)||statement.declarationList.declarations[0]?.name.getText(file)!==exported.text||statement.modifiers?.length||statement.declarationList.declarations.length!==1||!(statement.declarationList.flags&ts.NodeFlags.Const)||file.statements.filter(ts.isVariableStatement).length!==1)fail();}
205
+ else {if(!pluginsSupported.has(name)||plugins.includes(name))fail();plugins.push(name);}
206
+ }else if(ts.isVariableStatement(statement)){
207
+ if(statement.modifiers?.length||statement.declarationList.declarations.length!==1||!(statement.declarationList.flags&ts.NodeFlags.Const))fail();
208
+ const declaration=statement.declarationList.declarations[0];
209
+ if(!ts.isIdentifier(declaration.name)||!declaration.initializer)return fail();
210
+ if(ts.isIdentifier(exported)&&declaration.name.text===exported.text)continue;
211
+ if(aliases.has(declaration.name.text))fail();aliases.set(declaration.name.text,unwrap(declaration.initializer));
212
+ }
148
213
  else if(!ts.isExportAssignment(statement))fail();
149
214
  }
150
215
  if(plugins.includes('gate-evm-tools/delivery-hardhat')&&plugins[0]!=='gate-evm-tools/delivery-hardhat')fail();
151
216
  const userConfig:Data={},definition:Data={networks:{},plugins};
152
- for(const [key,node] of top){if(!['solidity','networks','paths','defaultNetwork'].includes(key))fail();if(key!=='networks')userConfig[key]=definition[key]=data(node);}
217
+ for(const [key,node] of top){
218
+ if(['solidity','networks','paths','defaultNetwork'].includes(key)){if(key!=='networks')userConfig[key]=definition[key]=data(node);}
219
+ else if(['zksolc','gasReporter','etherscan',...Object.keys(pluginFields)].includes(key)){const item=pluginConfiguration(key,node,env);userConfig[key]=item.value;definition[key]=item.definition;}
220
+ else fail();
221
+ }
153
222
  if(!('solidity'in userConfig))fail();compiler(userConfig.solidity);
154
223
  if(userConfig.paths){keys(userConfig.paths,['sources','cache','artifacts','tests']);for(const value of Object.values(userConfig.paths))if(typeof value!=='string'||!value||value.startsWith('/')||value.includes('\\')||value.split('/').some(p=>!p||p==='..'))fail();}
155
224
  if(userConfig.defaultNetwork!==undefined)validateNetworkAlias(userConfig.defaultNetwork,'defaultNetwork');
156
- userConfig.networks={};for(const [name,node] of networks){validateNetworkAlias(name,'network');const n=network(node,name==='hardhat',env,rpc.helper,credentialResolver);userConfig.networks[name]=n.value;definition.networks[name]=n.definition;}
157
- if(!userConfig.networks[selected])fail();
225
+ userConfig.networks={};for(const [name,node] of networks){validateNetworkAlias(name,'network');const n=network(node,name==='hardhat',env,rpc.helper,credentialResolver,aliases,usedAliases);userConfig.networks[name]=n.value;definition.networks[name]=n.definition;}
226
+ if([...aliases.keys()].some(name=>!usedAliases.has(name)))fail();
227
+ const target=userConfig.networks[selected];
228
+ if(!target||!Number.isSafeInteger(target.chainId)||target.chainId<=0||target.zksync===true)fail();
158
229
  return {userConfig,definition:deepFreeze(definition),plugins,explicitDate:Object.hasOwn(userConfig.networks.hardhat??{},'initialDate')};
159
230
  }catch{throw new Error('Unsupported static delivery configuration');}
160
231
  }
@@ -118,7 +118,7 @@ export function observeConfigurationHre(root:string,selected:string,parsed:Parse
118
118
  }
119
119
  const resolved=check(parsed),paths:Data={root:'.',configFile:'hardhat.config.ts'};
120
120
  for(const name of ['sources','cache','artifacts','tests']){const relative=path.relative(root,resolved.paths[name]).split(path.sep).join('/');paths[name]=relative;}
121
- const {url:_url,accounts:_accounts,httpHeaders:_headers,...http}=resolved.networks[selected];
121
+ const {url:_url,accounts:_accounts,httpHeaders:_headers,ethNetwork:_ethNetwork,...http}=resolved.networks[selected];
122
122
  const {accounts:_runnerAccounts,forking:_fork,initialDate:_date,...runner}=originalRunnerPolicy(resolved as any);
123
123
  const runnerPublic:Data=configurationData(runner);
124
124
  runnerPublic.accounts=parsed.definition.networks.hardhat?.accounts??{kind:'hardhat-default-hd'};
@@ -1,7 +1,7 @@
1
1
  /** Owned configuration/runtime contributor. It grants no planning, signing or execution authority. */
2
2
  import path from 'node:path';
3
3
  import {projectRoot,parseDeliveryProjectMarker} from './project-files';
4
- import {readInputBytes,inputDigest} from './input-files';
4
+ import {readInputBytes,readOptionalInputBytes,inputDigest} from './input-files';
5
5
  import {parseConfigurationDefinition} from './configuration-definition';
6
6
  import {observeConfigurationHre} from './configuration-hre';
7
7
  import {RuntimeObservation,type RuntimeEvidence} from './configuration-runtime';
@@ -34,14 +34,14 @@ export function openDeliveryConfiguration(raw:DeliveryConfigurationOptions):Deli
34
34
  exactObject(raw,['workspaceRoot','projectRoot','network'],'configuration options');
35
35
  const root=projectRoot(raw.workspaceRoot,raw.projectRoot);
36
36
  if(path.resolve(process.cwd())!==root)throw new Error();
37
- const config=readInputBytes(root,'hardhat.config.ts'),marker=readInputBytes(root,'.gate-template.json');
37
+ const config=readInputBytes(root,'hardhat.config.ts'),marker=readOptionalInputBytes(root,'.gate-template.json');
38
38
  // Marker describes generation provenance; current source is independently observed.
39
- parseDeliveryProjectMarker(marker);
39
+ if(marker!==undefined)parseDeliveryProjectMarker(marker);
40
40
  const parse=(bytes:Buffer)=>parseConfigurationDefinition(bytes,raw.network,projectRpcEnvironment(root,bytes.toString('utf8'),process.env));
41
41
  const parsed=parse(config);
42
42
  runtime=new RuntimeObservation(root);codec=openPlanCodecObservation();
43
43
  const hre=observeConfigurationHre(root,raw.network,parsed,runtime.files);
44
- const files=[{path:'hardhat.config.ts',digest:inputDigest(config)},{path:'.gate-template.json',digest:inputDigest(marker)}];
44
+ const files=[{path:'hardhat.config.ts',digest:inputDigest(config)},...(marker===undefined?[]:[{path:'.gate-template.json',digest:inputDigest(marker)}])];
45
45
  const observe=():DeepReadonly<DeliveryConfigurationEvidence>=>{
46
46
  // Reuse the actual owned compiler association when present, without recapturing any compiler jobs.
47
47
  const {ownedDeliveryCompiler}=require('./compiler') as typeof import('./compiler');
@@ -54,7 +54,9 @@ export function openDeliveryConfiguration(raw:DeliveryConfigurationOptions):Deli
54
54
  const close=()=>{runtime!.close();codec!.close();};
55
55
  const check=()=>{
56
56
  const current=readInputBytes(root,'hardhat.config.ts');
57
- if(inputDigest(current)!==files[0].digest||inputDigest(readInputBytes(root,'.gate-template.json'))!==files[1].digest)throw new Error();
57
+ const currentMarker=readOptionalInputBytes(root,'.gate-template.json');
58
+ if(inputDigest(current)!==files[0].digest||(marker===undefined)!==(currentMarker===undefined)
59
+ ||(currentMarker!==undefined&&inputDigest(currentMarker)!==files[1].digest))throw new Error();
58
60
  hre.assertUnchanged(parse(current));codec!.assertUnchanged();runtime!.assertUnchanged();
59
61
  };
60
62
  const assertOpen=()=>{if(status!=='open')throw new Error('Delivery configuration session closed');};
@@ -318,7 +318,7 @@ export function parseCustomOperations(raw: unknown): WorkflowStep[] {
318
318
  return deepFreeze(operations);
319
319
  }
320
320
  export interface CustomObservation {
321
- kind: "json" | "bytes" | "resolve";
321
+ kind: "json" | "bytes" | "resolve" | "contract";
322
322
  key: string;
323
323
  value: unknown;
324
324
  digest: string;
@@ -331,6 +331,7 @@ export interface CustomEvidence {
331
331
  module: { path: string; digest: string };
332
332
  input: unknown;
333
333
  declarations: {
334
+ chain?: boolean;
334
335
  files: string[];
335
336
  params: string[];
336
337
  state: string[];
@@ -344,10 +345,11 @@ export interface CustomEvidence {
344
345
  export function parseDeclarations(raw: unknown): CustomEvidence["declarations"] {
345
346
  const decl = exactObject(
346
347
  raw,
347
- ["files", "params", "state", "target"],
348
+ ["files", "params", "state", "target", ...(Object.hasOwn(raw as object,"chain")?["chain"]:[])],
348
349
  "custom declarations"
349
350
  );
350
351
  for (const [key, values] of Object.entries(decl)) {
352
+ if(key==='chain'){if(typeof values!=='boolean')throw Error('chain declaration must be boolean');continue;}
351
353
  if (
352
354
  !Array.isArray(values) ||
353
355
  values.some((v) => typeof v !== "string" || !v || v.trim() !== v) ||
@@ -428,11 +430,17 @@ export function parseCustomEvidence(raw: unknown): CustomEvidence {
428
430
  "custom observation"
429
431
  );
430
432
  if (
431
- !["json", "bytes", "resolve"].includes(String(o.kind)) ||
433
+ !["json", "bytes", "resolve", "contract"].includes(String(o.kind)) ||
432
434
  o.digest !== semanticHash(o.value)
433
435
  )
434
436
  throw new Error("Custom observation mismatch");
435
437
  const key = nonempty(o.key, "observation key");
438
+ if (o.kind === "contract") {
439
+ if(!declarations.chain||o.bytesDigest!==null||o.inputPath!==null)throw Error("Undeclared chain query");
440
+ const evidence=require('./query').parseContractQueryEvidence(o.value);
441
+ if(key!==evidence.method)throw Error('Query observation method mismatch');
442
+ continue;
443
+ }
436
444
  if (o.kind === "resolve") {
437
445
  if (o.inputPath !== null)
438
446
  throw new Error("Invalid resolved observation path");
@@ -56,6 +56,7 @@ export function ownedCustomSession(
56
56
  return owned;
57
57
  }
58
58
  interface CustomSessionInputs {
59
+ query?: (query: import('./scripts').ContractQuery) => Promise<{value:unknown;evidence:unknown}>;
59
60
  root: string;
60
61
  businessRoot: string;
61
62
  params: Record<string, unknown>;
@@ -133,6 +134,11 @@ export class CustomPlanningSession implements CustomPlanningInputSession {
133
134
  return this.#closure.readData(this.#input.businessRoot, name);
134
135
  };
135
136
  const ports = Object.freeze({
137
+ readContract: async (query: import('./scripts').ContractQuery) => {
138
+ assertOpen();if(!declarations.chain||!this.#input.query)throw Error('Declared controlled chain query port required');
139
+ const normalized=this.#closure.normalize(query) as import('./scripts').ContractQuery;
140
+ const result=await this.#input.query(normalized);assertOpen();observe('contract',normalized.method,result.evidence);return deepFreeze(strictData(result.value));
141
+ },
136
142
  readJson: async (name: string) => {
137
143
  const { bytes, path: inputPath } = read(name);
138
144
  return observe(
@@ -192,6 +198,10 @@ export class CustomPlanningSession implements CustomPlanningInputSession {
192
198
  throw new Error(
193
199
  "Custom planning must export planOperations; legacy exports are rejected"
194
200
  );
201
+ for(const prerequisite of (s.input as any)?.prerequisites??[]){
202
+ const resolve=(v:any):any=>typeof v==='string'&&v.startsWith('$')?ports.resolve(v.slice(1)):Array.isArray(v)?v.map(resolve):v&&typeof v==='object'?Object.fromEntries(Object.entries(v).map(([k,x])=>[k,resolve(x)])):v;
203
+ const {expect,...query}=resolve(prerequisite);const actual=await ports.readContract(query);require('./query').requireQueryResult({...query,expect},actual);
204
+ }
195
205
  operations = parseCustomOperations(
196
206
  this.#closure.normalize(
197
207
  await mod.planOperations(
@@ -200,6 +210,9 @@ export class CustomPlanningSession implements CustomPlanningInputSession {
200
210
  )
201
211
  )
202
212
  );
213
+ if(mod.delivery!==undefined)for(const operation of operations.filter(o=>['deploy','upgrade','call'].includes(o.kind))){
214
+ if(typeof operation.name!=='string'||!operation.name.trim()||typeof operation.description!=='string'||!operation.description.trim())throw Error(`Business operation ${operation.id} requires name and description before approval`);
215
+ }
203
216
  this.#closure.assertUnchanged();
204
217
  } finally {
205
218
  open = false;
@@ -0,0 +1,73 @@
1
+ /** One immutable copy of the original development preparation, reused by release history. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {randomUUID} from 'node:crypto';
5
+ import {canonicalJson} from '../workflow/lib/canonicalData';
6
+ import {developmentPath,byteHash} from './materialization';
7
+ import {readInputBytes} from './input-files';
8
+ import {projectRoot} from './project-files';
9
+ import {parseDeliveryPlan} from './plan';
10
+ import {safeId} from './protocol';
11
+ import type {DeliveryAggregate,DeliveryAggregateOptions} from './aggregate';
12
+
13
+ /** Publish complete bytes without replacing another writer's immutable file. */
14
+ export function writeImmutableDeliveryFile(root:string,relative:string,bytes:Buffer):{path:string;sha256:string} {
15
+ const file=developmentPath(root,relative);
16
+ const existing=()=>{if(!readInputBytes(root,relative).equals(bytes))throw Error(`Immutable delivery file conflict: ${relative}`);};
17
+ if(fs.existsSync(file)){existing();return {path:relative,sha256:byteHash(bytes)};}
18
+ fs.mkdirSync(path.dirname(file),{recursive:true,mode:0o700});
19
+ const temporary=`${file}.${randomUUID()}.tmp`,fd=fs.openSync(temporary,'wx',0o600);
20
+ try{fs.writeFileSync(fd,bytes);fs.fsyncSync(fd);}finally{fs.closeSync(fd);}
21
+ try{
22
+ try{fs.linkSync(temporary,file);}catch(error){if((error as NodeJS.ErrnoException).code!=='EEXIST')throw error;existing();}
23
+ }finally{fs.unlinkSync(temporary);}
24
+ const directory=fs.openSync(path.dirname(file),'r');try{fs.fsyncSync(directory);}finally{fs.closeSync(directory);}
25
+ return {path:relative,sha256:byteHash(bytes)};
26
+ }
27
+
28
+ export function persistDevelopmentInputs(input:{root:string;source:DeliveryAggregateOptions;aggregate:DeliveryAggregate}):{sourcePath:string;aggregatePath:string} {
29
+ const root=fs.realpathSync(input.root),{source,aggregate}=input,plan=parseDeliveryPlan(aggregate.collections[0]?.plan);
30
+ if(source.selection!=='development'||source.targets.length!==1||aggregate.collections.length!==1||plan.environment!=='dev'
31
+ ||projectRoot(source.workspaceRoot,source.projectRoot)!==root||source.targets[0].executionId!==plan.executionId||source.targets[0].request.network!==plan.target)
32
+ throw Error('Development inputs require the original single-target preparation');
33
+ const id=safeId(plan.executionId,'executionId'),base=`.gate-delivery/development/${id}`;
34
+ const sourcePath=`${base}.source.json`,aggregatePath=`${base}.aggregate.json`;
35
+ // Check both existing inputs before publishing either file.
36
+ const entries:[[string,unknown],[string,unknown]]=[[sourcePath,source],[aggregatePath,aggregate]];
37
+ for(const [file,value]of entries)if(fs.existsSync(developmentPath(root,file))&&canonicalJson(JSON.parse(readInputBytes(root,file).toString()))!==canonicalJson(value))
38
+ throw Error(`Immutable delivery file conflict: ${file}`);
39
+ // Existing preparation bytes remain authoritative, including their original JSON formatting.
40
+ for(const [file,value]of entries)if(!fs.existsSync(developmentPath(root,file)))writeImmutableDeliveryFile(root,file,Buffer.from(canonicalJson(value)+'\n'));
41
+ const snapshotPath=`${base}.inputs.snapshot.json`;
42
+ const expected=aggregate.inputs.files.filter(f=>!f.path.split('/').includes('node_modules'));
43
+ if(fs.existsSync(developmentPath(root,snapshotPath))){
44
+ const snapshot=JSON.parse(readInputBytes(root,snapshotPath).toString());
45
+ if(snapshot.schema!=='gate-delivery-development-inputs'||snapshot.version!==1||snapshot.inputsHash!==aggregate.inputsHash||!Array.isArray(snapshot.files)
46
+ ||canonicalJson(snapshot.files.map(({path,digest}:any)=>({path,digest})))!==canonicalJson(expected))throw Error('Original preparation snapshot identity mismatch');
47
+ for(const file of snapshot.files)if(typeof file.base64!=='string'||byteHash(Buffer.from(file.base64,'base64'))!==file.digest)throw Error('Original preparation snapshot bytes mismatch');
48
+ }else{
49
+ const files=expected.map(f=>{const bytes=readInputBytes(root,f.path);if(byteHash(bytes)!==f.digest)throw Error(`Preparation snapshot drift: ${f.path}`);return {...f,base64:bytes.toString('base64')};});
50
+ writeImmutableDeliveryFile(root,snapshotPath,Buffer.from(canonicalJson({schema:'gate-delivery-development-inputs',version:1,inputsHash:aggregate.inputsHash,files})+'\n'));
51
+ }
52
+ return {sourcePath,aggregatePath};
53
+ }
54
+
55
+ /** A host may replace an unsigned preparation only while holding the original durable execution claim. */
56
+ export async function invalidateDevelopmentPreparation(input:{root:string;executionId:string;store:import('./execution-store').ExecutionStore}){
57
+ const id=safeId(input.executionId,'executionId'),aggregate=JSON.parse(readInputBytes(input.root,`.gate-delivery/development/${id}.aggregate.json`).toString()),plan=parseDeliveryPlan(aggregate.collections[0]?.plan);
58
+ if(plan.executionId!==id||plan.environment!=='dev')throw Error('Original development preparation required');
59
+ const releases=path.join(input.root,`scripts/tasks/${plan.deploymentId}/releases`);
60
+ if(fs.existsSync(releases)&&fs.readdirSync(releases).some(name=>fs.existsSync(path.join(releases,name,'release.json'))))throw Error('Frozen release cannot be re-prepared; create a new round');
61
+ const marker=`.gate-delivery/development/${id}.superseded.json`;
62
+ const finality={kind:'confirmations',depth:String(Math.max(1,aggregate.inputs.business.targets[0].finality.confirmations))};
63
+ const {semanticHash}=require('../workflow/lib/canonicalData') as typeof import('../workflow/lib/canonicalData');
64
+ const context={executionId:id,planHash:plan.planHash,releaseHash:plan.inputsHash,operator:plan.operator,chainId:plan.chainId,target:plan.target,finality,policyHash:semanticHash(finality)};
65
+ const previous=await input.store.readExecution(id);
66
+ await input.store.withExecutionClaim({plan,contextHash:semanticHash({schema:'gate-delivery-eoa-context',version:1,context}),expectedRevision:previous?.revision??null},async(claim,state)=>{
67
+ if(state.transactions.some(t=>t.state!=='claimed'||t.unsigned!==null||t.signingRequestId!==null)||state.completedCount||state.safeEnvelopes.length)throw Error('Signing or execution already started; recover original execution or create a new round');
68
+ if(state.blockedReason!==null&&state.blockedReason!=='Superseded before signing')throw Error('Original execution is blocked for another reason');
69
+ if(state.blockedReason===null)input.store.blockExecution(claim,state.revision,'Superseded before signing');
70
+ writeImmutableDeliveryFile(input.root,marker,Buffer.from(canonicalJson({version:1,executionId:id,planHash:plan.planHash,reason:'reprepare-before-signing'})+'\n'));
71
+ });
72
+ return {deploymentId:plan.deploymentId,values:aggregate.inputs.business.parameters[0].resolved.values};
73
+ }
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import {createRequire} from 'node:module';
5
5
  import ts from 'typescript';
6
- import {existingCodeCoverage,requiredExistingAddresses} from './existing-code';
6
+ import {existingCodeCoverage,requiredExistingAddresses,interactionCoverageRegistry} from './existing-code';
7
7
  import {yieldCompilerIo} from './compiler-io';
8
8
  import {openDeliveryConfiguration} from './configuration';
9
9
  import {aggregateInputHash} from './execution-identity';
@@ -25,6 +25,7 @@ import {DeliveryOperationIdentities} from './identities';
25
25
  import {parsePreparationJournal,type PreparationEntry} from './journal';
26
26
  import type {DeliveryCollection} from './planning';
27
27
  import type {DeliveryAggregate,DeliveryAggregateOptions} from './aggregate';
28
+ export {persistDevelopmentInputs,invalidateDevelopmentPreparation} from './development-inputs';
28
29
  import {openAggregateSources} from './aggregate-inputs';
29
30
  import {loadDeliveryPublicTarget} from './config';
30
31
  import {inputDigest,readInputBytes} from './input-files';
@@ -115,6 +116,7 @@ export async function createDevelopmentDelivery(options:DevelopmentDeliveryOptio
115
116
  const identity=semanticHash({schema:'gate-delivery-development',version:1,executionId:plan.executionId,planHash:plan.planHash,target:plan.target,inputsHash:computed,sourceDefinitionHash:plan.sourceDefinitionHash,runtimeIdentity:plan.runtimeIdentity,context:options.execution.context});
116
117
  const recordPath=`scripts/tasks/${plan.deploymentId}/results/${plan.target}/${plan.executionId}/deployment.json`,sessionPath=`.gate-delivery/development/${plan.executionId}.json`;
117
118
  async function assertCurrent(){
119
+ if(fileBytes(root,`.gate-delivery/development/${plan.executionId}.superseded.json`)!==null)throw Error('Preparation superseded before signing; use the replacement execution');
118
120
  if(createRequire(path.join(root,'package.json'))('hardhat')!==hre)throw Error('Development requires the actual validated project HRE');
119
121
  const observation=openDeliveryConfiguration({workspaceRoot:source.workspaceRoot,projectRoot:source.projectRoot,network:plan.target});
120
122
  try {
@@ -156,7 +158,7 @@ export async function createDevelopmentDelivery(options:DevelopmentDeliveryOptio
156
158
  const session={version:1,manifests,identity,planHash:plan.planHash,recordPath,registry:validateContractInfo(JSON.parse(fileBytes(root,'contractInfo.json')??'{}')),metadata:validateDeploymentInfo(JSON.parse(fileBytes(root,'deploymentInfo.json')??'{"version":1,"networks":{}}'))};
157
159
  atomicDevelopmentWrite(root,sessionPath,json(session));return session;
158
160
  });
159
- function pureCoverage(){return existingCodeCoverage({target:plan.target,addresses:requiredExistingAddresses(collection!),registry:base.registry,metadata:base.metadata,compiler:rows.find(r=>r.target===plan.target)!.compiler,manifests:base.manifests??[]});}
161
+ function pureCoverage(){return existingCodeCoverage({target:plan.target,addresses:requiredExistingAddresses(collection!),registry:interactionCoverageRegistry(collection!,base.registry,plan.target),metadata:base.metadata,compiler:rows.find(r=>r.target===plan.target)!.compiler,manifests:base.manifests??[]});}
160
162
  async function assertExistingCode(){
161
163
  if(preparations.length)return;
162
164
  const coverage=pureCoverage(),checker=new BytecodeChecker(hre,{},'evm');
@@ -0,0 +1,31 @@
1
+ /** Source-neutral preparation locators. These files identify inputs; approval remains a host capability. */
2
+ import fs from 'node:fs';
3
+ import {randomUUID} from 'node:crypto';
4
+ import {safeId,address} from './protocol';
5
+ import {readInputBytes} from './input-files';
6
+ import {developmentPath,withDevelopmentFileLock} from './materialization';
7
+ import {writeImmutableDeliveryFile} from './development-inputs';
8
+ import {canonicalJson,deepFreeze} from '../workflow/lib/canonicalData';
9
+ import type {ProductionOptions} from './production';
10
+ export const executionRecordPath=(id:string)=>`.gate-delivery/executions/${safeId(id,'executionId')}.json`;
11
+ export function readExecutionRecord(root:string,id:string){return JSON.parse(readInputBytes(root,executionRecordPath(id)).toString());}
12
+ export function saveExecutionRecord(root:string,record:{kind:'development'|'production';deploymentId:string;executionId:string;[key:string]:unknown}){
13
+ safeId(record.deploymentId,'deploymentId');safeId(record.executionId,'executionId');
14
+ writeImmutableDeliveryFile(root,executionRecordPath(record.executionId),Buffer.from(canonicalJson(record)+'\n'));return record;
15
+ }
16
+ /** A release/target has one execution, including when its approving host changes. */
17
+ export async function claimProductionExecution(root:string,input:Omit<ProductionOptions,'executionId'>){
18
+ input={...input,operator:address(input.operator,'operator').toLowerCase()};
19
+ const index=`.gate-delivery/releases/${safeId(input.releaseRef.releaseId,'releaseId')}/${safeId(input.target,'target')}.execution.json`;
20
+ return withDevelopmentFileLock(root,async()=>{
21
+ if(fs.existsSync(developmentPath(root,index))){
22
+ const prior=JSON.parse(readInputBytes(root,index).toString());
23
+ const {executionId,...scope}=prior.options??{};
24
+ if(prior.kind!=='production'||canonicalJson(scope)!==canonicalJson(input)||prior.executionId!==executionId)throw Error('Original production execution scope mismatch; reconcile the original execution');
25
+ const descriptor=readExecutionRecord(root,executionId);if(canonicalJson(descriptor)!==canonicalJson(prior))throw Error('Original production preparation locator mismatch');
26
+ return deepFreeze(prior);
27
+ }
28
+ const executionId=randomUUID(),record={kind:'production' as const,deploymentId:input.deploymentId,executionId,options:{...input,executionId}};
29
+ saveExecutionRecord(root,record);writeImmutableDeliveryFile(root,index,Buffer.from(canonicalJson(record)+'\n'));return deepFreeze(record);
30
+ });
31
+ }
@@ -1,7 +1,7 @@
1
1
  /** Pure-call coverage from the accepted registration and owned compiler witness. No deployments are invented. */
2
2
  import {TRANSPARENT_PROXY_ARTIFACT,PROXY_ADMIN_ARTIFACT,transparentAdminAddress,artifactIdentity,parseTargetManifest,hashStorageLayout} from 'gate-evm-tools/workflow';
3
3
  import {canonicalJson,semanticHash} from '../workflow/lib/canonicalData';
4
- import {validateContractInfo} from '../workflow/lib/deployments';
4
+ import {validateContractInfo,type ContractInfo} from '../workflow/lib/deployments';
5
5
  import {validateDeploymentInfo} from '../workflow/lib/deploymentInfo';
6
6
  import {byteHash} from './materialization';
7
7
  import type {CompilerEvidence} from './compiler';
@@ -43,3 +43,14 @@ export async function collectExistingCodeArtifacts(input:{target:string;addresse
43
43
  for(const address of new Set(input.addresses.map(a=>a.toLowerCase()))){const entries=Object.entries(registry).filter(([,a])=>a.toLowerCase()===address);if(entries.length!==1)throw Error('Existing code target requires unique frozen registration');const [key]=entries[0],m=metadata[key.split(':')[0]];names.add(m?.actualContract??key.split(':').at(-1)!);if(m?.proxyKind==='uups'||m?.proxyKind==='transparent'){names.add(m.proxyKind==='transparent'?TRANSPARENT_PROXY_ARTIFACT:'@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy');if(m.proxyKind==='transparent')names.add(PROXY_ADMIN_ARTIFACT);}}
44
44
  for(const name of names)await input.readArtifact(name);
45
45
  }
46
+
47
+ /** Ephemeral interaction bindings for explicitly addressed contracts. Never written as deployment records. */
48
+ export function interactionCoverageRegistry(collection:Pick<DeliveryCollection,'plan'|'verification'>,registry:unknown,target:string):ContractInfo{
49
+ const copy=JSON.parse(canonicalJson(validateContractInfo(registry)));copy[target]??={};
50
+ const bindings=new Map<string,string>();
51
+ const bind=(address:unknown,contract:unknown)=>{if(typeof address!=='string'||!/^0x[\da-f]{40}$/i.test(address)||typeof contract!=='string')return;const a=address.toLowerCase(),old=bindings.get(a);if(old&&old!==contract)throw Error('Conflicting explicit interaction artifact');bindings.set(a,contract);};
52
+ for(const a of collection.verification)bind(a.address,a.abiName);
53
+ for(const c of collection.plan.customBindings)for(const op of c.operations)if(['call','view','check'].includes(op.kind))bind(op.address,op.abi??op.contract);
54
+ for(const [a,contract]of bindings)if(!Object.values(copy[target]).some(v=>(v as string).toLowerCase()===a))copy[target][`interaction_${a.slice(2,14)}:${contract}`]=a;
55
+ return copy;
56
+ }
@@ -0,0 +1,50 @@
1
+ /** Verify retained producer bytes in memory. Historical code is never extracted or executed. */
2
+ import {createHash} from 'node:crypto';
3
+ import {gunzipSync} from 'node:zlib';
4
+ import {byteHash} from './materialization';
5
+ import {object,portablePath} from './protocol';
6
+ import {canonicalJson} from '../workflow/lib/canonicalData';
7
+ import type {DeliveryAggregate} from './aggregate';
8
+
9
+ export function readHistoricalPackage(archive:Buffer,archiveFile:string,aggregate:DeliveryAggregate):ReadonlyMap<string,Buffer> {
10
+ if(archiveFile!==`.gate-delivery/packages/${byteHash(archive)}.json.gz`)throw Error('Historical archive content address mismatch');
11
+ const snapshot=object(JSON.parse(gunzipSync(archive,{maxOutputLength:128*1024*1024}).toString()),'historical package snapshot',['schema','version','runtimeIdentity','observedPackage','files']);
12
+ if(snapshot.schema!=='gate-delivery-package-snapshot'||snapshot.version!==2||!Array.isArray(snapshot.files))throw Error('Invalid historical package snapshot archive');
13
+ const original=aggregate.inputs.runtime.package;
14
+ const same=(a:unknown,b:unknown,label:string)=>{if(canonicalJson(a)!==canonicalJson(b))throw Error(`${label} mismatch`);};
15
+ same(snapshot.observedPackage,original,'Historical snapshot provenance');
16
+ same(snapshot.runtimeIdentity,aggregate.inputs.runtime.declared,'Historical snapshot runtime');
17
+ const files=new Map<string,Buffer>();
18
+ for(const raw of snapshot.files){
19
+ const file=object(raw,'historical package member',['path','digest','base64']),relative=portablePath(file.path,'historical package member');
20
+ if(relative.split('/').includes('node_modules')||files.has(relative)||typeof file.base64!=='string')throw Error('Invalid historical package member');
21
+ const bytes=Buffer.from(file.base64,'base64');
22
+ if(bytes.toString('base64')!==file.base64||byteHash(bytes)!==file.digest)throw Error('Historical snapshot archive bytes mismatch');
23
+ files.set(relative,bytes);
24
+ }
25
+ const payload=[...files].map(([path,bytes])=>({path,digest:byteHash(bytes)}));
26
+ same(payload,original.payload,'Historical archive complete payload');
27
+ same(byteHash(payload.map(f=>`${f.path}\0${f.digest}\n`).join('')),original.payloadDigest,'Historical payload digest');
28
+ // Match the producer's sorted directory traversal, including file/directory prefix ordering.
29
+ const children=new Map<string,Set<string>>();
30
+ for(const relative of files.keys()){
31
+ const parts=relative.split('/');let directory='';
32
+ for(let i=0;i<parts.length;i++){
33
+ if(files.has(directory))throw Error('Historical package file/directory collision');
34
+ if(!children.has(directory))children.set(directory,new Set());
35
+ children.get(directory)!.add(parts[i]);directory=directory?`${directory}/${parts[i]}`:parts[i];
36
+ }
37
+ }
38
+ const digest=createHash('sha256');
39
+ const visit=(directory:string)=>{
40
+ const names=children.get(directory);if(!names)throw Error('Historical executable directory missing');
41
+ for(const name of [...names].sort()){
42
+ const relative=`${directory}/${name}`,bytes=files.get(relative);
43
+ if(bytes){if(/\.(?:c?js)$/.test(relative)){digest.update(relative);digest.update(bytes);}}
44
+ else visit(relative);
45
+ }
46
+ };
47
+ for(const directory of ['dist-runtime','dist','dist-cjs','bin'])visit(directory);
48
+ same(digest.digest('hex'),aggregate.inputs.runtime.declared.digest,'Historical executable runtime digest');
49
+ return files;
50
+ }
@@ -3,9 +3,13 @@ import fs from 'node:fs';
3
3
  import { createHash } from 'node:crypto';
4
4
  import YAML from 'yaml';
5
5
  import { strictData } from './data';
6
- import { confinedFile } from './project-files';
6
+ import { confinedFile, stat } from './project-files';
7
7
 
8
8
  export const inputDigest = (bytes: string | Buffer): string => createHash('sha256').update(bytes).digest('hex');
9
+ /** Absence is an observed input too; unsafe existing paths still fail. */
10
+ export function readOptionalInputBytes(root: string, relative: string): Buffer | undefined {
11
+ return stat(confinedFile(root, relative)) ? readInputBytes(root, relative) : undefined;
12
+ }
9
13
  export function readInputBytes(root: string, relative: string): Buffer {
10
14
  const file = confinedFile(root, relative);
11
15
  const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
@@ -77,7 +77,7 @@ export function openDeliveryInputs(raw: DeliveryInputOptions): DeliveryInputHand
77
77
  if (!['development', 'release', 'all'].includes(input.selection)) throw new Error('Unsupported delivery input selection');
78
78
  const sourceInput = { workspaceRoot: input.workspaceRoot, projectRoot: input.projectRoot, request: input.request };
79
79
  const source = readOwnedDeliverySource(sourceInput);
80
- const scope = object(source.invocation.task.scope?.deployment, 'deployment');
80
+ const scope = source.scope;
81
81
  const deploymentId = safeId(scope.deploymentId, 'deploymentId'), businessRoot = `scripts/tasks/${deploymentId}`;
82
82
  const files: DeliveryInputSnapshot['files'] = [];
83
83
  const read = (name: string, format: 'json' | 'yaml') => {
@@ -85,9 +85,9 @@ export function openDeliveryInputs(raw: DeliveryInputOptions): DeliveryInputHand
85
85
  files.push({ path: relative, digest: inputDigest(bytes) });
86
86
  return parseInputDocument(bytes, format);
87
87
  };
88
- const task = object(read('task.yaml', 'yaml'), 'task.yaml', ['version', 'id', 'developmentNetwork', 'releaseTargetNetworks', 'requirementRef', 'productionRequirementRef', 'referenceDeploymentId']);
89
- if (task.version !== 1 || task.id !== deploymentId) throw new Error('Task version/deployment identity mismatch');
90
- same(parseDeploymentRequirementRef(task.requirementRef), scope.requirementRef, 'Task requirementRef');
88
+ const task = object(read('task.yaml', 'yaml'), 'task.yaml', ['version', 'id', 'developmentNetwork', 'releaseTargetNetworks', 'requirementRef', 'productionRequirementRef', 'referenceDeploymentId', 'source', 'script', 'selectedSteps', 'description']);
89
+ if (task.version !== (source.kind==='standalone'?2:1) || task.id !== deploymentId) throw new Error('Task version/deployment identity mismatch');
90
+ if(source.kind==='gateflow')same(parseDeploymentRequirementRef(task.requirementRef), scope.requirementRef, 'Task requirementRef');
91
91
  same(task.referenceDeploymentId ?? null, scope.referenceDeploymentId ?? null, 'Task referenceDeploymentId');
92
92
  same(task.developmentNetwork, scope.developmentNetwork, 'Task developmentNetwork');
93
93
  const release = task.releaseTargetNetworks as string[];
@@ -103,6 +103,10 @@ export function openDeliveryInputs(raw: DeliveryInputOptions): DeliveryInputHand
103
103
  if (manifest.id !== deploymentId) throw new Error('Manifest deployment identity mismatch');
104
104
  if (manifest.description !== undefined) nonempty(manifest.description, 'manifest description');
105
105
  const steps = parseSteps(manifest.steps);
106
+ if(source.kind==='standalone'){
107
+ const expected=require('./scripts').scriptManifest(source.projectRoot,task.script,task.selectedSteps);
108
+ same(steps,expected.steps,'Native script and selected business steps');
109
+ }
106
110
  const schema = object(read('parameters.schema.json', 'json'), 'parameters schema');
107
111
  const dev = scope.developmentNetwork as string;
108
112
  const all = [dev, ...release];
@@ -113,7 +117,7 @@ export function openDeliveryInputs(raw: DeliveryInputOptions): DeliveryInputHand
113
117
  const definition = exactObject(raw, ['version', 'module'], 'producer');
114
118
  nonempty(definition.version, 'producer version');
115
119
  const module = portablePath(definition.module, 'producer module');
116
- if (!module.startsWith(`${businessRoot}/`)) throw new Error('Producer module is outside deployment business root');
120
+ if (!module.startsWith(`${businessRoot}/`) && !module.startsWith('scripts/operations/')) throw new Error('Producer module is outside deployment business root');
117
121
  }
118
122
  const selected = input.selection === 'development' ? [dev] : input.selection === 'release' ? release : all;
119
123
  // Both the digest and every public projection use these exact buffers. Reopening
@@ -188,7 +192,7 @@ export function openDeliveryInputs(raw: DeliveryInputOptions): DeliveryInputHand
188
192
  for(const {closure} of resolved.values())closure?.closeReads();
189
193
  const current = readOwnedDeliverySource(sourceInput);
190
194
  same(current.sourceDefinitionHash, source.sourceDefinitionHash, 'Business source definition');
191
- same(current.invocation.task.scope?.deployment, scope, 'Business invocation');
195
+ same(current.scope, scope, 'Business invocation');
192
196
  for (const file of files) if (inputDigest(readInputBytes(source.projectRoot, file.path)) !== file.digest) throw new Error('Business input file drift');
193
197
  const parameters = [...resolved.values()].map(({ evidence, closure }) => {
194
198
  same(closure?.seal() ?? null, evidence.closure, 'Parameter producer closure');
@@ -29,9 +29,16 @@ export function readDevelopmentJournal(root:string,executionId:string):any|undef
29
29
  for(const f of j.files){exactObject(f,['path','before','after','beforeHash','afterHash'],'journal file');portablePath(f.path,'journal path');if((f.before!==null&&typeof f.before!=='string')||typeof f.after!=='string'||f.beforeHash!==(f.before===null?null:byteHash(f.before))||f.afterHash!==byteHash(f.after))throw Error('Invalid materialization journal bytes');}
30
30
  if(new Set(j.files.map((f:any)=>f.path)).size!==j.files.length||j.intentHash!==semanticHash({version:j.version,executionId:j.executionId,identity:j.identity,files:j.files}))throw Error('Invalid materialization journal identity');return j;
31
31
  }
32
+ const fileLockQueues=new Map<string,Promise<void>>();
32
33
  export async function withDevelopmentFileLock<T>(root:string,work:()=>Promise<T>):Promise<T> {
34
+ const key=fs.realpathSync(root),previous=fileLockQueues.get(key)??Promise.resolve();let release!:()=>void;
35
+ const queued=new Promise<void>(resolve=>{release=resolve;});fileLockQueues.set(key,queued);
36
+ await previous;
37
+ try{return await lockedDevelopmentFiles(key,work);}finally{release();if(fileLockQueues.get(key)===queued)fileLockQueues.delete(key);}
38
+ }
39
+ async function lockedDevelopmentFiles<T>(root:string,work:()=>Promise<T>):Promise<T> {
33
40
  const file=developmentPath(root,'.gate-delivery/materialization.sqlite');mkdir(path.dirname(file));const db=new DatabaseSync(file);
34
- try{db.exec('PRAGMA busy_timeout=5000; PRAGMA synchronous=FULL; BEGIN IMMEDIATE');return await work();}finally{try{db.exec('ROLLBACK');}finally{db.close();}}
41
+ let begun=false;try{db.exec('PRAGMA busy_timeout=5000; PRAGMA synchronous=FULL; BEGIN IMMEDIATE');begun=true;return await work();}finally{try{if(begun)db.exec('ROLLBACK');}finally{db.close();}}
35
42
  }
36
43
  /** Caller holds no file lock. onFileCommitted is a progress notification, never acceptance authority. */
37
44
  export async function commitDevelopmentFiles(input:DevelopmentFileCommit,onFileCommitted?:(path:string)=>void):Promise<any> {