@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 +17 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +53 -42
- package/package.json +7 -6
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: "
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
if (meta.
|
|
48
|
-
|
|
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
|
|
51
|
-
...v4.events
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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()
|
|
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.
|
|
726
|
-
? (meta.
|
|
727
|
-
: meta && meta.
|
|
728
|
-
? meta.
|
|
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
|
|
732
|
-
for (const v of [...body.topics
|
|
733
|
-
if (v.
|
|
734
|
-
const err = v.error
|
|
735
|
-
if (err.
|
|
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
|
|
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
|
+
"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": ">=
|
|
37
|
+
"@stellar/stellar-sdk": ">=17 <18"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@stellar/stellar-sdk": "^
|
|
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
|
".": {
|