@sorandomains/owner 0.8.0 → 0.10.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.
@@ -0,0 +1,262 @@
1
+ import { Address, Contract, Operation, Transaction, TransactionBuilder, authorizeEntry, rpc, nativeToScVal, xdr, } from "@stellar/stellar-sdk";
2
+ import { FundingReader } from "./funding-reader.js";
3
+ import { signContractAuthorization } from "./contract-wallet.js";
4
+ import { FundingError, sponsorIntentHash, sponsorQuoteToScVal, sponsoredIdentityToScVal, } from "./funding-types.js";
5
+ import { claimIntentToScVal, claimLabelToScVal, claimQuoteFromNative, } from "./native-types.js";
6
+ import { address, paymentDestinationToScVal, sc, unhex, } from "./native-codec.js";
7
+ import { authorizedInvocation, validateEligibilityAuthorization, } from "./native-auth.js";
8
+ /** Build from the app's reviewed intent and pinned shared contracts, never a remote invocation tree. */
9
+ export function sponsorPlan(request, lookup, primary, maxNetworkFee, eligibilityAccount) {
10
+ const { quote: q, intent } = request;
11
+ const method = "sponsor_" + q.action;
12
+ if (q.intentHash !== sponsorIntentHash(q.action, intent) ||
13
+ maxNetworkFee <= 0n)
14
+ throw new FundingError("Quote differs from the requested action", "authorization");
15
+ let args, target;
16
+ if (q.action === "claim") {
17
+ const i = intent, encoded = claimIntentToScVal(i), proof = request.proof ?? [];
18
+ if (i.claimant !== q.actor ||
19
+ i.resolver !== q.resolver ||
20
+ i.context.registrar !== q.registrar ||
21
+ i.context.network !== q.network ||
22
+ proof.length > 64)
23
+ throw new FundingError("Claim route or actor differs from quote", "authorization");
24
+ args = [
25
+ sponsorQuoteToScVal(q),
26
+ encoded,
27
+ xdr.ScVal.scvVec(proof.map((p) => sc.bytes(unhex(p)))),
28
+ ];
29
+ const children = [];
30
+ if (i.feeAmount > 0n)
31
+ children.push({
32
+ contract: i.feeToken,
33
+ method: "transfer",
34
+ args: [
35
+ sc.address(i.claimant),
36
+ sc.address(i.feeRecipient),
37
+ sc.i128(i.feeAmount),
38
+ ],
39
+ });
40
+ children.push({
41
+ contract: i.resolver,
42
+ method: "initialize_destination",
43
+ args: [
44
+ claimLabelToScVal(i.label),
45
+ sc.address(i.claimant),
46
+ sc.u64(i.expectedGeneration === null ? 0n : i.expectedGeneration + 1n),
47
+ paymentDestinationToScVal(i.destination),
48
+ ],
49
+ });
50
+ target = {
51
+ contract: q.registrar,
52
+ method: "claim",
53
+ args: [encoded],
54
+ children,
55
+ };
56
+ }
57
+ else {
58
+ const i = intent;
59
+ if (i.actor !== q.actor || request.proof?.length)
60
+ throw new FundingError("Identity actor or proof differs from action");
61
+ args = [sponsorQuoteToScVal(q), sponsoredIdentityToScVal(i)];
62
+ const reverse = q.action === "reverse";
63
+ target =
64
+ i.routingId === null
65
+ ? {
66
+ contract: reverse ? q.resolver : primary,
67
+ method: reverse ? "set_reverse" : "set_primary",
68
+ args: [
69
+ sc.address(i.actor),
70
+ nativeToScVal(i.name, { type: "string" }),
71
+ ],
72
+ }
73
+ : {
74
+ contract: lookup,
75
+ method: reverse ? "set_reverse_muxed" : "set_primary_muxed",
76
+ args: [
77
+ sc.address(i.actor),
78
+ sc.u64(i.routingId),
79
+ nativeToScVal(i.name, { type: "string" }),
80
+ ],
81
+ };
82
+ }
83
+ const plan = {
84
+ request,
85
+ method,
86
+ args,
87
+ actorInvocation: { contract: q.funding, method, args, children: [target] },
88
+ relayerInvocation: { contract: q.funding, method, args },
89
+ maxNetworkFee,
90
+ };
91
+ if (eligibilityAccount) {
92
+ if (q.action !== "claim" ||
93
+ eligibilityAccount === q.actor ||
94
+ eligibilityAccount === q.relayer)
95
+ throw new FundingError("Eligibility account must be separate from user and relayer");
96
+ plan.eligibility = {
97
+ account: address(eligibilityAccount, "account", "eligibility"),
98
+ invocation: {
99
+ contract: q.registrar,
100
+ method: "claim",
101
+ args: [claimIntentToScVal(intent)],
102
+ },
103
+ latestLedger: q.issuedLedger,
104
+ maxExpirationLedger: q.expiresLedger,
105
+ };
106
+ }
107
+ return plan;
108
+ }
109
+ function same(a, b) {
110
+ return a.toXDR("base64") === authorizedInvocation(b).toXDR("base64");
111
+ }
112
+ export function validateSponsorTransaction(tx, p, latestLedger, signed = false) {
113
+ const q = p.request.quote;
114
+ if (tx.source !== q.relayer ||
115
+ tx.operations.length !== 1 ||
116
+ tx.memo.type !== "none" ||
117
+ BigInt(tx.fee) > p.maxNetworkFee ||
118
+ tx.signatures.length ||
119
+ latestLedger < q.issuedLedger ||
120
+ latestLedger >= q.expiresLedger)
121
+ throw new FundingError("Unapproved source, operation, fee, signature or ledger", "authorization");
122
+ const op = tx.operations[0];
123
+ if (op.type !== "invokeHostFunction" ||
124
+ (op.source && op.source !== q.relayer) ||
125
+ op.func.type !== "hostFunctionTypeInvokeContract")
126
+ throw new FundingError("Unexpected sponsored operation");
127
+ const c = op.func.invokeContract;
128
+ if (Address.fromScAddress(c.contractAddress).toString() !== q.funding ||
129
+ c.functionName.toString() !== p.method ||
130
+ c.args.length !== p.args.length ||
131
+ c.args.some((v, n) => v.toXDR("base64") !== p.args[n].toXDR("base64")))
132
+ throw new FundingError("Transaction differs from approved request", "authorization");
133
+ const auth = op.auth ?? [];
134
+ if (auth.length !== (p.eligibility ? 3 : 2))
135
+ throw new FundingError("Unexpected sponsor authorization count", "authorization");
136
+ const sources = auth.filter((e) => e.credentials.type === "sorobanCredentialsSourceAccount");
137
+ if (sources.length !== 1 ||
138
+ !same(sources[0].rootInvocation, p.relayerInvocation))
139
+ throw new FundingError("Relayer authorization exceeds its approved role", "authorization");
140
+ const actors = auth.filter((e) => e.credentials.type === "sorobanCredentialsAddress" &&
141
+ Address.fromScAddress(e.credentials.address.address).toString() ===
142
+ q.actor);
143
+ if (actors.length !== 1 || !same(actors[0].rootInvocation, p.actorInvocation))
144
+ throw new FundingError("User authorization differs from approved action", "authorization");
145
+ const credential = actors[0].credentials;
146
+ if (credential.type !== "sorobanCredentialsAddress")
147
+ throw new FundingError("Missing actor credential");
148
+ if (signed) {
149
+ if (credential.address.signatureExpirationLedger <= latestLedger ||
150
+ credential.address.signatureExpirationLedger > q.expiresLedger ||
151
+ credential.address.signature.toXDR().length > 16384 ||
152
+ // G accounts use Stellar's native vector format. C accounts define their
153
+ // own ScVal signature; enforced simulation must run their __check_auth.
154
+ (q.actor.startsWith("G") &&
155
+ (credential.address.signature.type !== "scvVec" ||
156
+ !credential.address.signature.vec?.length)))
157
+ throw new FundingError("User authorization missing or expired", "authorization");
158
+ }
159
+ else if (credential.address.signature.type !== "scvVoid" &&
160
+ !(credential.address.signature.type === "scvVec" &&
161
+ !credential.address.signature.vec?.length))
162
+ throw new FundingError("Refusing to re-sign an authorization");
163
+ if (p.eligibility) {
164
+ const entries = auth.filter((e) => e.credentials.type === "sorobanCredentialsAddress" &&
165
+ Address.fromScAddress(e.credentials.address.address).toString() ===
166
+ p.eligibility.account);
167
+ if (entries.length !== 1)
168
+ throw new FundingError("Missing eligibility authorization");
169
+ validateEligibilityAuthorization(entries[0], { ...p.eligibility, latestLedger }, signed);
170
+ }
171
+ return actors[0];
172
+ }
173
+ export function replaceSponsorAuth(tx, auth, passphrase) {
174
+ const op = tx.operations[0];
175
+ if (op.type !== "invokeHostFunction")
176
+ throw new FundingError("Unexpected operation");
177
+ return TransactionBuilder.cloneFrom(tx, { networkPassphrase: passphrase })
178
+ .clearOperations()
179
+ .addOperation(Operation.invokeHostFunction({ func: op.func, auth }))
180
+ .build();
181
+ }
182
+ export class SoranSponsorship extends FundingReader {
183
+ async plan(request, maxNetworkFee) {
184
+ await this.validateQuote(request.quote);
185
+ let eligibility;
186
+ if (request.quote.action === "claim") {
187
+ const i = request.intent;
188
+ if (i.context.registry !== this.registryId)
189
+ throw new FundingError("Claim Registry differs from deployment");
190
+ if (i.claimant.startsWith("C")) {
191
+ const capability = await this.read(request.quote.registrar, "contract_claim_version", [], request.quote.issuedLedger);
192
+ if (capability.value !== 1)
193
+ throw new FundingError("This namespace needs a Registrar upgrade before contract wallets can claim", "unavailable");
194
+ }
195
+ const r = await this.read(request.quote.registrar, "claim_quote", [claimLabelToScVal(i.label), sc.address(i.claimant)], request.quote.issuedLedger);
196
+ const admission = claimQuoteFromNative(r.value).config?.settings
197
+ .admission;
198
+ if (admission?.type === "approval")
199
+ eligibility = admission.account;
200
+ }
201
+ return sponsorPlan(request, this.lookupId, this.primaryId, maxNetworkFee, eligibility);
202
+ }
203
+ async prepare(request, maxNetworkFee, inclusionFee = 100n) {
204
+ if (inclusionFee < 100n || inclusionFee > maxNetworkFee || inclusionFee > 0xffffffffn)
205
+ throw new FundingError("Invalid network inclusion fee");
206
+ const plan = await this.plan(request, maxNetworkFee);
207
+ const raw = new TransactionBuilder(await this.server.getAccount(request.quote.relayer), { fee: inclusionFee.toString(), networkPassphrase: this.passphrase })
208
+ .addOperation(new Contract(this.fundingId).call(plan.method, ...plan.args))
209
+ .setTimeout(60)
210
+ .build();
211
+ const sim = await this.server.simulateTransaction(raw, undefined, undefined, false);
212
+ if (rpc.Api.isSimulationRestore(sim))
213
+ throw new FundingError("Separate storage restoration is required; sponsorship is unavailable", "unavailable");
214
+ if (!rpc.Api.isSimulationSuccess(sim) || !sim.result)
215
+ throw new FundingError("Sponsored simulation failed", "unavailable");
216
+ const transaction = rpc.assembleTransaction(raw, sim).build();
217
+ validateSponsorTransaction(transaction, plan, sim.latestLedger);
218
+ return { transaction, plan, ledger: sim.latestLedger };
219
+ }
220
+ /** Sign only the caller's authorization. C wallets supply their own signature
221
+ * format; the service enforces __check_auth before the relayer signs. */
222
+ async authorize(request, encoded, signer, maxSponsorCharge, maxNetworkFee, eligibilityXdr) {
223
+ if (request.quote.charge > maxSponsorCharge)
224
+ throw new FundingError("Sponsor charge exceeds the reviewed amount");
225
+ address(request.quote.actor, "identity", "signing wallet");
226
+ const contractWallet = typeof signer === "object" && "signAuthorization" in signer ? signer : undefined;
227
+ if (request.quote.actor.startsWith("C") !== !!contractWallet)
228
+ throw new FundingError("Use the selected wallet's G signer or C authorization adapter", "authorization");
229
+ const plan = await this.plan(request, maxNetworkFee), parsed = TransactionBuilder.fromXDR(encoded, this.passphrase);
230
+ if (!(parsed instanceof Transaction))
231
+ throw new FundingError("Fee-bump envelopes are not supported by this transport");
232
+ const latest = await this.server.getLatestLedger();
233
+ const entry = validateSponsorTransaction(parsed, plan, latest.sequence);
234
+ const signed = contractWallet ? await signContractAuthorization(entry, {
235
+ account: request.quote.actor, latestLedger: latest.sequence, maxExpirationLedger: request.quote.expiresLedger,
236
+ }, contractWallet, this.passphrase) : await authorizeEntry(entry, signer, request.quote.expiresLedger, this.passphrase);
237
+ const op = parsed.operations[0];
238
+ if (op.type !== "invokeHostFunction")
239
+ throw new FundingError("Unexpected operation");
240
+ const auth = (op.auth ?? []).map((e) => e.credentials.type === "sorobanCredentialsAddress" &&
241
+ Address.fromScAddress(e.credentials.address.address).toString() ===
242
+ request.quote.actor
243
+ ? signed
244
+ : e);
245
+ if (plan.eligibility) {
246
+ if (!eligibilityXdr || eligibilityXdr.length > 32768)
247
+ throw new FundingError("This claim requires the app's eligibility authorization");
248
+ const approval = xdr.SorobanAuthorizationEntry.fromXDR(eligibilityXdr, "base64");
249
+ validateEligibilityAuthorization(approval, {
250
+ ...plan.eligibility,
251
+ latestLedger: latest.sequence,
252
+ });
253
+ const at = auth.findIndex((e) => e.credentials.type === "sorobanCredentialsAddress" &&
254
+ Address.fromScAddress(e.credentials.address.address).toString() ===
255
+ plan.eligibility.account);
256
+ auth[at] = approval;
257
+ }
258
+ const tx = replaceSponsorAuth(parsed, auth, this.passphrase);
259
+ validateSponsorTransaction(tx, plan, latest.sequence, true);
260
+ return tx.toXDR();
261
+ }
262
+ }
package/dist/index.d.ts CHANGED
@@ -158,6 +158,10 @@ export declare class SoranOwner {
158
158
  claimPolicy(namespace: string): Promise<ClaimConfig | null>;
159
159
  nativeClaimFeeToken(namespace: string): Promise<string>;
160
160
  private nativeOwnerWrite;
161
+ /** Current implementation after validating the Registry's native binding. */
162
+ registrarCodeHash(namespace: string): Promise<string>;
163
+ /** Owner-authorized, Registry-approved update; never uses the legacy tainting path. */
164
+ upgradeRegistrar(namespace: string, wasmHash: string, options?: NativeWriteOptions): Promise<Submitted>;
161
165
  configureClaims(namespace: string, settings: ClaimSettings, options?: NativeWriteOptions): Promise<Submitted & {
162
166
  config: ClaimConfig;
163
167
  }>;
@@ -281,3 +285,10 @@ export declare class SoranOwner {
281
285
  */
282
286
  private decodeFailure;
283
287
  }
288
+ export * from "./funding-types.js";
289
+ export { FundingReader, type FundingOptions } from "./funding-reader.js";
290
+ export { SoranFunding } from "./funding-owner.js";
291
+ export { SoranSponsorship, sponsorPlan, validateSponsorTransaction, type SponsorRequest, type SponsorPlan } from "./funding-user.js";
292
+ export { FundingServiceClient } from "./funding-service.js";
293
+ export { TestnetSponsorHistory, type SponsorHistory, type FundingReceipt } from "./funding-history.js";
294
+ export type { ContractWallet } from "./contract-wallet.js";
package/dist/index.js CHANGED
@@ -401,6 +401,32 @@ export class SoranOwner {
401
401
  return sendNative(context, { source, contract: cap.registrar, method, args, sourceInvocation: { contract: cap.registrar, method, args }, maxFeeStroops: context.maxFeeStroops }, options);
402
402
  });
