@sorandomains/owner 0.3.1 → 0.5.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
@@ -1,5 +1,10 @@
1
1
  # @sorandomains/owner
2
2
 
3
+ Version 0.5.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
4
+ and labels are validated before lowercase normalization; Unicode lookalikes are
5
+ rejected. Writes continue to target the owning Registry/Registrar/Resolver. Universal
6
+ Lookup is the read entry point in `@sorandomains/lookup` 0.5.0.
7
+
3
8
  Run your Soran namespace on Stellar, programmatically. Issue names to your
4
9
  users, manage their lifecycle, transfer the namespace, and — when you are
5
10
  ready — walk the one-way permanence door. Every operation is a transaction
@@ -68,3 +73,15 @@ trustless resolve, reverse lookup, and ownership assurance, with no signing
68
73
  code at all.
69
74
 
70
75
  Docs: <https://github.com/SoranDomains/docs> · License: MIT
76
+
77
+ ## Verified testnet deployment
78
+
79
+ Verified on 2026-09-05 at ledger 4515471. Network passphrase: `Test SDF Network ; September 2015`.
80
+
81
+ | Contract | Address |
82
+ |---|---|
83
+ | Registry | `CBSORANXTUFKBZK74AAM2ZM5OX2V7PIXUADM3HGP6WU3IDN7M3YEEDLU` |
84
+
85
+ Mainnet has no deployment preset. Custom networks must supply their own verified
86
+ addresses. Universal Lookup upgrades remain immediately executable; an address
87
+ and ABI version do not pin the code that will execute after a governance upgrade.
package/dist/index.d.ts CHANGED
@@ -40,7 +40,7 @@ export declare const DEPLOYMENTS: {
40
40
  readonly testnet: {
41
41
  readonly rpcUrl: "https://soroban-testnet.stellar.org";
42
42
  readonly passphrase: string;
43
- readonly registryId: "CAUEHYVLLNNDZ4H5QWCPBDWEONRI44SI3XYSEACB4U3HYILIVQGQAMNI";
43
+ readonly registryId: "CBSORANXTUFKBZK74AAM2ZM5OX2V7PIXUADM3HGP6WU3IDN7M3YEEDLU";
44
44
  };
45
45
  };
46
46
  /**
@@ -75,6 +75,8 @@ export declare class OwnerError extends Error {
75
75
  readonly txHash: string | null;
76
76
  constructor(message: string, contractId?: string | null, fn?: string | null, code?: number | null, codeName?: string | null, txHash?: string | null);
77
77
  }
78
+ /** Contract label rules: 1–63 bytes of a-z 0-9, `-` neither first nor last. */
79
+ export declare function normalizeLabel(value: string): string;
78
80
  /** One transaction issues at most this many names (contract MAX_BATCH). */
79
81
  export declare const MAX_BATCH = 23;
