Skip to content

poc(contracts): permissionles contract sign#12

Merged
AdriaCarrera merged 2 commits intochore/dapp-examplefrom
poc/permissionless-contract-sign
Jun 2, 2025
Merged

poc(contracts): permissionles contract sign#12
AdriaCarrera merged 2 commits intochore/dapp-examplefrom
poc/permissionless-contract-sign

Conversation

@GuillemGarciaDev
Copy link
Collaborator

@GuillemGarciaDev GuillemGarciaDev commented May 22, 2025

poc(contracts): permissionles contract sign

Changes 🛠️

app/package

  • Remove permissions on fa contract

Summary by CodeRabbit

  • New Features

    • Introduced a sample React wallet application with authentication, public key derivation, account creation, transaction signing, and MPC (Multi-Party Computation) integration.
    • Added MPC signing support to the FastAuth contract, with new contract fields and methods for MPC address and key version management.
    • Implemented a mock MPC contract and extended cross-contract interfaces for MPC signing.
  • Bug Fixes

    • Updated contract and script references to use new contract addresses and payloads.
  • Documentation

    • Expanded and clarified project and relayer documentation, including architecture diagrams and detailed feature descriptions.
  • Chores

    • Enhanced .gitignore files to cover more development artifacts and environment-specific files.
    • Updated workspace and package configurations to include new contracts and example projects.
  • Tests

    • Added integration tests for signing and MPC-related contract functionality.
  • Style

    • Added comprehensive CSS styling for the wallet application and its pages.

@coderabbitai
Copy link

coderabbitai bot commented May 22, 2025

📝 Walkthrough

Walkthrough

This update introduces a full-stack expansion for FastAuth with major changes across contracts, scripts, documentation, and a new example wallet application. Key changes include a new MPC mock contract, expanded JWT guard logic with detailed NEAR transaction types, refactored contract interfaces for MPC signing, and a React + Vite wallet example with Auth0 integration and transaction workflows. Supporting scripts, configuration, and documentation are also updated.

Changes

