Skip to content
Open
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
420 changes: 420 additions & 0 deletions packages/cli/abis/DotnsPopResolver.json

Large diffs are not rendered by default.

109 changes: 108 additions & 1 deletion packages/cli/src/cli/commands/delegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { Command } from "commander";
import chalk from "chalk";
import ora from "ora";
import { type Address } from "viem";
import { setNameDelegate, revokeNameDelegate, getNameDelegate } from "../../commands/delegate";
import {
setNameDelegate,
revokeNameDelegate,
getNameDelegate,
setRecordDelegate,
getRecordDelegate,
} from "../../commands/delegate";
import { resolveTransferRecipient } from "../transfer";
import { addAuthOptions } from "./authOptions";
import { prepareContext } from "../context";
Expand All @@ -19,6 +25,10 @@ interface DelegateCommonOptions {
rpc?: string;
}

interface DelegateRecordsOptions extends DelegateCommonOptions {
revoke?: boolean;
}

export function attachDelegateCommands(root: Command) {
const delegateCommand = root
.command("delegate")
Expand Down Expand Up @@ -144,4 +154,101 @@ export function attachDelegateCommands(root: Command) {
}
},
);

const recordsCommand = delegateCommand
.command("records <operator>")
.description(
"Let another account edit records on all your names; operator may be EVM, SS58, or .dot",
)
.option("--revoke", "Revoke record-editing access instead of granting it", false)
.option("--json", "Output result as JSON (suppresses all other output)", false);
addAuthOptions(recordsCommand).action(
async (operator: string, options: DelegateRecordsOptions, command: Command) => {
const jsonOutput = getJsonFlag(command);
try {
const mergedOptions = getMergedOptions(command, options);
const context = await maybeQuiet(jsonOutput, () =>
prepareContext({ ...mergedOptions, useRevive: true }),
);

const approved = !options.revoke;
if (!jsonOutput) {
console.log(
chalk.bold(approved ? "\n▶ Delegate records\n" : "\n▶ Revoke record delegate\n"),
);
}
const spinner = ora();

const operatorAddress = await maybeQuiet(jsonOutput, () =>
resolveTransferRecipient(context.clientWrapper!, context.substrateAddress, operator),
);

if (!jsonOutput) {
console.log(chalk.gray(" operator: ") + chalk.white(operatorAddress));
}

const result = await maybeQuiet(jsonOutput, () =>
setRecordDelegate(
context.clientWrapper!,
context.substrateAddress,
context.signer,
operatorAddress as Address,
approved,
spinner,
),
);

if (!emitJsonResult(jsonOutput, result)) {
console.log(chalk.gray(" tx: ") + chalk.blue(result.txHash));
console.log(chalk.green("\n✓ Complete\n"));
}
process.exit(0);
} catch (error) {
handleCommandError(jsonOutput, error);
}
},
);

const recordsStatusCommand = delegateCommand
.command("records-status <operator>")
.description("Show whether an operator may edit records on all your names")
.option("--json", "Output result as JSON (suppresses all other output)", false);
addAuthOptions(recordsStatusCommand).action(
async (operator: string, options: DelegateCommonOptions, command: Command) => {
const jsonOutput = getJsonFlag(command);
try {
const mergedOptions = getMergedOptions(command, options);
const context = await maybeQuiet(jsonOutput, () =>
prepareReadOnlyContext(mergedOptions as never),
);

if (!jsonOutput) console.log(chalk.bold("\n▶ Record delegate status\n"));
const spinner = ora();

const operatorAddress = await maybeQuiet(jsonOutput, () =>
resolveTransferRecipient(context.clientWrapper!, context.account.address, operator),
);

const approved = await maybeQuiet(jsonOutput, () =>
getRecordDelegate(
context.clientWrapper!,
context.account.address,
operatorAddress as Address,
spinner,
),
);

if (!emitJsonResult(jsonOutput, { operator: operatorAddress, approved })) {
console.log(chalk.gray(" operator: ") + chalk.white(operatorAddress));
console.log(
chalk.gray(" approved: ") + (approved ? chalk.green("yes") : chalk.gray("no")),
);
console.log(chalk.green("\n✓ Complete\n"));
}
process.exit(0);
} catch (error) {
handleCommandError(jsonOutput, error);
}
},
);
}
2 changes: 1 addition & 1 deletion packages/cli/src/cli/commands/lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export async function prepareReadOnlyContext(
await ensureAccountMappedWhenAuthenticated(clientWrapper, keypair, auth.resolvedFrom);

const evmAddress = await step("Resolving EVM address", async () =>
clientWrapper.getEvmAddress(keypair.address),
clientWrapper.resolveOwnEvmAddress(keypair.address),
);

console.log(chalk.gray("\n RPC: ") + chalk.white(rpc));
Expand Down
42 changes: 33 additions & 9 deletions packages/cli/src/cli/commands/pop.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Command } from "commander";
import chalk from "chalk";
import { getUserProofOfPersonhoodStatus } from "../../commands/register";
import {
getUserProofOfPersonhoodStatus,
getWhitelistStatus,
getPendingClaimLabels,
} from "../../commands/register";
import { addAuthOptions } from "./authOptions";
import type { CommandOptions } from "../../types/types";
import { ProofOfPersonhoodStatus } from "../../types/types";
Expand All @@ -18,6 +22,8 @@ export type PopInfoResult = {
substrate: string;
evm: string;
status: ProofOfPersonhoodStatus;
whitelisted: boolean;
pendingClaims: string[];
};

