@envolvr/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +65 -0
- package/dist/api.d.ts +76 -0
- package/dist/api.js +112 -0
- package/dist/attest.d.ts +58 -0
- package/dist/attest.js +85 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +171 -0
- package/dist/client.d.ts +84 -0
- package/dist/client.js +156 -0
- package/dist/evm.d.ts +41 -0
- package/dist/evm.js +102 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +7 -0
- package/dist/network.d.ts +19 -0
- package/dist/network.js +27 -0
- package/dist/receipts.d.ts +46 -0
- package/dist/receipts.js +100 -0
- package/dist/wallet.d.ts +14 -0
- package/dist/wallet.js +47 -0
- package/package.json +65 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Network } from './network.ts';
|
|
2
|
+
export type Receipt = Record<string, unknown> & {
|
|
3
|
+
receipt_id: string;
|
|
4
|
+
event_log: Array<Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
/** SHA-256 of the receipt's JCS bytes (RFC 8785): the value that is anchored. */
|
|
7
|
+
export declare function receiptDigest(receipt: unknown): `0x${string}`;
|
|
8
|
+
export interface Billing {
|
|
9
|
+
currency: string;
|
|
10
|
+
/** Exact cost as a decimal string, e.g. "0.00014". */
|
|
11
|
+
cost: string;
|
|
12
|
+
/** What was debited: the cost rounded up to the next micro-USD. */
|
|
13
|
+
billedMicroUsd: number;
|
|
14
|
+
rates: Record<string, string>;
|
|
15
|
+
tokens: {
|
|
16
|
+
prompt: number;
|
|
17
|
+
completion: number;
|
|
18
|
+
cache_read: number;
|
|
19
|
+
cache_creation: number;
|
|
20
|
+
};
|
|
21
|
+
payer?: string;
|
|
22
|
+
}
|
|
23
|
+
/** The receipt's billing.charged event, or undefined for an unpriced request. */
|
|
24
|
+
export declare function billingOf(receipt: Receipt): Billing | undefined;
|
|
25
|
+
/** The payer commitment a receipt charged to `apiKey` carries. */
|
|
26
|
+
export declare function payerCommitment(apiKey: string, receiptId: string): string;
|
|
27
|
+
export declare function chargedTo(receipt: Receipt, apiKey: string): boolean;
|
|
28
|
+
/** Check an inclusion proof against a root (OpenZeppelin sorted-pair Merkle tree). */
|
|
29
|
+
export declare function proofLeadsToRoot(digest: string, proof: string[], root: string): boolean;
|
|
30
|
+
export interface Anchor {
|
|
31
|
+
status: 'anchored' | 'pending' | 'unknown';
|
|
32
|
+
digest: string;
|
|
33
|
+
batchIndex?: number;
|
|
34
|
+
leafIndex?: number;
|
|
35
|
+
count?: number;
|
|
36
|
+
root?: string;
|
|
37
|
+
txHash?: string | null;
|
|
38
|
+
/** Unix seconds, read from the contract. */
|
|
39
|
+
anchoredAt?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Is this receipt anchored? The proof comes from envolvr's proof service as
|
|
43
|
+
* data only; the chain, contract and provider come from `network`, and the
|
|
44
|
+
* contract itself is asked.
|
|
45
|
+
*/
|
|
46
|
+
export declare function verifyAnchor(receipt: unknown, network: Network, fetchImpl?: typeof fetch): Promise<Anchor>;
|
package/dist/receipts.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Receipts: what each one says about billing, whether it was charged to you,
|
|
2
|
+
// and whether it is anchored on Robinhood Chain. Nothing here trusts envolvr:
|
|
3
|
+
// digests and commitments are computed locally, and the anchor is checked
|
|
4
|
+
// against the contract itself.
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { keccak_256 } from '@noble/hashes/sha3';
|
|
7
|
+
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
|
|
8
|
+
import canonicalize from 'canonicalize';
|
|
9
|
+
/** SHA-256 of the receipt's JCS bytes (RFC 8785): the value that is anchored. */
|
|
10
|
+
export function receiptDigest(receipt) {
|
|
11
|
+
const jcs = canonicalize(receipt);
|
|
12
|
+
if (jcs === undefined)
|
|
13
|
+
throw new Error('receipt is not JSON');
|
|
14
|
+
return `0x${createHash('sha256').update(jcs).digest('hex')}`;
|
|
15
|
+
}
|
|
16
|
+
/** The receipt's billing.charged event, or undefined for an unpriced request. */
|
|
17
|
+
export function billingOf(receipt) {
|
|
18
|
+
const e = receipt.event_log.find((x) => x.type === 'billing.charged');
|
|
19
|
+
if (!e)
|
|
20
|
+
return undefined;
|
|
21
|
+
return {
|
|
22
|
+
currency: String(e.currency), cost: String(e.cost), billedMicroUsd: Number(e.billed_micro_usd),
|
|
23
|
+
rates: e.rates, tokens: e.tokens,
|
|
24
|
+
payer: e.payer === undefined ? undefined : String(e.payer),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The payer commitment a receipt charged to `apiKey` carries. */
|
|
28
|
+
export function payerCommitment(apiKey, receiptId) {
|
|
29
|
+
const keyHash = createHash('sha256').update(apiKey, 'utf8').digest('hex');
|
|
30
|
+
return `sha256:${createHash('sha256').update(`billing.payer.v1:${keyHash}:${receiptId}`).digest('hex')}`;
|
|
31
|
+
}
|
|
32
|
+
export function chargedTo(receipt, apiKey) {
|
|
33
|
+
return billingOf(receipt)?.payer === payerCommitment(apiKey, receipt.receipt_id);
|
|
34
|
+
}
|
|
35
|
+
// ---- anchoring ----
|
|
36
|
+
const leafOf = (digest) => keccak_256(keccak_256(hexToBytes(digest.replace(/^0x/, ''))));
|
|
37
|
+
function hashPair(a, b) {
|
|
38
|
+
for (let i = 0; i < 32; i++) {
|
|
39
|
+
if (a[i] !== b[i])
|
|
40
|
+
return a[i] < b[i] ? keccak_256(concatBytes(a, b)) : keccak_256(concatBytes(b, a));
|
|
41
|
+
}
|
|
42
|
+
return keccak_256(concatBytes(a, b));
|
|
43
|
+
}
|
|
44
|
+
/** Check an inclusion proof against a root (OpenZeppelin sorted-pair Merkle tree). */
|
|
45
|
+
export function proofLeadsToRoot(digest, proof, root) {
|
|
46
|
+
let node = leafOf(digest);
|
|
47
|
+
for (const sibling of proof)
|
|
48
|
+
node = hashPair(node, hexToBytes(sibling.replace(/^0x/, '')));
|
|
49
|
+
return `0x${bytesToHex(node)}` === root.toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
const selector = (sig) => bytesToHex(keccak_256(utf8ToBytes(sig))).slice(0, 8);
|
|
52
|
+
const word = (v) => (typeof v === 'bigint' ? v.toString(16) : v.replace(/^0x/, '').toLowerCase()).padStart(64, '0');
|
|
53
|
+
/**
|
|
54
|
+
* Is this receipt anchored? The proof comes from envolvr's proof service as
|
|
55
|
+
* data only; the chain, contract and provider come from `network`, and the
|
|
56
|
+
* contract itself is asked.
|
|
57
|
+
*/
|
|
58
|
+
export async function verifyAnchor(receipt, network, fetchImpl = fetch) {
|
|
59
|
+
const digest = receiptDigest(receipt);
|
|
60
|
+
const url = `${network.control}/receipts/${digest}/proof`;
|
|
61
|
+
const res = await fetchImpl(url).catch((err) => {
|
|
62
|
+
throw new Error(`proof service unreachable (${new URL(url).origin}): ${err.message}`);
|
|
63
|
+
});
|
|
64
|
+
if (res.status === 404)
|
|
65
|
+
return { status: 'unknown', digest };
|
|
66
|
+
if (!res.ok)
|
|
67
|
+
throw new Error(`proof service answered HTTP ${res.status}`);
|
|
68
|
+
const p = await res.json();
|
|
69
|
+
if (p.status !== 'anchored')
|
|
70
|
+
return { status: 'pending', digest };
|
|
71
|
+
if (!proofLeadsToRoot(digest, p.proof, p.root))
|
|
72
|
+
throw new Error('the proof does not lead to its root');
|
|
73
|
+
const rpc = async (method, params) => {
|
|
74
|
+
const r = await fetchImpl(network.rpcUrl, {
|
|
75
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
|
|
77
|
+
});
|
|
78
|
+
const body = await r.json();
|
|
79
|
+
if (body.error || body.result === undefined)
|
|
80
|
+
throw new Error(`${method}: ${body.error?.message ?? `HTTP ${r.status}`}`);
|
|
81
|
+
return body.result;
|
|
82
|
+
};
|
|
83
|
+
const chainId = Number(BigInt(await rpc('eth_chainId', [])));
|
|
84
|
+
if (chainId !== network.chainId)
|
|
85
|
+
throw new Error(`RPC is on chain ${chainId}, expected ${network.chainId}`);
|
|
86
|
+
const call = (data) => rpc('eth_call', [{ to: network.receiptAnchor, data }, 'latest']);
|
|
87
|
+
const batch = await call(`0x${selector('batch(bytes32,uint256)')}${word(network.providerId)}${word(BigInt(p.batchIndex))}`);
|
|
88
|
+
const onChainRoot = `0x${batch.slice(2, 66)}`;
|
|
89
|
+
if (onChainRoot.toLowerCase() !== p.root.toLowerCase()) {
|
|
90
|
+
throw new Error(`batch ${p.batchIndex} on chain has root ${onChainRoot}, the proof claims ${p.root}`);
|
|
91
|
+
}
|
|
92
|
+
const verified = await call(`0x${selector('verifyReceipt(bytes32,uint256,bytes32,bytes32[])')}${word(network.providerId)}`
|
|
93
|
+
+ `${word(BigInt(p.batchIndex))}${word(digest)}${word(128n)}${word(BigInt(p.proof.length))}${p.proof.map(word).join('')}`);
|
|
94
|
+
if (BigInt(verified) !== 1n)
|
|
95
|
+
throw new Error('ReceiptAnchor.verifyReceipt returned false');
|
|
96
|
+
return {
|
|
97
|
+
status: 'anchored', digest, batchIndex: p.batchIndex, leafIndex: p.leafIndex, count: p.count, root: p.root,
|
|
98
|
+
txHash: p.txHash ?? null, anchoredAt: Number(BigInt(`0x${batch.slice(194, 258)}`)),
|
|
99
|
+
};
|
|
100
|
+
}
|
package/dist/wallet.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Signer {
|
|
2
|
+
address: string;
|
|
3
|
+
/** EIP-191 personal_sign of a UTF-8 message: 65 bytes as 0x hex. */
|
|
4
|
+
signMessage(message: string): Promise<string>;
|
|
5
|
+
/** Send a transaction and resolve once it is mined; returns its hash. Needed for deposits only. */
|
|
6
|
+
sendTransaction?(tx: {
|
|
7
|
+
to: string;
|
|
8
|
+
data: string;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function privateKeySigner(privateKey: string, opts: {
|
|
12
|
+
rpcUrl: string;
|
|
13
|
+
chainId: number;
|
|
14
|
+
}): Signer;
|
package/dist/wallet.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Wallets. envolvr needs a wallet for two things: signing the sign-in message
|
|
2
|
+
// (EIP-191) and, to deposit USDG, sending two transactions. Bring your own by
|
|
3
|
+
// implementing Signer (for example around viem or ethers), or use
|
|
4
|
+
// privateKeySigner for a plain key.
|
|
5
|
+
import { hexToBytes } from '@noble/hashes/utils';
|
|
6
|
+
import { addressOfKey, personalSign, Rpc, signTx } from "./evm.js";
|
|
7
|
+
export function privateKeySigner(privateKey, opts) {
|
|
8
|
+
const key = hexToBytes(privateKey.replace(/^0x/, ''));
|
|
9
|
+
if (key.length !== 32)
|
|
10
|
+
throw new Error('a private key is 32 bytes of hex');
|
|
11
|
+
const address = addressOfKey(key);
|
|
12
|
+
const rpc = new Rpc(opts.rpcUrl);
|
|
13
|
+
return {
|
|
14
|
+
address,
|
|
15
|
+
async signMessage(message) {
|
|
16
|
+
return personalSign(message, key);
|
|
17
|
+
},
|
|
18
|
+
async sendTransaction({ to, data }) {
|
|
19
|
+
const chainId = BigInt(await rpc.call('eth_chainId', []));
|
|
20
|
+
if (chainId !== BigInt(opts.chainId))
|
|
21
|
+
throw new Error(`RPC is on chain ${chainId}, expected ${opts.chainId}`);
|
|
22
|
+
const call = { from: address, to, data };
|
|
23
|
+
const [nonce, gas, block] = await Promise.all([
|
|
24
|
+
rpc.call('eth_getTransactionCount', [address, 'pending']),
|
|
25
|
+
rpc.call('eth_estimateGas', [call]),
|
|
26
|
+
rpc.call('eth_getBlockByNumber', ['latest', false]),
|
|
27
|
+
]);
|
|
28
|
+
const priority = await rpc.call('eth_maxPriorityFeePerGas', []).then(BigInt, () => 0n);
|
|
29
|
+
const baseFee = BigInt(block.baseFeePerGas ?? (await rpc.call('eth_gasPrice', [])));
|
|
30
|
+
const { raw, hash } = signTx({
|
|
31
|
+
chainId, nonce: BigInt(nonce), maxPriorityFeePerGas: priority, maxFeePerGas: baseFee * 2n + priority,
|
|
32
|
+
gas: (BigInt(gas) * 13n) / 10n, to, value: 0n, data,
|
|
33
|
+
}, key);
|
|
34
|
+
await rpc.call('eth_sendRawTransaction', [raw]);
|
|
35
|
+
for (let i = 0; i < 90; i++) {
|
|
36
|
+
const r = await rpc.call('eth_getTransactionReceipt', [hash]);
|
|
37
|
+
if (r) {
|
|
38
|
+
if (BigInt(r.status) !== 1n)
|
|
39
|
+
throw new Error(`transaction ${hash} reverted`);
|
|
40
|
+
return hash;
|
|
41
|
+
}
|
|
42
|
+
await new Promise((res) => setTimeout(res, 2_000));
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`transaction ${hash} not mined after 3 minutes`);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@envolvr/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "envolvr client: wallet sign-in, USDG deposits, private inference with signed receipts, and receipt verification (signature, billing, on-chain anchor).",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "envolvr",
|
|
7
|
+
"homepage": "https://envolvr.xyz/docs/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/envolvr/envolvr.git",
|
|
11
|
+
"directory": "sdk"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/envolvr/envolvr/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"inference",
|
|
19
|
+
"private",
|
|
20
|
+
"verifiable",
|
|
21
|
+
"tee",
|
|
22
|
+
"tdx",
|
|
23
|
+
"attestation",
|
|
24
|
+
"receipts",
|
|
25
|
+
"agents",
|
|
26
|
+
"openai",
|
|
27
|
+
"robinhood-chain",
|
|
28
|
+
"usdg"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"envolvr": "./dist/cli.js"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc -p tsconfig.json",
|
|
50
|
+
"test": "node --disable-warning=ExperimentalWarning --test test/*.test.ts",
|
|
51
|
+
"prepublishOnly": "tsc -p tsconfig.json"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@noble/curves": "^1.9.7",
|
|
55
|
+
"@noble/hashes": "^1.8.0",
|
|
56
|
+
"canonicalize": "^5.1.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^26.6.2",
|
|
60
|
+
"typescript": "5.9.3"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
}
|
|
65
|
+
}
|