Files / Paths Change Summary
.gitignore, examples/wallet/.gitignore Expanded ignore rules to cover more build, log, OS-specific, and IDE/editor files for root and wallet example.
README.md, docs/relayer/index.md, examples/wallet/README.md Enhanced documentation: project overview, features, architecture image, and wallet example intro.
pnpm-workspace.yaml Updated workspace packages: removed old contract, added wallet example and new contract directories.
contracts/fa-guard-jwt-rs256/Cargo.toml Added Rust dependencies: borsh, serde_with, thiserror.
contracts/fa-guard-jwt-rs256/src/jwt.rs Refactored and expanded JWT claims: replaced simple field with full NEAR transaction/action types, added serialization logic, and decimal formatting module.
contracts/fa-guard-jwt-rs256/src/lib.rs Changed verify method to return (bool, String, Vec<u8>) instead of string tuple; updated usage and tests accordingly.
contracts/fa/Cargo.toml Added mocks/mpc to workspace members.
contracts/fa/mocks/mpc/Cargo.toml, contracts/fa/mocks/mpc/src/lib.rs Introduced new Rust mock MPC contract with signing interface and dummy signature logic.
contracts/fa/package.json Split and extended mock build scripts for both external-guard and mpc.
contracts/fa/src/external_contracts.rs Added cross-contract MPC interface and related data structures (SignRequest, SignResponse).
contracts/fa/src/lib.rs Added MPC contract address/version state, owner setter/getter, new payable sign method, callbacks for verification and signing, and updated tests.
contracts/fa/src/permission.rs Removed obsolete FaPermission struct and logic.
contracts/fa/tests/test_integration.rs Added integration tests for signing flow and MPC state management.
examples/wallet/* Introduced new React + Vite wallet example: includes config, polyfills, ESLint, TypeScript setup, pages, components for Auth0 login, key derivation, transaction creation, signing, sending, and FastAuth relayer service logic.
scripts/jwt-verify.sh Updated contract addresses, JWT payloads, and calls to match new deployment; added MPC address and sign calls.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Wallet (React)
    participant Auth0
    participant FastAuthRelayer (JS)
    participant FastAuth Contract
    participant Guard Contract
    participant MPC Contract

    User->>Wallet (React): Clicks "Sign"
    Wallet (React)->>Auth0: Requests JWT (loginWithRedirect)
    Auth0-->>Wallet (React): Returns JWT
    Wallet (React)->>FastAuthRelayer: sign(jwt)
    FastAuthRelayer->>FastAuth Contract: sign(guard_id, jwt) [payable]
    FastAuth Contract->>Guard Contract: verify(jwt)
    Guard Contract-->>FastAuth Contract: (bool, sub, payload_bytes)
    FastAuth Contract->>MPC Contract: sign(SignRequest(payload_hash, path, key_version))
    MPC Contract-->>FastAuth Contract: SignResponse(signature)
    FastAuth Contract-->>FastAuthRelayer: SignResponse(signature)
    FastAuthRelayer-->>Wallet (React): SignResponse(signature)
    Wallet (React)->>FastAuthRelayer: send(signature, tx)
    FastAuthRelayer->>NEAR: Sends signed transaction
Loading

Possibly related PRs

  • [TA-4417]: jwt guard #5: Introduced the initial FaJwtGuard contract with JWT algorithm trait, registry, and verification proxy logic; this PR directly extends and refactors that guard contract, updating the verification interface and JWT claims structure.

Suggested labels

contracts

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


Note

⚡️ Faster reviews with caching

CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.
Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@GuillemGarciaDev GuillemGarciaDev changed the base branch from main to contracts/feat/sign-with-mpc May 22, 2025 09:20
@GuillemGarciaDev GuillemGarciaDev changed the base branch from contracts/feat/sign-with-mpc to chore/dapp-example May 22, 2025 09:21
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (24)
.gitignore (1)

41-41: ⚠️ Potential issue

Fix the ignore pattern for FastAuth JSON files.
The entry fast-auth-.json only matches a literal fast-auth-.json. To ignore any JSON file starting with fast-auth-, update it to:

-fast-auth-.json
+fast-auth-*.json
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

-fast-auth-.json
+fast-auth-*.json
🤖 Prompt for AI Agents
In the .gitignore file at line 41, the pattern 'fast-auth-.json' only matches
the exact filename 'fast-auth-.json'. To ignore all JSON files starting with
'fast-auth-', change the pattern to 'fast-auth-*.json' so it correctly matches
any file with that prefix and a .json extension.
examples/wallet/src/polyfills/buffer/index.ts (1)

1-3: 🛠️ Refactor suggestion

Fix TypeScript error on global Buffer assignment.
Directly assigning to globalThis.Buffer can produce a TS compile error (Property 'Buffer' does not exist on type 'GlobalThis'). Cast globalThis to any or augment the global scope.

Apply this diff:

 import { Buffer } from "buffer";
-globalThis.Buffer = Buffer;
+// Polyfill global Buffer for browser environments
+;(globalThis as any).Buffer = Buffer;

This ensures compatibility with TypeScript without additional declaration files.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

import { Buffer } from "buffer";

// Polyfill global Buffer for browser environments
;(globalThis as any).Buffer = Buffer;
🤖 Prompt for AI Agents
In examples/wallet/src/polyfills/buffer/index.ts lines 1 to 3, assigning Buffer
directly to globalThis.Buffer causes a TypeScript error because Buffer is not
declared on the GlobalThis type. Fix this by casting globalThis to any before
assignment, like (globalThis as any).Buffer = Buffer, to bypass the type error
without needing extra declaration files.
README.md (1)

43-43: 💡 Verification agent

🧩 Analysis chain

Verify that the add_guard method is now permissionless.

This aligns with the PR objective of enabling permissionless contract signing. Make sure the contract implementation actually removes the permissions from the add_guard method.


🏁 Script executed:

#!/bin/bash
# Search for the add_guard method implementation in the contract code
fd -t f -e rs -e json "fa.json\$|fa/src" | xargs rg -A 10 "add_guard|AddGuard"

Length of output: 6514


Remove ownership check from add_guard and update README

The add_guard method in contracts/fa/src/lib.rs still enforces an owner-only guard via the call to self.only_owner(), so it isn’t permissionless yet. To complete this PR:

  • In contracts/fa/src/lib.rs, remove the line:
    self.only_owner();
  • In README.md (around line 43), mark the checklist item as done:
    - [ ] `add_guard` method permissionless
    + [x] `add_guard` method permissionless

This aligns the documentation with the updated, permissionless implementation.

🤖 Prompt for AI Agents
In README.md at line 43, update the checklist item for the `add_guard` method to
mark it as done by changing "[ ]" to "[x]". Additionally, in
contracts/fa/src/lib.rs, locate the `add_guard` method and remove the line that
calls `self.only_owner();` to eliminate the ownership check and make the method
permissionless as intended.
examples/wallet/src/pages/Sign.tsx (1)

55-56: ⚠️ Potential issue

Add validation and sanitization for displayed parameters

The component displays parameters directly from URL query strings without validation or sanitization. This could lead to XSS vulnerabilities if malicious input is provided.

- Permissions: <span className="param-value">{permissions}</span>
+ Permissions: <span className="param-value">{sanitizeHtml(permissions)}</span>

Consider adding a sanitization function or library like DOMPurify to sanitize the values before rendering them, and implement validation to ensure the parameters meet expected formats.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In examples/wallet/src/pages/Sign.tsx around lines 55 to 56, the permissions
parameter is rendered directly from URL query strings without validation or
sanitization, posing an XSS risk. To fix this, implement validation to check
that permissions conform to expected formats and sanitize the value before
rendering using a library like DOMPurify or a custom sanitization function to
prevent injection of malicious code.
examples/wallet/src/components/CreateAccount.tsx (4)

14-16: 🛠️ Refactor suggestion

Improve form validation with user-friendly messages

Replace error throwing with proper form validation and user feedback.

- if (!accountId) {
-     throw new Error("Account ID is required");
- }
+ if (!accountId) {
+     setError("Account ID is required");
+     return;
+ }
+ 
+ // Add validation for NEAR account ID format
+ const nearAccountIdRegex = /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/;
+ if (!nearAccountIdRegex.test(accountId)) {
+     setError("Invalid NEAR account ID format");
+     return;
+ }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In examples/wallet/src/components/CreateAccount.tsx around lines 14 to 16, the
code throws an error when accountId is missing, which is not user-friendly.
Replace the error throwing with proper form validation logic that sets a
validation state or message, and display this message in the UI to provide
clear, user-friendly feedback instead of crashing the app.

17-18: ⚠️ Potential issue

Add proper error handling and loading state

The current implementation lacks error handling for the account creation process and doesn't provide user feedback on progress or results.

- console.log("Creating account", accountId, publicKey);
- relayer.createAccount(accountId, publicKey);
+ setLoading(true);
+ setError(null);
+ try {
+     await relayer.createAccount(accountId, publicKey);
+     setSuccess("Account created successfully!");
+ } catch (error) {
+     console.error("Error creating account:", error);
+     setError(`Failed to create account: ${error instanceof Error ? error.message : String(error)}`);
+ } finally {
+     setLoading(false);
+ }

You'll need to add state variables for loading, error, and success states, and update the component to display appropriate feedback.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In examples/wallet/src/components/CreateAccount.tsx around lines 17 to 18, the
account creation process lacks error handling and user feedback for loading and
success states. Introduce state variables to track loading, error, and success
statuses. Update the UI to reflect these states by showing a loading indicator
during the process, displaying error messages if creation fails, and confirming
success when the account is created. Wrap the createAccount call in a try-catch
block to handle errors properly and update the states accordingly.

8-10: 🛠️ Refactor suggestion

Improve error handling with user-friendly messages

Instead of throwing errors, consider displaying user-friendly error messages to enhance the user experience.

- if (!relayer) {
-     throw new Error("Relayer not initialized");
- }
+ if (!relayer) {
+     // Set an error state and render an error message
+     setError("Service temporarily unavailable. Please try again later.");
+     return;
+ }

You'll need to add a state variable for error messages and display it in the UI.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In examples/wallet/src/components/CreateAccount.tsx around lines 8 to 10,
replace the thrown error "Relayer not initialized" with a state variable to hold
error messages. Initialize a state variable for the error message, set it when
the relayer is not initialized, and update the component's JSX to conditionally
render this error message in a user-friendly way instead of throwing an error.

20-26: 🛠️ Refactor suggestion

Enhance the UI with feedback states

Update the UI to show loading states, error messages, and success confirmations to improve user experience.

 return (
     <div>
+        {error && <div className="error-message">{error}</div>}
+        {success && <div className="success-message">{success}</div>}
         <form onSubmit={handleSubmit}>
             <input type="text" placeholder="Enter account ID" name="accountid" />
-            <button type="submit">Create Account</button>
+            <button type="submit" disabled={loading}>
+                {loading ? "Creating..." : "Create Account"}
+            </button>
         </form>
     </div>
 );
🤖 Prompt for AI Agents
In examples/wallet/src/components/CreateAccount.tsx around lines 20 to 26, the
form UI lacks feedback states. Enhance the component by adding state variables
to track loading, error, and success statuses. Update the JSX to conditionally
render messages or indicators based on these states, such as showing a loading
spinner during submission, displaying error messages if submission fails, and
confirming success when the account is created.
examples/wallet/src/index.css (1)

57-63: ⚠️ Potential issue

Color contrast issues in dark mode

The span and li elements are set to black (#000) color, which will have poor contrast against the dark background (#242424) when in dark mode. This creates accessibility issues for users.

Consider modifying the color to adapt based on the color scheme:

span {
-    color: #000;
+    color: var(--text-color, #000);
}

li {
-    color: #000;
+    color: var(--text-color, #000);
}

@media (prefers-color-scheme: light) {
    :root {
        color: #213547;
        background-color: #ffffff;
+        --text-color: #000;
    }
    /* ... */
}

