diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2452646a..fb7dbbd3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,25 +1,24 @@ -# Workflow for deploying static content to GitHub Pages +# Workflow for deploying static content to GitHub Pages via gh-pages branch. +# Preserves any PR preview directories that already exist on gh-pages. name: Deploy static content to Pages on: push: branches: ['main'] + paths: + - 'frontend/**' workflow_dispatch: permissions: - contents: read - pages: write - id-token: write + # Required by peaceiris/actions-gh-pages to push built site to the gh-pages branch + contents: write concurrency: - group: 'pages' - cancel-in-progress: true + group: 'gh-pages-deploy' + cancel-in-progress: false jobs: deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: # Checkout repo @@ -41,27 +40,46 @@ jobs: npm install fi working-directory: ./frontend + # Build project - name: Build run: npm run build working-directory: ./frontend - env: + env: VITE_WALLETCONNECT_PROJECT_ID: ${{ secrets.VITE_WALLETCONNECT_PROJECT_ID }} VITE_CONTRACT_ADDRESS_11155111: ${{ secrets.VITE_CONTRACT_ADDRESS_11155111 }} VITE_CONTRACT_ADDRESS_61: ${{ secrets.VITE_CONTRACT_ADDRESS_61 }} VITE_CONTRACT_ADDRESS_137: ${{ secrets.VITE_CONTRACT_ADDRESS_137 }} - # Setup Pages - - name: Setup Pages - uses: actions/configure-pages@v5 + # Restore PR preview directories from gh-pages branch (if present) + # so that a production redeploy does not wipe them out. + - name: Restore PR preview directories from gh-pages + run: | + git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1 || { + echo "gh-pages branch does not exist yet — skipping restore." + exit 0 + } - # Upload artifact - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./frontend/dist + git fetch origin gh-pages --depth=1 - # Deploy to GitHub Pages - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + # Copy the pr-preview tree into the freshly built dist so the + # subsequent deploy action includes it in the push. + git checkout origin/gh-pages -- pr-preview/ 2>/dev/null && { + cp -r pr-preview ./frontend/dist/pr-preview + rm -rf pr-preview + } || { + echo "No pr-preview directory on gh-pages yet — skipping restore." + } + + # Add .nojekyll to prevent Jekyll processing + - name: Add .nojekyll + run: touch ./frontend/dist/.nojekyll + + # Deploy to gh-pages branch + - name: Deploy to gh-pages branch + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./frontend/dist + cname: chainvoice.stability.nexus + commit_message: "deploy: production site from ${{ github.sha }}" diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 00000000..b4055b6a --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,66 @@ +# Workflow 1 of 2 for PR previews (fork-safe). +# This is a lightweight trigger — it only uploads metadata. +# The actual build and deploy is handled by pr-deploy.yml via workflow_run, +# which runs in the base repository context with secrets and write permissions. +# +# WHY TWO WORKFLOWS? +# 1. Single workflow with `pull_request` + `contents: write` fails with 403 +# on org repos where GITHUB_TOKEN defaults to read-only. +# 2. Fork PRs never get secrets on `pull_request` trigger (GitHub blocks this). +# By building in pr-deploy.yml (workflow_run context), we get secrets so +# features like wallet connection work in PR previews. +# 3. Fork code never gets write access — pr-deploy.yml always runs from main. + +name: "PR Preview: Build" + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + paths: + - 'frontend/**' + +# Read-only permissions — this workflow only uploads metadata. +permissions: + contents: read + +jobs: + # ─── Signal a deploy when PR is opened or updated ─────────────────────── + signal-deploy: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + concurrency: + group: pr-preview-build-${{ github.event.pull_request.number }} + cancel-in-progress: true + steps: + - name: Save deploy metadata + run: | + mkdir -p ./pr-metadata + echo "${{ github.event.pull_request.number }}" > ./pr-metadata/pr_number + echo "${{ github.event.pull_request.head.sha }}" > ./pr-metadata/sha + echo "deploy" > ./pr-metadata/action + + - name: Upload PR metadata + uses: actions/upload-artifact@v4 + with: + name: pr-metadata-${{ github.event.pull_request.number }} + path: ./pr-metadata + retention-days: 1 + + # ─── Signal cleanup when PR is closed (merged or abandoned) ───────────── + signal-cleanup: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - name: Save cleanup metadata + run: | + mkdir -p ./pr-metadata + echo "${{ github.event.pull_request.number }}" > ./pr-metadata/pr_number + echo "${{ github.event.pull_request.head.sha }}" > ./pr-metadata/sha + echo "cleanup" > ./pr-metadata/action + + - name: Upload PR metadata + uses: actions/upload-artifact@v4 + with: + name: pr-metadata-${{ github.event.pull_request.number }} + path: ./pr-metadata + retention-days: 1 diff --git a/.github/workflows/pr-deploy.yml b/.github/workflows/pr-deploy.yml new file mode 100644 index 00000000..d1be53ed --- /dev/null +++ b/.github/workflows/pr-deploy.yml @@ -0,0 +1,271 @@ +# Workflow 2 of 2 for PR previews (fork-safe). +# Triggered by workflow_run after pr-build.yml completes. +# Runs in the BASE REPOSITORY context — has write permissions and secrets, +# so it can build with VITE_ env vars, push to gh-pages, and comment on PRs. +# +# SECURITY NOTES: +# - This workflow ALWAYS runs the code from the default branch (main), +# so fork PRs cannot modify the deploy/cleanup logic. +# - PR source code is checked out with persist-credentials: false, +# so build scripts cannot access GITHUB_TOKEN via git. +# - GITHUB_TOKEN is only passed to the peaceiris deploy action, never to build steps. +# +# PREREQUISITE (org admin action required): +# Go to github.com/StabilityNexus/Chainvoice/settings/actions +# → Workflow permissions → select "Read and write permissions" +# +# PRODUCTION IMPACT: None. +# - PR previews deploy to pr-preview/pr-N/ subdirectory only. +# - Production deploy.yml restores pr-preview/ dirs before deploying. +# - Different concurrency groups prevent conflicts. + +name: "PR Preview: Deploy" + +on: + workflow_run: + workflows: ["PR Preview: Build"] + types: [completed] + +permissions: + contents: write + pull-requests: write + +jobs: + deploy-preview: + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + concurrency: + group: pr-preview-deploy + cancel-in-progress: false + steps: + # ─── Download the metadata artifact to determine action ────────────── + - name: Download PR metadata + uses: actions/github-script@v7 + id: metadata + with: + script: | + // List artifacts from the triggering workflow run + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + // Find the metadata artifact + const metadataArtifact = artifacts.data.artifacts.find( + a => a.name.startsWith('pr-metadata-') + ); + if (!metadataArtifact) { + core.setFailed('No PR metadata artifact found'); + return; + } + + // Download and extract metadata + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: metadataArtifact.id, + archive_format: 'zip', + }); + + const fs = require('fs'); + fs.mkdirSync('./pr-metadata', { recursive: true }); + fs.writeFileSync('./pr-metadata.zip', Buffer.from(download.data)); + + - name: Extract and validate metadata + run: | + unzip -o ./pr-metadata.zip -d ./pr-metadata + + PR_NUMBER=$(cat ./pr-metadata/pr_number | tr -d '[:space:]') + PR_SHA=$(cat ./pr-metadata/sha | tr -d '[:space:]') + PR_ACTION=$(cat ./pr-metadata/action | tr -d '[:space:]') + + # Validate PR number is a positive integer (prevents injection) + if ! echo "$PR_NUMBER" | grep -qE '^[0-9]+$'; then + echo "::error::Invalid PR number: '$PR_NUMBER'" + exit 1 + fi + + # Validate action is one of the expected values + if [ "$PR_ACTION" != "deploy" ] && [ "$PR_ACTION" != "cleanup" ]; then + echo "::error::Invalid action: '$PR_ACTION' (expected 'deploy' or 'cleanup')" + exit 1 + fi + + # Validate SHA looks like a hex commit hash + if ! echo "$PR_SHA" | grep -qE '^[0-9a-f]{40}$'; then + echo "::error::Invalid SHA: '$PR_SHA'" + exit 1 + fi + + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + echo "PR_SHA=$PR_SHA" >> $GITHUB_ENV + echo "PR_ACTION=$PR_ACTION" >> $GITHUB_ENV + echo "✅ Metadata validated: PR #$PR_NUMBER, action=$PR_ACTION, sha=${PR_SHA:0:7}" + + # ─── Deploy preview (when action == deploy) ───────────────────────── + + # Checkout PR code with NO credentials persisted — build scripts + # cannot access GITHUB_TOKEN through git commands. + - name: Checkout PR code + if: env.PR_ACTION == 'deploy' + uses: actions/checkout@v4 + with: + ref: ${{ env.PR_SHA }} + persist-credentials: false + + - name: Setup Node + if: env.PR_ACTION == 'deploy' + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + if: env.PR_ACTION == 'deploy' + run: | + if [ -f "./frontend/package-lock.json" ]; then + npm ci + else + npm install + fi + working-directory: ./frontend + + - name: Build preview site + if: env.PR_ACTION == 'deploy' + run: npm run build -- --base /${{ github.event.workflow_run.repository.name }}/pr-preview/pr-${{ env.PR_NUMBER }}/ + working-directory: ./frontend + env: + # Secrets are available here because this workflow runs in the base + # repo context. This allows wallet connection to work in PR previews. + # Note: GITHUB_TOKEN is intentionally NOT passed to the build step. + VITE_WALLETCONNECT_PROJECT_ID: ${{ secrets.VITE_WALLETCONNECT_PROJECT_ID }} + VITE_CONTRACT_ADDRESS_11155111: ${{ secrets.VITE_CONTRACT_ADDRESS_11155111 }} + VITE_CONTRACT_ADDRESS_61: ${{ secrets.VITE_CONTRACT_ADDRESS_61 }} + VITE_CONTRACT_ADDRESS_137: ${{ secrets.VITE_CONTRACT_ADDRESS_137 }} + + - name: Add .nojekyll + if: env.PR_ACTION == 'deploy' + run: touch ./frontend/dist/.nojekyll + + - name: Deploy preview to gh-pages + if: env.PR_ACTION == 'deploy' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./frontend/dist + destination_dir: pr-preview/pr-${{ env.PR_NUMBER }} + keep_files: true + user_name: "github-actions[bot]" + user_email: "github-actions[bot]@users.noreply.github.com" + commit_message: "deploy: preview for PR #${{ env.PR_NUMBER }} (${{ env.PR_SHA }})" + + - name: Post or update deploy comment on PR + if: env.PR_ACTION == 'deploy' + uses: actions/github-script@v7 + with: + script: | + const prNumber = parseInt(process.env.PR_NUMBER); + const sha = process.env.PR_SHA.substring(0, 7); + const repoOwner = context.repo.owner; + const repoName = context.repo.repo; + + // Dynamically construct the preview environment URL. + // We use the default GitHub Pages domain format (https://.github.io/) + // because it universally supports both contributor forks and the main organization repository. + // If the main repository uses a custom domain (e.g., chainvoice.stability.nexus), + // GitHub Pages automatically redirects this standard URL to the custom domain. + const previewUrl = `https://${repoOwner}.github.io/${repoName}/pr-preview/pr-${prNumber}/`; + + const marker = ''; + const body = + `${marker}\n` + + `### 🔍 PR Preview\n\n` + + `| | |\n` + + `|---|---|\n` + + `| **Preview URL** | ${previewUrl} |\n` + + `| **Commit** | \`${sha}\` |\n` + + `| **Status** | ✅ Deployed |\n\n` + + `> This preview is updated on every commit and will be removed when the PR is closed.`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: repoOwner, + repo: repoName, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + body, + }); + } + + # ─── Cleanup preview (when action == cleanup) ────────────────────── + - name: Checkout repository + if: env.PR_ACTION == 'cleanup' + uses: actions/checkout@v4 + + - name: Remove preview from gh-pages + if: env.PR_ACTION == 'cleanup' + run: | + # Guard: if gh-pages doesn't exist yet there is nothing to clean up. + git ls-remote --exit-code --heads origin gh-pages > /dev/null 2>&1 || { + echo "gh-pages branch does not exist — nothing to clean up." + exit 0 + } + + # Fetch and create a local tracking branch. + git fetch origin gh-pages:gh-pages --depth=1 + git checkout gh-pages + + PR_DIR="pr-preview/pr-${{ env.PR_NUMBER }}" + if [ -d "$PR_DIR" ]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git rm -r -f "$PR_DIR" + git commit -m "cleanup: remove preview for PR #${{ env.PR_NUMBER }}" + git push --set-upstream origin gh-pages + echo "✅ Preview directory '$PR_DIR' removed from gh-pages." + else + echo "Preview directory '$PR_DIR' not found — nothing to clean up." + fi + + - name: Update PR comment to indicate removal + if: env.PR_ACTION == 'cleanup' + uses: actions/github-script@v7 + with: + script: | + const prNumber = parseInt(process.env.PR_NUMBER); + const marker = ''; + const body = + `${marker}\n` + + `### 🔍 PR Preview\n\n` + + `| | |\n` + + `|---|---|\n` + + `| **Status** | 🗑️ Preview removed (PR closed) |\n`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06cb3e89..16b5f6bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,26 +1,50 @@ image -## Smart Contract Contribution Guidelines -The following guidelines apply to all contributions that modify or introduce smart contract logic in ChainVoice. Since smart contracts form the core logic of the system, a higher standard of review and validation is required compared to frontend or general application changes. +## General Contribution & Review Process +The following guidelines apply to all contributions across the entire project (both smart contracts and frontend). + +**1. Pull Request Scope:** +Pull requests must remain focused and limited in scope. Each pull request should address a single feature, improvement, or bug fix. Large, multi-feature updates or broad refactors must be divided into smaller, logically structured changes before review to ensure clarity and effective feedback. + +**2. Build, Test & CI Requirements:** +Before opening a pull request, you must ensure that your local build is successful (`npm run build`), all relevant tests pass, and there are no new console errors or warnings in the browser. All automated CI checks must pass before requesting review. -**1. Introduction:** -Smart contract development in ChainVoice requires stricter standards than frontend or general application changes. Contract modifications involve compilation, logic validation, security review, and careful testing. Unlike UI updates, their impact is not immediately visible and often requires deployment and interaction to verify behavior. All contributors must adhere to the following guidelines to maintain code quality and security. +**3. Automated Code Review (CodeRabbit):** +All comments and suggestions raised by CodeRabbit must be carefully reviewed and addressed before requesting a mentor review. Pull requests with failing checks, console errors, or unresolved automated review comments will not be considered for manual review. -**2. Test Requirements:** +**4. Requesting a Review:** +Once your PR is ready and all CI checks pass, drop the PR link (along with the live preview URL, if applicable) in the [⁠Stability Nexus>#⁠Chainvoice](https://discord.com/channels/995968619034984528/1328282666335993856) Discord channel. Remember to include relevant demo screenshots or screen recordings showcasing your changes. + +--- + +## Smart Contract Contribution Guidelines +Since smart contracts form the core logic of the system, a higher standard of review and validation is required compared to frontend or general application changes. + +**1. Test Requirements:** Any pull request that modifies or introduces smart contract logic must include comprehensive automated tests. Tests must validate expected behavior, cover relevant edge cases, and properly test revert and failure scenarios. If existing logic is modified, corresponding tests must also be updated. Pull requests without sufficient test coverage will not be reviewed, as tests serve as the primary validation mechanism for contract correctness. -**3. Design and Architecture Approval:** +**2. Design and Architecture Approval:** For any major feature, architectural change, or significant contract modification, a detailed issue must be opened before implementation begins. The issue must clearly describe the proposed design, data structures, access control model, and expected behavior. Implementation may begin only after the approach has been discussed and approved to prevent architectural inconsistencies and unnecessary rework. -**4. Pull Request Scope:** -Pull requests must remain focused and limited in scope. Each pull request should address a single feature, improvement, or bug fix. Large, multi-feature updates or broad refactors must be divided into smaller, logically structured changes before review to ensure clarity and effective feedback. +**3. Review Standards:** +During review, emphasis will be placed on architectural soundness, correctness of state management, access control implementation, event emission consistency, potential security risks such as reentrancy or improper validation, and overall contract design quality. Where relevant, gas efficiency and upgrade safety will also be considered. -**5. Continuous Integration and Automated Review Requirements:** -All automated checks must pass before requesting review. This includes successful compilation, linting, and execution of all tests. If continuous integration fails, contributors are required to resolve the issues before requesting further review. ```In addition, all comments and suggestions raised by CodeRabbit must be carefully reviewed and addressed before requesting a mentor review.``` Pull requests with unresolved automated review comments will not be considered for manual review. Review time should focus on logic, architecture, and security considerations rather than basic correctness issues. +--- -**6. Review Standards:** -During review, emphasis will be placed on architectural soundness, correctness of state management, access control implementation, event emission consistency, potential security risks such as reentrancy or improper validation, and overall contract design quality. Where relevant, gas efficiency and upgrade safety will also be considered. +## Frontend Contribution Guidelines +The following guidelines apply to all contributions that modify or introduce frontend changes in ChainVoice. + +**1. Static Asset Paths:** +All static file references (images, icons, fonts, etc.) must use `import.meta.env.BASE_URL` instead of hardcoded absolute paths. This ensures assets resolve correctly in both the production site and automated PR preview deployments. +```jsx +// ✅ Correct + + +// ❌ Incorrect + +``` -Please ensure that all future contract-related contributions follow this process. If there are any questions or clarifications needed, feel free to raise them before proceeding with implementation. +**2. PR Preview Deployments:** +When a pull request modifies files inside `frontend/`, a live preview is automatically deployed to GitHub Pages. A bot comment with the preview URL will be posted on your PR. The preview is cleaned up automatically when the PR is closed or merged. -Thank you for your cooperation and continued contributions. +Thank you for your cooperation and continued contributions. \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 066d87a2..3323ab97 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -82,7 +82,7 @@ function App() { })} >
- + Loading...
}> }> diff --git a/frontend/src/components/InvoicePreview.jsx b/frontend/src/components/InvoicePreview.jsx index 3e7e895d..148a0f86 100644 --- a/frontend/src/components/InvoicePreview.jsx +++ b/frontend/src/components/InvoicePreview.jsx @@ -68,7 +68,7 @@ const InvoicePreview = ({
Chainvoice { diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 74ac36ed..b57e9f2f 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -149,7 +149,7 @@ function Navbar() { closeMobileMenu(); }} > - logo + logo

Chainvoice

diff --git a/frontend/src/page/About.jsx b/frontend/src/page/About.jsx index 7fec7b51..19225d6d 100644 --- a/frontend/src/page/About.jsx +++ b/frontend/src/page/About.jsx @@ -467,7 +467,7 @@ function About() { rel="noopener noreferrer" > Stability Nexus diff --git a/frontend/src/page/BatchPayment.jsx b/frontend/src/page/BatchPayment.jsx index 524524e6..faee3f0c 100644 --- a/frontend/src/page/BatchPayment.jsx +++ b/frontend/src/page/BatchPayment.jsx @@ -1312,7 +1312,7 @@ function BatchPayment() {
- Chainvoice + Chainvoice

Cha diff --git a/frontend/src/page/Landing.jsx b/frontend/src/page/Landing.jsx index cde39c8c..dd32f840 100644 --- a/frontend/src/page/Landing.jsx +++ b/frontend/src/page/Landing.jsx @@ -39,7 +39,7 @@ function Landing() {

Lit Protocol @@ -65,14 +65,14 @@ function Landing() { className="relative" > Secure Invoice Dashboard
Lit Protocol @@ -210,7 +210,7 @@ function Landing() { className="relative" > Token Payment Flow @@ -268,7 +268,7 @@ function Landing() { rel="noopener noreferrer" > Stability Nexus diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index 70912444..31f76d38 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -1638,7 +1638,7 @@ function ReceivedInvoice() {
Chainvoice { diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index e5278469..b4aff652 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -732,7 +732,7 @@ function SentInvoice() {
Chainvoice { diff --git a/frontend/src/utils/generateInvoicePDF.js b/frontend/src/utils/generateInvoicePDF.js index b4197936..5408d2e0 100644 --- a/frontend/src/utils/generateInvoicePDF.js +++ b/frontend/src/utils/generateInvoicePDF.js @@ -13,7 +13,8 @@ import { const loadLogoImage = async () => { try { const invoiceElement = document.getElementById("invoice-print"); - const logoImg = invoiceElement?.querySelector('img[src="/logo.png"]') || + const logoPath = `${import.meta.env.BASE_URL}logo.png`; + const logoImg = invoiceElement?.querySelector(`img[src="${logoPath}"]`) || invoiceElement?.querySelector('img[src*="logo"]'); if (logoImg) { @@ -52,7 +53,7 @@ const loadLogoImage = async () => { } try { - const response = await fetch('/logo.png', { + const response = await fetch(`${import.meta.env.BASE_URL}logo.png`, { method: 'GET', headers: { 'Cache-Control': 'no-cache', @@ -143,7 +144,7 @@ const loadLogoImage = async () => { reject(new Error('Image load failed')); }; - img.src = "/logo.png"; + img.src = `${import.meta.env.BASE_URL}logo.png`; }); return logoDataUrl;