UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

185 lines 7.58 kB
import SafeApiKit from '@safe-global/api-kit'; import Safe from '@safe-global/protocol-kit'; import { getMultiSendCallOnlyDeployment, getMultiSendDeployment, } from '@safe-global/safe-deployments'; import { assert, retryAsync, rootLogger } from '@hyperlane-xyz/utils'; export const SAFE_API_RETRIES = 10; export const SAFE_API_BASE_RETRY_MS = 1000; function isSafeApiKitConstructor(value) { return typeof value === 'function'; } export function safeApiKeyRequired(txServiceUrl) { return /safe\.global|5afe\.dev/.test(txServiceUrl); } export function normalizeSafeTxServiceUrl(txServiceUrl) { const trimmedUrl = txServiceUrl.replace(/\/+$/, ''); if (trimmedUrl.endsWith('/api')) return trimmedUrl; return `${trimmedUrl}/api`; } export function isSafeGlobalTxServiceUrl(txServiceUrl) { try { const url = new URL(normalizeSafeTxServiceUrl(txServiceUrl)); return (['api.safe.global', 'api.5afe.dev'].includes(url.hostname) && /^\/tx-service\/[^/]+\/api$/.test(url.pathname)); } catch { return false; } } export function getSafeApiKitConfig(chainId, txServiceUrl, gnosisSafeApiKey) { const normalizedTxServiceUrl = normalizeSafeTxServiceUrl(txServiceUrl); const apiKey = safeApiKeyRequired(normalizedTxServiceUrl) ? gnosisSafeApiKey : undefined; const baseConfig = { chainId: BigInt(chainId), apiKey, }; // Safe's hosted gateway authenticates API-key traffic correctly when API Kit // derives the service URL from chainId. Supplying txServiceUrl can hit lower // unauthenticated rate limits on some endpoints. if (apiKey && isSafeGlobalTxServiceUrl(normalizedTxServiceUrl)) { return baseConfig; } return { ...baseConfig, txServiceUrl: normalizedTxServiceUrl, }; } export function getSafeService(chain, multiProvider) { const { gnosisSafeTransactionServiceUrl, gnosisSafeApiKey } = multiProvider.getChainMetadata(chain); assert(gnosisSafeTransactionServiceUrl, `must provide tx service url for ${chain}`); const chainId = multiProvider.getEvmChainId(chain); assert(chainId, `Chain is not an EVM chain: ${chain}`); const config = getSafeApiKitConfig(chainId, gnosisSafeTransactionServiceUrl, gnosisSafeApiKey); assert(isSafeApiKitConstructor(SafeApiKit), '@safe-global/api-kit default export is not a constructor'); try { return new SafeApiKit(config); } catch (error) { if (error instanceof TypeError && error.message.includes('There is no transaction service available for chainId') && !config.txServiceUrl && isSafeGlobalTxServiceUrl(gnosisSafeTransactionServiceUrl)) { return new SafeApiKit({ ...config, txServiceUrl: normalizeSafeTxServiceUrl(gnosisSafeTransactionServiceUrl), }); } throw error; } } // This is the version of the Safe contracts that the SDK is compatible with. // Copied the MVP fields from https://github.com/safe-global/safe-core-sdk/blob/4d1c0e14630f951c2498e1d4dd521403af91d6e1/packages/protocol-kit/src/contracts/config.ts#L19 // because the SDK doesn't expose this value. const safeDeploymentsVersions = { '1.4.1': { multiSendVersion: '1.4.1', multiSendCallOnlyVersion: '1.4.1', }, '1.3.0': { multiSendVersion: '1.3.0', multiSendCallOnlyVersion: '1.3.0', }, '1.2.0': { multiSendVersion: '1.1.1', multiSendCallOnlyVersion: '1.3.0', }, '1.1.1': { multiSendVersion: '1.1.1', multiSendCallOnlyVersion: '1.3.0', }, '1.0.0': { multiSendVersion: '1.1.1', multiSendCallOnlyVersion: '1.3.0', }, }; // Override for chains that haven't yet been published in the safe-deployments package. // Temporary until PR to safe-deployments package is merged and SDK dependency is updated. const chainOverrides = { // zeronetwork 543210: { multiSend: '0x0dFcccB95225ffB03c6FBB2559B530C2B7C8A912', multiSendCallOnly: '0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F', }, // berachain 80094: { multiSend: '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761', multiSendCallOnly: '0x40A2aCCbd92BCA938b02010E17A5b8929b49130D', }, // igra 38833: { multiSend: '0x218543288004CD07832472D464648173c77D7eB7', multiSendCallOnly: '0xA83c336B20401Af773B6219BA5027174338D1836', }, }; export async function getSafe(chain, multiProvider, safeAddress, signer) { // Get the chain id for the given chain const chainId = `${multiProvider.getEvmChainId(chain)}`; // Get the safe version const safeService = getSafeService(chain, multiProvider); const { version: rawSafeVersion } = await retryAsync(() => safeService.getSafeInfo(safeAddress), SAFE_API_RETRIES, SAFE_API_BASE_RETRY_MS); // Remove any build metadata from the version e.g. 1.3.0+L2 --> 1.3.0 const safeVersion = rawSafeVersion.split(' ')[0].split('+')[0].split('-')[0]; // Get the multiSend and multiSendCallOnly deployments for the given chain let multiSend, multiSendCallOnly; if (chainOverrides[chainId]) { multiSend = { networkAddresses: { [chainId]: chainOverrides[chainId].multiSend }, }; multiSendCallOnly = { networkAddresses: { [chainId]: chainOverrides[chainId].multiSendCallOnly, }, }; } else if (safeDeploymentsVersions[safeVersion]) { const { multiSendVersion, multiSendCallOnlyVersion } = safeDeploymentsVersions[safeVersion]; multiSend = getMultiSendDeployment({ version: multiSendVersion, network: chainId, }); multiSendCallOnly = getMultiSendCallOnlyDeployment({ version: multiSendCallOnlyVersion, network: chainId, }); } // @ts-ignore return Safe.init({ provider: multiProvider.getChainMetadata(chain).rpcUrls[0].http, signer, safeAddress, contractNetworks: { [chainId]: { // Use the safe address for multiSendAddress and multiSendCallOnlyAddress // if the contract is not deployed or if the version is not found. multiSendAddress: multiSend?.networkAddresses[chainId] || safeAddress, multiSendCallOnlyAddress: multiSendCallOnly?.networkAddresses[chainId] || safeAddress, }, }, }); } export async function getSafeDelegates(service, safeAddress) { const delegateResponse = await retryAsync(() => service.getSafeDelegates({ safeAddress }), SAFE_API_RETRIES, SAFE_API_BASE_RETRY_MS); return delegateResponse.results.map((r) => r.delegate); } export async function canProposeSafeTransactions(proposer, chain, multiProvider, safeAddress) { let safeService; try { safeService = getSafeService(chain, multiProvider); } catch (e) { rootLogger.error('Failed to get Safe service for chain', { chain, chainName: multiProvider.tryGetChainName(chain), knownChains: multiProvider.getKnownChainNames(), error: e, }); return false; } const safe = await getSafe(chain, multiProvider, safeAddress); const delegates = await getSafeDelegates(safeService, safeAddress); const owners = await safe.getOwners(); return delegates.includes(proposer) || owners.includes(proposer); } //# sourceMappingURL=gnosisSafe.js.map