@sorandomains/owner 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,10 +5,12 @@
5
5
  > [release status](https://docs.soran.domains/reference/release-status) for package and service availability.
6
6
 
7
7
 
8
- Version 0.8.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
8
+ Version 0.12.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
9
9
  and labels are validated before lowercase normalization; Unicode lookalikes are
10
10
  rejected. Writes continue to target the owning Registry/Registrar/Resolver. Universal
11
- Lookup is the read entry point in `@sorandomains/lookup` 0.10.0.
11
+ Lookup is the read entry point in `@sorandomains/lookup` 0.10.0 or later.
12
+
13
+ Releases older than 0.11.0 (and any built against Lookup 0.6.0 to 0.9.0) default to the retired 5 and 6 September 2026 testnet stacks; upgrade rather than pinning a retired Registry. See [Older versions and retired stacks](../../README.md#older-versions-and-retired-stacks).
12
14
 
13
15
  Ownership, issuer, holder and treasury inputs remain G/C account or contract
14
16
  addresses. Muxed M addresses are payment destinations only: issue the name to its
@@ -36,6 +38,30 @@ const batch = await owner.issueBatch("acme", rows);
36
38
  for (const o of batch.outcomes) if (!o.issued) console.warn(o.label, o.reason);
37
39
  ```
38
40
 
41
+ ## Network address policy (unreleased native extension)
42
+
43
+ ```ts
44
+ await owner.setChainPolicy("solo", ["bitcoin", "ethereum", "base", "xrp"]);
45
+ const enabled = await owner.chainPolicy("solo");
46
+ await owner.setChainPolicy("solo", []); // disable all generic network records
47
+ ```
48
+
49
+ The current namespace owner sets one live policy for every existing and future
50
+ holder. The default policy is empty. Disabled records are hidden and cannot be
51
+ updated, but remain stored for re-enablement or removal by their holder. Duplicate
52
+ or unknown IDs are rejected. Each write freshly verifies the native route/version
53
+ and current namespace owner before signing the exact policy transaction. The owner
54
+ chooses available networks; holders choose their own destinations.
55
+
56
+ All methods require `multichain_version() == 1`; the existing published deployment
57
+ presets are not upgraded by installing this SDK. The source change does not deploy
58
+ contracts or publish packages. See [network formats and integration rules](../multichain/README.md)
59
+ for all 14 supported mainnets, binary formats, and shared codec exports. XRP Ledger
60
+ `xrp` preserves full mainnet X-addresses and their destination tags; `xrpl-evm` is a
61
+ separate EVM network. Never strip an X-address tag. For Stellar use the dedicated
62
+ payment API.
63
+
64
+
39
65
  ## What's in the box
40
66
 
41
67
  | Operation | What it does |
@@ -91,7 +117,7 @@ Read the [native claim APIs, security boundaries and complete signup flow](https
91
117
 
92
118
  ## Verified testnet deployment
93
119
 
94
- See the [deployment manifest](../../deploy/testnet/deployment.json) for confirmed code hashes, transaction receipts and verification scope. Network passphrase: `Test SDF Network ; September 2015`.
120
+ See the [deployment manifest](../../deployments/testnet.json) for confirmed code hashes, transaction receipts and verification scope. Network passphrase: `Test SDF Network ; September 2015`.
95
121
 
96
122
  | Contract | Address |
97
123
  |---|---|
@@ -121,3 +147,42 @@ The older `maxNativeFeeStroops` option remains supported: when the new option is
121
147
  On a governed Registry, native verification reads the exact per-namespace Registrar code pin and its upgrade history. Approved upgrades do not have to match the current factory default. Missing or malformed provenance still prevents signing and receipt confirmation; RPC failure never downgrades verification to a legacy rule.
122
148
 
123
149
  Historical claim recovery is read-only and binds the original intent to the frozen source Registrar and sealed migration commitments. It never rewrites the intent to a successor Registry or treats an unavailable receipt as permission to submit again. Deployment migration and package publication are separate; check the release status for the active addresses.
150
+
151
+ ## Namespace contract updates
152
+
153
+ The current namespace owner can update each governed child contract separately:
154
+
155
+ ```ts
156
+ const status = await client.contractUpgradeStatus("acme", "resolver", approvedResolverHash);
157
+ // status: { owner, contractId, currentHash, approved, governed }
158
+ const direction = contractUpgradeDirection("resolver", status.currentHash, approvedResolverHash);
159
+ if (status.approved && status.governed && (direction === "forward" || direction === "unreviewed-target")) {
160
+ await client.upgradeResolver("acme", approvedResolverHash, {
161
+ onPrepared: ({ hash, feeStroops }) => savePendingUpdate({ hash, feeStroops }),
162
+ });
163
+ }
164
+ await client.upgradeRegistrar("acme", approvedRegistrarHash);
165
+ ```
166
+
167
+ Registry approval says a hash may be installed on any namespace; it does not say the move is safe. Governance keeps superseded releases approved, so the chain itself accepts a downgrade (and a reinstall of the installed hash). The SDK therefore refuses, before preparing or signing anything: a reinstall of the installed hash, always; a **superseded** release (`CONTRACT_RELEASES[role].superseded`, a downgrade that drops later fixes); and the latest known release over installed code the SDK cannot place (it may be newer). `contractUpgradeDirection(role, currentHash, targetHash)` returns `"forward"`, `"same"`, `"backward"`, `"unrecognized-current"` or `"unreviewed-target"` (a release newer than this SDK, whose direction it cannot judge: only a reviewed hash from a trusted release record should reach it). A deliberate rollback passes `{ allowDowngrade: true }` to `upgradeRegistrar`/`upgradeResolver` after reviewing the older release for that namespace; it never overrides a reinstall. Keep an application-side allow-list of reviewed `from -> to` pairs, as the Soran console does; `CONTRACT_RELEASES` is a tested mirror of that catalog and lags any release newer than the SDK.
168
+
169
+ Use exact lowercase 32-byte hex hashes from a reviewed release. Registry governance must approve the implementation for its Registrar or Resolver role first. Approval permits an update; it does not upgrade existing namespace contracts. Each owner-signed transaction changes code at the existing contract address, executes immediately, and pays its own simulated network fee. Storage compatibility is a property of the selected release, not a guarantee for every approved implementation.
170
+
171
+ `contractUpgradeStatus` is read-only and checks both actual executables against the Registry's per-namespace code pins, clean attestations, Registrar anchors, and Resolver provenance/authority. It does not depend on claim configuration, claim-interface version or fee-token settings. A verified unsupported policy or revoked target is reported through `governed`/`approved`; missing or inconsistent evidence throws. This review is a snapshot: the write rechecks authorization and simulation before signing, and the contracts enforce current approval and ownership on chain.
172
+
173
+ Both update methods require the current owner's account as the transaction source and use the native transaction's exact operation, authorization-tree and fee checks. They never fall back to the legacy tainting upgrade path or restore archived storage automatically. Active migrations and frozen exports must be resolved before updating. If submission is interrupted, reconcile the error's original `txHash` before retrying. Signature quorum is validated by the network; contract-wallet owner authorization is not supported by these account-source helpers.
174
+
175
+ ### Namespace-wide child-name policy
176
+
177
+ On a Registrar with `subname_version() == 1`, the current namespace owner can
178
+ call `setSubnamePolicy(namespace, policy)` with `enabled`, `creation-disabled`
179
+ or `suspended`. The default is `creation-disabled`. Changes apply to all parent
180
+ holders, including usernames claimed before the change. `creation-disabled`
181
+ blocks new children while existing ones still work; `suspended` blocks child
182
+ resolution and record edits, preserves records, and permits their removal.
183
+
184
+ `subnamePolicy`, `subnameRecord` and `subnames(parent, { offset, limit })` provide
185
+ strict on-chain reads. Raw child records and pages include inactive/stale rows;
186
+ they do not establish payment readiness. The parent holder, not a separate
187
+ child wallet, controls `mail.fred.solo`, and its expiry and ownership generation
188
+ are tied to `fred.solo`. Child destinations may differ from the parent holder.
@@ -1,6 +1,6 @@
1
1
  import { Address, FeeBumpTransaction, Networks, Transaction, TransactionBuilder, xdr, hash, } from "@stellar/stellar-sdk";
2
2
  import { hex } from "./native-codec.js";
3
- import { FundingError, sponsorQuoteToScVal, } from "./funding-types.js";
3
+ import { FundingError, readBoundedText, sponsorQuoteToScVal, } from "./funding-types.js";
4
4
  /** Input must come from the caller's trusted chain provider, never a pasted/API receipt. */
5
5
  export function verifySponsorHistory(value, quote, expectedHash, latestLedger, passphrase, provider = "sdf-testnet-horizon") {
6
6
  const r = value;
@@ -116,8 +116,8 @@ export class TestnetSponsorHistory {
116
116
  return null;
117
117
  if (!response.ok)
118
118
  throw new FundingError("Sponsorship history unavailable; preserve the quote reference", "unavailable");
119
- const body = await response.text();
120
- if (body.length > 600000)
119
+ const body = await readBoundedText(response, 600000);
120
+ if (body === null)
121
121
  throw new FundingError("Sponsorship history response is too large", "unavailable");
122
122
  return verifySponsorHistory(JSON.parse(body), quote, hash, latestLedger, passphrase);
123
123
  }
@@ -32,6 +32,10 @@ export class FundingReader {
32
32
  .setTimeout(30)
33
33
  .build();
34
34
  const result = await this.server.simulateTransaction(tx, undefined, undefined, false);
35
+ // A simulation that needs a restore preamble still reports success, with the
36
+ // value computed as if the archived entry were live; it is not a live read.
37
+ if (rpc.Api.isSimulationRestore(result))
38
+ throw new FundingError("Cannot verify funding contract state: an archived entry needs restoring", "unavailable");
35
39
  if (!rpc.Api.isSimulationSuccess(result) ||
36
40
  !result.result ||
37
41
  !Number.isSafeInteger(result.latestLedger) ||
@@ -1,6 +1,8 @@
1
1
  import { TestnetSponsorHistory, sponsorRpcReceipt, } from "./funding-history.js";
2
2
  import { claimIntentToScVal } from "./native-types.js";
3
- import { FundingError, decodeSponsorQuote, sponsoredIdentityToScVal, sponsorIntentHash, } from "./funding-types.js";
3
+ import { FundingError, decodeSponsorQuote, readBoundedText, sponsoredIdentityToScVal, sponsorIntentHash, } from "./funding-types.js";
4
+ /** An offer carries at most a 100000 character transaction plus the quote XDR; nothing legitimate approaches this. */
5
+ const MAX_SERVICE_BODY_BYTES = 262_144;
4
6
  /** HTTP transports offers and status. The contract/RPC checks still authorize spending. */
5
7
  export class FundingServiceClient {
6
8
  client;
@@ -39,8 +41,18 @@ export class FundingServiceClient {
39
41
  throw new FundingError("Service response unavailable. Keep your quote reference and recover its status before submitting again.", "unavailable");
40
42
  }
41
43
  let value;
44
+ let text;
42
45
  try {
43
- value = (await r.json());
46
+ text = await readBoundedText(r, MAX_SERVICE_BODY_BYTES);
47
+ }
48
+ catch {
49
+ text = "";
50
+ }
51
+ // Too large is handled like an unreadable body: a submit's outcome is unknown.
52
+ if (text === null)
53
+ throw new FundingError("Service response was too large. Keep your quote reference and recover its status before submitting again.", path === "/v1/sponsorship/submit" ? "pending" : "unavailable");
54
+ try {
55
+ value = JSON.parse(text);
44
56
  }
45
57
  catch {
46
58
  // A proxy/gateway error page (e.g. a 502 HTML body) is not JSON. A
@@ -1,6 +1,8 @@
1
1
  import { xdr } from "@stellar/stellar-sdk";
2
2
  import { type PaymentDestination } from "./payment.js";
3
3
  import { type ClaimIntent } from "./native-types.js";
4
+ /** Read a response body as text, refusing more than `limit` bytes, declared or streamed, so a hostile or broken endpoint cannot make the client buffer it all. Returns null over the limit. */
5
+ export declare function readBoundedText(response: Response, limit: number): Promise<string | null>;
4
6
  export declare class FundingError extends Error {
5
7
  readonly kind: "invalid" | "unavailable" | "authorization" | "pending" | "failed";
6
8
  readonly transactionHash: string | null;
@@ -2,6 +2,36 @@ import { hash, nativeToScVal, scValToNative, xdr } from "@stellar/stellar-sdk";
2
2
  import { address, amount, bool, bytes32, exactObject, hex, hex32, label, paymentDestinationToScVal, sc, struct, u32, u64, unhex, } from "./native-codec.js";
3
3
  import { destinationFromNative } from "./payment.js";
4
4
  import { claimIntentToScVal } from "./native-types.js";
5
+ /** Read a response body as text, refusing more than `limit` bytes, declared or streamed, so a hostile or broken endpoint cannot make the client buffer it all. Returns null over the limit. */
6
+ export async function readBoundedText(response, limit) {
7
+ if (Number(response.headers.get("content-length") ?? 0) > limit) {
8
+ await response.body?.cancel().catch(() => undefined);
9
+ return null;
10
+ }
11
+ const reader = response.body?.getReader();
12
+ if (!reader)
13
+ return "";
14
+ const chunks = [];
15
+ let total = 0;
16
+ for (;;) {
17
+ const { done, value } = await reader.read();
18
+ if (done)
19
+ break;
20
+ total += value.byteLength;
21
+ if (total > limit) {
22
+ await reader.cancel().catch(() => undefined);
23
+ return null;
24
+ }
25
+ chunks.push(value);
26
+ }
27
+ const bytes = new Uint8Array(total);
28
+ let offset = 0;
29
+ for (const chunk of chunks) {
30
+ bytes.set(chunk, offset);
31
+ offset += chunk.byteLength;
32
+ }
33
+ return new TextDecoder().decode(bytes);
34
+ }
5
35
  export class FundingError extends Error {
6
36
  kind;
7
37
  transactionHash;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,50 @@
1
+ import { type SubnamePolicy, type SubnameRecord } from "./names.js";
2
+ export { type SubnamePolicy, type SubnameRecord } from "./names.js";
1
3
  import { type HistoricalRecoveryOptions } from "./native-history.js";
2
4
  import type { ClaimIntent } from "./native-types.js";
3
5
  export type { HistoricalRecoveryOptions } from "./native-history.js";
6
+ /**
7
+ * @sorandomains/owner — the write-side SDK for Soran namespace owners.
8
+ *
9
+ * Everything a namespace owner does after winning a namespace, programmatically:
10
+ *
11
+ * import { SoranOwner, keypairSigner } from "@sorandomains/owner";
12
+ *
13
+ * const owner = new SoranOwner({ signer: keypairSigner(process.env.OWNER_SECRET!) });
14
+ * await owner.issue("acme", "alice", "GDHN…"); // alice.acme now resolves
15
+ * await owner.issueBatch("acme", csvRows); // up to 23 per transaction
16
+ *
17
+ * TRUST MODEL. This SDK talks to the contracts directly over any Soroban RPC
18
+ * node — no Soran account, no hosted API, no third party in the path. Every
19
+ * operation here is authorized on chain: the Registrar and Registry check
20
+ * `require_auth` against the namespace owner's address, so the only credential
21
+ * that matters is the signer you supply. Soran's servers cannot perform any of
22
+ * these operations for you or against you.
23
+ *
24
+ * WHO CAN CALL WHAT. All operations below are namespace-OWNER powers except
25
+ * `acceptNamespaceTransfer`, which the proposed NEW owner signs. Name-holder
26
+ * powers (transferring an individual name, pointing it at a new address,
27
+ * electing a primary name) are the holder's alone — they live in the contracts'
28
+ * holder-authorized entry points and live in `@sorandomains/holder`; this
29
+ * package deliberately cannot exercise them.
30
+ *
31
+ * SIGNING. Supply a `TxSigner`: `keypairSigner(secret)` for backends, or wrap a
32
+ * browser wallet (Freighter / Stellar Wallets Kit expose exactly this
33
+ * `signTransaction(xdr, { networkPassphrase })` shape). The SDK builds and
34
+ * simulates each transaction, hands you the envelope to sign, and submits it.
35
+ * Operations on one SoranOwner instance are serialized so concurrent calls
36
+ * cannot race the account sequence number.
37
+ *
38
+ * READS. For resolution, reverse lookup, and assurance checks use the read-only
39
+ * companion package `@sorandomains/lookup` — wallets and apps should depend on
40
+ * that one only. The few reads here (policy, pending transfers, records) exist
41
+ * to support write flows.
42
+ */
43
+ import { type ChainNetwork } from "./multichain.js";
44
+ export { CHAIN_NETWORKS, ChainAddressError, chainNetwork, chainPolicyFromNative, chainPolicyToNative, encodeChainAddress, decodeChainAddress, type ChainNetwork, type ChainAddressErrorCode } from "./multichain.js";
4
45
  import { type NativeWriteOptions, type NativeCapability } from "./native-transport.js";
46
+ import { type ContractUpgradeOptions, type ContractUpgradeRole, type ContractUpgradeStatus } from "./native-upgrades.js";
47
+ export { CONTRACT_RELEASES, contractUpgradeDirection, type ContractUpgradeDirection, type ContractUpgradeOptions, type ContractUpgradeRole, type ContractUpgradeStatus } from "./native-upgrades.js";
5
48
  import { type ClaimSettings, type ClaimConfig, type ClaimUsage, type ApprovalUsage } from "./native-types.js";
6
49
  export * from "./native-types.js";
7
50
  export * from "./native-allowlist.js";
@@ -160,8 +203,13 @@ export declare class SoranOwner {
160
203
  private nativeOwnerWrite;
161
204
  /** Current implementation after validating the Registry's native binding. */
162
205
  registrarCodeHash(namespace: string): Promise<string>;
163
- /** Owner-authorized, Registry-approved update; never uses the legacy tainting path. */
164
- upgradeRegistrar(namespace: string, wasmHash: string, options?: NativeWriteOptions): Promise<Submitted>;
206
+ /** Verify both children and the selected implementation without signing or reading claim settings. */
207
+ contractUpgradeStatus(namespace: string, role: ContractUpgradeRole, wasmHash: string): Promise<ContractUpgradeStatus>;
208
+ private upgradeContract;
209
+ /** Current-owner, Registry-approved update; never uses the legacy tainting path. */
210
+ upgradeRegistrar(namespace: string, wasmHash: string, options?: ContractUpgradeOptions): Promise<Submitted>;
211
+ /** Current-owner, Registry-approved Resolver update with exact native authorization. */
212
+ upgradeResolver(namespace: string, wasmHash: string, options?: ContractUpgradeOptions): Promise<Submitted>;
165
213
  configureClaims(namespace: string, settings: ClaimSettings, options?: NativeWriteOptions): Promise<Submitted & {
166
214
  config: ClaimConfig;
167
215
  }>;
@@ -246,6 +294,12 @@ export declare class SoranOwner {
246
294
  * Refused (ResolverFrozen) once the namespace is permanent.
247
295
  */
248
296
  setResolver(namespace: string, resolver: string | null): Promise<Submitted>;
297
+ /** Networks currently enabled for all holders, including names already claimed. */
298
+ chainPolicy(namespace: string): Promise<ChainNetwork[]>;
299
+ /** Set the namespace-wide network allowlist. Empty disables all multichain records.
300
+ * Disabled destinations remain stored and become visible if re-enabled. */
301
+ setChainPolicy(namespace: string, networks: readonly ChainNetwork[]): Promise<Submitted>;
302
+ private multichainResolverOf;
249
303
  /** The namespace's immutable issuance policy. */
250
304
  policy(namespace: string): Promise<NamespacePolicy>;
251
305
  /** Has the namespace passed the one-way door? */
@@ -254,6 +308,30 @@ export declare class SoranOwner {
254
308
  nameState(namespace: string, label: string): Promise<NameState | null>;
255
309
  /** The pending namespace transfer on the Registry, or null. */
256
310
  pendingNamespaceTransfer(namespace: string): Promise<unknown | null>;
311
+ private subnameReader;
312
+ /** Live namespace policy. Creation-disabled keeps existing children usable; suspended hides them. */
313
+ subnamePolicy(namespace: string): Promise<SubnamePolicy>;
314
+ /** Raw child state, including tombstones and stale parent generations; this is not payment resolution. */
315
+ subnameRecord(name: string): Promise<SubnameRecord | null>;
316
+ /** Bounded on-chain historical labels, including removed children. Re-read metadata before treating a child as active. */
317
+ subnames(parent: string, options?: {
318
+ offset?: number;
319
+ limit?: number;
320
+ }): Promise<{
321
+ parent: string;
322
+ policy: SubnamePolicy;
323
+ records: {
324
+ parentNode: Uint8Array;
325
+ parentGeneration: bigint;
326
+ generation: bigint;
327
+ address: string;
328
+ active: boolean;
329
+ name: string;
330
+ }[];
331
+ offset: number;
332
+ nextOffset: number | null;
333
+ }>;
334
+ setSubnamePolicy(namespace: string, policy: SubnamePolicy): Promise<Submitted>;
257
335
  private readRecord;
258
336
  private readRecords;
259
337
  /** Simulation-only contract read — no signature, no fee, no state change. */
@@ -274,6 +352,8 @@ export declare class SoranOwner {
274
352
  * auth demands an address the envelope signature cannot satisfy.
275
353
  */
276
354
  private assertSatisfiableAuth;
355
+ /** Authorize exactly the selected policy with no additional child operations. */
356
+ private assertChainPolicyIntent;
277
357
  private attempt;
278
358
  private restore;
279
359
  private confirm;
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { subnamePolicyToNative } from "./names.js";
2
+ import { createSubnameReader, SUBNAME_REGISTRAR_ERRORS } from "./subnames-client.js";
1
3
  import { recoverFrozenClaim, historicalReadLedger } from "./native-history.js";
2
4
  /**
3
5
  * @sorandomains/owner — the write-side SDK for Soran namespace owners.
@@ -36,6 +38,8 @@ import { recoverFrozenClaim, historicalReadLedger } from "./native-history.js";
36
38
  * that one only. The few reads here (policy, pending transfers, records) exist
37
39
  * to support write flows.
38
40
  */
41
+ import { chainPolicyFromNative, chainPolicyToNative } from "./multichain.js";
42
+ export { CHAIN_NETWORKS, ChainAddressError, chainNetwork, chainPolicyFromNative, chainPolicyToNative, encodeChainAddress, decodeChainAddress } from "./multichain.js";
39
43
  import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, SorobanDataBuilder, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, xdr, } from "@stellar/stellar-sdk";
40
44
  /** Contract events from a transaction's meta, across meta versions (v3 puts
41
45
  * them on sorobanMeta; v4 nests them per-operation and in tx events). */
@@ -87,6 +91,8 @@ function decodeIssuedEvents(events, registrarId) {
87
91
  }
88
92
  import { NativeClaimError, address as nativeAddress, bool as nativeBool, bytes32 as nativeBytes32, hex as nativeHex, sc as nativeSc } from "./native-codec.js";
89
93
  import { requireNative, nativeCapability, sendNative } from "./native-transport.js";
94
+ import { contractUpgradeStatus as readContractUpgradeStatus, contractUpgradeDirection, upgradeHash } from "./native-upgrades.js";
95
+ export { CONTRACT_RELEASES, contractUpgradeDirection } from "./native-upgrades.js";
90
96
  import { claimSettingsToScVal, claimConfigFromNative, claimLabelToScVal, claimUsageFromNative, approvalUsageFromNative } from "./native-types.js";
91
97
  export * from "./native-types.js";
92
98
  export * from "./native-allowlist.js";
@@ -183,6 +189,15 @@ const REGISTRAR_ERRORS = {
183
189
  49: "MigrationUnavailable",
184
190
  50: "MigrationUnsupported",
185
191
  51: "PermanentLockDisabled",
192
+ 52: "InvalidRenewalGrace",
193
+ 53: "SubnameCreationDisabled", 54: "SubnamesSuspended",
194
+ 55: "SubnameGenerationMismatch", 56: "SubnameIndexUnavailable",
195
+ };
196
+ /** Error names shared by native multichain Resolver reads and writes. */
197
+ const CHAIN_RESOLVER_ERRORS = {
198
+ 2: "NotInitialized", 7: "InvalidAuthority", 33: "UnsupportedCoinType",
199
+ 34: "ChainDisabled", 35: "InvalidChainAddress", 36: "InvalidChainPolicy",
200
+ 37: "ChainContextMismatch", 38: "ChainUnavailable",
186
201
  };
187
202
  /** Registry contract error codes, by number. */
188
203
  const REGISTRY_ERRORS = {
@@ -227,6 +242,7 @@ const REGISTRY_ERRORS = {
227
242
  39: "UpgradePolicyMismatch",
228
243
  40: "UpgradeNotApproved",
229
244
  41: "PermanentLockDisabled",
245
+ 42: "BatchTooLarge",
230
246
  };
231
247
  /** Errors for a submit the network REJECTED as a stale sequence: nothing was included, so one retry with a fresh sequence is safe. */
232
248
  const BAD_SEQ_REJECTED = new WeakSet();
@@ -428,23 +444,46 @@ export class SoranOwner {
428
444
  throw new NativeClaimError("Registrar is not a Wasm contract", "unavailable");
429
445
  return nativeHex(instance.executable.wasmHash.value);
430
446
  }
431
- /** Owner-authorized, Registry-approved update; never uses the legacy tainting path. */
432
- async upgradeRegistrar(namespace, wasmHash, options = {}) {
433
- if (!/^[a-f0-9]{64}$/.test(wasmHash))
434
- throw new NativeClaimError("Registrar implementation hash must be lowercase hex32");
435
- const bytes = Uint8Array.from(wasmHash.match(/../g), value => parseInt(value, 16));
436
- const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
437
- const [governed, approved, current] = await Promise.all([
438
- this.read(cap.registrar, "upgrade_policy_version", []),
439
- this.read(this.registryId, "implementation_approved", [nativeSc.u32(0), nativeSc.bytes(bytes)]),
440
- this.registrarCodeHash(namespace),
441
- ]);
442
- if (governed !== 1 || approved !== true)
443
- throw new NativeClaimError("This Registrar upgrade is not approved by the Registry", "authorization");
444
- if (current === wasmHash)
445
- throw new NativeClaimError("This Registrar implementation is already installed");
446
- const result = await this.nativeOwnerWrite(namespace, "upgrade", [nativeSc.bytes(bytes)], options);
447
- return { hash: result.hash, ledger: result.ledger };
447
+ /** Verify both children and the selected implementation without signing or reading claim settings. */
448
+ contractUpgradeStatus(namespace, role, wasmHash) {
449
+ return readContractUpgradeStatus(this.nativeContext(), normalizeLabel(namespace), role, wasmHash);
450
+ }
451
+ upgradeContract(namespace, role, wasmHash, options) {
452
+ return this.serialize(async () => {
453
+ const args = [upgradeHash(wasmHash)];
454
+ const context = this.nativeContext();
455
+ const status = await readContractUpgradeStatus(context, normalizeLabel(namespace), role, wasmHash);
456
+ const source = nativeAddress(await this.signer.publicKey(), "account", "owner transaction source");
457
+ if (source !== status.owner)
458
+ throw new NativeClaimError("the current namespace owner must authorize this change", "authorization");
459
+ if (!status.governed || !status.approved)
460
+ throw new NativeClaimError("This contract upgrade is not approved by the Registry", "authorization");
461
+ const direction = contractUpgradeDirection(role, status.currentHash, wasmHash);
462
+ if (direction === "same")
463
+ throw new NativeClaimError("This contract implementation is already installed");
464
+ // Registry approval says a hash may be installed, not that the move is safe:
465
+ // older releases stay approved, and a downgrade drops their fixes.
466
+ if (options.allowDowngrade !== true) {
467
+ if (direction === "backward")
468
+ throw new NativeClaimError("Refusing to install a superseded implementation (a downgrade that drops later fixes). Install the latest reviewed release, or pass allowDowngrade: true only after reviewing the older release for this namespace.", "authorization");
469
+ if (direction === "unrecognized-current")
470
+ throw new NativeClaimError("This namespace runs an implementation this SDK does not recognise, so installing the latest known release may be a downgrade. Update the SDK, or pass allowDowngrade: true after verifying the installed release is older.", "authorization");
471
+ }
472
+ const result = await sendNative(context, {
473
+ source, contract: status.contractId, method: "upgrade", args,
474
+ sourceInvocation: { contract: status.contractId, method: "upgrade", args },
475
+ maxFeeStroops: context.maxFeeStroops,
476
+ }, options);
477
+ return { hash: result.hash, ledger: result.ledger };
478
+ });
479
+ }
480
+ /** Current-owner, Registry-approved update; never uses the legacy tainting path. */
481
+ upgradeRegistrar(namespace, wasmHash, options = {}) {
482
+ return this.upgradeContract(namespace, "registrar", wasmHash, options);
483
+ }
484
+ /** Current-owner, Registry-approved Resolver update with exact native authorization. */
485
+ upgradeResolver(namespace, wasmHash, options = {}) {
486
+ return this.upgradeContract(namespace, "resolver", wasmHash, options);
448
487
  }
449
488
  async configureClaims(namespace, settings, options = {}) {
450
489
  const encoded = claimSettingsToScVal(settings);
@@ -700,6 +739,44 @@ export class SoranOwner {
700
739
  const r = await this.invoke(this.registryId, "set_resolver", [nodeArg(namehash(namespace)), resolver === null ? xdr.ScVal.scvVoid() : addrArg(resolver)], REGISTRY_ERRORS);
701
740
  return { hash: r.hash, ledger: r.ledger };
702
741
  }
742
+ /** Networks currently enabled for all holders, including names already claimed. */
743
+ async chainPolicy(namespace) {
744
+ namespace = normalizeLabel(namespace);
745
+ const resolver = await this.multichainResolverOf(namespace);
746
+ return chainPolicyFromNative(await this.read(resolver, "chain_policy", []));
747
+ }
748
+ /** Set the namespace-wide network allowlist. Empty disables all multichain records.
749
+ * Disabled destinations remain stored and become visible if re-enabled. */
750
+ async setChainPolicy(namespace, networks) {
751
+ namespace = normalizeLabel(namespace);
752
+ const coins = chainPolicyToNative(networks);
753
+ const resolver = await this.multichainResolverOf(namespace);
754
+ await this.assertOwner(namespace);
755
+ const result = await this.invoke(resolver, "set_chain_policy", [
756
+ xdr.ScVal.scvVec(coins.map(coin => nativeToScVal(coin, { type: "u32" }))),
757
+ ], CHAIN_RESOLVER_ERRORS);
758
+ return { hash: result.hash, ledger: result.ledger };
759
+ }
760
+ async multichainResolverOf(namespace) {
761
+ const nsNode = namehash(namespace);
762
+ const args = [nodeArg(nsNode)];
763
+ const [resolver, registrar] = await Promise.all([
764
+ this.read(this.registryId, "resolver_of", args), this.read(this.registryId, "registrar_of", args),
765
+ ]);
766
+ if (typeof resolver !== "string" || !StrKey.isValidContract(resolver) || typeof registrar !== "string" || !StrKey.isValidContract(registrar))
767
+ throw new OwnerError("namespace has no native multichain route", this.registryId, "resolver_of", null, "UnsupportedImplementation");
768
+ const [anchor, authority, version, anchors] = await Promise.all([
769
+ this.read(resolver, "registry", []), this.read(resolver, "authority", []),
770
+ this.read(resolver, "multichain_version", []), this.read(registrar, "anchors", []),
771
+ ]);
772
+ if (anchor !== this.registryId || authority !== registrar || !Array.isArray(anchors) || anchors.length !== 2 ||
773
+ anchors[0] !== this.registryId || !(anchors[1] instanceof Uint8Array) || anchors[1].length !== 32 ||
774
+ !nsNode.every((byte, index) => byte === anchors[1][index]))
775
+ throw new OwnerError("multichain Resolver route does not match this Registry and namespace", resolver, "registry", 37, "ChainContextMismatch");
776
+ if (version !== 1)
777
+ throw new OwnerError("unsupported multichain Resolver version", resolver, "multichain_version", null, "UnsupportedImplementation");
778
+ return resolver;
779
+ }
703
780
  // ---- reads that support write flows --------------------------------------
704
781
  /** The namespace's immutable issuance policy. */
705
782
  async policy(namespace) {
@@ -730,6 +807,20 @@ export class SoranOwner {
730
807
  return this.read(this.registryId, "pending_transfer", [nodeArg(namehash(namespace))]);
731
808
  }
732
809
  // ---- internals -----------------------------------------------------------
810
+ subnameReader() { return createSubnameReader({ registryId: this.registryId, error: message => new OwnerError(message), read: (id, fn, args) => this.read(id, fn, args) }); }
811
+ /** Live namespace policy. Creation-disabled keeps existing children usable; suspended hides them. */
812
+ async subnamePolicy(namespace) { return this.subnameReader().policy(namespace); }
813
+ /** Raw child state, including tombstones and stale parent generations; this is not payment resolution. */
814
+ async subnameRecord(name) { return this.subnameReader().record(name); }
815
+ /** Bounded on-chain historical labels, including removed children. Re-read metadata before treating a child as active. */
816
+ async subnames(parent, options = {}) { return this.subnameReader().list(parent, options); }
817
+ async setSubnamePolicy(namespace, policy) {
818
+ const native = subnamePolicyToNative(policy);
819
+ const ctx = await this.subnameReader().context(namespace);
820
+ await this.assertOwner(ctx.namespace);
821
+ const result = await this.invoke(ctx.registrar, "set_subname_policy", [xdr.ScVal.scvVec([nativeToScVal(native[0], { type: "symbol" })])], { ...REGISTRAR_ERRORS, ...SUBNAME_REGISTRAR_ERRORS });
822
+ return { hash: result.hash, ledger: result.ledger };
823
+ }
733
824
  async readRecord(registrarId, label) {
734
825
  const rec = (await this.read(registrarId, "record_of", [labelArg(label)]));
735
826
  if (!rec)
@@ -762,7 +853,8 @@ export class SoranOwner {
762
853
  .build();
763
854
  const sim = await this.server.simulateTransaction(tx);
764
855
  if (rpc.Api.isSimulationError(sim)) {
765
- throw typedError(contractId, fn, sim.error, {});
856
+ const errors = fn.startsWith("subname_") ? SUBNAME_REGISTRAR_ERRORS : ["chain_policy", "chain_address", "multichain_version"].includes(fn) ? CHAIN_RESOLVER_ERRORS : {};
857
+ throw typedError(contractId, fn, sim.error, errors);
766
858
  }
767
859
  // An archived entry is NOT absence: reads cannot restore (no signer),
768
860
  // so surface the state honestly instead of reporting "does not exist".
@@ -838,6 +930,26 @@ export class SoranOwner {
838
930
  : `${fn}: this operation must be authorized by ${required}, but the signer is ${pub} — use that account's signer`, contractId, fn);
839
931
  }
840
932
  }
