@sorandomains/holder 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -9
- package/dist/index.d.ts +40 -5
- package/dist/index.js +93 -14
- package/dist/native-allowlist.d.ts +21 -0
- package/dist/native-allowlist.js +34 -0
- package/dist/native-approver.d.ts +21 -0
- package/dist/native-approver.js +36 -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-holder.d.ts +42 -0
- package/dist/native-holder.js +221 -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 +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
|
+
|
|
4
|
+
> Native claim testnet deployment verified at ledger 4534629 on 6 September 2026 (12:12 UTC). See the
|
|
5
|
+
> [release status](https://docs.soran.domains/reference/release-status) for package and service availability.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Version 0.5.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
|
|
4
9
|
and labels are validated before lowercase normalization; Unicode lookalikes are
|
|
5
10
|
rejected. Writes continue to target the owning Registry/Registrar/Resolver. Universal
|
|
6
|
-
Lookup is the read entry point in `@sorandomains/lookup` 0.
|
|
11
|
+
Lookup is the read entry point in `@sorandomains/lookup` 0.7.0.
|
|
7
12
|
|
|
8
13
|
Your Soran name, managed with your own key. The third piece of the SDK
|
|
9
14
|
trilogy: [`@sorandomains/lookup`](https://www.npmjs.com/package/@sorandomains/lookup)
|
|
@@ -45,7 +50,12 @@ await me.setPayment("alice.nova", { address: myAddress, memo: { type: "none" } }
|
|
|
45
50
|
|
|
46
51
|
ID values are canonical unsigned 64-bit decimal strings; text is exact nonempty
|
|
47
52
|
UTF-8 up to 28 bytes; hashes are 64 lowercase hex characters. Required memos work
|
|
48
|
-
only with G addresses. C addresses permit `none`.
|
|
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
|
|
|
@@ -113,14 +140,21 @@ Primary writes verify the Primary contract's Registry anchor before signing.
|
|
|
113
140
|
Custom Registry or passphrase settings do not inherit a Primary deployment pin;
|
|
114
141
|
supply the matching `primaryId` explicitly.
|
|
115
142
|
|
|
143
|
+
|
|
144
|
+
## Native username claiming
|
|
145
|
+
|
|
146
|
+
Native username claiming uses `claimQuote`, `createClaimIntent`, `buildClaim` and `claim`. The current namespace owner must first enable the public policy. The claimant's G wallet authorizes the exact username, complete receiving destination, owner price, policy version and deadline. `recoverClaim` reads the original immutable receipt; it never silently retries an uncertain transaction. `acceptNameTransferWithDestination` and `renewName` cover separately authorized holder lifecycle actions.
|
|
147
|
+
|
|
148
|
+
Read the [native claim APIs, security boundaries and complete signup flow](https://github.com/SoranDomains/sdk/blob/main/NATIVE-CLAIMS.md). G/no memo, G with ID/Text/Hash, full M/no separate memo and C/no memo remain supported payment destinations. Current transaction-signing adapters use classic G accounts.
|
|
149
|
+
|
|
116
150
|
## Verified testnet deployment
|
|
117
151
|
|
|
118
|
-
Verified on 2026
|
|
152
|
+
Verified on 6 September 2026 at ledger **4534629** (12:12 UTC). Network passphrase: `Test SDF Network ; September 2015`.
|
|
119
153
|
|
|
120
154
|
| Contract | Address |
|
|
121
155
|
|---|---|
|
|
122
|
-
| Registry | `
|
|
123
|
-
| Primary | `
|
|
156
|
+
| Registry | `CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK` |
|
|
157
|
+
| Primary | `CASORAN755O3GCQTRAHKDXLLCSDLNKAQWAP6MWABRSFVSHLJOEKAC7AB` |
|
|
124
158
|
|
|
125
159
|
Mainnet has no deployment preset. Custom networks must supply their own verified
|
|
126
160
|
addresses. Universal Lookup upgrades remain immediately executable; an address
|
package/dist/index.d.ts
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
* checks you hold the name (generation-gated) before accepting records, the
|
|
18
18
|
* reverse and primary claims are authorized by the ADDRESS itself, and name
|
|
19
19
|
* transfers move only when the recipient accepts. Namespace-owner powers
|
|
20
|
-
* (issue, reclaim,
|
|
21
|
-
* package deliberately cannot exercise them.
|
|
20
|
+
* (issue, reclaim, owner renewal, permanence) live in `@sorandomains/owner` and this
|
|
21
|
+
* package deliberately cannot exercise them. Native successor holders can
|
|
22
|
+
* independently renew an eligible lease through the separately authorized renewName.
|
|
22
23
|
*
|
|
23
24
|
* SIGNING. Same `TxSigner` contract as the owner SDK: `keypairSigner(secret)`
|
|
24
25
|
* for scripts, or wrap a browser wallet:
|
|
@@ -31,14 +32,33 @@
|
|
|
31
32
|
* calls cannot race the account sequence number.
|
|
32
33
|
*/
|
|
33
34
|
import { type PaymentDestination } from "./payment.js";
|
|
34
|
-
export { PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination, type PaymentMemo, type PaymentDestination } from "./payment.js";
|
|
35
|
+
export { encodeMuxedAddress, decodeMuxedAddress, PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination, type PaymentMemo, type PaymentDestination } from "./payment.js";
|
|
36
|
+
import { type ClaimSubmitOptions } from "./native-holder.js";
|
|
37
|
+
import type { ClaimIntent, TransferIntent, RenewIntent } from "./native-types.js";
|
|
38
|
+
import type { NativeWriteOptions } from "./native-transport.js";
|
|
39
|
+
export * from "./native-types.js";
|
|
40
|
+
export * from "./native-allowlist.js";
|
|
41
|
+
export * from "./native-approver.js";
|
|
42
|
+
export { NativeClaimError, paymentDestinationToScVal, namespaceNode as nativeNamespaceNode } from "./native-codec.js";
|
|
43
|
+
export { createClaimIntent, type ClaimRequestOptions, type ClaimSubmitOptions, type NativeClaimSubmission, type PreparedClaim } from "./native-holder.js";
|
|
44
|
+
export { signEligibilityAuthorization, validateEligibilityAuthorization, validateNativeTransaction, authorizedInvocation, type NativeAuthorizationPlan, type EligibilitySigner } from "./native-auth.js";
|
|
45
|
+
export type { NativeCapability, NativePrepared, NativeWriteOptions } from "./native-transport.js";
|
|
35
46
|
/** Known public deployments. Pass explicit options for anything else. */
|
|
36
47
|
export declare const DEPLOYMENTS: {
|
|
37
48
|
readonly testnet: {
|
|
38
49
|
readonly rpcUrl: "https://soroban-testnet.stellar.org";
|
|
39
50
|
readonly passphrase: string;
|
|
40
|
-
readonly registryId: "
|
|
41
|
-
readonly primaryId: "
|
|
51
|
+
readonly registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK";
|
|
52
|
+
readonly primaryId: "CASORAN755O3GCQTRAHKDXLLCSDLNKAQWAP6MWABRSFVSHLJOEKAC7AB";
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
56
|
+
export declare const LEGACY_DEPLOYMENTS: {
|
|
57
|
+
readonly testnet20260905: {
|
|
58
|
+
readonly rpcUrl: "https://soroban-testnet.stellar.org";
|
|
59
|
+
readonly passphrase: string;
|
|
60
|
+
readonly registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H";
|
|
61
|
+
readonly primaryId: "CCSORANJZOR5ZYTI4KAW34ESAQFMJAO4NKMTIVOVJOI2VDKCDK3RICXZ";
|
|
42
62
|
};
|
|
43
63
|
};
|
|
44
64
|
/** Same shape as the owner SDK's signer — wallet-kit compatible. */
|
|
@@ -82,6 +102,8 @@ export type Submitted = {
|
|
|
82
102
|
/** The standard profile keys shared with @sorandomains/lookup's profile(). */
|
|
83
103
|
export declare const PROFILE_KEYS: readonly ["org", "url", "email", "description", "avatar", "location", "twitter", "github"];
|
|
84
104
|
export type HolderOptions = {
|
|
105
|
+
/** Upper total network fee for successor native operations; default 5 XLM. */
|
|
106
|
+
maxNativeFeeStroops?: bigint;
|
|
85
107
|
/** Signs every transaction: the name HOLDER's account (or, for
|
|
86
108
|
* `acceptNameTransfer`, the proposed new holder's). */
|
|
87
109
|
signer: TxSigner;
|
|
@@ -101,6 +123,7 @@ export type HolderOptions = {
|
|
|
101
123
|
fee?: string;
|
|
102
124
|
};
|
|
103
125
|
export declare class SoranHolder {
|
|
126
|
+
private maxNativeFeeStroops;
|
|
104
127
|
private server;
|
|
105
128
|
private passphrase;
|
|
106
129
|
private registryId;
|
|
@@ -113,6 +136,16 @@ export declare class SoranHolder {
|
|
|
113
136
|
private resolvers;
|
|
114
137
|
private static POINTER_TTL_MS;
|
|
115
138
|
constructor(opts: HolderOptions);
|
|
139
|
+
private nativeClient;
|
|
140
|
+
nativeClaimCapability(namespace: string): Promise<import("./native-transport.js").NativeCapability>;
|
|
141
|
+
claimQuote(name: string, claimant?: string): Promise<import("./native-types.js").ClaimQuote>;
|
|
142
|
+
claimReceipt(namespace: string, claimant: string, requestId: string): Promise<import("./native-types.js").ClaimReceipt | null>;
|
|
143
|
+
recoverClaim(intent: ClaimIntent): Promise<import("./native-types.js").ClaimReceipt | null>;
|
|
144
|
+
buildClaim(intent: ClaimIntent, options?: ClaimSubmitOptions): Promise<import("./native-holder.js").PreparedClaim>;
|
|
145
|
+
claim(intent: ClaimIntent, options?: ClaimSubmitOptions): Promise<import("./native-holder.js").NativeClaimSubmission>;
|
|
146
|
+
renewalPreview(name: string): Promise<import("./native-types.js").DestinationPreview>;
|
|
147
|
+
acceptNameTransferWithDestination(intent: TransferIntent, options?: NativeWriteOptions): Promise<import("./native-holder.js").NativeClaimSubmission>;
|
|
148
|
+
renewName(intent: RenewIntent, options?: NativeWriteOptions): Promise<import("./native-holder.js").NativeClaimSubmission>;
|
|
116
149
|
/** Atomically update the forward address and complete payment instruction.
|
|
117
150
|
* Use memo {type:"none"} to explicitly publish a memo-free destination.
|
|
118
151
|
* The native Resolver updates its own records atomically. This method never
|
|
@@ -215,6 +248,8 @@ export declare class SoranHolder {
|
|
|
215
248
|
private serialize;
|
|
216
249
|
private sourceAccount;
|
|
217
250
|
private assertSatisfiableAuth;
|
|
251
|
+
/** Native payment writes authorize exactly the locally selected destination. */
|
|
252
|
+
private assertPaymentIntent;
|
|
218
253
|
private invoke;
|
|
219
254
|
private attempt;
|
|
220
255
|
private restore;
|
package/dist/index.js
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
* checks you hold the name (generation-gated) before accepting records, the
|
|
18
18
|
* reverse and primary claims are authorized by the ADDRESS itself, and name
|
|
19
19
|
* transfers move only when the recipient accepts. Namespace-owner powers
|
|
20
|
-
* (issue, reclaim,
|
|
21
|
-
* package deliberately cannot exercise them.
|
|
20
|
+
* (issue, reclaim, owner renewal, permanence) live in `@sorandomains/owner` and this
|
|
21
|
+
* package deliberately cannot exercise them. Native successor holders can
|
|
22
|
+
* independently renew an eligible lease through the separately authorized renewName.
|
|
22
23
|
*
|
|
23
24
|
* SIGNING. Same `TxSigner` contract as the owner SDK: `keypairSigner(secret)`
|
|
24
25
|
* for scripts, or wrap a browser wallet:
|
|
@@ -31,8 +32,16 @@
|
|
|
31
32
|
* calls cannot race the account sequence number.
|
|
32
33
|
*/
|
|
33
34
|
import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, } from "@stellar/stellar-sdk";
|
|
34
|
-
import { paymentFromNative, paymentMemoToScVal, validatePaymentDestination } from "./payment.js";
|
|
35
|
-
export { PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination } from "./payment.js";
|
|
35
|
+
import { decodeMuxedAddress, destinationFromNative, paymentFromNative, paymentMemoToScVal, validatePaymentDestination } from "./payment.js";
|
|
36
|
+
export { encodeMuxedAddress, decodeMuxedAddress, PAYMENT_RECORD_KEY, encodePaymentRecord, parsePaymentRecord, validatePaymentDestination } from "./payment.js";
|
|
37
|
+
import { NativeHolderClient } from "./native-holder.js";
|
|
38
|
+
import { NativeClaimError } from "./native-codec.js";
|
|
39
|
+
export * from "./native-types.js";
|
|
40
|
+
export * from "./native-allowlist.js";
|
|
41
|
+
export * from "./native-approver.js";
|
|
42
|
+
export { NativeClaimError, paymentDestinationToScVal, namespaceNode as nativeNamespaceNode } from "./native-codec.js";
|
|
43
|
+
export { createClaimIntent } from "./native-holder.js";
|
|
44
|
+
export { signEligibilityAuthorization, validateEligibilityAuthorization, validateNativeTransaction, authorizedInvocation } from "./native-auth.js";
|
|
36
45
|
// ---------------------------------------------------------------------------
|
|
37
46
|
// Deployments
|
|
38
47
|
// ---------------------------------------------------------------------------
|
|
@@ -41,8 +50,17 @@ export const DEPLOYMENTS = {
|
|
|
41
50
|
testnet: {
|
|
42
51
|
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
43
52
|
passphrase: Networks.TESTNET,
|
|
44
|
-
registryId: "
|
|
45
|
-
primaryId: "
|
|
53
|
+
registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK",
|
|
54
|
+
primaryId: "CASORAN755O3GCQTRAHKDXLLCSDLNKAQWAP6MWABRSFVSHLJOEKAC7AB",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
58
|
+
export const LEGACY_DEPLOYMENTS = {
|
|
59
|
+
testnet20260905: {
|
|
60
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
61
|
+
passphrase: Networks.TESTNET,
|
|
62
|
+
registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H",
|
|
63
|
+
primaryId: "CCSORANJZOR5ZYTI4KAW34ESAQFMJAO4NKMTIVOVJOI2VDKCDK3RICXZ",
|
|
46
64
|
},
|
|
47
65
|
};
|
|
48
66
|
/** A TxSigner over a raw secret key — for scripts and backends. */
|
|
@@ -81,6 +99,7 @@ const REGISTRAR_ERRORS = {
|
|
|
81
99
|
18: "InvalidPolicy",
|
|
82
100
|
19: "ExpiryOverflow",
|
|
83
101
|
20: "InvalidRegistry",
|
|
102
|
+
21: "InvalidClaimConfig", 22: "ClaimsNotConfigured", 23: "ClaimsPaused", 24: "StaleClaimPolicy", 25: "ClaimIntentMismatch", 26: "ClaimIntentExpired", 27: "ReservedName", 28: "NameNotReserved", 29: "PublicIssuanceRequired", 30: "WalletClaimLimit", 31: "InvalidEligibility", 32: "ApprovalAllowanceReached", 33: "ApprovalRateReached", 34: "RequestIdConflict", 35: "CounterOverflow", 36: "InvalidNativeBinding", 37: "UnsupportedClaimant", 38: "DuplicateLabel", 39: "RenewalTooEarly", 40: "RenewalLeaseLimit", 41: "DestinationInitializationFailed", 42: "FeeSettlementFailed",
|
|
84
103
|
};
|
|
85
104
|
const RESOLVER_ERRORS = {
|
|
86
105
|
1: "AlreadyInitialized",
|
|
@@ -103,6 +122,7 @@ const RESOLVER_ERRORS = {
|
|
|
103
122
|
18: "UsePaymentMethod",
|
|
104
123
|
19: "PaymentContextMismatch",
|
|
105
124
|
20: "PaymentUnavailable",
|
|
125
|
+
21: "MuxedDestination",
|
|
106
126
|
};
|
|
107
127
|
const PRIMARY_ERRORS = {
|
|
108
128
|
1: "MalformedName",
|
|
@@ -200,7 +220,11 @@ function nameNode(label, namespace) {
|
|
|
200
220
|
return new Uint8Array(hash(concatBytes(namehash(namespace), labelHash)));
|
|
201
221
|
}
|
|
202
222
|
const labelArg = (label) => nativeToScVal(utf8(label), { type: "bytes" });
|
|
203
|
-
const addrArg = (address) =>
|
|
223
|
+
const addrArg = (address) => {
|
|
224
|
+
if (!StrKey.isValidEd25519PublicKey(address) && !StrKey.isValidContract(address))
|
|
225
|
+
throw new HolderError("this account/address operation requires G or C; use setPayment for muxed destinations");
|
|
226
|
+
return nativeToScVal(address, { type: "address" });
|
|
227
|
+
};
|
|
204
228
|
const bytesArg = (b) => nativeToScVal(b, { type: "bytes" });
|
|
205
229
|
const SIM_SOURCE = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
|
|
206
230
|
/** The standard profile keys shared with @sorandomains/lookup's profile(). */
|
|
@@ -218,6 +242,7 @@ export const PROFILE_KEYS = [
|
|
|
218
242
|
// The client
|
|
219
243
|
// ---------------------------------------------------------------------------
|
|
220
244
|
export class SoranHolder {
|
|
245
|
+
maxNativeFeeStroops;
|
|
221
246
|
server;
|
|
222
247
|
passphrase;
|
|
223
248
|
registryId;
|
|
@@ -230,6 +255,9 @@ export class SoranHolder {
|
|
|
230
255
|
resolvers = new Map();
|
|
231
256
|
static POINTER_TTL_MS = 30_000;
|
|
232
257
|
constructor(opts) {
|
|
258
|
+
this.maxNativeFeeStroops = opts?.maxNativeFeeStroops ?? 50000000n;
|
|
259
|
+
if (typeof this.maxNativeFeeStroops !== "bigint" || this.maxNativeFeeStroops <= 0n || this.maxNativeFeeStroops > 4294967295n)
|
|
260
|
+
throw new NativeClaimError("maximum native network fee must be positive");
|
|
233
261
|
if (!opts?.signer)
|
|
234
262
|
throw new HolderError("HolderOptions.signer is required");
|
|
235
263
|
const d = DEPLOYMENTS[opts.network ?? "testnet"];
|
|
@@ -250,6 +278,19 @@ export class SoranHolder {
|
|
|
250
278
|
this.timeoutSecs = t;
|
|
251
279
|
this.fee = opts.fee ?? BASE_FEE;
|
|
252
280
|
}
|
|
281
|
+
nativeClient() {
|
|
282
|
+
return new NativeHolderClient({ registryId: this.registryId, passphrase: this.passphrase, server: this.server, signer: this.signer, fee: this.fee, timeoutSecs: this.timeoutSecs, maxFeeStroops: this.maxNativeFeeStroops,
|
|
283
|
+
read: (id, method, args) => this.read(id, method, args), serialize: work => this.serialize(work) });
|
|
284
|
+
}
|
|
285
|
+
nativeClaimCapability(namespace) { return this.nativeClient().nativeClaimCapability(namespace); }
|
|
286
|
+
claimQuote(name, claimant) { return this.nativeClient().claimQuote(name, claimant); }
|
|
287
|
+
claimReceipt(namespace, claimant, requestId) { return this.nativeClient().claimReceipt(namespace, claimant, requestId); }
|
|
288
|
+
recoverClaim(intent) { return this.nativeClient().recoverClaim(intent); }
|
|
289
|
+
buildClaim(intent, options = {}) { return this.nativeClient().buildClaim(intent, options); }
|
|
290
|
+
claim(intent, options = {}) { return this.nativeClient().claim(intent, options); }
|
|
291
|
+
renewalPreview(name) { return this.nativeClient().renewalPreview(name); }
|
|
292
|
+
acceptNameTransferWithDestination(intent, options = {}) { return this.nativeClient().acceptNameTransferWithDestination(intent, options); }
|
|
293
|
+
renewName(intent, options = {}) { return this.nativeClient().renewName(intent, options); }
|
|
253
294
|
// ---- resolution targets --------------------------------------------------
|
|
254
295
|
/** Atomically update the forward address and complete payment instruction.
|
|
255
296
|
* Use memo {type:"none"} to explicitly publish a memo-free destination.
|
|
@@ -264,8 +305,18 @@ export class SoranHolder {
|
|
|
264
305
|
catch (e) {
|
|
265
306
|
throw new HolderError(String(e));
|
|
266
307
|
}
|
|
267
|
-
const { resolver } = await this.paymentResolverOf(namespace);
|
|
308
|
+
const { resolver, version } = await this.paymentResolverOf(namespace);
|
|
268
309
|
const holder = await this.signer.publicKey();
|
|
310
|
+
if (StrKey.isValidMed25519PublicKey(payment.address)) {
|
|
311
|
+
if (version !== 2)
|
|
312
|
+
throw new HolderError("muxed destinations require native Resolver v2", resolver, "payment_version");
|
|
313
|
+
const muxed = decodeMuxedAddress(payment.address);
|
|
314
|
+
const r = await this.invoke(resolver, "set_muxed", [
|
|
315
|
+
nativeToScVal(`${label}.${namespace}`, { type: "string" }), addrArg(holder),
|
|
316
|
+
addrArg(muxed.account), nativeToScVal(BigInt(muxed.id), { type: "u64" }),
|
|
317
|
+
], RESOLVER_ERRORS);
|
|
318
|
+
return { hash: r.hash, ledger: r.ledger };
|
|
319
|
+
}
|
|
269
320
|
const r = await this.invoke(resolver, "set_payment", [
|
|
270
321
|
nativeToScVal(`${label}.${namespace}`, { type: "string" }), addrArg(holder),
|
|
271
322
|
addrArg(payment.address), paymentMemoToScVal(payment.memo),
|
|
@@ -525,21 +576,26 @@ export class SoranHolder {
|
|
|
525
576
|
!(anchors[1] instanceof Uint8Array) || anchors[1].length !== nsNode.length ||
|
|
526
577
|
!nsNode.every((byte, index) => anchors[1][index] === byte))
|
|
527
578
|
throw new HolderError("Registrar anchors do not match this Registry and namespace", registrar, "anchors");
|
|
528
|
-
if (version !== 1)
|
|
579
|
+
if (version !== 1 && version !== 2)
|
|
529
580
|
throw new HolderError("unsupported native payment Resolver version", resolver, "payment_version");
|
|
530
|
-
|
|
581
|
+
if (version === 2 && await this.read(resolver, "destination_version", []) !== 2)
|
|
582
|
+
throw new HolderError("unsupported Resolver destination version", resolver, "destination_version");
|
|
583
|
+
return { resolver, registrar, version };
|
|
531
584
|
}
|
|
532
585
|
async assertMemoFree(name) {
|
|
533
586
|
const { label, namespace } = parseName(name);
|
|
534
|
-
const { resolver, registrar } = await this.paymentResolverOf(namespace);
|
|
535
|
-
const
|
|
587
|
+
const { resolver, registrar, version } = await this.paymentResolverOf(namespace);
|
|
588
|
+
const fn = version === 2 ? "resolve_destination" : "resolve_payment";
|
|
589
|
+
const raw = await this.read(resolver, fn, [nativeToScVal(`${label}.${namespace}`, { type: "string" })]);
|
|
536
590
|
let payment;
|
|
537
591
|
try {
|
|
538
|
-
payment = paymentFromNative(raw);
|
|
592
|
+
payment = version === 2 ? destinationFromNative(raw) : paymentFromNative(raw);
|
|
539
593
|
}
|
|
540
594
|
catch (e) {
|
|
541
|
-
throw new HolderError(`invalid payment result: ${String(e)}`, resolver,
|
|
595
|
+
throw new HolderError(`invalid payment result: ${String(e)}`, resolver, fn);
|
|
542
596
|
}
|
|
597
|
+
if (StrKey.isValidMed25519PublicKey(payment.address))
|
|
598
|
+
throw new HolderError("this name has a muxed routing ID; use setPayment to explicitly update its destination");
|
|
543
599
|
if (payment.memo.type !== "none")
|
|
544
600
|
throw new HolderError("this name requires a memo; use setPayment to update address and memo together");
|
|
545
601
|
// Registrar-only write below cannot alter a Resolver's explicit addr/memo.
|
|
@@ -602,6 +658,26 @@ export class SoranHolder {
|
|
|
602
658
|
: `${fn}: this operation must be authorized by ${required}, but the signer is ${pub} — use that account's signer`, contractId, fn);
|
|
603
659
|
}
|
|
604
660
|
}
|
|
661
|
+
/** Native payment writes authorize exactly the locally selected destination. */
|
|
662
|
+
assertPaymentIntent(prepared, pub, contractId, fn, args) {
|
|
663
|
+
if (fn !== "set_payment" && fn !== "set_muxed")
|
|
664
|
+
return;
|
|
665
|
+
if (prepared.source !== pub || prepared.operations.length !== 1)
|
|
666
|
+
throw new HolderError("unexpected payment transaction source or operation count", contractId, fn);
|
|
667
|
+
const op = prepared.operations[0];
|
|
668
|
+
if (op.type !== "invokeHostFunction" || (op.source !== undefined && op.source !== pub) || op.func.type !== "hostFunctionTypeInvokeContract")
|
|
669
|
+
throw new HolderError("unexpected payment operation", contractId, fn);
|
|
670
|
+
const call = op.func.invokeContract;
|
|
671
|
+
if (Address.fromScAddress(call.contractAddress).toString() !== contractId || call.functionName.toString() !== fn ||
|
|
672
|
+
call.args.length !== args.length || call.args.some((value, i) => value.toXDR("base64") !== args[i].toXDR("base64")))
|
|
673
|
+
throw new HolderError("payment operation differs from selected intent", contractId, fn);
|
|
674
|
+
if (!op.auth || op.auth.length !== 1 || op.auth[0].credentials.type !== "sorobanCredentialsSourceAccount")
|
|
675
|
+
throw new HolderError("payment requires exactly the source holder authorization", contractId, fn);
|
|
676
|
+
const root = op.auth[0].rootInvocation;
|
|
677
|
+
if (root.function.type !== "sorobanAuthorizedFunctionTypeContractFn" || root.subInvocations.length ||
|
|
678
|
+
root.function.contractFn.toXDR("base64") !== call.toXDR("base64"))
|
|
679
|
+
throw new HolderError("payment authorization differs from selected intent", contractId, fn);
|
|
680
|
+
}
|
|
605
681
|
invoke(contractId, fn, args, errNames) {
|
|
606
682
|
return this.serialize(async () => {
|
|
607
683
|
try {
|
|
@@ -640,9 +716,12 @@ export class SoranHolder {
|
|
|
640
716
|
}
|
|
641
717
|
const prepared = rpc.assembleTransaction(tx, sim).build();
|
|
642
718
|
this.assertSatisfiableAuth(prepared, pub, contractId, fn);
|
|
719
|
+
this.assertPaymentIntent(prepared, pub, contractId, fn, args);
|
|
643
720
|
const txHash = toHex(prepared.hash()); // (SDK17) hash() is Uint8Array
|
|
644
721
|
const signed = await this.signEnvelope(prepared.toXDR());
|
|
645
722
|
const envelope = TransactionBuilder.fromXDR(signed, this.passphrase);
|
|
723
|
+
if ((fn === "set_payment" || fn === "set_muxed") && toHex(envelope.hash()) !== txHash)
|
|
724
|
+
throw new HolderError("signer changed the reviewed transaction body", contractId, fn);
|
|
646
725
|
let sent;
|
|
647
726
|
try {
|
|
648
727
|
sent = await this.server.sendTransaction(envelope);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ClaimAllowlistContext = {
|
|
2
|
+
network: string;
|
|
3
|
+
registry: string;
|
|
4
|
+
namespace: string;
|
|
5
|
+
registrar: string;
|
|
6
|
+
};
|
|
7
|
+
export type ClaimAllowlistBundle = {
|
|
8
|
+
version: 1;
|
|
9
|
+
context: ClaimAllowlistContext;
|
|
10
|
+
root: string;
|
|
11
|
+
entries: Array<{
|
|
12
|
+
account: string;
|
|
13
|
+
leaf: string;
|
|
14
|
+
proof: string[];
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
export declare function claimAllowlistLeaf(context: ClaimAllowlistContext, account: string): string;
|
|
18
|
+
export declare function claimAllowlistParent(left: string, right: string): string;
|
|
19
|
+
/** A complete list is required to generate a replacement root. Keep/distribute the returned proof bundle. */
|
|
20
|
+
export declare function buildClaimAllowlist(context: ClaimAllowlistContext, accounts: readonly string[]): ClaimAllowlistBundle;
|
|
21
|
+
export declare function verifyClaimAllowlistProof(context: ClaimAllowlistContext, account: string, proof: readonly string[], root: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { hash, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { NativeClaimError, address, hex, hex32, sc, unhex } from "./native-codec.js";
|
|
3
|
+
export function claimAllowlistLeaf(context, account) {
|
|
4
|
+
return hex(hash(xdr.ScVal.scvVec([sc.symbol("allow_v1"), sc.bytes(unhex(context.network)), sc.address(address(context.registry, "contract", "Registry")), sc.bytes(unhex(context.namespace)), sc.address(address(context.registrar, "contract", "Registrar")), sc.address(address(account, "account", "allowlisted claimant"))]).toXDR()));
|
|
5
|
+
}
|
|
6
|
+
export function claimAllowlistParent(left, right) { hex32(left, "left leaf"); hex32(right, "right leaf"); const [first, second] = left < right ? [left, right] : [right, left]; const bytes = new Uint8Array(65); bytes[0] = 1; bytes.set(unhex(first), 1); bytes.set(unhex(second), 33); return hex(hash(bytes)); }
|
|
7
|
+
/** A complete list is required to generate a replacement root. Keep/distribute the returned proof bundle. */
|
|
8
|
+
export function buildClaimAllowlist(context, accounts) {
|
|
9
|
+
if (!Array.isArray(accounts) || accounts.length === 0 || accounts.length > 65536)
|
|
10
|
+
throw new NativeClaimError("allowlist requires 1–65536 distinct G accounts");
|
|
11
|
+
const unique = new Set();
|
|
12
|
+
const entries = accounts.map(account => { address(account, "account", "allowlisted claimant"); if (unique.has(account))
|
|
13
|
+
throw new NativeClaimError("duplicate allowlist account"); unique.add(account); return { account, leaf: claimAllowlistLeaf(context, account), proof: [] }; }).sort((a, b) => a.leaf.localeCompare(b.leaf));
|
|
14
|
+
let layer = entries.map((entry, index) => ({ hash: entry.leaf, indices: [index] }));
|
|
15
|
+
while (layer.length > 1) {
|
|
16
|
+
const next = [];
|
|
17
|
+
for (let i = 0; i < layer.length; i += 2) {
|
|
18
|
+
const left = layer[i], right = layer[i + 1];
|
|
19
|
+
if (!right) {
|
|
20
|
+
next.push(left);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
for (const index of left.indices)
|
|
24
|
+
entries[index].proof.push(right.hash);
|
|
25
|
+
for (const index of right.indices)
|
|
26
|
+
entries[index].proof.push(left.hash);
|
|
27
|
+
next.push({ hash: claimAllowlistParent(left.hash, right.hash), indices: [...left.indices, ...right.indices] });
|
|
28
|
+
}
|
|
29
|
+
layer = next;
|
|
30
|
+
}
|
|
31
|
+
return { version: 1, context: { ...context }, root: layer[0].hash, entries };
|
|
32
|
+
}
|
|
33
|
+
export function verifyClaimAllowlistProof(context, account, proof, root) { hex32(root, "allowlist root"); if (!Array.isArray(proof) || proof.length > 32)
|
|
34
|
+
throw new NativeClaimError("allowlist proof exceeds 32 siblings"); return proof.reduce(claimAllowlistParent, claimAllowlistLeaf(context, account)) === root; }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type EligibilitySigner } from "./native-auth.js";
|
|
2
|
+
import { type ClaimIntent, type ClaimConfig } from "./native-types.js";
|
|
3
|
+
export type ClaimApprovalContext = {
|
|
4
|
+
/** Read this configuration and owner independently from the chain. Do not accept these from an applicant. */
|
|
5
|
+
config: ClaimConfig;
|
|
6
|
+
owner: string;
|
|
7
|
+
ownerEpoch: bigint;
|
|
8
|
+
networkPassphrase: string;
|
|
9
|
+
/** Independently pinned/read deployment, never copied from the applicant intent. network/namespace are hex32. */
|
|
10
|
+
network: string;
|
|
11
|
+
registry: string;
|
|
12
|
+
registrar: string;
|
|
13
|
+
namespace: string;
|
|
14
|
+
resolver: string;
|
|
15
|
+
latestLedger: number;
|
|
16
|
+
maxExpirationLedger: number;
|
|
17
|
+
expirationLedger: number;
|
|
18
|
+
};
|
|
19
|
+
/** Backend admission signing only. The application must first authenticate membership and the wallet binding.
|
|
20
|
+
* This function cannot turn an owner/treasury key into the configured admission account. */
|
|
21
|
+
export declare function signClaimEligibility(intent: ClaimIntent, unsignedEntryXdr: string, context: ClaimApprovalContext, signer: EligibilitySigner): Promise<string>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { hash, scValToNative, StrKey, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { NativeClaimError, address, hex, utf8 } from "./native-codec.js";
|
|
3
|
+
import { signEligibilityAuthorization } from "./native-auth.js";
|
|
4
|
+
import { claimIntentToScVal } from "./native-types.js";
|
|
5
|
+
/** Backend admission signing only. The application must first authenticate membership and the wallet binding.
|
|
6
|
+
* This function cannot turn an owner/treasury key into the configured admission account. */
|
|
7
|
+
export async function signClaimEligibility(intent, unsignedEntryXdr, context, signer) {
|
|
8
|
+
const encoded = claimIntentToScVal(intent), config = context.config, settings = config.settings;
|
|
9
|
+
if (context.network !== hex(hash(utf8(context.networkPassphrase))) || intent.context.network !== context.network || intent.context.registry !== context.registry || intent.context.registrar !== context.registrar || intent.context.namespace !== context.namespace || intent.resolver !== context.resolver || intent.ownerEpoch !== context.ownerEpoch || config.ownerEpoch !== context.ownerEpoch)
|
|
10
|
+
throw new NativeClaimError("approval intent differs from trusted deployment, namespace or current ownership epoch", "authorization");
|
|
11
|
+
address(context.owner, "identity", "namespace owner");
|
|
12
|
+
if (settings.mode !== "public" || !settings.enabled || settings.admission.type !== "approval")
|
|
13
|
+
throw new NativeClaimError("native app approval is not enabled", "authorization");
|
|
14
|
+
const account = settings.admission.account;
|
|
15
|
+
if (account === context.owner || account === settings.feeRecipient || account === intent.claimant)
|
|
16
|
+
throw new NativeClaimError("eligibility account must be separate from owner, treasury and claimant", "authorization");
|
|
17
|
+
if (intent.context.network !== hex(hash(utf8(context.networkPassphrase))) || intent.ownerEpoch !== config.ownerEpoch || intent.policyVersion !== config.policyVersion || intent.grantEpoch !== config.grantEpoch || intent.feeToken !== settings.feeToken || intent.feeAmount !== settings.feeAmount || intent.feeRecipient !== settings.feeRecipient)
|
|
18
|
+
throw new NativeClaimError("approval intent differs from independently read network/policy", "authorization");
|
|
19
|
+
if (intent.context.deadline - intent.context.validAfter > settings.approvalTtlSecs)
|
|
20
|
+
throw new NativeClaimError("approval exceeds the current business lifetime", "authorization");
|
|
21
|
+
const expected = { account, invocation: { contract: intent.context.registrar, method: "claim", args: [encoded] }, latestLedger: context.latestLedger, maxExpirationLedger: context.maxExpirationLedger };
|
|
22
|
+
const signed = await signEligibilityAuthorization(xdr.SorobanAuthorizationEntry.fromXDR(unsignedEntryXdr, "base64"), expected, signer, context.expirationLedger, context.networkPassphrase);
|
|
23
|
+
if (signed.credentials.type !== "sorobanCredentialsAddress")
|
|
24
|
+
throw new NativeClaimError("unsupported approval credential", "authorization");
|
|
25
|
+
const signatures = scValToNative(signed.credentials.address.signature);
|
|
26
|
+
if (!Array.isArray(signatures) || signatures.length === 0)
|
|
27
|
+
throw new NativeClaimError("approval is missing native account signatures", "authorization");
|
|
28
|
+
for (const signature of signatures) {
|
|
29
|
+
if (!(signature?.public_key instanceof Uint8Array) || signature.public_key.length !== 32)
|
|
30
|
+
throw new NativeClaimError("invalid native approval signer", "authorization");
|
|
31
|
+
const key = StrKey.encodeEd25519PublicKey(signature.public_key);
|
|
32
|
+
if ([context.owner, settings.feeRecipient, intent.claimant].includes(key))
|
|
33
|
+
throw new NativeClaimError("owner, treasury and claimant keys must never sign app eligibility", "authorization");
|
|
34
|
+
}
|
|
35
|
+
return signed.toXDR("base64");
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Transaction, authorizeEntry, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
export type NativeInvocation = {
|
|
3
|
+
contract: string;
|
|
4
|
+
method: string;
|
|
5
|
+
args: xdr.ScVal[];
|
|
6
|
+
children?: NativeInvocation[];
|
|
7
|
+
};
|
|
8
|
+
/** Derive this plan locally from a validated intent, never from a remote envelope. */
|
|
9
|
+
export type NativeAuthorizationPlan = {
|
|
10
|
+
source: string;
|
|
11
|
+
contract: string;
|
|
12
|
+
method: string;
|
|
13
|
+
args: xdr.ScVal[];
|
|
14
|
+
sourceInvocation: NativeInvocation;
|
|
15
|
+
eligibility?: {
|
|
16
|
+
account: string;
|
|
17
|
+
invocation: NativeInvocation;
|
|
18
|
+
latestLedger: number;
|
|
19
|
+
maxExpirationLedger: number;
|
|
20
|
+
};
|
|
21
|
+
maxFeeStroops: bigint;
|
|
22
|
+
};
|
|
23
|
+
export declare function authorizedInvocation(value: NativeInvocation): xdr.SorobanAuthorizedInvocation;
|
|
24
|
+
export declare function validateEligibilityAuthorization(entry: xdr.SorobanAuthorizationEntry, expected: NonNullable<NativeAuthorizationPlan["eligibility"]>, requireSigned?: boolean): void;
|
|
25
|
+
/** Native account signing callback; hardware/custody integration keeps keys outside the SDK. */
|
|
26
|
+
export type EligibilitySigner = Parameters<typeof authorizeEntry>[1];
|
|
27
|
+
/** Signs only the expected admission entry. This helper never signs a transaction. */
|
|
28
|
+
export declare function signEligibilityAuthorization(unsignedEntry: xdr.SorobanAuthorizationEntry, expected: NonNullable<NativeAuthorizationPlan["eligibility"]>, signer: EligibilitySigner, expirationLedger: number, networkPassphrase: string): Promise<xdr.SorobanAuthorizationEntry>;
|
|
29
|
+
/** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
|
|
30
|
+
export declare function validateNativeTransaction(tx: Transaction, plan: NativeAuthorizationPlan, requireEligibilitySigned?: boolean): void;
|
|
31
|
+
export declare function assertSignedBodyUnchanged(prepared: Transaction, signedXdr: string, passphrase: string): Transaction;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Address, Transaction, TransactionBuilder, authorizeEntry, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { NativeClaimError, address, hex, u32 } from "./native-codec.js";
|
|
3
|
+
export function authorizedInvocation(value) {
|
|
4
|
+
return new xdr.SorobanAuthorizedInvocation({
|
|
5
|
+
function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(new xdr.InvokeContractArgs({
|
|
6
|
+
contractAddress: new Address(address(value.contract, "contract", "invocation contract")).toScAddress(), functionName: value.method, args: value.args,
|
|
7
|
+
})), subInvocations: (value.children ?? []).map(authorizedInvocation),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function sameInvocation(actual, expected) {
|
|
11
|
+
return actual.toXDR("base64") === authorizedInvocation(expected).toXDR("base64");
|
|
12
|
+
}
|
|
13
|
+
export function validateEligibilityAuthorization(entry, expected, requireSigned = true) {
|
|
14
|
+
if (expected.invocation.method !== "claim" || expected.invocation.args.length !== 1 || (expected.invocation.children?.length ?? 0) !== 0)
|
|
15
|
+
throw new NativeClaimError("eligibility may authorize only one exact native claim intent", "authorization");
|
|
16
|
+
address(expected.account, "account", "eligibility account");
|
|
17
|
+
u32(expected.latestLedger, "latest ledger");
|
|
18
|
+
u32(expected.maxExpirationLedger, "maximum auth expiration ledger");
|
|
19
|
+
if (entry.credentials.type !== "sorobanCredentialsAddress")
|
|
20
|
+
throw new NativeClaimError("eligibility requires the supported native G address credential", "authorization");
|
|
21
|
+
const credential = entry.credentials.address;
|
|
22
|
+
if (Address.fromScAddress(credential.address).toString() !== expected.account || !sameInvocation(entry.rootInvocation, expected.invocation) || entry.rootInvocation.subInvocations.length !== 0)
|
|
23
|
+
throw new NativeClaimError("eligibility authorization differs from the exact claim intent", "authorization");
|
|
24
|
+
if (requireSigned) {
|
|
25
|
+
if (credential.signatureExpirationLedger <= expected.latestLedger || credential.signatureExpirationLedger > expected.maxExpirationLedger)
|
|
26
|
+
throw new NativeClaimError("eligibility authorization expiry is outside the approved ledger bound", "authorization");
|
|
27
|
+
if (credential.signature.type !== "scvVec" || !credential.signature.vec?.length)
|
|
28
|
+
throw new NativeClaimError("eligibility authorization has no native account signatures", "authorization");
|
|
29
|
+
}
|
|
30
|
+
else if (credential.signature.type !== "scvVoid" && !(credential.signature.type === "scvVec" && credential.signature.vec?.length === 0)) {
|
|
31
|
+
throw new NativeClaimError("refusing to sign an already signed eligibility credential", "authorization");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Signs only the expected admission entry. This helper never signs a transaction. */
|
|
35
|
+
export async function signEligibilityAuthorization(unsignedEntry, expected, signer, expirationLedger, networkPassphrase) {
|
|
36
|
+
validateEligibilityAuthorization(unsignedEntry, expected, false);
|
|
37
|
+
u32(expirationLedger, "expiration ledger");
|
|
38
|
+
if (expirationLedger <= expected.latestLedger || expirationLedger > expected.maxExpirationLedger)
|
|
39
|
+
throw new NativeClaimError("eligibility expiration is outside the approved range", "authorization");
|
|
40
|
+
if (!networkPassphrase)
|
|
41
|
+
throw new NativeClaimError("network passphrase is required");
|
|
42
|
+
const signed = await authorizeEntry(unsignedEntry, signer, expirationLedger, networkPassphrase);
|
|
43
|
+
validateEligibilityAuthorization(signed, expected);
|
|
44
|
+
return signed;
|
|
45
|
+
}
|
|
46
|
+
/** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
|
|
47
|
+
export function validateNativeTransaction(tx, plan, requireEligibilitySigned = true) {
|
|
48
|
+
address(plan.source, "account", "claim transaction source");
|
|
49
|
+
if (tx.source !== plan.source)
|
|
50
|
+
throw new NativeClaimError(`native transaction source ${tx.source} differs from reviewed source ${plan.source}`, "authorization");
|
|
51
|
+
if (tx.operations.length !== 1)
|
|
52
|
+
throw new NativeClaimError(`native transaction has ${tx.operations.length} operations; exactly one was reviewed`, "authorization");
|
|
53
|
+
if (tx.memo.type !== "none")
|
|
54
|
+
throw new NativeClaimError(`native transaction memo type ${tx.memo.type} differs from reviewed none`, "authorization");
|
|
55
|
+
if (BigInt(tx.fee) > plan.maxFeeStroops)
|
|
56
|
+
throw new NativeClaimError(`native transaction network fee ${tx.fee} stroops exceeds the configured maximum ${plan.maxFeeStroops} stroops`, "authorization");
|
|
57
|
+
const op = tx.operations[0];
|
|
58
|
+
if (op.type !== "invokeHostFunction" || op.source && op.source !== plan.source || op.func.type !== "hostFunctionTypeInvokeContract")
|
|
59
|
+
throw new NativeClaimError("unexpected native operation", "authorization");
|
|
60
|
+
const call = op.func.invokeContract;
|
|
61
|
+
if (Address.fromScAddress(call.contractAddress).toString() !== plan.contract || call.functionName.toString() !== plan.method || call.args.length !== plan.args.length || call.args.some((arg, i) => arg.toXDR("base64") !== plan.args[i].toXDR("base64")))
|
|
62
|
+
throw new NativeClaimError("native operation differs from the exact reviewed intent", "authorization");
|
|
63
|
+
const entries = op.auth ?? [];
|
|
64
|
+
if (entries.length !== (plan.eligibility ? 2 : 1))
|
|
65
|
+
throw new NativeClaimError("unexpected native authorization count", "authorization");
|
|
66
|
+
const sources = entries.filter(entry => entry.credentials.type === "sorobanCredentialsSourceAccount");
|
|
67
|
+
if (sources.length !== 1 || !sameInvocation(sources[0].rootInvocation, plan.sourceInvocation))
|
|
68
|
+
throw new NativeClaimError("holder or owner authorization differs from exact reviewed intent", "authorization");
|
|
69
|
+
if (plan.eligibility) {
|
|
70
|
+
if (plan.eligibility.account === plan.source)
|
|
71
|
+
throw new NativeClaimError("eligibility account must be separate from claimant", "authorization");
|
|
72
|
+
const separate = entries.filter(entry => entry.credentials.type !== "sorobanCredentialsSourceAccount");
|
|
73
|
+
if (separate.length !== 1)
|
|
74
|
+
throw new NativeClaimError("unexpected second authorizer", "authorization");
|
|
75
|
+
validateEligibilityAuthorization(separate[0], plan.eligibility, requireEligibilitySigned);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function assertSignedBodyUnchanged(prepared, signedXdr, passphrase) {
|
|
79
|
+
const signed = TransactionBuilder.fromXDR(signedXdr, passphrase);
|
|
80
|
+
if (!(signed instanceof Transaction) || hex(signed.hash()) !== hex(prepared.hash()))
|
|
81
|
+
throw new NativeClaimError("wallet changed the reviewed native transaction body", "authorization");
|
|
82
|
+
if (!signed.signatures.length)
|
|
83
|
+
throw new NativeClaimError("wallet returned an unsigned transaction", "authorization");
|
|
84
|
+
return signed;
|
|
85
|
+
}
|