@sorandomains/owner 0.5.1 → 0.7.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,9 +1,18 @@
1
1
  # @sorandomains/owner
2
2
 
3
- Version 0.5.1 targets Stellar SDK17 (`>=17 <18`). ASCII names
3
+
4
+ > Native claim testnet deployment verified at ledger 4534629 on 6 September 2026 (12:12 UTC). See the
5
+ > [release status](https://docs.soran.domains/reference/release-status) for package and service availability.
6
+
7
+
8
+ Version 0.7.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
4
9
  and labels are validated before lowercase normalization; Unicode lookalikes are
5
10
  rejected. Writes continue to target the owning Registry/Registrar/Resolver. Universal
6
- Lookup is the read entry point in `@sorandomains/lookup` 0.5.1.
11
+ Lookup is the read entry point in `@sorandomains/lookup` 0.7.0.
12
+
13
+ Ownership, issuer, holder and treasury inputs remain G/C account or contract
14
+ addresses. Muxed M addresses are payment destinations only: issue the name to its
15
+ actual G/C holder, then that holder can call Holder `setPayment` to publish M.
7
16
 
8
17
  Run your Soran namespace on Stellar, programmatically. Issue names to your
9
18
  users, manage their lifecycle, transfer the namespace, and — when you are
@@ -74,13 +83,20 @@ code at all.
74
83
 
75
84
  Docs: <https://github.com/SoranDomains/docs> · License: MIT
76
85
 
86
+
87
+ ## Native username claiming
88
+
89
+ `claimPolicy` and `nativeClaimCapability` read the current on-chain setup. Configure it with `configureClaims`, `setClaimsEnabled`/`pauseClaims`, `reserveNames`, `releaseReservations` and `assignReserved`. Manual mode retains ordinary owner issuance; Public mode routes ordinary issuance through the public claim rules, while explicit reserved assignment remains an owner power. The default disabled policy is not an open registration service.
90
+
91
+ Read the [native claim APIs, security boundaries and complete signup flow](https://github.com/SoranDomains/sdk/blob/main/NATIVE-CLAIMS.md). G/no memo, G with ID/Text/Hash, full M/no separate memo and C/no memo remain supported payment destinations. Current transaction-signing adapters use classic G accounts.
92
+
77
93
  ## Verified testnet deployment
78
94
 
79
- Verified on 2026-09-05 at ledger 4520986. Network passphrase: `Test SDF Network ; September 2015`.
95
+ Verified on 6 September 2026 at ledger **4534629** (12:12 UTC). Network passphrase: `Test SDF Network ; September 2015`.
80
96
 
81
97
  | Contract | Address |
82
98
  |---|---|
83
- | Registry | `CDSORANCV3IFF3MKHJ7KI4MKEJOJZFMTDVAZCD5XFOR4WTGNXJJNOKQE` |
99
+ | Registry | `CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK` |
84
100
 
85
101
  Mainnet has no deployment preset. Custom networks must supply their own verified
86
102
  addresses. Universal Lookup upgrades remain immediately executable; an address
package/dist/index.d.ts CHANGED
@@ -35,12 +35,27 @@
35
35
  * that one only. The few reads here (policy, pending transfers, records) exist
36
36
  * to support write flows.
37
37
  */
38
+ import { type NativeWriteOptions, type NativeCapability } from "./native-transport.js";
39
+ import { type ClaimSettings, type ClaimConfig, type ClaimUsage, type ApprovalUsage } from "./native-types.js";
40
+ export * from "./native-types.js";
41
+ export * from "./native-allowlist.js";
42
+ export { NativeClaimError, namespaceNode as nativeNamespaceNode, paymentDestinationToScVal } from "./native-codec.js";
43
+ export { validateNativeTransaction, authorizedInvocation, type NativeAuthorizationPlan } from "./native-auth.js";
44
+ export type { NativeCapability, NativePrepared, NativeWriteOptions } from "./native-transport.js";
38
45
  /** Known public deployments. Pass explicit options for anything else. */
39
46
  export declare const DEPLOYMENTS: {
40
47
  readonly testnet: {
41
48
  readonly rpcUrl: "https://soroban-testnet.stellar.org";
42
49
  readonly passphrase: string;
43
- readonly registryId: "CDSORANCV3IFF3MKHJ7KI4MKEJOJZFMTDVAZCD5XFOR4WTGNXJJNOKQE";
50
+ readonly registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK";
51
+ };
52
+ };
53
+ /** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
54
+ export declare const LEGACY_DEPLOYMENTS: {
55
+ readonly testnet20260905: {
56
+ readonly rpcUrl: "https://soroban-testnet.stellar.org";
57
+ readonly passphrase: string;
58
+ readonly registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H";
44
59
  };
45
60
  };
46
61
  /**
@@ -130,6 +145,8 @@ export type NameState = {
130
145
  generation: bigint;
131
146
  };
132
147
  export type OwnerOptions = {
148
+ /** Upper total network fee for successor native operations; default 5 XLM. */
149
+ maxNativeFeeStroops?: bigint;
133
150
  /** Signs every transaction. The namespace owner's account — or, for
134
151
  * `acceptNamespaceTransfer`, the proposed new owner's. */
135
152
  signer: TxSigner;
@@ -150,6 +167,7 @@ export type OwnerOptions = {
150
167
  fee?: string;
151
168
  };
152
169
  export declare class SoranOwner {
170
+ private maxNativeFeeStroops;
153
171
  private server;
154
172
  private passphrase;
155
173
  private registryId;
@@ -161,6 +179,38 @@ export declare class SoranOwner {
161
179
  private static REGISTRAR_TTL_MS;
162
180
  constructor(opts: OwnerOptions);
163
181
  /** The namespace's on-chain attested Registrar id. Cached per instance. */
182
+ private nativeConfirmed;
183
+ private nativeContext;
184
+ /** Native successor capability. A failed RPC read never means open or legacy. */
185
+ nativeClaimCapability(namespace: string): Promise<NativeCapability>;
186
+ claimPolicy(namespace: string): Promise<ClaimConfig | null>;
187
+ nativeClaimFeeToken(namespace: string): Promise<string>;
188
+ private nativeOwnerWrite;
189
+ configureClaims(namespace: string, settings: ClaimSettings, options?: NativeWriteOptions): Promise<Submitted & {
190
+ config: ClaimConfig;
191
+ }>;
192
+ setClaimsEnabled(namespace: string, enabled: boolean, options?: NativeWriteOptions): Promise<Submitted & {
193
+ config: ClaimConfig;
194
+ }>;
195
+ pauseClaims(namespace: string, options?: NativeWriteOptions): Promise<Submitted & {
196
+ config: ClaimConfig;
197
+ }>;
198
+ private updateReservations;
199
+ reserveNames(namespace: string, labels: readonly string[], options?: NativeWriteOptions): Promise<{
200
+ hash: string;
201
+ ledger: number;
202
+ updated: number;
203
+ }>;
204
+ releaseReservations(namespace: string, labels: readonly string[], options?: NativeWriteOptions): Promise<{
205
+ hash: string;
206
+ ledger: number;
207
+ updated: number;
208
+ }>;
209
+ isReserved(namespace: string, label: string): Promise<boolean>;
210
+ /** Privileged reserved assignment publishes the holder's default route; no custom destination is implied. */
211
+ assignReserved(namespace: string, label: string, holder: string, options?: NativeWriteOptions): Promise<IssueResult>;
212
+ nativeClaimUsage(namespace: string, holder: string): Promise<ClaimUsage>;
213
+ nativeApprovalUsage(namespace: string): Promise<ApprovalUsage>;
164
214
  registrarOf(namespace: string): Promise<string>;
165
215
  /** The namespace's current owner on the Registry, or null if unallocated. */
166
216
  namespaceOwner(namespace: string): Promise<string | null>;
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@
35
35
  * that one only. The few reads here (policy, pending transfers, records) exist
36
36
  * to support write flows.
37
37
  */
38
- import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, xdr, } from "@stellar/stellar-sdk";
38
+ import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, xdr, } from "@stellar/stellar-sdk";
39
39
  /** Contract events from a transaction's meta, across meta versions (v3 puts
40
40
  * them on sorobanMeta; v4 nests them per-operation and in tx events). */
41
41
  function extractContractEvents(meta) {
@@ -84,6 +84,13 @@ function decodeIssuedEvents(events, registrarId) {
84
84
  }
85
85
  return out;
86
86
  }
87
+ import { NativeClaimError, address as nativeAddress, bool as nativeBool, bytes32 as nativeBytes32, hex as nativeHex, sc as nativeSc } from "./native-codec.js";
88
+ import { requireNative, nativeCapability, sendNative } from "./native-transport.js";
89
+ import { claimSettingsToScVal, claimConfigFromNative, claimLabelToScVal, claimUsageFromNative, approvalUsageFromNative } from "./native-types.js";
90
+ export * from "./native-types.js";
91
+ export * from "./native-allowlist.js";
92
+ export { NativeClaimError, namespaceNode as nativeNamespaceNode, paymentDestinationToScVal } from "./native-codec.js";
93
+ export { validateNativeTransaction, authorizedInvocation } from "./native-auth.js";
87
94
  // ---------------------------------------------------------------------------
88
95
  // Deployments
89
96
  // ---------------------------------------------------------------------------
@@ -94,7 +101,17 @@ export const DEPLOYMENTS = {
94
101
  passphrase: Networks.TESTNET,
95
102
  // The immutable Registry. The Registrar for a namespace is discovered on
96
103
  // chain via `registrar_of(node)` — never configured by hand.
97
- registryId: "CDSORANCV3IFF3MKHJ7KI4MKEJOJZFMTDVAZCD5XFOR4WTGNXJJNOKQE",
104
+ registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK",
105
+ },
106
+ };
107
+ /** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
108
+ export const LEGACY_DEPLOYMENTS = {
109
+ testnet20260905: {
110
+ rpcUrl: "https://soroban-testnet.stellar.org",
111
+ passphrase: Networks.TESTNET,
112
+ // The immutable Registry. The Registrar for a namespace is discovered on
113
+ // chain via `registrar_of(node)` — never configured by hand.
114
+ registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H",
98
115
  },
99
116
  };
100
117
  /** A TxSigner over a raw secret key — for backends and scripts. */
@@ -134,6 +151,7 @@ const REGISTRAR_ERRORS = {
134
151
  18: "InvalidPolicy",
135
152
  19: "ExpiryOverflow",
136
153
  20: "InvalidRegistry",
154
+ 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",
137
155
  };
138
156
  /** Registry contract error codes, by number. */
139
157
  const REGISTRY_ERRORS = {
@@ -236,7 +254,11 @@ function namehash(namespace) {
236
254
  return new Uint8Array(hash(concatBytes(new Uint8Array(32), labelHash)));
237
255
  }
238
256
  const labelArg = (label) => nativeToScVal(utf8(label), { type: "bytes" });
239
- const addrArg = (address) => nativeToScVal(address, { type: "address" });
257
+ const addrArg = (address) => {
258
+ if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address))
259
+ throw new OwnerError("ownership and operator addresses must be G or C; muxed M is a payment destination only");
260
+ return nativeToScVal(address, { type: "address" });
261
+ };
240
262
  const nodeArg = (node) => nativeToScVal(node, { type: "bytes" });
