Conversation
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
WalkthroughThis 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
Sequence DiagramsPayroll Creation FlowsequenceDiagram
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
Payroll Execution FlowsequenceDiagram
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Areas requiring extra attention:
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
Signed-off-by: Marco Argentieri <3596602+tiero@users.noreply.github.com>
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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 replacingwindow.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 < 10is insufficient. Arkade addresses use bech32m encoding (starting with "ark1" or "tark1"). ImportArkAddressfrom the SDK and use itsdecode()method to validate the address format, or create a utility function. The same weak validation exists inexamples/payroll/src/utils/csv.tsand should be updated consistently.examples/payroll/src/services/lendaswap.ts (2)
120-126: Usesubstringinstead of deprecatedsubstr.
String.prototype.substr()is deprecated. Usesubstring(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
_configparameter 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:expiresAtmay not be a Date object if quote is ever restored from storage.Currently,
quote.expiresAt.toLocaleTimeString()assumesexpiresAtis aDateinstance. While the current flow doesn't persist quotes, this could break if quotes are cached or if the pattern fromlendaswapService(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: Usesubstringinstead of deprecatedsubstr.Same issue as in
lendaswap.ts-substris 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.
createWalletinstantiates new provider objects and creates a newWalletfor each call togetWalletBalance,getWalletAddress, orexecutePayroll. 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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
globalfor 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.
| {showFunding && ( | ||
| <FundingPanel | ||
| arkadeAddress="ark1qexampleaddress..." | ||
| requiredAmount={1000000} | ||
| onFunded={() => setRefreshKey((k) => k + 1)} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
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.
| 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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| {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> | ||
| )} |
There was a problem hiding this comment.
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.
| {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.
| 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`); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| if (!privateKey || privateKey.length < 64) { | ||
| setError("Please enter a valid private key (64 hex characters)"); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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"); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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"); | |
| } | |
| } |
| /** | ||
| * 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(); | ||
| } |
There was a problem hiding this comment.
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.
| // 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, | ||
| }); |
There was a problem hiding this comment.
🧩 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.
| // 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); |
There was a problem hiding this comment.
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.
| 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"); | ||
| } |
There was a problem hiding this comment.
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".
A minimal React + TypeScript web frontend demonstrating:
Features:
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.