@envolvr/sdk 0.1.1 → 0.2.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/README.md +24 -5
- package/dist/api.js +7 -6
- package/dist/cli.js +7 -12
- package/dist/client.d.ts +8 -0
- package/dist/client.js +24 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/network.d.ts +5 -0
- package/dist/network.js +20 -1
- package/dist/receipts.js +6 -5
- package/dist/topup.d.ts +61 -0
- package/dist/topup.js +109 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,12 +11,11 @@ the account with USDG, runs inference, keeps every receipt, and verifies it.
|
|
|
11
11
|
npm install @envolvr/sdk # Node 20+
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
## From zero to a verified receipt
|
|
14
|
+
## From zero to a verified receipt
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
export ENVOLVR_PRIVATE_KEY=0x… # a wallet with a little
|
|
17
|
+
export ENVOLVR_PRIVATE_KEY=0x… # a wallet with USDG and a little ETH on Robinhood Chain
|
|
18
18
|
npx @envolvr/sdk signin --save # sign a message, get an API key
|
|
19
|
-
npx @envolvr/sdk testnet-mint 20 # test USDG (testnet only)
|
|
20
19
|
npx @envolvr/sdk deposit 10 # credited net of the deposit fee, within seconds
|
|
21
20
|
npx @envolvr/sdk chat "Reply with the single word: sealed"
|
|
22
21
|
npx @envolvr/sdk verify --last # signature, billing, payer, on-chain anchor
|
|
@@ -33,9 +32,9 @@ is anchored at the end of its 10-minute slot (UTC).
|
|
|
33
32
|
## In code
|
|
34
33
|
|
|
35
34
|
```ts
|
|
36
|
-
import { Envolvr, privateKeySigner, signIn, depositUsdg,
|
|
35
|
+
import { Envolvr, privateKeySigner, signIn, depositUsdg, MAINNET, toMicros } from '@envolvr/sdk';
|
|
37
36
|
|
|
38
|
-
const wallet = privateKeySigner(process.env.KEY!, { rpcUrl:
|
|
37
|
+
const wallet = privateKeySigner(process.env.KEY!, { rpcUrl: MAINNET.rpcUrl, chainId: MAINNET.chainId });
|
|
39
38
|
const { apiKey } = await signIn(wallet); // keep it: shown once
|
|
40
39
|
await depositUsdg({ signer: wallet, amountMicros: toMicros('10') });
|
|
41
40
|
|
|
@@ -55,6 +54,26 @@ sending and checks every receipt, run the local verifying proxy
|
|
|
55
54
|
`npx private-ai-proxy serve https://api.envolvr.xyz` and point the client at
|
|
56
55
|
`http://127.0.0.1:4180/v1`.
|
|
57
56
|
|
|
57
|
+
### Automatic top-up
|
|
58
|
+
|
|
59
|
+
Keep an agent funded without watching its balance. When the balance falls below
|
|
60
|
+
`below`, the client deposits `amount` from `signer` and
|
|
61
|
+
waits until it is credited; a request refused for insufficient credit is topped
|
|
62
|
+
up and retried once. `maxPerDay` caps the deposits per UTC day (default: one
|
|
63
|
+
top-up). The signer can be the agent's own wallet or another one, which then
|
|
64
|
+
deposits for the agent's account.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const envolvr = new Envolvr({
|
|
68
|
+
apiKey,
|
|
69
|
+
autoTopUp: { signer: wallet, below: '5', amount: '20', maxPerDay: '100', onEvent: console.log },
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The cap is counted per process. `onEvent` hears every top-up (`topped-up`),
|
|
74
|
+
every day the cap stops one (`capped`) and every failure (`failed`); a failed
|
|
75
|
+
top-up never fails the request on its own.
|
|
76
|
+
|
|
58
77
|
Any wallet works for sign-in and deposits: implement `Signer` (`address`,
|
|
59
78
|
`signMessage`, and `sendTransaction` for deposits) around viem, ethers or a
|
|
60
79
|
hardware wallet.
|
package/dist/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// envolvr's HTTP surface for an agent: sign in with a wallet, read the account
|
|
2
2
|
// and the pricing, and move USDG into the credit vault.
|
|
3
3
|
import { decodeWords, encodeCall, Rpc } from "./evm.js";
|
|
4
|
-
import { TESTNET } from "./network.js";
|
|
4
|
+
import { assertContracts, MAINNET, TESTNET } from "./network.js";
|
|
5
5
|
async function json(res) {
|
|
6
6
|
const body = (await res.json().catch(() => ({})));
|
|
7
7
|
if (!res.ok)
|
|
@@ -17,7 +17,7 @@ export function signInMessage(wallet, nonce, issuedAt) {
|
|
|
17
17
|
* Sign in with a wallet and get an API key. The key is shown once; keep it.
|
|
18
18
|
* Each sign-in issues a new key; earlier keys keep working.
|
|
19
19
|
*/
|
|
20
|
-
export async function signIn(signer, network =
|
|
20
|
+
export async function signIn(signer, network = MAINNET) {
|
|
21
21
|
const n = await json(await fetch(`${network.control}/auth/nonce?wallet=${signer.address}`));
|
|
22
22
|
const expected = signInMessage(signer.address, n.nonce, n.issuedAt);
|
|
23
23
|
if (n.message !== expected)
|
|
@@ -41,7 +41,7 @@ export function closeAccountMessage(wallet, refundTo, nonce, issuedAt) {
|
|
|
41
41
|
* credit vault shortly after.
|
|
42
42
|
*/
|
|
43
43
|
export async function closeAccount(signer, opts = {}) {
|
|
44
|
-
const network = opts.network ??
|
|
44
|
+
const network = opts.network ?? MAINNET;
|
|
45
45
|
const refundTo = opts.refundTo ?? signer.address;
|
|
46
46
|
const n = await json(await fetch(`${network.control}/account/close/nonce?wallet=${signer.address}&refundTo=${refundTo}`));
|
|
47
47
|
const expected = closeAccountMessage(signer.address, refundTo, n.nonce, n.issuedAt);
|
|
@@ -54,10 +54,10 @@ export async function closeAccount(signer, opts = {}) {
|
|
|
54
54
|
body: JSON.stringify({ wallet: signer.address, refundTo, nonce: n.nonce, issuedAt: n.issuedAt, signature }),
|
|
55
55
|
}));
|
|
56
56
|
}
|
|
57
|
-
export async function getAccount(apiKey, network =
|
|
57
|
+
export async function getAccount(apiKey, network = MAINNET) {
|
|
58
58
|
return json(await fetch(`${network.control}/account`, { headers: { authorization: `Bearer ${apiKey}` } }));
|
|
59
59
|
}
|
|
60
|
-
export async function getPricing(network =
|
|
60
|
+
export async function getPricing(network = MAINNET) {
|
|
61
61
|
return json(await fetch(`${network.control}/pricing`));
|
|
62
62
|
}
|
|
63
63
|
/** What a deposit of `amountMicros` credits at `feeBps` (the fee rounds down). */
|
|
@@ -72,12 +72,13 @@ export function netCredit(amountMicros, feeBps) {
|
|
|
72
72
|
* vault is not credited.
|
|
73
73
|
*/
|
|
74
74
|
export async function depositUsdg(opts) {
|
|
75
|
-
const network = opts.network ??
|
|
75
|
+
const network = opts.network ?? MAINNET;
|
|
76
76
|
const { signer, amountMicros } = opts;
|
|
77
77
|
if (!signer.sendTransaction)
|
|
78
78
|
throw new Error('this signer cannot send transactions');
|
|
79
79
|
if (amountMicros <= 0n)
|
|
80
80
|
throw new Error('the amount must be positive');
|
|
81
|
+
assertContracts(network);
|
|
81
82
|
const rpc = new Rpc(network.rpcUrl);
|
|
82
83
|
const view = async (to, sig, args) => BigInt(decodeWords(await rpc.call('eth_call', [{ to, data: encodeCall(sig, args) }, 'latest']))[0]);
|
|
83
84
|
const balance = await view(network.usdg, 'balanceOf(address)', [signer.address]);
|
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// envolvr: sign in, fund, run private inference, and verify its receipts.
|
|
3
3
|
//
|
|
4
|
-
// ENVOLVR_PRIVATE_KEY wallet key, for signin
|
|
4
|
+
// ENVOLVR_PRIVATE_KEY wallet key, for signin and deposit
|
|
5
5
|
// ENVOLVR_API_KEY API key, for everything else (or ~/.envolvr/credentials.json)
|
|
6
6
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
-
import { closeAccount, depositUsdg, getPricing,
|
|
9
|
+
import { closeAccount, depositUsdg, getPricing, signIn } from "./api.js";
|
|
10
10
|
import { AUDIT_CHECKS, verifyGateway } from "./attest.js";
|
|
11
11
|
import { Envolvr, savedReceipts, verifySaved } from "./client.js";
|
|
12
|
-
import { fromMicros,
|
|
12
|
+
import { fromMicros, MAINNET, toMicros } from "./network.js";
|
|
13
13
|
import { privateKeySigner } from "./wallet.js";
|
|
14
14
|
const HELP = `envolvr: private inference your agent can prove
|
|
15
15
|
|
|
16
16
|
envolvr signin [--save] sign in with ENVOLVR_PRIVATE_KEY, print a new API key
|
|
17
|
-
envolvr testnet-mint <usd> mint test USDG to the wallet (testnet)
|
|
18
17
|
envolvr deposit <usd> [--for 0x…] deposit USDG (net of the deposit fee) to your balance
|
|
19
|
-
envolvr account balance
|
|
18
|
+
envolvr account your balance
|
|
20
19
|
envolvr pricing the deposit fee and how token prices are set
|
|
21
20
|
envolvr models models and per-token prices
|
|
22
21
|
envolvr chat <prompt> [--model m] [--provider '{"only":["near-ai"]}']
|
|
@@ -36,7 +35,7 @@ const flag = (name) => {
|
|
|
36
35
|
const i = args.indexOf(`--${name}`);
|
|
37
36
|
return i > 0 ? args[i + 1] : undefined;
|
|
38
37
|
};
|
|
39
|
-
const network =
|
|
38
|
+
const network = MAINNET;
|
|
40
39
|
const receiptDir = process.env.ENVOLVR_RECEIPTS ?? 'envolvr-receipts';
|
|
41
40
|
const credentialsPath = join(homedir(), '.envolvr', 'credentials.json');
|
|
42
41
|
function wallet() {
|
|
@@ -69,11 +68,6 @@ async function main() {
|
|
|
69
68
|
}
|
|
70
69
|
return;
|
|
71
70
|
}
|
|
72
|
-
case 'testnet-mint': {
|
|
73
|
-
const tx = await mintTestUsdg({ signer: wallet(), amountMicros: toMicros(args[1]), network });
|
|
74
|
-
console.log(`minted ${args[1]} test USDG: ${network.explorer}/tx/${tx}`);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
71
|
case 'deposit': {
|
|
78
72
|
const r = await depositUsdg({ signer: wallet(), amountMicros: toMicros(args[1]), account: flag('for'), network });
|
|
79
73
|
console.log(`deposited ${usd(r.amountMicros)} for ${r.account}: ${network.explorer}/tx/${r.depositTx}`);
|
|
@@ -82,7 +76,8 @@ async function main() {
|
|
|
82
76
|
}
|
|
83
77
|
case 'account': {
|
|
84
78
|
const a = await new Envolvr({ apiKey: apiKey(), network }).account();
|
|
85
|
-
|
|
79
|
+
const allowance = BigInt(a.allowanceTodayMicros) > 0n ? ` | allowance today ${usd(a.allowanceTodayMicros)}, left ${usd(a.allowanceLeftMicros)}` : '';
|
|
80
|
+
console.log(`${a.wallet}\nbalance ${usd(a.balanceMicros)}${allowance}`);
|
|
86
81
|
return;
|
|
87
82
|
}
|
|
88
83
|
case 'pricing': {
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AuditSummary, type PapResult } from './attest.ts';
|
|
2
2
|
import { type Network } from './network.ts';
|
|
3
|
+
import { type AutoTopUpOptions } from './topup.ts';
|
|
3
4
|
import { type Anchor, type Billing, type Receipt } from './receipts.ts';
|
|
4
5
|
export interface SavedReceipt {
|
|
5
6
|
id: string;
|
|
@@ -25,10 +26,16 @@ export declare class Envolvr {
|
|
|
25
26
|
readonly network: Network;
|
|
26
27
|
private apiKey;
|
|
27
28
|
private receiptDir;
|
|
29
|
+
private topUp;
|
|
30
|
+
/**
|
|
31
|
+
* `autoTopUp` keeps the account funded from a wallet: see AutoTopUpOptions.
|
|
32
|
+
* It covers chat(); a client built from `openai` handles its own 402s.
|
|
33
|
+
*/
|
|
28
34
|
constructor(opts: {
|
|
29
35
|
apiKey: string;
|
|
30
36
|
network?: Network;
|
|
31
37
|
receiptDir?: string;
|
|
38
|
+
autoTopUp?: AutoTopUpOptions;
|
|
32
39
|
});
|
|
33
40
|
/** Settings for the OpenAI SDK: `new OpenAI(envolvr.openai)`. Receipts are then yours to fetch. */
|
|
34
41
|
get openai(): {
|
|
@@ -49,6 +56,7 @@ export declare class Envolvr {
|
|
|
49
56
|
receiptId: string | null;
|
|
50
57
|
saved?: SavedReceipt;
|
|
51
58
|
}>;
|
|
59
|
+
private chatOnce;
|
|
52
60
|
/**
|
|
53
61
|
* Fetch a receipt (within an hour of the response) and save it. With
|
|
54
62
|
* `receiptDir`, the request and response bytes and the signing keyset's
|
package/dist/client.js
CHANGED
|
@@ -7,16 +7,24 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { getAccount, getPricing } from "./api.js";
|
|
9
9
|
import { auditReceipt, fetchAttestationReport, summarizeAudit } from "./attest.js";
|
|
10
|
-
import {
|
|
10
|
+
import { MAINNET } from "./network.js";
|
|
11
|
+
import { AutoTopUp } from "./topup.js";
|
|
11
12
|
import { billingOf, chargedTo, verifyAnchor } from "./receipts.js";
|
|
12
13
|
export class Envolvr {
|
|
13
14
|
network;
|
|
14
15
|
apiKey;
|
|
15
16
|
receiptDir;
|
|
17
|
+
topUp;
|
|
18
|
+
/**
|
|
19
|
+
* `autoTopUp` keeps the account funded from a wallet: see AutoTopUpOptions.
|
|
20
|
+
* It covers chat(); a client built from `openai` handles its own 402s.
|
|
21
|
+
*/
|
|
16
22
|
constructor(opts) {
|
|
17
23
|
this.apiKey = opts.apiKey;
|
|
18
|
-
this.network = opts.network ??
|
|
24
|
+
this.network = opts.network ?? MAINNET;
|
|
19
25
|
this.receiptDir = opts.receiptDir;
|
|
26
|
+
if (opts.autoTopUp)
|
|
27
|
+
this.topUp = new AutoTopUp(opts.autoTopUp, this.apiKey, this.network);
|
|
20
28
|
}
|
|
21
29
|
/** Settings for the OpenAI SDK: `new OpenAI(envolvr.openai)`. Receipts are then yours to fetch. */
|
|
22
30
|
get openai() {
|
|
@@ -41,6 +49,19 @@ export class Envolvr {
|
|
|
41
49
|
* to pin providers.
|
|
42
50
|
*/
|
|
43
51
|
async chat(body) {
|
|
52
|
+
await this.topUp?.beforeRequest();
|
|
53
|
+
try {
|
|
54
|
+
return await this.chatOnce(body);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// Out of credit: top up once and retry, when auto top-up is on and could add funds.
|
|
58
|
+
if (err.status === 402 && this.topUp && (await this.topUp.afterInsufficientCredit())) {
|
|
59
|
+
return this.chatOnce(body);
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async chatOnce(body) {
|
|
44
65
|
const request = JSON.stringify({ ...body, stream: false });
|
|
45
66
|
const res = await fetch(`${this.network.gateway}/v1/chat/completions`, {
|
|
46
67
|
method: 'POST', headers: { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' }, body: request,
|
|
@@ -122,7 +143,7 @@ export class Envolvr {
|
|
|
122
143
|
}
|
|
123
144
|
/** Verify a receipt saved by Envolvr.receipt (a directory with receipt.json). */
|
|
124
145
|
export async function verifySaved(dir, opts) {
|
|
125
|
-
const network = opts.network ??
|
|
146
|
+
const network = opts.network ?? MAINNET;
|
|
126
147
|
const receiptPath = join(dir, 'receipt.json');
|
|
127
148
|
const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'));
|
|
128
149
|
const reports = join(opts.receiptDir ?? join(dir, '..'), 'attestation');
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export { auditReceipt, fetchAttestationReport, runPap, verifyGateway } from './a
|
|
|
4
4
|
export type { PapResult } from './attest.ts';
|
|
5
5
|
export { Envolvr, savedReceipts, verifySaved } from './client.ts';
|
|
6
6
|
export type { SavedReceipt, Verification } from './client.ts';
|
|
7
|
-
export { fromMicros, TESTNET, toMicros } from './network.ts';
|
|
7
|
+
export { fromMicros, MAINNET, TESTNET, toMicros } from './network.ts';
|
|
8
|
+
export type { AutoTopUpOptions, TopUpEvent } from './topup.ts';
|
|
8
9
|
export type { Network } from './network.ts';
|
|
9
10
|
export { billingOf, chargedTo, payerCommitment, proofLeadsToRoot, receiptDigest, verifyAnchor } from './receipts.ts';
|
|
10
11
|
export type { Anchor, Billing, Receipt } from './receipts.ts';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
export { closeAccount, closeAccountMessage, depositUsdg, getAccount, getPricing, mintTestUsdg, netCredit, signIn, signInMessage, } from "./api.js";
|
|
3
3
|
export { auditReceipt, fetchAttestationReport, runPap, verifyGateway } from "./attest.js";
|
|
4
4
|
export { Envolvr, savedReceipts, verifySaved } from "./client.js";
|
|
5
|
-
export { fromMicros, TESTNET, toMicros } from "./network.js";
|
|
5
|
+
export { fromMicros, MAINNET, TESTNET, toMicros } from "./network.js";
|
|
6
6
|
export { billingOf, chargedTo, payerCommitment, proofLeadsToRoot, receiptDigest, verifyAnchor } from "./receipts.js";
|
|
7
7
|
export { privateKeySigner } from "./wallet.js";
|
package/dist/network.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export interface Network {
|
|
|
14
14
|
providerId: string;
|
|
15
15
|
}
|
|
16
16
|
export declare const TESTNET: Network;
|
|
17
|
+
/** The mainnet contract addresses are published at launch; until then they are this placeholder. */
|
|
18
|
+
export declare const PENDING_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
19
|
+
export declare const MAINNET: Network;
|
|
20
|
+
/** Throws before any on-chain step on a network whose contracts are not published yet. */
|
|
21
|
+
export declare function assertContracts(network: Network): void;
|
|
17
22
|
/** USDG and envolvr balances have 6 decimals: "1.5" -> 1500000n. */
|
|
18
23
|
export declare function toMicros(amount: string | number): bigint;
|
|
19
24
|
export declare function fromMicros(micros: bigint | string): string;
|
package/dist/network.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Where envolvr runs
|
|
1
|
+
// Where envolvr runs: Robinhood Chain mainnet, and the testnet.
|
|
2
2
|
export const TESTNET = {
|
|
3
3
|
gateway: 'https://api.envolvr.xyz',
|
|
4
4
|
control: 'https://auth.envolvr.xyz',
|
|
@@ -10,6 +10,25 @@ export const TESTNET = {
|
|
|
10
10
|
receiptAnchor: '0xff179938C830134D8E2922dAe7e16Ee4F0E33853',
|
|
11
11
|
providerId: '0x86b99987cd8f8ebddc2dd0efa61ed52b5db594430f12446db424da0d9c4b831f',
|
|
12
12
|
};
|
|
13
|
+
/** The mainnet contract addresses are published at launch; until then they are this placeholder. */
|
|
14
|
+
export const PENDING_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
15
|
+
export const MAINNET = {
|
|
16
|
+
gateway: 'https://api.envolvr.xyz',
|
|
17
|
+
control: 'https://auth.envolvr.xyz',
|
|
18
|
+
chainId: 4663,
|
|
19
|
+
rpcUrl: 'https://rpc.mainnet.chain.robinhood.com',
|
|
20
|
+
explorer: 'https://robin.etherscan.io',
|
|
21
|
+
usdg: '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168',
|
|
22
|
+
creditVault: '0x24DbbBd7d5eE9674D8A8B19a492E87A4957B1261',
|
|
23
|
+
receiptAnchor: '0x591fadf9d2D9d0A0fedEEd95d74321694797AF60',
|
|
24
|
+
providerId: '0x86b99987cd8f8ebddc2dd0efa61ed52b5db594430f12446db424da0d9c4b831f',
|
|
25
|
+
};
|
|
26
|
+
/** Throws before any on-chain step on a network whose contracts are not published yet. */
|
|
27
|
+
export function assertContracts(network) {
|
|
28
|
+
if ([network.usdg, network.creditVault].includes(PENDING_ADDRESS)) {
|
|
29
|
+
throw new Error(`the contracts on chain ${network.chainId} are published at launch: see https://envolvr.xyz/token/#contracts`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
13
32
|
/** USDG and envolvr balances have 6 decimals: "1.5" -> 1500000n. */
|
|
14
33
|
export function toMicros(amount) {
|
|
15
34
|
const s = String(amount).trim();
|
package/dist/receipts.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Receipts: what each one says about billing, whether it was charged to you,
|
|
2
2
|
// and whether it is anchored on Robinhood Chain. Nothing here trusts envolvr:
|
|
3
3
|
// digests and commitments are computed locally, and the anchor is checked
|
|
4
|
-
// against the contract itself.
|
|
5
|
-
|
|
4
|
+
// against the contract itself. No Node built-ins, so the site's in-browser
|
|
5
|
+
// verifier bundles this same file.
|
|
6
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
6
7
|
import { keccak_256 } from '@noble/hashes/sha3';
|
|
7
8
|
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
|
|
8
9
|
import canonicalize from 'canonicalize';
|
|
@@ -11,7 +12,7 @@ export function receiptDigest(receipt) {
|
|
|
11
12
|
const jcs = canonicalize(receipt);
|
|
12
13
|
if (jcs === undefined)
|
|
13
14
|
throw new Error('receipt is not JSON');
|
|
14
|
-
return `0x${
|
|
15
|
+
return `0x${bytesToHex(sha256(utf8ToBytes(jcs)))}`;
|
|
15
16
|
}
|
|
16
17
|
/** The receipt's billing.charged event, or undefined for an unpriced request. */
|
|
17
18
|
export function billingOf(receipt) {
|
|
@@ -26,8 +27,8 @@ export function billingOf(receipt) {
|
|
|
26
27
|
}
|
|
27
28
|
/** The payer commitment a receipt charged to `apiKey` carries. */
|
|
28
29
|
export function payerCommitment(apiKey, receiptId) {
|
|
29
|
-
const keyHash =
|
|
30
|
-
return `sha256:${
|
|
30
|
+
const keyHash = bytesToHex(sha256(utf8ToBytes(apiKey)));
|
|
31
|
+
return `sha256:${bytesToHex(sha256(utf8ToBytes(`billing.payer.v1:${keyHash}:${receiptId}`)))}`;
|
|
31
32
|
}
|
|
32
33
|
export function chargedTo(receipt, apiKey) {
|
|
33
34
|
return billingOf(receipt)?.payer === payerCommitment(apiKey, receipt.receipt_id);
|
package/dist/topup.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Network } from './network.ts';
|
|
2
|
+
import type { Signer } from './wallet.ts';
|
|
3
|
+
export interface AutoTopUpOptions {
|
|
4
|
+
/** The wallet that pays: it needs USDG, and a little ETH for gas. */
|
|
5
|
+
signer: Signer;
|
|
6
|
+
/** Top up when allowance left plus balance falls below this, USD (for example '5'). */
|
|
7
|
+
below: string;
|
|
8
|
+
/** How much to deposit each time, USD. The account is credited net of the deposit fee. */
|
|
9
|
+
amount: string;
|
|
10
|
+
/** Most to deposit per UTC day, USD. Defaults to `amount`: one top-up a day. */
|
|
11
|
+
maxPerDay?: string;
|
|
12
|
+
/** How often to read the account before a request, ms. Default 30 000. */
|
|
13
|
+
checkEveryMs?: number;
|
|
14
|
+
/** How long to wait for a mined deposit to be credited, ms. Default 120 000. */
|
|
15
|
+
creditTimeoutMs?: number;
|
|
16
|
+
/** Told about every top-up, skip and failure. */
|
|
17
|
+
onEvent?: (event: TopUpEvent) => void;
|
|
18
|
+
}
|
|
19
|
+
export type TopUpEvent = {
|
|
20
|
+
type: 'topped-up';
|
|
21
|
+
amountMicros: bigint;
|
|
22
|
+
depositTx: string;
|
|
23
|
+
creditedMicros: bigint;
|
|
24
|
+
} | {
|
|
25
|
+
type: 'capped';
|
|
26
|
+
availableMicros: bigint;
|
|
27
|
+
spentTodayMicros: bigint;
|
|
28
|
+
maxPerDayMicros: bigint;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'failed';
|
|
31
|
+
error: Error;
|
|
32
|
+
};
|
|
33
|
+
export declare class AutoTopUp {
|
|
34
|
+
private readonly opts;
|
|
35
|
+
private readonly below;
|
|
36
|
+
private readonly amount;
|
|
37
|
+
private readonly maxPerDay;
|
|
38
|
+
private lastCheck;
|
|
39
|
+
private running;
|
|
40
|
+
private day;
|
|
41
|
+
private spentToday;
|
|
42
|
+
private cappedNoticeDay;
|
|
43
|
+
private toppedUp;
|
|
44
|
+
private readonly apiKey;
|
|
45
|
+
private readonly network;
|
|
46
|
+
private readonly now;
|
|
47
|
+
private readonly sleep;
|
|
48
|
+
constructor(opts: AutoTopUpOptions, apiKey: string, network: Network, clock?: {
|
|
49
|
+
now?: () => number;
|
|
50
|
+
sleep?: (ms: number) => Promise<unknown>;
|
|
51
|
+
});
|
|
52
|
+
/** Before a request: read the account at most every `checkEveryMs`, top up if it is low. */
|
|
53
|
+
beforeRequest(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* After a 402: read the account now, whatever the interval, and top up if it is
|
|
56
|
+
* low. True when a top-up was credited, so the request can be retried.
|
|
57
|
+
*/
|
|
58
|
+
afterInsufficientCredit(): Promise<boolean>;
|
|
59
|
+
private ensure;
|
|
60
|
+
private run;
|
|
61
|
+
}
|
package/dist/topup.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Automatic top-up: keep an account funded from a wallet, within a daily cap.
|
|
2
|
+
//
|
|
3
|
+
// Before a request (at most every `checkEveryMs`), and whenever the gateway
|
|
4
|
+
// answers 402 insufficient credit, the client reads the account. When the
|
|
5
|
+
// staking allowance left plus the USDG balance is below `below`, it deposits
|
|
6
|
+
// `amount` from `signer` (for the account's own wallet, or with depositFor when
|
|
7
|
+
// the signer is another wallet, such as the person funding an agent) and waits
|
|
8
|
+
// until the control plane has credited it. Concurrent requests share one
|
|
9
|
+
// top-up. Deposits per UTC day stop at `maxPerDay` (default: one top-up), as
|
|
10
|
+
// counted by this process.
|
|
11
|
+
import { depositUsdg, getAccount } from "./api.js";
|
|
12
|
+
import { fromMicros, toMicros } from "./network.js";
|
|
13
|
+
const DAY_MS = 86_400_000;
|
|
14
|
+
const available = (a) => BigInt(a.balanceMicros) + BigInt(a.allowanceLeftMicros);
|
|
15
|
+
export class AutoTopUp {
|
|
16
|
+
opts;
|
|
17
|
+
below;
|
|
18
|
+
amount;
|
|
19
|
+
maxPerDay;
|
|
20
|
+
lastCheck = 0;
|
|
21
|
+
running;
|
|
22
|
+
day = -1;
|
|
23
|
+
spentToday = 0n;
|
|
24
|
+
cappedNoticeDay = -1;
|
|
25
|
+
toppedUp = 0;
|
|
26
|
+
apiKey;
|
|
27
|
+
network;
|
|
28
|
+
now;
|
|
29
|
+
sleep;
|
|
30
|
+
constructor(opts, apiKey, network, clock = {}) {
|
|
31
|
+
this.opts = opts;
|
|
32
|
+
this.apiKey = apiKey;
|
|
33
|
+
this.network = network;
|
|
34
|
+
this.now = clock.now ?? Date.now;
|
|
35
|
+
this.sleep = clock.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
36
|
+
this.below = toMicros(opts.below);
|
|
37
|
+
this.amount = toMicros(opts.amount);
|
|
38
|
+
this.maxPerDay = toMicros(opts.maxPerDay ?? opts.amount);
|
|
39
|
+
if (this.amount <= 0n)
|
|
40
|
+
throw new Error('autoTopUp.amount must be positive');
|
|
41
|
+
if (this.maxPerDay < this.amount)
|
|
42
|
+
throw new Error('autoTopUp.maxPerDay must be at least autoTopUp.amount');
|
|
43
|
+
}
|
|
44
|
+
/** Before a request: read the account at most every `checkEveryMs`, top up if it is low. */
|
|
45
|
+
async beforeRequest() {
|
|
46
|
+
if (this.running)
|
|
47
|
+
return this.running;
|
|
48
|
+
if (this.now() - this.lastCheck < (this.opts.checkEveryMs ?? 30_000))
|
|
49
|
+
return;
|
|
50
|
+
return this.ensure();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* After a 402: read the account now, whatever the interval, and top up if it is
|
|
54
|
+
* low. True when a top-up was credited, so the request can be retried.
|
|
55
|
+
*/
|
|
56
|
+
async afterInsufficientCredit() {
|
|
57
|
+
const toppedUp = this.toppedUp;
|
|
58
|
+
await this.ensure();
|
|
59
|
+
return this.toppedUp > toppedUp;
|
|
60
|
+
}
|
|
61
|
+
ensure() {
|
|
62
|
+
this.running ??= this.run().finally(() => { this.running = undefined; });
|
|
63
|
+
return this.running;
|
|
64
|
+
}
|
|
65
|
+
async run() {
|
|
66
|
+
try {
|
|
67
|
+
this.lastCheck = this.now();
|
|
68
|
+
const account = await getAccount(this.apiKey, this.network);
|
|
69
|
+
const have = available(account);
|
|
70
|
+
if (have >= this.below)
|
|
71
|
+
return;
|
|
72
|
+
const day = Math.floor(this.now() / DAY_MS);
|
|
73
|
+
if (day !== this.day) {
|
|
74
|
+
this.day = day;
|
|
75
|
+
this.spentToday = 0n;
|
|
76
|
+
}
|
|
77
|
+
if (this.spentToday + this.amount > this.maxPerDay) {
|
|
78
|
+
if (this.cappedNoticeDay !== day) {
|
|
79
|
+
this.cappedNoticeDay = day;
|
|
80
|
+
this.opts.onEvent?.({ type: 'capped', availableMicros: have, spentTodayMicros: this.spentToday, maxPerDayMicros: this.maxPerDay });
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const { depositTx, expectedCreditMicros } = await depositUsdg({
|
|
85
|
+
signer: this.opts.signer, amountMicros: this.amount, account: account.wallet, network: this.network,
|
|
86
|
+
});
|
|
87
|
+
this.spentToday += this.amount;
|
|
88
|
+
// The deposit is mined; wait until the control plane has credited it.
|
|
89
|
+
const target = BigInt(account.balanceMicros) + expectedCreditMicros;
|
|
90
|
+
const deadline = this.now() + (this.opts.creditTimeoutMs ?? 120_000);
|
|
91
|
+
let credited = 0n;
|
|
92
|
+
while (this.now() < deadline) {
|
|
93
|
+
const a = await getAccount(this.apiKey, this.network);
|
|
94
|
+
credited = BigInt(a.balanceMicros) - BigInt(account.balanceMicros);
|
|
95
|
+
if (BigInt(a.balanceMicros) >= target)
|
|
96
|
+
break;
|
|
97
|
+
await this.sleep(3_000);
|
|
98
|
+
}
|
|
99
|
+
if (credited < expectedCreditMicros) {
|
|
100
|
+
throw new Error(`deposit ${depositTx} was mined but ${fromMicros(expectedCreditMicros)} USD was not credited within the timeout`);
|
|
101
|
+
}
|
|
102
|
+
this.toppedUp++;
|
|
103
|
+
this.opts.onEvent?.({ type: 'topped-up', amountMicros: this.amount, depositTx, creditedMicros: credited });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
this.opts.onEvent?.({ type: 'failed', error: err instanceof Error ? err : new Error(String(err)) });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envolvr/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "envolvr client: wallet sign-in, USDG deposits, private inference with signed receipts, and receipt verification (signature, billing, on-chain anchor).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "envolvr",
|