feat(gateway): OpenAI-compatible custodial gateway

New multi-tenant gateway package: external developers point any OpenAI SDK at
the marketplace. Each API key is its own custodial buyer wallet (AES-256-GCM
sealed under GATEWAY_MASTER_KEY); every request posts an on-chain escrow from
that user's wallet.

- POST /openai/v1/chat/completions (one escrow/call, llm.text.generate.v1,
  buffered + pseudo-stream), chat sessions (one escrow/session, llm.chat.v1,
  real SSE), GET /openai/v1/models, /signup, /account, /account/withdraw.
- Settles by resolving the Submitted ref via the indexer + awaited Accept;
  state-aware reclaim sweeper + per-wallet auto-consolidation for collateral.
- Reuses the buyer Marketplace SDK via new buyer exports ./sdk; new shared
  buildWithdrawTx (custodial exit). Runs via tsx; Dockerfile + mainnet compose.

Design in docs/gateway.md. Typecheck passes for all packages; 20 vitest tests
pass. Not yet exercised against a live chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
david
2026-06-22 10:19:12 +02:00
parent 900b2259e0
commit 6de62b8cf0
41 changed files with 3165 additions and 0 deletions

View File

@@ -38,3 +38,10 @@ export type {
ConsolidateWalletFlowResult,
} from "./wallet/runConsolidateWallet.js";
export { runConsolidateWallet } from "./wallet/runConsolidateWallet.js";
// ── Wallet withdraw (custodial exit; live-only) ─────────────────────────
export type {
WithdrawWalletParams,
WithdrawWalletBuildResult,
} from "./wallet/withdrawWallet.js";
export { buildWithdrawTx } from "./wallet/withdrawWallet.js";

View File

@@ -0,0 +1,81 @@
/**
* withdrawWallet.ts — custodial-exit tx builder (wallet → external address).
*
* Mirrors consolidateWallet.ts but pays a chosen amount to an arbitrary bech32
* address (lucid returns the change to the wallet). This is the withdrawal path
* for the gateway's custodial wallets: a user can move their unspent AP3X back
* out. No script, no datum, no native assets.
*
* `amountLovelace` omitted ⇒ withdraw all available minus a fee reserve.
* Live-only (needs a real Ogmios endpoint).
*/
import type { LiveOgmiosProvider } from "../../chain/LiveOgmiosProvider.js";
import type { BuildResult, WalletKey } from "../types.js";
import { TxConstructionError } from "../types.js";
import { OgmiosLucidProvider } from "../../chain/OgmiosLucidProvider.js";
import { createLucidContext } from "../internal/lucidContext.js";
import { DEFAULT_FEE_RESERVE } from "./planConsolidate.js";
export interface WithdrawWalletParams {
chain: LiveOgmiosProvider;
walletKey: WalletKey;
/** Destination bech32 address. */
toAddress: string;
/** Lovelace to send. Omit to withdraw all available minus the fee reserve. */
amountLovelace?: bigint;
}
export interface WithdrawWalletBuildResult extends BuildResult {
amountLovelace: bigint;
toAddress: string;
}
export async function buildWithdrawTx(
params: WithdrawWalletParams,
): Promise<WithdrawWalletBuildResult> {
const { chain, walletKey, toAddress } = params;
const provider = new OgmiosLucidProvider({ ogmiosUrl: chain.url, fetch: chain.fetchImpl });
const { lucid } = await createLucidContext(
provider,
walletKey,
{ networkId: 1, systemStartUnix: 0, slotLengthMs: 1000 },
{ usePresetProtocolParameters: true },
);
const realWalletUtxos = await lucid.wallet().getUtxos();
const total = realWalletUtxos.reduce((sum, u) => sum + (u.assets.lovelace ?? 0n), 0n);
const spendable = total - DEFAULT_FEE_RESERVE;
const amount = params.amountLovelace ?? spendable;
if (amount <= 0n) {
throw new TxConstructionError("withdraw_amount_invalid", "nothing to withdraw");
}
if (amount > spendable) {
throw new TxConstructionError(
"withdraw_insufficient",
`requested ${amount} exceeds withdrawable ${spendable} (total ${total} minus fee reserve)`,
);
}
let signed;
try {
const txBuilder = lucid
.newTx()
.pay.ToAddress(toAddress, { lovelace: amount })
.addSignerKey(walletKey.pubKeyHash)
.setMinFee(500_000n);
const completed = await txBuilder.complete({ presetWalletInputs: realWalletUtxos, localUPLCEval: false });
signed = await completed.sign.withWallet().complete();
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new TxConstructionError("withdraw_build_failed", msg);
}
const txCborHex = signed.toCBOR();
const expectedTxHash = signed.toHash();
await chain.submitTx(txCborHex);
return { txCborHex, expectedTxHash, amountLovelace: amount, toAddress };
}