From 91455f767b07fe9a48e4c9f03c6273770787bda7 Mon Sep 17 00:00:00 2001 From: w3nerick <152233475+w3nerick@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:05:20 -0600 Subject: [PATCH] Presentador sin username del Host: cuenta del wallet o username escrito MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polkadot Desktop 0.1.4 responde «No primary username for this session» a getUserId aunque haya sesión, y testalk firmaba el recibo con la cuenta de la app (sin saldo para el sello). Ahora busca entre las cuentas del wallet (getLegacyAccounts) la que tenga username en People chain (Resources.Consumers), y si no, el presentador escribe su username junto al wallet. Co-Authored-By: Claude Opus 5.5 --- app/src/lib/people.ts | 22 ++++++++++++++++++++++ app/src/lib/signer.ts | 36 +++++++++++++++++++++++++++++++++--- app/src/views/presenter.ts | 16 +++++++++++++++- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/app/src/lib/people.ts b/app/src/lib/people.ts index b1478a8..8bd76a5 100644 --- a/app/src/lib/people.ts +++ b/app/src/lib/people.ts @@ -60,6 +60,28 @@ async function ownerVia(client: PolkadotClient, username: string): Promise (u == null ? null : typeof u === 'string' + ? (/^0x([0-9a-f]{2})*$/i.test(u) ? new TextDecoder().decode(Uint8Array.from(u.slice(2).match(/../g) ?? [], b => parseInt(b, 16))) : u) + : new TextDecoder().decode(u)); + +async function usernameVia(client: PolkadotClient, ss58: string): Promise { + const q = client.getUnsafeApi().query.Resources.Consumers.getValue(ss58) as Promise<{ full_username?: Bytes; lite_username?: Bytes } | undefined>; + const r = await withTimeout(q, PEOPLE_QUERY_MS); + if (r === TIMED_OUT) throw new Error('People chain no respondió'); + return bytesToText(r?.full_username ?? r?.lite_username); +} + +/** Username de una cuenta (`Resources.Consumers`): el completo si es persona verificada, si no el lite. */ +export async function usernameOf(ss58: string): Promise { + if (!isInsideContainerSync()) return usernameVia(viaPublic(), ss58); + try { + return await usernameVia(await viaHost(), ss58); + } catch { + return usernameVia(viaPublic(), ss58); + } +} + /** * Llave pública (hex) dueña de `username`, o `null` si el username no existe. * Lanza si no se pudo consultar: eso es "sin comprobar", no "falso". diff --git a/app/src/lib/signer.ts b/app/src/lib/signer.ts index 39f22ff..36da459 100644 --- a/app/src/lib/signer.ts +++ b/app/src/lib/signer.ts @@ -24,7 +24,7 @@ import { hexToU8a, u8aToHex } from '@polkadot/util'; import { encodeAddress } from '@polkadot/util-crypto'; import { waitForHost, withTimeout, describeError, TIMED_OUT, HOST_QUERY_MS, HOST_SUBMIT_MS } from './host'; import { APP_DOTNS } from './network'; -import { usernameOwner } from './people'; +import { usernameOf, usernameOwner } from './people'; export type SignerKind = 'identity' | 'app' | 'rehearsal'; @@ -63,8 +63,10 @@ export function hasIdentitySigner(): boolean { return identity !== null; } -/** Abre un diálogo: llamar solo desde un gesto del usuario. */ -export async function connectSpeaker(): Promise { +/** Abre un diálogo: llamar solo desde un gesto del usuario. + * `typedUsername`: el que escribe la persona cuando el host no lo entrega + * (Polkadot Desktop 0.1.4: «No primary username for this session»). */ +export async function connectSpeaker(typedUsername?: string): Promise { const inside = isInsideContainerSync(); if (inside && !(await waitForHost())) throw new Error(`No hay canal con Polkadot App. Abre ${APP_DOTNS} desde la app.`); @@ -97,6 +99,14 @@ export async function connectSpeaker(): Promise { identityNote = u === TIMED_OUT ? 'el host no entregó tu username a tiempo (¿aprobaste compartir tu identidad?)' : !u.ok ? `el host no entregó tu username: ${describeError(u.error)}` : 'tu cuenta no tiene username'; + // Sin username del host: el escrito o la cuenta del wallet que tenga username en People chain. + // Firmar con la identidad solo necesita su llave pública (getLegacyAccountSigner). + try { + const found = await identityWithoutHostUsername(typedUsername); + if (found) { username = found.username; identity = found.identity; identityNote = null; } + } catch (e) { + identityNote = `${identityNote}; ${(e as Error).message}`; + } } else { try { const owner = await usernameOwner(username); @@ -126,6 +136,26 @@ export async function connectSpeaker(): Promise { return speaker; } +async function identityWithoutHostUsername(typed?: string): Promise<{ username: string; identity: NonNullable } | null> { + const ap = await withTimeout(getAccountsProvider(), HOST_QUERY_MS); + if (ap === TIMED_OUT || !ap) throw new Error('el host no entregó el proveedor de cuentas'); + const name = typed?.trim().toLowerCase(); + if (name) { + const owner = await usernameOwner(name); + if (!owner) throw new Error(`${name} no aparece en People chain`); + const publicKey = hexToU8a(owner); + return { username: name, identity: { address: encodeAddress(publicKey, 42), pubkey: owner, signer: ap.getLegacyAccountSigner({ publicKey, name }) } }; + } + const accounts = await withTimeout(ap.getLegacyAccounts().match(v => v, () => []), HOST_QUERY_MS); + if (accounts === TIMED_OUT) return null; + for (const a of accounts) { + const address = encodeAddress(a.publicKey, 42); + const n = await usernameOf(address).catch(() => null); + if (n) return { username: n, identity: { address, pubkey: u8aToHex(a.publicKey), signer: ap.getLegacyAccountSigner({ publicKey: a.publicKey, name: n }) } }; + } + return null; +} + /** Respaldo: firmar con la cuenta de la app si el host no firma con identidad. */ export function useAppAccount(): Speaker { if (!appKey) throw new Error('Conecta tu wallet primero.'); diff --git a/app/src/views/presenter.ts b/app/src/views/presenter.ts index 391ba43..91ae63a 100644 --- a/app/src/views/presenter.ts +++ b/app/src/views/presenter.ts @@ -204,10 +204,24 @@ export function renderPresenter(root: HTMLElement): Cleanup { walletRow.className = `check-row ${s ? 'ok' : ''}`; walletRow.innerHTML = s ? `${tag(s.kind === 'app' ? 'warn' : 'ok')}
${esc(s.username ?? shortAddr(s.address))} -
${signerNote(s)}
` +
${signerNote(s)}
+ ${s.kind === 'app' && !s.rehearsal ? `
+
` : ''}` : `${tag('idle')}
Wallet del speaker
Firma el recibo al final
`; start.disabled = !s; + walletRow.querySelector('#use-typed')?.addEventListener('click', async ev => { + const b = ev.currentTarget as HTMLButtonElement; + const typed = (walletRow.querySelector('#typed-user') as HTMLInputElement).value; + b.disabled = true; + err.innerHTML = ''; + try { + drawWallet(await connectSpeaker(typed)); + } catch (e) { + err.innerHTML = `
${icon('warningCircle')}${esc((e as Error).message)}
`; + b.disabled = false; + } + }); walletRow.querySelector('#connect')?.addEventListener('click', async ev => { const b = ev.currentTarget as HTMLButtonElement; b.disabled = true;