@blindmarket/sdk 0.1.4

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.
Files changed (244) hide show
  1. package/README.md +72 -0
  2. package/biome.json +19 -0
  3. package/dist/BlindEscrowClient-CyPuKMge.d.cts +54 -0
  4. package/dist/BlindEscrowClient-R7iGpE-n.d.ts +54 -0
  5. package/dist/KeyStore-Be_SSpCt.d.cts +21 -0
  6. package/dist/KeyStore-k1JgYPJY.d.ts +21 -0
  7. package/dist/Signer-BnqH_qew.d.cts +38 -0
  8. package/dist/Signer-LColE0Uo.d.ts +38 -0
  9. package/dist/Storage-4QOrh7rm.d.ts +17 -0
  10. package/dist/Storage-Cf4IJRMk.d.cts +17 -0
  11. package/dist/TaskRegistryClient-Ch6lYqM_.d.ts +24 -0
  12. package/dist/TaskRegistryClient-CrQjddDc.d.cts +24 -0
  13. package/dist/aes-J-7HrHze.d.cts +17 -0
  14. package/dist/aes-J-7HrHze.d.ts +17 -0
  15. package/dist/agent/Agent.d.ts +59 -0
  16. package/dist/agent/Agent.js +73 -0
  17. package/dist/agent/index.cjs +425 -0
  18. package/dist/agent/index.cjs.map +1 -0
  19. package/dist/agent/index.d.cts +63 -0
  20. package/dist/agent/index.d.ts +1 -0
  21. package/dist/agent/index.js +1 -0
  22. package/dist/agent/index.js.map +1 -0
  23. package/dist/api/index.cjs +4 -0
  24. package/dist/api/index.cjs.map +1 -0
  25. package/dist/api/index.d.cts +2 -0
  26. package/dist/api/index.d.ts +1 -0
  27. package/dist/api/index.js +1 -0
  28. package/dist/api/index.js.map +1 -0
  29. package/dist/chain/BlindEscrowClient.d.ts +51 -0
  30. package/dist/chain/BlindEscrowClient.js +142 -0
  31. package/dist/chain/BlindReputationClient.d.ts +16 -0
  32. package/dist/chain/BlindReputationClient.js +45 -0
  33. package/dist/chain/ChainClient.d.ts +26 -0
  34. package/dist/chain/ChainClient.js +46 -0
  35. package/dist/chain/TaskRegistryClient.d.ts +21 -0
  36. package/dist/chain/TaskRegistryClient.js +70 -0
  37. package/dist/chain/abi/BlindEscrow.json +1113 -0
  38. package/dist/chain/abi/BlindReputation.json +447 -0
  39. package/dist/chain/abi/TaskRegistry.json +586 -0
  40. package/dist/chain/abi/index.d.ts +4 -0
  41. package/dist/chain/abi/index.js +4 -0
  42. package/dist/chain/errors.d.ts +8 -0
  43. package/dist/chain/errors.js +32 -0
  44. package/dist/chain/index.cjs +2484 -0
  45. package/dist/chain/index.cjs.map +1 -0
  46. package/dist/chain/index.d.cts +2305 -0
  47. package/dist/chain/index.d.ts +6 -0
  48. package/dist/chain/index.js +6 -0
  49. package/dist/chain/index.js.map +1 -0
  50. package/dist/compute/index.cjs +4 -0
  51. package/dist/compute/index.cjs.map +1 -0
  52. package/dist/compute/index.d.cts +2 -0
  53. package/dist/compute/index.d.ts +1 -0
  54. package/dist/compute/index.js +1 -0
  55. package/dist/compute/index.js.map +1 -0
  56. package/dist/crypto/aes.d.ts +15 -0
  57. package/dist/crypto/aes.js +81 -0
  58. package/dist/crypto/constants.d.ts +7 -0
  59. package/dist/crypto/constants.js +7 -0
  60. package/dist/crypto/ecies.d.ts +8 -0
  61. package/dist/crypto/ecies.js +51 -0
  62. package/dist/crypto/envelope.d.ts +35 -0
  63. package/dist/crypto/envelope.js +103 -0
  64. package/dist/crypto/hkdf.d.ts +5 -0
  65. package/dist/crypto/hkdf.js +34 -0
  66. package/dist/crypto/index.cjs +424 -0
  67. package/dist/crypto/index.cjs.map +1 -0
  68. package/dist/crypto/index.d.cts +74 -0
  69. package/dist/crypto/index.d.ts +6 -0
  70. package/dist/crypto/index.js +6 -0
  71. package/dist/crypto/index.js.map +1 -0
  72. package/dist/crypto/keys.d.ts +12 -0
  73. package/dist/crypto/keys.js +42 -0
  74. package/dist/errors/BlindBountyError.d.ts +14 -0
  75. package/dist/errors/BlindBountyError.js +30 -0
  76. package/dist/errors/codes.d.ts +19 -0
  77. package/dist/errors/codes.js +48 -0
  78. package/dist/errors/index.cjs +136 -0
  79. package/dist/errors/index.cjs.map +1 -0
  80. package/dist/errors/index.d.cts +60 -0
  81. package/dist/errors/index.d.ts +3 -0
  82. package/dist/errors/index.js +3 -0
  83. package/dist/errors/index.js.map +1 -0
  84. package/dist/errors/subclasses.d.ts +26 -0
  85. package/dist/errors/subclasses.js +41 -0
  86. package/dist/events/EventBus.d.ts +13 -0
  87. package/dist/events/EventBus.js +41 -0
  88. package/dist/events/index.cjs +41 -0
  89. package/dist/events/index.cjs.map +1 -0
  90. package/dist/events/index.d.cts +61 -0
  91. package/dist/events/index.d.ts +2 -0
  92. package/dist/events/index.js +2 -0
  93. package/dist/events/index.js.map +1 -0
  94. package/dist/events/types.d.ts +45 -0
  95. package/dist/events/types.js +1 -0
  96. package/dist/index.cjs +4 -0
  97. package/dist/index.cjs.map +1 -0
  98. package/dist/index.d.cts +2 -0
  99. package/dist/index.d.ts +129 -0
  100. package/dist/index.js +154 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/keystore/InMemoryKeyStore.d.ts +21 -0
  103. package/dist/keystore/InMemoryKeyStore.js +111 -0
  104. package/dist/keystore/KeyStore.d.ts +18 -0
  105. package/dist/keystore/KeyStore.js +1 -0
  106. package/dist/keystore/index.cjs +335 -0
  107. package/dist/keystore/index.cjs.map +1 -0
  108. package/dist/keystore/index.d.cts +23 -0
  109. package/dist/keystore/index.d.ts +2 -0
  110. package/dist/keystore/index.js +2 -0
  111. package/dist/keystore/index.js.map +1 -0
  112. package/dist/network/index.cjs +74 -0
  113. package/dist/network/index.cjs.map +1 -0
  114. package/dist/network/index.d.cts +36 -0
  115. package/dist/network/index.d.ts +3 -0
  116. package/dist/network/index.js +3 -0
  117. package/dist/network/index.js.map +1 -0
  118. package/dist/network/presets.d.ts +24 -0
  119. package/dist/network/presets.js +23 -0
  120. package/dist/network/resolve.d.ts +7 -0
  121. package/dist/network/resolve.js +17 -0
  122. package/dist/network/types.d.ts +16 -0
  123. package/dist/network/types.js +1 -0
  124. package/dist/signer/EthersSigner.d.ts +24 -0
  125. package/dist/signer/EthersSigner.js +68 -0
  126. package/dist/signer/PrivateKeySigner.d.ts +14 -0
  127. package/dist/signer/PrivateKeySigner.js +18 -0
  128. package/dist/signer/Signer.d.ts +35 -0
  129. package/dist/signer/Signer.js +1 -0
  130. package/dist/signer/index.cjs +121 -0
  131. package/dist/signer/index.cjs.map +1 -0
  132. package/dist/signer/index.d.cts +40 -0
  133. package/dist/signer/index.d.ts +3 -0
  134. package/dist/signer/index.js +3 -0
  135. package/dist/signer/index.js.map +1 -0
  136. package/dist/storage/MemoryStorage.d.ts +18 -0
  137. package/dist/storage/MemoryStorage.js +40 -0
  138. package/dist/storage/Storage.d.ts +14 -0
  139. package/dist/storage/Storage.js +1 -0
  140. package/dist/storage/ZgStorage.d.ts +24 -0
  141. package/dist/storage/ZgStorage.js +102 -0
  142. package/dist/storage/index.cjs +221 -0
  143. package/dist/storage/index.cjs.map +1 -0
  144. package/dist/storage/index.d.cts +44 -0
  145. package/dist/storage/index.d.ts +3 -0
  146. package/dist/storage/index.js +3 -0
  147. package/dist/storage/index.js.map +1 -0
  148. package/dist/testing/index.cjs +4 -0
  149. package/dist/testing/index.cjs.map +1 -0
  150. package/dist/testing/index.d.cts +2 -0
  151. package/dist/testing/index.d.ts +1 -0
  152. package/dist/testing/index.js +1 -0
  153. package/dist/testing/index.js.map +1 -0
  154. package/dist/types-BOG5Ub1z.d.cts +31 -0
  155. package/dist/types-BOG5Ub1z.d.ts +31 -0
  156. package/dist/types-V7_ZmUb-.d.cts +19 -0
  157. package/dist/types-hAsITUQ7.d.ts +19 -0
  158. package/dist/types.d.ts +58 -0
  159. package/dist/types.js +1 -0
  160. package/dist/verifier/index.cjs +4 -0
  161. package/dist/verifier/index.cjs.map +1 -0
  162. package/dist/verifier/index.d.cts +2 -0
  163. package/dist/verifier/index.d.ts +1 -0
  164. package/dist/verifier/index.js +1 -0
  165. package/dist/verifier/index.js.map +1 -0
  166. package/dist/worker/Worker.d.ts +53 -0
  167. package/dist/worker/Worker.js +53 -0
  168. package/dist/worker/index.cjs +382 -0
  169. package/dist/worker/index.cjs.map +1 -0
  170. package/dist/worker/index.d.cts +58 -0
  171. package/dist/worker/index.d.ts +1 -0
  172. package/dist/worker/index.js +1 -0
  173. package/dist/worker/index.js.map +1 -0
  174. package/examples/agent-demo.ts +121 -0
  175. package/package.json +46 -0
  176. package/scripts/gen-crypto-fixtures.mjs +72 -0
  177. package/src/agent/Agent.ts +122 -0
  178. package/src/agent/index.ts +1 -0
  179. package/src/api/index.ts +1 -0
  180. package/src/chain/BlindEscrowClient.ts +182 -0
  181. package/src/chain/BlindReputationClient.ts +55 -0
  182. package/src/chain/ChainClient.ts +59 -0
  183. package/src/chain/TaskRegistryClient.ts +82 -0
  184. package/src/chain/abi/BlindEscrow.json +1113 -0
  185. package/src/chain/abi/BlindReputation.json +447 -0
  186. package/src/chain/abi/TaskRegistry.json +586 -0
  187. package/src/chain/abi/index.ts +5 -0
  188. package/src/chain/errors.ts +43 -0
  189. package/src/chain/index.ts +6 -0
  190. package/src/compute/index.ts +1 -0
  191. package/src/crypto/aes.ts +104 -0
  192. package/src/crypto/constants.ts +7 -0
  193. package/src/crypto/ecies.ts +60 -0
  194. package/src/crypto/envelope.ts +131 -0
  195. package/src/crypto/hkdf.ts +45 -0
  196. package/src/crypto/index.ts +6 -0
  197. package/src/crypto/keys.ts +56 -0
  198. package/src/errors/BlindMarketError.ts +38 -0
  199. package/src/errors/codes.ts +67 -0
  200. package/src/errors/index.ts +3 -0
  201. package/src/errors/subclasses.ts +59 -0
  202. package/src/events/EventBus.ts +45 -0
  203. package/src/events/index.ts +2 -0
  204. package/src/events/types.ts +13 -0
  205. package/src/index.ts +222 -0
  206. package/src/keystore/InMemoryKeyStore.ts +146 -0
  207. package/src/keystore/KeyStore.ts +19 -0
  208. package/src/keystore/index.ts +2 -0
  209. package/src/network/index.ts +3 -0
  210. package/src/network/presets.ts +27 -0
  211. package/src/network/resolve.ts +19 -0
  212. package/src/network/types.ts +18 -0
  213. package/src/signer/EthersSigner.ts +82 -0
  214. package/src/signer/PrivateKeySigner.ts +19 -0
  215. package/src/signer/Signer.ts +40 -0
  216. package/src/signer/index.ts +3 -0
  217. package/src/storage/MemoryStorage.ts +49 -0
  218. package/src/storage/Storage.ts +15 -0
  219. package/src/storage/ZgStorage.ts +124 -0
  220. package/src/storage/index.ts +3 -0
  221. package/src/testing/index.ts +1 -0
  222. package/src/types.ts +76 -0
  223. package/src/verifier/index.ts +1 -0
  224. package/src/worker/Worker.ts +98 -0
  225. package/src/worker/index.ts +1 -0
  226. package/test/agent.test.ts +193 -0
  227. package/test/chain.test.ts +145 -0
  228. package/test/crypto.aes.test.ts +81 -0
  229. package/test/crypto.compat.test.ts +33 -0
  230. package/test/crypto.ecies.test.ts +64 -0
  231. package/test/envelope.test.ts +82 -0
  232. package/test/errors.test.ts +64 -0
  233. package/test/events.test.ts +59 -0
  234. package/test/fixtures/crypto.json +16 -0
  235. package/test/keystore.test.ts +103 -0
  236. package/test/network.test.ts +53 -0
  237. package/test/signer.test.ts +66 -0
  238. package/test/smoke.test.ts +7 -0
  239. package/test/storage.test.ts +55 -0
  240. package/test/worker.test.ts +181 -0
  241. package/tsconfig.build.json +1 -0
  242. package/tsconfig.json +13 -0
  243. package/tsup.config.ts +32 -0
  244. package/vitest.config.ts +17 -0
