Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/legacy/legacy-cli-imports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Smoke test for `legacy-cli.ts` module-load.
*
* Why this exists:
* The L1-namespace destructure at the top of legacy-cli.ts
* (`const { encrypt, decrypt, hexToWIF, generatePrivateKey,
* generateAddressFromMasterKey } = L1;`) is the line that broke when
* sphere-sdk's namespace layout changed. The fix in this PR re-anchors
* those imports against the L1 namespace, but the existing 94-test
* suite never imports legacy-cli directly — every test in
* `src/index.test.ts` works through commander's `createCli()` /
* `buildLegacyArgv()` factories which lazily delegate to legacy-cli
* at call time, AFTER the test runner has already imported its own
* modules. As a result, a regression that broke the legacy-cli
* module-load (e.g., a future SDK rename of `decrypt` again) would
* pass `npm run typecheck` and `npm test` while breaking
* `sphere wallet …` at runtime — exactly the failure mode the
* legacy-cli regression that motivated this PR exhibited.
*
* This test forces a static import of legacy-cli at test load time.
* If the destructure ever fails (TypeError: Cannot destructure
* property '<name>' of 'L1' as it is undefined), Vitest reports a
* clean module-load failure with a precise pointer to the bad symbol.
*
* Coverage scope: this is INTENTIONALLY shallow — we don't exercise
* the encrypt/decrypt round-trip (that needs a wallet fixture and
* crosses into integration-test territory). We just confirm the
* module loads, which is enough to catch SDK-export drift.
*/

import { describe, it, expect } from 'vitest';

describe('legacy-cli module load', () => {
it('imports without throwing — proves the L1 destructure resolved', async () => {
// Dynamic import inside the test so a failure surfaces as a test
// failure rather than as a module-collection-time crash that
// takes down the whole vitest suite.
//
// This single test subsumes the entire scope of round-1's
// intent: if the `const { encrypt, decrypt, ... } = L1;`
// destructure at the top of legacy-cli.ts ever fails (L1
// undefined, name renamed, etc.), the dynamic import below
// throws at evaluation time and Vitest reports a clean failure
// pointing at the bad symbol. No additional SDK-side checks
// are needed — they would test the SDK in isolation, not the
// module under review, creating false coverage signal.
const mod = await import('./legacy-cli.js');
expect(mod.legacyMain).toBeDefined();
expect(typeof mod.legacyMain).toBe('function');
});
});
46 changes: 35 additions & 11 deletions src/legacy/legacy-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';
import { encrypt, decrypt } from '@unicitylabs/sphere-sdk';
// `encrypt`, `decrypt`, `hexToWIF`, `generatePrivateKey`, and
// `generateAddressFromMasterKey` are no longer top-level exports of
// @unicitylabs/sphere-sdk — they live in the L1 (alpha-chain) namespace
// as of the latest SDK refactor. Destructuring at module load preserves
// the existing call-site shape so the diff is contained to this block.
// Other helpers (parseWalletText, isValidPrivateKey, etc.) are still
// top-level exports.
import { L1 } from '@unicitylabs/sphere-sdk';
const { encrypt, decrypt, hexToWIF, generatePrivateKey, generateAddressFromMasterKey } = L1;
import { parseWalletText, isTextWalletEncrypted, parseAndDecryptWalletText } from '@unicitylabs/sphere-sdk';
import { parseWalletDat, isSQLiteDatabase, isWalletDatEncrypted } from '@unicitylabs/sphere-sdk';
import { isValidPrivateKey, base58Encode, base58Decode } from '@unicitylabs/sphere-sdk';
import { hexToWIF, generatePrivateKey } from '@unicitylabs/sphere-sdk';
import { toSmallestUnit, toHumanReadable, formatAmount } from '@unicitylabs/sphere-sdk';
import { getPublicKey } from '@unicitylabs/sphere-sdk';
import { generateAddressFromMasterKey } from '@unicitylabs/sphere-sdk';
import { Sphere } from '@unicitylabs/sphere-sdk';
import { createNodeProviders } from '@unicitylabs/sphere-sdk/impl/nodejs';
import { TokenRegistry } from '@unicitylabs/sphere-sdk';
Expand All @@ -23,14 +29,32 @@ import { tokenToTxf } from '@unicitylabs/sphere-sdk';
import type { NetworkType } from '@unicitylabs/sphere-sdk';
import type { TransportProvider } from '@unicitylabs/sphere-sdk';
import type { ProviderStatus } from '@unicitylabs/sphere-sdk';
import type {
CreateInvoiceRequest,
InvoiceRequestedAsset,
GetInvoicesOptions,
PayInvoiceParams,
ReturnPaymentParams,
TxfToken,
} from '@unicitylabs/sphere-sdk';
import type { TxfToken } from '@unicitylabs/sphere-sdk';

// CreateInvoiceRequest, InvoiceRequestedAsset, GetInvoicesOptions,
// PayInvoiceParams, ReturnPaymentParams are declared inside the SDK's
// AccountingModule but are not re-exported at the package root in the
// current SDK build. Extract them structurally from the public method
// signatures on `Sphere.prototype.accounting` so the types stay synced
// to whatever the SDK exposes — no manual mirroring required, and a
// future SDK signature change surfaces here as a typecheck error.
//
// QoL note: these aliases are STRUCTURALLY EQUIVALENT to the SDK's
// named interfaces (`interface CreateInvoiceRequest` etc.) but TS
// error messages at call sites will display the structural expansion
// rather than the SDK's name. If the SDK ever re-exports the named
// types at the package root, prefer importing them directly to
// restore the named-type error messages.
//
// TODO: open a sphere-sdk issue requesting AccountingModule's
// Invoice* interfaces be re-exported at the package root. Until
// then this `Parameters<>` extraction is the maintainable path.
type SphereAccounting = NonNullable<Sphere['accounting']>;
type CreateInvoiceRequest = Parameters<SphereAccounting['createInvoice']>[0];
type InvoiceRequestedAsset = NonNullable<CreateInvoiceRequest['targets']>[number]['assets'][number];
type GetInvoicesOptions = NonNullable<Parameters<SphereAccounting['getInvoices']>[0]>;
type PayInvoiceParams = Parameters<SphereAccounting['payInvoice']>[1];
type ReturnPaymentParams = Parameters<SphereAccounting['returnInvoicePayment']>[1];

/**
* Strip prototype-pollution-prone keys from user-supplied JSON before passing
Expand Down
Loading