Skip to content

Add payroll tool example with Arkade SDK#248

Draft
tiero wants to merge 2 commits intomasterfrom
claude/add-payroll-example-01SQktgUqomZzzW39inpsobc
Draft

Add payroll tool example with Arkade SDK#248
tiero wants to merge 2 commits intomasterfrom
claude/add-payroll-example-01SQktgUqomZzzW39inpsobc

Conversation

@tiero
Copy link
Copy Markdown
Member

@tiero tiero commented Dec 10, 2025

A minimal React + TypeScript web frontend demonstrating:

  • Assistant view: Create payroll batches with +/- UX and CSV import
  • Admin view: View pending payrolls, execute with private key signing
  • Lendaswap integration for funding via USDT on Ethereum

Features:

  • Multi-recipient payment support using wallet.settle()
  • Local storage persistence for payroll batches
  • Balance checking before execution
  • Responsive dark theme UI

Summary by CodeRabbit

  • New Features

    • Added Payroll example application with Assistant and Admin role-based views
    • Enabled payroll batch creation with recipient lists and CSV import capability
    • Execute batch payments with wallet integration and balance verification
    • Integrated Lendaswap for funding payrolls via USDT-to-BTC conversion
  • Documentation

    • Added comprehensive README with setup instructions, workflows, and integration examples

✏️ Tip: You can customize this high-level summary in your review settings.

A minimal React + TypeScript web frontend demonstrating:
- Assistant view: Create payroll batches with +/- UX and CSV import
- Admin view: View pending payrolls, execute with private key signing
- Lendaswap integration for funding via USDT on Ethereum

Features:
- Multi-recipient payment support using wallet.settle()
- Local storage persistence for payroll batches
- Balance checking before execution
- Responsive dark theme UI
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Dec 10, 2025

Walkthrough

This PR introduces a complete payroll example application with React, TypeScript, and Vite. It features role-based views (Assistant/Admin), payroll batch creation/execution, wallet management via Arkade SDK, and USDT-to-BTC swapping via Lendaswap integration. Includes CSV import, localStorage persistence, and comprehensive styling.

Changes

Cohort / File(s) Change Summary
Project Configuration
.gitignore, package.json, tsconfig.json, vite.config.ts, index.html, vite-env.d.ts
Configuration files and setup for Vite/React/TypeScript project with Node.js dependencies and build tooling
Documentation
README.md
Comprehensive guide detailing payroll example features, setup, development steps, architecture, workflows, SDK integration patterns, and security considerations
Core Application
src/main.tsx, src/App.tsx, src/App.css
React 18 entry point, main App component with role-based views (Assistant/Admin) and state management, plus dark-themed CSS design system with responsive layout
Payroll Components
src/components/PayrollForm.tsx, src/components/PendingPayrolls.tsx, src/components/FundingPanel.tsx, src/components/index.ts
Form for creating payroll batches with CSV import, component for managing/executing pending payrolls with modal, multi-step funding panel for Lendaswap USDT-to-BTC swaps, and component barrel exports
Payroll Service
src/services/payroll.ts
ArkadePayrollService implementing CRUD operations, wallet utilities, payroll execution with balance checks, and localStorage persistence
Lendaswap Integration
src/services/lendaswap.ts
LendaswapService implementing quote generation, swap order creation, deposit confirmation, and Arkade claim flow with localStorage persistence
Service & Type Exports
src/services/index.ts, src/types/index.ts
Barrel exports for services and comprehensive type definitions for payroll batches, recipients, network config, wallet balances, and swap operations
Utilities
src/utils/csv.ts, src/utils/index.ts
CSV parsing/generation and amount formatting utilities with validation and error handling

Sequence Diagrams

Payroll Creation Flow

sequenceDiagram
    participant User
    participant PayrollForm
    participant PayrollService
    participant Storage as localStorage
    
    User->>PayrollForm: Enter name, recipients, submit
    PayrollForm->>PayrollForm: Validate inputs
    PayrollForm->>PayrollService: createPayroll(params)
    PayrollService->>PayrollService: Generate ID, set status: draft
    PayrollService->>Storage: Save batch
    PayrollService-->>PayrollForm: Return PayrollBatch
    PayrollForm->>User: Show success, reset form
Loading

Payroll Execution Flow

sequenceDiagram
    participant User
    participant ExecuteModal
    participant PayrollService
    participant ArkadeSDK as Arkade SDK
    participant Blockchain
    
    User->>ExecuteModal: Enter private key, click Execute
    ExecuteModal->>PayrollService: getWalletBalance(privateKey)
    PayrollService->>ArkadeSDK: Check balance
    ArkadeSDK-->>PayrollService: Return balance
    PayrollService-->>ExecuteModal: Display balance
    
    alt Balance sufficient
        User->>ExecuteModal: Confirm execution
        ExecuteModal->>PayrollService: executePayroll(payrollId, privateKey)
        PayrollService->>ArkadeSDK: Create multi-output transaction
        ArkadeSDK->>Blockchain: Submit transaction
        Blockchain-->>ArkadeSDK: Return txId
        ArkadeSDK-->>PayrollService: Success with arkTxId
        PayrollService->>Storage: Update batch status: executed
        PayrollService-->>ExecuteModal: Return arkTxId
        ExecuteModal->>User: Show success message
    else Insufficient balance
        ExecuteModal->>User: Show error message
    end
