@blindmarket/sdk 0.1.8 → 0.4.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/dist/executor/WorkerRuntime.d.ts +21 -0
- package/dist/executor/WorkerRuntime.js +56 -27
- package/dist/index.d.ts +75 -9
- package/dist/index.js +52 -3
- package/package.json +8 -1
- package/biome.json +0 -19
- package/examples/agent-demo.ts +0 -121
- package/scripts/gen-crypto-fixtures.mjs +0 -72
- package/src/agent/Agent.ts +0 -122
- package/src/agent/index.ts +0 -1
- package/src/api/index.ts +0 -1
- package/src/chain/BlindEscrowClient.ts +0 -196
- package/src/chain/BlindReputationClient.ts +0 -60
- package/src/chain/ChainClient.ts +0 -65
- package/src/chain/IBlindMarketChain.ts +0 -109
- package/src/chain/TaskRegistryClient.ts +0 -91
- package/src/chain/abi/BlindEscrow.json +0 -1113
- package/src/chain/abi/BlindReputation.json +0 -447
- package/src/chain/abi/TaskRegistry.json +0 -586
- package/src/chain/abi/index.ts +0 -5
- package/src/chain/createChainAdapter.ts +0 -40
- package/src/chain/domain-types.ts +0 -92
- package/src/chain/errors.ts +0 -43
- package/src/chain/evm/EVMChainAdapter.ts +0 -212
- package/src/chain/evm/index.ts +0 -1
- package/src/chain/index.ts +0 -12
- package/src/compute/index.ts +0 -1
- package/src/crypto/aes.ts +0 -104
- package/src/crypto/constants.ts +0 -7
- package/src/crypto/ecies.ts +0 -60
- package/src/crypto/envelope.ts +0 -131
- package/src/crypto/hkdf.ts +0 -45
- package/src/crypto/index.ts +0 -6
- package/src/crypto/keys.ts +0 -56
- package/src/errors/BlindMarketError.ts +0 -38
- package/src/errors/codes.ts +0 -67
- package/src/errors/index.ts +0 -3
- package/src/errors/subclasses.ts +0 -59
- package/src/events/EventBus.ts +0 -45
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -13
- package/src/executor/WorkerRuntime.ts +0 -338
- package/src/executor/index.ts +0 -8
- package/src/index.ts +0 -636
- package/src/keystore/InMemoryKeyStore.ts +0 -146
- package/src/keystore/KeyStore.ts +0 -19
- package/src/keystore/index.ts +0 -2
- package/src/network/index.ts +0 -3
- package/src/network/presets.ts +0 -66
- package/src/network/resolve.ts +0 -31
- package/src/network/types.ts +0 -20
- package/src/signer/EthersSigner.ts +0 -82
- package/src/signer/PrivateKeySigner.ts +0 -19
- package/src/signer/Signer.ts +0 -40
- package/src/signer/index.ts +0 -3
- package/src/storage/MemoryStorage.ts +0 -49
- package/src/storage/Storage.ts +0 -15
- package/src/storage/ZgStorage.ts +0 -124
- package/src/storage/index.ts +0 -3
- package/src/testing/index.ts +0 -1
- package/src/tools/adapters.ts +0 -177
- package/src/tools/helpers.ts +0 -199
- package/src/tools/index.ts +0 -59
- package/src/tools/types.ts +0 -47
- package/src/types.ts +0 -297
- package/src/verifier/index.ts +0 -1
- package/src/worker/Worker.ts +0 -98
- package/src/worker/index.ts +0 -1
- package/test/agent.test.ts +0 -193
- package/test/chain.test.ts +0 -145
- package/test/crypto.aes.test.ts +0 -81
- package/test/crypto.compat.test.ts +0 -33
- package/test/crypto.ecies.test.ts +0 -64
- package/test/envelope.test.ts +0 -82
- package/test/errors.test.ts +0 -64
- package/test/events.test.ts +0 -59
- package/test/fixtures/crypto.json +0 -16
- package/test/keystore.test.ts +0 -103
- package/test/network.test.ts +0 -53
- package/test/signer.test.ts +0 -66
- package/test/smoke.test.ts +0 -7
- package/test/storage.test.ts +0 -55
- package/test/worker.test.ts +0 -181
- package/tsconfig.build.json +0 -1
- package/tsconfig.json +0 -14
- package/tsup.config.ts +0 -32
- package/vitest.config.ts +0 -17
package/src/crypto/hkdf.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { CryptoError } from '../errors/index.js';
|
|
2
|
-
import { ECIES_HKDF_INFO } from './constants.js';
|
|
3
|
-
|
|
4
|
-
function getSubtle(): SubtleCrypto {
|
|
5
|
-
const c = (globalThis as { crypto?: Crypto }).crypto;
|
|
6
|
-
if (!c?.subtle) {
|
|
7
|
-
throw new CryptoError('CRYPTO/UNSUPPORTED_CIPHER', 'Web Crypto (crypto.subtle) is not available');
|
|
8
|
-
}
|
|
9
|
-
return c.subtle;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function toArrayBuffer(u: Uint8Array): ArrayBuffer {
|
|
13
|
-
const ab = new ArrayBuffer(u.byteLength);
|
|
14
|
-
new Uint8Array(ab).set(u);
|
|
15
|
-
return ab;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* HKDF-SHA256. Defaults match backend: empty salt, the ECIES domain separation info,
|
|
20
|
-
* 32-byte output length. Pass custom values for other uses.
|
|
21
|
-
*/
|
|
22
|
-
export async function hkdfSha256(
|
|
23
|
-
ikm: Uint8Array,
|
|
24
|
-
length = 32,
|
|
25
|
-
info: Uint8Array = ECIES_HKDF_INFO,
|
|
26
|
-
salt: Uint8Array = new Uint8Array(0),
|
|
27
|
-
): Promise<Uint8Array> {
|
|
28
|
-
try {
|
|
29
|
-
const subtle = getSubtle();
|
|
30
|
-
const baseKey = await subtle.importKey('raw', toArrayBuffer(ikm), 'HKDF', false, ['deriveBits']);
|
|
31
|
-
const derived = await subtle.deriveBits(
|
|
32
|
-
{
|
|
33
|
-
name: 'HKDF',
|
|
34
|
-
hash: 'SHA-256',
|
|
35
|
-
info: toArrayBuffer(info),
|
|
36
|
-
salt: toArrayBuffer(salt),
|
|
37
|
-
},
|
|
38
|
-
baseKey,
|
|
39
|
-
length * 8,
|
|
40
|
-
);
|
|
41
|
-
return new Uint8Array(derived);
|
|
42
|
-
} catch (cause) {
|
|
43
|
-
throw new CryptoError('CRYPTO/INVALID_KEY', 'HKDF derivation failed', { cause });
|
|
44
|
-
}
|
|
45
|
-
}
|
package/src/crypto/index.ts
DELETED
package/src/crypto/keys.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { ethers } from 'ethers';
|
|
2
|
-
import { CryptoError } from '../errors/index.js';
|
|
3
|
-
|
|
4
|
-
export interface KeyPair {
|
|
5
|
-
privateKey: string;
|
|
6
|
-
publicKey: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** Fresh secp256k1 keypair. Hex strings returned un-prefixed (no 0x). */
|
|
10
|
-
export function generateKeyPair(): KeyPair {
|
|
11
|
-
const w = ethers.Wallet.createRandom();
|
|
12
|
-
return {
|
|
13
|
-
privateKey: w.privateKey.slice(2),
|
|
14
|
-
publicKey: w.signingKey.publicKey.slice(2),
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Recover the uncompressed public key (un-prefixed hex) from a private key. */
|
|
19
|
-
export function derivePublicKey(privateKey: string): string {
|
|
20
|
-
const sk = new ethers.SigningKey(`0x${strip(privateKey)}`);
|
|
21
|
-
return sk.publicKey.slice(2);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function strip(hex: string): string {
|
|
25
|
-
return hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function assertUncompressedPubKey(pub: string): void {
|
|
29
|
-
const s = strip(pub);
|
|
30
|
-
if (s.length !== 130 || !/^04/i.test(s)) {
|
|
31
|
-
throw new CryptoError(
|
|
32
|
-
'CRYPTO/INVALID_KEY',
|
|
33
|
-
'expected uncompressed secp256k1 pubkey (65 bytes hex, starts with 04)',
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function hexToBytes(hex: string): Uint8Array {
|
|
39
|
-
const s = strip(hex);
|
|
40
|
-
if (s.length % 2 !== 0) {
|
|
41
|
-
throw new CryptoError('CRYPTO/INVALID_KEY', 'hex string has odd length');
|
|
42
|
-
}
|
|
43
|
-
const out = new Uint8Array(s.length / 2);
|
|
44
|
-
for (let i = 0; i < out.length; i++) {
|
|
45
|
-
const byte = Number.parseInt(s.substring(i * 2, i * 2 + 2), 16);
|
|
46
|
-
if (Number.isNaN(byte)) {
|
|
47
|
-
throw new CryptoError('CRYPTO/INVALID_KEY', `invalid hex byte at position ${i * 2}`);
|
|
48
|
-
}
|
|
49
|
-
out[i] = byte;
|
|
50
|
-
}
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function bytesToHex(u: Uint8Array): string {
|
|
55
|
-
return Array.from(u, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
56
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { ErrorCode } from './codes.js';
|
|
2
|
-
|
|
3
|
-
export interface ErrorOptions {
|
|
4
|
-
retriable?: boolean;
|
|
5
|
-
context?: Record<string, unknown>;
|
|
6
|
-
cause?: unknown;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export abstract class BlindMarketError extends Error {
|
|
10
|
-
readonly code: ErrorCode;
|
|
11
|
-
readonly retriable: boolean;
|
|
12
|
-
readonly context?: Record<string, unknown>;
|
|
13
|
-
override readonly cause?: unknown;
|
|
14
|
-
|
|
15
|
-
constructor(code: ErrorCode, message: string, opts: ErrorOptions = {}) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.code = code;
|
|
18
|
-
this.retriable = opts.retriable ?? false;
|
|
19
|
-
if (opts.context !== undefined) this.context = opts.context;
|
|
20
|
-
if (opts.cause !== undefined) this.cause = opts.cause;
|
|
21
|
-
Object.defineProperty(this, 'name', {
|
|
22
|
-
value: this.constructor.name,
|
|
23
|
-
enumerable: false,
|
|
24
|
-
configurable: true,
|
|
25
|
-
writable: true,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
toJSON(): Record<string, unknown> {
|
|
30
|
-
return {
|
|
31
|
-
name: this.name,
|
|
32
|
-
code: this.code,
|
|
33
|
-
message: this.message,
|
|
34
|
-
retriable: this.retriable,
|
|
35
|
-
...(this.context ? { context: this.context } : {}),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
}
|
package/src/errors/codes.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
export const ErrorCodes = {
|
|
2
|
-
Config: ['CONFIG/INVALID_NETWORK', 'CONFIG/MISSING_SIGNER', 'CONFIG/BAD_CONFIG'] as const,
|
|
3
|
-
Crypto: [
|
|
4
|
-
'CRYPTO/INVALID_KEY',
|
|
5
|
-
'CRYPTO/DECRYPT_FAILED',
|
|
6
|
-
'CRYPTO/INTEGRITY_CHECK',
|
|
7
|
-
'CRYPTO/UNSUPPORTED_CIPHER',
|
|
8
|
-
] as const,
|
|
9
|
-
Storage: [
|
|
10
|
-
'STORAGE/UPLOAD_FAILED',
|
|
11
|
-
'STORAGE/DOWNLOAD_FAILED',
|
|
12
|
-
'STORAGE/MERKLE_MISMATCH',
|
|
13
|
-
'STORAGE/NOT_FOUND',
|
|
14
|
-
'STORAGE/INDEXER_UNREACHABLE',
|
|
15
|
-
] as const,
|
|
16
|
-
Chain: [
|
|
17
|
-
'CHAIN/TX_REVERTED',
|
|
18
|
-
'CHAIN/TX_TIMED_OUT',
|
|
19
|
-
'CHAIN/NONCE_CONFLICT',
|
|
20
|
-
'CHAIN/INSUFFICIENT_BALANCE',
|
|
21
|
-
'CHAIN/NETWORK_MISMATCH',
|
|
22
|
-
'CHAIN/CONTRACT_NOT_DEPLOYED',
|
|
23
|
-
] as const,
|
|
24
|
-
Compute: [
|
|
25
|
-
'COMPUTE/BROKER_UNREACHABLE',
|
|
26
|
-
'COMPUTE/NO_PROVIDERS',
|
|
27
|
-
'COMPUTE/LEDGER_INSUFFICIENT',
|
|
28
|
-
'COMPUTE/TEE_ATTESTATION_INVALID',
|
|
29
|
-
'COMPUTE/VERIFICATION_TIMEOUT',
|
|
30
|
-
] as const,
|
|
31
|
-
Api: [
|
|
32
|
-
'API/AUTH_REQUIRED',
|
|
33
|
-
'API/AUTH_EXPIRED',
|
|
34
|
-
'API/FORBIDDEN',
|
|
35
|
-
'API/RATE_LIMITED',
|
|
36
|
-
'API/VALIDATION',
|
|
37
|
-
'API/SERVER',
|
|
38
|
-
'API/NOT_FOUND',
|
|
39
|
-
] as const,
|
|
40
|
-
Lifecycle: [
|
|
41
|
-
'LIFECYCLE/ILLEGAL_TRANSITION',
|
|
42
|
-
'LIFECYCLE/TASK_NOT_FOUND',
|
|
43
|
-
'LIFECYCLE/NOT_ASSIGNED',
|
|
44
|
-
'LIFECYCLE/ALREADY_SUBMITTED',
|
|
45
|
-
'LIFECYCLE/CLAIM_NOT_READY',
|
|
46
|
-
] as const,
|
|
47
|
-
Timeout: ['TIMEOUT'] as const,
|
|
48
|
-
} as const;
|
|
49
|
-
|
|
50
|
-
export type ConfigCode = (typeof ErrorCodes.Config)[number];
|
|
51
|
-
export type CryptoCode = (typeof ErrorCodes.Crypto)[number];
|
|
52
|
-
export type StorageCode = (typeof ErrorCodes.Storage)[number];
|
|
53
|
-
export type ChainCode = (typeof ErrorCodes.Chain)[number];
|
|
54
|
-
export type ComputeCode = (typeof ErrorCodes.Compute)[number];
|
|
55
|
-
export type ApiCode = (typeof ErrorCodes.Api)[number];
|
|
56
|
-
export type LifecycleCode = (typeof ErrorCodes.Lifecycle)[number];
|
|
57
|
-
export type TimeoutCode = (typeof ErrorCodes.Timeout)[number];
|
|
58
|
-
|
|
59
|
-
export type ErrorCode =
|
|
60
|
-
| ConfigCode
|
|
61
|
-
| CryptoCode
|
|
62
|
-
| StorageCode
|
|
63
|
-
| ChainCode
|
|
64
|
-
| ComputeCode
|
|
65
|
-
| ApiCode
|
|
66
|
-
| LifecycleCode
|
|
67
|
-
| TimeoutCode;
|
package/src/errors/index.ts
DELETED
package/src/errors/subclasses.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { BlindMarketError, type ErrorOptions } from './BlindMarketError.js';
|
|
2
|
-
import type {
|
|
3
|
-
ApiCode,
|
|
4
|
-
ChainCode,
|
|
5
|
-
ComputeCode,
|
|
6
|
-
ConfigCode,
|
|
7
|
-
CryptoCode,
|
|
8
|
-
LifecycleCode,
|
|
9
|
-
StorageCode,
|
|
10
|
-
TimeoutCode,
|
|
11
|
-
} from './codes.js';
|
|
12
|
-
|
|
13
|
-
export class ConfigError extends BlindMarketError {
|
|
14
|
-
constructor(code: ConfigCode, msg: string, opts?: ErrorOptions) {
|
|
15
|
-
super(code, msg, opts);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class CryptoError extends BlindMarketError {
|
|
20
|
-
constructor(code: CryptoCode, msg: string, opts?: ErrorOptions) {
|
|
21
|
-
super(code, msg, opts);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class StorageError extends BlindMarketError {
|
|
26
|
-
constructor(code: StorageCode, msg: string, opts?: ErrorOptions) {
|
|
27
|
-
super(code, msg, opts);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class ChainError extends BlindMarketError {
|
|
32
|
-
constructor(code: ChainCode, msg: string, opts?: ErrorOptions) {
|
|
33
|
-
super(code, msg, opts);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export class ComputeError extends BlindMarketError {
|
|
38
|
-
constructor(code: ComputeCode, msg: string, opts?: ErrorOptions) {
|
|
39
|
-
super(code, msg, opts);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export class ApiError extends BlindMarketError {
|
|
44
|
-
constructor(code: ApiCode, msg: string, opts?: ErrorOptions) {
|
|
45
|
-
super(code, msg, opts);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export class LifecycleError extends BlindMarketError {
|
|
50
|
-
constructor(code: LifecycleCode, msg: string, opts?: ErrorOptions) {
|
|
51
|
-
super(code, msg, opts);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export class TimeoutError extends BlindMarketError {
|
|
56
|
-
constructor(code: TimeoutCode, msg: string, opts?: ErrorOptions) {
|
|
57
|
-
super(code, msg, opts);
|
|
58
|
-
}
|
|
59
|
-
}
|
package/src/events/EventBus.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
type Handler<T> = (payload: T) => void;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Typed pub/sub. Listener exceptions are swallowed so one bad subscriber
|
|
5
|
-
* cannot block others — telemetry plugins can observe failures via onError.
|
|
6
|
-
*/
|
|
7
|
-
export class EventBus<E> {
|
|
8
|
-
private handlers: { [K in keyof E]?: Set<Handler<E[K]>> } = {};
|
|
9
|
-
|
|
10
|
-
on<K extends keyof E>(event: K, fn: Handler<E[K]>): () => void {
|
|
11
|
-
let set = this.handlers[event];
|
|
12
|
-
if (!set) {
|
|
13
|
-
set = new Set<Handler<E[K]>>();
|
|
14
|
-
this.handlers[event] = set;
|
|
15
|
-
}
|
|
16
|
-
set.add(fn);
|
|
17
|
-
return () => {
|
|
18
|
-
this.off(event, fn);
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
once<K extends keyof E>(event: K, fn: Handler<E[K]>): () => void {
|
|
23
|
-
const wrapped: Handler<E[K]> = (p) => {
|
|
24
|
-
this.off(event, wrapped);
|
|
25
|
-
fn(p);
|
|
26
|
-
};
|
|
27
|
-
return this.on(event, wrapped);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
off<K extends keyof E>(event: K, fn: Handler<E[K]>): void {
|
|
31
|
-
this.handlers[event]?.delete(fn);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
emit<K extends keyof E>(event: K, payload: E[K]): void {
|
|
35
|
-
const hs = this.handlers[event];
|
|
36
|
-
if (!hs) return;
|
|
37
|
-
for (const h of hs) {
|
|
38
|
-
try {
|
|
39
|
-
h(payload);
|
|
40
|
-
} catch {
|
|
41
|
-
// Isolated per-listener — prevents one bad subscriber from blocking others.
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
package/src/events/index.ts
DELETED
package/src/events/types.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Address, Hex, RootHash, TaskId } from '../types.js';
|
|
2
|
-
|
|
3
|
-
export interface SdkEvents {
|
|
4
|
-
'task.created': { taskId: TaskId; txHash: Hex; rootHash: RootHash };
|
|
5
|
-
'task.assigned': { taskId: TaskId; worker: Address; txHash: Hex };
|
|
6
|
-
'task.cancelled': { taskId: TaskId; txHash: Hex };
|
|
7
|
-
'evidence.submitted': { taskId: TaskId; rootHash: RootHash; txHash: Hex };
|
|
8
|
-
'verification.done': { taskId: TaskId; passed: boolean; confidence: number };
|
|
9
|
-
'payment.claimed': { taskId: TaskId; txHash: Hex; amount: bigint };
|
|
10
|
-
'tx.sent': { hash: Hex; from: Address; to: Address };
|
|
11
|
-
'tx.confirmed': { hash: Hex; blockNumber: number };
|
|
12
|
-
'tx.failed': { hash: Hex; reason: string };
|
|
13
|
-
}
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import { BlindMarket } from '../index.js';
|
|
2
|
-
import { eciesDecrypt, aesDecrypt } from '../crypto/index.js';
|
|
3
|
-
import type {
|
|
4
|
-
A2ATaskState, AgentCapability, ExecutorProfile, Message,
|
|
5
|
-
} from '../types.js';
|
|
6
|
-
|
|
7
|
-
// ── Config ──────────────────────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
export interface WorkerRuntimeConfig {
|
|
10
|
-
apiKey: string;
|
|
11
|
-
apiBase?: string;
|
|
12
|
-
displayName: string;
|
|
13
|
-
capabilities: AgentCapability[];
|
|
14
|
-
executeTask: ExecuteTaskHandler;
|
|
15
|
-
existingPrivateKey?: string;
|
|
16
|
-
existingAddress?: string;
|
|
17
|
-
existingPublicKey?: string;
|
|
18
|
-
minReward?: string;
|
|
19
|
-
preferredCapabilities?: AgentCapability[];
|
|
20
|
-
browseIntervalMs?: number;
|
|
21
|
-
watchIntervalMs?: number;
|
|
22
|
-
maxConcurrentTasks?: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ── Types ───────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export type ExecuteTaskHandler = (ctx: TaskContext) => Promise<Record<string, unknown>>;
|
|
28
|
-
|
|
29
|
-
export interface TaskContext {
|
|
30
|
-
taskId: string;
|
|
31
|
-
task: A2ATaskState;
|
|
32
|
-
instructions: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface TaskExecutionInfo {
|
|
36
|
-
taskId: string;
|
|
37
|
-
status: 'bidding' | 'assigned' | 'working' | 'submitted' | 'completed' | 'failed';
|
|
38
|
-
task?: A2ATaskState;
|
|
39
|
-
error?: string;
|
|
40
|
-
startedAt: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type WorkerRuntimeEvent =
|
|
44
|
-
| { type: 'started' }
|
|
45
|
-
| { type: 'stopped' }
|
|
46
|
-
| { type: 'paused' }
|
|
47
|
-
| { type: 'resumed' }
|
|
48
|
-
| { type: 'registered'; profile: ExecutorProfile }
|
|
49
|
-
| { type: 'task_found'; taskId: string; task: A2ATaskState }
|
|
50
|
-
| { type: 'task_bidded'; taskId: string }
|
|
51
|
-
| { type: 'task_assigned'; taskId: string }
|
|
52
|
-
| { type: 'task_accepted'; taskId: string }
|
|
53
|
-
| { type: 'task_working'; taskId: string }
|
|
54
|
-
| { type: 'task_executed'; taskId: string }
|
|
55
|
-
| { type: 'task_submitted'; taskId: string; result: Record<string, unknown> }
|
|
56
|
-
| { type: 'task_failed'; taskId: string; error: string }
|
|
57
|
-
| { type: 'message_received'; message: Message; count: number }
|
|
58
|
-
| { type: 'browse_done'; found: number }
|
|
59
|
-
| { type: 'error'; error: string };
|
|
60
|
-
|
|
61
|
-
// ── Defaults ────────────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
const DEFAULTS = {
|
|
64
|
-
browseIntervalMs: 15_000,
|
|
65
|
-
watchIntervalMs: 5_000,
|
|
66
|
-
maxConcurrentTasks: 3,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// ── WorkerRuntime ───────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
export class WorkerRuntime {
|
|
72
|
-
private bb: BlindMarket;
|
|
73
|
-
private config: WorkerRuntimeConfig & typeof DEFAULTS;
|
|
74
|
-
private wallet?: { address: string; privateKey: string; publicKey: string };
|
|
75
|
-
private profile?: ExecutorProfile;
|
|
76
|
-
private running = false;
|
|
77
|
-
private paused = false;
|
|
78
|
-
private browseTimer?: ReturnType<typeof setInterval>;
|
|
79
|
-
private watchTimers = new Map<string, ReturnType<typeof setInterval>>();
|
|
80
|
-
private executions = new Map<string, TaskExecutionInfo>();
|
|
81
|
-
private listeners = new Set<(event: WorkerRuntimeEvent) => void>();
|
|
82
|
-
|
|
83
|
-
constructor(config: WorkerRuntimeConfig) {
|
|
84
|
-
this.config = { ...DEFAULTS, ...config };
|
|
85
|
-
this.bb = new BlindMarket({ apiKey: config.apiKey, apiBase: config.apiBase });
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ── Status ──────────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
get isRunning(): boolean {
|
|
91
|
-
return this.running;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
get isPaused(): boolean {
|
|
95
|
-
return this.paused;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
get activeExecutions(): TaskExecutionInfo[] {
|
|
99
|
-
return [...this.executions.values()];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Get own executor profile (available after start). */
|
|
103
|
-
get executorProfile(): ExecutorProfile | undefined {
|
|
104
|
-
return this.profile;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Get own wallet info (available after start). */
|
|
108
|
-
get executorWallet(): { address: string; publicKey: string } | undefined {
|
|
109
|
-
return this.wallet;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ── Events ──────────────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
on(listener: (event: WorkerRuntimeEvent) => void): () => void {
|
|
115
|
-
this.listeners.add(listener);
|
|
116
|
-
return () => this.listeners.delete(listener);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private emit(event: WorkerRuntimeEvent): void {
|
|
120
|
-
for (const l of this.listeners) l(event);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
async start(): Promise<ExecutorProfile> {
|
|
126
|
-
if (this.running) return this.profile!;
|
|
127
|
-
|
|
128
|
-
// 1. Register or restore executor
|
|
129
|
-
if (this.config.existingPrivateKey && this.config.existingAddress && this.config.existingPublicKey) {
|
|
130
|
-
this.wallet = {
|
|
131
|
-
address: this.config.existingAddress,
|
|
132
|
-
privateKey: this.config.existingPrivateKey,
|
|
133
|
-
publicKey: this.config.existingPublicKey,
|
|
134
|
-
};
|
|
135
|
-
// Fetch existing profile — backend identifies by API key
|
|
136
|
-
const result = await this.bb.getExecutorProfile();
|
|
137
|
-
this.profile = result.agent;
|
|
138
|
-
} else {
|
|
139
|
-
const result = await this.bb.createAgent({
|
|
140
|
-
displayName: this.config.displayName,
|
|
141
|
-
capabilities: this.config.capabilities,
|
|
142
|
-
minReward: this.config.minReward,
|
|
143
|
-
preferredCapabilities: this.config.preferredCapabilities,
|
|
144
|
-
});
|
|
145
|
-
this.wallet = {
|
|
146
|
-
address: result.wallet.address,
|
|
147
|
-
privateKey: result.wallet.privateKey,
|
|
148
|
-
publicKey: result.wallet.publicKey,
|
|
149
|
-
};
|
|
150
|
-
this.profile = result.executor;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
this.emit({ type: 'registered', profile: this.profile });
|
|
154
|
-
|
|
155
|
-
// 2. Start the browse loop
|
|
156
|
-
this.running = true;
|
|
157
|
-
this.startBrowseLoop();
|
|
158
|
-
this.emit({ type: 'started' });
|
|
159
|
-
|
|
160
|
-
return this.profile;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
stop(): void {
|
|
164
|
-
this.running = false;
|
|
165
|
-
if (this.browseTimer) {
|
|
166
|
-
clearInterval(this.browseTimer);
|
|
167
|
-
this.browseTimer = undefined;
|
|
168
|
-
}
|
|
169
|
-
for (const t of this.watchTimers.values()) clearInterval(t);
|
|
170
|
-
this.watchTimers.clear();
|
|
171
|
-
this.emit({ type: 'stopped' });
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
pause(): void {
|
|
175
|
-
if (!this.running || this.paused) return;
|
|
176
|
-
this.paused = true;
|
|
177
|
-
this.emit({ type: 'paused' });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
resume(): void {
|
|
181
|
-
if (!this.running || !this.paused) return;
|
|
182
|
-
this.paused = false;
|
|
183
|
-
this.emit({ type: 'resumed' });
|
|
184
|
-
// Immediately browse instead of waiting for next interval
|
|
185
|
-
this.browse();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── Browse loop ─────────────────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
private startBrowseLoop(): void {
|
|
191
|
-
const ms = this.config.browseIntervalMs;
|
|
192
|
-
this.browseTimer = setInterval(() => this.browse(), ms);
|
|
193
|
-
this.browse();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
private async browse(): Promise<void> {
|
|
197
|
-
if (this.paused) return;
|
|
198
|
-
try {
|
|
199
|
-
const active = this.activeExecutions;
|
|
200
|
-
const inFlight = active.filter(e => e.status === 'bidding' || e.status === 'working').length;
|
|
201
|
-
if (inFlight >= this.config.maxConcurrentTasks) return;
|
|
202
|
-
|
|
203
|
-
const result = await this.bb.browseA2ATasks({
|
|
204
|
-
capabilities: this.config.capabilities,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
this.emit({ type: 'browse_done', found: result.tasks.length });
|
|
208
|
-
|
|
209
|
-
for (const task of result.tasks) {
|
|
210
|
-
if (this.executions.has(task.taskId)) continue;
|
|
211
|
-
if (task.status !== 'open' && task.status !== 'bidding') continue;
|
|
212
|
-
if (task.executorAddress && task.executorAddress !== this.wallet?.address) continue;
|
|
213
|
-
|
|
214
|
-
this.executions.set(task.taskId, {
|
|
215
|
-
taskId: task.taskId,
|
|
216
|
-
status: 'bidding',
|
|
217
|
-
task,
|
|
218
|
-
startedAt: Date.now(),
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
this.emit({ type: 'task_found', taskId: task.taskId, task });
|
|
222
|
-
|
|
223
|
-
await this.bb.bidOnTask(task.taskId);
|
|
224
|
-
this.emit({ type: 'task_bidded', taskId: task.taskId });
|
|
225
|
-
|
|
226
|
-
this.watchForAssignment(task.taskId);
|
|
227
|
-
}
|
|
228
|
-
} catch (err) {
|
|
229
|
-
this.emit({ type: 'error', error: `Browse failed: ${err}` });
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ── Assignment watching ─────────────────────────────────────────────────
|
|
234
|
-
|
|
235
|
-
private watchForAssignment(taskId: string): void {
|
|
236
|
-
if (!this.running) return;
|
|
237
|
-
const ms = this.config.watchIntervalMs;
|
|
238
|
-
const timer = setInterval(async () => {
|
|
239
|
-
if (!this.running || this.paused) return;
|
|
240
|
-
try {
|
|
241
|
-
const detail = await this.bb.getTask(taskId);
|
|
242
|
-
const a2a = detail.a2aState;
|
|
243
|
-
if (!a2a) return;
|
|
244
|
-
|
|
245
|
-
if (a2a.status === 'assigned') {
|
|
246
|
-
clearInterval(timer);
|
|
247
|
-
this.watchTimers.delete(taskId);
|
|
248
|
-
this.emit({ type: 'task_assigned', taskId });
|
|
249
|
-
await this.executeTask(taskId, a2a);
|
|
250
|
-
}
|
|
251
|
-
} catch {
|
|
252
|
-
// Retry next tick
|
|
253
|
-
}
|
|
254
|
-
}, ms);
|
|
255
|
-
this.watchTimers.set(taskId, timer);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── Task execution ──────────────────────────────────────────────────────
|
|
259
|
-
|
|
260
|
-
private async executeTask(taskId: string, a2a: A2ATaskState): Promise<void> {
|
|
261
|
-
const exec = this.executions.get(taskId);
|
|
262
|
-
if (!exec) return;
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
exec.status = 'assigned';
|
|
266
|
-
|
|
267
|
-
// Accept task — get the ECIES-wrapped AES key
|
|
268
|
-
const acceptResult = await this.bb.acceptTask(taskId);
|
|
269
|
-
exec.task = acceptResult.task;
|
|
270
|
-
this.emit({ type: 'task_accepted', taskId });
|
|
271
|
-
|
|
272
|
-
// Get full task detail for the storage root hash
|
|
273
|
-
const detail = await this.bb.getTask(taskId);
|
|
274
|
-
const taskHash = detail.taskHash;
|
|
275
|
-
|
|
276
|
-
// Decrypt the wrapped key with the worker's private key
|
|
277
|
-
let instructions = '';
|
|
278
|
-
if (acceptResult.wrappedKey && taskHash) {
|
|
279
|
-
const wrappedBytes = this.decodeWrappedKey(acceptResult.wrappedKey);
|
|
280
|
-
const aesKey = await eciesDecrypt(wrappedBytes, this.wallet!.privateKey);
|
|
281
|
-
|
|
282
|
-
// Download ciphertext from storage via REST API
|
|
283
|
-
const storageResult = await this.bb.downloadBlob(taskHash);
|
|
284
|
-
const ciphertext = this.hexToBytes(storageResult.data);
|
|
285
|
-
|
|
286
|
-
// AES decrypt to get plaintext instructions
|
|
287
|
-
const plaintext = await aesDecrypt(ciphertext, aesKey);
|
|
288
|
-
instructions = new TextDecoder().decode(plaintext);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
exec.status = 'working';
|
|
292
|
-
this.emit({ type: 'task_working', taskId });
|
|
293
|
-
|
|
294
|
-
// Call the user-provided execution handler with decrypted instructions
|
|
295
|
-
const result = await this.config.executeTask({
|
|
296
|
-
taskId,
|
|
297
|
-
task: acceptResult.task,
|
|
298
|
-
instructions,
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
exec.status = 'submitted';
|
|
302
|
-
this.emit({ type: 'task_executed', taskId });
|
|
303
|
-
|
|
304
|
-
// Submit result
|
|
305
|
-
const submitResult = await this.bb.submitResult(taskId, result);
|
|
306
|
-
|
|
307
|
-
exec.status = 'completed';
|
|
308
|
-
this.emit({ type: 'task_submitted', taskId, result: submitResult });
|
|
309
|
-
} catch (err) {
|
|
310
|
-
exec.status = 'failed';
|
|
311
|
-
exec.error = String(err);
|
|
312
|
-
this.emit({ type: 'task_failed', taskId, error: String(err) });
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
317
|
-
|
|
318
|
-
private decodeWrappedKey(wrappedKey: Record<string, string>): Uint8Array {
|
|
319
|
-
// Backend returns wrapped key as hex or base64 in a record
|
|
320
|
-
const hex = Object.values(wrappedKey)[0];
|
|
321
|
-
if (!hex) throw new Error('Empty wrapped key');
|
|
322
|
-
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
323
|
-
const bytes = new Uint8Array(clean.length / 2);
|
|
324
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
325
|
-
bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
|
|
326
|
-
}
|
|
327
|
-
return bytes;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private hexToBytes(hex: string): Uint8Array {
|
|
331
|
-
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
332
|
-
const bytes = new Uint8Array(clean.length / 2);
|
|
333
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
334
|
-
bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
|
|
335
|
-
}
|
|
336
|
-
return bytes;
|
|
337
|
-
}
|
|
338
|
-
}
|