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/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/main.py b/v2/gas_sponsorship/python/main.py new file mode 100644 index 00000000..9062e186 --- /dev/null +++ b/v2/gas_sponsorship/python/main.py @@ -0,0 +1,130 @@ +# 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") + +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 + +# ============================================================================ +# EXAMPLE 1: Fund Earn Account with Gas Sponsorship +# ============================================================================ + +# SNIPPET START 3 + # Get EIP-712 typed data for Permit2 approval + 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") + approve_response = None + else: + raise +# SNIPPET END 3 + +# SNIPPET START 4 + # Sign EIP-712 typed data with owner's private key + if approve_response and approve_response.eip_712: + approve_eip712 = approve_response.eip_712.model_dump(by_alias=True, mode="json") + approve_signature = sign_eip712(owner_account, approve_eip712) + else: + approve_eip712 = None + approve_signature = None +# SNIPPET END 4 + +# SNIPPET START 5 + # Prepare and send Permit2 approval transaction + if approve_eip712 and approve_signature: + prepare_response = compass_api.gas_sponsorship.gas_sponsorship_prepare( + owner=WALLET_ADDRESS, + chain=models.Chain.BASE, + eip_712=approve_eip712, + signature=approve_signature, + sender=sender_account.address, + ) + 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 5 + +# ============================================================================ +# EXAMPLE 2: Manage Earn Position (Deposit) with Gas Sponsorship +# ============================================================================ + +# 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"}, + action=models.EarnManageRequestAction.DEPOSIT, + amount="0.5", + gas_sponsorship=True, + fee=None, + ) +# SNIPPET END 6 + +# SNIPPET START 7 + # Sign EIP-712 typed data with owner's private key + import json + 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 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, + signature=manage_signature, + sender=sender_account.address, + ) + 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 8 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..ade0b825 --- /dev/null +++ b/v2/gas_sponsorship/typescript/package.json @@ -0,0 +1,25 @@ +{ + "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": "^2.1.11", + "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..ef81a2e5 --- /dev/null +++ b/v2/gas_sponsorship/typescript/src/index.ts @@ -0,0 +1,168 @@ +// 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 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 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 + +// ============================================================================ +// EXAMPLE 1: Fund Earn Account with Gas Sponsorship +// ============================================================================ + +// SNIPPET START 3 +// Get EIP-712 typed data for Permit2 approval +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"); + approveResponse = null; + } else { + throw error; + } +} +// SNIPPET END 3 + +// SNIPPET START 4 +// Sign EIP-712 typed data with owner's private key +let approveEip712, approveSignature; +if (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 { + approveEip712 = null; + approveSignature = null; +} +// SNIPPET END 4 + +// SNIPPET START 5 +// Prepare and send Permit2 approval transaction +if (approveEip712 && approveSignature) { + const prepareResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ + owner: WALLET_ADDRESS, + chain: "base", + eip712: approveEip712, + signature: approveSignature, + sender: senderAccount.address, + }); + 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 5 + +// ============================================================================ +// EXAMPLE 2: Manage Earn Position (Deposit) with Gas Sponsorship +// ============================================================================ + +// SNIPPET START 6 +// Get EIP-712 typed data for 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, +} as any); +// SNIPPET END 6 + +// 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: types, + primaryType: manageEip712.primaryType as string, // Use "SafeTx" from API + message: manageEip712.message as any, +}); +// SNIPPET END 7 + +// SNIPPET START 8 +// Prepare and send deposit transaction +const prepareResponse = await compass.gasSponsorship.gasSponsorshipPrepare({ + owner: WALLET_ADDRESS, + chain: "base", + eip712: manageEip712, + signature: manageSignature, + sender: senderAccount.address, +}); +await sendTransaction(prepareResponse.transaction as any, senderWalletClient, publicClient); +console.log("Gas-sponsored deposit transaction confirmed"); +// SNIPPET END 8 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"] +} +