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:
- 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).
- Rejects garbage with a clear error instead of silently doing a nametag lookup that may or may not succeed.
- 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.
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>Sphere.resolve(...)without normalization (payments send --to,dm, etc. should be audited).Where the gap lives
src/legacy/legacy-cli.ts:5328:The SDK's
NostrTransportProvider.resolve()does fall through toresolveNametagInfo(identifier)for any string that isn't a recognized address prefix, so barebob-demo06and@bob-demo06should be equivalent. They aren't in practice (see sphere-sdk#457), and even setting that aside, this CLI permissiveness:--to bb-demo06(missingo) silently becomes a nametag query forbb-demo06.--to alpha1...with a typo in the L1 address falls through to the nametag query instead of failing with "invalid L1 address"."bob-demo06"(no@) as thecounterpartyfield 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:This:
--to bob-demo06and--to @bob-demo06are guaranteed to take the same SDK code path (closes the demo's surprise).counterpartyfield, so scripts can round-trip the value.The same
normalizeRecipienthelper 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 --tosphere dmsphere swap propose --tosphere swap propose --escrow(already accepts DIRECT://; verify bare-name auto-prefix is desired here too)Acceptance
sphere swap propose --to bob-demo06and--to @bob-demo06produce identicalcounterpartyoutput (@bob-demo06).sphere swap propose --to <garbage>exits non-zero with a clear "not a recognized recipient form" message before opening the Sphere instance.test/integration/cli-swap-*.integration.test.tsstill pass.normalizeRecipient(or equivalent) is applied to all--tohandlers inlegacy-cli.ts.Related
transportPubkey ?? chainPubkeyfallback). Fixing this CLI issue alone wouldn't have prevented the demo's miss; both are needed.