aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-27 15:07:31 -0700
committerAdam Soltys <[email protected]>2026-09-27 15:07:31 -0700
commit10b7a97a3ecfc2f8e084c81520c78942ac745ec5 (patch)
treea8778b6d22a6bb6d4f750bcb221b455c187b0731 /src
parent31fea2d1e463424387828163bda9facc2fee8c0d (diff)
downloadcoinosv3-10b7a97a3ecfc2f8e084c81520c78942ac745ec5.tar.gz
coinosv3-10b7a97a3ecfc2f8e084c81520c78942ac745ec5.zip
Sending to a coinos.io name shows the person, not the ark address it became.
The recipient box keeps the name, with their face beside it and "Sent instantly, no fee."; the review names them too. The ark1… the name resolved to is plumbing, and read as gibberish to anyone new to this. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
Diffstat (limited to 'src')
-rw-r--r--src/app.js15
-rw-r--r--src/features/ark.js7
-rw-r--r--src/features/messages.js1
-rw-r--r--src/features/names.js13
-rw-r--r--src/i18n.js11
5 files changed, 34 insertions, 13 deletions
diff --git a/src/app.js b/src/app.js
index 8b93766..271667c 100644
--- a/src/app.js
+++ b/src/app.js
@@ -4855,6 +4855,10 @@ function recipientRow(s, r, i) {
// same idea for destination annotations (e.g. the lightning-address zap
// button): they only exist in the rendered tree, so a flip must re-render
r._note = !!featureHook('sendFormNote', r.address);
+ // A payment name resolved to an address on the person's behalf: keep
+ // showing the name (and their face) — the ark1… it became is plumbing.
+ const viaOf = () => (r.via && r.via.ark === r.address ? r.via : null);
+ const via = viaOf();
// Updated imperatively on input (and on render) so paste, typing, and scan
// all reflect immediately without disrupting the input's focus/cursor.
@@ -4867,7 +4871,7 @@ function recipientRow(s, r, i) {
const a = r.address.trim();
const nodes = addrVerifyNodes(a);
check.replaceChildren(...nodes);
- check.style.display = a ? '' : 'none';
+ check.style.display = a && !viaOf() ? '' : 'none';
};
// The suggestions panel gets the same imperative treatment — see the
// sendSearcher note: a render between key repeats kills backspace-hold.
@@ -4885,7 +4889,7 @@ function recipientRow(s, r, i) {
const tryFeature = (v, typed) => featureMatchSend(v, typed);
const addrInput = h('input', {
type: 'text', class: 'mono-input grow', placeholder: i === 0 ? t('destPlaceholder') : 'bc1q…',
- autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: r.address,
+ autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: via ? via.name : r.address,
// the delay lets the keyboard start opening (and any render() swap of
// this input) before parkSendField measures the live activeElement.
// warmSearch pre-opens the search transports so the first keystroke's
@@ -4932,6 +4936,10 @@ function recipientRow(s, r, i) {
!single && h('button', { type: 'button', class: 'btn-sm', title: t('remove'), onClick: () => { s.recipients.splice(i, 1); render(); } }, '✕')
),
check,
+ via ? h('div', { class: 'row gap6 send-via', style: 'align-items:center' },
+ via.pk ? featureHook('avatarNode', via.pk) : null,
+ h('span', { class: 'small' }, via.name),
+ h('span', { class: 'small faint' }, t('sendNameHint'))) : null,
suggest,
r._ready ? h('div', { class: 'input-group' },
h('input', {
@@ -5002,7 +5010,8 @@ function sendForm() {
),
// any feature may annotate the destination (ark hint, lightning-address
// zap offer, …) — each hook decides for itself whether the text is its
- s.recipients.length === 1 ? (featureHook('sendFormNote', plainAddr) || null) : null,
+ s.recipients.length === 1 && !(s.recipients[0].via && s.recipients[0].via.ark === s.recipients[0].address)
+ ? (featureHook('sendFormNote', plainAddr) || null) : null,
ready && !arkDest && h(
'div',
{ class: 'field' },
diff --git a/src/features/ark.js b/src/features/ark.js
index 2a47ef5..eae6b75 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -766,7 +766,9 @@ export function arkFeature(ctx) {
{ class: 'card col', style: 'gap:12px' },
h('h3', {}, t('arkSendTitle')),
row(t('lnPayAmount'), fmtAmount(a.amountSat) + ' ' + unitLabel()),
- row(t('arkPayTo'), shortAddr(a.address, 14)),
+ row(t('arkPayTo'), a.via
+ ? h('span', { class: 'row gap6', style: 'align-items:center' }, a.via.pk ? ctx.hook('avatarNode', a.via.pk) : null, a.via.name)
+ : shortAddr(a.address, 14)),
row(t('networkFee'), t('arkNoFee')),
ui.sendError ? h('div', { class: 'notice err' }, ui.sendError) : null,
// An amount Spending can't cover isn't a dead end while Savings can:
@@ -3446,7 +3448,8 @@ export function arkFeature(ctx) {
// spend a different balance than the one selected.
ui.sendError = t('arkPayFromSpending');
} else {
- ui.arkSend = { address: dest, amountSat: sats };
+ const via = s.recipients[0].via && s.recipients[0].via.ark === dest ? s.recipients[0].via : null;
+ ui.arkSend = { address: dest, amountSat: sats, via };
}
render();
})();
diff --git a/src/features/messages.js b/src/features/messages.js
index a55e735..cd0631a 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -8504,6 +8504,7 @@ export function messagesFeature(ctx) {
// nobody yet — an established nostr user keeps their own list.
identitySignedInNew() { setTimeout(() => seedNewIdentity({ onlyIfNoFollows: true }), 0); return true; },
openProfile(pk) { openProfile(pk); return true; },
+ avatarNode(pk, cls) { return avatar(pk, cls || 'chat-avatar mini', false); },
init() {
const session = ++feedWarmSession;
// this wallet's cached faces, from its own namespace — the keys are
diff --git a/src/features/names.js b/src/features/names.js
index f6afc95..a34334f 100644
--- a/src/features/names.js
+++ b/src/features/names.js
@@ -322,14 +322,15 @@ export function namesFeature(ctx) {
// mirrors, so ask it directly instead of dead-ending in the Lightning
// fallback — which for staging names points at a webroot that serves HTML.
const OUR_DOMAINS = ['coinos.io', 'staging.coinos.io', 'halwallet.app'];
- async function registrarUri(name, domain) {
+ async function registrarLookup(name, domain) {
try {
const r = await withTimeout(
fetch(`${REGISTRAR}/name/${encodeURIComponent(name)}?domain=${encodeURIComponent(domain)}`).then((x) => x.json()),
8000, 'registrar');
- return r && r.taken && r.uri ? r.uri : null;
+ return r && r.taken ? { uri: r.uri || null, pubkey: /^[0-9a-f]{64}$/.test(r.pubkey || '') ? r.pubkey : null } : null;
} catch { return null; }
}
+ const registrarUri = (name, domain) => registrarLookup(name, domain).then((r) => (r && r.uri) || null);
function beginResolve(text) {
const parsed = parsePaymentName(text);
@@ -338,9 +339,14 @@ export function namesFeature(ctx) {
render();
(async () => {
let uri = null, resolveErr = null;
+ // our registrar also knows WHO owns the name: the send form shows the
+ // person (face + name) rather than the ark address the name resolves
+ // to, which reads as gibberish to anyone new to this
+ const ownerP = OUR_DOMAINS.includes(parsed.domain) ? registrarLookup(parsed.name, parsed.domain) : Promise.resolve(null);
try { uri = await resolveBip353(parsed.name, parsed.domain); } catch (e) { resolveErr = e; }
if (ui.nameResolve?.text !== text) return;
- if (!uri && OUR_DOMAINS.includes(parsed.domain)) uri = await registrarUri(parsed.name, parsed.domain);
+ const owner = await ownerP;
+ if (!uri && owner) uri = owner.uri;
if (ui.nameResolve?.text !== text) return;
ui.nameResolve = null;
if (!uri && resolveErr) {
@@ -357,6 +363,7 @@ export function namesFeature(ctx) {
// whose review would only send it back there.
if (await Promise.resolve(hook('arkBoardIfOwn', ark)).catch(() => false)) return;
ui.send.recipients[0].address = ark;
+ ui.send.recipients[0].via = { name: `${parsed.name}@${parsed.domain}`, ark, pk: (owner && owner.pubkey) || null };
render();
return;
}
diff --git a/src/i18n.js b/src/i18n.js
index 9a2b4bc..a271605 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1185,14 +1185,15 @@ const STR = {
arkLnCancelInvoice: 'Cancel invoice',
arkExited: 'Moved to Savings',
arkExitHistory: 'Moved to Savings',
- arkRefreshStarted: 'Refresh submitted — completes with the next round.',
+ arkRefreshStarted: 'Refresh submitted. Completes with the next round.',
arkSendTitle: 'Send',
- arkSendHint: 'Ark address — sent instantly off-chain, no mining fee.',
- arkOwnAddrBoard: "That's your own Spending address — moving from Savings instead.",
+ arkSendHint: 'Ark address: sent instantly off-chain, no mining fee.',
+ sendNameHint: 'Sent instantly, no fee.',
+ arkOwnAddrBoard: "That's your own Spending address. Moving from Savings instead.",
arkLnExceedsSpending: 'That invoice asks for {need} but Spending holds {have}.',
- arkPayFromSpending: 'Ark payments come from your Spending balance — switch to Spending to send this.',
+ arkPayFromSpending: 'Ark payments come from your Spending balance. Switch to Spending to send this.',
arkPayTo: 'To',
- arkNoFee: 'none — instant off-chain send',
+ arkNoFee: 'none, instant off-chain send',
arkSendBtn: 'Send',
arkSentTitle: 'Sent!',
arkBoarded: 'Boarded to Ark',