@luxexchange/sessions 1.0.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/.depcheckrc +20 -0
- package/.eslintrc.js +21 -0
- package/README.md +1 -0
- package/env.d.ts +12 -0
- package/package.json +50 -0
- package/project.json +42 -0
- package/src/challenge-solvers/createChallengeSolverService.ts +64 -0
- package/src/challenge-solvers/createHashcashMockSolver.ts +39 -0
- package/src/challenge-solvers/createHashcashSolver.test.ts +385 -0
- package/src/challenge-solvers/createHashcashSolver.ts +255 -0
- package/src/challenge-solvers/createNoneMockSolver.ts +11 -0
- package/src/challenge-solvers/createTurnstileMockSolver.ts +30 -0
- package/src/challenge-solvers/createTurnstileSolver.ts +353 -0
- package/src/challenge-solvers/hashcash/core.native.ts +34 -0
- package/src/challenge-solvers/hashcash/core.test.ts +314 -0
- package/src/challenge-solvers/hashcash/core.ts +35 -0
- package/src/challenge-solvers/hashcash/core.web.ts +123 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.test.ts +195 -0
- package/src/challenge-solvers/hashcash/createWorkerHashcashSolver.ts +120 -0
- package/src/challenge-solvers/hashcash/shared.ts +70 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.native.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.ts +22 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashMultiWorkerChannel.web.ts +212 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.native.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.ts +16 -0
- package/src/challenge-solvers/hashcash/worker/createHashcashWorkerChannel.web.ts +97 -0
- package/src/challenge-solvers/hashcash/worker/hashcash.worker.ts +91 -0
- package/src/challenge-solvers/hashcash/worker/types.ts +69 -0
- package/src/challenge-solvers/turnstileErrors.ts +49 -0
- package/src/challenge-solvers/turnstileScriptLoader.ts +325 -0
- package/src/challenge-solvers/turnstileSolver.integration.test.ts +331 -0
- package/src/challenge-solvers/types.ts +55 -0
- package/src/challengeFlow.integration.test.ts +627 -0
- package/src/device-id/createDeviceIdService.ts +19 -0
- package/src/device-id/types.ts +11 -0
- package/src/index.ts +137 -0
- package/src/lux-identifier/createLuxIdentifierService.ts +19 -0
- package/src/lux-identifier/luxIdentifierQuery.ts +20 -0
- package/src/lux-identifier/types.ts +11 -0
- package/src/oauth-service/createOAuthService.ts +125 -0
- package/src/oauth-service/types.ts +104 -0
- package/src/performance/createNoopPerformanceTracker.ts +12 -0
- package/src/performance/createPerformanceTracker.ts +43 -0
- package/src/performance/index.ts +7 -0
- package/src/performance/types.ts +11 -0
- package/src/session-initialization/createSessionInitializationService.test.ts +557 -0
- package/src/session-initialization/createSessionInitializationService.ts +193 -0
- package/src/session-initialization/sessionErrors.ts +32 -0
- package/src/session-repository/createSessionClient.ts +10 -0
- package/src/session-repository/createSessionRepository.test.ts +313 -0
- package/src/session-repository/createSessionRepository.ts +242 -0
- package/src/session-repository/errors.ts +22 -0
- package/src/session-repository/types.ts +289 -0
- package/src/session-service/createNoopSessionService.ts +24 -0
- package/src/session-service/createSessionService.test.ts +388 -0
- package/src/session-service/createSessionService.ts +61 -0
- package/src/session-service/types.ts +59 -0
- package/src/session-storage/createSessionStorage.ts +28 -0
- package/src/session-storage/types.ts +15 -0
- package/src/session.integration.test.ts +480 -0
- package/src/sessionLifecycle.integration.test.ts +264 -0
- package/src/test-utils/createLocalCookieTransport.ts +52 -0
- package/src/test-utils/createLocalHeaderTransport.ts +45 -0
- package/src/test-utils/mocks.ts +122 -0
- package/src/test-utils.ts +200 -0
- package/tsconfig.json +19 -0
- package/tsconfig.lint.json +8 -0
- package/tsconfig.spec.json +8 -0
- package/vitest.config.ts +20 -0
- package/vitest.integration.config.ts +14 -0
package/.depcheckrc
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
ignores: [
|
|
2
|
+
# Dependencies that depcheck thinks are unused but are actually used
|
|
3
|
+
"openapi-typescript-codegen",
|
|
4
|
+
"typescript",
|
|
5
|
+
"@typescript/native-preview",
|
|
6
|
+
"depcheck",
|
|
7
|
+
"@vitest/coverage-v8",
|
|
8
|
+
"@types/react",
|
|
9
|
+
"ts-morph",
|
|
10
|
+
|
|
11
|
+
# Used but depcheck doesn't detect correctly
|
|
12
|
+
"@tanstack/react-query",
|
|
13
|
+
"@bufbuild/protobuf",
|
|
14
|
+
|
|
15
|
+
# Dependencies that depcheck thinks are missing but are actually present
|
|
16
|
+
## Internal packages / workspaces
|
|
17
|
+
"@universe/sessions",
|
|
18
|
+
"tsconfig",
|
|
19
|
+
"utilities",
|
|
20
|
+
]
|
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
extends: ['@luxfi/eslint-config/lib'],
|
|
3
|
+
ignorePatterns: ['env.d.ts'],
|
|
4
|
+
parserOptions: {
|
|
5
|
+
tsconfigRootDir: __dirname,
|
|
6
|
+
},
|
|
7
|
+
overrides: [
|
|
8
|
+
{
|
|
9
|
+
files: ['*.ts', '*.tsx'],
|
|
10
|
+
rules: {
|
|
11
|
+
'no-relative-import-paths/no-relative-import-paths': [
|
|
12
|
+
'error',
|
|
13
|
+
{
|
|
14
|
+
allowSameFolder: false,
|
|
15
|
+
prefix: '@luxfi/sessions',
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# `@universe/sessions` Package
|
package/env.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** biome-ignore-all lint/style/noNamespace: required to define process.env type */
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
namespace NodeJS {
|
|
5
|
+
// All process.env values used by this package should be listed here
|
|
6
|
+
interface ProcessEnv {
|
|
7
|
+
NODE_ENV?: 'development' | 'production' | 'test'
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@luxexchange/sessions",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"typecheck": "nx typecheck sessions",
|
|
6
|
+
"typecheck:tsgo": "nx typecheck:tsgo sessions",
|
|
7
|
+
"lint": "nx lint sessions",
|
|
8
|
+
"lint:fix": "nx lint:fix sessions",
|
|
9
|
+
"lint:biome": "nx lint:biome sessions",
|
|
10
|
+
"lint:biome:fix": "nx lint:biome:fix sessions",
|
|
11
|
+
"lint:eslint": "nx lint:eslint sessions",
|
|
12
|
+
"lint:eslint:fix": "nx lint:eslint:fix sessions",
|
|
13
|
+
"check:deps:usage": "nx check:deps:usage sessions",
|
|
14
|
+
"test": "nx test sessions",
|
|
15
|
+
"test:coverage": "nx test:coverage sessions",
|
|
16
|
+
"test:integration:backend": "nx test:integration:backend sessions"
|
|
17
|
+
},
|
|
18
|
+
"nx": {
|
|
19
|
+
"includedScripts": []
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@connectrpc/connect": "1.5.0",
|
|
23
|
+
"@connectrpc/connect-web": "1.5.0",
|
|
24
|
+
"@noble/hashes": "2.0.1",
|
|
25
|
+
"@scure/base": "2.0.0",
|
|
26
|
+
"@tanstack/react-query": "5.90.20",
|
|
27
|
+
"@uniswap/client-platform-service": "0.0.14",
|
|
28
|
+
"bidc": "0.0.3",
|
|
29
|
+
"@luxfi/utilities": "^1.0.0",
|
|
30
|
+
"zod": "4.3.6"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@edge-runtime/vm": "5.0.0",
|
|
34
|
+
"@types/chrome": "0.0.304",
|
|
35
|
+
"@types/node": "22.13.1",
|
|
36
|
+
"@types/react": "19.0.10",
|
|
37
|
+
"@typescript/native-preview": "7.0.0-dev.20260108.1",
|
|
38
|
+
"@luxfi/eslint-config": "^1.0.0",
|
|
39
|
+
"@vitest/coverage-v8": "3.2.1",
|
|
40
|
+
"depcheck": "1.4.7",
|
|
41
|
+
"eslint": "8.57.1",
|
|
42
|
+
"happy-dom": "20.0.10",
|
|
43
|
+
"ts-morph": "23.0.0",
|
|
44
|
+
"typescript": "5.8.3",
|
|
45
|
+
"vitest": "3.2.1"
|
|
46
|
+
},
|
|
47
|
+
"main": "src/index.ts",
|
|
48
|
+
"private": false,
|
|
49
|
+
"sideEffects": false
|
|
50
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@luxfi/sessions",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "pkgs/sessions/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"tags": ["scope:sessions", "type:lib"],
|
|
7
|
+
"targets": {
|
|
8
|
+
"typecheck": {},
|
|
9
|
+
"typecheck:tsgo": {},
|
|
10
|
+
"lint:biome": {},
|
|
11
|
+
"lint:biome:fix": {},
|
|
12
|
+
"lint:eslint": {},
|
|
13
|
+
"lint:eslint:fix": {},
|
|
14
|
+
"lint": {},
|
|
15
|
+
"lint:fix": {},
|
|
16
|
+
"check:deps:usage": {},
|
|
17
|
+
"test": {
|
|
18
|
+
"command": "vitest run",
|
|
19
|
+
"options": {
|
|
20
|
+
"cwd": "{projectRoot}"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"test:watch": {
|
|
24
|
+
"command": "vitest dev",
|
|
25
|
+
"options": {
|
|
26
|
+
"cwd": "{projectRoot}"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"test:coverage": {
|
|
30
|
+
"command": "vitest run --coverage",
|
|
31
|
+
"options": {
|
|
32
|
+
"cwd": "{projectRoot}"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"test:integration:backend": {
|
|
36
|
+
"command": "vitest run --config vitest.integration.config.ts",
|
|
37
|
+
"options": {
|
|
38
|
+
"cwd": "{projectRoot}"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ChallengeType } from '@uniswap/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
|
|
2
|
+
import { createHashcashMockSolver } from '@luxexchange/sessions/src/challenge-solvers/createHashcashMockSolver'
|
|
3
|
+
import { createNoneMockSolver } from '@luxexchange/sessions/src/challenge-solvers/createNoneMockSolver'
|
|
4
|
+
import { createTurnstileMockSolver } from '@luxexchange/sessions/src/challenge-solvers/createTurnstileMockSolver'
|
|
5
|
+
import type { ChallengeSolver, ChallengeSolverService } from '@luxexchange/sessions/src/challenge-solvers/types'
|
|
6
|
+
import type { Logger } from '@luxfi/utilities/src/logger/logger'
|
|
7
|
+
|
|
8
|
+
interface CreateChallengeSolverServiceContext {
|
|
9
|
+
/**
|
|
10
|
+
* Optional custom solvers to override defaults
|
|
11
|
+
* Allows injection of real implementations or custom mocks
|
|
12
|
+
*/
|
|
13
|
+
solvers?: Map<ChallengeType, ChallengeSolver>
|
|
14
|
+
/**
|
|
15
|
+
* Optional logger to use for debugging
|
|
16
|
+
*/
|
|
17
|
+
getLogger?: () => Logger
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createChallengeSolverService(ctx: CreateChallengeSolverServiceContext = {}): ChallengeSolverService {
|
|
21
|
+
// Use injected solvers or fall back to default mocks
|
|
22
|
+
const solvers = ctx.solvers ?? createDefaultSolvers()
|
|
23
|
+
|
|
24
|
+
function getSolver(type: ChallengeType): ChallengeSolver | null {
|
|
25
|
+
// Handle UNSPECIFIED type explicitly
|
|
26
|
+
if (type === ChallengeType.UNSPECIFIED) {
|
|
27
|
+
return {
|
|
28
|
+
solve: async (): Promise<string> => {
|
|
29
|
+
throw new Error('No solver available for challenge type: UNSPECIFIED')
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const value = solvers.get(type) ?? null
|
|
35
|
+
|
|
36
|
+
if (ctx.getLogger) {
|
|
37
|
+
ctx
|
|
38
|
+
.getLogger()
|
|
39
|
+
.debug(
|
|
40
|
+
'createChallengeSolverService',
|
|
41
|
+
'getSolver',
|
|
42
|
+
`Solver for challenge type ${type} is ${value ? 'available' : 'not available'}`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return value
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { getSolver }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates the default set of mock solvers for development/testing
|
|
54
|
+
*/
|
|
55
|
+
function createDefaultSolvers(): Map<ChallengeType, ChallengeSolver> {
|
|
56
|
+
return new Map<ChallengeType, ChallengeSolver>([
|
|
57
|
+
[ChallengeType.UNSPECIFIED, createNoneMockSolver()],
|
|
58
|
+
[ChallengeType.TURNSTILE, createTurnstileMockSolver()],
|
|
59
|
+
[ChallengeType.HASHCASH, createHashcashMockSolver()],
|
|
60
|
+
])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { createChallengeSolverService }
|
|
64
|
+
export type { CreateChallengeSolverServiceContext }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ChallengeData, ChallengeSolver } from '@luxfi/sessions/src/challenge-solvers/types'
|
|
2
|
+
import { sleep } from '@luxfi/utilities/src/time/timing'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a mock Hashcash proof-of-work solver for development/testing
|
|
6
|
+
*
|
|
7
|
+
* In production, this would:
|
|
8
|
+
* - Extract difficulty/bits from challengeData.extra
|
|
9
|
+
* - Iterate through nonces to find hash with required leading zeros
|
|
10
|
+
* - Return actual proof-of-work solution
|
|
11
|
+
*
|
|
12
|
+
* Hashcash format: version:bits:date:resource::nonce:base64
|
|
13
|
+
*/
|
|
14
|
+
function createHashcashMockSolver(): ChallengeSolver {
|
|
15
|
+
async function solve(challengeData: ChallengeData): Promise<string> {
|
|
16
|
+
// Extract difficulty from extra data
|
|
17
|
+
const difficulty = challengeData.extra?.['bits'] || '20'
|
|
18
|
+
|
|
19
|
+
// Simulate proof-of-work computation time
|
|
20
|
+
// Real implementation would iterate through nonces
|
|
21
|
+
const iterations = parseInt(difficulty) / 4
|
|
22
|
+
for (let i = 0; i < iterations; i++) {
|
|
23
|
+
await sleep(100) // Simulate work
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Generate mock hashcash solution
|
|
27
|
+
const nonce = Math.random().toString(36).substring(2, 15)
|
|
28
|
+
const mockHash = btoa(`${challengeData.challengeId}${nonce}`).slice(0, 27)
|
|
29
|
+
const timestamp = new Date().toISOString().slice(2, 10).replace(/-/g, '')
|
|
30
|
+
const resource = challengeData.challengeId.slice(0, 16)
|
|
31
|
+
|
|
32
|
+
// Return in hashcash format
|
|
33
|
+
return `1:${difficulty}:${timestamp}:${resource}::${nonce}:${mockHash}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { solve }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { createHashcashMockSolver }
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { ChallengeType } from '@uniswap/client-platform-service/dist/uniswap/platformservice/v1/sessionService_pb'
|
|
2
|
+
import { createHashcashSolver } from '@luxexchange/sessions/src/challenge-solvers/createHashcashSolver'
|
|
3
|
+
import type { HashcashWorkerChannel } from '@luxexchange/sessions/src/challenge-solvers/hashcash/worker/types'
|
|
4
|
+
import type { ChallengeData } from '@luxexchange/sessions/src/challenge-solvers/types'
|
|
5
|
+
import type { PerformanceTracker } from '@luxexchange/sessions/src/performance/types'
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
// Mock performance tracker for testing
|
|
9
|
+
function createMockPerformanceTracker(): PerformanceTracker {
|
|
10
|
+
let time = 0
|
|
11
|
+
return {
|
|
12
|
+
now: (): number => {
|
|
13
|
+
time += 100
|
|
14
|
+
return time
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('createHashcashSolver', () => {
|
|
20
|
+
const mockPerformanceTracker = createMockPerformanceTracker()
|
|
21
|
+
const solver = createHashcashSolver({ performanceTracker: mockPerformanceTracker })
|
|
22
|
+
|
|
23
|
+
// Real backend example data
|
|
24
|
+
const backendExample = {
|
|
25
|
+
difficulty: 1,
|
|
26
|
+
subject: 'Lux',
|
|
27
|
+
algorithm: 'sha256' as const,
|
|
28
|
+
nonce: 'Qlquffem7d8RrL6fmveE68XK0KxcoczdiVpFrV1qeUk=',
|
|
29
|
+
max_proof_length: 10000,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
it('successfully solves a valid challenge', async () => {
|
|
33
|
+
const challengeData: ChallengeData = {
|
|
34
|
+
challengeId: 'test-challenge-123',
|
|
35
|
+
challengeType: ChallengeType.HASHCASH,
|
|
36
|
+
extra: {
|
|
37
|
+
challengeData: JSON.stringify(backendExample),
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const solution = await solver.solve(challengeData)
|
|
42
|
+
|
|
43
|
+
// Check format: "${subject}:${nonce}:${counter}"
|
|
44
|
+
expect(solution).toMatch(/^Lux:[A-Za-z0-9+/=]+:\d+$/)
|
|
45
|
+
expect(solution.startsWith('Lux:')).toBe(true)
|
|
46
|
+
expect(solution).toContain(backendExample.nonce) // Should contain the base64 nonce
|
|
47
|
+
|
|
48
|
+
// Verify the solution has all three parts
|
|
49
|
+
const parts = solution.split(':')
|
|
50
|
+
expect(parts.length).toBe(3)
|
|
51
|
+
expect(parts[0]).toBe('Lux')
|
|
52
|
+
expect(parts[1]).toBe(backendExample.nonce)
|
|
53
|
+
expect(Number.parseInt(parts[2], 10)).toBeGreaterThanOrEqual(0)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('throws error when challengeData is missing', async () => {
|
|
57
|
+
const challengeData: ChallengeData = {
|
|
58
|
+
challengeId: 'test-challenge-123',
|
|
59
|
+
challengeType: ChallengeType.HASHCASH,
|
|
60
|
+
extra: {}, // Missing challengeData
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await expect(solver.solve(challengeData)).rejects.toThrow('Missing challengeData in challenge extra field')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('throws error when challengeData is not valid JSON', async () => {
|
|
67
|
+
const challengeData: ChallengeData = {
|
|
68
|
+
challengeId: 'test-challenge-123',
|
|
69
|
+
challengeType: ChallengeType.HASHCASH,
|
|
70
|
+
extra: {
|
|
71
|
+
challengeData: 'not-valid-json{',
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await expect(solver.solve(challengeData)).rejects.toThrow('Failed to parse challenge JSON')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('throws error when required fields are missing', async () => {
|
|
79
|
+
const invalidChallenge = {
|
|
80
|
+
difficulty: 1,
|
|
81
|
+
// Missing: nonce, subject, algorithm
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const challengeData: ChallengeData = {
|
|
85
|
+
challengeId: 'test-challenge-123',
|
|
86
|
+
challengeType: ChallengeType.HASHCASH,
|
|
87
|
+
extra: {
|
|
88
|
+
challengeData: JSON.stringify(invalidChallenge),
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Zod will report all missing fields at once
|
|
93
|
+
await expect(solver.solve(challengeData)).rejects.toThrow('Invalid challenge data')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('throws error when algorithm is not sha256', async () => {
|
|
97
|
+
const invalidChallenge = {
|
|
98
|
+
...backendExample,
|
|
99
|
+
algorithm: 'sha512' as any, // Wrong algorithm
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const challengeData: ChallengeData = {
|
|
103
|
+
challengeId: 'test-challenge-123',
|
|
104
|
+
challengeType: ChallengeType.HASHCASH,
|
|
105
|
+
extra: {
|
|
106
|
+
challengeData: JSON.stringify(invalidChallenge),
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Zod will report the literal mismatch
|
|
111
|
+
await expect(solver.solve(challengeData)).rejects.toThrow('Invalid challenge data: algorithm')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('handles high difficulty that cannot be solved', async () => {
|
|
115
|
+
const impossibleChallenge = {
|
|
116
|
+
...backendExample,
|
|
117
|
+
difficulty: 30, // Very high difficulty
|
|
118
|
+
max_proof_length: 10, // Very small search space
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const challengeData: ChallengeData = {
|
|
122
|
+
challengeId: 'test-challenge-123',
|
|
123
|
+
challengeType: ChallengeType.HASHCASH,
|
|
124
|
+
extra: {
|
|
125
|
+
challengeData: JSON.stringify(impossibleChallenge),
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await expect(solver.solve(challengeData)).rejects.toThrow('Failed to find valid proof within allowed range')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('handles missing extra field gracefully', async () => {
|
|
133
|
+
const challengeData: ChallengeData = {
|
|
134
|
+
challengeId: 'test-challenge-123',
|
|
135
|
+
challengeType: ChallengeType.HASHCASH,
|
|
136
|
+
// No extra field at all
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await expect(solver.solve(challengeData)).rejects.toThrow('Missing challengeData in challenge extra field')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('analytics callback', () => {
|
|
143
|
+
it('reports success with difficulty and iteration count', async () => {
|
|
144
|
+
const onSolveCompleted = vi.fn()
|
|
145
|
+
const solverWithAnalytics = createHashcashSolver({
|
|
146
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
147
|
+
onSolveCompleted,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const challengeData: ChallengeData = {
|
|
151
|
+
challengeId: 'test-challenge-123',
|
|
152
|
+
challengeType: ChallengeType.HASHCASH,
|
|
153
|
+
extra: {
|
|
154
|
+
challengeData: JSON.stringify(backendExample),
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await solverWithAnalytics.solve(challengeData)
|
|
159
|
+
|
|
160
|
+
expect(onSolveCompleted).toHaveBeenCalledWith(
|
|
161
|
+
expect.objectContaining({
|
|
162
|
+
success: true,
|
|
163
|
+
difficulty: 1,
|
|
164
|
+
usedWorker: false,
|
|
165
|
+
}),
|
|
166
|
+
)
|
|
167
|
+
expect(onSolveCompleted.mock.calls[0][0].iterationCount).toBeGreaterThan(0)
|
|
168
|
+
expect(onSolveCompleted.mock.calls[0][0].durationMs).toBeGreaterThanOrEqual(0)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('reports failure with validation error type', async () => {
|
|
172
|
+
const onSolveCompleted = vi.fn()
|
|
173
|
+
const solverWithAnalytics = createHashcashSolver({
|
|
174
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
175
|
+
onSolveCompleted,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const challengeData: ChallengeData = {
|
|
179
|
+
challengeId: 'test-challenge-123',
|
|
180
|
+
challengeType: ChallengeType.HASHCASH,
|
|
181
|
+
extra: {}, // Missing challengeData
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await expect(solverWithAnalytics.solve(challengeData)).rejects.toThrow()
|
|
185
|
+
|
|
186
|
+
expect(onSolveCompleted).toHaveBeenCalledWith(
|
|
187
|
+
expect.objectContaining({
|
|
188
|
+
success: false,
|
|
189
|
+
errorType: 'validation',
|
|
190
|
+
usedWorker: false,
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('reports failure when proof cannot be found', async () => {
|
|
196
|
+
const onSolveCompleted = vi.fn()
|
|
197
|
+
const solverWithAnalytics = createHashcashSolver({
|
|
198
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
199
|
+
onSolveCompleted,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const impossibleChallenge = {
|
|
203
|
+
...backendExample,
|
|
204
|
+
difficulty: 30,
|
|
205
|
+
max_proof_length: 10,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const challengeData: ChallengeData = {
|
|
209
|
+
challengeId: 'test-challenge-123',
|
|
210
|
+
challengeType: ChallengeType.HASHCASH,
|
|
211
|
+
extra: {
|
|
212
|
+
challengeData: JSON.stringify(impossibleChallenge),
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await expect(solverWithAnalytics.solve(challengeData)).rejects.toThrow()
|
|
217
|
+
|
|
218
|
+
expect(onSolveCompleted).toHaveBeenCalledWith(
|
|
219
|
+
expect.objectContaining({
|
|
220
|
+
success: false,
|
|
221
|
+
errorType: 'no_proof',
|
|
222
|
+
difficulty: 30,
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('reports usedWorker: true when worker is provided', async () => {
|
|
228
|
+
const onSolveCompleted = vi.fn()
|
|
229
|
+
const mockWorkerChannel: HashcashWorkerChannel = {
|
|
230
|
+
api: {
|
|
231
|
+
findProof: vi.fn().mockResolvedValue({ counter: 42, hash: new Uint8Array([0]), attempts: 100 }),
|
|
232
|
+
cancel: vi.fn(),
|
|
233
|
+
},
|
|
234
|
+
terminate: vi.fn(),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const solverWithWorker = createHashcashSolver({
|
|
238
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
239
|
+
getWorkerChannel: () => mockWorkerChannel,
|
|
240
|
+
onSolveCompleted,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const challengeData: ChallengeData = {
|
|
244
|
+
challengeId: 'test-challenge-123',
|
|
245
|
+
challengeType: ChallengeType.HASHCASH,
|
|
246
|
+
extra: {
|
|
247
|
+
challengeData: JSON.stringify(backendExample),
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await solverWithWorker.solve(challengeData)
|
|
252
|
+
|
|
253
|
+
expect(onSolveCompleted).toHaveBeenCalledWith(
|
|
254
|
+
expect.objectContaining({
|
|
255
|
+
success: true,
|
|
256
|
+
usedWorker: true,
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('with worker channel', () => {
|
|
263
|
+
it('uses worker channel when provided', async () => {
|
|
264
|
+
const mockFindProof = vi.fn().mockResolvedValue({ counter: 42, hash: new Uint8Array([0]) })
|
|
265
|
+
const mockTerminate = vi.fn()
|
|
266
|
+
|
|
267
|
+
const mockWorkerChannel: HashcashWorkerChannel = {
|
|
268
|
+
api: {
|
|
269
|
+
findProof: mockFindProof,
|
|
270
|
+
cancel: vi.fn(),
|
|
271
|
+
},
|
|
272
|
+
terminate: mockTerminate,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const solverWithWorker = createHashcashSolver({
|
|
276
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
277
|
+
getWorkerChannel: () => mockWorkerChannel,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const challengeData: ChallengeData = {
|
|
281
|
+
challengeId: 'test-challenge-123',
|
|
282
|
+
challengeType: ChallengeType.HASHCASH,
|
|
283
|
+
extra: {
|
|
284
|
+
challengeData: JSON.stringify(backendExample),
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const solution = await solverWithWorker.solve(challengeData)
|
|
289
|
+
|
|
290
|
+
expect(mockFindProof).toHaveBeenCalledWith({
|
|
291
|
+
challenge: backendExample,
|
|
292
|
+
rangeStart: 0,
|
|
293
|
+
rangeSize: backendExample.max_proof_length,
|
|
294
|
+
})
|
|
295
|
+
expect(mockTerminate).toHaveBeenCalled()
|
|
296
|
+
expect(solution).toBe(`Lux:${backendExample.nonce}:42`)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('terminates worker channel even on error', async () => {
|
|
300
|
+
const mockTerminate = vi.fn()
|
|
301
|
+
|
|
302
|
+
const mockWorkerChannel: HashcashWorkerChannel = {
|
|
303
|
+
api: {
|
|
304
|
+
findProof: vi.fn().mockRejectedValue(new Error('Worker error')),
|
|
305
|
+
cancel: vi.fn(),
|
|
306
|
+
},
|
|
307
|
+
terminate: mockTerminate,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const solverWithWorker = createHashcashSolver({
|
|
311
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
312
|
+
getWorkerChannel: () => mockWorkerChannel,
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const challengeData: ChallengeData = {
|
|
316
|
+
challengeId: 'test-challenge-123',
|
|
317
|
+
challengeType: ChallengeType.HASHCASH,
|
|
318
|
+
extra: {
|
|
319
|
+
challengeData: JSON.stringify(backendExample),
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
await expect(solverWithWorker.solve(challengeData)).rejects.toThrow('Worker error')
|
|
324
|
+
expect(mockTerminate).toHaveBeenCalled()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('throws error when worker returns null', async () => {
|
|
328
|
+
const mockTerminate = vi.fn()
|
|
329
|
+
|
|
330
|
+
const mockWorkerChannel: HashcashWorkerChannel = {
|
|
331
|
+
api: {
|
|
332
|
+
findProof: vi.fn().mockResolvedValue(null),
|
|
333
|
+
cancel: vi.fn(),
|
|
334
|
+
},
|
|
335
|
+
terminate: mockTerminate,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const solverWithWorker = createHashcashSolver({
|
|
339
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
340
|
+
getWorkerChannel: () => mockWorkerChannel,
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const challengeData: ChallengeData = {
|
|
344
|
+
challengeId: 'test-challenge-123',
|
|
345
|
+
challengeType: ChallengeType.HASHCASH,
|
|
346
|
+
extra: {
|
|
347
|
+
challengeData: JSON.stringify(backendExample),
|
|
348
|
+
},
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await expect(solverWithWorker.solve(challengeData)).rejects.toThrow('Failed to find valid proof')
|
|
352
|
+
expect(mockTerminate).toHaveBeenCalled()
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('throws error when worker is busy with another operation', async () => {
|
|
356
|
+
const mockTerminate = vi.fn()
|
|
357
|
+
|
|
358
|
+
const mockWorkerChannel: HashcashWorkerChannel = {
|
|
359
|
+
api: {
|
|
360
|
+
findProof: vi
|
|
361
|
+
.fn()
|
|
362
|
+
.mockRejectedValue(new Error('Worker is busy - another findProof operation is in progress')),
|
|
363
|
+
cancel: vi.fn(),
|
|
364
|
+
},
|
|
365
|
+
terminate: mockTerminate,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const solverWithWorker = createHashcashSolver({
|
|
369
|
+
performanceTracker: createMockPerformanceTracker(),
|
|
370
|
+
getWorkerChannel: () => mockWorkerChannel,
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const challengeData: ChallengeData = {
|
|
374
|
+
challengeId: 'test-challenge-123',
|
|
375
|
+
challengeType: ChallengeType.HASHCASH,
|
|
376
|
+
extra: {
|
|
377
|
+
challengeData: JSON.stringify(backendExample),
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await expect(solverWithWorker.solve(challengeData)).rejects.toThrow('Worker is busy')
|
|
382
|
+
expect(mockTerminate).toHaveBeenCalled()
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
})
|