80
82
  export type Submitted = {
package/dist/index.js CHANGED
@@ -42,13 +42,15 @@ function extractContractEvents(meta) {
42
42
  try {
43
43
  if (!meta)
44
44
  return [];
45
- if (meta.switch() === 3)
46
- return meta.v3().sorobanMeta()?.events() ?? [];
47
- if (meta.switch() === 4) {
48
- const v4 = meta.v4();
45
+ // (SDK17/js-xdr v5) TransactionMeta discriminates on `.type`; the arm value
46
+ // is `.value`; struct fields (sorobanMeta, operations, events) are properties.
47
+ if (meta.type === "v3")
48
+ return meta.value.sorobanMeta?.events ?? [];
49
+ if (meta.type === "v4") {
50
+ const v4 = meta.value;
49
51
  return [
50
- ...v4.operations().flatMap((op) => op.events()),
51
- ...v4.events().map((te) => te.event()),
52
+ ...v4.operations.flatMap((op) => op.events),
53
+ ...v4.events.map((te) => te.event),
52
54
  ];
53
55
  }
54
56
  return [];
@@ -62,16 +64,16 @@ function decodeIssuedEvents(events, registrarId) {
62
64
  const out = [];
63
65
  for (const ev of events) {
64
66
  try {
65
- const cid = ev.contractId();
67
+ const cid = ev.contractId; // (SDK17) property, ContractId | null
66
68
  if (!cid)
67
69
  continue;
68
- if (Address.contract(cid).toString() !== registrarId)
70
+ if (Address.contract(cid.toBytes()).toString() !== registrarId)
69
71
  continue;
70
- const body = ev.body().v0();
71
- const topics = body.topics();
72
+ const body = ev.body.value; // ContractEventBody → ContractEventV0
73
+ const topics = body.topics;
72
74
  if (topics.length < 1 || scValToNative(topics[0]) !== "issued")
73
75
  continue;
74
- const data = scValToNative(body.data());
76
+ const data = scValToNative(body.data);
75
77
  if (!(data?.[0] instanceof Uint8Array) || typeof data?.[1] !== "string")
76
78
  continue;
77
79
  out.push({ label: new TextDecoder().decode(data[0]), holder: data[1] });
@@ -92,7 +94,7 @@ export const DEPLOYMENTS = {
92
94
  passphrase: Networks.TESTNET,
93
95
  // The immutable Registry. The Registrar for a namespace is discovered on
94
96
  // chain via `registrar_of(node)` — never configured by hand.
95
- registryId: "CAUEHYVLLNNDZ4H5QWCPBDWEONRI44SI3XYSEACB4U3HYILIVQGQAMNI",
97
+ registryId: "CBSORANXTUFKBZK74AAM2ZM5OX2V7PIXUADM3HGP6WU3IDN7M3YEEDLU",
96
98
  },
97
99
  };
98
100
  /** A TxSigner over a raw secret key — for backends and scripts. */
@@ -203,6 +205,13 @@ function typedError(contractId, fn, raw, names, txHash = null) {
203
205
  // ---------------------------------------------------------------------------
204
206
  const LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
205
207
  /** Contract label rules: 1–63 bytes of a-z 0-9, `-` neither first nor last. */
208
+ export function normalizeLabel(value) {
209
+ if (typeof value !== "string" || /[^\x00-\x7f]/.test(value))
210
+ throw new OwnerError("label must contain ASCII characters only");
211
+ const normalized = value.toLowerCase();
212
+ assertLabel(normalized);
213
+ return normalized;
214
+ }
206
215
  function assertLabel(label) {
207
216
  if (label.length < 1 || label.length > 63 || !LABEL_RE.test(label)) {
208
217
  throw new OwnerError(`invalid label "${label}" — 1-63 chars of a-z, 0-9, and non-edge hyphens`);
@@ -278,7 +287,7 @@ export class SoranOwner {
278
287
  // ---- discovery -----------------------------------------------------------
279
288
  /** The namespace's on-chain attested Registrar id. Cached per instance. */
280
289
  async registrarOf(namespace) {
281
- assertLabel(namespace);
290
+ namespace = normalizeLabel(namespace);
282
291
  const hit = this.registrars.get(namespace);
283
292
  if (hit && Date.now() - hit.at < SoranOwner.REGISTRAR_TTL_MS)
284
293
  return hit.value;
@@ -293,7 +302,7 @@ export class SoranOwner {
293
302
  }
294
303
  /** The namespace's current owner on the Registry, or null if unallocated. */
295
304
  async namespaceOwner(namespace) {
296
- assertLabel(namespace);
305
+ namespace = normalizeLabel(namespace);
297
306
  return (await this.read(this.registryId, "owner_of", [
298
307
  nodeArg(namehash(namespace)),
299
308
  ]));
@@ -317,7 +326,7 @@ export class SoranOwner {
317
326
  // ---- registrar: issuance -------------------------------------------------
318
327
  /** Issue `label.namespace` to `holder`. Owner-signed; term set by policy. */
319
328
  async issue(namespace, label, holder) {
320
- assertLabel(label);
329
+ label = normalizeLabel(label);
321
330
  const registrarId = await this.registrarOf(namespace);
322
331
  const r = await this.invoke(registrarId, "issue", [labelArg(label), addrArg(holder)], REGISTRAR_ERRORS);
323
332
  return { hash: r.hash, ledger: r.ledger, node: toHex(r.returnValue) };
@@ -336,8 +345,7 @@ export class SoranOwner {
336
345
  if (entries.length > MAX_BATCH) {
337
346
  throw new OwnerError(`issueBatch: ${entries.length} entries exceeds the contract cap of ${MAX_BATCH} per transaction — split the batch`);
338
347
  }
339
- for (const e of entries)
340
- assertLabel(e.label);
348
+ entries = entries.map(e => ({ ...e, label: normalizeLabel(e.label) }));
341
349
  const registrarId = await this.registrarOf(namespace);
342
350
  const r = await this.invoke(registrarId, "issue_batch", [
343
351
  xdr.ScVal.scvVec(entries.map((e) => labelArg(e.label))),
@@ -422,14 +430,14 @@ export class SoranOwner {
422
430
  * policy never allowed it and after `make_permanent` removed it forever.
423
431
  */
424
432
  async reclaim(namespace, label) {
425
- assertLabel(label);
433
+ label = normalizeLabel(label);
426
434
  const registrarId = await this.registrarOf(namespace);
427
435
  const r = await this.invoke(registrarId, "reclaim", [labelArg(label)], REGISTRAR_ERRORS);
428
436
  return { hash: r.hash, ledger: r.ledger };
429
437
  }
430
438
  /** Extend a finite-term name. Returns the new expiry (unix seconds). */
431
439
  async renew(namespace, label, extendSecs) {
432
- assertLabel(label);
440
+ label = normalizeLabel(label);
433
441
  const registrarId = await this.registrarOf(namespace);
434
442
  const r = await this.invoke(registrarId, "renew", [labelArg(label), nativeToScVal(extendSecs, { type: "u64" })], REGISTRAR_ERRORS);
435
443
  return { hash: r.hash, ledger: r.ledger, expiresAt: BigInt(r.returnValue) };
@@ -461,19 +469,19 @@ export class SoranOwner {
461
469
  * accepts; re-proposing replaces the pending offer, cancel withdraws it.
462
470
  */
463
471
  async proposeNamespaceTransfer(namespace, to) {
464
- assertLabel(namespace);
472
+ namespace = normalizeLabel(namespace);
465
473
  const r = await this.invoke(this.registryId, "propose_transfer", [nodeArg(namehash(namespace)), addrArg(to)], REGISTRY_ERRORS);
466
474
  return { hash: r.hash, ledger: r.ledger };
467
475
  }
468
476
  /** Accept a namespace offered to you. Signer must be the PROPOSED owner. */
469
477
  async acceptNamespaceTransfer(namespace) {
470
- assertLabel(namespace);
478
+ namespace = normalizeLabel(namespace);
471
479
  const r = await this.invoke(this.registryId, "accept_transfer", [nodeArg(namehash(namespace))], REGISTRY_ERRORS);
472
480
  return { hash: r.hash, ledger: r.ledger };
473
481
  }
474
482
  /** Withdraw a pending namespace transfer. Current owner only. */
475
483
  async cancelNamespaceTransfer(namespace) {
476
- assertLabel(namespace);
484
+ namespace = normalizeLabel(namespace);
477
485
  const r = await this.invoke(this.registryId, "cancel_transfer", [nodeArg(namehash(namespace))], REGISTRY_ERRORS);
478
486
  return { hash: r.hash, ledger: r.ledger };
479
487
  }
@@ -482,7 +490,7 @@ export class SoranOwner {
482
490
  * Refused (ResolverFrozen) once the namespace is permanent.
483
491
  */
484
492
  async setResolver(namespace, resolver) {
485
- assertLabel(namespace);
493
+ namespace = normalizeLabel(namespace);
486
494
  const r = await this.invoke(this.registryId, "set_resolver", [nodeArg(namehash(namespace)), resolver === null ? xdr.ScVal.scvVoid() : addrArg(resolver)], REGISTRY_ERRORS);
487
495
  return { hash: r.hash, ledger: r.ledger };
488
496
  }
@@ -506,13 +514,13 @@ export class SoranOwner {
506
514
  }
507
515
  /** Current state of `label.namespace`, or null if never issued. */
508
516
  async nameState(namespace, label) {
509
- assertLabel(label);
517
+ label = normalizeLabel(label);
510
518
  const registrarId = await this.registrarOf(namespace);
511
519
  return this.readRecord(registrarId, label);
512
520
  }
513
521
  /** The pending namespace transfer on the Registry, or null. */
514
522
  async pendingNamespaceTransfer(namespace) {
515
- assertLabel(namespace);
523
+ namespace = normalizeLabel(namespace);
516
524
  return this.read(this.registryId, "pending_transfer", [nodeArg(namehash(namespace))]);
517
525
  }
518
526
  // ---- internals -----------------------------------------------------------
@@ -598,12 +606,12 @@ export class SoranOwner {
598
606
  assertSatisfiableAuth(prepared, pub, contractId, fn) {
599
607
  const op = prepared.operations[0];
600
608
  for (const entry of op?.auth ?? []) {
601
- const cred = entry.credentials();
602
- if (cred.switch() !== xdr.SorobanCredentialsType.sorobanCredentialsAddress())
609
+ const cred = entry.credentials;
610
+ if (cred.type !== "sorobanCredentialsAddress")
603
611
  continue;
604
612
  let required;
605
613
  try {
606
- required = Address.fromScAddress(cred.address().address()).toString();
614
+ required = Address.fromScAddress(cred.address.address).toString();
607
615
  }
608
616
  catch {
609
617
  continue; // unreadable credential — let the chain be the judge
@@ -621,7 +629,10 @@ export class SoranOwner {
621
629
  .setTimeout(this.timeoutSecs)
622
630
  .build();
623
631
  let tx = build(await this.sourceAccount(pub, fn));
624
- let sim = await this.server.simulateTransaction(tx);
632
+ // (SDK17/P28) useUpgradedAuth=false keeps legacy V1 SorobanCredentials rather
633
+ // than CAP-71 address-bound V2 — valid against P27 (pre-vote) and P28 (post),
634
+ // and keeps assertSatisfiableAuth's `sorobanCredentialsAddress` check exact.
635
+ let sim = await this.server.simulateTransaction(tx, undefined, undefined, false);
625
636
  // Archived entries (e.g. a dormant Registrar's instance) need restoring
626
637
  // before the call can run. Each restore is its own owner-signed
627
638
  // transaction; two rounds cover an entry archiving mid-flow, and beyond
@@ -632,7 +643,7 @@ export class SoranOwner {
632
643
  }
633
644
  await this.restore(sim, pub);
634
645
  tx = build(await this.sourceAccount(pub, fn));
635
- sim = await this.server.simulateTransaction(tx);
646
+ sim = await this.server.simulateTransaction(tx, undefined, undefined, false);
636
647
  }
637
648
  if (rpc.Api.isSimulationError(sim)) {
638
649
  throw typedError(contractId, fn, sim.error, errNames);
@@ -641,7 +652,7 @@ export class SoranOwner {
641
652
  this.assertSatisfiableAuth(prepared, pub, contractId, fn);
642
653
  // The hash is fixed before signatures — compute it now so every failure
643
654
  // past this point can carry it (the "re-check before retrying" contract).
644
- const txHash = prepared.hash().toString("hex");
655
+ const txHash = toHex(prepared.hash()); // (SDK17) hash() is Uint8Array
645
656
  const signed = await this.signEnvelope(prepared.toXDR());
646
657
  const envelope = TransactionBuilder.fromXDR(signed, this.passphrase);
647
658
  let sent;
@@ -722,18 +733,18 @@ export class SoranOwner {
722
733
  let code = null;
723
734
  try {
724
735
  const meta = got.resultMetaXdr;
725
- const diags = got.diagnosticEventsXdr ?? (meta && meta.switch() === 3
726
- ? (meta.v3().sorobanMeta()?.diagnosticEvents() ?? [])
727
- : meta && meta.switch() === 4
728
- ? meta.v4().diagnosticEvents()
736
+ const diags = got.diagnosticEventsXdr ?? (meta && meta.type === "v3"
737
+ ? (meta.value.sorobanMeta?.diagnosticEvents ?? [])
738
+ : meta && meta.type === "v4"
739
+ ? meta.value.diagnosticEvents
729
740
  : []);
730
741
  outer: for (const d of diags) {
731
- const body = d.event().body().v0();
732
- for (const v of [...body.topics(), body.data()]) {
733
- if (v.switch() === xdr.ScValType.scvError()) {
734
- const err = v.error();
735
- if (err.switch() === xdr.ScErrorType.sceContract()) {
736
- code = err.contractCode();
742
+ const body = d.event.body.value;
743
+ for (const v of [...body.topics, body.data]) {
744
+ if (v.type === "scvError") {
745
+ const err = v.error;
746
+ if (err.type === "sceContract") {
747
+ code = err.contractCode;
737
748
  break outer;
738
749
  }
739
750
  }
@@ -745,7 +756,7 @@ export class SoranOwner {
745
756
  }
746
757
  let resultCode = `tx status ${got.status}`;
747
758
  try {
748
- resultCode = got.resultXdr?.result().switch().name ?? resultCode;
759
+ resultCode = got.resultXdr?.result.type ?? resultCode;
749
760
  }
750
761
  catch {
751
762
  /* keep the plain status */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorandomains/owner",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Run your Soran namespace on Stellar \u2014 issue names, manage lifecycle, transfer, and walk the one-way permanence door.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,7 +12,8 @@
12
12
  "build": "tsc",
13
13
  "typecheck": "tsc --noEmit",
14
14
  "prepublishOnly": "tsc",
15
- "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')\""
15
+ "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')\"",
16
+ "test": "node --import tsx test/decode-failure.test.mts && node --import tsx --test test/integration.test.mts"
16
17
  },
17
18
  "license": "MIT",
18
19
  "repository": {
@@ -33,14 +34,14 @@
33
34
  "issuance"
34
35
  ],
35
36
  "peerDependencies": {
36
- "@stellar/stellar-sdk": ">=13 <17"
37
+ "@stellar/stellar-sdk": ">=17 <18"
37
38
  },
38
39
  "devDependencies": {
39
- "@stellar/stellar-sdk": "^16.2.0",
40
+ "@stellar/stellar-sdk": "^17.0.1",
40
41
  "@types/node": "^22.20.1",
42
+ "esbuild": "^0.25.0",
41
43
  "tsx": "^4.19.2",
42
- "typescript": "^5.7.3",
43
- "esbuild": "^0.25.0"
44
+ "typescript": "^5.7.3"
44
45
  },
45
46
  "exports": {
46
47
  ".": {