@sorandomains/owner 0.6.0 → 0.7.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 +13 -6
- package/dist/index.d.ts +50 -0
- package/dist/index.js +102 -1
- package/dist/native-allowlist.d.ts +21 -0
- package/dist/native-allowlist.js +34 -0
- package/dist/native-auth.d.ts +31 -0
- package/dist/native-auth.js +85 -0
- package/dist/native-codec.d.ts +38 -0
- package/dist/native-codec.js +95 -0
- package/dist/native-transport.d.ts +66 -0
- package/dist/native-transport.js +136 -0
- package/dist/native-types.d.ts +166 -0
- package/dist/native-types.js +98 -0
- package/dist/payment.d.ts +37 -0
- package/dist/payment.js +148 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @sorandomains/owner
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> ledger
|
|
3
|
+
|
|
4
|
+
> Native claim testnet deployment verified at ledger 4534629 on 6 September 2026 (12:12 UTC). See the
|
|
5
5
|
> [release status](https://docs.soran.domains/reference/release-status) for package and service availability.
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
Version 0.
|
|
8
|
+
Version 0.7.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
|
|
9
9
|
and labels are validated before lowercase normalization; Unicode lookalikes are
|
|
10
10
|
rejected. Writes continue to target the owning Registry/Registrar/Resolver. Universal
|
|
11
|
-
Lookup is the read entry point in `@sorandomains/lookup` 0.
|
|
11
|
+
Lookup is the read entry point in `@sorandomains/lookup` 0.7.0.
|
|
12
12
|
|
|
13
13
|
Ownership, issuer, holder and treasury inputs remain G/C account or contract
|
|
14
14
|
addresses. Muxed M addresses are payment destinations only: issue the name to its
|
|
@@ -83,13 +83,20 @@ code at all.
|
|
|
83
83
|
|
|
84
84
|
Docs: <https://github.com/SoranDomains/docs> · License: MIT
|
|
85
85
|
|
|
86
|
+
|
|
87
|
+
## Native username claiming
|
|
88
|
+
|
|
89
|
+
`claimPolicy` and `nativeClaimCapability` read the current on-chain setup. Configure it with `configureClaims`, `setClaimsEnabled`/`pauseClaims`, `reserveNames`, `releaseReservations` and `assignReserved`. Manual mode retains ordinary owner issuance; Public mode routes ordinary issuance through the public claim rules, while explicit reserved assignment remains an owner power. The default disabled policy is not an open registration service.
|
|
90
|
+
|
|
91
|
+
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.
|
|
92
|
+
|
|
86
93
|
## Verified testnet deployment
|
|
87
94
|
|
|
88
|
-
Verified on
|
|
95
|
+
Verified on 6 September 2026 at ledger **4534629** (12:12 UTC). Network passphrase: `Test SDF Network ; September 2015`.
|
|
89
96
|
|
|
90
97
|
| Contract | Address |
|
|
91
98
|
|---|---|
|
|
92
|
-
| Registry | `
|
|
99
|
+
| Registry | `CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK` |
|
|
93
100
|
|
|
94
101
|
Mainnet has no deployment preset. Custom networks must supply their own verified
|
|
95
102
|
addresses. Universal Lookup upgrades remain immediately executable; an address
|
package/dist/index.d.ts
CHANGED
|
@@ -35,9 +35,24 @@
|
|
|
35
35
|
* that one only. The few reads here (policy, pending transfers, records) exist
|
|
36
36
|
* to support write flows.
|
|
37
37
|
*/
|
|
38
|
+
import { type NativeWriteOptions, type NativeCapability } from "./native-transport.js";
|
|
39
|
+
import { type ClaimSettings, type ClaimConfig, type ClaimUsage, type ApprovalUsage } from "./native-types.js";
|
|
40
|
+
export * from "./native-types.js";
|
|
41
|
+
export * from "./native-allowlist.js";
|
|
42
|
+
export { NativeClaimError, namespaceNode as nativeNamespaceNode, paymentDestinationToScVal } from "./native-codec.js";
|
|
43
|
+
export { validateNativeTransaction, authorizedInvocation, type NativeAuthorizationPlan } from "./native-auth.js";
|
|
44
|
+
export type { NativeCapability, NativePrepared, NativeWriteOptions } from "./native-transport.js";
|
|
38
45
|
/** Known public deployments. Pass explicit options for anything else. */
|
|
39
46
|
export declare const DEPLOYMENTS: {
|
|
40
47
|
readonly testnet: {
|
|
48
|
+
readonly rpcUrl: "https://soroban-testnet.stellar.org";
|
|
49
|
+
readonly passphrase: string;
|
|
50
|
+
readonly registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK";
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
54
|
+
export declare const LEGACY_DEPLOYMENTS: {
|
|
55
|
+
readonly testnet20260905: {
|
|
41
56
|
readonly rpcUrl: "https://soroban-testnet.stellar.org";
|
|
42
57
|
readonly passphrase: string;
|
|
43
58
|
readonly registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H";
|
|
@@ -130,6 +145,8 @@ export type NameState = {
|
|
|
130
145
|
generation: bigint;
|
|
131
146
|
};
|
|
132
147
|
export type OwnerOptions = {
|
|
148
|
+
/** Upper total network fee for successor native operations; default 5 XLM. */
|
|
149
|
+
maxNativeFeeStroops?: bigint;
|
|
133
150
|
/** Signs every transaction. The namespace owner's account — or, for
|
|
134
151
|
* `acceptNamespaceTransfer`, the proposed new owner's. */
|
|
135
152
|
signer: TxSigner;
|
|
@@ -150,6 +167,7 @@ export type OwnerOptions = {
|
|
|
150
167
|
fee?: string;
|
|
151
168
|
};
|
|
152
169
|
export declare class SoranOwner {
|
|
170
|
+
private maxNativeFeeStroops;
|
|
153
171
|
private server;
|
|
154
172
|
private passphrase;
|
|
155
173
|
private registryId;
|
|
@@ -161,6 +179,38 @@ export declare class SoranOwner {
|
|
|
161
179
|
private static REGISTRAR_TTL_MS;
|
|
162
180
|
constructor(opts: OwnerOptions);
|
|
163
181
|
/** The namespace's on-chain attested Registrar id. Cached per instance. */
|
|
182
|
+
private nativeConfirmed;
|
|
183
|
+
private nativeContext;
|
|
184
|
+
/** Native successor capability. A failed RPC read never means open or legacy. */
|
|
185
|
+
nativeClaimCapability(namespace: string): Promise<NativeCapability>;
|
|
186
|
+
claimPolicy(namespace: string): Promise<ClaimConfig | null>;
|
|
187
|
+
nativeClaimFeeToken(namespace: string): Promise<string>;
|
|
188
|
+
private nativeOwnerWrite;
|
|
189
|
+
configureClaims(namespace: string, settings: ClaimSettings, options?: NativeWriteOptions): Promise<Submitted & {
|
|
190
|
+
config: ClaimConfig;
|
|
191
|
+
}>;
|
|
192
|
+
setClaimsEnabled(namespace: string, enabled: boolean, options?: NativeWriteOptions): Promise<Submitted & {
|
|
193
|
+
config: ClaimConfig;
|
|
194
|
+
}>;
|
|
195
|
+
pauseClaims(namespace: string, options?: NativeWriteOptions): Promise<Submitted & {
|
|
196
|
+
config: ClaimConfig;
|
|
197
|
+
}>;
|
|
198
|
+
private updateReservations;
|
|
199
|
+
reserveNames(namespace: string, labels: readonly string[], options?: NativeWriteOptions): Promise<{
|
|
200
|
+
hash: string;
|
|
201
|
+
ledger: number;
|
|
202
|
+
updated: number;
|
|
203
|
+
}>;
|
|
204
|
+
releaseReservations(namespace: string, labels: readonly string[], options?: NativeWriteOptions): Promise<{
|
|
205
|
+
hash: string;
|
|
206
|
+
ledger: number;
|
|
207
|
+
updated: number;
|
|
208
|
+
}>;
|
|
209
|
+
isReserved(namespace: string, label: string): Promise<boolean>;
|
|
210
|
+
/** Privileged reserved assignment publishes the holder's default route; no custom destination is implied. */
|
|
211
|
+
assignReserved(namespace: string, label: string, holder: string, options?: NativeWriteOptions): Promise<IssueResult>;
|
|
212
|
+
nativeClaimUsage(namespace: string, holder: string): Promise<ClaimUsage>;
|
|
213
|
+
nativeApprovalUsage(namespace: string): Promise<ApprovalUsage>;
|
|
164
214
|
registrarOf(namespace: string): Promise<string>;
|
|
165
215
|
/** The namespace's current owner on the Registry, or null if unallocated. */
|
|
166
216
|
namespaceOwner(namespace: string): Promise<string | null>;
|
package/dist/index.js
CHANGED
|
@@ -84,12 +84,29 @@ function decodeIssuedEvents(events, registrarId) {
|
|
|
84
84
|
}
|
|
85
85
|
return out;
|
|
86
86
|
}
|
|
87
|
+
import { NativeClaimError, address as nativeAddress, bool as nativeBool, bytes32 as nativeBytes32, hex as nativeHex, sc as nativeSc } from "./native-codec.js";
|
|
88
|
+
import { requireNative, nativeCapability, sendNative } from "./native-transport.js";
|
|
89
|
+
import { claimSettingsToScVal, claimConfigFromNative, claimLabelToScVal, claimUsageFromNative, approvalUsageFromNative } from "./native-types.js";
|
|
90
|
+
export * from "./native-types.js";
|
|
91
|
+
export * from "./native-allowlist.js";
|
|
92
|
+
export { NativeClaimError, namespaceNode as nativeNamespaceNode, paymentDestinationToScVal } from "./native-codec.js";
|
|
93
|
+
export { validateNativeTransaction, authorizedInvocation } from "./native-auth.js";
|
|
87
94
|
// ---------------------------------------------------------------------------
|
|
88
95
|
// Deployments
|
|
89
96
|
// ---------------------------------------------------------------------------
|
|
90
97
|
/** Known public deployments. Pass explicit options for anything else. */
|
|
91
98
|
export const DEPLOYMENTS = {
|
|
92
99
|
testnet: {
|
|
100
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
101
|
+
passphrase: Networks.TESTNET,
|
|
102
|
+
// The immutable Registry. The Registrar for a namespace is discovered on
|
|
103
|
+
// chain via `registrar_of(node)` — never configured by hand.
|
|
104
|
+
registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK",
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
108
|
+
export const LEGACY_DEPLOYMENTS = {
|
|
109
|
+
testnet20260905: {
|
|
93
110
|
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
94
111
|
passphrase: Networks.TESTNET,
|
|
95
112
|
// The immutable Registry. The Registrar for a namespace is discovered on
|
|
@@ -134,6 +151,7 @@ const REGISTRAR_ERRORS = {
|
|
|
134
151
|
18: "InvalidPolicy",
|
|
135
152
|
19: "ExpiryOverflow",
|
|
136
153
|
20: "InvalidRegistry",
|
|
154
|
+
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",
|
|
137
155
|
};
|
|
138
156
|
/** Registry contract error codes, by number. */
|
|
139
157
|
const REGISTRY_ERRORS = {
|
|
@@ -256,6 +274,7 @@ export const MAX_BATCH = 23;
|
|
|
256
274
|
// The client
|
|
257
275
|
// ---------------------------------------------------------------------------
|
|
258
276
|
export class SoranOwner {
|
|
277
|
+
maxNativeFeeStroops;
|
|
259
278
|
server;
|
|
260
279
|
passphrase;
|
|
261
280
|
registryId;
|
|
@@ -271,6 +290,9 @@ export class SoranOwner {
|
|
|
271
290
|
registrars = new Map();
|
|
272
291
|
static REGISTRAR_TTL_MS = 30_000;
|
|
273
292
|
constructor(opts) {
|
|
293
|
+
this.maxNativeFeeStroops = opts?.maxNativeFeeStroops ?? 50000000n;
|
|
294
|
+
if (typeof this.maxNativeFeeStroops !== "bigint" || this.maxNativeFeeStroops <= 0n || this.maxNativeFeeStroops > 4294967295n)
|
|
295
|
+
throw new NativeClaimError("maximum native network fee must be positive");
|
|
274
296
|
if (!opts?.signer)
|
|
275
297
|
throw new OwnerError("OwnerOptions.signer is required");
|
|
276
298
|
const d = DEPLOYMENTS[opts.network ?? "testnet"];
|
|
@@ -290,6 +312,85 @@ export class SoranOwner {
|
|
|
290
312
|
}
|
|
291
313
|
// ---- discovery -----------------------------------------------------------
|
|
292
314
|
/** The namespace's on-chain attested Registrar id. Cached per instance. */
|
|
315
|
+
nativeConfirmed(result, decode) {
|
|
316
|
+
try {
|
|
317
|
+
return decode(result.value);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
throw new NativeClaimError(`transaction confirmed but its result could not be verified: ${String(error)}`, "pending", result.hash);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
nativeContext() {
|
|
324
|
+
return { registryId: this.registryId, passphrase: this.passphrase, server: this.server, signer: this.signer, fee: this.fee, timeoutSecs: this.timeoutSecs, maxFeeStroops: this.maxNativeFeeStroops,
|
|
325
|
+
read: (id, method, args) => this.read(id, method, args), serialize: work => this.serialize(work) };
|
|
326
|
+
}
|
|
327
|
+
/** Native successor capability. A failed RPC read never means open or legacy. */
|
|
328
|
+
nativeClaimCapability(namespace) { return nativeCapability(this.nativeContext(), normalizeLabel(namespace)); }
|
|
329
|
+
async claimPolicy(namespace) {
|
|
330
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
331
|
+
const raw = await this.read(cap.registrar, "claim_config", []);
|
|
332
|
+
return raw === null ? null : claimConfigFromNative(raw);
|
|
333
|
+
}
|
|
334
|
+
async nativeClaimFeeToken(namespace) {
|
|
335
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
336
|
+
return nativeAddress(await this.read(cap.registrar, "native_fee_token", []), "contract", "native XLM token");
|
|
337
|
+
}
|
|
338
|
+
async nativeOwnerWrite(namespace, method, args, options = {}) {
|
|
339
|
+
return this.serialize(async () => {
|
|
340
|
+
const context = this.nativeContext();
|
|
341
|
+
const cap = await requireNative(context, normalizeLabel(namespace));
|
|
342
|
+
const source = nativeAddress(await this.signer.publicKey(), "account", "owner transaction source");
|
|
343
|
+
if (cap.owner !== source)
|
|
344
|
+
throw new NativeClaimError("the current namespace owner must authorize this change", "authorization");
|
|
345
|
+
return sendNative(context, { source, contract: cap.registrar, method, args, sourceInvocation: { contract: cap.registrar, method, args }, maxFeeStroops: context.maxFeeStroops }, options);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async configureClaims(namespace, settings, options = {}) {
|
|
349
|
+
const encoded = claimSettingsToScVal(settings);
|
|
350
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
351
|
+
const token = nativeAddress(await this.read(cap.registrar, "native_fee_token", []), "contract", "native fee token");
|
|
352
|
+
if (settings.feeToken !== token)
|
|
353
|
+
throw new NativeClaimError("username fee must use this network's canonical XLM token");
|
|
354
|
+
if (settings.admission.type === "approval" && settings.admission.account === cap.owner)
|
|
355
|
+
throw new NativeClaimError("eligibility account must be separate from namespace owner");
|
|
356
|
+
const result = await this.nativeOwnerWrite(namespace, "configure_claims", [encoded], options);
|
|
357
|
+
return { hash: result.hash, ledger: result.ledger, config: this.nativeConfirmed(result, claimConfigFromNative) };
|
|
358
|
+
}
|
|
359
|
+
async setClaimsEnabled(namespace, enabled, options = {}) {
|
|
360
|
+
const result = await this.nativeOwnerWrite(namespace, "set_claim_enabled", [nativeSc.bool(nativeBool(enabled, "claim enabled"))], options);
|
|
361
|
+
return { hash: result.hash, ledger: result.ledger, config: this.nativeConfirmed(result, claimConfigFromNative) };
|
|
362
|
+
}
|
|
363
|
+
pauseClaims(namespace, options = {}) { return this.setClaimsEnabled(namespace, false, options); }
|
|
364
|
+
async updateReservations(namespace, labels, reserved, options) {
|
|
365
|
+
if (!Array.isArray(labels) || labels.length < 1 || labels.length > 23)
|
|
366
|
+
throw new NativeClaimError("reservation batches must contain 1–23 labels");
|
|
367
|
+
const normalized = labels.map(normalizeLabel);
|
|
368
|
+
if (new Set(normalized).size !== normalized.length)
|
|
369
|
+
throw new NativeClaimError("duplicate reservation label");
|
|
370
|
+
const result = await this.nativeOwnerWrite(namespace, "set_reserved", [xdr.ScVal.scvVec(normalized.map(claimLabelToScVal)), nativeSc.bool(reserved)], options);
|
|
371
|
+
if (typeof result.value !== "number" || result.value !== normalized.length)
|
|
372
|
+
throw new NativeClaimError("confirmed reservation result differs from batch; inspect the transaction before retrying", "pending", result.hash);
|
|
373
|
+
return { hash: result.hash, ledger: result.ledger, updated: result.value };
|
|
374
|
+
}
|
|
375
|
+
reserveNames(namespace, labels, options = {}) { return this.updateReservations(namespace, labels, true, options); }
|
|
376
|
+
releaseReservations(namespace, labels, options = {}) { return this.updateReservations(namespace, labels, false, options); }
|
|
377
|
+
async isReserved(namespace, label) {
|
|
378
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
379
|
+
return nativeBool(await this.read(cap.registrar, "is_reserved", [claimLabelToScVal(normalizeLabel(label))]), "reservation");
|
|
380
|
+
}
|
|
381
|
+
/** Privileged reserved assignment publishes the holder's default route; no custom destination is implied. */
|
|
382
|
+
async assignReserved(namespace, label, holder, options = {}) {
|
|
383
|
+
const result = await this.nativeOwnerWrite(namespace, "issue_reserved", [claimLabelToScVal(normalizeLabel(label)), nativeSc.address(nativeAddress(holder, "identity", "reserved holder"))], options);
|
|
384
|
+
return { hash: result.hash, ledger: result.ledger, node: this.nativeConfirmed(result, raw => nativeHex(nativeBytes32(raw, "issued name node"))) };
|
|
385
|
+
}
|
|
386
|
+
async nativeClaimUsage(namespace, holder) {
|
|
387
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
388
|
+
return claimUsageFromNative(await this.read(cap.registrar, "claim_usage", [nativeSc.address(nativeAddress(holder, "identity", "holder"))]));
|
|
389
|
+
}
|
|
390
|
+
async nativeApprovalUsage(namespace) {
|
|
391
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
392
|
+
return approvalUsageFromNative(await this.read(cap.registrar, "approval_usage", []));
|
|
393
|
+
}
|
|
293
394
|
async registrarOf(namespace) {
|
|
294
395
|
namespace = normalizeLabel(namespace);
|
|
295
396
|
const hit = this.registrars.get(namespace);
|
|
@@ -561,7 +662,7 @@ export class SoranOwner {
|
|
|
561
662
|
throw new OwnerError(`${fn}: the on-chain entry is archived (rent lapsed) — any write operation restores it automatically, or restore/touch it first`, contractId, fn);
|
|
562
663
|
}
|
|
563
664
|
if (!rpc.Api.isSimulationSuccess(sim) || !sim.result?.retval)
|
|
564
|
-
return
|
|
665
|
+
throw new OwnerError(`${fn}: missing simulation return value`, contractId, fn);
|
|
565
666
|
const v = scValToNative(sim.result.retval);
|
|
566
667
|
return v === undefined ? null : v;
|
|
567
668
|
}
|
|
@@ -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,31 @@
|
|
|
1
|
+
import { Transaction, authorizeEntry, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
export type NativeInvocation = {
|
|
3
|
+
contract: string;
|
|
4
|
+
method: string;
|
|
5
|
+
args: xdr.ScVal[];
|
|
6
|
+
children?: NativeInvocation[];
|
|
7
|
+
};
|
|
8
|
+
/** Derive this plan locally from a validated intent, never from a remote envelope. */
|
|
9
|
+
export type NativeAuthorizationPlan = {
|
|
10
|
+
source: string;
|
|
11
|
+
contract: string;
|
|
12
|
+
method: string;
|
|
13
|
+
args: xdr.ScVal[];
|
|
14
|
+
sourceInvocation: NativeInvocation;
|
|
15
|
+
eligibility?: {
|
|
16
|
+
account: string;
|
|
17
|
+
invocation: NativeInvocation;
|
|
18
|
+
latestLedger: number;
|
|
19
|
+
maxExpirationLedger: number;
|
|
20
|
+
};
|
|
21
|
+
maxFeeStroops: bigint;
|
|
22
|
+
};
|
|
23
|
+
export declare function authorizedInvocation(value: NativeInvocation): xdr.SorobanAuthorizedInvocation;
|
|
24
|
+
export declare function validateEligibilityAuthorization(entry: xdr.SorobanAuthorizationEntry, expected: NonNullable<NativeAuthorizationPlan["eligibility"]>, requireSigned?: boolean): void;
|
|
25
|
+
/** Native account signing callback; hardware/custody integration keeps keys outside the SDK. */
|
|
26
|
+
export type EligibilitySigner = Parameters<typeof authorizeEntry>[1];
|
|
27
|
+
/** Signs only the expected admission entry. This helper never signs a transaction. */
|
|
28
|
+
export declare function signEligibilityAuthorization(unsignedEntry: xdr.SorobanAuthorizationEntry, expected: NonNullable<NativeAuthorizationPlan["eligibility"]>, signer: EligibilitySigner, expirationLedger: number, networkPassphrase: string): Promise<xdr.SorobanAuthorizationEntry>;
|
|
29
|
+
/** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
|
|
30
|
+
export declare function validateNativeTransaction(tx: Transaction, plan: NativeAuthorizationPlan, requireEligibilitySigned?: boolean): void;
|
|
31
|
+
export declare function assertSignedBodyUnchanged(prepared: Transaction, signedXdr: string, passphrase: string): Transaction;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Address, Transaction, TransactionBuilder, authorizeEntry, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { NativeClaimError, address, hex, u32 } from "./native-codec.js";
|
|
3
|
+
export function authorizedInvocation(value) {
|
|
4
|
+
return new xdr.SorobanAuthorizedInvocation({
|
|
5
|
+
function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(new xdr.InvokeContractArgs({
|
|
6
|
+
contractAddress: new Address(address(value.contract, "contract", "invocation contract")).toScAddress(), functionName: value.method, args: value.args,
|
|
7
|
+
})), subInvocations: (value.children ?? []).map(authorizedInvocation),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function sameInvocation(actual, expected) {
|
|
11
|
+
return actual.toXDR("base64") === authorizedInvocation(expected).toXDR("base64");
|
|
12
|
+
}
|
|
13
|
+
export function validateEligibilityAuthorization(entry, expected, requireSigned = true) {
|
|
14
|
+
if (expected.invocation.method !== "claim" || expected.invocation.args.length !== 1 || (expected.invocation.children?.length ?? 0) !== 0)
|
|
15
|
+
throw new NativeClaimError("eligibility may authorize only one exact native claim intent", "authorization");
|
|
16
|
+
address(expected.account, "account", "eligibility account");
|
|
17
|
+
u32(expected.latestLedger, "latest ledger");
|
|
18
|
+
u32(expected.maxExpirationLedger, "maximum auth expiration ledger");
|
|
19
|
+
if (entry.credentials.type !== "sorobanCredentialsAddress")
|
|
20
|
+
throw new NativeClaimError("eligibility requires the supported native G address credential", "authorization");
|
|
21
|
+
const credential = entry.credentials.address;
|
|
22
|
+
if (Address.fromScAddress(credential.address).toString() !== expected.account || !sameInvocation(entry.rootInvocation, expected.invocation) || entry.rootInvocation.subInvocations.length !== 0)
|
|
23
|
+
throw new NativeClaimError("eligibility authorization differs from the exact claim intent", "authorization");
|
|
24
|
+
if (requireSigned) {
|
|
25
|
+
if (credential.signatureExpirationLedger <= expected.latestLedger || credential.signatureExpirationLedger > expected.maxExpirationLedger)
|
|
26
|
+
throw new NativeClaimError("eligibility authorization expiry is outside the approved ledger bound", "authorization");
|
|
27
|
+
if (credential.signature.type !== "scvVec" || !credential.signature.vec?.length)
|
|
28
|
+
throw new NativeClaimError("eligibility authorization has no native account signatures", "authorization");
|
|
29
|
+
}
|
|
30
|
+
else if (credential.signature.type !== "scvVoid" && !(credential.signature.type === "scvVec" && credential.signature.vec?.length === 0)) {
|
|
31
|
+
throw new NativeClaimError("refusing to sign an already signed eligibility credential", "authorization");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Signs only the expected admission entry. This helper never signs a transaction. */
|
|
35
|
+
export async function signEligibilityAuthorization(unsignedEntry, expected, signer, expirationLedger, networkPassphrase) {
|
|
36
|
+
validateEligibilityAuthorization(unsignedEntry, expected, false);
|
|
37
|
+
u32(expirationLedger, "expiration ledger");
|
|
38
|
+
if (expirationLedger <= expected.latestLedger || expirationLedger > expected.maxExpirationLedger)
|
|
39
|
+
throw new NativeClaimError("eligibility expiration is outside the approved range", "authorization");
|
|
40
|
+
if (!networkPassphrase)
|
|
41
|
+
throw new NativeClaimError("network passphrase is required");
|
|
42
|
+
const signed = await authorizeEntry(unsignedEntry, signer, expirationLedger, networkPassphrase);
|
|
43
|
+
validateEligibilityAuthorization(signed, expected);
|
|
44
|
+
return signed;
|
|
45
|
+
}
|
|
46
|
+
/** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
|
|
47
|
+
export function validateNativeTransaction(tx, plan, requireEligibilitySigned = true) {
|
|
48
|
+
address(plan.source, "account", "claim transaction source");
|
|
49
|
+
if (tx.source !== plan.source)
|
|
50
|
+
throw new NativeClaimError(`native transaction source ${tx.source} differs from reviewed source ${plan.source}`, "authorization");
|
|
51
|
+
if (tx.operations.length !== 1)
|
|
52
|
+
throw new NativeClaimError(`native transaction has ${tx.operations.length} operations; exactly one was reviewed`, "authorization");
|
|
53
|
+
if (tx.memo.type !== "none")
|
|
54
|
+
throw new NativeClaimError(`native transaction memo type ${tx.memo.type} differs from reviewed none`, "authorization");
|
|
55
|
+
if (BigInt(tx.fee) > plan.maxFeeStroops)
|
|
56
|
+
throw new NativeClaimError(`native transaction network fee ${tx.fee} stroops exceeds the configured maximum ${plan.maxFeeStroops} stroops`, "authorization");
|
|
57
|
+
const op = tx.operations[0];
|
|
58
|
+
if (op.type !== "invokeHostFunction" || op.source && op.source !== plan.source || op.func.type !== "hostFunctionTypeInvokeContract")
|
|
59
|
+
throw new NativeClaimError("unexpected native operation", "authorization");
|
|
60
|
+
const call = op.func.invokeContract;
|
|
61
|
+
if (Address.fromScAddress(call.contractAddress).toString() !== plan.contract || call.functionName.toString() !== plan.method || call.args.length !== plan.args.length || call.args.some((arg, i) => arg.toXDR("base64") !== plan.args[i].toXDR("base64")))
|
|
62
|
+
throw new NativeClaimError("native operation differs from the exact reviewed intent", "authorization");
|
|
63
|
+
const entries = op.auth ?? [];
|
|
64
|
+
if (entries.length !== (plan.eligibility ? 2 : 1))
|
|
65
|
+
throw new NativeClaimError("unexpected native authorization count", "authorization");
|
|
66
|
+
const sources = entries.filter(entry => entry.credentials.type === "sorobanCredentialsSourceAccount");
|
|
67
|
+
if (sources.length !== 1 || !sameInvocation(sources[0].rootInvocation, plan.sourceInvocation))
|
|
68
|
+
throw new NativeClaimError("holder or owner authorization differs from exact reviewed intent", "authorization");
|
|
69
|
+
if (plan.eligibility) {
|
|
70
|
+
if (plan.eligibility.account === plan.source)
|
|
71
|
+
throw new NativeClaimError("eligibility account must be separate from claimant", "authorization");
|
|
72
|
+
const separate = entries.filter(entry => entry.credentials.type !== "sorobanCredentialsSourceAccount");
|
|
73
|
+
if (separate.length !== 1)
|
|
74
|
+
throw new NativeClaimError("unexpected second authorizer", "authorization");
|
|
75
|
+
validateEligibilityAuthorization(separate[0], plan.eligibility, requireEligibilitySigned);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function assertSignedBodyUnchanged(prepared, signedXdr, passphrase) {
|
|
79
|
+
const signed = TransactionBuilder.fromXDR(signedXdr, passphrase);
|
|
80
|
+
if (!(signed instanceof Transaction) || hex(signed.hash()) !== hex(prepared.hash()))
|
|
81
|
+
throw new NativeClaimError("wallet changed the reviewed native transaction body", "authorization");
|
|
82
|
+
if (!signed.signatures.length)
|
|
83
|
+
throw new NativeClaimError("wallet returned an unsigned transaction", "authorization");
|
|
84
|
+
return signed;
|
|
85
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Local successor-ABI helpers. No deployed defaults are changed by this module. */
|
|
2
|
+
import { xdr } from "@stellar/stellar-sdk";
|
|
3
|
+
import { type PaymentDestination } from "./payment.js";
|
|
4
|
+
export declare const NATIVE_REGISTRAR_ERRORS: Record<number, string>;
|
|
5
|
+
export declare class NativeClaimError extends Error {
|
|
6
|
+
readonly kind: "unsupported" | "unavailable" | "invalid" | "authorization" | "pending" | "failed";
|
|
7
|
+
readonly txHash: string | null;
|
|
8
|
+
readonly contractCode: number | null;
|
|
9
|
+
readonly contractError: string | null;
|
|
10
|
+
constructor(message: string, kind?: "unsupported" | "unavailable" | "invalid" | "authorization" | "pending" | "failed", txHash?: string | null);
|
|
11
|
+
}
|
|
12
|
+
export declare function exactObject(raw: unknown, keys: readonly string[], label: string): Record<string, unknown>;
|
|
13
|
+
export declare function u64(value: unknown, label: string): bigint;
|
|
14
|
+
export declare function u32(value: unknown, label: string): number;
|
|
15
|
+
export declare function amount(value: unknown, label: string): bigint;
|
|
16
|
+
export declare function bool(value: unknown, label: string): boolean;
|
|
17
|
+
export declare function address(value: unknown, kind: "account" | "contract" | "identity", label: string): string;
|
|
18
|
+
export declare function bytes32(value: unknown, label: string): Uint8Array;
|
|
19
|
+
export declare function hex32(value: unknown, label: string): string;
|
|
20
|
+
export declare const hex: (value: Uint8Array) => string;
|
|
21
|
+
export declare const unhex: (value: string) => Uint8Array;
|
|
22
|
+
export declare const utf8: (value: string) => Uint8Array;
|
|
23
|
+
export declare function label(value: unknown): string;
|
|
24
|
+
export declare function namespaceNode(namespace: string): Uint8Array;
|
|
25
|
+
export declare const sc: {
|
|
26
|
+
address: (value: string) => xdr.ScVal;
|
|
27
|
+
bytes: (value: Uint8Array) => xdr.ScVal;
|
|
28
|
+
u64: (value: bigint) => xdr.ScVal;
|
|
29
|
+
u32: (value: number) => xdr.ScVal;
|
|
30
|
+
i128: (value: bigint) => xdr.ScVal;
|
|
31
|
+
bool: (value: boolean) => xdr.ScVal;
|
|
32
|
+
symbol: (value: string) => xdr.ScVal;
|
|
33
|
+
option: (value: xdr.ScVal | null) => xdr.ScVal;
|
|
34
|
+
};
|
|
35
|
+
/** Contract structs are canonical symbol-keyed maps, not generic JSON maps. */
|
|
36
|
+
export declare function struct(fields: Record<string, xdr.ScVal>): xdr.ScVal;
|
|
37
|
+
export declare function paymentDestinationToScVal(raw: PaymentDestination): xdr.ScVal;
|
|
38
|
+
export declare function sameScVal(left: xdr.ScVal, right: xdr.ScVal): boolean;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Local successor-ABI helpers. No deployed defaults are changed by this module. */
|
|
2
|
+
import { Address, StrKey, hash, nativeToScVal, xdr } from "@stellar/stellar-sdk";
|
|
3
|
+
import { decodeMuxedAddress, paymentMemoToScVal, validatePaymentDestination } from "./payment.js";
|
|
4
|
+
export const NATIVE_REGISTRAR_ERRORS = { 21: "InvalidClaimConfig", 22: "ClaimsNotConfigured", 23: "ClaimsPaused", 24: "StaleClaimPolicy", 25: "ClaimIntentMismatch", 26: "ClaimIntentExpired", 27: "ReservedName", 28: "NameNotReserved", 29: "PublicIssuanceRequired", 30: "WalletClaimLimit", 31: "InvalidEligibility", 32: "ApprovalAllowanceReached", 33: "ApprovalRateReached", 34: "RequestIdConflict", 35: "CounterOverflow", 36: "InvalidNativeBinding", 37: "UnsupportedClaimant", 38: "DuplicateLabel", 39: "RenewalTooEarly", 40: "RenewalLeaseLimit", 41: "DestinationInitializationFailed", 42: "FeeSettlementFailed" };
|
|
5
|
+
export class NativeClaimError extends Error {
|
|
6
|
+
kind;
|
|
7
|
+
txHash;
|
|
8
|
+
contractCode;
|
|
9
|
+
contractError;
|
|
10
|
+
constructor(message, kind = "invalid", txHash = null) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.kind = kind;
|
|
13
|
+
this.txHash = txHash;
|
|
14
|
+
this.name = "NativeClaimError";
|
|
15
|
+
const match = /Error\(Contract, #(\d+)\)/.exec(message);
|
|
16
|
+
this.contractCode = match ? Number(match[1]) : null;
|
|
17
|
+
this.contractError = this.contractCode === null ? null : NATIVE_REGISTRAR_ERRORS[this.contractCode] ?? null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function exactObject(raw, keys, label) {
|
|
21
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).sort().join(",") !== [...keys].sort().join(","))
|
|
22
|
+
throw new NativeClaimError(`invalid ${label} fields`);
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
export function u64(value, label) {
|
|
26
|
+
if (typeof value !== "bigint" || value < 0n || value > 18446744073709551615n)
|
|
27
|
+
throw new NativeClaimError(`${label} must be an unsigned 64-bit bigint`);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export function u32(value, label) {
|
|
31
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 4294967295)
|
|
32
|
+
throw new NativeClaimError(`${label} must be an unsigned 32-bit number`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
export function amount(value, label) {
|
|
36
|
+
if (typeof value !== "bigint" || value < 0n || value > (1n << 127n) - 1n)
|
|
37
|
+
throw new NativeClaimError(`${label} must be a nonnegative i128 bigint`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
export function bool(value, label) {
|
|
41
|
+
if (typeof value !== "boolean")
|
|
42
|
+
throw new NativeClaimError(`${label} must be boolean`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
export function address(value, kind, label) {
|
|
46
|
+
if (typeof value !== "string" || !(kind !== "contract" && StrKey.isValidEd25519PublicKey(value) || kind !== "account" && StrKey.isValidContract(value)))
|
|
47
|
+
throw new NativeClaimError(`${label} must be a valid ${kind === "identity" ? "G or C" : kind === "account" ? "G" : "C"} address`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
export function bytes32(value, label) {
|
|
51
|
+
if (!(value instanceof Uint8Array) || value.length !== 32)
|
|
52
|
+
throw new NativeClaimError(`${label} must contain exactly 32 bytes`);
|
|
53
|
+
return new Uint8Array(value);
|
|
54
|
+
}
|
|
55
|
+
export function hex32(value, label) {
|
|
56
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value))
|
|
57
|
+
throw new NativeClaimError(`${label} must be 64 lowercase hex characters`);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
export const hex = (value) => Array.from(value, byte => byte.toString(16).padStart(2, "0")).join("");
|
|
61
|
+
export const unhex = (value) => Uint8Array.from(hex32(value, "32-byte value").match(/../g), byte => parseInt(byte, 16));
|
|
62
|
+
export const utf8 = (value) => new TextEncoder().encode(value);
|
|
63
|
+
export function label(value) {
|
|
64
|
+
if (typeof value !== "string" || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(value))
|
|
65
|
+
throw new NativeClaimError("intent label must be canonical lowercase ASCII, 1–63 characters");
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
export function namespaceNode(namespace) {
|
|
69
|
+
const combined = new Uint8Array(64);
|
|
70
|
+
combined.set(hash(utf8(label(namespace))), 32);
|
|
71
|
+
return new Uint8Array(hash(combined));
|
|
72
|
+
}
|
|
73
|
+
export const sc = {
|
|
74
|
+
address: (value) => new Address(value).toScVal(),
|
|
75
|
+
bytes: (value) => nativeToScVal(value, { type: "bytes" }),
|
|
76
|
+
u64: (value) => nativeToScVal(value, { type: "u64" }),
|
|
77
|
+
u32: (value) => nativeToScVal(value, { type: "u32" }),
|
|
78
|
+
i128: (value) => nativeToScVal(value, { type: "i128" }),
|
|
79
|
+
bool: (value) => nativeToScVal(value, { type: "bool" }),
|
|
80
|
+
symbol: (value) => nativeToScVal(value, { type: "symbol" }),
|
|
81
|
+
option: (value) => value ?? xdr.ScVal.scvVoid(),
|
|
82
|
+
};
|
|
83
|
+
/** Contract structs are canonical symbol-keyed maps, not generic JSON maps. */
|
|
84
|
+
export function struct(fields) {
|
|
85
|
+
return xdr.ScVal.scvMap(Object.keys(fields).sort().map(key => new xdr.ScMapEntry({ key: sc.symbol(key), val: fields[key] })));
|
|
86
|
+
}
|
|
87
|
+
export function paymentDestinationToScVal(raw) {
|
|
88
|
+
const payment = validatePaymentDestination(raw);
|
|
89
|
+
if (StrKey.isValidMed25519PublicKey(payment.address)) {
|
|
90
|
+
const value = decodeMuxedAddress(payment.address);
|
|
91
|
+
return xdr.ScVal.scvVec([sc.symbol("Muxed"), struct({ account: sc.address(value.account), id: sc.u64(BigInt(value.id)) })]);
|
|
92
|
+
}
|
|
93
|
+
return xdr.ScVal.scvVec([sc.symbol("Direct"), struct({ address: sc.address(payment.address), memo: paymentMemoToScVal(payment.memo) })]);
|
|
94
|
+
}
|
|
95
|
+
export function sameScVal(left, right) { return left.toXDR("base64") === right.toXDR("base64"); }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Transaction, rpc, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { type NativeAuthorizationPlan } from "./native-auth.js";
|
|
3
|
+
export type NativeSigner = {
|
|
4
|
+
publicKey(): string | Promise<string>;
|
|
5
|
+
signTransaction(encoded: string, opts: {
|
|
6
|
+
networkPassphrase: string;
|
|
7
|
+
}): Promise<string | {
|
|
8
|
+
signedTxXdr: string;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
export type NativeContext = {
|
|
12
|
+
registryId: string;
|
|
13
|
+
passphrase: string;
|
|
14
|
+
server: rpc.Server;
|
|
15
|
+
signer: NativeSigner;
|
|
16
|
+
fee: string;
|
|
17
|
+
timeoutSecs: number;
|
|
18
|
+
maxFeeStroops: bigint;
|
|
19
|
+
read(contract: string, method: string, args: xdr.ScVal[]): Promise<unknown>;
|
|
20
|
+
serialize<T>(work: () => Promise<T>): Promise<T>;
|
|
21
|
+
};
|
|
22
|
+
export type NativeCapability = {
|
|
23
|
+
supported: false;
|
|
24
|
+
registrar: string;
|
|
25
|
+
reason: "legacy-template" | "unsupported-version";
|
|
26
|
+
} | {
|
|
27
|
+
supported: true;
|
|
28
|
+
version: 1;
|
|
29
|
+
registrar: string;
|
|
30
|
+
resolver: string;
|
|
31
|
+
registry: string;
|
|
32
|
+
namespace: string;
|
|
33
|
+
owner: string;
|
|
34
|
+
ownerEpoch: bigint;
|
|
35
|
+
};
|
|
36
|
+
/** Historical reads must not trust an upgraded Registrar that can fabricate receipts. Resolver changes do not block this Registrar-only check. */
|
|
37
|
+
export declare function verifyRegistrarProvenance(context: NativeContext, registrar: string, namespace: string): Promise<void>;
|
|
38
|
+
export declare function nativeCapability(context: NativeContext, namespace: string): Promise<NativeCapability>;
|
|
39
|
+
export declare function requireNative(context: NativeContext, namespace: string): Promise<Extract<NativeCapability, {
|
|
40
|
+
supported: true;
|
|
41
|
+
}>>;
|
|
42
|
+
export type NativePrepared = {
|
|
43
|
+
transactionXdr: string;
|
|
44
|
+
hash: string;
|
|
45
|
+
networkPassphrase: string;
|
|
46
|
+
feeStroops: bigint;
|
|
47
|
+
eligibilityEntryXdr: string | null;
|
|
48
|
+
};
|
|
49
|
+
export type NativeWriteOptions = {
|
|
50
|
+
/** Exact native admission authorization from the app. This is not an owner transaction signature. */
|
|
51
|
+
eligibilityAuthorization?: string;
|
|
52
|
+
/** Persist the public recovery reference before wallet signing/broadcast. Rejecting stops submission. */
|
|
53
|
+
onPrepared?: (prepared: NativePrepared) => void | Promise<void>;
|
|
54
|
+
};
|
|
55
|
+
export declare function prepareNative(context: NativeContext, plan: NativeAuthorizationPlan, options?: NativeWriteOptions): Promise<{
|
|
56
|
+
transaction: Transaction;
|
|
57
|
+
prepared: NativePrepared;
|
|
58
|
+
}>;
|
|
59
|
+
/** No automatic transaction retry, implicit restoration or hash-losing error path. */
|
|
60
|
+
export declare function sendNative(context: NativeContext, plan: NativeAuthorizationPlan, options?: NativeWriteOptions): Promise<{
|
|
61
|
+
hash: string;
|
|
62
|
+
ledger: number;
|
|
63
|
+
value: unknown;
|
|
64
|
+
}>;
|
|
65
|
+
/** Bind terminal RPC context before classifying success or failure. An incomplete response remains uncertain. */
|
|
66
|
+
export declare function validateNativeTerminalReceipt(receipt: rpc.Api.GetSuccessfulTransactionResponse | rpc.Api.GetFailedTransactionResponse, expectedHash: string, passphrase: string): void;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Contract, Operation, Transaction, TransactionBuilder, rpc, scValToNative, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { NativeClaimError, address, bytes32, hex, namespaceNode, sc, unhex } from "./native-codec.js";
|
|
3
|
+
import { assertSignedBodyUnchanged, validateEligibilityAuthorization, validateNativeTransaction } from "./native-auth.js";
|
|
4
|
+
/** This published legacy code has owner-only issuance. Missing RPC data is never legacy detection. */
|
|
5
|
+
const LEGACY_REGISTRAR_HASH = "ed06b3374ff4342b4a2316fc546505132cd8fd35be6666571cf088710af9bbc6";
|
|
6
|
+
/** Historical reads must not trust an upgraded Registrar that can fabricate receipts. Resolver changes do not block this Registrar-only check. */
|
|
7
|
+
export async function verifyRegistrarProvenance(context, registrar, namespace) {
|
|
8
|
+
const node = sc.bytes(unhex(namespace));
|
|
9
|
+
const [attested, tainted, templates, instance] = await Promise.all([
|
|
10
|
+
context.read(context.registryId, "registrar_of", [node]), context.read(context.registryId, "registrar_tainted", [node]),
|
|
11
|
+
context.read(context.registryId, "template_hashes", []), context.server.getContractInstance(registrar),
|
|
12
|
+
]);
|
|
13
|
+
if (attested !== registrar || tainted !== false || !Array.isArray(templates) || templates.length !== 2 || instance.executable.type !== "contractExecutableWasm")
|
|
14
|
+
throw new NativeClaimError("Registrar provenance is unavailable or tainted; refusing authoritative claim/receipt reads", "unavailable");
|
|
15
|
+
if (hex(bytes32(templates[0], "Registry Registrar template")) !== hex(instance.executable.wasmHash.value))
|
|
16
|
+
throw new NativeClaimError("Registrar executable differs from immutable Registry template", "unavailable");
|
|
17
|
+
}
|
|
18
|
+
export async function nativeCapability(context, namespace) {
|
|
19
|
+
const node = namespaceNode(namespace), nodeArg = sc.bytes(node);
|
|
20
|
+
const registrar = address(await context.read(context.registryId, "registrar_of", [nodeArg]), "contract", "namespace Registrar");
|
|
21
|
+
const instance = await context.server.getContractInstance(registrar);
|
|
22
|
+
if (instance.executable.type === "contractExecutableWasm" && hex(instance.executable.wasmHash.value) === LEGACY_REGISTRAR_HASH)
|
|
23
|
+
return { supported: false, registrar, reason: "legacy-template" };
|
|
24
|
+
await verifyRegistrarProvenance(context, registrar, hex(node));
|
|
25
|
+
const version = await context.read(registrar, "claim_version", []);
|
|
26
|
+
if (typeof version !== "number" || !Number.isInteger(version))
|
|
27
|
+
throw new NativeClaimError("malformed native claim capability", "unavailable");
|
|
28
|
+
if (version !== 1)
|
|
29
|
+
return { supported: false, registrar, reason: "unsupported-version" };
|
|
30
|
+
const [pair, epoch, owner, anchors] = await Promise.all([
|
|
31
|
+
context.read(context.registryId, "native_contracts", [nodeArg]), context.read(context.registryId, "owner_epoch", [nodeArg]),
|
|
32
|
+
context.read(context.registryId, "owner_of", [nodeArg]), context.read(registrar, "anchors", []),
|
|
33
|
+
]);
|
|
34
|
+
if (!Array.isArray(pair) || pair.length !== 2 || pair[0] !== registrar)
|
|
35
|
+
throw new NativeClaimError("native contract binding does not match Registrar", "unavailable");
|
|
36
|
+
const resolver = address(pair[1], "contract", "native Resolver");
|
|
37
|
+
if (!Array.isArray(anchors) || anchors.length !== 2 || anchors[0] !== context.registryId || !(anchors[1] instanceof Uint8Array) || hex(anchors[1]) !== hex(node))
|
|
38
|
+
throw new NativeClaimError("Registrar anchors differ from selected namespace", "unavailable");
|
|
39
|
+
if (typeof epoch !== "bigint" || epoch < 1n)
|
|
40
|
+
throw new NativeClaimError("invalid namespace ownership epoch", "unavailable");
|
|
41
|
+
if (await context.read(resolver, "initialization_version", []) !== 1)
|
|
42
|
+
throw new NativeClaimError("unsupported native Resolver initializer", "unsupported");
|
|
43
|
+
return { supported: true, version: 1, registrar, resolver, registry: context.registryId, namespace, owner: address(owner, "identity", "namespace owner"), ownerEpoch: epoch };
|
|
44
|
+
}
|
|
45
|
+
export async function requireNative(context, namespace) {
|
|
46
|
+
const capability = await nativeCapability(context, namespace);
|
|
47
|
+
if (!capability.supported)
|
|
48
|
+
throw new NativeClaimError(`namespace has no supported native claim interface (${capability.reason})`, "unsupported");
|
|
49
|
+
return capability;
|
|
50
|
+
}
|
|
51
|
+
export async function prepareNative(context, plan, options = {}) {
|
|
52
|
+
const source = address(await context.signer.publicKey(), "account", "transaction signer");
|
|
53
|
+
if (source !== plan.source)
|
|
54
|
+
throw new NativeClaimError("wallet changed since the intent was reviewed", "authorization");
|
|
55
|
+
const raw = new TransactionBuilder(await context.server.getAccount(source), { fee: context.fee, networkPassphrase: context.passphrase })
|
|
56
|
+
.addOperation(new Contract(plan.contract).call(plan.method, ...plan.args)).setTimeout(context.timeoutSecs).build();
|
|
57
|
+
const simulate = async (tx) => {
|
|
58
|
+
const result = await context.server.simulateTransaction(tx, undefined, undefined, false);
|
|
59
|
+
if (rpc.Api.isSimulationRestore(result))
|
|
60
|
+
throw new NativeClaimError("native operation requires storage restoration; review and restore separately before resuming this intent", "unavailable");
|
|
61
|
+
if (rpc.Api.isSimulationError(result))
|
|
62
|
+
throw new NativeClaimError(`${plan.method}: ${result.error}`, "failed");
|
|
63
|
+
if (!rpc.Api.isSimulationSuccess(result) || !result.result)
|
|
64
|
+
throw new NativeClaimError("native simulation returned no result", "unavailable");
|
|
65
|
+
return result;
|
|
66
|
+
};
|
|
67
|
+
let transaction = rpc.assembleTransaction(raw, await simulate(raw)).build();
|
|
68
|
+
validateNativeTransaction(transaction, plan, false);
|
|
69
|
+
if (options.eligibilityAuthorization) {
|
|
70
|
+
if (options.eligibilityAuthorization.length > 32768)
|
|
71
|
+
throw new NativeClaimError("eligibility authorization is too large", "authorization");
|
|
72
|
+
if (!plan.eligibility)
|
|
73
|
+
throw new NativeClaimError("this native operation does not require an app approver", "authorization");
|
|
74
|
+
const signed = xdr.SorobanAuthorizationEntry.fromXDR(options.eligibilityAuthorization, "base64");
|
|
75
|
+
validateEligibilityAuthorization(signed, plan.eligibility);
|
|
76
|
+
const op = transaction.operations[0];
|
|
77
|
+
if (op.type !== "invokeHostFunction")
|
|
78
|
+
throw new NativeClaimError("unexpected prepared operation");
|
|
79
|
+
const auth = (op.auth ?? []).map(entry => entry.credentials.type === "sorobanCredentialsSourceAccount" ? entry : signed);
|
|
80
|
+
transaction = TransactionBuilder.cloneFrom(transaction, { networkPassphrase: context.passphrase }).clearOperations().addOperation(Operation.invokeHostFunction({ func: op.func, auth })).build();
|
|
81
|
+
transaction = rpc.assembleTransaction(transaction, await simulate(transaction)).build();
|
|
82
|
+
validateNativeTransaction(transaction, plan);
|
|
83
|
+
}
|
|
84
|
+
const op = transaction.operations[0];
|
|
85
|
+
if (op.type !== "invokeHostFunction")
|
|
86
|
+
throw new NativeClaimError("unexpected native operation");
|
|
87
|
+
const approval = (op.auth ?? []).find(entry => entry.credentials.type !== "sorobanCredentialsSourceAccount");
|
|
88
|
+
return { transaction, prepared: { transactionXdr: transaction.toXDR(), hash: hex(transaction.hash()), networkPassphrase: context.passphrase, feeStroops: BigInt(transaction.fee), eligibilityEntryXdr: approval?.toXDR("base64") ?? null } };
|
|
89
|
+
}
|
|
90
|
+
/** No automatic transaction retry, implicit restoration or hash-losing error path. */
|
|
91
|
+
export async function sendNative(context, plan, options = {}) {
|
|
92
|
+
const { transaction, prepared } = await prepareNative(context, plan, options);
|
|
93
|
+
validateNativeTransaction(transaction, plan);
|
|
94
|
+
await options.onPrepared?.(prepared);
|
|
95
|
+
if (await context.signer.publicKey() !== plan.source)
|
|
96
|
+
throw new NativeClaimError("wallet changed before signing", "authorization");
|
|
97
|
+
const result = await context.signer.signTransaction(prepared.transactionXdr, { networkPassphrase: context.passphrase });
|
|
98
|
+
const signed = assertSignedBodyUnchanged(transaction, typeof result === "string" ? result : result.signedTxXdr, context.passphrase);
|
|
99
|
+
try {
|
|
100
|
+
const sent = await context.server.sendTransaction(signed);
|
|
101
|
+
if (sent.hash !== prepared.hash)
|
|
102
|
+
throw new NativeClaimError("RPC returned a different transaction hash", "pending", prepared.hash);
|
|
103
|
+
if (sent.status === "ERROR" || sent.status === "TRY_AGAIN_LATER")
|
|
104
|
+
throw new NativeClaimError(`transaction was not accepted (${sent.status}); reconcile the original receipt and hash`, "pending", prepared.hash);
|
|
105
|
+
for (let attempt = 0; attempt < context.timeoutSecs + 5; attempt++) {
|
|
106
|
+
const receipt = await context.server.getTransaction(prepared.hash);
|
|
107
|
+
if (receipt.status === "SUCCESS" || receipt.status === "FAILED") {
|
|
108
|
+
// Status alone cannot identify the reviewed transaction or establish inclusion.
|
|
109
|
+
validateNativeTerminalReceipt(receipt, prepared.hash, context.passphrase);
|
|
110
|
+
if (receipt.status === "SUCCESS")
|
|
111
|
+
return { hash: prepared.hash, ledger: receipt.ledger, value: receipt.returnValue ? scValToNative(receipt.returnValue) : null };
|
|
112
|
+
throw new NativeClaimError("native transaction failed on chain", "failed", prepared.hash);
|
|
113
|
+
}
|
|
114
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
115
|
+
}
|
|
116
|
+
throw new NativeClaimError("transaction outcome is unknown; recover the original receipt and hash before replacing the request", "pending", prepared.hash);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof NativeClaimError && error.txHash)
|
|
120
|
+
throw error;
|
|
121
|
+
throw new NativeClaimError(`submission/confirmation interrupted: ${String(error)}; reconcile the original receipt`, "pending", prepared.hash);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Bind terminal RPC context before classifying success or failure. An incomplete response remains uncertain. */
|
|
125
|
+
export function validateNativeTerminalReceipt(receipt, expectedHash, passphrase) {
|
|
126
|
+
try {
|
|
127
|
+
if (receipt.txHash !== expectedHash || !Number.isInteger(receipt.ledger) || receipt.ledger <= 0 || receipt.ledger > 0xffff_ffff || !receipt.envelopeXdr)
|
|
128
|
+
throw new Error("missing or mismatched hash/ledger/envelope");
|
|
129
|
+
const included = TransactionBuilder.fromXDR(receipt.envelopeXdr, passphrase);
|
|
130
|
+
if (!(included instanceof Transaction) || hex(included.hash()) !== expectedHash)
|
|
131
|
+
throw new Error("different included envelope");
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new NativeClaimError("terminal RPC response does not prove inclusion of the original reviewed transaction; reconcile its hash", "pending", expectedHash);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { type PaymentDestination } from "./payment.js";
|
|
3
|
+
export type ClaimAdmission = {
|
|
4
|
+
type: "open";
|
|
5
|
+
} | {
|
|
6
|
+
type: "allowlist";
|
|
7
|
+
root: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "approval";
|
|
10
|
+
account: string;
|
|
11
|
+
};
|
|
12
|
+
export type ClaimSettings = {
|
|
13
|
+
mode: "manual" | "public";
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
admission: ClaimAdmission;
|
|
16
|
+
feeToken: string;
|
|
17
|
+
feeAmount: bigint;
|
|
18
|
+
feeRecipient: string;
|
|
19
|
+
walletLimit: bigint;
|
|
20
|
+
approvalAllowance: bigint;
|
|
21
|
+
approvalRateLimit: bigint;
|
|
22
|
+
approvalWindowSecs: bigint;
|
|
23
|
+
approvalTtlSecs: bigint;
|
|
24
|
+
};
|
|
25
|
+
export type ClaimConfig = {
|
|
26
|
+
settings: ClaimSettings;
|
|
27
|
+
ownerEpoch: bigint;
|
|
28
|
+
policyVersion: bigint;
|
|
29
|
+
grantEpoch: bigint;
|
|
30
|
+
};
|
|
31
|
+
export type RequestContext = {
|
|
32
|
+
network: string;
|
|
33
|
+
registry: string;
|
|
34
|
+
registrar: string;
|
|
35
|
+
namespace: string;
|
|
36
|
+
requestId: string;
|
|
37
|
+
validAfter: bigint;
|
|
38
|
+
deadline: bigint;
|
|
39
|
+
};
|
|
40
|
+
export type ClaimIntent = {
|
|
41
|
+
context: RequestContext;
|
|
42
|
+
label: string;
|
|
43
|
+
claimant: string;
|
|
44
|
+
destination: PaymentDestination;
|
|
45
|
+
resolver: string;
|
|
46
|
+
expectedGeneration: bigint | null;
|
|
47
|
+
expectedExpiry: bigint | null;
|
|
48
|
+
termSecs: bigint;
|
|
49
|
+
ownerEpoch: bigint;
|
|
50
|
+
policyVersion: bigint;
|
|
51
|
+
grantEpoch: bigint;
|
|
52
|
+
feeToken: string;
|
|
53
|
+
feeAmount: bigint;
|
|
54
|
+
feeRecipient: string;
|
|
55
|
+
};
|
|
56
|
+
export type ReservedIntent = {
|
|
57
|
+
context: RequestContext;
|
|
58
|
+
label: string;
|
|
59
|
+
holder: string;
|
|
60
|
+
destination: PaymentDestination;
|
|
61
|
+
resolver: string;
|
|
62
|
+
expectedGeneration: bigint | null;
|
|
63
|
+
expectedExpiry: bigint | null;
|
|
64
|
+
termSecs: bigint;
|
|
65
|
+
ownerEpoch: bigint;
|
|
66
|
+
policyVersion: bigint;
|
|
67
|
+
};
|
|
68
|
+
export type TransferIntent = {
|
|
69
|
+
context: RequestContext;
|
|
70
|
+
label: string;
|
|
71
|
+
from: string;
|
|
72
|
+
to: string;
|
|
73
|
+
expectedGeneration: bigint;
|
|
74
|
+
expectedExpiry: bigint;
|
|
75
|
+
proposalExpires: bigint;
|
|
76
|
+
destination: PaymentDestination;
|
|
77
|
+
resolver: string;
|
|
78
|
+
};
|
|
79
|
+
export type RenewIntent = {
|
|
80
|
+
context: RequestContext;
|
|
81
|
+
label: string;
|
|
82
|
+
holder: string;
|
|
83
|
+
expectedGeneration: bigint;
|
|
84
|
+
expectedExpiry: bigint;
|
|
85
|
+
termSecs: bigint;
|
|
86
|
+
minNewExpiry: bigint;
|
|
87
|
+
maxNewExpiry: bigint;
|
|
88
|
+
};
|
|
89
|
+
export type ClaimReceipt = {
|
|
90
|
+
operation: "claim" | "reserved" | "transfer" | "renew";
|
|
91
|
+
intentHash: string;
|
|
92
|
+
node: string;
|
|
93
|
+
holder: string;
|
|
94
|
+
generation: bigint;
|
|
95
|
+
expiresAt: bigint;
|
|
96
|
+
feeAmount: bigint;
|
|
97
|
+
feeToken: string;
|
|
98
|
+
feeRecipient: string | null;
|
|
99
|
+
ledger: number;
|
|
100
|
+
timestamp: bigint;
|
|
101
|
+
};
|
|
102
|
+
export type ClaimResult = {
|
|
103
|
+
status: "fresh" | "replayed";
|
|
104
|
+
receipt: ClaimReceipt;
|
|
105
|
+
};
|
|
106
|
+
export type ClaimUsage = {
|
|
107
|
+
publicClaims: bigint;
|
|
108
|
+
reservedIssues: bigint;
|
|
109
|
+
};
|
|
110
|
+
export type ApprovalUsage = {
|
|
111
|
+
total: bigint;
|
|
112
|
+
windowUsed: bigint;
|
|
113
|
+
windowEnds: bigint;
|
|
114
|
+
};
|
|
115
|
+
export type ClaimQuote = {
|
|
116
|
+
network: string;
|
|
117
|
+
registry: string;
|
|
118
|
+
registrar: string;
|
|
119
|
+
namespace: string;
|
|
120
|
+
resolver: string;
|
|
121
|
+
label: string;
|
|
122
|
+
node: string;
|
|
123
|
+
claimant: string;
|
|
124
|
+
config: ClaimConfig | null;
|
|
125
|
+
ownerEpoch: bigint;
|
|
126
|
+
policyVersion: bigint;
|
|
127
|
+
available: boolean;
|
|
128
|
+
reserved: boolean;
|
|
129
|
+
generation: bigint | null;
|
|
130
|
+
expiresAt: bigint | null;
|
|
131
|
+
termSecs: bigint;
|
|
132
|
+
now: bigint;
|
|
133
|
+
usage: ClaimUsage;
|
|
134
|
+
approvalUsage: ApprovalUsage;
|
|
135
|
+
};
|
|
136
|
+
export type DestinationPreview = {
|
|
137
|
+
node: string;
|
|
138
|
+
holder: string;
|
|
139
|
+
generation: bigint;
|
|
140
|
+
expiresAt: bigint;
|
|
141
|
+
active: boolean;
|
|
142
|
+
destination: PaymentDestination;
|
|
143
|
+
};
|
|
144
|
+
export declare function claimSettingsToScVal(value: ClaimSettings): xdr.ScVal;
|
|
145
|
+
export declare const claimPolicyInputToScVal: typeof claimSettingsToScVal;
|
|
146
|
+
export declare const claimLabelToScVal: (value: string) => xdr.ScVal;
|
|
147
|
+
export declare const claimConfigFromNative: (value: unknown) => ClaimConfig;
|
|
148
|
+
export declare const claimQuoteFromNative: (value: unknown) => ClaimQuote;
|
|
149
|
+
export declare const claimUsageFromNative: (value: unknown) => ClaimUsage;
|
|
150
|
+
export declare const approvalUsageFromNative: (value: unknown) => ApprovalUsage;
|
|
151
|
+
export declare const destinationPreviewFromNative: (value: unknown) => DestinationPreview;
|
|
152
|
+
export declare const claimReceiptFromNative: (value: unknown) => ClaimReceipt;
|
|
153
|
+
export declare function claimIntentToScVal(value: ClaimIntent): xdr.ScVal;
|
|
154
|
+
export declare function reservedIntentToScVal(value: ReservedIntent): xdr.ScVal;
|
|
155
|
+
export declare function transferIntentToScVal(value: TransferIntent): xdr.ScVal;
|
|
156
|
+
export declare function renewIntentToScVal(value: RenewIntent): xdr.ScVal;
|
|
157
|
+
export declare const claimIntentFromNative: (value: unknown) => ClaimIntent;
|
|
158
|
+
export declare const transferIntentFromNative: (value: unknown) => TransferIntent;
|
|
159
|
+
export declare const renewIntentFromNative: (value: unknown) => RenewIntent;
|
|
160
|
+
export declare function claimResultFromNative(value: unknown): ClaimResult;
|
|
161
|
+
export declare function nativeIntentHash(method: "claim" | "issue_reserved_with_destination" | "accept_transfer_with_destination" | "renew_holder", value: xdr.ScVal): string;
|
|
162
|
+
/** Lossless public recovery JSON; only declared bigint fields receive the tagged encoding. */
|
|
163
|
+
export declare function stringifyNativeIntent(value: ClaimIntent | TransferIntent | RenewIntent | ReservedIntent): string;
|
|
164
|
+
export declare function parseNativeClaimIntent(json: string): ClaimIntent;
|
|
165
|
+
export declare function parseNativeTransferIntent(json: string): TransferIntent;
|
|
166
|
+
export declare function parseNativeRenewIntent(json: string): RenewIntent;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { hash, scValToNative, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { destinationFromNative } from "./payment.js";
|
|
3
|
+
import { NativeClaimError, address, amount, bool, bytes32, exactObject, hex, hex32, label, paymentDestinationToScVal, sc, struct, u32, u64, unhex, utf8 } from "./native-codec.js";
|
|
4
|
+
const primitive = (validate, encode) => ({ encode: v => encode(validate(v)), decode: validate });
|
|
5
|
+
const U64 = primitive(v => u64(v, "u64 field"), sc.u64), U32 = primitive(v => u32(v, "u32 field"), sc.u32), AMOUNT = primitive(v => amount(v, "fee amount"), sc.i128), BOOL = primitive(v => bool(v, "boolean field"), sc.bool);
|
|
6
|
+
const ACCOUNT = primitive(v => address(v, "account", "account"), sc.address), CONTRACT = primitive(v => address(v, "contract", "contract"), sc.address), IDENTITY = primitive(v => address(v, "identity", "identity"), sc.address);
|
|
7
|
+
const HEX = { encode: v => sc.bytes(unhex(hex32(v, "bytes32 field"))), decode: v => hex(bytes32(v, "bytes32 field")) };
|
|
8
|
+
const LABEL = { encode: v => sc.bytes(utf8(label(v))), decode: v => { if (!(v instanceof Uint8Array))
|
|
9
|
+
throw new NativeClaimError("label must be on-chain Bytes"); return label(new TextDecoder("utf-8", { fatal: true }).decode(v)); } };
|
|
10
|
+
const DESTINATION = { encode: v => paymentDestinationToScVal(v), decode: destinationFromNative };
|
|
11
|
+
const optional = (inner) => ({ encode: v => v === null ? sc.option(null) : inner.encode(v), decode: v => v === null || v === undefined ? null : inner.decode(v) });
|
|
12
|
+
const enumeration = (mapping) => ({ encode: v => { if (typeof v !== "string" || !Object.hasOwn(mapping, v))
|
|
13
|
+
throw new NativeClaimError("unsupported enum value"); return xdr.ScVal.scvVec([sc.symbol(mapping[v])]); }, decode: v => { if (!Array.isArray(v) || v.length !== 1)
|
|
14
|
+
throw new NativeClaimError("invalid enum result"); const entry = Object.entries(mapping).find(([, tag]) => tag === v[0]); if (!entry)
|
|
15
|
+
throw new NativeClaimError("unknown enum tag"); return entry[0]; } });
|
|
16
|
+
const ADMISSION = { encode: v => { if (!v || typeof v !== "object")
|
|
17
|
+
throw new NativeClaimError("invalid admission"); const a = v; if (a.type === "open") {
|
|
18
|
+
exactObject(a, ["type"], "open admission");
|
|
19
|
+
return xdr.ScVal.scvVec([sc.symbol("Open")]);
|
|
20
|
+
} if (a.type === "allowlist") {
|
|
21
|
+
exactObject(a, ["type", "root"], "allowlist admission");
|
|
22
|
+
return xdr.ScVal.scvVec([sc.symbol("Allowlist"), HEX.encode(a.root)]);
|
|
23
|
+
} if (a.type === "approval") {
|
|
24
|
+
exactObject(a, ["type", "account"], "approval admission");
|
|
25
|
+
return xdr.ScVal.scvVec([sc.symbol("Approval"), ACCOUNT.encode(a.account)]);
|
|
26
|
+
} throw new NativeClaimError("unsupported admission"); }, decode: v => { if (!Array.isArray(v))
|
|
27
|
+
throw new NativeClaimError("invalid admission enum"); if (v.length === 1 && v[0] === "Open")
|
|
28
|
+
return { type: "open" }; if (v.length === 2 && v[0] === "Allowlist")
|
|
29
|
+
return { type: "allowlist", root: HEX.decode(v[1]) }; if (v.length === 2 && v[0] === "Approval")
|
|
30
|
+
return { type: "approval", account: ACCOUNT.decode(v[1]) }; throw new NativeClaimError("unknown admission enum"); } };
|
|
31
|
+
const snake = (s) => s.replace(/[A-Z]/g, c => "_" + c.toLowerCase());
|
|
32
|
+
function model(fields) { return { encode: v => { const value = exactObject(v, Object.keys(fields), "native input"); return struct(Object.fromEntries(Object.entries(fields).map(([name, codec]) => [snake(name), codec.encode(value[name])]))); }, decode: v => { const value = exactObject(v, Object.keys(fields).map(snake), "native result"); return Object.fromEntries(Object.entries(fields).map(([name, codec]) => [name, codec.decode(value[snake(name)])])); } }; }
|
|
33
|
+
const SETTINGS = model({ mode: enumeration({ manual: "Manual", public: "Public" }), enabled: BOOL, admission: ADMISSION, feeToken: CONTRACT, feeAmount: AMOUNT, feeRecipient: ACCOUNT, walletLimit: U64, approvalAllowance: U64, approvalRateLimit: U64, approvalWindowSecs: U64, approvalTtlSecs: U64 });
|
|
34
|
+
const CONFIG = model({ settings: SETTINGS, ownerEpoch: U64, policyVersion: U64, grantEpoch: U64 });
|
|
35
|
+
const CONTEXT = model({ network: HEX, registry: CONTRACT, registrar: CONTRACT, namespace: HEX, requestId: HEX, validAfter: U64, deadline: U64 });
|
|
36
|
+
const CLAIM = model({ context: CONTEXT, label: LABEL, claimant: ACCOUNT, destination: DESTINATION, resolver: CONTRACT, expectedGeneration: optional(U64), expectedExpiry: optional(U64), termSecs: U64, ownerEpoch: U64, policyVersion: U64, grantEpoch: U64, feeToken: CONTRACT, feeAmount: AMOUNT, feeRecipient: ACCOUNT });
|
|
37
|
+
const RESERVED = model({ context: CONTEXT, label: LABEL, holder: ACCOUNT, destination: DESTINATION, resolver: CONTRACT, expectedGeneration: optional(U64), expectedExpiry: optional(U64), termSecs: U64, ownerEpoch: U64, policyVersion: U64 });
|
|
38
|
+
const TRANSFER = model({ context: CONTEXT, label: LABEL, from: IDENTITY, to: ACCOUNT, expectedGeneration: U64, expectedExpiry: U64, proposalExpires: U64, destination: DESTINATION, resolver: CONTRACT });
|
|
39
|
+
const RENEW = model({ context: CONTEXT, label: LABEL, holder: ACCOUNT, expectedGeneration: U64, expectedExpiry: U64, termSecs: U64, minNewExpiry: U64, maxNewExpiry: U64 });
|
|
40
|
+
const USAGE = model({ publicClaims: U64, reservedIssues: U64 }), APPROVAL_USAGE = model({ total: U64, windowUsed: U64, windowEnds: U64 });
|
|
41
|
+
const CONFIG_STATE = { encode: v => v === null ? xdr.ScVal.scvVec([sc.symbol("Unconfigured")]) : xdr.ScVal.scvVec([sc.symbol("Configured"), CONFIG.encode(v)]), decode: v => { if (!Array.isArray(v))
|
|
42
|
+
throw new NativeClaimError("invalid claim config state"); if (v.length === 1 && v[0] === "Unconfigured")
|
|
43
|
+
return null; if (v.length === 2 && v[0] === "Configured")
|
|
44
|
+
return CONFIG.decode(v[1]); throw new NativeClaimError("invalid claim config state"); } };
|
|
45
|
+
const QUOTE = model({ network: HEX, registry: CONTRACT, registrar: CONTRACT, namespace: HEX, resolver: CONTRACT, label: LABEL, node: HEX, claimant: ACCOUNT, config: CONFIG_STATE, ownerEpoch: U64, policyVersion: U64, available: BOOL, reserved: BOOL, generation: optional(U64), expiresAt: optional(U64), termSecs: U64, now: U64, usage: USAGE, approvalUsage: APPROVAL_USAGE });
|
|
46
|
+
const RECEIPT = model({ operation: enumeration({ claim: "Claim", reserved: "Reserved", transfer: "Transfer", renew: "Renew" }), intentHash: HEX, node: HEX, holder: IDENTITY, generation: U64, expiresAt: U64, feeAmount: AMOUNT, feeToken: CONTRACT, feeRecipient: optional(ACCOUNT), ledger: U32, timestamp: U64 });
|
|
47
|
+
const PREVIEW = model({ node: HEX, holder: IDENTITY, generation: U64, expiresAt: U64, active: BOOL, destination: DESTINATION });
|
|
48
|
+
export function claimSettingsToScVal(value) {
|
|
49
|
+
const encoded = SETTINGS.encode(value);
|
|
50
|
+
if (value.admission.type === "approval") {
|
|
51
|
+
if (value.approvalAllowance === 0n || value.approvalRateLimit === 0n || value.approvalWindowSecs === 0n || value.approvalWindowSecs > 31536000n || value.approvalTtlSecs === 0n || value.approvalTtlSecs > 3600n)
|
|
52
|
+
throw new NativeClaimError("approval mode requires explicit bounded admission budgets, window and lifetime");
|
|
53
|
+
if (value.admission.account === value.feeRecipient)
|
|
54
|
+
throw new NativeClaimError("eligibility account must differ from fee treasury");
|
|
55
|
+
}
|
|
56
|
+
else if (value.approvalAllowance !== 0n || value.approvalRateLimit !== 0n || value.approvalWindowSecs !== 0n || value.approvalTtlSecs !== 0n)
|
|
57
|
+
throw new NativeClaimError("approval settings must be zero outside approval mode");
|
|
58
|
+
return encoded;
|
|
59
|
+
}
|
|
60
|
+
export const claimPolicyInputToScVal = claimSettingsToScVal;
|
|
61
|
+
export const claimLabelToScVal = (value) => LABEL.encode(value);
|
|
62
|
+
export const claimConfigFromNative = (value) => CONFIG.decode(value);
|
|
63
|
+
export const claimQuoteFromNative = (value) => QUOTE.decode(value);
|
|
64
|
+
export const claimUsageFromNative = (value) => USAGE.decode(value);
|
|
65
|
+
export const approvalUsageFromNative = (value) => APPROVAL_USAGE.decode(value);
|
|
66
|
+
export const destinationPreviewFromNative = (value) => PREVIEW.decode(value);
|
|
67
|
+
export const claimReceiptFromNative = (value) => RECEIPT.decode(value);
|
|
68
|
+
function validContext(c) { if (c.deadline <= c.validAfter || c.deadline - c.validAfter > 3600n)
|
|
69
|
+
throw new NativeClaimError("business intent lifetime must be positive and at most 3600 seconds"); }
|
|
70
|
+
export function claimIntentToScVal(value) { const result = CLAIM.encode(value); validContext(value.context); if ((value.expectedGeneration === null) !== (value.expectedExpiry === null))
|
|
71
|
+
throw new NativeClaimError("generation and expiry expectations must both be present or absent"); return result; }
|
|
72
|
+
export function reservedIntentToScVal(value) { const result = RESERVED.encode(value); validContext(value.context); if ((value.expectedGeneration === null) !== (value.expectedExpiry === null))
|
|
73
|
+
throw new NativeClaimError("generation and expiry expectations must both be present or absent"); return result; }
|
|
74
|
+
export function transferIntentToScVal(value) { const result = TRANSFER.encode(value); validContext(value.context); return result; }
|
|
75
|
+
export function renewIntentToScVal(value) { const result = RENEW.encode(value); validContext(value.context); if (value.termSecs === 0n || value.maxNewExpiry < value.minNewExpiry)
|
|
76
|
+
throw new NativeClaimError("invalid renewal bounds"); return result; }
|
|
77
|
+
export const claimIntentFromNative = (value) => CLAIM.decode(value);
|
|
78
|
+
export const transferIntentFromNative = (value) => TRANSFER.decode(value);
|
|
79
|
+
export const renewIntentFromNative = (value) => RENEW.decode(value);
|
|
80
|
+
export function claimResultFromNative(value) { if (!Array.isArray(value) || value.length !== 2 || !["Fresh", "Replayed"].includes(value[0]))
|
|
81
|
+
throw new NativeClaimError("invalid claim result"); return { status: value[0] === "Fresh" ? "fresh" : "replayed", receipt: claimReceiptFromNative(value[1]) }; }
|
|
82
|
+
export function nativeIntentHash(method, value) { return hex(hash(xdr.ScVal.scvVec([sc.symbol(method), value]).toXDR())); }
|
|
83
|
+
/** Lossless public recovery JSON; only declared bigint fields receive the tagged encoding. */
|
|
84
|
+
export function stringifyNativeIntent(value) { return JSON.stringify(value, (_key, v) => typeof v === "bigint" ? { $u64: v.toString() } : v); }
|
|
85
|
+
export function parseNativeClaimIntent(json) { if (json.length > 16384)
|
|
86
|
+
throw new NativeClaimError("claim recovery reference is too large"); const raw = JSON.parse(json, (_key, v) => { if (v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).join(",") === "$u64") {
|
|
87
|
+
if (typeof v.$u64 !== "string" || !/^(0|[1-9][0-9]*)$/.test(v.$u64))
|
|
88
|
+
throw new NativeClaimError("invalid recovery integer");
|
|
89
|
+
return BigInt(v.$u64);
|
|
90
|
+
} return v; }); return claimIntentFromNative(scValToNative(claimIntentToScVal(raw))); }
|
|
91
|
+
export function parseNativeTransferIntent(json) { return parseLifecycleIntent(json, TRANSFER, transferIntentToScVal); }
|
|
92
|
+
export function parseNativeRenewIntent(json) { return parseLifecycleIntent(json, RENEW, renewIntentToScVal); }
|
|
93
|
+
function parseLifecycleIntent(json, codec, encode) { if (json.length > 16384)
|
|
94
|
+
throw new NativeClaimError("lifecycle intent is too large"); const raw = JSON.parse(json, (_key, v) => { if (v && typeof v === "object" && !Array.isArray(v) && Object.keys(v).join(",") === "$u64") {
|
|
95
|
+
if (typeof v.$u64 !== "string" || !/^(0|[1-9][0-9]*)$/.test(v.$u64))
|
|
96
|
+
throw new NativeClaimError("invalid recovery integer");
|
|
97
|
+
return BigInt(v.$u64);
|
|
98
|
+
} return v; }); return codec.decode(scValToNative(encode(raw))); }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Direct v1 and muxed v2 payment records. Kept identical in the independently published lookup
|
|
2
|
+
* and holder packages; the conformance tests exercise both copies. */
|
|
3
|
+
import { xdr } from "@stellar/stellar-sdk";
|
|
4
|
+
export declare const PAYMENT_RECORD_KEY = "payment";
|
|
5
|
+
export type PaymentMemo = {
|
|
6
|
+
type: "none";
|
|
7
|
+
} | {
|
|
8
|
+
type: "id";
|
|
9
|
+
value: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "text";
|
|
12
|
+
value: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: "hash";
|
|
15
|
+
value: string;
|
|
16
|
+
};
|
|
17
|
+
export type PaymentDestination = {
|
|
18
|
+
address: string;
|
|
19
|
+
memo: PaymentMemo;
|
|
20
|
+
};
|
|
21
|
+
/** Reject invalid data without normalizing meaningful memo bytes. */
|
|
22
|
+
export declare function validatePaymentDestination(value: unknown): PaymentDestination;
|
|
23
|
+
export declare function encodePaymentRecord(value: PaymentDestination): string;
|
|
24
|
+
/** Empty/missing records are not explicit payment instructions. */
|
|
25
|
+
export declare function parsePaymentRecord(raw: string): PaymentDestination;
|
|
26
|
+
export declare function paymentMemoToScVal(memo: PaymentMemo): xdr.ScVal;
|
|
27
|
+
/** Decode Soroban contract enum values, checking arity and native types. */
|
|
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
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Direct v1 and muxed v2 payment records. Kept identical in the independently published lookup
|
|
2
|
+
* and holder packages; the conformance tests exercise both copies. */
|
|
3
|
+
import { StrKey, nativeToScVal, xdr } from "@stellar/stellar-sdk";
|
|
4
|
+
export const PAYMENT_RECORD_KEY = "payment";
|
|
5
|
+
const MAX_U64 = 18446744073709551615n;
|
|
6
|
+
const utf8 = (value) => new TextEncoder().encode(value);
|
|
7
|
+
/** Reject invalid data without normalizing meaningful memo bytes. */
|
|
8
|
+
export function validatePaymentDestination(value) {
|
|
9
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10
|
+
throw new Error("invalid payment destination");
|
|
11
|
+
const p = value;
|
|
12
|
+
if (Object.keys(p).sort().join(",") !== "address,memo")
|
|
13
|
+
throw new Error("unexpected payment fields");
|
|
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
|
+
if (!p.memo || typeof p.memo !== "object" || Array.isArray(p.memo))
|
|
17
|
+
throw new Error("invalid memo");
|
|
18
|
+
const m = p.memo;
|
|
19
|
+
if (m.type === "none") {
|
|
20
|
+
if (Object.keys(m).join(",") !== "type")
|
|
21
|
+
throw new Error("none memo cannot have a value");
|
|
22
|
+
if (StrKey.isValidMed25519PublicKey(p.address))
|
|
23
|
+
decodeMuxedAddress(p.address);
|
|
24
|
+
return { address: p.address, memo: { type: "none" } };
|
|
25
|
+
}
|
|
26
|
+
if (!StrKey.isValidEd25519PublicKey(p.address))
|
|
27
|
+
throw new Error("memos require a classic G address");
|
|
28
|
+
if (Object.keys(m).sort().join(",") !== "type,value" || typeof m.value !== "string")
|
|
29
|
+
throw new Error("memo value must be a string");
|
|
30
|
+
switch (m.type) {
|
|
31
|
+
case "id":
|
|
32
|
+
if (!/^(0|[1-9][0-9]{0,19})$/.test(m.value) || BigInt(m.value) > MAX_U64)
|
|
33
|
+
throw new Error("memo ID must be canonical decimal u64");
|
|
34
|
+
return { address: p.address, memo: { type: "id", value: m.value } };
|
|
35
|
+
case "text": {
|
|
36
|
+
const bytes = utf8(m.value);
|
|
37
|
+
if (!bytes.length || bytes.length > 28 || new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes) !== m.value)
|
|
38
|
+
throw new Error("memo text must be valid UTF-8, 1–28 bytes");
|
|
39
|
+
return { address: p.address, memo: { type: "text", value: m.value } };
|
|
40
|
+
}
|
|
41
|
+
case "hash":
|
|
42
|
+
if (!/^[0-9a-f]{64}$/.test(m.value))
|
|
43
|
+
throw new Error("memo hash must be 32 bytes as lowercase hex");
|
|
44
|
+
return { address: p.address, memo: { type: "hash", value: m.value } };
|
|
45
|
+
default: throw new Error("unsupported memo type");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function encodePaymentRecord(value) {
|
|
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
|
+
}
|
|
54
|
+
return `1|${p.address}|${p.memo.type}|${p.memo.type === "none" ? "" : p.memo.value}`;
|
|
55
|
+
}
|
|
56
|
+
/** Empty/missing records are not explicit payment instructions. */
|
|
57
|
+
export function parsePaymentRecord(raw) {
|
|
58
|
+
if (typeof raw !== "string" || utf8(raw).length > 128)
|
|
59
|
+
throw new Error("invalid payment record length");
|
|
60
|
+
const first = raw.indexOf("|");
|
|
61
|
+
const second = raw.indexOf("|", first + 1);
|
|
62
|
+
const third = raw.indexOf("|", second + 1);
|
|
63
|
+
if (first < 0 || second < 0 || third < 0)
|
|
64
|
+
throw new Error("unsupported payment record");
|
|
65
|
+
const version = raw.slice(0, first);
|
|
66
|
+
const address = raw.slice(first + 1, second);
|
|
67
|
+
const type = raw.slice(second + 1, third);
|
|
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");
|
|
73
|
+
if (type === "none" && value !== "")
|
|
74
|
+
throw new Error("none memo cannot have a value");
|
|
75
|
+
return validatePaymentDestination({ address, memo: type === "none" ? { type } : { type, value } });
|
|
76
|
+
}
|
|
77
|
+
export function paymentMemoToScVal(memo) {
|
|
78
|
+
const symbol = (s) => nativeToScVal(s, { type: "symbol" });
|
|
79
|
+
switch (memo.type) {
|
|
80
|
+
case "none": return xdr.ScVal.scvVec([symbol("None")]);
|
|
81
|
+
case "id": return xdr.ScVal.scvVec([symbol("Id"), nativeToScVal(BigInt(memo.value), { type: "u64" })]);
|
|
82
|
+
case "text": return xdr.ScVal.scvVec([symbol("Text"), nativeToScVal(memo.value, { type: "string" })]);
|
|
83
|
+
case "hash": return xdr.ScVal.scvVec([symbol("Hash"), nativeToScVal(Uint8Array.from(memo.value.match(/../g), (b) => parseInt(b, 16)), { type: "bytes" })]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Decode Soroban contract enum values, checking arity and native types. */
|
|
87
|
+
export function paymentFromNative(raw) {
|
|
88
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
89
|
+
throw new Error("invalid payment result");
|
|
90
|
+
const p = raw;
|
|
91
|
+
if (Object.keys(p).sort().join(",") !== "address,memo" || !Array.isArray(p.memo))
|
|
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");
|
|
95
|
+
const m = p.memo;
|
|
96
|
+
let memo;
|
|
97
|
+
if (m.length === 1 && m[0] === "None")
|
|
98
|
+
memo = { type: "none" };
|
|
99
|
+
else if (m.length === 2 && m[0] === "Id" && typeof m[1] === "bigint")
|
|
100
|
+
memo = { type: "id", value: m[1].toString() };
|
|
101
|
+
else if (m.length === 2 && m[0] === "Text" && typeof m[1] === "string")
|
|
102
|
+
memo = { type: "text", value: m[1] };
|
|
103
|
+
else if (m.length === 2 && m[0] === "Hash" && m[1] instanceof Uint8Array && m[1].length === 32)
|
|
104
|
+
memo = { type: "hash", value: Array.from(m[1], (b) => b.toString(16).padStart(2, "0")).join("") };
|
|
105
|
+
else
|
|
106
|
+
throw new Error("invalid payment memo result");
|
|
107
|
+
return validatePaymentDestination({ address: p.address, memo });
|
|
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/owner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Run your Soran namespace on Stellar \u2014 issue names, manage lifecycle, transfer, and walk the one-way permanence door.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"typecheck": "tsc --noEmit",
|
|
14
14
|
"prepublishOnly": "tsc",
|
|
15
15
|
"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')\"",
|
|
16
|
-
"test": "node --import tsx test/decode-failure.test.mts && node --import tsx --test test/integration.test.mts"
|
|
16
|
+
"test": "node --import tsx test/decode-failure.test.mts && node --import tsx --test test/integration.test.mts test/native-*.test.mts"
|
|
17
17
|
},
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"repository": {
|