Skip to content

Commit 16ba27b

Browse files
authored
chore: code cleanup and fixes (#181)
## Description This PR introduces minor fixes related to UI and the CLI. ## Type - [x] Bug fix - [ ] Feature - [ ] Breaking change - [ ] Documentation - [ ] Chore ## Package - [ ] `@parity/dotns-cli` - [ ] Root/monorepo - [ ] Documentation ## Related Issues ## Fixes ## Checklist ### Code - [x] Follows project style - [x] `bun run lint` passes - [x] `bun run format` passes - [x] `bun run typecheck` passes ### Documentation - [ ] README updated if needed - [ ] Types updated if needed ### Breaking Changes - [x] No breaking changes - [ ] Breaking changes documented below **Breaking changes:** ## Testing How to test: 1. 2. ## Notes
1 parent b59877f commit 16ba27b

12 files changed

Lines changed: 204 additions & 51 deletions

File tree

packages/cli/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,8 +446,10 @@ dotns --password test-password store get mykey --account default
446446
dotns --password test-password store set mykey "my value" --account default
447447
dotns --password test-password store delete mykey --account default
448448

449-
# List the .dot names in your Label Store, and cached CIDs
449+
# List the .dot names in your Label Store (second-level names only by default;
450+
# add --all to include subdomains), and cached CIDs
450451
dotns --password test-password store names --account default
452+
dotns --password test-password store names --all --account default
451453
dotns --password test-password store cids --account default
452454

453455
# Settle any pending names from the PoP controller into your Label Store

packages/cli/src/cli/commands/escrow.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
formatRefundEntryLine,
2020
} from "../../commands/escrow";
2121
import { listStoreNames } from "../../commands/storeManagement";
22+
import { resolveTransferRecipient } from "../transfer";
2223
import { addAuthOptions } from "./authOptions";
2324
import { prepareContext } from "../context";
2425
import { prepareReadOnlyContext } from "./lookup";
@@ -30,10 +31,22 @@ import {
3031
handleCommandError,
3132
} from "./jsonHelpers";
3233
import { formatWeiAsEther } from "../../utils/formatting";
34+
import type { ReadOnlyContext } from "../../types/types";
3335

3436
const DEFAULT_REFUND_PAGE_SIZE = 50;
3537
const MAX_REFUND_PAGE_SIZE = 200;
3638

