@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 +36 -9
- package/dist/index.d.ts +5 -3
- package/dist/index.js +55 -12
- package/dist/payment.d.ts +10 -1
- package/dist/payment.js +57 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# @sorandomains/holder
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
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`.
|
|
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
|
|
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
|
|
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 | `
|
|
123
|
-
| Primary | `
|
|
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: "
|
|
41
|
-
readonly primaryId: "
|
|
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: "
|
|
45
|
-
primaryId: "
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
+
"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": {
|