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.
- package/README.md +6 -2
- package/bin/cli.js +3 -0
- package/bin/delivery.cjs +11 -6
- package/bin/migration.js +38 -0
- package/dist/commands/prepare.d.ts.map +1 -1
- package/dist/commands/prepare.js +7 -1
- package/dist/commands/prepare.js.map +1 -1
- package/dist/core/BytecodeChecker.d.ts.map +1 -1
- package/dist/core/BytecodeChecker.js +27 -4
- package/dist/core/BytecodeChecker.js.map +1 -1
- package/dist/core/preparation/artifacts.d.ts.map +1 -1
- package/dist/core/preparation/artifacts.js +12 -5
- package/dist/core/preparation/artifacts.js.map +1 -1
- package/dist/core/preparation/index.d.ts +3 -0
- package/dist/core/preparation/index.d.ts.map +1 -1
- package/dist/core/preparation/index.js +9 -2
- package/dist/core/preparation/index.js.map +1 -1
- package/dist/core/preparation/zksync.d.ts.map +1 -1
- package/dist/core/preparation/zksync.js +5 -0
- package/dist/core/preparation/zksync.js.map +1 -1
- package/dist/core/validateAndGenerateCalldata.d.ts.map +1 -1
- package/dist/core/validateAndGenerateCalldata.js +3 -0
- package/dist/core/validateAndGenerateCalldata.js.map +1 -1
- package/dist/deployment-zksync/index.d.ts +40 -0
- package/dist/deployment-zksync/index.d.ts.map +1 -0
- package/dist/deployment-zksync/index.js +59 -0
- package/dist/deployment-zksync/index.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -12
- package/dist/index.js.map +1 -1
- package/dist/migration/index.d.ts +76 -0
- package/dist/migration/index.d.ts.map +1 -0
- package/dist/migration/index.js +284 -0
- package/dist/migration/index.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist-cjs/core/BytecodeChecker.js +27 -4
- package/dist-cjs/core/BytecodeChecker.js.map +1 -1
- package/dist-cjs/core/preparation/artifacts.js +11 -4
- package/dist-cjs/core/preparation/artifacts.js.map +1 -1
- package/dist-cjs/core/preparation/index.js +47 -6
- package/dist-cjs/core/preparation/index.js.map +1 -1
- package/dist-cjs/core/preparation/zksync.js +5 -0
- package/dist-cjs/core/preparation/zksync.js.map +1 -1
- package/dist-cjs/deployment-zksync/index.js +67 -0
- package/dist-cjs/deployment-zksync/index.js.map +1 -0
- package/dist-cjs/migration/index.js +328 -0
- package/dist-cjs/migration/index.js.map +1 -0
- package/dist-runtime/delivery/aggregate-diagnostics.js +3 -0
- package/dist-runtime/delivery/aggregate-diagnostics.js.map +1 -1
- package/dist-runtime/delivery/aggregate-inputs.d.ts +32 -6
- package/dist-runtime/delivery/aggregate-inputs.js +7 -3
- package/dist-runtime/delivery/aggregate-inputs.js.map +1 -1
- package/dist-runtime/delivery/aggregate-worker.js +3 -3
- package/dist-runtime/delivery/aggregate-worker.js.map +1 -1
- package/dist-runtime/delivery/aggregate.js +2 -2
- package/dist-runtime/delivery/aggregate.js.map +1 -1
- package/dist-runtime/delivery/cli-host.d.ts +17 -0
- package/dist-runtime/delivery/cli-host.js +118 -0
- package/dist-runtime/delivery/cli-host.js.map +1 -0
- package/dist-runtime/delivery/cli.d.ts +114 -0
- package/dist-runtime/delivery/cli.js +228 -0
- package/dist-runtime/delivery/cli.js.map +1 -0
- package/dist-runtime/delivery/compiler-dependency-pins.js +30 -1
- package/dist-runtime/delivery/compiler-dependency-pins.js.map +1 -1
- package/dist-runtime/delivery/configuration-definition.js +152 -11
- package/dist-runtime/delivery/configuration-definition.js.map +1 -1
- package/dist-runtime/delivery/configuration-hre.js +1 -1
- package/dist-runtime/delivery/configuration-hre.js.map +1 -1
- package/dist-runtime/delivery/configuration.js +7 -4
- package/dist-runtime/delivery/configuration.js.map +1 -1
- package/dist-runtime/delivery/custom-format.d.ts +2 -1
- package/dist-runtime/delivery/custom-format.js +15 -2
- package/dist-runtime/delivery/custom-format.js.map +1 -1
- package/dist-runtime/delivery/custom.d.ts +4 -0
- package/dist-runtime/delivery/custom.js +21 -0
- package/dist-runtime/delivery/custom.js.map +1 -1
- package/dist-runtime/delivery/development-inputs.d.ts +23 -0
- package/dist-runtime/delivery/development-inputs.js +117 -0
- package/dist-runtime/delivery/development-inputs.js.map +1 -0
- package/dist-runtime/delivery/development.d.ts +1 -0
- package/dist-runtime/delivery/development.js +7 -1
- package/dist-runtime/delivery/development.js.map +1 -1
- package/dist-runtime/delivery/execution-records.d.ts +16 -0
- package/dist-runtime/delivery/execution-records.js +48 -0
- package/dist-runtime/delivery/execution-records.js.map +1 -0
- package/dist-runtime/delivery/existing-code.d.ts +3 -0
- package/dist-runtime/delivery/existing-code.js +20 -0
- package/dist-runtime/delivery/existing-code.js.map +1 -1
- package/dist-runtime/delivery/history-package.d.ts +2 -0
- package/dist-runtime/delivery/history-package.js +70 -0
- package/dist-runtime/delivery/history-package.js.map +1 -0
- package/dist-runtime/delivery/input-files.d.ts +2 -0
- package/dist-runtime/delivery/input-files.js +5 -0
- package/dist-runtime/delivery/input-files.js.map +1 -1
- package/dist-runtime/delivery/inputs.js +11 -6
- package/dist-runtime/delivery/inputs.js.map +1 -1
- package/dist-runtime/delivery/materialization.js +20 -1
- package/dist-runtime/delivery/materialization.js.map +1 -1
- package/dist-runtime/delivery/planning.d.ts +1 -0
- package/dist-runtime/delivery/planning.js +10 -2
- package/dist-runtime/delivery/planning.js.map +1 -1
- package/dist-runtime/delivery/production-finalization.js +3 -2
- package/dist-runtime/delivery/production-finalization.js.map +1 -1
- package/dist-runtime/delivery/production.d.ts +2 -0
- package/dist-runtime/delivery/production.js +5 -4
- package/dist-runtime/delivery/production.js.map +1 -1
- package/dist-runtime/delivery/project-files.js +6 -1
- package/dist-runtime/delivery/project-files.js.map +1 -1
- package/dist-runtime/delivery/project-rpc.d.ts +1 -1
- package/dist-runtime/delivery/project-rpc.js +10 -4
- package/dist-runtime/delivery/project-rpc.js.map +1 -1
- package/dist-runtime/delivery/protocol.d.ts +6 -4
- package/dist-runtime/delivery/protocol.js +16 -5
- package/dist-runtime/delivery/protocol.js.map +1 -1
- package/dist-runtime/delivery/query.d.ts +21 -0
- package/dist-runtime/delivery/query.js +58 -0
- package/dist-runtime/delivery/query.js.map +1 -0
- package/dist-runtime/delivery/release-graph.d.ts +17 -0
- package/dist-runtime/delivery/release-graph.js +23 -0
- package/dist-runtime/delivery/release-graph.js.map +1 -1
- package/dist-runtime/delivery/release-history-capture.js +26 -27
- package/dist-runtime/delivery/release-history-capture.js.map +1 -1
- package/dist-runtime/delivery/release-history.d.ts +2 -1
- package/dist-runtime/delivery/release-history.js +30 -52
- package/dist-runtime/delivery/release-history.js.map +1 -1
- package/dist-runtime/delivery/release.d.ts +22 -0
- package/dist-runtime/delivery/release.js +13 -3
- package/dist-runtime/delivery/release.js.map +1 -1
- package/dist-runtime/delivery/scripts.d.ts +116 -0
- package/dist-runtime/delivery/scripts.js +289 -0
- package/dist-runtime/delivery/scripts.js.map +1 -0
- package/dist-runtime/delivery/setup.d.ts +1 -1
- package/dist-runtime/delivery/setup.js +26 -36
- package/dist-runtime/delivery/setup.js.map +1 -1
- package/dist-runtime/delivery/source.d.ts +5 -2
- package/dist-runtime/delivery/source.js +4 -3
- package/dist-runtime/delivery/source.js.map +1 -1
- package/dist-runtime/delivery/standalone-source.d.ts +40 -0
- package/dist-runtime/delivery/standalone-source.js +54 -0
- package/dist-runtime/delivery/standalone-source.js.map +1 -0
- package/dist-runtime/runtime-identity.json +2 -2
- package/dist-runtime/workflow/lib/calls.js +21 -1
- package/dist-runtime/workflow/lib/calls.js.map +1 -1
- package/docs/configuration-runtime-observation.md +4 -2
- package/docs/delivery-custom-input-closure.md +2 -0
- package/docs/delivery-development.md +2 -0
- package/docs/delivery-inputs.md +2 -0
- package/docs/delivery-release.md +20 -2
- package/docs/delivery-setup.md +25 -20
- package/docs/delivery-source.md +4 -2
- package/docs/migration.md +108 -0
- package/docs/script-execution.md +85 -0
- package/package.json +42 -19
- package/runtime/delivery/aggregate-diagnostics.ts +2 -0
- package/runtime/delivery/aggregate-inputs.ts +6 -5
- package/runtime/delivery/aggregate-worker.ts +3 -3
- package/runtime/delivery/aggregate.ts +2 -2
- package/runtime/delivery/cli-host.ts +55 -0
- package/runtime/delivery/cli.ts +124 -0
- package/runtime/delivery/compiler-dependency-pins.ts +30 -1
- package/runtime/delivery/configuration-definition.ts +80 -9
- package/runtime/delivery/configuration-hre.ts +1 -1
- package/runtime/delivery/configuration.ts +7 -5
- package/runtime/delivery/custom-format.ts +11 -3
- package/runtime/delivery/custom.ts +13 -0
- package/runtime/delivery/development-inputs.ts +73 -0
- package/runtime/delivery/development.ts +4 -2
- package/runtime/delivery/execution-records.ts +31 -0
- package/runtime/delivery/existing-code.ts +12 -1
- package/runtime/delivery/history-package.ts +50 -0
- package/runtime/delivery/input-files.ts +5 -1
- package/runtime/delivery/inputs.ts +10 -6
- package/runtime/delivery/materialization.ts +8 -1
- package/runtime/delivery/planning.ts +10 -2
- package/runtime/delivery/production-finalization.ts +3 -2
- package/runtime/delivery/production.ts +5 -4
- package/runtime/delivery/project-files.ts +5 -1
- package/runtime/delivery/project-rpc.ts +10 -4
- package/runtime/delivery/protocol.ts +13 -5
- package/runtime/delivery/query.ts +34 -0
- package/runtime/delivery/release-graph.ts +17 -0
- package/runtime/delivery/release-history-capture.ts +21 -10
- package/runtime/delivery/release-history.ts +20 -19
- package/runtime/delivery/release.ts +7 -4
- package/runtime/delivery/scripts.ts +162 -0
- package/runtime/delivery/setup.ts +21 -31
- package/runtime/delivery/source.ts +7 -3
- package/runtime/delivery/standalone-source.ts +40 -0
- package/runtime/workflow/lib/calls.ts +19 -1
- package/dist-runtime/delivery/toolchain.d.ts +0 -3
- package/dist-runtime/delivery/toolchain.js +0 -52
- package/dist-runtime/delivery/toolchain.js.map +0 -1
- 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'){
|
|
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(
|
|
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(!
|
|
147
|
-
}else if(ts.isVariableStatement(statement)){
|
|
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){
|
|
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(
|
|
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=
|
|
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)}
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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> {
|