openzoo 0.48.23 → 0.48.24
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/lib/info.js +70 -12
- package/lib/pay.js +60 -0
- package/lib/wrap.js +18 -4
- package/package.json +1 -1
package/lib/info.js
CHANGED
|
@@ -24,8 +24,53 @@ function fmtUi(raw, decimals) {
|
|
|
24
24
|
* price is known ($1 stables); everything else gets an honest `?`.
|
|
25
25
|
* A chain whose RPC does not answer prints as unreachable — never as zero.
|
|
26
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* USD per whole token, by SYMBOL, straight from the 402 quote.
|
|
29
|
+
*
|
|
30
|
+
* The quote already carries `extra.tokenUsd` for every asset it settles in —
|
|
31
|
+
* the very "priced at the 402" this file kept promising in a footnote while
|
|
32
|
+
* printing `$?`. So a wallet holding 845,486 TOKEN summed to "≈ $0.11 known".
|
|
33
|
+
* Symbols come back wrapped (wTOKENx, wLEOSx, wUSDGx); strip the wrapper so a
|
|
34
|
+
* row for the plain token the user actually holds finds its price.
|
|
35
|
+
*/
|
|
36
|
+
async function quotedPrices() {
|
|
37
|
+
const out = {};
|
|
38
|
+
try {
|
|
39
|
+
// Imported here, not at module scope, matching affordableUsd below — this
|
|
40
|
+
// file is loaded by `openzoo address`, which must work with no network.
|
|
41
|
+
const { withNamespace } = await import('./namespace.js');
|
|
42
|
+
const r = await fetch(`${config.apiBase}/v1/credits/topup`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
45
|
+
body: JSON.stringify({ usd: 1 }),
|
|
46
|
+
});
|
|
47
|
+
if (r.status !== 402) return out;
|
|
48
|
+
const ch = await r.json().catch(() => ({}));
|
|
49
|
+
for (const row of ch.accepts || []) {
|
|
50
|
+
const usd = Number(row?.extra?.tokenUsd);
|
|
51
|
+
const sym = String(row?.extra?.symbol || '');
|
|
52
|
+
if (!sym || !Number.isFinite(usd)) continue;
|
|
53
|
+
// wTOKENx -> TOKEN, wUSDGx -> USDG, yUSDCx -> USDC
|
|
54
|
+
const plain = sym.replace(/^[wy]/, '').replace(/x$/, '').toUpperCase();
|
|
55
|
+
out[sym.toUpperCase()] = usd;
|
|
56
|
+
if (!(plain in out)) out[plain] = usd;
|
|
57
|
+
}
|
|
58
|
+
} catch { /* offline: fall back to printing what we know */ }
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
27
62
|
export async function printBalance() {
|
|
28
63
|
const { keypair, evmPrivateKey } = loadOrCreateWallet();
|
|
64
|
+
const px = await quotedPrices();
|
|
65
|
+
const priceOf = (sym) => px[String(sym).toUpperCase()];
|
|
66
|
+
const show = (sym, ui) => {
|
|
67
|
+
const p = priceOf(sym);
|
|
68
|
+
if (p == null) return ' ($?)';
|
|
69
|
+
const v = ui * p;
|
|
70
|
+
// Sub-cent holdings read as "$0.00", which is indistinguishable from
|
|
71
|
+
// worthless; show enough digits that a real balance is visible.
|
|
72
|
+
return ` ($${v >= 0.01 || v === 0 ? v.toFixed(2) : v.toFixed(6)})`;
|
|
73
|
+
};
|
|
29
74
|
const evmAddress = privateKeyToAccount(evmPrivateKey).address;
|
|
30
75
|
const connection = new Connection(config.rpcUrl, 'confirmed');
|
|
31
76
|
|
|
@@ -34,14 +79,17 @@ export async function printBalance() {
|
|
|
34
79
|
Promise.all(FUNDING_ASSETS.map((a) => tokenBalance(connection, keypair.publicKey, a.mint))),
|
|
35
80
|
connection.getBalance(keypair.publicKey),
|
|
36
81
|
]);
|
|
37
|
-
|
|
38
|
-
|
|
82
|
+
// Seeded at zero: every asset is priced from the quote below, so seeding
|
|
83
|
+
// with USDC would count it twice.
|
|
84
|
+
let knownUsd = 0;
|
|
39
85
|
let anyFunds = balances.some((b) => b.raw);
|
|
40
86
|
|
|
41
87
|
console.log(`Solana — ${keypair.publicKey.toBase58()}`);
|
|
42
88
|
FUNDING_ASSETS.forEach((a, i) => {
|
|
43
|
-
const
|
|
44
|
-
|
|
89
|
+
const ui = balances[i].ui ?? 0;
|
|
90
|
+
const p = priceOf(a.symbol);
|
|
91
|
+
if (p != null) knownUsd += ui * p;
|
|
92
|
+
console.log(` ${a.symbol.padEnd(11)}: ${ui}${show(a.symbol, ui)}`);
|
|
45
93
|
});
|
|
46
94
|
console.log(` ${'SOL'.padEnd(11)}: ${lamports / 1e9} (gas — optional, payments are sponsored)`);
|
|
47
95
|
|
|
@@ -57,9 +105,9 @@ export async function printBalance() {
|
|
|
57
105
|
assets.forEach((a, i) => {
|
|
58
106
|
const ui = fmtUi(raws[i], a.decimals);
|
|
59
107
|
if (raws[i] > 0n) anyFunds = true;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
console.log(` ${a.symbol.padEnd(11)}: ${ui}${
|
|
108
|
+
const p = priceOf(a.symbol) ?? a.usd;
|
|
109
|
+
if (p != null) knownUsd += ui * p;
|
|
110
|
+
console.log(` ${a.symbol.padEnd(11)}: ${ui}${p != null ? show(a.symbol, ui) : ' ($?)'}`);
|
|
63
111
|
});
|
|
64
112
|
console.log(` ${'ETH'.padEnd(11)}: ${fmtUi(native, 18)} (gas — optional, payments are sponsored)`);
|
|
65
113
|
} catch {
|
|
@@ -67,7 +115,7 @@ export async function printBalance() {
|
|
|
67
115
|
}
|
|
68
116
|
}
|
|
69
117
|
|
|
70
|
-
console.log(`value : ≈ $${knownUsd.toFixed(2)}
|
|
118
|
+
console.log(`value : ≈ $${knownUsd.toFixed(2)}${Object.keys(px).length ? ' (every asset priced at the 402)' : ' known (402 unreachable — unpriced assets shown as $?)'}`);
|
|
71
119
|
if (!anyFunds) {
|
|
72
120
|
console.log(`fund : ${fundingLine(keypair.publicKey.toBase58())}`);
|
|
73
121
|
console.log(` or USDC on Base to ${evmAddress}`);
|
|
@@ -110,10 +158,20 @@ export async function affordableUsd() {
|
|
|
110
158
|
const perUsd = BigInt(row.maxAmountRequired || '0');
|
|
111
159
|
if (perUsd <= 0n) continue;
|
|
112
160
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
161
|
+
// SPENDABLE, not merely wrapped. Every quote names the WRAPPER, and the
|
|
162
|
+
// plain token is converted at payment time (topUpQuotedAsset /
|
|
163
|
+
// acquireWrappedIfNeeded) — so counting only pre-wrapped balance told a
|
|
164
|
+
// user holding 14,847 TOKEN that their wallet "covers only $1.0230" and
|
|
165
|
+
// sent them off to fund an account that was already funded.
|
|
166
|
+
let usd;
|
|
167
|
+
if (client.spendableUsdForAccept) {
|
|
168
|
+
usd = await client.spendableUsdForAccept(row);
|
|
169
|
+
} else {
|
|
170
|
+
const bal = await client.balanceForAccept?.(row);
|
|
171
|
+
const raw = typeof bal === 'bigint' ? bal : BigInt(bal?.raw ?? 0);
|
|
172
|
+
usd = Number(raw * 1000n / perUsd) / 1000;
|
|
173
|
+
}
|
|
174
|
+
if (Number.isFinite(usd) && usd > best) best = usd;
|
|
117
175
|
} catch { /* a rail we cannot read is simply not a candidate */ }
|
|
118
176
|
}
|
|
119
177
|
return best;
|
package/lib/pay.js
CHANGED
|
@@ -150,6 +150,66 @@ export class PayClient {
|
|
|
150
150
|
return BigInt(raw || 0);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* What this wallet can actually PAY with on this rail, in settlement-asset
|
|
155
|
+
* units — wrapped holdings PLUS the plain token it can convert on the fly.
|
|
156
|
+
*
|
|
157
|
+
* balanceForAccept answers a different question: how much of the WRAPPED
|
|
158
|
+
* asset is sitting there right now. Every quote names the wrapper (wTOKENx,
|
|
159
|
+
* the RH vault twins), so a wallet holding 845,486 plain TOKEN read as
|
|
160
|
+
* "$3.14 covered" — only the sliver already wrapped — while `openzoo balance`
|
|
161
|
+
* showed the full holding priced as `$?`. Two different wrong answers about
|
|
162
|
+
* the same wallet, and a user was told to fund an account with $177 in it.
|
|
163
|
+
*
|
|
164
|
+
* buildPaymentFor already wraps what it needs at payment time (topUpQuotedAsset
|
|
165
|
+
* on Solana, acquireWrappedIfNeeded on EVM), so the convertible balance IS
|
|
166
|
+
* spendable and the estimate was simply lying by omission. Advisory only:
|
|
167
|
+
* anything unreadable degrades to the wrapped figure rather than throwing,
|
|
168
|
+
* because this feeds a display and a top-up hint, never a settlement.
|
|
169
|
+
*/
|
|
170
|
+
async spendableUsdForAccept(accept) {
|
|
171
|
+
const perUsd = BigInt(accept.maxAmountRequired || '0');
|
|
172
|
+
if (perUsd <= 0n) return 0;
|
|
173
|
+
const wrappedRaw = await this.balanceForAccept(accept).catch(() => 0n);
|
|
174
|
+
let usd = Number(wrappedRaw * 1000n / perUsd) / 1000;
|
|
175
|
+
|
|
176
|
+
// Price the PLAIN token with the quote's own tokenUsd, NOT with the pool's
|
|
177
|
+
// supply/reserves ratio. That ratio is share accounting, not a price: the
|
|
178
|
+
// live wTOKENx pool reads supply/reserves ≈ 5659, so treating it as a
|
|
179
|
+
// conversion rate valued 845,486 TOKEN at $1,004,132. The 402 already
|
|
180
|
+
// publishes what a whole token is worth, and the wrapper is a claim on the
|
|
181
|
+
// same asset — so underlying and wrapped are priced identically, which is
|
|
182
|
+
// exactly what `openzoo balance` now shows.
|
|
183
|
+
const px = Number(accept?.extra?.tokenUsd);
|
|
184
|
+
if (!Number.isFinite(px) || px <= 0) return usd;
|
|
185
|
+
const rail = railOf(accept);
|
|
186
|
+
try {
|
|
187
|
+
if (rail === 'solana') {
|
|
188
|
+
const pool = await resolvePool(this.connection, accept.asset);
|
|
189
|
+
if (!pool) return usd;
|
|
190
|
+
const under = await tokenBalance(this.connection, this.keypair.publicKey,
|
|
191
|
+
pool.underlying.toBase58());
|
|
192
|
+
usd += Number(under?.ui || 0) * px;
|
|
193
|
+
return usd;
|
|
194
|
+
}
|
|
195
|
+
// EVM wrappers are ERC-4626; asset() names the plain token behind them.
|
|
196
|
+
const { createPublicClient, http } = await import('viem');
|
|
197
|
+
const pc = createPublicClient({ transport: http(evmRpcFor(rail)) });
|
|
198
|
+
const ABI = [
|
|
199
|
+
{ name: 'asset', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'address' }] },
|
|
200
|
+
{ name: 'decimals', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint8' }] },
|
|
201
|
+
];
|
|
202
|
+
const under = await pc.readContract({ address: accept.asset, abi: ABI, functionName: 'asset', args: [] });
|
|
203
|
+
const raw = await evmTokenBalance({ rpcUrl: evmRpcFor(rail), token: under, owner: this.evmAddress });
|
|
204
|
+
if (!raw || raw <= 0n) return usd;
|
|
205
|
+
const dec = Number(accept?.extra?.decimals ?? 18);
|
|
206
|
+
usd += (Number(raw) / 10 ** dec) * px;
|
|
207
|
+
return usd;
|
|
208
|
+
} catch {
|
|
209
|
+
return usd; // not a wrapper, or a rail we cannot read
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
153
213
|
async buildPaymentFor(accept, onStage) {
|
|
154
214
|
const rail = railOf(accept);
|
|
155
215
|
if (rail === 'solana') {
|
package/lib/wrap.js
CHANGED
|
@@ -180,6 +180,19 @@ export async function poolState(connection, pool) {
|
|
|
180
180
|
export function buildWrapInstructions({ pool, owner, depositRaw, rentPayer = owner }) {
|
|
181
181
|
const userWrapped = getAssociatedTokenAddressSync(pool.wrapped, owner, false, pool.wrappedProgram);
|
|
182
182
|
const userUnderlying = getAssociatedTokenAddressSync(pool.underlying, owner, false, pool.underlyingProgram);
|
|
183
|
+
// NINE ACCOUNTS, AND THE PROGRAM PULLS THE DEPOSIT ITSELF.
|
|
184
|
+
//
|
|
185
|
+
// This used to emit three instructions — ensure ATA, Wrap, then a separate
|
|
186
|
+
// TransferChecked moving the underlying into escrow — because the program
|
|
187
|
+
// only minted shares and trusted that the caller's own transfer would follow.
|
|
188
|
+
// Nothing enforced it. On 2026-08-18 a caller sent the Wrap instruction ALONE
|
|
189
|
+
// and minted shares backed by nothing, then unwrapped them: 829,559 TOKEN out
|
|
190
|
+
// of the vault, NAV 1 -> 0.000177.
|
|
191
|
+
//
|
|
192
|
+
// The deployed program (slot 440219442) now CPIs the transfer itself, so the
|
|
193
|
+
// Wrap instruction carries the depositor's source account and signature and
|
|
194
|
+
// the separate transfer is GONE. A 5-account call is rejected outright with
|
|
195
|
+
// NotEnoughAccounts (0x6a) — verified against mainnet by simulation.
|
|
183
196
|
const wrapIx = new TransactionInstruction({
|
|
184
197
|
programId: pool.programId || WRAP_PROGRAM_ID,
|
|
185
198
|
keys: [
|
|
@@ -188,16 +201,17 @@ export function buildWrapInstructions({ pool, owner, depositRaw, rentPayer = own
|
|
|
188
201
|
{ pubkey: userWrapped, isSigner: false, isWritable: true },
|
|
189
202
|
{ pubkey: pool.authority, isSigner: false, isWritable: false },
|
|
190
203
|
{ pubkey: pool.wrappedProgram, isSigner: false, isWritable: false },
|
|
204
|
+
// the deposit the program will pull, and who authorises it
|
|
205
|
+
{ pubkey: userUnderlying, isSigner: false, isWritable: true },
|
|
206
|
+
{ pubkey: owner, isSigner: true, isWritable: false },
|
|
207
|
+
{ pubkey: pool.underlying, isSigner: false, isWritable: false },
|
|
208
|
+
{ pubkey: pool.underlyingProgram, isSigner: false, isWritable: false },
|
|
191
209
|
],
|
|
192
210
|
data: Buffer.concat([Buffer.from([1]), u64le(depositRaw), Buffer.from([pool.bump])]),
|
|
193
211
|
});
|
|
194
212
|
return [
|
|
195
213
|
createAssociatedTokenAccountIdempotentInstruction(rentPayer, userWrapped, owner, pool.wrapped, pool.wrappedProgram),
|
|
196
214
|
wrapIx,
|
|
197
|
-
createTransferCheckedInstruction(
|
|
198
|
-
userUnderlying, pool.underlying, pool.escrow, owner,
|
|
199
|
-
depositRaw, pool.underlyingDecimals, [], pool.underlyingProgram,
|
|
200
|
-
),
|
|
201
215
|
];
|
|
202
216
|
}
|
|
203
217
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.24",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|