@tangle-network/tcloud
Version:
TypeScript SDK and CLI for Tangle Router, Sandbox, model routing, and agent service calls
315 lines (314 loc) • 9.32 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const require_client = require("./client-BWymW7iu.cjs");
let viem_accounts = require("viem/accounts");
let viem = require("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 = (0, viem.keccak256)((0, viem.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 = (0, viem_accounts.privateKeyToAccount)(privateKey);
const commitment = (0, viem.keccak256)((0, viem.encodeAbiParameters)((0, viem.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 = (0, viem_accounts.privateKeyToAccount)(wallet.privateKey);
const domainSeparator = (0, viem.keccak256)((0, viem.encodeAbiParameters)((0, viem.parseAbiParameters)("bytes32, bytes32, bytes32, uint256, address"), [
(0, viem.keccak256)((0, viem.toBytes)("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")),
(0, viem.keccak256)((0, viem.toBytes)(DEFAULT_DOMAIN.name)),
(0, viem.keccak256)((0, viem.toBytes)(DEFAULT_DOMAIN.version)),
BigInt(params.chainId),
params.creditsAddress
]));
const structHash = (0, viem.keccak256)((0, viem.encodeAbiParameters)((0, viem.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 = (0, viem.keccak256)((0, viem.concat)([
(0, viem.toBytes)("0x1901"),
(0, viem.toBytes)(domainSeparator),
(0, viem.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 require_client.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
exports.createShieldedClient = createShieldedClient;
exports.estimateCost = estimateCost;
exports.generateWallet = generateWallet;
exports.signSpendAuth = signSpendAuth;