Conversation
…and the ramp/deposit feature flow
There was a problem hiding this comment.
Pull request overview
This PR implements the private send functionality for a cryptocurrency wallet, enabling users to send assets privately using MLXDR receiving addresses. The implementation includes deposit, receive, and send flows with dedicated confirmation screens, UTXO selection with privacy level controls, and integration with a privacy provider.
Changes:
- Implemented backend handlers for deposit, receive, and private send operations with UTXO selection, change distribution, and bundle submission to privacy provider
- Created frontend pages and templates for deposit/receive/send flows with confirmation screens showing operation previews
- Added routing, state management, and API services to support the new flows with home screen integration
Reviewed changes
Copilot reviewed 28 out of 29 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
src/background/handlers/private/send.ts |
New send handler with UTXO selection, change partitioning, and bundle preparation/submission |
src/background/handlers/private/send.types.ts |
Type definitions for send and prepare-send requests/responses |
src/background/handlers/private/receive.ts |
Receive handler generating MLXDR addresses from reserved UTXOs |
src/background/handlers/private/receive.types.ts |
Type definitions for receive operations |
src/background/handlers/private/deposit.ts |
Deposit handler with entropy-based UTXO creation |
src/background/handlers/private/deposit.types.ts |
Type definitions for deposit operations |
src/background/services/privacy-provider-client.ts |
Enhanced with token validation, auth error handling, and bundle submission |
src/background/utils/random-partition.ts |
New utility for cryptographically secure random amount partitioning |
src/background/messages.ts |
Added message types for deposit, receive, send, and prepare-send |
src/background/handler.ts |
Registered new handlers in the message routing system |
src/popup/pages/send-page.tsx |
Send form with MLXDR input, amount, and privacy level selection |
src/popup/pages/send-confirmation-page.tsx |
Send confirmation with operation preview (CREATE/SPEND) |
src/popup/pages/receive-page.tsx |
Receive form for generating MLXDR addresses |
src/popup/pages/receive-confirmation-page.tsx |
Display generated MLXDR with UTXO breakdown |
src/popup/pages/deposit-page.tsx |
Deposit form with method and entropy selection |
src/popup/pages/deposit-review-page.tsx |
Deposit confirmation with operation summary |
src/popup/templates/*.tsx |
Reusable templates for forms and confirmation screens |
src/popup/api/*.ts |
API service wrappers for new backend handlers |
src/popup/hooks/state.tsx |
State management for send/receive/deposit flows and routing |
src/popup/app.tsx |
Route definitions for new pages |
src/popup/templates/home-template.tsx |
Home UI with Send/Receive/Ramp action buttons |
src/popup/pages/home-page.tsx |
Wired action handlers to state navigation |
deno.json |
Removed empty lines in scripts section |
deno.lock |
Dependency version updates |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const prepared = await prepareSendOperations({ | ||
| network, | ||
| channelId, | ||
| providerId, | ||
| accountId, | ||
| receiverOperationsMLXDR: receiverOperationsMLXDR!, | ||
| amount: amount!, | ||
| entropyLevel: entropyLevel!, |
There was a problem hiding this comment.
Using non-null assertion operators (!) here is unsafe. If receiverOperationsMLXDR, amount, or entropyLevel are undefined at this point, the function should return an error response rather than potentially causing runtime errors. Add proper validation before calling prepareSendOperations or handle the case where these values might be missing.
| const prepared = await prepareSendOperations({ | |
| network, | |
| channelId, | |
| providerId, | |
| accountId, | |
| receiverOperationsMLXDR: receiverOperationsMLXDR!, | |
| amount: amount!, | |
| entropyLevel: entropyLevel!, | |
| const missingFields: string[] = []; | |
| if (receiverOperationsMLXDR == null) { | |
| missingFields.push("receiverOperationsMLXDR"); | |
| } | |
| if (amount == null) { | |
| missingFields.push("amount"); | |
| } | |
| if (entropyLevel == null) { | |
| missingFields.push("entropyLevel"); | |
| } | |
| if (missingFields.length > 0) { | |
| return { | |
| type: MessageType.Send, | |
| ok: false, | |
| error: { | |
| code: "INVALID_REQUEST", | |
| message: `Missing required fields: ${missingFields.join(", ")}`, | |
| }, | |
| }; | |
| } | |
| const prepared = await prepareSendOperations({ | |
| network, | |
| channelId, | |
| providerId, | |
| accountId, | |
| receiverOperationsMLXDR, | |
| amount, | |
| entropyLevel, |
| useMemo(() => { | ||
| if (!formData) return; | ||
| getPrivateChannels({ network }) | ||
| .then((res) => { | ||
| if (res.ok) { | ||
| setPrivateChannels({ | ||
| channels: res.channels, | ||
| selectedChannelId: res.selectedChannelId, | ||
| }); | ||
| } | ||
| }) | ||
| .catch((err) => { | ||
| console.error("Failed to load private channels", err); | ||
| }); | ||
| }, [network, formData]); |
There was a problem hiding this comment.
useMemo is being used incorrectly here for side effects. useMemo should only be used for memoizing computed values, not for triggering side effects like API calls. This should be useEffect instead. The current code may cause the API call to be executed multiple times unpredictably.
| disabled={!canStartDeposit} | ||
| onClick={action.onClick} |
There was a problem hiding this comment.
All three action buttons (Receive, Send, Ramp) use the same canStartDeposit condition to determine if they should be disabled. However, this variable is specifically named for deposits. The Send button should potentially have its own condition check or the variable should be renamed to something more generic like canPerformPrivateAction to reflect that it gates all three operations.
| function getUtxoCountFromEntropyLevel( | ||
| level: "LOW" | "MEDIUM" | "HIGH" | "V_HIGH", | ||
| ): number { | ||
| switch (level) { | ||
| case "LOW": | ||
| return 1; | ||
| case "MEDIUM": | ||
| return 5; | ||
| case "HIGH": | ||
| return 10; | ||
| case "V_HIGH": | ||
| return 20; | ||
| default: | ||
| return 5; | ||
| } | ||
| } |
There was a problem hiding this comment.
The getUtxoCountFromEntropyLevel function is duplicated in both deposit-review-page.tsx and deposit.ts. This creates a maintenance issue where changes to entropy mapping must be made in multiple places. Extract this to a shared utility module.
| console.log("operationsMLXDR", operationsMLXDR); | ||
|
|
There was a problem hiding this comment.
Debug console.log statement left in production code. This should either be removed or replaced with a proper logging mechanism if the MLXDR operations need to be logged.
| console.log("operationsMLXDR", operationsMLXDR); |
| useMemo(() => { | ||
| if (!formData) return; | ||
| getPrivateChannels({ network }) | ||
| .then((res) => { | ||
| if (res.ok) { | ||
| setPrivateChannels({ | ||
| channels: res.channels, | ||
| selectedChannelId: res.selectedChannelId, | ||
| }); | ||
| } | ||
| }) | ||
| .catch((err) => { | ||
| console.error("Failed to load private channels", err); | ||
| }); | ||
| }, [network, formData]); |
There was a problem hiding this comment.
useMemo is being used incorrectly here for side effects. useMemo should only be used for memoizing computed values, not for triggering side effects like API calls. This should be useEffect instead. The current code may cause the API call to be executed multiple times unpredictably.
| sumWeights += weight; | ||
| } | ||
|
|
||
| // Distribuir o remaining proporcionalmente aos pesos |
There was a problem hiding this comment.
Comment is in Portuguese ("Distribuir o remaining proporcionalmente aos pesos" = "Distribute the remaining proportionally to the weights"). Should be in English for consistency with the rest of the codebase.
| // Distribuir o remaining proporcionalmente aos pesos | |
| // Distribute the remaining amount proportionally to the weights |
| function getFeeForEntropyLevel(level: EntropyLevel): number { | ||
| switch (level) { | ||
| case "LOW": | ||
| return 0.1; | ||
| case "MEDIUM": | ||
| return 0.25; | ||
| case "HIGH": | ||
| return 0.5; | ||
| case "V_HIGH": | ||
| return 1.0; | ||
| default: | ||
| return 0.25; | ||
| } | ||
| } |
There was a problem hiding this comment.
The getFeeForEntropyLevel function is duplicated across multiple files with slightly different return values. In send-page.tsx it returns LOW: 0.1, MEDIUM: 0.25, HIGH: 0.5, V_HIGH: 1.0, while in send-confirmation-page.tsx it has the same values, but in deposit.ts it returns LOW: 0.05, MEDIUM: 0.25, HIGH: 0.5, V_HIGH: 0.75. This function should be extracted to a shared utility file to ensure consistency and avoid maintenance issues.
| <CardFooter className="px-0 space-y-2"> | ||
| <Button | ||
| className="w-full" | ||
| disabled={busy} | ||
| onClick={handleExecute} | ||
| loading={busy} | ||
| > | ||
| Execute Transaction | ||
| </Button> | ||
| <Button | ||
| variant="outline" | ||
| className="w-full" | ||
| disabled={busy} | ||
| onClick={() => actions.goSend()} | ||
| > | ||
| Go Back | ||
| </Button> | ||
| </CardFooter> |
There was a problem hiding this comment.
The CardFooter component is being used outside of a Card component. According to the typical card component pattern, CardFooter should be a child of Card. Either wrap this in a Card component or use a plain div if the card styling is not needed.
| <CardFooter className="px-0"> | ||
| <Button | ||
| className="w-full" | ||
| disabled={!canSubmit || busy} | ||
| onClick={handleSubmit} | ||
| loading={busy} | ||
| > | ||
| Review Transfer | ||
| </Button> | ||
| </CardFooter> |
There was a problem hiding this comment.
The CardFooter component is being used outside of a Card component. According to the typical card component pattern, CardFooter should be a child of Card. Either wrap this in a Card component or use a plain div if the card styling is not needed.
fazzatti
left a comment
There was a problem hiding this comment.
Just a few points and the bot also caught some interesting things that might be worth checking.
| import { partitionAmountRandom } from "@/background/utils/random-partition.ts"; | ||
| import { Buffer } from "node:buffer"; | ||
|
|
||
| async function rpcGetLatestLedger(params: { rpcUrl: string }) { |
There was a problem hiding this comment.
Instead of rebuilding this function from scratch, try using the RPC client from the stellar sdk. We have the client at the NETWORK_RPC_SERVER so you can just use it like this:
const currentLedger = (await NETWORK_RPC_SERVER.getLatestLedger()).sequence;
There was a problem hiding this comment.
oh wait, I got the variable from the provider platform, in case we don't have it ready here, you can initialize one importing Server from @stellar/stellar-sdk/rpc. Just initialize it with the same rpcUrl and you should be good
| import { Buffer } from "node:buffer"; | ||
| import { bytesToBase64 } from "@/common/utils/bytes-to-base64.ts"; | ||
|
|
||
| async function rpcGetLatestLedger(params: { rpcUrl: string }) { |
There was a problem hiding this comment.
duplicated function, we can use a single one or the Server client as in the other comment
| // Distribute the leftover randomly among parts | ||
| const remainderCount = Number(remainderExtra); | ||
| const indices = new Set<number>(); | ||
|
|
||
| // Generate unique random indices to receive +1 | ||
| while (indices.size < remainderCount && indices.size < partsCount) { | ||
| const idx = Number( | ||
| randomIntInRangeBigInt(0n, BigInt(partsCount - 1)), | ||
| ); | ||
| indices.add(idx); | ||
| } | ||
|
|
||
| // Add +1 to the selected indices | ||
| for (const idx of indices) { | ||
| amounts[idx] += 1n; |
There was a problem hiding this comment.
This sounds like an issue indeed, if more leftover is unassigned, this means more money being left as 'tip'or 'fee' on the table.
This PR implements the private send flow, allowing users to send assets to MLXDR receiving addresses generated by the private receive flow. The implementation follows the same patterns as the deposit/receive flows and aligns with the sandbox reference implementation, including a dedicated confirmation screen with a preview of CREATE and SPEND operations before submission.
Overview
Users can now send funds from a private channel to a MLXDR receiving address by specifying:
The system will:
UtxoBasedStellarAccountFeatures
Send Form (
SendPage)Send Confirmation (
SendConfirmationPage)Home Integration
Technical Changes
Backend
Handler:
src/background/handlers/private/send.tsUtxoBasedStellarAccountselectUTXOsForTransferwith a random strategy and retry logicpartitionAmountRandomto randomize change distributionPrivacyProviderClientprepareSendOperationsfunction for the prepare/preview flow“Prepare” handler:
handlePrepareSendprepareSendOperationsto:operationsMLXDRarray ready for submissionTypes:
src/background/handlers/private/send.types.tsSendRequest: network, channelId, providerId, accountId, receiverOperationsMLXDR, amount, entropyLevel, optionalpreparedOperationsMLXDRSendResponse: ok, id, hash, errorPrepareSendRequest: same shape asSendRequest(without prepared operations)PrepareSendResponse: ok,createOperations,spendOperations,operationsMLXDR, totals/counters, errorMessage system:
src/background/messages.tsMessageType.Send = "SEND"MessageType.PrepareSend = "PREPARE_SEND"SendRequest/SendResponseandPrepareSendRequest/PrepareSendResponseBackground router:
src/background/handler.tshandleSendforMessageType.SendhandlePrepareSendforMessageType.PrepareSendFrontend
API services:
src/popup/api/send.tssend(params: SendRequest): Promise<SendResponse>MessageType.SendprepareSend(params: PrepareSendRequest): Promise<PrepareSendResponse>MessageType.PrepareSendState management:
src/popup/hooks/state.tsxsendsend-confirmationsendFormData?: { channelId, providerId, receiverOperationsMLXDR, amount, entropyLevel }sendResult?: { createOperations, spendOperations, operationsMLXDR, totalSpendAmount, changeAmount, receiverAmount, numSpends, numCreates }goSend(channelId?, providerId?)goSendConfirmation()setSendFormData(data)setSendResult(data)clearSendData()Pages:
src/popup/pages/send-page.tsxprepareSend()on “Review Transfer”:sendFormDatain statePrepareSendsendResult(operations and summary) in statesend-confirmationsrc/popup/pages/send-confirmation-page.tsxsendFormDataandsendResultfrom state<numSpends> SPEND operations, <numCreates> CREATE operationssend()withpreparedOperationsMLXDRfromsendResultRouter:
src/popup/app.tsxsendandsend-confirmation(if not already present)Home integration:
src/popup/templates/home-template.tsxonStartSendcallback prop for the private Send buttonsrc/popup/pages/home-page.tsxonStartSend={(channelId, providerId) => actions.goSend(channelId, providerId)}Implementation Details
Entropy mapping:
Slots are allocated between:
UTXO selection:
UtxoBasedStellarAccount.selectUTXOsForTransfer(totalToSpend, "random")Change distribution:
partitionAmountRandomto randomly split change across reserved change UTXOsExpiration handling:
latestLedger.sequence + 1000for SPEND signingError handling:
LOCKED,NOT_FOUND,INVALID_MLXDR,INSUFFICIENT_BALANCE,INVALID_SESSION,AUTH_FAILED,SEND_FAILED)PrivacyProviderAuthErrorFiles Changed
New / Significantly Updated Backend Files:
src/background/handlers/private/send.types.tssrc/background/handlers/private/send.tsBackend Integration:
src/background/messages.ts— AddedSendandPrepareSendmessage types and mappingssrc/background/handler.ts— RegisteredhandleSendandhandlePrepareSendNew / Updated Frontend Files:
src/popup/api/send.tssrc/popup/pages/send-page.tsxsrc/popup/pages/send-confirmation-page.tsxState & Routing:
src/popup/hooks/state.tsx— Added send routes and state (sendFormData,sendResult)src/popup/app.tsx— Addedsendandsend-confirmationroute handlingHome Integration:
src/popup/templates/home-template.tsx— Added send action propsrc/popup/pages/home-page.tsx— Wires send button togoSendTesting Notes
Form validation
Prepare flow
prepareSendwith valid MLXDR and various entropy levelscreateOperationsandspendOperationsare populatedoperationsMLXDRlength matchesnumCreates + numSpendstotalSpendAmount,changeAmount,receiverAmount) make senseConfirmation UI
Execution
preparedOperationsMLXDRis submitted (no re-preparation)Navigation
Future Enhancements