Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
acf0fc7
feat(sdk): add DotNsError for the DotNS registry surface
TarikGul Aug 10, 2026
453d23c
feat(sdk): add DotNS registry read/write surface (chain wiring pending)
TarikGul Aug 11, 2026
cc26ec1
refactor(sdk): replace DotNS throw-stubs with the registry surface
TarikGul Aug 11, 2026
53f6718
chore(changeset): DotNS registry surface (@parity/product-sdk minor)
TarikGul Aug 11, 2026
5529d42
feat(sdk): add DotNS namehash
TarikGul Aug 11, 2026
d624f52
feat(sdk): add DotNS resolution contract ABIs + Paseo AH addresses
TarikGul Aug 11, 2026
f360a43
feat(sdk): wire DotNS resolveDotNs / reverseDotNs to chain
TarikGul Aug 11, 2026
43f2649
chore(changeset): DotNS reads wired, writes pending
TarikGul Aug 11, 2026
7944907
feat(sdk): add DotNS write + pricing contract ABIs
TarikGul Aug 11, 2026
f3381cd
feat(sdk): wire DotNS writes (setRecord + registration)
TarikGul Aug 11, 2026
efc215e
chore(changeset): DotNS writes now wired
TarikGul Aug 11, 2026
02e154b
fix(sdk): correct the DotNS Paseo Asset Hub contract addresses
Imod7 Aug 18, 2026
fcc2f44
fix(sdk): resolve DotNS names through the registry's real resolver state
Imod7 Aug 18, 2026
7e57171
fix(sdk): dry-run DotNS writes as the caller, not the pallet-revive a…
Imod7 Aug 18, 2026
fc4ca0d
fix(sdk): defer and correctly price the DotNS register call
Imod7 Aug 18, 2026
6e8ca5e
fix(sdk): carry contract error causes, and resolve DotNS subnames
Imod7 Aug 18, 2026
3010aaa
chore(sdk): correct the DotNS changeset, docs and dead code
Imod7 Aug 18, 2026
e990576
fix(sdk): check availability before the DotNS commit, and report it h…
Imod7 Aug 18, 2026
7cefd95
merge main + resolve conflicts
Imod7 Aug 18, 2026
eab627b
refactor(sdk): parameterise DotNS namehash on the TLD
Imod7 Aug 19, 2026
14d7c11
refactor(sdk): require an explicit DotNS TLD suffix
Imod7 Aug 19, 2026
d990dc4
feat(sdk): read the DotNS TLD from the chain
Imod7 Aug 19, 2026
0e30a72
fix(sdk): hash DotNS names under the deployment's TLD
Imod7 Aug 19, 2026
ff5e90e
refactor(sdk): rename PASEO_ASSETHUB_DOTNS to DOTNS_ADDRESSES
Imod7 Aug 19, 2026
9b9afe7
fix(sdk): only ask the forward resolver for an address
Imod7 Aug 19, 2026
628b0a0
fix(sdk): report a malformed DotNS origin as an error, not a throw
Imod7 Aug 20, 2026
5ef3db0
docs(sdk): state that the DotNS write inputs are H160, not SS58
Imod7 Aug 20, 2026
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
43 changes: 43 additions & 0 deletions product-sdk/packages/sdk/src/identity/dotns-abis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2026 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
//
// Guards on the transcribed deployment table. These check the shape of the
// table, not that it names the live deployment: only a diff against
// `paritytech/dotns` `deployments/paseo-assethub/420420417.json` can do that,
// and it is a network fetch. See the comment on DOTNS_ADDRESSES for the
// commit the values were copied from, and re-check it when bumping.
import { isValidH160 } from "@parity/product-sdk-address";
import { describe, expect, test } from "vitest";
import { DOTNS_ADDRESSES } from "./dotns-abis.js";

describe("DOTNS_ADDRESSES", () => {
test("every entry is a well-formed H160", () => {
for (const [name, address] of Object.entries(DOTNS_ADDRESSES)) {
expect(isValidH160(address), `${name} = ${address}`).toBe(true);
}
});

test("no two entries share an address", () => {
// Six addresses transcribed by hand: a duplicated paste is the
// transcription error most likely to go unnoticed, because the wrong
// contract still answers and only the decode looks odd.
const entries = Object.entries(DOTNS_ADDRESSES);
const seen = new Map<string, string>();
for (const [name, address] of entries) {
const key = address.toLowerCase();
expect(seen.get(key), `${name} duplicates ${seen.get(key)}`).toBeUndefined();
seen.set(key, name);
}
expect(seen.size).toBe(entries.length);
});

test("the resolver and the reverse resolver are different contracts", () => {
// Load-bearing beyond general distinctness: resolveDotNs decides whether
// a node has a forward record by comparing the registry's resolver
// pointer against the reverse resolver, so collapsing these two would
// make every registered name look resolvable.
expect(DOTNS_ADDRESSES.resolver.toLowerCase()).not.toBe(
DOTNS_ADDRESSES.reverseResolver.toLowerCase(),
);
});
});
274 changes: 274 additions & 0 deletions product-sdk/packages/sdk/src/identity/dotns-abis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// Copyright 2026 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
/**
* Minimal ABIs for the DotNS contracts: the read and write methods this module
* calls, and nothing else. Sourced from `paritytech/dotns` (`contracts/registry`,
* `contracts/resolvers`, `contracts/registrars`, `contracts/pop`).
*/
import type { AbiEntry } from "@parity/product-sdk-contracts";