403
403
  }
404
+ /** Current implementation after validating the Registry's native binding. */
405
+ async registrarCodeHash(namespace) {
406
+ const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
407
+ const instance = await this.server.getContractInstance(cap.registrar);
408
+ if (instance.executable.type !== "contractExecutableWasm")
409
+ throw new NativeClaimError("Registrar is not a Wasm contract", "unavailable");
410
+ return nativeHex(instance.executable.wasmHash.value);
411
+ }
412
+ /** Owner-authorized, Registry-approved update; never uses the legacy tainting path. */
413
+ async upgradeRegistrar(namespace, wasmHash, options = {}) {
414
+ if (!/^[a-f0-9]{64}$/.test(wasmHash))
415
+ throw new NativeClaimError("Registrar implementation hash must be lowercase hex32");
416
+ const bytes = Uint8Array.from(wasmHash.match(/../g), value => parseInt(value, 16));
417
+ const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
418
+ const [governed, approved, current] = await Promise.all([
419
+ this.read(cap.registrar, "upgrade_policy_version", []),
420
+ this.read(this.registryId, "implementation_approved", [nativeSc.u32(0), nativeSc.bytes(bytes)]),
421
+ this.registrarCodeHash(namespace),
422
+ ]);
423
+ if (governed !== 1 || approved !== true)
424
+ throw new NativeClaimError("This Registrar upgrade is not approved by the Registry", "authorization");
425
+ if (current === wasmHash)
426
+ throw new NativeClaimError("This Registrar implementation is already installed");
427
+ const result = await this.nativeOwnerWrite(namespace, "upgrade", [nativeSc.bytes(bytes)], options);
428
+ return { hash: result.hash, ledger: result.ledger };
429
+ }
404
430
  async configureClaims(namespace, settings, options = {}) {
405
431
  const encoded = claimSettingsToScVal(settings);
406
432
  const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
@@ -970,3 +996,9 @@ export class SoranOwner {
970
996
  return new OwnerError(`${fn} failed at inclusion (${resultCode})`, contractId, fn, null, null, hash);
971
997
  }
972
998
  }
