@sorandomains/holder 0.3.1 → 0.4.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
+ > Native muxed release. The testnet deployment below was verified on chain at
4
+ > ledger 4521644 on 5 September 2026 (18:10 UTC). See the
5
+ > [release status](https://docs.soran.domains/reference/release-status) for package and service availability.
6
+
7
+
8
+ Version 0.4.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.6.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
 
@@ -115,12 +142,12 @@ supply the matching `primaryId` explicitly.
115
142
 
116
143
  ## Verified testnet deployment
117
144
 
118
- Verified on 2026-09-05 at ledger 4520986. Network passphrase: `Test SDF Network ; September 2015`.
145
+ Verified on 5 September 2026 at ledger **4521644** (18:10 UTC). Network passphrase: `Test SDF Network ; September 2015`.
119
146
 
120
147
  | Contract | Address |
121
148
  |---|---|
122
- | Registry | `CDSORANCV3IFF3MKHJ7KI4MKEJOJZFMTDVAZCD5XFOR4WTGNXJJNOKQE` |
123
- | Primary | `CCSORANOADXKLSW5CUANBW5WZFVCNZ5KZ4KNMIUWOZOES3LXYRUYZ56X` |
149
+ | Registry | `CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H` |
150
+ | Primary | `CCSORANJZOR5ZYTI4KAW34ESAQFMJAO4NKMTIVOVJOI2VDKCDK3RICXZ` |
124
151
 
125
152
  Mainnet has no deployment preset. Custom networks must supply their own verified
126
153
  addresses. Universal Lookup upgrades remain immediately executable; an address
package/dist/index.d.ts CHANGED
@@ -31,14 +31,14 @@
31
31
  * calls cannot race the account sequence number.
32
32
  */
33
33
  import { type PaymentDestination } from "./payment.js";
34
- export { PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination, type PaymentMemo, type PaymentDestination } from "./payment.js";
34
+ export { encodeMuxedAddress, decodeMuxedAddress, PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination, type PaymentMemo, type PaymentDestination } from "./payment.js";
35
35
  /** Known public deployments. Pass explicit options for anything else. */
36
36
  export declare const DEPLOYMENTS: {
37
37
  readonly testnet: {
38
38
  readonly rpcUrl: "https://soroban-testnet.stellar.org";
39
39
  readonly passphrase: string;
40
- readonly registryId: "CDSORANCV3IFF3MKHJ7KI4MKEJOJZFMTDVAZCD5XFOR4WTGNXJJNOKQE";
41
- readonly primaryId: "CCSORANOADXKLSW5CUANBW5WZFVCNZ5KZ4KNMIUWOZOES3LXYRUYZ56X";
40
+ readonly registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H";
41
+ readonly primaryId: "CCSORANJZOR5ZYTI4KAW34ESAQFMJAO4NKMTIVOVJOI2VDKCDK3RICXZ";
42
42
  };
43
43
  };
44
44
  /** Same shape as the owner SDK's signer — wallet-kit compatible. */
@@ -215,6 +215,8 @@ export declare class SoranHolder {
215
215
  private serialize;
216
216
  private sourceAccount;
217
217
  private assertSatisfiableAuth;
218
+ /** Native payment writes authorize exactly the locally selected destination. */
219
+ private assertPaymentIntent;
218
220
  private invoke;
219
221
  private attempt;
220
222
  private restore;
package/dist/index.js CHANGED
@@ -31,8 +31,8 @@
31
31
  * calls cannot race the account sequence number.
32
32
  */
33
33
  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";
34
+ import { decodeMuxedAddress, destinationFromNative, paymentFromNative, paymentMemoToScVal, validatePaymentDestination } from "./payment.js";
35
+ export { encodeMuxedAddress, decodeMuxedAddress, PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination } from "./payment.js";
36
36
  // ---------------------------------------------------------------------------
37
37
  // Deployments
38
38
  // ---------------------------------------------------------------------------
@@ -41,8 +41,8 @@ export const DEPLOYMENTS = {
41
41
  testnet: {
42
42
  rpcUrl: "https://soroban-testnet.stellar.org",
43
43
  passphrase: Networks.TESTNET,
44
- registryId: "CDSORANCV3IFF3MKHJ7KI4MKEJOJZFMTDVAZCD5XFOR4WTGNXJJNOKQE",
45
- primaryId: "CCSORANOADXKLSW5CUANBW5WZFVCNZ5KZ4KNMIUWOZOES3LXYRUYZ56X",
44
+ registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H",
45
+ primaryId: "CCSORANJZOR5ZYTI4KAW34ESAQFMJAO4NKMTIVOVJOI2VDKCDK3RICXZ",
46
46
  },
47
47
  };
48
48
  /** A TxSigner over a raw secret key — for scripts and backends. */
@@ -103,6 +103,7 @@ const RESOLVER_ERRORS = {
103
103
  18: "UsePaymentMethod",
104
104
  19: "PaymentContextMismatch",
105
105
  20: "PaymentUnavailable",
106
+ 21: "MuxedDestination",
106
107
  };
107
108
  const PRIMARY_ERRORS = {
108
109
  1: "MalformedName",
@@ -200,7 +201,11 @@ function nameNode(label, namespace) {
200
201
  return new Uint8Array(hash(concatBytes(namehash(namespace), labelHash)));
201
202
  }
202
203
  const labelArg = (label) => nativeToScVal(utf8(label), { type: "bytes" });
203
- const addrArg = (address) => nativeToScVal(address, { type: "address" });
204
+ const addrArg = (address) => {
205
+ if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address))
206
+ throw new HolderError("this account/address operation requires G or C; use setPayment for muxed destinations");
207
+ return nativeToScVal(address, { type: "address" });
208
+ };
204
209
  const bytesArg = (b) => nativeToScVal(b, { type: "bytes" });
205
210
  const SIM_SOURCE = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
206
211
  /** The standard profile keys shared with @sorandomains/lookup's profile(). */
@@ -264,8 +269,18 @@ export class SoranHolder {
264
269
  catch (e) {
265
270
  throw new HolderError(String(e));
266
271
  }
267
- const { resolver } = await this.paymentResolverOf(namespace);
272
+ const { resolver, version } = await this.paymentResolverOf(namespace);
268
273
  const holder = await this.signer.publicKey();
274
+ if (StrKey.isValidMed25519PublicKey(payment.address)) {
275
+ if (version !== 2)
276
+ throw new HolderError("muxed destinations require native Resolver v2", resolver, "payment_version");
277
+ const muxed = decodeMuxedAddress(payment.address);
278
+ const r = await this.invoke(resolver, "set_muxed", [
279
+ nativeToScVal(`${label}.${namespace}`, { type: "string" }), addrArg(holder),
280
+ addrArg(muxed.account), nativeToScVal(BigInt(muxed.id), { type: "u64" }),
281
+ ], RESOLVER_ERRORS);
282
+ return { hash: r.hash, ledger: r.ledger };
283
+ }
269
284
  const r = await this.invoke(resolver, "set_payment", [
270
285
  nativeToScVal(`${label}.${namespace}`, { type: "string" }), addrArg(holder),
271
286
  addrArg(payment.address), paymentMemoToScVal(payment.memo),
@@ -525,21 +540,26 @@ export class SoranHolder {
525
540
  !(anchors[1] instanceof Uint8Array) || anchors[1].length !== nsNode.length ||
526
541
  !nsNode.every((byte, index) => anchors[1][index] === byte))
527
542
  throw new HolderError("Registrar anchors do not match this Registry and namespace", registrar, "anchors");
528
- if (version !== 1)
543
+ if (version !== 1 && version !== 2)
529
544
  throw new HolderError("unsupported native payment Resolver version", resolver, "payment_version");
530
- return { resolver, registrar };
545
+ if (version === 2 && await this.read(resolver, "destination_version", []) !== 2)
546
+ throw new HolderError("unsupported Resolver destination version", resolver, "destination_version");
547
+ return { resolver, registrar, version };
531
548
  }
532
549
  async assertMemoFree(name) {
533
550
  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" })]);
551
+ const { resolver, registrar, version } = await this.paymentResolverOf(namespace);
552
+ const fn = version === 2 ? "resolve_destination" : "resolve_payment";
553
+ const raw = await this.read(resolver, fn, [nativeToScVal(`${label}.${namespace}`, { type: "string" })]);
536
554
  let payment;
537
555
  try {
538
- payment = paymentFromNative(raw);
556
+ payment = version === 2 ? destinationFromNative(raw) : paymentFromNative(raw);
539
557
  }
540
558
  catch (e) {
541
- throw new HolderError(`invalid payment result: ${String(e)}`, resolver, "resolve_payment");
559
+ throw new HolderError(`invalid payment result: ${String(e)}`, resolver, fn);
542
560
  }
561
+ if (StrKey.isValidMed25519PublicKey(payment.address))
562
+ throw new HolderError("this name has a muxed routing ID; use setPayment to explicitly update its destination");
543
563
  if (payment.memo.type !== "none")
544
564
  throw new HolderError("this name requires a memo; use setPayment to update address and memo together");
545
565
  // Registrar-only write below cannot alter a Resolver's explicit addr/memo.
@@ -602,6 +622,26 @@ export class SoranHolder {
602
622
  : `${fn}: this operation must be authorized by ${required}, but the signer is ${pub} — use that account's signer`, contractId, fn);
603
623
  }
604
624
  }
625
+ /** Native payment writes authorize exactly the locally selected destination. */
626
+ assertPaymentIntent(prepared, pub, contractId, fn, args) {
627
+ if (fn !== "set_payment" && fn !== "set_muxed")
628
+ return;
629
+ if (prepared.source !== pub || prepared.operations.length !== 1)
630
+ throw new HolderError("unexpected payment transaction source or operation count", contractId, fn);
631
+ const op = prepared.operations[0];
632
+ if (op.type !== "invokeHostFunction" || (op.source !== undefined && op.source !== pub) || op.func.type !== "hostFunctionTypeInvokeContract")
633
+ throw new HolderError("unexpected payment operation", contractId, fn);
634
+ const call = op.func.invokeContract;
635
+ if (Address.fromScAddress(call.contractAddress).toString() !== contractId || call.functionName.toString() !== fn ||
636
+ call.args.length !== args.length || call.args.some((value, i) => value.toXDR("base64") !== args[i].toXDR("base64")))
637
+ throw new HolderError("payment operation differs from selected intent", contractId, fn);
638
+ if (!op.auth || op.auth.length !== 1 || op.auth[0].credentials.type !== "sorobanCredentialsSourceAccount")
639
+ throw new HolderError("payment requires exactly the source holder authorization", contractId, fn);
640
+ const root = op.auth[0].rootInvocation;
641
+ if (root.function.type !== "sorobanAuthorizedFunctionTypeContractFn" || root.subInvocations.length ||
642
+ root.function.contractFn.toXDR("base64") !== call.toXDR("base64"))
643
+ throw new HolderError("payment authorization differs from selected intent", contractId, fn);
644
+ }
605
645
  invoke(contractId, fn, args, errNames) {
606
646
  return this.serialize(async () => {
607
647
  try {
@@ -640,9 +680,12 @@ export class SoranHolder {
640
680
  }
641
681
  const prepared = rpc.assembleTransaction(tx, sim).build();
642
682
  this.assertSatisfiableAuth(prepared, pub, contractId, fn);
683
+ this.assertPaymentIntent(prepared, pub, contractId, fn, args);
643
684
  const txHash = toHex(prepared.hash()); // (SDK17) hash() is Uint8Array
644
685
  const signed = await this.signEnvelope(prepared.toXDR());
645
686
  const envelope = TransactionBuilder.fromXDR(signed, this.passphrase);
687
+ if ((fn === "set_payment" || fn === "set_muxed") && toHex(envelope.hash()) !== txHash)
688
+ throw new HolderError("signer changed the reviewed transaction body", contractId, fn);
646
689
  let sent;
647
690
  try {
648
691
  sent = await this.server.sendTransaction(envelope);
package/dist/payment.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** Payment wire format v1. Kept identical in the independently published lookup
1
+ /** Direct v1 and muxed v2 payment records. Kept identical in the independently published lookup
2
2
  * and holder packages; the conformance tests exercise both copies. */
3
3
  import { xdr } from "@stellar/stellar-sdk";
4
4
  export declare const PAYMENT_RECORD_KEY = "payment";
@@ -26,3 +26,12 @@ export declare function parsePaymentRecord(raw: string): PaymentDestination;
26
26
  export declare function paymentMemoToScVal(memo: PaymentMemo): xdr.ScVal;
27
27
  /** Decode Soroban contract enum values, checking arity and native types. */
28
28
  export declare function paymentFromNative(raw: unknown): PaymentDestination;
29
+ /** Decode a full muxed destination without treating its operation ID as a memo. */
30
+ export declare function decodeMuxedAddress(address: string): {
31
+ account: string;
32
+ id: string;
33
+ };
34
+ /** Encode only canonical G + exact decimal u64; numbers are never accepted. */
35
+ export declare function encodeMuxedAddress(account: string, id: string): string;
36
+ /** Strict additive on-chain destination enum; v1 payment structs stay separate. */
37
+ export declare function destinationFromNative(raw: unknown): PaymentDestination;
package/dist/payment.js CHANGED
@@ -1,4 +1,4 @@
1
- /** Payment wire format v1. Kept identical in the independently published lookup
1
+ /** Direct v1 and muxed v2 payment records. Kept identical in the independently published lookup
2
2
  * and holder packages; the conformance tests exercise both copies. */
3
3
  import { StrKey, nativeToScVal, xdr } from "@stellar/stellar-sdk";
4
4
  export const PAYMENT_RECORD_KEY = "payment";
@@ -11,14 +11,16 @@ export function validatePaymentDestination(value) {
11
11
  const p = value;
12
12
  if (Object.keys(p).sort().join(",") !== "address,memo")
13
13
  throw new Error("unexpected payment fields");
14
- if (typeof p.address !== "string" || (!StrKey.isValidEd25519PublicKey(p.address) && !StrKey.isValidContract(p.address)))
15
- throw new Error("payment address must be a valid G or C address");
14
+ if (typeof p.address !== "string" || (!StrKey.isValidEd25519PublicKey(p.address) && !StrKey.isValidContract(p.address) && !StrKey.isValidMed25519PublicKey(p.address)))
15
+ throw new Error("payment address must be a valid G, C or M address");
16
16
  if (!p.memo || typeof p.memo !== "object" || Array.isArray(p.memo))
17
17
  throw new Error("invalid memo");
18
18
  const m = p.memo;
19
19
  if (m.type === "none") {
20
20
  if (Object.keys(m).join(",") !== "type")
21
21
  throw new Error("none memo cannot have a value");
22
+ if (StrKey.isValidMed25519PublicKey(p.address))
23
+ decodeMuxedAddress(p.address);
22
24
  return { address: p.address, memo: { type: "none" } };
23
25
  }
24
26
  if (!StrKey.isValidEd25519PublicKey(p.address))
@@ -45,6 +47,10 @@ export function validatePaymentDestination(value) {
45
47
  }
46
48
  export function encodePaymentRecord(value) {
47
49
  const p = validatePaymentDestination(value);
50
+ if (StrKey.isValidMed25519PublicKey(p.address)) {
51
+ const muxed = decodeMuxedAddress(p.address);
52
+ return `2|${muxed.account}|muxed|${muxed.id}`;
53
+ }
48
54
  return `1|${p.address}|${p.memo.type}|${p.memo.type === "none" ? "" : p.memo.value}`;
49
55
  }
50
56
  /** Empty/missing records are not explicit payment instructions. */
@@ -54,11 +60,16 @@ export function parsePaymentRecord(raw) {
54
60
  const first = raw.indexOf("|");
55
61
  const second = raw.indexOf("|", first + 1);
56
62
  const third = raw.indexOf("|", second + 1);
57
- if (first < 0 || second < 0 || third < 0 || raw.slice(0, first) !== "1")
63
+ if (first < 0 || second < 0 || third < 0)
58
64
  throw new Error("unsupported payment record");
65
+ const version = raw.slice(0, first);
59
66
  const address = raw.slice(first + 1, second);
60
67
  const type = raw.slice(second + 1, third);
61
68
  const value = raw.slice(third + 1);
69
+ if (version === "2" && type === "muxed")
70
+ return { address: encodeMuxedAddress(address, value), memo: { type: "none" } };
71
+ if (version !== "1" || (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address)))
72
+ throw new Error("unsupported payment record");
62
73
  if (type === "none" && value !== "")
63
74
  throw new Error("none memo cannot have a value");
64
75
  return validatePaymentDestination({ address, memo: type === "none" ? { type } : { type, value } });
@@ -79,6 +90,8 @@ export function paymentFromNative(raw) {
79
90
  const p = raw;
80
91
  if (Object.keys(p).sort().join(",") !== "address,memo" || !Array.isArray(p.memo))
81
92
  throw new Error("invalid payment result fields");
93
+ if (typeof p.address !== "string" || (!StrKey.isValidEd25519PublicKey(p.address) && !StrKey.isValidContract(p.address)))
94
+ throw new Error("direct payment must contain a G or C address");
82
95
  const m = p.memo;
83
96
  let memo;
84
97
  if (m.length === 1 && m[0] === "None")
@@ -93,3 +106,43 @@ export function paymentFromNative(raw) {
93
106
  throw new Error("invalid payment memo result");
94
107
  return validatePaymentDestination({ address: p.address, memo });
95
108
  }
109
+ /** Decode a full muxed destination without treating its operation ID as a memo. */
110
+ export function decodeMuxedAddress(address) {
111
+ if (typeof address !== "string" || !StrKey.isValidMed25519PublicKey(address))
112
+ throw new Error("invalid muxed M address");
113
+ const raw = StrKey.decodeMed25519PublicKey(address);
114
+ if (raw.length !== 40 || StrKey.encodeMed25519PublicKey(raw) !== address)
115
+ throw new Error("noncanonical muxed address");
116
+ let id = 0n;
117
+ for (const byte of raw.subarray(32))
118
+ id = (id << 8n) | BigInt(byte);
119
+ return { account: StrKey.encodeEd25519PublicKey(raw.subarray(0, 32)), id: id.toString() };
120
+ }
121
+ /** Encode only canonical G + exact decimal u64; numbers are never accepted. */
122
+ export function encodeMuxedAddress(account, id) {
123
+ if (typeof account !== "string" || !StrKey.isValidEd25519PublicKey(account))
124
+ throw new Error("muxed base must be a G account");
125
+ if (typeof id !== "string" || !/^(0|[1-9][0-9]{0,19})$/.test(id) || BigInt(id) > MAX_U64)
126
+ throw new Error("muxed ID must be canonical decimal u64");
127
+ const raw = new Uint8Array(40);
128
+ raw.set(StrKey.decodeEd25519PublicKey(account));
129
+ let value = BigInt(id);
130
+ for (let i = 39; i >= 32; i--) {
131
+ raw[i] = Number(value & 255n);
132
+ value >>= 8n;
133
+ }
134
+ return StrKey.encodeMed25519PublicKey(raw);
135
+ }
136
+ /** Strict additive on-chain destination enum; v1 payment structs stay separate. */
137
+ export function destinationFromNative(raw) {
138
+ if (!Array.isArray(raw) || raw.length !== 2)
139
+ throw new Error("invalid destination enum");
140
+ if (raw[0] === "Direct")
141
+ return paymentFromNative(raw[1]);
142
+ if (raw[0] !== "Muxed" || !raw[1] || typeof raw[1] !== "object" || Array.isArray(raw[1]))
143
+ throw new Error("invalid destination variant");
144
+ const value = raw[1];
145
+ if (Object.keys(value).sort().join(",") !== "account,id" || typeof value.account !== "string" || typeof value.id !== "bigint")
146
+ throw new Error("invalid muxed destination fields");
147
+ return { address: encodeMuxedAddress(value.account, value.id.toString()), memo: { type: "none" } };
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorandomains/holder",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Manage your own Soran name on Stellar \u2014 records, profile, reverse, primary, and transfers, signed by your key.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,7 +19,7 @@
19
19
  "typecheck": "tsc --noEmit",
20
20
  "prepublishOnly": "tsc",
21
21
  "check:browser": "esbuild dist/index.js --bundle --platform=browser --external:@stellar/stellar-sdk --outfile=browser-check.js --legal-comments=none --log-level=error && node -e \"const fs=require('fs');const s=fs.readFileSync('browser-check.js','utf8');fs.unlinkSync('browser-check.js');if(/\\bBuffer\\b/.test(s)){console.error('FAIL: bare Buffer reference in browser bundle');process.exit(1)}console.log('browser bundle clean')\"",
22
- "test": "node --import tsx test/decode-failure.test.mts && node --import tsx --test test/payment.test.mts test/integration.test.mts"
22
+ "test": "node --import tsx test/decode-failure.test.mts && node --import tsx --test test/payment.test.mts test/integration.test.mts test/muxed.test.mts"
23
23
  },
24
24
  "license": "MIT",
25
25
  "repository": {