@sorandomains/owner 0.7.0 → 0.8.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 +25 -5
- package/dist/index.d.ts +14 -39
- package/dist/index.js +113 -16
- package/dist/native-code-policy.d.ts +14 -0
- package/dist/native-code-policy.js +53 -0
- package/dist/native-history.d.ts +26 -0
- package/dist/native-history.js +162 -0
- package/dist/native-transport.js +6 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,18 +5,17 @@
|
|
|
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.8.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.8.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
|
|
15
15
|
actual G/C holder, then that holder can call Holder `setPayment` to publish M.
|
|
16
16
|
|
|
17
17
|
Run your Soran namespace on Stellar, programmatically. Issue names to your
|
|
18
|
-
users, manage their lifecycle
|
|
19
|
-
ready — walk the one-way permanence door. Every operation is a transaction
|
|
18
|
+
users, manage their lifecycle and transfer the namespace. Every operation is a transaction
|
|
20
19
|
your own key signs and any Soroban RPC node submits: no Soran account, no
|
|
21
20
|
hosted API in the path.
|
|
22
21
|
|
|
@@ -45,7 +44,7 @@ for (const o of batch.outcomes) if (!o.issued) console.warn(o.label, o.reason);
|
|
|
45
44
|
| `reclaim` | Take a name back — only where the namespace policy allows it |
|
|
46
45
|
| `renew` | Extend a finite-term name; returns the new expiry |
|
|
47
46
|
| `setTreasury` | Route reclaim custody to a treasury address |
|
|
48
|
-
| `makePermanent` |
|
|
47
|
+
| `makePermanent` | Legacy deployment operation; unavailable on governed testnet contracts |
|
|
49
48
|
| `proposeNamespaceTransfer` / `acceptNamespaceTransfer` / `cancelNamespaceTransfer` | Two-step, accept-to-move namespace transfer |
|
|
50
49
|
| `setResolver` | Point the namespace at a resolver (frozen once permanent) |
|
|
51
50
|
| `policy` / `isPermanent` / `nameState` / `pendingNamespaceTransfer` / `namespaceOwner` / `registrarOf` / `assertOwner` | Reads that support the write flows |
|
|
@@ -101,3 +100,24 @@ Verified on 6 September 2026 at ledger **4534629** (12:12 UTC). Network passphra
|
|
|
101
100
|
Mainnet has no deployment preset. Custom networks must supply their own verified
|
|
102
101
|
addresses. Universal Lookup upgrades remain immediately executable; an address
|
|
103
102
|
and ABI version do not pin the code that will execute after a governance upgrade.
|
|
103
|
+
|
|
104
|
+
## Network fee limits
|
|
105
|
+
|
|
106
|
+
Available in 0.8.0.
|
|
107
|
+
|
|
108
|
+
Every write and automatic restoration has a total network-fee limit, including the base and resource fees. The default is **5 XLM** (`50_000_000n` stroops). Set `maxNetworkFeeStroops` on the client only after reviewing a higher storage/rent estimate. This limits each transaction separately; it is not a daily or batch spending budget, and namespace/username prices are separate.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const client = new SoranOwner({
|
|
112
|
+
signer,
|
|
113
|
+
maxNetworkFeeStroops: 50_000_000n,
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The older `maxNativeFeeStroops` option remains supported: when the new option is omitted, it also supplies the total ceiling. If both are set, native operations use the lower value and other writes use `maxNetworkFeeStroops`. An excessive estimate is rejected before wallet approval. After a possible submission, errors retain the original transaction hash; reconcile it before creating a replacement transaction.
|
|
118
|
+
|
|
119
|
+
## Governed testnet code and migration recovery
|
|
120
|
+
|
|
121
|
+
On a governed Registry, native verification reads the exact per-namespace Registrar code pin and its upgrade history. Approved upgrades do not have to match the current factory default. Missing or malformed provenance still prevents signing and receipt confirmation; RPC failure never downgrades verification to a legacy rule.
|
|
122
|
+
|
|
123
|
+
Historical claim recovery is read-only and binds the original intent to the frozen source Registrar and sealed migration commitments. It never rewrites the intent to a successor Registry or treats an unavailable receipt as permission to submit again. Deployment migration and package publication are separate; check the release status for the active addresses.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,40 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Everything a namespace owner does after winning a namespace, programmatically:
|
|
5
|
-
*
|
|
6
|
-
* import { SoranOwner, keypairSigner } from "@sorandomains/owner";
|
|
7
|
-
*
|
|
8
|
-
* const owner = new SoranOwner({ signer: keypairSigner(process.env.OWNER_SECRET!) });
|
|
9
|
-
* await owner.issue("acme", "alice", "GDHN…"); // alice.acme now resolves
|
|
10
|
-
* await owner.issueBatch("acme", csvRows); // up to 23 per transaction
|
|
11
|
-
*
|
|
12
|
-
* TRUST MODEL. This SDK talks to the contracts directly over any Soroban RPC
|
|
13
|
-
* node — no Soran account, no hosted API, no third party in the path. Every
|
|
14
|
-
* operation here is authorized on chain: the Registrar and Registry check
|
|
15
|
-
* `require_auth` against the namespace owner's address, so the only credential
|
|
16
|
-
* that matters is the signer you supply. Soran's servers cannot perform any of
|
|
17
|
-
* these operations for you or against you.
|
|
18
|
-
*
|
|
19
|
-
* WHO CAN CALL WHAT. All operations below are namespace-OWNER powers except
|
|
20
|
-
* `acceptNamespaceTransfer`, which the proposed NEW owner signs. Name-holder
|
|
21
|
-
* powers (transferring an individual name, pointing it at a new address,
|
|
22
|
-
* electing a primary name) are the holder's alone — they live in the contracts'
|
|
23
|
-
* holder-authorized entry points and live in `@sorandomains/holder`; this
|
|
24
|
-
* package deliberately cannot exercise them.
|
|
25
|
-
*
|
|
26
|
-
* SIGNING. Supply a `TxSigner`: `keypairSigner(secret)` for backends, or wrap a
|
|
27
|
-
* browser wallet (Freighter / Stellar Wallets Kit expose exactly this
|
|
28
|
-
* `signTransaction(xdr, { networkPassphrase })` shape). The SDK builds and
|
|
29
|
-
* simulates each transaction, hands you the envelope to sign, and submits it.
|
|
30
|
-
* Operations on one SoranOwner instance are serialized so concurrent calls
|
|
31
|
-
* cannot race the account sequence number.
|
|
32
|
-
*
|
|
33
|
-
* READS. For resolution, reverse lookup, and assurance checks use the read-only
|
|
34
|
-
* companion package `@sorandomains/lookup` — wallets and apps should depend on
|
|
35
|
-
* that one only. The few reads here (policy, pending transfers, records) exist
|
|
36
|
-
* to support write flows.
|
|
37
|
-
*/
|
|
1
|
+
import { type HistoricalRecoveryOptions } from "./native-history.js";
|
|
2
|
+
import type { ClaimIntent } from "./native-types.js";
|
|
3
|
+
export type { HistoricalRecoveryOptions } from "./native-history.js";
|
|
38
4
|
import { type NativeWriteOptions, type NativeCapability } from "./native-transport.js";
|
|
39
5
|
import { type ClaimSettings, type ClaimConfig, type ClaimUsage, type ApprovalUsage } from "./native-types.js";
|
|
40
6
|
export * from "./native-types.js";
|
|
@@ -47,7 +13,7 @@ export declare const DEPLOYMENTS: {
|
|
|
47
13
|
readonly testnet: {
|
|
48
14
|
readonly rpcUrl: "https://soroban-testnet.stellar.org";
|
|
49
15
|
readonly passphrase: string;
|
|
50
|
-
readonly registryId: "
|
|
16
|
+
readonly registryId: "CCSORANDPQINYOYB5SVO45WJP2LBBYKC72HHUIRVXB4J6RUZKDAUW7G4";
|
|
51
17
|
};
|
|
52
18
|
};
|
|
53
19
|
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
@@ -145,7 +111,10 @@ export type NameState = {
|
|
|
145
111
|
generation: bigint;
|
|
146
112
|
};
|
|
147
113
|
export type OwnerOptions = {
|
|
148
|
-
/**
|
|
114
|
+
/** Maximum total network fee per transaction, including restoration. Default 5 XLM.
|
|
115
|
+
* If omitted, an explicit maxNativeFeeStroops also supplies this ceiling. */
|
|
116
|
+
maxNetworkFeeStroops?: bigint;
|
|
117
|
+
/** Optional additional native-operation ceiling; the lower configured limit wins. */
|
|
149
118
|
maxNativeFeeStroops?: bigint;
|
|
150
119
|
/** Signs every transaction. The namespace owner's account — or, for
|
|
151
120
|
* `acceptNamespaceTransfer`, the proposed new owner's. */
|
|
@@ -168,6 +137,7 @@ export type OwnerOptions = {
|
|
|
168
137
|
};
|
|
169
138
|
export declare class SoranOwner {
|
|
170
139
|
private maxNativeFeeStroops;
|
|
140
|
+
private maxNetworkFeeStroops;
|
|
171
141
|
private server;
|
|
172
142
|
private passphrase;
|
|
173
143
|
private registryId;
|
|
@@ -181,6 +151,8 @@ export declare class SoranOwner {
|
|
|
181
151
|
/** The namespace's on-chain attested Registrar id. Cached per instance. */
|
|
182
152
|
private nativeConfirmed;
|
|
183
153
|
private nativeContext;
|
|
154
|
+
/** Read the original frozen-source receipt after a fully sealed migration. Never submits. */
|
|
155
|
+
recoverHistoricalClaim(intent: ClaimIntent, options: HistoricalRecoveryOptions): Promise<import("./native-types.js").ClaimReceipt | null>;
|
|
184
156
|
/** Native successor capability. A failed RPC read never means open or legacy. */
|
|
185
157
|
nativeClaimCapability(namespace: string): Promise<NativeCapability>;
|
|
186
158
|
claimPolicy(namespace: string): Promise<ClaimConfig | null>;
|
|
@@ -282,6 +254,9 @@ export declare class SoranOwner {
|
|
|
282
254
|
private readRecords;
|
|
283
255
|
/** Simulation-only contract read — no signature, no fee, no state change. */
|
|
284
256
|
private read;
|
|
257
|
+
private readWithLedger;
|
|
258
|
+
private simulateRead;
|
|
259
|
+
private assertNetworkFee;
|
|
285
260
|
private signEnvelope;
|
|
286
261
|
private serialize;
|
|
287
262
|
/** The signer's live account (sequence source) — with an actionable error
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recoverFrozenClaim, historicalReadLedger } from "./native-history.js";
|
|
1
2
|
/**
|
|
2
3
|
* @sorandomains/owner — the write-side SDK for Soran namespace owners.
|
|
3
4
|
*
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
* that one only. The few reads here (policy, pending transfers, records) exist
|
|
36
37
|
* to support write flows.
|
|
37
38
|
*/
|
|
38
|
-
import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, xdr, } from "@stellar/stellar-sdk";
|
|
39
|
+
import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, SorobanDataBuilder, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, xdr, } from "@stellar/stellar-sdk";
|
|
39
40
|
/** Contract events from a transaction's meta, across meta versions (v3 puts
|
|
40
41
|
* them on sorobanMeta; v4 nests them per-operation and in tx events). */
|
|
41
42
|
function extractContractEvents(meta) {
|
|
@@ -99,9 +100,9 @@ export const DEPLOYMENTS = {
|
|
|
99
100
|
testnet: {
|
|
100
101
|
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
101
102
|
passphrase: Networks.TESTNET,
|
|
102
|
-
// The
|
|
103
|
+
// The governed Registry. The Registrar for a namespace is discovered on
|
|
103
104
|
// chain via `registrar_of(node)` — never configured by hand.
|
|
104
|
-
registryId: "
|
|
105
|
+
registryId: "CCSORANDPQINYOYB5SVO45WJP2LBBYKC72HHUIRVXB4J6RUZKDAUW7G4",
|
|
105
106
|
},
|
|
106
107
|
};
|
|
107
108
|
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
@@ -151,7 +152,37 @@ const REGISTRAR_ERRORS = {
|
|
|
151
152
|
18: "InvalidPolicy",
|
|
152
153
|
19: "ExpiryOverflow",
|
|
153
154
|
20: "InvalidRegistry",
|
|
154
|
-
21: "InvalidClaimConfig",
|
|
155
|
+
21: "InvalidClaimConfig",
|
|
156
|
+
22: "ClaimsNotConfigured",
|
|
157
|
+
23: "ClaimsPaused",
|
|
158
|
+
24: "StaleClaimPolicy",
|
|
159
|
+
25: "ClaimIntentMismatch",
|
|
160
|
+
26: "ClaimIntentExpired",
|
|
161
|
+
27: "ReservedName",
|
|
162
|
+
28: "NameNotReserved",
|
|
163
|
+
29: "PublicIssuanceRequired",
|
|
164
|
+
30: "WalletClaimLimit",
|
|
165
|
+
31: "InvalidEligibility",
|
|
166
|
+
32: "ApprovalAllowanceReached",
|
|
167
|
+
33: "ApprovalRateReached",
|
|
168
|
+
34: "RequestIdConflict",
|
|
169
|
+
35: "CounterOverflow",
|
|
170
|
+
36: "InvalidNativeBinding",
|
|
171
|
+
37: "UnsupportedClaimant",
|
|
172
|
+
38: "DuplicateLabel",
|
|
173
|
+
39: "RenewalTooEarly",
|
|
174
|
+
40: "RenewalLeaseLimit",
|
|
175
|
+
41: "DestinationInitializationFailed",
|
|
176
|
+
42: "FeeSettlementFailed",
|
|
177
|
+
43: "MigrationInProgress",
|
|
178
|
+
44: "MigrationAlreadyStarted",
|
|
179
|
+
45: "MigrationNotActive",
|
|
180
|
+
46: "MigrationMismatch",
|
|
181
|
+
47: "MigrationDuplicate",
|
|
182
|
+
48: "MigrationIncomplete",
|
|
183
|
+
49: "MigrationUnavailable",
|
|
184
|
+
50: "MigrationUnsupported",
|
|
185
|
+
51: "PermanentLockDisabled",
|
|
155
186
|
};
|
|
156
187
|
/** Registry contract error codes, by number. */
|
|
157
188
|
const REGISTRY_ERRORS = {
|
|
@@ -183,6 +214,19 @@ const REGISTRY_ERRORS = {
|
|
|
183
214
|
26: "AnchorMismatch",
|
|
184
215
|
27: "InvalidAllocator",
|
|
185
216
|
28: "StaleEra",
|
|
217
|
+
29: "OwnershipEpochUnavailable",
|
|
218
|
+
30: "OwnershipEpochOverflow",
|
|
219
|
+
31: "MigrationInProgress",
|
|
220
|
+
32: "MigrationAlreadyStarted",
|
|
221
|
+
33: "MigrationNotActive",
|
|
222
|
+
34: "MigrationMismatch",
|
|
223
|
+
35: "MigrationIncomplete",
|
|
224
|
+
36: "MigrationDuplicate",
|
|
225
|
+
37: "MigrationUnsupported",
|
|
226
|
+
38: "MigrationUnavailable",
|
|
227
|
+
39: "UpgradePolicyMismatch",
|
|
228
|
+
40: "UpgradeNotApproved",
|
|
229
|
+
41: "PermanentLockDisabled",
|
|
186
230
|
};
|
|
187
231
|
/**
|
|
188
232
|
* A failed owner operation. When the contract itself rejected the call,
|
|
@@ -275,6 +319,7 @@ export const MAX_BATCH = 23;
|
|
|
275
319
|
// ---------------------------------------------------------------------------
|
|
276
320
|
export class SoranOwner {
|
|
277
321
|
maxNativeFeeStroops;
|
|
322
|
+
maxNetworkFeeStroops;
|
|
278
323
|
server;
|
|
279
324
|
passphrase;
|
|
280
325
|
registryId;
|
|
@@ -290,9 +335,13 @@ export class SoranOwner {
|
|
|
290
335
|
registrars = new Map();
|
|
291
336
|
static REGISTRAR_TTL_MS = 30_000;
|
|
292
337
|
constructor(opts) {
|
|
293
|
-
this.
|
|
294
|
-
|
|
295
|
-
|
|
338
|
+
this.maxNetworkFeeStroops = opts?.maxNetworkFeeStroops ?? opts?.maxNativeFeeStroops ?? 50000000n;
|
|
339
|
+
const nativeLimit = opts?.maxNativeFeeStroops ?? this.maxNetworkFeeStroops;
|
|
340
|
+
for (const limit of [this.maxNetworkFeeStroops, nativeLimit]) {
|
|
341
|
+
if (typeof limit !== "bigint" || limit <= 0n || limit > 4294967295n)
|
|
342
|
+
throw new OwnerError("maximum network fee must be a bigint between 1 and 4294967295 stroops");
|
|
343
|
+
}
|
|
344
|
+
this.maxNativeFeeStroops = nativeLimit < this.maxNetworkFeeStroops ? nativeLimit : this.maxNetworkFeeStroops;
|
|
296
345
|
if (!opts?.signer)
|
|
297
346
|
throw new OwnerError("OwnerOptions.signer is required");
|
|
298
347
|
const d = DEPLOYMENTS[opts.network ?? "testnet"];
|
|
@@ -309,6 +358,8 @@ export class SoranOwner {
|
|
|
309
358
|
}
|
|
310
359
|
this.timeoutSecs = t;
|
|
311
360
|
this.fee = opts.fee ?? BASE_FEE;
|
|
361
|
+
if (!/^[1-9][0-9]*$/.test(this.fee) || BigInt(this.fee) > 4294967295n)
|
|
362
|
+
throw new OwnerError("base network fee must be canonical decimal stroops between 1 and 4294967295");
|
|
312
363
|
}
|
|
313
364
|
// ---- discovery -----------------------------------------------------------
|
|
314
365
|
/** The namespace's on-chain attested Registrar id. Cached per instance. */
|
|
@@ -324,6 +375,11 @@ export class SoranOwner {
|
|
|
324
375
|
return { registryId: this.registryId, passphrase: this.passphrase, server: this.server, signer: this.signer, fee: this.fee, timeoutSecs: this.timeoutSecs, maxFeeStroops: this.maxNativeFeeStroops,
|
|
325
376
|
read: (id, method, args) => this.read(id, method, args), serialize: work => this.serialize(work) };
|
|
326
377
|
}
|
|
378
|
+
/** Read the original frozen-source receipt after a fully sealed migration. Never submits. */
|
|
379
|
+
recoverHistoricalClaim(intent, options) {
|
|
380
|
+
return recoverFrozenClaim({ registryId: this.registryId, passphrase: this.passphrase, server: this.server,
|
|
381
|
+
readWithLedger: (id, method, args) => this.readWithLedger(id, method, args) }, intent, options);
|
|
382
|
+
}
|
|
327
383
|
/** Native successor capability. A failed RPC read never means open or legacy. */
|
|
328
384
|
nativeClaimCapability(namespace) { return nativeCapability(this.nativeContext(), normalizeLabel(namespace)); }
|
|
329
385
|
async claimPolicy(namespace) {
|
|
@@ -645,6 +701,13 @@ export class SoranOwner {
|
|
|
645
701
|
}
|
|
646
702
|
/** Simulation-only contract read — no signature, no fee, no state change. */
|
|
647
703
|
async read(contractId, fn, args) {
|
|
704
|
+
return (await this.simulateRead(contractId, fn, args)).value;
|
|
705
|
+
}
|
|
706
|
+
async readWithLedger(contractId, fn, args) {
|
|
707
|
+
const result = await this.simulateRead(contractId, fn, args);
|
|
708
|
+
return { value: result.value, ledger: historicalReadLedger(result.ledger) };
|
|
709
|
+
}
|
|
710
|
+
async simulateRead(contractId, fn, args) {
|
|
648
711
|
const tx = new TransactionBuilder(new Account(SIM_SOURCE, "0"), {
|
|
649
712
|
fee: BASE_FEE,
|
|
650
713
|
networkPassphrase: this.passphrase,
|
|
@@ -664,7 +727,11 @@ export class SoranOwner {
|
|
|
664
727
|
if (!rpc.Api.isSimulationSuccess(sim) || !sim.result?.retval)
|
|
665
728
|
throw new OwnerError(`${fn}: missing simulation return value`, contractId, fn);
|
|
666
729
|
const v = scValToNative(sim.result.retval);
|
|
667
|
-
return v === undefined ? null : v;
|
|
730
|
+
return { value: v === undefined ? null : v, ledger: sim.latestLedger };
|
|
731
|
+
}
|
|
732
|
+
assertNetworkFee(fee, fn) {
|
|
733
|
+
if (!/^[1-9][0-9]*$/.test(fee) || BigInt(fee) > this.maxNetworkFeeStroops)
|
|
734
|
+
throw new OwnerError(`${fn}: network fee ${fee} stroops exceeds the configured maximum ${this.maxNetworkFeeStroops} stroops`, null, fn);
|
|
668
735
|
}
|
|
669
736
|
async signEnvelope(xdrBase64) {
|
|
670
737
|
const signed = await this.signer.signTransaction(xdrBase64, {
|
|
@@ -695,7 +762,7 @@ export class SoranOwner {
|
|
|
695
762
|
catch (e) {
|
|
696
763
|
// A stale sequence (another process moved the account) is safe to
|
|
697
764
|
// retry once with a fresh sequence — nothing was included.
|
|
698
|
-
if (/txBadSeq|bad_seq/i.test(String(e))) {
|
|
765
|
+
if (!(e instanceof OwnerError && e.txHash) && /txBadSeq|bad_seq/i.test(String(e))) {
|
|
699
766
|
return await this.attempt(contractId, fn, args, errNames);
|
|
700
767
|
}
|
|
701
768
|
throw e;
|
|
@@ -754,12 +821,15 @@ export class SoranOwner {
|
|
|
754
821
|
throw typedError(contractId, fn, sim.error, errNames);
|
|
755
822
|
}
|
|
756
823
|
const prepared = rpc.assembleTransaction(tx, sim).build();
|
|
824
|
+
this.assertNetworkFee(prepared.fee, fn);
|
|
757
825
|
this.assertSatisfiableAuth(prepared, pub, contractId, fn);
|
|
758
826
|
// The hash is fixed before signatures — compute it now so every failure
|
|
759
827
|
// past this point can carry it (the "re-check before retrying" contract).
|
|
760
828
|
const txHash = toHex(prepared.hash()); // (SDK17) hash() is Uint8Array
|
|
761
829
|
const signed = await this.signEnvelope(prepared.toXDR());
|
|
762
830
|
const envelope = TransactionBuilder.fromXDR(signed, this.passphrase);
|
|
831
|
+
if (toHex(envelope.hash()) !== txHash)
|
|
832
|
+
throw new OwnerError("signer changed the reviewed transaction body", contractId, fn);
|
|
763
833
|
let sent;
|
|
764
834
|
try {
|
|
765
835
|
sent = await this.server.sendTransaction(envelope);
|
|
@@ -787,21 +857,48 @@ export class SoranOwner {
|
|
|
787
857
|
}
|
|
788
858
|
async restore(sim, pub) {
|
|
789
859
|
const pre = sim.restorePreamble;
|
|
790
|
-
|
|
860
|
+
if (!/^(0|[1-9][0-9]*)$/.test(pre.minResourceFee))
|
|
861
|
+
throw new OwnerError("invalid restoration resource fee", null, "restore_footprint");
|
|
862
|
+
const quoted = BigInt(pre.minResourceFee);
|
|
863
|
+
const data = pre.transactionData.build();
|
|
864
|
+
const encoded = BigInt(data.resourceFee.toString());
|
|
865
|
+
if (encoded < 0n)
|
|
866
|
+
throw new OwnerError("invalid encoded restoration resource fee", null, "restore_footprint");
|
|
867
|
+
const resourceFee = quoted > encoded ? quoted : encoded;
|
|
868
|
+
this.assertNetworkFee((BigInt(this.fee) + resourceFee).toString(), "restore_footprint");
|
|
791
869
|
const tx = new TransactionBuilder(await this.sourceAccount(pub, "restore_footprint"), {
|
|
792
|
-
|
|
870
|
+
// SDK 17 adds Soroban resourceFee itself; supply the base only once.
|
|
871
|
+
fee: this.fee,
|
|
793
872
|
networkPassphrase: this.passphrase,
|
|
794
873
|
})
|
|
795
|
-
.setSorobanData(
|
|
874
|
+
.setSorobanData(new SorobanDataBuilder(data).setResourceFee(resourceFee.toString()).build())
|
|
796
875
|
.addOperation(Operation.restoreFootprint({}))
|
|
797
876
|
.setTimeout(this.timeoutSecs)
|
|
798
877
|
.build();
|
|
878
|
+
this.assertNetworkFee(tx.fee, "restore_footprint");
|
|
879
|
+
const txHash = toHex(tx.hash());
|
|
799
880
|
const signed = await this.signEnvelope(tx.toXDR());
|
|
800
|
-
const
|
|
801
|
-
if (
|
|
802
|
-
throw new OwnerError(
|
|
881
|
+
const envelope = TransactionBuilder.fromXDR(signed, this.passphrase);
|
|
882
|
+
if (toHex(envelope.hash()) !== txHash)
|
|
883
|
+
throw new OwnerError("signer changed the reviewed restoration body", null, "restore_footprint");
|
|
884
|
+
let sent;
|
|
885
|
+
try {
|
|
886
|
+
sent = await this.server.sendTransaction(envelope);
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
throw new OwnerError(`restoration submission is uncertain (${String(error)}); check ${txHash} before retrying`, null, "restore_footprint", null, null, txHash);
|
|
890
|
+
}
|
|
891
|
+
if (sent.status === "ERROR" || sent.status === "TRY_AGAIN_LATER") {
|
|
892
|
+
throw new OwnerError(`restore_footprint: submit rejected: ${JSON.stringify(sent.errorResult ?? sent.status)}`, null, "restore_footprint", null, null, txHash);
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
await this.confirm(txHash, "", "restore_footprint", {});
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
if (error instanceof OwnerError)
|
|
899
|
+
throw error;
|
|
900
|
+
throw new OwnerError(`restoration confirmation is uncertain (${String(error)}); check ${txHash} before retrying`, null, "restore_footprint", null, null, txHash);
|
|
803
901
|
}
|
|
804
|
-
await this.confirm(sent.hash, "", "restore_footprint", {});
|
|
805
902
|
}
|
|
806
903
|
// Poll past the tx time bound (timeoutSecs) so we never declare failure
|
|
807
904
|
// while the transaction is still valid and pending inclusion.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { xdr, type rpc } from '@stellar/stellar-sdk';
|
|
2
|
+
type Context = {
|
|
3
|
+
registryId: string;
|
|
4
|
+
server: Pick<rpc.Server, 'getLedgerEntries'>;
|
|
5
|
+
read(contract: string, method: string, args: xdr.ScVal[]): Promise<unknown>;
|
|
6
|
+
readWithLedger?(contract: string, method: string, args: xdr.ScVal[]): Promise<{
|
|
7
|
+
value: unknown;
|
|
8
|
+
ledger: number;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
/** Select policy from an exact Registry instance proof. Failed RPC/method reads
|
|
12
|
+
* never downgrade governed verification to the historical template rule. */
|
|
13
|
+
export declare function expectedRegistrarCode(context: Context, namespace: string, minimumLedger?: number): Promise<string>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Contract, scValToNative, xdr } from '@stellar/stellar-sdk';
|
|
2
|
+
import { NativeClaimError, address, bytes32, exactObject, hex, sc, u32, unhex } from './native-codec.js';
|
|
3
|
+
function fail(message) { throw new NativeClaimError(`Registry code policy: ${message}`, 'unavailable'); }
|
|
4
|
+
function ledger(value, minimum) {
|
|
5
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < minimum || value < 1 || value > 0xffffffff)
|
|
6
|
+
fail('missing or stale ledger context');
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
/** Select policy from an exact Registry instance proof. Failed RPC/method reads
|
|
10
|
+
* never downgrade governed verification to the historical template rule. */
|
|
11
|
+
export async function expectedRegistrarCode(context, namespace, minimumLedger) {
|
|
12
|
+
const key = new Contract(context.registryId).getFootprint();
|
|
13
|
+
const proof = await context.server.getLedgerEntries(key);
|
|
14
|
+
const observed = ledger(proof?.latestLedger, minimumLedger ?? 1);
|
|
15
|
+
const entry = proof.entries?.[0];
|
|
16
|
+
if (proof.entries?.length !== 1 || !entry || entry.key.toXDR('base64') !== key.toXDR('base64') || entry.val.type !== 'contractData')
|
|
17
|
+
fail('missing or mismatched instance');
|
|
18
|
+
const data = entry.val.value;
|
|
19
|
+
if (data.contract.toXDR('base64') !== new Contract(context.registryId).address().toScAddress().toXDR('base64') || data.key.type !== 'scvLedgerKeyContractInstance' || data.durability !== xdr.ContractDataDurability.persistent || data.val.type !== 'scvContractInstance' || data.val.value.executable.type !== 'contractExecutableWasm')
|
|
20
|
+
fail('invalid Registry Wasm instance');
|
|
21
|
+
const storage = data.val.value.storage;
|
|
22
|
+
if (storage !== null && !Array.isArray(storage))
|
|
23
|
+
fail('malformed instance storage');
|
|
24
|
+
const authorityKey = xdr.ScVal.scvVec([xdr.ScVal.scvSymbol('AuthorityV1')]).toXDR('base64');
|
|
25
|
+
const authorities = (storage ?? []).filter(row => row.key.toXDR('base64') === authorityKey);
|
|
26
|
+
if (authorities.length > 1)
|
|
27
|
+
fail('duplicate authority');
|
|
28
|
+
const read = async (method, args = []) => {
|
|
29
|
+
if (minimumLedger === undefined)
|
|
30
|
+
return context.read(context.registryId, method, args);
|
|
31
|
+
if (!context.readWithLedger)
|
|
32
|
+
fail('fresh policy reader unavailable');
|
|
33
|
+
const result = await context.readWithLedger(context.registryId, method, args);
|
|
34
|
+
ledger(result?.ledger, Math.max(observed, minimumLedger));
|
|
35
|
+
return result.value;
|
|
36
|
+
};
|
|
37
|
+
if (authorities.length === 0) {
|
|
38
|
+
const templates = await read('template_hashes');
|
|
39
|
+
if (!Array.isArray(templates) || templates.length !== 2)
|
|
40
|
+
fail('malformed historical templates');
|
|
41
|
+
return hex(bytes32(templates[0], 'Registry Registrar template'));
|
|
42
|
+
}
|
|
43
|
+
if (authorities[0].val.type !== 'scvAddress')
|
|
44
|
+
fail('malformed upgrade authority');
|
|
45
|
+
address(scValToNative(authorities[0].val), 'identity', 'Registry upgrade authority');
|
|
46
|
+
if (await read('upgrade_policy_version') !== 1)
|
|
47
|
+
fail('unsupported governed policy');
|
|
48
|
+
const code = exactObject(await read('native_code', [sc.bytes(unhex(namespace))]), ['registrar', 'resolver', 'registrar_updates', 'resolver_updates'], 'native code');
|
|
49
|
+
u32(code.registrar_updates, 'Registrar upgrade count');
|
|
50
|
+
u32(code.resolver_updates, 'Resolver upgrade count');
|
|
51
|
+
bytes32(code.resolver, 'Registry Resolver code');
|
|
52
|
+
return hex(bytes32(code.registrar, 'Registry Registrar code'));
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { xdr, type rpc } from "@stellar/stellar-sdk";
|
|
2
|
+
import { type ClaimIntent, type ClaimReceipt } from "./native-types.js";
|
|
3
|
+
/** Use a deployment's trusted stable Lookup, never a value supplied by a claim. */
|
|
4
|
+
export type HistoricalRecoveryOptions = {
|
|
5
|
+
lookupId: string;
|
|
6
|
+
};
|
|
7
|
+
export type HistoricalRead = {
|
|
8
|
+
value: unknown;
|
|
9
|
+
ledger: number;
|
|
10
|
+
};
|
|
11
|
+
export type HistoricalContext = {
|
|
12
|
+
registryId: string;
|
|
13
|
+
passphrase: string;
|
|
14
|
+
server: Pick<rpc.Server, "getLedgerEntries">;
|
|
15
|
+
readWithLedger(contract: string, method: string, args: xdr.ScVal[]): Promise<HistoricalRead>;
|
|
16
|
+
};
|
|
17
|
+
export declare function historicalReadLedger(value: unknown, minimum?: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* Read a completed claim from its original frozen Registrar after cutover.
|
|
20
|
+
* The canonical Lookup's governance-approved sealed migration is an explicit
|
|
21
|
+
* trust anchor for source history before freeze. Current code hashes cannot
|
|
22
|
+
* independently prove that no malicious upgrade occurred before that review.
|
|
23
|
+
* This function never signs, submits, rewrites an intent or enables a replay.
|
|
24
|
+
* A verified null is historical absence, not permission to submit an old intent.
|
|
25
|
+
*/
|
|
26
|
+
export declare function recoverFrozenClaim(context: HistoricalContext, input: ClaimIntent, options: HistoricalRecoveryOptions): Promise<ClaimReceipt | null>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Contract, hash, scValToNative, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { NativeClaimError, address, bool, bytes32, exactObject, hex, namespaceNode, sc, struct, u32, u64, unhex, utf8 } from "./native-codec.js";
|
|
3
|
+
import { claimIntentFromNative, claimIntentToScVal, claimReceiptFromNative, nativeIntentHash } from "./native-types.js";
|
|
4
|
+
// A tainted Registrar is not generally trusted. Only this reviewed export
|
|
5
|
+
// implementation is eligible, after canonical Lookup confirms sealed migration.
|
|
6
|
+
const REVIEWED_SOURCE_HASH = "26e03cd1d7fbfa46fe442406fb1b88022ec8cd8bd9123bed570f971fce596a14";
|
|
7
|
+
const PLAN_FIELDS = ["source_registry", "source_registry_hash", "namespace_root", "namespace_count", "ownership_sequence", "primary_source", "primary_source_hash", "primary_target", "primary_target_hash", "primary_root", "primary_count"];
|
|
8
|
+
const LEAF_FIELDS = ["label", "owner", "owner_epoch", "source_registrar", "source_registrar_hash", "source_resolver", "source_resolver_hash", "treasury", "policy", "pending_transfer", "registrar_root", "registrar_count", "resolver_root", "resolver_count"];
|
|
9
|
+
function fail(message) { throw new NativeClaimError(`historical recovery: ${message}`, "unavailable"); }
|
|
10
|
+
export function historicalReadLedger(value, minimum = 1) {
|
|
11
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value < 1 || value > 0xffff_ffff)
|
|
12
|
+
return fail("RPC read has missing, invalid or stale ledger context");
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function sameBytes(value, expected, what) {
|
|
16
|
+
if (hex(bytes32(value, what)) !== expected)
|
|
17
|
+
fail(`${what} differs from the saved intent or migration commitment`);
|
|
18
|
+
}
|
|
19
|
+
function listOfOne(value, namespace, what) {
|
|
20
|
+
if (!Array.isArray(value) || value.length !== 1)
|
|
21
|
+
fail(`${what} is incomplete`);
|
|
22
|
+
sameBytes(value[0], namespace, what);
|
|
23
|
+
}
|
|
24
|
+
function leafScVal(raw) {
|
|
25
|
+
const policy = exactObject(raw.policy, ["reclaimable", "transferable", "tradeable", "default_term_secs", "trade_fee_bps"], "migration policy");
|
|
26
|
+
if (!(raw.label instanceof Uint8Array) || !Array.isArray(raw.pending_transfer) || raw.pending_transfer.length !== 0)
|
|
27
|
+
return fail("invalid namespace label or pending transfer in migration commitment");
|
|
28
|
+
const fields = {
|
|
29
|
+
label: sc.bytes(new Uint8Array(raw.label)), pending_transfer: xdr.ScVal.scvVec([]),
|
|
30
|
+
policy: struct({ reclaimable: sc.bool(bool(policy.reclaimable, "reclaimable")), transferable: sc.bool(bool(policy.transferable, "transferable")), tradeable: sc.bool(bool(policy.tradeable, "tradeable")), default_term_secs: sc.u64(u64(policy.default_term_secs, "default term")), trade_fee_bps: sc.u32(u32(policy.trade_fee_bps, "trade fee")) }),
|
|
31
|
+
owner_epoch: sc.u64(u64(raw.owner_epoch, "owner epoch")),
|
|
32
|
+
};
|
|
33
|
+
for (const key of ["owner", "treasury"])
|
|
34
|
+
fields[key] = sc.address(address(raw[key], "identity", key));
|
|
35
|
+
for (const key of ["source_registrar", "source_resolver"])
|
|
36
|
+
fields[key] = sc.address(address(raw[key], "contract", key));
|
|
37
|
+
for (const key of ["source_registrar_hash", "source_resolver_hash", "registrar_root", "resolver_root"])
|
|
38
|
+
fields[key] = sc.bytes(bytes32(raw[key], key));
|
|
39
|
+
for (const key of ["registrar_count", "resolver_count"]) {
|
|
40
|
+
const count = u32(raw[key], key);
|
|
41
|
+
if (count < 1 || count > 1024)
|
|
42
|
+
fail("invalid migration record count");
|
|
43
|
+
fields[key] = sc.u32(count);
|
|
44
|
+
}
|
|
45
|
+
return struct(fields);
|
|
46
|
+
}
|
|
47
|
+
function namespaceLeafHash(network, source, target, leaf) {
|
|
48
|
+
const pieces = [utf8("soran:registry-migration:v1\0"), unhex(network), sc.address(source).toXDR(), sc.address(target).toXDR(), leaf.toXDR()];
|
|
49
|
+
const bytes = new Uint8Array(pieces.reduce((n, piece) => n + piece.length, 0));
|
|
50
|
+
let offset = 0;
|
|
51
|
+
for (const piece of pieces) {
|
|
52
|
+
bytes.set(piece, offset);
|
|
53
|
+
offset += piece.length;
|
|
54
|
+
}
|
|
55
|
+
return hex(hash(bytes));
|
|
56
|
+
}
|
|
57
|
+
async function executableHash(context, contract, minimumLedger) {
|
|
58
|
+
const key = new Contract(contract).getFootprint();
|
|
59
|
+
const proof = await context.server.getLedgerEntries(key);
|
|
60
|
+
historicalReadLedger(proof?.latestLedger, minimumLedger);
|
|
61
|
+
const entry = proof.entries?.[0];
|
|
62
|
+
if (proof.entries?.length !== 1 || !entry || entry.key.toXDR("base64") !== key.toXDR("base64") || entry.val.type !== "contractData")
|
|
63
|
+
return fail("executable proof is missing or for another contract");
|
|
64
|
+
const data = entry.val.value;
|
|
65
|
+
if (data.contract.toXDR("base64") !== new Contract(contract).address().toScAddress().toXDR("base64") || data.key.type !== "scvLedgerKeyContractInstance" || data.durability !== xdr.ContractDataDurability.persistent || data.val.type !== "scvContractInstance" || data.val.value.executable.type !== "contractExecutableWasm")
|
|
66
|
+
return fail("executable proof is not the requested persistent Wasm instance");
|
|
67
|
+
return hex(data.val.value.executable.wasmHash.value);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read a completed claim from its original frozen Registrar after cutover.
|
|
71
|
+
* The canonical Lookup's governance-approved sealed migration is an explicit
|
|
72
|
+
* trust anchor for source history before freeze. Current code hashes cannot
|
|
73
|
+
* independently prove that no malicious upgrade occurred before that review.
|
|
74
|
+
* This function never signs, submits, rewrites an intent or enables a replay.
|
|
75
|
+
* A verified null is historical absence, not permission to submit an old intent.
|
|
76
|
+
*/
|
|
77
|
+
export async function recoverFrozenClaim(context, input, options) {
|
|
78
|
+
// Snapshot all inputs before the first await, including the trust anchor.
|
|
79
|
+
const encoded = claimIntentToScVal(input), intent = claimIntentFromNative(scValToNative(encoded));
|
|
80
|
+
const lookup = address(exactObject(options, ["lookupId"], "historical recovery options").lookupId, "contract", "trusted Lookup");
|
|
81
|
+
const target = address(context.registryId, "contract", "successor Registry");
|
|
82
|
+
const network = hex(hash(utf8(context.passphrase)));
|
|
83
|
+
if (intent.context.network !== network || intent.context.registry === target)
|
|
84
|
+
fail("requires the saved original network and a different successor Registry");
|
|
85
|
+
// The rollout is deliberately bounded to the reviewed single-Nova migration.
|
|
86
|
+
if (intent.context.namespace !== hex(namespaceNode("nova")))
|
|
87
|
+
fail("unsupported migration namespace");
|
|
88
|
+
const request = intent.context, node = sc.bytes(unhex(request.namespace));
|
|
89
|
+
// Establish completed cutover and freeze BEFORE observing receipt absence.
|
|
90
|
+
// A later freeze cannot make an earlier null snapshot authoritative.
|
|
91
|
+
let frozenLedger = 1;
|
|
92
|
+
const beforeReceipt = async (contract, method) => {
|
|
93
|
+
const result = await context.readWithLedger(contract, method, []);
|
|
94
|
+
frozenLedger = historicalReadLedger(result?.ledger, frozenLedger);
|
|
95
|
+
return result.value;
|
|
96
|
+
};
|
|
97
|
+
if (await beforeReceipt(lookup, "registry") !== target)
|
|
98
|
+
fail("canonical Lookup has not completed the successor cutover");
|
|
99
|
+
const beforeState = exactObject(await beforeReceipt(target, "migration_status"), ["plan", "imported", "completed", "sealed", "primary_initialized"], "migration status");
|
|
100
|
+
if (beforeState.sealed !== true || beforeState.primary_initialized !== true)
|
|
101
|
+
fail("successor migration is not fully sealed");
|
|
102
|
+
if (await beforeReceipt(request.registrar, "migration_export_frozen") !== true)
|
|
103
|
+
fail("source Registrar is not frozen before receipt observation");
|
|
104
|
+
const snapshot = await context.readWithLedger(request.registrar, "claim_receipt", [sc.address(intent.claimant), sc.bytes(unhex(request.requestId))]);
|
|
105
|
+
const observed = historicalReadLedger(snapshot?.ledger, frozenLedger);
|
|
106
|
+
const read = async (contract, method, args = []) => {
|
|
107
|
+
const result = await context.readWithLedger(contract, method, args);
|
|
108
|
+
historicalReadLedger(result?.ledger, observed);
|
|
109
|
+
return result.value;
|
|
110
|
+
};
|
|
111
|
+
const [lookupRegistry, lookupVersion, version, active, stateRaw, leafRaw, frozen, exportVersion, exportTarget, anchors, attested, sourceHash, sourceRegistryHash] = await Promise.all([
|
|
112
|
+
read(lookup, "registry"), read(lookup, "registry_migration_version"), read(target, "migration_version"), read(target, "migration_active"),
|
|
113
|
+
read(target, "migration_status"), read(target, "migration_namespace", [node]),
|
|
114
|
+
read(request.registrar, "migration_export_frozen"), read(request.registrar, "migration_export_version"), read(request.registrar, "migration_export_target"),
|
|
115
|
+
read(request.registrar, "anchors"), read(request.registry, "registrar_of", [node]),
|
|
116
|
+
executableHash(context, request.registrar, observed), executableHash(context, request.registry, observed),
|
|
117
|
+
]);
|
|
118
|
+
if (lookupRegistry !== target || lookupVersion !== 1 || version !== 1 || active !== false)
|
|
119
|
+
fail("canonical Lookup has not completed the supported successor cutover");
|
|
120
|
+
const state = exactObject(stateRaw, ["plan", "imported", "completed", "sealed", "primary_initialized"], "migration status");
|
|
121
|
+
if (state.sealed !== true || state.primary_initialized !== true)
|
|
122
|
+
fail("successor migration is not fully sealed");
|
|
123
|
+
const plan = exactObject(state.plan, PLAN_FIELDS, "migration plan");
|
|
124
|
+
if (plan.source_registry !== request.registry || plan.namespace_count !== 1)
|
|
125
|
+
fail("migration plan targets another source or namespace set");
|
|
126
|
+
sameBytes(plan.source_registry_hash, sourceRegistryHash, "source Registry executable");
|
|
127
|
+
u64(plan.ownership_sequence, "ownership sequence");
|
|
128
|
+
for (const key of ["primary_source", "primary_target"])
|
|
129
|
+
address(plan[key], "contract", key);
|
|
130
|
+
for (const key of ["primary_source_hash", "primary_target_hash", "primary_root"])
|
|
131
|
+
bytes32(plan[key], key);
|
|
132
|
+
u32(plan.primary_count, "primary count");
|
|
133
|
+
listOfOne(state.imported, request.namespace, "imported namespaces");
|
|
134
|
+
listOfOne(state.completed, request.namespace, "completed namespaces");
|
|
135
|
+
const leaf = exactObject(leafRaw, LEAF_FIELDS, "migration namespace");
|
|
136
|
+
const epoch = u64(leaf.owner_epoch, "migration owner epoch");
|
|
137
|
+
if (epoch === 0n || epoch > plan.ownership_sequence)
|
|
138
|
+
fail("migration ownership sequence is inconsistent");
|
|
139
|
+
if (leaf.source_registrar !== request.registrar || !(leaf.label instanceof Uint8Array) || hex(leaf.label) !== hex(utf8("nova")))
|
|
140
|
+
fail("namespace commitment differs from the saved source contracts");
|
|
141
|
+
// Historical Resolver/policy/owner settings need not equal current migration
|
|
142
|
+
// settings: the complete original receipt hash binds their original values.
|
|
143
|
+
if (sourceHash !== REVIEWED_SOURCE_HASH)
|
|
144
|
+
fail("source Registrar is not the reviewed frozen export implementation");
|
|
145
|
+
sameBytes(leaf.source_registrar_hash, sourceHash, "source Registrar executable");
|
|
146
|
+
sameBytes(plan.namespace_root, namespaceLeafHash(network, request.registry, target, leafScVal(leaf)), "namespace Merkle root");
|
|
147
|
+
if (frozen !== true || exportVersion !== 1 || exportTarget !== target || attested !== request.registrar || !Array.isArray(anchors) || anchors.length !== 2 || anchors[0] !== request.registry)
|
|
148
|
+
fail("source Registrar is not irreversibly frozen for this exact successor");
|
|
149
|
+
sameBytes(anchors[1], request.namespace, "source namespace anchor");
|
|
150
|
+
if (snapshot.value === null)
|
|
151
|
+
return null;
|
|
152
|
+
const receipt = claimReceiptFromNative(snapshot.value);
|
|
153
|
+
historicalReadLedger(receipt.ledger);
|
|
154
|
+
const name = new Uint8Array(64);
|
|
155
|
+
name.set(unhex(request.namespace));
|
|
156
|
+
name.set(hash(utf8(intent.label)), 32);
|
|
157
|
+
const generation = intent.expectedGeneration === null ? 0n : intent.expectedGeneration + 1n;
|
|
158
|
+
const expiry = intent.termSecs === 0n ? 0n : receipt.timestamp + intent.termSecs;
|
|
159
|
+
if (receipt.operation !== "claim" || receipt.intentHash !== nativeIntentHash("claim", encoded) || receipt.holder !== intent.claimant || receipt.node !== hex(hash(name)) || receipt.generation !== generation || receipt.expiresAt !== expiry || receipt.feeToken !== intent.feeToken || receipt.feeAmount !== intent.feeAmount || receipt.feeRecipient !== (intent.feeAmount > 0n ? intent.feeRecipient : null) || receipt.ledger > observed || receipt.timestamp < request.validAfter || receipt.timestamp > request.deadline)
|
|
160
|
+
fail("source receipt differs from the complete original immutable claim intent");
|
|
161
|
+
return receipt;
|
|
162
|
+
}
|
package/dist/native-transport.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { expectedRegistrarCode } from "./native-code-policy.js";
|
|
1
2
|
import { Contract, Operation, Transaction, TransactionBuilder, rpc, scValToNative, xdr } from "@stellar/stellar-sdk";
|
|
2
|
-
import { NativeClaimError, address,
|
|
3
|
+
import { NativeClaimError, address, hex, namespaceNode, sc, unhex } from "./native-codec.js";
|
|
3
4
|
import { assertSignedBodyUnchanged, validateEligibilityAuthorization, validateNativeTransaction } from "./native-auth.js";
|
|
4
5
|
/** This published legacy code has owner-only issuance. Missing RPC data is never legacy detection. */
|
|
5
6
|
const LEGACY_REGISTRAR_HASH = "ed06b3374ff4342b4a2316fc546505132cd8fd35be6666571cf088710af9bbc6";
|
|
@@ -8,12 +9,12 @@ export async function verifyRegistrarProvenance(context, registrar, namespace) {
|
|
|
8
9
|
const node = sc.bytes(unhex(namespace));
|
|
9
10
|
const [attested, tainted, templates, instance] = await Promise.all([
|
|
10
11
|
context.read(context.registryId, "registrar_of", [node]), context.read(context.registryId, "registrar_tainted", [node]),
|
|
11
|
-
|
|
12
|
+
expectedRegistrarCode(context, namespace), context.server.getContractInstance(registrar),
|
|
12
13
|
]);
|
|
13
|
-
if (attested !== registrar || tainted !== false ||
|
|
14
|
+
if (attested !== registrar || tainted !== false || instance.executable.type !== "contractExecutableWasm")
|
|
14
15
|
throw new NativeClaimError("Registrar provenance is unavailable or tainted; refusing authoritative claim/receipt reads", "unavailable");
|
|
15
|
-
if (
|
|
16
|
-
throw new NativeClaimError("Registrar executable differs from
|
|
16
|
+
if (templates !== hex(instance.executable.wasmHash.value))
|
|
17
|
+
throw new NativeClaimError("Registrar executable differs from Registry-approved code", "unavailable");
|
|
17
18
|
}
|
|
18
19
|
export async function nativeCapability(context, namespace) {
|
|
19
20
|
const node = namespaceNode(namespace), nodeArg = sc.bytes(node);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sorandomains/owner",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Run your Soran namespace on Stellar
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Run your Soran namespace on Stellar: configure claims, issue names and manage namespace policies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|