diff --git a/README.md b/README.md index 2a9468a..28c5039 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,14 @@ The project enables developers to build reactive decentralized applications with 3. [Event Flow](#event-flow) 4. [Local Development Guide](#local-development-guide) 5. [Smart Contract Upgrade Guide](#smart-contract-upgrade-guide) -6. [Features](#features) -7. [Use Cases](#use-cases) -8. [Tech Stack](#tech-stack) -9. [Contributing](#contributing) -10. [License](#license) +6. [Contract API Examples](#contract-api-examples) +7. [Freighter Troubleshooting](#freighter-troubleshooting) +8. [Wallet UX States](#wallet-ux-states) +9. [Features](#features) +10. [Use Cases](#use-cases) +11. [Tech Stack](#tech-stack) +12. [Contributing](#contributing) +13. [License](#license) > **Listener service docs**: [Notification Failure Recovery](NOTIFICATION_FAILURE_RECOVERY.md) — retry lifecycle, configuration, and troubleshooting. @@ -416,6 +419,152 @@ changes. --- +## Contract API Examples + +Full examples with concrete parameter values live in [`docs/contract-api.md`](docs/contract-api.md). +Quick reference below. + +### subscribe — create a group + +```bash +stellar contract invoke \ + --id --source creator-key --network testnet \ + -- create \ + --id 0000000000000000000000000000000000000000000000000000000000000001 \ + --name "Team Alpha Plan" \ + --creator GABC1234...XYZ \ + --usage_count 100 \ + --payment_token CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC +``` + +Expected: group stored on-chain with `is_active = true`; `AutoshareCreated` event emitted; creator debited `100 × usage_fee` tokens. + +### execute_payment — top up usages + +```bash +stellar contract invoke \ + --id --source payer-key --network testnet \ + -- topup_subscription \ + --id 0000000000000000000000000000000000000000000000000000000000000001 \ + --additional_usages 50 \ + --payment_token CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC \ + --payer GABC1234...XYZ +``` + +Expected: `usage_count` increases by 50; payer debited `50 × usage_fee`; `PaymentHistory` record appended. + +### cancel — deactivate a group + +```bash +stellar contract invoke \ + --id --source creator-key --network testnet \ + -- deactivate_group \ + --id 0000000000000000000000000000000000000000000000000000000000000001 \ + --caller GABC1234...XYZ +``` + +Expected: `is_active` set to `false`; `GroupDeactivated` event emitted; subsequent payment calls will fail. + +--- + +## Freighter Troubleshooting + +Common Freighter wallet issues and how to resolve them. + +### Extension not detected + +**Symptom:** `window.freighter` is `undefined` after page load. + +**Steps:** +1. Install the [Freighter extension](https://www.freighter.app/) for Chrome or Firefox. +2. Refresh the page — Freighter injects `window.freighter` on load. +3. If still undefined, check that the extension is enabled in your browser's extension manager. +4. Disable other wallet extensions temporarily; some conflict with Freighter's injection. + +### Connection request never appears + +**Symptom:** Calling `freighter.requestAccess()` returns but no popup opens. + +**Steps:** +1. Click the Freighter icon in the browser toolbar and unlock the wallet with your password. +2. Check that the site URL matches the allowed origins in Freighter → Settings → Connected Apps. +3. Disable popup-blocking for `localhost` or your dApp domain. +4. Try in an incognito window with only Freighter enabled. + +### User rejected the connection + +**Symptom:** `freighter.requestAccess()` throws or returns `{ error: "User declined" }`. + +**Steps:** +1. Re-prompt the user — the rejection is not permanent. +2. If auto-rejected, go to Freighter → Connected Apps and remove the site entry, then retry. + +### Signing request times out or hangs + +**Symptom:** `freighter.signTransaction()` never resolves. + +**Steps:** +1. Unlock Freighter and switch to the correct network (Testnet / Mainnet). +2. Check that the transaction's `networkPassphrase` matches the network selected in Freighter. +3. Ensure the transaction fee is sufficient — underfunded transactions are silently dropped. +4. Rebuild the transaction with a fresh sequence number if the account state changed. + +### Wrong network selected + +**Symptom:** Transaction is signed but fails with `txBAD_SEQ` or similar. + +**Steps:** +1. Open Freighter and switch to the matching network (Testnet for development, Mainnet for production). +2. Verify `Networks.TESTNET` / `Networks.MAINNET` passphrase is passed to the transaction builder. + +### Permission prompt appears on every page load + +**Symptom:** Freighter asks for access each session. + +**Steps:** +1. Call `freighter.isConnected()` before `requestAccess()`; skip the prompt when already connected. +2. Ensure your dApp is served over HTTPS (or `localhost`) — Freighter restricts persistent permissions to secure origins. + +--- + +## Wallet UX States + +The frontend models four wallet connection states. Every UI that depends on the wallet must handle all of them. + +| State | Description | User-facing message | +|-------|-------------|---------------------| +| `disconnected` | No wallet connected or access not yet granted | "Connect your Freighter wallet to continue." | +| `connected` | Wallet access granted, public key available, no pending action | "Wallet connected: `G…XYZ`" | +| `waiting_for_signature` | Transaction built and sent to Freighter, awaiting user approval | "Check Freighter — please approve the transaction." | +| `error` | Connection failed, user rejected, or transaction error | "Wallet error: \. Please try again." | + +### State transition diagram + +``` +disconnected + │ user clicks "Connect" + ▼ +[requestAccess()] + │ granted │ rejected / error + ▼ ▼ +connected error ──► disconnected (retry) + │ user submits form + ▼ +[signTransaction()] + │ pending approval + ▼ +waiting_for_signature + │ approved │ rejected / timeout + ▼ ▼ +connected error +``` + +### Implementation reference + +See [`frontend/src/components/SubscriptionForm.tsx`](frontend/src/components/SubscriptionForm.tsx) for a working example of all four states. + +--- + ## Features * 📡 Real-time blockchain event monitoring diff --git a/docs/contract-api.md b/docs/contract-api.md new file mode 100644 index 0000000..6718f74 --- /dev/null +++ b/docs/contract-api.md @@ -0,0 +1,206 @@ +# AutoShare Contract API Examples + +Reference examples for integrating with the AutoShare Soroban contract. +All Stellar CLI invocations target testnet; swap `--network testnet` for `--network mainnet` in production. + +--- + +## subscribe (create) + +Creates a new AutoShare group and locks in the initial payment for `usage_count` usages. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| `id` | `BytesN<32>` | Unique group identifier (32-byte hex) | +| `name` | `String` | Human-readable group name | +| `creator` | `Address` | Wallet address of the creator | +| `usage_count` | `u32` | Number of usages to pre-purchase | +| `payment_token` | `Address` | Token contract address used for payment | + +**Stellar CLI** + +```bash +stellar contract invoke \ + --id \ + --source creator-key \ + --network testnet \ + -- \ + create \ + --id 0000000000000000000000000000000000000000000000000000000000000001 \ + --name "Team Alpha Plan" \ + --creator GABC1234...XYZ \ + --usage_count 100 \ + --payment_token CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC +``` + +**Expected outcome** + +- A new group is stored on-chain with `is_active = true` and `usage_count = 100`. +- Contract emits `AutoshareCreated { creator, group_id }`. +- The creator's wallet is debited `usage_count × usage_fee` tokens. + +**JavaScript / TypeScript SDK** + +```typescript +import { Contract, SorobanRpc, TransactionBuilder, Networks, BASE_FEE } from "@stellar/stellar-sdk"; + +const CONTRACT_ID = ""; +const server = new SorobanRpc.Server("https://soroban-testnet.stellar.org"); + +const groupId = Buffer.alloc(32, 0); +groupId[31] = 1; // unique id bytes + +const tx = new TransactionBuilder(creatorAccount, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, +}) + .addOperation( + contract.call( + "create", + xdr.ScVal.scvBytes(groupId), // id + nativeToScVal("Team Alpha Plan"), // name + nativeToScVal(creatorAddress, { type: "address" }), // creator + nativeToScVal(100, { type: "u32" }), // usage_count + nativeToScVal(tokenAddress, { type: "address" }), // payment_token + ) + ) + .setTimeout(30) + .build(); +``` + +--- + +## execute_payment (topup_subscription) + +Tops up an existing group by purchasing additional usages. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| `id` | `BytesN<32>` | Group identifier | +| `additional_usages` | `u32` | Number of usages to add | +| `payment_token` | `Address` | Token used for payment | +| `payer` | `Address` | Address authorising the payment | + +**Stellar CLI** + +```bash +stellar contract invoke \ + --id \ + --source payer-key \ + --network testnet \ + -- \ + topup_subscription \ + --id 0000000000000000000000000000000000000000000000000000000000000001 \ + --additional_usages 50 \ + --payment_token CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC \ + --payer GABC1234...XYZ +``` + +**Expected outcome** + +- `usage_count` for the group increases by `50`. +- Payer is debited `50 × usage_fee` tokens. +- A `PaymentHistory` record is appended for the payer and the group. + +**JavaScript / TypeScript SDK** + +```typescript +const tx = new TransactionBuilder(payerAccount, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, +}) + .addOperation( + contract.call( + "topup_subscription", + xdr.ScVal.scvBytes(groupId), + nativeToScVal(50, { type: "u32" }), + nativeToScVal(tokenAddress, { type: "address" }), + nativeToScVal(payerAddress, { type: "address" }), + ) + ) + .setTimeout(30) + .build(); +``` + +--- + +## cancel (deactivate_group) + +Deactivates a group. Only the creator can call this. The group data is preserved on-chain for audit purposes. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| `id` | `BytesN<32>` | Group identifier | +| `caller` | `Address` | Must match the group's creator address | + +**Stellar CLI** + +```bash +stellar contract invoke \ + --id \ + --source creator-key \ + --network testnet \ + -- \ + deactivate_group \ + --id 0000000000000000000000000000000000000000000000000000000000000001 \ + --caller GABC1234...XYZ +``` + +**Expected outcome** + +- Group `is_active` is set to `false`. +- Contract emits `GroupDeactivated { creator, group_id }`. +- Subsequent payment attempts against the group will fail with an error. + +**Error cases** + +| Error | Cause | +|-------|-------| +| `Unauthorized` | `caller` does not match the group creator | +| `GroupNotFound` | No group exists for the given `id` | +| `GroupAlreadyInactive` | Group is already deactivated | + +**JavaScript / TypeScript SDK** + +```typescript +const tx = new TransactionBuilder(creatorAccount, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, +}) + .addOperation( + contract.call( + "deactivate_group", + xdr.ScVal.scvBytes(groupId), + nativeToScVal(creatorAddress, { type: "address" }), + ) + ) + .setTimeout(30) + .build(); +``` + +--- + +## Re-activating a cancelled group + +Use `activate_group` with the same `id` and creator `caller` to restore the group. + +```bash +stellar contract invoke \ + --id \ + --source creator-key \ + --network testnet \ + -- \ + activate_group \ + --id 0000000000000000000000000000000000000000000000000000000000000001 \ + --caller GABC1234...XYZ +``` + +--- + +> Full interface specification: [`contract/contracts/hello-world/src/interfaces/autoshare.rs`](../contract/contracts/hello-world/src/interfaces/autoshare.rs) diff --git a/frontend/src/components/SubscriptionForm.tsx b/frontend/src/components/SubscriptionForm.tsx new file mode 100644 index 0000000..c4b95bb --- /dev/null +++ b/frontend/src/components/SubscriptionForm.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { useState } from "react"; + +// --------------------------------------------------------------------------- +// Wallet UX states +// disconnected – no wallet connected / access not yet granted +// connected – access granted, public key available, no pending action +// waiting_for_signature – tx sent to Freighter, awaiting user approval +// error – connection failed, user rejected, or tx error +// --------------------------------------------------------------------------- +type WalletState = "disconnected" | "connected" | "waiting_for_signature" | "error"; + +interface FormValues { + groupName: string; + usageCount: number; +} + +export default function SubscriptionForm() { + const [walletState, setWalletState] = useState("disconnected"); + const [publicKey, setPublicKey] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [form, setForm] = useState({ groupName: "", usageCount: 10 }); + const [txHash, setTxHash] = useState(null); + + // -- wallet helpers -------------------------------------------------------- + + async function connectWallet() { + setErrorMessage(null); + try { + // @ts-expect-error – freighter is injected by the browser extension + const freighter = window.freighter; + if (!freighter) throw new Error("Freighter extension not found. Please install it from freighter.app."); + + const isConnected: boolean = await freighter.isConnected(); + if (!isConnected) await freighter.requestAccess(); + + const key: string = await freighter.getPublicKey(); + setPublicKey(key); + setWalletState("connected"); + } catch (err: unknown) { + setErrorMessage(err instanceof Error ? err.message : String(err)); + setWalletState("error"); + } + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (walletState !== "connected" || !publicKey) return; + + setErrorMessage(null); + setWalletState("waiting_for_signature"); + + try { + // Build a minimal transaction envelope. In a real integration you would + // call the Soroban SDK here; this stub demonstrates the UX flow. + const xdrEnvelope = buildSubscriptionTx(form, publicKey); + + // @ts-expect-error – freighter is injected by the browser extension + const signed: { signedTxXdr: string } = await window.freighter.signTransaction( + xdrEnvelope, + { network: "TESTNET", networkPassphrase: "Test SDF Network ; September 2015" } + ); + + // Submit to Stellar RPC (stubbed). + const hash = await submitTransaction(signed.signedTxXdr); + setTxHash(hash); + setWalletState("connected"); + } catch (err: unknown) { + setErrorMessage(err instanceof Error ? err.message : String(err)); + setWalletState("error"); + } + } + + function retry() { + setErrorMessage(null); + setWalletState(publicKey ? "connected" : "disconnected"); + } + + // -- render ---------------------------------------------------------------- + + return ( +
+

Create Subscription Group

+ + + + {walletState === "connected" && ( +
+
+ + setForm({ ...form, groupName: e.target.value })} + className="mt-1 block w-full border rounded px-3 py-2 text-sm" + placeholder="Team Alpha Plan" + /> +
+ +
+ + setForm({ ...form, usageCount: Number(e.target.value) })} + className="mt-1 block w-full border rounded px-3 py-2 text-sm" + /> +
+ + +
+ )} + + {txHash && ( +

+ ✓ Transaction submitted:{" "} + + {txHash.slice(0, 12)}… + +

+ )} +
+ ); +} + +// --------------------------------------------------------------------------- +// WalletStatusBanner — renders the correct message for each wallet state +// --------------------------------------------------------------------------- +interface BannerProps { + state: WalletState; + publicKey: string | null; + error: string | null; + onConnect: () => void; + onRetry: () => void; +} + +function WalletStatusBanner({ state, publicKey, error, onConnect, onRetry }: BannerProps) { + switch (state) { + case "disconnected": + return ( +
+ Connect your Freighter wallet to continue. + +
+ ); + + case "connected": + return ( +
+ Wallet connected: {publicKey} +
+ ); + + case "waiting_for_signature": + return ( +
+ + Check Freighter — please approve the transaction. +
+ ); + + case "error": + return ( +
+ Wallet error: {error ?? "Unknown error"}. Please try again. + +
+ ); + } +} + +// --------------------------------------------------------------------------- +// Stubs — replace with real Soroban SDK calls +// --------------------------------------------------------------------------- +function buildSubscriptionTx(_form: FormValues, _creator: string): string { + // TODO: build XDR using @stellar/stellar-sdk and the AutoShare contract + return "AAAAAA=="; +} + +async function submitTransaction(_xdr: string): Promise { + // TODO: submit via SorobanRpc.Server.sendTransaction() + return "stub-tx-hash"; +}