diff --git a/docs/api/api-reference.md b/docs/api/api-reference.md index dd3bb30..dddd62a 100644 --- a/docs/api/api-reference.md +++ b/docs/api/api-reference.md @@ -1,612 +1,443 @@ -# 🌐 Galaxy DevKit API Reference +# Galaxy DevKit REST API Reference -Complete API documentation for Galaxy DevKit services. +This document describes the actual HTTP endpoints implemented in `packages/api/rest/`. All endpoints reflect the live route code — no invented fields. -## 📋 Table of Contents +> **Note on private keys:** The Galaxy DevKit REST API never returns or accepts private keys. The non-custodial architecture keeps all private key material on the client device. -- [Authentication](#-authentication) -- [REST API](#-rest-api) -- [GraphQL API](#-graphql-api) -- [WebSocket API](#-websocket-api) -- [Error Handling](#-error-handling) -- [Rate Limits](#-rate-limits) +## Base URL -## 🔐 Authentication +| Environment | URL | +|-------------|-----| +| Local development | `http://localhost:3000` | +| Testnet | Configure via `STELLAR_RPC_URL` / `STELLAR_HORIZON_URL` env vars | -### API Key Authentication -```bash -# Header -Authorization: Bearer your-api-key +## Authentication -# Or in SDK -const galaxy = new GalaxySDK({ - apiKey: 'your-api-key' -}); -``` +Most mutating endpoints require a valid JWT from Supabase Auth. -### JWT Authentication -```bash -# For user-specific operations -Authorization: Bearer jwt-token +```http +Authorization: Bearer ``` -## 🌐 REST API +Read-only DeFi endpoints (quotes, positions, analytics) are unauthenticated. -### Base URLs -- **Production**: `https://api.galaxy-devkit.com` -- **Testnet**: `https://testnet-api.galaxy-devkit.com` -- **Local**: `http://localhost:3000` +--- -### Wallets API +## DeFi — Soroswap -#### Create Wallet -```http -POST /api/v1/wallets -Content-Type: application/json -Authorization: Bearer your-api-key +### GET /api/v1/defi/swap/quote + +Get a swap quote from Soroswap. No authentication required. + +**Query parameters** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `assetIn` | string | Yes | Input asset. `XLM` or `NATIVE` for native XLM; `CODE:ISSUER` for non-native (e.g. `USDC:GA5ZS...`) | +| `assetOut` | string | Yes | Output asset (same format) | +| `amountIn` | string | Yes | Amount of `assetIn` to swap (decimal string, e.g. `"100"`) | + +**Response 200** +```json { - "userId": "user123", - "network": "testnet", - "metadata": { - "name": "My Wallet", - "description": "Personal wallet" - } + "amountOut": "14.2300000", + "priceImpact": "0.003", + "path": ["XLM", "USDC"], + "minAmountOut": "14.0900000" } ``` -**Response:** +**Error 400** + ```json { - "id": "wallet_abc123", - "publicKey": "GABC123...", - "privateKey": "SABC123...", - "network": "testnet", - "createdAt": "2024-12-01T00:00:00Z", - "metadata": { - "name": "My Wallet", - "description": "Personal wallet" + "error": { + "code": "VALIDATION_ERROR", + "message": "assetIn, assetOut, and amountIn are required query parameters", + "details": {} } } ``` -#### Get Wallet -```http -GET /api/v1/wallets/{walletId} -Authorization: Bearer your-api-key +**Example** + +```bash +curl "http://localhost:3000/api/v1/defi/swap/quote?assetIn=XLM&assetOut=USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN&amountIn=100" ``` -**Response:** +--- + +### POST /api/v1/defi/swap + +Build an unsigned Soroswap swap transaction and return the XDR for client-side signing. **Requires JWT.** + +**Request body** + ```json { - "id": "wallet_abc123", - "publicKey": "GABC123...", - "network": "testnet", - "balance": [ - { - "asset": "XLM", - "amount": "1000.0000000", - "limit": null - } - ], - "createdAt": "2024-12-01T00:00:00Z" + "assetIn": "XLM", + "assetOut": "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "amountIn": "100", + "minAmountOut": "14.09", + "signerPublicKey": "GABC123..." } ``` -#### List Wallets -```http -GET /api/v1/wallets?userId=user123&limit=10&offset=0 -Authorization: Bearer your-api-key -``` +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `assetIn` | string | Yes | Input asset | +| `assetOut` | string | Yes | Output asset | +| `amountIn` | string | Yes | Amount to swap | +| `minAmountOut` | string | Yes | Slippage floor | +| `signerPublicKey` | string | Yes | Stellar public key of the signer (G...) | -#### Update Wallet -```http -PUT /api/v1/wallets/{walletId} -Content-Type: application/json -Authorization: Bearer your-api-key +**Response 200** — unsigned transaction XDR ready for client signing +```json { - "metadata": { - "name": "Updated Wallet Name" - } + "xdr": "AAAAAgAAAA...", + "network": "testnet" } ``` -#### Delete Wallet -```http -DELETE /api/v1/wallets/{walletId} -Authorization: Bearer your-api-key -``` +--- -### Payments API +## DeFi — Blend (lending) -#### Send Payment -```http -POST /api/v1/payments -Content-Type: application/json -Authorization: Bearer your-api-key +### GET /api/v1/defi/blend/position/:publicKey -{ - "from": "source-address", - "to": "destination-address", - "amount": "10.5", - "asset": "XLM", - "memo": "Payment description", - "fee": "0.00001" -} -``` +Get the Blend lending position for a Stellar public key. No authentication required. + +**Path parameter:** `publicKey` — Stellar account public key (G...) + +**Response 200** -**Response:** ```json { - "id": "payment_xyz789", - "hash": "abc123def456...", - "status": "success", - "ledger": "12345", - "createdAt": "2024-12-01T00:00:00Z", - "from": "source-address", - "to": "destination-address", - "amount": "10.5", - "asset": "XLM" + "supplied": [ + { "asset": "USDC", "amount": "500.0000000", "apy": "0.042" } + ], + "borrowed": [ + { "asset": "XLM", "amount": "1000.0000000", "apy": "0.061" } + ], + "healthFactor": "1.82", + "collateralValue": "510.00", + "debtValue": "120.00" } ``` -#### Get Payment -```http -GET /api/v1/payments/{paymentId} -Authorization: Bearer your-api-key -``` +--- -#### List Payments -```http -GET /api/v1/payments?walletId=wallet123&limit=10&offset=0 -Authorization: Bearer your-api-key -``` +### POST /api/v1/defi/blend/supply -### Transactions API +Build an unsigned Blend supply transaction. **Requires JWT.** -#### Get Transaction -```http -GET /api/v1/transactions/{txHash} -Authorization: Bearer your-api-key -``` +**Request body** -**Response:** ```json { - "hash": "abc123def456...", - "source": "source-address", - "destination": "destination-address", - "amount": "10.5", - "asset": "XLM", - "memo": "Payment description", - "status": "success", - "ledger": "12345", - "createdAt": "2024-12-01T00:00:00Z" + "asset": "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "amount": "500", + "signerPublicKey": "GABC123..." } ``` -#### List Transactions -```http -GET /api/v1/transactions?walletId=wallet123&limit=10&offset=0 -Authorization: Bearer your-api-key -``` +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `asset` | string | Yes | Asset to supply (`CODE:ISSUER` or `XLM`) | +| `amount` | string | Yes | Amount to supply | +| `signerPublicKey` | string | Yes | Signer's Stellar public key | -### Smart Contracts API - -#### Deploy Contract -```http -POST /api/v1/contracts/deploy -Content-Type: application/json -Authorization: Bearer your-api-key +**Response 200** — unsigned XDR +```json { - "contractType": "smart-swap", - "network": "testnet", - "parameters": { - "fee": "0.01", - "admin": "admin-address" - } + "xdr": "AAAAAgAAAA...", + "network": "testnet" } ``` -**Response:** +--- + +### POST /api/v1/defi/blend/withdraw + +Build an unsigned Blend withdrawal transaction. **Requires JWT.** + +**Request body** — same shape as `/blend/supply` + +--- + +### POST /api/v1/defi/blend/borrow + +Build an unsigned Blend borrow transaction. **Requires JWT.** + +**Request body** — same shape as `/blend/supply` + +--- + +### POST /api/v1/defi/blend/repay + +Build an unsigned Blend repay transaction. **Requires JWT.** + +**Request body** — same shape as `/blend/supply` + +--- + +## DeFi — Liquidity pools + +### POST /api/v1/defi/liquidity/add + +Build an unsigned Soroswap add-liquidity transaction. **Requires JWT.** + +**Request body** + ```json { - "id": "contract_def456", - "address": "contract-address", - "type": "smart-swap", - "network": "testnet", - "status": "deployed", - "createdAt": "2024-12-01T00:00:00Z" + "assetA": "XLM", + "assetB": "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "amountA": "1000", + "amountB": "142", + "signerPublicKey": "GABC123..." } ``` -#### Call Contract -```http -POST /api/v1/contracts/{contractId}/call -Content-Type: application/json -Authorization: Bearer your-api-key +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `assetA` | string | Yes | First asset in the pair | +| `assetB` | string | Yes | Second asset in the pair | +| `amountA` | string | Yes | Amount of assetA to deposit | +| `amountB` | string | Yes | Amount of assetB to deposit | +| `signerPublicKey` | string | Yes | Signer's Stellar public key | + +**Response 200** — unsigned XDR +```json { - "method": "swap", - "parameters": { - "fromAsset": "XLM", - "toAsset": "USDC", - "amount": "100" - } + "xdr": "AAAAAgAAAA...", + "network": "testnet" } ``` -#### Get Contract -```http -GET /api/v1/contracts/{contractId} -Authorization: Bearer your-api-key -``` +--- -#### List Contracts -```http -GET /api/v1/contracts?userId=user123&limit=10&offset=0 -Authorization: Bearer your-api-key -``` +### POST /api/v1/defi/liquidity/remove -### Automation API +Build an unsigned Soroswap remove-liquidity transaction. **Requires JWT.** -#### Create Automation Rule -```http -POST /api/v1/automation/rules -Content-Type: application/json -Authorization: Bearer your-api-key +**Request body** +```json { - "name": "Auto Buy XLM", - "trigger": { - "type": "price", - "condition": { - "asset": "XLM", - "operator": "less_than", - "value": "0.10" - } - }, - "action": { - "type": "buy", - "parameters": { - "asset": "XLM", - "amount": "100" - } - } + "assetA": "XLM", + "assetB": "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "poolAddress": "CPOOL...", + "lpAmount": "50", + "minAmountA": "490", + "minAmountB": "69", + "signerPublicKey": "GABC123..." } ``` -#### List Automation Rules -```http -GET /api/v1/automation/rules?userId=user123 -Authorization: Bearer your-api-key -``` +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `assetA` | string | Yes | First asset | +| `assetB` | string | Yes | Second asset | +| `poolAddress` | string | Yes | Soroswap pool contract address | +| `lpAmount` | string | Yes | LP token amount to redeem | +| `minAmountA` | string | No | Slippage floor for assetA | +| `minAmountB` | string | No | Slippage floor for assetB | +| `signerPublicKey` | string | Yes | Signer's Stellar public key | -#### Update Automation Rule -```http -PUT /api/v1/automation/rules/{ruleId} -Content-Type: application/json -Authorization: Bearer your-api-key +--- -{ - "name": "Updated Auto Buy XLM", - "enabled": true -} -``` +### GET /api/v1/defi/pools/analytics -#### Delete Automation Rule -```http -DELETE /api/v1/automation/rules/{ruleId} -Authorization: Bearer your-api-key -``` +Get liquidity pool analytics (TVL, spot prices, fee APR). No authentication required. -## 🔍 GraphQL API +**Query parameters** -### Endpoint -``` -https://api.galaxy-devkit.com/graphql -``` +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `poolAddress` | string | No | If omitted, returns analytics for all pools. If provided, returns data for that single pool. | -### Schema - -#### Queries - -```graphql -# Get user wallets -query GetWallets($userId: String!) { - wallets(userId: $userId) { - id - publicKey - network - balance { - asset - amount - limit - } - createdAt - } -} +**Response 200 — single pool** -# Get wallet transactions -query GetWalletTransactions($walletId: String!, $limit: Int) { - wallet(id: $walletId) { - transactions(limit: $limit) { - hash - source - destination - amount - asset - status - createdAt - } - } -} - -# Get smart contracts -query GetContracts($userId: String!) { - contracts(userId: $userId) { - id - address - type - network - status - createdAt - } +```json +{ + "poolAddress": "CPOOL...", + "tvl": "284000.00", + "spotPriceAtoB": "0.1423", + "spotPriceBtoA": "7.026", + "feeApr": "0.0312", + "volume24h": "45200.00" } ``` -#### Mutations +**Response 200 — all pools** (array of the above shape) -```graphql -# Create wallet -mutation CreateWallet($input: CreateWalletInput!) { - createWallet(input: $input) { - id - publicKey - network - createdAt - } -} +--- -# Send payment -mutation SendPayment($input: SendPaymentInput!) { - sendPayment(input: $input) { - id - hash - status - createdAt - } -} +## Wallets — Fee-sponsored transaction submission -# Deploy contract -mutation DeployContract($input: DeployContractInput!) { - deployContract(input: $input) { - id - address - type - status - createdAt - } -} -``` +### POST /api/v1/wallets/submit-tx -#### Subscriptions - -```graphql -# Real-time wallet updates -subscription WalletUpdates($walletId: String!) { - walletUpdated(walletId: $walletId) { - id - balance { - asset - amount - } - lastTransaction { - hash - amount - createdAt - } - } -} +Wrap a client-signed (fee-less) Soroban XDR in a fee-bump envelope using the server's sponsor account and submit it to Stellar. **No JWT required** (rate-limited by wallet ID + global limits). -# Real-time transaction updates -subscription TransactionUpdates($walletId: String!) { - transactionCreated(walletId: $walletId) { - hash - source - destination - amount - asset - status - createdAt - } -} +This is the submission endpoint for non-custodial smart wallet transactions. The backend signs only the fee-bump outer envelope — it never touches the inner transaction or user keys. -# Real-time contract events -subscription ContractEvents($contractId: String!) { - contractEvent(contractId: $contractId) { - id - event - data - createdAt - } +**Request body** + +```json +{ + "signedTxXdr": "AAAAAgAAAA...", + "walletId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } ``` -## 🔌 WebSocket API +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `signedTxXdr` | string | Yes | Base-64 encoded signed Soroban transaction XDR (must NOT already be a fee-bump) | +| `walletId` | string | Yes | UUID of the smart wallet in the `smart_wallets` table | -### Connection -```javascript -const ws = new WebSocket('wss://api.galaxy-devkit.com/ws'); +**Response 200** -ws.onopen = () => { - // Authenticate - ws.send(JSON.stringify({ - type: 'auth', - token: 'your-api-key' - })); -}; +```json +{ + "transactionHash": "a1b2c3d4...", + "ledger": 5012345 +} ``` -### Message Types +**Error 400** — missing/invalid body fields or XDR parse failure -#### Subscribe to Channel -```javascript -ws.send(JSON.stringify({ - type: 'subscribe', - channel: 'wallet:wallet123' -})); +```json +{ + "error": "Missing or invalid `signedTxXdr` — expected a base-64 XDR string" +} ``` -#### Unsubscribe from Channel -```javascript -ws.send(JSON.stringify({ - type: 'unsubscribe', - channel: 'wallet:wallet123' -})); +**Error 404** — wallet not found in `smart_wallets` table + +```json +{ + "error": "Wallet 3fa85f64-... not found in smart_wallets" +} ``` -#### Real-time Updates -```javascript -ws.onmessage = (event) => { - const data = JSON.parse(event.data); - - switch (data.type) { - case 'wallet_updated': - console.log('Wallet balance updated:', data.balance); - break; - case 'transaction_created': - console.log('New transaction:', data.transaction); - break; - case 'contract_event': - console.log('Contract event:', data.event); - break; - } -}; +**Error 502** — Stellar RPC submission error + +```json +{ + "error": "Stellar RPC submission failed" +} ``` -### Available Channels +--- -- `wallet:{walletId}` - Wallet updates -- `transactions:{walletId}` - Transaction updates -- `contract:{contractId}` - Contract events -- `automation:{userId}` - Automation updates +## Error response format -## ❌ Error Handling +All endpoints return errors in this envelope: -### Error Response Format ```json { "error": { - "code": "INVALID_REQUEST", - "message": "Invalid request parameters", - "details": { - "field": "amount", - "reason": "Must be a positive number" - } + "code": "VALIDATION_ERROR", + "message": "Human-readable message", + "details": {} } } ``` -### Error Codes +For the `submit-tx` endpoint, errors are a plain string field: -| Code | Description | -|------|-------------| -| `INVALID_REQUEST` | Invalid request parameters | -| `UNAUTHORIZED` | Invalid or missing API key | -| `FORBIDDEN` | Insufficient permissions | -| `NOT_FOUND` | Resource not found | -| `RATE_LIMITED` | Rate limit exceeded | -| `INTERNAL_ERROR` | Server error | +```json +{ "error": "Description" } +``` -## 🚦 Rate Limits +### Error codes -### Limits per API Key -- **REST API**: 1000 requests/hour -- **GraphQL**: 500 queries/hour -- **WebSocket**: 10 concurrent connections +| Code | HTTP status | Description | +|------|-------------|-------------| +| `VALIDATION_ERROR` | 400 | Missing or invalid request parameters | +| `UNAUTHORIZED` | 401 | Missing or invalid JWT | +| `NOT_FOUND` | 404 | Resource does not exist | +| `INTERNAL_ERROR` | 500 | Server-side error | +| *(plain string)* | 400 / 502 | Used by `submit-tx` endpoint | -### Headers -```http -X-RateLimit-Limit: 1000 -X-RateLimit-Remaining: 999 -X-RateLimit-Reset: 1640995200 -``` +--- -## 📝 SDK Examples +## Rate limiting -### TypeScript SDK -```typescript -import { GalaxySDK } from '@galaxy/sdk-typescript'; +The `submit-tx` endpoint applies two rate limiters: -const galaxy = new GalaxySDK({ - apiKey: 'your-api-key', - network: 'testnet' -}); +- **Per user** — enforced by `userSubmitTxLimiter` +- **Global** — enforced by `globalSubmitTxLimiter` -// Create wallet -const wallet = await galaxy.wallets.create({ - userId: 'user123' -}); +General DeFi endpoints use the shared `rateLimiterMiddleware` applied at the server level. -// Send payment -const payment = await galaxy.payments.send({ - from: wallet.publicKey, - to: 'destination-address', - amount: '10', - asset: 'XLM' -}); +--- + +## Environment variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `SUPABASE_URL` | Yes | Supabase project URL | +| `SUPABASE_SERVICE_ROLE_KEY` | Yes | Supabase service-role key (server only) | +| `STELLAR_RPC_URL` | No | Soroban RPC endpoint (default: testnet) | +| `STELLAR_HORIZON_URL` | No | Horizon endpoint (default: testnet) | +| `STELLAR_NETWORK_PASSPHRASE` | No | Network passphrase (default: testnet) | +| `FEE_SPONSOR_SECRET_KEY` | Yes (submit-tx) | Secret key of the fee-sponsor account | +| `FEE_BUMP_BASE_FEE` | No | Base fee for fee-bump txs in stroops (default: `1000000`) | +| `BLEND_POOL_ADDRESS` | No | Blend pool contract address | +| `BLEND_ORACLE_ADDRESS` | No | Blend oracle contract address | +| `SOROSWAP_ROUTER_ADDRESS` | No | Soroswap router contract address | +| `SOROSWAP_FACTORY_ADDRESS` | No | Soroswap factory contract address | +| `PORT` | No | HTTP server port (default: `3000`) | + +--- + +## WebSocket API + +The WebSocket server (`packages/api/websocket/`) is a Socket.io server available separately from the REST API. It provides real-time streams for market prices, transaction status, and automation events. + +**Connection** -// Subscribe to updates -galaxy.websocket.subscribe('wallet:wallet123', (update) => { - console.log('Wallet updated:', update); +```ts +import { io } from 'socket.io-client'; + +const socket = io('http://localhost:3001', { + auth: { token: supabaseJwt }, }); ``` -### Python SDK -```python -from galaxy_sdk import GalaxySDK - -galaxy = GalaxySDK( - api_key='your-api-key', - network='testnet' -) - -# Create wallet -wallet = galaxy.wallets.create( - user_id='user123' -) - -# Send payment -payment = galaxy.payments.send( - from_address=wallet.public_key, - to_address='destination-address', - amount='10', - asset='XLM' -) +**Market data channel** + +```ts +socket.emit('subscribe:market', { symbol: 'XLM/USD' }); +socket.on('market:price', (data) => console.log(data)); ``` -### JavaScript SDK -```javascript -import { GalaxySDK } from '@galaxy/sdk-javascript'; +**Transaction status channel** -const galaxy = new GalaxySDK({ - apiKey: 'your-api-key', - network: 'testnet' -}); +```ts +socket.emit('subscribe:transaction', { hash: 'abc123...' }); +socket.on('transaction:status', (data) => console.log(data.status)); +``` -// Create wallet -const wallet = await galaxy.wallets.create({ - userId: 'user123' -}); +**Automation events channel** -// Send payment -const payment = await galaxy.payments.send({ - from: wallet.publicKey, - to: 'destination-address', - amount: '10', - asset: 'XLM' -}); +```ts +socket.emit('subscribe:automation', { userId: 'user-abc' }); +socket.on('automation:triggered', (event) => console.log(event)); +socket.on('automation:executed', (result) => console.log(result)); ``` + +--- + +## Related docs + +- [Getting Started](../guides/getting-started.md) +- [Oracle Integration Guide](../guides/oracle-integration.md) — price feeds wired to automation +- [Social Login Integration Guide](../guides/social-login-integration.md) — WebAuthn + OAuth wallet onboarding +- [Smart Wallet Integration Guide](../smart-wallet/integration-guide.md) — producing signed XDR for `submit-tx` diff --git a/docs/architecture/defi-aggregation-flow.md b/docs/architecture/defi-aggregation-flow.md index b07e1fb..71881ef 100644 --- a/docs/architecture/defi-aggregation-flow.md +++ b/docs/architecture/defi-aggregation-flow.md @@ -6,6 +6,7 @@ This document shows how quote selection, wallet authorization, and submission fi ```mermaid flowchart LR + Oracle["OracleAggregator\n(CoinGecko / CMC)"] --> Router Request["Swap / route request"] --> Router["Aggregator or protocol router"] Router --> Soroswap["Soroswap adapter"] Router --> Blend["Blend / lending adapter"] @@ -16,6 +17,8 @@ flowchart LR Best --> Wallet["Smart wallet signing path"] Wallet --> Sponsor["Fee sponsor"] Sponsor --> Stellar["Stellar network"] + Oracle -.->|"price feeds"| Automation["AutomationService\n(price triggers)"] + Automation -.->|"triggers swap rule"| Request ``` ## End-to-End Sequence @@ -42,3 +45,12 @@ sequenceDiagram - High-frequency flows should avoid a biometric prompt on every swap. - Session keys let bots or automations sign repeated transactions within a bounded TTL. - Revocation and expiry keep those delegated rights time-limited. + +## Oracle node + +The `OracleAggregator` (shown at the top of the routing diagram) feeds two consumers: + +1. **Direct price queries** — applications call `aggregator.getAggregatedPrice('XLM/USD')` to get the current validated price before routing a swap. +2. **Automation price triggers** — `AutomationService` polls the oracle on each evaluation cycle; when a `PRICE` condition threshold is crossed the rule fires and injects a new swap request into the routing path. + +See the [Oracle Integration Guide](../guides/oracle-integration.md) for implementation details. diff --git a/docs/cli/oracle.md b/docs/cli/oracle.md index 04cf87b..a41ddad 100644 --- a/docs/cli/oracle.md +++ b/docs/cli/oracle.md @@ -266,4 +266,5 @@ Custom sources are stored in `.galaxy/oracles.json` in the current working direc ## Related - [Oracle Core Package](../../packages/core/oracles/README.md) +- [Oracle Integration Guide](../guides/oracle-integration.md) — off-chain feeds, on-chain Soroban oracle, and automation price triggers - [CLI Overview](./interactive.md) diff --git a/docs/contracts/deployment-runbook.md b/docs/contracts/deployment-runbook.md new file mode 100644 index 0000000..7129001 --- /dev/null +++ b/docs/contracts/deployment-runbook.md @@ -0,0 +1,352 @@ +# Smart Wallet Contract Deployment Runbook + +Step-by-step operational runbook for deploying the `smart-wallet-account` factory and wallet contracts from local development through testnet to mainnet, including fee-bump sponsor account setup. + +> Related: [Contract reference](./smart-wallet-contract.md) | [Existing deployment guide](./deployment.md) + +--- + +## Prerequisites + +### Tools + +```bash +# Rust toolchain with wasm target +rustup target add wasm32-unknown-unknown + +# Stellar CLI (includes Soroban support) — v21+ +cargo install --locked stellar-cli --features opt + +# Verify +stellar --version +``` + +### Accounts + +You need two funded accounts before deployment: + +| Account | Purpose | +|---------|---------| +| `deployer` | Signs contract upload and deployment transactions | +| `fee-sponsor` | Signs fee-bump outer envelopes for all user wallet operations | + +### Environment variables + +Copy `.env.example` to `.env.local` and fill in: + +```bash +# Stellar network +STELLAR_NETWORK=testnet +STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org +STELLAR_RPC_URL=https://soroban-testnet.stellar.org +STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" + +# Deployer identity (name registered with Stellar CLI) +DEPLOYER_IDENTITY=deployer + +# Fee-bump sponsor +FEE_SPONSOR_SECRET_KEY=S... # sponsor account secret key +FEE_BUMP_BASE_FEE=1000000 # 0.1 XLM in stroops + +# Filled in after deployment: +FACTORY_CONTRACT_ID= +WALLET_WASM_HASH= +``` + +--- + +## Step 1 — Create and fund accounts + +### Deployer account + +```bash +# Generate a new identity +stellar keys generate deployer --network testnet + +# Fund via Friendbot (testnet only) +stellar keys fund deployer --network testnet + +# Verify balance +stellar account info deployer --network testnet +``` + +### Fee-sponsor account + +The fee-sponsor account pays network fees for all non-custodial user wallet transactions. It must maintain a sufficient XLM balance. + +```bash +# Generate sponsor keypair (save the secret key — you will need it in .env) +stellar keys generate fee-sponsor --network testnet +stellar keys fund fee-sponsor --network testnet + +# Export and store the secret key in .env.local as FEE_SPONSOR_SECRET_KEY +stellar keys show fee-sponsor +``` + +**Recommended minimum balance:** 100 XLM on testnet; 500 XLM on mainnet (monitor and replenish). + +--- + +## Step 2 — Build the contracts + +From the repository root: + +```bash +cd packages/contracts/smart-wallet-account + +# Build all contracts in the workspace (wallet + factory) +cargo build --target wasm32-unknown-unknown --release + +# Or use the Stellar CLI build command (writes to target/wasm32-unknown-unknown/release/) +stellar contract build +``` + +Expected output files: + +``` +target/wasm32-unknown-unknown/release/smart_wallet_account.wasm +target/wasm32-unknown-unknown/release/factory.wasm +``` + +--- + +## Step 3 — Upload the wallet WASM + +The wallet WASM must be uploaded first. The factory stores its hash so it can deploy new wallet instances deterministically. + +```bash +# Upload wallet WASM — note the returned WASM hash +WALLET_WASM_HASH=$(stellar contract upload \ + --wasm target/wasm32-unknown-unknown/release/smart_wallet_account.wasm \ + --source deployer \ + --network testnet) + +echo "WALLET_WASM_HASH=$WALLET_WASM_HASH" +``` + +Save `WALLET_WASM_HASH` to `.env.local`. + +--- + +## Step 4 — Deploy the factory contract + +```bash +# Deploy the factory contract +FACTORY_CONTRACT_ID=$(stellar contract deploy \ + --wasm target/wasm32-unknown-unknown/release/factory.wasm \ + --source deployer \ + --network testnet) + +echo "FACTORY_CONTRACT_ID=$FACTORY_CONTRACT_ID" +``` + +Save `FACTORY_CONTRACT_ID` to `.env.local`. + +--- + +## Step 5 — Initialize the factory + +Register the wallet WASM hash with the factory. This can only be called once. + +```bash +stellar contract invoke \ + --id "$FACTORY_CONTRACT_ID" \ + --source deployer \ + --network testnet \ + -- init \ + --wallet_wasm_hash "$WALLET_WASM_HASH" +``` + +Verify initialization succeeded: + +```bash +# Should return the stored wasm hash +stellar contract invoke \ + --id "$FACTORY_CONTRACT_ID" \ + --source deployer \ + --network testnet \ + -- get_wallet \ + --credential_id "dGVzdA==" # base64("test") — expect null/None +``` + +--- + +## Step 6 — Deploy a test wallet instance + +Verify the factory can deploy a wallet by running a test deployment with a dummy credential. + +```bash +# Base64-encode a test credential ID +TEST_CREDENTIAL=$(echo -n "test-credential-001" | base64) + +# A test P-256 public key (65 bytes, 0x04 prefix + 32-byte x + 32-byte y) +# For a real deployment, supply the actual WebAuthn public key +TEST_PUBLIC_KEY="04$(python3 -c "import os; print(os.urandom(64).hex())")" + +stellar contract invoke \ + --id "$FACTORY_CONTRACT_ID" \ + --source deployer \ + --network testnet \ + -- deploy \ + --deployer deployer \ + --credential_id "$TEST_CREDENTIAL" \ + --public_key "$TEST_PUBLIC_KEY" +``` + +The returned address is the new wallet contract. Verify it is registered: + +```bash +stellar contract invoke \ + --id "$FACTORY_CONTRACT_ID" \ + --source deployer \ + --network testnet \ + -- get_wallet \ + --credential_id "$TEST_CREDENTIAL" +``` + +--- + +## Step 7 — Fee-bump sponsor setup + +The `POST /api/v1/wallets/submit-tx` endpoint wraps every user-signed XDR in a fee-bump transaction before submitting to the Stellar network. This lets users transact without holding XLM. + +### How it works + +1. Client builds and signs a **fee-less** Soroban transaction (base fee = 0). +2. Client POSTs the signed XDR + wallet ID to `/api/v1/wallets/submit-tx`. +3. The server reads `FEE_SPONSOR_SECRET_KEY` from the environment. +4. The server wraps the inner transaction in a `FeeBumpTransaction` and signs the outer envelope. +5. The fee-bump transaction is submitted to Soroban RPC. +6. The sponsor account pays all fees. The user pays nothing. + +### Configure the Galaxy DevKit API server + +In your server environment (`.env.local` or deployment secrets): + +```bash +FEE_SPONSOR_SECRET_KEY=S... # sponsor account Stellar secret +FEE_BUMP_BASE_FEE=1000000 # 0.1 XLM per tx (adjust for mainnet fee pressure) +STELLAR_RPC_URL=https://soroban-testnet.stellar.org +STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" +``` + +### Monitoring the sponsor balance + +Set up an alert to replenish the sponsor account before it runs dry: + +```bash +# Check sponsor balance +stellar account info fee-sponsor --network testnet | grep XLM +``` + +A depleted sponsor account will cause all `submit-tx` calls to return `502`. Keep at least 50 XLM buffer on testnet, 200 XLM on mainnet. + +--- + +## Step 8 — Testnet smoke test + +Run the end-to-end integration test against your deployed contracts: + +```bash +# Set env vars before running +export FACTORY_CONTRACT_ID="..." +export WALLET_WASM_HASH="..." +export STELLAR_RPC_URL="https://soroban-testnet.stellar.org" +export FEE_SPONSOR_SECRET_KEY="S..." + +# Run the e2e test suite +npx jest packages/core/wallet/src/tests/smart-wallet.service.test.ts --testNamePattern="deploy" + +# Or run the full e2e suite (requires Playwright + virtual authenticator) +npx playwright test packages/core/wallet/src/tests/smart-wallet.e2e.test.ts +``` + +Expected output: all deploy, sign, and submit steps pass. + +--- + +## Step 9 — Mainnet promotion checklist + +Before deploying to mainnet, complete every item: + +- [ ] All testnet smoke tests pass with the final WASM builds +- [ ] WASM files are compiled with `--release` flag +- [ ] `FACTORY_CONTRACT_ID` and `WALLET_WASM_HASH` recorded in internal runbook / secrets manager +- [ ] Fee-sponsor account funded with ≥ 500 XLM +- [ ] Fee-sponsor account has **no other signers** — it should be a dedicated account +- [ ] `FEE_BUMP_BASE_FEE` adjusted for current mainnet fee pressure (check `stellar network status --network mainnet`) +- [ ] `STELLAR_NETWORK_PASSPHRASE` set to `"Public Global Stellar Network ; September 2015"` +- [ ] `STELLAR_RPC_URL` pointed at a reliable mainnet Soroban RPC endpoint +- [ ] Rate-limit configuration on `submit-tx` endpoint reviewed +- [ ] Alert configured on sponsor account balance +- [ ] Deployment commands reviewed and confirmed by a second engineer + +**Deploy to mainnet (same steps as testnet, with `--network mainnet`):** + +```bash +stellar contract upload \ + --wasm target/wasm32-unknown-unknown/release/smart_wallet_account.wasm \ + --source deployer \ + --network mainnet + +stellar contract deploy \ + --wasm target/wasm32-unknown-unknown/release/factory.wasm \ + --source deployer \ + --network mainnet + +stellar contract invoke \ + --id "$FACTORY_CONTRACT_ID" \ + --source deployer \ + --network mainnet \ + -- init \ + --wallet_wasm_hash "$WALLET_WASM_HASH" +``` + +--- + +## Environment variable reference + +| Variable | Required | Description | +|----------|----------|-------------| +| `FACTORY_CONTRACT_ID` | Yes | Deployed factory contract address (C...) | +| `WALLET_WASM_HASH` | Yes | Uploaded wallet WASM hash (hex) | +| `FEE_SPONSOR_SECRET_KEY` | Yes | Stellar secret key (S...) of the fee-sponsor account | +| `FEE_BUMP_BASE_FEE` | No | Base fee in stroops for fee-bump txs (default: `1000000` = 0.1 XLM) | +| `STELLAR_RPC_URL` | No | Soroban RPC endpoint (default: testnet) | +| `STELLAR_HORIZON_URL` | No | Horizon endpoint (default: testnet) | +| `STELLAR_NETWORK_PASSPHRASE` | No | Network passphrase (default: testnet) | +| `SUPABASE_URL` | Yes (API) | Supabase project URL | +| `SUPABASE_SERVICE_ROLE_KEY` | Yes (API) | Supabase service-role key | + +--- + +## Troubleshooting + +### `Error: FEE_SPONSOR_SECRET_KEY is not configured` + +The API server is missing the sponsor secret. Add it to your environment and restart. + +### `Wallet not found in smart_wallets` + +The `walletId` sent to `submit-tx` does not exist in the `smart_wallets` Supabase table. The wallet must be registered in the database before transactions can be sponsored. + +### `XDR is already a fee-bump transaction` + +The client sent a pre-wrapped fee-bump. The `submit-tx` endpoint accepts only inner (non-fee-bump) signed transactions. + +### Factory `init` fails with `AlreadyInitialized` + +`init` was already called. The factory is deployed and ready — skip to Step 6. + +### `Insufficient sources` from OracleAggregator during automation + +The oracle cannot reach at least `minSources` live sources. Check `CoinGeckoSource` rate limits and API key configuration. + +--- + +## Related docs + +- [Contract reference](./smart-wallet-contract.md) — factory and wallet contract API +- [Deployment guide](./deployment.md) — original deployment guide (deploy script) +- [Smart Wallet Integration Guide](../smart-wallet/integration-guide.md) — TypeScript SDK usage +- [REST API Reference](../api/api-reference.md) — `submit-tx` endpoint diff --git a/docs/contracts/deployment.md b/docs/contracts/deployment.md index 63847e0..af34ff2 100644 --- a/docs/contracts/deployment.md +++ b/docs/contracts/deployment.md @@ -95,5 +95,6 @@ sequenceDiagram ## Related Docs - [Contract reference](./smart-wallet-contract.md) +- [Deployment runbook](./deployment-runbook.md) — full step-by-step guide including fee-bump sponsor setup and mainnet promotion checklist - [Package README](../../packages/contracts/smart-wallet-account/README.md) - [Stellar testnet docs](https://developers.stellar.org/docs/networks/testnet) diff --git a/docs/contracts/smart-wallet-contract.md b/docs/contracts/smart-wallet-contract.md index ad954ee..da12b43 100644 --- a/docs/contracts/smart-wallet-contract.md +++ b/docs/contracts/smart-wallet-contract.md @@ -92,7 +92,39 @@ flowchart LR - `SmartWalletService.addSigner()` and `removeSigner()` build Soroban invocations and return fee-less XDR for sponsorship. - The session credential ID should be stable across registration, signing, and revocation. In the TypeScript integration this is derived from the raw session public key bytes. +## Fee-bump sponsor architecture + +Galaxy DevKit uses a **fee-bump sponsor** pattern so users never need to hold XLM to pay transaction fees. + +``` +Client (browser) Server (Galaxy DevKit API) +────────────────── ───────────────────────── +SmartWalletService.sign() POST /api/v1/wallets/submit-tx + │ │ + │ 1. Simulate tx → authEntry │ + │ 2. WebAuthn assertion │ + │ 3. Attach signature to authEntry │ + │ 4. Build fee-less XDR (fee = 0) │ + │ │ + └──── signedTxXdr ───────────────────► │ + │ 5. Parse inner tx + │ 6. buildFeeBumpTransaction(sponsorKeypair, fee, innerTx) + │ 7. feeBumpTx.sign(sponsorKeypair) + │ 8. Submit to Soroban RPC + │ 9. Poll until confirmed + │ + ◄── { transactionHash, ledger } +``` + +**Security properties:** +- The backend signs only the outer fee-bump envelope with `FEE_SPONSOR_SECRET_KEY`. +- The backend never sees, derives, or touches the user's private key. +- The inner transaction (user's signed operation) is unmodified. + +See the [Deployment Runbook](./deployment-runbook.md) for sponsor account setup and the [REST API Reference](../api/api-reference.md) for the `submit-tx` endpoint. + ## Useful External References - [Stellar smart contracts docs](https://developers.stellar.org/docs/build/smart-contracts/overview) - [Soroban CLI docs](https://developers.stellar.org/docs/tools/soroban-cli) +- [Deployment runbook](./deployment-runbook.md) — step-by-step deploy guide with fee-bump sponsor setup diff --git a/docs/guides/oracle-integration.md b/docs/guides/oracle-integration.md new file mode 100644 index 0000000..0c6941c --- /dev/null +++ b/docs/guides/oracle-integration.md @@ -0,0 +1,398 @@ +# Oracle Integration Guide + +This guide explains how the three Oracle layers in Galaxy DevKit fit together: **off-chain price fetching** → **on-chain Soroban oracle** → **Automation price-trigger rules**. + +## Architecture overview + +``` +┌─────────────────────────────────────────────────────┐ +│ Off-chain layer │ +│ CoinGeckoSource / CoinMarketCapSource │ +│ OracleAggregator (median, TWAP, weighted, mean) │ +│ In-memory cache (TTL 30 s), circuit breaker │ +└────────────────────────┬────────────────────────────┘ + │ AggregatedPrice + ▼ +┌─────────────────────────────────────────────────────┐ +│ On-chain layer (Soroban oracle contract) │ +│ Prices written on-chain for Soroban dApps │ +│ Read via oracle.getPrice('XLM/USDC') │ +└────────────────────────┬────────────────────────────┘ + │ price threshold check + ▼ +┌─────────────────────────────────────────────────────┐ +│ Automation layer │ +│ AutomationService + ConditionEvaluator │ +│ PRICE trigger → fires rule when threshold crossed │ +└─────────────────────────────────────────────────────┘ +``` + +## Layer 1 — Off-chain price feeds + +`OracleAggregator` polls one or more off-chain sources, validates the data, removes statistical outliers, and returns a single `AggregatedPrice`. + +### Supported assets (CoinGecko source) + +`XLM`, `BTC`, `ETH`, `USDC`, `USDT`, `SOL`, `ADA`, `DOT`, `AVAX`, `LINK`, `UNI`, `ATOM`, `DOGE`, `MATIC`, `LTC`, `BCH`, `XRP`, `ALGO`, `NEAR` and more — see `CoinGeckoSource` for the full mapping. + +### Quickstart + +```ts +import { OracleAggregator } from '@galaxy-kj/core-oracles'; +import { CoinGeckoSource } from '@galaxy-kj/core-oracles/sources/real/CoinGeckoSource'; +import { MedianStrategy } from '@galaxy-kj/core-oracles/aggregator/strategies/MedianStrategy'; + +// Build the aggregator +const aggregator = new OracleAggregator( + { + minSources: 1, // require at least 1 live source + maxDeviationPercent: 10, // discard prices >10% from median + maxStalenessMs: 60_000, // reject prices older than 60 s + enableOutlierDetection: true, + outlierThreshold: 2.0, // Z-score threshold + }, + { ttlMs: 30_000 } // cache TTL +); + +// Register a source (optionally supply a CoinGecko API key for higher rate limits) +aggregator.addSource(new CoinGeckoSource(process.env.COINGECKO_API_KEY), 1.0); + +// Fetch an aggregated price +const price = await aggregator.getAggregatedPrice('XLM/USD'); +console.log(price.price); // e.g. 0.1234 +console.log(price.confidence); // 0–1 based on source agreement +console.log(price.sourcesUsed); // ['coingecko'] +``` + +### Aggregation strategies + +| Strategy | Class | Description | +|----------|-------|-------------| +| `median` | `MedianStrategy` | Median of all source prices (default, outlier-resistant) | +| `mean` | `MeanStrategy` | Simple arithmetic average | +| `weighted` | `WeightedAverageStrategy` | Weighted by per-source `weight` values | +| `twap` | `TWAPStrategy` | Time-weighted average; uses recency of each sample | + +```ts +import { TWAPStrategy } from '@galaxy-kj/core-oracles/aggregator/strategies/TWAPStrategy'; + +aggregator.setStrategy(new TWAPStrategy()); +const twapPrice = await aggregator.getAggregatedPrice('XLM/USD'); +``` + +### Adding multiple sources + +```ts +import { CoinGeckoSource } from '@galaxy-kj/core-oracles/sources/real/CoinGeckoSource'; +import { CoinMarketCapSource } from '@galaxy-kj/core-oracles/sources/real/CoinMarketCapSource'; + +aggregator.addSource(new CoinGeckoSource(), 1.0); +aggregator.addSource(new CoinMarketCapSource(process.env.CMC_API_KEY), 0.8); + +// At least minSources must respond for a successful aggregation +const price = await aggregator.getAggregatedPrice('BTC/USD'); +``` + +### Caching behaviour + +Results are cached in memory. Within the `maxStalenessMs` window (default 60 s), subsequent calls return the cached value without hitting remote APIs. Call `aggregator.clearCache()` to force a fresh fetch. + +### Circuit breaker + +Each source has an independent circuit breaker. After `failureThreshold` (default 5) consecutive errors the source is placed in `OPEN` state and skipped. After `resetTimeoutMs` (default 60 s) it transitions to `HALF_OPEN` and will be retried. + +```ts +const health = await aggregator.getSourceHealth(); +// Map e.g. { coingecko: true, coinmarketcap: false } +``` + +### CLI equivalent + +```bash +galaxy oracle price XLM/USD --strategy median +galaxy oracle price XLM/USD --strategy twap --watch 10s +galaxy oracle sources list +galaxy oracle validate XLM/USD --threshold 5 +``` + +See [Oracle CLI reference](../cli/oracle.md) for the full command reference. + +--- + +## Layer 2 — On-chain Soroban oracle + +> **Status:** The on-chain Soroban oracle contract is tracked in issue #167. The TypeScript interface described here matches the planned contract surface. + +The on-chain oracle stores a price feed on Stellar/Soroban so that other Soroban contracts can read price data directly without an off-chain call. + +### Contract interface + +```rust +// Soroban oracle contract (planned — issue #167) +pub fn set_price(env: Env, asset: Symbol, price: i128, timestamp: u64); +pub fn get_price(env: Env, asset: Symbol) -> Option; +pub fn get_prices(env: Env, assets: Vec) -> Vec>; +``` + +### TypeScript SDK usage (planned) + +```ts +import { OracleAggregator } from '@galaxy-kj/core-oracles'; + +// Once the on-chain oracle contract is deployed, the SDK will expose: +const price = await oracle.getPrice('XLM/USDC'); +// Returns: { price: number, timestamp: Date, confidence: number } +``` + +### Update cadence + +The off-chain aggregator (Layer 1) fetches prices and writes the result to the Soroban oracle contract on a configurable interval. A typical production setup might update every 5–30 seconds, batching multiple assets in one transaction to minimise fees. + +--- + +## Layer 3 — Automation price-trigger rules + +`AutomationService` integrates directly with `OracleAggregator` to evaluate price conditions and fire automated Stellar operations when thresholds are crossed. + +### Price trigger rule shape + +```ts +import { + AutomationRule, + AutomationStatus, + TriggerType, + ConditionLogic, + ConditionOperator, + ExecutionType, +} from '@galaxy-kj/core-automation'; + +const priceRule: AutomationRule = { + id: 'rule-xlm-swap-001', + name: 'Swap when XLM/USDC > 0.15', + userId: 'user-abc', + status: AutomationStatus.ACTIVE, + triggerType: TriggerType.PRICE, + + conditionGroup: { + logic: ConditionLogic.AND, + conditions: [ + { + type: 'price', + id: 'cond-1', + asset: 'XLM', + operator: ConditionOperator.GREATER_THAN, + threshold: 0.15, + quoteAsset: 'USD', + }, + ], + }, + + executionType: ExecutionType.STELLAR_SWAP, + executionConfig: { + swapConfig: { + sendAsset: { code: 'XLM' }, + sendMax: '500', + destinationAsset: { + code: 'USDC', + issuer: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN', + }, + destinationAmount: '75', + destinationAccount: 'GDESTINATION...', + }, + retryAttempts: 3, + retryDelay: 5000, + memo: 'auto-swap', + }, + + createdAt: new Date(), + updatedAt: new Date(), + executionCount: 0, + failureCount: 0, +}; +``` + +### Wiring the oracle to AutomationService + +Pass your configured `OracleAggregator` instance to `AutomationService` so that price-condition evaluation uses live prices: + +```ts +import { OracleAggregator } from '@galaxy-kj/core-oracles'; +import { CoinGeckoSource } from '@galaxy-kj/core-oracles/sources/real/CoinGeckoSource'; +import { AutomationService } from '@galaxy-kj/core-automation'; + +const aggregator = new OracleAggregator({ minSources: 1 }); +aggregator.addSource(new CoinGeckoSource(process.env.COINGECKO_API_KEY)); + +const automation = new AutomationService({ + network: { + type: 'TESTNET', + horizonUrl: 'https://horizon-testnet.stellar.org', + networkPassphrase: 'Test SDF Network ; September 2015', + }, + sourceSecret: process.env.AUTOMATION_SIGNER_SECRET!, + oracle: aggregator, // <-- inject oracle here +}); +``` + +### Registering and running the rule + +```ts +// Register the rule +await automation.registerRule(priceRule); + +// The service evaluates PRICE-trigger rules on a polling interval +// (configurable; default is driven by the cron scheduler). +// You can also trigger evaluation manually: +automation.emit('evaluate', { ruleId: priceRule.id }); +``` + +### Listening for execution results + +```ts +automation.on('execution:success', (result) => { + console.log('Rule fired:', result.ruleId); + console.log('TX hash:', result.transactionHash); +}); + +automation.on('execution:failure', (result) => { + console.error('Rule failed:', result.ruleId, result.error?.message); +}); +``` + +--- + +## End-to-end example — trigger a swap when XLM/USDC > 0.15 + +The following self-contained example runs on testnet. + +```ts +import { OracleAggregator } from '@galaxy-kj/core-oracles'; +import { CoinGeckoSource } from '@galaxy-kj/core-oracles/sources/real/CoinGeckoSource'; +import { AutomationService } from '@galaxy-kj/core-automation'; +import { + AutomationRule, + AutomationStatus, + TriggerType, + ConditionLogic, + ConditionOperator, + ExecutionType, +} from '@galaxy-kj/core-automation/types/automation-types'; + +async function main() { + // ── 1. Build oracle ─────────────────────────────────────────────── + const oracle = new OracleAggregator({ minSources: 1, maxStalenessMs: 30_000 }); + oracle.addSource(new CoinGeckoSource()); + + // Quick sanity check + const current = await oracle.getAggregatedPrice('XLM/USD'); + console.log('Current XLM/USD:', current.price); + + // ── 2. Build automation service ─────────────────────────────────── + const automation = new AutomationService({ + network: { + type: 'TESTNET', + horizonUrl: 'https://horizon-testnet.stellar.org', + networkPassphrase: 'Test SDF Network ; September 2015', + }, + sourceSecret: process.env.AUTOMATION_SIGNER_SECRET!, + oracle, + }); + + // ── 3. Define the rule ──────────────────────────────────────────── + const rule: AutomationRule = { + id: 'e2e-swap-rule', + name: 'Swap XLM → USDC when price exceeds 0.15', + userId: 'demo-user', + status: AutomationStatus.ACTIVE, + triggerType: TriggerType.PRICE, + conditionGroup: { + logic: ConditionLogic.AND, + conditions: [ + { + type: 'price', + id: 'xlm-price-check', + asset: 'XLM', + operator: ConditionOperator.GREATER_THAN, + threshold: 0.15, + }, + ], + }, + executionType: ExecutionType.STELLAR_SWAP, + executionConfig: { + swapConfig: { + sendAsset: {}, // native XLM + sendMax: '100', + destinationAsset: { + code: 'USDC', + issuer: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5', // testnet + }, + destinationAmount: '14', + destinationAccount: process.env.DESTINATION_ADDRESS!, + }, + retryAttempts: 2, + memo: 'oracle-triggered-swap', + }, + createdAt: new Date(), + updatedAt: new Date(), + executionCount: 0, + failureCount: 0, + maxExecutions: 1, // fire once then stop + }; + + // ── 4. Register and listen ──────────────────────────────────────── + await automation.registerRule(rule); + + automation.on('execution:success', (result) => { + console.log('Swap executed! TX:', result.transactionHash); + process.exit(0); + }); + + automation.on('execution:failure', (result) => { + console.error('Swap failed:', result.error?.message); + process.exit(1); + }); + + console.log('Watching XLM/USD price. Will swap when > 0.15 …'); +} + +main().catch(console.error); +``` + +Run on testnet: + +```bash +AUTOMATION_SIGNER_SECRET=S... DESTINATION_ADDRESS=G... npx ts-node e2e-oracle-swap.ts +``` + +--- + +## Configuration reference + +### OracleAggregator options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `minSources` | `number` | `2` | Minimum live sources required for a valid price | +| `maxDeviationPercent` | `number` | `10` | Max % spread allowed between sources | +| `maxStalenessMs` | `number` | `60000` | Reject cached entries older than this | +| `enableOutlierDetection` | `boolean` | `true` | Filter statistical outliers via Z-score | +| `outlierThreshold` | `number` | `2.0` | Z-score threshold for outlier removal | + +### AutomationService options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `network` | `StellarNetwork` | TESTNET | Horizon URL + network passphrase | +| `sourceSecret` | `string` | — | Stellar secret key used to sign automation transactions | +| `oracle` | `OracleAggregator` | — | Oracle instance for PRICE trigger evaluation | +| `maxConcurrentExecutions` | `number` | `10` | Max parallel rule executions | +| `executionTimeout` | `number` | `300000` | Per-execution timeout in ms | + +--- + +## Related docs + +- [Oracle CLI reference](../cli/oracle.md) — `galaxy oracle price`, `history`, `validate` +- [Automation types](../../packages/core/automation/src/types/automation-types.ts) — full type definitions +- [DeFi aggregation flow](../architecture/defi-aggregation-flow.md) — how oracle feeds connect to the swap routing layer +- [Guides index](./index.md) diff --git a/docs/guides/social-login-integration.md b/docs/guides/social-login-integration.md new file mode 100644 index 0000000..f8a05da --- /dev/null +++ b/docs/guides/social-login-integration.md @@ -0,0 +1,337 @@ +# Social Login Integration Guide + +This guide explains how to use `SocialLoginProvider` to onboard users from an OAuth provider (Supabase Auth, Auth0, Google OAuth) into a non-custodial Stellar wallet backed by a WebAuthn passkey — without the backend ever touching private key material. + +## Two-layer security model + +`SocialLoginProvider` is **not** a standard OAuth library and is **not** a standard WebAuthn library. It is the bridge between them. + +| Layer | Purpose | Who verifies it | +|-------|---------|----------------| +| OAuth JWT | Identifies the user (email / social account) | Your backend + Supabase / Auth0 | +| WebAuthn passkey | Protects the Stellar private key on-device | The client device (TPM / Secure Enclave) | + +These two layers are **independent**. The OAuth token never touches the key. The private key never leaves the device. + +``` +┌──────────────────────────────────────────────────────────┐ +│ OAuth layer (identity) │ +│ Google/Auth0/Supabase → JWT → your backend │ +│ Backend extracts userId and passes it to client │ +└──────────────────────────────────────────────────────────┘ + │ userId only (no token forwarded to WebAuthn) + ▼ +┌──────────────────────────────────────────────────────────┐ +│ WebAuthn layer (key protection) │ +│ navigator.credentials.create / .get │ +│ Platform authenticator (Touch ID, Windows Hello, …) │ +│ Private key lives in TEE / Secure Enclave — never sent │ +└──────────────────────────────────────────────────────────┘ + │ publicKey65Bytes (safe to store), credentialId + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Stellar layer (wallet) │ +│ SmartWalletService uses credential to sign Soroban txs │ +└──────────────────────────────────────────────────────────┘ +``` + +## Sequence diagrams + +### Onboarding (first login) + +```mermaid +sequenceDiagram + participant User + participant Browser + participant OAuthProvider as OAuth Provider + participant Backend as Your Backend (Supabase) + participant Authenticator as Platform Authenticator + + User->>OAuthProvider: Sign in with Google / email + OAuthProvider-->>Backend: JWT (identity verified) + Backend-->>Browser: userId extracted from JWT + Browser->>Authenticator: navigator.credentials.create (WebAuthn registration) + Authenticator-->>Browser: PublicKeyCredential (credentialId + publicKey) + Browser->>Backend: Store { userId, credentialId, publicKey65Bytes, network } + Note over Backend: Private key NEVER sent — only public key stored +``` + +### Returning user (login) + +```mermaid +sequenceDiagram + participant User + participant Browser + participant OAuthProvider as OAuth Provider + participant Backend as Your Backend (Supabase) + participant Authenticator as Platform Authenticator + + User->>OAuthProvider: Sign in with Google / email + OAuthProvider-->>Backend: JWT verified + Backend-->>Browser: userId + Browser->>Authenticator: navigator.credentials.get (WebAuthn assertion) + Authenticator-->>Browser: Signed assertion + Browser->>Backend: Verify { userId, credentialId } + Backend-->>Browser: Session authorized +``` + +## Installation + +```bash +npm install @galaxy-kj/core-wallet +``` + +## Setting up the providers + +```ts +import { WebAuthNProvider } from '@galaxy-kj/core-wallet/auth/providers/WebAuthNProvider'; +import { SocialLoginProvider } from '@galaxy-kj/core-wallet/auth/providers/SocialLoginProvider'; + +const webAuthnProvider = new WebAuthNProvider({ + rpId: 'yourdomain.com', // must match window.location.hostname in production + rpName: 'Your App Name', +}); + +const socialLogin = new SocialLoginProvider(webAuthnProvider); +``` + +## Onboarding flow + +Call `onboard(userId)` after your OAuth provider has authenticated the user and you have extracted their `userId`. This registers a new WebAuthn passkey on the device. + +```ts +// After Supabase / OAuth sign-in succeeds and you have the userId: +async function onboardUser(userId: string) { + const result = await socialLogin.onboard(userId); + // result = { userId, credentialId, publicKey65Bytes } + + // Store these in Supabase — publicKey65Bytes is safe to persist + await supabase.from('smart_wallets').insert({ + user_id: result.userId, + credential_id: result.credentialId, + public_key: Buffer.from(result.publicKey65Bytes).toString('base64'), + network: 'testnet', + }); + + return result; +} +``` + +**What to store in Supabase:** + +| Column | Value | Notes | +|--------|-------|-------| +| `user_id` | `result.userId` | From OAuth provider | +| `credential_id` | `result.credentialId` | WebAuthn credential identifier | +| `public_key` | `Buffer.from(result.publicKey65Bytes).toString('base64')` | 65-byte P-256 uncompressed point — safe to store | +| `network` | `'testnet'` or `'mainnet'` | Stellar network | + +**Never store:** the private key, the seed phrase, or any derived secret. The backend has no access to these and should not. + +## Login flow + +Call `login(userId)` on subsequent visits after OAuth re-authentication. This asserts the existing passkey. + +```ts +async function loginUser(userId: string) { + const result = await socialLogin.login(userId); + // result = { userId, credentialId } + + // Verify that credentialId belongs to userId in your backend + const { data: wallet } = await supabase + .from('smart_wallets') + .select('credential_id, public_key') + .eq('user_id', result.userId) + .eq('credential_id', result.credentialId) + .single(); + + if (!wallet) { + throw new Error('Unknown credential — passkey not registered for this account'); + } + + // User is now authenticated at both layers + return { userId: result.userId, credentialId: result.credentialId }; +} +``` + +## Full Supabase example + +The example below wires together Supabase Auth (Google OAuth) with `SocialLoginProvider`. + +```ts +import { createClient } from '@supabase/supabase-js'; +import { WebAuthNProvider } from '@galaxy-kj/core-wallet/auth/providers/WebAuthNProvider'; +import { SocialLoginProvider } from '@galaxy-kj/core-wallet/auth/providers/SocialLoginProvider'; +import { SmartWalletService } from '@galaxy-kj/core-wallet'; + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!); + +const webAuthnProvider = new WebAuthNProvider({ rpId: window.location.hostname }); +const socialLogin = new SocialLoginProvider(webAuthnProvider); + +// ── Sign-in / sign-up handler ───────────────────────────────────────────────── +async function handleAuthCallback() { + // 1. Finish OAuth exchange + const { data: { session }, error } = await supabase.auth.getSession(); + if (error || !session) throw new Error('OAuth session missing'); + + const userId = session.user.id; // Supabase UUID + + // 2. Check whether this user already has a passkey registered + const { data: existing } = await supabase + .from('smart_wallets') + .select('id') + .eq('user_id', userId) + .maybeSingle(); + + if (existing) { + // Returning user — assert passkey + const loginResult = await socialLogin.login(userId); + console.log('Authenticated credential:', loginResult.credentialId); + } else { + // New user — register passkey and create wallet record + const onboardResult = await socialLogin.onboard(userId); + + await supabase.from('smart_wallets').insert({ + user_id: onboardResult.userId, + credential_id: onboardResult.credentialId, + public_key: Buffer.from(onboardResult.publicKey65Bytes).toString('base64'), + network: 'testnet', + }); + + console.log('Wallet created for', userId); + } +} +``` + +## Provider examples + +### Auth0 + +```ts +// After Auth0 redirect callback: +const { user } = await auth0Client.handleRedirectCallback(); +const userId = user.sub; // Auth0 subject — stable per user + +const result = await socialLogin.onboard(userId); +``` + +### Google OAuth (raw) + +```ts +// After Google sign-in returns an ID token and you decode it server-side: +// Send userId from your backend to the client +const result = await socialLogin.onboard(googleUserId); +``` + +## Backend server-side verification + +For sensitive operations your backend should verify **both** layers before authorising a request. + +```ts +// Pseudocode — adapt to your framework +async function verifyDualAuth(jwtToken: string, credentialId: string) { + // Layer 1: verify OAuth JWT + const payload = verifyJwt(jwtToken, SUPABASE_JWT_SECRET); + const userId = payload.sub; + + // Layer 2: verify credentialId belongs to this user + const { data: wallet } = await supabase + .from('smart_wallets') + .select('credential_id') + .eq('user_id', userId) + .eq('credential_id', credentialId) + .single(); + + if (!wallet) throw new Error('Credential does not belong to this user'); + + return { userId, credentialId }; +} +``` + +For WebAuthn assertion verification (e.g., from a relay flow), use `@simplewebauthn/server` on the Node.js backend: + +```ts +import { verifyAuthenticationResponse } from '@simplewebauthn/server'; + +const verification = await verifyAuthenticationResponse({ + response: clientAuthResponse, + expectedChallenge, + expectedOrigin: 'https://yourdomain.com', + expectedRPID: 'yourdomain.com', + authenticator: { + credentialPublicKey: storedPublicKeyBytes, + credentialID: Buffer.from(storedCredentialId, 'base64'), + counter: storedCounter, + }, +}); +``` + +## Security guarantees + +| Property | Guarantee | +|----------|-----------| +| Private key stays on device | `SocialLoginProvider` only calls `navigator.credentials.create/get`. The private key never leaves the platform authenticator. | +| Public key is safe to store | The 65-byte uncompressed P-256 point (`publicKey65Bytes`) can be stored in Supabase without security risk. | +| OAuth token does not derive keys | The JWT is only used to identify the user (`userId`). No key derivation is performed from OAuth tokens — the two layers are independent. | +| Passkey is device-bound | Loss of device does not expose the key. Use [Social Recovery](../smart-wallet/integration-guide.md) for backup. | +| Replay protection | Each WebAuthn assertion uses a fresh server-issued challenge. Reuse of an assertion will fail verification. | + +**Do NOT store:** +- Private keys +- Seed phrases +- Session key secrets +- Any bytes returned by the platform authenticator that are not the `credentialId` or public key + +## Error handling + +| Error | Cause | Resolution | +|-------|-------|-----------| +| `No credentials found. Please onboard first.` | `login()` called before `onboard()` | Run the onboarding flow first | +| `WebAuthn authentication failed` | User cancelled or authenticator error | Show retry prompt | +| `Authentication cancelled` | User dismissed the passkey prompt | Treat as user cancellation | +| `NotAllowedError` | Browser policy or missing gesture | Ensure call is inside a user gesture | +| `SecurityError` | `rpId` mismatch | `rpId` must match `window.location.hostname` | + +## Testing + +```ts +import { WebAuthNProvider } from '@galaxy-kj/core-wallet/auth/providers/WebAuthNProvider'; +import { SocialLoginProvider } from '@galaxy-kj/core-wallet/auth/providers/SocialLoginProvider'; + +// Mock navigator.credentials +const mockCredential = { + id: 'test-credential-id', + rawId: new ArrayBuffer(32), + response: { + getPublicKey: () => new ArrayBuffer(91), // 26-byte header + 65-byte point + attestationObject: new ArrayBuffer(0), + clientDataJSON: new ArrayBuffer(0), + }, + type: 'public-key', +}; + +Object.defineProperty(globalThis.navigator, 'credentials', { + configurable: true, + value: { + create: jest.fn().mockResolvedValue(mockCredential), + get: jest.fn().mockResolvedValue({ ...mockCredential, response: {} }), + }, +}); + +const provider = new WebAuthNProvider({ rpId: 'localhost' }); +const socialLogin = new SocialLoginProvider(provider); + +test('onboard registers a credential', async () => { + const result = await socialLogin.onboard('user-abc'); + expect(result.userId).toBe('user-abc'); + expect(result.credentialId).toBeDefined(); + expect(result.publicKey65Bytes).toHaveLength(65); +}); +``` + +## Related docs + +- [WebAuthn Integration Guide](../smart-wallet/webauthn-guide.md) — deep-dive on WebAuthn internals and `SmartWalletService` integration +- [Smart Wallet Integration Guide](../smart-wallet/integration-guide.md) — full wallet lifecycle including session keys and social recovery +- [Getting Started](./getting-started.md) — project setup diff --git a/docs/smart-wallet/webauthn-guide.md b/docs/smart-wallet/webauthn-guide.md index 007f659..30a0d28 100644 --- a/docs/smart-wallet/webauthn-guide.md +++ b/docs/smart-wallet/webauthn-guide.md @@ -149,3 +149,4 @@ Object.defineProperty(global.navigator, 'credentials', { - `packages/core/wallet/src/tests/smart-wallet.service.test.ts` - [FEATURE] Multi-device Passkey Support (#164) - [DOCS] Smart Wallet Service API reference (#178) +- [Social Login Integration Guide](../guides/social-login-integration.md) — OAuth + WebAuthn two-layer model, full Supabase onboarding example diff --git a/packages/core/wallet/auth/README.md b/packages/core/wallet/auth/README.md index c675107..d1fca0a 100644 --- a/packages/core/wallet/auth/README.md +++ b/packages/core/wallet/auth/README.md @@ -128,6 +128,12 @@ Key features: ## Testing +See `packages/core/wallet/src/tests/smart-wallet.service.test.ts` for Jest mocks of WebAuthN flows. + +## Further reading + +- [Social Login Integration Guide](../../../../docs/guides/social-login-integration.md) — OAuth + WebAuthn two-layer model, Supabase onboarding, and security guarantees +- [WebAuthn Integration Guide](../../../../docs/smart-wallet/webauthn-guide.md) — registration/assertion flows, environment compatibility, and testing ```bash cd packages/core/wallet/auth npm test