/**
* `DotnsRegistry` — node → resolver / owner, plus the owner-gated pointer write.
*
* `setResolver` is needed because registration parks `records[node].resolver` on
* the reverse resolver, so the pointer must be moved once before any address
* record is readable.
*/
export const DOTNS_REGISTRY_ABI: AbiEntry[] = [
{
type: "function",
name: "resolver",
inputs: [{ name: "node", type: "bytes32" }],
outputs: [{ name: "", type: "address" }],
stateMutability: "view",
},
{
type: "function",
name: "owner",
inputs: [{ name: "node", type: "bytes32" }],
outputs: [{ name: "", type: "address" }],
stateMutability: "view",
},
{
type: "function",
name: "setResolver",
inputs: [
{ name: "node", type: "bytes32" },
{ name: "resolverAddr", type: "address" },
],
outputs: [],
stateMutability: "nonpayable",
},
];

/** `DotnsResolver` — node → resolved address. */
export const DOTNS_RESOLVER_ABI: AbiEntry[] = [
{
type: "function",
name: "addressOf",
inputs: [{ name: "node", type: "bytes32" }],
outputs: [{ name: "value", type: "address" }],
stateMutability: "view",
},
];

/** `DotnsReverseResolver` — account → primary name. */
export const DOTNS_REVERSE_RESOLVER_ABI: AbiEntry[] = [
{
type: "function",
name: "nameOf",
inputs: [{ name: "addr", type: "address" }],
outputs: [{ name: "name", type: "string" }],
stateMutability: "view",
},
];

/** `DotnsResolver` write — set a node's resolved address (owner-gated). */
export const DOTNS_RESOLVER_WRITE_ABI: AbiEntry[] = [
{
type: "function",
name: "setAddress",
inputs: [
{ name: "node", type: "bytes32" },
{ name: "value", type: "address" },
],
outputs: [],
stateMutability: "nonpayable",
},
];

