spare-sdk 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +76 -2
- package/dist/add-donation.d.ts +15 -0
- package/dist/add-donation.js +143 -0
- package/dist/add-donation.js.map +1 -0
- package/dist/amount.d.ts +11 -0
- package/dist/amount.js +30 -0
- package/dist/amount.js.map +1 -0
- package/dist/app-id.d.ts +3 -0
- package/dist/app-id.js +9 -0
- package/dist/app-id.js.map +1 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +13 -0
- package/dist/constants.js.map +1 -0
- package/dist/donate-after.d.ts +33 -0
- package/dist/donate-after.js +75 -0
- package/dist/donate-after.js.map +1 -0
- package/dist/eligibility.d.ts +24 -0
- package/dist/eligibility.js +42 -0
- package/dist/eligibility.js.map +1 -0
- package/dist/fee.d.ts +5 -0
- package/dist/fee.js +17 -0
- package/dist/fee.js.map +1 -0
- package/dist/horizon.d.ts +38 -0
- package/dist/horizon.js +52 -0
- package/dist/horizon.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/ports.d.ts +37 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/signing.d.ts +10 -0
- package/dist/signing.js +14 -0
- package/dist/signing.js.map +1 -0
- package/dist/spend.d.ts +20 -0
- package/dist/spend.js +60 -0
- package/dist/spend.js.map +1 -0
- package/dist/stellar-toml.d.ts +10 -0
- package/dist/stellar-toml.js +59 -0
- package/dist/stellar-toml.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jesús Chávez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# spare-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Add a per-transaction USDC micro-donation to any Stellar app. The donation goes straight to the foundation's account; Spare never holds funds. Circle USDC only, domain-verified foundations only.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
pnpm add spare-sdk @stellar/stellar-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Classic transaction: `addDonation`
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { addDonation, HorizonAccountPort } from "spare-sdk";
|
|
13
|
+
|
|
14
|
+
const result = await addDonation(unsignedTx, {
|
|
15
|
+
foundation: { account: "G...", domain: "foundation.org" }, // must be listed in its stellar.toml
|
|
16
|
+
amount: "0.50", // or: roundUp: true (only when the purchase is USDC)
|
|
17
|
+
appId: "my-app", // <= 22 ASCII bytes, memo becomes "spare:my-app"
|
|
18
|
+
maxAmount: "5", // your cap per donation
|
|
19
|
+
accounts: new HorizonAccountPort("https://horizon.stellar.org"),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
switch (result.status) {
|
|
23
|
+
case "offered": // ask the user "Add US$<result.donation.amount>?", then sign result.transaction
|
|
24
|
+
case "not-offered": // sign the original tx; result.reason says why (e.g. "insufficient-balance")
|
|
25
|
+
case "passthrough": // signed / fee-bump / multisig / extra signers / Soroban: sign the original tx
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`offered` returns a new unsigned tx (new hash, fee raised by one operation, memo set only if it was empty and no destination requires one). Nothing is added without the user's explicit yes.
|
|
30
|
+
|
|
31
|
+
`@stellar/stellar-sdk` is a peer dependency: install it next to `spare-sdk` (one copy shared with your app).
|
|
32
|
+
|
|
33
|
+
### Round-up
|
|
34
|
+
|
|
35
|
+
Only when the whole tx is a Circle-USDC purchase (payments, `pathPaymentStrictSend`, sell offers). The SDK reports what it used, so show these instead of recomputing: `result.donation.purchase` / `purchaseStroops` (the USDC purchase) and `roundedTotal` / `roundedTotalStroops` (purchase + donation). They are set on round-up only.
|
|
36
|
+
|
|
37
|
+
- Already whole (e.g. 5.00): `round-up-already-whole`.
|
|
38
|
+
- `pathPaymentStrictReceive`, XLM or other-asset ops, or unmodelled ops in the same tx: `round-up-needs-usdc-purchase` (the real spend is unknown up front, so Spare never rounds `sendMax`).
|
|
39
|
+
|
|
40
|
+
### Reason codes
|
|
41
|
+
|
|
42
|
+
`passthrough` (tx untouched): `fee-bump`, `already-signed`, `soroban`, `multiple-sources`, `multisig` (the master key alone cannot meet the tx's threshold: medium, or high with `setOptions`/`accountMerge`), `extra-signers` (CAP-21 `extraSigners` precondition).
|
|
43
|
+
|
|
44
|
+
`not-offered`: `invalid-options` (incl. malformed `baseFee`), `invalid-app-id`, `invalid-amount`, `invalid-foundation`, `amount-over-cap`, `round-up-needs-usdc-purchase`, `round-up-already-whole`, `foundation-not-verified`, `foundation-trustline-missing`, `foundation-trustline-unauthorized`, `foundation-trustline-full` (no room under the trustline limit), `user-trustline-missing`, `user-trustline-unauthorized`, `unsupported-operation` (the tx has ops that can spend funds in ways Spare does not model, e.g. `manageBuyOffer`, `createClaimableBalance`, `liquidityPoolDeposit`, `accountMerge`), `insufficient-balance` (USDC, or XLM for the fee plus any XLM the tx sends), `account-lookup-failed`, `main-tx-not-successful`, and, from `donateAfter` only, `multisig`.
|
|
45
|
+
|
|
46
|
+
### Memo and SEP-29
|
|
47
|
+
|
|
48
|
+
If a payment, path payment or merge destination has `config.memo_required` set, Spare does not set its memo, so your app's own `checkMemoRequired` still fires. The donation is still offered, with `donation.attributed === false`. A destination that cannot be read is treated as memo-required.
|
|
49
|
+
|
|
50
|
+
### Fees
|
|
51
|
+
|
|
52
|
+
`baseFee` is per operation, digits only, minimum 100 stroops. A malformed value gives `not-offered` / `invalid-options`; it never throws. A tx with a fee of 0 is raised to 100 per operation, and the same number is used for the balance check and the built tx.
|
|
53
|
+
|
|
54
|
+
## Soroban call: `donateAfter`
|
|
55
|
+
|
|
56
|
+
A Soroban tx holds one operation, so the donation is a separate payment after success. It needs a second signature: tell the user before they accept.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { donateAfter } from "spare-sdk";
|
|
60
|
+
|
|
61
|
+
const follow = await donateAfter(
|
|
62
|
+
{ successful: true, sourceAccount: userAccount, hash },
|
|
63
|
+
{ foundation, amount: "0.50", appId: "my-app", maxAmount: "5",
|
|
64
|
+
accounts, networkPassphrase: Networks.PUBLIC },
|
|
65
|
+
);
|
|
66
|
+
if (follow.status === "offered") { /* confirm, then sign follow.transaction */ }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The follow-up is a single payment (medium threshold). If the user's account needs more than its master key to sign that, nothing is offered (`multisig`).
|
|
70
|
+
|
|
71
|
+
## Account port
|
|
72
|
+
|
|
73
|
+
`AccountPort.loadAccount(id)` returns an `AccountSnapshot`: `sequence`, `xlmAvailable`, `usdc` (`{ balance, authorized, headroom }` or `null`), `masterWeight`, `thresholds` (`{ low, medium, high }`) and `memoRequired`. `HorizonAccountPort` fills it from Horizon (`headroom` = limit - balance - buying liabilities; `memoRequired` from the `config.memo_required` data entry). `requiresMultipleSignatures` is gone: Spare decides from `masterWeight` and `thresholds` per transaction.
|
|
74
|
+
|
|
75
|
+
## Helpers
|
|
76
|
+
|
|
77
|
+
`verifyFoundation(foundation)`, `listVerifiedFoundations(candidates)` (SEP-1 `ACCOUNTS` check: https only, no redirects, 5 s timeout, body capped at 100 KB), `parseUsdc`, `formatStroops`, `roundUpToWholeDollar`. Network access (`fetch`, `AccountPort`) is injectable.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FeeBumpTransaction, Transaction } from "@stellar/stellar-sdk";
|
|
2
|
+
import type { CommonOptions, DonationResult } from "./types.js";
|
|
3
|
+
/** Provide exactly one of `amount` (fixed USDC) or `roundUp: true` (USDC purchases only). */
|
|
4
|
+
export type AddDonationOptions = CommonOptions & {
|
|
5
|
+
amount?: string;
|
|
6
|
+
roundUp?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Appends a USDC donation to an unsigned classic tx, if (and only if) every check passes.
|
|
10
|
+
* Never throws for ineligible input: inspect `status`.
|
|
11
|
+
* - "offered": `transaction` is a new unsigned tx (new hash, higher fee); show `donation.amount` to the user.
|
|
12
|
+
* - "not-offered": tx is fine, but no donation should be shown. Use the original.
|
|
13
|
+
* - "passthrough": Spare cannot safely modify this tx. Use the original.
|
|
14
|
+
*/
|
|
15
|
+
export declare function addDonation(tx: Transaction | FeeBumpTransaction, opts: AddDonationOptions): Promise<DonationResult>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Asset, FeeBumpTransaction, Memo, Operation, TransactionBuilder, } from "@stellar/stellar-sdk";
|
|
2
|
+
import { formatStroops, parseUsdc, roundUpToWholeDollar, toStroops } from "./amount.js";
|
|
3
|
+
import { memoFor } from "./app-id.js";
|
|
4
|
+
import { USDC_CODE, USDC_ISSUER } from "./constants.js";
|
|
5
|
+
import { checkEligibility } from "./eligibility.js";
|
|
6
|
+
import { perOpFee } from "./fee.js";
|
|
7
|
+
import { canSignAlone, requiredThreshold } from "./signing.js";
|
|
8
|
+
import { analyzeSpend } from "./spend.js";
|
|
9
|
+
const SOROBAN_OPS = new Set(["invokeHostFunction", "extendFootprintTtl", "restoreFootprint"]);
|
|
10
|
+
/** Why Spare must not touch this tx, or null when it is an unsigned, single-source classic tx. */
|
|
11
|
+
function structuralPassthrough(tx) {
|
|
12
|
+
if (tx instanceof FeeBumpTransaction)
|
|
13
|
+
return "fee-bump";
|
|
14
|
+
if (tx.signatures.length > 0)
|
|
15
|
+
return "already-signed";
|
|
16
|
+
if (tx.extraSigners && tx.extraSigners.length > 0)
|
|
17
|
+
return "extra-signers"; // CAP-21 precondition
|
|
18
|
+
if (tx.operations.some((op) => SOROBAN_OPS.has(op.type)))
|
|
19
|
+
return "soroban";
|
|
20
|
+
if (tx.operations.some((op) => op.source !== undefined && op.source !== tx.source))
|
|
21
|
+
return "multiple-sources";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** Destinations that receive funds from the tx: where a SEP-29 memo may be required. */
|
|
25
|
+
function destinations(ops) {
|
|
26
|
+
const out = new Set();
|
|
27
|
+
for (const op of ops) {
|
|
28
|
+
if (op.type === "payment" || op.type === "pathPaymentStrictSend" ||
|
|
29
|
+
op.type === "pathPaymentStrictReceive" || op.type === "accountMerge")
|
|
30
|
+
out.add(op.destination);
|
|
31
|
+
}
|
|
32
|
+
return [...out];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* True unless every destination is known not to require a memo (SEP-29). Fails closed: a
|
|
36
|
+
* destination we cannot read counts as requiring one. A missing account does not.
|
|
37
|
+
*/
|
|
38
|
+
async function anyMemoRequired(accounts, ops) {
|
|
39
|
+
const checks = await Promise.all(destinations(ops).map(async (d) => {
|
|
40
|
+
try {
|
|
41
|
+
return (await accounts.loadAccount(d))?.memoRequired === true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
return checks.some(Boolean);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Appends a USDC donation to an unsigned classic tx, if (and only if) every check passes.
|
|
51
|
+
* Never throws for ineligible input: inspect `status`.
|
|
52
|
+
* - "offered": `transaction` is a new unsigned tx (new hash, higher fee); show `donation.amount` to the user.
|
|
53
|
+
* - "not-offered": tx is fine, but no donation should be shown. Use the original.
|
|
54
|
+
* - "passthrough": Spare cannot safely modify this tx. Use the original.
|
|
55
|
+
*/
|
|
56
|
+
export async function addDonation(tx, opts) {
|
|
57
|
+
const pass = structuralPassthrough(tx);
|
|
58
|
+
if (pass)
|
|
59
|
+
return { status: "passthrough", reason: pass, transaction: tx };
|
|
60
|
+
const t = tx;
|
|
61
|
+
const no = (reason) => ({ status: "not-offered", reason, transaction: t });
|
|
62
|
+
if ((opts.amount === undefined) === (opts.roundUp !== true))
|
|
63
|
+
return no("invalid-options");
|
|
64
|
+
const feeFallback = (BigInt(t.fee) + BigInt(t.operations.length) - 1n) / BigInt(t.operations.length);
|
|
65
|
+
const perOp = perOpFee(opts.baseFee, feeFallback);
|
|
66
|
+
if (perOp === null)
|
|
67
|
+
return no("invalid-options");
|
|
68
|
+
const spend = analyzeSpend(t.operations);
|
|
69
|
+
if (spend.unsupported)
|
|
70
|
+
return no("unsupported-operation");
|
|
71
|
+
let amount;
|
|
72
|
+
if (opts.roundUp === true) {
|
|
73
|
+
if (spend.other || spend.uncertain || spend.usdc === 0n)
|
|
74
|
+
return no("round-up-needs-usdc-purchase");
|
|
75
|
+
amount = roundUpToWholeDollar(spend.usdc);
|
|
76
|
+
if (amount === 0n)
|
|
77
|
+
return no("round-up-already-whole");
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
amount = parseUsdc(opts.amount);
|
|
81
|
+
if (amount === null)
|
|
82
|
+
return no("invalid-amount");
|
|
83
|
+
}
|
|
84
|
+
let user;
|
|
85
|
+
try {
|
|
86
|
+
user = await opts.accounts.loadAccount(t.source);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return no("account-lookup-failed");
|
|
90
|
+
}
|
|
91
|
+
if (!user)
|
|
92
|
+
return no("account-lookup-failed");
|
|
93
|
+
if (!canSignAlone(user, requiredThreshold(t.operations)))
|
|
94
|
+
return { status: "passthrough", reason: "multisig", transaction: t };
|
|
95
|
+
// Fee: one extra operation at the tx's own per-op rate (ceil, min 100), unless overridden.
|
|
96
|
+
const newFee = perOp * (BigInt(t.operations.length) + 1n);
|
|
97
|
+
const reason = await checkEligibility({
|
|
98
|
+
accounts: opts.accounts,
|
|
99
|
+
...(opts.fetch && { fetch: opts.fetch }),
|
|
100
|
+
foundation: opts.foundation,
|
|
101
|
+
appId: opts.appId,
|
|
102
|
+
amount,
|
|
103
|
+
cap: toStroops(opts.maxAmount),
|
|
104
|
+
source: t.source,
|
|
105
|
+
user,
|
|
106
|
+
usdcSpend: spend.usdc,
|
|
107
|
+
fee: newFee,
|
|
108
|
+
xlmSpend: spend.xlm,
|
|
109
|
+
});
|
|
110
|
+
if (reason)
|
|
111
|
+
return no(reason);
|
|
112
|
+
// Our memo would bypass a destination's SEP-29 memo_required check: leave the memo unset so
|
|
113
|
+
// the app's own checkMemoRequired still fires.
|
|
114
|
+
const attributed = t.memo.type === "none" && !(await anyMemoRequired(opts.accounts, t.operations));
|
|
115
|
+
const builder = TransactionBuilder.cloneFrom(t, {
|
|
116
|
+
fee: perOp.toString(),
|
|
117
|
+
...(attributed && { memo: Memo.text(memoFor(opts.appId)) }),
|
|
118
|
+
});
|
|
119
|
+
builder.addOperation(Operation.payment({
|
|
120
|
+
destination: opts.foundation.account,
|
|
121
|
+
asset: new Asset(USDC_CODE, USDC_ISSUER),
|
|
122
|
+
amount: formatStroops(amount),
|
|
123
|
+
}));
|
|
124
|
+
const out = builder.build();
|
|
125
|
+
return {
|
|
126
|
+
status: "offered",
|
|
127
|
+
transaction: out,
|
|
128
|
+
fee: BigInt(out.fee),
|
|
129
|
+
donation: {
|
|
130
|
+
amount: formatStroops(amount),
|
|
131
|
+
amountStroops: amount,
|
|
132
|
+
foundation: opts.foundation.account,
|
|
133
|
+
attributed,
|
|
134
|
+
...(opts.roundUp === true && {
|
|
135
|
+
purchase: formatStroops(spend.usdc),
|
|
136
|
+
purchaseStroops: spend.usdc,
|
|
137
|
+
roundedTotal: formatStroops(spend.usdc + amount),
|
|
138
|
+
roundedTotalStroops: spend.usdc + amount,
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=add-donation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-donation.js","sourceRoot":"","sources":["../src/add-donation.ts"],"names":[],"mappings":"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"}
|
package/dist/amount.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Decimal string ("0.5", up to 7 decimals) to stroops. No floats. Returns null if malformed. */
|
|
2
|
+
export declare function toStroops(s: string): bigint | null;
|
|
3
|
+
/** Like {@link toStroops} but also rejects zero: a donation must be positive. */
|
|
4
|
+
export declare function parseUsdc(s: string): bigint | null;
|
|
5
|
+
/** Stroops to the 7-decimal string Stellar operations expect. */
|
|
6
|
+
export declare function formatStroops(v: bigint): string;
|
|
7
|
+
/**
|
|
8
|
+
* Gap between `purchase` and the next whole dollar.
|
|
9
|
+
* A purchase that is already a whole dollar yields 0n (no donation); we never default to a full $1.
|
|
10
|
+
*/
|
|
11
|
+
export declare function roundUpToWholeDollar(purchase: bigint): bigint;
|
package/dist/amount.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MAX_STROOPS, STROOPS_PER_UNIT } from "./constants.js";
|
|
2
|
+
const AMOUNT_RE = /^\d+(\.\d{1,7})?$/;
|
|
3
|
+
/** Decimal string ("0.5", up to 7 decimals) to stroops. No floats. Returns null if malformed. */
|
|
4
|
+
export function toStroops(s) {
|
|
5
|
+
if (!AMOUNT_RE.test(s))
|
|
6
|
+
return null;
|
|
7
|
+
const [int = "0", frac = ""] = s.split(".");
|
|
8
|
+
const v = BigInt(int) * STROOPS_PER_UNIT + BigInt(frac.padEnd(7, "0"));
|
|
9
|
+
return v > MAX_STROOPS ? null : v;
|
|
10
|
+
}
|
|
11
|
+
/** Like {@link toStroops} but also rejects zero: a donation must be positive. */
|
|
12
|
+
export function parseUsdc(s) {
|
|
13
|
+
const v = toStroops(s);
|
|
14
|
+
return v === null || v === 0n ? null : v;
|
|
15
|
+
}
|
|
16
|
+
/** Stroops to the 7-decimal string Stellar operations expect. */
|
|
17
|
+
export function formatStroops(v) {
|
|
18
|
+
const int = v / STROOPS_PER_UNIT;
|
|
19
|
+
const frac = (v % STROOPS_PER_UNIT).toString().padStart(7, "0");
|
|
20
|
+
return `${int}.${frac}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Gap between `purchase` and the next whole dollar.
|
|
24
|
+
* A purchase that is already a whole dollar yields 0n (no donation); we never default to a full $1.
|
|
25
|
+
*/
|
|
26
|
+
export function roundUpToWholeDollar(purchase) {
|
|
27
|
+
const rem = purchase % STROOPS_PER_UNIT;
|
|
28
|
+
return rem === 0n ? 0n : STROOPS_PER_UNIT - rem;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=amount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"amount.js","sourceRoot":"","sources":["../src/amount.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAE/D,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAEtC,iGAAiG;AACjG,MAAM,UAAU,SAAS,CAAC,CAAS;IACjC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,CAAC,GAAG,GAAG,GAAG,EAAE,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5C,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACvE,OAAO,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,SAAS,CAAC,CAAS;IACjC,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;IACvB,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,aAAa,CAAC,CAAS;IACrC,MAAM,GAAG,GAAG,CAAC,GAAG,gBAAgB,CAAC;IACjC,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAChE,OAAO,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,MAAM,GAAG,GAAG,QAAQ,GAAG,gBAAgB,CAAC;IACxC,OAAO,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,gBAAgB,GAAG,GAAG,CAAC;AAClD,CAAC"}
|
package/dist/app-id.d.ts
ADDED
package/dist/app-id.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { MAX_APP_ID_BYTES, MEMO_PREFIX } from "./constants.js";
|
|
2
|
+
/** 1-22 printable ASCII chars (no spaces) so `spare:<appId>` fits a 28-byte text memo. */
|
|
3
|
+
export function isValidAppId(appId) {
|
|
4
|
+
return appId.length <= MAX_APP_ID_BYTES && /^[\x21-\x7E]+$/.test(appId);
|
|
5
|
+
}
|
|
6
|
+
export function memoFor(appId) {
|
|
7
|
+
return `${MEMO_PREFIX}${appId}`;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=app-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-id.js","sourceRoot":"","sources":["../src/app-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE/D,0FAA0F;AAC1F,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,KAAK,CAAC,MAAM,IAAI,gBAAgB,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,GAAG,WAAW,GAAG,KAAK,EAAE,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Circle's USDC issuer on Stellar. Spare accepts no other USDC. */
|
|
2
|
+
export declare const USDC_ISSUER = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
|
|
3
|
+
export declare const USDC_CODE = "USDC";
|
|
4
|
+
export declare const STROOPS_PER_UNIT = 10000000n;
|
|
5
|
+
/** Stellar's max int64 amount, in stroops. */
|
|
6
|
+
export declare const MAX_STROOPS = 9223372036854775807n;
|
|
7
|
+
export declare const MEMO_PREFIX = "spare:";
|
|
8
|
+
export declare const MAX_APP_ID_BYTES = 22;
|
|
9
|
+
export declare const DEFAULT_BASE_FEE = "100";
|
|
10
|
+
export declare const MIN_BASE_FEE = 100n;
|
|
11
|
+
/** SEP-1: stellar.toml must not exceed 100KB. */
|
|
12
|
+
export declare const MAX_TOML_BYTES: number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Circle's USDC issuer on Stellar. Spare accepts no other USDC. */
|
|
2
|
+
export const USDC_ISSUER = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
|
|
3
|
+
export const USDC_CODE = "USDC";
|
|
4
|
+
export const STROOPS_PER_UNIT = 10000000n;
|
|
5
|
+
/** Stellar's max int64 amount, in stroops. */
|
|
6
|
+
export const MAX_STROOPS = 9223372036854775807n;
|
|
7
|
+
export const MEMO_PREFIX = "spare:";
|
|
8
|
+
export const MAX_APP_ID_BYTES = 22; // 28-byte text memo minus "spare:"
|
|
9
|
+
export const DEFAULT_BASE_FEE = "100";
|
|
10
|
+
export const MIN_BASE_FEE = 100n;
|
|
11
|
+
/** SEP-1: stellar.toml must not exceed 100KB. */
|
|
12
|
+
export const MAX_TOML_BYTES = 100 * 1024;
|
|
13
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,MAAM,CAAC,MAAM,WAAW,GAAG,0DAA0D,CAAC;AACtF,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEhC,MAAM,CAAC,MAAM,gBAAgB,GAAG,SAAW,CAAC;AAC5C,8CAA8C;AAC9C,MAAM,CAAC,MAAM,WAAW,GAAG,oBAA0B,CAAC;AAEtD,MAAM,CAAC,MAAM,WAAW,GAAG,QAAQ,CAAC;AACpC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC,CAAC,mCAAmC;AACvE,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AACtC,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AACjC,iDAAiD;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,GAAG,IAAI,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Transaction } from "@stellar/stellar-sdk";
|
|
2
|
+
import type { CommonOptions, DonationInfo, NotOfferedReason } from "./types.js";
|
|
3
|
+
/** Outcome of the user's main (Soroban) transaction. */
|
|
4
|
+
export interface MainTxResult {
|
|
5
|
+
successful: boolean;
|
|
6
|
+
/** Account that signed the main tx and will pay the donation. */
|
|
7
|
+
sourceAccount: string;
|
|
8
|
+
hash?: string;
|
|
9
|
+
}
|
|
10
|
+
export type DonateAfterOptions = CommonOptions & {
|
|
11
|
+
/** Fixed amount in USDC. Round-up is not available on this path. */
|
|
12
|
+
amount: string;
|
|
13
|
+
networkPassphrase: string;
|
|
14
|
+
/** Seconds the follow-up tx stays valid. Default 300. */
|
|
15
|
+
timeoutSeconds?: number;
|
|
16
|
+
};
|
|
17
|
+
export type DonateAfterResult = {
|
|
18
|
+
status: "offered";
|
|
19
|
+
transaction: Transaction;
|
|
20
|
+
donation: DonationInfo;
|
|
21
|
+
fee: bigint;
|
|
22
|
+
/** Always true: the UI must say so before the user accepts. */
|
|
23
|
+
requiresSecondSignature: true;
|
|
24
|
+
} | {
|
|
25
|
+
status: "not-offered";
|
|
26
|
+
reason: NotOfferedReason;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Soroban path: an InvokeHostFunction tx holds a single operation, so the donation is a separate
|
|
30
|
+
* payment tx, built only after the main tx succeeded. Needs a second signature. Never throws for
|
|
31
|
+
* ineligible input.
|
|
32
|
+
*/
|
|
33
|
+
export declare function donateAfter(result: MainTxResult, opts: DonateAfterOptions): Promise<DonateAfterResult>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Account, Asset, Memo, Operation, TransactionBuilder, } from "@stellar/stellar-sdk";
|
|
2
|
+
import { formatStroops, parseUsdc, toStroops } from "./amount.js";
|
|
3
|
+
import { memoFor } from "./app-id.js";
|
|
4
|
+
import { USDC_CODE, USDC_ISSUER } from "./constants.js";
|
|
5
|
+
import { checkEligibility } from "./eligibility.js";
|
|
6
|
+
import { perOpFee } from "./fee.js";
|
|
7
|
+
import { canSignAlone } from "./signing.js";
|
|
8
|
+
/**
|
|
9
|
+
* Soroban path: an InvokeHostFunction tx holds a single operation, so the donation is a separate
|
|
10
|
+
* payment tx, built only after the main tx succeeded. Needs a second signature. Never throws for
|
|
11
|
+
* ineligible input.
|
|
12
|
+
*/
|
|
13
|
+
export async function donateAfter(result, opts) {
|
|
14
|
+
const no = (reason) => ({ status: "not-offered", reason });
|
|
15
|
+
if (!result.successful)
|
|
16
|
+
return no("main-tx-not-successful");
|
|
17
|
+
const amount = parseUsdc(opts.amount);
|
|
18
|
+
if (amount === null)
|
|
19
|
+
return no("invalid-amount");
|
|
20
|
+
const baseFee = perOpFee(opts.baseFee);
|
|
21
|
+
if (baseFee === null)
|
|
22
|
+
return no("invalid-options");
|
|
23
|
+
let user;
|
|
24
|
+
try {
|
|
25
|
+
user = await opts.accounts.loadAccount(result.sourceAccount);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return no("account-lookup-failed");
|
|
29
|
+
}
|
|
30
|
+
if (!user)
|
|
31
|
+
return no("account-lookup-failed");
|
|
32
|
+
// The follow-up is a single payment: medium threshold. If the master key is not enough the
|
|
33
|
+
// user would need other signers, which this flow cannot collect.
|
|
34
|
+
if (!canSignAlone(user, "medium"))
|
|
35
|
+
return no("multisig");
|
|
36
|
+
const reason = await checkEligibility({
|
|
37
|
+
accounts: opts.accounts,
|
|
38
|
+
...(opts.fetch && { fetch: opts.fetch }),
|
|
39
|
+
foundation: opts.foundation,
|
|
40
|
+
appId: opts.appId,
|
|
41
|
+
amount,
|
|
42
|
+
cap: toStroops(opts.maxAmount),
|
|
43
|
+
source: result.sourceAccount,
|
|
44
|
+
user,
|
|
45
|
+
usdcSpend: 0n, // the main tx already settled
|
|
46
|
+
fee: baseFee,
|
|
47
|
+
});
|
|
48
|
+
if (reason)
|
|
49
|
+
return no(reason);
|
|
50
|
+
const tx = new TransactionBuilder(new Account(result.sourceAccount, user.sequence), {
|
|
51
|
+
fee: baseFee.toString(),
|
|
52
|
+
networkPassphrase: opts.networkPassphrase,
|
|
53
|
+
memo: Memo.text(memoFor(opts.appId)),
|
|
54
|
+
})
|
|
55
|
+
.addOperation(Operation.payment({
|
|
56
|
+
destination: opts.foundation.account,
|
|
57
|
+
asset: new Asset(USDC_CODE, USDC_ISSUER),
|
|
58
|
+
amount: formatStroops(amount),
|
|
59
|
+
}))
|
|
60
|
+
.setTimeout(opts.timeoutSeconds ?? 300)
|
|
61
|
+
.build();
|
|
62
|
+
return {
|
|
63
|
+
status: "offered",
|
|
64
|
+
transaction: tx,
|
|
65
|
+
fee: BigInt(tx.fee),
|
|
66
|
+
requiresSecondSignature: true,
|
|
67
|
+
donation: {
|
|
68
|
+
amount: formatStroops(amount),
|
|
69
|
+
amountStroops: amount,
|
|
70
|
+
foundation: opts.foundation.account,
|
|
71
|
+
attributed: true,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=donate-after.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"donate-after.js","sourceRoot":"","sources":["../src/donate-after.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AccountPort, AccountSnapshot, FetchLike, Foundation } from "./ports.js";
|
|
2
|
+
import type { NotOfferedReason } from "./types.js";
|
|
3
|
+
export interface EligibilityInput {
|
|
4
|
+
accounts: AccountPort;
|
|
5
|
+
fetch?: FetchLike;
|
|
6
|
+
foundation: Foundation;
|
|
7
|
+
appId: string;
|
|
8
|
+
amount: bigint;
|
|
9
|
+
cap: bigint | null;
|
|
10
|
+
/** Account that will sign and pay. */
|
|
11
|
+
source: string;
|
|
12
|
+
user: AccountSnapshot;
|
|
13
|
+
/** Circle-USDC the same tx already spends (0 when none or when it is already settled). */
|
|
14
|
+
usdcSpend: bigint;
|
|
15
|
+
/** Total fee the user will pay, in stroops (XLM). */
|
|
16
|
+
fee: bigint;
|
|
17
|
+
/** XLM the same tx already spends (payments, createAccount...), stroops. Default 0. */
|
|
18
|
+
xlmSpend?: bigint;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Cheap, offline checks first; network checks after. Returns the first failing reason,
|
|
22
|
+
* or null when a donation may be offered. Fails closed on any lookup error.
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkEligibility(i: EligibilityInput): Promise<NotOfferedReason | null>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { StrKey } from "@stellar/stellar-sdk";
|
|
2
|
+
import { isValidAppId } from "./app-id.js";
|
|
3
|
+
import { verifyFoundation } from "./stellar-toml.js";
|
|
4
|
+
/**
|
|
5
|
+
* Cheap, offline checks first; network checks after. Returns the first failing reason,
|
|
6
|
+
* or null when a donation may be offered. Fails closed on any lookup error.
|
|
7
|
+
*/
|
|
8
|
+
export async function checkEligibility(i) {
|
|
9
|
+
if (!isValidAppId(i.appId))
|
|
10
|
+
return "invalid-app-id";
|
|
11
|
+
if (!StrKey.isValidEd25519PublicKey(i.foundation.account) || i.foundation.account === i.source)
|
|
12
|
+
return "invalid-foundation";
|
|
13
|
+
if (i.cap === null)
|
|
14
|
+
return "invalid-amount";
|
|
15
|
+
if (i.amount > i.cap)
|
|
16
|
+
return "amount-over-cap";
|
|
17
|
+
if (!(await verifyFoundation(i.foundation, i.fetch)))
|
|
18
|
+
return "foundation-not-verified";
|
|
19
|
+
let foundation;
|
|
20
|
+
try {
|
|
21
|
+
foundation = await i.accounts.loadAccount(i.foundation.account);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return "account-lookup-failed";
|
|
25
|
+
}
|
|
26
|
+
if (!foundation?.usdc)
|
|
27
|
+
return "foundation-trustline-missing";
|
|
28
|
+
if (!foundation.usdc.authorized)
|
|
29
|
+
return "foundation-trustline-unauthorized";
|
|
30
|
+
if (foundation.usdc.headroom < i.amount)
|
|
31
|
+
return "foundation-trustline-full";
|
|
32
|
+
if (!i.user.usdc)
|
|
33
|
+
return "user-trustline-missing";
|
|
34
|
+
if (!i.user.usdc.authorized)
|
|
35
|
+
return "user-trustline-unauthorized";
|
|
36
|
+
if (i.user.usdc.balance < i.usdcSpend + i.amount)
|
|
37
|
+
return "insufficient-balance";
|
|
38
|
+
if (i.user.xlmAvailable < i.fee + (i.xlmSpend ?? 0n))
|
|
39
|
+
return "insufficient-balance";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=eligibility.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eligibility.js","sourceRoot":"","sources":["../src/eligibility.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAqBrD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,CAAmB;IACxD,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM;QAC5F,OAAO,oBAAoB,CAAC;IAC9B,IAAI,CAAC,CAAC,GAAG,KAAK,IAAI;QAAE,OAAO,gBAAgB,CAAC;IAC5C,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG;QAAE,OAAO,iBAAiB,CAAC;IAE/C,IAAI,CAAC,CAAC,MAAM,gBAAgB,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,yBAAyB,CAAC;IAEvF,IAAI,UAAkC,CAAC;IACvC,IAAI,CAAC;QACH,UAAU,GAAG,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,uBAAuB,CAAC;IACjC,CAAC;IACD,IAAI,CAAC,UAAU,EAAE,IAAI;QAAE,OAAO,8BAA8B,CAAC;IAC7D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU;QAAE,OAAO,mCAAmC,CAAC;IAC5E,IAAI,UAAU,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,MAAM;QAAE,OAAO,2BAA2B,CAAC;IAE5E,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI;QAAE,OAAO,wBAAwB,CAAC;IAClD,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU;QAAE,OAAO,6BAA6B,CAAC;IAClE,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,MAAM;QAAE,OAAO,sBAAsB,CAAC;IAChF,IAAI,CAAC,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC;QAAE,OAAO,sBAAsB,CAAC;IACpF,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/fee.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-op fee from user input: `undefined` -> `fallback` (DEFAULT_BASE_FEE when not given),
|
|
3
|
+
* malformed -> null. Never below {@link MIN_BASE_FEE}, so eligibility and build use one number.
|
|
4
|
+
*/
|
|
5
|
+
export declare function perOpFee(input: string | undefined, fallback?: bigint): bigint | null;
|
package/dist/fee.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DEFAULT_BASE_FEE, MIN_BASE_FEE } from "./constants.js";
|
|
2
|
+
/** Digits only, at most 15 of them: the total fee always stays far below int64. */
|
|
3
|
+
const FEE_RE = /^\d{1,15}$/;
|
|
4
|
+
/**
|
|
5
|
+
* Per-op fee from user input: `undefined` -> `fallback` (DEFAULT_BASE_FEE when not given),
|
|
6
|
+
* malformed -> null. Never below {@link MIN_BASE_FEE}, so eligibility and build use one number.
|
|
7
|
+
*/
|
|
8
|
+
export function perOpFee(input, fallback = BigInt(DEFAULT_BASE_FEE)) {
|
|
9
|
+
let v = fallback;
|
|
10
|
+
if (input !== undefined) {
|
|
11
|
+
if (!FEE_RE.test(input))
|
|
12
|
+
return null;
|
|
13
|
+
v = BigInt(input);
|
|
14
|
+
}
|
|
15
|
+
return v < MIN_BASE_FEE ? MIN_BASE_FEE : v;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=fee.js.map
|
package/dist/fee.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fee.js","sourceRoot":"","sources":["../src/fee.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEhE,mFAAmF;AACnF,MAAM,MAAM,GAAG,YAAY,CAAC;AAE5B;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAyB,EAAE,WAAmB,MAAM,CAAC,gBAAgB,CAAC;IAC7F,IAAI,CAAC,GAAG,QAAQ,CAAC;IACjB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACrC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Horizon } from "@stellar/stellar-sdk";
|
|
2
|
+
import type { AccountPort, AccountSnapshot } from "./ports.js";
|
|
3
|
+
/** The subset of Horizon's account record that Spare reads. */
|
|
4
|
+
export interface HorizonAccountRecord {
|
|
5
|
+
account_id: string;
|
|
6
|
+
sequence: string;
|
|
7
|
+
subentry_count: number;
|
|
8
|
+
num_sponsoring: number;
|
|
9
|
+
num_sponsored: number;
|
|
10
|
+
thresholds: {
|
|
11
|
+
low_threshold: number;
|
|
12
|
+
med_threshold: number;
|
|
13
|
+
high_threshold: number;
|
|
14
|
+
};
|
|
15
|
+
signers: {
|
|
16
|
+
key: string;
|
|
17
|
+
weight: number;
|
|
18
|
+
}[];
|
|
19
|
+
/** Account data entries, base64-encoded values. */
|
|
20
|
+
data_attr?: Record<string, string>;
|
|
21
|
+
balances: {
|
|
22
|
+
asset_type: string;
|
|
23
|
+
asset_code?: string;
|
|
24
|
+
asset_issuer?: string;
|
|
25
|
+
balance: string;
|
|
26
|
+
limit?: string;
|
|
27
|
+
buying_liabilities?: string;
|
|
28
|
+
selling_liabilities?: string;
|
|
29
|
+
is_authorized?: boolean;
|
|
30
|
+
}[];
|
|
31
|
+
}
|
|
32
|
+
export declare function toAccountSnapshot(r: HorizonAccountRecord): AccountSnapshot;
|
|
33
|
+
/** AccountPort backed by a Horizon server. */
|
|
34
|
+
export declare class HorizonAccountPort implements AccountPort {
|
|
35
|
+
private readonly server;
|
|
36
|
+
constructor(horizon?: string | Horizon.Server);
|
|
37
|
+
loadAccount(accountId: string): Promise<AccountSnapshot | null>;
|
|
38
|
+
}
|
package/dist/horizon.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Horizon } from "@stellar/stellar-sdk";
|
|
2
|
+
import { toStroops } from "./amount.js";
|
|
3
|
+
import { USDC_CODE, USDC_ISSUER } from "./constants.js";
|
|
4
|
+
const RESERVE_UNIT = 5000000n; // 0.5 XLM
|
|
5
|
+
const net = (balance, liabilities) => (balance > liabilities ? balance - liabilities : 0n);
|
|
6
|
+
export function toAccountSnapshot(r) {
|
|
7
|
+
const native = r.balances.find((b) => b.asset_type === "native");
|
|
8
|
+
const reserve = BigInt(2 + r.subentry_count + r.num_sponsoring - r.num_sponsored) * RESERVE_UNIT;
|
|
9
|
+
const nativeNet = net(toStroops(native?.balance ?? "0") ?? 0n, (toStroops(native?.selling_liabilities ?? "0") ?? 0n) + reserve);
|
|
10
|
+
const line = r.balances.find((b) => b.asset_code === USDC_CODE && b.asset_issuer === USDC_ISSUER && b.asset_type !== "native");
|
|
11
|
+
const masterWeight = r.signers.find((s) => s.key === r.account_id)?.weight ?? 0;
|
|
12
|
+
// SEP-29: data entry config.memo_required = "1" (base64 "MQ==").
|
|
13
|
+
const memoRequired = r.data_attr?.["config.memo_required"] === "MQ==";
|
|
14
|
+
return {
|
|
15
|
+
sequence: r.sequence,
|
|
16
|
+
xlmAvailable: nativeNet,
|
|
17
|
+
usdc: line
|
|
18
|
+
? {
|
|
19
|
+
balance: net(toStroops(line.balance) ?? 0n, toStroops(line.selling_liabilities ?? "0") ?? 0n),
|
|
20
|
+
authorized: line.is_authorized === true,
|
|
21
|
+
// A missing limit is unknown: zero headroom keeps us from donating into a full line.
|
|
22
|
+
headroom: net(toStroops(line.limit ?? "0") ?? 0n, (toStroops(line.balance) ?? 0n) + (toStroops(line.buying_liabilities ?? "0") ?? 0n)),
|
|
23
|
+
}
|
|
24
|
+
: null,
|
|
25
|
+
masterWeight,
|
|
26
|
+
thresholds: {
|
|
27
|
+
low: r.thresholds.low_threshold,
|
|
28
|
+
medium: r.thresholds.med_threshold,
|
|
29
|
+
high: r.thresholds.high_threshold,
|
|
30
|
+
},
|
|
31
|
+
memoRequired,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** AccountPort backed by a Horizon server. */
|
|
35
|
+
export class HorizonAccountPort {
|
|
36
|
+
server;
|
|
37
|
+
constructor(horizon = "https://horizon.stellar.org") {
|
|
38
|
+
this.server = typeof horizon === "string" ? new Horizon.Server(horizon) : horizon;
|
|
39
|
+
}
|
|
40
|
+
async loadAccount(accountId) {
|
|
41
|
+
try {
|
|
42
|
+
const rec = await this.server.accounts().accountId(accountId).call();
|
|
43
|
+
return toAccountSnapshot(rec);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (e.response?.status === 404)
|
|
47
|
+
return null;
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=horizon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"horizon.js","sourceRoot":"","sources":["../src/horizon.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { addDonation, type AddDonationOptions } from "./add-donation.js";
|
|
2
|
+
export { donateAfter, type DonateAfterOptions, type DonateAfterResult, type MainTxResult } from "./donate-after.js";
|
|
3
|
+
export { verifyFoundation, listVerifiedFoundations } from "./stellar-toml.js";
|
|
4
|
+
export { HorizonAccountPort, toAccountSnapshot, type HorizonAccountRecord } from "./horizon.js";
|
|
5
|
+
export { parseUsdc, formatStroops, roundUpToWholeDollar } from "./amount.js";
|
|
6
|
+
export { isValidAppId, memoFor } from "./app-id.js";
|
|
7
|
+
export { USDC_ISSUER, USDC_CODE } from "./constants.js";
|
|
8
|
+
export type { AccountPort, AccountSnapshot, FetchLike, Foundation } from "./ports.js";
|
|
9
|
+
export type { CommonOptions, DonationInfo, DonationResult, NotOfferedReason, PassthroughReason, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { addDonation } from "./add-donation.js";
|
|
2
|
+
export { donateAfter } from "./donate-after.js";
|
|
3
|
+
export { verifyFoundation, listVerifiedFoundations } from "./stellar-toml.js";
|
|
4
|
+
export { HorizonAccountPort, toAccountSnapshot } from "./horizon.js";
|
|
5
|
+
export { parseUsdc, formatStroops, roundUpToWholeDollar } from "./amount.js";
|
|
6
|
+
export { isValidAppId, memoFor } from "./app-id.js";
|
|
7
|
+
export { USDC_ISSUER, USDC_CODE } from "./constants.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA2B,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,WAAW,EAAsE,MAAM,mBAAmB,CAAC;AACpH,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAA6B,MAAM,cAAc,CAAC;AAChG,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Injectable fetch (same shape as the global one). */
|
|
2
|
+
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
/** What Spare needs to know about an account. All amounts in stroops. */
|
|
4
|
+
export interface AccountSnapshot {
|
|
5
|
+
/** Current sequence number (the next tx uses sequence + 1). */
|
|
6
|
+
sequence: string;
|
|
7
|
+
/** XLM spendable on fees: balance minus reserve and selling liabilities. */
|
|
8
|
+
xlmAvailable: bigint;
|
|
9
|
+
/** Circle-USDC trustline, or null when there is none. */
|
|
10
|
+
usdc: {
|
|
11
|
+
/** Spendable balance: balance minus selling liabilities. */
|
|
12
|
+
balance: bigint;
|
|
13
|
+
authorized: boolean;
|
|
14
|
+
/** Room left before the trustline limit: limit - balance - buying liabilities (0 when full). */
|
|
15
|
+
headroom: bigint;
|
|
16
|
+
} | null;
|
|
17
|
+
/** Weight of the account's own (master) key; 0 when it is disabled. */
|
|
18
|
+
masterWeight: number;
|
|
19
|
+
/** Signing thresholds (0-255). The domain decides whether the master key alone is enough. */
|
|
20
|
+
thresholds: {
|
|
21
|
+
low: number;
|
|
22
|
+
medium: number;
|
|
23
|
+
high: number;
|
|
24
|
+
};
|
|
25
|
+
/** SEP-29: the account has `config.memo_required` = "1" and expects a memo on incoming payments. */
|
|
26
|
+
memoRequired: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface AccountPort {
|
|
29
|
+
/** Resolves null when the account does not exist. May throw on network errors. */
|
|
30
|
+
loadAccount(accountId: string): Promise<AccountSnapshot | null>;
|
|
31
|
+
}
|
|
32
|
+
export interface Foundation {
|
|
33
|
+
/** Stellar G... account that receives donations. */
|
|
34
|
+
account: string;
|
|
35
|
+
/** Bare hostname whose stellar.toml must list `account`. */
|
|
36
|
+
domain: string;
|
|
37
|
+
}
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Transaction } from "@stellar/stellar-sdk";
|
|
2
|
+
import type { AccountSnapshot } from "./ports.js";
|
|
3
|
+
export type Threshold = "medium" | "high";
|
|
4
|
+
/**
|
|
5
|
+
* Threshold category the tx needs. Any payment is medium; setOptions and accountMerge are
|
|
6
|
+
* treated as high (conservative: setOptions is only high when it touches signers/thresholds).
|
|
7
|
+
*/
|
|
8
|
+
export declare function requiredThreshold(ops: Transaction["operations"]): Threshold;
|
|
9
|
+
/** True when the account's master key alone meets the threshold (a signature needs weight >= 1). */
|
|
10
|
+
export declare function canSignAlone(account: AccountSnapshot, needed: Threshold): boolean;
|
package/dist/signing.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const HIGH_OPS = new Set(["setOptions", "accountMerge"]);
|
|
2
|
+
/**
|
|
3
|
+
* Threshold category the tx needs. Any payment is medium; setOptions and accountMerge are
|
|
4
|
+
* treated as high (conservative: setOptions is only high when it touches signers/thresholds).
|
|
5
|
+
*/
|
|
6
|
+
export function requiredThreshold(ops) {
|
|
7
|
+
return ops.some((op) => HIGH_OPS.has(op.type)) ? "high" : "medium";
|
|
8
|
+
}
|
|
9
|
+
/** True when the account's master key alone meets the threshold (a signature needs weight >= 1). */
|
|
10
|
+
export function canSignAlone(account, needed) {
|
|
11
|
+
const required = Math.max(account.thresholds[needed], 1);
|
|
12
|
+
return account.masterWeight >= required;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=signing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signing.js","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"AAKA,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC,CAAC;AAEzD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAA8B;IAC9D,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AACrE,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,YAAY,CAAC,OAAwB,EAAE,MAAiB;IACtE,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACzD,OAAO,OAAO,CAAC,YAAY,IAAI,QAAQ,CAAC;AAC1C,CAAC"}
|
package/dist/spend.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Asset, Transaction } from "@stellar/stellar-sdk";
|
|
2
|
+
export declare const isCircleUsdc: (a: Asset) => boolean;
|
|
3
|
+
export interface Spend {
|
|
4
|
+
/** Circle-USDC the tx can spend, stroops (an upper bound for strictReceive). */
|
|
5
|
+
usdc: bigint;
|
|
6
|
+
/** Native XLM the tx can spend through payments, path payments, offers and createAccount, stroops. */
|
|
7
|
+
xlm: bigint;
|
|
8
|
+
/** True if the tx also spends or does anything not modelled as Circle USDC (XLM, lookalikes, unknown ops). */
|
|
9
|
+
other: boolean;
|
|
10
|
+
/** True when the USDC spend is only a ceiling (pathPaymentStrictReceive): round-up cannot know it. */
|
|
11
|
+
uncertain: boolean;
|
|
12
|
+
/** True when an op can spend funds in a way Spare does not model. Never guess: offer nothing. */
|
|
13
|
+
unsupported: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What the tx's operations spend. Modelled: payment, createAccount, path payments and sell offers.
|
|
17
|
+
* Ops that can spend but are not modelled are flagged `unsupported`; any other unknown op type
|
|
18
|
+
* sets `other` so a purchase is never mistaken for a pure USDC one.
|
|
19
|
+
*/
|
|
20
|
+
export declare function analyzeSpend(ops: Transaction["operations"]): Spend;
|
package/dist/spend.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { toStroops } from "./amount.js";
|
|
2
|
+
import { USDC_CODE, USDC_ISSUER } from "./constants.js";
|
|
3
|
+
export const isCircleUsdc = (a) => a.getCode() === USDC_CODE && a.getIssuer() === USDC_ISSUER;
|
|
4
|
+
/** Ops that move no funds out of the source for our purposes. */
|
|
5
|
+
const NO_SPEND = new Set([
|
|
6
|
+
"changeTrust", "allowTrust", "setTrustLineFlags", "setOptions", "manageData", "bumpSequence",
|
|
7
|
+
"claimClaimableBalance", "beginSponsoringFutureReserves", "endSponsoringFutureReserves",
|
|
8
|
+
"revokeSponsorship",
|
|
9
|
+
]);
|
|
10
|
+
/** Ops that can spend USDC/XLM in ways we do not model (amounts depend on the book, a pool or the whole balance). */
|
|
11
|
+
const UNSUPPORTED = new Set([
|
|
12
|
+
"manageBuyOffer", "createClaimableBalance", "liquidityPoolDeposit", "accountMerge",
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* What the tx's operations spend. Modelled: payment, createAccount, path payments and sell offers.
|
|
16
|
+
* Ops that can spend but are not modelled are flagged `unsupported`; any other unknown op type
|
|
17
|
+
* sets `other` so a purchase is never mistaken for a pure USDC one.
|
|
18
|
+
*/
|
|
19
|
+
export function analyzeSpend(ops) {
|
|
20
|
+
const s = { usdc: 0n, xlm: 0n, other: false, uncertain: false, unsupported: false };
|
|
21
|
+
const add = (asset, amount) => {
|
|
22
|
+
const v = toStroops(amount) ?? 0n;
|
|
23
|
+
if (isCircleUsdc(asset))
|
|
24
|
+
s.usdc += v;
|
|
25
|
+
else {
|
|
26
|
+
s.other = true;
|
|
27
|
+
if (asset.isNative())
|
|
28
|
+
s.xlm += v;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
for (const op of ops) {
|
|
32
|
+
switch (op.type) {
|
|
33
|
+
case "payment":
|
|
34
|
+
add(op.asset, op.amount);
|
|
35
|
+
break;
|
|
36
|
+
case "pathPaymentStrictSend":
|
|
37
|
+
add(op.sendAsset, op.sendAmount);
|
|
38
|
+
break;
|
|
39
|
+
case "pathPaymentStrictReceive":
|
|
40
|
+
add(op.sendAsset, op.sendMax);
|
|
41
|
+
s.uncertain = true;
|
|
42
|
+
break;
|
|
43
|
+
case "manageSellOffer":
|
|
44
|
+
case "createPassiveSellOffer":
|
|
45
|
+
add(op.selling, op.amount);
|
|
46
|
+
break;
|
|
47
|
+
case "createAccount":
|
|
48
|
+
s.other = true;
|
|
49
|
+
s.xlm += toStroops(op.startingBalance) ?? 0n;
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
if (UNSUPPORTED.has(op.type))
|
|
53
|
+
s.unsupported = true;
|
|
54
|
+
else if (!NO_SPEND.has(op.type))
|
|
55
|
+
s.other = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return s;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=spend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spend.js","sourceRoot":"","sources":["../src/spend.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAExD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAQ,EAAW,EAAE,CAChD,CAAC,CAAC,OAAO,EAAE,KAAK,SAAS,IAAI,CAAC,CAAC,SAAS,EAAE,KAAK,WAAW,CAAC;AAe7D,iEAAiE;AACjE,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC;IACvB,aAAa,EAAE,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc;IAC5F,uBAAuB,EAAE,+BAA+B,EAAE,6BAA6B;IACvF,mBAAmB;CACpB,CAAC,CAAC;AAEH,qHAAqH;AACrH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC;IAC1B,gBAAgB,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,cAAc;CACnF,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,GAA8B;IACzD,MAAM,CAAC,GAAU,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IAC3F,MAAM,GAAG,GAAG,CAAC,KAAY,EAAE,MAAc,EAAE,EAAE;QAC3C,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAClC,IAAI,YAAY,CAAC,KAAK,CAAC;YAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;aAChC,CAAC;YACJ,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC;YACf,IAAI,KAAK,CAAC,QAAQ,EAAE;gBAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC;IACF,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,SAAS;gBAAE,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;gBAAC,MAAM;YAChD,KAAK,uBAAuB;gBAAE,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC;gBAAC,MAAM;YACtE,KAAK,0BAA0B;gBAC7B,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;gBAC9B,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC;gBACnB,MAAM;YACR,KAAK,iBAAiB,CAAC;YACvB,KAAK,wBAAwB;gBAAE,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;gBAAC,MAAM;YACjE,KAAK,eAAe;gBAClB,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC;gBACf,CAAC,CAAC,GAAG,IAAI,SAAS,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;gBAC7C,MAAM;YACR;gBACE,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC;oBAAE,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC;qBAC9C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC;oBAAE,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC;QACpD,CAAC;IACH,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FetchLike, Foundation } from "./ports.js";
|
|
2
|
+
/**
|
|
3
|
+
* SEP-1 domain verification: true only if `foundation.account` appears in the
|
|
4
|
+
* ACCOUNTS list of https://<domain>/.well-known/stellar.toml. Fails closed:
|
|
5
|
+
* no redirects, 5s timeout, body capped at 100KB (checked on Content-Length and while streaming).
|
|
6
|
+
* "Verified" means exactly this, nothing more.
|
|
7
|
+
*/
|
|
8
|
+
export declare function verifyFoundation(foundation: Foundation, fetchImpl?: FetchLike): Promise<boolean>;
|
|
9
|
+
/** Filters candidates down to those whose stellar.toml lists their account. */
|
|
10
|
+
export declare function listVerifiedFoundations(candidates: readonly Foundation[], fetchImpl?: FetchLike): Promise<Foundation[]>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { parse } from "smol-toml";
|
|
2
|
+
import { MAX_TOML_BYTES } from "./constants.js";
|
|
3
|
+
const HOSTNAME_RE = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
|
|
4
|
+
/** Reads the body as UTF-8 text, giving up (null) as soon as it exceeds `max` bytes. */
|
|
5
|
+
async function readCapped(res, max) {
|
|
6
|
+
const declared = res.headers.get("content-length");
|
|
7
|
+
if (declared !== null && Number(declared) > max)
|
|
8
|
+
return null;
|
|
9
|
+
if (!res.body)
|
|
10
|
+
return "";
|
|
11
|
+
const reader = res.body.getReader();
|
|
12
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
13
|
+
let received = 0;
|
|
14
|
+
let text = "";
|
|
15
|
+
for (;;) {
|
|
16
|
+
const { done, value } = await reader.read();
|
|
17
|
+
if (done)
|
|
18
|
+
break;
|
|
19
|
+
received += value.byteLength;
|
|
20
|
+
if (received > max) {
|
|
21
|
+
await reader.cancel().catch(() => undefined);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
text += decoder.decode(value, { stream: true });
|
|
25
|
+
}
|
|
26
|
+
return text + decoder.decode();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* SEP-1 domain verification: true only if `foundation.account` appears in the
|
|
30
|
+
* ACCOUNTS list of https://<domain>/.well-known/stellar.toml. Fails closed:
|
|
31
|
+
* no redirects, 5s timeout, body capped at 100KB (checked on Content-Length and while streaming).
|
|
32
|
+
* "Verified" means exactly this, nothing more.
|
|
33
|
+
*/
|
|
34
|
+
export async function verifyFoundation(foundation, fetchImpl = globalThis.fetch) {
|
|
35
|
+
if (!HOSTNAME_RE.test(foundation.domain))
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetchImpl(`https://${foundation.domain}/.well-known/stellar.toml`, {
|
|
39
|
+
redirect: "error",
|
|
40
|
+
signal: AbortSignal.timeout(5000),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok || res.redirected)
|
|
43
|
+
return false;
|
|
44
|
+
const text = await readCapped(res, MAX_TOML_BYTES);
|
|
45
|
+
if (text === null)
|
|
46
|
+
return false;
|
|
47
|
+
const accounts = parse(text)["ACCOUNTS"];
|
|
48
|
+
return Array.isArray(accounts) && accounts.includes(foundation.account);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Filters candidates down to those whose stellar.toml lists their account. */
|
|
55
|
+
export async function listVerifiedFoundations(candidates, fetchImpl = globalThis.fetch) {
|
|
56
|
+
const checks = await Promise.all(candidates.map((c) => verifyFoundation(c, fetchImpl)));
|
|
57
|
+
return candidates.filter((_, i) => checks[i]);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=stellar-toml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stellar-toml.js","sourceRoot":"","sources":["../src/stellar-toml.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FeeBumpTransaction, Transaction } from "@stellar/stellar-sdk";
|
|
2
|
+
import type { AccountPort, FetchLike, Foundation } from "./ports.js";
|
|
3
|
+
/** The tx is left untouched: Spare cannot safely modify it. */
|
|
4
|
+
export type PassthroughReason = "fee-bump" | "already-signed" | "soroban" | "multiple-sources" | "multisig" | "extra-signers";
|
|
5
|
+
/** The tx is fine but a donation must not be offered. */
|
|
6
|
+
export type NotOfferedReason = "invalid-options" | "invalid-app-id" | "invalid-amount" | "invalid-foundation" | "amount-over-cap" | "round-up-needs-usdc-purchase" | "round-up-already-whole" | "foundation-not-verified" | "foundation-trustline-missing" | "foundation-trustline-unauthorized" | "foundation-trustline-full" | "user-trustline-missing" | "user-trustline-unauthorized" | "unsupported-operation" | "multisig" | "insufficient-balance" | "account-lookup-failed" | "main-tx-not-successful";
|
|
7
|
+
export interface DonationInfo {
|
|
8
|
+
/** Amount in USDC, 7 decimals. Show this to the user before signing. */
|
|
9
|
+
amount: string;
|
|
10
|
+
amountStroops: bigint;
|
|
11
|
+
foundation: string;
|
|
12
|
+
/** False when the tx already had a memo, so the donation cannot be tagged for the app. */
|
|
13
|
+
attributed: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Round-up only (addDonation): the USDC purchase the tx already makes, and that plus the
|
|
16
|
+
* donation. Show these instead of recomputing them.
|
|
17
|
+
*/
|
|
18
|
+
purchase?: string;
|
|
19
|
+
purchaseStroops?: bigint;
|
|
20
|
+
roundedTotal?: string;
|
|
21
|
+
roundedTotalStroops?: bigint;
|
|
22
|
+
}
|
|
23
|
+
export type DonationResult = {
|
|
24
|
+
status: "offered";
|
|
25
|
+
transaction: Transaction;
|
|
26
|
+
donation: DonationInfo;
|
|
27
|
+
fee: bigint;
|
|
28
|
+
} | {
|
|
29
|
+
status: "not-offered";
|
|
30
|
+
reason: NotOfferedReason;
|
|
31
|
+
transaction: Transaction;
|
|
32
|
+
} | {
|
|
33
|
+
status: "passthrough";
|
|
34
|
+
reason: PassthroughReason;
|
|
35
|
+
transaction: Transaction | FeeBumpTransaction;
|
|
36
|
+
};
|
|
37
|
+
export interface CommonOptions {
|
|
38
|
+
foundation: Foundation;
|
|
39
|
+
appId: string;
|
|
40
|
+
/** App-set cap per donation, in USDC. */
|
|
41
|
+
maxAmount: string;
|
|
42
|
+
accounts: AccountPort;
|
|
43
|
+
/** Defaults to global fetch. */
|
|
44
|
+
fetch?: FetchLike;
|
|
45
|
+
/**
|
|
46
|
+
* Per-operation fee rate in stroops (digits only, minimum 100). Defaults to the tx's own
|
|
47
|
+
* per-op rate (addDonation) or 100 (donateAfter). Total fee = rate x operation count.
|
|
48
|
+
* Anything malformed yields `not-offered` / `invalid-options`.
|
|
49
|
+
*/
|
|
50
|
+
baseFee?: string;
|
|
51
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spare-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Add a per-transaction USDC micro-donation to any Stellar app.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"smol-toml": "^1.9.1"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@stellar/stellar-sdk": "^17.4.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@stellar/stellar-sdk": "^17.4.0",
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"typescript": "^5.9.0",
|
|
31
|
+
"vitest": "^3.2.0"
|
|
32
|
+
},
|
|
33
|
+
"author": "Jesús Chávez Chávez",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/thesatieofcoding/Spare.git",
|
|
37
|
+
"directory": "packages/spare-sdk"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/thesatieofcoding/Spare/tree/main/packages/spare-sdk#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/thesatieofcoding/Spare/issues"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"stellar",
|
|
48
|
+
"donations",
|
|
49
|
+
"usdc",
|
|
50
|
+
"soroban",
|
|
51
|
+
"wallets-kit"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -p tsconfig.build.json",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
57
|
+
"lint": "tsc --noEmit -p tsconfig.json"
|
|
58
|
+
}
|
|
6
59
|
}
|