@sorandomains/owner 0.10.1 → 0.12.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 +68 -3
- package/dist/funding-history.js +3 -3
- package/dist/funding-reader.js +4 -0
- package/dist/funding-service.js +14 -2
- package/dist/funding-types.d.ts +2 -0
- package/dist/funding-types.js +30 -0
- package/dist/index.d.ts +82 -2
- package/dist/index.js +131 -18
- package/dist/multichain.d.ts +99 -0
- package/dist/multichain.js +204 -0
- package/dist/names.d.ts +29 -0
- package/dist/names.js +83 -0
- package/dist/native-code-policy.d.ts +7 -0
- package/dist/native-code-policy.js +7 -6
- package/dist/native-codec.js +1 -1
- package/dist/native-transport.d.ts +2 -0
- package/dist/native-transport.js +18 -1
- package/dist/native-upgrades.d.ts +48 -0
- package/dist/native-upgrades.js +93 -0
- package/dist/subnames-client.d.ts +49 -0
- package/dist/subnames-client.js +63 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
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.12.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.10.0.
|
|
11
|
+
Lookup is the read entry point in `@sorandomains/lookup` 0.10.0 or later.
|
|
12
|
+
|
|
13
|
+
Releases older than 0.11.0 (and any built against Lookup 0.6.0 to 0.9.0) default to the retired 5 and 6 September 2026 testnet stacks; upgrade rather than pinning a retired Registry. See [Older versions and retired stacks](../../README.md#older-versions-and-retired-stacks).
|
|
12
14
|
|
|
13
15
|
Ownership, issuer, holder and treasury inputs remain G/C account or contract
|
|
14
16
|
addresses. Muxed M addresses are payment destinations only: issue the name to its
|
|
@@ -36,6 +38,30 @@ const batch = await owner.issueBatch("acme", rows);
|
|
|
36
38
|
for (const o of batch.outcomes) if (!o.issued) console.warn(o.label, o.reason);
|
|
37
39
|
```
|
|
38
40
|
|
|
41
|
+
## Network address policy (unreleased native extension)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
await owner.setChainPolicy("solo", ["bitcoin", "ethereum", "base", "xrp"]);
|
|
45
|
+
const enabled = await owner.chainPolicy("solo");
|
|
46
|
+
await owner.setChainPolicy("solo", []); // disable all generic network records
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The current namespace owner sets one live policy for every existing and future
|
|
50
|
+
holder. The default policy is empty. Disabled records are hidden and cannot be
|
|
51
|
+
updated, but remain stored for re-enablement or removal by their holder. Duplicate
|
|
52
|
+
or unknown IDs are rejected. Each write freshly verifies the native route/version
|
|
53
|
+
and current namespace owner before signing the exact policy transaction. The owner
|
|
54
|
+
chooses available networks; holders choose their own destinations.
|
|
55
|
+
|
|
56
|
+
All methods require `multichain_version() == 1`; the existing published deployment
|
|
57
|
+
presets are not upgraded by installing this SDK. The source change does not deploy
|
|
58
|
+
contracts or publish packages. See [network formats and integration rules](../multichain/README.md)
|
|
59
|
+
for all 14 supported mainnets, binary formats, and shared codec exports. XRP Ledger
|
|
60
|
+
`xrp` preserves full mainnet X-addresses and their destination tags; `xrpl-evm` is a
|
|
61
|
+
separate EVM network. Never strip an X-address tag. For Stellar use the dedicated
|
|
62
|
+
payment API.
|
|
63
|
+
|
|
64
|
+
|
|
39
65
|
## What's in the box
|
|
40
66
|
|
|
41
67
|
| Operation | What it does |
|
|
@@ -91,7 +117,7 @@ Read the [native claim APIs, security boundaries and complete signup flow](https
|
|
|
91
117
|
|
|
92
118
|
## Verified testnet deployment
|
|
93
119
|
|
|
94
|
-
See the [deployment manifest](../../
|
|
120
|
+
See the [deployment manifest](../../deployments/testnet.json) for confirmed code hashes, transaction receipts and verification scope. Network passphrase: `Test SDF Network ; September 2015`.
|
|
95
121
|
|
|
96
122
|
| Contract | Address |
|
|
97
123
|
|---|---|
|
|
@@ -121,3 +147,42 @@ The older `maxNativeFeeStroops` option remains supported: when the new option is
|
|
|
121
147
|
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
148
|
|
|
123
149
|
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.
|
|
150
|
+
|
|
151
|
+
## Namespace contract updates
|
|
152
|
+
|
|
153
|
+
The current namespace owner can update each governed child contract separately:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const status = await client.contractUpgradeStatus("acme", "resolver", approvedResolverHash);
|
|
157
|
+
// status: { owner, contractId, currentHash, approved, governed }
|
|
158
|
+
const direction = contractUpgradeDirection("resolver", status.currentHash, approvedResolverHash);
|
|
159
|
+
if (status.approved && status.governed && (direction === "forward" || direction === "unreviewed-target")) {
|
|
160
|
+
await client.upgradeResolver("acme", approvedResolverHash, {
|
|
161
|
+
onPrepared: ({ hash, feeStroops }) => savePendingUpdate({ hash, feeStroops }),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
await client.upgradeRegistrar("acme", approvedRegistrarHash);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Registry approval says a hash may be installed on any namespace; it does not say the move is safe. Governance keeps superseded releases approved, so the chain itself accepts a downgrade (and a reinstall of the installed hash). The SDK therefore refuses, before preparing or signing anything: a reinstall of the installed hash, always; a **superseded** release (`CONTRACT_RELEASES[role].superseded`, a downgrade that drops later fixes); and the latest known release over installed code the SDK cannot place (it may be newer). `contractUpgradeDirection(role, currentHash, targetHash)` returns `"forward"`, `"same"`, `"backward"`, `"unrecognized-current"` or `"unreviewed-target"` (a release newer than this SDK, whose direction it cannot judge: only a reviewed hash from a trusted release record should reach it). A deliberate rollback passes `{ allowDowngrade: true }` to `upgradeRegistrar`/`upgradeResolver` after reviewing the older release for that namespace; it never overrides a reinstall. Keep an application-side allow-list of reviewed `from -> to` pairs, as the Soran console does; `CONTRACT_RELEASES` is a tested mirror of that catalog and lags any release newer than the SDK.
|
|
168
|
+
|
|
169
|
+
Use exact lowercase 32-byte hex hashes from a reviewed release. Registry governance must approve the implementation for its Registrar or Resolver role first. Approval permits an update; it does not upgrade existing namespace contracts. Each owner-signed transaction changes code at the existing contract address, executes immediately, and pays its own simulated network fee. Storage compatibility is a property of the selected release, not a guarantee for every approved implementation.
|
|
170
|
+
|
|
171
|
+
`contractUpgradeStatus` is read-only and checks both actual executables against the Registry's per-namespace code pins, clean attestations, Registrar anchors, and Resolver provenance/authority. It does not depend on claim configuration, claim-interface version or fee-token settings. A verified unsupported policy or revoked target is reported through `governed`/`approved`; missing or inconsistent evidence throws. This review is a snapshot: the write rechecks authorization and simulation before signing, and the contracts enforce current approval and ownership on chain.
|
|
172
|
+
|
|
173
|
+
Both update methods require the current owner's account as the transaction source and use the native transaction's exact operation, authorization-tree and fee checks. They never fall back to the legacy tainting upgrade path or restore archived storage automatically. Active migrations and frozen exports must be resolved before updating. If submission is interrupted, reconcile the error's original `txHash` before retrying. Signature quorum is validated by the network; contract-wallet owner authorization is not supported by these account-source helpers.
|
|
174
|
+
|
|
175
|
+
### Namespace-wide child-name policy
|
|
176
|
+
|
|
177
|
+
On a Registrar with `subname_version() == 1`, the current namespace owner can
|
|
178
|
+
call `setSubnamePolicy(namespace, policy)` with `enabled`, `creation-disabled`
|
|
179
|
+
or `suspended`. The default is `creation-disabled`. Changes apply to all parent
|
|
180
|
+
holders, including usernames claimed before the change. `creation-disabled`
|
|
181
|
+
blocks new children while existing ones still work; `suspended` blocks child
|
|
182
|
+
resolution and record edits, preserves records, and permits their removal.
|
|
183
|
+
|
|
184
|
+
`subnamePolicy`, `subnameRecord` and `subnames(parent, { offset, limit })` provide
|
|
185
|
+
strict on-chain reads. Raw child records and pages include inactive/stale rows;
|
|
186
|
+
they do not establish payment readiness. The parent holder, not a separate
|
|
187
|
+
child wallet, controls `mail.fred.solo`, and its expiry and ownership generation
|
|
188
|
+
are tied to `fred.solo`. Child destinations may differ from the parent holder.
|
package/dist/funding-history.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Address, FeeBumpTransaction, Networks, Transaction, TransactionBuilder, xdr, hash, } from "@stellar/stellar-sdk";
|
|
2
2
|
import { hex } from "./native-codec.js";
|
|
3
|
-
import { FundingError, sponsorQuoteToScVal, } from "./funding-types.js";
|
|
3
|
+
import { FundingError, readBoundedText, sponsorQuoteToScVal, } from "./funding-types.js";
|
|
4
4
|
/** Input must come from the caller's trusted chain provider, never a pasted/API receipt. */
|
|
5
5
|
export function verifySponsorHistory(value, quote, expectedHash, latestLedger, passphrase, provider = "sdf-testnet-horizon") {
|
|
6
6
|
const r = value;
|
|
@@ -116,8 +116,8 @@ export class TestnetSponsorHistory {
|
|
|
116
116
|
return null;
|
|
117
117
|
if (!response.ok)
|
|
118
118
|
throw new FundingError("Sponsorship history unavailable; preserve the quote reference", "unavailable");
|
|
119
|
-
const body = await response
|
|
120
|
-
if (body
|
|
119
|
+
const body = await readBoundedText(response, 600000);
|
|
120
|
+
if (body === null)
|
|
121
121
|
throw new FundingError("Sponsorship history response is too large", "unavailable");
|
|
122
122
|
return verifySponsorHistory(JSON.parse(body), quote, hash, latestLedger, passphrase);
|
|
123
123
|
}
|
package/dist/funding-reader.js
CHANGED
|
@@ -32,6 +32,10 @@ export class FundingReader {
|
|
|
32
32
|
.setTimeout(30)
|
|
33
33
|
.build();
|
|
34
34
|
const result = await this.server.simulateTransaction(tx, undefined, undefined, false);
|
|
35
|
+
// A simulation that needs a restore preamble still reports success, with the
|
|
36
|
+
// value computed as if the archived entry were live; it is not a live read.
|
|
37
|
+
if (rpc.Api.isSimulationRestore(result))
|
|
38
|
+
throw new FundingError("Cannot verify funding contract state: an archived entry needs restoring", "unavailable");
|
|
35
39
|
if (!rpc.Api.isSimulationSuccess(result) ||
|
|
36
40
|
!result.result ||
|
|
37
41
|
!Number.isSafeInteger(result.latestLedger) ||
|
package/dist/funding-service.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { TestnetSponsorHistory, sponsorRpcReceipt, } from "./funding-history.js";
|
|
2
2
|
import { claimIntentToScVal } from "./native-types.js";
|
|
3
|
-
import { FundingError, decodeSponsorQuote, sponsoredIdentityToScVal, sponsorIntentHash, } from "./funding-types.js";
|
|
3
|
+
import { FundingError, decodeSponsorQuote, readBoundedText, sponsoredIdentityToScVal, sponsorIntentHash, } from "./funding-types.js";
|
|
4
|
+
/** An offer carries at most a 100000 character transaction plus the quote XDR; nothing legitimate approaches this. */
|
|
5
|
+
const MAX_SERVICE_BODY_BYTES = 262_144;
|
|
4
6
|
/** HTTP transports offers and status. The contract/RPC checks still authorize spending. */
|
|
5
7
|
export class FundingServiceClient {
|
|
6
8
|
client;
|
|
@@ -39,8 +41,18 @@ export class FundingServiceClient {
|
|
|
39
41
|
throw new FundingError("Service response unavailable. Keep your quote reference and recover its status before submitting again.", "unavailable");
|
|
40
42
|
}
|
|
41
43
|
let value;
|
|
44
|
+
let text;
|
|
42
45
|
try {
|
|
43
|
-
|
|
46
|
+
text = await readBoundedText(r, MAX_SERVICE_BODY_BYTES);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
text = "";
|
|
50
|
+
}
|
|
51
|
+
// Too large is handled like an unreadable body: a submit's outcome is unknown.
|
|
52
|
+
if (text === null)
|
|
53
|
+
throw new FundingError("Service response was too large. Keep your quote reference and recover its status before submitting again.", path === "/v1/sponsorship/submit" ? "pending" : "unavailable");
|
|
54
|
+
try {
|
|
55
|
+
value = JSON.parse(text);
|
|
44
56
|
}
|
|
45
57
|
catch {
|
|
46
58
|
// A proxy/gateway error page (e.g. a 502 HTML body) is not JSON. A
|
package/dist/funding-types.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { xdr } from "@stellar/stellar-sdk";
|
|
2
2
|
import { type PaymentDestination } from "./payment.js";
|
|
3
3
|
import { type ClaimIntent } from "./native-types.js";
|
|
4
|
+
/** Read a response body as text, refusing more than `limit` bytes, declared or streamed, so a hostile or broken endpoint cannot make the client buffer it all. Returns null over the limit. */
|
|
5
|
+
export declare function readBoundedText(response: Response, limit: number): Promise<string | null>;
|
|
4
6
|
export declare class FundingError extends Error {
|
|
5
7
|
readonly kind: "invalid" | "unavailable" | "authorization" | "pending" | "failed";
|
|
6
8
|
readonly transactionHash: string | null;
|
package/dist/funding-types.js
CHANGED
|
@@ -2,6 +2,36 @@ import { hash, nativeToScVal, scValToNative, xdr } from "@stellar/stellar-sdk";
|
|
|
2
2
|
import { address, amount, bool, bytes32, exactObject, hex, hex32, label, paymentDestinationToScVal, sc, struct, u32, u64, unhex, } from "./native-codec.js";
|
|
3
3
|
import { destinationFromNative } from "./payment.js";
|
|
4
4
|
import { claimIntentToScVal } from "./native-types.js";
|
|
5
|
+
/** Read a response body as text, refusing more than `limit` bytes, declared or streamed, so a hostile or broken endpoint cannot make the client buffer it all. Returns null over the limit. */
|
|
6
|
+
export async function readBoundedText(response, limit) {
|
|
7
|
+
if (Number(response.headers.get("content-length") ?? 0) > limit) {
|
|
8
|
+
await response.body?.cancel().catch(() => undefined);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const reader = response.body?.getReader();
|
|
12
|
+
if (!reader)
|
|
13
|
+
return "";
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let total = 0;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const { done, value } = await reader.read();
|
|
18
|
+
if (done)
|
|
19
|
+
break;
|
|
20
|
+
total += value.byteLength;
|
|
21
|
+
if (total > limit) {
|
|
22
|
+
await reader.cancel().catch(() => undefined);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
chunks.push(value);
|
|
26
|
+
}
|
|
27
|
+
const bytes = new Uint8Array(total);
|
|
28
|
+
let offset = 0;
|
|
29
|
+
for (const chunk of chunks) {
|
|
30
|
+
bytes.set(chunk, offset);
|
|
31
|
+
offset += chunk.byteLength;
|
|
32
|
+
}
|
|
33
|
+
return new TextDecoder().decode(bytes);
|
|
34
|
+
}
|
|
5
35
|
export class FundingError extends Error {
|
|
6
36
|
kind;
|
|
7
37
|
transactionHash;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
|
+
import { type SubnamePolicy, type SubnameRecord } from "./names.js";
|
|
2
|
+
export { type SubnamePolicy, type SubnameRecord } from "./names.js";
|
|
1
3
|
import { type HistoricalRecoveryOptions } from "./native-history.js";
|
|
2
4
|
import type { ClaimIntent } from "./native-types.js";
|
|
3
5
|
export type { HistoricalRecoveryOptions } from "./native-history.js";
|
|
6
|
+
/**
|
|
7
|
+
* @sorandomains/owner — the write-side SDK for Soran namespace owners.
|
|
8
|
+
*
|
|
9
|
+
* Everything a namespace owner does after winning a namespace, programmatically:
|
|
10
|
+
*
|
|
11
|
+
* import { SoranOwner, keypairSigner } from "@sorandomains/owner";
|
|
12
|
+
*
|
|
13
|
+
* const owner = new SoranOwner({ signer: keypairSigner(process.env.OWNER_SECRET!) });
|
|
14
|
+
* await owner.issue("acme", "alice", "GDHN…"); // alice.acme now resolves
|
|
15
|
+
* await owner.issueBatch("acme", csvRows); // up to 23 per transaction
|
|
16
|
+
*
|
|
17
|
+
* TRUST MODEL. This SDK talks to the contracts directly over any Soroban RPC
|
|
18
|
+
* node — no Soran account, no hosted API, no third party in the path. Every
|
|
19
|
+
* operation here is authorized on chain: the Registrar and Registry check
|
|
20
|
+
* `require_auth` against the namespace owner's address, so the only credential
|
|
21
|
+
* that matters is the signer you supply. Soran's servers cannot perform any of
|
|
22
|
+
* these operations for you or against you.
|
|
23
|
+
*
|
|
24
|
+
* WHO CAN CALL WHAT. All operations below are namespace-OWNER powers except
|
|
25
|
+
* `acceptNamespaceTransfer`, which the proposed NEW owner signs. Name-holder
|
|
26
|
+
* powers (transferring an individual name, pointing it at a new address,
|
|
27
|
+
* electing a primary name) are the holder's alone — they live in the contracts'
|
|
28
|
+
* holder-authorized entry points and live in `@sorandomains/holder`; this
|
|
29
|
+
* package deliberately cannot exercise them.
|
|
30
|
+
*
|
|
31
|
+
* SIGNING. Supply a `TxSigner`: `keypairSigner(secret)` for backends, or wrap a
|
|
32
|
+
* browser wallet (Freighter / Stellar Wallets Kit expose exactly this
|
|
33
|
+
* `signTransaction(xdr, { networkPassphrase })` shape). The SDK builds and
|
|
34
|
+
* simulates each transaction, hands you the envelope to sign, and submits it.
|
|
35
|
+
* Operations on one SoranOwner instance are serialized so concurrent calls
|
|
36
|
+
* cannot race the account sequence number.
|
|
37
|
+
*
|
|
38
|
+
* READS. For resolution, reverse lookup, and assurance checks use the read-only
|
|
39
|
+
* companion package `@sorandomains/lookup` — wallets and apps should depend on
|
|
40
|
+
* that one only. The few reads here (policy, pending transfers, records) exist
|
|
41
|
+
* to support write flows.
|
|
42
|
+
*/
|
|
43
|
+
import { type ChainNetwork } from "./multichain.js";
|
|
44
|
+
export { CHAIN_NETWORKS, ChainAddressError, chainNetwork, chainPolicyFromNative, chainPolicyToNative, encodeChainAddress, decodeChainAddress, type ChainNetwork, type ChainAddressErrorCode } from "./multichain.js";
|
|
4
45
|
import { type NativeWriteOptions, type NativeCapability } from "./native-transport.js";
|
|
46
|
+
import { type ContractUpgradeOptions, type ContractUpgradeRole, type ContractUpgradeStatus } from "./native-upgrades.js";
|
|
47
|
+
export { CONTRACT_RELEASES, contractUpgradeDirection, type ContractUpgradeDirection, type ContractUpgradeOptions, type ContractUpgradeRole, type ContractUpgradeStatus } from "./native-upgrades.js";
|
|
5
48
|
import { type ClaimSettings, type ClaimConfig, type ClaimUsage, type ApprovalUsage } from "./native-types.js";
|
|
6
49
|
export * from "./native-types.js";
|
|
7
50
|
export * from "./native-allowlist.js";
|
|
@@ -160,8 +203,13 @@ export declare class SoranOwner {
|
|
|
160
203
|
private nativeOwnerWrite;
|
|
161
204
|
/** Current implementation after validating the Registry's native binding. */
|
|
162
205
|
registrarCodeHash(namespace: string): Promise<string>;
|
|
163
|
-
/**
|
|
164
|
-
|
|
206
|
+
/** Verify both children and the selected implementation without signing or reading claim settings. */
|
|
207
|
+
contractUpgradeStatus(namespace: string, role: ContractUpgradeRole, wasmHash: string): Promise<ContractUpgradeStatus>;
|
|
208
|
+
private upgradeContract;
|
|
209
|
+
/** Current-owner, Registry-approved update; never uses the legacy tainting path. */
|
|
210
|
+
upgradeRegistrar(namespace: string, wasmHash: string, options?: ContractUpgradeOptions): Promise<Submitted>;
|
|
211
|
+
/** Current-owner, Registry-approved Resolver update with exact native authorization. */
|
|
212
|
+
upgradeResolver(namespace: string, wasmHash: string, options?: ContractUpgradeOptions): Promise<Submitted>;
|
|
165
213
|
configureClaims(namespace: string, settings: ClaimSettings, options?: NativeWriteOptions): Promise<Submitted & {
|
|
166
214
|
config: ClaimConfig;
|
|
167
215
|
}>;
|
|
@@ -246,6 +294,12 @@ export declare class SoranOwner {
|
|
|
246
294
|
* Refused (ResolverFrozen) once the namespace is permanent.
|
|
247
295
|
*/
|
|
248
296
|
setResolver(namespace: string, resolver: string | null): Promise<Submitted>;
|
|
297
|
+
/** Networks currently enabled for all holders, including names already claimed. */
|
|
298
|
+
chainPolicy(namespace: string): Promise<ChainNetwork[]>;
|
|
299
|
+
/** Set the namespace-wide network allowlist. Empty disables all multichain records.
|
|
300
|
+
* Disabled destinations remain stored and become visible if re-enabled. */
|
|
301
|
+
setChainPolicy(namespace: string, networks: readonly ChainNetwork[]): Promise<Submitted>;
|
|
302
|
+
private multichainResolverOf;
|
|
249
303
|
/** The namespace's immutable issuance policy. */
|
|
250
304
|
policy(namespace: string): Promise<NamespacePolicy>;
|
|
251
305
|
/** Has the namespace passed the one-way door? */
|
|
@@ -254,6 +308,30 @@ export declare class SoranOwner {
|
|
|
254
308
|
nameState(namespace: string, label: string): Promise<NameState | null>;
|
|
255
309
|
/** The pending namespace transfer on the Registry, or null. */
|
|
256
310
|
pendingNamespaceTransfer(namespace: string): Promise<unknown | null>;
|
|
311
|
+
private subnameReader;
|
|
312
|
+
/** Live namespace policy. Creation-disabled keeps existing children usable; suspended hides them. */
|
|
313
|
+
subnamePolicy(namespace: string): Promise<SubnamePolicy>;
|
|
314
|
+
/** Raw child state, including tombstones and stale parent generations; this is not payment resolution. */
|
|
315
|
+
subnameRecord(name: string): Promise<SubnameRecord | null>;
|
|
316
|
+
/** Bounded on-chain historical labels, including removed children. Re-read metadata before treating a child as active. */
|
|
317
|
+
subnames(parent: string, options?: {
|
|
318
|
+
offset?: number;
|
|
319
|
+
limit?: number;
|
|
320
|
+
}): Promise<{
|
|
321
|
+
parent: string;
|
|
322
|
+
policy: SubnamePolicy;
|
|
323
|
+
records: {
|
|
324
|
+
parentNode: Uint8Array;
|
|
325
|
+
parentGeneration: bigint;
|
|
326
|
+
generation: bigint;
|
|
327
|
+
address: string;
|
|
328
|
+
active: boolean;
|
|
329
|
+
name: string;
|
|
330
|
+
}[];
|
|
331
|
+
offset: number;
|
|
332
|
+
nextOffset: number | null;
|
|
333
|
+
}>;
|
|
334
|
+
setSubnamePolicy(namespace: string, policy: SubnamePolicy): Promise<Submitted>;
|
|
257
335
|
private readRecord;
|
|
258
336
|
private readRecords;
|
|
259
337
|
/** Simulation-only contract read — no signature, no fee, no state change. */
|
|
@@ -274,6 +352,8 @@ export declare class SoranOwner {
|
|
|
274
352
|
* auth demands an address the envelope signature cannot satisfy.
|
|
275
353
|
*/
|
|
276
354
|
private assertSatisfiableAuth;
|
|
355
|
+
/** Authorize exactly the selected policy with no additional child operations. */
|
|
356
|
+
private assertChainPolicyIntent;
|
|
277
357
|
private attempt;
|
|
278
358
|
private restore;
|
|
279
359
|
private confirm;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { subnamePolicyToNative } from "./names.js";
|
|
2
|
+
import { createSubnameReader, SUBNAME_REGISTRAR_ERRORS } from "./subnames-client.js";
|
|
1
3
|
import { recoverFrozenClaim, historicalReadLedger } from "./native-history.js";
|
|
2
4
|
/**
|
|
3
5
|
* @sorandomains/owner — the write-side SDK for Soran namespace owners.
|
|
@@ -36,6 +38,8 @@ import { recoverFrozenClaim, historicalReadLedger } from "./native-history.js";
|
|
|
36
38
|
* that one only. The few reads here (policy, pending transfers, records) exist
|
|
37
39
|
* to support write flows.
|
|
38
40
|
*/
|
|
41
|
+
import { chainPolicyFromNative, chainPolicyToNative } from "./multichain.js";
|
|
42
|
+
export { CHAIN_NETWORKS, ChainAddressError, chainNetwork, chainPolicyFromNative, chainPolicyToNative, encodeChainAddress, decodeChainAddress } from "./multichain.js";
|
|
39
43
|
import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, SorobanDataBuilder, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, xdr, } from "@stellar/stellar-sdk";
|
|
40
44
|
/** Contract events from a transaction's meta, across meta versions (v3 puts
|
|
41
45
|
* them on sorobanMeta; v4 nests them per-operation and in tx events). */
|
|
@@ -87,6 +91,8 @@ function decodeIssuedEvents(events, registrarId) {
|
|
|
87
91
|
}
|
|
88
92
|
import { NativeClaimError, address as nativeAddress, bool as nativeBool, bytes32 as nativeBytes32, hex as nativeHex, sc as nativeSc } from "./native-codec.js";
|
|
89
93
|
import { requireNative, nativeCapability, sendNative } from "./native-transport.js";
|
|
94
|
+
import { contractUpgradeStatus as readContractUpgradeStatus, contractUpgradeDirection, upgradeHash } from "./native-upgrades.js";
|
|
95
|
+
export { CONTRACT_RELEASES, contractUpgradeDirection } from "./native-upgrades.js";
|
|
90
96
|
import { claimSettingsToScVal, claimConfigFromNative, claimLabelToScVal, claimUsageFromNative, approvalUsageFromNative } from "./native-types.js";
|
|
91
97
|
export * from "./native-types.js";
|
|
92
98
|
export * from "./native-allowlist.js";
|
|
@@ -183,6 +189,15 @@ const REGISTRAR_ERRORS = {
|
|
|
183
189
|
49: "MigrationUnavailable",
|
|
184
190
|
50: "MigrationUnsupported",
|
|
185
191
|
51: "PermanentLockDisabled",
|
|
192
|
+
52: "InvalidRenewalGrace",
|
|
193
|
+
53: "SubnameCreationDisabled", 54: "SubnamesSuspended",
|
|
194
|
+
55: "SubnameGenerationMismatch", 56: "SubnameIndexUnavailable",
|
|
195
|
+
};
|
|
196
|
+
/** Error names shared by native multichain Resolver reads and writes. */
|
|
197
|
+
const CHAIN_RESOLVER_ERRORS = {
|
|
198
|
+
2: "NotInitialized", 7: "InvalidAuthority", 33: "UnsupportedCoinType",
|
|
199
|
+
34: "ChainDisabled", 35: "InvalidChainAddress", 36: "InvalidChainPolicy",
|
|
200
|
+
37: "ChainContextMismatch", 38: "ChainUnavailable",
|
|
186
201
|
};
|
|
187
202
|
/** Registry contract error codes, by number. */
|
|
188
203
|
const REGISTRY_ERRORS = {
|
|
@@ -227,6 +242,7 @@ const REGISTRY_ERRORS = {
|
|
|
227
242
|
39: "UpgradePolicyMismatch",
|
|
228
243
|
40: "UpgradeNotApproved",
|
|
229
244
|
41: "PermanentLockDisabled",
|
|
245
|
+
42: "BatchTooLarge",
|
|
230
246
|
};
|
|
231
247
|
/** Errors for a submit the network REJECTED as a stale sequence: nothing was included, so one retry with a fresh sequence is safe. */
|
|
232
248
|
const BAD_SEQ_REJECTED = new WeakSet();
|
|
@@ -428,23 +444,46 @@ export class SoranOwner {
|
|
|
428
444
|
throw new NativeClaimError("Registrar is not a Wasm contract", "unavailable");
|
|
429
445
|
return nativeHex(instance.executable.wasmHash.value);
|
|
430
446
|
}
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
this.
|
|
439
|
-
|
|
440
|
-
this.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
447
|
+
/** Verify both children and the selected implementation without signing or reading claim settings. */
|
|
448
|
+
contractUpgradeStatus(namespace, role, wasmHash) {
|
|
449
|
+
return readContractUpgradeStatus(this.nativeContext(), normalizeLabel(namespace), role, wasmHash);
|
|
450
|
+
}
|
|
451
|
+
upgradeContract(namespace, role, wasmHash, options) {
|
|
452
|
+
return this.serialize(async () => {
|
|
453
|
+
const args = [upgradeHash(wasmHash)];
|
|
454
|
+
const context = this.nativeContext();
|
|
455
|
+
const status = await readContractUpgradeStatus(context, normalizeLabel(namespace), role, wasmHash);
|
|
456
|
+
const source = nativeAddress(await this.signer.publicKey(), "account", "owner transaction source");
|
|
457
|
+
if (source !== status.owner)
|
|
458
|
+
throw new NativeClaimError("the current namespace owner must authorize this change", "authorization");
|
|
459
|
+
if (!status.governed || !status.approved)
|
|
460
|
+
throw new NativeClaimError("This contract upgrade is not approved by the Registry", "authorization");
|
|
461
|
+
const direction = contractUpgradeDirection(role, status.currentHash, wasmHash);
|
|
462
|
+
if (direction === "same")
|
|
463
|
+
throw new NativeClaimError("This contract implementation is already installed");
|
|
464
|
+
// Registry approval says a hash may be installed, not that the move is safe:
|
|
465
|
+
// older releases stay approved, and a downgrade drops their fixes.
|
|
466
|
+
if (options.allowDowngrade !== true) {
|
|
467
|
+
if (direction === "backward")
|
|
468
|
+
throw new NativeClaimError("Refusing to install a superseded implementation (a downgrade that drops later fixes). Install the latest reviewed release, or pass allowDowngrade: true only after reviewing the older release for this namespace.", "authorization");
|
|
469
|
+
if (direction === "unrecognized-current")
|
|
470
|
+
throw new NativeClaimError("This namespace runs an implementation this SDK does not recognise, so installing the latest known release may be a downgrade. Update the SDK, or pass allowDowngrade: true after verifying the installed release is older.", "authorization");
|
|
471
|
+
}
|
|
472
|
+
const result = await sendNative(context, {
|
|
473
|
+
source, contract: status.contractId, method: "upgrade", args,
|
|
474
|
+
sourceInvocation: { contract: status.contractId, method: "upgrade", args },
|
|
475
|
+
maxFeeStroops: context.maxFeeStroops,
|
|
476
|
+
}, options);
|
|
477
|
+
return { hash: result.hash, ledger: result.ledger };
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/** Current-owner, Registry-approved update; never uses the legacy tainting path. */
|
|
481
|
+
upgradeRegistrar(namespace, wasmHash, options = {}) {
|
|
482
|
+
return this.upgradeContract(namespace, "registrar", wasmHash, options);
|
|
483
|
+
}
|
|
484
|
+
/** Current-owner, Registry-approved Resolver update with exact native authorization. */
|
|
485
|
+
upgradeResolver(namespace, wasmHash, options = {}) {
|
|
486
|
+
return this.upgradeContract(namespace, "resolver", wasmHash, options);
|
|
448
487
|
}
|
|
449
488
|
async configureClaims(namespace, settings, options = {}) {
|
|
450
489
|
const encoded = claimSettingsToScVal(settings);
|
|
@@ -700,6 +739,44 @@ export class SoranOwner {
|
|
|
700
739
|
const r = await this.invoke(this.registryId, "set_resolver", [nodeArg(namehash(namespace)), resolver === null ? xdr.ScVal.scvVoid() : addrArg(resolver)], REGISTRY_ERRORS);
|
|
701
740
|
return { hash: r.hash, ledger: r.ledger };
|
|
702
741
|
}
|
|
742
|
+
/** Networks currently enabled for all holders, including names already claimed. */
|
|
743
|
+
async chainPolicy(namespace) {
|
|
744
|
+
namespace = normalizeLabel(namespace);
|
|
745
|
+
const resolver = await this.multichainResolverOf(namespace);
|
|
746
|
+
return chainPolicyFromNative(await this.read(resolver, "chain_policy", []));
|
|
747
|
+
}
|
|
748
|
+
/** Set the namespace-wide network allowlist. Empty disables all multichain records.
|
|
749
|
+
* Disabled destinations remain stored and become visible if re-enabled. */
|
|
750
|
+
async setChainPolicy(namespace, networks) {
|
|
751
|
+
namespace = normalizeLabel(namespace);
|
|
752
|
+
const coins = chainPolicyToNative(networks);
|
|
753
|
+
const resolver = await this.multichainResolverOf(namespace);
|
|
754
|
+
await this.assertOwner(namespace);
|
|
755
|
+
const result = await this.invoke(resolver, "set_chain_policy", [
|
|
756
|
+
xdr.ScVal.scvVec(coins.map(coin => nativeToScVal(coin, { type: "u32" }))),
|
|
757
|
+
], CHAIN_RESOLVER_ERRORS);
|
|
758
|
+
return { hash: result.hash, ledger: result.ledger };
|
|
759
|
+
}
|
|
760
|
+
async multichainResolverOf(namespace) {
|
|
761
|
+
const nsNode = namehash(namespace);
|
|
762
|
+
const args = [nodeArg(nsNode)];
|
|
763
|
+
const [resolver, registrar] = await Promise.all([
|
|
764
|
+
this.read(this.registryId, "resolver_of", args), this.read(this.registryId, "registrar_of", args),
|
|
765
|
+
]);
|
|
766
|
+
if (typeof resolver !== "string" || !StrKey.isValidContract(resolver) || typeof registrar !== "string" || !StrKey.isValidContract(registrar))
|
|
767
|
+
throw new OwnerError("namespace has no native multichain route", this.registryId, "resolver_of", null, "UnsupportedImplementation");
|
|
768
|
+
const [anchor, authority, version, anchors] = await Promise.all([
|
|
769
|
+
this.read(resolver, "registry", []), this.read(resolver, "authority", []),
|
|
770
|
+
this.read(resolver, "multichain_version", []), this.read(registrar, "anchors", []),
|
|
771
|
+
]);
|
|
772
|
+
if (anchor !== this.registryId || authority !== registrar || !Array.isArray(anchors) || anchors.length !== 2 ||
|
|
773
|
+
anchors[0] !== this.registryId || !(anchors[1] instanceof Uint8Array) || anchors[1].length !== 32 ||
|
|
774
|
+
!nsNode.every((byte, index) => byte === anchors[1][index]))
|
|
775
|
+
throw new OwnerError("multichain Resolver route does not match this Registry and namespace", resolver, "registry", 37, "ChainContextMismatch");
|
|
776
|
+
if (version !== 1)
|
|
777
|
+
throw new OwnerError("unsupported multichain Resolver version", resolver, "multichain_version", null, "UnsupportedImplementation");
|
|
778
|
+
return resolver;
|
|
779
|
+
}
|
|
703
780
|
// ---- reads that support write flows --------------------------------------
|
|
704
781
|
/** The namespace's immutable issuance policy. */
|
|
705
782
|
async policy(namespace) {
|
|
@@ -730,6 +807,20 @@ export class SoranOwner {
|
|
|
730
807
|
return this.read(this.registryId, "pending_transfer", [nodeArg(namehash(namespace))]);
|
|
731
808
|
}
|
|
732
809
|
// ---- internals -----------------------------------------------------------
|
|
810
|
+
subnameReader() { return createSubnameReader({ registryId: this.registryId, error: message => new OwnerError(message), read: (id, fn, args) => this.read(id, fn, args) }); }
|
|
811
|
+
/** Live namespace policy. Creation-disabled keeps existing children usable; suspended hides them. */
|
|
812
|
+
async subnamePolicy(namespace) { return this.subnameReader().policy(namespace); }
|
|
813
|
+
/** Raw child state, including tombstones and stale parent generations; this is not payment resolution. */
|
|
814
|
+
async subnameRecord(name) { return this.subnameReader().record(name); }
|
|
815
|
+
/** Bounded on-chain historical labels, including removed children. Re-read metadata before treating a child as active. */
|
|
816
|
+
async subnames(parent, options = {}) { return this.subnameReader().list(parent, options); }
|
|
817
|
+
async setSubnamePolicy(namespace, policy) {
|
|
818
|
+
const native = subnamePolicyToNative(policy);
|
|
819
|
+
const ctx = await this.subnameReader().context(namespace);
|
|
820
|
+
await this.assertOwner(ctx.namespace);
|
|
821
|
+
const result = await this.invoke(ctx.registrar, "set_subname_policy", [xdr.ScVal.scvVec([nativeToScVal(native[0], { type: "symbol" })])], { ...REGISTRAR_ERRORS, ...SUBNAME_REGISTRAR_ERRORS });
|
|
822
|
+
return { hash: result.hash, ledger: result.ledger };
|
|
823
|
+
}
|
|
733
824
|
async readRecord(registrarId, label) {
|
|
734
825
|
const rec = (await this.read(registrarId, "record_of", [labelArg(label)]));
|
|
735
826
|
if (!rec)
|
|
@@ -762,7 +853,8 @@ export class SoranOwner {
|
|
|
762
853
|
.build();
|
|
763
854
|
const sim = await this.server.simulateTransaction(tx);
|
|
764
855
|
if (rpc.Api.isSimulationError(sim)) {
|
|
765
|
-
|
|
856
|
+
const errors = fn.startsWith("subname_") ? SUBNAME_REGISTRAR_ERRORS : ["chain_policy", "chain_address", "multichain_version"].includes(fn) ? CHAIN_RESOLVER_ERRORS : {};
|
|
857
|
+
throw typedError(contractId, fn, sim.error, errors);
|
|
766
858
|
}
|
|
767
859
|
// An archived entry is NOT absence: reads cannot restore (no signer),
|
|
768
860
|
// so surface the state honestly instead of reporting "does not exist".
|
|
@@ -838,6 +930,26 @@ export class SoranOwner {
|
|
|
838
930
|
: `${fn}: this operation must be authorized by ${required}, but the signer is ${pub} — use that account's signer`, contractId, fn);
|
|
839
931
|
}
|
|
840
932
|
}
|
|
933
|
+
/** Authorize exactly the selected policy with no additional child operations. */
|
|
934
|
+
assertChainPolicyIntent(prepared, pub, contractId, fn, args) {
|
|
935
|
+
if (!["set_chain_policy", "set_subname_policy"].includes(fn))
|
|
936
|
+
return;
|
|
937
|
+
if (prepared.source !== pub || prepared.operations.length !== 1)
|
|
938
|
+
throw new OwnerError("unexpected chain policy transaction", contractId, fn);
|
|
939
|
+
const op = prepared.operations[0];
|
|
940
|
+
if (op.type !== "invokeHostFunction" || (op.source !== undefined && op.source !== pub) || op.func.type !== "hostFunctionTypeInvokeContract")
|
|
941
|
+
throw new OwnerError("unexpected chain policy operation", contractId, fn);
|
|
942
|
+
const call = op.func.invokeContract;
|
|
943
|
+
if (Address.fromScAddress(call.contractAddress).toString() !== contractId || call.functionName.toString() !== fn ||
|
|
944
|
+
call.args.length !== args.length || call.args.some((value, i) => value.toXDR("base64") !== args[i].toXDR("base64")))
|
|
945
|
+
throw new OwnerError("chain policy operation differs from selected intent", contractId, fn);
|
|
946
|
+
if (!op.auth || op.auth.length !== 1 || op.auth[0].credentials.type !== "sorobanCredentialsSourceAccount")
|
|
947
|
+
throw new OwnerError("chain policy requires exactly the source owner authorization", contractId, fn);
|
|
948
|
+
const root = op.auth[0].rootInvocation;
|
|
949
|
+
if (root.function.type !== "sorobanAuthorizedFunctionTypeContractFn" || root.subInvocations.length ||
|
|
950
|
+
root.function.contractFn.toXDR("base64") !== call.toXDR("base64"))
|
|
951
|
+
throw new OwnerError("chain policy authorization differs from selected intent", contractId, fn);
|
|
952
|
+
}
|
|
841
953
|
async attempt(contractId, fn, args, errNames) {
|
|
842
954
|
const pub = await this.signer.publicKey();
|
|
843
955
|
const contract = new Contract(contractId);
|
|
@@ -868,6 +980,7 @@ export class SoranOwner {
|
|
|
868
980
|
const prepared = rpc.assembleTransaction(tx, sim).build();
|
|
869
981
|
this.assertNetworkFee(prepared.fee, fn);
|
|
870
982
|
this.assertSatisfiableAuth(prepared, pub, contractId, fn);
|
|
983
|
+
this.assertChainPolicyIntent(prepared, pub, contractId, fn, args);
|
|
871
984
|
// The hash is fixed before signatures — compute it now so every failure
|
|
872
985
|
// past this point can carry it (the "re-check before retrying" contract).
|
|
873
986
|
const txHash = toHex(prepared.hash()); // (SDK17) hash() is Uint8Array
|