From 20645222ca41f32a5b65d859c1f6ee5f2e0e3397 Mon Sep 17 00:00:00 2001 From: elisabethd Date: Sun, 30 Nov 2025 12:34:31 +0000 Subject: [PATCH 1/2] WIP: Gas sponsorship Python example - signature validation issue - Implemented gas-sponsored Earn Account funding and position management - Python version has 'Invalid signature' error that needs debugging - TypeScript version works correctly - Issue appears to be in EIP-712 signing/validation flow for Python SDK --- WORKFLOW.md | 104 ++++++++++ .../python/README.md | 61 ++++++ v2/gas_sponsored_earn_deposit/python/main.py | 73 +++++++ .../python/pyproject.toml | 11 + .../typescript/README.md | 61 ++++++ .../typescript/package.json | 26 +++ .../typescript/src/index.ts | 96 +++++++++ .../typescript/tsconfig.json | 17 ++ v2/gas_sponsorship/python/.env.example | 5 + v2/gas_sponsorship/python/README.md | 93 +++++++++ .../PKG-INFO | 8 + .../SOURCES.txt | 8 + .../dependency_links.txt | 1 + .../requires.txt | 4 + .../top_level.txt | 1 + v2/gas_sponsorship/python/main.py | 161 +++++++++++++++ v2/gas_sponsorship/python/pyproject.toml | 11 + v2/gas_sponsorship/typescript/.env.example | 5 + v2/gas_sponsorship/typescript/README.md | 77 +++++++ v2/gas_sponsorship/typescript/package.json | 26 +++ v2/gas_sponsorship/typescript/src/index.ts | 191 ++++++++++++++++++ v2/gas_sponsorship/typescript/tsconfig.json | 17 ++ 22 files changed, 1057 insertions(+) create mode 100644 WORKFLOW.md create mode 100644 v2/gas_sponsored_earn_deposit/python/README.md create mode 100644 v2/gas_sponsored_earn_deposit/python/main.py create mode 100644 v2/gas_sponsored_earn_deposit/python/pyproject.toml create mode 100644 v2/gas_sponsored_earn_deposit/typescript/README.md create mode 100644 v2/gas_sponsored_earn_deposit/typescript/package.json create mode 100644 v2/gas_sponsored_earn_deposit/typescript/src/index.ts create mode 100644 v2/gas_sponsored_earn_deposit/typescript/tsconfig.json create mode 100644 v2/gas_sponsorship/python/.env.example create mode 100644 v2/gas_sponsorship/python/README.md create mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/PKG-INFO create mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/SOURCES.txt create mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/dependency_links.txt create mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/requires.txt create mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/top_level.txt create mode 100644 v2/gas_sponsorship/python/main.py create mode 100644 v2/gas_sponsorship/python/pyproject.toml create mode 100644 v2/gas_sponsorship/typescript/.env.example create mode 100644 v2/gas_sponsorship/typescript/README.md create mode 100644 v2/gas_sponsorship/typescript/package.json create mode 100644 v2/gas_sponsorship/typescript/src/index.ts create mode 100644 v2/gas_sponsorship/typescript/tsconfig.json diff --git a/WORKFLOW.md b/WORKFLOW.md new file mode 100644 index 00000000..59b133a6 --- /dev/null +++ b/WORKFLOW.md @@ -0,0 +1,104 @@ +# Workflow: Working with api_usecases in mono repo + +This folder contains a clone of the public [CompassLabs/api_usecases](https://github.com/CompassLabs/api_usecases) repository. + +## Setup + +The `api_usecases` folder is: +- ✅ Cloned from the public repo +- ✅ Has its own git history (separate from mono repo) +- ✅ Ignored by mono repo's `.gitignore` (won't be committed to mono) +- ✅ Can be worked on independently + +## Workflow: Adding New Use Cases + +### 1. Create a Branch in api_usecases + +```bash +cd api_usecases +git checkout -b feature/new-use-case +``` + +### 2. Create Your Use Case + +Add your code following the existing structure: + +``` +api_usecases/ + v2/ + your_use_case/ + typescript/ + src/ + index.ts + package.json + python/ + main.py + pyproject.toml +``` + +**Important:** Add snippet markers in your code: + +**TypeScript:** +```typescript +// SNIPPET START 1 +import { CompassClient } from '@compass-labs/api-sdk'; +// ... your code ... +// SNIPPET END 1 +``` + +**Python:** +```python +# SNIPPET START 1 +from compass_api_sdk import CompassClient +# ... your code ... +# SNIPPET END 1 +``` + +### 3. Test Your Code + +Make sure your use case works before proceeding. + +### 4. Commit and Push to Public Repo + +```bash +cd api_usecases +git add . +git commit -m "Add new use case: your-use-case" +git push origin feature/new-use-case +``` + +Then create a PR in the public repo: https://github.com/CompassLabs/api_usecases + +### 5. Create Docs Page in mono Repo + +Once your code is merged to `main` in the public repo: + +1. Create a new `.mdx` file in `api_docs/v2/examples/your-use-case.mdx` +2. Use `GithubCodeBlock` to reference your code (see `api_docs/USE_CASES_GUIDE.md`) +3. Add the page to `api_docs/docs.json` navigation +4. Commit and push to mono repo + +## Updating api_usecases + +To pull latest changes from the public repo: + +```bash +cd api_usecases +git checkout main +git pull origin main +``` + +## Current Structure + +- `v0/` - Version 0 examples +- `v1/` - Version 1 examples (basic_examples, pendle, aave_looping, transaction_bundler) +- `v2/` - Version 2 examples +- `wallet-earn/` - Wallet earn demo + +## Notes + +- The `api_usecases` folder is **not** tracked by the mono repo +- All changes to use cases should be committed to the public repo +- Docs pages in `api_docs/` reference code via GitHub raw URLs +- Always use snippet markers for code sections you want to show in docs + diff --git a/v2/gas_sponsored_earn_deposit/python/README.md b/v2/gas_sponsored_earn_deposit/python/README.md new file mode 100644 index 00000000..a821fc78 --- /dev/null +++ b/v2/gas_sponsored_earn_deposit/python/README.md @@ -0,0 +1,61 @@ +# Gas-Sponsored Earn Deposit - Python Example + +This example demonstrates how to deposit into a Morpho vault with gas sponsorship using the Compass API Python SDK. + +## Prerequisites + +- Python 3.8+ installed +- A Compass API key ([Get one here](https://auth-compasslabs-ai.auth.eu-west-2.amazoncognito.com/login?client_id=2l366l2b3dok7k71nbnu8r1u36&redirect_uri=https://api.compasslabs.ai/auth/callback&response_type=code&scope=openid+email+profile)) +- An existing Earn Account with sufficient USDC balance +- Two wallet addresses: + - `owner`: The wallet that owns the Earn Account (signs the EIP-712 typed data) + - `sender`: The wallet that pays for gas (signs and broadcasts the transaction) + +## Setup + +1. Install dependencies: +```bash +pip install -e . +``` + +Or install manually: +```bash +pip install compass-api-sdk python-dotenv web3 eth-account +``` + +2. Copy the example environment file: +```bash +cp .env.example .env +``` + +3. Fill in your `.env` file with your actual values: + - `COMPASS_API_KEY`: Your Compass API key + - `WALLET_ADDRESS`: Your wallet address (owner of the Earn Account) + - `OWNER_PRIVATE_KEY`: Owner's private key (to sign EIP-712 typed data) + - `SENDER_PRIVATE_KEY`: Sender's private key (to sign and broadcast the transaction) + - `BASE_RPC_URL`: Your Base mainnet RPC URL + +## Run + +```bash +python main.py +``` + +## What This Does + +This example demonstrates the 3-step gas sponsorship flow: + +1. **Get EIP-712 typed data**: Calls `/v2/earn/manage` with `gas_sponsorship: True` to get EIP-712 typed data +2. **Sign typed data**: The `owner` signs the EIP-712 typed data off-chain (no gas required) +3. **Prepare and execute**: Calls `/v2/gas_sponsorship/prepare` with the signature, then the `sender` signs and broadcasts the transaction (sender pays gas) + +## Notes + +- This example deposits 0.5 USDC into the Steakhouse USDC vault on Morpho (`0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183`) +- The `owner` must be the address that owns the Earn Account +- The `sender` can be any address that has ETH on Base to pay for gas +- The `owner` and `sender` can be the same address, but they serve different roles in the flow +- Make sure your Earn Account has sufficient USDC balance +- Make sure the `sender` wallet has enough ETH on Base to cover gas fees +- **Important**: Make sure you're using `compass-api-sdk` version 2.0.1 or later + diff --git a/v2/gas_sponsored_earn_deposit/python/main.py b/v2/gas_sponsored_earn_deposit/python/main.py new file mode 100644 index 00000000..1c190c5f --- /dev/null +++ b/v2/gas_sponsored_earn_deposit/python/main.py @@ -0,0 +1,73 @@ +# SNIPPET START 1 +from compass_api_sdk import CompassAPI, models +import os +from dotenv import load_dotenv +from web3 import Web3 +from eth_account import Account +from eth_account.messages import encode_typed_data + +load_dotenv() + +COMPASS_API_KEY = os.getenv("COMPASS_API_KEY") +WALLET_ADDRESS = os.getenv("WALLET_ADDRESS") +OWNER_PRIVATE_KEY = os.getenv("OWNER_PRIVATE_KEY") +SENDER_PRIVATE_KEY = os.getenv("SENDER_PRIVATE_KEY") +BASE_RPC_URL = os.getenv("BASE_RPC_URL") +# SNIPPET END 1 + +# SNIPPET START 2 +with CompassAPI(api_key_auth=COMPASS_API_KEY) as compass_api: +# SNIPPET END 2 + +# SNIPPET START 3 + # Get EIP-712 typed data for gas-sponsored deposit + manage_response = compass_api.earn.earn_manage( + owner=WALLET_ADDRESS, + chain=models.Chain.BASE, + venue={ + "type": "VAULT", + "vault_address": "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", + }, + action=models.EarnManageRequestAction.DEPOSIT, + amount="0.5", + gas_sponsorship=True, + fee=None, + ) +# SNIPPET END 3 + +# SNIPPET START 4 + # Sign EIP-712 typed data with owner's private key + owner_account = Account.from_key(OWNER_PRIVATE_KEY if OWNER_PRIVATE_KEY.startswith("0x") else f"0x{OWNER_PRIVATE_KEY}") + eip712 = manage_response.eip712 + encoded_data = encode_typed_data(full_message=eip712) + signed_message = owner_account.sign_message(encoded_data) + signature = f"0x{signed_message.signature.hex()}" +# SNIPPET END 4 + +# SNIPPET START 5 + # Prepare gas-sponsored transaction + sender_account = Account.from_key(SENDER_PRIVATE_KEY if SENDER_PRIVATE_KEY.startswith("0x") else f"0x{SENDER_PRIVATE_KEY}") + prepare_response = compass_api.gas_sponsorship.gas_sponsorship_prepare( + owner=WALLET_ADDRESS, + chain=models.Chain.BASE, + eip712=eip712, + signature=signature, + sender=sender_account.address, + ) +# SNIPPET END 5 + +# SNIPPET START 6 + # Sign and broadcast transaction with sender's private key + w3 = Web3(Web3.HTTPProvider(BASE_RPC_URL)) + tx_dict = prepare_response.model_dump(by_alias=True) + signed_tx = w3.eth.account.sign_transaction(tx_dict["transaction"], SENDER_PRIVATE_KEY) + tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) + + print(f"Transaction hash: {tx_hash.hex()}") + print(f"View on BaseScan: https://basescan.org/tx/{tx_hash.hex()}") + + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + print(f"Transaction confirmed in block: {receipt.blockNumber}") + print("Gas-sponsored deposit transaction confirmed") +# SNIPPET END 6 + diff --git a/v2/gas_sponsored_earn_deposit/python/pyproject.toml b/v2/gas_sponsored_earn_deposit/python/pyproject.toml new file mode 100644 index 00000000..8f11379d --- /dev/null +++ b/v2/gas_sponsored_earn_deposit/python/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "gas_sponsored_earn_deposit_python_example" +version = "1.0.0" +description = "Example: Gas-Sponsored Earn Deposit using Compass API" +dependencies = [ + "compass-api-sdk", + "python-dotenv", + "web3", + "eth-account", +] + diff --git a/v2/gas_sponsored_earn_deposit/typescript/README.md b/v2/gas_sponsored_earn_deposit/typescript/README.md new file mode 100644 index 00000000..9d2b3dbe --- /dev/null +++ b/v2/gas_sponsored_earn_deposit/typescript/README.md @@ -0,0 +1,61 @@ +# Gas-Sponsored Earn Deposit - TypeScript Example + +This example demonstrates how to deposit into a Morpho vault with gas sponsorship using the Compass API TypeScript SDK. + +## Prerequisites + +- Node.js 18+ installed +- A Compass API key ([Get one here](https://auth-compasslabs-ai.auth.eu-west-2.amazoncognito.com/login?client_id=2l366l2b3dok7k71nbnu8r1u36&redirect_uri=https://api.compasslabs.ai/auth/callback&response_type=code&scope=openid+email+profile)) +- An existing Earn Account with sufficient USDC balance +- Two wallet addresses: + - `owner`: The wallet that owns the Earn Account (signs the EIP-712 typed data) + - `sender`: The wallet that pays for gas (signs and broadcasts the transaction) + +## Setup + +1. Install dependencies: +```bash +npm install +``` + +2. Copy the example environment file: +```bash +cp .env.example .env +``` + +3. Fill in your `.env` file with your actual values: + - `COMPASS_API_KEY`: Your Compass API key + - `WALLET_ADDRESS`: Your wallet address (owner of the Earn Account) + - `OWNER_PRIVATE_KEY`: Owner's private key (to sign EIP-712 typed data) + - `SENDER_PRIVATE_KEY`: Sender's private key (to sign and broadcast the transaction) + - `BASE_RPC_URL`: Your Base mainnet RPC URL + +## Run + +```bash +npm run dev +``` + +Or build and run: +```bash +npm run build +npm start +``` + +## What This Does + +This example demonstrates the 3-step gas sponsorship flow: + +1. **Get EIP-712 typed data**: Calls `/v2/earn/manage` with `gasSponsorship: true` to get EIP-712 typed data +2. **Sign typed data**: The `owner` signs the EIP-712 typed data off-chain (no gas required) +3. **Prepare and execute**: Calls `/v2/gas_sponsorship/prepare` with the signature, then the `sender` signs and broadcasts the transaction (sender pays gas) + +## Notes + +- This example deposits 0.5 USDC into the Steakhouse USDC vault on Morpho (`0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183`) +- The `owner` must be the address that owns the Earn Account +- The `sender` can be any address that has ETH on Base to pay for gas +- The `owner` and `sender` can be the same address, but they serve different roles in the flow +- Make sure your Earn Account has sufficient USDC balance +- Make sure the `sender` wallet has enough ETH on Base to cover gas fees + diff --git a/v2/gas_sponsored_earn_deposit/typescript/package.json b/v2/gas_sponsored_earn_deposit/typescript/package.json new file mode 100644 index 00000000..3f2a7754 --- /dev/null +++ b/v2/gas_sponsored_earn_deposit/typescript/package.json @@ -0,0 +1,26 @@ +{ + "name": "gas_sponsored_earn_deposit_typescript_example", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "scripts": { + "build": "tsc", + "start": "tsc && node dist/index.js", + "dev": "ts-node --esm src/index.ts" + }, + "author": "", + "license": "ISC", + "description": "Example: Gas-Sponsored Earn Deposit using Compass API", + "dependencies": { + "@compass-labs/api-sdk": "^1.3.5", + "dotenv": "^16.5.0", + "viem": "^2.31.0" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "prettier": "^3.6.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} + diff --git a/v2/gas_sponsored_earn_deposit/typescript/src/index.ts b/v2/gas_sponsored_earn_deposit/typescript/src/index.ts new file mode 100644 index 00000000..b4a06be0 --- /dev/null +++ b/v2/gas_sponsored_earn_deposit/typescript/src/index.ts @@ -0,0 +1,96 @@ +// SNIPPET START 1 +import { CompassApiSDK } from "@compass-labs/api-sdk"; +import dotenv from "dotenv"; +import { privateKeyToAccount } from "viem/accounts"; +import { base } from "viem/chains"; +import { http, createWalletClient, createPublicClient, signTypedData } from "viem"; + +dotenv.config(); + +const COMPASS_API_KEY = process.env.COMPASS_API_KEY as string; +const WALLET_ADDRESS = process.env.WALLET_ADDRESS as `0x${string}`; +const OWNER_PRIVATE_KEY = (process.env.OWNER_PRIVATE_KEY?.startsWith("0x") + ? process.env.OWNER_PRIVATE_KEY + : `0x${process.env.OWNER_PRIVATE_KEY}`) as `0x${string}`; +const SENDER_PRIVATE_KEY = (process.env.SENDER_PRIVATE_KEY?.startsWith("0x") + ? process.env.SENDER_PRIVATE_KEY + : `0x${process.env.SENDER_PRIVATE_KEY}`) as `0x${string}`; +const BASE_RPC_URL = process.env.BASE_RPC_URL as string; +// SNIPPET END 1 + +// SNIPPET START 2 +const compass = new CompassApiSDK({ + apiKeyAuth: COMPASS_API_KEY, +}); +// SNIPPET END 2 + +// SNIPPET START 3 +// Get EIP-712 typed data for gas-sponsored deposit +const manageResponse = await compass.earn.earnManage({ + owner: WALLET_ADDRESS, + chain: "base", + venue: { + type: "VAULT", + vaultAddress: "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", + }, + action: "DEPOSIT", + amount: "0.5", + gasSponsorship: true, + fee: null, +}); +// SNIPPET END 3 + +// SNIPPET START 4 +// Sign EIP-712 typed data with owner's private key +const ownerAccount = privateKeyToAccount(OWNER_PRIVATE_KEY); +const eip712 = manageResponse.eip712!; +const signature = await signTypedData({ + account: ownerAccount, + domain: eip712.domain as any, + types: eip712.types as any, + primaryType: eip712.primaryType, + message: eip712.message as any, +}); +// SNIPPET END 4 + +// SNIPPET START 5 +// Prepare gas-sponsored transaction +const senderAccount = privateKeyToAccount(SENDER_PRIVATE_KEY); +const prepareResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ + owner: WALLET_ADDRESS, + chain: "base", + eip712: eip712, + signature: signature, + sender: senderAccount.address, +}); +// SNIPPET END 5 + +// SNIPPET START 6 +// Sign and broadcast transaction with sender's private key +const walletClient = createWalletClient({ + account: senderAccount, + chain: base, + transport: http(BASE_RPC_URL), +}); +const publicClient = createPublicClient({ + chain: base, + transport: http(BASE_RPC_URL), +}); + +const transaction = prepareResponse.transaction as any; +const txHash = await walletClient.sendTransaction({ + ...transaction, + value: BigInt(transaction.value || 0), + gas: BigInt(transaction.gas), + maxFeePerGas: BigInt(transaction.maxFeePerGas), + maxPriorityFeePerGas: BigInt(transaction.maxPriorityFeePerGas), +}); + +console.log(`Transaction hash: ${txHash}`); +console.log(`View on BaseScan: https://basescan.org/tx/${txHash}`); + +const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); +console.log(`Transaction confirmed in block: ${receipt.blockNumber}`); +console.log("Gas-sponsored deposit transaction confirmed"); +// SNIPPET END 6 + diff --git a/v2/gas_sponsored_earn_deposit/typescript/tsconfig.json b/v2/gas_sponsored_earn_deposit/typescript/tsconfig.json new file mode 100644 index 00000000..d127c035 --- /dev/null +++ b/v2/gas_sponsored_earn_deposit/typescript/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "ESNext", + "lib": ["ES2020"], + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} + diff --git a/v2/gas_sponsorship/python/.env.example b/v2/gas_sponsorship/python/.env.example new file mode 100644 index 00000000..0caf4124 --- /dev/null +++ b/v2/gas_sponsorship/python/.env.example @@ -0,0 +1,5 @@ +COMPASS_API_KEY=your_api_key_here +WALLET_ADDRESS=0xYourOwnerWalletAddress +OWNER_PRIVATE_KEY=your_owner_private_key_here +SENDER_PRIVATE_KEY=your_sender_private_key_here +BASE_RPC_URL=https://mainnet.base.org diff --git a/v2/gas_sponsorship/python/README.md b/v2/gas_sponsorship/python/README.md new file mode 100644 index 00000000..f78c5fba --- /dev/null +++ b/v2/gas_sponsorship/python/README.md @@ -0,0 +1,93 @@ +# Gas Sponsorship - Python Example + +This example demonstrates two gas sponsorship use cases using the Compass API Python SDK: + +1. **Fund Earn Account with Gas Sponsorship**: Approve and transfer tokens from your wallet to your Earn Account, with gas paid by a sponsor +2. **Manage Earn Position with Gas Sponsorship**: Deposit into a Morpho vault from your Earn Account, with gas paid by a sponsor + +## Prerequisites + +- Python 3.8+ installed +- A Compass API key ([Get one here](https://auth-compasslabs-ai.auth.eu-west-2.amazoncognito.com/login?client_id=2l366l2b3dok7k71nbnu8r1u36&redirect_uri=https://api.compasslabs.ai/auth/callback&response_type=code&scope=openid+email+profile)) +- Two wallet addresses: + - `owner`: The wallet that owns the Earn Account (signs EIP-712 typed data off-chain) + - `sender`: The wallet that pays for gas (signs and broadcasts transactions) + +## Setup + +1. Install dependencies: +```bash +pip install -e . +``` + +Or install manually: +```bash +pip install compass-api-sdk python-dotenv web3 eth-account +``` + +2. Copy the example environment file: +```bash +cp .env.example .env +``` + +3. Fill in your `.env` file with your actual values: + - `COMPASS_API_KEY`: Your Compass API key + - `WALLET_ADDRESS`: Your wallet address (owner of the Earn Account) + - `OWNER_PRIVATE_KEY`: Owner's private key (to sign EIP-712 typed data) + - `SENDER_PRIVATE_KEY`: Sender's private key (to sign and broadcast transactions) + - `BASE_RPC_URL`: Your Base mainnet RPC URL + +## Run + +```bash +python main.py +``` + +## What This Does + +### Example 1: Fund Earn Account with Gas Sponsorship + +This demonstrates the 4-step flow to fund an Earn Account with gas sponsorship: + +1. **Get EIP-712 typed data**: Calls `/v2/gas_sponsorship/approve_transfer` with `gas_sponsorship: True` to get Permit2 approval typed data +2. **Sign typed data**: The `owner` signs the EIP-712 typed data off-chain (no gas required) +3. **Prepare transaction**: Calls `/v2/gas_sponsorship/prepare` with the signature to get the approval transaction +4. **Execute**: The `sender` signs and broadcasts the transaction (sender pays gas) + +After this, the Earn Account can be funded using `/v2/earn/transfer` with gas sponsorship enabled. + +### Example 2: Manage Earn Position with Gas Sponsorship + +This demonstrates the 4-step flow to deposit into a vault with gas sponsorship: + +1. **Get EIP-712 typed data**: Calls `/v2/earn/manage` with `gas_sponsorship: True` to get deposit typed data +2. **Sign typed data**: The `owner` signs the EIP-712 typed data off-chain (no gas required) +3. **Prepare transaction**: Calls `/v2/gas_sponsorship/prepare` with the signature to get the deposit transaction +4. **Execute**: The `sender` signs and broadcasts the transaction (sender pays gas) + +## Notes + +- The `owner` must be the address that owns the Earn Account +- The `sender` can be any address that has ETH on Base to pay for gas +- The `owner` and `sender` can be the same address, but they serve different roles in the flow +- Example 2 deposits 0.5 USDC into the Steakhouse USDC vault on Morpho (`0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183`) +- Make sure your Earn Account has sufficient USDC balance for deposits +- Make sure the `sender` wallet has enough ETH on Base to cover gas fees +- **Important**: Make sure you're using `compass-api-sdk` version 2.0.1 or later + +## Known Issues + +**Example 2 (Manage Earn Position) - Signature Validation Error:** + +There is a known issue with the Python SDK's EIP-712 structure handling for `BatchedSafeOperationsResponse` (SafeTx). When calling `/v2/gas_sponsorship/prepare` with EIP-712 data from `/v2/earn/manage`, the signature validation may fail with "Invalid signature" error. + +**Workaround:** +- The TypeScript version works correctly. Please use the TypeScript example for managing Earn positions with gas sponsorship. +- Example 1 (Fund Earn Account) works correctly in Python. + +**Root Cause:** +The Python SDK requires a dict format for EIP-712 data, but there's a structure mismatch between what we sign (for `eth_account` compatibility) and what the backend validates. The TypeScript SDK handles this correctly by passing the model object directly. + +**Status:** +This is a known limitation. Please contact the API team about Python SDK's EIP-712 serialization for `BatchedSafeOperationsResponse`. + diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/PKG-INFO b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/PKG-INFO new file mode 100644 index 00000000..e90faa64 --- /dev/null +++ b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/PKG-INFO @@ -0,0 +1,8 @@ +Metadata-Version: 2.4 +Name: gas_sponsorship_python_example +Version: 1.0.0 +Summary: Example: Gas Sponsorship for Earn Account Funding and Position Management using Compass API +Requires-Dist: compass-api-sdk +Requires-Dist: python-dotenv +Requires-Dist: web3 +Requires-Dist: eth-account diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/SOURCES.txt b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/SOURCES.txt new file mode 100644 index 00000000..3f7c1ae9 --- /dev/null +++ b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/SOURCES.txt @@ -0,0 +1,8 @@ +README.md +main.py +pyproject.toml +gas_sponsorship_python_example.egg-info/PKG-INFO +gas_sponsorship_python_example.egg-info/SOURCES.txt +gas_sponsorship_python_example.egg-info/dependency_links.txt +gas_sponsorship_python_example.egg-info/requires.txt +gas_sponsorship_python_example.egg-info/top_level.txt \ No newline at end of file diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/dependency_links.txt b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/requires.txt b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/requires.txt new file mode 100644 index 00000000..dea15ae1 --- /dev/null +++ b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/requires.txt @@ -0,0 +1,4 @@ +compass-api-sdk +python-dotenv +web3 +eth-account diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/top_level.txt b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/top_level.txt new file mode 100644 index 00000000..ba2906d0 --- /dev/null +++ b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/top_level.txt @@ -0,0 +1 @@ +main diff --git a/v2/gas_sponsorship/python/main.py b/v2/gas_sponsorship/python/main.py new file mode 100644 index 00000000..c2dbb9ce --- /dev/null +++ b/v2/gas_sponsorship/python/main.py @@ -0,0 +1,161 @@ +# SNIPPET START 1 +from compass_api_sdk import CompassAPI, models +import os +from dotenv import load_dotenv +from web3 import Web3 +from eth_account import Account +from eth_account.messages import encode_typed_data + +load_dotenv() + +COMPASS_API_KEY = os.getenv("COMPASS_API_KEY") +WALLET_ADDRESS = os.getenv("WALLET_ADDRESS") +OWNER_PRIVATE_KEY = os.getenv("OWNER_PRIVATE_KEY") +SENDER_PRIVATE_KEY = os.getenv("SENDER_PRIVATE_KEY") +BASE_RPC_URL = os.getenv("BASE_RPC_URL") +# SNIPPET END 1 + +# SNIPPET START 2 +with CompassAPI(api_key_auth=COMPASS_API_KEY) as compass_api: +# SNIPPET END 2 + +# ============================================================================ +# EXAMPLE 1: Fund Earn Account with Gas Sponsorship +# ============================================================================ + +# SNIPPET START 3 + # Step 1: Get EIP-712 typed data for Permit2 approval (gas-sponsored) + # Returns EIP-712 typed data that must be signed by the owner off-chain + # Note: If allowance is already set, this will raise an error - you can skip to Example 2 + try: + approve_response = compass_api.gas_sponsorship.gas_sponsorship_approve_transfer( + owner=WALLET_ADDRESS, + chain=models.Chain.BASE, + token="USDC", + gas_sponsorship=True, + ) + except Exception as e: + if "allowance already set" in str(e).lower(): + print("Permit2 approval already exists - skipping to Example 2 (Manage Position)") + # Skip Example 1 and go to Example 2 + approve_response = None + else: + raise +# SNIPPET END 3 + +# SNIPPET START 4 + # Step 2: Sign EIP-712 typed data with owner's private key (off-chain, no gas) + # This signature from Step 2 is required as input for Step 3 + owner_account = Account.from_key(OWNER_PRIVATE_KEY if OWNER_PRIVATE_KEY.startswith("0x") else f"0x{OWNER_PRIVATE_KEY}") + if approve_response and approve_response.eip_712: + approve_eip712 = approve_response.eip_712 + encoded_data = encode_typed_data(full_message=approve_eip712) + signed_message = owner_account.sign_message(encoded_data) + approve_signature = f"0x{signed_message.signature.hex()}" + else: + print("Skipping Example 1 - Permit2 approval already exists") + approve_eip712 = None + approve_signature = None +# SNIPPET END 4 + +# SNIPPET START 5 + # Step 3: Prepare gas-sponsored Permit2 approval transaction + # Uses the signature from Step 2 as input. The sender will pay for gas to execute the Permit2 approval + sender_account = Account.from_key(SENDER_PRIVATE_KEY if SENDER_PRIVATE_KEY.startswith("0x") else f"0x{SENDER_PRIVATE_KEY}") + if approve_eip712 and approve_signature: + prepare_approve_response = compass_api.gas_sponsorship.gas_sponsorship_prepare( + owner=WALLET_ADDRESS, + chain=models.Chain.BASE, + eip_712=approve_eip712.model_dump(by_alias=True), # Convert to dict + signature=approve_signature, # Signature from Step 2 + sender=sender_account.address, + ) + else: + prepare_approve_response = None +# SNIPPET END 5 + +# SNIPPET START 6 + # Step 4: Sign and broadcast Permit2 approval transaction with sender's private key + if prepare_approve_response: + w3 = Web3(Web3.HTTPProvider(BASE_RPC_URL)) + approve_tx_dict = prepare_approve_response.model_dump(by_alias=True) + signed_approve_tx = w3.eth.account.sign_transaction(approve_tx_dict["transaction"], SENDER_PRIVATE_KEY) + approve_tx_hash = w3.eth.send_raw_transaction(signed_approve_tx.rawTransaction) + + print(f"Permit2 approval transaction hash: {approve_tx_hash.hex()}") + print(f"View on BaseScan: https://basescan.org/tx/{approve_tx_hash.hex()}") + + approve_receipt = w3.eth.wait_for_transaction_receipt(approve_tx_hash) + print(f"Permit2 approval confirmed in block: {approve_receipt.blockNumber}") + print("Earn Account can now be funded with gas sponsorship") + else: + print("Skipping Example 1 transaction - Permit2 approval already exists") +# SNIPPET END 6 + +# ============================================================================ +# EXAMPLE 2: Manage Earn Position (Deposit) with Gas Sponsorship +# ============================================================================ + +# SNIPPET START 7 + # Step 1: Get EIP-712 typed data for gas-sponsored deposit + # Returns EIP-712 typed data that must be signed by the owner off-chain + manage_response = compass_api.earn.earn_manage( + owner=WALLET_ADDRESS, + chain=models.Chain.BASE, + venue={ + "type": "VAULT", + "vault_address": "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", + }, + action=models.EarnManageRequestAction.DEPOSIT, + amount="0.5", + gas_sponsorship=True, + fee=None, + ) +# SNIPPET END 7 + +# SNIPPET START 8 + # Step 2: Sign EIP-712 typed data with owner's private key (off-chain, no gas) + # This signature from Step 2 is required as input for Step 3 + # Test code pattern: signs with deposit_tx['eip_712'] which is dict from response.json() (with ints) + # Sign with the dict structure (ints) - exactly like the test code does + manage_eip712_output = manage_response.eip_712 + # Get dict format (like response.json()) - with ints, not enums + eip712_dict = manage_eip712_output.model_dump(by_alias=True, mode="json") + import json + eip712_dict = json.loads(json.dumps(eip712_dict, default=str)) + # Sign with dict structure (ints) - matches test code pattern + encoded_manage_data = encode_typed_data(full_message=eip712_dict) + signed_manage_message = owner_account.sign_message(encoded_manage_data) + manage_signature = f"0x{signed_manage_message.signature.hex()}" +# SNIPPET END 8 + +# SNIPPET START 9 + # Step 3: Prepare gas-sponsored deposit transaction + # Uses the signature from Step 2 as input. The sender will pay for gas to execute the deposit + # SDK expects Input model, not Output model - convert dict to Input model + # FastAPI converts Input model to BatchedSafeOperationsResponse model (same as what we signed with) + # Backend validates with model_dump() - matches what we signed with + eip712_input = models.BatchedSafeOperationsResponseInput(**eip712_dict) + prepare_manage_response = compass_api.gas_sponsorship.gas_sponsorship_prepare( + owner=WALLET_ADDRESS, + chain=models.Chain.BASE, + eip_712=eip712_input, # Pass Input model - SDK expects this + signature=manage_signature, # Signature from Step 2 (signed with backend model_dump() structure) + sender=sender_account.address, + ) +# SNIPPET END 9 + +# SNIPPET START 10 + # Step 4: Sign and broadcast deposit transaction with sender's private key + manage_tx_dict = prepare_manage_response.model_dump(by_alias=True) + signed_manage_tx = w3.eth.account.sign_transaction(manage_tx_dict["transaction"], SENDER_PRIVATE_KEY) + manage_tx_hash = w3.eth.send_raw_transaction(signed_manage_tx.rawTransaction) + + print(f"Deposit transaction hash: {manage_tx_hash.hex()}") + print(f"View on BaseScan: https://basescan.org/tx/{manage_tx_hash.hex()}") + + manage_receipt = w3.eth.wait_for_transaction_receipt(manage_tx_hash) + print(f"Deposit confirmed in block: {manage_receipt.blockNumber}") + print("Gas-sponsored deposit transaction confirmed") +# SNIPPET END 10 + diff --git a/v2/gas_sponsorship/python/pyproject.toml b/v2/gas_sponsorship/python/pyproject.toml new file mode 100644 index 00000000..05384095 --- /dev/null +++ b/v2/gas_sponsorship/python/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "gas_sponsorship_python_example" +version = "1.0.0" +description = "Example: Gas Sponsorship for Earn Account Funding and Position Management using Compass API" +dependencies = [ + "compass-api-sdk", + "python-dotenv", + "web3", + "eth-account", +] + diff --git a/v2/gas_sponsorship/typescript/.env.example b/v2/gas_sponsorship/typescript/.env.example new file mode 100644 index 00000000..0caf4124 --- /dev/null +++ b/v2/gas_sponsorship/typescript/.env.example @@ -0,0 +1,5 @@ +COMPASS_API_KEY=your_api_key_here +WALLET_ADDRESS=0xYourOwnerWalletAddress +OWNER_PRIVATE_KEY=your_owner_private_key_here +SENDER_PRIVATE_KEY=your_sender_private_key_here +BASE_RPC_URL=https://mainnet.base.org diff --git a/v2/gas_sponsorship/typescript/README.md b/v2/gas_sponsorship/typescript/README.md new file mode 100644 index 00000000..b2007599 --- /dev/null +++ b/v2/gas_sponsorship/typescript/README.md @@ -0,0 +1,77 @@ +# Gas Sponsorship - TypeScript Example + +This example demonstrates two gas sponsorship use cases using the Compass API TypeScript SDK: + +1. **Fund Earn Account with Gas Sponsorship**: Approve and transfer tokens from your wallet to your Earn Account, with gas paid by a sponsor +2. **Manage Earn Position with Gas Sponsorship**: Deposit into a Morpho vault from your Earn Account, with gas paid by a sponsor + +## Prerequisites + +- Node.js 18+ installed +- A Compass API key ([Get one here](https://auth-compasslabs-ai.auth.eu-west-2.amazoncognito.com/login?client_id=2l366l2b3dok7k71nbnu8r1u36&redirect_uri=https://api.compasslabs.ai/auth/callback&response_type=code&scope=openid+email+profile)) +- Two wallet addresses: + - `owner`: The wallet that owns the Earn Account (signs EIP-712 typed data off-chain) + - `sender`: The wallet that pays for gas (signs and broadcasts transactions) + +## Setup + +1. Install dependencies: +```bash +npm install +``` + +2. Copy the example environment file: +```bash +cp .env.example .env +``` + +3. Fill in your `.env` file with your actual values: + - `COMPASS_API_KEY`: Your Compass API key + - `WALLET_ADDRESS`: Your wallet address (owner of the Earn Account) + - `OWNER_PRIVATE_KEY`: Owner's private key (to sign EIP-712 typed data) + - `SENDER_PRIVATE_KEY`: Sender's private key (to sign and broadcast transactions) + - `BASE_RPC_URL`: Your Base mainnet RPC URL + +## Run + +```bash +npm run dev +``` + +Or build and run: +```bash +npm run build +npm start +``` + +## What This Does + +### Example 1: Fund Earn Account with Gas Sponsorship + +This demonstrates the 4-step flow to fund an Earn Account with gas sponsorship: + +1. **Get EIP-712 typed data**: Calls `/v2/gas_sponsorship/approve_transfer` with `gasSponsorship: true` to get Permit2 approval typed data +2. **Sign typed data**: The `owner` signs the EIP-712 typed data off-chain (no gas required) +3. **Prepare transaction**: Calls `/v2/gas_sponsorship/prepare` with the signature to get the approval transaction +4. **Execute**: The `sender` signs and broadcasts the transaction (sender pays gas) + +After this, the Earn Account can be funded using `/v2/earn/transfer` with gas sponsorship enabled. + +### Example 2: Manage Earn Position with Gas Sponsorship + +This demonstrates the 4-step flow to deposit into a vault with gas sponsorship: + +1. **Get EIP-712 typed data**: Calls `/v2/earn/manage` with `gasSponsorship: true` to get deposit typed data +2. **Sign typed data**: The `owner` signs the EIP-712 typed data off-chain (no gas required) +3. **Prepare transaction**: Calls `/v2/gas_sponsorship/prepare` with the signature to get the deposit transaction +4. **Execute**: The `sender` signs and broadcasts the transaction (sender pays gas) + +## Notes + +- The `owner` must be the address that owns the Earn Account +- The `sender` can be any address that has ETH on Base to pay for gas +- The `owner` and `sender` can be the same address, but they serve different roles in the flow +- Example 1 deposits into the Steakhouse USDC vault on Morpho (`0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183`) +- Make sure your Earn Account has sufficient USDC balance for deposits +- Make sure the `sender` wallet has enough ETH on Base to cover gas fees + diff --git a/v2/gas_sponsorship/typescript/package.json b/v2/gas_sponsorship/typescript/package.json new file mode 100644 index 00000000..2685933b --- /dev/null +++ b/v2/gas_sponsorship/typescript/package.json @@ -0,0 +1,26 @@ +{ + "name": "gas_sponsorship_typescript_example", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "scripts": { + "build": "tsc", + "start": "tsc && node dist/index.js", + "dev": "ts-node --esm src/index.ts" + }, + "author": "", + "license": "ISC", + "description": "Example: Gas Sponsorship for Earn Account Funding and Position Management using Compass API", + "dependencies": { + "@compass-labs/api-sdk": "^1.3.5", + "dotenv": "^16.5.0", + "viem": "^2.31.0" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "prettier": "^3.6.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} + diff --git a/v2/gas_sponsorship/typescript/src/index.ts b/v2/gas_sponsorship/typescript/src/index.ts new file mode 100644 index 00000000..cdcbc02b --- /dev/null +++ b/v2/gas_sponsorship/typescript/src/index.ts @@ -0,0 +1,191 @@ +// SNIPPET START 1 +import { CompassApiSDK } from "@compass-labs/api-sdk"; +import dotenv from "dotenv"; +import { privateKeyToAccount } from "viem/accounts"; +import { base } from "viem/chains"; +import { http, createWalletClient, createPublicClient } from "viem"; + +dotenv.config(); + +const COMPASS_API_KEY = process.env.COMPASS_API_KEY as string; +const WALLET_ADDRESS = process.env.WALLET_ADDRESS as `0x${string}`; +const OWNER_PRIVATE_KEY = (process.env.OWNER_PRIVATE_KEY?.startsWith("0x") + ? process.env.OWNER_PRIVATE_KEY + : `0x${process.env.OWNER_PRIVATE_KEY}`) as `0x${string}`; +const SENDER_PRIVATE_KEY = (process.env.SENDER_PRIVATE_KEY?.startsWith("0x") + ? process.env.SENDER_PRIVATE_KEY + : `0x${process.env.SENDER_PRIVATE_KEY}`) as `0x${string}`; +const BASE_RPC_URL = process.env.BASE_RPC_URL as string; +// SNIPPET END 1 + +// SNIPPET START 2 +const compass = new CompassApiSDK({ + apiKeyAuth: COMPASS_API_KEY, +}); +// SNIPPET END 2 + +// ============================================================================ +// EXAMPLE 1: Fund Earn Account with Gas Sponsorship +// ============================================================================ + +// SNIPPET START 3 +// Step 1: Get EIP-712 typed data for Permit2 approval (gas-sponsored) +// Returns EIP-712 typed data that must be signed by the owner off-chain +let approveResponse; +try { + approveResponse = await compass.gasSponsorship.gasSponsorshipApproveTransfer({ + owner: WALLET_ADDRESS, + chain: "base", + token: "USDC", + gasSponsorship: true, + }); +} catch (error: any) { + if (error.body?.includes("Token allowance already set")) { + console.log("Permit2 approval already exists - skipping to Example 2 (Manage Position)"); + approveResponse = null; + } else { + throw error; + } +} +// SNIPPET END 3 + +// SNIPPET START 4 +// Step 2: Sign EIP-712 typed data with owner's private key (off-chain, no gas) +// This signature from Step 2 is required as input for Step 3 +const ownerAccount = privateKeyToAccount(OWNER_PRIVATE_KEY); +const ownerWalletClient = createWalletClient({ + account: ownerAccount, + chain: base, + transport: http(BASE_RPC_URL), +}); +let approveEip712, approveSignature; +if (approveResponse && approveResponse.eip712) { + approveEip712 = approveResponse.eip712; + approveSignature = await ownerWalletClient.signTypedData({ + domain: approveEip712.domain as any, + types: approveEip712.types as any, + primaryType: approveEip712.primaryType as string, + message: approveEip712.message as any, + }); +} else { + console.log("Skipping Example 1 - Permit2 approval already exists"); + approveEip712 = null; + approveSignature = null; +} +// SNIPPET END 4 + +// SNIPPET START 5 +// Step 3: Prepare gas-sponsored Permit2 approval transaction +// Uses the signature from Step 2 as input. The sender will pay for gas to execute the Permit2 approval +const senderAccount = privateKeyToAccount(SENDER_PRIVATE_KEY); +let prepareApproveResponse; +if (approveEip712 && approveSignature) { + prepareApproveResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ + owner: WALLET_ADDRESS, + chain: "base", + eip712: approveEip712, + signature: approveSignature, // Signature from Step 2 + sender: senderAccount.address, + }); +} else { + prepareApproveResponse = null; +} +// SNIPPET END 5 + +// SNIPPET START 6 +// Step 4: Sign and broadcast Permit2 approval transaction with sender's private key +const walletClient = createWalletClient({ + account: senderAccount, + chain: base, + transport: http(BASE_RPC_URL), +}); +const publicClient = createPublicClient({ + chain: base, + transport: http(BASE_RPC_URL), +}); + +if (prepareApproveResponse) { + const approveTransaction = prepareApproveResponse.transaction as any; + const approveTxHash = await walletClient.sendTransaction({ + ...approveTransaction, + value: BigInt(approveTransaction.value || 0), + gas: BigInt(approveTransaction.gas), + maxFeePerGas: BigInt(approveTransaction.maxFeePerGas), + maxPriorityFeePerGas: BigInt(approveTransaction.maxPriorityFeePerGas), + }); + + console.log(`Permit2 approval transaction hash: ${approveTxHash}`); + console.log(`View on BaseScan: https://basescan.org/tx/${approveTxHash}`); + + const approveReceipt = await publicClient.waitForTransactionReceipt({ hash: approveTxHash }); + console.log(`Permit2 approval confirmed in block: ${approveReceipt.blockNumber}`); + console.log("Earn Account can now be funded with gas sponsorship"); +} else { + console.log("Skipping Example 1 transaction - Permit2 approval already exists"); +} +// SNIPPET END 6 + +// ============================================================================ +// EXAMPLE 2: Manage Earn Position (Deposit) with Gas Sponsorship +// ============================================================================ + +// SNIPPET START 7 +// Step 1: Get EIP-712 typed data for gas-sponsored deposit +// Returns EIP-712 typed data that must be signed by the owner off-chain +const manageResponse = await compass.earn.earnManage({ + owner: WALLET_ADDRESS, + chain: "base", + venue: { + type: "VAULT", + vaultAddress: "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", + }, + action: "DEPOSIT", + amount: "0.5", + gasSponsorship: true, + fee: null, +} as any); +// SNIPPET END 7 + +// SNIPPET START 8 +// Step 2: Sign EIP-712 typed data with owner's private key (off-chain, no gas) +// This signature from Step 2 is required as input for Step 3 +const manageEip712 = manageResponse.eip712!; +const manageSignature = await ownerWalletClient.signTypedData({ + domain: manageEip712.domain as any, + types: manageEip712.types as any, + primaryType: manageEip712.primaryType as string, + message: manageEip712.message as any, +}); +// SNIPPET END 8 + +// SNIPPET START 9 +// Step 3: Prepare gas-sponsored deposit transaction +// Uses the signature from Step 2 as input. The sender will pay for gas to execute the deposit +const prepareManageResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ + owner: WALLET_ADDRESS, + chain: "base", + eip712: manageEip712, + signature: manageSignature, // Signature from Step 2 + sender: senderAccount.address, +}); +// SNIPPET END 9 + +// SNIPPET START 10 +// Step 4: Sign and broadcast deposit transaction with sender's private key +const manageTransaction = prepareManageResponse.transaction as any; +const manageTxHash = await walletClient.sendTransaction({ + ...manageTransaction, + value: BigInt(manageTransaction.value || 0), + gas: BigInt(manageTransaction.gas), + maxFeePerGas: BigInt(manageTransaction.maxFeePerGas), + maxPriorityFeePerGas: BigInt(manageTransaction.maxPriorityFeePerGas), +}); + +console.log(`Deposit transaction hash: ${manageTxHash}`); +console.log(`View on BaseScan: https://basescan.org/tx/${manageTxHash}`); + +const manageReceipt = await publicClient.waitForTransactionReceipt({ hash: manageTxHash }); +console.log(`Deposit confirmed in block: ${manageReceipt.blockNumber}`); +console.log("Gas-sponsored deposit transaction confirmed"); +// SNIPPET END 10 + diff --git a/v2/gas_sponsorship/typescript/tsconfig.json b/v2/gas_sponsorship/typescript/tsconfig.json new file mode 100644 index 00000000..d127c035 --- /dev/null +++ b/v2/gas_sponsorship/typescript/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "ESNext", + "lib": ["ES2020"], + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} + From a1d3d25bd04e34e79e8df31f0fd7338baa8e9a1c Mon Sep 17 00:00:00 2001 From: elisabethd Date: Wed, 3 Dec 2025 11:49:29 +0000 Subject: [PATCH 2/2] Fix gas sponsorship examples: add 0x prefix to tx hash, fix EIP-712 signature, remove unnecessary files - Fixed Python transaction hash to include 0x prefix - Fixed TypeScript EIP-712 signature issue by normalizing types object - Updated TypeScript SDK to latest version - Removed gas_sponsored_earn_deposit folder - Removed build artifacts (.egg-info, __pycache__, dist, node_modules) - Updated .gitignore to prevent committing build artifacts - Removed WORKFLOW.md (not related to gas_sponsorship) --- .gitignore | 5 +- WORKFLOW.md | 104 ----------- .../python/README.md | 61 ------- v2/gas_sponsored_earn_deposit/python/main.py | 73 -------- .../python/pyproject.toml | 11 -- .../typescript/README.md | 61 ------- .../typescript/package.json | 26 --- .../typescript/src/index.ts | 96 ---------- .../typescript/tsconfig.json | 17 -- .../PKG-INFO | 8 - .../SOURCES.txt | 8 - .../dependency_links.txt | 1 - .../requires.txt | 4 - .../top_level.txt | 1 - v2/gas_sponsorship/python/main.py | 137 ++++++-------- v2/gas_sponsorship/typescript/package.json | 3 +- v2/gas_sponsorship/typescript/src/index.ts | 167 ++++++++---------- 17 files changed, 130 insertions(+), 653 deletions(-) delete mode 100644 WORKFLOW.md delete mode 100644 v2/gas_sponsored_earn_deposit/python/README.md delete mode 100644 v2/gas_sponsored_earn_deposit/python/main.py delete mode 100644 v2/gas_sponsored_earn_deposit/python/pyproject.toml delete mode 100644 v2/gas_sponsored_earn_deposit/typescript/README.md delete mode 100644 v2/gas_sponsored_earn_deposit/typescript/package.json delete mode 100644 v2/gas_sponsored_earn_deposit/typescript/src/index.ts delete mode 100644 v2/gas_sponsored_earn_deposit/typescript/tsconfig.json delete mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/PKG-INFO delete mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/SOURCES.txt delete mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/dependency_links.txt delete mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/requires.txt delete mode 100644 v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/top_level.txt diff --git a/.gitignore b/.gitignore index c0e4f854..9d2008b5 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,7 @@ next-env.d.ts .next/ -**/dist/ \ No newline at end of file +**/dist/ +**/__pycache__/ +**/*.egg-info/ +**/node_modules/ \ No newline at end of file diff --git a/WORKFLOW.md b/WORKFLOW.md deleted file mode 100644 index 59b133a6..00000000 --- a/WORKFLOW.md +++ /dev/null @@ -1,104 +0,0 @@ -# Workflow: Working with api_usecases in mono repo - -This folder contains a clone of the public [CompassLabs/api_usecases](https://github.com/CompassLabs/api_usecases) repository. - -## Setup - -The `api_usecases` folder is: -- ✅ Cloned from the public repo -- ✅ Has its own git history (separate from mono repo) -- ✅ Ignored by mono repo's `.gitignore` (won't be committed to mono) -- ✅ Can be worked on independently - -## Workflow: Adding New Use Cases - -### 1. Create a Branch in api_usecases - -```bash -cd api_usecases -git checkout -b feature/new-use-case -``` - -### 2. Create Your Use Case - -Add your code following the existing structure: - -``` -api_usecases/ - v2/ - your_use_case/ - typescript/ - src/ - index.ts - package.json - python/ - main.py - pyproject.toml -``` - -**Important:** Add snippet markers in your code: - -**TypeScript:** -```typescript -// SNIPPET START 1 -import { CompassClient } from '@compass-labs/api-sdk'; -// ... your code ... -// SNIPPET END 1 -``` - -**Python:** -```python -# SNIPPET START 1 -from compass_api_sdk import CompassClient -# ... your code ... -# SNIPPET END 1 -``` - -### 3. Test Your Code - -Make sure your use case works before proceeding. - -### 4. Commit and Push to Public Repo - -```bash -cd api_usecases -git add . -git commit -m "Add new use case: your-use-case" -git push origin feature/new-use-case -``` - -Then create a PR in the public repo: https://github.com/CompassLabs/api_usecases - -### 5. Create Docs Page in mono Repo - -Once your code is merged to `main` in the public repo: - -1. Create a new `.mdx` file in `api_docs/v2/examples/your-use-case.mdx` -2. Use `GithubCodeBlock` to reference your code (see `api_docs/USE_CASES_GUIDE.md`) -3. Add the page to `api_docs/docs.json` navigation -4. Commit and push to mono repo - -## Updating api_usecases - -To pull latest changes from the public repo: - -```bash -cd api_usecases -git checkout main -git pull origin main -``` - -## Current Structure - -- `v0/` - Version 0 examples -- `v1/` - Version 1 examples (basic_examples, pendle, aave_looping, transaction_bundler) -- `v2/` - Version 2 examples -- `wallet-earn/` - Wallet earn demo - -## Notes - -- The `api_usecases` folder is **not** tracked by the mono repo -- All changes to use cases should be committed to the public repo -- Docs pages in `api_docs/` reference code via GitHub raw URLs -- Always use snippet markers for code sections you want to show in docs - diff --git a/v2/gas_sponsored_earn_deposit/python/README.md b/v2/gas_sponsored_earn_deposit/python/README.md deleted file mode 100644 index a821fc78..00000000 --- a/v2/gas_sponsored_earn_deposit/python/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Gas-Sponsored Earn Deposit - Python Example - -This example demonstrates how to deposit into a Morpho vault with gas sponsorship using the Compass API Python SDK. - -## Prerequisites - -- Python 3.8+ installed -- A Compass API key ([Get one here](https://auth-compasslabs-ai.auth.eu-west-2.amazoncognito.com/login?client_id=2l366l2b3dok7k71nbnu8r1u36&redirect_uri=https://api.compasslabs.ai/auth/callback&response_type=code&scope=openid+email+profile)) -- An existing Earn Account with sufficient USDC balance -- Two wallet addresses: - - `owner`: The wallet that owns the Earn Account (signs the EIP-712 typed data) - - `sender`: The wallet that pays for gas (signs and broadcasts the transaction) - -## Setup - -1. Install dependencies: -```bash -pip install -e . -``` - -Or install manually: -```bash -pip install compass-api-sdk python-dotenv web3 eth-account -``` - -2. Copy the example environment file: -```bash -cp .env.example .env -``` - -3. Fill in your `.env` file with your actual values: - - `COMPASS_API_KEY`: Your Compass API key - - `WALLET_ADDRESS`: Your wallet address (owner of the Earn Account) - - `OWNER_PRIVATE_KEY`: Owner's private key (to sign EIP-712 typed data) - - `SENDER_PRIVATE_KEY`: Sender's private key (to sign and broadcast the transaction) - - `BASE_RPC_URL`: Your Base mainnet RPC URL - -## Run - -```bash -python main.py -``` - -## What This Does - -This example demonstrates the 3-step gas sponsorship flow: - -1. **Get EIP-712 typed data**: Calls `/v2/earn/manage` with `gas_sponsorship: True` to get EIP-712 typed data -2. **Sign typed data**: The `owner` signs the EIP-712 typed data off-chain (no gas required) -3. **Prepare and execute**: Calls `/v2/gas_sponsorship/prepare` with the signature, then the `sender` signs and broadcasts the transaction (sender pays gas) - -## Notes - -- This example deposits 0.5 USDC into the Steakhouse USDC vault on Morpho (`0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183`) -- The `owner` must be the address that owns the Earn Account -- The `sender` can be any address that has ETH on Base to pay for gas -- The `owner` and `sender` can be the same address, but they serve different roles in the flow -- Make sure your Earn Account has sufficient USDC balance -- Make sure the `sender` wallet has enough ETH on Base to cover gas fees -- **Important**: Make sure you're using `compass-api-sdk` version 2.0.1 or later - diff --git a/v2/gas_sponsored_earn_deposit/python/main.py b/v2/gas_sponsored_earn_deposit/python/main.py deleted file mode 100644 index 1c190c5f..00000000 --- a/v2/gas_sponsored_earn_deposit/python/main.py +++ /dev/null @@ -1,73 +0,0 @@ -# SNIPPET START 1 -from compass_api_sdk import CompassAPI, models -import os -from dotenv import load_dotenv -from web3 import Web3 -from eth_account import Account -from eth_account.messages import encode_typed_data - -load_dotenv() - -COMPASS_API_KEY = os.getenv("COMPASS_API_KEY") -WALLET_ADDRESS = os.getenv("WALLET_ADDRESS") -OWNER_PRIVATE_KEY = os.getenv("OWNER_PRIVATE_KEY") -SENDER_PRIVATE_KEY = os.getenv("SENDER_PRIVATE_KEY") -BASE_RPC_URL = os.getenv("BASE_RPC_URL") -# SNIPPET END 1 - -# SNIPPET START 2 -with CompassAPI(api_key_auth=COMPASS_API_KEY) as compass_api: -# SNIPPET END 2 - -# SNIPPET START 3 - # Get EIP-712 typed data for gas-sponsored deposit - manage_response = compass_api.earn.earn_manage( - owner=WALLET_ADDRESS, - chain=models.Chain.BASE, - venue={ - "type": "VAULT", - "vault_address": "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", - }, - action=models.EarnManageRequestAction.DEPOSIT, - amount="0.5", - gas_sponsorship=True, - fee=None, - ) -# SNIPPET END 3 - -# SNIPPET START 4 - # Sign EIP-712 typed data with owner's private key - owner_account = Account.from_key(OWNER_PRIVATE_KEY if OWNER_PRIVATE_KEY.startswith("0x") else f"0x{OWNER_PRIVATE_KEY}") - eip712 = manage_response.eip712 - encoded_data = encode_typed_data(full_message=eip712) - signed_message = owner_account.sign_message(encoded_data) - signature = f"0x{signed_message.signature.hex()}" -# SNIPPET END 4 - -# SNIPPET START 5 - # Prepare gas-sponsored transaction - sender_account = Account.from_key(SENDER_PRIVATE_KEY if SENDER_PRIVATE_KEY.startswith("0x") else f"0x{SENDER_PRIVATE_KEY}") - prepare_response = compass_api.gas_sponsorship.gas_sponsorship_prepare( - owner=WALLET_ADDRESS, - chain=models.Chain.BASE, - eip712=eip712, - signature=signature, - sender=sender_account.address, - ) -# SNIPPET END 5 - -# SNIPPET START 6 - # Sign and broadcast transaction with sender's private key - w3 = Web3(Web3.HTTPProvider(BASE_RPC_URL)) - tx_dict = prepare_response.model_dump(by_alias=True) - signed_tx = w3.eth.account.sign_transaction(tx_dict["transaction"], SENDER_PRIVATE_KEY) - tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) - - print(f"Transaction hash: {tx_hash.hex()}") - print(f"View on BaseScan: https://basescan.org/tx/{tx_hash.hex()}") - - receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - print(f"Transaction confirmed in block: {receipt.blockNumber}") - print("Gas-sponsored deposit transaction confirmed") -# SNIPPET END 6 - diff --git a/v2/gas_sponsored_earn_deposit/python/pyproject.toml b/v2/gas_sponsored_earn_deposit/python/pyproject.toml deleted file mode 100644 index 8f11379d..00000000 --- a/v2/gas_sponsored_earn_deposit/python/pyproject.toml +++ /dev/null @@ -1,11 +0,0 @@ -[project] -name = "gas_sponsored_earn_deposit_python_example" -version = "1.0.0" -description = "Example: Gas-Sponsored Earn Deposit using Compass API" -dependencies = [ - "compass-api-sdk", - "python-dotenv", - "web3", - "eth-account", -] - diff --git a/v2/gas_sponsored_earn_deposit/typescript/README.md b/v2/gas_sponsored_earn_deposit/typescript/README.md deleted file mode 100644 index 9d2b3dbe..00000000 --- a/v2/gas_sponsored_earn_deposit/typescript/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Gas-Sponsored Earn Deposit - TypeScript Example - -This example demonstrates how to deposit into a Morpho vault with gas sponsorship using the Compass API TypeScript SDK. - -## Prerequisites - -- Node.js 18+ installed -- A Compass API key ([Get one here](https://auth-compasslabs-ai.auth.eu-west-2.amazoncognito.com/login?client_id=2l366l2b3dok7k71nbnu8r1u36&redirect_uri=https://api.compasslabs.ai/auth/callback&response_type=code&scope=openid+email+profile)) -- An existing Earn Account with sufficient USDC balance -- Two wallet addresses: - - `owner`: The wallet that owns the Earn Account (signs the EIP-712 typed data) - - `sender`: The wallet that pays for gas (signs and broadcasts the transaction) - -## Setup - -1. Install dependencies: -```bash -npm install -``` - -2. Copy the example environment file: -```bash -cp .env.example .env -``` - -3. Fill in your `.env` file with your actual values: - - `COMPASS_API_KEY`: Your Compass API key - - `WALLET_ADDRESS`: Your wallet address (owner of the Earn Account) - - `OWNER_PRIVATE_KEY`: Owner's private key (to sign EIP-712 typed data) - - `SENDER_PRIVATE_KEY`: Sender's private key (to sign and broadcast the transaction) - - `BASE_RPC_URL`: Your Base mainnet RPC URL - -## Run - -```bash -npm run dev -``` - -Or build and run: -```bash -npm run build -npm start -``` - -## What This Does - -This example demonstrates the 3-step gas sponsorship flow: - -1. **Get EIP-712 typed data**: Calls `/v2/earn/manage` with `gasSponsorship: true` to get EIP-712 typed data -2. **Sign typed data**: The `owner` signs the EIP-712 typed data off-chain (no gas required) -3. **Prepare and execute**: Calls `/v2/gas_sponsorship/prepare` with the signature, then the `sender` signs and broadcasts the transaction (sender pays gas) - -## Notes - -- This example deposits 0.5 USDC into the Steakhouse USDC vault on Morpho (`0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183`) -- The `owner` must be the address that owns the Earn Account -- The `sender` can be any address that has ETH on Base to pay for gas -- The `owner` and `sender` can be the same address, but they serve different roles in the flow -- Make sure your Earn Account has sufficient USDC balance -- Make sure the `sender` wallet has enough ETH on Base to cover gas fees - diff --git a/v2/gas_sponsored_earn_deposit/typescript/package.json b/v2/gas_sponsored_earn_deposit/typescript/package.json deleted file mode 100644 index 3f2a7754..00000000 --- a/v2/gas_sponsored_earn_deposit/typescript/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "gas_sponsored_earn_deposit_typescript_example", - "version": "1.0.0", - "type": "module", - "main": "index.js", - "scripts": { - "build": "tsc", - "start": "tsc && node dist/index.js", - "dev": "ts-node --esm src/index.ts" - }, - "author": "", - "license": "ISC", - "description": "Example: Gas-Sponsored Earn Deposit using Compass API", - "dependencies": { - "@compass-labs/api-sdk": "^1.3.5", - "dotenv": "^16.5.0", - "viem": "^2.31.0" - }, - "devDependencies": { - "@types/node": "^24.0.0", - "prettier": "^3.6.2", - "ts-node": "^10.9.2", - "typescript": "^5.8.3" - } -} - diff --git a/v2/gas_sponsored_earn_deposit/typescript/src/index.ts b/v2/gas_sponsored_earn_deposit/typescript/src/index.ts deleted file mode 100644 index b4a06be0..00000000 --- a/v2/gas_sponsored_earn_deposit/typescript/src/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -// SNIPPET START 1 -import { CompassApiSDK } from "@compass-labs/api-sdk"; -import dotenv from "dotenv"; -import { privateKeyToAccount } from "viem/accounts"; -import { base } from "viem/chains"; -import { http, createWalletClient, createPublicClient, signTypedData } from "viem"; - -dotenv.config(); - -const COMPASS_API_KEY = process.env.COMPASS_API_KEY as string; -const WALLET_ADDRESS = process.env.WALLET_ADDRESS as `0x${string}`; -const OWNER_PRIVATE_KEY = (process.env.OWNER_PRIVATE_KEY?.startsWith("0x") - ? process.env.OWNER_PRIVATE_KEY - : `0x${process.env.OWNER_PRIVATE_KEY}`) as `0x${string}`; -const SENDER_PRIVATE_KEY = (process.env.SENDER_PRIVATE_KEY?.startsWith("0x") - ? process.env.SENDER_PRIVATE_KEY - : `0x${process.env.SENDER_PRIVATE_KEY}`) as `0x${string}`; -const BASE_RPC_URL = process.env.BASE_RPC_URL as string; -// SNIPPET END 1 - -// SNIPPET START 2 -const compass = new CompassApiSDK({ - apiKeyAuth: COMPASS_API_KEY, -}); -// SNIPPET END 2 - -// SNIPPET START 3 -// Get EIP-712 typed data for gas-sponsored deposit -const manageResponse = await compass.earn.earnManage({ - owner: WALLET_ADDRESS, - chain: "base", - venue: { - type: "VAULT", - vaultAddress: "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", - }, - action: "DEPOSIT", - amount: "0.5", - gasSponsorship: true, - fee: null, -}); -// SNIPPET END 3 - -// SNIPPET START 4 -// Sign EIP-712 typed data with owner's private key -const ownerAccount = privateKeyToAccount(OWNER_PRIVATE_KEY); -const eip712 = manageResponse.eip712!; -const signature = await signTypedData({ - account: ownerAccount, - domain: eip712.domain as any, - types: eip712.types as any, - primaryType: eip712.primaryType, - message: eip712.message as any, -}); -// SNIPPET END 4 - -// SNIPPET START 5 -// Prepare gas-sponsored transaction -const senderAccount = privateKeyToAccount(SENDER_PRIVATE_KEY); -const prepareResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ - owner: WALLET_ADDRESS, - chain: "base", - eip712: eip712, - signature: signature, - sender: senderAccount.address, -}); -// SNIPPET END 5 - -// SNIPPET START 6 -// Sign and broadcast transaction with sender's private key -const walletClient = createWalletClient({ - account: senderAccount, - chain: base, - transport: http(BASE_RPC_URL), -}); -const publicClient = createPublicClient({ - chain: base, - transport: http(BASE_RPC_URL), -}); - -const transaction = prepareResponse.transaction as any; -const txHash = await walletClient.sendTransaction({ - ...transaction, - value: BigInt(transaction.value || 0), - gas: BigInt(transaction.gas), - maxFeePerGas: BigInt(transaction.maxFeePerGas), - maxPriorityFeePerGas: BigInt(transaction.maxPriorityFeePerGas), -}); - -console.log(`Transaction hash: ${txHash}`); -console.log(`View on BaseScan: https://basescan.org/tx/${txHash}`); - -const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); -console.log(`Transaction confirmed in block: ${receipt.blockNumber}`); -console.log("Gas-sponsored deposit transaction confirmed"); -// SNIPPET END 6 - diff --git a/v2/gas_sponsored_earn_deposit/typescript/tsconfig.json b/v2/gas_sponsored_earn_deposit/typescript/tsconfig.json deleted file mode 100644 index d127c035..00000000 --- a/v2/gas_sponsored_earn_deposit/typescript/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "es2020", - "module": "ESNext", - "lib": ["ES2020"], - "moduleResolution": "node", - "esModuleInterop": true, - "skipLibCheck": true, - "strict": true, - "resolveJsonModule": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} - diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/PKG-INFO b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/PKG-INFO deleted file mode 100644 index e90faa64..00000000 --- a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/PKG-INFO +++ /dev/null @@ -1,8 +0,0 @@ -Metadata-Version: 2.4 -Name: gas_sponsorship_python_example -Version: 1.0.0 -Summary: Example: Gas Sponsorship for Earn Account Funding and Position Management using Compass API -Requires-Dist: compass-api-sdk -Requires-Dist: python-dotenv -Requires-Dist: web3 -Requires-Dist: eth-account diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/SOURCES.txt b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/SOURCES.txt deleted file mode 100644 index 3f7c1ae9..00000000 --- a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/SOURCES.txt +++ /dev/null @@ -1,8 +0,0 @@ -README.md -main.py -pyproject.toml -gas_sponsorship_python_example.egg-info/PKG-INFO -gas_sponsorship_python_example.egg-info/SOURCES.txt -gas_sponsorship_python_example.egg-info/dependency_links.txt -gas_sponsorship_python_example.egg-info/requires.txt -gas_sponsorship_python_example.egg-info/top_level.txt \ No newline at end of file diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/dependency_links.txt b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/dependency_links.txt deleted file mode 100644 index 8b137891..00000000 --- a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/requires.txt b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/requires.txt deleted file mode 100644 index dea15ae1..00000000 --- a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/requires.txt +++ /dev/null @@ -1,4 +0,0 @@ -compass-api-sdk -python-dotenv -web3 -eth-account diff --git a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/top_level.txt b/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/top_level.txt deleted file mode 100644 index ba2906d0..00000000 --- a/v2/gas_sponsorship/python/gas_sponsorship_python_example.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -main diff --git a/v2/gas_sponsorship/python/main.py b/v2/gas_sponsorship/python/main.py index c2dbb9ce..9062e186 100644 --- a/v2/gas_sponsorship/python/main.py +++ b/v2/gas_sponsorship/python/main.py @@ -13,10 +13,32 @@ OWNER_PRIVATE_KEY = os.getenv("OWNER_PRIVATE_KEY") SENDER_PRIVATE_KEY = os.getenv("SENDER_PRIVATE_KEY") BASE_RPC_URL = os.getenv("BASE_RPC_URL") + +w3 = Web3(Web3.HTTPProvider(BASE_RPC_URL)) + +def normalize_private_key(key: str) -> str: + return key if key.startswith("0x") else f"0x{key}" + +def sign_eip712(account: Account, eip712: dict) -> str: + encoded = encode_typed_data(full_message=eip712) + signed = account.sign_message(encoded) + return f"0x{signed.signature.hex()}" + +def send_transaction(tx_dict: dict, private_key: str) -> str: + signed_tx = w3.eth.account.sign_transaction(tx_dict, private_key) + tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + tx_hash_hex = f"0x{tx_hash.hex()}" + print(f"Deposit transaction hash: {tx_hash_hex}") + print(f"View on BaseScan: https://basescan.org/tx/{tx_hash_hex}") + print(f"Deposit confirmed in block: {receipt.blockNumber}") + return tx_hash_hex # SNIPPET END 1 # SNIPPET START 2 with CompassAPI(api_key_auth=COMPASS_API_KEY) as compass_api: + owner_account = Account.from_key(normalize_private_key(OWNER_PRIVATE_KEY)) + sender_account = Account.from_key(normalize_private_key(SENDER_PRIVATE_KEY)) # SNIPPET END 2 # ============================================================================ @@ -24,9 +46,7 @@ # ============================================================================ # SNIPPET START 3 - # Step 1: Get EIP-712 typed data for Permit2 approval (gas-sponsored) - # Returns EIP-712 typed data that must be signed by the owner off-chain - # Note: If allowance is already set, this will raise an error - you can skip to Example 2 + # Get EIP-712 typed data for Permit2 approval try: approve_response = compass_api.gas_sponsorship.gas_sponsorship_approve_transfer( owner=WALLET_ADDRESS, @@ -36,126 +56,75 @@ ) except Exception as e: if "allowance already set" in str(e).lower(): - print("Permit2 approval already exists - skipping to Example 2 (Manage Position)") - # Skip Example 1 and go to Example 2 + print("Permit2 approval already exists - skipping to Example 2") approve_response = None else: raise # SNIPPET END 3 # SNIPPET START 4 - # Step 2: Sign EIP-712 typed data with owner's private key (off-chain, no gas) - # This signature from Step 2 is required as input for Step 3 - owner_account = Account.from_key(OWNER_PRIVATE_KEY if OWNER_PRIVATE_KEY.startswith("0x") else f"0x{OWNER_PRIVATE_KEY}") + # Sign EIP-712 typed data with owner's private key if approve_response and approve_response.eip_712: - approve_eip712 = approve_response.eip_712 - encoded_data = encode_typed_data(full_message=approve_eip712) - signed_message = owner_account.sign_message(encoded_data) - approve_signature = f"0x{signed_message.signature.hex()}" + approve_eip712 = approve_response.eip_712.model_dump(by_alias=True, mode="json") + approve_signature = sign_eip712(owner_account, approve_eip712) else: - print("Skipping Example 1 - Permit2 approval already exists") approve_eip712 = None approve_signature = None # SNIPPET END 4 # SNIPPET START 5 - # Step 3: Prepare gas-sponsored Permit2 approval transaction - # Uses the signature from Step 2 as input. The sender will pay for gas to execute the Permit2 approval - sender_account = Account.from_key(SENDER_PRIVATE_KEY if SENDER_PRIVATE_KEY.startswith("0x") else f"0x{SENDER_PRIVATE_KEY}") + # Prepare and send Permit2 approval transaction if approve_eip712 and approve_signature: - prepare_approve_response = compass_api.gas_sponsorship.gas_sponsorship_prepare( + prepare_response = compass_api.gas_sponsorship.gas_sponsorship_prepare( owner=WALLET_ADDRESS, chain=models.Chain.BASE, - eip_712=approve_eip712.model_dump(by_alias=True), # Convert to dict - signature=approve_signature, # Signature from Step 2 + eip_712=approve_eip712, + signature=approve_signature, sender=sender_account.address, ) - else: - prepare_approve_response = None -# SNIPPET END 5 - -# SNIPPET START 6 - # Step 4: Sign and broadcast Permit2 approval transaction with sender's private key - if prepare_approve_response: - w3 = Web3(Web3.HTTPProvider(BASE_RPC_URL)) - approve_tx_dict = prepare_approve_response.model_dump(by_alias=True) - signed_approve_tx = w3.eth.account.sign_transaction(approve_tx_dict["transaction"], SENDER_PRIVATE_KEY) - approve_tx_hash = w3.eth.send_raw_transaction(signed_approve_tx.rawTransaction) - - print(f"Permit2 approval transaction hash: {approve_tx_hash.hex()}") - print(f"View on BaseScan: https://basescan.org/tx/{approve_tx_hash.hex()}") - - approve_receipt = w3.eth.wait_for_transaction_receipt(approve_tx_hash) - print(f"Permit2 approval confirmed in block: {approve_receipt.blockNumber}") + tx_dict = prepare_response.model_dump(by_alias=True)["transaction"] + send_transaction(tx_dict, SENDER_PRIVATE_KEY) print("Earn Account can now be funded with gas sponsorship") else: print("Skipping Example 1 transaction - Permit2 approval already exists") -# SNIPPET END 6 +# SNIPPET END 5 # ============================================================================ # EXAMPLE 2: Manage Earn Position (Deposit) with Gas Sponsorship # ============================================================================ -# SNIPPET START 7 - # Step 1: Get EIP-712 typed data for gas-sponsored deposit - # Returns EIP-712 typed data that must be signed by the owner off-chain +# SNIPPET START 6 + # Get EIP-712 typed data for deposit manage_response = compass_api.earn.earn_manage( owner=WALLET_ADDRESS, chain=models.Chain.BASE, - venue={ - "type": "VAULT", - "vault_address": "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183", - }, + venue={"type": "VAULT", "vault_address": "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183"}, action=models.EarnManageRequestAction.DEPOSIT, amount="0.5", gas_sponsorship=True, fee=None, ) -# SNIPPET END 7 +# SNIPPET END 6 -# SNIPPET START 8 - # Step 2: Sign EIP-712 typed data with owner's private key (off-chain, no gas) - # This signature from Step 2 is required as input for Step 3 - # Test code pattern: signs with deposit_tx['eip_712'] which is dict from response.json() (with ints) - # Sign with the dict structure (ints) - exactly like the test code does - manage_eip712_output = manage_response.eip_712 - # Get dict format (like response.json()) - with ints, not enums - eip712_dict = manage_eip712_output.model_dump(by_alias=True, mode="json") +# SNIPPET START 7 + # Sign EIP-712 typed data with owner's private key import json - eip712_dict = json.loads(json.dumps(eip712_dict, default=str)) - # Sign with dict structure (ints) - matches test code pattern - encoded_manage_data = encode_typed_data(full_message=eip712_dict) - signed_manage_message = owner_account.sign_message(encoded_manage_data) - manage_signature = f"0x{signed_manage_message.signature.hex()}" -# SNIPPET END 8 + manage_eip712 = manage_response.eip_712.model_dump(by_alias=True, mode="json") + manage_eip712 = json.loads(json.dumps(manage_eip712, default=str)) + manage_signature = sign_eip712(owner_account, manage_eip712) +# SNIPPET END 7 -# SNIPPET START 9 - # Step 3: Prepare gas-sponsored deposit transaction - # Uses the signature from Step 2 as input. The sender will pay for gas to execute the deposit - # SDK expects Input model, not Output model - convert dict to Input model - # FastAPI converts Input model to BatchedSafeOperationsResponse model (same as what we signed with) - # Backend validates with model_dump() - matches what we signed with - eip712_input = models.BatchedSafeOperationsResponseInput(**eip712_dict) - prepare_manage_response = compass_api.gas_sponsorship.gas_sponsorship_prepare( +# SNIPPET START 8 + # Prepare and send deposit transaction + eip712_input = models.BatchedSafeOperationsResponseInput(**manage_eip712) + prepare_response = compass_api.gas_sponsorship.gas_sponsorship_prepare( owner=WALLET_ADDRESS, chain=models.Chain.BASE, - eip_712=eip712_input, # Pass Input model - SDK expects this - signature=manage_signature, # Signature from Step 2 (signed with backend model_dump() structure) + eip_712=eip712_input, + signature=manage_signature, sender=sender_account.address, ) -# SNIPPET END 9 - -# SNIPPET START 10 - # Step 4: Sign and broadcast deposit transaction with sender's private key - manage_tx_dict = prepare_manage_response.model_dump(by_alias=True) - signed_manage_tx = w3.eth.account.sign_transaction(manage_tx_dict["transaction"], SENDER_PRIVATE_KEY) - manage_tx_hash = w3.eth.send_raw_transaction(signed_manage_tx.rawTransaction) - - print(f"Deposit transaction hash: {manage_tx_hash.hex()}") - print(f"View on BaseScan: https://basescan.org/tx/{manage_tx_hash.hex()}") - - manage_receipt = w3.eth.wait_for_transaction_receipt(manage_tx_hash) - print(f"Deposit confirmed in block: {manage_receipt.blockNumber}") + tx_dict = prepare_response.model_dump(by_alias=True)["transaction"] + send_transaction(tx_dict, SENDER_PRIVATE_KEY) print("Gas-sponsored deposit transaction confirmed") -# SNIPPET END 10 - +# SNIPPET END 8 diff --git a/v2/gas_sponsorship/typescript/package.json b/v2/gas_sponsorship/typescript/package.json index 2685933b..ade0b825 100644 --- a/v2/gas_sponsorship/typescript/package.json +++ b/v2/gas_sponsorship/typescript/package.json @@ -12,7 +12,7 @@ "license": "ISC", "description": "Example: Gas Sponsorship for Earn Account Funding and Position Management using Compass API", "dependencies": { - "@compass-labs/api-sdk": "^1.3.5", + "@compass-labs/api-sdk": "^2.1.11", "dotenv": "^16.5.0", "viem": "^2.31.0" }, @@ -23,4 +23,3 @@ "typescript": "^5.8.3" } } - diff --git a/v2/gas_sponsorship/typescript/src/index.ts b/v2/gas_sponsorship/typescript/src/index.ts index cdcbc02b..ef81a2e5 100644 --- a/v2/gas_sponsorship/typescript/src/index.ts +++ b/v2/gas_sponsorship/typescript/src/index.ts @@ -9,18 +9,52 @@ dotenv.config(); const COMPASS_API_KEY = process.env.COMPASS_API_KEY as string; const WALLET_ADDRESS = process.env.WALLET_ADDRESS as `0x${string}`; -const OWNER_PRIVATE_KEY = (process.env.OWNER_PRIVATE_KEY?.startsWith("0x") - ? process.env.OWNER_PRIVATE_KEY - : `0x${process.env.OWNER_PRIVATE_KEY}`) as `0x${string}`; -const SENDER_PRIVATE_KEY = (process.env.SENDER_PRIVATE_KEY?.startsWith("0x") - ? process.env.SENDER_PRIVATE_KEY - : `0x${process.env.SENDER_PRIVATE_KEY}`) as `0x${string}`; const BASE_RPC_URL = process.env.BASE_RPC_URL as string; + +const normalizePrivateKey = (key: string | undefined): `0x${string}` => { + if (!key) throw new Error("Private key not set"); + return (key.startsWith("0x") ? key : `0x${key}`) as `0x${string}`; +}; + +const OWNER_PRIVATE_KEY = normalizePrivateKey(process.env.OWNER_PRIVATE_KEY); +const SENDER_PRIVATE_KEY = normalizePrivateKey(process.env.SENDER_PRIVATE_KEY); + +const sendTransaction = async (tx: any, walletClient: any, publicClient: any) => { + const txHash = await walletClient.sendTransaction({ + ...tx, + value: BigInt(tx.value || 0), + gas: BigInt(tx.gas), + maxFeePerGas: BigInt(tx.maxFeePerGas), + maxPriorityFeePerGas: BigInt(tx.maxPriorityFeePerGas), + }); + const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + console.log(`Transaction hash: ${txHash}`); + console.log(`View on BaseScan: https://basescan.org/tx/0x${txHash}`); + console.log(`Confirmed in block: ${receipt.blockNumber}`); + return txHash; +}; // SNIPPET END 1 // SNIPPET START 2 -const compass = new CompassApiSDK({ - apiKeyAuth: COMPASS_API_KEY, +const compass = new CompassApiSDK({ apiKeyAuth: COMPASS_API_KEY }); +const ownerAccount = privateKeyToAccount(OWNER_PRIVATE_KEY); +const senderAccount = privateKeyToAccount(SENDER_PRIVATE_KEY); + +const ownerWalletClient = createWalletClient({ + account: ownerAccount, + chain: base, + transport: http(BASE_RPC_URL), +}); + +const senderWalletClient = createWalletClient({ + account: senderAccount, + chain: base, + transport: http(BASE_RPC_URL), +}); + +const publicClient = createPublicClient({ + chain: base, + transport: http(BASE_RPC_URL), }); // SNIPPET END 2 @@ -29,8 +63,7 @@ const compass = new CompassApiSDK({ // ============================================================================ // SNIPPET START 3 -// Step 1: Get EIP-712 typed data for Permit2 approval (gas-sponsored) -// Returns EIP-712 typed data that must be signed by the owner off-chain +// Get EIP-712 typed data for Permit2 approval let approveResponse; try { approveResponse = await compass.gasSponsorship.gasSponsorshipApproveTransfer({ @@ -41,7 +74,7 @@ try { }); } catch (error: any) { if (error.body?.includes("Token allowance already set")) { - console.log("Permit2 approval already exists - skipping to Example 2 (Manage Position)"); + console.log("Permit2 approval already exists - skipping to Example 2"); approveResponse = null; } else { throw error; @@ -50,16 +83,9 @@ try { // SNIPPET END 3 // SNIPPET START 4 -// Step 2: Sign EIP-712 typed data with owner's private key (off-chain, no gas) -// This signature from Step 2 is required as input for Step 3 -const ownerAccount = privateKeyToAccount(OWNER_PRIVATE_KEY); -const ownerWalletClient = createWalletClient({ - account: ownerAccount, - chain: base, - transport: http(BASE_RPC_URL), -}); +// Sign EIP-712 typed data with owner's private key let approveEip712, approveSignature; -if (approveResponse && approveResponse.eip712) { +if (approveResponse?.eip712) { approveEip712 = approveResponse.eip712; approveSignature = await ownerWalletClient.signTypedData({ domain: approveEip712.domain as any, @@ -68,70 +94,34 @@ if (approveResponse && approveResponse.eip712) { message: approveEip712.message as any, }); } else { - console.log("Skipping Example 1 - Permit2 approval already exists"); approveEip712 = null; approveSignature = null; } // SNIPPET END 4 // SNIPPET START 5 -// Step 3: Prepare gas-sponsored Permit2 approval transaction -// Uses the signature from Step 2 as input. The sender will pay for gas to execute the Permit2 approval -const senderAccount = privateKeyToAccount(SENDER_PRIVATE_KEY); -let prepareApproveResponse; +// Prepare and send Permit2 approval transaction if (approveEip712 && approveSignature) { - prepareApproveResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ + const prepareResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ owner: WALLET_ADDRESS, chain: "base", eip712: approveEip712, - signature: approveSignature, // Signature from Step 2 + signature: approveSignature, sender: senderAccount.address, }); -} else { - prepareApproveResponse = null; -} -// SNIPPET END 5 - -// SNIPPET START 6 -// Step 4: Sign and broadcast Permit2 approval transaction with sender's private key -const walletClient = createWalletClient({ - account: senderAccount, - chain: base, - transport: http(BASE_RPC_URL), -}); -const publicClient = createPublicClient({ - chain: base, - transport: http(BASE_RPC_URL), -}); - -if (prepareApproveResponse) { - const approveTransaction = prepareApproveResponse.transaction as any; - const approveTxHash = await walletClient.sendTransaction({ - ...approveTransaction, - value: BigInt(approveTransaction.value || 0), - gas: BigInt(approveTransaction.gas), - maxFeePerGas: BigInt(approveTransaction.maxFeePerGas), - maxPriorityFeePerGas: BigInt(approveTransaction.maxPriorityFeePerGas), - }); - - console.log(`Permit2 approval transaction hash: ${approveTxHash}`); - console.log(`View on BaseScan: https://basescan.org/tx/${approveTxHash}`); - - const approveReceipt = await publicClient.waitForTransactionReceipt({ hash: approveTxHash }); - console.log(`Permit2 approval confirmed in block: ${approveReceipt.blockNumber}`); + await sendTransaction(prepareResponse.transaction as any, senderWalletClient, publicClient); console.log("Earn Account can now be funded with gas sponsorship"); } else { console.log("Skipping Example 1 transaction - Permit2 approval already exists"); } -// SNIPPET END 6 +// SNIPPET END 5 // ============================================================================ // EXAMPLE 2: Manage Earn Position (Deposit) with Gas Sponsorship // ============================================================================ -// SNIPPET START 7 -// Step 1: Get EIP-712 typed data for gas-sponsored deposit -// Returns EIP-712 typed data that must be signed by the owner off-chain +// SNIPPET START 6 +// Get EIP-712 typed data for deposit const manageResponse = await compass.earn.earnManage({ owner: WALLET_ADDRESS, chain: "base", @@ -144,48 +134,35 @@ const manageResponse = await compass.earn.earnManage({ gasSponsorship: true, fee: null, } as any); -// SNIPPET END 7 +// SNIPPET END 6 -// SNIPPET START 8 -// Step 2: Sign EIP-712 typed data with owner's private key (off-chain, no gas) -// This signature from Step 2 is required as input for Step 3 +// SNIPPET START 7 +// Sign EIP-712 typed data with owner's private key const manageEip712 = manageResponse.eip712!; +// Normalize types: API returns types with "safeTx" (lowercase) but primaryType "SafeTx" (capital) +// We need to ensure the types object has the key matching the primaryType for viem +const types = { ...manageEip712.types } as any; +if (types.safeTx && !types.SafeTx) { + // Add SafeTx key if it doesn't exist (for viem compatibility) + types.SafeTx = types.safeTx; +} const manageSignature = await ownerWalletClient.signTypedData({ domain: manageEip712.domain as any, - types: manageEip712.types as any, - primaryType: manageEip712.primaryType as string, + types: types, + primaryType: manageEip712.primaryType as string, // Use "SafeTx" from API message: manageEip712.message as any, }); -// SNIPPET END 8 +// SNIPPET END 7 -// SNIPPET START 9 -// Step 3: Prepare gas-sponsored deposit transaction -// Uses the signature from Step 2 as input. The sender will pay for gas to execute the deposit -const prepareManageResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ +// SNIPPET START 8 +// Prepare and send deposit transaction +const prepareResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ owner: WALLET_ADDRESS, chain: "base", eip712: manageEip712, - signature: manageSignature, // Signature from Step 2 + signature: manageSignature, sender: senderAccount.address, }); -// SNIPPET END 9 - -// SNIPPET START 10 -// Step 4: Sign and broadcast deposit transaction with sender's private key -const manageTransaction = prepareManageResponse.transaction as any; -const manageTxHash = await walletClient.sendTransaction({ - ...manageTransaction, - value: BigInt(manageTransaction.value || 0), - gas: BigInt(manageTransaction.gas), - maxFeePerGas: BigInt(manageTransaction.maxFeePerGas), - maxPriorityFeePerGas: BigInt(manageTransaction.maxPriorityFeePerGas), -}); - -console.log(`Deposit transaction hash: ${manageTxHash}`); -console.log(`View on BaseScan: https://basescan.org/tx/${manageTxHash}`); - -const manageReceipt = await publicClient.waitForTransactionReceipt({ hash: manageTxHash }); -console.log(`Deposit confirmed in block: ${manageReceipt.blockNumber}`); +await sendTransaction(prepareResponse.transaction as any, senderWalletClient, publicClient); console.log("Gas-sponsored deposit transaction confirmed"); -// SNIPPET END 10 - +// SNIPPET END 8