UNPKG

@tangle-network/tcloud

Version:

TypeScript SDK and CLI for Tangle Router, Sandbox, model routing, and agent service calls

311 lines (310 loc) • 8.95 kB
import { n as TCloudClient } from "./client-BnSLiyHL.js"; import { privateKeyToAccount } from "viem/accounts"; import { concat, encodeAbiParameters, keccak256, parseAbiParameters, toBytes } from "viem"; //#region src/shielded.ts /** * tcloud/shielded — Ephemeral wallet generation, SpendAuth signing, private inference. * * import { TCloud } from 'tcloud' * const client = TCloud.shielded() // anonymous, no API key */ const SPEND_TYPEHASH = keccak256(toBytes("SpendAuthorization(bytes32 commitment,uint64 serviceId,uint8 jobIndex,uint256 amount,address operator,uint256 nonce,uint64 expiry)")); const DEFAULT_DOMAIN = { name: "ShieldedCredits", version: "1" }; /** Generate a new ephemeral shielded wallet */ function generateWallet() { const privateKeyBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)); const privateKey = "0x" + Array.from(privateKeyBytes).map((b) => b.toString(16).padStart(2, "0")).join(""); const saltBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)); const salt = "0x" + Array.from(saltBytes).map((b) => b.toString(16).padStart(2, "0")).join(""); const account = privateKeyToAccount(privateKey); const commitment = keccak256(encodeAbiParameters(parseAbiParameters("address, bytes32"), [account.address, salt])); return { privateKey, address: account.address, commitment, salt }; } /** Sign a SpendAuth for a request */ async function signSpendAuth(wallet, params) { const account = privateKeyToAccount(wallet.privateKey); const domainSeparator = keccak256(encodeAbiParameters(parseAbiParameters("bytes32, bytes32, bytes32, uint256, address"), [ keccak256(toBytes("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")), keccak256(toBytes(DEFAULT_DOMAIN.name)), keccak256(toBytes(DEFAULT_DOMAIN.version)), BigInt(params.chainId), params.creditsAddress ])); const structHash = keccak256(encodeAbiParameters(parseAbiParameters("bytes32, bytes32, uint64, uint8, uint256, address, uint256, uint64"), [ SPEND_TYPEHASH, wallet.commitment, params.serviceId, params.jobIndex, params.amount, params.operator, params.nonce, params.expiry ])); const digest = keccak256(concat([ toBytes("0x1901"), toBytes(domainSeparator), toBytes(structHash) ])); const signature = await account.sign({ hash: digest }); return { commitment: wallet.commitment, serviceId: params.serviceId.toString(), jobIndex: params.jobIndex, amount: params.amount.toString(), operator: params.operator, nonce: params.nonce.toString(), expiry: params.expiry.toString(), signature }; } /** Estimate cost in tsUSD base units (6 decimals) */ function estimateCost(inputTokens, maxOutputTokens, inputPricePerM = .15, outputPricePerM = .6) { const cost = inputTokens / 1e6 * inputPricePerM + maxOutputTokens / 1e6 * outputPricePerM; return BigInt(Math.ceil(cost * 1e6)); } /** * Create a shielded TCloudClient that signs SpendAuth automatically. * Every request is anonymous — no API key, no identity. */ function createShieldedClient(config = {}) { const wallet = config.wallet || generateWallet(); const chainId = config.chainId || 3799; const creditsAddress = config.creditsAddress || "0x0000000000000000000000000000000000000000"; const operatorAddress = config.operatorAddress || "0x0000000000000000000000000000000000000000"; const serviceId = config.serviceId || 1n; let nonce = 0n; const client = new TCloudClient({ ...config, apiKey: void 0 }); client.setSpendAuthSigner(async () => { const currentNonce = nonce++; const amount = estimateCost(500, 4096); const buffered = amount + amount / 5n; return signSpendAuth(wallet, { serviceId, jobIndex: 0, amount: buffered, operator: operatorAddress, nonce: currentNonce, expiry: BigInt(Math.floor(Date.now() / 1e3) + 300), chainId, creditsAddress }); }); const monitor = { lastBalance: 0n, timer: null, replenishing: false }; if (config.autoReplenish) { const ar = config.autoReplenish; const intervalMs = ar.checkIntervalMs ?? 3e4; const check = async () => { try { const balance = await fetchBalance(wallet.commitment, creditsAddress, chainId); monitor.lastBalance = balance; if (balance < ar.minBalance && !monitor.replenishing) { monitor.replenishing = true; try { if (ar.fundingSource === "relayer") await replenishViaRelayer(ar.relayerUrl, wallet.commitment, wallet.privateKey); else await replenishDirect(ar.fundingWalletKey, ar.tokenAddress, ar.replenishAmount, wallet.commitment, wallet.address, creditsAddress, chainId); monitor.lastBalance = await fetchBalance(wallet.commitment, creditsAddress, chainId); console.log(`[tcloud/shielded] replenished. balance=${monitor.lastBalance}`); } finally { monitor.replenishing = false; } } } catch (err) { console.error("[tcloud/shielded] auto-replenish error:", err instanceof Error ? err.message : String(err)); } }; check(); monitor.timer = setInterval(() => void check(), intervalMs); } function stopAutoReplenish() { if (monitor.timer) { clearInterval(monitor.timer); monitor.timer = null; } } return Object.assign(client, { wallet, stopAutoReplenish }); } const GET_ACCOUNT_ABI = [{ type: "function", name: "getAccount", inputs: [{ name: "commitment", type: "bytes32" }], outputs: [{ name: "", type: "tuple", components: [ { name: "spendingKey", type: "address" }, { name: "token", type: "address" }, { name: "balance", type: "uint256" }, { name: "totalFunded", type: "uint256" }, { name: "totalSpent", type: "uint256" }, { name: "nonce", type: "uint256" } ] }], stateMutability: "view" }]; const FUND_CREDITS_ABI = [{ type: "function", name: "fundCredits", inputs: [ { name: "token", type: "address" }, { name: "amount", type: "uint256" }, { name: "commitment", type: "bytes32" }, { name: "spendingKey", type: "address" } ], outputs: [], stateMutability: "nonpayable" }]; const ERC20_APPROVE_ABI = [{ type: "function", name: "approve", inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }], outputs: [{ name: "", type: "bool" }], stateMutability: "nonpayable" }]; function makeChain(chainId, rpcUrl) { return { id: chainId, name: `chain-${chainId}`, nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [rpcUrl] } } }; } function getRpcUrl(chainId) { if (chainId === 3799) return "https://testnet-rpc.tangle.tools"; if (chainId === 5845) return "https://rpc.tangle.tools"; return "http://localhost:8545"; } async function fetchBalance(commitment, creditsAddress, chainId) { const { createPublicClient, http } = await import("viem"); const rpcUrl = getRpcUrl(chainId); return (await createPublicClient({ chain: makeChain(chainId, rpcUrl), transport: http(rpcUrl) }).readContract({ address: creditsAddress, abi: GET_ACCOUNT_ABI, functionName: "getAccount", args: [commitment] })).balance; } async function replenishViaRelayer(relayerUrl, commitment, spendingKey) { const res = await fetch(`${relayerUrl.replace(/\/$/, "")}/relay/fund-credits`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ anchorProof: { proof: "0x", auxPublicInputs: "0x", externalData: "0x", publicInputs: "0x", encryptions: "0x" }, commitment, spendingKey }) }); if (!res.ok) { const body = await res.text(); throw new Error(`relayer fund-credits failed (${res.status}): ${body}`); } } async function replenishDirect(fundingKey, tokenAddress, amount, commitment, spendingKeyAddress, creditsAddress, chainId) { const { createPublicClient, createWalletClient, http } = await import("viem"); const { privateKeyToAccount: toAccount } = await import("viem/accounts"); const rpcUrl = getRpcUrl(chainId); const chain = makeChain(chainId, rpcUrl); const account = toAccount(fundingKey); const pub = createPublicClient({ chain, transport: http(rpcUrl) }); const wal = createWalletClient({ account, chain, transport: http(rpcUrl) }); const approveHash = await wal.writeContract({ address: tokenAddress, abi: ERC20_APPROVE_ABI, functionName: "approve", args: [creditsAddress, amount] }); await pub.waitForTransactionReceipt({ hash: approveHash }); const fundHash = await wal.writeContract({ address: creditsAddress, abi: FUND_CREDITS_ABI, functionName: "fundCredits", args: [ tokenAddress, amount, commitment, spendingKeyAddress ] }); if ((await pub.waitForTransactionReceipt({ hash: fundHash })).status !== "success") throw new Error(`fundCredits reverted (tx: ${fundHash})`); } //#endregion export { createShieldedClient, estimateCost, generateWallet, signSpendAuth };