@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 +17 -0
- package/chain.d.mts +32 -0
- package/chain.mjs +163 -0
- package/decision.mjs +7 -0
- package/package.json +7 -1
- package/prism.d.mts +1 -0
- package/prism.mjs +15 -3
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.
|
|
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
|
-
|
|
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)) {
|