Repository navigation
Commit c06b4e9
feat(qt): DashPay profiles and contacts
Contacts run over DIP-15 as the mobile wallets implement it. Sending a request derives our receiving keychain for the contact, serializes it in the 69-byte compact form (parent fingerprint, chain code, public key), has the wallet compute the ECDH secret between our ENCRYPTION key and the recipient key the SDK's mint-side policy selects, and the accountReference MAC over the compact xpub, and hands only those 32-byte outputs to build_contact_request, which encrypts and assembles the document. The rotation version of a resend comes from the chain: our latest request to that contact is unmasked with our MAC and its version bumped, so the unique (ownerId, toUserId, accountReference) index cannot reject it and nothing is lost on seed recovery. The request is confirmed by a proved re-query of our sent requests, repeated with a backoff for three minutes. A request Platform accepted for broadcast is never reported as not sent: the UI says it was sent and is being confirmed, the contacts list shows it as a sent request, and a confirmation that takes longer hands over to the contacts refresh; only a typed refusal is a failure. A new contact request waits while one is being confirmed or while the profile signed at registration still holds the identity's first DashPay nonce.
Accepting a request checks the sender and recipient key purposes against the SDK's receive policy and never runs ECDH with the MASTER key (a request from a wallet too old to have encryption keys is refused with a visible reason), decrypts the compact xpub with our key at recipientKeyIndex through dip15_decrypt_xpub, validates it, imports our receiving keychain with a rescan birth time that is ours (the time of our own confirmed request, or now on a first accept, never the counterparty's document time), labels the chain for transaction history, stores the contact's xpub and sends the reciprocal request, all under a single wallet unlock. A contact is established only once both directions are on chain and its key is imported. A request that answers ours establishes the contact with no broadcast, as the mobile wallets do: a refresh or an unlock decrypts it and imports the keychains without asking for the passphrase. Until then the row is Accepted: it asks for an unlock only while the wallet is locked, otherwise it shows why finishing failed, and a request that cannot be read is not retried on every refresh.
Profiles are a display name and a public message, no avatar: the profile is read (proved) before a replace so every field another wallet set is carried through, and the update is confirmed only by a proved re-read at the next revision. Contact metadata (username, profile name) is cached from proved reads only, and a change is shown by rebuilding the rows from the records it was written to, without reading the contact requests again; a profile name is always shown as an untrusted profile name, never as a verified identity. The dashboard gains the contacts list with accept, Add contact… (a username lookup that sends a contact request), and the profile dialog.
Tests (test_dash-qt over FakePlatformClient and a real descriptor wallet): decryption with the ECDH secret the wallet derives, the MASTER-key and wrong-purpose refusals, the full accept with a birth time no earlier than our own request and the labelled receiving chain, accepting after our own request sending nothing (contactAcceptAfterOurRequestSendsNothing), one passphrase prompt per accept (contactAcceptAsksToUnlockOnce), an answered request established on unlock with no broadcast and the Accepted row state (answeredRequestEstablishesContact), an answered request that cannot be read showing why without the unlock wording and not being retried (answeredRequestThatCannotFinishSaysWhy), the resend bumping the on-chain version with the ENCRYPTION sender key and the SDK-selected recipient key, the profile replace carrying the existing document and confirmed by proof, an accepted request reported as sent and being confirmed rather than failed (contactRequestConfirmationIsNeverAFailure), search results carrying the proved profile name read once a session, and contact metadata shown without re-reading the requests (contactMetadataShownWithoutRereadingRequests).
The profile dialog cannot be edited or saved before the current profile has loaded, since a save would publish empty fields over it; saves, searches and contact requests show a busy bar. The contacts section puts the selected row's actions next to Add contact… in its header, sizes its table to its rows (three to twelve, then it scrolls) with the columns as wide as their content and Status next to the data, hides an empty Profile name column, has a loading state and a compact empty state that says contacts are paid by username from the Send tab, clears success messages after a few seconds, and offers Ignore / Hide contact, kept on this wallet under contact/hidden/ because a contact request can be neither rejected nor withdrawn on Platform. Rows and search results act on a double click or Enter, never on the single click some platforms activate rows with, since both write to Platform. The Add a contact dialog lays out Close and Send contact request itself so the primary stays last on every platform, keeps its column widths from one search to the next, and shows each result's proved profile name, read once a session (at most one page of 25). The profile dialog keeps each label on the line of its field, gives both character counters the width of the longest count so the name field and the message box end at the same edge, and grows to show a whole error. The light and dark themes give the contacts table a text colour, and the contacts and search tables one visible selection. The contacts list is not read while the node has pushed no evonode endpoints (network activity off, syncing, or not pushed again yet after a pause): a refresh asked for then, or while one is running, runs once they arrive or it ends, and a read that failed because the endpoints went away is not reported. The dashboard header puts Edit profile… to the right of the name block, top-aligned, and a success message there clears itself; a failed profile read is retried once 30 s later while the tab is shown (one pending retry, like the credits). Tests: the profile dialog waits for the loaded profile and lines its fields up, an ignored request leaves the list until shown again or asked, and turning network activity off and on shows no contacts error while the endpoints are not back and clears one when they are (contactsWaitForEndpointsOnResume).
There is no Refresh: the list reads itself again when the dashboard is shown, on a new ChainLock while it is shown (at most once a minute), on a five-minute fallback, and after the user's own changes (a request sent or accepted, a profile saved); a failed read is tried again after 30 s, 60 s, 2, 4 and then every 10 minutes, and its error line offers Try again. Last updated at … shows under the list only while it may be out of date. Nothing is read while the list is hidden, paused or without endpoints. The dashboard has one filled button: the selected row's Accept (or Unlock to finish, Try again) while it needs an answer, otherwise Add contact… (the empty state's own while there are no contacts), and none while the registration card holds the page's action. With requests waiting, the first is selected when the list is shown, without taking the focus, so its Accept is visible at once. Add a contact asks for a Username (buddy label, placeholder Their DashPay username), says the answering evonode sees what is looked up, and looks nothing up before three characters, the shortest username. Edit profile… waits for the profile to be read, and a failed read offers Add profile…, disabled until one succeeds. A connected contact's tooltip says to pay them from Send by typing their username or pressing @. Tests: the dashboard offers no Send, Disable, Refresh or Find people and offers Add contact… (dashboardHasNoSendDisableOrRefresh); one filled button in each registration and contacts state (onlyOneFilledButton); ChainLock reads throttled to one a minute, the failure backoff and its reset, Last updated only while stale, nothing read while hidden (contactsRefreshFollowsChainLocks); showing the tab twice within 30 s reads once (showRefreshThrottled); the first waiting request selected with a filled Accept (firstIncomingRequestSelected); nothing looked up under three characters (addContactNeedsThreeCharacters).
A contact that sends again (a DIP-15 re-send, say with new payment addresses) is one row, and only its newest request counts: requests are ranked by createdAt and then accountReference, as the mobile wallets rank them, so both ends pay the same chain. An established contact whose newest request is not the one it was established from is re-established from it without a broadcast, and a changed xpub restarts its payment cursor at the first address. A new request (or a newer one from a known sender) reads the contact's username and profile again, and otherwise every contacts refresh re-reads them once five minutes have passed, so a profile edit shows within minutes. Add contact reads the chosen result's identity once a session (a send reads it too) and marks one with no key a contact request can be encrypted to as Can't receive contact requests; a request we sent, on chain, recorded by this wallet or being confirmed, reads as Request sent; the Note column shows only when a row has a note. Tests: a re-send replaces the earlier request, is re-established from its xpub with the cursor restarted, and listing the same requests again changes nothing (contactResendUsesNewestRequest); an identity that cannot receive is marked, cannot be sent to and is read once (addContactMarksIdentitiesThatCannotReceive).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>1 parent c10b24b commit c06b4e9
24 files changed
Lines changed: 4815 additions & 27 deletions
File tree
- src
- qt
- platform
- res/css
- test
- test/util
- test/lint
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
127 | 130 | | |
128 | 131 | | |
129 | 132 | | |
130 | 133 | | |
131 | 134 | | |
132 | 135 | | |
133 | | - | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
134 | 139 | | |
135 | 140 | | |
136 | 141 | | |
| |||
237 | 242 | | |
238 | 243 | | |
239 | 244 | | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
240 | 248 | | |
241 | 249 | | |
242 | 250 | | |
243 | 251 | | |
244 | 252 | | |
245 | 253 | | |
246 | | - | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
247 | 257 | | |
248 | 258 | | |
249 | 259 | | |
| |||
380 | 390 | | |
381 | 391 | | |
382 | 392 | | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
383 | 396 | | |
384 | 397 | | |
385 | 398 | | |
386 | 399 | | |
387 | 400 | | |
388 | 401 | | |
389 | | - | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
390 | 405 | | |
391 | 406 | | |
392 | 407 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
0 commit comments