function formatPopStatus(status: ProofOfPersonhoodStatus): "none" | "lite" | "full" | "reserved" {
Expand All @@ -36,16 +42,19 @@ function formatPopStatus(status: ProofOfPersonhoodStatus): "none" | "lite" | "fu

async function readPopInfo(options: CommandOptions): Promise<PopInfoResult> {
const context = await prepareReadOnlyContext(options as any);
const status = await getUserProofOfPersonhoodStatus(
context.clientWrapper!,
context.account.address,
context.evmAddress as Address,
);
const evm = context.evmAddress as Address;
const [status, whitelisted, pendingClaims] = await Promise.all([
getUserProofOfPersonhoodStatus(context.clientWrapper!, context.account.address, evm),
getWhitelistStatus(context.clientWrapper!, context.account.address, evm),
getPendingClaimLabels(context.clientWrapper!, context.account.address, evm),
]);

return {
substrate: context.account.address,
evm: context.evmAddress!,
status,
whitelisted,
pendingClaims,
};
}

Expand All @@ -72,12 +81,27 @@ export function attachPopCommands(root: Command): void {
evm: info.evm,
status: formatPopStatus(info.status),
statusCode: info.status,
whitelisted: info.whitelisted,
pendingClaims: info.pendingClaims,
})
) {
console.log(chalk.bold("\n📋 ProofOfPersonhood Status\n"));
console.log(chalk.gray(" substrate: ") + chalk.white(info.substrate));
console.log(chalk.gray(" evm: ") + chalk.white(info.evm));
console.log(chalk.gray(" status: ") + chalk.white(formatPopStatus(info.status)));
console.log(chalk.gray(" substrate: ") + chalk.white(info.substrate));
console.log(chalk.gray(" evm: ") + chalk.white(info.evm));
console.log(chalk.gray(" status: ") + chalk.white(formatPopStatus(info.status)));
console.log(
chalk.gray(" whitelisted:") +
" " +
(info.whitelisted
? chalk.green("yes (may register governance-reserved names)")
: chalk.gray("no")),
);
if (info.pendingClaims.length > 0) {
console.log(
chalk.gray(" pending: ") +
chalk.yellow(`${info.pendingClaims.join(", ")} (run "dotns store sync" to settle)`),
);
}
console.log(chalk.green("\n✓ PoP Status Retrieved\n"));
}

Expand Down
107 changes: 107 additions & 0 deletions packages/cli/src/cli/commands/primary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Command } from "commander";
import chalk from "chalk";
import ora from "ora";
import { type Address } from "viem";
import { setPrimaryName, getPrimaryName } from "../../commands/reverseRecord";
import { resolveTransferRecipient } from "../transfer";
import { addAuthOptions } from "./authOptions";
import { prepareContext } from "../context";
import { prepareReadOnlyContext } from "./lookup";
import {
getMergedOptions,
getJsonFlag,
maybeQuiet,
emitJsonResult,
handleCommandError,
} from "./jsonHelpers";

interface PrimaryCommonOptions {
rpc?: string;
}

