diff --git a/docs/INTEGRATOR_GUIDE.md b/docs/INTEGRATOR_GUIDE.md new file mode 100644 index 00000000..eea27d5b --- /dev/null +++ b/docs/INTEGRATOR_GUIDE.md @@ -0,0 +1,236 @@ +# Trivela Integrator Guide: Embedding Rewards in Third-Party Apps + +This guide walks partners and developers through embedding **Trivela rewards** into their own +applications. By integrating Trivela, you can reward users with Stellar-native assets (like XLM or +custom tokens) when they complete actions (such as making a purchase, completing a task, or reaching +a milestone) in your app. + +--- + +## ๐Ÿ—๏ธ Architecture Overview + +The integration relies on three main parts: + +1. **Your App (Frontend & Backend)**: Triggers user events and handles custom rewards experiences. +2. **Trivela Backend API**: Manages metadata, verifies claims, and registers on-chain Soroban + interactions. +3. **Soroban Smart Contracts**: Enforces campaign rules and handles trustless on-chain claiming. + +```mermaid +sequenceDiagram + participant User as User Wallet + participant PartnerApp as Partner App Backend + participant Trivela as Trivela Backend + participant Stellar as Soroban Contracts + + User->>PartnerApp: Complete Action (e.g. Purchase) + PartnerApp->>Trivela: POST /api/campaigns/:id/interact (Credit points) + Trivela->>Stellar: Update on-chain points / state + Trivela-->>PartnerApp: Webhook Dispatch (campaign.updated) + User->>PartnerApp: Request Claim + PartnerApp-->>User: Provide Claim parameters + User->>Stellar: Tx: claim() / register_private() + Stellar-->>User: Disburse assets to user wallet +``` + +--- + +## ๐Ÿš€ Step 1: SDK & Type Setup + +To ensure type-safety when interacting with the Trivela REST API, install `@trivela/client` in your +project: + +```bash +npm install @trivela/client +``` + +This package contains TypeScript typings for all request and response payloads, ensuring error-free +communication with the Trivela Backend. + +--- + +## ๐Ÿ”ง Step 2: Creating a Campaign + +First, set up a campaign via the Trivela Admin Dashboard or programmatically. To do it +programmatically, use your organization's API credentials to hit the Trivela Backend: + +```typescript +import type { CampaignCreate } from '@trivela/client'; + +async function createRewardCampaign() { + const response = await fetch('https://api.trivela.com/api/v1/campaigns', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.TRIVELA_API_KEY}`, + }, + body: JSON.stringify({ + name: 'Partnership Rewards Program', + description: 'Earn rewards for completing milestones.', + reward_xlm: 5, + max_participants: 500, + admin_public_key: process.env.ADMIN_PUBLIC_KEY, + } as CampaignCreate), + }); + + const campaign = await response.json(); + console.log(`Campaign created successfully: ${campaign.id}`); + return campaign.id; +} +``` + +--- + +## ๐Ÿ‘ฅ Step 3: Registering Participants + +Before users can accumulate points, they must be registered as active participants in your campaign. +Register them when they opt-in or connect their wallet in your app: + +```typescript +async function enrollUser(campaignId: string, userWalletAddress: string) { + const response = await fetch(`https://api.trivela.com/api/v1/campaigns/${campaignId}/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.TRIVELA_API_KEY}`, + }, + body: JSON.stringify({ + wallet_address: userWalletAddress, + }), + }); + + if (!response.ok) { + throw new Error('Failed to enroll user in rewards campaign'); + } +} +``` + +--- + +## โšก Step 4: Awarding Rewards (Interactions) + +When a user performs a key action in your app, notify the Trivela API to record the interaction and +update their on-chain points: + +```typescript +async function rewardUserAction(campaignId: string, userWalletAddress: string, actionType: string) { + const response = await fetch(`https://api.trivela.com/api/v1/campaigns/${campaignId}/interact`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.TRIVELA_API_KEY}`, + }, + body: JSON.stringify({ + wallet_address: userWalletAddress, + action: actionType, + value: 10, // credit 10 points + }), + }); + + if (response.ok) { + console.log(`Credited points to ${userWalletAddress} for action: ${actionType}`); + } +} +``` + +--- + +## ๐Ÿ”’ Step 5: Webhook Signature Verification + +Trivela dispatches HTTP POST webhooks to your backend whenever campaign events or claims occur. To +prevent replay attacks and spoofing, you **must** verify the payload's signature using the signing +secret provided in the Trivela dashboard. + +Install `@trivela/webhook-verify` to perform a timing-safe signature comparison: + +```bash +npm install @trivela/webhook-verify +``` + +Here is how to set up signature verification in a Node.js server: + +```javascript +import { constructEvent } from '@trivela/webhook-verify'; +import express from 'express'; + +const app = express(); + +// Use express.raw() to get the raw string body (necessary for cryptographic signature verification) +app.post('/api/webhooks/trivela', express.raw({ type: 'application/json' }), (req, res) => { + const signature = req.headers['x-trivela-signature']; + const secret = process.env.TRIVELA_WEBHOOK_SECRET; + + let event; + try { + // Verifies and parses the payload raw string body against the signature + event = constructEvent(req.body.toString('utf8'), signature, secret); + } catch (err) { + console.error(`[Webhook Signature Failed]: ${err.message}`); + return res.status(400).send(`Webhook verification failed: ${err.message}`); + } + + // Handle the event payload + console.log(`Received verified webhook event: ${event.type}`); + switch (event.type) { + case 'campaign.updated': + // Handle campaign state updates + break; + default: + console.log(`Unhandled webhook event type: ${event.type}`); + } + + res.sendStatus(200); +}); +``` + +--- + +## ๐Ÿ›ก๏ธ Step 6: Frontend Claims & ZK Proofs + +For private campaigns, users claim rewards using zero-knowledge proofs so their identities are kept +completely confidential on-chain. + +To generate a ZK proof in the browser without locking the main thread, use the Web Worker prover +package `@trivela/sdk/zk`: + +```typescript +import { generateClaimProof, isZkSupported } from '@trivela/sdk/zk'; + +async function handlePrivateRewardClaim(userSecret: string, amount: number) { + if (!isZkSupported()) { + throw new Error('Zero-knowledge proofs are not supported in this browser.'); + } + + // 1. Generate the ZK claim proof inputs + const inputs = { + secret: userSecret, + claimAmount: amount, + }; + + try { + // 2. Generate the proof (runs off-thread in a Web Worker) + const proof = await generateClaimProof(inputs, { + onProgress: (percent) => console.log(`Proving progress: ${percent}%`), + }); + + // 3. Submit proof bytes and nullifier directly to the campaign contract + await submitToSorobanContract(proof.nullifier, proof.proofBytes); + console.log('Private claim successfully registered!'); + } catch (error) { + console.error('Failed to generate ZK claim proof:', error); + } +} +``` + +--- + +## ๐Ÿงช Runnable Examples + +For a fully interactive, local setup demonstrating this end-to-end integration flow, explore the +`examples/` folder in the repository: + +- **[Point-Based Loyalty Example](../examples/loyalty/README.md)**: A complete, runnable simulation + of campaign creation, participant registration, point accumulation, and claim processing using the + REST API. +- **[Partner Webhook & ZK Integration](../examples/partner-integration/README.md)**: Demonstrates + setting up a backend webhook verification server and launching ZK browser integrations. diff --git a/examples/README.md b/examples/README.md index 03f684c5..60f4c903 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,11 +3,12 @@ Runnable integration examples for common Trivela use cases. Each example targets the local dev stack (`compose up` from repo root) but works against any Trivela deployment. -| Example | Description | -| ---------------------------- | ------------------------------------------- | -| [loyalty/](loyalty/) | Point-based loyalty campaign with XLM claim | -| [airdrop/](airdrop/) | Bulk XLM airdrop from a CSV wallet list | -| [dao-rewards/](dao-rewards/) | Weighted DAO governance reward distribution | +| Example | Description | +| -------------------------------------------- | ------------------------------------------------- | +| [loyalty/](loyalty/) | Point-based loyalty campaign with XLM claim | +| [airdrop/](airdrop/) | Bulk XLM airdrop from a CSV wallet list | +| [dao-rewards/](dao-rewards/) | Weighted DAO governance reward distribution | +| [partner-integration/](partner-integration/) | Webhook verification & interaction crediting demo | ## Quick start diff --git a/examples/partner-integration/.env.example b/examples/partner-integration/.env.example new file mode 100644 index 00000000..db133e99 --- /dev/null +++ b/examples/partner-integration/.env.example @@ -0,0 +1,3 @@ +TRIVELA_API_URL=http://localhost:3001 +TRIVELA_WEBHOOK_SECRET=super_secret_signing_key_from_dashboard +PORT=4000 diff --git a/examples/partner-integration/README.md b/examples/partner-integration/README.md new file mode 100644 index 00000000..f976c627 --- /dev/null +++ b/examples/partner-integration/README.md @@ -0,0 +1,36 @@ +# Trivela Example: Partner Integration + +Demonstrates how to embed Trivela rewards into a third-party application backend and frontend. + +## What it shows + +- Simulating a user purchase and calling Trivela `POST /api/campaigns/:id/interact` to credit + points. +- Setting up a webhook handler to receive cryptographically signed updates from Trivela. +- Verifying the signature (`X-Trivela-Signature` header) timing-safely using + `@trivela/webhook-verify`. + +## Prerequisites + +- Node.js 18+ +- Trivela backend running locally (`compose up` from repo root) + +## Setup + +1. Copy the environment template: + ```bash + cp .env.example .env + ``` +2. Fill in `TRIVELA_API_URL` and `TRIVELA_WEBHOOK_SECRET` in `.env`. + +## Run + +Run the integration server: + +```bash +node index.js +``` + +The server will start on port `4000`. You can trigger a mock user action by visiting +`http://localhost:4000/mock-purchase`, and you can test webhook verification by sending POST +requests to `http://localhost:4000/webhook`. diff --git a/examples/partner-integration/index.js b/examples/partner-integration/index.js new file mode 100644 index 00000000..4aeade29 --- /dev/null +++ b/examples/partner-integration/index.js @@ -0,0 +1,108 @@ +#!/usr/bin/env node +/** + * Trivela Partner Integration Example + * + * Demonstrates a partner backend API server integrating Trivela rewards. + * It provides: + * 1. A mock purchase endpoint that credits points via the Trivela REST API. + * 2. A webhook listener that validates signature cryptographic payloads timing-safely. + */ + +import http from 'node:http'; +import { URL } from 'node:url'; +import * as dotenv from 'dotenv'; +import { constructEvent } from '../../sdk/webhook-verify/src/index.js'; + +dotenv.config(); + +const PORT = process.env.PORT || 4000; +const TRIVELA_API = process.env.TRIVELA_API_URL || 'http://localhost:3001'; +const WEBHOOK_SECRET = process.env.TRIVELA_WEBHOOK_SECRET || 'super_secret_signing_key_from_dashboard'; + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://${req.headers.host}`); + + // CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Trivela-Signature'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // 1. Webhook Endpoint + if (req.method === 'POST' && url.pathname === '/webhook') { + const body = []; + req.on('data', (chunk) => { + body.push(chunk); + }).on('end', () => { + const rawPayload = Buffer.concat(body).toString('utf8'); + const signature = req.headers['x-trivela-signature']; + + try { + // Timing-safely verify signature using webhook-verify SDK + const event = constructEvent(rawPayload, signature, WEBHOOK_SECRET); + const sanitizedEventType = event.type.replace(/[\r\n\t]/g, '_'); + console.log(`[Webhook Verified]: Received event type: ${sanitizedEventType}`); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ verified: true, eventType: event.type })); + } catch (err) { + console.error(`[Webhook Verification Failed]: ${err.message}`); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: err.message })); + } + }); + } + + // 2. Mock User Action Endpoint (Purchase) + else if (req.method === 'GET' && url.pathname === '/mock-purchase') { + const campaignId = url.searchParams.get('campaignId') || 'test-campaign'; + const walletAddress = url.searchParams.get('wallet') || 'GBABC321XYZ'; + + try { + console.log(`[Simulation]: Crediting points for campaign ${campaignId} to wallet ${walletAddress}`); + + const response = await fetch(`${TRIVELA_API}/api/v1/campaigns/${campaignId}/interact`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + wallet_address: walletAddress, + action: 'partner_purchase', + value: 15 + }) + }); + + if (!response.ok) { + throw new Error(`Trivela API returned status ${response.status}: ${await response.text()}`); + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + message: `Purchase completed. 15 points credited to wallet: ${walletAddress}` + })); + } catch (err) { + console.error(`[Mock Purchase Failed]: ${err.message}`); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: err.message })); + } + } + + // Not Found + else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Route Not Found'); + } +}); + +server.listen(PORT, () => { + console.log(`\n==================================================`); + console.log(`Trivela Partner Integration Server running on port ${PORT}`); + console.log(`- Webhook path: POST http://localhost:${PORT}/webhook`); + console.log(`- Mock purchase: GET http://localhost:${PORT}/mock-purchase?campaignId=CAMP_ID&wallet=WALLET_ADDR`); + console.log(`==================================================\n`); +});