39+
async function resolveRecipientOption(
40+
jsonOutput: boolean,
41+
context: ReadOnlyContext,
42+
recipientOption: string | undefined,
43+
): Promise<Address> {
44+
if (!recipientOption) return context.evmAddress as Address;
45+
return maybeQuiet(jsonOutput, () =>
46+
resolveTransferRecipient(context.clientWrapper, context.account.address, recipientOption),
47+
);
48+
}
49+
3750
interface EscrowCommonOptions {
3851
rpc?: string;
3952
}
@@ -110,7 +123,10 @@ export function attachEscrowCommands(root: Command) {
110123
const balanceCommand = escrowCommand
111124
.command("balance")
112125
.description("Show the caller's claimable pull-payment balance")
113-
.option("--recipient <address>", "Recipient EVM address (defaults to caller)")
126+
.option(
127+
"--recipient <address>",
128+
"Recipient EVM address, SS58 address, or .dot label (defaults to caller)",
129+
)
114130
.option("--json", "Output result as JSON (suppresses all other output)", false);
115131
addAuthOptions(balanceCommand).action(async (options: RefundListOptions, command: Command) => {
116132
const jsonOutput = getJsonFlag(command);
@@ -120,7 +136,7 @@ export function attachEscrowCommands(root: Command) {
120136
prepareReadOnlyContext(mergedOptions as any),
121137
);
122138

123-
const recipient = (options.recipient ?? context.evmAddress) as Address;
139+
const recipient = await resolveRecipientOption(jsonOutput, context, options.recipient);
124140

125141
if (!jsonOutput) console.log(chalk.bold("\n▶ Escrow balance\n"));
126142
const spinner = ora();
@@ -143,7 +159,10 @@ export function attachEscrowCommands(root: Command) {
143159
const positionsCommand = escrowCommand
144160
.command("positions")
145161
.description("List all escrow positions for the caller and the total locked")
146-
.option("--recipient <address>", "Recipient EVM address (defaults to caller)")
162+
.option(
163+
"--recipient <address>",
164+
"Recipient EVM address, SS58 address, or .dot label (defaults to caller)",
165+
)
147166
.option("--json", "Output result as JSON (suppresses all other output)", false);
148167
addAuthOptions(positionsCommand).action(async (options: RefundListOptions, command: Command) => {
149168
const jsonOutput = getJsonFlag(command);
@@ -153,7 +172,7 @@ export function attachEscrowCommands(root: Command) {
153172
prepareReadOnlyContext(mergedOptions as any),
154173
);
155174

156-
const recipient = (options.recipient ?? context.evmAddress) as Address;
175+
const recipient = await resolveRecipientOption(jsonOutput, context, options.recipient);
157176

158177
if (!jsonOutput) console.log(chalk.bold("\n▶ Escrow positions\n"));
159178
const spinner = ora();
@@ -344,7 +363,10 @@ export function attachEscrowCommands(root: Command) {
344363
const refundsListCommand = refundsCommand
345364
.command("list")
346365
.description("List pending refund entries for the caller (or a specified recipient)")
347-
.option("--recipient <address>", "Recipient EVM address (defaults to caller)")
366+
.option(
367+
"--recipient <address>",
368+
"Recipient EVM address, SS58 address, or .dot label (defaults to caller)",
369+
)
348370
.option("--offset <n>", "Page offset", "0")
349371
.option(
350372
"--limit <n>",
@@ -369,7 +391,7 @@ export function attachEscrowCommands(root: Command) {
369391
throw new Error(`limit must be between 1 and ${MAX_REFUND_PAGE_SIZE}`);
370392
}
371393

372-
const recipient = (options.recipient ?? context.evmAddress) as Address;
394+
const recipient = await resolveRecipientOption(jsonOutput, context, options.recipient);
373395

374396
if (!jsonOutput) console.log(chalk.bold("\n▶ Refund ledger\n"));
375397
const spinner = ora();

packages/cli/src/cli/commands/store.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { prepareAssetHubContext } from "../context";
66
import { prepareReadOnlyContext } from "./lookup";
77
import { getJsonFlag, maybeQuiet } from "./jsonHelpers";
88
import { formatErrorMessage } from "../../utils/formatting";
9+
import { isSecondLevelDotName } from "../../utils/validation";
910
import {
1011
claimUserStore,
1112
getStoreInfo,
@@ -98,7 +99,7 @@ export function attachStoreCommands(root: Command): void {
9899

99100
const listCommand = storeCommand
100101
.command("list")
101-
.description("List all values in your Store")
102+
.description("List all values in your UserStore")
102103
.option("--json", "Output result as JSON", false);
103104

104105
addAuthOptions(listCommand).action(async (options: any, cmd: any) => {
@@ -128,7 +129,8 @@ export function attachStoreCommands(root: Command): void {
128129

129130
const namesCommand = storeCommand
130131
.command("names")
131-
.description("List all .dot names in your Store")
132+
.description("List .dot names in your LabelStore (second-level names only by default)")
133+
.option("--all", "Include subdomains (default: only second-level .dot names)", false)
132134
.option("--json", "Output result as JSON", false);
133135

134136
addAuthOptions(namesCommand).action(async (options: any, cmd: any) => {
@@ -140,7 +142,8 @@ export function attachStoreCommands(root: Command): void {
140142
prepareReadOnlyContext(merged),
141143
);
142144

143-
const names = await listStoreNames(clientWrapper, account.address, evmAddress as Address);
145+
const allNames = await listStoreNames(clientWrapper, account.address, evmAddress as Address);
146+
const names = options.all ? allNames : allNames.filter(isSecondLevelDotName);
144147

145148
if (jsonOutput) {
146149
console.log(JSON.stringify({ names }));

packages/cli/src/commands/escrow.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -351,16 +351,8 @@ export async function listRefunds(
351351
[recipient],
352352
);
353353

354-
const ids = await performContractCall<bigint[]>(
355-
clientWrapper,
356-
originSubstrateAddress,
357-
CONTRACTS.DOTNS_NAME_ESCROW,
358-
DOTNS_NAME_ESCROW_ABI,
359-
"pendingRefundIds",
360-
[recipient, BigInt(offset), BigInt(limit)],
361-
);
362-
363-
const entries = await performContractCall<RawRefundEntry[]>(
354+
// pendingRefunds has two outputs, so the call decodes to a [ids, entries] tuple.
355+
const [ids, entries] = await performContractCall<[bigint[], RawRefundEntry[]]>(
364356
clientWrapper,
365357
originSubstrateAddress,
366358
CONTRACTS.DOTNS_NAME_ESCROW,

packages/cli/src/utils/validation.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export function asLabel(name: string): string {
2525
return raw.endsWith(".dot") ? raw.slice(0, -4) : raw;
2626
}
2727

28+
// True for a single label under .dot ("alice", "alice.dot"), false for subdomains ("sub.alice").
29+
export function isSecondLevelDotName(name: string): boolean {
30+
return asLabel(name).split(".").filter(Boolean).length === 1;
31+
}
32+
2833
// A single canonical DNS label, mirroring the contract's StringUtils._isDnsLabel
2934
// (PopRules._requireCanonicalLabel / registry subnode rules): lowercase ASCII
3035
// letters, digits and hyphen only, no leading or trailing hyphen, length 1-63, no
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { checksumAddress } from "viem";
3+
import { resolveTransferRecipient } from "../../../src/cli/transfer";
4+
5+
// Only the EVM-address and unrecognised-input branches are exercised here; the
6+
// SS58 and .dot-label branches resolve via the chain client and belong to
7+
// integration coverage.
8+
const NO_CLIENT = null as never;
9+
10+
describe("resolveTransferRecipient", () => {
11+
test("returns a raw EVM address in checksummed form without touching the chain", async () => {
12+
const lower = "0x35cdb23ff7fc86e8dccd577ca309bfea9c978d20";
13+
await expect(resolveTransferRecipient(NO_CLIENT, "", lower)).resolves.toBe(
14+
checksumAddress(lower),
15+
);
16+
});
17+
18+
test("rejects input that is neither an EVM address, SS58 address, nor .dot label", async () => {
19+
await expect(resolveTransferRecipient(NO_CLIENT, "", "not a name!!")).rejects.toThrow(
20+
"Unrecognised recipient",
21+
);
22+
});
23+
});

packages/cli/tests/unit/escrow/escrowFormatting.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test";
2-
import type { Address } from "viem";
2+
import { decodeFunctionResult, encodeFunctionResult, type Address } from "viem";
33
import {
44
formatRefundEntryLine,
55
totalEscrowAmount,
@@ -9,6 +9,7 @@ import {
99
formatPositionStatus,
1010
formatPositionsTable,
1111
} from "../../../src/commands/escrow";
12+
import { DOTNS_NAME_ESCROW_ABI } from "../../../src/utils/constants";
1213

1314
function stripAnsi(input: string): string {
1415
// forge-lint-equivalent: keep the ANSI assertions readable by stripping colour codes.
@@ -169,3 +170,54 @@ describe("formatPositionsTable", () => {
169170
expect(lines[1]).toContain("cooldown 1m 0s");
170171
});
171172
});
173+
174+
const REFUND_RECIPIENT = "0x1111111111111111111111111111111111111111" as Address;
175+
176+
// pendingRefunds has two outputs, so decodeFunctionResult yields a [ids, entries]
177+
// tuple; listRefunds depends on that shape.
178+
describe("pendingRefunds multi-output decode", () => {
179+
test("decodes to a [ids, entries] tuple", () => {
180+
const encoded = encodeFunctionResult({
181+
abi: DOTNS_NAME_ESCROW_ABI,
182+
functionName: "pendingRefunds",
183+
result: [
184+
[7n, 8n],
185+
[
186+
{ recipient: REFUND_RECIPIENT, amount: 10n, availableAt: 0n, tokenId: 100n },
187+
{ recipient: REFUND_RECIPIENT, amount: 20n, availableAt: 0n, tokenId: 200n },
188+
],
189+
],
190+
});
191+
192+
const [ids, entries] = decodeFunctionResult({
193+
abi: DOTNS_NAME_ESCROW_ABI,
194+
functionName: "pendingRefunds",
195+
data: encoded,
196+
}) as [
197+
bigint[],
198+
{ recipient: Address; amount: bigint; availableAt: bigint; tokenId: bigint }[],
199+
];
200+
201+
expect(ids).toEqual([7n, 8n]);
202+
expect(entries).toHaveLength(2);
203+
expect(entries[0]!.amount).toBe(10n);
204+
expect(entries[1]!.tokenId).toBe(200n);
205+
});
206+
207+
test("decodes an empty ledger to a pair of empty arrays", () => {
208+
const encoded = encodeFunctionResult({
209+
abi: DOTNS_NAME_ESCROW_ABI,
210+
functionName: "pendingRefunds",
211+
result: [[], []],
212+
});
213+
214+
const [ids, entries] = decodeFunctionResult({
215+
abi: DOTNS_NAME_ESCROW_ABI,
216+
functionName: "pendingRefunds",
217+
data: encoded,
218+
}) as [bigint[], unknown[]];
219+
220+
expect(ids).toEqual([]);
221+
expect(entries).toEqual([]);
222+
});
223+
});

packages/cli/tests/unit/utils/validation.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
validateGovernanceLabel,
55
validateCanonicalLabel,
66
isCanonicalLabel,
7+
isSecondLevelDotName,
78
countTrailingDigits,
89
stripTrailingDigits,
910
} from "../../../src/utils/validation";
@@ -33,6 +34,20 @@ describe("isCanonicalLabel", () => {
3334
});
3435
});
3536

37+
describe("isSecondLevelDotName", () => {
38+
test("accepts a single label, with or without the .dot suffix", () => {
39+
expect(isSecondLevelDotName("alice")).toBe(true);
40+
expect(isSecondLevelDotName("alice.dot")).toBe(true);
41+
expect(isSecondLevelDotName("ALICE.DOT")).toBe(true);
42+
});
43+
44+
test("rejects subdomains", () => {
45+
expect(isSecondLevelDotName("sub.alice")).toBe(false);
46+
expect(isSecondLevelDotName("sub.alice.dot")).toBe(false);
47+
expect(isSecondLevelDotName("a.b.c.dot")).toBe(false);
48+
});
49+
});
50+
3651
describe("validateCanonicalLabel", () => {
3752
test("throws for a name containing a dot, naming the role", () => {
3853
expect(() => validateCanonicalLabel("sphakjjj.77", "subname")).toThrow(/subname/);

packages/ui/src/router/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { createRouter, createWebHashHistory } from "vue-router";
22
import { lazyLoad, isChunkLoadError } from "@/lib/lazyLoad";
33
import { formatErrorMessage } from "@/lib/errorHandling";
44

5+
// /upload and /preview share one component so Vue Router reuses the instance
6+
// between them; a separate lazyLoad() per route would unmount PreviewView on the
7+
// transition and discard the post-upload hand-off cache (blank preview).
8+
const previewView = lazyLoad(() => import("../views/PreviewView.vue"));
9+
510
const routes = [
611
{
712
path: "/",
@@ -27,12 +32,12 @@ const routes = [
2732
{
2833
path: "/upload",
2934
name: "Upload",
30-
component: lazyLoad(() => import("../views/PreviewView.vue")),
35+
component: previewView,
3136
},
3237
{
3338
path: "/preview/:encoded?",
3439
name: "PreviewEncoded",
35-
component: lazyLoad(() => import("../views/PreviewView.vue")),
40+
component: previewView,
3641
},
3742
{
3843
path: "/docs",

packages/ui/src/store/useUserStoreManager.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,25 @@ import type { DotnsAvailability } from "@/type";
1717
import { useResolverStore } from "./useResolverStore";
1818
import { useWalletStore } from "./useWalletStore";
1919

20-
// Maximum store entries enumerated per page. A user with more than 256 entries
21-
// would need a paginated read; treat as a soft cap (shared by both stores).
20+
// Store getters cap each getLabels/getKeys call at this size, so reads must page.
2221
const STORE_PAGE_SIZE = 256n;
2322

2423
const ZERO: Address = zeroAddress;
2524

25+
// Pages a store getter to completion; fetchPage returns one page, or null on query failure.
26+
async function readAllPages<T>(
27+
fetchPage: (offset: bigint, limit: bigint) => Promise<readonly T[] | null>,
28+
): Promise<T[]> {
29+
const items: T[] = [];
30+
for (let offset = 0n; ; offset += STORE_PAGE_SIZE) {
31+
const page = await fetchPage(offset, STORE_PAGE_SIZE);
32+
if (!page) break;
33+
items.push(...page);
34+
if (page.length < Number(STORE_PAGE_SIZE)) break;
35+
}
36+
return items;
37+
}
38+
2639
// UserStore keys are bytes32. To stay interoperable with the CLI (`dotns store`),
2740
// a plain string key is hashed exactly as the CLI does: keccak256(toHex(value)).
2841
function userStoreKey(value: string): Hash {
@@ -81,11 +94,12 @@ export const useUserStoreManager = defineStore("userStoreManager", () => {
8194
const labelStore = await getLabelStore(evm);
8295
if (labelStore === ZERO) return [];
8396
const store = await getProxyContract("@dotns/label-store", labelStore);
84-
const result = await store.getLabels!.query(0n, STORE_PAGE_SIZE, {
85-
origin: ZERO_SUBSTRATE_ADDRESS,
97+
return readAllPages<string>(async (offset, limit) => {
98+
const result = await store.getLabels!.query(offset, limit, {
99+
origin: ZERO_SUBSTRATE_ADDRESS,
100+
});
101+
return result.success ? ((result.value as string[]) ?? []) : null;
86102
});
87-
if (!result.success) return [];
88-
return (result.value as string[]) ?? [];
89103
});
90104
}
91105

@@ -200,11 +214,12 @@ export const useUserStoreManager = defineStore("userStoreManager", () => {
200214
if (store === ZERO) return [];
201215

202216
const proxy = await getProxyContract("@dotns/user-store", store);
203-
const keysResult = await proxy.getKeys!.query(0n, STORE_PAGE_SIZE, {
204-
origin: ZERO_SUBSTRATE_ADDRESS,
217+
const keys = await readAllPages<Hash>(async (offset, limit) => {
218+
const keysResult = await proxy.getKeys!.query(offset, limit, {
219+
origin: ZERO_SUBSTRATE_ADDRESS,
220+
});
221+
return keysResult.success ? ((keysResult.value as Hash[]) ?? []) : null;
205222
});
206-
if (!keysResult.success) return [];
207-
const keys = (keysResult.value as Hash[]) ?? [];
208223

209224
const cids: string[] = [];
210225
for (const key of keys) {

0 commit comments

Comments
 (0)