@sorandomains/owner 0.11.0 → 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
  |---|---|
@@ -129,7 +155,8 @@ The current namespace owner can update each governed child contract separately:
129
155
  ```ts
130
156
  const status = await client.contractUpgradeStatus("acme", "resolver", approvedResolverHash);
131
157
  // status: { owner, contractId, currentHash, approved, governed }
132
- if (status.approved && status.governed && status.currentHash !== approvedResolverHash) {
158
+ const direction = contractUpgradeDirection("resolver", status.currentHash, approvedResolverHash);
159
+ if (status.approved && status.governed && (direction === "forward" || direction === "unreviewed-target")) {
133
160
  await client.upgradeResolver("acme", approvedResolverHash, {
134
161
  onPrepared: ({ hash, feeStroops }) => savePendingUpdate({ hash, feeStroops }),
135
162
  });
@@ -137,8 +164,25 @@ if (status.approved && status.governed && status.currentHash !== approvedResolve
137
164
  await client.upgradeRegistrar("acme", approvedRegistrarHash);
138
165
  ```
139
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
+
140
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.
141
170
 
142
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.
143
172
 
144
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,9 +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";
5
- import { type ContractUpgradeRole, type ContractUpgradeStatus } from "./native-upgrades.js";
6
- export type { ContractUpgradeRole, ContractUpgradeStatus } from "./native-upgrades.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";
7
48
  import { type ClaimSettings, type ClaimConfig, type ClaimUsage, type ApprovalUsage } from "./native-types.js";
8
49
  export * from "./native-types.js";
9
50
  export * from "./native-allowlist.js";