@media (prefers-color-scheme: dark) {
+    :root {
+        --text-color: rgba(255, 255, 255, 0.87);
+    }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

span {
    color: var(--text-color, #000);
}

li {
    color: var(--text-color, #000);
}

@media (prefers-color-scheme: light) {
    :root {
        color: #213547;
        background-color: #ffffff;
        --text-color: #000;
    }
    /* ... */
}

@media (prefers-color-scheme: dark) {
    :root {
        --text-color: rgba(255, 255, 255, 0.87);
    }
}
🤖 Prompt for AI Agents
In examples/wallet/src/index.css around lines 57 to 63, the span and li elements
are styled with black color (#000), which causes poor contrast against the dark
background (#242424) in dark mode. To fix this, update the CSS to use media
queries or CSS variables that adjust the text color based on the user's color
scheme preference, ensuring sufficient contrast in both light and dark modes for
accessibility.
examples/wallet/src/components/SignButton.tsx (2)

16-27: 🛠️ Refactor suggestion

Add error handling for sign operation.

The handleSign method lacks proper error handling. If the relayer operation fails or if JWT decoding fails, the error is not caught or displayed to the user.

 const handleSign = async () => {
+    try {
       if (!relayer) {
           throw new Error("Relayer not initialized");
       }
       const jwt = await getAccessTokenSilently();
       const claims = jose.decodeJwt(jwt);
       const tx = Transaction.decode(claims["fatxn"] as Uint8Array<ArrayBufferLike>);
       setTx(tx);
       const result = await relayer?.sign(jwt);

       setSignature(JSON.parse(Buffer.from(result.status.SuccessValue, "base64").toString()));
+    } catch (error) {
+        console.error("Error signing transaction:", error);
+        // Consider adding state to display errors to the user
+    }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const handleSign = async () => {
        try {
            if (!relayer) {
                throw new Error("Relayer not initialized");
            }
            const jwt = await getAccessTokenSilently();
            const claims = jose.decodeJwt(jwt);
            const tx = Transaction.decode(claims["fatxn"] as Uint8Array<ArrayBufferLike>);
            setTx(tx);
            const result = await relayer?.sign(jwt);

            setSignature(JSON.parse(Buffer.from(result.status.SuccessValue, "base64").toString()));
        } catch (error) {
            console.error("Error signing transaction:", error);
            // Consider adding state to display errors to the user
        }
    };
🤖 Prompt for AI Agents
In examples/wallet/src/components/SignButton.tsx around lines 16 to 27, the
handleSign function lacks error handling for potential failures in relayer
operations and JWT decoding. Wrap the entire function body in a try-catch block
to catch any errors thrown during these operations. In the catch block, handle
the error appropriately by logging it or displaying a user-friendly error
message to inform the user of the failure.

29-56: 🛠️ Refactor suggestion

Improve error handling and validation in the send method.

The handleSend method has several potential points of failure without proper error handling or validation:

  1. No validation for signature format
  2. No error handling for cryptographic operations
  3. No handling for failed transaction sends
 const handleSend = async () => {
+    try {
       if (!signature) throw new Error("no signature");

       const secp256k1 = new EC("secp256k1");

       // Inputs from MPC signer
       // @ts-expect-error
       const compressedR = signature["big_r"]["affine_point"] as string;
       // @ts-expect-error
       const sHex = signature["s"]["scalar"] as string;
       // @ts-expect-error
       const recoveryId = signature["recovery_id"] as number;
+
+      // Validate required signature components
+      if (!compressedR || !sHex || recoveryId === undefined) {
+          throw new Error("Invalid signature format");
+      }

       // 1. Decompress big_r to get x (used as r scalar)
       const point = secp256k1.keyFromPublic(compressedR, "hex").getPublic();
       const r = point.getX().toArrayLike(Buffer, "be", 32);

       // 2. s is already the scalar
       const s = Buffer.from(sHex, "hex");

       // 3. Normalize recovery ID
       const v = recoveryId & 0x01; // should be 0 or 1

       // 4. Final 65-byte signature
       const sig = Buffer.concat([r, s, Buffer.from([v])]);

       await relayer?.send(sig, tx as Transaction);
+      // Consider adding success feedback to the user
+    } catch (error) {
+      console.error("Error sending transaction:", error);
+      // Consider adding state to display errors to the user
+    }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    const handleSend = async () => {
        try {
            if (!signature) throw new Error("no signature");

            const secp256k1 = new EC("secp256k1");

            // Inputs from MPC signer
            // @ts-expect-error
            const compressedR = signature["big_r"]["affine_point"] as string;
            // @ts-expect-error
            const sHex = signature["s"]["scalar"] as string;
            // @ts-expect-error
            const recoveryId = signature["recovery_id"] as number;

            // Validate required signature components
            if (!compressedR || !sHex || recoveryId === undefined) {
                throw new Error("Invalid signature format");
            }

            // 1. Decompress big_r to get x (used as r scalar)
            const point = secp256k1.keyFromPublic(compressedR, "hex").getPublic();
            const r = point.getX().toArrayLike(Buffer, "be", 32);

            // 2. s is already the scalar
            const s = Buffer.from(sHex, "hex");

            // 3. Normalize recovery ID
            const v = recoveryId & 0x01; // should be 0 or 1

            // 4. Final 65-byte signature
            const sig = Buffer.concat([r, s, Buffer.from([v])]);

            await relayer?.send(sig, tx as Transaction);
            // Consider adding success feedback to the user
        } catch (error) {
            console.error("Error sending transaction:", error);
            // Consider adding state to display errors to the user
        }
    };
🤖 Prompt for AI Agents
In examples/wallet/src/components/SignButton.tsx around lines 29 to 56, the
handleSend function lacks validation for the signature format, error handling
for cryptographic operations, and handling for failed transaction sends. To fix
this, add checks to validate the structure and types of the signature properties
before use, wrap cryptographic operations in try-catch blocks to catch and
handle errors gracefully, and handle possible failures from the relayer.send
call by using try-catch or checking the result to provide appropriate error
feedback or retries.
examples/wallet/src/pages/Home.tsx (1)

19-19: ⚠️ Potential issue

Remove JWT logging to console in production.

Logging JWT tokens to the console is a security risk in production environments as they contain sensitive information. Consider removing this or using a debug flag.

-<button onClick={() => getAccessTokenSilently().then(console.log)}>Get JWT</button>
+<button onClick={() => getAccessTokenSilently().then(() => console.log("JWT token retrieved"))}>Get JWT</button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            <button onClick={() => getAccessTokenSilently().then(() => console.log("JWT token retrieved"))}>
              Get JWT
            </button>
🤖 Prompt for AI Agents
In examples/wallet/src/pages/Home.tsx at line 19, the code logs the JWT token to
the console, which is a security risk in production. Modify the button's onClick
handler to conditionally log the token only in development mode by checking an
environment variable or debug flag, or remove the console.log entirely to avoid
exposing sensitive information.
examples/wallet/src/components/DerivePublicKeyButton.tsx (2)

14-20: 🛠️ Refactor suggestion

Add proper error handling for key derivation.

The handleClick method throws errors but doesn't catch them, which could lead to unhandled exceptions and poor user experience.

+const [error, setError] = useState<string | null>(null);
+const [isLoading, setIsLoading] = useState(false);

 const handleClick = async () => {
+    setError(null);
+    setIsLoading(true);
+    try {
         if (!relayer) {
             throw new Error("Relayer not initialized");
         }
         const publicKey = await relayer?.derivePublicKey(sub);
         setPublicKey(publicKey);
+    } catch (err) {
+        setError(err instanceof Error ? err.message : "Failed to derive public key");
+        console.error("Error deriving public key:", err);
+    } finally {
+        setIsLoading(false);
+    }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    // add loading and error state
    const [error, setError] = useState<string | null>(null);
    const [isLoading, setIsLoading] = useState(false);

    const handleClick = async () => {
        setError(null);
        setIsLoading(true);
        try {
            if (!relayer) {
                throw new Error("Relayer not initialized");
            }
            const publicKey = await relayer?.derivePublicKey(sub);
            setPublicKey(publicKey);
        } catch (err) {
            setError(err instanceof Error ? err.message : "Failed to derive public key");
            console.error("Error deriving public key:", err);
        } finally {
            setIsLoading(false);
        }
    };
🤖 Prompt for AI Agents
In examples/wallet/src/components/DerivePublicKeyButton.tsx around lines 14 to
20, the handleClick function throws errors without catching them, risking
unhandled exceptions. Wrap the asynchronous code inside handleClick with a
try-catch block to catch any errors during key derivation. In the catch block,
handle the error gracefully, such as by logging it or showing a user-friendly
message, to improve user experience and prevent crashes.

22-52: 🛠️ Refactor suggestion

Add form validation and improve error handling.

The handleTransfer method lacks input validation for form fields and proper error handling for the asynchronous operations.

+const [transferError, setTransferError] = useState<string | null>(null);
+const [isTransferring, setIsTransferring] = useState(false);

 const handleTransfer = async (e: React.FormEvent<HTMLFormElement>) => {
     e.preventDefault();
+    setTransferError(null);
+    setIsTransferring(true);
+    try {
         if (!relayer) {
             throw new Error("Relayer not initialized");
         }
         if (!publicKey) {
             throw new Error("Public key not derived");
         }
         const formData = new FormData(e.target as HTMLFormElement);
         const accountId = formData.get("accountid") as string;
         const receiverId = formData.get("receiverid") as string;
         const amount = formData.get("amount") as string;
+        
+        // Validate form inputs
+        if (!accountId || !receiverId || !amount) {
+            throw new Error("All fields are required");
+        }
+        
+        // Validate amount is a valid number
+        const amountNum = parseFloat(amount);
+        if (isNaN(amountNum) || amountNum <= 0) {
+            throw new Error("Amount must be a positive number");
+        }
         
         const tx = await relayer?.createTransfer(accountId, PublicKey.fromString(publicKey), receiverId, amount);
-        console.log("tx", tx);
-        console.log("encodedTransaction", encodeTransaction(tx).toString());
+        
         const messageBytes: number[] = [];
         encodeTransaction(tx).forEach((b) => {
             messageBytes.push(b);
         });

         await loginWithRedirect({
             authorizationParams: {
                 imageUrl:
                     "https://media.licdn.com/dms/image/v2/D4D0BAQH5KL-Ge_0iug/company-logo_200_200/company-logo_200_200/0/1696280807541/peersyst_technology_logo?e=2147483647&v=beta&t=uFYvQ5g6HDoIprYhNNV_zC7tzlBkvmPRkWzuLuDpHtc",
                 name: "Peersyst Technology",
                 transaction: messageBytes,
                 receiverId: tx.receiverId,
                 actions: tx.actions,
             },
         });
+    } catch (err) {
+        setTransferError(err instanceof Error ? err.message : "Failed to create transfer");
+        console.error("Error creating transfer:", err);
+    } finally {
+        setIsTransferring(false);
+    }
 };
🤖 Prompt for AI Agents
In examples/wallet/src/components/DerivePublicKeyButton.tsx around lines 22 to
52, the handleTransfer function lacks validation for form inputs and does not
handle errors from async calls. Add checks to ensure accountId, receiverId, and
amount are valid and non-empty before proceeding. Wrap the asynchronous
operations in try-catch blocks to catch and handle errors gracefully, providing
user feedback or logging errors instead of throwing uncaught exceptions.
contracts/fa/src/external_contracts.rs (2)

7-10: ⚠️ Potential issue

Mismatch between declared guard interface and actual callback signature

verify is declared to return (bool, String, String), yet FastAuth::on_verify_callback (see contracts/fa/src/lib.rs l.219-231) expects (bool, String, Vec<u8>).
If the guard implementation was already updated to return a Vec<u8> (as hinted by the callback), this interface is now stale and cross-contract calls will fail to deserialize, panicking at runtime.

-    fn verify(&self, jwt: String) -> (bool, String, String);
+    fn verify(&self, jwt: String) -> (bool, String, Vec<u8>);

Please align both sides of the interface before deploying.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

#[ext_contract(external_guard)]
pub trait ExternalGuard {
    fn verify(&self, jwt: String) -> (bool, String, Vec<u8>);
}
🤖 Prompt for AI Agents
In contracts/fa/src/external_contracts.rs around lines 7 to 10, the
ExternalGuard trait's verify method returns (bool, String, String), but the
on_verify_callback in contracts/fa/src/lib.rs expects (bool, String, Vec<u8>).
Update the verify method's return type to (bool, String, Vec<u8>) to match the
callback signature and ensure proper deserialization during cross-contract
calls.

41-44: 🛠️ Refactor suggestion

Consider making sign a mutating call

Most MPC implementations persist some metadata (nonces, transcripts, etc.).
Declaring the trait method with &self marks it as a view for NEAR.
If the MPC contract needs to touch state, the method must be &mut self (i.e. a change-method) or the call will be rejected.

-    fn sign(&self, request: SignRequest) -> PromiseOrValue<SignResponse>;
+    fn sign(&mut self, request: SignRequest) -> PromiseOrValue<SignResponse>;

Verify the actual MPC contract implementation and adjust as necessary.

🤖 Prompt for AI Agents
In contracts/fa/src/external_contracts.rs around lines 41 to 44, the sign method
in the MPCContract trait is declared with &self, marking it as a view method.
Since MPC implementations often modify state, change the method receiver from
&self to &mut self to make it a mutating call. Verify the MPC contract
implementation to confirm state changes and update the trait method signature
accordingly to avoid call rejections.
contracts/fa/src/lib.rs (1)

262-288: ⚠️ Potential issue

Attached deposit is silently kept on failed verification

If verification fails, the callback returns an empty Promise::new(env::current_account_id()), leaving the original deposit locked in the FastAuth contract.
Attackers could intentionally send invalid JWTs to drain users’ funds.

Consider immediately refunding the caller:

-            env::log_str("Guard verification failed");
-            return Promise::new(env::current_account_id());
+            env::log_str("Guard verification failed – refunding deposit");
+            Promise::new(env::predecessor_account_id()).transfer(attached_deposit)

Do the same for the !verification_result branch.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In contracts/fa/src/lib.rs around lines 262 to 288, when verification fails or
is rejected, the attached deposit is not refunded and remains locked in the
contract. To fix this, modify both error branches to create a Promise that
transfers the attached_deposit back to the caller's account instead of returning
an empty Promise. Use env::predecessor_account_id() as the recipient and attach
the full attached_deposit amount for refunding.
examples/wallet/src/services/fast-auth-relayer.ts (2)

10-14: 🛠️ Refactor suggestion

Hard-coded private key and account ID – security risk

Committing real credentials to the repo exposes the relayer account to anyone.
Inject them through environment variables or a secure secrets manager instead.

-const RELAYER_PRIVATE_KEY = "...";
-const RELAYER_ACCOUNT_ID = "bosisthenear.testnet";
+const RELAYER_PRIVATE_KEY = process.env.RELAYER_PRIVATE_KEY!;
+const RELAYER_ACCOUNT_ID  = process.env.RELAYER_ACCOUNT_ID!;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

// TODO: Replace with your actual private key and account ID
-const RELAYER_PRIVATE_KEY = "ed25519:5txww6eaySfKnDTXDRK7H425qpiTyk4biE6rPeC6qwdzYDv5Xw5S258yWXdafgdfwdEBcW3SvfKJ9L5BNVMnitmJ"; // Replace with your private key
-const RELAYER_ACCOUNT_ID = "bosisthenear.testnet"; // Replace with your account ID
+const RELAYER_PRIVATE_KEY = process.env.RELAYER_PRIVATE_KEY!;
+const RELAYER_ACCOUNT_ID  = process.env.RELAYER_ACCOUNT_ID!;
const FAST_AUTH_CONTRACT_ID = "fa-poc.testnet";
🤖 Prompt for AI Agents
In examples/wallet/src/services/fast-auth-relayer.ts around lines 10 to 14, the
private key and account ID are hard-coded, posing a security risk. Remove the
hard-coded values and instead read RELAYER_PRIVATE_KEY and RELAYER_ACCOUNT_ID
from environment variables using process.env. Add appropriate checks to ensure
these environment variables are set and provide clear error messages if they are
missing.

105-119: ⚠️ Potential issue

Incorrect receiver ID in createAccount transaction

createTransaction expects (signer_id, public_key, receiver_id, ...).
Passing "testnet" as receiver_id will attempt to create an account named testnet, which obviously exists and will be rejected.

-                "testnet",
+                accountId,

Additionally, a CreateAccount + Transfer action pair is required; invoking a functionCall("create_account", …) won't work.
Consider using createAccount + transfer actions from near-api-js/lib/transaction.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            const tx = createTransaction(
                this.accountId,
                signerPublicKey,
                accountId,
                nonce,
                [
                    functionCall(
                        "create_account",
                        { new_public_key: publicKey, new_account_id: accountId },
                        300000000000000n,
                        BigInt(parseNearAmount("0")!),
                    ),
                ],
                base_decode(accessKey.block_hash),
            );
🤖 Prompt for AI Agents
In examples/wallet/src/services/fast-auth-relayer.ts around lines 105 to 119,
the createTransaction call incorrectly uses "testnet" as the receiver_id, which
should be the new account ID instead. Replace the functionCall action with a
proper CreateAccount action followed by a Transfer action to fund the new
account, using the appropriate action creators from near-api-js/lib/transaction.
Ensure the receiver_id parameter is set to the new account ID to correctly
target the account creation.
contracts/fa-guard-jwt-rs256/src/jwt.rs (5)

16-34: 🛠️ Refactor suggestion

Field names use CamelCase instead of Rust snake_case; consider rename for JSON compatibility

All structs (Transaction, Action payloads, etc.) use signerId, publicKey, … which is atypical for Rust and will surprise contributors & tooling.
If the JSON must stay in CamelCase to align with NEAR RPC, prefer idiomatic field names with explicit #[serde(rename = "...")]:

pub struct Transaction {
    #[serde(rename = "signerId")]
    signer_id: AccountId,
    // …
}

Maintains readability and external compatibility.

🤖 Prompt for AI Agents
In contracts/fa-guard-jwt-rs256/src/jwt.rs between lines 16 and 34, the struct
Transaction uses CamelCase field names which is against Rust's snake_case
convention. To fix this, rename all fields to snake_case (e.g., signer_id,
public_key) and add #[serde(rename = "...")] attributes with the original
CamelCase names to each field to maintain JSON compatibility with NEAR RPC. This
keeps the code idiomatic while preserving external interface expectations.

70-90: 🛠️ Refactor suggestion

AccessKey representation is incomplete and diverges from NEAR spec

The real NEAR AccessKey includes a nonce and a permission enum (FullAccess / FunctionCall).
This stripped-down version (only accountId & publicKey) will prevent round-tripping full transactions and may break signature verification.

Consider:

  1. Re-using near_primitives::account::AccessKey via borsh/serde feature flags, or
  2. Replicating the complete structure (including nonce & AccessKeyPermission).

Failing to do so may cause silent data loss when tokens are deserialised.

🤖 Prompt for AI Agents
In contracts/fa-guard-jwt-rs256/src/jwt.rs between lines 70 and 90, the
AccessKey struct is incomplete as it lacks the nonce and permission fields
defined in the NEAR specification. To fix this, either import and reuse the full
AccessKey struct from near_primitives::account with the appropriate borsh/serde
feature flags enabled, or extend the current AccessKey struct to include the
nonce field and a permission enum matching NEAR's AccessKeyPermission
(FullAccess and FunctionCall). This ensures full compatibility and prevents data
loss during serialization and deserialization.

2-3: ⚠️ Potential issue

Nonce re-export is missing in near_sdk – will not compile

near_sdk currently re-exports AccountId, CryptoHash, PublicKey, and Gas, but does not re-export the Nonce alias that lives in near_primitives::types.
use near_sdk::{ … Nonce }; therefore fails to compile.

-use near_sdk::{AccountId, CryptoHash, PublicKey, Gas, Nonce};
+use near_sdk::{AccountId, CryptoHash, PublicKey, Gas};
+use near_primitives::types::Nonce;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

use near_sdk::{AccountId, CryptoHash, PublicKey, Gas};
use near_primitives::types::Nonce;
use near_sdk::borsh::{BorshDeserialize, BorshSerialize};
🤖 Prompt for AI Agents
In contracts/fa-guard-jwt-rs256/src/jwt.rs at lines 2 to 3, the import statement
tries to use Nonce from near_sdk, but Nonce is not re-exported there and causes
a compilation error. To fix this, remove Nonce from the near_sdk import and
instead import Nonce directly from near_primitives::types. Adjust the use
statements accordingly to ensure Nonce is correctly imported from its original
crate.

162-169: ⚠️ Potential issue

gas should also use the dec_format wrapper to avoid JS precision loss

Gas is an alias for u64; serialising it as a JSON number risks truncation in JS environments.
Apply the same decimal-string wrapper already used for deposit/stake.

-    pub gas: Gas,
+    #[serde(with = "dec_format")]
+    pub gas: Gas,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

pub struct FunctionCallAction {
    pub methodName: String,
    #[serde_as(as = "Base64")]
    pub args: Vec<u8>,
    #[serde(with = "dec_format")]
    pub gas: Gas,
    #[serde(with = "dec_format")]
    pub deposit: u128,
}
🤖 Prompt for AI Agents
In contracts/fa-guard-jwt-rs256/src/jwt.rs around lines 162 to 169, the Gas
field in FunctionCallAction is currently serialized as a JSON number, which can
cause precision loss in JavaScript. To fix this, apply the dec_format serde
wrapper to the gas field, similar to how it is applied to the deposit field,
ensuring gas is serialized as a decimal string to avoid JS precision issues.

8-14: ⚠️ Potential issue

fatxn should be Base64-encoded & struct should derive Debug/Clone

Vec<u8> is currently serialised as a JSON array of numbers, producing JWTs that are both huge and non-portable.
Using serde_with::Base64 (already in the dependency list) keeps the token compact and matches the encoding you applied to other binary fields.

-#[derive(Serialize, Deserialize)]
-pub struct FaJwtCustomClaims {
-    /// The FastAuth permissions claim that specifies what actions are allowed
-    pub fatxn: Vec<u8>,
-    /// The subject identifier claim that uniquely identifies the user
-    pub sub: String,
-}
+#[serde_as]
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct FaJwtCustomClaims {
+    /// Binary transaction payload – Base64-encoded inside the JWT
+    #[serde_as(as = "Base64")]
+    pub fatxn: Vec<u8>,
+    /// Subject identifier
+    pub sub: String,
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FaJwtCustomClaims {
    /// Binary transaction payload – Base64-encoded inside the JWT
    #[serde_as(as = "Base64")]
    pub fatxn: Vec<u8>,
    /// Subject identifier
    pub sub: String,
}
🤖 Prompt for AI Agents
In contracts/fa-guard-jwt-rs256/src/jwt.rs around lines 8 to 14, the fatxn field
is a Vec<u8> that serializes as a JSON array, making JWTs large and
non-portable. To fix this, derive Debug and Clone for the FaJwtCustomClaims
struct and apply serde_with::Base64 encoding to the fatxn field to ensure it is
Base64-encoded during serialization, keeping the token compact and consistent
with other binary fields.

@AdriaCarrera AdriaCarrera merged commit 50fc878 into chore/dapp-example Jun 2, 2025
4 checks passed
@AdriaCarrera AdriaCarrera deleted the poc/permissionless-contract-sign branch June 2, 2025 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments