@sorandomains/holder 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # @sorandomains/holder
2
2
 
3
- Version 0.3.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.5.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.
7
12
 
8
13
  Your Soran name, managed with your own key. The third piece of the SDK
9
14
  trilogy: [`@sorandomains/lookup`](https://www.npmjs.com/package/@sorandomains/lookup)
@@ -45,7 +50,12 @@ await me.setPayment("alice.nova", { address: myAddress, memo: { type: "none" } }
45
50
 
46
51
  ID values are canonical unsigned 64-bit decimal strings; text is exact nonempty
47
52
  UTF-8 up to 28 bytes; hashes are 64 lowercase hex characters. Required memos work
48
- only with G addresses. C addresses permit `none`. The Resolver authorizes the
53
+ only with G addresses. C addresses permit `none`. A full muxed M address also
54
+ requires `none`: its embedded ID belongs to the operation destination, not a
55
+ transaction memo. On native Resolver v2, `setPayment` decodes M and signs
56
+ `set_muxed(name, holder, baseG, exactU64Id)`; G/C uses `set_payment` as before.
57
+ The signed authorization must match the exact selected call, base account and ID.
58
+ A signer returning a different transaction body is rejected before submission. The Resolver authorizes the
49
59
  current holder and updates its records atomically. Failures never retry as separate
50
60
  address and text writes. Old unsupported Resolvers fail closed; no payment-specific
51
61
  contract address is configured.
@@ -53,17 +63,34 @@ contract address is configured.
53
63
  `setText` and `clearText` reserve `payment` for `setPayment`. Configured missing or
54
64
  empty instructions remain errors. Remove a memo with explicit `none`.
55
65
  `setRecord` invokes native `set_addr`, which atomically permits ordinary names and
56
- updates an existing valid `none` tuple while rejecting required memos or broken
66
+ updates an existing valid direct `none` tuple while rejecting muxed routes, required memos or broken
57
67
  state. A concurrently added memo cannot be replaced by a client-side None rewrite.
58
68
  `setAddress` retains its Registrar-only semantics and first requires a valid native
59
- `none` result. It changes only the built-in target; an explicit Resolver payment
69
+ direct `none` result. It changes only the built-in target; an explicit Resolver payment
60
70
  record continues to take precedence. Failed preflight reads never permit a write.
61
71
 
62
72
  Resolver selection follows the namespace owner's Registry pointer. Compatibility
63
73
  and anchor checks do not prove custom/upgraded code is trustworthy. Upgraded
64
74
  Resolvers remain supported. Payment readers must use `resolvePayment` and preserve
65
75
  the returned memo; old deployed code and direct Registrar reads cannot be upgraded
66
- by installing this SDK. The verified deployment used by this release is listed below.
76
+ by installing this SDK. The verified deployment for this release is listed below.
77
+
78
+ ### Publish a muxed destination
79
+
80
+ ```ts
81
+ await me.setPayment("customer420.nova", {
82
+ address: "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAABUTGI4",
83
+ memo: { type: "none" },
84
+ });
85
+ ```
86
+
87
+ This illustrative M address contains the G account shown in Stellar's examples
88
+ and ID 420. Publish only the actual route supplied by the recipient. No separate
89
+ ID/text/hash memo is permitted. V1 Resolvers cannot store M and fail before signing.
90
+ Use `setPayment` to explicitly replace or remove muxed routing; `setAddress` and
91
+ `setRecord` remain G/C account-address operations. Namespace/name ownership,
92
+ operator and signer addresses remain G/C. Muxed payment destinations do not act
93
+ as separate reverse/Primary identities.
67
94
 
68
95
  ## What's in the box
69
96
 
@@ -113,14 +140,21 @@ Primary writes verify the Primary contract's Registry anchor before signing.
113
140
  Custom Registry or passphrase settings do not inherit a Primary deployment pin;
114
141
  supply the matching `primaryId` explicitly.
115
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
+
116
150
  ## Verified testnet deployment
117
151
 
118
- Verified on 2026-09-05 at ledger 4520986. 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`.
119
153
 
120
154
  | Contract | Address |
121
155
  |---|---|
122
- | Registry | `CDSORANCV3IFF3MKHJ7KI4MKEJOJZFMTDVAZCD5XFOR4WTGNXJJNOKQE` |
123
- | Primary | `CCSORANOADXKLSW5CUANBW5WZFVCNZ5KZ4KNMIUWOZOES3LXYRUYZ56X` |
156
+ | Registry | `CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK` |
157
+ | Primary | `CASORAN755O3GCQTRAHKDXLLCSDLNKAQWAP6MWABRSFVSHLJOEKAC7AB` |
124
158
 
125
159
  Mainnet has no deployment preset. Custom networks must supply their own verified
126
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:
@@ -31,14 +32,33 @@
31
32
  * calls cannot race the account sequence number.
32
33
  */
33
34
  import { type PaymentDestination } from "./payment.js";
34
- export { PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination, type PaymentMemo, type PaymentDestination } from "./payment.js";
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: {
38
49
  readonly rpcUrl: "https://soroban-testnet.stellar.org";
39
50
  readonly passphrase: string;
40
- readonly registryId: "CDSORANCV3IFF3MKHJ7KI4MKEJOJZFMTDVAZCD5XFOR4WTGNXJJNOKQE";
41
- readonly primaryId: "CCSORANOADXKLSW5CUANBW5WZFVCNZ5KZ4KNMIUWOZOES3LXYRUYZ56X";
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: {
58
+ readonly rpcUrl: "https://soroban-testnet.stellar.org";
59
+ readonly passphrase: string;
60
+ readonly registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H";
61
+ readonly primaryId: "CCSORANJZOR5ZYTI4KAW34ESAQFMJAO4NKMTIVOVJOI2VDKCDK3RICXZ";
42
62
  };
43
63
  };
44
64
  /** Same shape as the owner SDK's signer — wallet-kit compatible. */
@@ -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
@@ -215,6 +248,8 @@ export declare class SoranHolder {
215
248
  private serialize;
216
249
  private sourceAccount;
217
250
  private assertSatisfiableAuth;
251
+ /** Native payment writes authorize exactly the locally selected destination. */
252
+ private assertPaymentIntent;
218
253
  private invoke;
219
254
  private attempt;
220
255
  private restore;
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:
@@ -31,8 +32,16 @@
31
32
  * calls cannot race the account sequence number.
32
33
  */
33
34
  import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, } from "@stellar/stellar-sdk";
34
- import { paymentFromNative, paymentMemoToScVal, validatePaymentDestination } from "./payment.js";
35
- export { PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination } from "./payment.js";
35
+ import { decodeMuxedAddress, destinationFromNative, paymentFromNative, paymentMemoToScVal, validatePaymentDestination } from "./payment.js";
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
  // ---------------------------------------------------------------------------
@@ -41,8 +50,17 @@ export const DEPLOYMENTS = {
41
50
  testnet: {
42
51
  rpcUrl: "https://soroban-testnet.stellar.org",
43
52
  passphrase: Networks.TESTNET,
44
- registryId: "CDSORANCV3IFF3MKHJ7KI4MKEJOJZFMTDVAZCD5XFOR4WTGNXJJNOKQE",
45
- primaryId: "CCSORANOADXKLSW5CUANBW5WZFVCNZ5KZ4KNMIUWOZOES3LXYRUYZ56X",
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: {
60
+ rpcUrl: "https://soroban-testnet.stellar.org",
61
+ passphrase: Networks.TESTNET,
62
+ registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H",
63
+ primaryId: "CCSORANJZOR5ZYTI4KAW34ESAQFMJAO4NKMTIVOVJOI2VDKCDK3RICXZ",
46
64
  },
47
65
  };
48
66
  /** A TxSigner over a raw secret key — for scripts and backends. */
@@ -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",
@@ -103,6 +122,7 @@ const RESOLVER_ERRORS = {
103
122
  18: "UsePaymentMethod",
104
123
  19: "PaymentContextMismatch",
105
124
  20: "PaymentUnavailable",
125
+ 21: "MuxedDestination",
106
126
  };
107
127
  const PRIMARY_ERRORS = {
108
128
  1: "MalformedName",
@@ -200,7 +220,11 @@ function nameNode(label, namespace) {
200
220
  return new Uint8Array(hash(concatBytes(namehash(namespace), labelHash)));
201
221
  }
202
222
  const labelArg = (label) => nativeToScVal(utf8(label), { type: "bytes" });
203
- const addrArg = (address) => nativeToScVal(address, { type: "address" });
223
+ const addrArg = (address) => {
224
+ if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address))
225
+ throw new HolderError("this account/address operation requires G or C; use setPayment for muxed destinations");
226
+ return nativeToScVal(address, { type: "address" });
227
+ };
204
228
  const bytesArg = (b) => nativeToScVal(b, { type: "bytes" });
205
229
  const SIM_SOURCE = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
206
230
  /** The standard profile keys shared with @sorandomains/lookup's profile(). */
@@ -218,6 +242,7 @@ export const PROFILE_KEYS = [
218
242
  // The client
219
243
  // ---------------------------------------------------------------------------
220
244
  export class SoranHolder {
245
+ maxNativeFeeStroops;
221
246
  server;
222
247
  passphrase;
223
248
  registryId;
@@ -230,6 +255,9 @@ export class SoranHolder {
230
255
  resolvers = new Map();
231
256
  static POINTER_TTL_MS = 30_000;
232
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");
233
261
  if (!opts?.signer)
234
262
  throw new HolderError("HolderOptions.signer is required");
235
263
  const d = DEPLOYMENTS[opts.network ?? "testnet"];
@@ -250,6 +278,19 @@ export class SoranHolder {
250
278
  this.timeoutSecs = t;
251
279
  this.fee = opts.fee ?? BASE_FEE;
252
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); }
253
294
  // ---- resolution targets --------------------------------------------------
254
295
  /** Atomically update the forward address and complete payment instruction.
255
296
  * Use memo {type:"none"} to explicitly publish a memo-free destination.
@@ -264,8 +305,18 @@ export class SoranHolder {
264
305
  catch (e) {
265
306
  throw new HolderError(String(e));
266
307
  }
267
- const { resolver } = await this.paymentResolverOf(namespace);
308
+ const { resolver, version } = await this.paymentResolverOf(namespace);
268
309
  const holder = await this.signer.publicKey();
310
+ if (StrKey.isValidMed25519PublicKey(payment.address)) {
311
+ if (version !== 2)
312
+ throw new HolderError("muxed destinations require native Resolver v2", resolver, "payment_version");
313
+ const muxed = decodeMuxedAddress(payment.address);
314
+ const r = await this.invoke(resolver, "set_muxed", [
315
+ nativeToScVal(`${label}.${namespace}`, { type: "string" }), addrArg(holder),
316
+ addrArg(muxed.account), nativeToScVal(BigInt(muxed.id), { type: "u64" }),
317
+ ], RESOLVER_ERRORS);
318
+ return { hash: r.hash, ledger: r.ledger };
319
+ }
269
320
  const r = await this.invoke(resolver, "set_payment", [
270
321
  nativeToScVal(`${label}.${namespace}`, { type: "string" }), addrArg(holder),
271
322
  addrArg(payment.address), paymentMemoToScVal(payment.memo),
@@ -525,21 +576,26 @@ export class SoranHolder {
525
576
  !(anchors[1] instanceof Uint8Array) || anchors[1].length !== nsNode.length ||
526
577
  !nsNode.every((byte, index) => anchors[1][index] === byte))
527
578
  throw new HolderError("Registrar anchors do not match this Registry and namespace", registrar, "anchors");
528
- if (version !== 1)
579
+ if (version !== 1 && version !== 2)
529
580
  throw new HolderError("unsupported native payment Resolver version", resolver, "payment_version");
530
- return { resolver, registrar };
581
+ if (version === 2 && await this.read(resolver, "destination_version", []) !== 2)
582
+ throw new HolderError("unsupported Resolver destination version", resolver, "destination_version");
583
+ return { resolver, registrar, version };
531
584
  }
532
585
  async assertMemoFree(name) {
533
586
  const { label, namespace } = parseName(name);
534
- const { resolver, registrar } = await this.paymentResolverOf(namespace);
535
- const raw = await this.read(resolver, "resolve_payment", [nativeToScVal(`${label}.${namespace}`, { type: "string" })]);
587
+ const { resolver, registrar, version } = await this.paymentResolverOf(namespace);
588
+ const fn = version === 2 ? "resolve_destination" : "resolve_payment";
589
+ const raw = await this.read(resolver, fn, [nativeToScVal(`${label}.${namespace}`, { type: "string" })]);
536
590
  let payment;
537
591
  try {
538
- payment = paymentFromNative(raw);
592
+ payment = version === 2 ? destinationFromNative(raw) : paymentFromNative(raw);
539
593
  }
540
594
  catch (e) {
541
- throw new HolderError(`invalid payment result: ${String(e)}`, resolver, "resolve_payment");
595
+ throw new HolderError(`invalid payment result: ${String(e)}`, resolver, fn);
542
596
  }
597
+ if (StrKey.isValidMed25519PublicKey(payment.address))
598
+ throw new HolderError("this name has a muxed routing ID; use setPayment to explicitly update its destination");
543
599
  if (payment.memo.type !== "none")
544
600
  throw new HolderError("this name requires a memo; use setPayment to update address and memo together");
545
601
  // Registrar-only write below cannot alter a Resolver's explicit addr/memo.
@@ -602,6 +658,26 @@ export class SoranHolder {
602
658
  : `${fn}: this operation must be authorized by ${required}, but the signer is ${pub} — use that account's signer`, contractId, fn);
603
659
  }
604
660
  }
661
+ /** Native payment writes authorize exactly the locally selected destination. */
662
+ assertPaymentIntent(prepared, pub, contractId, fn, args) {
663
+ if (fn !== "set_payment" && fn !== "set_muxed")
664
+ return;
665
+ if (prepared.source !== pub || prepared.operations.length !== 1)
666
+ throw new HolderError("unexpected payment transaction source or operation count", contractId, fn);
667
+ const op = prepared.operations[0];
668
+ if (op.type !== "invokeHostFunction" || (op.source !== undefined && op.source !== pub) || op.func.type !== "hostFunctionTypeInvokeContract")
669
+ throw new HolderError("unexpected payment operation", contractId, fn);
670
+ const call = op.func.invokeContract;
671
+ if (Address.fromScAddress(call.contractAddress).toString() !== contractId || call.functionName.toString() !== fn ||
672
+ call.args.length !== args.length || call.args.some((value, i) => value.toXDR("base64") !== args[i].toXDR("base64")))
673
+ throw new HolderError("payment operation differs from selected intent", contractId, fn);
674
+ if (!op.auth || op.auth.length !== 1 || op.auth[0].credentials.type !== "sorobanCredentialsSourceAccount")
675
+ throw new HolderError("payment requires exactly the source holder authorization", contractId, fn);
676
+ const root = op.auth[0].rootInvocation;
677
+ if (root.function.type !== "sorobanAuthorizedFunctionTypeContractFn" || root.subInvocations.length ||
678
+ root.function.contractFn.toXDR("base64") !== call.toXDR("base64"))
679
+ throw new HolderError("payment authorization differs from selected intent", contractId, fn);
680
+ }
605
681
  invoke(contractId, fn, args, errNames) {
606
682
  return this.serialize(async () => {
607
683
  try {
@@ -640,9 +716,12 @@ export class SoranHolder {
640
716
  }
641
717
  const prepared = rpc.assembleTransaction(tx, sim).build();
642
718
  this.assertSatisfiableAuth(prepared, pub, contractId, fn);
719
+ this.assertPaymentIntent(prepared, pub, contractId, fn, args);
643
720
  const txHash = toHex(prepared.hash()); // (SDK17) hash() is Uint8Array
644
721
  const signed = await this.signEnvelope(prepared.toXDR());
645
722
  const envelope = TransactionBuilder.fromXDR(signed, this.passphrase);
723
+ if ((fn === "set_payment" || fn === "set_muxed") && toHex(envelope.hash()) !== txHash)
724
+ throw new HolderError("signer changed the reviewed transaction body", contractId, fn);
646
725
  let sent;
647
726
  try {
648
727
  sent = await this.server.sendTransaction(envelope);
@@ -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
+ }