@prismnetwork/agent-sdk 0.7.11 → 0.8.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
@@ -191,10 +191,27 @@ with `host_key_unpublished`.
191
191
 
192
192
  `lease()` (and the lower-level `fund()`) reproduce the escrow's quote binding: `clientReference = keccak256(quote_id)`, `approve(escrow, maximum_escrow)`, then `createLease(...)`, waiting 12 confirmations.
193
193
 
194
+ Pass `decision` and `policy` to `lease()` to authorise the spend first. The decision is checked against the policy before a quote is taken, and its hash is bound into the deposit: `clientReference = keccak256(keccak256(quote_id) ‖ decision_hash)`. Anyone holding the decision can recompute the reference and see it existed before the money moved. It does not show the decision was right. The helpers live in `@prismnetwork/agent-sdk/decision`.
195
+
194
196
  ## Funding
195
197
 
196
198
  The wallet needs two balances on Robinhood Chain (id 4663): USDG (`0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`, 6 decimals) for the lease deposit, and native ETH for gas. Bridge from L1 to fund a fresh wallet. `authenticate()`, `offers()`, and `quote()` need neither, so the read paths work before you fund anything.
197
199
 
200
+ ## Prism Chain
201
+
202
+ Prism Chain (id 77476) is the Layer 3 for GPU compute, settled on Robinhood Chain. `@prismnetwork/agent-sdk/chain` exports the chain for viem and the bridge helpers:
203
+
204
+ ```js
205
+ import { depositToken, prismChain, withdraw } from "@prismnetwork/agent-sdk/chain";
206
+
207
+ // Robinhood Chain → Prism Chain, arrives in about a minute.
208
+ await depositToken(robinhoodWallet, USDG, 5_000_000n);
209
+ // Prism Chain → Robinhood Chain, claimable there after about a day.
210
+ await withdraw(prismWallet, 1_000_000n, { token: USDG });
211
+ ```
212
+
213
+ `depositEth` moves ETH the same way. Claim a finished withdrawal at [bridge.prismnetwork.tech](https://bridge.prismnetwork.tech). Leases still fund and settle on Robinhood Chain.
214
+
198
215
  ## Requirements
199
216
 
200
217
  Node >= 20, `viem` ^2 (peer), and `ssh`, `ssh-keygen` and `ssh-keyscan` on PATH
package/chain.d.mts ADDED
@@ -0,0 +1,32 @@
1
+ import type { Address, Chain, Hash, WalletClient } from "viem";
2
+
3
+ export declare const prismChain: Chain;
4
+ export declare const PRISM_CHAIN_BRIDGE_URL: string;
5
+ export declare const prismChainContracts: {
6
+ rollup: Address;
7
+ inbox: Address;
8
+ outbox: Address;
9
+ parentRouter: Address;
10
+ parentStandardGateway: Address;
11
+ childRouter: Address;
12
+ arbSys: Address;
13
+ };
14
+
15
+ export interface TokenDepositFees {
16
+ maxGas: bigint;
17
+ gasPriceBid: bigint;
18
+ maxSubmissionCost: bigint;
19
+ /** Wei of ETH sent with the deposit; the unused part is refunded on Prism Chain. */
20
+ value: bigint;
21
+ }
22
+
23
+ interface RpcOptions {
24
+ parentRpcUrl?: string;
25
+ childRpcUrl?: string;
26
+ }
27
+
28
+ export declare function prismChainToken(token: Address, options?: { rpcUrl?: string }): Promise<Address>;
29
+ export declare function tokenDepositFees(token: Address, options?: RpcOptions): Promise<TokenDepositFees>;
30
+ export declare function depositEth(wallet: WalletClient, amount: bigint): Promise<Hash>;
31
+ export declare function depositToken(wallet: WalletClient, token: Address, amount: bigint, options?: RpcOptions): Promise<Hash>;
32
+ export declare function withdraw(wallet: WalletClient, amount: bigint, options?: { token?: Address | null }): Promise<Hash>;
package/chain.mjs ADDED
@@ -0,0 +1,163 @@
1
+ /// Prism Chain: the Layer 3 for GPU compute, settled on Robinhood Chain.
2
+ ///
3
+ /// Assets reach it through the canonical Arbitrum token bridge. A deposit lands
4
+ /// on Prism Chain in about a minute. A withdrawal starts on Prism Chain,
5
+ /// finalizes on Robinhood Chain after the rollup's confirmation window (about a
6
+ /// day), and is then claimed there; https://bridge.prismnetwork.tech does the
7
+ /// claim. Leases still settle on Robinhood Chain today.
8
+ import { createPublicClient, defineChain, encodeAbiParameters, erc20Abi, http, parseAbi } from "viem";
9
+
10
+ // Kept here rather than imported from prism.mjs, which pulls in Node modules;
11
+ // this file stays usable in a browser.
12
+ const robinhoodChain = defineChain({
13
+ id: 4663,
14
+ name: "Robinhood Chain",
15
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
16
+ rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
17
+ });
18
+
19
+ export const prismChain = defineChain({
20
+ id: 77476,
21
+ name: "Prism Chain",
22
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
23
+ rpcUrls: {
24
+ default: { http: ["https://rpc.prismnetwork.tech"], webSocket: ["wss://ws.prismnetwork.tech"] },
25
+ },
26
+ blockExplorers: { default: { name: "Prism Chain explorer", url: "https://explorer.prismnetwork.tech" } },
27
+ sourceId: robinhoodChain.id,
28
+ });
29
+
30
+ export const PRISM_CHAIN_BRIDGE_URL = "https://bridge.prismnetwork.tech";
31
+
32
+ /// The rollup on Robinhood Chain and both halves of the token bridge.
33
+ export const prismChainContracts = {
34
+ rollup: "0x8232Ce617E7F1031F5B7bdcD72678dbdF2fDafeB",
35
+ inbox: "0x2FbD2fF90f56C58A8789Fc56e8b31F26e660438f",
36
+ outbox: "0x8cC14bF09cE7a1bf2415C70E5E4B9f50cD4a889d",
37
+ parentRouter: "0x52f5aF48C1DF13E0e1ABbe04F99622aabF6B2CD2",
38
+ parentStandardGateway: "0x1274b56aE5566014bB73dfE0A2247ab5BbC96470",
39
+ childRouter: "0x12d1c6c0b4b97278A7E2961eF16105Ba2dd2e859",
40
+ arbSys: "0x0000000000000000000000000000000000000064",
41
+ };
42
+
43
+ const inboxAbi = parseAbi([
44
+ "function depositEth() payable returns (uint256)",
45
+ "function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) view returns (uint256)",
46
+ ]);
47
+ const parentRouterAbi = parseAbi([
48
+ "function getGateway(address token) view returns (address)",
49
+ "function outboundTransferCustomRefund(address token, address refundTo, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes data) payable returns (bytes)",
50
+ ]);
51
+ const childRouterAbi = parseAbi([
52
+ "function calculateL2TokenAddress(address l1Token) view returns (address)",
53
+ "function outboundTransfer(address l1Token, address to, uint256 amount, bytes data) payable returns (bytes)",
54
+ ]);
55
+ const arbSysAbi = parseAbi(["function withdrawEth(address destination) payable returns (uint256)"]);
56
+
57
+ const parent = (rpcUrl) => createPublicClient({ chain: robinhoodChain, transport: http(rpcUrl) });
58
+ const child = (rpcUrl) => createPublicClient({ chain: prismChain, transport: http(rpcUrl) });
59
+
60
+ /// Where a Robinhood Chain token lives on Prism Chain. Its contract exists once
61
+ /// the first deposit of that token has landed.
62
+ export function prismChainToken(token, { rpcUrl } = {}) {
63
+ return child(rpcUrl).readContract({
64
+ address: prismChainContracts.childRouter,
65
+ abi: childRouterAbi,
66
+ functionName: "calculateL2TokenAddress",
67
+ args: [token],
68
+ });
69
+ }
70
+
71
+ /// What a token deposit prepays for its call on Prism Chain, in wei of ETH on
72
+ /// Robinhood Chain. The first deposit of a token also deploys its contract
73
+ /// there, which needs more gas. Whatever goes unused is refunded to the sender
74
+ /// on Prism Chain, so this is a ceiling, not a price.
75
+ export async function tokenDepositFees(token, { parentRpcUrl, childRpcUrl } = {}) {
76
+ const [block, childGasPrice, deployed] = await Promise.all([
77
+ parent(parentRpcUrl).getBlock(),
78
+ child(childRpcUrl).getGasPrice(),
79
+ prismChainToken(token, { rpcUrl: childRpcUrl }).then((address) => child(childRpcUrl).getCode({ address })),
80
+ ]);
81
+ const maxGas = deployed && deployed !== "0x" ? 300_000n : 1_200_000n;
82
+ const gasPriceBid = childGasPrice * 2n;
83
+ const submission = await parent(parentRpcUrl).readContract({
84
+ address: prismChainContracts.inbox,
85
+ abi: inboxAbi,
86
+ functionName: "calculateRetryableSubmissionFee",
87
+ args: [2_000n, block.baseFeePerGas ?? 0n],
88
+ });
89
+ const maxSubmissionCost = submission * 4n;
90
+ return { maxGas, gasPriceBid, maxSubmissionCost, value: maxSubmissionCost + maxGas * gasPriceBid };
91
+ }
92
+
93
+ /// Move ETH from Robinhood Chain to the same address on Prism Chain. `wallet`
94
+ /// is a viem wallet client on Robinhood Chain. Returns the Robinhood Chain
95
+ /// transaction hash.
96
+ export function depositEth(wallet, amount) {
97
+ return wallet.writeContract({
98
+ chain: robinhoodChain,
99
+ address: prismChainContracts.inbox,
100
+ abi: inboxAbi,
101
+ functionName: "depositEth",
102
+ value: amount,
103
+ });
104
+ }
105
+
106
+ /// Move an ERC-20 such as USDG from Robinhood Chain to the same address on
107
+ /// Prism Chain. Approves the token's gateway first when the allowance is short.
108
+ export async function depositToken(wallet, token, amount, { parentRpcUrl, childRpcUrl } = {}) {
109
+ const account = wallet.account.address;
110
+ const reader = parent(parentRpcUrl);
111
+ const gateway = await reader.readContract({
112
+ address: prismChainContracts.parentRouter,
113
+ abi: parentRouterAbi,
114
+ functionName: "getGateway",
115
+ args: [token],
116
+ });
117
+ const allowance = await reader.readContract({ address: token, abi: erc20Abi, functionName: "allowance", args: [account, gateway] });
118
+ if (allowance < amount) {
119
+ const approval = await wallet.writeContract({
120
+ chain: robinhoodChain,
121
+ address: token,
122
+ abi: erc20Abi,
123
+ functionName: "approve",
124
+ args: [gateway, amount],
125
+ });
126
+ await reader.waitForTransactionReceipt({ hash: approval });
127
+ }
128
+ const fees = await tokenDepositFees(token, { parentRpcUrl, childRpcUrl });
129
+ const data = encodeAbiParameters([{ type: "uint256" }, { type: "bytes" }], [fees.maxSubmissionCost, "0x"]);
130
+ return wallet.writeContract({
131
+ chain: robinhoodChain,
132
+ address: prismChainContracts.parentRouter,
133
+ abi: parentRouterAbi,
134
+ functionName: "outboundTransferCustomRefund",
135
+ args: [token, account, account, amount, fees.maxGas, fees.gasPriceBid, data],
136
+ value: fees.value,
137
+ });
138
+ }
139
+
140
+ /// Start moving ETH, or a bridged token given by its Robinhood Chain address,
141
+ /// back to Robinhood Chain. `wallet` is a viem wallet client on Prism Chain.
142
+ /// The withdrawal is claimable on Robinhood Chain after about a day, at
143
+ /// PRISM_CHAIN_BRIDGE_URL. Returns the Prism Chain transaction hash.
144
+ export function withdraw(wallet, amount, { token = null } = {}) {
145
+ const account = wallet.account.address;
146
+ if (!token) {
147
+ return wallet.writeContract({
148
+ chain: prismChain,
149
+ address: prismChainContracts.arbSys,
150
+ abi: arbSysAbi,
151
+ functionName: "withdrawEth",
152
+ args: [account],
153
+ value: amount,
154
+ });
155
+ }
156
+ return wallet.writeContract({
157
+ chain: prismChain,
158
+ address: prismChainContracts.childRouter,
159
+ abi: childRouterAbi,
160
+ functionName: "outboundTransfer",
161
+ args: [token, account, amount, "0x"],
162
+ });
163
+ }
package/decision.mjs CHANGED
@@ -124,6 +124,13 @@ export function leaseReference(quoteId, d) {
124
124
  return keccak256(concatBytes([hexToBytes(quoteDigest), hexToBytes(keccak256(canonical(d)))]));
125
125
  }
126
126
 
127
+ /// The hex digest the control plane needs to know which derivation to expect.
128
+ /// It is the decision's hash, not the decision: the service can check that the
129
+ /// funding log commits to something, and cannot read what that something says.
130
+ export function decisionDigest(d) {
131
+ return d ? keccak256(canonical(d)) : null;
132
+ }
133
+
127
134
  /// Whether this decision is the one that funded that lease.
128
135
  export function referenceMatches(quoteId, d, reference) {
129
136
  return leaseReference(quoteId, d).toLowerCase() === String(reference).toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prismnetwork/agent-sdk",
3
- "version": "0.7.11",
3
+ "version": "0.8.0",
4
4
  "description": "Headless GPU leasing and renter-encrypted storage on Prism Network for wallet-holding agents.",
5
5
  "type": "module",
6
6
  "main": "prism.mjs",
@@ -9,6 +9,10 @@
9
9
  "types": "./prism.d.mts",
10
10
  "default": "./prism.mjs"
11
11
  },
12
+ "./chain": {
13
+ "types": "./chain.d.mts",
14
+ "default": "./chain.mjs"
15
+ },
12
16
  "./decision": {
13
17
  "default": "./decision.mjs"
14
18
  },
@@ -40,6 +44,8 @@
40
44
  "files": [
41
45
  "prism.mjs",
42
46
  "prism.d.mts",
47
+ "chain.mjs",
48
+ "chain.d.mts",
43
49
  "attest.mjs",
44
50
  "attest.d.mts",
45
51
  "decision.mjs",
package/prism.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { AttestationResult, VerifyConfidentialOptions, WorkloadPin } from "./attest.d.mts";
2
2
 
3
3
  export declare const robinhoodChain: unknown;
4
+ export { PRISM_CHAIN_BRIDGE_URL, prismChain, prismChainContracts } from "./chain.d.mts";
4
5
  export declare const USDG: string;
5
6
  export declare const DEFAULT_IMAGE: string;
6
7
  export declare const TRUST_CLASSES: readonly ["open", "isolated", "attested", "confidential"];
package/prism.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // Prism Network agent SDK: headless GPU leasing for wallet-holding agents.
2
2
  // No browser, no Privy. Authenticate with a wallet signature, pay on-chain, run.
3
3
  import { execFileSync, spawn } from "node:child_process";
4
- import { authorise, leaseReference } from "./decision.mjs";
4
+ import { authorise, decisionDigest, leaseReference } from "./decision.mjs";
5
5
  import { mkdtempSync, readFileSync, rmSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
@@ -15,6 +15,7 @@ import {
15
15
  stringToBytes,
16
16
  } from "viem";
17
17
  import { privateKeyToAccount } from "viem/accounts";
18
+ export { PRISM_CHAIN_BRIDGE_URL, prismChain, prismChainContracts } from "./chain.mjs";
18
19
  import { appraiseWorkload, DEFAULT_CONFIDENTIAL_BASE, EXPECTED_WORKLOAD, verifyConfidential } from "./attest.mjs";
19
20
  import { decryptResponse, encryptChatRequest } from "./e2ee.mjs";
20
21
  import { hostKeyArgs, HostKeyError } from "./hostkey.mjs";
@@ -334,12 +335,17 @@ export class PrismAgent {
334
335
  }
335
336
  }
336
337
 
337
- async confirm({ quoteId, transactionHash, sshAuthorizedKey }) {
338
+ /// `decisionHash` travels when the lease was authorised: the client
339
+ /// reference on chain is derived from it, and the control plane cannot check
340
+ /// the funding log without knowing which derivation to expect. The digest is
341
+ /// all it gets; the decision stays here.
342
+ async confirm({ quoteId, transactionHash, sshAuthorizedKey, decisionHash = null }) {
338
343
  return this.#proxy("POST", ["leases", "confirm"], {
339
344
  body: {
340
345
  quote_id: quoteId,
341
346
  transaction_hash: transactionHash,
342
347
  ssh_authorized_key: sshAuthorizedKey,
348
+ ...(decisionHash ? { decision_hash: decisionHash } : {}),
343
349
  },
344
350
  });
345
351
  }
@@ -426,7 +432,12 @@ export class PrismAgent {
426
432
  maxDeposit = null,
427
433
  minTrustClass = "open",
428
434
  command = null,
435
+ decision = null,
436
+ policy = null,
429
437
  } = {}) {
438
+ // Checked before a quote is taken, so a refusal costs nothing and does not
439
+ // hold capacity against other renters while it expires.
440
+ if (policy) authorise(policy, decision ?? { action: "unstated", source: "none", answers: [] });
430
441
  if (!this.session) await this.authenticate();
431
442
  // A wallet with no balance at all cannot fund anything, and a doomed quote
432
443
  // still holds capacity against other renters until it expires. Refuse
@@ -463,11 +474,12 @@ export class PrismAgent {
463
474
  if (maxDeposit != null && parseBaseUnits(quote.maximum_escrow, "maximum_escrow") > BigInt(maxDeposit)) {
464
475
  throw new PrismError(402, "cost_exceeds_max", { required: quote.maximum_escrow, max: String(maxDeposit) });
465
476
  }
466
- funded = await this.#fundNow(quote);
477
+ funded = await this.#fundNow(quote, decision);
467
478
  return this.confirm({
468
479
  quoteId: quote.quote_id,
469
480
  transactionHash: funded.hash,
470
481
  sshAuthorizedKey: key.publicKey,
482
+ decisionHash: decisionDigest(decision),
471
483
  });
472
484
  });
473
485
  if (!Number.isInteger(record?.lease_id)) {