@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 +24 -5
- package/dist/funding-history.d.ts +25 -0
- package/dist/funding-history.js +124 -0
- package/dist/funding-owner.d.ts +45 -0
- package/dist/funding-owner.js +97 -0
- package/dist/funding-reader.d.ts +43 -0
- package/dist/funding-reader.js +98 -0
- package/dist/funding-service.d.ts +33 -0
- package/dist/funding-service.js +121 -0
- package/dist/funding-types.d.ts +102 -0
- package/dist/funding-types.js +249 -0
- package/dist/funding-user.d.ts +38 -0
- package/dist/funding-user.js +250 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +8 -2
- package/package.json +2 -2
|
@@ -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
|
@@ -13,7 +13,7 @@ export declare const DEPLOYMENTS: {
|
|
|
13
13
|
readonly testnet: {
|
|
14
14
|
readonly rpcUrl: "https://soroban-testnet.stellar.org";
|
|
15
15
|
readonly passphrase: string;
|
|
16
|
-
readonly registryId: "
|
|
16
|
+
readonly registryId: "CCSORANDPQINYOYB5SVO45WJP2LBBYKC72HHUIRVXB4J6RUZKDAUW7G4";
|
|
17
17
|
};
|
|
18
18
|
};
|
|
19
19
|
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
@@ -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
|
@@ -100,9 +100,9 @@ export const DEPLOYMENTS = {
|
|
|
100
100
|
testnet: {
|
|
101
101
|
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
102
102
|
passphrase: Networks.TESTNET,
|
|
103
|
-
// The
|
|
103
|
+
// The governed Registry. The Registrar for a namespace is discovered on
|
|
104
104
|
// chain via `registrar_of(node)` — never configured by hand.
|
|
105
|
-
registryId: "
|
|
105
|
+
registryId: "CCSORANDPQINYOYB5SVO45WJP2LBBYKC72HHUIRVXB4J6RUZKDAUW7G4",
|
|
106
106
|
},
|
|
107
107
|
};
|
|
108
108
|
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
@@ -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";
|