@taskforest/dark-forest 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/README.md +19 -0
- package/dist/__tests__/dark-forest.test.d.ts +1 -0
- package/dist/__tests__/dark-forest.test.js +64 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +164 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @taskforest/dark-forest
|
|
2
|
+
|
|
3
|
+
Dark Forest payment helpers for TaskForest.
|
|
4
|
+
|
|
5
|
+
This package is intentionally separate from `@taskforest/sdk`.
|
|
6
|
+
Use it when you need MagicBlock PER delegation, payment-channel orchestration, or Dark Forest demo flows.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @taskforest/dark-forest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Scope
|
|
15
|
+
|
|
16
|
+
- Escrow wrapper helpers
|
|
17
|
+
- PER endpoints and validator constants
|
|
18
|
+
- MPP session orchestration helpers
|
|
19
|
+
- Settlement state reads
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
8
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
9
|
+
const index_1 = require("../index");
|
|
10
|
+
(0, node_test_1.default)('exports stable public constants', () => {
|
|
11
|
+
strict_1.default.equal(index_1.DARK_FOREST_PROGRAM_ID.toBase58(), '4hNP2tU5r5GgyASTrou84kWHbCwdyXVJJN4mve99rjgs');
|
|
12
|
+
strict_1.default.equal(index_1.ESCROW_SEED.toString(), 'escrow');
|
|
13
|
+
strict_1.default.equal(index_1.SETTLEMENT_SEED.toString(), 'settlement');
|
|
14
|
+
strict_1.default.equal(index_1.PER_ENDPOINTS.devnet, 'https://tee.magicblock.app');
|
|
15
|
+
strict_1.default.ok(index_1.TEE_VALIDATORS.devnet);
|
|
16
|
+
});
|
|
17
|
+
(0, node_test_1.default)('derives deterministic escrow and settlement PDAs', () => {
|
|
18
|
+
const escrowOne = index_1.DarkForestPayments.escrowPda(42);
|
|
19
|
+
const escrowTwo = index_1.DarkForestPayments.escrowPda(42);
|
|
20
|
+
const settlementOne = index_1.DarkForestPayments.settlementPda(42);
|
|
21
|
+
const settlementTwo = index_1.DarkForestPayments.settlementPda(42);
|
|
22
|
+
strict_1.default.equal(escrowOne.toBase58(), escrowTwo.toBase58());
|
|
23
|
+
strict_1.default.equal(settlementOne.toBase58(), settlementTwo.toBase58());
|
|
24
|
+
strict_1.default.notEqual(escrowOne.toBase58(), settlementOne.toBase58());
|
|
25
|
+
});
|
|
26
|
+
(0, node_test_1.default)('connectToPer returns a Solana connection', () => {
|
|
27
|
+
const payments = Object.create(index_1.DarkForestPayments.prototype);
|
|
28
|
+
const connection = payments.connectToPer(index_1.PER_ENDPOINTS.devnet);
|
|
29
|
+
strict_1.default.ok(connection instanceof web3_js_1.Connection);
|
|
30
|
+
strict_1.default.equal(connection.rpcEndpoint, index_1.PER_ENDPOINTS.devnet);
|
|
31
|
+
});
|
|
32
|
+
(0, node_test_1.default)('tracks an in-memory private session lifecycle', async () => {
|
|
33
|
+
const payments = Object.create(index_1.DarkForestPayments.prototype);
|
|
34
|
+
payments.activeSessions = new Map();
|
|
35
|
+
payments.createEscrowWrapper = async () => 'escrow-tx';
|
|
36
|
+
payments.delegateToPer = async () => 'delegate-tx';
|
|
37
|
+
payments.recordSettlement = async () => 'settlement-tx';
|
|
38
|
+
const session = await payments.startPrivateSession(77, index_1.DARK_FOREST_PROGRAM_ID, {
|
|
39
|
+
agentEndpoint: 'https://agent.taskforest.xyz',
|
|
40
|
+
token: 'SOL',
|
|
41
|
+
budgetLamports: 1500000,
|
|
42
|
+
perEndpoint: index_1.PER_ENDPOINTS.devnet,
|
|
43
|
+
});
|
|
44
|
+
strict_1.default.equal(session.escrowId, 77);
|
|
45
|
+
strict_1.default.equal(session.isActive, true);
|
|
46
|
+
strict_1.default.equal(payments.getActiveSession(77)?.requestCount, 0);
|
|
47
|
+
await payments.recordPayment(77, 500000);
|
|
48
|
+
strict_1.default.equal(payments.getActiveSession(77)?.totalPaid, 500000);
|
|
49
|
+
strict_1.default.equal(payments.getActiveSession(77)?.requestCount, 1);
|
|
50
|
+
const tx = await payments.closeSession(77);
|
|
51
|
+
strict_1.default.equal(tx, 'settlement-tx');
|
|
52
|
+
strict_1.default.equal(payments.getActiveSession(77), undefined);
|
|
53
|
+
});
|
|
54
|
+
(0, node_test_1.default)('returns null for unreadable escrow and settlement accounts', async () => {
|
|
55
|
+
const payments = Object.create(index_1.DarkForestPayments.prototype);
|
|
56
|
+
payments.program = {
|
|
57
|
+
account: {
|
|
58
|
+
escrowWrapper: { fetch: async () => { throw new Error('missing'); } },
|
|
59
|
+
settlementRecord: { fetch: async () => { throw new Error('missing'); } },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
strict_1.default.equal(await payments.getEscrow(5), null);
|
|
63
|
+
strict_1.default.equal(await payments.getSettlement(5), null);
|
|
64
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { AnchorProvider, type Idl } from '@coral-xyz/anchor';
|
|
4
|
+
import { Connection, PublicKey } from '@solana/web3.js';
|
|
5
|
+
export declare const DARK_FOREST_PROGRAM_ID: PublicKey;
|
|
6
|
+
export declare const ESCROW_SEED: Buffer;
|
|
7
|
+
export declare const SETTLEMENT_SEED: Buffer;
|
|
8
|
+
export declare const TEE_VALIDATORS: {
|
|
9
|
+
readonly mainnet: PublicKey;
|
|
10
|
+
readonly devnet: PublicKey;
|
|
11
|
+
};
|
|
12
|
+
export declare const PER_ENDPOINTS: {
|
|
13
|
+
readonly mainnet: "https://mainnet-tee.magicblock.app";
|
|
14
|
+
readonly devnet: "https://tee.magicblock.app";
|
|
15
|
+
readonly devnetRouter: "https://devnet-router.magicblock.app";
|
|
16
|
+
};
|
|
17
|
+
export type MppSessionConfig = {
|
|
18
|
+
agentEndpoint: string;
|
|
19
|
+
token: 'SOL' | 'USDC' | 'PYUSD';
|
|
20
|
+
budgetLamports: number;
|
|
21
|
+
perEndpoint: string;
|
|
22
|
+
};
|
|
23
|
+
export type MppSessionState = {
|
|
24
|
+
sessionId: string;
|
|
25
|
+
escrowId: number;
|
|
26
|
+
totalPaid: number;
|
|
27
|
+
requestCount: number;
|
|
28
|
+
isActive: boolean;
|
|
29
|
+
};
|
|
30
|
+
export interface EscrowState {
|
|
31
|
+
escrowId: number;
|
|
32
|
+
jobPubkey: PublicKey;
|
|
33
|
+
poster: PublicKey;
|
|
34
|
+
agent: PublicKey;
|
|
35
|
+
deposited: number;
|
|
36
|
+
status: 'Active' | 'Delegated' | 'Settled';
|
|
37
|
+
teePubkey: number[];
|
|
38
|
+
teeVerified: boolean;
|
|
39
|
+
mppSessionId: number[];
|
|
40
|
+
createdAt: number;
|
|
41
|
+
}
|
|
42
|
+
export interface SettlementState {
|
|
43
|
+
escrowId: number;
|
|
44
|
+
jobPubkey: PublicKey;
|
|
45
|
+
poster: PublicKey;
|
|
46
|
+
agent: PublicKey;
|
|
47
|
+
totalDeposited: number;
|
|
48
|
+
totalPaid: number;
|
|
49
|
+
settledAt: number;
|
|
50
|
+
settlementHash: number[];
|
|
51
|
+
}
|
|
52
|
+
export declare class DarkForestPayments {
|
|
53
|
+
private program;
|
|
54
|
+
private provider;
|
|
55
|
+
private activeSessions;
|
|
56
|
+
constructor(provider: AnchorProvider, idl: Idl);
|
|
57
|
+
connectToPer(endpoint?: string): Connection;
|
|
58
|
+
createEscrowWrapper(escrowId: number, jobPubkey: PublicKey, depositSol: number, mppSessionId?: number[]): Promise<string>;
|
|
59
|
+
delegateToPer(escrowId: number, validator?: PublicKey): Promise<string>;
|
|
60
|
+
verifyTeeAttestation(escrowId: number, attestationReport: Buffer, teePubkey: number[]): Promise<string>;
|
|
61
|
+
recordSettlement(escrowId: number, totalPaidSol: number): Promise<string>;
|
|
62
|
+
startPrivateSession(escrowId: number, jobPubkey: PublicKey, config: MppSessionConfig): Promise<MppSessionState>;
|
|
63
|
+
recordPayment(escrowId: number, amountLamports: number): Promise<void>;
|
|
64
|
+
closeSession(escrowId: number): Promise<string>;
|
|
65
|
+
getEscrow(escrowId: number): Promise<EscrowState | null>;
|
|
66
|
+
getSettlement(escrowId: number): Promise<SettlementState | null>;
|
|
67
|
+
getActiveSession(escrowId: number): MppSessionState | undefined;
|
|
68
|
+
static escrowPda(escrowId: number): PublicKey;
|
|
69
|
+
static settlementPda(escrowId: number): PublicKey;
|
|
70
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DarkForestPayments = exports.PER_ENDPOINTS = exports.TEE_VALIDATORS = exports.SETTLEMENT_SEED = exports.ESCROW_SEED = exports.DARK_FOREST_PROGRAM_ID = void 0;
|
|
4
|
+
const anchor_1 = require("@coral-xyz/anchor");
|
|
5
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
6
|
+
exports.DARK_FOREST_PROGRAM_ID = new web3_js_1.PublicKey('4hNP2tU5r5GgyASTrou84kWHbCwdyXVJJN4mve99rjgs');
|
|
7
|
+
exports.ESCROW_SEED = Buffer.from('escrow');
|
|
8
|
+
exports.SETTLEMENT_SEED = Buffer.from('settlement');
|
|
9
|
+
exports.TEE_VALIDATORS = {
|
|
10
|
+
mainnet: new web3_js_1.PublicKey('MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo'),
|
|
11
|
+
devnet: new web3_js_1.PublicKey('FnE6VJT5QNZdedZPnCoLsARgBwoE6DeJNjBs2H1gySXA'),
|
|
12
|
+
};
|
|
13
|
+
exports.PER_ENDPOINTS = {
|
|
14
|
+
mainnet: 'https://mainnet-tee.magicblock.app',
|
|
15
|
+
devnet: 'https://tee.magicblock.app',
|
|
16
|
+
devnetRouter: 'https://devnet-router.magicblock.app',
|
|
17
|
+
};
|
|
18
|
+
function deriveEscrowPda(escrowId) {
|
|
19
|
+
return web3_js_1.PublicKey.findProgramAddressSync([exports.ESCROW_SEED, new anchor_1.BN(escrowId).toArrayLike(Buffer, 'le', 8)], exports.DARK_FOREST_PROGRAM_ID);
|
|
20
|
+
}
|
|
21
|
+
function deriveSettlementPda(escrowId) {
|
|
22
|
+
return web3_js_1.PublicKey.findProgramAddressSync([exports.SETTLEMENT_SEED, new anchor_1.BN(escrowId).toArrayLike(Buffer, 'le', 8)], exports.DARK_FOREST_PROGRAM_ID);
|
|
23
|
+
}
|
|
24
|
+
class DarkForestPayments {
|
|
25
|
+
constructor(provider, idl) {
|
|
26
|
+
this.activeSessions = new Map();
|
|
27
|
+
this.provider = provider;
|
|
28
|
+
this.program = new anchor_1.Program(idl, provider);
|
|
29
|
+
}
|
|
30
|
+
connectToPer(endpoint = exports.PER_ENDPOINTS.devnet) {
|
|
31
|
+
return new web3_js_1.Connection(endpoint, 'confirmed');
|
|
32
|
+
}
|
|
33
|
+
async createEscrowWrapper(escrowId, jobPubkey, depositSol, mppSessionId = Array(32).fill(0)) {
|
|
34
|
+
const [escrowPda] = deriveEscrowPda(escrowId);
|
|
35
|
+
return this.program.methods
|
|
36
|
+
.createEscrowWrapper(new anchor_1.BN(escrowId), new anchor_1.BN(Math.floor(depositSol * web3_js_1.LAMPORTS_PER_SOL)), mppSessionId)
|
|
37
|
+
.accounts({
|
|
38
|
+
job: jobPubkey,
|
|
39
|
+
escrow: escrowPda,
|
|
40
|
+
poster: this.provider.wallet.publicKey,
|
|
41
|
+
systemProgram: web3_js_1.SystemProgram.programId,
|
|
42
|
+
})
|
|
43
|
+
.rpc();
|
|
44
|
+
}
|
|
45
|
+
async delegateToPer(escrowId, validator = exports.TEE_VALIDATORS.devnet) {
|
|
46
|
+
const [escrowPda] = deriveEscrowPda(escrowId);
|
|
47
|
+
return this.program.methods
|
|
48
|
+
.delegateToPer()
|
|
49
|
+
.accounts({
|
|
50
|
+
pda: escrowPda,
|
|
51
|
+
payer: this.provider.wallet.publicKey,
|
|
52
|
+
validator,
|
|
53
|
+
})
|
|
54
|
+
.rpc();
|
|
55
|
+
}
|
|
56
|
+
async verifyTeeAttestation(escrowId, attestationReport, teePubkey) {
|
|
57
|
+
const [escrowPda] = deriveEscrowPda(escrowId);
|
|
58
|
+
return this.program.methods
|
|
59
|
+
.verifyTeeAttestation(new anchor_1.BN(escrowId), attestationReport, teePubkey)
|
|
60
|
+
.accounts({
|
|
61
|
+
escrow: escrowPda,
|
|
62
|
+
payer: this.provider.wallet.publicKey,
|
|
63
|
+
})
|
|
64
|
+
.rpc();
|
|
65
|
+
}
|
|
66
|
+
async recordSettlement(escrowId, totalPaidSol) {
|
|
67
|
+
const [escrowPda] = deriveEscrowPda(escrowId);
|
|
68
|
+
const [settlementPda] = deriveSettlementPda(escrowId);
|
|
69
|
+
return this.program.methods
|
|
70
|
+
.recordSettlement(new anchor_1.BN(escrowId), new anchor_1.BN(Math.floor(totalPaidSol * web3_js_1.LAMPORTS_PER_SOL)))
|
|
71
|
+
.accounts({
|
|
72
|
+
escrow: escrowPda,
|
|
73
|
+
settlementRecord: settlementPda,
|
|
74
|
+
poster: this.provider.wallet.publicKey,
|
|
75
|
+
payer: this.provider.wallet.publicKey,
|
|
76
|
+
systemProgram: web3_js_1.SystemProgram.programId,
|
|
77
|
+
})
|
|
78
|
+
.rpc();
|
|
79
|
+
}
|
|
80
|
+
async startPrivateSession(escrowId, jobPubkey, config) {
|
|
81
|
+
const depositSol = config.budgetLamports / web3_js_1.LAMPORTS_PER_SOL;
|
|
82
|
+
const sessionId = `dark-${escrowId}-${Date.now()}`;
|
|
83
|
+
const sessionIdBytes = Array.from(Buffer.from(sessionId.padEnd(32, '\0').slice(0, 32)));
|
|
84
|
+
await this.createEscrowWrapper(escrowId, jobPubkey, depositSol, sessionIdBytes);
|
|
85
|
+
await this.delegateToPer(escrowId);
|
|
86
|
+
const session = {
|
|
87
|
+
sessionId,
|
|
88
|
+
escrowId,
|
|
89
|
+
totalPaid: 0,
|
|
90
|
+
requestCount: 0,
|
|
91
|
+
isActive: true,
|
|
92
|
+
};
|
|
93
|
+
this.activeSessions.set(escrowId, session);
|
|
94
|
+
return session;
|
|
95
|
+
}
|
|
96
|
+
async recordPayment(escrowId, amountLamports) {
|
|
97
|
+
const session = this.activeSessions.get(escrowId);
|
|
98
|
+
if (!session || !session.isActive)
|
|
99
|
+
throw new Error('No active session for this escrow');
|
|
100
|
+
session.totalPaid += amountLamports;
|
|
101
|
+
session.requestCount += 1;
|
|
102
|
+
}
|
|
103
|
+
async closeSession(escrowId) {
|
|
104
|
+
const session = this.activeSessions.get(escrowId);
|
|
105
|
+
if (!session)
|
|
106
|
+
throw new Error('No session for this escrow');
|
|
107
|
+
session.isActive = false;
|
|
108
|
+
const totalPaidSol = session.totalPaid / web3_js_1.LAMPORTS_PER_SOL;
|
|
109
|
+
const tx = await this.recordSettlement(escrowId, totalPaidSol);
|
|
110
|
+
this.activeSessions.delete(escrowId);
|
|
111
|
+
return tx;
|
|
112
|
+
}
|
|
113
|
+
async getEscrow(escrowId) {
|
|
114
|
+
const [escrowPda] = deriveEscrowPda(escrowId);
|
|
115
|
+
try {
|
|
116
|
+
const account = await this.program.account.escrowWrapper.fetch(escrowPda);
|
|
117
|
+
const statusMap = { 0: 'Active', 1: 'Delegated', 2: 'Settled' };
|
|
118
|
+
return {
|
|
119
|
+
escrowId: account.escrowId.toNumber(),
|
|
120
|
+
jobPubkey: account.jobPubkey,
|
|
121
|
+
poster: account.poster,
|
|
122
|
+
agent: account.agent,
|
|
123
|
+
deposited: account.deposited.toNumber(),
|
|
124
|
+
status: statusMap[account.status.active !== undefined ? 0 : account.status.delegated !== undefined ? 1 : 2] ?? 'Active',
|
|
125
|
+
teePubkey: account.teePubkey,
|
|
126
|
+
teeVerified: account.teeVerified,
|
|
127
|
+
mppSessionId: account.mppSessionId,
|
|
128
|
+
createdAt: account.createdAt.toNumber(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async getSettlement(escrowId) {
|
|
136
|
+
const [settlementPda] = deriveSettlementPda(escrowId);
|
|
137
|
+
try {
|
|
138
|
+
const account = await this.program.account.settlementRecord.fetch(settlementPda);
|
|
139
|
+
return {
|
|
140
|
+
escrowId: account.escrowId.toNumber(),
|
|
141
|
+
jobPubkey: account.jobPubkey,
|
|
142
|
+
poster: account.poster,
|
|
143
|
+
agent: account.agent,
|
|
144
|
+
totalDeposited: account.totalDeposited.toNumber(),
|
|
145
|
+
totalPaid: account.totalPaid.toNumber(),
|
|
146
|
+
settledAt: account.settledAt.toNumber(),
|
|
147
|
+
settlementHash: account.settlementHash,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
getActiveSession(escrowId) {
|
|
155
|
+
return this.activeSessions.get(escrowId);
|
|
156
|
+
}
|
|
157
|
+
static escrowPda(escrowId) {
|
|
158
|
+
return deriveEscrowPda(escrowId)[0];
|
|
159
|
+
}
|
|
160
|
+
static settlementPda(escrowId) {
|
|
161
|
+
return deriveSettlementPda(escrowId)[0];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.DarkForestPayments = DarkForestPayments;
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@taskforest/dark-forest",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dark Forest payment helpers for TaskForest: MPP wrapper, PER delegation, and settlement utilities",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"solana",
|
|
12
|
+
"taskforest",
|
|
13
|
+
"payments",
|
|
14
|
+
"magicblock",
|
|
15
|
+
"per",
|
|
16
|
+
"mpp"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/jimmdd/taskforest-protocol"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://taskforest.xyz",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"test": "npm run build && node --test dist/__tests__/*.test.js",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@coral-xyz/anchor": "^0.30.1",
|
|
31
|
+
"@solana/web3.js": "^1.95.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.4.0"
|
|
35
|
+
}
|
|
36
|
+
}
|