241
263
  function toHex(v) {
242
264
  if (!(v instanceof Uint8Array)) {
@@ -252,6 +274,7 @@ export const MAX_BATCH = 23;
252
274
  // The client
253
275
  // ---------------------------------------------------------------------------
254
276
  export class SoranOwner {
277
+ maxNativeFeeStroops;
255
278
  server;
256
279
  passphrase;
257
280
  registryId;
@@ -267,6 +290,9 @@ export class SoranOwner {
267
290
  registrars = new Map();
268
291
  static REGISTRAR_TTL_MS = 30_000;
269
292
  constructor(opts) {
293
+ this.maxNativeFeeStroops = opts?.maxNativeFeeStroops ?? 50000000n;
294
+ if (typeof this.maxNativeFeeStroops !== "bigint" || this.maxNativeFeeStroops <= 0n || this.maxNativeFeeStroops > 4294967295n)
295
+ throw new NativeClaimError("maximum native network fee must be positive");
270
296
  if (!opts?.signer)
271
297
  throw new OwnerError("OwnerOptions.signer is required");
272
298
  const d = DEPLOYMENTS[opts.network ?? "testnet"];
@@ -286,6 +312,85 @@ export class SoranOwner {
286
312
  }
287
313
  // ---- discovery -----------------------------------------------------------
288
314
  /** The namespace's on-chain attested Registrar id. Cached per instance. */
315
+ nativeConfirmed(result, decode) {
316
+ try {
317
+ return decode(result.value);
318
+ }
319
+ catch (error) {
320
+ throw new NativeClaimError(`transaction confirmed but its result could not be verified: ${String(error)}`, "pending", result.hash);
321
+ }
322
+ }
323
+ nativeContext() {
324
+ return { registryId: this.registryId, passphrase: this.passphrase, server: this.server, signer: this.signer, fee: this.fee, timeoutSecs: this.timeoutSecs, maxFeeStroops: this.maxNativeFeeStroops,
325
+ read: (id, method, args) => this.read(id, method, args), serialize: work => this.serialize(work) };
326
+ }
327
+ /** Native successor capability. A failed RPC read never means open or legacy. */
328
+ nativeClaimCapability(namespace) { return nativeCapability(this.nativeContext(), normalizeLabel(namespace)); }
329
+ async claimPolicy(namespace) {
330
+ const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
331
+ const raw = await this.read(cap.registrar, "claim_config", []);
332
+ return raw === null ? null : claimConfigFromNative(raw);
333
+ }
334
+ async nativeClaimFeeToken(namespace) {
335
+ const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
336
+ return nativeAddress(await this.read(cap.registrar, "native_fee_token", []), "contract", "native XLM token");
337
+ }
338
+ async nativeOwnerWrite(namespace, method, args, options = {}) {
339
+ return this.serialize(async () => {
340
+ const context = this.nativeContext();
341
+ const cap = await requireNative(context, normalizeLabel(namespace));
342
+ const source = nativeAddress(await this.signer.publicKey(), "account", "owner transaction source");
343
+ if (cap.owner !== source)
344
+ throw new NativeClaimError("the current namespace owner must authorize this change", "authorization");
345
+ return sendNative(context, { source, contract: cap.registrar, method, args, sourceInvocation: { contract: cap.registrar, method, args }, maxFeeStroops: context.maxFeeStroops }, options);
346
+ });
347
+ }
348
+ async configureClaims(namespace, settings, options = {}) {
349
+ const encoded = claimSettingsToScVal(settings);
350
+ const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
351
+ const token = nativeAddress(await this.read(cap.registrar, "native_fee_token", []), "contract", "native fee token");
352
+ if (settings.feeToken !== token)
353
+ throw new NativeClaimError("username fee must use this network's canonical XLM token");
354
+ if (settings.admission.type === "approval" && settings.admission.account === cap.owner)
355
+ throw new NativeClaimError("eligibility account must be separate from namespace owner");
356
+ const result = await this.nativeOwnerWrite(namespace, "configure_claims", [encoded], options);
357
+ return { hash: result.hash, ledger: result.ledger, config: this.nativeConfirmed(result, claimConfigFromNative) };
358
+ }
359
+ async setClaimsEnabled(namespace, enabled, options = {}) {
360
+ const result = await this.nativeOwnerWrite(namespace, "set_claim_enabled", [nativeSc.bool(nativeBool(enabled, "claim enabled"))], options);
361
+ return { hash: result.hash, ledger: result.ledger, config: this.nativeConfirmed(result, claimConfigFromNative) };
362
+ }
363
+ pauseClaims(namespace, options = {}) { return this.setClaimsEnabled(namespace, false, options); }
364
+ async updateReservations(namespace, labels, reserved, options) {
365
+ if (!Array.isArray(labels) || labels.length < 1 || labels.length > 23)
366
+ throw new NativeClaimError("reservation batches must contain 1–23 labels");
367
+ const normalized = labels.map(normalizeLabel);
368
+ if (new Set(normalized).size !== normalized.length)
369
+ throw new NativeClaimError("duplicate reservation label");
370
+ const result = await this.nativeOwnerWrite(namespace, "set_reserved", [xdr.ScVal.scvVec(normalized.map(claimLabelToScVal)), nativeSc.bool(reserved)], options);
371
+ if (typeof result.value !== "number" || result.value !== normalized.length)
372
+ throw new NativeClaimError("confirmed reservation result differs from batch; inspect the transaction before retrying", "pending", result.hash);
373
+ return { hash: result.hash, ledger: result.ledger, updated: result.value };
374
+ }
375
+ reserveNames(namespace, labels, options = {}) { return this.updateReservations(namespace, labels, true, options); }
376
+ releaseReservations(namespace, labels, options = {}) { return this.updateReservations(namespace, labels, false, options); }
377
+ async isReserved(namespace, label) {
378
+ const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
379
+ return nativeBool(await this.read(cap.registrar, "is_reserved", [claimLabelToScVal(normalizeLabel(label))]), "reservation");
380
+ }
381
+ /** Privileged reserved assignment publishes the holder's default route; no custom destination is implied. */
382
+ async assignReserved(namespace, label, holder, options = {}) {
383
+ const result = await this.nativeOwnerWrite(namespace, "issue_reserved", [claimLabelToScVal(normalizeLabel(label)), nativeSc.address(nativeAddress(holder, "identity", "reserved holder"))], options);
384
+ return { hash: result.hash, ledger: result.ledger, node: this.nativeConfirmed(result, raw => nativeHex(nativeBytes32(raw, "issued name node"))) };
385
+ }
386
+ async nativeClaimUsage(namespace, holder) {
387
+ const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
388
+ return claimUsageFromNative(await this.read(cap.registrar, "claim_usage", [nativeSc.address(nativeAddress(holder, "identity", "holder"))]));
389
+ }
390
+ async nativeApprovalUsage(namespace) {
391
+ const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
392
+ return approvalUsageFromNative(await this.read(cap.registrar, "approval_usage", []));
393
+ }
289
394
  async registrarOf(namespace) {
290
395
  namespace = normalizeLabel(namespace);
291
396
  const hit = this.registrars.get(namespace);
@@ -557,7 +662,7 @@ export class SoranOwner {
557
662
  throw new OwnerError(`${fn}: the on-chain entry is archived (rent lapsed) — any write operation restores it automatically, or restore/touch it first`, contractId, fn);
558
663
  }
559
664
  if (!rpc.Api.isSimulationSuccess(sim) || !sim.result?.retval)
560
- return null;
665
+ throw new OwnerError(`${fn}: missing simulation return value`, contractId, fn);
561
666
  const v = scValToNative(sim.result.retval);
562
667
  return v === undefined ? null : v;
563
668
  }
@@ -0,0 +1,21 @@
1
+ export type ClaimAllowlistContext = {
2
+ network: string;
3
+ registry: string;
4
+ namespace: string;
5
+ registrar: string;
6
+ };
7
+ export type ClaimAllowlistBundle = {
8
+ version: 1;
9
+ context: ClaimAllowlistContext;
10
+ root: string;
11
+ entries: Array<{
12
+ account: string;
13
+ leaf: string;
14
+ proof: string[];
15
+ }>;
16
+ };
17
+ export declare function claimAllowlistLeaf(context: ClaimAllowlistContext, account: string): string;
18
+ export declare function claimAllowlistParent(left: string, right: string): string;
19
+ /** A complete list is required to generate a replacement root. Keep/distribute the returned proof bundle. */
20
+ export declare function buildClaimAllowlist(context: ClaimAllowlistContext, accounts: readonly string[]): ClaimAllowlistBundle;
21
+ export declare function verifyClaimAllowlistProof(context: ClaimAllowlistContext, account: string, proof: readonly string[], root: string): boolean;
@@ -0,0 +1,34 @@
1
+ import { hash, xdr } from "@stellar/stellar-sdk";
2
+ import { NativeClaimError, address, hex, hex32, sc, unhex } from "./native-codec.js";
3
+ export function claimAllowlistLeaf(context, account) {
4
+ return hex(hash(xdr.ScVal.scvVec([sc.symbol("allow_v1"), sc.bytes(unhex(context.network)), sc.address(address(context.registry, "contract", "Registry")), sc.bytes(unhex(context.namespace)), sc.address(address(context.registrar, "contract", "Registrar")), sc.address(address(account, "account", "allowlisted claimant"))]).toXDR()));
5
+ }
6
+ export function claimAllowlistParent(left, right) { hex32(left, "left leaf"); hex32(right, "right leaf"); const [first, second] = left < right ? [left, right] : [right, left]; const bytes = new Uint8Array(65); bytes[0] = 1; bytes.set(unhex(first), 1); bytes.set(unhex(second), 33); return hex(hash(bytes)); }
7
+ /** A complete list is required to generate a replacement root. Keep/distribute the returned proof bundle. */
8
+ export function buildClaimAllowlist(context, accounts) {
9
+ if (!Array.isArray(accounts) || accounts.length === 0 || accounts.length > 65536)
10
+ throw new NativeClaimError("allowlist requires 1–65536 distinct G accounts");
11
+ const unique = new Set();
12
+ const entries = accounts.map(account => { address(account, "account", "allowlisted claimant"); if (unique.has(account))
13
+ throw new NativeClaimError("duplicate allowlist account"); unique.add(account); return { account, leaf: claimAllowlistLeaf(context, account), proof: [] }; }).sort((a, b) => a.leaf.localeCompare(b.leaf));
14
+ let layer = entries.map((entry, index) => ({ hash: entry.leaf, indices: [index] }));
15
+ while (layer.length > 1) {
16
+ const next = [];
17
+ for (let i = 0; i < layer.length; i += 2) {
18
+ const left = layer[i], right = layer[i + 1];
19
+ if (!right) {
20
+ next.push(left);
21
+ continue;
22
+ }
23
+ for (const index of left.indices)
24
+ entries[index].proof.push(right.hash);
25
+ for (const index of right.indices)
26
+ entries[index].proof.push(left.hash);
27
+ next.push({ hash: claimAllowlistParent(left.hash, right.hash), indices: [...left.indices, ...right.indices] });
28
+ }
29
+ layer = next;
30
+ }
31
+ return { version: 1, context: { ...context }, root: layer[0].hash, entries };
32
+ }
33
+ export function verifyClaimAllowlistProof(context, account, proof, root) { hex32(root, "allowlist root"); if (!Array.isArray(proof) || proof.length > 32)
34
+ throw new NativeClaimError("allowlist proof exceeds 32 siblings"); return proof.reduce(claimAllowlistParent, claimAllowlistLeaf(context, account)) === root; }
@@ -0,0 +1,31 @@
1
+ import { Transaction, authorizeEntry, xdr } from "@stellar/stellar-sdk";
2
+ export type NativeInvocation = {
3
+ contract: string;
4
+ method: string;
5
+ args: xdr.ScVal[];
6
+ children?: NativeInvocation[];
7
+ };
8
+ /** Derive this plan locally from a validated intent, never from a remote envelope. */
9
+ export type NativeAuthorizationPlan = {
10
+ source: string;
11
+ contract: string;
12
+ method: string;
13
+ args: xdr.ScVal[];
14
+ sourceInvocation: NativeInvocation;
15
+ eligibility?: {
16
+ account: string;
17
+ invocation: NativeInvocation;
18
+ latestLedger: number;
19
+ maxExpirationLedger: number;
20
+ };
21
+ maxFeeStroops: bigint;
22
+ };
23
+ export declare function authorizedInvocation(value: NativeInvocation): xdr.SorobanAuthorizedInvocation;
24
+ export declare function validateEligibilityAuthorization(entry: xdr.SorobanAuthorizationEntry, expected: NonNullable<NativeAuthorizationPlan["eligibility"]>, requireSigned?: boolean): void;
25
+ /** Native account signing callback; hardware/custody integration keeps keys outside the SDK. */
26
+ export type EligibilitySigner = Parameters<typeof authorizeEntry>[1];
27
+ /** Signs only the expected admission entry. This helper never signs a transaction. */
28
+ export declare function signEligibilityAuthorization(unsignedEntry: xdr.SorobanAuthorizationEntry, expected: NonNullable<NativeAuthorizationPlan["eligibility"]>, signer: EligibilitySigner, expirationLedger: number, networkPassphrase: string): Promise<xdr.SorobanAuthorizationEntry>;
29
+ /** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
30
+ export declare function validateNativeTransaction(tx: Transaction, plan: NativeAuthorizationPlan, requireEligibilitySigned?: boolean): void;
31
+ export declare function assertSignedBodyUnchanged(prepared: Transaction, signedXdr: string, passphrase: string): Transaction;
@@ -0,0 +1,85 @@
1
+ import { Address, Transaction, TransactionBuilder, authorizeEntry, xdr } from "@stellar/stellar-sdk";
2
+ import { NativeClaimError, address, hex, u32 } from "./native-codec.js";
3
+ export function authorizedInvocation(value) {
4
+ return new xdr.SorobanAuthorizedInvocation({
5
+ function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(new xdr.InvokeContractArgs({
6
+ contractAddress: new Address(address(value.contract, "contract", "invocation contract")).toScAddress(), functionName: value.method, args: value.args,
7
+ })), subInvocations: (value.children ?? []).map(authorizedInvocation),
8
+ });
9
+ }
10
+ function sameInvocation(actual, expected) {
11
+ return actual.toXDR("base64") === authorizedInvocation(expected).toXDR("base64");
12
+ }
13
+ export function validateEligibilityAuthorization(entry, expected, requireSigned = true) {
14
+ if (expected.invocation.method !== "claim" || expected.invocation.args.length !== 1 || (expected.invocation.children?.length ?? 0) !== 0)
15
+ throw new NativeClaimError("eligibility may authorize only one exact native claim intent", "authorization");
16
+ address(expected.account, "account", "eligibility account");
17
+ u32(expected.latestLedger, "latest ledger");
18
+ u32(expected.maxExpirationLedger, "maximum auth expiration ledger");
19
+ if (entry.credentials.type !== "sorobanCredentialsAddress")
20
+ throw new NativeClaimError("eligibility requires the supported native G address credential", "authorization");
21
+ const credential = entry.credentials.address;
22
+ if (Address.fromScAddress(credential.address).toString() !== expected.account || !sameInvocation(entry.rootInvocation, expected.invocation) || entry.rootInvocation.subInvocations.length !== 0)
23
+ throw new NativeClaimError("eligibility authorization differs from the exact claim intent", "authorization");
24
+ if (requireSigned) {
25
+ if (credential.signatureExpirationLedger <= expected.latestLedger || credential.signatureExpirationLedger > expected.maxExpirationLedger)
26
+ throw new NativeClaimError("eligibility authorization expiry is outside the approved ledger bound", "authorization");
27
+ if (credential.signature.type !== "scvVec" || !credential.signature.vec?.length)
28
+ throw new NativeClaimError("eligibility authorization has no native account signatures", "authorization");
29
+ }
30
+ else if (credential.signature.type !== "scvVoid" && !(credential.signature.type === "scvVec" && credential.signature.vec?.length === 0)) {
31
+ throw new NativeClaimError("refusing to sign an already signed eligibility credential", "authorization");
32
+ }
33
+ }
34
+ /** Signs only the expected admission entry. This helper never signs a transaction. */
35
+ export async function signEligibilityAuthorization(unsignedEntry, expected, signer, expirationLedger, networkPassphrase) {
36
+ validateEligibilityAuthorization(unsignedEntry, expected, false);
37
+ u32(expirationLedger, "expiration ledger");
38
+ if (expirationLedger <= expected.latestLedger || expirationLedger > expected.maxExpirationLedger)
39
+ throw new NativeClaimError("eligibility expiration is outside the approved range", "authorization");
40
+ if (!networkPassphrase)
41
+ throw new NativeClaimError("network passphrase is required");
42
+ const signed = await authorizeEntry(unsignedEntry, signer, expirationLedger, networkPassphrase);
43
+ validateEligibilityAuthorization(signed, expected);
44
+ return signed;
45
+ }
46
+ /** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
47
+ export function validateNativeTransaction(tx, plan, requireEligibilitySigned = true) {
48
+ address(plan.source, "account", "claim transaction source");
49
+ if (tx.source !== plan.source)
50
+ throw new NativeClaimError(`native transaction source ${tx.source} differs from reviewed source ${plan.source}`, "authorization");
51
+ if (tx.operations.length !== 1)
52
+ throw new NativeClaimError(`native transaction has ${tx.operations.length} operations; exactly one was reviewed`, "authorization");
53
+ if (tx.memo.type !== "none")
54
+ throw new NativeClaimError(`native transaction memo type ${tx.memo.type} differs from reviewed none`, "authorization");
55
+ if (BigInt(tx.fee) > plan.maxFeeStroops)
56
+ throw new NativeClaimError(`native transaction network fee ${tx.fee} stroops exceeds the configured maximum ${plan.maxFeeStroops} stroops`, "authorization");
57
+ const op = tx.operations[0];
58
+ if (op.type !== "invokeHostFunction" || op.source && op.source !== plan.source || op.func.type !== "hostFunctionTypeInvokeContract")
59
+ throw new NativeClaimError("unexpected native operation", "authorization");
60
+ const call = op.func.invokeContract;
61
+ if (Address.fromScAddress(call.contractAddress).toString() !== plan.contract || call.functionName.toString() !== plan.method || call.args.length !== plan.args.length || call.args.some((arg, i) => arg.toXDR("base64") !== plan.args[i].toXDR("base64")))
62
+ throw new NativeClaimError("native operation differs from the exact reviewed intent", "authorization");
63
+ const entries = op.auth ?? [];
64
+ if (entries.length !== (plan.eligibility ? 2 : 1))
65
+ throw new NativeClaimError("unexpected native authorization count", "authorization");
66
+ const sources = entries.filter(entry => entry.credentials.type === "sorobanCredentialsSourceAccount");
67
+ if (sources.length !== 1 || !sameInvocation(sources[0].rootInvocation, plan.sourceInvocation))
68
+ throw new NativeClaimError("holder or owner authorization differs from exact reviewed intent", "authorization");
69
+ if (plan.eligibility) {
70
+ if (plan.eligibility.account === plan.source)
71
+ throw new NativeClaimError("eligibility account must be separate from claimant", "authorization");
72
+ const separate = entries.filter(entry => entry.credentials.type !== "sorobanCredentialsSourceAccount");
73
+ if (separate.length !== 1)
74
+ throw new NativeClaimError("unexpected second authorizer", "authorization");
75
+ validateEligibilityAuthorization(separate[0], plan.eligibility, requireEligibilitySigned);
76
+ }
77
+ }
78
+ export function assertSignedBodyUnchanged(prepared, signedXdr, passphrase) {
79
+ const signed = TransactionBuilder.fromXDR(signedXdr, passphrase);
80
+ if (!(signed instanceof Transaction) || hex(signed.hash()) !== hex(prepared.hash()))
81
+ throw new NativeClaimError("wallet changed the reviewed native transaction body", "authorization");
82
+ if (!signed.signatures.length)
83
+ throw new NativeClaimError("wallet returned an unsigned transaction", "authorization");
84
+ return signed;
85
+ }
@@ -0,0 +1,38 @@
1
+ /** Local successor-ABI helpers. No deployed defaults are changed by this module. */
2
+ import { xdr } from "@stellar/stellar-sdk";
3
+ import { type PaymentDestination } from "./payment.js";
4
+ export declare const NATIVE_REGISTRAR_ERRORS: Record<number, string>;
5
+ export declare class NativeClaimError extends Error {
6
+ readonly kind: "unsupported" | "unavailable" | "invalid" | "authorization" | "pending" | "failed";
7
+ readonly txHash: string | null;
8
+ readonly contractCode: number | null;
9
+ readonly contractError: string | null;
10
+ constructor(message: string, kind?: "unsupported" | "unavailable" | "invalid" | "authorization" | "pending" | "failed", txHash?: string | null);
11
+ }
12
+ export declare function exactObject(raw: unknown, keys: readonly string[], label: string): Record<string, unknown>;
13
+ export declare function u64(value: unknown, label: string): bigint;
14
+ export declare function u32(value: unknown, label: string): number;
15
+ export declare function amount(value: unknown, label: string): bigint;
16
+ export declare function bool(value: unknown, label: string): boolean;
17
+ export declare function address(value: unknown, kind: "account" | "contract" | "identity", label: string): string;
18
+ export declare function bytes32(value: unknown, label: string): Uint8Array;
19
+ export declare function hex32(value: unknown, label: string): string;
20
+ export declare const hex: (value: Uint8Array) => string;
21
+ export declare const unhex: (value: string) => Uint8Array;
22
+ export declare const utf8: (value: string) => Uint8Array;
23
+ export declare function label(value: unknown): string;
24
+ export declare function namespaceNode(namespace: string): Uint8Array;
25
+ export declare const sc: {
26
+ address: (value: string) => xdr.ScVal;
27
+ bytes: (value: Uint8Array) => xdr.ScVal;
28
+ u64: (value: bigint) => xdr.ScVal;
29
+ u32: (value: number) => xdr.ScVal;
30
+ i128: (value: bigint) => xdr.ScVal;
31
+ bool: (value: boolean) => xdr.ScVal;
32
+ symbol: (value: string) => xdr.ScVal;
33
+ option: (value: xdr.ScVal | null) => xdr.ScVal;
34
+ };
35
+ /** Contract structs are canonical symbol-keyed maps, not generic JSON maps. */
36
+ export declare function struct(fields: Record<string, xdr.ScVal>): xdr.ScVal;
37
+ export declare function paymentDestinationToScVal(raw: PaymentDestination): xdr.ScVal;
38
+ export declare function sameScVal(left: xdr.ScVal, right: xdr.ScVal): boolean;
@@ -0,0 +1,95 @@
1
+ /** Local successor-ABI helpers. No deployed defaults are changed by this module. */
2
+ import { Address, StrKey, hash, nativeToScVal, xdr } from "@stellar/stellar-sdk";
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" };
5
+ export class NativeClaimError extends Error {
6
+ kind;
7
+ txHash;
8
+ contractCode;
9
+ contractError;
10
+ constructor(message, kind = "invalid", txHash = null) {
11
+ super(message);
12
+ this.kind = kind;
13
+ this.txHash = txHash;
14
+ this.name = "NativeClaimError";
15
+ const match = /Error\(Contract, #(\d+)\)/.exec(message);
16
+ this.contractCode = match ? Number(match[1]) : null;
17
+ this.contractError = this.contractCode === null ? null : NATIVE_REGISTRAR_ERRORS[this.contractCode] ?? null;
18
+ }
19
+ }
20
+ export function exactObject(raw, keys, label) {
21
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).sort().join(",") !== [...keys].sort().join(","))
22
+ throw new NativeClaimError(`invalid ${label} fields`);
23
+ return raw;
24
+ }
25
+ export function u64(value, label) {
26
+ if (typeof value !== "bigint" || value < 0n || value > 18446744073709551615n)
27
+ throw new NativeClaimError(`${label} must be an unsigned 64-bit bigint`);
28
+ return value;
29
+ }
30
+ export function u32(value, label) {
31
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 4294967295)
32
+ throw new NativeClaimError(`${label} must be an unsigned 32-bit number`);
33
+ return value;
34
+ }
35
+ export function amount(value, label) {
36
+ if (typeof value !== "bigint" || value < 0n || value > (1n << 127n) - 1n)
37
+ throw new NativeClaimError(`${label} must be a nonnegative i128 bigint`);
38
+ return value;
39
+ }
40
+ export function bool(value, label) {
41
+ if (typeof value !== "boolean")
42
+ throw new NativeClaimError(`${label} must be boolean`);
43
+ return value;
44
+ }
45
+ export function address(value, kind, label) {
46
+ if (typeof value !== "string" || !(kind !== "contract" && StrKey.isValidEd25519PublicKey(value) || kind !== "account" && StrKey.isValidContract(value)))
47
+ throw new NativeClaimError(`${label} must be a valid ${kind === "identity" ? "G or C" : kind === "account" ? "G" : "C"} address`);
48
+ return value;
49
+ }
50
+ export function bytes32(value, label) {
51
+ if (!(value instanceof Uint8Array) || value.length !== 32)
52
+ throw new NativeClaimError(`${label} must contain exactly 32 bytes`);
53
+ return new Uint8Array(value);
54
+ }
55
+ export function hex32(value, label) {
56
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value))
57
+ throw new NativeClaimError(`${label} must be 64 lowercase hex characters`);
58
+ return value;
59
+ }
60
+ export const hex = (value) => Array.from(value, byte => byte.toString(16).padStart(2, "0")).join("");
61
+ export const unhex = (value) => Uint8Array.from(hex32(value, "32-byte value").match(/../g), byte => parseInt(byte, 16));
62
+ export const utf8 = (value) => new TextEncoder().encode(value);
63
+ export function label(value) {
64
+ if (typeof value !== "string" || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(value))
65
+ throw new NativeClaimError("intent label must be canonical lowercase ASCII, 1–63 characters");
66
+ return value;
67
+ }
68
+ export function namespaceNode(namespace) {
69
+ const combined = new Uint8Array(64);
70
+ combined.set(hash(utf8(label(namespace))), 32);
71
+ return new Uint8Array(hash(combined));
72
+ }
73
+ export const sc = {
74
+ address: (value) => new Address(value).toScVal(),
75
+ bytes: (value) => nativeToScVal(value, { type: "bytes" }),
76
+ u64: (value) => nativeToScVal(value, { type: "u64" }),
77
+ u32: (value) => nativeToScVal(value, { type: "u32" }),
78
+ i128: (value) => nativeToScVal(value, { type: "i128" }),
79
+ bool: (value) => nativeToScVal(value, { type: "bool" }),
80
+ symbol: (value) => nativeToScVal(value, { type: "symbol" }),
81
+ option: (value) => value ?? xdr.ScVal.scvVoid(),
82
+ };
83
+ /** Contract structs are canonical symbol-keyed maps, not generic JSON maps. */
84
+ export function struct(fields) {
85
+ return xdr.ScVal.scvMap(Object.keys(fields).sort().map(key => new xdr.ScMapEntry({ key: sc.symbol(key), val: fields[key] })));
86
+ }
87
+ export function paymentDestinationToScVal(raw) {
88
+ const payment = validatePaymentDestination(raw);
89
+ if (StrKey.isValidMed25519PublicKey(payment.address)) {
90
+ const value = decodeMuxedAddress(payment.address);
91
+ return xdr.ScVal.scvVec([sc.symbol("Muxed"), struct({ account: sc.address(value.account), id: sc.u64(BigInt(value.id)) })]);
92
+ }
93
+ return xdr.ScVal.scvVec([sc.symbol("Direct"), struct({ address: sc.address(payment.address), memo: paymentMemoToScVal(payment.memo) })]);
94
+ }
95
+ export function sameScVal(left, right) { return left.toXDR("base64") === right.toXDR("base64"); }