@sorandomains/owner 0.8.0-migration.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
- Preview 0.8.0-migration.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
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
+ }