@sorandomains/holder 0.4.0 → 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,14 +1,14 @@
1
1
  # @sorandomains/holder
2
2
 
3
- > Native muxed release. The testnet deployment below was verified on chain at
4
- > ledger 4521644 on 5 September 2026 (18:10 UTC). See the
3
+
4
+ > Native claim testnet deployment verified at ledger 4534629 on 6 September 2026 (12:12 UTC). See the
5
5
  > [release status](https://docs.soran.domains/reference/release-status) for package and service availability.
6
6
 
7
7
 
8
- Version 0.4.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
8
+ Version 0.5.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.6.0.
11
+ Lookup is the read entry point in `@sorandomains/lookup` 0.7.0.
12
12
 
13
13
  Your Soran name, managed with your own key. The third piece of the SDK
14
14
  trilogy: [`@sorandomains/lookup`](https://www.npmjs.com/package/@sorandomains/lookup)
@@ -140,14 +140,21 @@ Primary writes verify the Primary contract's Registry anchor before signing.
140
140
  Custom Registry or passphrase settings do not inherit a Primary deployment pin;
141
141
  supply the matching `primaryId` explicitly.
142
142
 
143
+
144
+ ## Native username claiming
145
+
146
+ Native username claiming uses `claimQuote`, `createClaimIntent`, `buildClaim` and `claim`. The current namespace owner must first enable the public policy. The claimant's G wallet authorizes the exact username, complete receiving destination, owner price, policy version and deadline. `recoverClaim` reads the original immutable receipt; it never silently retries an uncertain transaction. `acceptNameTransferWithDestination` and `renewName` cover separately authorized holder lifecycle actions.
147
+
148
+ 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.
149
+
143
150
  ## Verified testnet deployment
144
151
 
145
- Verified on 5 September 2026 at ledger **4521644** (18:10 UTC). Network passphrase: `Test SDF Network ; September 2015`.
152
+ Verified on 6 September 2026 at ledger **4534629** (12:12 UTC). Network passphrase: `Test SDF Network ; September 2015`.
146
153
 
147
154
  | Contract | Address |
148
155
  |---|---|
149
- | Registry | `CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H` |
150
- | Primary | `CCSORANJZOR5ZYTI4KAW34ESAQFMJAO4NKMTIVOVJOI2VDKCDK3RICXZ` |
156
+ | Registry | `CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK` |
157
+ | Primary | `CASORAN755O3GCQTRAHKDXLLCSDLNKAQWAP6MWABRSFVSHLJOEKAC7AB` |
151
158
 
152
159
  Mainnet has no deployment preset. Custom networks must supply their own verified
153
160
  addresses. Universal Lookup upgrades remain immediately executable; an address
package/dist/index.d.ts CHANGED
@@ -17,8 +17,9 @@
17
17
  * checks you hold the name (generation-gated) before accepting records, the
18
18
  * reverse and primary claims are authorized by the ADDRESS itself, and name
19
19
  * transfers move only when the recipient accepts. Namespace-owner powers
20
- * (issue, reclaim, renew, permanence) live in `@sorandomains/owner` and this
21
- * package deliberately cannot exercise them.
20
+ * (issue, reclaim, owner renewal, permanence) live in `@sorandomains/owner` and this
21
+ * package deliberately cannot exercise them. Native successor holders can
22
+ * independently renew an eligible lease through the separately authorized renewName.
22
23
  *
23
24
  * SIGNING. Same `TxSigner` contract as the owner SDK: `keypairSigner(secret)`
24
25
  * for scripts, or wrap a browser wallet:
@@ -32,9 +33,28 @@
32
33
  */
33
34
  import { type PaymentDestination } from "./payment.js";
34
35
  export { encodeMuxedAddress, decodeMuxedAddress, PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination, type PaymentMemo, type PaymentDestination } from "./payment.js";
36
+ import { type ClaimSubmitOptions } from "./native-holder.js";
37
+ import type { ClaimIntent, TransferIntent, RenewIntent } from "./native-types.js";
38
+ import type { NativeWriteOptions } from "./native-transport.js";
39
+ export * from "./native-types.js";
40
+ export * from "./native-allowlist.js";
41
+ export * from "./native-approver.js";
42
+ export { NativeClaimError, paymentDestinationToScVal, namespaceNode as nativeNamespaceNode } from "./native-codec.js";
43
+ export { createClaimIntent, type ClaimRequestOptions, type ClaimSubmitOptions, type NativeClaimSubmission, type PreparedClaim } from "./native-holder.js";
44
+ export { signEligibilityAuthorization, validateEligibilityAuthorization, validateNativeTransaction, authorizedInvocation, type NativeAuthorizationPlan, type EligibilitySigner } from "./native-auth.js";
45
+ export type { NativeCapability, NativePrepared, NativeWriteOptions } from "./native-transport.js";
35
46
  /** Known public deployments. Pass explicit options for anything else. */
36
47
  export declare const DEPLOYMENTS: {
37
48
  readonly testnet: {
49
+ readonly rpcUrl: "https://soroban-testnet.stellar.org";
50
+ readonly passphrase: string;
51
+ readonly registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK";
52
+ readonly primaryId: "CASORAN755O3GCQTRAHKDXLLCSDLNKAQWAP6MWABRSFVSHLJOEKAC7AB";
53
+ };
54
+ };
55
+ /** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
56
+ export declare const LEGACY_DEPLOYMENTS: {
57
+ readonly testnet20260905: {
38
58
  readonly rpcUrl: "https://soroban-testnet.stellar.org";
39
59
  readonly passphrase: string;
40
60
  readonly registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H";
@@ -82,6 +102,8 @@ export type Submitted = {
82
102
  /** The standard profile keys shared with @sorandomains/lookup's profile(). */
83
103
  export declare const PROFILE_KEYS: readonly ["org", "url", "email", "description", "avatar", "location", "twitter", "github"];
84
104
  export type HolderOptions = {
105
+ /** Upper total network fee for successor native operations; default 5 XLM. */
106
+ maxNativeFeeStroops?: bigint;
85
107
  /** Signs every transaction: the name HOLDER's account (or, for
86
108
  * `acceptNameTransfer`, the proposed new holder's). */
87
109
  signer: TxSigner;
@@ -101,6 +123,7 @@ export type HolderOptions = {
101
123
  fee?: string;
102
124
  };
103
125
  export declare class SoranHolder {
126
+ private maxNativeFeeStroops;
104
127
  private server;
105
128
  private passphrase;
106
129
  private registryId;
@@ -113,6 +136,16 @@ export declare class SoranHolder {
113
136
  private resolvers;
114
137
  private static POINTER_TTL_MS;
115
138
  constructor(opts: HolderOptions);
139
+ private nativeClient;
140
+ nativeClaimCapability(namespace: string): Promise<import("./native-transport.js").NativeCapability>;
141
+ claimQuote(name: string, claimant?: string): Promise<import("./native-types.js").ClaimQuote>;
142
+ claimReceipt(namespace: string, claimant: string, requestId: string): Promise<import("./native-types.js").ClaimReceipt | null>;
143
+ recoverClaim(intent: ClaimIntent): Promise<import("./native-types.js").ClaimReceipt | null>;
144
+ buildClaim(intent: ClaimIntent, options?: ClaimSubmitOptions): Promise<import("./native-holder.js").PreparedClaim>;
145
+ claim(intent: ClaimIntent, options?: ClaimSubmitOptions): Promise<import("./native-holder.js").NativeClaimSubmission>;
146
+ renewalPreview(name: string): Promise<import("./native-types.js").DestinationPreview>;
147
+ acceptNameTransferWithDestination(intent: TransferIntent, options?: NativeWriteOptions): Promise<import("./native-holder.js").NativeClaimSubmission>;
148
+ renewName(intent: RenewIntent, options?: NativeWriteOptions): Promise<import("./native-holder.js").NativeClaimSubmission>;
116
149
  /** Atomically update the forward address and complete payment instruction.
117
150
  * Use memo {type:"none"} to explicitly publish a memo-free destination.
118
151
  * The native Resolver updates its own records atomically. This method never
package/dist/index.js CHANGED
@@ -17,8 +17,9 @@
17
17
  * checks you hold the name (generation-gated) before accepting records, the
18
18
  * reverse and primary claims are authorized by the ADDRESS itself, and name
19
19
  * transfers move only when the recipient accepts. Namespace-owner powers
20
- * (issue, reclaim, renew, permanence) live in `@sorandomains/owner` and this
21
- * package deliberately cannot exercise them.
20
+ * (issue, reclaim, owner renewal, permanence) live in `@sorandomains/owner` and this
21
+ * package deliberately cannot exercise them. Native successor holders can
22
+ * independently renew an eligible lease through the separately authorized renewName.
22
23
  *
23
24
  * SIGNING. Same `TxSigner` contract as the owner SDK: `keypairSigner(secret)`
24
25
  * for scripts, or wrap a browser wallet:
@@ -33,12 +34,29 @@
33
34
  import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, } from "@stellar/stellar-sdk";
34
35
  import { decodeMuxedAddress, destinationFromNative, paymentFromNative, paymentMemoToScVal, validatePaymentDestination } from "./payment.js";
35
36
  export { encodeMuxedAddress, decodeMuxedAddress, PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination } from "./payment.js";
37
+ import { NativeHolderClient } from "./native-holder.js";
38
+ import { NativeClaimError } from "./native-codec.js";
39
+ export * from "./native-types.js";
40
+ export * from "./native-allowlist.js";
41
+ export * from "./native-approver.js";
42
+ export { NativeClaimError, paymentDestinationToScVal, namespaceNode as nativeNamespaceNode } from "./native-codec.js";
43
+ export { createClaimIntent } from "./native-holder.js";
44
+ export { signEligibilityAuthorization, validateEligibilityAuthorization, validateNativeTransaction, authorizedInvocation } from "./native-auth.js";
36
45
  // ---------------------------------------------------------------------------
37
46
  // Deployments
38
47
  // ---------------------------------------------------------------------------
39
48
  /** Known public deployments. Pass explicit options for anything else. */
40
49
  export const DEPLOYMENTS = {
41
50
  testnet: {
51
+ rpcUrl: "https://soroban-testnet.stellar.org",
52
+ passphrase: Networks.TESTNET,
53
+ registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK",
54
+ primaryId: "CASORAN755O3GCQTRAHKDXLLCSDLNKAQWAP6MWABRSFVSHLJOEKAC7AB",
55
+ },
56
+ };
57
+ /** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
58
+ export const LEGACY_DEPLOYMENTS = {
59
+ testnet20260905: {
42
60
  rpcUrl: "https://soroban-testnet.stellar.org",
43
61
  passphrase: Networks.TESTNET,
44
62
  registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H",
@@ -81,6 +99,7 @@ const REGISTRAR_ERRORS = {
81
99
  18: "InvalidPolicy",
82
100
  19: "ExpiryOverflow",
83
101
  20: "InvalidRegistry",
102
+ 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",
84
103
  };
85
104
  const RESOLVER_ERRORS = {
86
105
  1: "AlreadyInitialized",
@@ -223,6 +242,7 @@ export const PROFILE_KEYS = [
223
242
  // The client
224
243
  // ---------------------------------------------------------------------------
225
244
  export class SoranHolder {
245
+ maxNativeFeeStroops;
226
246
  server;
227
247
  passphrase;
228
248
  registryId;
@@ -235,6 +255,9 @@ export class SoranHolder {
235
255
  resolvers = new Map();
236
256
  static POINTER_TTL_MS = 30_000;
237
257
  constructor(opts) {
258
+ this.maxNativeFeeStroops = opts?.maxNativeFeeStroops ?? 50000000n;
259
+ if (typeof this.maxNativeFeeStroops !== "bigint" || this.maxNativeFeeStroops <= 0n || this.maxNativeFeeStroops > 4294967295n)
260
+ throw new NativeClaimError("maximum native network fee must be positive");
238
261
  if (!opts?.signer)
239
262
  throw new HolderError("HolderOptions.signer is required");
240
263
  const d = DEPLOYMENTS[opts.network ?? "testnet"];
@@ -255,6 +278,19 @@ export class SoranHolder {
255
278
  this.timeoutSecs = t;
256
279
  this.fee = opts.fee ?? BASE_FEE;
257
280
  }
281
+ nativeClient() {
282
+ return new NativeHolderClient({ registryId: this.registryId, passphrase: this.passphrase, server: this.server, signer: this.signer, fee: this.fee, timeoutSecs: this.timeoutSecs, maxFeeStroops: this.maxNativeFeeStroops,
283
+ read: (id, method, args) => this.read(id, method, args), serialize: work => this.serialize(work) });
284
+ }
285
+ nativeClaimCapability(namespace) { return this.nativeClient().nativeClaimCapability(namespace); }
286
+ claimQuote(name, claimant) { return this.nativeClient().claimQuote(name, claimant); }
287
+ claimReceipt(namespace, claimant, requestId) { return this.nativeClient().claimReceipt(namespace, claimant, requestId); }
288
+ recoverClaim(intent) { return this.nativeClient().recoverClaim(intent); }
289
+ buildClaim(intent, options = {}) { return this.nativeClient().buildClaim(intent, options); }
290
+ claim(intent, options = {}) { return this.nativeClient().claim(intent, options); }
291
+ renewalPreview(name) { return this.nativeClient().renewalPreview(name); }
292
+ acceptNameTransferWithDestination(intent, options = {}) { return this.nativeClient().acceptNameTransferWithDestination(intent, options); }
293
+ renewName(intent, options = {}) { return this.nativeClient().renewName(intent, options); }
258
294
  // ---- resolution targets --------------------------------------------------
259
295
  /** Atomically update the forward address and complete payment instruction.
260
296
  * Use memo {type:"none"} to explicitly publish a memo-free destination.
@@ -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,21 @@
1
+ import { type EligibilitySigner } from "./native-auth.js";
2
+ import { type ClaimIntent, type ClaimConfig } from "./native-types.js";
3
+ export type ClaimApprovalContext = {
4
+ /** Read this configuration and owner independently from the chain. Do not accept these from an applicant. */
5
+ config: ClaimConfig;
6
+ owner: string;
7
+ ownerEpoch: bigint;
8
+ networkPassphrase: string;
9
+ /** Independently pinned/read deployment, never copied from the applicant intent. network/namespace are hex32. */
10
+ network: string;
11
+ registry: string;
12
+ registrar: string;
13
+ namespace: string;
14
+ resolver: string;
15
+ latestLedger: number;
16
+ maxExpirationLedger: number;
17
+ expirationLedger: number;
18
+ };
19
+ /** Backend admission signing only. The application must first authenticate membership and the wallet binding.
20
+ * This function cannot turn an owner/treasury key into the configured admission account. */
21
+ export declare function signClaimEligibility(intent: ClaimIntent, unsignedEntryXdr: string, context: ClaimApprovalContext, signer: EligibilitySigner): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import { hash, scValToNative, StrKey, xdr } from "@stellar/stellar-sdk";
2
+ import { NativeClaimError, address, hex, utf8 } from "./native-codec.js";
3
+ import { signEligibilityAuthorization } from "./native-auth.js";
4
+ import { claimIntentToScVal } from "./native-types.js";
5
+ /** Backend admission signing only. The application must first authenticate membership and the wallet binding.
6
+ * This function cannot turn an owner/treasury key into the configured admission account. */
7
+ export async function signClaimEligibility(intent, unsignedEntryXdr, context, signer) {
8
+ const encoded = claimIntentToScVal(intent), config = context.config, settings = config.settings;
9
+ if (context.network !== hex(hash(utf8(context.networkPassphrase))) || intent.context.network !== context.network || intent.context.registry !== context.registry || intent.context.registrar !== context.registrar || intent.context.namespace !== context.namespace || intent.resolver !== context.resolver || intent.ownerEpoch !== context.ownerEpoch || config.ownerEpoch !== context.ownerEpoch)
10
+ throw new NativeClaimError("approval intent differs from trusted deployment, namespace or current ownership epoch", "authorization");
11
+ address(context.owner, "identity", "namespace owner");
12
+ if (settings.mode !== "public" || !settings.enabled || settings.admission.type !== "approval")
13
+ throw new NativeClaimError("native app approval is not enabled", "authorization");
14
+ const account = settings.admission.account;
15
+ if (account === context.owner || account === settings.feeRecipient || account === intent.claimant)
16
+ throw new NativeClaimError("eligibility account must be separate from owner, treasury and claimant", "authorization");
17
+ if (intent.context.network !== hex(hash(utf8(context.networkPassphrase))) || intent.ownerEpoch !== config.ownerEpoch || intent.policyVersion !== config.policyVersion || intent.grantEpoch !== config.grantEpoch || intent.feeToken !== settings.feeToken || intent.feeAmount !== settings.feeAmount || intent.feeRecipient !== settings.feeRecipient)
18
+ throw new NativeClaimError("approval intent differs from independently read network/policy", "authorization");
19
+ if (intent.context.deadline - intent.context.validAfter > settings.approvalTtlSecs)
20
+ throw new NativeClaimError("approval exceeds the current business lifetime", "authorization");
21
+ const expected = { account, invocation: { contract: intent.context.registrar, method: "claim", args: [encoded] }, latestLedger: context.latestLedger, maxExpirationLedger: context.maxExpirationLedger };
22
+ const signed = await signEligibilityAuthorization(xdr.SorobanAuthorizationEntry.fromXDR(unsignedEntryXdr, "base64"), expected, signer, context.expirationLedger, context.networkPassphrase);
23
+ if (signed.credentials.type !== "sorobanCredentialsAddress")
24
+ throw new NativeClaimError("unsupported approval credential", "authorization");
25
+ const signatures = scValToNative(signed.credentials.address.signature);
26
+ if (!Array.isArray(signatures) || signatures.length === 0)
27
+ throw new NativeClaimError("approval is missing native account signatures", "authorization");
28
+ for (const signature of signatures) {
29
+ if (!(signature?.public_key instanceof Uint8Array) || signature.public_key.length !== 32)
30
+ throw new NativeClaimError("invalid native approval signer", "authorization");
31
+ const key = StrKey.encodeEd25519PublicKey(signature.public_key);
32
+ if ([context.owner, settings.feeRecipient, intent.claimant].includes(key))
33
+ throw new NativeClaimError("owner, treasury and claimant keys must never sign app eligibility", "authorization");
34
+ }
35
+ return signed.toXDR("base64");
36
+ }
@@ -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"); }
@@ -0,0 +1,42 @@
1
+ import { type NativeAuthorizationPlan } from "./native-auth.js";
2
+ import { type NativeContext, type NativeWriteOptions, type NativePrepared } from "./native-transport.js";
3
+ import { type ClaimIntent, type ClaimQuote, type ClaimReceipt, type ClaimResult, type TransferIntent, type RenewIntent, type DestinationPreview } from "./native-types.js";
4
+ import { type PaymentDestination } from "./payment.js";
5
+ export type ClaimRequestOptions = {
6
+ requestId: string;
7
+ deadline: bigint;
8
+ validAfter?: bigint;
9
+ };
10
+ export type ClaimSubmitOptions = NativeWriteOptions & {
11
+ proof?: readonly string[];
12
+ };
13
+ export type NativeClaimSubmission = ClaimResult & {
14
+ transaction: {
15
+ hash: string;
16
+ ledger: number;
17
+ } | null;
18
+ };
19
+ export type PreparedClaim = NativePrepared & {
20
+ intent: ClaimIntent;
21
+ plan: NativeAuthorizationPlan;
22
+ };
23
+ export declare function createClaimIntent(quote: ClaimQuote, destination: PaymentDestination, options: ClaimRequestOptions): ClaimIntent;
24
+ export declare class NativeHolderClient {
25
+ private context;
26
+ constructor(context: NativeContext);
27
+ nativeClaimCapability(namespace: string): Promise<import("./native-transport.js").NativeCapability>;
28
+ private registrar;
29
+ private checkRegistrar;
30
+ claimQuote(name: string, claimant?: string): Promise<ClaimQuote>;
31
+ private checkQuote;
32
+ claimReceipt(namespace: string, claimant: string, requestId: string): Promise<ClaimReceipt | null>;
33
+ private receiptAt;
34
+ recoverClaim(intent: ClaimIntent): Promise<ClaimReceipt | null>;
35
+ private planClaim;
36
+ buildClaim(intent: ClaimIntent, options?: ClaimSubmitOptions): Promise<PreparedClaim>;
37
+ claim(intent: ClaimIntent, options?: ClaimSubmitOptions): Promise<NativeClaimSubmission>;
38
+ renewalPreview(name: string): Promise<DestinationPreview>;
39
+ acceptNameTransferWithDestination(input: TransferIntent, options?: NativeWriteOptions): Promise<NativeClaimSubmission>;
40
+ renewName(input: RenewIntent, options?: NativeWriteOptions): Promise<NativeClaimSubmission>;
41
+ private lifecycle;
42
+ }