Loading

Funding Flow (Lendaswap USDT→BTC)

sequenceDiagram
    participant User
    participant FundingPanel
    participant LendaswapService
    participant Ethereum
    participant Arkade as Arkade Network
    
    User->>FundingPanel: Enter USDT amount (Step: quote)
    FundingPanel->>LendaswapService: getQuote(usdtAmount)
    LendaswapService-->>FundingPanel: Return SwapQuote
    FundingPanel->>User: Show quote details
    
    User->>FundingPanel: Create swap order
    FundingPanel->>LendaswapService: createSwapOrder(amount, arkadeAddress)
    LendaswapService-->>FundingPanel: Return SwapOrder with HTLC
    FundingPanel->>User: Show deposit instructions (Step: deposit)
    
    User->>Ethereum: Send USDT to HTLC contract
    Ethereum-->>User: Provide txHash
    
    User->>FundingPanel: Enter txHash, confirm deposit
    FundingPanel->>LendaswapService: confirmDeposit(swapId, txHash)
    LendaswapService-->>FundingPanel: Mark as deposited (Step: claim)
    FundingPanel->>User: Guide to claim on Arkade
    
    User->>FundingPanel: Claim on Arkade
    FundingPanel->>LendaswapService: claimOnArkade(swapId)
    LendaswapService->>Arkade: Claim BTC with HTLC preimage
    Arkade-->>LendaswapService: Return arkTxId (Step: complete)
    LendaswapService-->>FundingPanel: Success
    FundingPanel->>User: Show success, trigger refresh
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • src/services/payroll.ts — Core business logic for wallet balance checks, payroll execution with blockchain interaction, transaction construction, and error handling; validate the SDK integration patterns and state update logic
  • src/services/lendaswap.ts — Multi-step swap flow with quote generation, order persistence, and deposit/claim interactions; review HTLC contract details and localStorage persistence mechanism
  • src/components/PendingPayrolls.tsx — Private key handling in ExecuteModal, local balance validation before execution, error messaging, and modal state management
  • src/components/FundingPanel.tsx — Stateful step machine (quote→deposit→claim→complete), async operations across multiple services, form validation, and error recovery paths
  • src/types/index.ts — Ensure type contracts align with service implementations and component prop requirements across the application

Suggested reviewers

  • bordalix
  • altafan

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main addition: a new payroll tool example that demonstrates Arkade SDK usage.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/add-payroll-example-01SQktgUqomZzzW39inpsobc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Signed-off-by: Marco Argentieri <3596602+tiero@users.noreply.github.com>
@tiero
Copy link
Copy Markdown
Member Author

tiero commented Dec 10, 2025

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Dec 10, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@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.

Actionable comments posted: 11

🧹 Nitpick comments (10)
examples/payroll/src/utils/csv.ts (1)

42-45: Address validation is insufficient.

The current check only validates that the address is longer than 10 characters. This doesn't verify the address format is valid for Ark/Bitcoin addresses.

Consider adding proper address validation:

