@tokagent/tokagentos 2.0.13 → 2.0.15
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/package.json +1 -1
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/package.json +1 -1
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/dashboard/app.js +8 -3
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/credits-routes.ts +42 -5
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/estimate-routes.ts +4 -6
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/topup-routes.ts +35 -8
- package/templates-manifest.json +1 -1
package/package.json
CHANGED
|
@@ -716,10 +716,15 @@ function renderTopBar() {
|
|
|
716
716
|
|
|
717
717
|
function renderKpis() {
|
|
718
718
|
const c = state.credits;
|
|
719
|
-
|
|
719
|
+
// Server returns BOTH nested (c.ledger.balance) and flat (c.balance).
|
|
720
|
+
// Prefer nested for clients that expect the structured shape, fall back
|
|
721
|
+
// to flat for forward/backward compat with the simpler API surface.
|
|
722
|
+
const balance = c?.ledger?.balance ?? c?.balance ?? c?.onChainCredits;
|
|
723
|
+
const reserved = c?.ledger?.reserved ?? c?.reserved ?? 0n;
|
|
724
|
+
const accrued = c?.ledger?.accrued ?? c?.accrued ?? 0n;
|
|
720
725
|
$("#kpi-balance").textContent = fmtPton(balance);
|
|
721
|
-
$("#kpi-reserved").textContent = fmtPton(
|
|
722
|
-
$("#kpi-accrued").textContent = fmtPton(
|
|
726
|
+
$("#kpi-reserved").textContent = fmtPton(reserved) + " PTON";
|
|
727
|
+
$("#kpi-accrued").textContent = fmtPton(accrued) + " PTON";
|
|
723
728
|
$("#kpi-balance-usd").textContent = fmtUsdFromAttoPton(balance, state.tonUsd);
|
|
724
729
|
// Wallet holdings (outside the vault). ETH and PTON share 18 decimals so
|
|
725
730
|
// fmtPton works for both — only the unit label differs.
|
package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/credits-routes.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "../state.js";
|
|
20
20
|
import { resolveBillingIdentity } from "../middleware/api-key-resolve.js";
|
|
21
21
|
import { pickForward, forward, ensureClientReady } from "../lib/forward.js";
|
|
22
|
-
import { creditState } from "@tokagentos/billing";
|
|
22
|
+
import { creditState, hydrate as hydrateCredits, readCredits } from "@tokagentos/billing";
|
|
23
23
|
import { eq } from "drizzle-orm";
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
@@ -63,7 +63,7 @@ async function handleGetCreditsMe(
|
|
|
63
63
|
_runtime: IAgentRuntime,
|
|
64
64
|
): Promise<void> {
|
|
65
65
|
if (!isBillingStateInitialized()) return billingUnavailable(res);
|
|
66
|
-
const { db, config } = getServerBillingState();
|
|
66
|
+
const { db, config, clients } = getServerBillingState();
|
|
67
67
|
if (!config.enabled) return billingUnavailable(res);
|
|
68
68
|
|
|
69
69
|
const identity = await resolveBillingIdentity(toIncomingMessage(req));
|
|
@@ -75,6 +75,35 @@ async function handleGetCreditsMe(
|
|
|
75
75
|
const wallet: Address = identity.wallet;
|
|
76
76
|
const walletKey = wallet.toLowerCase();
|
|
77
77
|
|
|
78
|
+
// Sync on-chain credits → DB ledger BEFORE returning the balance.
|
|
79
|
+
//
|
|
80
|
+
// Why hydrate-on-read instead of a separate deposit watcher service:
|
|
81
|
+
// - depositX402 is now PERMISSIONLESS (any wallet can submit a user's
|
|
82
|
+
// signed EIP-3009 auth) — handleTopupSettle is only ONE possible
|
|
83
|
+
// submitter, so we can't rely on the settle path to credit the DB.
|
|
84
|
+
// - A dedicated event listener could miss events during agent
|
|
85
|
+
// downtime or RPC outages; hydrate-on-read self-heals at request
|
|
86
|
+
// time.
|
|
87
|
+
// - The vault's `credits[user]` mapping is the source of truth.
|
|
88
|
+
// hydrate() reconciles: balance = onChain - (reserved + accrued).
|
|
89
|
+
//
|
|
90
|
+
// Costs: one eth_call per dashboard refresh. Acceptable — this route is
|
|
91
|
+
// not on the hot inference path.
|
|
92
|
+
//
|
|
93
|
+
// Failure handling: if the RPC call throws, fall back to the stale DB
|
|
94
|
+
// row rather than 500ing. The user sees a slightly old balance instead
|
|
95
|
+
// of a broken page.
|
|
96
|
+
try {
|
|
97
|
+
const onChainCredits = await readCredits(
|
|
98
|
+
clients,
|
|
99
|
+
config.vaultAddress,
|
|
100
|
+
wallet,
|
|
101
|
+
);
|
|
102
|
+
await hydrateCredits(db, wallet, onChainCredits);
|
|
103
|
+
} catch (_err) {
|
|
104
|
+
// Swallow — fall back to whatever the DB has. Logged at hydrate level.
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
// Read the credit state row (may not exist for a new wallet).
|
|
79
108
|
const rows = await db
|
|
80
109
|
.select()
|
|
@@ -83,11 +112,19 @@ async function handleGetCreditsMe(
|
|
|
83
112
|
|
|
84
113
|
const row = rows[0];
|
|
85
114
|
|
|
115
|
+
// Response shape: both flat (legacy clients) and nested under `ledger`
|
|
116
|
+
// (the dashboard SPA which reads `c.ledger.balance`). Keeping both keys
|
|
117
|
+
// is cheap and avoids a contract break for any external caller already
|
|
118
|
+
// depending on the flat shape.
|
|
119
|
+
const balance = row ? row.balance.toString() : "0";
|
|
120
|
+
const reserved = row ? row.reserved.toString() : "0";
|
|
121
|
+
const accrued = row ? row.accrued.toString() : "0";
|
|
86
122
|
res.status(200).json({
|
|
87
123
|
wallet,
|
|
88
|
-
balance
|
|
89
|
-
reserved
|
|
90
|
-
accrued
|
|
124
|
+
balance,
|
|
125
|
+
reserved,
|
|
126
|
+
accrued,
|
|
127
|
+
ledger: { balance, reserved, accrued },
|
|
91
128
|
});
|
|
92
129
|
}
|
|
93
130
|
|
package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/estimate-routes.ts
CHANGED
|
@@ -263,7 +263,7 @@ async function handleCountTokens(
|
|
|
263
263
|
* or `{ "available": false }` when no price is cached.
|
|
264
264
|
*/
|
|
265
265
|
async function handlePrice(
|
|
266
|
-
|
|
266
|
+
_req: RouteRequest,
|
|
267
267
|
res: RouteResponse,
|
|
268
268
|
_runtime: IAgentRuntime,
|
|
269
269
|
): Promise<void> {
|
|
@@ -271,11 +271,9 @@ async function handlePrice(
|
|
|
271
271
|
const { config, twapCache } = getBillingState();
|
|
272
272
|
if (!config.enabled) return billingUnavailable(res);
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
274
|
+
// TON/USD is a public oracle read — no identity required. The dashboard
|
|
275
|
+
// KPI shows this rate before the user has signed in, so gating it on
|
|
276
|
+
// SIWE auth would leave the price displayed as "—" until login completes.
|
|
279
277
|
|
|
280
278
|
// 1. Priority override — operator-pinned price freeze.
|
|
281
279
|
// `BILLING_FIXED_TON_USD` is env-only (not exposed in the setup wizard),
|
|
@@ -194,26 +194,53 @@ async function handleTopupQuote(
|
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// Parse optional amountUsd from body; fall back to
|
|
197
|
+
// Parse optional amountPton OR amountUsd from body; fall back to default.
|
|
198
|
+
// Prefer amountPton when both are present — it's the exact value the user
|
|
199
|
+
// signs in the EIP-3009 authorization, and settlement strictly equality-
|
|
200
|
+
// checks signature.value === quote.amountPton. Routing the user's intent
|
|
201
|
+
// through a USD round-trip would round and mismatch the signature.
|
|
198
202
|
const body = req.body as Record<string, unknown> | undefined;
|
|
203
|
+
let amountPton: bigint;
|
|
199
204
|
let amountUsd: number;
|
|
200
|
-
if (body?.["
|
|
205
|
+
if (body?.["amountPton"] !== undefined) {
|
|
206
|
+
const raw = body["amountPton"];
|
|
207
|
+
let parsed: bigint;
|
|
208
|
+
try {
|
|
209
|
+
// Accept either a decimal string (atto-units) or a JS number for
|
|
210
|
+
// backward compat with older clients.
|
|
211
|
+
if (typeof raw === "string") parsed = BigInt(raw);
|
|
212
|
+
else if (typeof raw === "number" && Number.isFinite(raw) && raw > 0)
|
|
213
|
+
parsed = BigInt(Math.floor(raw));
|
|
214
|
+
else throw new Error("amountPton must be a positive decimal string or number (atto-units)");
|
|
215
|
+
} catch (e) {
|
|
216
|
+
res.status(400).json({
|
|
217
|
+
error: e instanceof Error ? e.message : "Invalid amountPton",
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (parsed <= 0n) {
|
|
222
|
+
res.status(400).json({ error: "amountPton must be > 0" });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
amountPton = parsed;
|
|
226
|
+
// Derive USD for the response envelope. Settlement only checks PTON.
|
|
227
|
+
amountUsd = (Number(amountPton) / 1e18) * tonUsd;
|
|
228
|
+
} else if (body?.["amountUsd"] !== undefined) {
|
|
201
229
|
const raw = body["amountUsd"];
|
|
202
230
|
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
|
|
203
231
|
res.status(400).json({ error: "amountUsd must be a positive finite number." });
|
|
204
232
|
return;
|
|
205
233
|
}
|
|
206
234
|
amountUsd = raw;
|
|
235
|
+
amountPton = usdToPton(amountUsd, tonUsd);
|
|
207
236
|
} else {
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
amountUsd = Number(defaultPton) * tonUsd / 1e18;
|
|
237
|
+
// Neither provided — use configured default PTON amount.
|
|
238
|
+
amountPton = config.topupAmountPton;
|
|
239
|
+
amountUsd = Number(amountPton) * tonUsd / 1e18;
|
|
212
240
|
}
|
|
213
241
|
|
|
214
|
-
const amountPton = usdToPton(amountUsd, tonUsd);
|
|
215
242
|
if (amountPton <= 0n) {
|
|
216
|
-
res.status(400).json({ error: "Computed PTON amount is zero — check
|
|
243
|
+
res.status(400).json({ error: "Computed PTON amount is zero — check amount." });
|
|
217
244
|
return;
|
|
218
245
|
}
|
|
219
246
|
|