export function attachPrimaryCommands(root: Command) {
const primaryCommand = root
.command("primary")
.description("Manage the primary (reverse) name shown for your account");
addAuthOptions(primaryCommand);

const setCommand = primaryCommand
.command("set <name>")
.description("Set one of your names as the primary name for your account")
.option("--json", "Output result as JSON (suppresses all other output)", false);
addAuthOptions(setCommand).action(
async (name: string, options: PrimaryCommonOptions, command: Command) => {
const jsonOutput = getJsonFlag(command);
try {
const mergedOptions = getMergedOptions(command, options);
const context = await maybeQuiet(jsonOutput, () =>
prepareContext({ ...mergedOptions, useRevive: true }),
);

if (!jsonOutput) console.log(chalk.bold("\n▶ Set primary name\n"));
const spinner = ora();

const result = await maybeQuiet(jsonOutput, () =>
setPrimaryName(
context.clientWrapper!,
context.substrateAddress,
context.signer,
name,
spinner,
),
);

if (!emitJsonResult(jsonOutput, result)) {
console.log(chalk.gray(" name: ") + chalk.cyan(result.name));
console.log(chalk.gray(" tx: ") + chalk.blue(result.txHash));
console.log(chalk.green("\n✓ Complete\n"));
}
process.exit(0);
} catch (error) {
handleCommandError(jsonOutput, error);
}
},
);

const statusCommand = primaryCommand
.command("status [address]")
.description("Show the primary name for an account (defaults to your own)")
.option("--json", "Output result as JSON (suppresses all other output)", false);
addAuthOptions(statusCommand).action(
async (address: string | undefined, options: PrimaryCommonOptions, command: Command) => {
const jsonOutput = getJsonFlag(command);
try {
const mergedOptions = getMergedOptions(command, options);
const context = await maybeQuiet(jsonOutput, () =>
prepareReadOnlyContext(mergedOptions as never),
);

if (!jsonOutput) console.log(chalk.bold("\n▶ Primary name\n"));
const spinner = ora();

const targetEvm = address
? ((await maybeQuiet(jsonOutput, () =>
resolveTransferRecipient(context.clientWrapper!, context.account.address, address),
)) as Address)
: await context.clientWrapper!.getEvmAddress(context.account.address);

const name = await maybeQuiet(jsonOutput, () =>
getPrimaryName(context.clientWrapper!, context.account.address, targetEvm, spinner),
);

if (!emitJsonResult(jsonOutput, { address: targetEvm, name })) {
console.log(chalk.gray(" address: ") + chalk.white(targetEvm));
if (name === null) {
console.log(chalk.gray(" no primary name set"));
} else {
console.log(chalk.gray(" primary: ") + chalk.cyan(name));
}
console.log(chalk.green("\n✓ Complete\n"));
}
process.exit(0);
} catch (error) {
handleCommandError(jsonOutput, error);
}
},
);
}
1 change: 1 addition & 0 deletions packages/cli/src/cli/commands/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ async function finalizeRegularReveal(params: {
params.substrateAddress,
params.label,
params.ownerEvmAddress,
params.evmAddress,
),
);

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { banner } from "./ui";
import { attachStoreCommands } from "./commands/store";
import { attachEscrowCommands } from "./commands/escrow";
import { attachDelegateCommands } from "./commands/delegate";
import { attachPrimaryCommands } from "./commands/primary";
import { ENV } from "./env";

export function createProgram() {
Expand All @@ -32,6 +33,7 @@ export function createProgram() {
attachStoreCommands(program);
attachEscrowCommands(program);
attachDelegateCommands(program);
attachPrimaryCommands(program);

if (!process.argv.includes("--json")) banner();

Expand Down
18 changes: 13 additions & 5 deletions packages/cli/src/cli/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import ora from "ora";
import { checksumAddress, isAddress, zeroAddress, type Address } from "viem";
import { ReviveClientWrapper } from "../client/polkadotClient";
import { CONTRACTS, DOTNS_REGISTRAR_ABI } from "../utils/constants";
import { validateDomainLabel } from "../utils/validation";
import { validateDomainLabel, isValidSubstrateAddress } from "../utils/validation";
import { formatErrorMessage, formatWeiAsEther, convertWeiToNativeCeil } from "../utils/formatting";
import {
computeDomainTokenId,
Expand Down Expand Up @@ -48,8 +48,17 @@ export async function resolveTransferRecipient(
): Promise<Address> {
const input = recipientIdentifier.trim();

// Classify in priority order; an SS58 address must be matched before the label
// branch because its lowercased form is all [a-z0-9] and would pass isLabelLike.
if (isAddress(input)) return toChecksummed(input as Address);

if (isValidSubstrateAddress(input)) {
const spinner = ora(`Resolving ${chalk.white(input)} to EVM address`).start();
const evmAddress = await clientWrapper.getEvmAddress(input);
spinner.succeed(`${chalk.white(input)} → ${chalk.white(toChecksummed(evmAddress))}`);
return toChecksummed(evmAddress);
}

const label = asDotLabel(input);
if (isLabelLike(label)) {
const spinner = ora(`Resolving ${chalk.cyan(label + ".dot")} to owner address`).start();
Expand All @@ -65,10 +74,9 @@ export async function resolveTransferRecipient(
return toChecksummed(ownerAddress);
}

const spinner = ora(`Resolving recipient address`).start();
const evmAddress = await clientWrapper.getEvmAddress(input);
spinner.succeed(`${chalk.white(input)} → ${chalk.white(toChecksummed(evmAddress))}`);
return toChecksummed(evmAddress);
throw new Error(
`Unrecognised recipient "${input}" — expected an EVM address, SS58 address, or .dot label.`,
);
}

export async function transferDomain(
Expand Down
Loading
Loading