{"id":25951515,"url":"https://github.com/5afe/client-gateway-demo","last_synced_at":"2026-06-07T17:31:26.655Z","repository":{"id":280605613,"uuid":"942552519","full_name":"5afe/client-gateway-demo","owner":"5afe","description":"This tutorial will demonstrate how to use https://safe-client.safe.global/api#/ to assist interact programmatically with your account and how to migrate from using the https://github.com/safe-global/safe-transaction-service","archived":false,"fork":false,"pushed_at":"2025-03-04T09:46:18.000Z","size":0,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-04T10:34:08.367Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/5afe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-04T09:33:32.000Z","updated_at":"2025-03-04T09:46:22.000Z","dependencies_parsed_at":"2025-03-04T10:34:12.212Z","dependency_job_id":"12d75c48-ec65-4bfb-8372-b7cc1453bd80","html_url":"https://github.com/5afe/client-gateway-demo","commit_stats":null,"previous_names":["5afe/client-gateway-demo"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/5afe%2Fclient-gateway-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/5afe%2Fclient-gateway-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/5afe%2Fclient-gateway-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/5afe%2Fclient-gateway-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/5afe","download_url":"https://codeload.github.com/5afe/client-gateway-demo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241859996,"owners_count":20032318,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-04T13:48:41.396Z","updated_at":"2026-06-07T17:31:26.155Z","avatar_url":"https://github.com/5afe.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Safe Client API Tutorial\n\n## Introduction\n\nThis tutorial demonstrates how to use the [Safe Client API](https://safe-client.safe.global/api#/) to interact programmatically with your Safe account and how to migrate from using the [Safe Transaction Service](https://github.com/safe-global/safe-transaction-service).\n\nPreviously, users were using the [Safe Transaction Service](https://github.com/safe-global/safe-transaction-service) either directly or in conjunction with the [@safe-global/api-kit](https://www.npmjs.com/package/@safe-global/api-kit) npm package.\n\n## What You'll Learn\n\nThis guide provides a quick introduction to:\n\n- Verify if a chain is available\n- Fetch information about a Safe\n- Fetch the transaction history\n- Fetch pending transactions\n- Propose a transaction\n- Sign a transaction\n- Execute a transaction\n\n## API URL Structure\n\n### Previous Transaction Service\n\nPreviously, a new transaction service with a specific URL was created for each chain with the following pattern:\n\n```\nhttps://safe-transaction-\u003cnetworkName\u003e.safe.global\n```\n\nFor example: [https://safe-transaction-polygon.safe.global](https://safe-transaction-polygon.safe.global/)\n\nNotice that the URL holds the chain name, therefore the data will only come from one chain.\n\n### New Safe Client API\n\nWith the Safe Client API, the URL is chain agnostic:\n\n```\nhttps://safe-client.safe.global/api#/\n```\n\nThe `chainId` will be passed as a parameter to the endpoint you are querying, making it more flexible and consistent across different chains.\n\n## Verifying Chain Availability\n\nTo check if a specific chain is supported by the Safe Client API, you can use the `/v1/chains` endpoint. This endpoint returns information about all supported chains, or you can query a specific chain using `/v1/chains/{chainId}`.\n\n### Using curl\n\nTo get all supported chains:\n```bash\ncurl -X GET \"https://safe-client.safe.global/v1/chains\"\n```\n\nTo check a specific chain (e.g., Ethereum Mainnet with chainId \"1\"):\n```bash\ncurl -X GET \"https://safe-client.safe.global/v1/chains/1\"\n```\n\n### Using JavaScript Fetch\n\nTo get all supported chains:\n```javascript\nconst getAllChains = async () =\u003e {\n  try {\n    const response = await fetch('https://safe-client.safe.global/v1/chains');\n    const data = await response.json();\n    console.log('Supported chains:', data);\n  } catch (error) {\n    console.error('Error fetching chains:', error);\n  }\n};\n```\n\nTo check a specific chain:\n```javascript\nconst checkChain = async (chainId) =\u003e {\n  try {\n    const response = await fetch(`https://safe-client.safe.global/v1/chains/${chainId}`);\n    if (response.ok) {\n      const chainData = await response.json();\n      console.log('Chain is supported:', chainData);\n      return true;\n    } else {\n      console.log('Chain is not supported');\n      return false;\n    }\n  } catch (error) {\n    console.error('Error checking chain:', error);\n    return false;\n  }\n};\n```\n\n### Response Structure\n\nThe response for a supported chain will include detailed information about the chain:\n\n```json\n{\n  \"chainId\": \"1\",\n  \"chainName\": \"Ethereum\",\n  \"description\": \"Ethereum Mainnet\",\n  \"l2\": false,\n  \"isTestnet\": false,\n  \"nativeCurrency\": {\n    \"name\": \"Ether\",\n    \"symbol\": \"ETH\",\n    \"decimals\": 18,\n    \"logoUri\": \"...\"\n  },\n  \"transactionService\": \"...\",\n  \"blockExplorerUriTemplate\": {\n    \"address\": \"...\",\n    \"txHash\": \"...\",\n    \"api\": \"...\"\n  },\n  // Additional chain properties...\n}\n```\n\n## Fetching Safe Information\n\nTo get detailed information about a Safe, you can use the `/v1/chains/{chainId}/safes/{safeAddress}` endpoint. This endpoint provides comprehensive details about a Safe's configuration, owners, and current state.\n\n### Using curl\n\n```bash\n# Replace with your actual chain ID and Safe address\ncurl -X GET \"https://safe-client.safe.global/v1/chains/1/safes/0x123...789\"\n```\n\n### Using JavaScript Fetch\n\n```javascript\nconst getSafeInfo = async (chainId, safeAddress) =\u003e {\n  try {\n    const response = await fetch(\n      `https://safe-client.safe.global/v1/chains/${chainId}/safes/${safeAddress}`\n    );\n    \n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n    \n    const safeInfo = await response.json();\n    return safeInfo;\n  } catch (error) {\n    console.error('Error fetching Safe info:', error);\n    throw error;\n  }\n};\n```\n\n### Response Structure\n\nThe response includes detailed information about the Safe:\n\n```json\n{\n  \"address\": {\n    \"value\": \"0x123...789\",\n    \"name\": null,\n    \"logoUri\": null\n  },\n  \"chainId\": \"1\",\n  \"nonce\": 42,\n  \"threshold\": 2,\n  \"owners\": [\n    {\n      \"value\": \"0xowner1...abc\",\n      \"name\": null,\n      \"logoUri\": null\n    },\n    {\n      \"value\": \"0xowner2...def\",\n      \"name\": null,\n      \"logoUri\": null\n    }\n  ],\n  \"implementation\": {\n    \"value\": \"0ximpl...xyz\",\n    \"name\": null,\n    \"logoUri\": null\n  },\n  \"implementationVersionState\": \"UP_TO_DATE\",\n  \"modules\": null,\n  \"fallbackHandler\": null,\n  \"guard\": null,\n  \"version\": \"1.3.0\"\n}\n```\n\n### Understanding the Response\n\n- `address`: The Safe's address on the blockchain\n- `chainId`: The chain where the Safe is deployed\n- `nonce`: Current transaction count\n- `threshold`: Number of required confirmations for transactions\n- `owners`: List of addresses that can sign transactions\n- `implementation`: The Safe contract implementation address\n- `implementationVersionState`: Indicates if the Safe contract is up to date\n- `version`: The version of the Safe contract\n\n### Additional Safe Information\n\nYou can also fetch additional details about a Safe:\n\n#### Get Safe Nonces\n```javascript\nconst getSafeNonces = async (chainId, safeAddress) =\u003e {\n  try {\n    const response = await fetch(\n      `https://safe-client.safe.global/v1/chains/${chainId}/safes/${safeAddress}/nonces`\n    );\n    const nonces = await response.json();\n    return nonces;\n  } catch (error) {\n    console.error('Error fetching nonces:', error);\n    throw error;\n  }\n};\n```\n\n#### Get Safe Creation Info\n```javascript\nconst getSafeCreationInfo = async (chainId, safeAddress) =\u003e {\n  try {\n    const response = await fetch(\n      `https://safe-client.safe.global/v1/chains/${chainId}/safes/${safeAddress}/creation`\n    );\n    const creationInfo = await response.json();\n    return creationInfo;\n  } catch (error) {\n    console.error('Error fetching creation info:', error);\n    throw error;\n  }\n};\n```\n\n## Fetching Transaction History\n\nThe Safe Client API provides a comprehensive endpoint to fetch the transaction history of a Safe. You can use the `/v1/chains/{chainId}/safes/{safeAddress}/transactions/history` endpoint to retrieve all historical transactions.\n\n### Using curl\n\n```bash\n# Basic usage\ncurl -X GET \"https://safe-client.safe.global/v1/chains/1/safes/0x123...789/transactions/history\"\n\n# With optional parameters\ncurl -X GET \"https://safe-client.safe.global/v1/chains/1/safes/0x123...789/transactions/history?trusted=true\u0026cursor=CURSOR_VALUE\"\n```\n\n### Using JavaScript Fetch\n\n```javascript\nconst getTransactionHistory = async (chainId, safeAddress, options = {}) =\u003e {\n  try {\n    const queryParams = new URLSearchParams({\n      ...(options.trusted !== undefined \u0026\u0026 { trusted: options.trusted }),\n      ...(options.cursor \u0026\u0026 { cursor: options.cursor }),\n      ...(options.timezone \u0026\u0026 { timezone: options.timezone })\n    }).toString();\n\n    const url = `https://safe-client.safe.global/v1/chains/${chainId}/safes/${safeAddress}/transactions/history${\n      queryParams ? `?${queryParams}` : ''\n    }`;\n\n    const response = await fetch(url);\n    \n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n    \n    const history = await response.json();\n    return history;\n  } catch (error) {\n    console.error('Error fetching transaction history:', error);\n    throw error;\n  }\n};\n```\n\n### Response Structure\n\nThe response includes a paginated list of transactions with detailed information:\n\n```json\n{\n  \"count\": 100,\n  \"next\": \"https://safe-client.safe.global/v1/chains/1/safes/0x123...789/transactions/history?cursor=NEXT_CURSOR\",\n  \"previous\": null,\n  \"results\": [\n    {\n      \"type\": \"TRANSACTION\",\n      \"transaction\": {\n        \"id\": \"tx_id_1\",\n        \"timestamp\": 1677654321,\n        \"txStatus\": \"SUCCESS\",\n        \"txInfo\": {\n          \"type\": \"Transfer\",\n          \"sender\": {\n            \"value\": \"0xsender...123\"\n          },\n          \"recipient\": {\n            \"value\": \"0xrecipient...456\"\n          },\n          \"direction\": \"OUTGOING\",\n          \"transferInfo\": {\n            \"type\": \"NATIVE_COIN\",\n            \"value\": \"1000000000000000000\"\n          }\n        },\n        \"executionInfo\": {\n          \"type\": \"MULTISIG\",\n          \"nonce\": 42,\n          \"confirmationsRequired\": 2,\n          \"confirmationsSubmitted\": 2\n        }\n      },\n      \"conflictType\": \"None\"\n    },\n    {\n      \"type\": \"DATE_LABEL\",\n      \"timestamp\": 1677654000\n    }\n  ]\n}\n```\n\n### Pagination and Filtering\n\nTo handle pagination and implement filtering:\n\n```javascript\nconst getAllTransactionHistory = async (chainId, safeAddress, options = {}) =\u003e {\n  let allTransactions = [];\n  let nextCursor = null;\n  \n  do {\n    const queryOptions = {\n      ...options,\n      ...(nextCursor \u0026\u0026 { cursor: nextCursor })\n    };\n    \n    const response = await getTransactionHistory(chainId, safeAddress, queryOptions);\n    \n    allTransactions = allTransactions.concat(response.results);\n    nextCursor = new URL(response.next).searchParams.get('cursor');\n  } while (nextCursor);\n  \n  return allTransactions;\n};\n```\n\n### Filtering by Transaction Type\n\n```javascript\nconst filterTransactionsByType = (transactions, type) =\u003e {\n  return transactions.filter(tx =\u003e \n    tx.type === 'TRANSACTION' \u0026\u0026 tx.transaction.txInfo.type === type\n  );\n};\n\n// Example usage:\nconst getTransferTransactions = async (chainId, safeAddress) =\u003e {\n  const history = await getTransactionHistory(chainId, safeAddress);\n  return filterTransactionsByType(history.results, 'Transfer');\n};\n```\n\n## Fetching Pending Transactions\n\nThe Safe Client API provides an endpoint to fetch pending transactions that are awaiting confirmations or execution. You can use the `/v1/chains/{chainId}/safes/{safeAddress}/transactions/queued` endpoint to retrieve these transactions.\n\n### Using curl\n\n```bash\n# Basic usage\ncurl -X GET \"https://safe-client.safe.global/v1/chains/1/safes/0x123...789/transactions/queued\"\n\n# With optional parameters\ncurl -X GET \"https://safe-client.safe.global/v1/chains/1/safes/0x123...789/transactions/queued?trusted=true\u0026cursor=CURSOR_VALUE\"\n```\n\n### Using JavaScript Fetch\n\n```javascript\nconst getPendingTransactions = async (chainId, safeAddress, options = {}) =\u003e {\n  try {\n    const queryParams = new URLSearchParams({\n      ...(options.trusted !== undefined \u0026\u0026 { trusted: options.trusted }),\n      ...(options.cursor \u0026\u0026 { cursor: options.cursor })\n    }).toString();\n\n    const url = `https://safe-client.safe.global/v1/chains/${chainId}/safes/${safeAddress}/transactions/queued${\n      queryParams ? `?${queryParams}` : ''\n    }`;\n\n    const response = await fetch(url);\n    \n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n    \n    const pendingTxs = await response.json();\n    return pendingTxs;\n  } catch (error) {\n    console.error('Error fetching pending transactions:', error);\n    throw error;\n  }\n};\n```\n\n### Response Structure\n\nThe response includes a paginated list of pending transactions:\n\n```json\n{\n  \"count\": 2,\n  \"next\": null,\n  \"previous\": null,\n  \"results\": [\n    {\n      \"type\": \"TRANSACTION\",\n      \"transaction\": {\n        \"id\": \"multisig_0x123...789_1\",\n        \"timestamp\": 1677654321,\n        \"txStatus\": \"AWAITING_CONFIRMATIONS\",\n        \"txInfo\": {\n          \"type\": \"Transfer\",\n          \"sender\": {\n            \"value\": \"0xsender...123\"\n          },\n          \"recipient\": {\n            \"value\": \"0xrecipient...456\"\n          },\n          \"direction\": \"OUTGOING\",\n          \"transferInfo\": {\n            \"type\": \"ERC20\",\n            \"tokenAddress\": \"0xtoken...789\",\n            \"value\": \"1000000000000000000\"\n          }\n        },\n        \"executionInfo\": {\n          \"type\": \"MULTISIG\",\n          \"nonce\": 42,\n          \"confirmationsRequired\": 2,\n          \"confirmationsSubmitted\": 1\n        }\n      },\n      \"conflictType\": \"None\"\n    }\n  ]\n}\n```\n\n### Understanding the Response\n\nThe response contains several key pieces of information:\n\n- `count`: Total number of pending transactions\n- `results`: Array of transaction objects, each containing:\n  - `type`: Type of the queue item (usually \"TRANSACTION\")\n  - `transaction`: The transaction details including:\n    - `id`: Unique transaction identifier\n    - `txStatus`: Current status (e.g., \"AWAITING_CONFIRMATIONS\", \"AWAITING_EXECUTION\")\n    - `txInfo`: Information about the transaction type and details\n    - `executionInfo`: Information about required and submitted confirmations\n\n### Pagination\n\nTo fetch all pending transactions with pagination:\n\n```javascript\nconst getAllPendingTransactions = async (chainId, safeAddress) =\u003e {\n  let allTransactions = [];\n  let nextCursor = null;\n  \n  do {\n    const options = nextCursor ? { cursor: nextCursor } : {};\n    const response = await getPendingTransactions(chainId, safeAddress, options);\n    \n    allTransactions = allTransactions.concat(response.results);\n    nextCursor = response.next ? new URL(response.next).searchParams.get('cursor') : null;\n  } while (nextCursor);\n  \n  return allTransactions;\n};\n```\n\n### Filtering Pending Transactions\n\nTo filter pending transactions by specific criteria:\n\n```javascript\nconst filterPendingByConfirmations = (transactions, requiredConfirmations) =\u003e {\n  return transactions.filter(tx =\u003e \n    tx.type === 'TRANSACTION' \u0026\u0026 \n    tx.transaction.executionInfo.confirmationsSubmitted \u003c requiredConfirmations\n  );\n};\n\n// Example usage:\nconst getTransactionsNeedingConfirmations = async (chainId, safeAddress) =\u003e {\n  const pendingTxs = await getPendingTransactions(chainId, safeAddress);\n  return filterPendingByConfirmations(pendingTxs.results, 2);\n};\n```\n\n## Proposing a Transaction\n\nThe Safe Client API allows you to propose new transactions for a Safe using the `/v1/chains/{chainId}/transactions/{safeAddress}/propose` endpoint.\n\n### Using curl\n\n```bash\ncurl -X POST \"https://safe-client.safe.global/v1/chains/{chainId}/transactions/{safeAddress}/propose\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"to\": \"0xrecipient...456\",\n    \"value\": \"1000000000000000000\",\n    \"data\": \"0x\",\n    \"nonce\": \"1\",\n    \"operation\": 0,\n    \"safeTxGas\": \"0\",\n    \"baseGas\": \"0\",\n    \"gasPrice\": \"0\",\n    \"gasToken\": \"0x0000000000000000000000000000000000000000\",\n    \"refundReceiver\": \"0x0000000000000000000000000000000000000000\",\n    \"safeTxHash\": \"0xhash...789\",\n    \"sender\": \"0xsender...123\",\n    \"signature\": \"0xsig...abc\",\n    \"origin\": \"Safe UI\"\n  }'\n```\n\n### Using JavaScript Fetch\n\n```javascript\nconst proposeTransaction = async (chainId, safeAddress, transactionData) =\u003e {\n  try {\n    const response = await fetch(\n      `https://safe-client.safe.global/v1/chains/${chainId}/transactions/${safeAddress}/propose`,\n      {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify(transactionData)\n      }\n    );\n    \n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n    \n    const result = await response.json();\n    return result;\n  } catch (error) {\n    console.error('Error proposing transaction:', error);\n    throw error;\n  }\n};\n```\n\nThe Safe transaction hash can get computed by using our SDK https://docs.safe.global/reference-sdk-protocol-kit/transactions/gettransactionhash or can be fetched directly from the smart contract https://github.com/safe-global/safe-smart-account/blob/21dc82410445637820f600c7399a804ad55841d5/contracts/Safe.sol#L383.\n\n### Request Structure\n\n```typescript\ninterface ProposeTransactionRequest {\n  to: string;                 // Target contract/recipient address\n  value: string;             // Amount in wei to transfer\n  data: string;              // Transaction data (hex-encoded)\n  nonce: string;             // Safe transaction nonce\n  operation: number;         // Operation type (0 = CALL, 1 = DELEGATE_CALL)\n  safeTxGas: string;         // Gas to use for the safe transaction\n  baseGas: string;           // Base gas cost\n  gasPrice: string;          // Gas price for the transaction\n  gasToken: string;          // Token address for gas payment (0x0 for native token)\n  refundReceiver: string;    // Address to receive gas payment refund\n  safeTxHash: string;        // Hash of the safe transaction\n  sender: string;            // Address of the transaction sender\n  signature?: string;        // Signature of the sender (optional)\n  origin?: string;          // Origin of the transaction (optional)\n}\n\ninterface TransactionResponse {\n  safeTxHash: string;\n  txHash?: string;\n  sender: AddressInfo;\n  confirmationsRequired: number;\n  confirmationsSubmitted: number;\n  signatures: Array\u003c{\n    signer: string;\n    signature: string;\n    data: string;\n  }\u003e;\n  executionDate?: string;\n  submissionDate: string;\n  status: 'AWAITING_CONFIRMATIONS' | 'AWAITING_EXECUTION' | 'SUCCESS' | 'FAILED' | 'CANCELLED';\n}\n```\n\n### Example Usage\n\nHere's an example of proposing an ETH transfer transaction:\n\n```javascript\nconst proposeEthTransfer = async (chainId, safeAddress, recipientAddress, amountInWei) =\u003e {\n  const transactionData = {\n    to: recipientAddress,\n    value: amountInWei,\n    data: '0x',  // Empty data for ETH transfer\n    nonce: '1',  // Should be fetched from the Safe\n    operation: 0,\n    safeTxGas: '0',\n    baseGas: '0',\n    gasPrice: '0',\n    gasToken: '0x0000000000000000000000000000000000000000',\n    refundReceiver: '0x0000000000000000000000000000000000000000',\n    safeTxHash: '0x...', // Should be calculated based on the transaction data\n    sender: '0x...', // Address of the sender\n    signature: '0x...', // Optional: Signature of the sender\n    origin: 'API Tutorial' // Optional: Origin of the transaction\n  };\n\n  return await proposeTransaction(chainId, safeAddress, transactionData);\n};\n```\n\n### Response Example\n\n```json\n{\n  \"safeTxHash\": \"0x...\",\n  \"txHash\": null,\n  \"sender\": {\n    \"value\": \"0x...\",\n    \"name\": null,\n    \"logoUri\": null\n  },\n  \"confirmationsRequired\": 2,\n  \"confirmationsSubmitted\": 1,\n  \"signatures\": [\n    {\n      \"signer\": \"0x...\",\n      \"signature\": \"0x...\",\n      \"data\": \"0x...\"\n    }\n  ],\n  \"executionDate\": null,\n  \"submissionDate\": \"2024-02-20T10:00:00Z\",\n  \"status\": \"AWAITING_CONFIRMATIONS\"\n}\n```\n\n### Computing Safe Transaction Hash with Viem\n\nTo compute the Safe transaction hash using viem, you'll need to implement the same hashing logic as the Smart Contract. Here's how to do it:\n\n```typescript\nimport { encodeAbiParameters, encodePacked, keccak256 } from 'viem'\n\ninterface SafeTransactionData {\n  to: string\n  value: bigint\n  data: string\n  operation: number\n  safeTxGas: bigint\n  baseGas: bigint\n  gasPrice: bigint\n  gasToken: string\n  refundReceiver: string\n  nonce: bigint\n}\n\n// Constants\nconst SAFE_TX_TYPEHASH = '0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8'\nconst DOMAIN_SEPARATOR_TYPEHASH = '0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218';\n\nconst calculateSafeTransactionHash = async (\n  safeAddress: string,\n  chainId: bigint,\n  safeTransactionData: SafeTransactionData\n) =\u003e {\n  // 1. Calculate domain separator\n  const domainSeparator = keccak256(\n    encodeAbiParameters(\n      [\n        { type: 'bytes32', name: 'typeHash' },\n        { type: 'uint256', name: 'chainId' },\n        { type: 'address', name: 'safe' },\n      ],\n      [\n        DOMAIN_SEPARATOR_TYPEHASH,\n        chainId,\n        safeAddress as `0x${string}`,\n      ]\n    )\n  )\n\n  // 2. Calculate safeTxHash\n  const safeTxHash = keccak256(\n    encodeAbiParameters(\n      [\n        { type: 'bytes32', name: 'typeHash' },\n        { type: 'address', name: 'to' },\n        { type: 'uint256', name: 'value' },\n        { type: 'bytes32', name: 'dataHash' },\n        { type: 'uint8', name: 'operation' },\n        { type: 'uint256', name: 'safeTxGas' },\n        { type: 'uint256', name: 'baseGas' },\n        { type: 'uint256', name: 'gasPrice' },\n        { type: 'address', name: 'gasToken' },\n        { type: 'address', name: 'refundReceiver' },\n        { type: 'uint256', name: 'nonce' }\n      ],\n      [\n        SAFE_TX_TYPEHASH,\n        safeTransactionData.to as `0x${string}`,\n        safeTransactionData.value,\n        keccak256(safeTransactionData.data as `0x${string}`),\n        safeTransactionData.operation,\n        safeTransactionData.safeTxGas,\n        safeTransactionData.baseGas,\n        safeTransactionData.gasPrice,\n        safeTransactionData.gasToken as `0x${string}`,\n        safeTransactionData.refundReceiver as `0x${string}`,\n        safeTransactionData.nonce\n      ]\n    )\n  )\n\n  // 3. Encode final hash\n  return keccak256(\n    encodePacked(\n      ['bytes1', 'bytes1', 'bytes32', 'bytes32'],\n      ['0x19', '0x01', domainSeparator, safeTxHash]\n    )\n  )\n}\n\n// Example usage\nconst computeAndProposeTx = async (\n  chainId: bigint,\n  safeAddress: string,\n  transactionData: SafeTransactionData\n) =\u003e {\n  const safeTxHash = await calculateSafeTransactionHash(\n    safeAddress,\n    chainId,\n    transactionData\n  )\n\n  const proposalData = {\n    ...transactionData,\n    safeTxHash,\n    value: transactionData.value.toString(),\n    safeTxGas: transactionData.safeTxGas.toString(),\n    baseGas: transactionData.baseGas.toString(),\n    gasPrice: transactionData.gasPrice.toString(),\n    nonce: transactionData.nonce.toString()\n  }\n\n  return await proposeTransaction(chainId.toString(), safeAddress, proposalData)\n}\n\n// Example with actual values\nconst example = async () =\u003e {\n  const tx = {\n    to: '0x1234...', // recipient address\n    value: BigInt(1000000000000000000), // 1 ETH in wei\n    data: '0x', // empty data for ETH transfer\n    operation: 0, // CALL\n    safeTxGas: BigInt(0),\n    baseGas: BigInt(0),\n    gasPrice: BigInt(0),\n    gasToken: '0x0000000000000000000000000000000000000000',\n    refundReceiver: '0x0000000000000000000000000000000000000000',\n    nonce: BigInt(1) // should be fetched from the Safe\n  }\n\n  const result = await computeAndProposeTx(\n    BigInt(1), // chainId (1 for Ethereum mainnet)\n    '0xYourSafeAddress...',\n    tx\n  )\n  console.log('Transaction proposed:', result)\n}\n```\n\n### Understanding the Hash Computation\n\nThe hash computation follows these steps:\n\n1. **Domain Separator**: Creates a unique identifier for the Safe contract on a specific chain\n   ```solidity\n   keccak256(\n     abi.encode(\n       keccak256(\"EIP712Domain(uint256 chainId,address verifyingContract)\"),\n       chainId,\n       address(this)\n     )\n   )\n   ```\n\n2. **Safe Transaction Hash**: Hashes the transaction parameters\n   ```solidity\n   bytes32 safeTxHash = keccak256(\n     abi.encode(\n       SAFE_TX_TYPEHASH,\n       to,\n       value,\n       keccak256(data),\n       operation,\n       safeTxGas,\n       baseGas,\n       gasPrice,\n       gasToken,\n       refundReceiver,\n       _nonce\n     )\n   )\n   ```\n\n3. **Final Hash**: Combines everything according to EIP-712\n   ```solidity\n   keccak256(\n     abi.encodePacked(\n       bytes1(0x19),\n       bytes1(0x01),\n       domainSeparator,\n       safeTxHash\n     )\n   )\n   ```\n\nThe viem implementation uses the same logic but with TypeScript types and viem's encoding functions.\n\n## Signing a Transaction\n\nAfter proposing a transaction, other owners need to sign it. You can add signatures using the `/v1/chains/{chainId}/transactions/{safeTxHash}/confirmations` endpoint.\n\n### Using curl\n\n```bash\ncurl -X POST \"https://safe-client.safe.global/v1/chains/{chainId}/transactions/{safeTxHash}/confirmations\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"signature\": \"0x...\"\n  }'\n```\n\n### Using JavaScript Fetch with Viem\n\n```typescript\nimport { type Hash, type Hex, SignatureType } from 'viem'\nimport { privateKeyToAccount } from 'viem/accounts'\n\ninterface SignTransactionRequest {\n  signature: string;\n}\n\nconst signTransaction = async (chainId: string, safeTxHash: Hash, signature: Hex) =\u003e {\n  try {\n    const response = await fetch(\n      `https://safe-client.safe.global/v1/chains/${chainId}/transactions/${safeTxHash}/confirmations`,\n      {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ signature })\n      }\n    );\n    \n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n    \n    return await response.json();\n  } catch (error) {\n    console.error('Error signing transaction:', error);\n    throw error;\n  }\n};\n\n// Helper function to generate signature\nconst generateSignature = async (\n  privateKey: Hex,\n  safeTxHash: Hash\n): Promise\u003cHex\u003e =\u003e {\n  const account = privateKeyToAccount(privateKey)\n  return await account.signMessage({\n    message: { raw: safeTxHash }\n  }) as Hex\n}\n\n// Example usage combining hash computation and signing\nconst signSafeTransaction = async (\n  chainId: bigint,\n  safeAddress: string,\n  privateKey: Hex,\n  transactionData: SafeTransactionData\n) =\u003e {\n  // 1. Calculate the safe transaction hash\n  const safeTxHash = await calculateSafeTransactionHash(\n    safeAddress,\n    chainId,\n    transactionData\n  )\n\n  // 2. Generate signature\n  const signature = await generateSignature(privateKey, safeTxHash)\n\n  // 3. Submit signature\n  return await signTransaction(\n    chainId.toString(),\n    safeTxHash,\n    signature\n  )\n}\n```\n\n### Response Structure\n\n```typescript\ninterface SignatureResponse {\n  safeTxHash: Hash;\n  signature: Hex;\n  sender: AddressInfo;\n  confirmationsRequired: number;\n  confirmationsSubmitted: number;\n  signatures: Array\u003c{\n    signer: string;\n    signature: string;\n    data?: string;\n  }\u003e;\n}\n```\n\nExample response:\n```json\n{\n  \"safeTxHash\": \"0x...\",\n  \"signature\": \"0x...\",\n  \"sender\": {\n    \"value\": \"0x...\",\n    \"name\": null,\n    \"logoUri\": null\n  },\n  \"confirmationsRequired\": 2,\n  \"confirmationsSubmitted\": 2,\n  \"signatures\": [\n    {\n      \"signer\": \"0x...\",\n      \"signature\": \"0x...\"\n    }\n  ]\n}\n```\n\n### Verifying Signatures\n\nYou can verify if a transaction has enough signatures before execution:\n\n```typescript\nconst verifySignatures = async (chainId: string, safeTxHash: Hash) =\u003e {\n  try {\n    const response = await fetch(\n      `https://safe-client.safe.global/v1/chains/${chainId}/transactions/${safeTxHash}`\n    );\n    \n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n    \n    const txInfo = await response.json();\n    return txInfo.confirmationsSubmitted \u003e= txInfo.confirmationsRequired;\n  } catch (error) {\n    console.error('Error verifying signatures:', error);\n    throw error;\n  }\n};\n```\n\n### Complete Signing Flow Example\n\n```typescript\nconst completeSigningFlow = async (\n  chainId: bigint,\n  safeAddress: string,\n  privateKey: Hex,\n  transactionData: SafeTransactionData\n) =\u003e {\n  // 1. Calculate hash and sign\n  const result = await signSafeTransaction(\n    chainId,\n    safeAddress,\n    privateKey,\n    transactionData\n  );\n\n  // 2. Verify if we have enough signatures\n  const readyToExecute = await verifySignatures(\n    chainId.toString(),\n    result.safeTxHash\n  );\n\n  return {\n    ...result,\n    readyToExecute\n  };\n};\n```\n\n## Executing a Transaction\n\nOnce a transaction has collected enough signatures, any address can execute it on-chain. This is done using the Safe contract directly, not through the Safe Client API.\n\n### Using Viem\n\n```typescript\nimport { createPublicClient, http, createWalletClient, type Hash, type Hex } from 'viem'\nimport { mainnet } from 'viem/chains'\n\ninterface ExecuteTransactionParams {\n  to: string\n  value: bigint\n  data: string\n  operation: number\n  safeTxGas: bigint\n  baseGas: bigint\n  gasPrice: bigint\n  gasToken: string\n  refundReceiver: string\n  signatures: string\n}\n\n// ABI for the execTransaction function\nconst safeAbi = [{\n  \"inputs\": [\n    { \"name\": \"to\", \"type\": \"address\" },\n    { \"name\": \"value\", \"type\": \"uint256\" },\n    { \"name\": \"data\", \"type\": \"bytes\" },\n    { \"name\": \"operation\", \"type\": \"uint8\" },\n    { \"name\": \"safeTxGas\", \"type\": \"uint256\" },\n    { \"name\": \"baseGas\", \"type\": \"uint256\" },\n    { \"name\": \"gasPrice\", \"type\": \"uint256\" },\n    { \"name\": \"gasToken\", \"type\": \"address\" },\n    { \"name\": \"refundReceiver\", \"type\": \"address\" },\n    { \"name\": \"signatures\", \"type\": \"bytes\" }\n  ],\n  \"name\": \"execTransaction\",\n  \"outputs\": [{ \"name\": \"success\", \"type\": \"bool\" }],\n  \"type\": \"function\"\n}] as const\n\nconst executeTransaction = async (\n  chainId: bigint,\n  safeAddress: string,\n  params: ExecuteTransactionParams,\n  account: `0x${string}`\n) =\u003e {\n  // 1. Create Viem clients\n  const publicClient = createPublicClient({\n    chain: mainnet,\n    transport: http()\n  })\n\n  const walletClient = createWalletClient({\n    chain: mainnet,\n    transport: http()\n  })\n\n  // 2. Prepare transaction\n  const { request } = await publicClient.simulateContract({\n    address: safeAddress as `0x${string}`,\n    abi: safeAbi,\n    functionName: 'execTransaction',\n    args: [\n      params.to as `0x${string}`,\n      params.value,\n      params.data as `0x${string}`,\n      params.operation,\n      params.safeTxGas,\n      params.baseGas,\n      params.gasPrice,\n      params.gasToken as `0x${string}`,\n      params.refundReceiver as `0x${string}`,\n      params.signatures as `0x${string}`\n    ],\n    account\n  })\n\n  // 3. Execute transaction\n  const hash = await walletClient.writeContract(request)\n  \n  // 4. Wait for transaction\n  const receipt = await publicClient.waitForTransactionReceipt({ hash })\n  \n  return receipt\n}\n\n// Helper function to combine signatures\nconst combineSignatures = (signatures: Array\u003c{ signer: string, signature: string }\u003e) =\u003e {\n  // Sort signatures by signer address (required by the Safe contract)\n  const sortedSigs = signatures.sort((a, b) =\u003e \n    a.signer.toLowerCase().localeCompare(b.signer.toLowerCase())\n  )\n  \n  // Concatenate signatures\n  return sortedSigs.map(sig =\u003e sig.signature.slice(2)).join('')\n}\n\n// Complete execution flow\nconst completeExecutionFlow = async (\n  chainId: bigint,\n  safeAddress: string,\n  safeTxHash: Hash,\n  executorAddress: `0x${string}`\n) =\u003e {\n  try {\n    // 1. Get transaction details from Safe Client API\n    const response = await fetch(\n      `https://safe-client.safe.global/v1/chains/${chainId}/transactions/${safeTxHash}`\n    )\n    \n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`)\n    }\n    \n    const txInfo = await response.json()\n    \n    // 2. Verify enough signatures\n    if (txInfo.confirmationsSubmitted \u003c txInfo.confirmationsRequired) {\n      throw new Error('Not enough signatures to execute transaction')\n    }\n    \n    // 3. Combine signatures\n    const signatures = `0x${combineSignatures(txInfo.signatures)}`\n    \n    // 4. Execute transaction\n    const receipt = await executeTransaction(\n      chainId,\n      safeAddress,\n      {\n        ...txInfo.transactionData,\n        signatures\n      },\n      executorAddress\n    )\n    \n    return receipt\n  } catch (error) {\n    console.error('Error executing transaction:', error)\n    throw error\n  }\n}\n\n// Example usage\nconst example = async () =\u003e {\n  const chainId = BigInt(1) // Ethereum mainnet\n  const safeAddress = '0xYourSafeAddress...'\n  const safeTxHash = '0xYourSafeTxHash...'\n  const executorAddress = '0xYourAddress...' // Can be any address\n\n  try {\n    const receipt = await completeExecutionFlow(\n      chainId,\n      safeAddress,\n      safeTxHash as Hash,\n      executorAddress as `0x${string}`\n    )\n    \n    console.log('Transaction executed:', receipt.transactionHash)\n  } catch (error) {\n    console.error('Failed to execute transaction:', error)\n  }\n}\n```\n\n### Understanding the Execution Process\n\n1. **Signature Requirements**\n   - All required signatures must be collected before execution\n   - All signatures must be of type eth_\n   - Signatures must be sorted by signer address (ascending order)\n   - Anyone can execute the transaction once signatures are collected\n\n2. **Gas Considerations**\n   - The executor pays for the gas of the execution transaction\n   - If `gasToken` is not zero address, the executor can be refunded in ERC20 tokens\n   - `safeTxGas` is the gas limit for the internal transaction\n\n3. **Execution Flow**\n   - Verify enough signatures are collected\n   - Combine signatures in correct order\n   - Submit transaction to the Safe contract\n   - Wait for transaction confirmation\n\n4. **Response**\n   - The execution returns a transaction receipt\n   - Success can be verified through transaction status and events\n   - The Safe Client API will eventually reflect the executed status\n\n### Monitoring Execution Status\n\nYou can monitor the execution status through the Safe Client API:\n\n```typescript\nconst monitorExecutionStatus = async (\n  chainId: string,\n  safeTxHash: Hash,\n  maxAttempts = 10,\n  intervalMs = 5000\n) =\u003e {\n  for (let attempt = 0; attempt \u003c maxAttempts; attempt++) {\n    const response = await fetch(\n      `https://safe-client.safe.global/v1/chains/${chainId}/transactions/${safeTxHash}`\n    )\n    \n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`)\n    }\n    \n    const txInfo = await response.json()\n    \n    if (txInfo.txStatus === 'SUCCESS') {\n      return txInfo\n    } else if (txInfo.txStatus === 'FAILED') {\n      throw new Error('Transaction execution failed')\n    }\n    \n    // Wait before next attempt\n    await new Promise(resolve =\u003e setTimeout(resolve, intervalMs))\n  }\n  \n  throw new Error('Timeout waiting for transaction execution')\n}\n```\n\nThis completes the full cycle of Safe transaction management:\n1. Proposing a transaction\n2. Computing the transaction hash\n3. Collecting signatures\n4. Executing the transaction\n5. Monitoring the execution status","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F5afe%2Fclient-gateway-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F5afe%2Fclient-gateway-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F5afe%2Fclient-gateway-demo/lists"}