Skip to content

swap propose --to: normalize bare nametags to @form (or reject) instead of passing through raw #46

Description

@vrogojin

Symptom

sphere swap propose --to bob-demo06 ... (bare nametag, no @) silently misroutes the DM in a way that gives no error to the proposer. The companion SDK issue (sphere-sdk#457, link below) is the real root cause — but the CLI shouldn't have let the bare form through unchallenged in the first place.

Same pattern likely affects:

  • sphere swap propose --to <name>
  • Any other CLI command that accepts a peer identifier and passes it through to Sphere.resolve(...) without normalization (payments send --to, dm, etc. should be audited).

Where the gap lives

src/legacy/legacy-cli.ts:5328:

const deal = {
  partyA: sphere.identity!.directAddress!,
  partyB: args[toIdx + 1],   // ← raw --to value, no normalization, no validation
  ...
};

The SDK's NostrTransportProvider.resolve() does fall through to resolveNametagInfo(identifier) for any string that isn't a recognized address prefix, so bare bob-demo06 and @bob-demo06 should be equivalent. They aren't in practice (see sphere-sdk#457), and even setting that aside, this CLI permissiveness:

  • Hides typos. --to bb-demo06 (missing o) silently becomes a nametag query for bb-demo06.
  • Hides format mismatches. --to alpha1... with a typo in the L1 address falls through to the nametag query instead of failing with "invalid L1 address".
  • Encodes "bob-demo06" (no @) as the counterparty field in the proposal output JSON — which a user/script then can't paste back into a later CLI invocation without remembering to add @.

Suggested fix

Normalize-or-reject at the CLI boundary, before calling proposeSwap. Concretely:

import { isValidAddress, parseAddress } from '@unicitylabs/sphere-sdk';

function normalizeRecipient(raw: string): string {
  // Already a recognized address form.
  if (isValidAddress(raw)) return raw;
  // Recognized chain pubkey forms.
  if (/^0[23][0-9a-f]{64}$/i.test(raw)) return raw;
  if (/^[0-9a-f]{64}$/i.test(raw))      return raw;
  if (/^alpha1[a-z0-9]+$/i.test(raw) || /^alphat1[a-z0-9]+$/i.test(raw)) return raw;
  // Bare nametag — auto-prefix.
  if (/^[a-z0-9][a-z0-9_-]{0,29}$/i.test(raw)) return '@' + raw;
  // Nothing matched — refuse.
  failWithHelp('swap-propose', `--to "${raw}" is not a recognized recipient form (use @nametag, DIRECT://..., PROXY://..., alpha1..., or a chain pubkey hex)`);
}

const deal = {
  partyA: sphere.identity!.directAddress!,
  partyB: normalizeRecipient(args[toIdx + 1]),
  ...
};

This:

  1. Auto-prefixes bare nametags so --to bob-demo06 and --to @bob-demo06 are guaranteed to take the same SDK code path (closes the demo's surprise).
  2. Rejects garbage with a clear error instead of silently doing a nametag lookup that may or may not succeed.
  3. Echoes the normalized form back in the JSON output's counterparty field, so scripts can round-trip the value.

The same normalizeRecipient helper should be applied to any other CLI command that takes a --to / recipient argument and currently passes it through raw. Quick audit candidates:

  • sphere payments send --to
  • sphere dm
  • sphere swap propose --to
  • sphere swap propose --escrow (already accepts DIRECT://; verify bare-name auto-prefix is desired here too)

Acceptance

  • sphere swap propose --to bob-demo06 and --to @bob-demo06 produce identical counterparty output (@bob-demo06).
  • sphere swap propose --to <garbage> exits non-zero with a clear "not a recognized recipient form" message before opening the Sphere instance.
  • Existing tests in test/integration/cli-swap-*.integration.test.ts still pass.
  • The shared normalizeRecipient (or equivalent) is applied to all --to handlers in legacy-cli.ts.

Related

  • sphere-sdk#457 — the actual silent black-hole cause (transportPubkey ?? chainPubkey fallback). Fixing this CLI issue alone wouldn't have prevented the demo's miss; both are needed.
  • Discovered during demo06 (Discord demo hour) — see thread on those two issues for the live log.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions