@sorandomains/owner 0.10.0 → 0.11.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 +21 -0
- package/dist/funding-service.js +10 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +56 -19
- package/dist/native-code-policy.d.ts +7 -0
- package/dist/native-code-policy.js +7 -6
- package/dist/native-upgrades.d.ts +13 -0
- package/dist/native-upgrades.js +54 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -121,3 +121,24 @@ The older `maxNativeFeeStroops` option remains supported: when the new option is
|
|
|
121
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
122
|
|
|
123
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.
|
|
124
|
+
|
|
125
|
+
## Namespace contract updates
|
|
126
|
+
|
|
127
|
+
The current namespace owner can update each governed child contract separately:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const status = await client.contractUpgradeStatus("acme", "resolver", approvedResolverHash);
|
|
131
|
+
// status: { owner, contractId, currentHash, approved, governed }
|
|
132
|
+
if (status.approved && status.governed && status.currentHash !== approvedResolverHash) {
|
|
133
|
+
await client.upgradeResolver("acme", approvedResolverHash, {
|
|
134
|
+
onPrepared: ({ hash, feeStroops }) => savePendingUpdate({ hash, feeStroops }),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
await client.upgradeRegistrar("acme", approvedRegistrarHash);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
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.
|
|
141
|
+
|
|
142
|
+
`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.
|
|
143
|
+
|
|
144
|
+
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.
|
package/dist/funding-service.js
CHANGED
|
@@ -38,7 +38,16 @@ export class FundingServiceClient {
|
|
|
38
38
|
catch {
|
|
39
39
|
throw new FundingError("Service response unavailable. Keep your quote reference and recover its status before submitting again.", "unavailable");
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
let value;
|
|
42
|
+
try {
|
|
43
|
+
value = (await r.json());
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A proxy/gateway error page (e.g. a 502 HTML body) is not JSON. A
|
|
47
|
+
// submit's outcome is then unknown, so it reads as pending; anything
|
|
48
|
+
// else simply could not be answered.
|
|
49
|
+
throw new FundingError("Service response was not valid JSON. Keep your quote reference and recover its status before submitting again.", path === "/v1/sponsorship/submit" ? "pending" : "unavailable");
|
|
50
|
+
}
|
|
42
51
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
43
52
|
throw new FundingError("Invalid service response", "unavailable");
|
|
44
53
|
if (!r.ok || value.error)
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type HistoricalRecoveryOptions } from "./native-history.js";
|
|
|
2
2
|
import type { ClaimIntent } from "./native-types.js";
|
|
3
3
|
export type { HistoricalRecoveryOptions } from "./native-history.js";
|
|
4
4
|
import { type NativeWriteOptions, type NativeCapability } from "./native-transport.js";
|
|
5
|
+
import { type ContractUpgradeRole, type ContractUpgradeStatus } from "./native-upgrades.js";
|
|
6
|
+
export type { ContractUpgradeRole, ContractUpgradeStatus } from "./native-upgrades.js";
|
|
5
7
|
import { type ClaimSettings, type ClaimConfig, type ClaimUsage, type ApprovalUsage } from "./native-types.js";
|
|
6
8
|
export * from "./native-types.js";
|
|
7
9
|
export * from "./native-allowlist.js";
|
|
@@ -160,8 +162,13 @@ export declare class SoranOwner {
|
|
|
160
162
|
private nativeOwnerWrite;
|
|
161
163
|
/** Current implementation after validating the Registry's native binding. */
|
|
162
164
|
registrarCodeHash(namespace: string): Promise<string>;
|
|
163
|
-
/**
|
|
165
|
+
/** Verify both children and the selected implementation without signing or reading claim settings. */
|
|
166
|
+
contractUpgradeStatus(namespace: string, role: ContractUpgradeRole, wasmHash: string): Promise<ContractUpgradeStatus>;
|
|
167
|
+
private upgradeContract;
|
|
168
|
+
/** Current-owner, Registry-approved update; never uses the legacy tainting path. */
|
|
164
169
|
upgradeRegistrar(namespace: string, wasmHash: string, options?: NativeWriteOptions): Promise<Submitted>;
|
|
170
|
+
/** Current-owner, Registry-approved Resolver update with exact native authorization. */
|
|
171
|
+
upgradeResolver(namespace: string, wasmHash: string, options?: NativeWriteOptions): Promise<Submitted>;
|
|
165
172
|
configureClaims(namespace: string, settings: ClaimSettings, options?: NativeWriteOptions): Promise<Submitted & {
|
|
166
173
|
config: ClaimConfig;
|
|
167
174
|
}>;
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,7 @@ function decodeIssuedEvents(events, registrarId) {
|
|
|
87
87
|
}
|
|
88
88
|
import { NativeClaimError, address as nativeAddress, bool as nativeBool, bytes32 as nativeBytes32, hex as nativeHex, sc as nativeSc } from "./native-codec.js";
|
|
89
89
|
import { requireNative, nativeCapability, sendNative } from "./native-transport.js";
|
|
90
|
+
import { contractUpgradeStatus as readContractUpgradeStatus, upgradeHash } from "./native-upgrades.js";
|
|
90
91
|
import { claimSettingsToScVal, claimConfigFromNative, claimLabelToScVal, claimUsageFromNative, approvalUsageFromNative } from "./native-types.js";
|
|
91
92
|
export * from "./native-types.js";
|
|
92
93
|
export * from "./native-allowlist.js";
|
|
@@ -228,6 +229,25 @@ const REGISTRY_ERRORS = {
|
|
|
228
229
|
40: "UpgradeNotApproved",
|
|
229
230
|
41: "PermanentLockDisabled",
|
|
230
231
|
};
|
|
232
|
+
/** Errors for a submit the network REJECTED as a stale sequence: nothing was included, so one retry with a fresh sequence is safe. */
|
|
233
|
+
const BAD_SEQ_REJECTED = new WeakSet();
|
|
234
|
+
function isBadSeqResult(result) {
|
|
235
|
+
try {
|
|
236
|
+
const field = result?.result;
|
|
237
|
+
const inner = (typeof field === "function" ? field.call(result) : field);
|
|
238
|
+
if (inner && typeof inner === "object" && "type" in inner)
|
|
239
|
+
return inner.type === "txBadSeq";
|
|
240
|
+
throw new Error("unrecognised result");
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
try {
|
|
244
|
+
return /txBadSeq|bad_seq/i.test(String(result?.toXDR?.() ?? ""));
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
231
251
|
/**
|
|
232
252
|
* A failed owner operation. When the contract itself rejected the call,
|
|
233
253
|
* `code`/`codeName` carry its typed error (e.g. code 9 / "NotNamespaceOwner"
|
|
@@ -409,23 +429,37 @@ export class SoranOwner {
|
|
|
409
429
|
throw new NativeClaimError("Registrar is not a Wasm contract", "unavailable");
|
|
410
430
|
return nativeHex(instance.executable.wasmHash.value);
|
|
411
431
|
}
|
|
412
|
-
/**
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
this.
|
|
420
|
-
|
|
421
|
-
this.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
432
|
+
/** Verify both children and the selected implementation without signing or reading claim settings. */
|
|
433
|
+
contractUpgradeStatus(namespace, role, wasmHash) {
|
|
434
|
+
return readContractUpgradeStatus(this.nativeContext(), normalizeLabel(namespace), role, wasmHash);
|
|
435
|
+
}
|
|
436
|
+
upgradeContract(namespace, role, wasmHash, options) {
|
|
437
|
+
return this.serialize(async () => {
|
|
438
|
+
const args = [upgradeHash(wasmHash)];
|
|
439
|
+
const context = this.nativeContext();
|
|
440
|
+
const status = await readContractUpgradeStatus(context, normalizeLabel(namespace), role, wasmHash);
|
|
441
|
+
const source = nativeAddress(await this.signer.publicKey(), "account", "owner transaction source");
|
|
442
|
+
if (source !== status.owner)
|
|
443
|
+
throw new NativeClaimError("the current namespace owner must authorize this change", "authorization");
|
|
444
|
+
if (!status.governed || !status.approved)
|
|
445
|
+
throw new NativeClaimError("This contract upgrade is not approved by the Registry", "authorization");
|
|
446
|
+
if (status.currentHash === wasmHash)
|
|
447
|
+
throw new NativeClaimError("This contract implementation is already installed");
|
|
448
|
+
const result = await sendNative(context, {
|
|
449
|
+
source, contract: status.contractId, method: "upgrade", args,
|
|
450
|
+
sourceInvocation: { contract: status.contractId, method: "upgrade", args },
|
|
451
|
+
maxFeeStroops: context.maxFeeStroops,
|
|
452
|
+
}, options);
|
|
453
|
+
return { hash: result.hash, ledger: result.ledger };
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/** Current-owner, Registry-approved update; never uses the legacy tainting path. */
|
|
457
|
+
upgradeRegistrar(namespace, wasmHash, options = {}) {
|
|
458
|
+
return this.upgradeContract(namespace, "registrar", wasmHash, options);
|
|
459
|
+
}
|
|
460
|
+
/** Current-owner, Registry-approved Resolver update with exact native authorization. */
|
|
461
|
+
upgradeResolver(namespace, wasmHash, options = {}) {
|
|
462
|
+
return this.upgradeContract(namespace, "resolver", wasmHash, options);
|
|
429
463
|
}
|
|
430
464
|
async configureClaims(namespace, settings, options = {}) {
|
|
431
465
|
const encoded = claimSettingsToScVal(settings);
|
|
@@ -788,7 +822,7 @@ export class SoranOwner {
|
|
|
788
822
|
catch (e) {
|
|
789
823
|
// A stale sequence (another process moved the account) is safe to
|
|
790
824
|
// retry once with a fresh sequence — nothing was included.
|
|
791
|
-
if (!(e instanceof OwnerError && e.txHash) && /txBadSeq|bad_seq/i.test(String(e))) {
|
|
825
|
+
if ((BAD_SEQ_REJECTED.has(e) || (!(e instanceof OwnerError && e.txHash) && /txBadSeq|bad_seq/i.test(String(e))))) {
|
|
792
826
|
return await this.attempt(contractId, fn, args, errNames);
|
|
793
827
|
}
|
|
794
828
|
throw e;
|
|
@@ -864,7 +898,10 @@ export class SoranOwner {
|
|
|
864
898
|
throw new OwnerError(`${fn}: submit failed after signing (${String(e)}) — transaction ${txHash} may or may not have reached the network; check the hash before retrying`, contractId, fn, null, null, txHash);
|
|
865
899
|
}
|
|
866
900
|
if (sent.status === "ERROR") {
|
|
867
|
-
|
|
901
|
+
const rejected = new OwnerError(`${fn}: submit rejected: ${JSON.stringify(sent.errorResult ?? sent.status)}`, contractId, fn, null, null, txHash);
|
|
902
|
+
if (isBadSeqResult(sent.errorResult))
|
|
903
|
+
BAD_SEQ_REJECTED.add(rejected);
|
|
904
|
+
throw rejected;
|
|
868
905
|
}
|
|
869
906
|
if (sent.status === "TRY_AGAIN_LATER") {
|
|
870
907
|
throw new OwnerError(`${fn}: the network did not accept the transaction (TRY_AGAIN_LATER) — it was NOT queued; safe to retry shortly`, contractId, fn, null, null, txHash);
|
|
@@ -10,5 +10,12 @@ type Context = {
|
|
|
10
10
|
};
|
|
11
11
|
/** Select policy from an exact Registry instance proof. Failed RPC/method reads
|
|
12
12
|
* never downgrade governed verification to the historical template rule. */
|
|
13
|
+
export type ExpectedNativeCode = {
|
|
14
|
+
governed: boolean;
|
|
15
|
+
registrar: string;
|
|
16
|
+
resolver: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function expectedNativeCode(context: Context, namespace: string, minimumLedger?: number): Promise<ExpectedNativeCode>;
|
|
19
|
+
/** Registrar-only consumers retain the same governed/legacy policy selection. */
|
|
13
20
|
export declare function expectedRegistrarCode(context: Context, namespace: string, minimumLedger?: number): Promise<string>;
|
|
14
21
|
export {};
|
|
@@ -6,9 +6,7 @@ function ledger(value, minimum) {
|
|
|
6
6
|
fail('missing or stale ledger context');
|
|
7
7
|
return value;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
* never downgrade governed verification to the historical template rule. */
|
|
11
|
-
export async function expectedRegistrarCode(context, namespace, minimumLedger) {
|
|
9
|
+
export async function expectedNativeCode(context, namespace, minimumLedger) {
|
|
12
10
|
const key = new Contract(context.registryId).getFootprint();
|
|
13
11
|
const proof = await context.server.getLedgerEntries(key);
|
|
14
12
|
const observed = ledger(proof?.latestLedger, minimumLedger ?? 1);
|
|
@@ -38,7 +36,7 @@ export async function expectedRegistrarCode(context, namespace, minimumLedger) {
|
|
|
38
36
|
const templates = await read('template_hashes');
|
|
39
37
|
if (!Array.isArray(templates) || templates.length !== 2)
|
|
40
38
|
fail('malformed historical templates');
|
|
41
|
-
return hex(bytes32(templates[0], 'Registry Registrar template'));
|
|
39
|
+
return { governed: false, registrar: hex(bytes32(templates[0], 'Registry Registrar template')), resolver: hex(bytes32(templates[1], 'Registry Resolver template')) };
|
|
42
40
|
}
|
|
43
41
|
if (authorities[0].val.type !== 'scvAddress')
|
|
44
42
|
fail('malformed upgrade authority');
|
|
@@ -48,6 +46,9 @@ export async function expectedRegistrarCode(context, namespace, minimumLedger) {
|
|
|
48
46
|
const code = exactObject(await read('native_code', [sc.bytes(unhex(namespace))]), ['registrar', 'resolver', 'registrar_updates', 'resolver_updates'], 'native code');
|
|
49
47
|
u32(code.registrar_updates, 'Registrar upgrade count');
|
|
50
48
|
u32(code.resolver_updates, 'Resolver upgrade count');
|
|
51
|
-
bytes32(code.resolver, 'Registry Resolver code');
|
|
52
|
-
|
|
49
|
+
return { governed: true, registrar: hex(bytes32(code.registrar, 'Registry Registrar code')), resolver: hex(bytes32(code.resolver, 'Registry Resolver code')) };
|
|
50
|
+
}
|
|
51
|
+
/** Registrar-only consumers retain the same governed/legacy policy selection. */
|
|
52
|
+
export async function expectedRegistrarCode(context, namespace, minimumLedger) {
|
|
53
|
+
return (await expectedNativeCode(context, namespace, minimumLedger)).registrar;
|
|
53
54
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import type { NativeContext } from "./native-transport.js";
|
|
3
|
+
export type ContractUpgradeRole = "registrar" | "resolver";
|
|
4
|
+
export type ContractUpgradeStatus = {
|
|
5
|
+
owner: string;
|
|
6
|
+
contractId: string;
|
|
7
|
+
currentHash: string;
|
|
8
|
+
approved: boolean;
|
|
9
|
+
governed: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function upgradeHash(hash: string): xdr.ScVal;
|
|
12
|
+
/** Independent of claim versions, claim configuration and fee-token policy. */
|
|
13
|
+
export declare function contractUpgradeStatus(context: NativeContext, namespace: string, role: ContractUpgradeRole, wasmHash: string): Promise<ContractUpgradeStatus>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NativeClaimError, address, bool, bytes32, exactObject, hex, namespaceNode, sc, u32 } from "./native-codec.js";
|
|
2
|
+
import { expectedNativeCode } from "./native-code-policy.js";
|
|
3
|
+
export function upgradeHash(hash) {
|
|
4
|
+
if (typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash))
|
|
5
|
+
throw new NativeClaimError("Contract implementation hash must be lowercase hex32");
|
|
6
|
+
return sc.bytes(Uint8Array.from(hash.match(/../g), value => parseInt(value, 16)));
|
|
7
|
+
}
|
|
8
|
+
/** Independent of claim versions, claim configuration and fee-token policy. */
|
|
9
|
+
export async function contractUpgradeStatus(context, namespace, role, wasmHash) {
|
|
10
|
+
if (role !== "registrar" && role !== "resolver")
|
|
11
|
+
throw new NativeClaimError("Unknown namespace contract role");
|
|
12
|
+
const target = upgradeHash(wasmHash);
|
|
13
|
+
const node = namespaceNode(namespace), args = [sc.bytes(node)];
|
|
14
|
+
const [pair, owner, code, registrar, resolver, selected, registrarTainted, resolverTainted] = await Promise.all([
|
|
15
|
+
context.read(context.registryId, "native_contracts", args),
|
|
16
|
+
context.read(context.registryId, "owner_of", args),
|
|
17
|
+
expectedNativeCode(context, hex(node)),
|
|
18
|
+
context.read(context.registryId, "registrar_of", args),
|
|
19
|
+
context.read(context.registryId, "attested_resolver_of", args),
|
|
20
|
+
context.read(context.registryId, "resolver_of", args),
|
|
21
|
+
context.read(context.registryId, "registrar_tainted", args),
|
|
22
|
+
context.read(context.registryId, "resolver_tainted", args),
|
|
23
|
+
]);
|
|
24
|
+
const fail = (message) => { throw new NativeClaimError(message, "unavailable"); };
|
|
25
|
+
if (!Array.isArray(pair) || pair.length !== 2 || pair[0] !== registrar || pair[1] !== resolver || selected !== resolver || registrarTainted !== false || resolverTainted !== false)
|
|
26
|
+
fail("Namespace contracts are missing, tainted or differ from the Registry's native binding");
|
|
27
|
+
const registrarId = address(registrar, "contract", "namespace Registrar");
|
|
28
|
+
const resolverId = address(resolver, "contract", "namespace Resolver");
|
|
29
|
+
const contractId = role === "registrar" ? registrarId : resolverId;
|
|
30
|
+
const [registrarInstance, resolverInstance, anchors, provenanceRaw, authority, registry, version] = await Promise.all([
|
|
31
|
+
context.server.getContractInstance(registrarId),
|
|
32
|
+
context.server.getContractInstance(resolverId),
|
|
33
|
+
context.read(registrarId, "anchors", []),
|
|
34
|
+
context.read(resolverId, "provenance", []),
|
|
35
|
+
context.read(resolverId, "authority", []),
|
|
36
|
+
context.read(resolverId, "registry", []),
|
|
37
|
+
// A verified legacy Registry is safely reported as disallowed without probing a new child ABI.
|
|
38
|
+
code.governed ? context.read(contractId, "upgrade_policy_version", []) : 0,
|
|
39
|
+
]);
|
|
40
|
+
for (const [instance, expected] of [[registrarInstance, code.registrar], [resolverInstance, code.resolver]]) {
|
|
41
|
+
if (instance.executable.type !== "contractExecutableWasm" || hex(instance.executable.wasmHash.value) !== expected)
|
|
42
|
+
fail("Namespace contract executable differs from Registry-approved code");
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(anchors) || anchors.length !== 2 || anchors[0] !== context.registryId || hex(bytes32(anchors[1], "Registrar namespace")) !== hex(node))
|
|
45
|
+
fail("Registrar anchors differ from selected namespace");
|
|
46
|
+
const provenance = exactObject(provenanceRaw, ["registry", "node"], "Resolver provenance");
|
|
47
|
+
if (provenance.registry !== context.registryId || hex(bytes32(provenance.node, "Resolver namespace")) !== hex(node) || authority !== registrarId || registry !== context.registryId)
|
|
48
|
+
fail("Resolver provenance or authority differs from selected namespace");
|
|
49
|
+
const governed = code.governed && u32(version, "contract upgrade policy") === 1;
|
|
50
|
+
const approved = code.governed
|
|
51
|
+
? bool(await context.read(context.registryId, "implementation_approved", [sc.u32(role === "registrar" ? 0 : 1), target]), "implementation approval")
|
|
52
|
+
: false;
|
|
53
|
+
return { owner: address(owner, "identity", "namespace owner"), contractId, currentHash: code[role], approved, governed };
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sorandomains/owner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Run your Soran namespace on Stellar: configure claims, issue names and manage namespace policies.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
6
9
|
"main": "./dist/index.js",
|
|
7
10
|
"types": "./dist/index.d.ts",
|
|
8
11
|
"files": [
|