933
+ /** Authorize exactly the selected policy with no additional child operations. */
934
+ assertChainPolicyIntent(prepared, pub, contractId, fn, args) {
935
+ if (!["set_chain_policy", "set_subname_policy"].includes(fn))
936
+ return;
937
+ if (prepared.source !== pub || prepared.operations.length !== 1)
938
+ throw new OwnerError("unexpected chain policy transaction", contractId, fn);
939
+ const op = prepared.operations[0];
940
+ if (op.type !== "invokeHostFunction" || (op.source !== undefined && op.source !== pub) || op.func.type !== "hostFunctionTypeInvokeContract")
941
+ throw new OwnerError("unexpected chain policy operation", contractId, fn);
942
+ const call = op.func.invokeContract;
943
+ if (Address.fromScAddress(call.contractAddress).toString() !== contractId || call.functionName.toString() !== fn ||
944
+ call.args.length !== args.length || call.args.some((value, i) => value.toXDR("base64") !== args[i].toXDR("base64")))
945
+ throw new OwnerError("chain policy operation differs from selected intent", contractId, fn);
946
+ if (!op.auth || op.auth.length !== 1 || op.auth[0].credentials.type !== "sorobanCredentialsSourceAccount")
947
+ throw new OwnerError("chain policy requires exactly the source owner authorization", contractId, fn);
948
+ const root = op.auth[0].rootInvocation;
949
+ if (root.function.type !== "sorobanAuthorizedFunctionTypeContractFn" || root.subInvocations.length ||
950
+ root.function.contractFn.toXDR("base64") !== call.toXDR("base64"))
951
+ throw new OwnerError("chain policy authorization differs from selected intent", contractId, fn);
952
+ }
841
953
  async attempt(contractId, fn, args, errNames) {
842
954
  const pub = await this.signer.publicKey();
843
955
  const contract = new Contract(contractId);
@@ -868,6 +980,7 @@ export class SoranOwner {
868
980
  const prepared = rpc.assembleTransaction(tx, sim).build();
869
981
  this.assertNetworkFee(prepared.fee, fn);
870
982
  this.assertSatisfiableAuth(prepared, pub, contractId, fn);
983
+ this.assertChainPolicyIntent(prepared, pub, contractId, fn, args);
871
984
  // The hash is fixed before signatures — compute it now so every failure
872
985
  // past this point can carry it (the "re-check before retrying" contract).
873
986
  const txHash = toHex(prepared.hash()); // (SDK17) hash() is Uint8Array