fix: align deployment manifest schema

This commit is contained in:
golem
2026-08-21 02:55:41 -06:00
parent be8f01ea4e
commit c6bf8a683f
8 changed files with 389 additions and 139 deletions
+3 -3
View File
@@ -39,9 +39,9 @@ contract CheckState is DemoScript {
console2.log("paused", bank.paused()); console2.log("paused", bank.paused());
console2.log("version", bank.contractVersion()); console2.log("version", bank.contractVersion());
for (uint256 i; i < manifest.actors.length; ++i) { for (uint256 i; i < manifest.actors.length; ++i) {
console2.log(manifest.actorLabels[i], manifest.actors[i]); console2.log(manifest.actors[i].label, manifest.actors[i].address_);
console2.log(" bank balance", bank.balanceOf(manifest.actors[i])); console2.log(" bank balance", bank.balanceOf(manifest.actors[i].address_));
console2.log(" token balance", token.balanceOf(manifest.actors[i])); console2.log(" token balance", token.balanceOf(manifest.actors[i].address_));
} }
console2.log("reserves", reserves); console2.log("reserves", reserves);
console2.log("liabilities", liabilities); console2.log("liabilities", liabilities);
+12 -17
View File
@@ -37,35 +37,30 @@ contract DeployV1 is DemoScript {
Manifest memory manifest; Manifest memory manifest;
manifest.schemaVersion = 1; manifest.schemaVersion = 1;
manifest.network = block.chainid == ANVIL_CHAIN_ID ? "anvil" : "base-sepolia"; manifest.network = block.chainid == ANVIL_CHAIN_ID ? "anvil" : "baseSepolia";
manifest.chainId = block.chainid; manifest.chainId = block.chainid;
manifest.deploymentBlock = 0; manifest.deploymentBlock = 0;
manifest.rpcUrl = block.chainid == ANVIL_CHAIN_ID ? "http://127.0.0.1:8545" : "https://sepolia.base.org"; manifest.rpcUrl = block.chainid == ANVIL_CHAIN_ID ? "http://127.0.0.1:8545" : "";
manifest.explorerUrl = block.chainid == ANVIL_CHAIN_ID ? "" : "https://sepolia.basescan.org";
manifest.token = tokenAddress; manifest.token = tokenAddress;
manifest.proxy = proxy; manifest.proxy = proxy;
manifest.implementation = implementation; manifest.implementation = implementation;
manifest.owner = sender; manifest.owner = sender;
(manifest.actorLabels, manifest.actors) = _actors(sender); manifest.actors = _actors(sender);
_writeManifest(_manifestPath(PENDING_MANIFEST_PATH), manifest); _writeManifest(_manifestPath(PENDING_MANIFEST_PATH), manifest);
} }
function _actors(address sender) private view returns (string[] memory labels, address[] memory actors) { function _actors(address sender) private view returns (Actor[] memory actors) {
if (block.chainid == ANVIL_CHAIN_ID) { if (block.chainid == ANVIL_CHAIN_ID) {
labels = new string[](3); actors = new Actor[](3);
actors = new address[](3); actors[0] = Actor({label: "owner", address_: sender});
labels[0] = "owner"; (, actors[1].address_) = _deriveLocalActor(block.chainid, 1);
labels[1] = "Alice"; (, actors[2].address_) = _deriveLocalActor(block.chainid, 2);
labels[2] = "Bob"; actors[1].label = "Alice";
actors[0] = sender; actors[2].label = "Bob";
(, actors[1]) = _deriveLocalActor(block.chainid, 1);
(, actors[2]) = _deriveLocalActor(block.chainid, 2);
} else { } else {
labels = new string[](1); actors = new Actor[](1);
actors = new address[](1); actors[0] = Actor({label: "owner", address_: sender});
labels[0] = "owner";
actors[0] = sender;
} }
} }
} }
+2 -2
View File
@@ -15,8 +15,8 @@ contract SeedV1Demo is DemoScript {
(uint256 aliceKey, address alice) = _deriveLocalActor(block.chainid, 1); (uint256 aliceKey, address alice) = _deriveLocalActor(block.chainid, 1);
(uint256 bobKey, address bob) = _deriveLocalActor(block.chainid, 2); (uint256 bobKey, address bob) = _deriveLocalActor(block.chainid, 2);
_assertAddress("owner actor", manifest.owner, owner); _assertAddress("owner actor", manifest.owner, owner);
_assertAddress("Alice actor", manifest.actors[1], alice); _assertAddress("Alice actor", manifest.actors[1].address_, alice);
_assertAddress("Bob actor", manifest.actors[2], bob); _assertAddress("Bob actor", manifest.actors[2].address_, bob);
MockUSDC token = MockUSDC(manifest.token); MockUSDC token = MockUSDC(manifest.token);
BankV1 bank = BankV1(manifest.proxy); BankV1 bank = BankV1(manifest.proxy);
+98 -33
View File
@@ -23,19 +23,23 @@ abstract contract DemoScript is Script {
error UnexpectedState(string label, uint256 expected, uint256 actual); error UnexpectedState(string label, uint256 expected, uint256 actual);
error UnexpectedAddress(string label, address expected, address actual); error UnexpectedAddress(string label, address expected, address actual);
struct Actor {
string label;
address address_;
}
struct Manifest { struct Manifest {
uint256 schemaVersion; uint256 schemaVersion;
string network; string network;
uint256 chainId; uint256 chainId;
uint256 deploymentBlock; uint256 deploymentBlock;
string rpcUrl; string rpcUrl;
string explorerUrl; string explorerBaseUrl;
address token; address token;
address proxy; address proxy;
address implementation; address implementation;
address owner; address owner;
string[] actorLabels; Actor[] actors;
address[] actors;
} }
function _requireSupportedChain(uint256 chainId) internal pure { function _requireSupportedChain(uint256 chainId) internal pure {
@@ -58,22 +62,23 @@ abstract contract DemoScript is Script {
function _readManifest(string memory path, bool active) internal view returns (Manifest memory manifest) { function _readManifest(string memory path, bool active) internal view returns (Manifest memory manifest) {
string memory json = vm.readFile(path); string memory json = vm.readFile(path);
_assertExactManifestSchema(json);
manifest.schemaVersion = vm.parseJsonUint(json, ".schemaVersion"); manifest.schemaVersion = vm.parseJsonUint(json, ".schemaVersion");
manifest.network = vm.parseJsonString(json, ".network"); manifest.network = vm.parseJsonString(json, ".network");
manifest.chainId = vm.parseJsonUint(json, ".chainId"); manifest.chainId = vm.parseJsonUint(json, ".chainId");
manifest.deploymentBlock = vm.parseJsonUint(json, ".deploymentBlock"); manifest.deploymentBlock = vm.parseJsonUint(json, ".deploymentBlock");
manifest.rpcUrl = vm.parseJsonString(json, ".rpcUrl"); if (vm.keyExistsJson(json, ".rpcUrl")) manifest.rpcUrl = vm.parseJsonString(json, ".rpcUrl");
manifest.explorerUrl = vm.parseJsonString(json, ".explorerUrl"); if (vm.keyExistsJson(json, ".explorerBaseUrl")) {
manifest.explorerBaseUrl = vm.parseJsonString(json, ".explorerBaseUrl");
}
manifest.token = vm.parseJsonAddress(json, ".token"); manifest.token = vm.parseJsonAddress(json, ".token");
manifest.proxy = vm.parseJsonAddress(json, ".proxy"); manifest.proxy = vm.parseJsonAddress(json, ".proxy");
manifest.implementation = vm.parseJsonAddress(json, ".implementation"); manifest.implementation = vm.parseJsonAddress(json, ".implementation");
manifest.owner = vm.parseJsonAddress(json, ".owner"); manifest.owner = vm.parseJsonAddress(json, ".owner");
manifest.actorLabels = vm.parseJsonStringArray(json, ".actorLabels");
manifest.actors = vm.parseJsonAddressArray(json, ".actors");
if (manifest.schemaVersion != 1) revert InvalidManifestSchema(manifest.schemaVersion); if (manifest.schemaVersion != 1) revert InvalidManifestSchema(manifest.schemaVersion);
if (manifest.chainId != block.chainid) revert ManifestChainMismatch(block.chainid, manifest.chainId); if (manifest.chainId != block.chainid) revert ManifestChainMismatch(block.chainid, manifest.chainId);
if (active && manifest.deploymentBlock == 0) revert InvalidDeploymentBlock(); if (active && manifest.deploymentBlock == 0) revert InvalidDeploymentBlock();
manifest.actors = _parseActors(json, manifest.chainId);
_validateActorConfiguration(manifest); _validateActorConfiguration(manifest);
_requireCode("token", manifest.token); _requireCode("token", manifest.token);
_requireCode("proxy", manifest.proxy); _requireCode("proxy", manifest.proxy);
@@ -84,29 +89,63 @@ abstract contract DemoScript is Script {
if (target == address(0) || target.code.length == 0) revert MissingCode(label, target); if (target == address(0) || target.code.length == 0) revert MissingCode(label, target);
} }
function _assertExactManifestSchema(string memory json) private view {
string[] memory keys = vm.parseJsonKeys(json, ".");
if (keys.length < 9 || keys.length > 11) revert InvalidManifestSchema(0);
bool[9] memory required;
for (uint256 i; i < keys.length; ++i) {
string memory key = keys[i];
if (_equals(key, "schemaVersion")) required[0] = true;
else if (_equals(key, "network")) required[1] = true;
else if (_equals(key, "chainId")) required[2] = true;
else if (_equals(key, "deploymentBlock")) required[3] = true;
else if (_equals(key, "token")) required[4] = true;
else if (_equals(key, "proxy")) required[5] = true;
else if (_equals(key, "implementation")) required[6] = true;
else if (_equals(key, "owner")) required[7] = true;
else if (_equals(key, "actors")) required[8] = true;
else if (!_equals(key, "rpcUrl") && !_equals(key, "explorerBaseUrl")) revert InvalidManifestSchema(0);
}
for (uint256 i; i < required.length; ++i) {
if (!required[i]) revert InvalidManifestSchema(0);
}
}
function _parseActors(string memory json, uint256 chainId) private view returns (Actor[] memory actors) {
uint256 actorCount = chainId == ANVIL_CHAIN_ID ? 3 : 1;
actors = new Actor[](actorCount);
for (uint256 i; i < actorCount; ++i) {
string memory index = vm.toString(i);
actors[i].label = vm.parseJsonString(json, string.concat(".actors[", index, "].label"));
actors[i].address_ = vm.parseJsonAddress(json, string.concat(".actors[", index, "].address"));
}
if (vm.keyExistsJson(json, string.concat(".actors[", vm.toString(actorCount), "]"))) {
revert InvalidActorConfiguration();
}
}
function _validateActorConfiguration(Manifest memory manifest) internal pure { function _validateActorConfiguration(Manifest memory manifest) internal pure {
if (manifest.actorLabels.length != manifest.actors.length) revert InvalidActorConfiguration();
if (manifest.chainId == ANVIL_CHAIN_ID) { if (manifest.chainId == ANVIL_CHAIN_ID) {
if ( if (
manifest.actorLabels.length != 3 || !_equals(manifest.network, "anvil") manifest.actors.length != 3 || !_equals(manifest.network, "anvil")
|| !_equals(manifest.actorLabels[0], "owner") || !_equals(manifest.actorLabels[1], "Alice") || !_equals(manifest.actors[0].label, "owner") || !_equals(manifest.actors[1].label, "Alice")
|| !_equals(manifest.actorLabels[2], "Bob") || !_equals(manifest.actors[2].label, "Bob")
) revert InvalidActorConfiguration(); ) revert InvalidActorConfiguration();
(, address owner) = _deriveLocalActor(ANVIL_CHAIN_ID, 0); (, address owner) = _deriveLocalActor(ANVIL_CHAIN_ID, 0);
(, address alice) = _deriveLocalActor(ANVIL_CHAIN_ID, 1); (, address alice) = _deriveLocalActor(ANVIL_CHAIN_ID, 1);
(, address bob) = _deriveLocalActor(ANVIL_CHAIN_ID, 2); (, address bob) = _deriveLocalActor(ANVIL_CHAIN_ID, 2);
if ( if (
manifest.actors[0] != manifest.owner || manifest.actors[0] != owner || manifest.actors[1] != alice manifest.actors[0].address_ != manifest.owner || manifest.actors[0].address_ != owner
|| manifest.actors[2] != bob || manifest.actors[1].address_ != alice || manifest.actors[2].address_ != bob
) revert InvalidActorConfiguration(); ) revert InvalidActorConfiguration();
return; return;
} }
if ( if (
manifest.chainId != BASE_SEPOLIA_CHAIN_ID || manifest.actorLabels.length != 1 manifest.chainId != BASE_SEPOLIA_CHAIN_ID || manifest.actors.length != 1
|| !_equals(manifest.network, "base-sepolia") || !_equals(manifest.actorLabels[0], "owner") || !_equals(manifest.network, "baseSepolia") || !_equals(manifest.actors[0].label, "owner")
|| manifest.actors[0] != manifest.owner || manifest.actors[0].address_ != manifest.owner
) revert InvalidActorConfiguration(); ) revert InvalidActorConfiguration();
} }
@@ -122,20 +161,46 @@ abstract contract DemoScript is Script {
return keccak256(bytes(left)) == keccak256(bytes(right)); return keccak256(bytes(left)) == keccak256(bytes(right));
} }
function _serializeManifest(Manifest memory manifest) internal returns (string memory json) { function _serializeManifest(Manifest memory manifest) internal view returns (string memory json) {
string memory objectKey = "demo-manifest"; json = string.concat(
vm.serializeUint(objectKey, "schemaVersion", manifest.schemaVersion); '{"schemaVersion":',
vm.serializeString(objectKey, "network", manifest.network); vm.toString(manifest.schemaVersion),
vm.serializeUint(objectKey, "chainId", manifest.chainId); ',"network":"',
vm.serializeUint(objectKey, "deploymentBlock", manifest.deploymentBlock); manifest.network,
vm.serializeString(objectKey, "rpcUrl", manifest.rpcUrl); '","chainId":',
vm.serializeString(objectKey, "explorerUrl", manifest.explorerUrl); vm.toString(manifest.chainId),
vm.serializeAddress(objectKey, "token", manifest.token); ',"deploymentBlock":',
vm.serializeAddress(objectKey, "proxy", manifest.proxy); vm.toString(manifest.deploymentBlock)
vm.serializeAddress(objectKey, "implementation", manifest.implementation); );
vm.serializeAddress(objectKey, "owner", manifest.owner); if (bytes(manifest.rpcUrl).length != 0) json = string.concat(json, ',"rpcUrl":"', manifest.rpcUrl, '"');
vm.serializeString(objectKey, "actorLabels", manifest.actorLabels); if (bytes(manifest.explorerBaseUrl).length != 0) {
json = vm.serializeAddress(objectKey, "actors", manifest.actors); json = string.concat(json, ',"explorerBaseUrl":"', manifest.explorerBaseUrl, '"');
}
json = string.concat(
json,
',"token":"',
vm.toString(manifest.token),
'","proxy":"',
vm.toString(manifest.proxy),
'","implementation":"',
vm.toString(manifest.implementation),
'","owner":"',
vm.toString(manifest.owner),
'","actors":',
_serializeActors(manifest.actors),
"}"
);
}
function _serializeActors(Actor[] memory actors) private view returns (string memory json) {
json = "[";
for (uint256 i; i < actors.length; ++i) {
if (i != 0) json = string.concat(json, ",");
json = string.concat(
json, '{"label":"', actors[i].label, '","address":"', vm.toString(actors[i].address_), '"}'
);
}
json = string.concat(json, "]");
} }
function _writeManifest(string memory path, Manifest memory manifest) internal { function _writeManifest(string memory path, Manifest memory manifest) internal {
@@ -153,8 +218,8 @@ abstract contract DemoScript is Script {
function _assertV1State(Manifest memory manifest) internal view { function _assertV1State(Manifest memory manifest) internal view {
BankV1 bank = BankV1(manifest.proxy); BankV1 bank = BankV1(manifest.proxy);
_assertUint("Alice internal balance", 900e6, bank.balanceOf(manifest.actors[1])); _assertUint("Alice internal balance", 900e6, bank.balanceOf(manifest.actors[1].address_));
_assertUint("Bob internal balance", 500e6, bank.balanceOf(manifest.actors[2])); _assertUint("Bob internal balance", 500e6, bank.balanceOf(manifest.actors[2].address_));
_assertUint("liabilities", 1_400e6, bank.totalLiabilities()); _assertUint("liabilities", 1_400e6, bank.totalLiabilities());
_assertUint("reserves", 1_400e6, MockUSDC(manifest.token).balanceOf(manifest.proxy)); _assertUint("reserves", 1_400e6, MockUSDC(manifest.token).balanceOf(manifest.proxy));
_assertUint("version", 1, bank.contractVersion()); _assertUint("version", 1, bank.contractVersion());
+145 -41
View File
@@ -125,18 +125,38 @@ contract ScriptPreflightTest is Test {
harness.readManifest(path, true); harness.readManifest(path, true);
} }
function testManifestRejectsNonParallelActorArrays() public { function testAnvilManifestRejectsUnexpectedActorObjectCount() public {
string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION); string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION);
_etchManifestContracts(); _etchManifestContracts();
vm.writeJson('["owner","Alice"]', path, ".actorLabels"); vm.writeJson(
vm.expectRevert(DemoScript.InvalidActorConfiguration.selector); string.concat(
'[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"}]'
),
path,
".actors"
);
vm.expectRevert();
harness.readManifest(path, true); harness.readManifest(path, true);
} }
function testAnvilManifestRejectsUnexpectedActorLabels() public { function testAnvilManifestRejectsUnexpectedActorLabels() public {
string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION); string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION);
_etchManifestContracts(); _etchManifestContracts();
vm.writeJson('["owner","Mallory","Bob"]', path, ".actorLabels"); vm.writeJson(
string.concat(
'[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Mallory","address":"',
vm.toString(ALICE),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]'
),
path,
".actors"
);
vm.expectRevert(DemoScript.InvalidActorConfiguration.selector); vm.expectRevert(DemoScript.InvalidActorConfiguration.selector);
harness.readManifest(path, true); harness.readManifest(path, true);
} }
@@ -146,7 +166,11 @@ contract ScriptPreflightTest is Test {
_etchManifestContracts(); _etchManifestContracts();
vm.writeJson( vm.writeJson(
string.concat( string.concat(
'["', vm.toString(ALICE), '","', vm.toString(OWNER), '","0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"]' '[{"label":"owner","address":"',
vm.toString(ALICE),
'"},{"label":"Alice","address":"',
vm.toString(OWNER),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]'
), ),
path, path,
".actors" ".actors"
@@ -156,11 +180,20 @@ contract ScriptPreflightTest is Test {
} }
function testBaseManifestRequiresOnlyOwnerActor() public { function testBaseManifestRequiresOnlyOwnerActor() public {
string memory path = _writeBaseManifest(); string memory path = _writePublicBaseManifest();
vm.chainId(84532); vm.chainId(84532);
_etchManifestContracts(); _etchManifestContracts();
vm.writeJson('["owner","Alice"]', path, ".actorLabels"); vm.writeJson(
vm.writeJson(string.concat('["', vm.toString(OWNER), '","', vm.toString(ALICE), '"]'), path, ".actors"); string.concat(
'[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"}]'
),
path,
".actors"
);
vm.expectRevert(DemoScript.InvalidActorConfiguration.selector); vm.expectRevert(DemoScript.InvalidActorConfiguration.selector);
harness.readManifest(path, true); harness.readManifest(path, true);
} }
@@ -169,6 +202,41 @@ contract ScriptPreflightTest is Test {
assertEq(harness.educationalWarning(), unicode"Educational demo — mock token — never use real funds."); assertEq(harness.educationalWarning(), unicode"Educational demo — mock token — never use real funds.");
} }
function testNestedActorsRoundTripThroughManifestSerialization() public {
string memory path = _writePublicAnvilManifest();
_etchManifestContracts();
DemoScript.Manifest memory manifest = harness.readManifest(path, true);
assertEq(manifest.actors[0].label, "owner");
assertEq(manifest.actors[1].address_, ALICE);
string memory serialized = harness.serializeManifest(manifest);
assertFalse(_contains(serialized, "actorLabels"));
assertFalse(_contains(serialized, "explorerUrl"));
vm.writeFile(path, serialized);
DemoScript.Manifest memory roundTrip = harness.readManifest(path, true);
assertEq(roundTrip.actors[2].label, "Bob");
assertEq(roundTrip.actors[2].address_, 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC);
}
function testLegacyParallelActorManifestIsRejected() public {
string memory path = _writeLegacyParallelActorManifest();
_etchManifestContracts();
vm.expectRevert(abi.encodeWithSelector(DemoScript.InvalidManifestSchema.selector, uint256(0)));
harness.readManifest(path, true);
}
function testBaseSepoliaManifestUsesCamelCaseAndOmitsUnavailableUrls() public {
string memory path = _writePublicBaseManifest();
vm.chainId(84532);
_etchManifestContracts();
DemoScript.Manifest memory manifest = harness.readManifest(path, true);
assertEq(manifest.network, "baseSepolia");
assertEq(manifest.rpcUrl, "");
assertEq(manifest.explorerBaseUrl, "");
assertEq(manifest.actors[0].label, "owner");
assertEq(manifest.actors[0].address_, OWNER);
}
function testSerializedManifestContainsPublicAddressesAndNoSecrets() public { function testSerializedManifestContainsPublicAddressesAndNoSecrets() public {
DemoScript.Manifest memory manifest = DemoScript.Manifest({ DemoScript.Manifest memory manifest = DemoScript.Manifest({
schemaVersion: 1, schemaVersion: 1,
@@ -176,12 +244,11 @@ contract ScriptPreflightTest is Test {
chainId: 31337, chainId: 31337,
deploymentBlock: 0, deploymentBlock: 0,
rpcUrl: "http://127.0.0.1:8545", rpcUrl: "http://127.0.0.1:8545",
explorerUrl: "", explorerBaseUrl: "",
token: TOKEN, token: TOKEN,
proxy: PROXY, proxy: PROXY,
implementation: IMPLEMENTATION, implementation: IMPLEMENTATION,
owner: OWNER, owner: OWNER,
actorLabels: _labels(),
actors: _actors() actors: _actors()
}); });
@@ -212,12 +279,12 @@ contract ScriptPreflightTest is Test {
assertEq(BankV1(proxy).owner(), OWNER); assertEq(BankV1(proxy).owner(), OWNER);
assertEq(address(BankV1(proxy).asset()), token); assertEq(address(BankV1(proxy).asset()), token);
assertEq(BankV1(proxy).contractVersion(), 1); assertEq(BankV1(proxy).contractVersion(), 1);
assertEq(manifest.actorLabels[0], "owner"); assertEq(manifest.actors[0].label, "owner");
assertEq(manifest.actorLabels[1], "Alice"); assertEq(manifest.actors[1].label, "Alice");
assertEq(manifest.actorLabels[2], "Bob"); assertEq(manifest.actors[2].label, "Bob");
assertEq(manifest.actors[0], OWNER); assertEq(manifest.actors[0].address_, OWNER);
assertEq(manifest.actors[1], ALICE); assertEq(manifest.actors[1].address_, ALICE);
assertEq(manifest.actors[2], 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC); assertEq(manifest.actors[2].address_, 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC);
} }
function testSeedV1DemoExecutesExactActOneStateAndCheckStateAcceptsIt() public { function testSeedV1DemoExecutesExactActOneStateAndCheckStateAcceptsIt() public {
@@ -227,12 +294,12 @@ contract ScriptPreflightTest is Test {
new SeedV1Demo().run(); new SeedV1Demo().run();
BankV1 bank = BankV1(manifest.proxy); BankV1 bank = BankV1(manifest.proxy);
assertEq(bank.balanceOf(manifest.actors[1]), 900e6); assertEq(bank.balanceOf(manifest.actors[1].address_), 900e6);
assertEq(bank.balanceOf(manifest.actors[2]), 500e6); assertEq(bank.balanceOf(manifest.actors[2].address_), 500e6);
assertEq(bank.totalLiabilities(), 1_400e6); assertEq(bank.totalLiabilities(), 1_400e6);
assertEq(MockUSDC(manifest.token).balanceOf(manifest.proxy), 1_400e6); assertEq(MockUSDC(manifest.token).balanceOf(manifest.proxy), 1_400e6);
assertEq(MockUSDC(manifest.token).balanceOf(manifest.actors[1]), 1_100e6); assertEq(MockUSDC(manifest.token).balanceOf(manifest.actors[1].address_), 1_100e6);
assertEq(MockUSDC(manifest.token).balanceOf(manifest.actors[2]), 500e6); assertEq(MockUSDC(manifest.token).balanceOf(manifest.actors[2].address_), 500e6);
vm.setEnv("DEMO_EXPECTED_STAGE", "v1"); vm.setEnv("DEMO_EXPECTED_STAGE", "v1");
new CheckState().run(); new CheckState().run();
@@ -266,7 +333,7 @@ contract ScriptPreflightTest is Test {
vm.toString(chainId), vm.toString(chainId),
',"deploymentBlock":', ',"deploymentBlock":',
vm.toString(deploymentBlock), vm.toString(deploymentBlock),
',"rpcUrl":"http://127.0.0.1:8545","explorerUrl":"","token":"', ',"rpcUrl":"http://127.0.0.1:8545","token":"',
vm.toString(token), vm.toString(token),
'","proxy":"', '","proxy":"',
vm.toString(proxy), vm.toString(proxy),
@@ -274,11 +341,11 @@ contract ScriptPreflightTest is Test {
vm.toString(implementation), vm.toString(implementation),
'","owner":"', '","owner":"',
vm.toString(OWNER), vm.toString(OWNER),
'","actorLabels":["owner","Alice","Bob"],"actors":["', '","actors":[{"label":"owner","address":"',
vm.toString(OWNER), vm.toString(OWNER),
'","', '"},{"label":"Alice","address":"',
vm.toString(ALICE), vm.toString(ALICE),
'","0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"]}' '"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}'
); );
vm.writeFile(path, json); vm.writeFile(path, json);
} }
@@ -293,12 +360,12 @@ contract ScriptPreflightTest is Test {
manifest = harness.readManifest(path, true); manifest = harness.readManifest(path, true);
} }
function _writeBaseManifest() internal returns (string memory path) { function _writePublicAnvilManifest() internal returns (string memory path) {
path = string.concat(fixtureDir, "/base-manifest.json"); path = string.concat(fixtureDir, "/public-anvil-manifest.json");
vm.writeFile( vm.writeFile(
path, path,
string.concat( string.concat(
'{"schemaVersion":1,"network":"base-sepolia","chainId":84532,"deploymentBlock":1,"rpcUrl":"https://sepolia.base.org","explorerUrl":"https://sepolia.basescan.org","token":"', '{"schemaVersion":1,"network":"anvil","chainId":31337,"deploymentBlock":1,"rpcUrl":"http://127.0.0.1:8545","token":"',
vm.toString(TOKEN), vm.toString(TOKEN),
'","proxy":"', '","proxy":"',
vm.toString(PROXY), vm.toString(PROXY),
@@ -306,9 +373,53 @@ contract ScriptPreflightTest is Test {
vm.toString(IMPLEMENTATION), vm.toString(IMPLEMENTATION),
'","owner":"', '","owner":"',
vm.toString(OWNER), vm.toString(OWNER),
'","actorLabels":["owner"],"actors":["', '","actors":[{"label":"owner","address":"',
vm.toString(OWNER), vm.toString(OWNER),
'"]}' '"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}'
)
);
}
function _writeLegacyParallelActorManifest() internal returns (string memory path) {
path = string.concat(fixtureDir, "/legacy-parallel-actors.json");
vm.writeFile(
path,
string.concat(
'{"schemaVersion":1,"network":"anvil","chainId":31337,"deploymentBlock":1,"rpcUrl":"http://127.0.0.1:8545","token":"',
vm.toString(TOKEN),
'","proxy":"',
vm.toString(PROXY),
'","implementation":"',
vm.toString(IMPLEMENTATION),
'","owner":"',
vm.toString(OWNER),
'","actorLabels":["owner","Alice","Bob"],"actors":[{"label":"owner","address":"',
vm.toString(OWNER),
'"},{"label":"Alice","address":"',
vm.toString(ALICE),
'"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}'
)
);
}
function _writePublicBaseManifest() internal returns (string memory path) {
path = string.concat(fixtureDir, "/public-base-manifest.json");
vm.writeFile(
path,
string.concat(
'{"schemaVersion":1,"network":"baseSepolia","chainId":84532,"deploymentBlock":1,"token":"',
vm.toString(TOKEN),
'","proxy":"',
vm.toString(PROXY),
'","implementation":"',
vm.toString(IMPLEMENTATION),
'","owner":"',
vm.toString(OWNER),
'","actors":[{"label":"owner","address":"',
vm.toString(OWNER),
'"}]}'
) )
); );
} }
@@ -319,18 +430,11 @@ contract ScriptPreflightTest is Test {
vm.etch(IMPLEMENTATION, hex"00"); vm.etch(IMPLEMENTATION, hex"00");
} }
function _labels() internal pure returns (string[] memory labels) { function _actors() internal pure returns (DemoScript.Actor[] memory actors) {
labels = new string[](3); actors = new DemoScript.Actor[](3);
labels[0] = "owner"; actors[0] = DemoScript.Actor({label: "owner", address_: OWNER});
labels[1] = "Alice"; actors[1] = DemoScript.Actor({label: "Alice", address_: ALICE});
labels[2] = "Bob"; actors[2] = DemoScript.Actor({label: "Bob", address_: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC});
}
function _actors() internal pure returns (address[] memory actors) {
actors = new address[](3);
actors[0] = OWNER;
actors[1] = ALICE;
actors[2] = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;
} }
function _contains(string memory haystack, string memory needle) internal pure returns (bool) { function _contains(string memory haystack, string memory needle) internal pure returns (bool) {
+66 -25
View File
@@ -8,22 +8,20 @@ export const EDUCATIONAL_WARNING = "Educational demo — mock token — never us
const NETWORKS = { const NETWORKS = {
anvil: { anvil: {
name: "anvil", chainId: 31337, canonical: "anvil.json", recovery: "make reset-local", rpcUrl: "http://127.0.0.1:8545", explorerUrl: "", name: "anvil", chainId: 31337, canonical: "anvil.json", recovery: "make reset-local", rpcUrl: "http://127.0.0.1:8545",
}, },
"base-sepolia": { baseSepolia: {
name: "base-sepolia", chainId: 84532, canonical: "base-sepolia.json", recovery: "make archive-base-manifest", rpcUrl: "https://sepolia.base.org", explorerUrl: "https://sepolia.basescan.org", name: "baseSepolia", chainId: 84532, canonical: "base-sepolia.json", recovery: "make archive-base-manifest",
}, },
}; };
const MANIFEST_FIELDS = [ const REQUIRED_MANIFEST_FIELDS = ["schemaVersion", "network", "chainId", "deploymentBlock", "token", "proxy", "implementation", "owner", "actors"];
"schemaVersion", "network", "chainId", "deploymentBlock", "rpcUrl", "explorerUrl", "token", "proxy", "implementation", "owner", "actorLabels", "actors", const OPTIONAL_MANIFEST_FIELDS = ["rpcUrl", "explorerBaseUrl"];
];
const ANVIL_TEST_PHRASE = "test test test test test test test test test test test junk"; const ANVIL_TEST_PHRASE = "test test test test test test test test test test test junk";
const PROHIBITED_STRING_VALUE = /(?:private[_ -]?key|mnemonic|secret|password|credential|api[_ -]?key)|0x[a-fA-F0-9]{64}/i; const PROHIBITED_STRING_VALUE = /(?:private[_ -]?key|mnemonic|secret|password|credential|api[_ -]?key)|0x[a-fA-F0-9]{64}/i;
export function networkSpec(network) { export function networkSpec(network) {
const normalized = network === "baseSepolia" ? "base-sepolia" : network; const spec = NETWORKS[network];
const spec = NETWORKS[normalized];
if (!spec) throw new Error(`unsupported deployment network: ${network}`); if (!spec) throw new Error(`unsupported deployment network: ${network}`);
return spec; return spec;
} }
@@ -106,24 +104,26 @@ export function validateManifest(manifest, { pending = false } = {}) {
if (!Number.isSafeInteger(manifest.deploymentBlock) || manifest.deploymentBlock < (pending ? 0 : 1)) { if (!Number.isSafeInteger(manifest.deploymentBlock) || manifest.deploymentBlock < (pending ? 0 : 1)) {
throw new Error(`manifest deploymentBlock must be ${pending ? "a nonnegative integer" : "at least 1"}`); throw new Error(`manifest deploymentBlock must be ${pending ? "a nonnegative integer" : "at least 1"}`);
} }
if (manifest.rpcUrl !== spec.rpcUrl || manifest.explorerUrl !== spec.explorerUrl) { assertManifestUrls(manifest, spec);
throw new Error(`manifest display URLs must use the public ${manifest.network} endpoints`);
}
for (const field of ["token", "proxy", "implementation", "owner"]) assertAddress(manifest[field], field); for (const field of ["token", "proxy", "implementation", "owner"]) assertAddress(manifest[field], field);
if (!Array.isArray(manifest.actorLabels) || !Array.isArray(manifest.actors) || manifest.actorLabels.length !== manifest.actors.length || manifest.actors.length === 0) { if (!Array.isArray(manifest.actors) || manifest.actors.length === 0) throw new Error("manifest actors must be a nonempty array");
throw new Error("manifest actors and actorLabels must be nonempty parallel arrays");
}
const labels = new Set(); const labels = new Set();
const actors = new Set(); const actors = new Set();
for (let index = 0; index < manifest.actors.length; index += 1) { for (const [index, actorRecord] of manifest.actors.entries()) {
const label = manifest.actorLabels[index]; if (!actorRecord || typeof actorRecord !== "object" || Array.isArray(actorRecord)) throw new Error(`actors[${index}] must be an object`);
const keys = Object.keys(actorRecord);
if (keys.length !== 2 || !Object.hasOwn(actorRecord, "label") || !Object.hasOwn(actorRecord, "address")) {
throw new Error(`actors[${index}] must contain exactly label and address`);
}
const { label, address } = actorRecord;
if (typeof label !== "string" || label.trim() === "" || labels.has(label)) throw new Error("manifest actor labels must be unique nonempty strings"); if (typeof label !== "string" || label.trim() === "" || labels.has(label)) throw new Error("manifest actor labels must be unique nonempty strings");
labels.add(label); labels.add(label);
assertAddress(manifest.actors[index], `actors[${index}]`); assertAddress(address, `actors[${index}].address`);
const actor = manifest.actors[index].toLowerCase(); const actor = address.toLowerCase();
if (actors.has(actor)) throw new Error("manifest actors must be unique"); if (actors.has(actor)) throw new Error("manifest actors must be unique");
actors.add(actor); actors.add(actor);
} }
assertActorConfiguration(manifest);
} }
export async function atomicWriteJson(path, value) { export async function atomicWriteJson(path, value) {
@@ -149,14 +149,55 @@ function assertAddress(value, label) {
} }
function assertExactSchema(manifest) { function assertExactSchema(manifest) {
for (const field of MANIFEST_FIELDS) { for (const field of REQUIRED_MANIFEST_FIELDS) {
if (!Object.hasOwn(manifest, field)) throw new Error(`manifest is missing required field ${field}`); if (!Object.hasOwn(manifest, field)) throw new Error(`manifest is missing required field ${field}`);
} }
for (const field of Object.keys(manifest)) { for (const field of Object.keys(manifest)) {
if (!MANIFEST_FIELDS.includes(field)) throw new Error(`manifest contains unknown field ${field}`); if (![...REQUIRED_MANIFEST_FIELDS, ...OPTIONAL_MANIFEST_FIELDS].includes(field)) throw new Error(`manifest contains unknown field ${field}`);
} }
} }
function assertManifestUrls(manifest, spec) {
if (manifest.network === "anvil") {
if (manifest.rpcUrl !== spec.rpcUrl) throw new Error("manifest anvil rpcUrl must use the local public endpoint");
if (Object.hasOwn(manifest, "explorerBaseUrl")) throw new Error("manifest anvil must omit explorerBaseUrl");
return;
}
for (const field of OPTIONAL_MANIFEST_FIELDS) {
if (Object.hasOwn(manifest, field)) assertPublicUrl(manifest[field], field);
}
}
function assertPublicUrl(value, field) {
if (typeof value !== "string") throw new Error(`manifest ${field} must be a URL string`);
let url;
try {
url = new URL(value);
} catch {
throw new Error(`manifest ${field} must be a public URL`);
}
if ((url.protocol !== "https:" && url.protocol !== "http:") || url.username || url.password) {
throw new Error(`manifest ${field} must be a public URL without credentials`);
}
}
function assertActorConfiguration(manifest) {
const { actors, network, owner } = manifest;
if (network === "anvil") {
const expected = [
["owner", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"],
["Alice", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"],
["Bob", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"],
];
if (actors.length !== expected.length || actors.some((actor, index) => actor.label !== expected[index][0] || actor.address.toLowerCase() !== expected[index][1].toLowerCase())) {
throw new Error("manifest anvil actors must match the documented local actor configuration");
}
} else if (actors.length !== 1 || actors[0].label !== "owner") {
throw new Error("manifest baseSepolia must contain only the owner actor");
}
if (actors[0].address.toLowerCase() !== owner.toLowerCase()) throw new Error("manifest owner must be actor zero");
}
function rejectProhibitedStringValues(value, path = "") { function rejectProhibitedStringValues(value, path = "") {
if (typeof value === "string") { if (typeof value === "string") {
if (value.includes(ANVIL_TEST_PHRASE) || PROHIBITED_STRING_VALUE.test(value)) { if (value.includes(ANVIL_TEST_PHRASE) || PROHIBITED_STRING_VALUE.test(value)) {
@@ -171,6 +212,7 @@ function rejectProhibitedStringValues(value, path = "") {
if (!value || typeof value !== "object") return; if (!value || typeof value !== "object") return;
for (const [key, nested] of Object.entries(value)) { for (const [key, nested] of Object.entries(value)) {
const nestedPath = path ? `${path}.${key}` : key; const nestedPath = path ? `${path}.${key}` : key;
rejectProhibitedStringValues(key, nestedPath);
rejectProhibitedStringValues(nested, nestedPath); rejectProhibitedStringValues(nested, nestedPath);
} }
} }
@@ -181,14 +223,13 @@ function publicManifest(manifest, deploymentBlock) {
network: manifest.network, network: manifest.network,
chainId: manifest.chainId, chainId: manifest.chainId,
deploymentBlock, deploymentBlock,
rpcUrl: manifest.rpcUrl, ...(Object.hasOwn(manifest, "rpcUrl") ? { rpcUrl: manifest.rpcUrl } : {}),
explorerUrl: manifest.explorerUrl, ...(Object.hasOwn(manifest, "explorerBaseUrl") ? { explorerBaseUrl: manifest.explorerBaseUrl } : {}),
token: manifest.token, token: manifest.token,
proxy: manifest.proxy, proxy: manifest.proxy,
implementation: manifest.implementation, implementation: manifest.implementation,
owner: manifest.owner, owner: manifest.owner,
actorLabels: [...manifest.actorLabels], actors: manifest.actors.map(({ label, address }) => ({ label, address })),
actors: [...manifest.actors],
}; };
} }
@@ -240,7 +281,7 @@ export async function runFinalizeCli(argv, { root = process.cwd(), log = console
if (network !== "--rpc-url" || typeof rest[0] !== "string" || rest.length !== 1) throw new Error("usage: finalize-manifest.mjs deploy --rpc-url <url>"); if (network !== "--rpc-url" || typeof rest[0] !== "string" || rest.length !== 1) throw new Error("usage: finalize-manifest.mjs deploy --rpc-url <url>");
return finalizeDeployment({ root, rpc: fetchRpc(rest[0]) }); return finalizeDeployment({ root, rpc: fetchRpc(rest[0]) });
} }
throw new Error("usage: finalize-manifest.mjs preflight-deploy <anvil|base-sepolia> | deploy --rpc-url <url>"); throw new Error("usage: finalize-manifest.mjs preflight-deploy <anvil|baseSepolia> | deploy --rpc-url <url>");
} }
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
+1 -1
View File
@@ -16,7 +16,7 @@ export async function selectManifest({ root = process.cwd(), network }) {
export async function runSelectCli(argv, { root = process.cwd(), log = console.log } = {}) { export async function runSelectCli(argv, { root = process.cwd(), log = console.log } = {}) {
log(EDUCATIONAL_WARNING); log(EDUCATIONAL_WARNING);
if (argv.length !== 1) throw new Error("usage: select-manifest.mjs <anvil|base-sepolia>"); if (argv.length !== 1) throw new Error("usage: select-manifest.mjs <anvil|baseSepolia>");
return selectManifest({ root, network: argv[0] }); return selectManifest({ root, network: argv[0] });
} }
+62 -17
View File
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import test from "node:test"; import test from "node:test";
import { atomicWrite, finalizeDeployment, preflightDeploy, runFinalizeCli } from "./finalize-manifest.mjs"; import { atomicWrite, finalizeDeployment, preflightDeploy, runFinalizeCli, validateManifest } from "./finalize-manifest.mjs";
import { runSelectCli, selectManifest } from "./select-manifest.mjs"; import { runSelectCli, selectManifest } from "./select-manifest.mjs";
const TOKEN = "0x1000000000000000000000000000000000000001"; const TOKEN = "0x1000000000000000000000000000000000000001";
@@ -21,9 +21,9 @@ test("preflight rejects an existing target canonical manifest with the safe reco
() => preflightDeploy({ root, network: "anvil" }), () => preflightDeploy({ root, network: "anvil" }),
/make reset-local/ /make reset-local/
); );
await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "base-sepolia", chainId: 84532 })); await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "baseSepolia", chainId: 84532 }));
await assert.rejects( await assert.rejects(
() => preflightDeploy({ root, network: "base-sepolia" }), () => preflightDeploy({ root, network: "baseSepolia" }),
/make archive-base-manifest/ /make archive-base-manifest/
); );
}); });
@@ -42,17 +42,38 @@ test("direct manifest CLIs print the exact educational warning", async () => {
}); });
}); });
test("finalizer confirms a nested public actor manifest and preserves omitted Base URLs", async () => {
await withFixture(async (root) => {
const pending = manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0 });
await writeJson(join(root, "deployments", "pending.json"), pending);
await writeBroadcast(root, pending);
const output = await finalizeDeployment({ root, rpc: fakeRpc({ chainId: "0x14a34" }) });
assert.deepEqual(output.manifest, { ...pending, deploymentBlock: 42 });
assert.equal(Object.hasOwn(output.manifest, "rpcUrl"), false);
assert.equal(Object.hasOwn(output.manifest, "explorerBaseUrl"), false);
assert.deepEqual(output.manifest.actors, [{ label: "owner", address: OWNER }]);
});
});
test("validator rejects the legacy parallel actor and explorer schema", () => {
assert.throws(
() => validateManifest(legacyManifest()),
/unknown field|missing required field/
);
});
test("finalizer writes only a receipt-confirmed manifest and preserves active until selection", async () => { test("finalizer writes only a receipt-confirmed manifest and preserves active until selection", async () => {
await withFixture(async (root) => { await withFixture(async (root) => {
const pending = manifest({ deploymentBlock: 0 }); const pending = manifest({ deploymentBlock: 0 });
await writeJson(join(root, "deployments", "pending.json"), pending); await writeJson(join(root, "deployments", "pending.json"), pending);
await writeJson(join(root, "deployments", "active.json"), manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 88 })); await writeJson(join(root, "deployments", "active.json"), manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 88 }));
await writeBroadcast(root, pending, { hash: "0xaaa", contractAddress: PROXY }); await writeBroadcast(root, pending, { hash: "0xaaa", contractAddress: PROXY });
const output = await finalizeDeployment({ root, rpc: fakeRpc() }); const output = await finalizeDeployment({ root, rpc: fakeRpc() });
assert.equal(output.path, join(root, "deployments", "anvil.json")); assert.equal(output.path, join(root, "deployments", "anvil.json"));
assert.deepEqual(await readJson(output.path), { ...pending, deploymentBlock: 42 }); assert.deepEqual(await readJson(output.path), { ...pending, deploymentBlock: 42 });
assert.deepEqual(await readJson(join(root, "deployments", "active.json")), manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 88 })); assert.deepEqual(await readJson(join(root, "deployments", "active.json")), manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 88 }));
}); });
}); });
@@ -67,9 +88,9 @@ test("finalizer rejects invalid receipt, transaction, RPC, code, slot, and secre
["missing code", { rpc: fakeRpc({ missingCode: TOKEN }) }, /has no code/], ["missing code", { rpc: fakeRpc({ missingCode: TOKEN }) }, /has no code/],
["implementation slot mismatch", { rpc: fakeRpc({ slot: TOKEN }) }, /implementation slot/], ["implementation slot mismatch", { rpc: fakeRpc({ slot: TOKEN }) }, /implementation slot/],
["secret-bearing pending manifest", { pending: manifest({ rpcUrl: "https://user:password@example.invalid" }) }, /credential|secret|endpoints/i], ["secret-bearing pending manifest", { pending: manifest({ rpcUrl: "https://user:password@example.invalid" }) }, /credential|secret|endpoints/i],
["mnemonic in actor label", { pending: manifest({ deploymentBlock: 0, actorLabels: ["owner", "test test test test test test test test test test test junk", "Bob"] }) }, /prohibited/i], ["mnemonic in actor label", { pending: manifest({ deploymentBlock: 0, actors: localActors("test test test test test test test test test test test junk") }) }, /prohibited/i],
["private key in actor label", { pending: manifest({ deploymentBlock: 0, actorLabels: ["owner", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", "Bob"] }) }, /prohibited/i], ["private key in actor label", { pending: manifest({ deploymentBlock: 0, actors: localActors("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") }) }, /prohibited/i],
["credential in RPC path", { pending: manifest({ deploymentBlock: 0, rpcUrl: "https://sepolia.base.org/v1/secret-token" }) }, /public endpoint|prohibited/i], ["credential in RPC path", { pending: manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0, rpcUrl: "https://sepolia.base.org/v1/secret-token" }) }, /public URL|prohibited/i],
["unknown manifest field", { pending: manifest({ deploymentBlock: 0, harmlessLookingField: "not allowed" }) }, /unknown field/i], ["unknown manifest field", { pending: manifest({ deploymentBlock: 0, harmlessLookingField: "not allowed" }) }, /unknown field/i],
]; ];
@@ -92,7 +113,7 @@ test("finalizer rejects invalid receipt, transaction, RPC, code, slot, and secre
test("finalizer failure leaves an initially absent canonical manifest absent", async () => { test("finalizer failure leaves an initially absent canonical manifest absent", async () => {
await withFixture(async (root) => { await withFixture(async (root) => {
const pending = manifest({ deploymentBlock: 0, actorLabels: ["owner", "MNEMONIC", "Bob"] }); const pending = manifest({ deploymentBlock: 0, actors: localActors("MNEMONIC") });
await writeJson(join(root, "deployments", "pending.json"), pending); await writeJson(join(root, "deployments", "pending.json"), pending);
await writeJson(join(root, "deployments", "active.json"), manifest({ deploymentBlock: 8 })); await writeJson(join(root, "deployments", "active.json"), manifest({ deploymentBlock: 8 }));
await writeBroadcast(root, pending); await writeBroadcast(root, pending);
@@ -105,23 +126,23 @@ test("finalizer failure leaves an initially absent canonical manifest absent", a
test("selection atomically replaces active with only a valid named canonical manifest", async () => { test("selection atomically replaces active with only a valid named canonical manifest", async () => {
await withFixture(async (root) => { await withFixture(async (root) => {
const anvil = manifest({ deploymentBlock: 31 }); const anvil = manifest({ deploymentBlock: 31 });
const base = manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 32 }); const base = manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 32 });
await writeJson(join(root, "deployments", "anvil.json"), anvil); await writeJson(join(root, "deployments", "anvil.json"), anvil);
await writeJson(join(root, "deployments", "base-sepolia.json"), base); await writeJson(join(root, "deployments", "base-sepolia.json"), base);
await selectManifest({ root, network: "anvil" }); await selectManifest({ root, network: "anvil" });
const anvilBytes = await readFile(join(root, "deployments", "anvil.json")); const anvilBytes = await readFile(join(root, "deployments", "anvil.json"));
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), anvilBytes); assert.deepEqual(await readFile(join(root, "deployments", "active.json")), anvilBytes);
await selectManifest({ root, network: "base-sepolia" }); await selectManifest({ root, network: "baseSepolia" });
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), anvilBytes); assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), anvilBytes);
assert.deepEqual(await readJson(join(root, "deployments", "active.json")), base); assert.deepEqual(await readJson(join(root, "deployments", "active.json")), base);
const baseBytes = await readFile(join(root, "deployments", "base-sepolia.json")); const baseBytes = await readFile(join(root, "deployments", "base-sepolia.json"));
await selectManifest({ root, network: "anvil" }); await selectManifest({ root, network: "anvil" });
assert.deepEqual(await readFile(join(root, "deployments", "base-sepolia.json")), baseBytes); assert.deepEqual(await readFile(join(root, "deployments", "base-sepolia.json")), baseBytes);
await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 0 })); await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0 }));
const beforeActive = await readFile(join(root, "deployments", "active.json")); const beforeActive = await readFile(join(root, "deployments", "active.json"));
await assert.rejects(() => selectManifest({ root, network: "base-sepolia" }), /deploymentBlock/); await assert.rejects(() => selectManifest({ root, network: "baseSepolia" }), /deploymentBlock/);
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive); assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive);
}); });
}); });
@@ -157,14 +178,30 @@ test("atomic writes remove the ignored staging file when rename fails", async ()
}); });
function manifest(overrides = {}) { function manifest(overrides = {}) {
const baseSepolia = overrides.network === "base-sepolia"; const baseSepolia = overrides.network === "baseSepolia";
return { return {
schemaVersion: 1, schemaVersion: 1,
network: baseSepolia ? "base-sepolia" : "anvil", network: baseSepolia ? "baseSepolia" : "anvil",
chainId: baseSepolia ? 84532 : 31337, chainId: baseSepolia ? 84532 : 31337,
deploymentBlock: 1, deploymentBlock: 1,
rpcUrl: baseSepolia ? "https://sepolia.base.org" : "http://127.0.0.1:8545", ...(baseSepolia ? {} : { rpcUrl: "http://127.0.0.1:8545" }),
explorerUrl: baseSepolia ? "https://sepolia.basescan.org" : "", token: TOKEN,
proxy: PROXY,
implementation: IMPLEMENTATION,
owner: OWNER,
actors: baseSepolia ? [{ label: "owner", address: OWNER }] : localActors(),
...overrides,
};
}
function legacyManifest(overrides = {}) {
return {
schemaVersion: 1,
network: "anvil",
chainId: 31337,
deploymentBlock: 1,
rpcUrl: "http://127.0.0.1:8545",
explorerUrl: "",
token: TOKEN, token: TOKEN,
proxy: PROXY, proxy: PROXY,
implementation: IMPLEMENTATION, implementation: IMPLEMENTATION,
@@ -175,6 +212,14 @@ function manifest(overrides = {}) {
}; };
} }
function localActors(aliceLabel = "Alice") {
return [
{ label: "owner", address: OWNER },
{ label: aliceLabel, address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" },
{ label: "Bob", address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" },
];
}
async function withFixture(fn) { async function withFixture(fn) {
const root = await mkdtemp(join(tmpdir(), "uups-finalizer-")); const root = await mkdtemp(join(tmpdir(), "uups-finalizer-"));
try { try {