run402-mcp
Version:
MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.
214 lines • 9.23 kB
JavaScript
/**
* `wallets` namespace — the agent's local wallet and its server-side label.
*
* `status`, `create`, and `export` touch the local wallet file (`wallet.json`)
* through optional provider methods; sandbox providers that don't implement
* them report `{configured: false}` or throw a descriptive error. `faucet`
* requests testnet USDC into the wallet and works in any environment.
*
* The label is the cross-machine / WEB-visible human name for a wallet (e.g.
* "kychon"), stored server-side keyed by the wallet address and authenticated
* by the wallet's SIWX signature. It is display metadata only — never key
* material, no custody.
*
* Both label methods are intentionally BEST-EFFORT: they swallow errors (including a
* not-yet-deployed endpoint / 404 / offline) and return a null/`ok:false`
* sentinel instead of throwing. This lets `run402 wallets new|rename|import`
* stay fully functional locally and before the gateway endpoint ships — the
* local folder name is always the source of truth; the server label is a
* mirror that catches up.
*
* Canonical call shape is the wallet-scoped handle: `r.wallet(address)` →
* `.getLabel()` / `.setLabel(label)`. The two-string `r.wallets.setLabel(address,
* label)` form is deprecated (see the `sdk-call-shape-conventions` change).
*/
import { gateSecret } from "../secret-gate.js";
import { LocalError } from "../errors.js";
/** Shared read impl — used by both `Wallets.getLabel` and `ScopedWallet.getLabel`. */
async function readLabel(client, address) {
try {
const res = await client.request(`/wallets/v1/${encodeURIComponent(address)}/label`, { context: "reading wallet label", withAuth: false });
return res?.label ?? null;
}
catch {
return null;
}
}
/** Shared write impl — used by both `Wallets.setLabel` and `ScopedWallet.setLabel`. */
async function putLabel(client, address, label) {
try {
await client.request(`/wallets/v1/${encodeURIComponent(address)}/label`, { method: "POST", context: "setting wallet label", withAuth: true, body: { label } });
return { ok: true };
}
catch {
return { ok: false };
}
}
export class Wallets {
client;
constructor(client) {
this.client = client;
}
/** Inspect the local wallet. Returns `{configured: false}` when absent. */
async status() {
const reader = this.client.credentials.readWallet;
if (!reader) {
return { address: "", configured: false };
}
const data = await reader.call(this.client.credentials);
if (!data)
return { address: "", configured: false };
return {
address: data.address,
created: data.created,
// Map the on-disk `funded` marker (which only tracks faucet invocations)
// to `faucet_used` in the output. `!!` coerces undefined → false so
// callers can branch cleanly.
faucet_used: !!data.funded,
lastFaucet: data.lastFaucet,
path: this.client.credentials.getWalletPath?.call(this.client.credentials),
rail: data.rail ?? "x402",
...(data.lightning
? {
lightning: {
wallet_id: data.lightning.wallet_id,
lightning_address: data.lightning.lightning_address,
budget_sats: data.lightning.budget_sats,
starter_sats: data.lightning.starter_sats,
custody: data.lightning.custody,
has_pairing: true,
minted_at: data.lightning.minted_at,
},
}
: {}),
configured: true,
};
}
/**
* Generate a new wallet keypair and persist it. Throws when the
* provider already holds a wallet (don't overwrite silently) or
* when the provider doesn't support local wallet management.
*/
async create() {
gateSecret(this.client, "wallets.create");
const reader = this.client.credentials.readWallet;
const creator = this.client.credentials.createWallet;
const saver = this.client.credentials.saveWallet;
if (!creator || !saver) {
throw new LocalError("This credential provider does not support wallet creation. Use @run402/sdk/node for local wallet management.", "creating wallet");
}
if (reader) {
const existing = await reader.call(this.client.credentials);
if (existing) {
throw new LocalError(`Wallet already exists at ${this.client.credentials.getWalletPath?.call(this.client.credentials) ?? "(local path unknown)"}. Delete it manually to regenerate.`, "creating wallet");
}
}
const data = await creator.call(this.client.credentials);
await saver.call(this.client.credentials, data);
return {
address: data.address,
created: data.created ?? new Date().toISOString(),
path: this.client.credentials.getWalletPath?.call(this.client.credentials),
};
}
/** Return the wallet address (safe to share). Throws if none is configured. */
async export() {
const reader = this.client.credentials.readWallet;
if (!reader) {
throw new LocalError("This credential provider does not expose the local wallet.", "exporting wallet");
}
const data = await reader.call(this.client.credentials);
if (!data) {
throw new LocalError("No local wallet is configured.", "exporting wallet");
}
return data.address;
}
/**
* Request testnet USDC from the Run402 faucet. When `address` is omitted,
* the SDK uses the provider's local wallet address. Updates the
* wallet's internal `funded` marker on success (surfaced as
* `faucet_used` in `status()`).
*/
async faucet(addressOrOptions) {
const opts = typeof addressOrOptions === "string"
? { address: addressOrOptions }
: addressOrOptions ?? {};
let resolvedAddress = opts.address;
if (!resolvedAddress) {
const reader = this.client.credentials.readWallet;
if (!reader) {
throw new LocalError("No address provided and no local wallet is available.", "requesting faucet funds");
}
const data = await reader.call(this.client.credentials);
if (!data) {
throw new LocalError("No address provided and no local wallet is configured.", "requesting faucet funds");
}
resolvedAddress = data.address;
}
// Wire shape is snake_case (`transaction_hash`, `amount_usd_micros`);
// normalize to the typed camelCase surface.
const wire = await this.client.request("/faucet/v1", {
method: "POST",
...(opts.idempotencyKey ? { headers: { "Idempotency-Key": opts.idempotencyKey } } : {}),
body: { address: resolvedAddress },
withAuth: false,
context: "requesting faucet funds",
});
const result = {
...(wire.funding_status ? { fundingStatus: wire.funding_status } : {}),
transactionHash: wire.transaction_hash,
amountUsdMicros: wire.amount_usd_micros,
amount: (wire.amount_usd_micros / 1_000_000).toFixed(2),
token: wire.token,
network: wire.network,
};
// Best-effort update of the local wallet's funded/lastFaucet fields.
const reader = this.client.credentials.readWallet;
const saver = this.client.credentials.saveWallet;
if (reader && saver) {
try {
const data = await reader.call(this.client.credentials);
if (data) {
await saver.call(this.client.credentials, {
...data,
funded: true,
lastFaucet: new Date().toISOString(),
});
}
}
catch {
// non-fatal — the on-chain transfer succeeded
}
}
return result;
}
/**
* Read a wallet's server-side label, or null on any error (unset, 404,
* offline). Public read — no auth required.
*/
async getLabel(address) {
return readLabel(this.client, address);
}
}
/**
* Wallet-scoped sub-client returned by `r.wallet(address)`. Binds the address
* so neither label method takes it as a swappable positional. Lazy — no key or
* network access at construction.
*/
export class ScopedWallet {
client;
address;
constructor(client, address) {
this.client = client;
this.address = address;
}
/** Read this wallet's server-side label, or null. */
getLabel() {
return readLabel(this.client, this.address);
}
/** Set this wallet's server-side label. Best-effort; never throws. */
setLabel(label) {
return putLabel(this.client, this.address, label);
}
}
//# sourceMappingURL=wallets.js.map