@sorandomains/owner 0.8.0 → 0.9.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 CHANGED
@@ -1,14 +1,14 @@
1
1
  # @sorandomains/owner
2
2
 
3
3
 
4
- > Native claim testnet deployment verified at ledger 4534629 on 6 September 2026 (12:12 UTC). See the
4
+ > Governed testnet migration sealed at ledger 4604192 on 10 September 2026. See the
5
5
  > [release status](https://docs.soran.domains/reference/release-status) for package and service availability.
6
6
 
7
7
 
8
8
  Version 0.8.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
9
9
  and labels are validated before lowercase normalization; Unicode lookalikes are
10
10
  rejected. Writes continue to target the owning Registry/Registrar/Resolver. Universal
11
- Lookup is the read entry point in `@sorandomains/lookup` 0.8.0.
11
+ Lookup is the read entry point in `@sorandomains/lookup` 0.10.0.
12
12
 
13
13
  Ownership, issuer, holder and treasury inputs remain G/C account or contract
14
14
  addresses. Muxed M addresses are payment destinations only: issue the name to its
@@ -91,11 +91,11 @@ Read the [native claim APIs, security boundaries and complete signup flow](https
91
91
 
92
92
  ## Verified testnet deployment
93
93
 
94
- Verified on 6 September 2026 at ledger **4534629** (12:12 UTC). Network passphrase: `Test SDF Network ; September 2015`.
94
+ See the [deployment manifest](../../deployments/testnet.json) for confirmed code hashes, transaction receipts and verification scope. Network passphrase: `Test SDF Network ; September 2015`.
95
95
 
96
96
  | Contract | Address |
97
97
  |---|---|
98
- | Registry | `CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK` |
98
+ | Registry | `CCSORANDPQINYOYB5SVO45WJP2LBBYKC72HHUIRVXB4J6RUZKDAUW7G4` |
99
99
 
100
100
  Mainnet has no deployment preset. Custom networks must supply their own verified
101
101
  addresses. Universal Lookup upgrades remain immediately executable; an address
@@ -121,3 +121,22 @@ 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
+
126
+ ## Namespace sponsorship
127
+
128
+ This package supports on-chain funding and sponsored actions on Stellar testnet.
129
+ `SoranFunding` manages an owner's deposit, spending limits, pause controls and withdrawals.
130
+ `SoranSponsorship` builds and checks an exact sponsored action, while `FundingServiceClient`
131
+ requests quotes and recovers transaction outcomes from a compatible service.
132
+
133
+ The Soran service uses fixed quotes based on live Stellar fee estimates. A successful action
134
+ charges the agreed amount; Soran retains any difference from the actual network fee.
135
+ There is no separate refund transaction or later debit. Failed actions do not debit the
136
+ namespace's funding position. A separate username price remains payable by the claimant.
137
+
138
+ User authorization remains required. A wallet must support Soroban authorization-entry
139
+ signing; a transaction-only wallet cannot silently switch to a user-paid transaction.
140
+ Always preserve the canonical quote before submission and recover its status after a timeout.
141
+ Funding balances and permissions are verified on chain, and terminal outcomes are checked
142
+ against trusted Stellar RPC/history providers.
@@ -0,0 +1,25 @@
1
+ import { rpc } from "@stellar/stellar-sdk";
2
+ import { type SponsorQuote } from "./funding-types.js";
3
+ /** Receipt evidence is retained for audit; its bytes alone are not a chain-inclusion proof. */
4
+ export type FundingReceipt = {
5
+ transactionHash: string;
6
+ envelopeHash: string;
7
+ envelopeXdr: string;
8
+ resultXdr: string;
9
+ ledger: number;
10
+ observedLedger: number;
11
+ status: "success" | "failed";
12
+ provider: "stellar-rpc" | "sdf-testnet-horizon";
13
+ };
14
+ export interface SponsorHistory {
15
+ find(hash: string, quote: SponsorQuote, latestLedger: number, passphrase: string): Promise<FundingReceipt | null>;
16
+ }
17
+ /** Input must come from the caller's trusted chain provider, never a pasted/API receipt. */
18
+ export declare function verifySponsorHistory(value: unknown, quote: SponsorQuote, expectedHash: string, latestLedger: number, passphrase: string, provider?: FundingReceipt["provider"]): FundingReceipt | null;
19
+ export declare function sponsorRpcReceipt(result: rpc.Api.GetSuccessfulTransactionResponse | rpc.Api.GetFailedTransactionResponse, quote: SponsorQuote, hash: string, passphrase: string): FundingReceipt;
20
+ /** The fixed provider is trusted for canonical inclusion, just as Stellar RPC is. */
21
+ export declare class TestnetSponsorHistory implements SponsorHistory {
22
+ private request;
23
+ constructor(request?: typeof fetch);
24
+ find(hash: string, quote: SponsorQuote, latestLedger: number, passphrase: string): Promise<FundingReceipt | null>;
25
+ }
@@ -0,0 +1,124 @@
1
+ import { Address, FeeBumpTransaction, Networks, Transaction, TransactionBuilder, xdr, hash, } from "@stellar/stellar-sdk";
2
+ import { hex } from "./native-codec.js";
3
+ import { FundingError, sponsorQuoteToScVal, } from "./funding-types.js";
4
+ /** Input must come from the caller's trusted chain provider, never a pasted/API receipt. */
5
+ export function verifySponsorHistory(value, quote, expectedHash, latestLedger, passphrase, provider = "sdf-testnet-horizon") {
6
+ const r = value;
7
+ if (passphrase !== Networks.TESTNET ||
8
+ quote.network !== hex(hash(new TextEncoder().encode(passphrase))) ||
9
+ !/^[a-f0-9]{64}$/.test(expectedHash) ||
10
+ !Number.isInteger(latestLedger) ||
11
+ latestLedger < 1 ||
12
+ latestLedger > 0xffff_ffff ||
13
+ !r ||
14
+ typeof r !== "object" ||
15
+ typeof r.hash !== "string" ||
16
+ !Number.isInteger(r.ledger) ||
17
+ r.ledger < quote.issuedLedger ||
18
+ r.ledger > latestLedger ||
19
+ typeof r.successful !== "boolean" ||
20
+ typeof r.envelope_xdr !== "string" ||
21
+ r.envelope_xdr.length > 200000 ||
22
+ typeof r.result_xdr !== "string" ||
23
+ r.result_xdr.length > 200000)
24
+ return null;
25
+ try {
26
+ const envelope = TransactionBuilder.fromXDR(r.envelope_xdr, passphrase);
27
+ const tx = envelope instanceof FeeBumpTransaction
28
+ ? envelope.innerTransaction
29
+ : envelope;
30
+ if (!(tx instanceof Transaction) ||
31
+ hex(envelope.hash()) !== r.hash ||
32
+ hex(tx.hash()) !== expectedHash)
33
+ return null;
34
+ const op = tx.operations[0];
35
+ if (tx.source !== quote.relayer ||
36
+ tx.operations.length !== 1 ||
37
+ op.type !== "invokeHostFunction" ||
38
+ (op.source && op.source !== quote.relayer) ||
39
+ op.func.type !== "hostFunctionTypeInvokeContract" ||
40
+ Address.fromScAddress(op.func.invokeContract.contractAddress).toString() !== quote.funding ||
41
+ op.func.invokeContract.functionName.toString() !==
42
+ "sponsor_" + quote.action ||
43
+ op.func.invokeContract.args[0]?.toXDR("base64") !==
44
+ sponsorQuoteToScVal(quote).toXDR("base64"))
45
+ return null;
46
+ const result = xdr.TransactionResult.fromXDR(r.result_xdr, "base64").result;
47
+ let outcome;
48
+ if (envelope instanceof FeeBumpTransaction) {
49
+ if (result.type !== "txFeeBumpInnerSuccess" &&
50
+ result.type !== "txFeeBumpInnerFailed")
51
+ return null;
52
+ if (hex(result.innerResultPair.transactionHash.value) !== expectedHash)
53
+ return null;
54
+ outcome = result.innerResultPair.result.result.type;
55
+ if ((result.type === "txFeeBumpInnerSuccess") !==
56
+ (outcome === "txSuccess"))
57
+ return null;
58
+ }
59
+ else
60
+ outcome = result.type;
61
+ if (!["txSuccess", "txFailed"].includes(outcome) ||
62
+ r.successful !== (outcome === "txSuccess"))
63
+ return null;
64
+ return {
65
+ transactionHash: expectedHash,
66
+ envelopeHash: r.hash,
67
+ envelopeXdr: r.envelope_xdr,
68
+ resultXdr: r.result_xdr,
69
+ ledger: r.ledger,
70
+ observedLedger: latestLedger,
71
+ status: r.successful ? "success" : "failed",
72
+ provider,
73
+ };
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ export function sponsorRpcReceipt(result, quote, hash, passphrase) {
80
+ try {
81
+ const envelope = TransactionBuilder.fromXDR(result.envelopeXdr, passphrase);
82
+ if (![hash, hex(envelope.hash())].includes(result.txHash))
83
+ throw Error();
84
+ const receipt = verifySponsorHistory({
85
+ hash: hex(envelope.hash()),
86
+ ledger: result.ledger,
87
+ successful: result.status === "SUCCESS",
88
+ envelope_xdr: envelope.toXDR(),
89
+ result_xdr: result.resultXdr.toXDR("base64"),
90
+ }, quote, hash, result.latestLedger, passphrase, "stellar-rpc");
91
+ if (receipt)
92
+ return receipt;
93
+ }
94
+ catch {
95
+ /* malformed or unrelated terminal data stays uncertain */
96
+ }
97
+ throw new FundingError("Receipt does not prove the reviewed sponsorship transaction", "pending", hash);
98
+ }
99
+ /** The fixed provider is trusted for canonical inclusion, just as Stellar RPC is. */
100
+ export class TestnetSponsorHistory {
101
+ request;
102
+ constructor(request = (input, init) => globalThis.fetch(input, init)) {
103
+ this.request = request;
104
+ }
105
+ async find(hash, quote, latestLedger, passphrase) {
106
+ if (passphrase !== Networks.TESTNET || !/^[a-f0-9]{64}$/.test(hash))
107
+ throw new FundingError("Invalid sponsorship history request", "unavailable");
108
+ const options = {
109
+ credentials: "omit",
110
+ redirect: "error",
111
+ cache: "no-store",
112
+ signal: AbortSignal.timeout(10000),
113
+ };
114
+ const response = await this.request(`https://horizon-testnet.stellar.org/transactions/${hash}`, options);
115
+ if (response.status === 404)
116
+ return null;
117
+ if (!response.ok)
118
+ throw new FundingError("Sponsorship history unavailable; preserve the quote reference", "unavailable");
119
+ const body = await response.text();
120
+ if (body.length > 600000)
121
+ throw new FundingError("Sponsorship history response is too large", "unavailable");
122
+ return verifySponsorHistory(JSON.parse(body), quote, hash, latestLedger, passphrase);
123
+ }
124
+ }
@@ -0,0 +1,45 @@
1
+ import { FundingReader, type FundingOptions } from "./funding-reader.js";
2
+ import { type SponsorPolicy } from "./funding-types.js";
3
+ import { type NativeSigner, type NativeWriteOptions } from "./native-transport.js";
4
+ /** Direct owner operations work even when the sponsorship service is offline. */
5
+ export declare class SoranFunding extends FundingReader {
6
+ private context;
7
+ private tail;
8
+ constructor(options: FundingOptions & {
9
+ signer: NativeSigner;
10
+ maxNetworkFeeStroops: bigint;
11
+ });
12
+ private serialize;
13
+ private write;
14
+ open(namespace: string, options?: NativeWriteOptions): Promise<{
15
+ hash: string;
16
+ ledger: number;
17
+ value: unknown;
18
+ }>;
19
+ private controller;
20
+ deposit(id: string, amount: bigint, options?: NativeWriteOptions): Promise<{
21
+ hash: string;
22
+ ledger: number;
23
+ value: unknown;
24
+ }>;
25
+ setPolicy(id: string, policy: SponsorPolicy, options?: NativeWriteOptions): Promise<{
26
+ hash: string;
27
+ ledger: number;
28
+ value: unknown;
29
+ }>;
30
+ setPaused(id: string, paused: boolean, options?: NativeWriteOptions): Promise<{
31
+ hash: string;
32
+ ledger: number;
33
+ value: unknown;
34
+ }>;
35
+ withdraw(id: string, amount: bigint, recipient: string, options?: NativeWriteOptions): Promise<{
36
+ hash: string;
37
+ ledger: number;
38
+ value: unknown;
39
+ }>;
40
+ close(id: string, options?: NativeWriteOptions): Promise<{
41
+ hash: string;
42
+ ledger: number;
43
+ value: unknown;
44
+ }>;
45
+ }
@@ -0,0 +1,97 @@
1
+ import { Asset } from "@stellar/stellar-sdk";
2
+ import { FundingReader } from "./funding-reader.js";
3
+ import { FundingError, sponsorPolicyToScVal, } from "./funding-types.js";
4
+ import { address, namespaceNode, sc, unhex } from "./native-codec.js";
5
+ import { sendNative, } from "./native-transport.js";
6
+ /** Direct owner operations work even when the sponsorship service is offline. */
7
+ export class SoranFunding extends FundingReader {
8
+ context;
9
+ tail = Promise.resolve();
10
+ constructor(options) {
11
+ super(options);
12
+ if (options.maxNetworkFeeStroops <= 0n)
13
+ throw new FundingError("Choose a positive network fee limit");
14
+ const context = {
15
+ registryId: this.registryId,
16
+ passphrase: this.passphrase,
17
+ server: this.server,
18
+ signer: options.signer,
19
+ fee: "100",
20
+ timeoutSecs: 60,
21
+ maxFeeStroops: options.maxNetworkFeeStroops,
22
+ read: async (c, m, a) => (await this.read(c, m, a)).value,
23
+ readWithLedger: (c, m, a) => this.read(c, m, a),
24
+ serialize: (work) => this.serialize(work),
25
+ };
26
+ this.context = context;
27
+ }
28
+ serialize(work) {
29
+ const next = this.tail.then(work, work);
30
+ this.tail = next.catch(() => undefined);
31
+ return next;
32
+ }
33
+ async write(method, args, controller, options, children = []) {
34
+ const source = address(await this.context.signer.publicKey(), "account", "owner wallet");
35
+ if (source !== controller)
36
+ throw new FundingError("Connect the funding position's controller wallet", "authorization");
37
+ await this.configuration();
38
+ return this.serialize(() => sendNative(this.context, {
39
+ source,
40
+ contract: this.fundingId,
41
+ method,
42
+ args,
43
+ sourceInvocation: {
44
+ contract: this.fundingId,
45
+ method,
46
+ args,
47
+ children,
48
+ },
49
+ maxFeeStroops: this.context.maxFeeStroops,
50
+ }, options));
51
+ }
52
+ async open(namespace, options = {}) {
53
+ const node = sc.bytes(namespaceNode(namespace));
54
+ const owner = address((await this.read(this.registryId, "owner_of", [node])).value, "identity", "namespace owner");
55
+ return this.write("open_fund", [node], owner, options);
56
+ }
57
+ async controller(id) {
58
+ const f = await this.position(id);
59
+ if (!f)
60
+ throw new FundingError("Funding position does not exist");
61
+ return f.controller;
62
+ }
63
+ async deposit(id, amount, options = {}) {
64
+ if (amount <= 0n)
65
+ throw new FundingError("Deposit must be positive");
66
+ const controller = await this.controller(id);
67
+ return this.write("deposit", [sc.bytes(unhex(id)), sc.i128(amount)], controller, options, [
68
+ {
69
+ contract: Asset.native().contractId(this.passphrase),
70
+ method: "transfer",
71
+ args: [
72
+ sc.address(controller),
73
+ sc.address(this.fundingId),
74
+ sc.i128(amount),
75
+ ],
76
+ },
77
+ ]);
78
+ }
79
+ async setPolicy(id, policy, options = {}) {
80
+ return this.write("set_policy", [sc.bytes(unhex(id)), sponsorPolicyToScVal(policy)], await this.controller(id), options);
81
+ }
82
+ async setPaused(id, paused, options = {}) {
83
+ return this.write("set_paused", [sc.bytes(unhex(id)), sc.bool(paused)], await this.controller(id), options);
84
+ }
85
+ async withdraw(id, amount, recipient, options = {}) {
86
+ if (amount <= 0n || recipient === this.fundingId)
87
+ throw new FundingError("Invalid withdrawal");
88
+ return this.write("withdraw", [
89
+ sc.bytes(unhex(id)),
90
+ sc.i128(amount),
91
+ sc.address(address(recipient, "identity", "withdrawal recipient")),
92
+ ], await this.controller(id), options);
93
+ }
94
+ async close(id, options = {}) {
95
+ return this.write("close_fund", [sc.bytes(unhex(id))], await this.controller(id), options);
96
+ }
97
+ }
@@ -0,0 +1,43 @@
1
+ import { rpc, xdr } from "@stellar/stellar-sdk";
2
+ import { type SponsorQuote } from "./funding-types.js";
3
+ export type FundingOptions = {
4
+ fundingId: string;
5
+ registryId: string;
6
+ lookupId: string;
7
+ primaryId: string;
8
+ rpcUrl?: string;
9
+ networkPassphrase?: string;
10
+ server?: rpc.Server;
11
+ };
12
+ export declare class FundingReader {
13
+ readonly fundingId: string;
14
+ readonly registryId: string;
15
+ readonly lookupId: string;
16
+ readonly primaryId: string;
17
+ readonly passphrase: string;
18
+ readonly server: rpc.Server;
19
+ readonly networkId: string;
20
+ constructor(options: FundingOptions);
21
+ read(contract: string, method: string, args: xdr.ScVal[], minimumLedger?: number): Promise<{
22
+ value: unknown;
23
+ ledger: number;
24
+ }>;
25
+ configuration(): Promise<{
26
+ ledger: number;
27
+ registry: string;
28
+ nativeAsset: string;
29
+ lookup: string;
30
+ primary: string;
31
+ governance: string;
32
+ paused: boolean;
33
+ revision: bigint;
34
+ }>;
35
+ currentPosition(namespace: string): Promise<{
36
+ id: string;
37
+ position: import("./funding-types.js").FundingPosition | null;
38
+ }>;
39
+ position(id: string): Promise<import("./funding-types.js").FundingPosition | null>;
40
+ policy(id: string): Promise<import("./funding-types.js").SponsorPolicy | null>;
41
+ actorState(id: string, actor: string): Promise<import("./funding-types.js").SponsorActorState>;
42
+ validateQuote(quote: SponsorQuote): Promise<void>;
43
+ }
@@ -0,0 +1,98 @@
1
+ import { Account, Asset, Contract, Networks, StrKey, TransactionBuilder, hash, rpc, scValToNative, } from "@stellar/stellar-sdk";
2
+ import { address, hex, namespaceNode, sc, unhex, utf8, } from "./native-codec.js";
3
+ import { FundingError, fundingConfigurationFromNative, fundingPositionFromNative, fundingPositionId, sponsorActorFromNative, sponsorPolicyFromNative, sponsorQuoteToScVal, } from "./funding-types.js";
4
+ export class FundingReader {
5
+ fundingId;
6
+ registryId;
7
+ lookupId;
8
+ primaryId;
9
+ passphrase;
10
+ server;
11
+ networkId;
12
+ constructor(options) {
13
+ this.fundingId = address(options.fundingId, "contract", "funding deployment");
14
+ this.registryId = address(options.registryId, "contract", "Registry");
15
+ this.lookupId = address(options.lookupId, "contract", "Lookup");
16
+ this.primaryId = address(options.primaryId, "contract", "Primary");
17
+ this.passphrase = options.networkPassphrase ?? Networks.TESTNET;
18
+ if (this.passphrase !== Networks.TESTNET)
19
+ throw new FundingError("Funding V1 is currently restricted to testnet");
20
+ this.networkId = hex(hash(utf8(this.passphrase)));
21
+ this.server =
22
+ options.server ??
23
+ new rpc.Server(options.rpcUrl ?? "https://soroban-testnet.stellar.org");
24
+ }
25
+ async read(contract, method, args, minimumLedger = 1) {
26
+ const source = new Account(StrKey.encodeEd25519PublicKey(new Uint8Array(32)), "0");
27
+ const tx = new TransactionBuilder(source, {
28
+ fee: "100",
29
+ networkPassphrase: this.passphrase,
30
+ })
31
+ .addOperation(new Contract(contract).call(method, ...args))
32
+ .setTimeout(30)
33
+ .build();
34
+ const result = await this.server.simulateTransaction(tx, undefined, undefined, false);
35
+ if (!rpc.Api.isSimulationSuccess(result) ||
36
+ !result.result ||
37
+ !Number.isSafeInteger(result.latestLedger) ||
38
+ result.latestLedger < minimumLedger)
39
+ throw new FundingError("Cannot verify funding contract state: " +
40
+ ("error" in result ? result.error : "missing or stale response"), "unavailable");
41
+ return {
42
+ value: scValToNative(result.result.retval),
43
+ ledger: result.latestLedger,
44
+ };
45
+ }
46
+ async configuration() {
47
+ const version = await this.read(this.fundingId, "version", []);
48
+ if (version.value !== 1)
49
+ throw new FundingError("Unsupported Funding contract version", "unavailable");
50
+ const r = await this.read(this.fundingId, "configuration", [], version.ledger);
51
+ const c = fundingConfigurationFromNative(r.value);
52
+ if (c.registry !== this.registryId ||
53
+ c.lookup !== this.lookupId ||
54
+ c.primary !== this.primaryId ||
55
+ c.nativeAsset !== Asset.native().contractId(this.passphrase))
56
+ throw new FundingError("Funding contract anchors differ from the configured deployment", "authorization");
57
+ return { ...c, ledger: r.ledger };
58
+ }
59
+ async currentPosition(namespace) {
60
+ await this.configuration();
61
+ const node = hex(namespaceNode(namespace));
62
+ const epoch = await this.read(this.registryId, "owner_epoch", [
63
+ sc.bytes(unhex(node)),
64
+ ]);
65
+ if (typeof epoch.value !== "bigint" || epoch.value < 1n)
66
+ throw new FundingError("Namespace ownership epoch unavailable", "unavailable");
67
+ const id = fundingPositionId(node, epoch.value);
68
+ return { id, position: await this.position(id) };
69
+ }
70
+ async position(id) {
71
+ const r = await this.read(this.fundingId, "fund", [sc.bytes(unhex(id))]);
72
+ if (r.value === null || r.value === undefined)
73
+ return null;
74
+ const position = fundingPositionFromNative(r.value);
75
+ if (fundingPositionId(position.namespace, position.ownerEpoch) !== id)
76
+ throw new FundingError("Returned funding position has a different namespace or ownership epoch", "unavailable");
77
+ return position;
78
+ }
79
+ async policy(id) {
80
+ const r = await this.read(this.fundingId, "policy", [sc.bytes(unhex(id))]);
81
+ return r.value === null || r.value === undefined
82
+ ? null
83
+ : sponsorPolicyFromNative(r.value);
84
+ }
85
+ async actorState(id, actor) {
86
+ const r = await this.read(this.fundingId, "actor_state", [
87
+ sc.bytes(unhex(id)),
88
+ sc.address(address(actor, "identity", "actor")),
89
+ ]);
90
+ return sponsorActorFromNative(r.value);
91
+ }
92
+ async validateQuote(quote) {
93
+ if (quote.funding !== this.fundingId || quote.network !== this.networkId)
94
+ throw new FundingError("Quote belongs to another deployment", "authorization");
95
+ await this.configuration();
96
+ await this.read(this.fundingId, "quote_status", [sponsorQuoteToScVal(quote)], quote.issuedLedger);
97
+ }
98
+ }
@@ -0,0 +1,33 @@
1
+ import { type SponsorHistory } from "./funding-history.js";
2
+ import { type ClaimIntent } from "./native-types.js";
3
+ import { type SponsorQuote, type SponsoredAction, type SponsoredIdentityIntent } from "./funding-types.js";
4
+ import { SoranSponsorship } from "./funding-user.js";
5
+ /** HTTP transports offers and status. The contract/RPC checks still authorize spending. */
6
+ export declare class FundingServiceClient {
7
+ private client;
8
+ private request;
9
+ private history;
10
+ private base;
11
+ constructor(baseUrl: string, client: SoranSponsorship, request?: typeof fetch, history?: SponsorHistory);
12
+ private call;
13
+ quote(action: SponsoredAction, intent: ClaimIntent | SponsoredIdentityIntent, maxSponsorCharge: bigint, proof?: readonly string[]): Promise<{
14
+ request: {
15
+ quote: SponsorQuote;
16
+ intent: ClaimIntent | SponsoredIdentityIntent;
17
+ proof: readonly string[];
18
+ };
19
+ transactionXdr: any;
20
+ }>;
21
+ submit(quote: SponsorQuote, authorizedTransactionXdr: string): Promise<{
22
+ id: string;
23
+ status: "quoted" | "dispatching" | "pending" | "unknown" | "success" | "failed" | "expired";
24
+ transactionHash: string | null;
25
+ }>;
26
+ status(quote: SponsorQuote): Promise<{
27
+ id: string;
28
+ status: "quoted" | "dispatching" | "pending" | "unknown" | "success" | "failed" | "expired";
29
+ transactionHash: string | null;
30
+ }>;
31
+ private id;
32
+ private result;
33
+ }
@@ -0,0 +1,121 @@
1
+ import { TestnetSponsorHistory, sponsorRpcReceipt, } from "./funding-history.js";
2
+ import { claimIntentToScVal } from "./native-types.js";
3
+ import { FundingError, decodeSponsorQuote, sponsoredIdentityToScVal, sponsorIntentHash, } from "./funding-types.js";
4
+ /** HTTP transports offers and status. The contract/RPC checks still authorize spending. */
5
+ export class FundingServiceClient {
6
+ client;
7
+ request;
8
+ history;
9
+ base;
10
+ constructor(baseUrl, client, request = (input, init) => globalThis.fetch(input, init), history = new TestnetSponsorHistory()) {
11
+ this.client = client;
12
+ this.request = request;
13
+ this.history = history;
14
+ const url = new URL(baseUrl);
15
+ if (url.username ||
16
+ url.password ||
17
+ url.hash ||
18
+ url.search ||
19
+ (url.protocol !== "https:" &&
20
+ !(url.protocol === "http:" &&
21
+ ["localhost", "127.0.0.1"].includes(url.hostname))))
22
+ throw new FundingError("Sponsorship API must use HTTPS or a local development endpoint");
23
+ this.base = url.toString().replace(/\/$/, "");
24
+ }
25
+ async call(path, body) {
26
+ let r;
27
+ try {
28
+ r = await this.request(this.base + path, {
29
+ method: body === undefined ? "GET" : "POST",
30
+ headers: body === undefined
31
+ ? undefined
32
+ : { "content-type": "application/json" },
33
+ body: body === undefined ? undefined : JSON.stringify(body),
34
+ signal: AbortSignal.timeout(30000),
35
+ redirect: "error",
36
+ });
37
+ }
38
+ catch {
39
+ throw new FundingError("Service response unavailable. Keep your quote reference and recover its status before submitting again.", "unavailable");
40
+ }
41
+ const value = (await r.json());
42
+ if (!value || typeof value !== "object" || Array.isArray(value))
43
+ throw new FundingError("Invalid service response", "unavailable");
44
+ if (!r.ok || value.error)
45
+ throw new FundingError(typeof value.detail === "string"
46
+ ? value.detail
47
+ : "Sponsorship request stopped", value.error === "pending" ? "pending" : "unavailable", typeof value.transactionHash === "string"
48
+ ? value.transactionHash
49
+ : null);
50
+ return value;
51
+ }
52
+ async quote(action, intent, maxSponsorCharge, proof = []) {
53
+ const encoded = action === "claim"
54
+ ? claimIntentToScVal(intent)
55
+ : sponsoredIdentityToScVal(intent);
56
+ const offer = await this.call("/v1/sponsorship/quote", {
57
+ action,
58
+ intentXdr: encoded.toXDR("base64"),
59
+ proof,
60
+ });
61
+ const quote = decodeSponsorQuote(offer.quoteXdr);
62
+ if (offer.id !== quote.quoteId ||
63
+ quote.action !== action ||
64
+ quote.intentHash !== sponsorIntentHash(action, intent) ||
65
+ quote.charge > maxSponsorCharge ||
66
+ typeof offer.transactionXdr !== "string" ||
67
+ offer.transactionXdr.length > 100000)
68
+ throw new FundingError("Sponsorship offer differs from the action or charge you reviewed", "authorization");
69
+ await this.client.validateQuote(quote);
70
+ return {
71
+ request: { quote, intent, proof },
72
+ transactionXdr: offer.transactionXdr,
73
+ };
74
+ }
75
+ async submit(quote, authorizedTransactionXdr) {
76
+ this.id(quote.quoteId);
77
+ const result = await this.call("/v1/sponsorship/submit", {
78
+ quoteId: quote.quoteId,
79
+ transactionXdr: authorizedTransactionXdr,
80
+ });
81
+ return this.result(quote, result);
82
+ }
83
+ async status(quote) {
84
+ this.id(quote.quoteId);
85
+ return this.result(quote, await this.call(`/v1/sponsorship/status/${quote.quoteId}`));
86
+ }
87
+ id(id) {
88
+ if (!/^[a-f0-9]{64}$/.test(id))
89
+ throw new FundingError("Invalid quote ID");
90
+ }
91
+ async result(quote, value) {
92
+ if (value.id !== quote.quoteId ||
93
+ quote.funding !== this.client.fundingId ||
94
+ quote.network !== this.client.networkId ||
95
+ ![
96
+ "quoted",
97
+ "dispatching",
98
+ "pending",
99
+ "unknown",
100
+ "success",
101
+ "failed",
102
+ "expired",
103
+ ].includes(value.status) ||
104
+ (value.transactionHash !== null &&
105
+ !/^[a-f0-9]{64}$/.test(value.transactionHash)))
106
+ throw new FundingError("Unverifiable sponsorship status; preserve the original quote reference", "unavailable");
107
+ if (value.status === "success" || value.status === "failed") {
108
+ if (!value.transactionHash)
109
+ throw new FundingError("Missing confirmed transaction hash", "unavailable");
110
+ const result = await this.client.server.getTransaction(value.transactionHash);
111
+ const receipt = result.status === "SUCCESS" || result.status === "FAILED"
112
+ ? sponsorRpcReceipt(result, quote, value.transactionHash, this.client.passphrase)
113
+ : await this.history.find(value.transactionHash, quote, (await this.client.server.getLatestLedger()).sequence, this.client.passphrase);
114
+ // Fetch independently from a trusted chain provider. API receipt bytes or
115
+ // a persisted status alone cannot establish canonical inclusion.
116
+ if (!receipt || receipt.status !== value.status)
117
+ throw new FundingError("Service outcome is not confirmed by chain history", "pending", value.transactionHash);
118
+ }
119
+ return value;
120
+ }
121
+ }
@@ -0,0 +1,102 @@
1
+ import { xdr } from "@stellar/stellar-sdk";
2
+ import { type PaymentDestination } from "./payment.js";
3
+ import { type ClaimIntent } from "./native-types.js";
4
+ export declare class FundingError extends Error {
5
+ readonly kind: "invalid" | "unavailable" | "authorization" | "pending" | "failed";
6
+ readonly transactionHash: string | null;
7
+ constructor(message: string, kind?: "invalid" | "unavailable" | "authorization" | "pending" | "failed", transactionHash?: string | null);
8
+ }
9
+ export type SponsoredAction = "claim" | "reverse" | "primary";
10
+ export type FundingConfiguration = {
11
+ registry: string;
12
+ nativeAsset: string;
13
+ lookup: string;
14
+ primary: string;
15
+ governance: string;
16
+ paused: boolean;
17
+ revision: bigint;
18
+ };
19
+ export type FundingPosition = {
20
+ namespace: string;
21
+ ownerEpoch: bigint;
22
+ controller: string;
23
+ balance: bigint;
24
+ spentTotal: bigint;
25
+ utcDay: bigint;
26
+ spentToday: bigint;
27
+ policyRevision: bigint;
28
+ status: "paused" | "active" | "closed";
29
+ };
30
+ export type SponsorPolicy = {
31
+ claimCap: bigint;
32
+ reverseCap: bigint;
33
+ primaryCap: bigint;
34
+ dailyCap: bigint;
35
+ totalCap: bigint;
36
+ claimsPerWallet: bigint;
37
+ identityPerDay: number;
38
+ relayer: string;
39
+ quoteLifetime: number;
40
+ validUntil: bigint;
41
+ };
42
+ export type SponsorQuote = {
43
+ network: string;
44
+ funding: string;
45
+ configRevision: bigint;
46
+ fundId: string;
47
+ policyRevision: bigint;
48
+ action: SponsoredAction;
49
+ actor: string;
50
+ nonce: bigint;
51
+ quoteId: string;
52
+ intentHash: string;
53
+ registrar: string;
54
+ resolver: string;
55
+ relayer: string;
56
+ charge: bigint;
57
+ issuedLedger: number;
58
+ expiresLedger: number;
59
+ };
60
+ export type SponsoredIdentityIntent = {
61
+ name: string;
62
+ actor: string;
63
+ routingId: bigint | null;
64
+ generation: bigint;
65
+ destination: PaymentDestination;
66
+ previous: string | null;
67
+ };
68
+ export type SponsorReceipt = {
69
+ quoteId: string;
70
+ intentHash: string;
71
+ actor: string;
72
+ nonce: bigint;
73
+ action: SponsoredAction;
74
+ charge: bigint;
75
+ relayer: string;
76
+ balance: bigint;
77
+ ledger: number;
78
+ };
79
+ export type SponsorActorState = {
80
+ nonce: bigint;
81
+ claims: bigint;
82
+ utcDay: bigint;
83
+ identityToday: number;
84
+ lastReceipt: SponsorReceipt | null;
85
+ };
86
+ export declare const fundingConfigurationFromNative: (v: unknown) => FundingConfiguration;
87
+ export declare const fundingPositionFromNative: (v: unknown) => FundingPosition;
88
+ export declare const sponsorPolicyFromNative: (v: unknown) => SponsorPolicy;
89
+ export declare const sponsorQuoteFromNative: (v: unknown) => SponsorQuote;
90
+ export declare const sponsoredIdentityFromNative: (v: unknown) => SponsoredIdentityIntent;
91
+ export declare const sponsorReceiptFromNative: (v: unknown) => SponsorReceipt;
92
+ export declare const sponsorActorFromNative: (v: unknown) => SponsorActorState;
93
+ export declare const sponsoredActionToScVal: (v: SponsoredAction) => xdr.ScVal;
94
+ export declare function sponsorPolicyToScVal(v: SponsorPolicy): xdr.ScVal;
95
+ export declare function sponsorQuoteToScVal(v: SponsorQuote): xdr.ScVal;
96
+ export declare const sponsoredIdentityToScVal: (v: SponsoredIdentityIntent) => xdr.ScVal;
97
+ export declare function sponsorIntentHash(action: SponsoredAction, intent: ClaimIntent | SponsoredIdentityIntent): string;
98
+ export declare function fundingPositionId(namespace: string, ownerEpoch: bigint): string;
99
+ /** Canonical XDR is used for transport; JSON bigint coercion is never accepted. */
100
+ export declare const encodeSponsorQuote: (v: SponsorQuote) => string;
101
+ export declare function decodeSponsorQuote(encoded: string): SponsorQuote;
102
+ export declare const FUNDING_ERRORS: Record<number, string>;
@@ -0,0 +1,249 @@
1
+ import { hash, nativeToScVal, scValToNative, xdr } from "@stellar/stellar-sdk";
2
+ import { address, amount, bool, bytes32, exactObject, hex, hex32, label, paymentDestinationToScVal, sc, struct, u32, u64, unhex, } from "./native-codec.js";
3
+ import { destinationFromNative } from "./payment.js";
4
+ import { claimIntentToScVal } from "./native-types.js";
5
+ export class FundingError extends Error {
6
+ kind;
7
+ transactionHash;
8
+ constructor(message, kind = "invalid", transactionHash = null) {
9
+ super(message);
10
+ this.kind = kind;
11
+ this.transactionHash = transactionHash;
12
+ this.name = "FundingError";
13
+ }
14
+ }
15
+ const primitive = (validate, encode) => ({ encode: (v) => encode(validate(v)), decode: validate });
16
+ const U64 = primitive((v) => u64(v, "funding u64"), sc.u64), U32 = primitive((v) => u32(v, "funding u32"), sc.u32);
17
+ const MONEY = primitive((v) => amount(v, "funding amount"), sc.i128), BOOL = primitive((v) => bool(v, "funding boolean"), sc.bool);
18
+ const G = primitive((v) => address(v, "account", "funding account"), sc.address), C = primitive((v) => address(v, "contract", "funding contract"), sc.address), ID = primitive((v) => address(v, "identity", "funding controller"), sc.address);
19
+ const HEX = {
20
+ encode: (v) => sc.bytes(unhex(hex32(v, "funding hash"))),
21
+ decode: (v) => hex(bytes32(v, "funding hash")),
22
+ };
23
+ function fullName(v) {
24
+ if (typeof v !== "string")
25
+ throw new FundingError("name must be a string");
26
+ const parts = v.split(".");
27
+ if (parts.length !== 2)
28
+ throw new FundingError("name must include its namespace");
29
+ parts.forEach(label);
30
+ return v;
31
+ }
32
+ const NAME = primitive(fullName, (v) => nativeToScVal(v, { type: "string" }));
33
+ const optional = (c) => ({
34
+ encode: (v) => (v === null ? xdr.ScVal.scvVoid() : c.encode(v)),
35
+ decode: (v) => (v === null || v === undefined ? null : c.decode(v)),
36
+ });
37
+ const enumeration = (tags) => ({
38
+ encode: (v) => {
39
+ if (typeof v !== "string" || !Object.hasOwn(tags, v))
40
+ throw new FundingError("unknown funding enum");
41
+ return xdr.ScVal.scvVec([sc.symbol(tags[v])]);
42
+ },
43
+ decode: (v) => {
44
+ if (!Array.isArray(v) || v.length !== 1)
45
+ throw new FundingError("malformed funding enum");
46
+ const pair = Object.entries(tags).find(([, tag]) => tag === v[0]);
47
+ if (!pair)
48
+ throw new FundingError("unknown funding enum");
49
+ return pair[0];
50
+ },
51
+ });
52
+ const ACTION = enumeration({
53
+ claim: "Claim",
54
+ reverse: "Reverse",
55
+ primary: "Primary",
56
+ });
57
+ const snake = (v) => v.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
58
+ function model(fields) {
59
+ return {
60
+ encode: (v) => {
61
+ const r = exactObject(v, Object.keys(fields), "funding input");
62
+ return struct(Object.fromEntries(Object.entries(fields).map(([k, c]) => [snake(k), c.encode(r[k])])));
63
+ },
64
+ decode: (v) => {
65
+ const r = exactObject(v, Object.keys(fields).map(snake), "funding result");
66
+ return Object.fromEntries(Object.entries(fields).map(([k, c]) => [k, c.decode(r[snake(k)])]));
67
+ },
68
+ };
69
+ }
70
+ const CONFIG = model({
71
+ registry: C,
72
+ nativeAsset: C,
73
+ lookup: C,
74
+ primary: C,
75
+ governance: ID,
76
+ paused: BOOL,
77
+ revision: U64,
78
+ });
79
+ const POSITION = model({
80
+ namespace: HEX,
81
+ ownerEpoch: U64,
82
+ controller: ID,
83
+ balance: MONEY,
84
+ spentTotal: MONEY,
85
+ utcDay: U64,
86
+ spentToday: MONEY,
87
+ policyRevision: U64,
88
+ status: enumeration({ paused: "Paused", active: "Active", closed: "Closed" }),
89
+ });
90
+ const POLICY = model({
91
+ claimCap: MONEY,
92
+ reverseCap: MONEY,
93
+ primaryCap: MONEY,
94
+ dailyCap: MONEY,
95
+ totalCap: MONEY,
96
+ claimsPerWallet: U64,
97
+ identityPerDay: U32,
98
+ relayer: G,
99
+ quoteLifetime: U32,
100
+ validUntil: U64,
101
+ });
102
+ const QUOTE = model({
103
+ network: HEX,
104
+ funding: C,
105
+ configRevision: U64,
106
+ fundId: HEX,
107
+ policyRevision: U64,
108
+ action: ACTION,
109
+ actor: ID,
110
+ nonce: U64,
111
+ quoteId: HEX,
112
+ intentHash: HEX,
113
+ registrar: C,
114
+ resolver: C,
115
+ relayer: G,
116
+ charge: MONEY,
117
+ issuedLedger: U32,
118
+ expiresLedger: U32,
119
+ });
120
+ const DEST = {
121
+ encode: (v) => paymentDestinationToScVal(v),
122
+ decode: destinationFromNative,
123
+ };
124
+ const IDENTITY = model({
125
+ name: NAME,
126
+ actor: ID,
127
+ routingId: optional(U64),
128
+ generation: U64,
129
+ destination: DEST,
130
+ previous: optional(NAME),
131
+ });
132
+ const RECEIPT = model({
133
+ quoteId: HEX,
134
+ intentHash: HEX,
135
+ actor: ID,
136
+ nonce: U64,
137
+ action: ACTION,
138
+ charge: MONEY,
139
+ relayer: G,
140
+ balance: MONEY,
141
+ ledger: U32,
142
+ });
143
+ const LAST_RECEIPT = {
144
+ encode: (v) => xdr.ScVal.scvVec(v === null
145
+ ? [sc.symbol("Empty")]
146
+ : [sc.symbol("Recorded"), RECEIPT.encode(v)]),
147
+ decode: (v) => {
148
+ if (Array.isArray(v) && v.length === 1 && v[0] === "Empty")
149
+ return null;
150
+ if (Array.isArray(v) && v.length === 2 && v[0] === "Recorded")
151
+ return RECEIPT.decode(v[1]);
152
+ throw new FundingError("malformed receipt state");
153
+ },
154
+ };
155
+ const ACTOR = model({
156
+ nonce: U64,
157
+ claims: U64,
158
+ utcDay: U64,
159
+ identityToday: U32,
160
+ lastReceipt: LAST_RECEIPT,
161
+ });
162
+ export const fundingConfigurationFromNative = (v) => CONFIG.decode(v);
163
+ export const fundingPositionFromNative = (v) => POSITION.decode(v);
164
+ export const sponsorPolicyFromNative = (v) => POLICY.decode(v);
165
+ export const sponsorQuoteFromNative = (v) => QUOTE.decode(v);
166
+ export const sponsoredIdentityFromNative = (v) => IDENTITY.decode(v);
167
+ export const sponsorReceiptFromNative = (v) => RECEIPT.decode(v);
168
+ export const sponsorActorFromNative = (v) => ACTOR.decode(v);
169
+ export const sponsoredActionToScVal = (v) => ACTION.encode(v);
170
+ export function sponsorPolicyToScVal(v) {
171
+ const value = POLICY.encode(v);
172
+ if (v.dailyCap === 0n ||
173
+ v.totalCap === 0n ||
174
+ v.dailyCap > v.totalCap ||
175
+ [v.claimCap, v.reverseCap, v.primaryCap].some((n) => n > v.dailyCap) ||
176
+ [v.claimCap, v.reverseCap, v.primaryCap].every((n) => n === 0n) ||
177
+ (v.claimCap > 0n && v.claimsPerWallet === 0n) ||
178
+ ((v.reverseCap > 0n || v.primaryCap > 0n) && v.identityPerDay === 0) ||
179
+ v.quoteLifetime < 1 ||
180
+ v.quoteLifetime > 120 ||
181
+ v.validUntil === 0n)
182
+ throw new FundingError("invalid coverage, spending limits or policy lifetime");
183
+ return value;
184
+ }
185
+ export function sponsorQuoteToScVal(v) {
186
+ const result = QUOTE.encode(v);
187
+ if (v.charge === 0n ||
188
+ v.actor === v.relayer ||
189
+ v.expiresLedger <= v.issuedLedger ||
190
+ v.expiresLedger - v.issuedLedger > 120)
191
+ throw new FundingError("invalid sponsor charge, actor or quote lifetime");
192
+ return result;
193
+ }
194
+ export const sponsoredIdentityToScVal = (v) => IDENTITY.encode(v);
195
+ export function sponsorIntentHash(action, intent) {
196
+ const encoded = action === "claim"
197
+ ? claimIntentToScVal(intent)
198
+ : sponsoredIdentityToScVal(intent);
199
+ return hex(hash(xdr.ScVal.scvVec([sc.symbol("sponsor_" + action), encoded]).toXDR()));
200
+ }
201
+ export function fundingPositionId(namespace, ownerEpoch) {
202
+ return hex(hash(xdr.ScVal.scvVec([
203
+ sc.symbol("fund_v1"),
204
+ HEX.encode(namespace),
205
+ U64.encode(ownerEpoch),
206
+ ]).toXDR()));
207
+ }
208
+ /** Canonical XDR is used for transport; JSON bigint coercion is never accepted. */
209
+ export const encodeSponsorQuote = (v) => sponsorQuoteToScVal(v).toXDR("base64");
210
+ export function decodeSponsorQuote(encoded) {
211
+ if (typeof encoded !== "string" || encoded.length > 16_384)
212
+ throw new FundingError("invalid sponsor quote size");
213
+ const q = sponsorQuoteFromNative(scValToNative(xdr.ScVal.fromXDR(encoded, "base64")));
214
+ sponsorQuoteToScVal(q);
215
+ return q;
216
+ }
217
+ export const FUNDING_ERRORS = {
218
+ 1: "Not initialized",
219
+ 2: "Unsupported network",
220
+ 3: "Invalid configuration",
221
+ 4: "Funding position not found",
222
+ 5: "Funding position already exists",
223
+ 6: "Funding position closed",
224
+ 7: "Namespace ownership changed",
225
+ 8: "Contract state unavailable",
226
+ 9: "Invalid amount",
227
+ 10: "Insufficient sponsorship funds",
228
+ 11: "Invalid spending policy",
229
+ 12: "Spending policy not configured",
230
+ 13: "Sponsorship paused",
231
+ 14: "Quote is stale",
232
+ 15: "Quote expired",
233
+ 16: "Invalid quote",
234
+ 17: "Spending limit reached",
235
+ 18: "Wallet allowance reached",
236
+ 19: "Request already used",
237
+ 20: "Action is not sponsored",
238
+ 21: "Namespace contract changed",
239
+ 22: "Action differs from approval",
240
+ 23: "Action already completed",
241
+ 24: "Name operation failed",
242
+ 25: "Sponsor payment failed",
243
+ 26: "Amount or counter overflow",
244
+ 27: "Re-entry refused",
245
+ 28: "Invalid name",
246
+ 29: "Name inactive",
247
+ 30: "Name state changed",
248
+ 31: "Upgrade does not match",
249
+ };
@@ -0,0 +1,38 @@
1
+ import { Transaction, authorizeEntry, xdr } from "@stellar/stellar-sdk";
2
+ import { FundingReader } from "./funding-reader.js";
3
+ import { type SponsorQuote, type SponsoredIdentityIntent } from "./funding-types.js";
4
+ import { type ClaimIntent } from "./native-types.js";
5
+ import { type NativeInvocation } from "./native-auth.js";
6
+ export type SponsorRequest = {
7
+ quote: SponsorQuote;
8
+ intent: ClaimIntent | SponsoredIdentityIntent;
9
+ proof?: readonly string[];
10
+ };
11
+ export type SponsorPlan = {
12
+ request: SponsorRequest;
13
+ method: string;
14
+ args: xdr.ScVal[];
15
+ actorInvocation: NativeInvocation;
16
+ relayerInvocation: NativeInvocation;
17
+ eligibility?: {
18
+ account: string;
19
+ invocation: NativeInvocation;
20
+ latestLedger: number;
21
+ maxExpirationLedger: number;
22
+ };
23
+ maxNetworkFee: bigint;
24
+ };
25
+ /** Build from the app's reviewed intent and pinned shared contracts, never a remote invocation tree. */
26
+ export declare function sponsorPlan(request: SponsorRequest, lookup: string, primary: string, maxNetworkFee: bigint, eligibilityAccount?: string): SponsorPlan;
27
+ export declare function validateSponsorTransaction(tx: Transaction, p: SponsorPlan, latestLedger: number, signed?: boolean): xdr.SorobanAuthorizationEntry;
28
+ export declare function replaceSponsorAuth(tx: Transaction, auth: xdr.SorobanAuthorizationEntry[], passphrase: string): Transaction;
29
+ export declare class SoranSponsorship extends FundingReader {
30
+ plan(request: SponsorRequest, maxNetworkFee: bigint): Promise<SponsorPlan>;
31
+ prepare(request: SponsorRequest, maxNetworkFee: bigint, inclusionFee?: bigint): Promise<{
32
+ transaction: Transaction;
33
+ plan: SponsorPlan;
34
+ ledger: number;
35
+ }>;
36
+ /** Sign only the caller's G authorization. Wallet adapters retain custody of keys. */
37
+ authorize(request: SponsorRequest, encoded: string, signer: Parameters<typeof authorizeEntry>[1], maxSponsorCharge: bigint, maxNetworkFee: bigint, eligibilityXdr?: string): Promise<string>;
38
+ }
@@ -0,0 +1,250 @@
1
+ import { Address, Contract, Operation, Transaction, TransactionBuilder, authorizeEntry, rpc, nativeToScVal, xdr, } from "@stellar/stellar-sdk";
2
+ import { FundingReader } from "./funding-reader.js";
3
+ import { FundingError, sponsorIntentHash, sponsorQuoteToScVal, sponsoredIdentityToScVal, } from "./funding-types.js";
4
+ import { claimIntentToScVal, claimLabelToScVal, claimQuoteFromNative, } from "./native-types.js";
5
+ import { address, paymentDestinationToScVal, sc, unhex, } from "./native-codec.js";
6
+ import { authorizedInvocation, validateEligibilityAuthorization, } from "./native-auth.js";
7
+ /** Build from the app's reviewed intent and pinned shared contracts, never a remote invocation tree. */
8
+ export function sponsorPlan(request, lookup, primary, maxNetworkFee, eligibilityAccount) {
9
+ const { quote: q, intent } = request;
10
+ const method = "sponsor_" + q.action;
11
+ if (q.intentHash !== sponsorIntentHash(q.action, intent) ||
12
+ maxNetworkFee <= 0n)
13
+ throw new FundingError("Quote differs from the requested action", "authorization");
14
+ let args, target;
15
+ if (q.action === "claim") {
16
+ const i = intent, encoded = claimIntentToScVal(i), proof = request.proof ?? [];
17
+ if (i.claimant !== q.actor ||
18
+ i.resolver !== q.resolver ||
19
+ i.context.registrar !== q.registrar ||
20
+ i.context.network !== q.network ||
21
+ proof.length > 64)
22
+ throw new FundingError("Claim route or actor differs from quote", "authorization");
23
+ args = [
24
+ sponsorQuoteToScVal(q),
25
+ encoded,
26
+ xdr.ScVal.scvVec(proof.map((p) => sc.bytes(unhex(p)))),
27
+ ];
28
+ const children = [];
29
+ if (i.feeAmount > 0n)
30
+ children.push({
31
+ contract: i.feeToken,
32
+ method: "transfer",
33
+ args: [
34
+ sc.address(i.claimant),
35
+ sc.address(i.feeRecipient),
36
+ sc.i128(i.feeAmount),
37
+ ],
38
+ });
39
+ children.push({
40
+ contract: i.resolver,
41
+ method: "initialize_destination",
42
+ args: [
43
+ claimLabelToScVal(i.label),
44
+ sc.address(i.claimant),
45
+ sc.u64(i.expectedGeneration === null ? 0n : i.expectedGeneration + 1n),
46
+ paymentDestinationToScVal(i.destination),
47
+ ],
48
+ });
49
+ target = {
50
+ contract: q.registrar,
51
+ method: "claim",
52
+ args: [encoded],
53
+ children,
54
+ };
55
+ }
56
+ else {
57
+ const i = intent;
58
+ if (i.actor !== q.actor || request.proof?.length)
59
+ throw new FundingError("Identity actor or proof differs from action");
60
+ args = [sponsorQuoteToScVal(q), sponsoredIdentityToScVal(i)];
61
+ const reverse = q.action === "reverse";
62
+ target =
63
+ i.routingId === null
64
+ ? {
65
+ contract: reverse ? q.resolver : primary,
66
+ method: reverse ? "set_reverse" : "set_primary",
67
+ args: [
68
+ sc.address(i.actor),
69
+ nativeToScVal(i.name, { type: "string" }),
70
+ ],
71
+ }
72
+ : {
73
+ contract: lookup,
74
+ method: reverse ? "set_reverse_muxed" : "set_primary_muxed",
75
+ args: [
76
+ sc.address(i.actor),
77
+ sc.u64(i.routingId),
78
+ nativeToScVal(i.name, { type: "string" }),
79
+ ],
80
+ };
81
+ }
82
+ const plan = {
83
+ request,
84
+ method,
85
+ args,
86
+ actorInvocation: { contract: q.funding, method, args, children: [target] },
87
+ relayerInvocation: { contract: q.funding, method, args },
88
+ maxNetworkFee,
89
+ };
90
+ if (eligibilityAccount) {
91
+ if (q.action !== "claim" ||
92
+ eligibilityAccount === q.actor ||
93
+ eligibilityAccount === q.relayer)
94
+ throw new FundingError("Eligibility account must be separate from user and relayer");
95
+ plan.eligibility = {
96
+ account: address(eligibilityAccount, "account", "eligibility"),
97
+ invocation: {
98
+ contract: q.registrar,
99
+ method: "claim",
100
+ args: [claimIntentToScVal(intent)],
101
+ },
102
+ latestLedger: q.issuedLedger,
103
+ maxExpirationLedger: q.expiresLedger,
104
+ };
105
+ }
106
+ return plan;
107
+ }
108
+ function same(a, b) {
109
+ return a.toXDR("base64") === authorizedInvocation(b).toXDR("base64");
110
+ }
111
+ export function validateSponsorTransaction(tx, p, latestLedger, signed = false) {
112
+ const q = p.request.quote;
113
+ if (tx.source !== q.relayer ||
114
+ tx.operations.length !== 1 ||
115
+ tx.memo.type !== "none" ||
116
+ BigInt(tx.fee) > p.maxNetworkFee ||
117
+ tx.signatures.length ||
118
+ latestLedger < q.issuedLedger ||
119
+ latestLedger >= q.expiresLedger)
120
+ throw new FundingError("Unapproved source, operation, fee, signature or ledger", "authorization");
121
+ const op = tx.operations[0];
122
+ if (op.type !== "invokeHostFunction" ||
123
+ (op.source && op.source !== q.relayer) ||
124
+ op.func.type !== "hostFunctionTypeInvokeContract")
125
+ throw new FundingError("Unexpected sponsored operation");
126
+ const c = op.func.invokeContract;
127
+ if (Address.fromScAddress(c.contractAddress).toString() !== q.funding ||
128
+ c.functionName.toString() !== p.method ||
129
+ c.args.length !== p.args.length ||
130
+ c.args.some((v, n) => v.toXDR("base64") !== p.args[n].toXDR("base64")))
131
+ throw new FundingError("Transaction differs from approved request", "authorization");
132
+ const auth = op.auth ?? [];
133
+ if (auth.length !== (p.eligibility ? 3 : 2))
134
+ throw new FundingError("Unexpected sponsor authorization count", "authorization");
135
+ const sources = auth.filter((e) => e.credentials.type === "sorobanCredentialsSourceAccount");
136
+ if (sources.length !== 1 ||
137
+ !same(sources[0].rootInvocation, p.relayerInvocation))
138
+ throw new FundingError("Relayer authorization exceeds its approved role", "authorization");
139
+ const actors = auth.filter((e) => e.credentials.type === "sorobanCredentialsAddress" &&
140
+ Address.fromScAddress(e.credentials.address.address).toString() ===
141
+ q.actor);
142
+ if (actors.length !== 1 || !same(actors[0].rootInvocation, p.actorInvocation))
143
+ throw new FundingError("User authorization differs from approved action", "authorization");
144
+ const credential = actors[0].credentials;
145
+ if (credential.type !== "sorobanCredentialsAddress")
146
+ throw new FundingError("Missing actor credential");
147
+ if (signed) {
148
+ if (credential.address.signatureExpirationLedger <= latestLedger ||
149
+ credential.address.signatureExpirationLedger > q.expiresLedger ||
150
+ credential.address.signature.toXDR().length > 16384 ||
151
+ // G accounts use Stellar's native vector format. C accounts define their
152
+ // own ScVal signature; enforced simulation must run their __check_auth.
153
+ (q.actor.startsWith("G") &&
154
+ (credential.address.signature.type !== "scvVec" ||
155
+ !credential.address.signature.vec?.length)))
156
+ throw new FundingError("User authorization missing or expired", "authorization");
157
+ }
158
+ else if (credential.address.signature.type !== "scvVoid" &&
159
+ !(credential.address.signature.type === "scvVec" &&
160
+ !credential.address.signature.vec?.length))
161
+ throw new FundingError("Refusing to re-sign an authorization");
162
+ if (p.eligibility) {
163
+ const entries = auth.filter((e) => e.credentials.type === "sorobanCredentialsAddress" &&
164
+ Address.fromScAddress(e.credentials.address.address).toString() ===
165
+ p.eligibility.account);
166
+ if (entries.length !== 1)
167
+ throw new FundingError("Missing eligibility authorization");
168
+ validateEligibilityAuthorization(entries[0], { ...p.eligibility, latestLedger }, signed);
169
+ }
170
+ return actors[0];
171
+ }
172
+ export function replaceSponsorAuth(tx, auth, passphrase) {
173
+ const op = tx.operations[0];
174
+ if (op.type !== "invokeHostFunction")
175
+ throw new FundingError("Unexpected operation");
176
+ return TransactionBuilder.cloneFrom(tx, { networkPassphrase: passphrase })
177
+ .clearOperations()
178
+ .addOperation(Operation.invokeHostFunction({ func: op.func, auth }))
179
+ .build();
180
+ }
181
+ export class SoranSponsorship extends FundingReader {
182
+ async plan(request, maxNetworkFee) {
183
+ await this.validateQuote(request.quote);
184
+ let eligibility;
185
+ if (request.quote.action === "claim") {
186
+ const i = request.intent;
187
+ if (i.context.registry !== this.registryId)
188
+ throw new FundingError("Claim Registry differs from deployment");
189
+ const r = await this.read(request.quote.registrar, "claim_quote", [claimLabelToScVal(i.label), sc.address(i.claimant)], request.quote.issuedLedger);
190
+ const admission = claimQuoteFromNative(r.value).config?.settings
191
+ .admission;
192
+ if (admission?.type === "approval")
193
+ eligibility = admission.account;
194
+ }
195
+ return sponsorPlan(request, this.lookupId, this.primaryId, maxNetworkFee, eligibility);
196
+ }
197
+ async prepare(request, maxNetworkFee, inclusionFee = 100n) {
198
+ if (inclusionFee < 100n || inclusionFee > maxNetworkFee || inclusionFee > 0xffffffffn)
199
+ throw new FundingError("Invalid network inclusion fee");
200
+ const plan = await this.plan(request, maxNetworkFee);
201
+ const raw = new TransactionBuilder(await this.server.getAccount(request.quote.relayer), { fee: inclusionFee.toString(), networkPassphrase: this.passphrase })
202
+ .addOperation(new Contract(this.fundingId).call(plan.method, ...plan.args))
203
+ .setTimeout(60)
204
+ .build();
205
+ const sim = await this.server.simulateTransaction(raw, undefined, undefined, false);
206
+ if (rpc.Api.isSimulationRestore(sim))
207
+ throw new FundingError("Separate storage restoration is required; sponsorship is unavailable", "unavailable");
208
+ if (!rpc.Api.isSimulationSuccess(sim) || !sim.result)
209
+ throw new FundingError("Sponsored simulation failed", "unavailable");
210
+ const transaction = rpc.assembleTransaction(raw, sim).build();
211
+ validateSponsorTransaction(transaction, plan, sim.latestLedger);
212
+ return { transaction, plan, ledger: sim.latestLedger };
213
+ }
214
+ /** Sign only the caller's G authorization. Wallet adapters retain custody of keys. */
215
+ async authorize(request, encoded, signer, maxSponsorCharge, maxNetworkFee, eligibilityXdr) {
216
+ if (request.quote.charge > maxSponsorCharge)
217
+ throw new FundingError("Sponsor charge exceeds the reviewed amount");
218
+ address(request.quote.actor, "account", "this adapter's signing wallet");
219
+ const plan = await this.plan(request, maxNetworkFee), parsed = TransactionBuilder.fromXDR(encoded, this.passphrase);
220
+ if (!(parsed instanceof Transaction))
221
+ throw new FundingError("Fee-bump envelopes are not supported by this transport");
222
+ const latest = await this.server.getLatestLedger();
223
+ const entry = validateSponsorTransaction(parsed, plan, latest.sequence);
224
+ const signed = await authorizeEntry(entry, signer, request.quote.expiresLedger, this.passphrase);
225
+ const op = parsed.operations[0];
226
+ if (op.type !== "invokeHostFunction")
227
+ throw new FundingError("Unexpected operation");
228
+ const auth = (op.auth ?? []).map((e) => e.credentials.type === "sorobanCredentialsAddress" &&
229
+ Address.fromScAddress(e.credentials.address.address).toString() ===
230
+ request.quote.actor
231
+ ? signed
232
+ : e);
233
+ if (plan.eligibility) {
234
+ if (!eligibilityXdr || eligibilityXdr.length > 32768)
235
+ throw new FundingError("This claim requires the app's eligibility authorization");
236
+ const approval = xdr.SorobanAuthorizationEntry.fromXDR(eligibilityXdr, "base64");
237
+ validateEligibilityAuthorization(approval, {
238
+ ...plan.eligibility,
239
+ latestLedger: latest.sequence,
240
+ });
241
+ const at = auth.findIndex((e) => e.credentials.type === "sorobanCredentialsAddress" &&
242
+ Address.fromScAddress(e.credentials.address.address).toString() ===
243
+ plan.eligibility.account);
244
+ auth[at] = approval;
245
+ }
246
+ const tx = replaceSponsorAuth(parsed, auth, this.passphrase);
247
+ validateSponsorTransaction(tx, plan, latest.sequence, true);
248
+ return tx.toXDR();
249
+ }
250
+ }
package/dist/index.d.ts CHANGED
@@ -281,3 +281,9 @@ export declare class SoranOwner {
281
281
  */
282
282
  private decodeFailure;
283
283
  }
284
+ export * from "./funding-types.js";
285
+ export { FundingReader, type FundingOptions } from "./funding-reader.js";
286
+ export { SoranFunding } from "./funding-owner.js";
287
+ export { SoranSponsorship, sponsorPlan, validateSponsorTransaction, type SponsorRequest, type SponsorPlan } from "./funding-user.js";
288
+ export { FundingServiceClient } from "./funding-service.js";
289
+ export { TestnetSponsorHistory, type SponsorHistory, type FundingReceipt } from "./funding-history.js";
package/dist/index.js CHANGED
@@ -970,3 +970,9 @@ export class SoranOwner {
970
970
  return new OwnerError(`${fn} failed at inclusion (${resultCode})`, contractId, fn, null, null, hash);
971
971
  }
972
972
  }
973
+ export * from "./funding-types.js";
974
+ export { FundingReader } from "./funding-reader.js";
975
+ export { SoranFunding } from "./funding-owner.js";
976
+ export { SoranSponsorship, sponsorPlan, validateSponsorTransaction } from "./funding-user.js";
977
+ export { FundingServiceClient } from "./funding-service.js";
978
+ export { TestnetSponsorHistory } from "./funding-history.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorandomains/owner",
3
- "version": "0.8.0",
3
+ "version": "0.9.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": {