{"version":3,"file":"safe-service.mjs","names":[],"sources":["../../../src/services/safe-service.ts"],"sourcesContent":["/**\n * Safe Multisig Service — Safe Transaction Service API client.\n *\n * Interacts with Safe{Wallet} (formerly Gnosis Safe) multisig wallets via\n * the Safe Transaction Service REST API. Supports Ethereum and Base.\n *\n * API: https://safe-transaction-service.safe.global\n * No SDK dependency — direct REST calls via guardedFetch.\n */\n\nimport { guardedFetch } from './endpoint-allowlist.js';\n\n// ── Chain-specific API URLs ──────────────────────────────────────────────\n\nconst SAFE_API_URLS: Record<number, string> = {\n  1: 'https://safe-transaction-mainnet.safe.global/api',\n  8453: 'https://safe-transaction-base.safe.global/api',\n  42161: 'https://safe-transaction-arbitrum.safe.global/api',\n  10: 'https://safe-transaction-optimism.safe.global/api',\n  137: 'https://safe-transaction-polygon.safe.global/api',\n};\n\n// ── Types ────────────────────────────────────────────────────────────────\n\nexport interface SafeInfo {\n  address: string;\n  nonce: number;\n  threshold: number;\n  owners: string[];\n  modules: string[];\n  fallbackHandler: string;\n  guard: string;\n  version: string;\n  chainId: number;\n}\n\nexport interface SafeBalance {\n  tokenAddress: string | null;\n  token: { name: string; symbol: string; decimals: number } | null;\n  balance: string;\n}\n\nexport interface SafeTransaction {\n  safeTxHash: string;\n  to: string;\n  value: string;\n  data: string | null;\n  operation: number;\n  nonce: number;\n  submissionDate: string;\n  executionDate: string | null;\n  isExecuted: boolean;\n  isSuccessful: boolean | null;\n  confirmationsRequired: number;\n  confirmations: Array<{\n    owner: string;\n    submissionDate: string;\n    signatureType: string;\n  }>;\n  executor: string | null;\n  transactionHash: string | null;\n  dataDecoded: any | null;\n}\n\nexport interface ProposeTransactionParams {\n  safeAddress: string;\n  to: string;\n  value: string;\n  data: string;\n  operation?: number;\n  safeTxGas?: string;\n  baseGas?: string;\n  gasPrice?: string;\n  gasToken?: string;\n  refundReceiver?: string;\n  nonce?: number;\n  signature: string;\n  sender: string;\n}\n\n// ── Service ──────────────────────────────────────────────────────────────\n\nexport class SafeService {\n  /**\n   * Get Safe info (threshold, owners, nonce, version).\n   */\n  async getInfo(safeAddress: string, chainId = 1): Promise<SafeInfo> {\n    const data = await this.apiGet(`/v1/safes/${safeAddress}/`, chainId);\n    return {\n      address: data.address,\n      nonce: data.nonce ?? 0,\n      threshold: data.threshold ?? 0,\n      owners: data.owners ?? [],\n      modules: data.modules ?? [],\n      fallbackHandler: data.fallbackHandler ?? '',\n      guard: data.guard ?? '',\n      version: data.version ?? '',\n      chainId,\n    };\n  }\n\n  /**\n   * Get Safe balances (ETH + ERC-20 tokens).\n   */\n  async getBalances(safeAddress: string, chainId = 1): Promise<SafeBalance[]> {\n    const data = await this.apiGet(\n      `/v1/safes/${safeAddress}/balances/?trusted=true&exclude_spam=true`,\n      chainId,\n    );\n    return (data ?? []).map((b: any) => ({\n      tokenAddress: b.tokenAddress,\n      token: b.token ? {\n        name: b.token.name,\n        symbol: b.token.symbol,\n        decimals: b.token.decimals,\n      } : null,\n      balance: b.balance ?? '0',\n    }));\n  }\n\n  /**\n   * Get pending (queued) transactions for a Safe.\n   */\n  async getPendingTransactions(\n    safeAddress: string,\n    chainId = 1,\n    limit = 20,\n  ): Promise<SafeTransaction[]> {\n    const data = await this.apiGet(\n      `/v1/safes/${safeAddress}/multisig-transactions/?executed=false&limit=${limit}&ordering=-nonce`,\n      chainId,\n    );\n    return (data?.results ?? []).map(this.mapTransaction);\n  }\n\n  /**\n   * Get transaction history for a Safe.\n   */\n  async getTransactionHistory(\n    safeAddress: string,\n    chainId = 1,\n    limit = 20,\n  ): Promise<SafeTransaction[]> {\n    const data = await this.apiGet(\n      `/v1/safes/${safeAddress}/multisig-transactions/?executed=true&limit=${limit}&ordering=-executionDate`,\n      chainId,\n    );\n    return (data?.results ?? []).map(this.mapTransaction);\n  }\n\n  /**\n   * Get a specific transaction by safeTxHash.\n   */\n  async getTransaction(safeTxHash: string, chainId = 1): Promise<SafeTransaction> {\n    const data = await this.apiGet(`/v1/multisig-transactions/${safeTxHash}/`, chainId);\n    return this.mapTransaction(data);\n  }\n\n  /**\n   * Confirm (sign) a pending transaction.\n   * The signature must be generated off-chain using the Safe signing scheme.\n   */\n  async confirmTransaction(\n    safeTxHash: string,\n    signature: string,\n    chainId = 1,\n  ): Promise<{ success: boolean }> {\n    const baseUrl = this.getApiUrl(chainId);\n    const response = await guardedFetch(\n      `${baseUrl}/v1/multisig-transactions/${safeTxHash}/confirmations/`,\n      {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ signature }),\n        signal: AbortSignal.timeout(15_000),\n      },\n    );\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => '');\n      throw new Error(`Confirm failed (${response.status}): ${text.slice(0, 200)}`);\n    }\n\n    return { success: true };\n  }\n\n  /**\n   * Propose a new transaction to a Safe.\n   * Requires off-chain signature of the Safe transaction hash.\n   */\n  async proposeTransaction(\n    params: ProposeTransactionParams,\n    chainId = 1,\n  ): Promise<{ success: boolean }> {\n    const baseUrl = this.getApiUrl(chainId);\n    const response = await guardedFetch(\n      `${baseUrl}/v1/safes/${params.safeAddress}/multisig-transactions/`,\n      {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          to: params.to,\n          value: params.value,\n          data: params.data || '0x',\n          operation: params.operation ?? 0,\n          safeTxGas: params.safeTxGas ?? '0',\n          baseGas: params.baseGas ?? '0',\n          gasPrice: params.gasPrice ?? '0',\n          gasToken: params.gasToken ?? '0x0000000000000000000000000000000000000000',\n          refundReceiver: params.refundReceiver ?? '0x0000000000000000000000000000000000000000',\n          nonce: params.nonce,\n          contractTransactionHash: null,\n          sender: params.sender,\n          signature: params.signature,\n          origin: 'openclawnch',\n        }),\n        signal: AbortSignal.timeout(15_000),\n      },\n    );\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => '');\n      throw new Error(`Propose failed (${response.status}): ${text.slice(0, 200)}`);\n    }\n\n    return { success: true };\n  }\n\n  /**\n   * Check if an address is an owner of a Safe.\n   */\n  async isOwner(safeAddress: string, ownerAddress: string, chainId = 1): Promise<boolean> {\n    const info = await this.getInfo(safeAddress, chainId);\n    return info.owners.some(o => o.toLowerCase() === ownerAddress.toLowerCase());\n  }\n\n  /**\n   * Resolve chain from input string.\n   */\n  resolveChainId(chain?: string): number {\n    if (!chain) return 1;\n    switch (chain.toLowerCase()) {\n      case 'base': return 8453;\n      case 'arbitrum': case 'arb': return 42161;\n      case 'optimism': case 'op': return 10;\n      case 'polygon': case 'matic': return 137;\n      case 'ethereum': case 'eth': case 'mainnet': default: return 1;\n    }\n  }\n\n  // ── Internal ───────────────────────────────────────────────────────\n\n  private getApiUrl(chainId: number): string {\n    const url = SAFE_API_URLS[chainId];\n    if (!url) throw new Error(`Safe Transaction Service not available for chain ${chainId}.`);\n    return url;\n  }\n\n  private async apiGet(path: string, chainId: number): Promise<any> {\n    const baseUrl = this.getApiUrl(chainId);\n    const response = await guardedFetch(`${baseUrl}${path}`, {\n      headers: { accept: 'application/json' },\n      signal: AbortSignal.timeout(15_000),\n    });\n\n    if (!response.ok) {\n      const text = await response.text().catch(() => '');\n      throw new Error(`Safe API ${response.status}: ${text.slice(0, 200)}`);\n    }\n\n    return response.json();\n  }\n\n  private mapTransaction(tx: any): SafeTransaction {\n    return {\n      safeTxHash: tx.safeTxHash ?? '',\n      to: tx.to ?? '',\n      value: tx.value ?? '0',\n      data: tx.data,\n      operation: tx.operation ?? 0,\n      nonce: tx.nonce ?? 0,\n      submissionDate: tx.submissionDate ?? '',\n      executionDate: tx.executionDate,\n      isExecuted: tx.isExecuted ?? false,\n      isSuccessful: tx.isSuccessful,\n      confirmationsRequired: tx.confirmationsRequired ?? 0,\n      confirmations: (tx.confirmations ?? []).map((c: any) => ({\n        owner: c.owner ?? '',\n        submissionDate: c.submissionDate ?? '',\n        signatureType: c.signatureType ?? '',\n      })),\n      executor: tx.executor,\n      transactionHash: tx.transactionHash,\n      dataDecoded: tx.dataDecoded,\n    };\n  }\n}\n\n// ── Singleton ────────────────────────────────────────────────────────────\n\nlet _instance: SafeService | null = null;\n\nexport function getSafeService(): SafeService {\n  if (!_instance) {\n    _instance = new SafeService();\n  }\n  return _instance;\n}\n\nexport function resetSafeService(): void {\n  _instance = null;\n}\n"],"mappings":"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"}