|
| 1 | +// Full-path HTTP e2e: the viem SDK signs a SpendAuth and calls a running |
| 2 | +// operator (operator-lite) which validates it (EIP-712 recover + on-chain |
| 3 | +// getAccount balance/key check) and proxies the completion to its backend |
| 4 | +// (cli-bridge → a real local coding harness). Proves the entire billed |
| 5 | +// inference path end to end. Reads ../../.env.local; operator at OPERATOR_API. |
| 6 | +// |
| 7 | +// OPERATOR_API=http://127.0.0.1:9100 node sdk/scripts/http-e2e.mjs |
| 8 | + |
| 9 | +import { readFileSync } from 'node:fs' |
| 10 | +import { fileURLToPath } from 'node:url' |
| 11 | +import { dirname, resolve } from 'node:path' |
| 12 | +import { createPublicClient, http, getAddress } from 'viem' |
| 13 | + |
| 14 | +import { |
| 15 | + createInferenceClient, |
| 16 | + createLocalSpendSigner, |
| 17 | +} from '../dist/index.js' |
| 18 | + |
| 19 | +const here = dirname(fileURLToPath(import.meta.url)) |
| 20 | +const env = Object.fromEntries( |
| 21 | + readFileSync(resolve(here, '../../.env.local'), 'utf8') |
| 22 | + .split('\n') |
| 23 | + .map((l) => l.match(/^([A-Z0-9_]+)=(.*)$/)) |
| 24 | + .filter(Boolean) |
| 25 | + .map((m) => [m[1], m[2].trim()]), |
| 26 | +) |
| 27 | + |
| 28 | +const OPERATOR_API = process.env.OPERATOR_API ?? 'http://127.0.0.1:9100' |
| 29 | +const CHAIN_ID = Number(env.CHAIN_ID ?? '31337') |
| 30 | + |
| 31 | +let failed = 0 |
| 32 | +const assert = (cond, desc, detail = '') => |
| 33 | + cond |
| 34 | + ? console.log(` PASS ${desc}`) |
| 35 | + : (failed++, console.error(` FAIL ${desc} ${detail}`)) |
| 36 | + |
| 37 | +const chain = { |
| 38 | + id: CHAIN_ID, |
| 39 | + name: 'anvil', |
| 40 | + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, |
| 41 | + rpcUrls: { default: { http: [env.RPC_URL] } }, |
| 42 | +} |
| 43 | +const publicClient = createPublicClient({ chain, transport: http(env.RPC_URL) }) |
| 44 | + |
| 45 | +const client = createInferenceClient({ |
| 46 | + operatorUrl: OPERATOR_API, |
| 47 | + shieldedCreditsAddress: getAddress(env.SHIELDED_CREDITS), |
| 48 | + chainId: CHAIN_ID, |
| 49 | + commitment: env.COMMITMENT, |
| 50 | + serviceId: 1n, |
| 51 | + operatorAddress: getAddress(env.OPERATOR_ADDR), |
| 52 | + signer: createLocalSpendSigner(env.USER_KEY), |
| 53 | + model: 'claude-code/sonnet', |
| 54 | + pricePerInputToken: 1n, |
| 55 | + pricePerOutputToken: 2n, |
| 56 | +}) |
| 57 | + |
| 58 | +async function main() { |
| 59 | + console.log(`[http-e2e] operator=${OPERATOR_API} chain=${CHAIN_ID}`) |
| 60 | + |
| 61 | + const nonce = await client.syncNonce(publicClient) |
| 62 | + console.log(`[http-e2e] synced spend nonce=${nonce}`) |
| 63 | + |
| 64 | + console.log('[http-e2e] sending SpendAuth-gated chat (real backend, may take a while)…') |
| 65 | + const res = await client.chat( |
| 66 | + [{ role: 'user', content: 'Reply with exactly one word: hello' }], |
| 67 | + { maxTokens: 64 }, |
| 68 | + ) |
| 69 | + |
| 70 | + const content = res?.choices?.[0]?.message?.content ?? '' |
| 71 | + console.log(`[http-e2e] model=${res?.model} content=${JSON.stringify(content)}`) |
| 72 | + console.log(`[http-e2e] usage=${JSON.stringify(res?.usage)}`) |
| 73 | + |
| 74 | + assert(typeof content === 'string' && content.length > 0, 'completion has content') |
| 75 | + assert((res?.usage?.total_tokens ?? 0) > 0, 'usage reports tokens', `usage=${JSON.stringify(res?.usage)}`) |
| 76 | + |
| 77 | + // The operator consumed the SpendAuth → on-chain nonce advanced. |
| 78 | + const after = await client.syncNonce(publicClient) |
| 79 | + assert(after === nonce + 1n, 'on-chain spend nonce advanced after the request', `before=${nonce} after=${after}`) |
| 80 | + |
| 81 | + console.log(failed === 0 ? '\n[http-e2e] ALL PASS' : `\n[http-e2e] ${failed} FAILED`) |
| 82 | + process.exit(failed === 0 ? 0 : 1) |
| 83 | +} |
| 84 | + |
| 85 | +main().catch((err) => { |
| 86 | + console.error('[http-e2e] error:', err?.message ?? err) |
| 87 | + process.exit(1) |
| 88 | +}) |
0 commit comments