@@ -0,0 +1,124 @@
1
+ import { Indexer, MemData } from '@0gfoundation/0g-ts-sdk';
2
+ import type { ethers } from 'ethers';
3
+ import { StorageError } from '../errors/index.js';
4
+ import type { RootHash, UploadResult } from '../types.js';
5
+ import type { Storage } from './Storage.js';
6
+
7
+ export interface ZgStorageOptions {
8
+ /** 0G Storage indexer URL, e.g. 'https://indexer-storage-testnet-turbo.0g.ai'. */
9
+ indexerUrl: string;
10
+ /** 0G chain RPC URL — passed to the indexer for upload fee submission. */
11
+ rpcUrl: string;
12
+ /** Wallet used to pay for uploads. Reads don't require a signer. */
13
+ signer?: ethers.Signer;
14
+ }
15
+
16
+ /**
17
+ * Node-only adapter over @0gfoundation/0g-ts-sdk. Browser consumers must
18
+ * provide their own Storage adapter (IPFS, custom server-side proxy, etc.)
19
+ * because the 0G TS SDK uses Node-specific filesystem APIs for downloads.
20
+ */
21
+ export class ZgStorage implements Storage {
22
+ private readonly indexer: Indexer;
23
+ private readonly opts: ZgStorageOptions;
24
+
25
+ constructor(opts: ZgStorageOptions) {
26
+ this.opts = opts;
27
+ this.indexer = new Indexer(opts.indexerUrl);
28
+ }
29
+
30
+ async upload(bytes: Uint8Array): Promise<UploadResult> {
31
+ if (!this.opts.signer) {
32
+ throw new StorageError('STORAGE/UPLOAD_FAILED', 'signer required for uploads');
33
+ }
34
+ const memData = new MemData(copy(bytes));
35
+ const [tree, treeErr] = await memData.merkleTree();
36
+ if (treeErr !== null || !tree) {
37
+ throw new StorageError('STORAGE/UPLOAD_FAILED', 'merkle tree computation failed', { cause: treeErr ?? undefined });
38
+ }
39
+ const rootHash = tree.rootHash() as RootHash;
40
+
41
+ let txHash: string | undefined;
42
+ try {
43
+ // The 0G SDK's Signer types are pinned to a specific ethers minor version
44
+ // that may not match the host project's. Runtime is compatible; the cast
45
+ // avoids false-negative type errors across ethers versions.
46
+ // biome-ignore lint/suspicious/noExplicitAny: see comment above
47
+ const [tx, uploadErr] = await this.indexer.upload(memData, this.opts.rpcUrl, this.opts.signer as any);
48
+ if (uploadErr !== null) {
49
+ throw new StorageError('STORAGE/UPLOAD_FAILED', 'indexer upload failed', {
50
+ cause: uploadErr,
51
+ retriable: true,
52
+ });
53
+ }
54
+ const maybe = tx as { txHash?: string } | null;
55
+ txHash = maybe?.txHash;
56
+ } catch (err) {
57
+ if (err instanceof StorageError) throw err;
58
+ throw new StorageError('STORAGE/UPLOAD_FAILED', 'unexpected upload error', {
59
+ cause: err,
60
+ retriable: true,
61
+ });
62
+ }
63
+
64
+ return {
65
+ rootHash,
66
+ ...(txHash ? { txHash: txHash as `0x${string}` } : {}),
67
+ size: bytes.byteLength,
68
+ };
69
+ }
70
+
71
+ async download(rootHash: RootHash): Promise<Uint8Array> {
72
+ // 0G SDK downloads to a filesystem path. We write to a tmp file, read bytes back,
73
+ // and delete the tmp file regardless of success/failure.
74
+ const [nodeFs, nodePath, nodeOs, nodeCrypto] = await loadNodeFs();
75
+ const rand = nodeCrypto.randomBytes(8).toString('hex');
76
+ const tmp = nodePath.join(nodeOs.tmpdir(), `0g-${rootHash.replace(/^0x/, '')}-${rand}`);
77
+ try {
78
+ const err = await this.indexer.download(rootHash, tmp, true);
79
+ if (err !== null) {
80
+ throw new StorageError('STORAGE/NOT_FOUND', `no blob at ${rootHash}`, {
81
+ cause: err,
82
+ retriable: true,
83
+ });
84
+ }
85
+ return new Uint8Array(nodeFs.readFileSync(tmp));
86
+ } finally {
87
+ try {
88
+ nodeFs.unlinkSync(tmp);
89
+ } catch {
90
+ // ignore cleanup errors
91
+ }
92
+ }
93
+ }
94
+
95
+ async hash(bytes: Uint8Array): Promise<RootHash> {
96
+ const memData = new MemData(copy(bytes));
97
+ const [tree, err] = await memData.merkleTree();
98
+ if (err !== null || !tree) {
99
+ throw new StorageError('STORAGE/UPLOAD_FAILED', 'merkle tree computation failed', { cause: err ?? undefined });
100
+ }
101
+ return tree.rootHash() as RootHash;
102
+ }
103
+ }
104
+
105
+ function copy(u: Uint8Array): Uint8Array {
106
+ const c = new Uint8Array(u.byteLength);
107
+ c.set(u);
108
+ return c;
109
+ }
110
+
111
+ type NodeFs = { readFileSync: (p: string) => Buffer; unlinkSync: (p: string) => void };
112
+ type NodePath = { join: (...p: string[]) => string };
113
+ type NodeOs = { tmpdir: () => string };
114
+ type NodeCrypto = { randomBytes: (n: number) => { toString: (enc: 'hex') => string } };
115
+
116
+ async function loadNodeFs(): Promise<[NodeFs, NodePath, NodeOs, NodeCrypto]> {
117
+ const [fs, path, os, crypto] = await Promise.all([
118
+ import('node:fs'),
119
+ import('node:path'),
120
+ import('node:os'),
121
+ import('node:crypto'),
122
+ ]);
123
+ return [fs as unknown as NodeFs, path as unknown as NodePath, os as unknown as NodeOs, crypto as unknown as NodeCrypto];
124
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Storage.js';
2
+ export * from './MemoryStorage.js';
3
+ export * from './ZgStorage.js';
@@ -0,0 +1 @@
1
+ export {};
package/src/types.ts ADDED
@@ -0,0 +1,76 @@
1
+ export type Address = `0x${string}`;
2
+ export type Hex = `0x${string}`;
3
+ export type TaskId = bigint;
4
+ export type RootHash = Hex;
5
+
6
+ export type TokenSymbol = 'USDC' | 'A0GI' | string;
7
+
8
+ export interface TokenRef {
9
+ address: Address;
10
+ symbol?: TokenSymbol;
11
+ decimals?: number;
12
+ }
13
+
14
+ export interface Reward {
15
+ token: Address | TokenSymbol;
16
+ amount: bigint;
17
+ }
18
+
19
+ export type TaskStatus =
20
+ | 'funded'
21
+ | 'assigned'
22
+ | 'submitted'
23
+ | 'verified'
24
+ | 'completed'
25
+ | 'cancelled';
26
+
27
+ export interface TaskMetadata {
28
+ category: string;
29
+ locationZone: string;
30
+ reward: Reward;
31
+ deadline?: Date;
32
+ extra?: Record<string, string>;
33
+ }
34
+
35
+ export interface TaskRecord extends TaskMetadata {
36
+ taskId: TaskId;
37
+ agent: Address;
38
+ worker?: Address;
39
+ taskHash: RootHash;
40
+ evidenceHash?: RootHash;
41
+ status: TaskStatus;
42
+ createdAt: Date;
43
+ }
44
+
45
+ export interface TaskKey {
46
+ /** AES-256 symmetric key (32 bytes) used to encrypt task content. */
47
+ aesKey: Uint8Array;
48
+ createdAt: Date;
49
+ }
50
+
51
+ export interface TaskKeyRef {
52
+ taskId: TaskId;
53
+ createdAt: Date;
54
+ }
55
+
56
+ export interface UploadResult {
57
+ rootHash: RootHash;
58
+ txHash?: Hex;
59
+ size: number;
60
+ }
61
+
62
+ export interface VerificationResult {
63
+ passed: boolean;
64
+ confidence: number;
65
+ model: string;
66
+ attestation?: Hex;
67
+ completedAt: Date;
68
+ }
69
+
70
+ export interface TxReceiptLike {
71
+ hash: Hex;
72
+ blockNumber: number;
73
+ gasUsed: bigint;
74
+ }
75
+
76
+ export type Awaitable<T> = T | Promise<T>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ import type { BlindEscrowClient, OnChainTask, TaskRegistryClient } from '../chain/index.js';
2
+ import { aesDecrypt, eciesDecrypt, sealEvidence } from '../crypto/index.js';
3
+ import type { EventBus, SdkEvents } from '../events/index.js';
4
+ import type { Signer } from '../signer/index.js';
5
+ import type { Storage } from '../storage/index.js';
6
+ import type { Hex, RootHash, TaskId, TxReceiptLike } from '../types.js';
7
+
8
+ export interface WorkerDeps {
9
+ escrow: BlindEscrowClient;
10
+ registry: TaskRegistryClient;
11
+ storage: Storage;
12
+ signer: Signer;
13
+ events?: EventBus<SdkEvents>;
14
+ }
15
+
16
+ export interface DecryptInstructionsInput {
17
+ /** ECIES-wrapped AES key delivered out-of-band by the agent (e.g. via the backend API). */
18
+ wrappedKey: Uint8Array;
19
+ /** Worker's private key, hex with or without 0x prefix. */
20
+ workerPrivKey: string;
21
+ /** Storage root hash from the on-chain task metadata. */
22
+ taskHash: RootHash;
23
+ }
24
+
25
+ export interface SubmitEvidenceInput {
26
+ taskId: TaskId;
27
+ evidence: string | Uint8Array;
28
+ /** Agent's uncompressed secp256k1 pubkey (retrieved via the backend or chain). */
29
+ agentPubKey: string;
30
+ /** TEE enclave pubkey for Sealed Inference (from 0G Compute broker). */
31
+ enclavePubKey: string;
32
+ }
33
+
34
+ export interface SubmitEvidenceResult {
35
+ rootHash: RootHash;
36
+ txHash: Hex;
37
+ receipt: TxReceiptLike;
38
+ }
39
+
40
+ /**
41
+ * Worker-side flows: browse open tasks, decrypt assigned instructions, seal
42
+ * evidence for agent + TEE, and claim payment after the deadline if
43
+ * verification stalls. Peer pubkeys and wrapped keys are passed explicitly;
44
+ * the SDK does not assume a specific message-passing substrate.
45
+ */
46
+ export class Worker {
47
+ constructor(private readonly deps: WorkerDeps) {}
48
+
49
+ async browse(offset: bigint, limit: bigint): Promise<TaskId[]> {
50
+ return this.deps.registry.getOpenTasks(offset, limit);
51
+ }
52
+
53
+ async getTaskDetail(taskId: TaskId): Promise<OnChainTask> {
54
+ return this.deps.escrow.getTask(taskId);
55
+ }
56
+
57
+ async decryptInstructions(input: DecryptInstructionsInput): Promise<Uint8Array> {
58
+ const aesKey = await eciesDecrypt(input.wrappedKey, input.workerPrivKey);
59
+ const ciphertext = await this.deps.storage.download(input.taskHash);
60
+ return aesDecrypt(ciphertext, aesKey);
61
+ }
62
+
63
+ async submitEvidence(input: SubmitEvidenceInput): Promise<SubmitEvidenceResult> {
64
+ const evidenceBytes = typeof input.evidence === 'string'
65
+ ? new TextEncoder().encode(input.evidence)
66
+ : input.evidence;
67
+
68
+ const { envelope } = await sealEvidence({
69
+ evidence: evidenceBytes,
70
+ agentPubKey: input.agentPubKey,
71
+ enclavePubKey: input.enclavePubKey,
72
+ });
73
+
74
+ const uploaded = await this.deps.storage.upload(envelope);
75
+ const rootHash = uploaded.rootHash;
76
+
77
+ const receipt = await this.deps.escrow.submitEvidence(input.taskId, rootHash);
78
+
79
+ this.deps.events?.emit('evidence.submitted', {
80
+ taskId: input.taskId,
81
+ rootHash,
82
+ txHash: receipt.hash,
83
+ });
84
+
85
+ return { rootHash, txHash: receipt.hash, receipt };
86
+ }
87
+
88
+ /**
89
+ * Worker escape hatch: reclaim escrow after the deadline if completeVerification
90
+ * was never called. Payment released per contract logic (85% worker, 15% treasury
91
+ * or full refund depending on submission state — see BlindEscrow.claimTimeout).
92
+ */
93
+ async claimTimeout(taskId: TaskId): Promise<TxReceiptLike> {
94
+ const receipt = await this.deps.escrow.claimTimeout(taskId);
95
+ this.deps.events?.emit('payment.claimed', { taskId, txHash: receipt.hash, amount: 0n });
96
+ return receipt;
97
+ }
98
+ }
@@ -0,0 +1 @@
1
+ export * from './Worker.js';
@@ -0,0 +1,193 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Agent } from '../src/agent/index.js';
3
+ import type { BlindEscrowClient, CreateTaskArgs } from '../src/chain/index.js';
4
+ import { aesDecrypt, eciesDecrypt, generateKeyPair, sealEvidence } from '../src/crypto/index.js';
5
+ import { EventBus, type SdkEvents } from '../src/events/index.js';
6
+ import { InMemoryKeyStore } from '../src/keystore/index.js';
7
+ import { PrivateKeySigner } from '../src/signer/index.js';
8
+ import { MemoryStorage } from '../src/storage/index.js';
9
+ import type { Address, Hex, TaskId, TxReceiptLike } from '../src/types.js';
10
+
11
+ const AGENT_PK = '0x0123456789012345678901234567890123456789012345678901234567890123';
12
+ const WORKER_PK = '0x1000000000000000000000000000000000000000000000000000000000000001';
13
+ const FAKE_USDC = '0x317227efcA18D004E12CA8046AEf7E1597458F25' as Address;
14
+ const FAKE_WORKER = '0x2000000000000000000000000000000000000002' as Address;
15
+
16
+ interface EscrowCall<M extends string> {
17
+ method: M;
18
+ args: unknown;
19
+ }
20
+
21
+ /**
22
+ * In-memory BlindEscrow mock. Records every call for assertions and hands back
23
+ * deterministic task ids / receipts so the Agent flow can be exercised without
24
+ * a real chain.
25
+ */
26
+ class MockEscrow {
27
+ readonly calls: EscrowCall<'createTask' | 'assignWorker' | 'cancelTask'>[] = [];
28
+ private nextId = 1n;
29
+
30
+ async createTask(args: CreateTaskArgs) {
31
+ this.calls.push({ method: 'createTask', args });
32
+ const taskId = this.nextId++;
33
+ const receipt: TxReceiptLike = { hash: `0x${'a'.repeat(64)}` as Hex, blockNumber: 1, gasUsed: 21000n };
34
+ return { taskId, receipt };
35
+ }
36
+
37
+ async assignWorker(taskId: TaskId, worker: Address): Promise<TxReceiptLike> {
38
+ this.calls.push({ method: 'assignWorker', args: { taskId, worker } });
39
+ return { hash: `0x${'b'.repeat(64)}` as Hex, blockNumber: 2, gasUsed: 21000n };
40
+ }
41
+
42
+ async cancelTask(taskId: TaskId): Promise<TxReceiptLike> {
43
+ this.calls.push({ method: 'cancelTask', args: { taskId } });
44
+ return { hash: `0x${'c'.repeat(64)}` as Hex, blockNumber: 3, gasUsed: 21000n };
45
+ }
46
+ }
47
+
48
+ function mkDeps() {
49
+ const storage = new MemoryStorage();
50
+ const keystore = new InMemoryKeyStore();
51
+ const signer = new PrivateKeySigner(AGENT_PK);
52
+ const events = new EventBus<SdkEvents>();
53
+ const escrow = new MockEscrow();
54
+ return {
55
+ storage,
56
+ keystore,
57
+ signer,
58
+ events,
59
+ escrow,
60
+ agent: new Agent({
61
+ // biome-ignore lint/suspicious/noExplicitAny: MockEscrow matches the subset the Agent uses
62
+ escrow: escrow as any as BlindEscrowClient,
63
+ storage,
64
+ keystore,
65
+ signer,
66
+ events,
67
+ }),
68
+ };
69
+ }
70
+
71
+ describe('Agent.createTask', () => {
72
+ it('encrypts + uploads + calls chain + stashes key + emits event', async () => {
73
+ const { agent, escrow, storage, keystore, events } = mkDeps();
74
+
75
+ const emitted: SdkEvents['task.created'][] = [];
76
+ events.on('task.created', (p) => {
77
+ emitted.push(p);
78
+ });
79
+
80
+ const deadline = new Date(Date.now() + 3600_000);
81
+ const res = await agent.createTask({
82
+ instructions: 'Take 3 photos of the storefront',
83
+ category: 'photography',
84
+ locationZone: 'Lagos, NG',
85
+ reward: { token: FAKE_USDC, amount: 5_000000n },
86
+ deadline,
87
+ });
88
+
89
+ expect(res.taskId).toBe(1n);
90
+ expect(res.rootHash).toMatch(/^0x[0-9a-f]{64}$/);
91
+ expect(escrow.calls).toHaveLength(1);
92
+ expect(escrow.calls[0]?.method).toBe('createTask');
93
+
94
+ // Keystore now holds the task key for this id
95
+ const stored = await keystore.getTaskKey(1n);
96
+ expect(stored).not.toBeNull();
97
+ expect(stored?.aesKey).toEqual(res.taskKey.aesKey);
98
+
99
+ // The uploaded blob decrypts back to the original instructions
100
+ const blob = await storage.download(res.rootHash);
101
+ const pt = await aesDecrypt(blob, res.taskKey.aesKey);
102
+ expect(new TextDecoder().decode(pt)).toBe('Take 3 photos of the storefront');
103
+
104
+ // Event fired
105
+ expect(emitted).toHaveLength(1);
106
+ expect(emitted[0]?.taskId).toBe(1n);
107
+ expect(emitted[0]?.rootHash).toBe(res.rootHash);
108
+ });
109
+
110
+ it('accepts Uint8Array instructions as well as strings', async () => {
111
+ const { agent, storage } = mkDeps();
112
+ const res = await agent.createTask({
113
+ instructions: new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
114
+ category: 'x',
115
+ locationZone: 'y',
116
+ reward: { token: FAKE_USDC, amount: 1n },
117
+ deadline: new Date(Date.now() + 3600_000),
118
+ });
119
+ const blob = await storage.download(res.rootHash);
120
+ const pt = await aesDecrypt(blob, res.taskKey.aesKey);
121
+ expect(Array.from(pt)).toEqual([0xde, 0xad, 0xbe, 0xef]);
122
+ });
123
+ });
124
+
125
+ describe('Agent.assignWorker', () => {
126
+ it('wraps the task AES key to the worker pubkey and calls chain', async () => {
127
+ const { agent } = mkDeps();
128
+ const created = await agent.createTask({
129
+ instructions: 'hello',
130
+ category: 'c',
131
+ locationZone: 'z',
132
+ reward: { token: FAKE_USDC, amount: 1n },
133
+ deadline: new Date(Date.now() + 3600_000),
134
+ });
135
+ const worker = generateKeyPair();
136
+ const result = await agent.assignWorker(created.taskId, FAKE_WORKER, worker.publicKey);
137
+
138
+ // Worker can recover the AES key from the wrapped blob
139
+ const recoveredKey = await eciesDecrypt(result.wrappedKey, worker.privateKey);
140
+ expect(recoveredKey).toEqual(created.taskKey.aesKey);
141
+ });
142
+
143
+ it('throws LIFECYCLE/TASK_NOT_FOUND when task key is missing', async () => {
144
+ const { agent } = mkDeps();
145
+ const worker = generateKeyPair();
146
+ await expect(agent.assignWorker(999n, FAKE_WORKER, worker.publicKey)).rejects.toMatchObject({
147
+ code: 'LIFECYCLE/TASK_NOT_FOUND',
148
+ });
149
+ });
150
+ });
151
+
152
+ describe('Agent.cancelTask', () => {
153
+ it('calls chain and removes the task key from keystore', async () => {
154
+ const { agent, keystore, events } = mkDeps();
155
+ const created = await agent.createTask({
156
+ instructions: 'hello',
157
+ category: 'c',
158
+ locationZone: 'z',
159
+ reward: { token: FAKE_USDC, amount: 1n },
160
+ deadline: new Date(Date.now() + 3600_000),
161
+ });
162
+ expect(await keystore.getTaskKey(created.taskId)).not.toBeNull();
163
+
164
+ const cancelled: SdkEvents['task.cancelled'][] = [];
165
+ events.on('task.cancelled', (p) => cancelled.push(p));
166
+
167
+ await agent.cancelTask(created.taskId);
168
+ expect(await keystore.getTaskKey(created.taskId)).toBeNull();
169
+ expect(cancelled).toHaveLength(1);
170
+ });
171
+ });
172
+
173
+ describe('Agent.fetchAndDecryptEvidence', () => {
174
+ it('downloads the envelope from storage and unwraps with agent privkey', async () => {
175
+ const { agent, storage, signer } = mkDeps();
176
+ const agentPub = await signer.getPublicKey();
177
+ const enclave = generateKeyPair();
178
+
179
+ const evidence = new TextEncoder().encode('photos + timestamps');
180
+ const sealed = await sealEvidence({
181
+ evidence,
182
+ agentPubKey: agentPub,
183
+ enclavePubKey: enclave.publicKey,
184
+ });
185
+ const { rootHash } = await storage.upload(sealed.envelope);
186
+
187
+ const recovered = await agent.fetchAndDecryptEvidence(rootHash, AGENT_PK);
188
+ expect(new TextDecoder().decode(recovered)).toBe('photos + timestamps');
189
+ });
190
+ });
191
+
192
+ // silence unused-import warning for the imported pk
193
+ void WORKER_PK;
@@ -0,0 +1,145 @@
1
+ import { ethers } from 'ethers';
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ BlindEscrowAbi,
5
+ BlindEscrowClient,
6
+ BlindReputationAbi,
7
+ BlindReputationClient,
8
+ ChainClient,
9
+ TaskRegistryAbi,
10
+ TaskRegistryClient,
11
+ wrapChainError,
12
+ } from '../src/chain/index.js';
13
+ import { networks } from '../src/network/index.js';
14
+ import { PrivateKeySigner } from '../src/signer/index.js';
15
+ import type { Address } from '../src/types.js';
16
+
17
+ const FIXED_PK = '0x0123456789012345678901234567890123456789012345678901234567890123';
18
+ const RAND_ADDR = '0xFd4F93F5A7BE144c405D1D8fbEC63Fb776207681' as Address;
19
+
20
+ describe('ABIs', () => {
21
+ it('BlindEscrow ABI exposes the expected surface', () => {
22
+ const iface = new ethers.Interface(BlindEscrowAbi);
23
+ for (const name of [
24
+ 'createTask',
25
+ 'assignWorker',
26
+ 'submitEvidence',
27
+ 'completeVerification',
28
+ 'cancelTask',
29
+ 'getTask',
30
+ 'nextTaskId',
31
+ ]) {
32
+ expect(iface.getFunction(name), `missing ${name}`).not.toBeNull();
33
+ }
34
+ });
35
+
36
+ it('TaskRegistry ABI exposes the expected surface', () => {
37
+ const iface = new ethers.Interface(TaskRegistryAbi);
38
+ for (const name of ['getOpenTasks', 'getTaskMeta', 'totalTasks', 'openTaskCount']) {
39
+ expect(iface.getFunction(name), `missing ${name}`).not.toBeNull();
40
+ }
41
+ });
42
+
43
+ it('BlindReputation ABI exposes the expected surface', () => {
44
+ const iface = new ethers.Interface(BlindReputationAbi);
45
+ for (const name of ['rate', 'getReputation', 'hasBeenRated']) {
46
+ expect(iface.getFunction(name), `missing ${name}`).not.toBeNull();
47
+ }
48
+ });
49
+ });
50
+
51
+ describe('encoding round-trips (Interface level, no live RPC)', () => {
52
+ it('BlindEscrow.createTask encodes to a call-data blob matching the ABI', () => {
53
+ const iface = new ethers.Interface(BlindEscrowAbi);
54
+ const data = iface.encodeFunctionData('createTask', [
55
+ '0x' + '11'.repeat(32),
56
+ RAND_ADDR,
57
+ 1_000000n,
58
+ 'photography',
59
+ 'Lagos, NG',
60
+ 3600n,
61
+ ]);
62
+ const decoded = iface.decodeFunctionData('createTask', data);
63
+ expect(decoded[0]).toBe('0x' + '11'.repeat(32));
64
+ expect((decoded[1] as string).toLowerCase()).toBe(RAND_ADDR.toLowerCase());
65
+ expect(decoded[2]).toBe(1_000000n);
66
+ expect(decoded[3]).toBe('photography');
67
+ expect(decoded[4]).toBe('Lagos, NG');
68
+ expect(decoded[5]).toBe(3600n);
69
+ });
70
+
71
+ it('BlindReputation.rate encodes args correctly', () => {
72
+ const iface = new ethers.Interface(BlindReputationAbi);
73
+ const data = iface.encodeFunctionData('rate', [RAND_ADDR, 5, 42n]);
74
+ const decoded = iface.decodeFunctionData('rate', data);
75
+ expect((decoded[0] as string).toLowerCase()).toBe(RAND_ADDR.toLowerCase());
76
+ expect(Number(decoded[1])).toBe(5);
77
+ expect(decoded[2]).toBe(42n);
78
+ });
79
+ });
80
+
81
+ describe('ChainClient wiring', () => {
82
+ it('constructs with a signer and exposes typed sub-clients at the testnet addresses', () => {
83
+ const signer = new PrivateKeySigner(FIXED_PK);
84
+ // No provider attached — reads will fail, but construction should succeed
85
+ // because EthersSigner exposes .provider = null and we fall through to
86
+ // defaultProvider(network) for the RPC runner.
87
+ const c = new ChainClient({ network: networks.testnet, signer });
88
+ expect(c.escrow).toBeInstanceOf(BlindEscrowClient);
89
+ expect(c.registry).toBeInstanceOf(TaskRegistryClient);
90
+ expect(c.reputation).toBeInstanceOf(BlindReputationClient);
91
+ expect(c.escrow.address.toLowerCase()).toBe(networks.testnet.contracts.escrow.toLowerCase());
92
+ expect(c.registry.address.toLowerCase()).toBe(networks.testnet.contracts.registry.toLowerCase());
93
+ expect(c.reputation.address.toLowerCase()).toBe(networks.testnet.contracts.reputation.toLowerCase());
94
+ });
95
+
96
+ it('constructs read-only with only a provider', () => {
97
+ const provider = new ethers.JsonRpcProvider(networks.testnet.rpc[0], {
98
+ chainId: Number(networks.testnet.chainId),
99
+ name: networks.testnet.name,
100
+ });
101
+ const c = new ChainClient({ network: networks.testnet, provider });
102
+ expect(c.escrow).toBeInstanceOf(BlindEscrowClient);
103
+ });
104
+ });
105
+
106
+ describe('wrapChainError', () => {
107
+ it('maps INSUFFICIENT_FUNDS to CHAIN/INSUFFICIENT_BALANCE', () => {
108
+ const err = wrapChainError({ code: 'INSUFFICIENT_FUNDS', message: 'no funds' });
109
+ expect(err.code).toBe('CHAIN/INSUFFICIENT_BALANCE');
110
+ expect(err.retriable).toBe(false);
111
+ });
112
+
113
+ it('maps NONCE_EXPIRED to CHAIN/NONCE_CONFLICT (retriable)', () => {
114
+ const err = wrapChainError({ code: 'NONCE_EXPIRED', message: 'nonce' });
115
+ expect(err.code).toBe('CHAIN/NONCE_CONFLICT');
116
+ expect(err.retriable).toBe(true);
117
+ });
118
+
119
+ it('maps TIMEOUT to CHAIN/TX_TIMED_OUT (retriable)', () => {
120
+ const err = wrapChainError({ code: 'TIMEOUT', message: 't' });
121
+ expect(err.code).toBe('CHAIN/TX_TIMED_OUT');
122
+ expect(err.retriable).toBe(true);
123
+ });
124
+
125
+ it('maps CALL_EXCEPTION to CHAIN/TX_REVERTED and surfaces revert data', () => {
126
+ const err = wrapChainError({
127
+ code: 'CALL_EXCEPTION',
128
+ shortMessage: 'reverted',
129
+ data: '0xdeadbeef',
130
+ });
131
+ expect(err.code).toBe('CHAIN/TX_REVERTED');
132
+ expect(err.context).toEqual({ data: '0xdeadbeef' });
133
+ });
134
+
135
+ it('defaults unknown errors to CHAIN/TX_REVERTED', () => {
136
+ const err = wrapChainError(new Error('boom'));
137
+ expect(err.code).toBe('CHAIN/TX_REVERTED');
138
+ expect(err.retriable).toBe(false);
139
+ });
140
+
141
+ it('prefixes the hint', () => {
142
+ const err = wrapChainError({ message: 'x' }, 'createTask');
143
+ expect(err.message).toMatch(/^createTask:/);
144
+ });
145
+ });