Skip to content

Commit d437f14

Browse files
committed
Sync Safe tx service config with latest SDK
1 parent 5310d6c commit d437f14

3 files changed

Lines changed: 208 additions & 66 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
Interact with the [Safe API](https://docs.safe.global/sdk/api-kit) from Foundry scripts.
44

5+
Official transaction service URLs mirror the latest `safe-core-sdk` `packages/api-kit/src/utils/config.ts` network list.
6+
Third-party integrations are kept in a separate override layer so provider-specific endpoints stay isolated and easier to review.
7+
58
### Installation
69

710
```bash
@@ -89,7 +92,7 @@ ffi = true
8992

9093
### Third-party integrations
9194

92-
The following blockchains are integrated via third party APIs and not the official safe.global tx service:
95+
The following blockchains are integrated via third-party APIs and not the official `safe.global` tx service:
9396

9497
| Blockchain | Provider |
9598
| --- | --- |

src/Safe.sol

Lines changed: 113 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,18 @@ library Safe {
1212

1313
/// forge-lint: disable-next-line(screaming-snake-case-const)
1414
Vm constant vm = Vm(address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))));
15+
string constant SAFE_TRANSACTION_SERVICE_BASE_URL = "https://api.safe.global/tx-service";
16+
string constant PLUME_TRANSACTION_SERVICE_URL = "https://safe-transaction-plume.onchainden.com/api";
1517

1618
// https://github.com/safe-global/safe-smart-account/blob/release/v1.4.1/contracts/libraries/SafeStorage.sol
1719
bytes32 constant SAFE_THRESHOLD_STORAGE_SLOT = bytes32(uint256(4));
1820

19-
// https://github.com/safe-global/safe-deployments/blob/v1.37.32/src/assets/v1.3.0/multi_send_call_only.json
20-
address constant MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL = 0x40A2aCCbd92BCA938b02010E17A5b8929b49130D;
21-
address constant MULTI_SEND_CALL_ONLY_ADDRESS_EIP155 = 0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B;
22-
address constant MULTI_SEND_CALL_ONLY_ADDRESS_ZKSYNC = 0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F;
21+
// https://github.com/safe-global/safe-deployments/blob/c6a2025fca317b629d73d24b472c266418e2a4d6/src/assets/v1.3.0/multi_send_call_only.json
22+
address constant MULTI_SEND_CALL_ONLY_ADDRESS_V130_CANONICAL = 0x40A2aCCbd92BCA938b02010E17A5b8929b49130D;
23+
address constant MULTI_SEND_CALL_ONLY_ADDRESS_V130_ZKSYNC = 0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F;
24+
// https://github.com/safe-global/safe-deployments/blob/c6a2025fca317b629d73d24b472c266418e2a4d6/src/assets/v1.4.1/multi_send_call_only.json
25+
address constant MULTI_SEND_CALL_ONLY_ADDRESS_V141_CANONICAL = 0x9641d764fc13c8B624c04430C7356C1C7C8102e2;
26+
address constant MULTI_SEND_CALL_ONLY_ADDRESS_V141_ZKSYNC = 0x0408EF011960d02349d50286D20531229BCef773;
2327

2428
error ApiKitUrlNotFound(uint256 chainId);
2529
error MultiSendCallOnlyNotFound(uint256 chainId);
@@ -29,8 +33,6 @@ library Safe {
2933
struct Instance {
3034
address safe;
3135
HTTP.Client http;
32-
mapping(uint256 chainId => string) urls;
33-
mapping(uint256 chainId => MultiSendCallOnly) multiSendCallOnly;
3436
string requestBody;
3537
}
3638

@@ -58,54 +60,6 @@ library Safe {
5860
self.instances.push();
5961
Instance storage i = self.instances[self.instances.length - 1];
6062
i.safe = safe;
61-
// https://github.com/safe-global/safe-core-sdk/blob/4d89cb9b1559e4349c323a48a10caf685f7f8c88/packages/api-kit/src/utils/config.ts
62-
i.urls[1] = "https://api.safe.global/tx-service/eth/api";
63-
i.urls[10] = "https://api.safe.global/tx-service/oeth/api";
64-
i.urls[56] = "https://api.safe.global/tx-service/bnb/api";
65-
i.urls[100] = "https://api.safe.global/tx-service/gno/api";
66-
i.urls[130] = "https://api.safe.global/tx-service/unichain/api";
67-
i.urls[137] = "https://api.safe.global/tx-service/pol/api";
68-
i.urls[196] = "https://api.safe.global/tx-service/okb/api";
69-
i.urls[324] = "https://api.safe.global/tx-service/zksync/api";
70-
i.urls[480] = "https://api.safe.global/tx-service/wc/api";
71-
i.urls[999] = "https://api.safe.global/tx-service/hyper/api";
72-
i.urls[1101] = "https://api.safe.global/tx-service/zkevm/api";
73-
i.urls[5000] = "https://api.safe.global/tx-service/mantle/api";
74-
i.urls[8453] = "https://api.safe.global/tx-service/base/api";
75-
i.urls[42161] = "https://api.safe.global/tx-service/arb1/api";
76-
i.urls[42220] = "https://api.safe.global/tx-service/celo/api";
77-
i.urls[43114] = "https://api.safe.global/tx-service/avax/api";
78-
i.urls[59144] = "https://api.safe.global/tx-service/linea/api";
79-
i.urls[84532] = "https://api.safe.global/tx-service/basesep/api";
80-
i.urls[98866] = "https://safe-transaction-plume.onchainden.com/api";
81-
i.urls[534352] = "https://api.safe.global/tx-service/scr/api";
82-
i.urls[11155111] = "https://api.safe.global/tx-service/sep/api";
83-
i.urls[1313161554] = "https://api.safe.global/tx-service/aurora/api";
84-
85-
// https://github.com/safe-global/safe-deployments/blob/v1.37.32/src/assets/v1.3.0/multi_send_call_only.json
86-
i.multiSendCallOnly[1] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
87-
i.multiSendCallOnly[10] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
88-
i.multiSendCallOnly[56] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
89-
i.multiSendCallOnly[100] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
90-
i.multiSendCallOnly[130] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
91-
i.multiSendCallOnly[137] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
92-
i.multiSendCallOnly[196] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
93-
i.multiSendCallOnly[324] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_ZKSYNC);
94-
i.multiSendCallOnly[480] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
95-
i.multiSendCallOnly[999] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
96-
i.multiSendCallOnly[1101] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
97-
i.multiSendCallOnly[5000] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
98-
i.multiSendCallOnly[8453] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
99-
i.multiSendCallOnly[42161] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
100-
i.multiSendCallOnly[42220] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
101-
i.multiSendCallOnly[43114] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
102-
i.multiSendCallOnly[59144] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
103-
i.multiSendCallOnly[84532] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
104-
i.multiSendCallOnly[98866] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
105-
i.multiSendCallOnly[534352] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
106-
i.multiSendCallOnly[11155111] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
107-
i.multiSendCallOnly[1313161554] = MultiSendCallOnly(MULTI_SEND_CALL_ONLY_ADDRESS_CANONICAL);
108-
10963
i.http.initialize().withHeader("Content-Type", "application/json").withFollowRedirects(true);
11064
return self;
11165
}
@@ -114,20 +68,116 @@ library Safe {
11468
return self.instances[self.instances.length - 1];
11569
}
11670

117-
function getApiKitUrl(Client storage self, uint256 chainId) internal view returns (string memory) {
118-
string memory url = instance(self).urls[chainId];
119-
if (bytes(url).length == 0) {
120-
revert ApiKitUrlNotFound(chainId);
71+
function getApiKitUrl(Client storage, uint256 chainId) internal pure returns (string memory) {
72+
string memory thirdPartyApiKitUrl = getThirdPartyApiKitUrl(chainId);
73+
if (bytes(thirdPartyApiKitUrl).length > 0) {
74+
return thirdPartyApiKitUrl;
75+
}
76+
77+
return getTransactionServiceUrl(chainId);
78+
}
79+
80+
// Mirrors safe-global/safe-core-sdk/packages/api-kit/src/utils/config.ts on main.
81+
function getNetworkShortName(uint256 chainId) internal pure returns (string memory) {
82+
if (chainId == 1) return "eth";
83+
if (chainId == 10) return "oeth";
84+
if (chainId == 50) return "xdc";
85+
if (chainId == 56) return "bnb";
86+
if (chainId == 100) return "gno";
87+
if (chainId == 130) return "unichain";
88+
if (chainId == 137) return "pol";
89+
if (chainId == 143) return "monad";
90+
if (chainId == 146) return "sonic";
91+
if (chainId == 196) return "okb";
92+
if (chainId == 204) return "opbnb";
93+
if (chainId == 232) return "lens";
94+
if (chainId == 324) return "zksync";
95+
if (chainId == 480) return "wc";
96+
if (chainId == 988) return "stable";
97+
if (chainId == 999) return "hyper";
98+
if (chainId == 1101) return "zkevm";
99+
if (chainId == 3338) return "peaq";
100+
if (chainId == 3637) return "btc";
101+
if (chainId == 5000) return "mantle";
102+
if (chainId == 8453) return "base";
103+
if (chainId == 9745) return "plasma";
104+
if (chainId == 10143) return "monad-testnet";
105+
if (chainId == 10200) return "chi";
106+
if (chainId == 16661) return "0g";
107+
if (chainId == 42161) return "arb1";
108+
if (chainId == 42220) return "celo";
109+
if (chainId == 43111) return "hemi";
110+
if (chainId == 43114) return "avax";
111+
if (chainId == 57073) return "ink";
112+
if (chainId == 59144) return "linea";
113+
if (chainId == 80069) return "bep";
114+
if (chainId == 80094) return "berachain";
115+
if (chainId == 81224) return "codex";
116+
if (chainId == 84532) return "basesep";
117+
if (chainId == 534352) return "scr";
118+
if (chainId == 747474) return "katana";
119+
if (chainId == 11155111) return "sep";
120+
if (chainId == 1313161554) return "aurora";
121+
revert ApiKitUrlNotFound(chainId);
122+
}
123+
124+
function getTransactionServiceUrl(uint256 chainId) internal pure returns (string memory) {
125+
return string.concat(SAFE_TRANSACTION_SERVICE_BASE_URL, "/", getNetworkShortName(chainId), "/api");
126+
}
127+
128+
function getThirdPartyApiKitUrl(uint256 chainId) internal pure returns (string memory) {
129+
if (chainId == 98866) {
130+
return PLUME_TRANSACTION_SERVICE_URL;
121131
}
122-
return url;
132+
return "";
123133
}
124134

125-
function getMultiSendCallOnly(Client storage self, uint256 chainId) internal view returns (MultiSendCallOnly) {
126-
MultiSendCallOnly multiSendCallOnly = instance(self).multiSendCallOnly[chainId];
127-
if (address(multiSendCallOnly) == address(0)) {
135+
function getMultiSendCallOnly(Client storage, uint256 chainId) internal pure returns (MultiSendCallOnly) {
136+
address multiSendCallOnly = getThirdPartyMultiSendCallOnlyAddress(chainId);
137+
if (multiSendCallOnly == address(0)) {
138+
multiSendCallOnly = getOfficialMultiSendCallOnlyAddress(chainId);
139+
}
140+
if (multiSendCallOnly == address(0)) {
128141
revert MultiSendCallOnlyNotFound(chainId);
129142
}
130-
return multiSendCallOnly;
143+
return MultiSendCallOnly(multiSendCallOnly);
144+
}
145+
146+
function getOfficialMultiSendCallOnlyAddress(uint256 chainId) internal pure returns (address) {
147+
if (
148+
chainId == 1 || chainId == 10 || chainId == 56 || chainId == 100 || chainId == 130 || chainId == 137
149+
|| chainId == 196 || chainId == 480 || chainId == 999 || chainId == 1101 || chainId == 5000
150+
|| chainId == 8453 || chainId == 42161 || chainId == 42220 || chainId == 43114 || chainId == 59144
151+
|| chainId == 84532 || chainId == 534352 || chainId == 11155111 || chainId == 1313161554
152+
) {
153+
return MULTI_SEND_CALL_ONLY_ADDRESS_V130_CANONICAL;
154+
}
155+
156+
if (chainId == 324) {
157+
return MULTI_SEND_CALL_ONLY_ADDRESS_V130_ZKSYNC;
158+
}
159+
160+
if (
161+
chainId == 50 || chainId == 143 || chainId == 146 || chainId == 204 || chainId == 988 || chainId == 3338
162+
|| chainId == 3637 || chainId == 9745 || chainId == 10143 || chainId == 10200 || chainId == 16661
163+
|| chainId == 43111 || chainId == 57073 || chainId == 80069 || chainId == 80094 || chainId == 81224
164+
|| chainId == 747474
165+
) {
166+
return MULTI_SEND_CALL_ONLY_ADDRESS_V141_CANONICAL;
167+
}
168+
169+
if (chainId == 232) {
170+
return MULTI_SEND_CALL_ONLY_ADDRESS_V141_ZKSYNC;
171+
}
172+
173+
return address(0);
174+
}
175+
176+
function getThirdPartyMultiSendCallOnlyAddress(uint256 chainId) internal pure returns (address) {
177+
if (chainId == 98866) {
178+
return MULTI_SEND_CALL_ONLY_ADDRESS_V130_CANONICAL;
179+
}
180+
return address(0);
131181
}
132182

133183
function getNonce(Client storage self) internal view returns (uint256) {

test/Safe.t.sol

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ pragma solidity ^0.8.13;
33

44
import {Test, console} from "forge-std/Test.sol";
55
import {Safe} from "../src/Safe.sol";
6-
import {strings} from "solidity-stringutils/strings.sol";
76
import {IWETH} from "./interfaces/IWETH.sol";
87
import {Enum} from "safe-smart-account/common/Enum.sol";
98

109
contract SafeTest is Test {
1110
using Safe for *;
12-
using strings for *;
1311

1412
Safe.Client safe;
1513
address safeAddress = 0xF3a292Dda3F524EA20b5faF2EE0A1c4abA665e4F;
@@ -65,3 +63,94 @@ contract SafeTest is Test {
6563
safe.proposeTransactionsWithSignature(targets, datas, foundrySigner1, signature);
6664
}
6765
}
66+
67+
contract SafeConfigTest is Test {
68+
using Safe for *;
69+
70+
string constant SAFE_TRANSACTION_SERVICE_BASE_URL = "https://api.safe.global/tx-service";
71+
72+
Safe.Client safe;
73+
74+
function setUp() public {
75+
safe.initialize(address(0xBEEF));
76+
}
77+
78+
function test_Safe_getTransactionServiceUrl_matchesLatestOfficialSdkConfig() public pure {
79+
_assertOfficialTransactionServiceUrl(1, "eth");
80+
_assertOfficialTransactionServiceUrl(10, "oeth");
81+
_assertOfficialTransactionServiceUrl(50, "xdc");
82+
_assertOfficialTransactionServiceUrl(56, "bnb");
83+
_assertOfficialTransactionServiceUrl(100, "gno");
84+
_assertOfficialTransactionServiceUrl(130, "unichain");
85+
_assertOfficialTransactionServiceUrl(137, "pol");
86+
_assertOfficialTransactionServiceUrl(143, "monad");
87+
_assertOfficialTransactionServiceUrl(146, "sonic");
88+
_assertOfficialTransactionServiceUrl(196, "okb");
89+
_assertOfficialTransactionServiceUrl(204, "opbnb");
90+
_assertOfficialTransactionServiceUrl(232, "lens");
91+
_assertOfficialTransactionServiceUrl(324, "zksync");
92+
_assertOfficialTransactionServiceUrl(480, "wc");
93+
_assertOfficialTransactionServiceUrl(988, "stable");
94+
_assertOfficialTransactionServiceUrl(999, "hyper");
95+
_assertOfficialTransactionServiceUrl(1101, "zkevm");
96+
_assertOfficialTransactionServiceUrl(3338, "peaq");
97+
_assertOfficialTransactionServiceUrl(3637, "btc");
98+
_assertOfficialTransactionServiceUrl(5000, "mantle");
99+
_assertOfficialTransactionServiceUrl(8453, "base");
100+
_assertOfficialTransactionServiceUrl(9745, "plasma");
101+
_assertOfficialTransactionServiceUrl(10143, "monad-testnet");
102+
_assertOfficialTransactionServiceUrl(10200, "chi");
103+
_assertOfficialTransactionServiceUrl(16661, "0g");
104+
_assertOfficialTransactionServiceUrl(42161, "arb1");
105+
_assertOfficialTransactionServiceUrl(42220, "celo");
106+
_assertOfficialTransactionServiceUrl(43111, "hemi");
107+
_assertOfficialTransactionServiceUrl(43114, "avax");
108+
_assertOfficialTransactionServiceUrl(57073, "ink");
109+
_assertOfficialTransactionServiceUrl(59144, "linea");
110+
_assertOfficialTransactionServiceUrl(80069, "bep");
111+
_assertOfficialTransactionServiceUrl(80094, "berachain");
112+
_assertOfficialTransactionServiceUrl(81224, "codex");
113+
_assertOfficialTransactionServiceUrl(84532, "basesep");
114+
_assertOfficialTransactionServiceUrl(534352, "scr");
115+
_assertOfficialTransactionServiceUrl(747474, "katana");
116+
_assertOfficialTransactionServiceUrl(11155111, "sep");
117+
_assertOfficialTransactionServiceUrl(1313161554, "aurora");
118+
}
119+
120+
function test_Safe_getApiKitUrl_prefersThirdPartyOverrides() public view {
121+
assertEq(safe.getApiKitUrl(98866), "https://safe-transaction-plume.onchainden.com/api");
122+
}
123+
124+
function test_Safe_getApiKitUrl_revertsForUnknownChain() public {
125+
vm.expectRevert(abi.encodeWithSelector(Safe.ApiKitUrlNotFound.selector, 31337));
126+
this.exposedGetApiKitUrl(31337);
127+
}
128+
129+
function test_Safe_getMultiSendCallOnly_resolvesLegacyAndNewDeployments() public view {
130+
assertEq(address(safe.getMultiSendCallOnly(1)), 0x40A2aCCbd92BCA938b02010E17A5b8929b49130D);
131+
assertEq(address(safe.getMultiSendCallOnly(324)), 0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F);
132+
assertEq(address(safe.getMultiSendCallOnly(232)), 0x0408EF011960d02349d50286D20531229BCef773);
133+
assertEq(address(safe.getMultiSendCallOnly(10143)), 0x9641d764fc13c8B624c04430C7356C1C7C8102e2);
134+
assertEq(address(safe.getMultiSendCallOnly(98866)), 0x40A2aCCbd92BCA938b02010E17A5b8929b49130D);
135+
}
136+
137+
function test_Safe_getMultiSendCallOnly_revertsForUnknownChain() public {
138+
vm.expectRevert(abi.encodeWithSelector(Safe.MultiSendCallOnlyNotFound.selector, 31337));
139+
this.exposedGetMultiSendCallOnly(31337);
140+
}
141+
142+
function _assertOfficialTransactionServiceUrl(uint256 chainId, string memory shortName) private pure {
143+
assertEq(
144+
Safe.getTransactionServiceUrl(chainId),
145+
string.concat(SAFE_TRANSACTION_SERVICE_BASE_URL, "/", shortName, "/api")
146+
);
147+
}
148+
149+
function exposedGetApiKitUrl(uint256 chainId) external view returns (string memory) {
150+
return safe.getApiKitUrl(chainId);
151+
}
152+
153+
function exposedGetMultiSendCallOnly(uint256 chainId) external view returns (address) {
154+
return address(safe.getMultiSendCallOnly(chainId));
155+
}
156+
}

0 commit comments

Comments
 (0)