@sorandomains/holder 0.4.0 → 0.5.1

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. Receipt reads, receipt absence and confirmed transaction results are accepted only after a clean Registrar attestation and executable check with RPC ledger context at least as recent as the receipt read or transaction inclusion. Missing or older context stops recovery; retain the original request and transaction hash. This check trusts the configured RPC to report its state and ledger honestly. Later namespace-owner, claim-policy or Resolver changes alone do not invalidate a historical receipt, while a tainted or upgraded Registrar cannot supply authoritative history. `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
@@ -211,6 +244,8 @@ export declare class SoranHolder {
211
244
  private paymentResolverOf;
212
245
  private assertMemoFree;
213
246
  private read;
247
+ private readWithLedger;
248
+ private simulateRead;
214
249
  private signEnvelope;
215
250
  private serialize;
216
251
  private sourceAccount;
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,30 @@
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
+ import { requireReadLedger } from "./native-transport.js";
40
+ export * from "./native-types.js";
41
+ export * from "./native-allowlist.js";
42
+ export * from "./native-approver.js";
43
+ export { NativeClaimError, paymentDestinationToScVal, namespaceNode as nativeNamespaceNode } from "./native-codec.js";
44
+ export { createClaimIntent } from "./native-holder.js";
45
+ export { signEligibilityAuthorization, validateEligibilityAuthorization, validateNativeTransaction, authorizedInvocation } from "./native-auth.js";
36
46
  // ---------------------------------------------------------------------------
37
47
  // Deployments
38
48
  // ---------------------------------------------------------------------------
39
49
  /** Known public deployments. Pass explicit options for anything else. */
40
50
  export const DEPLOYMENTS = {
41
51
  testnet: {
52
+ rpcUrl: "https://soroban-testnet.stellar.org",
53
+ passphrase: Networks.TESTNET,
54
+ registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK",
55
+ primaryId: "CASORAN755O3GCQTRAHKDXLLCSDLNKAQWAP6MWABRSFVSHLJOEKAC7AB",
56
+ },
57
+ };
58
+ /** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
59
+ export const LEGACY_DEPLOYMENTS = {
60
+ testnet20260905: {
42
61
  rpcUrl: "https://soroban-testnet.stellar.org",
43
62
  passphrase: Networks.TESTNET,
44
63
  registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H",
@@ -81,6 +100,7 @@ const REGISTRAR_ERRORS = {
81
100
  18: "InvalidPolicy",
82
101
  19: "ExpiryOverflow",
83
102
  20: "InvalidRegistry",
103
+ 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
104
  };
85
105
  const RESOLVER_ERRORS = {
86
106
  1: "AlreadyInitialized",
@@ -223,6 +243,7 @@ export const PROFILE_KEYS = [
223
243
  // The client
224
244
  // ---------------------------------------------------------------------------
225
245
  export class SoranHolder {
246
+ maxNativeFeeStroops;
226
247
  server;
227
248
  passphrase;
228
249
  registryId;
@@ -235,6 +256,9 @@ export class SoranHolder {
235
256
  resolvers = new Map();
236
257
  static POINTER_TTL_MS = 30_000;
237
258
  constructor(opts) {
259
+ this.maxNativeFeeStroops = opts?.maxNativeFeeStroops ?? 50000000n;
260
+ if (typeof this.maxNativeFeeStroops !== "bigint" || this.maxNativeFeeStroops <= 0n || this.maxNativeFeeStroops > 4294967295n)
261
+ throw new NativeClaimError("maximum native network fee must be positive");
238
262
  if (!opts?.signer)
239
263
  throw new HolderError("HolderOptions.signer is required");
240
264
  const d = DEPLOYMENTS[opts.network ?? "testnet"];
@@ -255,6 +279,20 @@ export class SoranHolder {
255
279
  this.timeoutSecs = t;
256
280
  this.fee = opts.fee ?? BASE_FEE;
257
281
  }
282
+ nativeClient() {
283
+ return new NativeHolderClient({ registryId: this.registryId, passphrase: this.passphrase, server: this.server, signer: this.signer, fee: this.fee, timeoutSecs: this.timeoutSecs, maxFeeStroops: this.maxNativeFeeStroops,
284
+ read: (id, method, args) => this.read(id, method, args),
285
+ readWithLedger: (id, method, args) => this.readWithLedger(id, method, args), serialize: work => this.serialize(work) });
286
+ }
287
+ nativeClaimCapability(namespace) { return this.nativeClient().nativeClaimCapability(namespace); }
288
+ claimQuote(name, claimant) { return this.nativeClient().claimQuote(name, claimant); }
289
+ claimReceipt(namespace, claimant, requestId) { return this.nativeClient().claimReceipt(namespace, claimant, requestId); }
290
+ recoverClaim(intent) { return this.nativeClient().recoverClaim(intent); }
291
+ buildClaim(intent, options = {}) { return this.nativeClient().buildClaim(intent, options); }
292
+ claim(intent, options = {}) { return this.nativeClient().claim(intent, options); }
293
+ renewalPreview(name) { return this.nativeClient().renewalPreview(name); }
294
+ acceptNameTransferWithDestination(intent, options = {}) { return this.nativeClient().acceptNameTransferWithDestination(intent, options); }
295
+ renewName(intent, options = {}) { return this.nativeClient().renewName(intent, options); }
258
296
  // ---- resolution targets --------------------------------------------------
259
297
  /** Atomically update the forward address and complete payment instruction.
260
298
  * Use memo {type:"none"} to explicitly publish a memo-free destination.
@@ -567,6 +605,13 @@ export class SoranHolder {
567
605
  return registrar;
568
606
  }
569
607
  async read(contractId, fn, args) {
608
+ return (await this.simulateRead(contractId, fn, args)).value;
609
+ }
610
+ async readWithLedger(contractId, fn, args) {
611
+ const result = await this.simulateRead(contractId, fn, args);
612
+ return { value: result.value, ledger: requireReadLedger(result.ledger) };
613
+ }
614
+ async simulateRead(contractId, fn, args) {
570
615
  const tx = new TransactionBuilder(new Account(SIM_SOURCE, "0"), {
571
616
  fee: BASE_FEE,
572
617
  networkPassphrase: this.passphrase,
@@ -583,7 +628,7 @@ export class SoranHolder {
583
628
  if (!rpc.Api.isSimulationSuccess(sim) || !sim.result?.retval)
584
629
  throw new HolderError(`${fn}: missing simulation return value`, contractId, fn);
585
630
  const v = scValToNative(sim.result.retval);
586
- return v === undefined ? null : v;
631
+ return { value: v === undefined ? null : v, ledger: sim.latestLedger };
587
632
  }
588
633
  async signEnvelope(xdrBase64) {
589
634
  const signed = await this.signer.signTransaction(xdrBase64, {
@@ -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"); }