-// Validate address (basic check for Ark addresses)
-if (!address || address.length < 10) {
-    errors.push(`Line ${lineNumber}: Invalid address "${address}"`);
+// Validate address format (Ark addresses typically start with specific prefixes)
+const addressRegex = /^[a-km-zA-HJ-NP-Z1-9]{26,90}$/; // Basic Bitcoin-style address pattern
+if (!address || !addressRegex.test(address)) {
+    errors.push(`Line ${lineNumber}: Invalid address format "${address}"`);
     continue;
 }
examples/payroll/src/components/PendingPayrolls.tsx (2)

21-27: Consider extracting status colors to a shared constant.

The status color mapping is defined inline. For consistency with the CSS custom properties pattern used elsewhere in the app, consider defining these in the CSS or a shared constants file.

-    const statusColors: Record<string, string> = {
-        draft: "#6b7280",
-        pending: "#f59e0b",
-        approved: "#3b82f6",
-        executed: "#10b981",
-        failed: "#ef4444",
-    };

Then reference via CSS custom properties or import from a constants file.


338-338: Consider replacing window.confirm() with a custom modal.

The native confirm() dialog provides limited styling and UX control. For consistency with the rest of the app (which uses custom modals like ExecuteModal), consider implementing a reusable confirmation modal component.

examples/payroll/src/components/PayrollForm.tsx (1)

149-149: Use ArkAddress validation for proper Arkade address format checking.

The address validation r.address.length < 10 is insufficient. Arkade addresses use bech32m encoding (starting with "ark1" or "tark1"). Import ArkAddress from the SDK and use its decode() method to validate the address format, or create a utility function. The same weak validation exists in examples/payroll/src/utils/csv.ts and should be updated consistently.

examples/payroll/src/services/lendaswap.ts (2)

120-126: Use substring instead of deprecated substr.

String.prototype.substr() is deprecated. Use substring(2, 11) instead.

-        const swapId = `swap-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+        const swapId = `swap-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;

42-44: Unused config parameter.

The _config parameter is never used. Consider storing it for future use when integrating with the real Lendaswap API, or document why it's intentionally unused.

-    constructor(_config: NetworkConfig = DEFAULT_CONFIG) {
+    constructor(private config: NetworkConfig = DEFAULT_CONFIG) {
         this.loadOrdersFromStorage();
     }
examples/payroll/src/components/FundingPanel.tsx (1)

189-194: expiresAt may not be a Date object if quote is ever restored from storage.

Currently, quote.expiresAt.toLocaleTimeString() assumes expiresAt is a Date instance. While the current flow doesn't persist quotes, this could break if quotes are cached or if the pattern from lendaswapService (which has serialization issues with BigInt) is extended.

Consider defensive handling:

                                 <div className="quote-item">
                                     <span>Expires:</span>
                                     <span>
-                                        {quote.expiresAt.toLocaleTimeString()}
+                                        {new Date(quote.expiresAt).toLocaleTimeString()}
                                     </span>
                                 </div>
examples/payroll/src/services/payroll.ts (2)

20-22: Use substring instead of deprecated substr.

Same issue as in lendaswap.ts - substr is deprecated.

 function generateId(): string {
-    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+    return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
 }

191-206: Wallet is recreated on every operation.

createWallet instantiates new provider objects and creates a new Wallet for each call to getWalletBalance, getWalletAddress, or executePayroll. For a demo this is acceptable, but consider caching the wallet instance keyed by private key if performance becomes a concern.

examples/payroll/src/types/index.ts (1)

50-53: Security consideration: Private key handling.

While acceptable for a demo application, storing/passing private keys as plain strings (ExecutePayrollParams.privateKey) should be avoided in production. Consider adding a comment noting this is for demo purposes only.

 /**
  * Parameters for executing a payroll (admin action)
+ * NOTE: For demo purposes only. In production, use secure key management (HSM, KMS, etc.)
  */
 export interface ExecutePayrollParams {
     payrollId: string;
     privateKey: string;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b790c61 and 7e77f00.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • examples/payroll/.gitignore (1 hunks)
  • examples/payroll/README.md (1 hunks)
  • examples/payroll/index.html (1 hunks)
  • examples/payroll/package.json (1 hunks)
  • examples/payroll/src/App.css (1 hunks)
  • examples/payroll/src/App.tsx (1 hunks)
  • examples/payroll/src/components/FundingPanel.tsx (1 hunks)
  • examples/payroll/src/components/PayrollForm.tsx (1 hunks)
  • examples/payroll/src/components/PendingPayrolls.tsx (1 hunks)
  • examples/payroll/src/components/index.ts (1 hunks)
  • examples/payroll/src/main.tsx (1 hunks)
  • examples/payroll/src/services/index.ts (1 hunks)
  • examples/payroll/src/services/lendaswap.ts (1 hunks)
  • examples/payroll/src/services/payroll.ts (1 hunks)
  • examples/payroll/src/types/index.ts (1 hunks)
  • examples/payroll/src/utils/csv.ts (1 hunks)
  • examples/payroll/src/utils/index.ts (1 hunks)
  • examples/payroll/src/vite-env.d.ts (1 hunks)
  • examples/payroll/tsconfig.json (1 hunks)
  • examples/payroll/vite.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
examples/payroll/src/App.tsx (4)
examples/payroll/src/types/index.ts (2)
  • UserRole (107-107)
  • PayrollBatch (24-37)
examples/payroll/src/components/PayrollForm.tsx (1)
  • PayrollForm (67-282)
examples/payroll/src/components/FundingPanel.tsx (1)
  • FundingPanel (15-310)
examples/payroll/src/components/PendingPayrolls.tsx (1)
  • PendingPayrolls (314-444)
examples/payroll/src/components/FundingPanel.tsx (3)
examples/payroll/src/components/index.ts (1)
  • FundingPanel (3-3)
examples/payroll/src/services/lendaswap.ts (3)
  • SwapQuote (14-21)
  • SwapOrder (26-34)
  • lendaswapService (243-243)
examples/payroll/src/utils/csv.ts (1)
  • formatAmount (91-99)
examples/payroll/src/components/PayrollForm.tsx (3)
examples/payroll/src/types/index.ts (2)
  • PayrollRecipient (4-9)
  • PayrollBatch (24-37)
examples/payroll/src/utils/csv.ts (2)
  • parseCsv (7-68)
  • formatAmount (91-99)
examples/payroll/src/services/payroll.ts (1)
  • payrollService (362-362)
examples/payroll/src/utils/csv.ts (1)
examples/payroll/src/types/index.ts (2)
  • CsvParseResult (72-75)
  • PayrollRecipient (4-9)
examples/payroll/src/services/lendaswap.ts (3)
examples/payroll/src/services/index.ts (4)
  • SwapQuote (7-7)
  • SwapOrder (7-7)
  • LendaswapService (6-6)
  • DEFAULT_CONFIG (4-4)
examples/payroll/src/types/index.ts (1)
  • NetworkConfig (80-86)
examples/payroll/src/services/payroll.ts (1)
  • DEFAULT_CONFIG (27-33)
🪛 markdownlint-cli2 (0.18.1)
examples/payroll/README.md

39-39: Bare URL used

(MD034, no-bare-urls)


63-63: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (22)
examples/payroll/src/vite-env.d.ts (1)

1-1: LGTM! Standard Vite setup.

This is the standard Vite client types reference for TypeScript projects.

examples/payroll/.gitignore (1)

1-24: LGTM! Comprehensive coverage.

The gitignore includes all necessary patterns for a Node.js/React/Vite project.

examples/payroll/vite.config.ts (1)

1-17: LGTM! Well-configured for modern browsers.

The configuration correctly:

  • Sets up React plugin
  • Polyfills global for browser compatibility (common for Node-style SDKs)
  • Uses ES2022 target consistently with tsconfig.json
examples/payroll/package.json (2)

1-26: Well-structured package configuration.

The package.json is properly configured with:

  • Appropriate scripts for development workflow
  • Local SDK reference suitable for monorepo examples
  • Modern React 18 and TypeScript 5.7
  • Proper private flag to prevent accidental publishing

14-14: The @lendasat/lendaswap-sdk package version ^0.1.0 exists on npm and is accessible. No security vulnerabilities are currently reported for this package.

examples/payroll/tsconfig.json (1)

1-22: LGTM! Excellent TypeScript configuration.

The config properly sets:

  • ES2022 target aligned with Vite
  • Strict type checking with unused variable detection
  • Modern React JSX transform
  • Bundler module resolution for Vite compatibility
examples/payroll/src/utils/csv.ts (2)

84-86: LGTM! Correct BTC formatting.

The function correctly formats satoshis to BTC with 8 decimal places.


91-99: LGTM! Good user-friendly formatting.

The function provides appropriate unit scaling based on amount magnitude, making values easier to read.

examples/payroll/index.html (1)

1-17: LGTM! Clean HTML entry point.

The HTML correctly:

  • Uses modern HTML5 structure
  • Includes an appropriate Bitcoin symbol favicon
  • Loads the React entry point as an ES module
examples/payroll/src/main.tsx (1)

1-9: LGTM! Standard React 18 entry point.

The setup correctly:

  • Uses React 18's createRoot API
  • Enables StrictMode for development warnings
  • Mounts the App component

The non-null assertion on getElementById("root") is acceptable since the root div is defined in index.html.

examples/payroll/src/utils/index.ts (1)

1-1: LGTM!

Clean barrel export pattern for utility functions.

examples/payroll/src/components/index.ts (1)

1-3: LGTM!

Standard component barrel export pattern for clean imports.

examples/payroll/README.md (1)

1-176: LGTM!

Comprehensive documentation covering setup, features, architecture, and SDK integration examples. The static analysis hints about bare URL (line 39) and fenced code language (line 63) are acceptable in documentation context—the bare URL is for local development, and the architecture diagram intentionally uses a plain code fence.

examples/payroll/src/App.css (1)

1-1005: LGTM!

Well-structured CSS with a comprehensive dark theme design system. Good use of CSS custom properties for theming, consistent naming conventions, and responsive design patterns. The styling provides good UX with hover states, transitions, and clear visual hierarchy.

examples/payroll/src/services/index.ts (1)

1-7: LGTM!

Clean service barrel export with proper separation of types and values.

examples/payroll/src/App.tsx (1)

19-127: LGTM on overall app structure!

The App component provides a clean role-based UI with good separation between Assistant and Admin views. The state management and event flow (handlePayrollCreated, refreshKey propagation) are well-designed.

examples/payroll/src/components/PayrollForm.tsx (2)

17-61: LGTM on RecipientRow component!

Clean, reusable component for recipient input with proper event handling and placeholder text.


98-123: LGTM on CSV import handler!

Good error handling, proper FileReader usage, and correct file input reset to allow re-import of the same file.

examples/payroll/src/components/FundingPanel.tsx (1)

15-28: LGTM!

The component structure with typed props and well-organized state management using a step-based flow is clean and appropriate for this multi-step wizard pattern.

examples/payroll/src/services/payroll.ts (2)

239-250: LGTM on balance validation.

The balance check before execution prevents failed transactions due to insufficient funds, and the error message clearly communicates the required vs available amounts.


300-310: Failed payroll status persists the error, enabling retry.

Good error handling pattern - storing the error message and setting status to "failed" allows users to see what went wrong and retry via resetPayroll().

examples/payroll/src/types/index.ts (1)

1-107: Well-structured type definitions.

The type definitions are clean, well-documented, and provide a solid foundation for the payroll domain model. The use of discriminated status types (PayrollStatus) and optional fields for state-dependent data (approvedAt, arkTxId, etc.) is a good pattern.

Comment on lines +100 to +106
{showFunding && (
<FundingPanel
arkadeAddress="ark1qexampleaddress..."
requiredAmount={1000000}
onFunded={() => setRefreshKey((k) => k + 1)}
/>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace hardcoded values with dynamic data.

Lines 102-103 use hardcoded placeholder values for arkadeAddress and requiredAmount. The address should be derived from the admin's wallet, and the required amount should be calculated from pending payrolls.

Consider this approach:

+                        // Calculate total pending amount
+                        const pendingTotal = payrolls
+                            .filter(p => p.status === 'pending')
+                            .reduce((sum, p) => sum + p.totalAmount, 0);
+
                         {showFunding && (
                             <FundingPanel
-                                arkadeAddress="ark1qexampleaddress..."
-                                requiredAmount={1000000}
+                                arkadeAddress={/* derive from admin wallet */}
+                                requiredAmount={pendingTotal}
                                 onFunded={() => setRefreshKey((k) => k + 1)}
                             />
                         )}

Note: You'll need to access the payroll data and admin wallet address. Consider lifting this state or using the payroll service to compute these values.

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

🤖 Prompt for AI Agents
In examples/payroll/src/App.tsx around lines 100 to 106, the FundingPanel is
using hardcoded arkadeAddress and requiredAmount — replace these with dynamic
values: derive arkadeAddress from the admin wallet address (pull from the admin
wallet state or a wallet service) and compute requiredAmount by summing pending
payrolls (use the payroll service or lift payroll state up and reduce pending
items to calculate the total); pass these computed props into FundingPanel, add
loading/undefined guards (render nothing or a loader until admin address and
computed amount are available), and ensure they recompute when payrolls or admin
wallet change (useEffect or memoization) so onFunded still triggers refresh
correctly.

Comment on lines +57 to +75
const handleCreateOrder = async () => {
if (!quote) return;

setIsLoading(true);
setError(null);

try {
const orderResult = await lendaswapService.createSwapOrder(
quote.sourceAmount,
arkadeAddress
);
setOrder(orderResult);
setStep("deposit");
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to create order");
} finally {
setIsLoading(false);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Quote expiration is not validated before creating an order.

The quote has an expiresAt field, but handleCreateOrder doesn't check whether the quote has expired before proceeding. Users could unknowingly create orders with expired quotes.

     const handleCreateOrder = async () => {
         if (!quote) return;
+        
+        if (quote.expiresAt < new Date()) {
+            setError("Quote has expired. Please get a new quote.");
+            setQuote(null);
+            return;
+        }

         setIsLoading(true);
         setError(null);
📝 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.

Suggested change
const handleCreateOrder = async () => {
if (!quote) return;
setIsLoading(true);
setError(null);
try {
const orderResult = await lendaswapService.createSwapOrder(
quote.sourceAmount,
arkadeAddress
);
setOrder(orderResult);
setStep("deposit");
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to create order");
} finally {
setIsLoading(false);
}
};
const handleCreateOrder = async () => {
if (!quote) return;
if (quote.expiresAt < new Date()) {
setError("Quote has expired. Please get a new quote.");
setQuote(null);
return;
}
setIsLoading(true);
setError(null);
try {
const orderResult = await lendaswapService.createSwapOrder(
quote.sourceAmount,
arkadeAddress
);
setOrder(orderResult);
setStep("deposit");
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to create order");
} finally {
setIsLoading(false);
}
};
🤖 Prompt for AI Agents
In examples/payroll/src/components/FundingPanel.tsx around lines 57 to 75,
handleCreateOrder does not validate quote.expiresAt before creating an order;
add an early check that parses quote.expiresAt into a Date (or uses Date.now()
vs numeric timestamp), compare it to the current time, and if the quote is
expired set an appropriate error (e.g., "Quote has expired") and return without
calling lendaswapService.createSwapOrder; ensure isLoading is cleared
(setIsLoading(false)) when returning and keep existing try/catch behavior for
non-expiration errors.

Comment on lines +294 to +305
{step === "complete" && (
<div className="step-content success">
<h4>Swap Complete!</h4>
<p>Your BTC has been credited to your Arkade wallet.</p>
<button
className="btn-secondary"
onClick={() => setStep("quote")}
>
Make Another Swap
</button>
</div>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incomplete state reset when starting a new swap.

When the user clicks "Make Another Swap", only step is reset. The previous quote, order, txHash, and error states remain, which could cause confusion or stale data display on the next flow.

                     <button
                         className="btn-secondary"
-                        onClick={() => setStep("quote")}
+                        onClick={() => {
+                            setStep("quote");
+                            setQuote(null);
+                            setOrder(null);
+                            setTxHash("");
+                            setUsdtAmount("");
+                            setError(null);
+                        }}
                     >
                         Make Another Swap
                     </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.

Suggested change
{step === "complete" && (
<div className="step-content success">
<h4>Swap Complete!</h4>
<p>Your BTC has been credited to your Arkade wallet.</p>
<button
className="btn-secondary"
onClick={() => setStep("quote")}
>
Make Another Swap
</button>
</div>
)}
{step === "complete" && (
<div className="step-content success">
<h4>Swap Complete!</h4>
<p>Your BTC has been credited to your Arkade wallet.</p>
<button
className="btn-secondary"
onClick={() => {
setStep("quote");
setQuote(null);
setOrder(null);
setTxHash("");
setUsdtAmount("");
setError(null);
}}
>
Make Another Swap
</button>
</div>
)}
🤖 Prompt for AI Agents
In examples/payroll/src/components/FundingPanel.tsx around lines 294 to 305, the
"Make Another Swap" button only resets step to "quote" leaving previous quote,
order, txHash and error state intact; update the onClick handler to reset all
related states (call the appropriate setters to clear quote and order back to
null/initial, set txHash to empty string or null, clear error, and reset any
loading flags) so the component starts with a clean slate when beginning a new
swap.

Comment on lines +138 to +157
const validRecipients = recipients.filter(
(r) => r.address && r.amount > 0
);
if (validRecipients.length === 0) {
validationErrors.push(
"At least one valid recipient is required"
);
}

for (let i = 0; i < recipients.length; i++) {
const r = recipients[i];
if (r.address && r.address.length < 10) {
validationErrors.push(
`Recipient ${i + 1}: Invalid address`
);
}
if (r.address && (!r.amount || r.amount <= 0)) {
validationErrors.push(`Recipient ${i + 1}: Invalid amount`);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent validation logic may confuse users.

The code filters validRecipients (lines 138-140) to exclude empty rows, but then validates all recipients (lines 147-157), including empty ones. This mixed approach could confuse users—empty rows with no address might trigger validation errors even though they would be filtered out.

Consider validating only the non-empty recipients that will actually be submitted:

-            for (let i = 0; i < recipients.length; i++) {
-                const r = recipients[i];
-                if (r.address && r.address.length < 10) {
+            for (let i = 0; i < validRecipients.length; i++) {
+                const r = validRecipients[i];
+                if (r.address.length < 10) {
                     validationErrors.push(
                         `Recipient ${i + 1}: Invalid address`
                     );
                 }
-                if (r.address && (!r.amount || r.amount <= 0)) {
+                if (r.amount <= 0) {
                     validationErrors.push(`Recipient ${i + 1}: Invalid amount`);
                 }
             }

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

🤖 Prompt for AI Agents
In examples/payroll/src/components/PayrollForm.tsx around lines 138 to 157, the
code builds validRecipients by filtering out empty rows but then runs validation
over all recipients, causing empty rows to produce errors; update the validation
to iterate only over the recipients that will be submitted (the validRecipients)
so empty rows are ignored — either loop over validRecipients (and, if you need
original row numbers for messages, store the original index when filtering or
include an index in the filtered entries) and perform the address/amount checks
there, and keep the initial "At least one valid recipient" check as-is.

Comment on lines +133 to +136
if (!privateKey || privateKey.length < 64) {
setError("Please enter a valid private key (64 hex characters)");
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Strengthen private key validation.

The validation checks for privateKey.length < 64, but a valid hex-encoded private key should be exactly 64 characters. Additionally, the format should be validated to ensure it contains only valid hex characters.

Apply this diff:

-        if (!privateKey || privateKey.length < 64) {
-            setError("Please enter a valid private key (64 hex characters)");
+        if (!privateKey || privateKey.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(privateKey)) {
+            setError("Please enter a valid private key (64 hex characters)");
             return;
         }
📝 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.

Suggested change
if (!privateKey || privateKey.length < 64) {
setError("Please enter a valid private key (64 hex characters)");
return;
}
if (!privateKey || privateKey.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(privateKey)) {
setError("Please enter a valid private key (64 hex characters)");
return;
}
🤖 Prompt for AI Agents
In examples/payroll/src/components/PendingPayrolls.tsx around lines 133 to 136,
the current check allows keys shorter than 64 but doesn’t enforce exact length
or hex format; update validation to first trim the input, then require
privateKey.length === 64 and match /^[0-9a-fA-F]{64}$/, and if it fails call
setError with a message like "Please enter a valid 64-character hex private key"
and return.

Comment on lines +46 to +67
private loadOrdersFromStorage(): void {
try {
const stored = localStorage.getItem("lendaswap_orders");
if (stored) {
const parsed = JSON.parse(stored);
for (const order of parsed) {
this.orders.set(order.swapId, order);
}
}
} catch {
console.warn("Failed to load swap orders from storage");
}
}

private saveOrdersToStorage(): void {
try {
const data = Array.from(this.orders.values());
localStorage.setItem("lendaswap_orders", JSON.stringify(data));
} catch {
console.warn("Failed to save swap orders to storage");
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

BigInt values will be corrupted during localStorage serialization.

SwapOrder.sourceAmount is a bigint, but JSON.stringify() converts BigInt to nothing (throws or becomes null depending on environment). When orders are loaded back, sourceAmount will be lost or invalid.

Apply this diff to handle BigInt serialization:

     private loadOrdersFromStorage(): void {
         try {
             const stored = localStorage.getItem("lendaswap_orders");
             if (stored) {
-                const parsed = JSON.parse(stored);
+                const parsed = JSON.parse(stored, (key, value) => {
+                    if (key === "sourceAmount" && typeof value === "string") {
+                        return BigInt(value);
+                    }
+                    return value;
+                });
                 for (const order of parsed) {
                     this.orders.set(order.swapId, order);
                 }
             }
         } catch {
             console.warn("Failed to load swap orders from storage");
         }
     }

     private saveOrdersToStorage(): void {
         try {
             const data = Array.from(this.orders.values());
-            localStorage.setItem("lendaswap_orders", JSON.stringify(data));
+            localStorage.setItem("lendaswap_orders", JSON.stringify(data, (key, value) =>
+                typeof value === "bigint" ? value.toString() : value
+            ));
         } catch {
             console.warn("Failed to save swap orders to storage");
         }
     }
📝 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.

Suggested change
private loadOrdersFromStorage(): void {
try {
const stored = localStorage.getItem("lendaswap_orders");
if (stored) {
const parsed = JSON.parse(stored);
for (const order of parsed) {
this.orders.set(order.swapId, order);
}
}
} catch {
console.warn("Failed to load swap orders from storage");
}
}
private saveOrdersToStorage(): void {
try {
const data = Array.from(this.orders.values());
localStorage.setItem("lendaswap_orders", JSON.stringify(data));
} catch {
console.warn("Failed to save swap orders to storage");
}
}
private loadOrdersFromStorage(): void {
try {
const stored = localStorage.getItem("lendaswap_orders");
if (stored) {
const parsed = JSON.parse(stored, (key, value) => {
if (key === "sourceAmount" && typeof value === "string") {
return BigInt(value);
}
return value;
});
for (const order of parsed) {
this.orders.set(order.swapId, order);
}
}
} catch {
console.warn("Failed to load swap orders from storage");
}
}
private saveOrdersToStorage(): void {
try {
const data = Array.from(this.orders.values());
localStorage.setItem("lendaswap_orders", JSON.stringify(data, (key, value) =>
typeof value === "bigint" ? value.toString() : value
));
} catch {
console.warn("Failed to save swap orders to storage");
}
}

Comment on lines +183 to +199
/**
* Confirm that the Ethereum deposit was made
* This should be called after the user submits the Ethereum transaction
*/
async confirmDeposit(swapId: string, _txHash: string): Promise<void> {
const order = this.orders.get(swapId);
if (!order) {
throw new Error(`Swap order ${swapId} not found`);
}

// In real implementation, verify the transaction on-chain
// await client.confirmDeposit(swapId, txHash);

order.status = "deposited";
this.orders.set(swapId, order);
this.saveOrdersToStorage();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing state validation in confirmDeposit.

The method doesn't validate that the order is in "pending" status before transitioning to "deposited". This could allow invalid state transitions.

     async confirmDeposit(swapId: string, _txHash: string): Promise<void> {
         const order = this.orders.get(swapId);
         if (!order) {
             throw new Error(`Swap order ${swapId} not found`);
         }
+        if (order.status !== "pending") {
+            throw new Error(`Cannot confirm deposit for swap with status ${order.status}`);
+        }

         // In real implementation, verify the transaction on-chain
         // await client.confirmDeposit(swapId, txHash);

         order.status = "deposited";
🤖 Prompt for AI Agents
In examples/payroll/src/services/lendaswap.ts around lines 183 to 199,
confirmDeposit currently unconditionally sets order.status = "deposited"; add a
guard that checks order.status === "pending" and throw a descriptive error
(e.g., "Cannot confirm deposit for swap X in status Y") if not pending, only
then set status to "deposited", persist with this.orders.set(swapId, order) and
call this.saveOrdersToStorage(); this prevents invalid state transitions.

Comment on lines +270 to +286
// Get available VTXOs
const vtxos = await wallet.getVtxos();
const boardingUtxos = await wallet.getBoardingUtxos();

// Select inputs that cover the total amount
const inputs = [...boardingUtxos, ...vtxos];

// Create outputs array for settlement
const outputs = recipients.map((r) => ({
address: r.address,
amount: BigInt(r.amount),
}));

arkTxId = await wallet.settle({
inputs,
outputs,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

@arkade-os/sdk wallet settle method coin selection

💡 Result:

Short answer: wallet.settle lets you control coin selection by passing an inputs array (VirtualCoin or BoardingUtxo). If you omit inputs the SDK will automatically choose eligible VTXOs/UTXOs (from getVtxos()/getBoardingUtxos()) to satisfy the requested outputs; to force particular coins, pass them in via the inputs parameter. Also note outputs use BigInt amounts and you can set fee-related options where available. Sources: SDK docs and Arkade settlement docs. [1][2][3]

Sources

  • @arkade-os/sdk docs (settle signature, inputs/outputs examples). [1]
  • Arkade "Settlement Process" / batch swaps overview. [2]
  • Arkade onchain settlement / intent workflow (how inputs are expressed and used). [3]

Remove misleading comment or implement automatic coin selection.

The comment "Select inputs that cover the total amount" is inaccurate—the code passes all available VTXOs and boarding UTXOs without filtering. The SDK's settle() method can automatically select eligible inputs if the inputs parameter is omitted; either update the comment to reflect that all inputs are passed, or refactor to leverage the SDK's automatic coin selection by removing the explicit inputs parameter.

🤖 Prompt for AI Agents
In examples/payroll/src/services/payroll.ts around lines 270 to 286, the comment
"Select inputs that cover the total amount" is misleading because the code
currently passes all boardingUtxos and vtxos to settle; either update the
comment to reflect that all available UTXOs are being passed, or remove the
explicit inputs parameter so the SDK’s settle() does automatic coin selection
(i.e., call wallet.settle({ outputs }) instead); if you remove inputs, ensure no
other callsites expect inputs to be provided.

Comment on lines +54 to +58
// Convert to satoshis if needed (assume BTC if has decimal point with significant digits)
const amountSats =
amountStr.includes(".") && parseFloat(amountStr) < 1
? Math.round(amount * 100_000_000)
: Math.round(amount);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Ambiguous and inconsistent satoshi conversion logic.

The conversion logic has a critical flaw:

  • 0.5 → treated as BTC → 50,000,000 sats ✓
  • 1.5 → treated as sats → 2 sats ✗ (should be 150,000,000 sats)
  • 500.25 → treated as sats → 500 sats ✗ (should be 50,025,000,000 sats)

This creates a dangerous ambiguity where users could accidentally send 1000x less than intended.

Apply this diff to use a clear, explicit format requirement:

-// Convert to satoshis if needed (assume BTC if has decimal point with significant digits)
-const amountSats =
-    amountStr.includes(".") && parseFloat(amountStr) < 1
-        ? Math.round(amount * 100_000_000)
-        : Math.round(amount);
+// Convert to satoshis - always assume input is in BTC
+const amountSats = Math.round(amount * 100_000_000);
+
+// Validate reasonable range (0.00000001 BTC to 21,000,000 BTC)
+if (amountSats < 1 || amountSats > 21_000_000 * 100_000_000) {
+    errors.push(`Line ${lineNumber}: Amount out of reasonable range "${amountStr}"`);
+    continue;
+}

Alternative approach: Add a clear unit suffix requirement (e.g., "0.5 BTC" vs "50000000 sats") and parse accordingly.

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

🤖 Prompt for AI Agents
In examples/payroll/src/utils/csv.ts around lines 54 to 58, the current
heuristic that treats values <1 with a decimal as BTC and everything else as
sats is ambiguous and incorrect; enforce explicit units instead: require and
parse a unit suffix (e.g., "BTC" or "sats"/"sat") or a configurable default
clearly documented, and convert accordingly (BTC -> multiply by 100_000_000 then
round, sats -> parse as integer and validate). Update validation to reject or
normalize malformed values, trim and case-insensitive unit parsing, and add
clear error messages when unit is missing or ambiguous.

Comment on lines +73 to +79
export function generateCsv(recipients: PayrollRecipient[]): string {
const header = "Address,Amount,Name";
const rows = recipients.map(
(r) => `${r.address},${r.amount},${r.name || ""}`
);
return [header, ...rows].join("\n");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing CSV value escaping.

The function doesn't escape values that contain commas, quotes, or newlines, which could break the CSV format or create security issues.

Apply this diff to properly escape CSV values:

+/**
+ * Escape CSV value by wrapping in quotes if needed
+ */
+function escapeCsvValue(value: string): string {
+    if (value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r')) {
+        return `"${value.replace(/"/g, '""')}"`;
+    }
+    return value;
+}
+
 /**
  * Generate CSV content from recipients
  */
 export function generateCsv(recipients: PayrollRecipient[]): string {
     const header = "Address,Amount,Name";
     const rows = recipients.map(
-        (r) => `${r.address},${r.amount},${r.name || ""}`
+        (r) => `${escapeCsvValue(r.address)},${r.amount},${escapeCsvValue(r.name || "")}`
     );
     return [header, ...rows].join("\n");
 }
🤖 Prompt for AI Agents
In examples/payroll/src/utils/csv.ts around lines 73 to 79, the CSV generator
does not escape values containing commas, quotes, or newlines; implement a small
escapeCsv helper that for a given string converts undefined/null to empty
string, replaces any double quote with two double quotes, and wraps the field in
double quotes if it contains a comma, double quote, or newline; use this helper
for each field (address, amount converted to string, name) when building rows
and then join header and rows with "\n".

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