@sorandomains/owner 0.6.0 → 0.8.0-migration.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 +36 -9
- package/dist/index.d.ts +62 -37
- package/dist/index.js +209 -11
- package/dist/native-allowlist.d.ts +21 -0
- package/dist/native-allowlist.js +34 -0
- package/dist/native-auth.d.ts +31 -0
- package/dist/native-auth.js +85 -0
- package/dist/native-code-policy.d.ts +14 -0
- package/dist/native-code-policy.js +53 -0
- package/dist/native-codec.d.ts +38 -0
- package/dist/native-codec.js +95 -0
- package/dist/native-history.d.ts +26 -0
- package/dist/native-history.js +162 -0
- package/dist/native-transport.d.ts +66 -0
- package/dist/native-transport.js +137 -0
- package/dist/native-types.d.ts +166 -0
- package/dist/native-types.js +98 -0
- package/dist/payment.d.ts +37 -0
- package/dist/payment.js +148 -0
- package/package.json +3 -3
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Address, Transaction, TransactionBuilder, authorizeEntry, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { NativeClaimError, address, hex, u32 } from "./native-codec.js";
|
|
3
|
+
export function authorizedInvocation(value) {
|
|
4
|
+
return new xdr.SorobanAuthorizedInvocation({
|
|
5
|
+
function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(new xdr.InvokeContractArgs({
|
|
6
|
+
contractAddress: new Address(address(value.contract, "contract", "invocation contract")).toScAddress(), functionName: value.method, args: value.args,
|
|
7
|
+
})), subInvocations: (value.children ?? []).map(authorizedInvocation),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function sameInvocation(actual, expected) {
|
|
11
|
+
return actual.toXDR("base64") === authorizedInvocation(expected).toXDR("base64");
|
|
12
|
+
}
|
|
13
|
+
export function validateEligibilityAuthorization(entry, expected, requireSigned = true) {
|
|
14
|
+
if (expected.invocation.method !== "claim" || expected.invocation.args.length !== 1 || (expected.invocation.children?.length ?? 0) !== 0)
|
|
15
|
+
throw new NativeClaimError("eligibility may authorize only one exact native claim intent", "authorization");
|
|
16
|
+
address(expected.account, "account", "eligibility account");
|
|
17
|
+
u32(expected.latestLedger, "latest ledger");
|
|
18
|
+
u32(expected.maxExpirationLedger, "maximum auth expiration ledger");
|
|
19
|
+
if (entry.credentials.type !== "sorobanCredentialsAddress")
|
|
20
|
+
throw new NativeClaimError("eligibility requires the supported native G address credential", "authorization");
|
|
21
|
+
const credential = entry.credentials.address;
|
|
22
|
+
if (Address.fromScAddress(credential.address).toString() !== expected.account || !sameInvocation(entry.rootInvocation, expected.invocation) || entry.rootInvocation.subInvocations.length !== 0)
|
|
23
|
+
throw new NativeClaimError("eligibility authorization differs from the exact claim intent", "authorization");
|
|
24
|
+
if (requireSigned) {
|
|
25
|
+
if (credential.signatureExpirationLedger <= expected.latestLedger || credential.signatureExpirationLedger > expected.maxExpirationLedger)
|
|
26
|
+
throw new NativeClaimError("eligibility authorization expiry is outside the approved ledger bound", "authorization");
|
|
27
|
+
if (credential.signature.type !== "scvVec" || !credential.signature.vec?.length)
|
|
28
|
+
throw new NativeClaimError("eligibility authorization has no native account signatures", "authorization");
|
|
29
|
+
}
|
|
30
|
+
else if (credential.signature.type !== "scvVoid" && !(credential.signature.type === "scvVec" && credential.signature.vec?.length === 0)) {
|
|
31
|
+
throw new NativeClaimError("refusing to sign an already signed eligibility credential", "authorization");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Signs only the expected admission entry. This helper never signs a transaction. */
|
|
35
|
+
export async function signEligibilityAuthorization(unsignedEntry, expected, signer, expirationLedger, networkPassphrase) {
|
|
36
|
+
validateEligibilityAuthorization(unsignedEntry, expected, false);
|
|
37
|
+
u32(expirationLedger, "expiration ledger");
|
|
38
|
+
if (expirationLedger <= expected.latestLedger || expirationLedger > expected.maxExpirationLedger)
|
|
39
|
+
throw new NativeClaimError("eligibility expiration is outside the approved range", "authorization");
|
|
40
|
+
if (!networkPassphrase)
|
|
41
|
+
throw new NativeClaimError("network passphrase is required");
|
|
42
|
+
const signed = await authorizeEntry(unsignedEntry, signer, expirationLedger, networkPassphrase);
|
|
43
|
+
validateEligibilityAuthorization(signed, expected);
|
|
44
|
+
return signed;
|
|
45
|
+
}
|
|
46
|
+
/** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
|
|
47
|
+
export function validateNativeTransaction(tx, plan, requireEligibilitySigned = true) {
|
|
48
|
+
address(plan.source, "account", "claim transaction source");
|
|
49
|
+
if (tx.source !== plan.source)
|
|
50
|
+
throw new NativeClaimError(`native transaction source ${tx.source} differs from reviewed source ${plan.source}`, "authorization");
|
|
51
|
+
if (tx.operations.length !== 1)
|
|
52
|
+
throw new NativeClaimError(`native transaction has ${tx.operations.length} operations; exactly one was reviewed`, "authorization");
|
|
53
|
+
if (tx.memo.type !== "none")
|
|
54
|
+
throw new NativeClaimError(`native transaction memo type ${tx.memo.type} differs from reviewed none`, "authorization");
|
|
55
|
+
if (BigInt(tx.fee) > plan.maxFeeStroops)
|
|
56
|
+
throw new NativeClaimError(`native transaction network fee ${tx.fee} stroops exceeds the configured maximum ${plan.maxFeeStroops} stroops`, "authorization");
|
|
57
|
+
const op = tx.operations[0];
|
|
58
|
+
if (op.type !== "invokeHostFunction" || op.source && op.source !== plan.source || op.func.type !== "hostFunctionTypeInvokeContract")
|
|
59
|
+
throw new NativeClaimError("unexpected native operation", "authorization");
|
|
60
|
+
const call = op.func.invokeContract;
|
|
61
|
+
if (Address.fromScAddress(call.contractAddress).toString() !== plan.contract || call.functionName.toString() !== plan.method || call.args.length !== plan.args.length || call.args.some((arg, i) => arg.toXDR("base64") !== plan.args[i].toXDR("base64")))
|
|
62
|
+
throw new NativeClaimError("native operation differs from the exact reviewed intent", "authorization");
|
|
63
|
+
const entries = op.auth ?? [];
|
|
64
|
+
if (entries.length !== (plan.eligibility ? 2 : 1))
|
|
65
|
+
throw new NativeClaimError("unexpected native authorization count", "authorization");
|
|
66
|
+
const sources = entries.filter(entry => entry.credentials.type === "sorobanCredentialsSourceAccount");
|
|
67
|
+
if (sources.length !== 1 || !sameInvocation(sources[0].rootInvocation, plan.sourceInvocation))
|
|
68
|
+
throw new NativeClaimError("holder or owner authorization differs from exact reviewed intent", "authorization");
|
|
69
|
+
if (plan.eligibility) {
|
|
70
|
+
if (plan.eligibility.account === plan.source)
|
|
71
|
+
throw new NativeClaimError("eligibility account must be separate from claimant", "authorization");
|
|
72
|
+
const separate = entries.filter(entry => entry.credentials.type !== "sorobanCredentialsSourceAccount");
|
|
73
|
+
if (separate.length !== 1)
|
|
74
|
+
throw new NativeClaimError("unexpected second authorizer", "authorization");
|
|
75
|
+
validateEligibilityAuthorization(separate[0], plan.eligibility, requireEligibilitySigned);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function assertSignedBodyUnchanged(prepared, signedXdr, passphrase) {
|
|
79
|
+
const signed = TransactionBuilder.fromXDR(signedXdr, passphrase);
|
|
80
|
+
if (!(signed instanceof Transaction) || hex(signed.hash()) !== hex(prepared.hash()))
|
|
81
|
+
throw new NativeClaimError("wallet changed the reviewed native transaction body", "authorization");
|
|
82
|
+
if (!signed.signatures.length)
|
|
83
|
+
throw new NativeClaimError("wallet returned an unsigned transaction", "authorization");
|
|
84
|
+
return signed;
|
|
85
|
+
}
|
|
@@ -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,38 @@
|
|
|
1
|
+
/** Local successor-ABI helpers. No deployed defaults are changed by this module. */
|
|
2
|
+
import { xdr } from "@stellar/stellar-sdk";
|
|
3
|
+
import { type PaymentDestination } from "./payment.js";
|
|
4
|
+
export declare const NATIVE_REGISTRAR_ERRORS: Record<number, string>;
|
|
5
|
+
export declare class NativeClaimError extends Error {
|
|
6
|
+
readonly kind: "unsupported" | "unavailable" | "invalid" | "authorization" | "pending" | "failed";
|
|
7
|
+
readonly txHash: string | null;
|
|
8
|
+
readonly contractCode: number | null;
|
|
9
|
+
readonly contractError: string | null;
|
|
10
|
+
constructor(message: string, kind?: "unsupported" | "unavailable" | "invalid" | "authorization" | "pending" | "failed", txHash?: string | null);
|
|
11
|
+
}
|
|
12
|
+
export declare function exactObject(raw: unknown, keys: readonly string[], label: string): Record<string, unknown>;
|
|
13
|
+
export declare function u64(value: unknown, label: string): bigint;
|
|
14
|
+
export declare function u32(value: unknown, label: string): number;
|
|
15
|
+
export declare function amount(value: unknown, label: string): bigint;
|
|
16
|
+
export declare function bool(value: unknown, label: string): boolean;
|
|
17
|
+
export declare function address(value: unknown, kind: "account" | "contract" | "identity", label: string): string;
|
|
18
|
+
export declare function bytes32(value: unknown, label: string): Uint8Array;
|
|
19
|
+
export declare function hex32(value: unknown, label: string): string;
|
|
20
|
+
export declare const hex: (value: Uint8Array) => string;
|
|
21
|
+
export declare const unhex: (value: string) => Uint8Array;
|
|
22
|
+
export declare const utf8: (value: string) => Uint8Array;
|
|
23
|
+
export declare function label(value: unknown): string;
|
|
24
|
+
export declare function namespaceNode(namespace: string): Uint8Array;
|
|
25
|
+
export declare const sc: {
|
|
26
|
+
address: (value: string) => xdr.ScVal;
|
|
27
|
+
bytes: (value: Uint8Array) => xdr.ScVal;
|
|
28
|
+
u64: (value: bigint) => xdr.ScVal;
|
|
29
|
+
u32: (value: number) => xdr.ScVal;
|
|
30
|
+
i128: (value: bigint) => xdr.ScVal;
|
|
31
|
+
bool: (value: boolean) => xdr.ScVal;
|
|
32
|
+
symbol: (value: string) => xdr.ScVal;
|
|
33
|
+
option: (value: xdr.ScVal | null) => xdr.ScVal;
|
|
34
|
+
};
|
|
35
|
+
/** Contract structs are canonical symbol-keyed maps, not generic JSON maps. */
|
|
36
|
+
export declare function struct(fields: Record<string, xdr.ScVal>): xdr.ScVal;
|
|
37
|
+
export declare function paymentDestinationToScVal(raw: PaymentDestination): xdr.ScVal;
|
|
38
|
+
export declare function sameScVal(left: xdr.ScVal, right: xdr.ScVal): boolean;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Local successor-ABI helpers. No deployed defaults are changed by this module. */
|
|
2
|
+
import { Address, StrKey, hash, nativeToScVal, xdr } from "@stellar/stellar-sdk";
|
|
3
|
+
import { decodeMuxedAddress, paymentMemoToScVal, validatePaymentDestination } from "./payment.js";
|
|
4
|
+
export const NATIVE_REGISTRAR_ERRORS = { 21: "InvalidClaimConfig", 22: "ClaimsNotConfigured", 23: "ClaimsPaused", 24: "StaleClaimPolicy", 25: "ClaimIntentMismatch", 26: "ClaimIntentExpired", 27: "ReservedName", 28: "NameNotReserved", 29: "PublicIssuanceRequired", 30: "WalletClaimLimit", 31: "InvalidEligibility", 32: "ApprovalAllowanceReached", 33: "ApprovalRateReached", 34: "RequestIdConflict", 35: "CounterOverflow", 36: "InvalidNativeBinding", 37: "UnsupportedClaimant", 38: "DuplicateLabel", 39: "RenewalTooEarly", 40: "RenewalLeaseLimit", 41: "DestinationInitializationFailed", 42: "FeeSettlementFailed" };
|
|
5
|
+
export class NativeClaimError extends Error {
|
|
6
|
+
kind;
|
|
7
|
+
txHash;
|
|
8
|
+
contractCode;
|
|
9
|
+
contractError;
|
|
10
|
+
constructor(message, kind = "invalid", txHash = null) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.kind = kind;
|
|
13
|
+
this.txHash = txHash;
|
|
14
|
+
this.name = "NativeClaimError";
|
|
15
|
+
const match = /Error\(Contract, #(\d+)\)/.exec(message);
|
|
16
|
+
this.contractCode = match ? Number(match[1]) : null;
|
|
17
|
+
this.contractError = this.contractCode === null ? null : NATIVE_REGISTRAR_ERRORS[this.contractCode] ?? null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function exactObject(raw, keys, label) {
|
|
21
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).sort().join(",") !== [...keys].sort().join(","))
|
|
22
|
+
throw new NativeClaimError(`invalid ${label} fields`);
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
export function u64(value, label) {
|
|
26
|
+
if (typeof value !== "bigint" || value < 0n || value > 18446744073709551615n)
|
|
27
|
+
throw new NativeClaimError(`${label} must be an unsigned 64-bit bigint`);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export function u32(value, label) {
|
|
31
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 4294967295)
|
|
32
|
+
throw new NativeClaimError(`${label} must be an unsigned 32-bit number`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
export function amount(value, label) {
|
|
36
|
+
if (typeof value !== "bigint" || value < 0n || value > (1n << 127n) - 1n)
|
|
37
|
+
throw new NativeClaimError(`${label} must be a nonnegative i128 bigint`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
export function bool(value, label) {
|
|
41
|
+
if (typeof value !== "boolean")
|
|
42
|
+
throw new NativeClaimError(`${label} must be boolean`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
export function address(value, kind, label) {
|
|
46
|
+
if (typeof value !== "string" || !(kind !== "contract" && StrKey.isValidEd25519PublicKey(value) || kind !== "account" && StrKey.isValidContract(value)))
|
|
47
|
+
throw new NativeClaimError(`${label} must be a valid ${kind === "identity" ? "G or C" : kind === "account" ? "G" : "C"} address`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
export function bytes32(value, label) {
|
|
51
|
+
if (!(value instanceof Uint8Array) || value.length !== 32)
|
|
52
|
+
throw new NativeClaimError(`${label} must contain exactly 32 bytes`);
|
|
53
|
+
return new Uint8Array(value);
|
|
54
|
+
}
|
|
55
|
+
export function hex32(value, label) {
|
|
56
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value))
|
|
57
|
+
throw new NativeClaimError(`${label} must be 64 lowercase hex characters`);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
export const hex = (value) => Array.from(value, byte => byte.toString(16).padStart(2, "0")).join("");
|
|
61
|
+
export const unhex = (value) => Uint8Array.from(hex32(value, "32-byte value").match(/../g), byte => parseInt(byte, 16));
|
|
62
|
+
export const utf8 = (value) => new TextEncoder().encode(value);
|
|
63
|
+
export function label(value) {
|
|
64
|
+
if (typeof value !== "string" || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(value))
|
|
65
|
+
throw new NativeClaimError("intent label must be canonical lowercase ASCII, 1–63 characters");
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
export function namespaceNode(namespace) {
|
|
69
|
+
const combined = new Uint8Array(64);
|
|
70
|
+
combined.set(hash(utf8(label(namespace))), 32);
|
|
71
|
+
return new Uint8Array(hash(combined));
|
|
72
|
+
}
|
|
73
|
+
export const sc = {
|
|
74
|
+
address: (value) => new Address(value).toScVal(),
|
|
75
|
+
bytes: (value) => nativeToScVal(value, { type: "bytes" }),
|
|
76
|
+
u64: (value) => nativeToScVal(value, { type: "u64" }),
|
|
77
|
+
u32: (value) => nativeToScVal(value, { type: "u32" }),
|
|
78
|
+
i128: (value) => nativeToScVal(value, { type: "i128" }),
|
|
79
|
+
bool: (value) => nativeToScVal(value, { type: "bool" }),
|
|
80
|
+
symbol: (value) => nativeToScVal(value, { type: "symbol" }),
|
|
81
|
+
option: (value) => value ?? xdr.ScVal.scvVoid(),
|
|
82
|
+
};
|
|
83
|
+
/** Contract structs are canonical symbol-keyed maps, not generic JSON maps. */
|
|
84
|
+
export function struct(fields) {
|
|
85
|
+
return xdr.ScVal.scvMap(Object.keys(fields).sort().map(key => new xdr.ScMapEntry({ key: sc.symbol(key), val: fields[key] })));
|
|
86
|
+
}
|
|
87
|
+
export function paymentDestinationToScVal(raw) {
|
|
88
|
+
const payment = validatePaymentDestination(raw);
|
|
89
|
+
if (StrKey.isValidMed25519PublicKey(payment.address)) {
|
|
90
|
+
const value = decodeMuxedAddress(payment.address);
|
|
91
|
+
return xdr.ScVal.scvVec([sc.symbol("Muxed"), struct({ account: sc.address(value.account), id: sc.u64(BigInt(value.id)) })]);
|
|
92
|
+
}
|
|
93
|
+
return xdr.ScVal.scvVec([sc.symbol("Direct"), struct({ address: sc.address(payment.address), memo: paymentMemoToScVal(payment.memo) })]);
|
|
94
|
+
}
|
|
95
|
+
export function sameScVal(left, right) { return left.toXDR("base64") === right.toXDR("base64"); }
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Transaction, rpc, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { type NativeAuthorizationPlan } from "./native-auth.js";
|
|
3
|
+
export type NativeSigner = {
|
|
4
|
+
publicKey(): string | Promise<string>;
|
|
5
|
+
signTransaction(encoded: string, opts: {
|
|
6
|
+
networkPassphrase: string;
|
|
7
|
+
}): Promise<string | {
|
|
8
|
+
signedTxXdr: string;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
export type NativeContext = {
|
|
12
|
+
registryId: string;
|
|
13
|
+
passphrase: string;
|
|
14
|
+
server: rpc.Server;
|
|
15
|
+
signer: NativeSigner;
|
|
16
|
+
fee: string;
|
|
17
|
+
timeoutSecs: number;
|
|
18
|
+
maxFeeStroops: bigint;
|
|
19
|
+
read(contract: string, method: string, args: xdr.ScVal[]): Promise<unknown>;
|
|
20
|
+
serialize<T>(work: () => Promise<T>): Promise<T>;
|
|
21
|
+
};
|
|
22
|
+
export type NativeCapability = {
|
|
23
|
+
supported: false;
|
|
24
|
+
registrar: string;
|
|
25
|
+
reason: "legacy-template" | "unsupported-version";
|
|
26
|
+
} | {
|
|
27
|
+
supported: true;
|
|
28
|
+
version: 1;
|
|
29
|
+
registrar: string;
|
|
30
|
+
resolver: string;
|
|
31
|
+
registry: string;
|
|
32
|
+
namespace: string;
|
|
33
|
+
owner: string;
|
|
34
|
+
ownerEpoch: bigint;
|
|
35
|
+
};
|
|
36
|
+
/** Historical reads must not trust an upgraded Registrar that can fabricate receipts. Resolver changes do not block this Registrar-only check. */
|
|
37
|
+
export declare function verifyRegistrarProvenance(context: NativeContext, registrar: string, namespace: string): Promise<void>;
|
|
38
|
+
export declare function nativeCapability(context: NativeContext, namespace: string): Promise<NativeCapability>;
|
|
39
|
+
export declare function requireNative(context: NativeContext, namespace: string): Promise<Extract<NativeCapability, {
|
|
40
|
+
supported: true;
|
|
41
|
+
}>>;
|
|
42
|
+
export type NativePrepared = {
|
|
43
|
+
transactionXdr: string;
|
|
44
|
+
hash: string;
|
|
45
|
+
networkPassphrase: string;
|
|
46
|
+
feeStroops: bigint;
|
|
47
|
+
eligibilityEntryXdr: string | null;
|
|
48
|
+
};
|
|
49
|
+
export type NativeWriteOptions = {
|
|
50
|
+
/** Exact native admission authorization from the app. This is not an owner transaction signature. */
|
|
51
|
+
eligibilityAuthorization?: string;
|
|
52
|
+
/** Persist the public recovery reference before wallet signing/broadcast. Rejecting stops submission. */
|
|
53
|
+
onPrepared?: (prepared: NativePrepared) => void | Promise<void>;
|
|
54
|
+
};
|
|
55
|
+
export declare function prepareNative(context: NativeContext, plan: NativeAuthorizationPlan, options?: NativeWriteOptions): Promise<{
|
|
56
|
+
transaction: Transaction;
|
|
57
|
+
prepared: NativePrepared;
|
|
58
|
+
}>;
|
|
59
|
+
/** No automatic transaction retry, implicit restoration or hash-losing error path. */
|
|
60
|
+
export declare function sendNative(context: NativeContext, plan: NativeAuthorizationPlan, options?: NativeWriteOptions): Promise<{
|
|
61
|
+
hash: string;
|
|
62
|
+
ledger: number;
|
|
63
|
+
value: unknown;
|
|
64
|
+
}>;
|
|
65
|
+
/** Bind terminal RPC context before classifying success or failure. An incomplete response remains uncertain. */
|
|
66
|
+
export declare function validateNativeTerminalReceipt(receipt: rpc.Api.GetSuccessfulTransactionResponse | rpc.Api.GetFailedTransactionResponse, expectedHash: string, passphrase: string): void;
|