@@ -166,9 +207,9 @@ export declare class SoranOwner {
166
207
  contractUpgradeStatus(namespace: string, role: ContractUpgradeRole, wasmHash: string): Promise<ContractUpgradeStatus>;
167
208
  private upgradeContract;
168
209
  /** Current-owner, Registry-approved update; never uses the legacy tainting path. */
169
- upgradeRegistrar(namespace: string, wasmHash: string, options?: NativeWriteOptions): Promise<Submitted>;
210
+ upgradeRegistrar(namespace: string, wasmHash: string, options?: ContractUpgradeOptions): Promise<Submitted>;
170
211
  /** Current-owner, Registry-approved Resolver update with exact native authorization. */
171
- upgradeResolver(namespace: string, wasmHash: string, options?: NativeWriteOptions): Promise<Submitted>;
212
+ upgradeResolver(namespace: string, wasmHash: string, options?: ContractUpgradeOptions): Promise<Submitted>;
172
213
  configureClaims(namespace: string, settings: ClaimSettings, options?: NativeWriteOptions): Promise<Submitted & {
173
214
  config: ClaimConfig;
174
215
  }>;
@@ -253,6 +294,12 @@ export declare class SoranOwner {
253
294
  * Refused (ResolverFrozen) once the namespace is permanent.
254
295
  */
255
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;
256
303
  /** The namespace's immutable issuance policy. */
257
304
  policy(namespace: string): Promise<NamespacePolicy>;
258
305
  /** Has the namespace passed the one-way door? */
@@ -261,6 +308,30 @@ export declare class SoranOwner {
261
308
  nameState(namespace: string, label: string): Promise<NameState | null>;
262
309
  /** The pending namespace transfer on the Registry, or null. */
263
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>;
264
335
  private readRecord;
265
336
  private readRecords;
266
337
  /** Simulation-only contract read — no signature, no fee, no state change. */
@@ -281,6 +352,8 @@ export declare class SoranOwner {
281
352
  * auth demands an address the envelope signature cannot satisfy.
282
353
  */
283
354
  private assertSatisfiableAuth;
355
+ /** Authorize exactly the selected policy with no additional child operations. */
356
+ private assertChainPolicyIntent;
284
357
  private attempt;
285
358
  private restore;
286
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,7 +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";
90
- import { contractUpgradeStatus as readContractUpgradeStatus, upgradeHash } from "./native-upgrades.js";
94
+ import { contractUpgradeStatus as readContractUpgradeStatus, contractUpgradeDirection, upgradeHash } from "./native-upgrades.js";
95
+ export { CONTRACT_RELEASES, contractUpgradeDirection } from "./native-upgrades.js";
91
96
  import { claimSettingsToScVal, claimConfigFromNative, claimLabelToScVal, claimUsageFromNative, approvalUsageFromNative } from "./native-types.js";
92
97
  export * from "./native-types.js";
93
98
  export * from "./native-allowlist.js";
@@ -184,6 +189,15 @@ const REGISTRAR_ERRORS = {
184
189
  49: "MigrationUnavailable",
185
190
  50: "MigrationUnsupported",
186
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",
187
201
  };
188
202
  /** Registry contract error codes, by number. */
189
203
  const REGISTRY_ERRORS = {
@@ -228,6 +242,7 @@ const REGISTRY_ERRORS = {
228
242
  39: "UpgradePolicyMismatch",
229
243
  40: "UpgradeNotApproved",
230
244
  41: "PermanentLockDisabled",
245
+ 42: "BatchTooLarge",
231
246
  };
232
247
  /** Errors for a submit the network REJECTED as a stale sequence: nothing was included, so one retry with a fresh sequence is safe. */
233
248
  const BAD_SEQ_REJECTED = new WeakSet();
@@ -443,8 +458,17 @@ export class SoranOwner {
443
458
  throw new NativeClaimError("the current namespace owner must authorize this change", "authorization");
444
459
  if (!status.governed || !status.approved)
445
460
  throw new NativeClaimError("This contract upgrade is not approved by the Registry", "authorization");
446
- if (status.currentHash === wasmHash)
461
+ const direction = contractUpgradeDirection(role, status.currentHash, wasmHash);
462
+ if (direction === "same")
447
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
+ }
448
472
  const result = await sendNative(context, {
449
473
  source, contract: status.contractId, method: "upgrade", args,
450
474
  sourceInvocation: { contract: status.contractId, method: "upgrade", args },
@@ -715,6 +739,44 @@ export class SoranOwner {
715
739
  const r = await this.invoke(this.registryId, "set_resolver", [nodeArg(namehash(namespace)), resolver === null ? xdr.ScVal.scvVoid() : addrArg(resolver)], REGISTRY_ERRORS);
716
740
  return { hash: r.hash, ledger: r.ledger };
717
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
+ }
718
780
  // ---- reads that support write flows --------------------------------------
719
781
  /** The namespace's immutable issuance policy. */
720
782
  async policy(namespace) {
@@ -745,6 +807,20 @@ export class SoranOwner {
745
807
  return this.read(this.registryId, "pending_transfer", [nodeArg(namehash(namespace))]);
746
808
  }
747
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
+ }
748
824
  async readRecord(registrarId, label) {
749
825
  const rec = (await this.read(registrarId, "record_of", [labelArg(label)]));
750
826
  if (!rec)
@@ -777,7 +853,8 @@ export class SoranOwner {
777
853
  .build();
778
854
  const sim = await this.server.simulateTransaction(tx);
779
855
  if (rpc.Api.isSimulationError(sim)) {
780
- 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);
781
858
  }
782
859
  // An archived entry is NOT absence: reads cannot restore (no signer),
783
860
  // so surface the state honestly instead of reporting "does not exist".
@@ -853,6 +930,26 @@ export class SoranOwner {
853
930
  : `${fn}: this operation must be authorized by ${required}, but the signer is ${pub} — use that account's signer`, contractId, fn);
854
931
  }
855
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
+ }
856
953
  async attempt(contractId, fn, args, errNames) {
857
954
  const pub = await this.signer.publicKey();
858
955
  const contract = new Contract(contractId);
@@ -883,6 +980,7 @@ export class SoranOwner {
883
980
  const prepared = rpc.assembleTransaction(tx, sim).build();
884
981
  this.assertNetworkFee(prepared.fee, fn);
885
982
  this.assertSatisfiableAuth(prepared, pub, contractId, fn);
983
+ this.assertChainPolicyIntent(prepared, pub, contractId, fn, args);
886
984
  // The hash is fixed before signatures — compute it now so every failure
887
985
  // past this point can carry it (the "re-check before retrying" contract).
888
986
  const txHash = toHex(prepared.hash()); // (SDK17) hash() is Uint8Array
@@ -0,0 +1,99 @@
1
+ export declare const CHAIN_NETWORKS: readonly Readonly<{
2
+ readonly id: "bitcoin";
3
+ readonly name: "Bitcoin";
4
+ readonly coinType: 0;
5
+ readonly encoding: "bitcoin-script";
6
+ readonly caip2: "bip122:000000000019d6689c085ae165831e93";
7
+ } | {
8
+ readonly id: "ethereum";
9
+ readonly name: "Ethereum";
10
+ readonly coinType: 60;
11
+ readonly encoding: "evm";
12
+ readonly caip2: "eip155:1";
13
+ } | {
14
+ readonly id: "solana";
15
+ readonly name: "Solana";
16
+ readonly coinType: 501;
17
+ readonly encoding: "solana";
18
+ readonly caip2: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
19
+ } | {
20
+ readonly id: "optimism";
21
+ readonly name: "Optimism";
22
+ readonly coinType: 2147483658;
23
+ readonly encoding: "evm";
24
+ readonly caip2: "eip155:10";
25
+ } | {
26
+ readonly id: "polygon";
27
+ readonly name: "Polygon";
28
+ readonly coinType: 2147483785;
29
+ readonly encoding: "evm";
30
+ readonly caip2: "eip155:137";
31
+ } | {
32
+ readonly id: "base";
33
+ readonly name: "Base";
34
+ readonly coinType: 2147492101;
35
+ readonly encoding: "evm";
36
+ readonly caip2: "eip155:8453";
37
+ } | {
38
+ readonly id: "arbitrum";
39
+ readonly name: "Arbitrum";
40
+ readonly coinType: 2147525809;
41
+ readonly encoding: "evm";
42
+ readonly caip2: "eip155:42161";
43
+ } | {
44
+ readonly id: "bsc";
45
+ readonly name: "BNB Smart Chain";
46
+ readonly coinType: 2147483704;
47
+ readonly encoding: "evm";
48
+ readonly caip2: "eip155:56";
49
+ } | {
50
+ readonly id: "avalanche";
51
+ readonly name: "Avalanche C-Chain";
52
+ readonly coinType: 2147526762;
53
+ readonly encoding: "evm";
54
+ readonly caip2: "eip155:43114";
55
+ } | {
56
+ readonly id: "xrp";
57
+ readonly name: "XRP Ledger";
58
+ readonly coinType: 144;
59
+ readonly encoding: "xrp";
60
+ readonly caip2: "xrpl:0";
61
+ } | {
62
+ readonly id: "tron";
63
+ readonly name: "Tron";
64
+ readonly coinType: 195;
65
+ readonly encoding: "tron";
66
+ readonly caip2: "tron:728126428";
67
+ } | {
68
+ readonly id: "arc";
69
+ readonly name: "Arc";
70
+ readonly coinType: 2147488690;
71
+ readonly encoding: "evm";
72
+ readonly caip2: "eip155:5042";
73
+ } | {
74
+ readonly id: "tempo";
75
+ readonly name: "Tempo";
76
+ readonly coinType: 2147487865;
77
+ readonly encoding: "evm";
78
+ readonly caip2: "eip155:4217";
79
+ } | {
80
+ readonly id: "xrpl-evm";
81
+ readonly name: "XRPL EVM";
82
+ readonly coinType: 2148923648;
83
+ readonly encoding: "evm";
84
+ readonly caip2: "eip155:1440000";
85
+ }>[];
86
+ export type ChainNetwork = typeof CHAIN_NETWORKS[number]["id"];
87
+ export type ChainAddressErrorCode = "UNSUPPORTED_NETWORK" | "INVALID_ADDRESS" | "INVALID_POLICY";
88
+ export declare class ChainAddressError extends Error {
89
+ readonly code: ChainAddressErrorCode;
90
+ constructor(message: string, code: ChainAddressErrorCode);
91
+ }
92
+ export declare function chainNetwork(id: string): typeof CHAIN_NETWORKS[number];
93
+ /** Strictly decode the native Vec<u32> policy; never silently discard unknown networks. */
94
+ export declare function chainPolicyFromNative(raw: unknown): ChainNetwork[];
95
+ export declare function chainPolicyToNative(networks: readonly ChainNetwork[]): number[];
96
+ /** Validate and encode an explicitly selected destination network. Never infer a network. */
97
+ export declare function encodeChainAddress(network: ChainNetwork, address: string): Uint8Array;
98
+ /** Strictly decode native binary records to their canonical human-readable form. */
99
+ export declare function decodeChainAddress(network: ChainNetwork, bytes: Uint8Array): string;
@@ -0,0 +1,204 @@
1
+ /** Canonical multichain address codecs. Run sdk/multichain/sync.mjs after editing.
2
+ * Binary records follow ENSIP-9/11: EVM raw 20 bytes; Bitcoin scriptPubKey;
3
+ * Solana raw 32-byte public key. Stellar retains its separate payment/memo API.
4
+ * These are destination-network mainnet addresses even on Soran testnet.
5
+ */
6
+ import { base58, base58xrp, bech32, bech32m, createBase58check } from "@scure/base";
7
+ import { sha256 } from "@noble/hashes/sha2.js";
8
+ import { keccak_256 } from "@noble/hashes/sha3.js";
9
+ export const CHAIN_NETWORKS = Object.freeze([
10
+ { id: "bitcoin", name: "Bitcoin", coinType: 0, encoding: "bitcoin-script", caip2: "bip122:000000000019d6689c085ae165831e93" },
11
+ { id: "ethereum", name: "Ethereum", coinType: 60, encoding: "evm", caip2: "eip155:1" },
12
+ { id: "solana", name: "Solana", coinType: 501, encoding: "solana", caip2: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" },
13
+ { id: "optimism", name: "Optimism", coinType: 2147483658, encoding: "evm", caip2: "eip155:10" },
14
+ { id: "polygon", name: "Polygon", coinType: 2147483785, encoding: "evm", caip2: "eip155:137" },
15
+ { id: "base", name: "Base", coinType: 2147492101, encoding: "evm", caip2: "eip155:8453" },
16
+ { id: "arbitrum", name: "Arbitrum", coinType: 2147525809, encoding: "evm", caip2: "eip155:42161" },
17
+ { id: "bsc", name: "BNB Smart Chain", coinType: 2147483704, encoding: "evm", caip2: "eip155:56" },
18
+ { id: "avalanche", name: "Avalanche C-Chain", coinType: 2147526762, encoding: "evm", caip2: "eip155:43114" },
19
+ { id: "xrp", name: "XRP Ledger", coinType: 144, encoding: "xrp", caip2: "xrpl:0" },
20
+ { id: "tron", name: "Tron", coinType: 195, encoding: "tron", caip2: "tron:728126428" },
21
+ { id: "arc", name: "Arc", coinType: 2147488690, encoding: "evm", caip2: "eip155:5042" },
22
+ { id: "tempo", name: "Tempo", coinType: 2147487865, encoding: "evm", caip2: "eip155:4217" },
23
+ { id: "xrpl-evm", name: "XRPL EVM", coinType: 2148923648, encoding: "evm", caip2: "eip155:1440000" },
24
+ ].map(network => Object.freeze(network)));
25
+ export class ChainAddressError extends Error {
26
+ code;
27
+ constructor(message, code) {
28
+ super(message);
29
+ this.code = code;
30
+ this.name = "ChainAddressError";
31
+ }
32
+ }
33
+ export function chainNetwork(id) {
34
+ const network = CHAIN_NETWORKS.find(network => network.id === id);
35
+ if (!network)
36
+ throw new ChainAddressError(`unsupported address network: ${String(id)}`, "UNSUPPORTED_NETWORK");
37
+ return network;
38
+ }
39
+ /** Strictly decode the native Vec<u32> policy; never silently discard unknown networks. */
40
+ export function chainPolicyFromNative(raw) {
41
+ if (!Array.isArray(raw) || raw.length > CHAIN_NETWORKS.length)
42
+ throw new ChainAddressError("invalid chain policy: expected supported coin types", "INVALID_POLICY");
43
+ const seen = new Set();
44
+ return Array.from(raw, coin => {
45
+ const network = CHAIN_NETWORKS.find(network => network.coinType === coin);
46
+ if (typeof coin !== "number" || !Number.isInteger(coin) || !network || seen.has(coin))
47
+ throw new ChainAddressError("invalid chain policy: unknown or duplicate coin type", "INVALID_POLICY");
48
+ seen.add(coin);
49
+ return network.id;
50
+ });
51
+ }
52
+ export function chainPolicyToNative(networks) {
53
+ if (!Array.isArray(networks) || networks.length > CHAIN_NETWORKS.length)
54
+ throw new ChainAddressError("invalid chain policy", "INVALID_POLICY");
55
+ const coins = Array.from(networks, network => chainNetwork(network).coinType);
56
+ chainPolicyFromNative(coins);
57
+ return coins;
58
+ }
59
+ const btc58 = createBase58check(sha256);
60
+ const hex = (bytes) => Array.from(bytes, byte => byte.toString(16).padStart(2, "0")).join("");
61
+ const fromHex = (value) => Uint8Array.from(value.match(/../g), part => Number.parseInt(part, 16));
62
+ const nonzero = (bytes) => bytes.some(byte => byte !== 0);
63
+ const invalid = (message) => { throw new ChainAddressError(message, "INVALID_ADDRESS"); };
64
+ function evmChecksum(bytes) {
65
+ const lower = hex(bytes);
66
+ const digest = hex(keccak_256(new TextEncoder().encode(lower)));
67
+ return "0x" + [...lower].map((char, index) => Number.parseInt(digest[index], 16) >= 8 ? char.toUpperCase() : char).join("");
68
+ }
69
+ function encodeBitcoin(address) {
70
+ if (/^(bc1|tb1|bcrt1)/i.test(address)) {
71
+ // The checksum encoding is tied to witness version: BIP173 v0, BIP350 v1.
72
+ let decoded;
73
+ let variant;
74
+ try {
75
+ decoded = bech32.decode(address);
76
+ variant = 0;
77
+ }
78
+ catch {
79
+ decoded = bech32m.decode(address);
80
+ variant = 1;
81
+ }
82
+ if (decoded.prefix !== "bc")
83
+ return invalid("Bitcoin address must use mainnet");
84
+ const version = decoded.words[0];
85
+ if (version !== variant)
86
+ return invalid("unsupported Bitcoin witness version or checksum");
87
+ const program = bech32.fromWords(decoded.words.slice(1));
88
+ if ((version === 0 && program.length !== 20 && program.length !== 32) || (version === 1 && program.length !== 32))
89
+ return invalid("invalid Bitcoin witness program length");
90
+ return Uint8Array.from([version === 0 ? 0 : 0x51, program.length, ...program]);
91
+ }
92
+ const decoded = btc58.decode(address);
93
+ if (decoded.length !== 21)
94
+ return invalid("invalid Bitcoin address length");
95
+ if (decoded[0] === 0)
96
+ return Uint8Array.from([0x76, 0xa9, 0x14, ...decoded.slice(1), 0x88, 0xac]);
97
+ if (decoded[0] === 5)
98
+ return Uint8Array.from([0xa9, 0x14, ...decoded.slice(1), 0x87]);
99
+ return invalid("Bitcoin address must use mainnet");
100
+ }
101
+ function decodeBitcoin(bytes) {
102
+ if (bytes.length === 25 && bytes[0] === 0x76 && bytes[1] === 0xa9 && bytes[2] === 0x14 && bytes[23] === 0x88 && bytes[24] === 0xac)
103
+ return btc58.encode(Uint8Array.from([0, ...bytes.slice(3, 23)]));
104
+ if (bytes.length === 23 && bytes[0] === 0xa9 && bytes[1] === 0x14 && bytes[22] === 0x87)
105
+ return btc58.encode(Uint8Array.from([5, ...bytes.slice(2, 22)]));
106
+ if (bytes[0] === 0 && (bytes[1] === 20 || bytes[1] === 32) && bytes.length === bytes[1] + 2)
107
+ return bech32.encode("bc", [0, ...bech32.toWords(bytes.slice(2))]);
108
+ if (bytes[0] === 0x51 && bytes[1] === 32 && bytes.length === 34)
109
+ return bech32m.encode("bc", [1, ...bech32m.toWords(bytes.slice(2))]);
110
+ return invalid("unsupported or malformed Bitcoin scriptPubKey");
111
+ }
112
+ /** XRP keeps the version prefix and full X-address tag payload. A classic
113
+ * address alone cannot represent an exchange's required destination tag. */
114
+ function validateXrp(bytes) {
115
+ if (bytes.length === 21 && bytes[0] === 0 && nonzero(bytes.slice(1)))
116
+ return;
117
+ if (bytes.length === 31 && bytes[0] === 5 && bytes[1] === 0x44 && nonzero(bytes.slice(2, 22)) &&
118
+ (bytes[22] === 0 || bytes[22] === 1) && !nonzero(bytes.slice(27)) &&
119
+ (bytes[22] === 1 || !nonzero(bytes.slice(23, 27))))
120
+ return;
121
+ return invalid("invalid XRP classic or mainnet X-address payload");
122
+ }
123
+ function encodeXrp(bytes) {
124
+ validateXrp(bytes);
125
+ return base58xrp.encode(Uint8Array.from([...bytes, ...sha256(sha256(bytes)).slice(0, 4)]));
126
+ }
127
+ function decodeXrp(address) {
128
+ const decoded = base58xrp.decode(address);
129
+ if (decoded.length !== 25 && decoded.length !== 35)
130
+ return invalid("invalid XRP address length");
131
+ const bytes = decoded.slice(0, -4);
132
+ const checksum = sha256(sha256(bytes)).slice(0, 4);
133
+ if (!checksum.every((byte, index) => byte === decoded[bytes.length + index]))
134
+ return invalid("invalid XRP address checksum");
135
+ validateXrp(bytes);
136
+ if (encodeXrp(bytes) !== address)
137
+ return invalid("noncanonical XRP address");
138
+ return bytes;
139
+ }
140
+ function validateTron(bytes) {
141
+ if (bytes.length !== 21 || bytes[0] !== 0x41 || !nonzero(bytes.slice(1)))
142
+ return invalid("invalid Tron address payload");
143
+ }
144
+ /** Validate and encode an explicitly selected destination network. Never infer a network. */
145
+ export function encodeChainAddress(network, address) {
146
+ const metadata = chainNetwork(network);
147
+ if (typeof address !== "string" || address.length === 0 || address.length > 128 || /\s/.test(address))
148
+ return invalid("address must be a nonempty string without whitespace");
149
+ try {
150
+ if (metadata.encoding === "bitcoin-script")
151
+ return encodeBitcoin(address);
152
+ if (metadata.encoding === "xrp")
153
+ return decodeXrp(address);
154
+ if (metadata.encoding === "tron") {
155
+ const bytes = btc58.decode(address);
156
+ validateTron(bytes);
157
+ if (!address.startsWith("T") || btc58.encode(bytes) !== address)
158
+ return invalid("Tron address must use canonical T format");
159
+ return bytes;
160
+ }
161
+ if (metadata.encoding === "solana") {
162
+ const bytes = base58.decode(address);
163
+ if (bytes.length !== 32 || !nonzero(bytes) || base58.encode(bytes) !== address)
164
+ return invalid("invalid Solana public key");
165
+ return bytes;
166
+ }
167
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address))
168
+ return invalid("EVM address must contain 0x and 40 hexadecimal digits");
169
+ const bytes = fromHex(address.slice(2));
170
+ if (!nonzero(bytes))
171
+ return invalid("zero EVM address is not a payment destination");
172
+ const body = address.slice(2);
173
+ if (body !== body.toLowerCase() && body !== body.toUpperCase() && address !== evmChecksum(bytes))
174
+ return invalid("invalid EIP-55 address checksum");
175
+ return bytes;
176
+ }
177
+ catch (error) {
178
+ if (error instanceof ChainAddressError)
179
+ throw error;
180
+ return invalid(`invalid ${metadata.name} address or checksum`);
181
+ }
182
+ }
183
+ /** Strictly decode native binary records to their canonical human-readable form. */
184
+ export function decodeChainAddress(network, bytes) {
185
+ const metadata = chainNetwork(network);
186
+ if (!(bytes instanceof Uint8Array))
187
+ return invalid("chain address must be native bytes");
188
+ if (metadata.encoding === "bitcoin-script")
189
+ return decodeBitcoin(bytes);
190
+ if (metadata.encoding === "xrp")
191
+ return encodeXrp(bytes);
192
+ if (metadata.encoding === "tron") {
193
+ validateTron(bytes);
194
+ return btc58.encode(bytes);
195
+ }
196
+ if (metadata.encoding === "solana") {
197
+ if (bytes.length !== 32 || !nonzero(bytes))
198
+ return invalid("invalid Solana public key bytes");
199
+ return base58.encode(bytes);
200
+ }
201
+ if (bytes.length !== 20 || !nonzero(bytes))
202
+ return invalid("invalid EVM address bytes");
203
+ return evmChecksum(bytes);
204
+ }
@@ -0,0 +1,29 @@
1
+ export type ParsedResolvableName = {
2
+ name: string;
3
+ label: string;
4
+ namespace: string;
5
+ parent: string | null;
6
+ parentLabel: string;
7
+ childLabel: string | null;
8
+ };
9
+ export declare function parseNamespace(input: string): string;
10
+ export declare function parseResolvableName(input: string): ParsedResolvableName;
11
+ export declare function resolvableNameNode(input: string): Uint8Array;
12
+ export type SubnamePolicy = "enabled" | "creation-disabled" | "suspended";
13
+ export declare function subnamePolicyToNative(value: SubnamePolicy): string[];
14
+ export declare function subnamePolicyFromNative(raw: unknown): SubnamePolicy;
15
+ export type SubnameRecord = {
16
+ parentNode: Uint8Array;
17
+ parentGeneration: bigint;
18
+ generation: bigint;
19
+ address: string;
20
+ active: boolean;
21
+ };
22
+ export declare function exactGeneration(value: unknown): bigint;
23
+ /** Address validity is checked by each caller's Stellar SDK, without coercion. */
24
+ export declare function subnameRecordFromNative(raw: unknown, expectedParentNode: Uint8Array): SubnameRecord | null;
25
+ export declare function subnameLabelsFromNative(raw: unknown, limit: number): string[];
26
+ export declare function subnamePageInput(offset?: number, limit?: number): {
27
+ offset: number;
28
+ limit: number;
29
+ };
package/dist/names.js ADDED
@@ -0,0 +1,83 @@
1
+ // Generated by sdk/names/sync.mjs; edit sdk/names/names.ts.
2
+ /** Canonical resolution grammar. Registration and lifecycle actions still use two labels. */
3
+ import { sha256 } from "@noble/hashes/sha2.js";
4
+ const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
5
+ export function parseNamespace(input) {
6
+ if (typeof input !== "string" || !/^[A-Za-z0-9-]+$/.test(input))
7
+ throw new Error("invalid namespace");
8
+ const value = input.toLowerCase();
9
+ if (!LABEL.test(value))
10
+ throw new Error("invalid namespace");
11
+ return value;
12
+ }
13
+ export function parseResolvableName(input) {
14
+ if (typeof input !== "string" || !/^[A-Za-z0-9.-]+$/.test(input) || input.length > 191)
15
+ throw new Error("name must contain two or three ASCII labels");
16
+ const name = input.toLowerCase(), parts = name.split(".");
17
+ if ((parts.length !== 2 && parts.length !== 3) || !parts.every(label => LABEL.test(label)))
18
+ throw new Error("expected label.namespace or child.label.namespace");
19
+ const namespace = parts.at(-1), parentLabel = parts.at(-2);
20
+ return { name, label: parts.slice(0, -1).join("."), namespace, parent: parts.length === 3 ? `${parentLabel}.${namespace}` : null,
21
+ parentLabel, childLabel: parts.length === 3 ? parts[0] : null };
22
+ }
23
+ export function resolvableNameNode(input) {
24
+ const parts = parseResolvableName(input).name.split(".").reverse();
25
+ let node = new Uint8Array(32);
26
+ for (const part of parts) {
27
+ const joined = new Uint8Array(64);
28
+ joined.set(node);
29
+ joined.set(sha256(new TextEncoder().encode(part)), 32);
30
+ node = new Uint8Array(sha256(joined));
31
+ }
32
+ return node;
33
+ }
34
+ const POLICIES = { enabled: "Enabled", "creation-disabled": "CreationDisabled", suspended: "Suspended" };
35
+ export function subnamePolicyToNative(value) {
36
+ if (!Object.hasOwn(POLICIES, value))
37
+ throw new Error("invalid subname policy");
38
+ return [POLICIES[value]];
39
+ }
40
+ export function subnamePolicyFromNative(raw) {
41
+ if (!Array.isArray(raw) || raw.length !== 1 || typeof raw[0] !== "string")
42
+ throw new Error("invalid subname policy response");
43
+ const policy = Object.keys(POLICIES).find(key => POLICIES[key] === raw[0]);
44
+ if (!policy)
45
+ throw new Error("unsupported subname policy");
46
+ return policy;
47
+ }
48
+ export function exactGeneration(value) {
49
+ if (typeof value !== "bigint" || value < 0n || value > 18446744073709551615n)
50
+ throw new Error("generation must be an exact u64 bigint");
51
+ return value;
52
+ }
53
+ /** Address validity is checked by each caller's Stellar SDK, without coercion. */
54
+ export function subnameRecordFromNative(raw, expectedParentNode) {
55
+ if (raw === null)
56
+ return null;
57
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).sort().join(",") !== "active,address,generation,parent_generation,parent_node")
58
+ throw new Error("invalid subname record fields");
59
+ const r = raw;
60
+ if (!(r.parent_node instanceof Uint8Array) || r.parent_node.length !== 32 || expectedParentNode.length !== 32 || !r.parent_node.every((byte, i) => byte === expectedParentNode[i]) || typeof r.address !== "string" || typeof r.active !== "boolean")
61
+ throw new Error("invalid subname record response");
62
+ return { parentNode: new Uint8Array(r.parent_node), parentGeneration: exactGeneration(r.parent_generation), generation: exactGeneration(r.generation), address: r.address, active: r.active };
63
+ }
64
+ export function subnameLabelsFromNative(raw, limit) {
65
+ if (!Array.isArray(raw) || raw.length > limit)
66
+ throw new Error("invalid subname labels response");
67
+ const values = raw.map(label => {
68
+ if (!(label instanceof Uint8Array) || label.length < 1 || label.length > 63 || label.some(byte => byte > 127))
69
+ throw new Error("invalid subname label bytes");
70
+ const text = new TextDecoder().decode(label);
71
+ if (!LABEL.test(text))
72
+ throw new Error("invalid subname label");
73
+ return text;
74
+ });
75
+ if (new Set(values).size !== values.length)
76
+ throw new Error("duplicate subname label");
77
+ return values;
78
+ }
79
+ export function subnamePageInput(offset = 0, limit = 16) {
80
+ if (!Number.isInteger(offset) || offset < 0 || offset > 4294967295 || !Number.isInteger(limit) || limit < 1 || limit > 16 || offset + limit > 4294967295)
81
+ throw new Error("subname page requires an unsigned offset and a limit from 1 to 16");
82
+ return { offset, limit };
83
+ }
@@ -1,7 +1,7 @@
1
1
  /** Local successor-ABI helpers. No deployed defaults are changed by this module. */
2
2
  import { Address, StrKey, hash, nativeToScVal, xdr } from "@stellar/stellar-sdk";
3
3
  import { decodeMuxedAddress, paymentMemoToScVal, validatePaymentDestination } from "./payment.js";
4
- export const NATIVE_REGISTRAR_ERRORS = { 21: "InvalidClaimConfig", 22: "ClaimsNotConfigured", 23: "ClaimsPaused", 24: "StaleClaimPolicy", 25: "ClaimIntentMismatch", 26: "ClaimIntentExpired", 27: "ReservedName", 28: "NameNotReserved", 29: "PublicIssuanceRequired", 30: "WalletClaimLimit", 31: "InvalidEligibility", 32: "ApprovalAllowanceReached", 33: "ApprovalRateReached", 34: "RequestIdConflict", 35: "CounterOverflow", 36: "InvalidNativeBinding", 37: "UnsupportedClaimant", 38: "DuplicateLabel", 39: "RenewalTooEarly", 40: "RenewalLeaseLimit", 41: "DestinationInitializationFailed", 42: "FeeSettlementFailed" };
4
+ export const NATIVE_REGISTRAR_ERRORS = { 1: "AlreadyInitialized", 2: "NotInitialized", 3: "NameTaken", 4: "NameNotFound", 5: "NotReclaimable", 6: "NotTransferable", 7: "AlreadyPermanent", 8: "BatchTooLarge", 9: "NotNamespaceOwner", 10: "BadLabel", 11: "PermanentName", 12: "FiniteTermPolicy", 13: "PermanentlyLocked", 14: "NameExpired", 15: "NoPendingTransfer", 16: "TransferExpired", 17: "StaleTransfer", 18: "InvalidPolicy", 19: "ExpiryOverflow", 20: "InvalidRegistry", 21: "InvalidClaimConfig", 22: "ClaimsNotConfigured", 23: "ClaimsPaused", 24: "StaleClaimPolicy", 25: "ClaimIntentMismatch", 26: "ClaimIntentExpired", 27: "ReservedName", 28: "NameNotReserved", 29: "PublicIssuanceRequired", 30: "WalletClaimLimit", 31: "InvalidEligibility", 32: "ApprovalAllowanceReached", 33: "ApprovalRateReached", 34: "RequestIdConflict", 35: "CounterOverflow", 36: "InvalidNativeBinding", 37: "UnsupportedClaimant", 38: "DuplicateLabel", 39: "RenewalTooEarly", 40: "RenewalLeaseLimit", 41: "DestinationInitializationFailed", 42: "FeeSettlementFailed", 43: "MigrationInProgress", 44: "MigrationAlreadyStarted", 45: "MigrationNotActive", 46: "MigrationMismatch", 47: "MigrationDuplicate", 48: "MigrationIncomplete", 49: "MigrationUnavailable", 50: "MigrationUnsupported", 51: "PermanentLockDisabled", 52: "InvalidRenewalGrace", 53: "SubnameCreationDisabled", 54: "SubnamesSuspended", 55: "SubnameGenerationMismatch", 56: "SubnameIndexUnavailable" };
5
5
  export class NativeClaimError extends Error {
6
6
  kind;
7
7
  txHash;
@@ -1,5 +1,7 @@
1
1
  import { Transaction, rpc, xdr } from "@stellar/stellar-sdk";
2
2
  import { type NativeAuthorizationPlan } from "./native-auth.js";
3
+ /** RPC clients sometimes reject with a plain `{ code, message }` object, which String() renders as "[object Object]". */
4
+ export declare function describeRejection(error: unknown): string;
3
5
  export type NativeSigner = {
4
6
  publicKey(): string | Promise<string>;
5
7
  signTransaction(encoded: string, opts: {
@@ -2,6 +2,23 @@ import { expectedRegistrarCode } from "./native-code-policy.js";
2
2
  import { Contract, Operation, Transaction, TransactionBuilder, rpc, scValToNative, xdr } from "@stellar/stellar-sdk";
3
3
  import { NativeClaimError, address, hex, namespaceNode, sc, unhex } from "./native-codec.js";
4
4
  import { assertSignedBodyUnchanged, validateEligibilityAuthorization, validateNativeTransaction } from "./native-auth.js";
5
+ /** RPC clients sometimes reject with a plain `{ code, message }` object, which String() renders as "[object Object]". */
6
+ export function describeRejection(error) {
7
+ if (error instanceof Error || typeof error === "string")
8
+ return String(error);
9
+ if (error && typeof error === "object") {
10
+ const message = error.message;
11
+ if (typeof message === "string" && message)
12
+ return message;
13
+ try {
14
+ const json = JSON.stringify(error);
15
+ if (json && json !== "{}")
16
+ return json.length > 300 ? `${json.slice(0, 300)}...` : json;
17
+ }
18
+ catch { /* circular or unserialisable */ }
19
+ }
20
+ return "unknown error";
21
+ }
5
22
  /** This published legacy code has owner-only issuance. Missing RPC data is never legacy detection. */
6
23
  const LEGACY_REGISTRAR_HASH = "ed06b3374ff4342b4a2316fc546505132cd8fd35be6666571cf088710af9bbc6";
7
24
  /** Historical reads must not trust an upgraded Registrar that can fabricate receipts. Resolver changes do not block this Registrar-only check. */
@@ -119,7 +136,7 @@ export async function sendNative(context, plan, options = {}) {
119
136
  catch (error) {
120
137
  if (error instanceof NativeClaimError && error.txHash)
121
138
  throw error;
122
- throw new NativeClaimError(`submission/confirmation interrupted: ${String(error)}; reconcile the original receipt`, "pending", prepared.hash);
139
+ throw new NativeClaimError(`submission/confirmation interrupted: ${describeRejection(error)}; reconcile the original receipt`, "pending", prepared.hash);
123
140
  }
124
141
  }
125
142
  /** Bind terminal RPC context before classifying success or failure. An incomplete response remains uncertain. */
@@ -1,6 +1,10 @@
1
1
  import { xdr } from "@stellar/stellar-sdk";
2
- import type { NativeContext } from "./native-transport.js";
2
+ import type { NativeContext, NativeWriteOptions } from "./native-transport.js";
3
3
  export type ContractUpgradeRole = "registrar" | "resolver";
4
+ export type ContractUpgradeOptions = NativeWriteOptions & {
5
+ /** Explicit acknowledgement that installing a superseded (older) release, or the latest known release over code this SDK cannot place, is intended. A reinstall of the installed hash is never allowed. */
6
+ allowDowngrade?: boolean;
7
+ };
4
8
  export type ContractUpgradeStatus = {
5
9
  owner: string;
6
10
  contractId: string;
@@ -8,6 +12,37 @@ export type ContractUpgradeStatus = {
8
12
  approved: boolean;
9
13
  governed: boolean;
10
14
  };
15
+ /**
16
+ * Reviewed implementation lineage of the governed testnet Registry. Governance
17
+ * approval means "this hash may be installed on any namespace", not "this is a
18
+ * safe transition": superseded releases stay approved, so the chain accepts a
19
+ * downgrade. `latest` is the release owners are offered; `superseded` are the
20
+ * older releases it replaces. A test keeps this equal to the web catalog
21
+ * (web/lib/contractUpdates.ts) and the release record; adding a release means
22
+ * moving the old `latest` into `superseded` here and there.
23
+ */
24
+ export declare const CONTRACT_RELEASES: {
25
+ readonly registrar: {
26
+ readonly latest: "4d6d18625f4d890028acb794b24871c5c93f96dc15d94cf3d52cbf9713b07fb1";
27
+ readonly superseded: readonly ["42dc5f8b40932617d3f2667e1d26dad3e5e9a62c63de1fde7c016cb67a86c420", "5c8ae72ea7370ccf19af52b9bbc0007362be94464651ccdf68a2bb1f478b6f2c", "61ded153e6311075c1087c5639a5410222e23f7cade239ed2c946150af94b1c5", "26e03cd1d7fbfa46fe442406fb1b88022ec8cd8bd9123bed570f971fce596a14"];
28
+ };
29
+ readonly resolver: {
30
+ readonly latest: "941bf87f26cb817f3c6a52a326de855cca8a1dc4477e62a38ec4ffa5c7f43806";
31
+ readonly superseded: readonly ["cb927ffa19d6b1f3f0b0187048d885e200adb6de3fddd11ee3cfeb59df9c5c48", "351bf4e056b179b578fe2a4ea46f937bfafccc6538c7357ea70021b7038d91ea", "c6dfcd7bbf35943a13406888678334d0938d0fea54939906b00c2dc37b4a9a71", "5e7b4fb8d310d67ed6966a155e748d60be771422b3e8c34bbc14485f98525574"];
32
+ };
33
+ };
34
+ /**
35
+ * How installing `targetHash` over `currentHash` relates to the reviewed lineage:
36
+ * - "same": already installed (never installable again);
37
+ * - "backward": the target is a superseded release, so this is a downgrade;
38
+ * - "forward": the latest known release over a known superseded one;
39
+ * - "unrecognized-current": the latest known release over code this SDK cannot
40
+ * place, which may be a newer release, so installing it could be a downgrade;
41
+ * - "unreviewed-target": a release this SDK has no lineage for (newer than the SDK
42
+ * or custom); direction cannot be judged and the caller's review is trusted.
43
+ */
44
+ export type ContractUpgradeDirection = "forward" | "same" | "backward" | "unrecognized-current" | "unreviewed-target";
45
+ export declare function contractUpgradeDirection(role: ContractUpgradeRole, currentHash: string, targetHash: string): ContractUpgradeDirection;
11
46
  export declare function upgradeHash(hash: string): xdr.ScVal;
12
47
  /** Independent of claim versions, claim configuration and fee-token policy. */
13
48
  export declare function contractUpgradeStatus(context: NativeContext, namespace: string, role: ContractUpgradeRole, wasmHash: string): Promise<ContractUpgradeStatus>;
@@ -1,5 +1,44 @@
1
1
  import { NativeClaimError, address, bool, bytes32, exactObject, hex, namespaceNode, sc, u32 } from "./native-codec.js";
2
2
  import { expectedNativeCode } from "./native-code-policy.js";
3
+ /**
4
+ * Reviewed implementation lineage of the governed testnet Registry. Governance
5
+ * approval means "this hash may be installed on any namespace", not "this is a
6
+ * safe transition": superseded releases stay approved, so the chain accepts a
7
+ * downgrade. `latest` is the release owners are offered; `superseded` are the
8
+ * older releases it replaces. A test keeps this equal to the web catalog
9
+ * (web/lib/contractUpdates.ts) and the release record; adding a release means
10
+ * moving the old `latest` into `superseded` here and there.
11
+ */
12
+ export const CONTRACT_RELEASES = {
13
+ registrar: {
14
+ latest: "4d6d18625f4d890028acb794b24871c5c93f96dc15d94cf3d52cbf9713b07fb1",
15
+ superseded: [
16
+ "42dc5f8b40932617d3f2667e1d26dad3e5e9a62c63de1fde7c016cb67a86c420",
17
+ "5c8ae72ea7370ccf19af52b9bbc0007362be94464651ccdf68a2bb1f478b6f2c",
18
+ "61ded153e6311075c1087c5639a5410222e23f7cade239ed2c946150af94b1c5",
19
+ "26e03cd1d7fbfa46fe442406fb1b88022ec8cd8bd9123bed570f971fce596a14",
20
+ ],
21
+ },
22
+ resolver: {
23
+ latest: "941bf87f26cb817f3c6a52a326de855cca8a1dc4477e62a38ec4ffa5c7f43806",
24
+ superseded: [
25
+ "cb927ffa19d6b1f3f0b0187048d885e200adb6de3fddd11ee3cfeb59df9c5c48",
26
+ "351bf4e056b179b578fe2a4ea46f937bfafccc6538c7357ea70021b7038d91ea",
27
+ "c6dfcd7bbf35943a13406888678334d0938d0fea54939906b00c2dc37b4a9a71",
28
+ "5e7b4fb8d310d67ed6966a155e748d60be771422b3e8c34bbc14485f98525574",
29
+ ],
30
+ },
31
+ };
32
+ export function contractUpgradeDirection(role, currentHash, targetHash) {
33
+ const { latest, superseded } = CONTRACT_RELEASES[role];
34
+ if (currentHash === targetHash)
35
+ return "same";
36
+ if (superseded.includes(targetHash))
37
+ return "backward";
38
+ if (targetHash !== latest)
39
+ return "unreviewed-target";
40
+ return superseded.includes(currentHash) ? "forward" : "unrecognized-current";
41
+ }
3
42
  export function upgradeHash(hash) {
4
43
  if (typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash))
5
44
  throw new NativeClaimError("Contract implementation hash must be lowercase hex32");
@@ -0,0 +1,49 @@
1
+ import { type xdr } from "@stellar/stellar-sdk";
2
+ import { type SubnameRecord } from "./names.js";
3
+ export declare const SUBNAME_REGISTRAR_ERRORS: Record<number, string>;
4
+ export declare const subnameLabelArg: (value: string) => xdr.ScVal;
5
+ export declare function subnameParent(input: string): import("./names.js").ParsedResolvableName;
6
+ export declare function subnameParts(input: string): import("./names.js").ParsedResolvableName;
7
+ export declare function createSubnameReader(config: {
8
+ registryId: string;
9
+ error?: (message: string) => Error;
10
+ read: (id: string, fn: string, args: xdr.ScVal[]) => Promise<unknown>;
11
+ }): {
12
+ context: (input: string) => Promise<{
13
+ namespace: string;
14
+ registrar: string;
15
+ owner: string;
16
+ policy: import("./names.js").SubnamePolicy;
17
+ }>;
18
+ creationContext: (input: string) => Promise<{
19
+ namespace: string;
20
+ registrar: string;
21
+ owner: string;
22
+ policy: import("./names.js").SubnamePolicy;
23
+ }>;
24
+ recordAt: (ctx: Awaited<ReturnType<(input: string) => Promise<{
25
+ namespace: string;
26
+ registrar: string;
27
+ owner: string;
28
+ policy: import("./names.js").SubnamePolicy;
29
+ }>>>, parent: string, label: string) => Promise<SubnameRecord | null>;
30
+ record: (name: string) => Promise<SubnameRecord | null>;
31
+ policy: (namespace: string) => Promise<import("./names.js").SubnamePolicy>;
32
+ list: (parent: string, options?: {
33
+ offset?: number;
34
+ limit?: number;
35
+ }) => Promise<{
36
+ parent: string;
37
+ policy: import("./names.js").SubnamePolicy;
38
+ records: {
39
+ parentNode: Uint8Array;
40
+ parentGeneration: bigint;
41
+ generation: bigint;
42
+ address: string;
43
+ active: boolean;
44
+ name: string;
45
+ }[];
46
+ offset: number;
47
+ nextOffset: number | null;
48
+ }>;
49
+ };
@@ -0,0 +1,63 @@
1
+ // Generated by sdk/names/sync.mjs; edit sdk/names/subnames-client.ts.
2
+ import { hash, nativeToScVal, StrKey } from "@stellar/stellar-sdk";
3
+ import { parseNamespace, parseResolvableName, resolvableNameNode, subnamePolicyFromNative, subnameRecordFromNative, subnameLabelsFromNative, subnamePageInput } from "./names.js";
4
+ export const SUBNAME_REGISTRAR_ERRORS = { 53: "SubnameCreationDisabled", 54: "SubnamesSuspended", 55: "SubnameGenerationMismatch", 56: "SubnameIndexUnavailable" };
5
+ const account = (value) => typeof value === "string" && (StrKey.isValidEd25519PublicKey(value) || StrKey.isValidContract(value));
6
+ const contract = (value) => typeof value === "string" && StrKey.isValidContract(value);
7
+ export const subnameLabelArg = (value) => nativeToScVal(new TextEncoder().encode(value), { type: "bytes" });
8
+ export function subnameParent(input) { const parsed = parseResolvableName(input); if (parsed.childLabel)
9
+ throw new Error("parent must be label.namespace"); return parsed; }
10
+ export function subnameParts(input) { const parsed = parseResolvableName(input); if (!parsed.childLabel)
11
+ throw new Error("subname must be child.label.namespace"); return parsed; }
12
+ export function createSubnameReader(config) {
13
+ const read = config.read;
14
+ const failure = (message) => config.error?.(message) ?? new Error(message);
15
+ async function context(input) {
16
+ const namespace = parseNamespace(input);
17
+ if (!contract(config.registryId))
18
+ throw failure("invalid Registry");
19
+ const joined = new Uint8Array(64);
20
+ joined.set(hash(new TextEncoder().encode(namespace)), 32);
21
+ const node = hash(joined);
22
+ const arg = nativeToScVal(node, { type: "bytes" });
23
+ const [registrar, owner] = await Promise.all([read(config.registryId, "registrar_of", [arg]), read(config.registryId, "owner_of", [arg])]);
24
+ if (!contract(registrar) || !account(owner))
25
+ throw failure("namespace has no current Registrar or owner");
26
+ const anchors = await read(registrar, "anchors", []);
27
+ if (!Array.isArray(anchors) || anchors.length !== 2 || anchors[0] !== config.registryId || !(anchors[1] instanceof Uint8Array) || anchors[1].length !== 32 || !node.every((b, i) => b === anchors[1][i]))
28
+ throw failure("subname Registrar context mismatch");
29
+ if (await read(registrar, "subname_version", []) !== 1)
30
+ throw failure("subnames unsupported");
31
+ const policy = subnamePolicyFromNative(await read(registrar, "subname_policy", []));
32
+ return { namespace, registrar, owner, policy };
33
+ }
34
+ async function creationContext(input) {
35
+ const ctx = await context(input), joined = new Uint8Array(64);
36
+ joined.set(hash(new TextEncoder().encode(ctx.namespace)), 32);
37
+ const resolver = await read(config.registryId, "resolver_of", [nativeToScVal(hash(joined), { type: "bytes" })]);
38
+ if (!contract(resolver))
39
+ throw failure("subnames unsupported: no Resolver");
40
+ const [anchor, authority, version] = await Promise.all([read(resolver, "registry", []), read(resolver, "authority", []), read(resolver, "subname_version", [])]);
41
+ if (anchor !== config.registryId || authority !== ctx.registrar)
42
+ throw failure("subname Resolver context mismatch");
43
+ if (version !== 1)
44
+ throw failure("subnames unsupported by Resolver");
45
+ return ctx;
46
+ }
47
+ async function recordAt(ctx, parent, label) {
48
+ const record = subnameRecordFromNative(await read(ctx.registrar, "subname_record", [subnameLabelArg(parent), subnameLabelArg(label)]), resolvableNameNode(`${parent}.${ctx.namespace}`));
49
+ if (record && !account(record.address))
50
+ throw failure("invalid subname destination");
51
+ return record;
52
+ }
53
+ async function record(name) { const parsed = subnameParts(name), ctx = await context(parsed.namespace); return recordAt(ctx, parsed.parentLabel, parsed.childLabel); }
54
+ async function policy(namespace) { return (await context(namespace)).policy; }
55
+ async function list(parent, options = {}) {
56
+ const parsed = subnameParent(parent), { offset, limit } = subnamePageInput(options.offset, options.limit), ctx = await context(parsed.namespace);
57
+ const labels = subnameLabelsFromNative(await read(ctx.registrar, "subname_labels", [subnameLabelArg(parsed.label), nativeToScVal(offset, { type: "u32" }), nativeToScVal(limit, { type: "u32" })]), limit);
58
+ const records = await Promise.all(labels.map(async (label) => { const record = await recordAt(ctx, parsed.label, label); if (!record)
59
+ throw failure("subname index references missing record"); return { name: `${label}.${parsed.name}`, ...record }; }));
60
+ return { parent: parsed.name, policy: ctx.policy, records, offset, nextOffset: labels.length === limit ? offset + labels.length : null };
61
+ }
62
+ return { context, creationContext, recordAt, record, policy, list };
63
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@sorandomains/owner",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Run your Soran namespace on Stellar: configure claims, issue names and manage namespace policies.",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=20"
7
+ "node": ">=22.12.0"
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
@@ -16,7 +16,8 @@
16
16
  "typecheck": "tsc --noEmit",
17
17
  "prepublishOnly": "tsc",
18
18
  "check:browser": "esbuild dist/index.js --bundle --platform=browser --external:@stellar/stellar-sdk --outfile=browser-check.js --legal-comments=none --log-level=error && node -e \"const fs=require('fs');const s=fs.readFileSync('browser-check.js','utf8');fs.unlinkSync('browser-check.js');if(/\\bBuffer\\b/.test(s)){console.error('FAIL: bare Buffer reference in browser bundle');process.exit(1)}console.log('browser bundle clean')\"",
19
- "test": "node --import tsx test/decode-failure.test.mts && node --import tsx --test test/integration.test.mts test/native-*.test.mts && node --import tsx --test test/funding.test.mts test/funding-history.test.mts"
19
+ "test": "node --import tsx test/decode-failure.test.mts && node --import tsx --test test/integration.test.mts test/hardening.test.mts test/error-codes.test.mts test/native-*.test.mts && node --import tsx --test test/funding.test.mts test/funding-history.test.mts test/funding-bounds.test.mts && node --import tsx --test test/multichain.test.mts && node --import tsx --test test/subnames.test.mts",
20
+ "test:hardening": "node --import tsx --test test/hardening.test.mts"
20
21
  },
21
22
  "license": "MIT",
22
23
  "repository": {
@@ -51,5 +52,12 @@
51
52
  "types": "./dist/index.d.ts",
52
53
  "default": "./dist/index.js"
53
54
  }
55
+ },
56
+ "dependencies": {
57
+ "@noble/hashes": "2.4.0",
58
+ "@scure/base": "2.4.0"
59
+ },
60
+ "overrides": {
61
+ "axios": "1.20.0"
54
62
  }
55
63
  }