From 6c5791ae188826b5b62a866d9f9da7cfafaa0027 Mon Sep 17 00:00:00 2001 From: Siphamandla Mjoli Date: Thu, 23 Apr 2026 16:50:43 +0200 Subject: [PATCH 1/2] chore: QOF fixes --- packages/cli/src/cli/commands/bulletin.ts | 2 +- packages/cli/src/commands/auth.ts | 28 ++++-- packages/cli/src/commands/register.ts | 2 + packages/cli/src/utils/constants.ts | 2 + packages/cli/src/utils/formatting.ts | 12 ++- .../auth/resolveAuthSourceReadOnly.test.ts | 78 ++++++++++++++++ .../tests/unit/pop/setPopMapsAccount.test.ts | 92 +++++++++++++++++++ .../cli/tests/unit/utils/formatting.test.ts | 32 +++++++ 8 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 packages/cli/tests/unit/auth/resolveAuthSourceReadOnly.test.ts create mode 100644 packages/cli/tests/unit/pop/setPopMapsAccount.test.ts create mode 100644 packages/cli/tests/unit/utils/formatting.test.ts diff --git a/packages/cli/src/cli/commands/bulletin.ts b/packages/cli/src/cli/commands/bulletin.ts index 16d047f..72480d3 100644 --- a/packages/cli/src/cli/commands/bulletin.ts +++ b/packages/cli/src/cli/commands/bulletin.ts @@ -421,7 +421,7 @@ export function attachBulletinCommands(root: Command): void { ); const signerContext = await withBulletinHumanOutput(reporterMode, () => - prepareContext({ keyUri: signerKeyUri, useBulletin: true }), + prepareContext({ keyUri: signerKeyUri, useBulletin: true, bulletinRpc }), ); if (!jsonOutput) { diff --git a/packages/cli/src/commands/auth.ts b/packages/cli/src/commands/auth.ts index d8bf6d3..f5b0e15 100644 --- a/packages/cli/src/commands/auth.ts +++ b/packages/cli/src/commands/auth.ts @@ -33,7 +33,24 @@ function toSafeAccountFilename(accountName: string): string { return `${safeFilename}.json`; } +function resolveAuthSourceFromEnv(account: string): ResolvedAuthSource | undefined { + const envMnemonic = process.env[ENV.MNEMONIC]; + if (envMnemonic && envMnemonic.length > 0) { + return { source: envMnemonic, isKeyUri: false, resolvedFrom: "env", account }; + } + + const envKeyUri = process.env[ENV.KEY_URI]; + if (envKeyUri && envKeyUri.length > 0) { + return { source: envKeyUri, isKeyUri: true, resolvedFrom: "env", account }; + } + + return undefined; +} + export async function resolveAuthSourceReadOnly(): Promise { + const fromEnv = resolveAuthSourceFromEnv("readonly"); + if (fromEnv) return fromEnv; + return { source: DEFAULT_MNEMONIC, isKeyUri: false, @@ -52,15 +69,8 @@ export async function resolveAuthSource(opts: AuthSource): Promise 0) { - return { source: envMnemonic, isKeyUri: false, resolvedFrom: "env", account: accountName }; - } - if (envKeyUri && envKeyUri.length > 0) { - return { source: envKeyUri, isKeyUri: true, resolvedFrom: "env", account: accountName }; - } + const fromEnv = resolveAuthSourceFromEnv(accountName); + if (fromEnv) return fromEnv; const keystoreDirectoryPath = resolveKeystorePath(opts.keystorePath); diff --git a/packages/cli/src/commands/register.ts b/packages/cli/src/commands/register.ts index 2157e34..29b8dd4 100644 --- a/packages/cli/src/commands/register.ts +++ b/packages/cli/src/commands/register.ts @@ -714,6 +714,8 @@ export async function setUserProofOfPersonhoodStatus( const checkSpinner = ora(`Checking current PoP status for ${displayName}`).start(); try { + await clientWrapper.ensureAccountMapped(substrateAddress, signer); + const currentStatus = await getUserProofOfPersonhoodStatus( clientWrapper, substrateAddress, diff --git a/packages/cli/src/utils/constants.ts b/packages/cli/src/utils/constants.ts index 0edafad..1f2d9cc 100644 --- a/packages/cli/src/utils/constants.ts +++ b/packages/cli/src/utils/constants.ts @@ -23,6 +23,8 @@ export const DOT_NODE: Hex = "0x3fce7d1364a893e213bc4212792b517ffc88f5b13b86c8ef export const DECIMALS = 12n; +export const DECIMALS_DOT = 10n; + export const NATIVE_TO_ETH_RATIO = 1_000_000n; export const DEFAULT_MNEMONIC = diff --git a/packages/cli/src/utils/formatting.ts b/packages/cli/src/utils/formatting.ts index 6b56379..9d47cbe 100644 --- a/packages/cli/src/utils/formatting.ts +++ b/packages/cli/src/utils/formatting.ts @@ -3,15 +3,15 @@ import type { Ora } from "ora"; import { formatEther } from "viem"; import { printHumanDetail, printHumanFailure, printHumanSuccess } from "../cli/reporter"; import type { TransactionStatus } from "../types/types"; -import { DECIMALS, NATIVE_TO_ETH_RATIO } from "./constants"; +import { DECIMALS_DOT, NATIVE_TO_ETH_RATIO } from "./constants"; export function formatNativeBalance(valueInNativeUnits: bigint): string { - const divisor = 10n ** DECIMALS; + const divisor = 10n ** DECIMALS_DOT; const wholePart = valueInNativeUnits / divisor; const fractionalPart = valueInNativeUnits % divisor; let fractionalString = fractionalPart.toString(); - const missingZeroCount = DECIMALS - BigInt(fractionalString.length); + const missingZeroCount = DECIMALS_DOT - BigInt(fractionalString.length); if (missingZeroCount > 0n) { fractionalString = "0".repeat(Number(missingZeroCount)) + fractionalString; } @@ -24,9 +24,11 @@ export function parseNativeBalance(decimalValue: string): bigint { const wholePart = BigInt(parts[0] || "0"); const fractionalPart = parts[1] || "0"; - const paddedFraction = fractionalPart.padEnd(Number(DECIMALS), "0").slice(0, Number(DECIMALS)); + const paddedFraction = fractionalPart + .padEnd(Number(DECIMALS_DOT), "0") + .slice(0, Number(DECIMALS_DOT)); - return wholePart * 10n ** DECIMALS + BigInt(paddedFraction); + return wholePart * 10n ** DECIMALS_DOT + BigInt(paddedFraction); } export function convertNativeToWei(nativeValue: bigint): bigint { diff --git a/packages/cli/tests/unit/auth/resolveAuthSourceReadOnly.test.ts b/packages/cli/tests/unit/auth/resolveAuthSourceReadOnly.test.ts new file mode 100644 index 0000000..41d0dbf --- /dev/null +++ b/packages/cli/tests/unit/auth/resolveAuthSourceReadOnly.test.ts @@ -0,0 +1,78 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { resolveAuthSourceReadOnly } from "../../../src/commands/auth"; +import { DEFAULT_MNEMONIC } from "../../../src/utils/constants"; +import { ENV } from "../../../src/cli/env"; +import { ALICE_KEY_URI } from "../../_helpers/cliHelpers"; + +const CUSTOM_MNEMONIC = + "absorb oppose idea expire husband layer subject flip pause ahead daring stem"; + +const previousEnv = { + mnemonic: process.env[ENV.MNEMONIC], + keyUri: process.env[ENV.KEY_URI], +}; + +function clearAuthEnv(): void { + delete process.env[ENV.MNEMONIC]; + delete process.env[ENV.KEY_URI]; +} + +function restoreAuthEnv(): void { + if (previousEnv.mnemonic === undefined) delete process.env[ENV.MNEMONIC]; + else process.env[ENV.MNEMONIC] = previousEnv.mnemonic; + + if (previousEnv.keyUri === undefined) delete process.env[ENV.KEY_URI]; + else process.env[ENV.KEY_URI] = previousEnv.keyUri; +} + +afterEach(() => { + restoreAuthEnv(); +}); + +type EnvCase = { + label: string; + env: { mnemonic?: string; keyUri?: string }; + expected: { source: string; isKeyUri: boolean; resolvedFrom: "env" | "default" }; +}; + +const envCases: EnvCase[] = [ + { + label: "no env falls back to DEFAULT_MNEMONIC", + env: {}, + expected: { source: DEFAULT_MNEMONIC, isKeyUri: false, resolvedFrom: "default" }, + }, + { + label: "DOTNS_MNEMONIC is used when set", + env: { mnemonic: CUSTOM_MNEMONIC }, + expected: { source: CUSTOM_MNEMONIC, isKeyUri: false, resolvedFrom: "env" }, + }, + { + label: "DOTNS_KEY_URI is used when mnemonic is absent", + env: { keyUri: ALICE_KEY_URI }, + expected: { source: ALICE_KEY_URI, isKeyUri: true, resolvedFrom: "env" }, + }, + { + label: "DOTNS_MNEMONIC takes precedence over DOTNS_KEY_URI", + env: { mnemonic: CUSTOM_MNEMONIC, keyUri: ALICE_KEY_URI }, + expected: { source: CUSTOM_MNEMONIC, isKeyUri: false, resolvedFrom: "env" }, + }, + { + label: "empty DOTNS_MNEMONIC falls through to default", + env: { mnemonic: "" }, + expected: { source: DEFAULT_MNEMONIC, isKeyUri: false, resolvedFrom: "default" }, + }, +]; + +describe("resolveAuthSourceReadOnly honours environment variables (regression for #114)", () => { + test.each(envCases)("$label", async ({ env, expected }) => { + clearAuthEnv(); + if (env.mnemonic !== undefined) process.env[ENV.MNEMONIC] = env.mnemonic; + if (env.keyUri !== undefined) process.env[ENV.KEY_URI] = env.keyUri; + + const resolved = await resolveAuthSourceReadOnly(); + + expect(resolved.source).toBe(expected.source); + expect(resolved.isKeyUri).toBe(expected.isKeyUri); + expect(resolved.resolvedFrom).toBe(expected.resolvedFrom); + }); +}); diff --git a/packages/cli/tests/unit/pop/setPopMapsAccount.test.ts b/packages/cli/tests/unit/pop/setPopMapsAccount.test.ts new file mode 100644 index 0000000..98f5b2a --- /dev/null +++ b/packages/cli/tests/unit/pop/setPopMapsAccount.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, test } from "bun:test"; +import { encodeFunctionResult } from "viem"; +import type { PolkadotSigner } from "polkadot-api"; +import { + getUserProofOfPersonhoodStatus, + setUserProofOfPersonhoodStatus, +} from "../../../src/commands/register"; +import { POP_RULES_ABI } from "../../../src/utils/constants"; +import { ProofOfPersonhoodStatus } from "../../../src/types/types"; +import type { ReviveClientWrapper } from "../../../src/client/polkadotClient"; + +type CallEvent = "ensureAccountMapped" | "performDryRunCall" | "submitTransaction"; + +function createTrackingClient(currentStatus: ProofOfPersonhoodStatus) { + const callOrder: CallEvent[] = []; + + const encodedStatus = encodeFunctionResult({ + abi: POP_RULES_ABI, + functionName: "userPopStatus", + result: BigInt(currentStatus) as any, + }); + + const client = { + async ensureAccountMapped() { + callOrder.push("ensureAccountMapped"); + }, + async performDryRunCall() { + callOrder.push("performDryRunCall"); + return { result: { value: { data: encodedStatus, flags: 0n } } }; + }, + async submitTransaction() { + callOrder.push("submitTransaction"); + return "0xdeadbeef" as const; + }, + } as unknown as ReviveClientWrapper; + + return { client, callOrder }; +} + +const substrateAddress = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; +const evmAddress = "0x000000000000000000000000000000000000dead" as const; +const placeholderSigner = {} as PolkadotSigner; + +describe("setUserProofOfPersonhoodStatus maps account before dry-run (regression for #89)", () => { + test("ensureAccountMapped runs before the PoP dry-run read", async () => { + const { client, callOrder } = createTrackingClient( + ProofOfPersonhoodStatus.ProofOfPersonhoodFull, + ); + + await setUserProofOfPersonhoodStatus( + client, + substrateAddress, + placeholderSigner, + evmAddress, + "", + ProofOfPersonhoodStatus.ProofOfPersonhoodFull, + ); + + const mapIndex = callOrder.indexOf("ensureAccountMapped"); + const dryRunIndex = callOrder.indexOf("performDryRunCall"); + + expect(mapIndex).toBeGreaterThanOrEqual(0); + expect(dryRunIndex).toBeGreaterThanOrEqual(0); + expect(mapIndex).toBeLessThan(dryRunIndex); + }); + + test("skips submitTransaction when current status equals desired", async () => { + const { client, callOrder } = createTrackingClient( + ProofOfPersonhoodStatus.ProofOfPersonhoodLite, + ); + + await setUserProofOfPersonhoodStatus( + client, + substrateAddress, + placeholderSigner, + evmAddress, + "", + ProofOfPersonhoodStatus.ProofOfPersonhoodLite, + ); + + expect(callOrder).not.toContain("submitTransaction"); + }); + + test("getUserProofOfPersonhoodStatus alone does not map the account", async () => { + const { client, callOrder } = createTrackingClient(ProofOfPersonhoodStatus.NoStatus); + + await getUserProofOfPersonhoodStatus(client, substrateAddress, evmAddress); + + expect(callOrder).toContain("performDryRunCall"); + expect(callOrder).not.toContain("ensureAccountMapped"); + }); +}); diff --git a/packages/cli/tests/unit/utils/formatting.test.ts b/packages/cli/tests/unit/utils/formatting.test.ts new file mode 100644 index 0000000..93adb00 --- /dev/null +++ b/packages/cli/tests/unit/utils/formatting.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { formatNativeBalance, parseNativeBalance } from "../../../src/utils/formatting"; +import { DECIMALS, DECIMALS_DOT } from "../../../src/utils/constants"; + +describe("native balance formatting uses DOT/PAS 10 decimals", () => { + test("DECIMALS_DOT is 10 (native DOT/PAS) and DECIMALS is 12 (Revive native)", () => { + expect(DECIMALS_DOT).toBe(10n); + expect(DECIMALS).toBe(12n); + }); + + test("formatNativeBalance renders 5000 PAS from 5000 * 10^10 units", () => { + const fiveThousandPasInSmallestUnits = 5000n * 10n ** 10n; + expect(formatNativeBalance(fiveThousandPasInSmallestUnits)).toBe("5000.0000000000"); + }); + + test("formatNativeBalance renders fractional 0.1 PAS as 10^9 units", () => { + expect(formatNativeBalance(10n ** 9n)).toBe("0.1000000000"); + }); + + test("formatNativeBalance renders zero balance", () => { + expect(formatNativeBalance(0n)).toBe("0.0000000000"); + }); + + test("parseNativeBalance inverts formatNativeBalance", () => { + const original = 5000n * 10n ** 10n + 1234567890n; + expect(parseNativeBalance(formatNativeBalance(original))).toBe(original); + }); + + test("parseNativeBalance('0.1') is 10^9", () => { + expect(parseNativeBalance("0.1")).toBe(10n ** 9n); + }); +}); From 8bb56b3c5e063cabfa08b6489585e2433609be9b Mon Sep 17 00:00:00 2001 From: Siphamandla Mjoli Date: Thu, 23 Apr 2026 16:53:08 +0200 Subject: [PATCH 2/2] fix: code owners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 269e8b0..08544f6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @filvecchiato @waytwotall @andrew-ifrita @corey-hathaway @sphamjoli +* @waytwotall @andrew-ifrita @sphamjoli