/**
* `DotnsRegistrarController` — the commit-reveal registration flow. `register`
* is payable; the value is the price from {@link DOTNS_POP_RULES_ABI}.
* `Registration` = (label, owner, secret, reserved).
*/
export const DOTNS_REGISTRAR_CONTROLLER_ABI: AbiEntry[] = [
{
type: "function",
name: "available",
inputs: [{ name: "label", type: "string" }],
outputs: [{ name: "isAvailable", type: "bool" }],
stateMutability: "view",
},
{
type: "function",
name: "minCommitmentAge",
inputs: [],
outputs: [{ name: "", type: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "maxCommitmentAge",
inputs: [],
outputs: [{ name: "", type: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "makeCommitment",
inputs: [
{
name: "registration",
type: "tuple",
components: [
{ name: "label", type: "string" },
{ name: "owner", type: "address" },
{ name: "secret", type: "bytes32" },
{ name: "reserved", type: "bool" },
],
},
],
outputs: [{ name: "commitment", type: "bytes32" }],
stateMutability: "pure",
},
{
type: "function",
name: "commit",
inputs: [{ name: "commitment", type: "bytes32" }],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "register",
inputs: [
{
name: "registration",
type: "tuple",
components: [
{ name: "label", type: "string" },
{ name: "owner", type: "address" },
{ name: "secret", type: "bytes32" },
{ name: "reserved", type: "bool" },
],
},
],
outputs: [],
stateMutability: "payable",
},
];

/**
* `PopRules` — registration pricing.
*
* `price` is the label-only cost and runs no eligibility rules, so it is not
* what `register` charges. `register` uses the owner-aware variants and adds
* `transferFloor` when the payer is not the owner:
* `max(priceWithoutCheck(label, owner).price, transferFloor(label, payer, owner))`.
*
* `PopStatus` encodes as `uint8`: 0 NoStatus, 1 PopLite, 2 PopFull, 3 Reserved.
*/
export const DOTNS_POP_RULES_ABI: AbiEntry[] = [
{
type: "function",
name: "price",
inputs: [{ name: "name", type: "string" }],
outputs: [{ name: "cost", type: "uint256" }],
stateMutability: "view",
},
{
type: "function",
name: "priceWithoutCheck",
inputs: [
{ name: "name", type: "string" },
{ name: "userAddress", type: "address" },
],
outputs: [
{
name: "metadata",
type: "tuple",
components: [
{ name: "price", type: "uint256" },
{ name: "status", type: "uint8" },
{ name: "userStatus", type: "uint8" },
{ name: "message", type: "string" },
],
},
],
stateMutability: "view",
},
{
type: "function",
name: "classifyName",
inputs: [{ name: "name", type: "string" }],
outputs: [
{ name: "requirement", type: "uint8" },
{ name: "message", type: "string" },
],
stateMutability: "pure",
},
{
type: "function",
name: "transferFloor",
inputs: [
{ name: "name", type: "string" },
{ name: "from", type: "address" },
{ name: "to", type: "address" },
],
outputs: [{ name: "floor", type: "uint256" }],
stateMutability: "view",
},
];

/**
* `IDotnsProtocolRegistry`, the two TLD getters.
*
* The TLD is per network: `initialize(tldLabel)` fixes it once at deployment
* and there is no setter, so these are immutable for the life of a deployment.
* `tld()` returns the suffix with its leading dot (`".paseo"`); `tldNode()`
* returns `namehash(0, labelhash(label))`, which is derivable from the suffix
* and so is read only as a cross-check.
*
* Deployments predating `dotns` `b4096968` have neither getter — the TLD was a
* compile-time constant there — and revert with an empty payload.
*/
export const DOTNS_PROTOCOL_REGISTRY_ABI: AbiEntry[] = [
{
type: "function",
name: "tld",
inputs: [],
outputs: [{ name: "suffix", type: "string" }],
stateMutability: "view",
},
{
type: "function",
name: "tldNode",
inputs: [],
outputs: [{ name: "node", type: "bytes32" }],
stateMutability: "view",
},
];

/** `IPopRules.PopStatus`. Ordering is meaningful: a user meets a tier when `userStatus >= status`. */
export const POP_STATUS = { NoStatus: 0, PopLite: 1, PopFull: 2, Reserved: 3 } as const;

/**
* The deployed DotNS addresses. The same on every network.
*
* Not a per-network table, despite what the old name (`PASEO_ASSETHUB_DOTNS`)
* implied: every DotNS network is deployed through the same CREATE3 factory, so
* the addresses are identical everywhere and **only the TLD differs**. Verified
* on both Paseo Asset Hub Next V2 and Previewnet — `protocolRegistry.get(...)`
* returns the same registry on each, and all six addresses hold contracts on
* both. `paritytech/dotns` `DEPLOYMENTS.md` states the mechanism, and
* `preview-net-v1` confirms it from the deploy side: the TLD is passed only as
* registry init calldata, so it moves no address.
*
* So extending this module to another network means resolving its TLD, which
* `resolveTld` already does — not adding a second address table.
*
* Each address answers `DotnsProtocolRegistry.get(bytes32)` under its
* `DotnsConstants` key (the name below, right-padded to 32 bytes) and has a
* `Revive.AccountInfoOf` entry. They are pinned rather than resolved at runtime,
* so re-verify after a redeploy; `protocolRegistry` is the address to walk from
* if we ever resolve the rest dynamically.
*/
export const DOTNS_ADDRESSES = {
registry: "0xf34054fd76BbF85f216cf9908226D5f0A72E50CA",
reverseResolver: "0xee3883d7eB60Ee9BCD7F3bcD8f2f05302A9Cc035",
resolver: "0xbd1165E549DF96F083c0A16f61590927bC187009",
registrarController: "0xBdaA01bD1bA67d709F2b1fF286Da0d854977EA30",
popRules: "0x747B456bE03aec0b42bd85C51513730FBD45DA31",
protocolRegistry: "0xD19e3D0C97CF501125a04A97405e3e6592fa846E",
} as const;
34 changes: 34 additions & 0 deletions product-sdk/packages/sdk/src/identity/dotns-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2026 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
import type { SdkError } from "@parity/product-sdk-errors";

/**
* Error on the `Err` channel of a DotNS registry read/write. Implements the
* cross-package {@link SdkError} marker so `isSdkError(e)` recognizes it.
*
* `reason` distinguishes the failure kind so callers can branch without string
* matching.
*/
export type DotNsErrorReason =
| "InvalidName" // name failed isValidDotNsName
| "InvalidTld" // the deployment reported a TLD we cannot use, or the caller supplied an inconsistent one
| "TldMismatch" // a well-formed name, but rooted at another deployment's TLD
| "MissingOrigin" // a write was requested without the submitting account
| "InvalidOrigin" // the submitting account was supplied, but is not a decodable SS58 address
| "NameUnavailable" // already registered
| "NameReserved" // governance-held, or a base stem held by another user
| "OwnerStatusInsufficient" // the owner lacks the personhood tier the label requires
| "RegistryCall" // the on-chain contract call failed
| "Decode"; // the contract returned something we couldn't decode

export class DotNsError extends Error implements SdkError {
readonly isSdkError = true as const;
readonly source = "dotns";
readonly reason: DotNsErrorReason;

constructor(reason: DotNsErrorReason, message: string, options?: ErrorOptions) {
super(message, options);
this.name = "DotNsError";
this.reason = reason;
}
}
Loading
Loading