999
+ export * from "./funding-types.js";
1000
+ export { FundingReader } from "./funding-reader.js";
1001
+ export { SoranFunding } from "./funding-owner.js";
1002
+ export { SoranSponsorship, sponsorPlan, validateSponsorTransaction } from "./funding-user.js";
1003
+ export { FundingServiceClient } from "./funding-service.js";
1004
+ export { TestnetSponsorHistory } from "./funding-history.js";
@@ -1,15 +1,15 @@
1
1
  import { hash, xdr } from "@stellar/stellar-sdk";
2
2
  import { NativeClaimError, address, hex, hex32, sc, unhex } from "./native-codec.js";
3
3
  export function claimAllowlistLeaf(context, account) {
4
- return hex(hash(xdr.ScVal.scvVec([sc.symbol("allow_v1"), sc.bytes(unhex(context.network)), sc.address(address(context.registry, "contract", "Registry")), sc.bytes(unhex(context.namespace)), sc.address(address(context.registrar, "contract", "Registrar")), sc.address(address(account, "account", "allowlisted claimant"))]).toXDR()));
4
+ return hex(hash(xdr.ScVal.scvVec([sc.symbol("allow_v1"), sc.bytes(unhex(context.network)), sc.address(address(context.registry, "contract", "Registry")), sc.bytes(unhex(context.namespace)), sc.address(address(context.registrar, "contract", "Registrar")), sc.address(address(account, "identity", "allowlisted claimant"))]).toXDR()));
5
5
  }
