@envolvr/sdk 0.1.0 → 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 CHANGED
@@ -11,17 +11,19 @@ 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 (testnet)
14
+ ## From zero to a verified receipt
15
15
 
16
16
  ```bash
17
- export ENVOLVR_PRIVATE_KEY=0x… # a wallet with a little Robinhood testnet ETH for gas
18
- npx envolvr signin --save # sign a message, get an API key
19
- npx envolvr testnet-mint 20 # test USDG (testnet only)
20
- npx envolvr deposit 10 # credited net of the deposit fee, within seconds
21
- npx envolvr chat "Reply with the single word: sealed"
22
- npx envolvr verify --last # signature, billing, payer, on-chain anchor
17
+ export ENVOLVR_PRIVATE_KEY=0x… # a wallet with USDG and a little ETH on Robinhood Chain
18
+ npx @envolvr/sdk signin --save # sign a message, get an API key
19
+ npx @envolvr/sdk deposit 10 # credited net of the deposit fee, within seconds
20
+ npx @envolvr/sdk chat "Reply with the single word: sealed"
21
+ npx @envolvr/sdk verify --last # signature, billing, payer, on-chain anchor
23
22
  ```
24
23
 
24
+ Or `npm install -g @envolvr/sdk` and run `envolvr …`. Always name the scoped
25
+ package with npx: `npx envolvr` alone would fetch an unrelated package.
26
+
25
27
  `verify` prints what the receipt proves: the gateway's TDX attestation and the
26
28
  receipt signature (checked by `private-ai-proxy`), what was billed and that it
27
29
  was billed to your key, and the batch and transaction that anchor it. A receipt
@@ -30,9 +32,9 @@ is anchored at the end of its 10-minute slot (UTC).
30
32
  ## In code
31
33
 
32
34
  ```ts
33
- import { Envolvr, privateKeySigner, signIn, depositUsdg, TESTNET, toMicros } from '@envolvr/sdk';
35
+ import { Envolvr, privateKeySigner, signIn, depositUsdg, MAINNET, toMicros } from '@envolvr/sdk';
34
36
 
35
- const wallet = privateKeySigner(process.env.KEY!, { rpcUrl: TESTNET.rpcUrl, chainId: TESTNET.chainId });
37
+ const wallet = privateKeySigner(process.env.KEY!, { rpcUrl: MAINNET.rpcUrl, chainId: MAINNET.chainId });
36
38
  const { apiKey } = await signIn(wallet); // keep it: shown once
37
39
  await depositUsdg({ signer: wallet, amountMicros: toMicros('10') });
38
40
 
@@ -52,6 +54,26 @@ sending and checks every receipt, run the local verifying proxy
52
54
  `npx private-ai-proxy serve https://api.envolvr.xyz` and point the client at
53
55
  `http://127.0.0.1:4180/v1`.
54
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
+
55
77
  Any wallet works for sign-in and deposits: implement `Signer` (`address`,
56
78
  `signMessage`, and `sendTransaction` for deposits) around viem, ethers or a
57
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 = TESTNET) {
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 ?? TESTNET;
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 = TESTNET) {
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 = TESTNET) {
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 ?? TESTNET;
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, deposit and testnet-mint
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, mintTestUsdg, signIn } from "./api.js";
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, TESTNET, toMicros } from "./network.js";
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 and today's staking allowance
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 = TESTNET;
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
- console.log(`${a.wallet}\nbalance ${usd(a.balanceMicros)} | allowance today ${usd(a.allowanceTodayMicros)}, left ${usd(a.allowanceLeftMicros)}`);
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 { TESTNET } from "./network.js";
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 ?? TESTNET;
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 ?? TESTNET;
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. Robinhood Chain testnet until mainnet.
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
- import { createHash } from 'node:crypto';
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${createHash('sha256').update(jcs).digest('hex')}`;
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 = createHash('sha256').update(apiKey, 'utf8').digest('hex');
30
- return `sha256:${createHash('sha256').update(`billing.payer.v1:${keyHash}:${receiptId}`).digest('hex')}`;
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);
@@ -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.1.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",