Skip to content

Commit c6278a6

Browse files
committed
Add multisend differential test against deployments
1 parent 2257730 commit c6278a6

4 files changed

Lines changed: 130 additions & 2 deletions

File tree

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "safe-utils",
33
"devDependencies": {
4-
"@safe-global/api-kit": "4.1.0"
4+
"@safe-global/api-kit": "4.1.0",
5+
"@safe-global/safe-deployments": "1.37.53"
56
}
67
}

test/SafeDifferential.t.sol

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,31 @@ contract SafeDifferentialTest is Test {
3030
}
3131
}
3232

33+
function test_Safe_multiSendConfig_matchesSafeDeployments() public {
34+
(uint256[] memory chainIds, uint256[] memory counts, address[] memory addresses) =
35+
_readSafeDeploymentsMultiSendConfig();
36+
37+
assertEq(chainIds.length, counts.length);
38+
39+
uint256 cursor;
40+
for (uint256 i = 0; i < chainIds.length; i++) {
41+
address resolved = address(safe.getMultiSendCallOnly(chainIds[i]));
42+
bool found;
43+
44+
for (uint256 j = 0; j < counts[i]; j++) {
45+
if (resolved == addresses[cursor + j]) {
46+
found = true;
47+
break;
48+
}
49+
}
50+
51+
assertTrue(found, string.concat("unexpected multisend address for chain ", vm.toString(chainIds[i])));
52+
cursor += counts[i];
53+
}
54+
55+
assertEq(cursor, addresses.length);
56+
}
57+
3358
function _readTypeScriptSdkConfig() private returns (uint256[] memory chainIds, string[] memory urls) {
3459
string[] memory inputs = new string[](2);
3560
inputs[0] = "node";
@@ -39,4 +64,18 @@ contract SafeDifferentialTest is Test {
3964
chainIds = json.readUintArray(".chainIds");
4065
urls = json.readStringArray(".urls");
4166
}
67+
68+
function _readSafeDeploymentsMultiSendConfig()
69+
private
70+
returns (uint256[] memory chainIds, uint256[] memory counts, address[] memory addresses)
71+
{
72+
string[] memory inputs = new string[](2);
73+
inputs[0] = "node";
74+
inputs[1] = "test/ffi/safe-multisend-config.cjs";
75+
76+
string memory json = string(vm.ffi(inputs));
77+
chainIds = json.readUintArray(".chainIds");
78+
counts = json.readUintArray(".counts");
79+
addresses = json.readAddressArray(".addresses");
80+
}
4281
}

test/ffi/safe-multisend-config.cjs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
const vm = require("vm");
4+
5+
const MIN_EXPECTED_NETWORKS = 39;
6+
const SUPPORTED_THIRD_PARTY_CHAIN_IDS = [98866];
7+
const MULTI_SEND_VERSIONS = ["1.5.0", "1.4.1", "1.3.0"];
8+
9+
const apiKitBundlePath = require.resolve("@safe-global/api-kit");
10+
const apiKitBundle = fs.readFileSync(apiKitBundlePath, "utf8");
11+
12+
const networksMatch = apiKitBundle.match(/var networks = (\[[\s\S]*?\n\]);\nvar getNetworkShortName =/);
13+
if (!networksMatch) {
14+
throw new Error(`Unable to locate networks config in ${apiKitBundlePath}`);
15+
}
16+
17+
const networks = vm.runInNewContext(networksMatch[1]);
18+
if (!Array.isArray(networks) || networks.length < MIN_EXPECTED_NETWORKS) {
19+
throw new Error(`Unexpected networks config shape in ${apiKitBundlePath}`);
20+
}
21+
22+
const chainIds = [];
23+
const seenChainIds = new Set();
24+
25+
for (const network of networks) {
26+
const chainId = Number(network.chainId);
27+
if (!Number.isInteger(chainId)) {
28+
throw new Error(`Invalid chainId in ${apiKitBundlePath}: ${network.chainId}`);
29+
}
30+
if (seenChainIds.has(chainId)) {
31+
throw new Error(`Duplicate chainId in ${apiKitBundlePath}: ${chainId}`);
32+
}
33+
34+
seenChainIds.add(chainId);
35+
chainIds.push(chainId);
36+
}
37+
38+
for (const chainId of SUPPORTED_THIRD_PARTY_CHAIN_IDS) {
39+
if (seenChainIds.has(chainId)) {
40+
throw new Error(`Duplicate supported chainId: ${chainId}`);
41+
}
42+
43+
seenChainIds.add(chainId);
44+
chainIds.push(chainId);
45+
}
46+
47+
const safeDeploymentsRoot = path.dirname(require.resolve("@safe-global/safe-deployments/package.json"));
48+
const multiSendDeployments = MULTI_SEND_VERSIONS.map((version) => {
49+
const deploymentPath = path.join(
50+
safeDeploymentsRoot,
51+
`src/assets/v${version}/multi_send_call_only.json`,
52+
);
53+
return JSON.parse(fs.readFileSync(deploymentPath, "utf8"));
54+
});
55+
56+
const counts = [];
57+
const addresses = [];
58+
59+
for (const chainId of chainIds) {
60+
const validAddresses = new Set();
61+
62+
for (const deployment of multiSendDeployments) {
63+
const addressTypes = deployment.networkAddresses[String(chainId)];
64+
if (!addressTypes) {
65+
continue;
66+
}
67+
68+
const normalizedAddressTypes = Array.isArray(addressTypes) ? addressTypes : [addressTypes];
69+
for (const addressType of normalizedAddressTypes) {
70+
const address = deployment.deployments[addressType]?.address;
71+
if (typeof address !== "string" || address.length === 0) {
72+
throw new Error(`Invalid ${addressType} deployment for chain ${chainId}`);
73+
}
74+
75+
validAddresses.add(address);
76+
}
77+
}
78+
79+
if (validAddresses.size === 0) {
80+
throw new Error(`No MultiSendCallOnly deployment found for chain ${chainId}`);
81+
}
82+
83+
counts.push(validAddresses.size);
84+
addresses.push(...validAddresses);
85+
}
86+
87+
process.stdout.write(JSON.stringify({ chainIds, counts, addresses }));

0 commit comments

Comments
 (0)