6
6
  export function claimAllowlistParent(left, right) { hex32(left, "left leaf"); hex32(right, "right leaf"); const [first, second] = left < right ? [left, right] : [right, left]; const bytes = new Uint8Array(65); bytes[0] = 1; bytes.set(unhex(first), 1); bytes.set(unhex(second), 33); return hex(hash(bytes)); }
7
7
  /** A complete list is required to generate a replacement root. Keep/distribute the returned proof bundle. */
8
8
  export function buildClaimAllowlist(context, accounts) {
9
9
  if (!Array.isArray(accounts) || accounts.length === 0 || accounts.length > 65536)
10
- throw new NativeClaimError("allowlist requires 1–65536 distinct G accounts");
10
+ throw new NativeClaimError("allowlist requires 1–65536 distinct G or C accounts");
11
11
  const unique = new Set();
12
- const entries = accounts.map(account => { address(account, "account", "allowlisted claimant"); if (unique.has(account))
12
+ const entries = accounts.map(account => { address(account, "identity", "allowlisted claimant"); if (unique.has(account))
13
13
  throw new NativeClaimError("duplicate allowlist account"); unique.add(account); return { account, leaf: claimAllowlistLeaf(context, account), proof: [] }; }).sort((a, b) => a.leaf.localeCompare(b.leaf));
14
14
  let layer = entries.map((entry, index) => ({ hash: entry.leaf, indices: [index] }));
15
15
  while (layer.length > 1) {
@@ -1,4 +1,5 @@
1
1
  import { Transaction, authorizeEntry, xdr } from "@stellar/stellar-sdk";
2
+ import { type ContractAuthorization } from "./contract-wallet.js";
2
3
  export type NativeInvocation = {
3
4
  contract: string;
4
5
  method: string;
@@ -8,6 +9,8 @@ export type NativeInvocation = {
8
9
  /** Derive this plan locally from a validated intent, never from a remote envelope. */
9
10
  export type NativeAuthorizationPlan = {
10
11
  source: string;
12
+ /** When present, this contract wallet authorizes the holder invocation. */
13
+ actor?: ContractAuthorization;
11
14
  contract: string;
12
15
  method: string;
13
16
  args: xdr.ScVal[];
@@ -27,5 +30,5 @@ export type EligibilitySigner = Parameters<typeof authorizeEntry>[1];
27
30
  /** Signs only the expected admission entry. This helper never signs a transaction. */
28
31
  export declare function signEligibilityAuthorization(unsignedEntry: xdr.SorobanAuthorizationEntry, expected: NonNullable<NativeAuthorizationPlan["eligibility"]>, signer: EligibilitySigner, expirationLedger: number, networkPassphrase: string): Promise<xdr.SorobanAuthorizationEntry>;
29
32
  /** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
30
- export declare function validateNativeTransaction(tx: Transaction, plan: NativeAuthorizationPlan, requireEligibilitySigned?: boolean): void;
33
+ export declare function validateNativeTransaction(tx: Transaction, plan: NativeAuthorizationPlan, requireEligibilitySigned?: boolean, requireActorSigned?: boolean): void;
31
34
  export declare function assertSignedBodyUnchanged(prepared: Transaction, signedXdr: string, passphrase: string): Transaction;
@@ -1,5 +1,6 @@
1
1
  import { Address, Transaction, TransactionBuilder, authorizeEntry, xdr } from "@stellar/stellar-sdk";
2
2
  import { NativeClaimError, address, hex, u32 } from "./native-codec.js";
3
+ import { validateContractAuthorization } from "./contract-wallet.js";
3
4
  export function authorizedInvocation(value) {
4
5
  return new xdr.SorobanAuthorizedInvocation({
5
6
  function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(new xdr.InvokeContractArgs({
@@ -44,7 +45,7 @@ export async function signEligibilityAuthorization(unsignedEntry, expected, sign
44
45
  return signed;
45
46
  }
46
47
  /** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
47
- export function validateNativeTransaction(tx, plan, requireEligibilitySigned = true) {
48
+ export function validateNativeTransaction(tx, plan, requireEligibilitySigned = true, requireActorSigned = true) {
48
49
  address(plan.source, "account", "claim transaction source");
49
50
  if (tx.source !== plan.source)
50
51
  throw new NativeClaimError(`native transaction source ${tx.source} differs from reviewed source ${plan.source}`, "authorization");
@@ -64,12 +65,21 @@ export function validateNativeTransaction(tx, plan, requireEligibilitySigned = t
64
65
  if (entries.length !== (plan.eligibility ? 2 : 1))
65
66
  throw new NativeClaimError("unexpected native authorization count", "authorization");
66
67
  const sources = entries.filter(entry => entry.credentials.type === "sorobanCredentialsSourceAccount");
67
- if (sources.length !== 1 || !sameInvocation(sources[0].rootInvocation, plan.sourceInvocation))
68
+ if (plan.actor) {
69
+ const actors = entries.filter(entry => entry.credentials.type === "sorobanCredentialsAddress" &&
70
+ Address.fromScAddress(entry.credentials.address.address).toString() === plan.actor.account);
71
+ if (sources.length || actors.length !== 1 || !sameInvocation(actors[0].rootInvocation, plan.sourceInvocation))
72
+ throw new NativeClaimError("contract wallet authorization differs from exact reviewed intent", "authorization");
73
+ validateContractAuthorization(actors[0], plan.actor, requireActorSigned);
74
+ }
75
+ else if (sources.length !== 1 || !sameInvocation(sources[0].rootInvocation, plan.sourceInvocation)) {
68
76
  throw new NativeClaimError("holder or owner authorization differs from exact reviewed intent", "authorization");
77
+ }
69
78
  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");
79
+ if (plan.eligibility.account === plan.source || plan.eligibility.account === plan.actor?.account)
80
+ throw new NativeClaimError("eligibility account must be separate from claimant and transaction source", "authorization");
81
+ const separate = entries.filter(entry => entry.credentials.type === "sorobanCredentialsAddress" &&
82
+ Address.fromScAddress(entry.credentials.address.address).toString() === plan.eligibility.account);
73
83
  if (separate.length !== 1)
74
84
  throw new NativeClaimError("unexpected second authorizer", "authorization");
75
85
  validateEligibilityAuthorization(separate[0], plan.eligibility, requireEligibilitySigned);
@@ -33,16 +33,16 @@ function model(fields) { return { encode: v => { const value = exactObject(v, Ob
33
33
  const SETTINGS = model({ mode: enumeration({ manual: "Manual", public: "Public" }), enabled: BOOL, admission: ADMISSION, feeToken: CONTRACT, feeAmount: AMOUNT, feeRecipient: ACCOUNT, walletLimit: U64, approvalAllowance: U64, approvalRateLimit: U64, approvalWindowSecs: U64, approvalTtlSecs: U64 });
34
34
  const CONFIG = model({ settings: SETTINGS, ownerEpoch: U64, policyVersion: U64, grantEpoch: U64 });
35
35
  const CONTEXT = model({ network: HEX, registry: CONTRACT, registrar: CONTRACT, namespace: HEX, requestId: HEX, validAfter: U64, deadline: U64 });
36
- const CLAIM = model({ context: CONTEXT, label: LABEL, claimant: ACCOUNT, destination: DESTINATION, resolver: CONTRACT, expectedGeneration: optional(U64), expectedExpiry: optional(U64), termSecs: U64, ownerEpoch: U64, policyVersion: U64, grantEpoch: U64, feeToken: CONTRACT, feeAmount: AMOUNT, feeRecipient: ACCOUNT });
37
- const RESERVED = model({ context: CONTEXT, label: LABEL, holder: ACCOUNT, destination: DESTINATION, resolver: CONTRACT, expectedGeneration: optional(U64), expectedExpiry: optional(U64), termSecs: U64, ownerEpoch: U64, policyVersion: U64 });
38
- const TRANSFER = model({ context: CONTEXT, label: LABEL, from: IDENTITY, to: ACCOUNT, expectedGeneration: U64, expectedExpiry: U64, proposalExpires: U64, destination: DESTINATION, resolver: CONTRACT });
39
- const RENEW = model({ context: CONTEXT, label: LABEL, holder: ACCOUNT, expectedGeneration: U64, expectedExpiry: U64, termSecs: U64, minNewExpiry: U64, maxNewExpiry: U64 });
36
+ const CLAIM = model({ context: CONTEXT, label: LABEL, claimant: IDENTITY, destination: DESTINATION, resolver: CONTRACT, expectedGeneration: optional(U64), expectedExpiry: optional(U64), termSecs: U64, ownerEpoch: U64, policyVersion: U64, grantEpoch: U64, feeToken: CONTRACT, feeAmount: AMOUNT, feeRecipient: ACCOUNT });
37
+ const RESERVED = model({ context: CONTEXT, label: LABEL, holder: IDENTITY, destination: DESTINATION, resolver: CONTRACT, expectedGeneration: optional(U64), expectedExpiry: optional(U64), termSecs: U64, ownerEpoch: U64, policyVersion: U64 });
38
+ const TRANSFER = model({ context: CONTEXT, label: LABEL, from: IDENTITY, to: IDENTITY, expectedGeneration: U64, expectedExpiry: U64, proposalExpires: U64, destination: DESTINATION, resolver: CONTRACT });
39
+ const RENEW = model({ context: CONTEXT, label: LABEL, holder: IDENTITY, expectedGeneration: U64, expectedExpiry: U64, termSecs: U64, minNewExpiry: U64, maxNewExpiry: U64 });
40
40
  const USAGE = model({ publicClaims: U64, reservedIssues: U64 }), APPROVAL_USAGE = model({ total: U64, windowUsed: U64, windowEnds: U64 });
41
41
  const CONFIG_STATE = { encode: v => v === null ? xdr.ScVal.scvVec([sc.symbol("Unconfigured")]) : xdr.ScVal.scvVec([sc.symbol("Configured"), CONFIG.encode(v)]), decode: v => { if (!Array.isArray(v))
42
42
  throw new NativeClaimError("invalid claim config state"); if (v.length === 1 && v[0] === "Unconfigured")
43
43
  return null; if (v.length === 2 && v[0] === "Configured")
44
44
  return CONFIG.decode(v[1]); throw new NativeClaimError("invalid claim config state"); } };
45
- const QUOTE = model({ network: HEX, registry: CONTRACT, registrar: CONTRACT, namespace: HEX, resolver: CONTRACT, label: LABEL, node: HEX, claimant: ACCOUNT, config: CONFIG_STATE, ownerEpoch: U64, policyVersion: U64, available: BOOL, reserved: BOOL, generation: optional(U64), expiresAt: optional(U64), termSecs: U64, now: U64, usage: USAGE, approvalUsage: APPROVAL_USAGE });
45
+ const QUOTE = model({ network: HEX, registry: CONTRACT, registrar: CONTRACT, namespace: HEX, resolver: CONTRACT, label: LABEL, node: HEX, claimant: IDENTITY, config: CONFIG_STATE, ownerEpoch: U64, policyVersion: U64, available: BOOL, reserved: BOOL, generation: optional(U64), expiresAt: optional(U64), termSecs: U64, now: U64, usage: USAGE, approvalUsage: APPROVAL_USAGE });
46
46
  const RECEIPT = model({ operation: enumeration({ claim: "Claim", reserved: "Reserved", transfer: "Transfer", renew: "Renew" }), intentHash: HEX, node: HEX, holder: IDENTITY, generation: U64, expiresAt: U64, feeAmount: AMOUNT, feeToken: CONTRACT, feeRecipient: optional(ACCOUNT), ledger: U32, timestamp: U64 });
47
47
  const PREVIEW = model({ node: HEX, holder: IDENTITY, generation: U64, expiresAt: U64, active: BOOL, destination: DESTINATION });
48
48
  export function claimSettingsToScVal(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorandomains/owner",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Run your Soran namespace on Stellar: configure claims, issue names and manage namespace policies.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,7 @@
13
13
  "typecheck": "tsc --noEmit",
14
14
  "prepublishOnly": "tsc",
15
15
  "check:browser": "esbuild dist/index.js --bundle --platform=browser --external:@stellar/stellar-sdk --outfile=browser-check.js --legal-comments=none --log-level=error && node -e \"const fs=require('fs');const s=fs.readFileSync('browser-check.js','utf8');fs.unlinkSync('browser-check.js');if(/\\bBuffer\\b/.test(s)){console.error('FAIL: bare Buffer reference in browser bundle');process.exit(1)}console.log('browser bundle clean')\"",
16
- "test": "node --import tsx test/decode-failure.test.mts && node --import tsx --test test/integration.test.mts test/native-*.test.mts"
16
+ "test": "node --import tsx test/decode-failure.test.mts && node --import tsx --test test/integration.test.mts test/native-*.test.mts && node --import tsx --test test/funding.test.mts test/funding-history.test.mts"
17
17
  },
18
18
  "license": "MIT",
19
19
  "repository": {