@sorandomains/owner 0.10.1 → 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/index.d.ts +8 -1
- package/dist/index.js +32 -17
- 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 +1 -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/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";
|
|
@@ -428,23 +429,37 @@ export class SoranOwner {
|
|
|
428
429
|
throw new NativeClaimError("Registrar is not a Wasm contract", "unavailable");
|
|
429
430
|
return nativeHex(instance.executable.wasmHash.value);
|
|
430
431
|
}
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
this.
|
|
439
|
-
|
|
440
|
-
this.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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);
|
|
448
463
|
}
|
|
449
464
|
async configureClaims(namespace, settings, options = {}) {
|
|
450
465
|
const encoded = claimSettingsToScVal(settings);
|
|
@@ -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
|
+
}
|