@hivemind-os/collective-core 0.2.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/.test-data/05a845c4-1682-41b9-97e5-65a5263156c0/spending.sqlite +0 -0
- package/.test-data/10e18ba5-98f0-42e0-8899-06a09459ae85/agents.sqlite +0 -0
- package/.test-data/20464456-23cb-4ff7-8df5-3c129fb95a90/agents.sqlite +0 -0
- package/.test-data/2e2ec66c-e8b4-43eb-945f-cca84ed0a0f6/agents.sqlite +0 -0
- package/.test-data/2f5ef9b7-01da-4ba0-a6fa-af8063a2567c/agents.sqlite +0 -0
- package/.test-data/3ac13cd5-84c9-4e71-8b89-6d3ef4dd819c/agents.sqlite +0 -0
- package/.test-data/40b7dd4a-fae0-4a5d-a6a0-64c9048af35e/agents.sqlite +0 -0
- package/.test-data/59511a04-42f8-4286-bb1e-733795e08749/agents.sqlite +0 -0
- package/.test-data/6778e8c5-6eb5-416d-8aca-9d559cdf83e4/agents.sqlite +0 -0
- package/.test-data/7648dc86-df90-4460-8f9c-55cdb481324b/agents.sqlite +0 -0
- package/.test-data/77162d98-6b22-41b1-a50f-536fab739f8a/agents.sqlite +0 -0
- package/.test-data/798dbdab-cbe7-4edd-8a5b-ae99c285fe17/agents.sqlite +0 -0
- package/.test-data/8033d6ac-8b85-454d-b708-00ac695b22f8/agents.sqlite +0 -0
- package/.test-data/9155a4f4-cda3-487c-9eed-921c82d7550f/agents.sqlite +0 -0
- package/.test-data/9bfeee53-c231-46c3-8c93-2180933f5d50/agents.sqlite +0 -0
- package/.test-data/a4c64287-79f6-46e9-847d-2803c63a74fd/agents.sqlite +0 -0
- package/.test-data/b8f58952-1ed8-46ff-abd7-21fb86e9457f/agents.sqlite +0 -0
- package/.test-data/c3060504-3187-41ed-8532-82332be48b0b/spending.sqlite +0 -0
- package/.test-data/cc471629-8006-4fc1-b8a1-399d2df2cc4e/agents.sqlite +0 -0
- package/.test-data/dbca3bef-397d-4bbc-bd4c-4f7b14103e04/spending.sqlite +0 -0
- package/.test-data/f1283dd1-6602-4de7-a050-16aac7abc288/agents.sqlite +0 -0
- package/.turbo/turbo-build.log +14 -0
- package/dist/index.d.ts +1675 -0
- package/dist/index.js +8006 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/src/auth/device-flow.ts +108 -0
- package/src/auth/ed25519-provider.ts +43 -0
- package/src/auth/errors.ts +82 -0
- package/src/auth/evm-key.ts +55 -0
- package/src/auth/index.ts +8 -0
- package/src/auth/session-state.ts +25 -0
- package/src/auth/session-store.ts +510 -0
- package/src/auth/types.ts +81 -0
- package/src/auth/zklogin-provider.ts +902 -0
- package/src/blobstore/WALRUS_FINDINGS.md +284 -0
- package/src/blobstore/encrypted-store.ts +56 -0
- package/src/blobstore/fs-store.ts +91 -0
- package/src/blobstore/hybrid-store.ts +144 -0
- package/src/blobstore/index.ts +5 -0
- package/src/blobstore/interface.ts +33 -0
- package/src/blobstore/walrus-spike.ts +345 -0
- package/src/blobstore/walrus-store.ts +551 -0
- package/src/cache/agent-cache.ts +403 -0
- package/src/cache/index.ts +1 -0
- package/src/crypto/encryption.ts +152 -0
- package/src/crypto/index.ts +2 -0
- package/src/crypto/x25519.ts +41 -0
- package/src/dispute/client.ts +191 -0
- package/src/dispute/index.ts +1 -0
- package/src/events/index.ts +2 -0
- package/src/events/parser.ts +291 -0
- package/src/events/subscription.ts +131 -0
- package/src/evm/constants.ts +6 -0
- package/src/evm/index.ts +2 -0
- package/src/evm/wallet.ts +136 -0
- package/src/identity/did.ts +36 -0
- package/src/identity/index.ts +4 -0
- package/src/identity/keypair.ts +199 -0
- package/src/identity/signing.ts +28 -0
- package/src/index.ts +22 -0
- package/src/internal/parsing.ts +416 -0
- package/src/marketplace/client.ts +349 -0
- package/src/marketplace/index.ts +1 -0
- package/src/metering/hash-chain.ts +94 -0
- package/src/metering/index.ts +4 -0
- package/src/metering/meter.ts +80 -0
- package/src/metering/streaming.ts +196 -0
- package/src/metering/verification.ts +104 -0
- package/src/payment/index.ts +1 -0
- package/src/payment/rail-selector.ts +41 -0
- package/src/registry/client.ts +328 -0
- package/src/registry/index.ts +1 -0
- package/src/relay/consumer-client.ts +497 -0
- package/src/relay/index.ts +1 -0
- package/src/relay-registry/client.ts +295 -0
- package/src/relay-registry/discovery.ts +109 -0
- package/src/relay-registry/index.ts +2 -0
- package/src/reputation/anchor-client.ts +126 -0
- package/src/reputation/event-publisher.ts +67 -0
- package/src/reputation/index.ts +5 -0
- package/src/reputation/merkle.ts +79 -0
- package/src/reputation/score-calculator.ts +133 -0
- package/src/reputation/serialization.ts +37 -0
- package/src/reputation/store.ts +165 -0
- package/src/reputation/validation.ts +135 -0
- package/src/routing/circuit-breaker.ts +111 -0
- package/src/routing/fan-out.ts +266 -0
- package/src/routing/index.ts +4 -0
- package/src/routing/performance.ts +244 -0
- package/src/routing/selector.ts +225 -0
- package/src/spending/index.ts +1 -0
- package/src/spending/policy.ts +271 -0
- package/src/staking/client.ts +319 -0
- package/src/staking/index.ts +1 -0
- package/src/sui/client.ts +214 -0
- package/src/sui/index.ts +2 -0
- package/src/sui/tx-helpers.ts +1070 -0
- package/src/task/client.ts +215 -0
- package/src/task/index.ts +1 -0
- package/src/x402/client.ts +295 -0
- package/src/x402/index.ts +1 -0
- package/tests/auth/device-flow.test.ts +62 -0
- package/tests/auth/ed25519-provider.test.ts +24 -0
- package/tests/auth/evm-key.test.ts +31 -0
- package/tests/auth/session-store.test.ts +201 -0
- package/tests/auth/zklogin-provider.test.ts +366 -0
- package/tests/blobstore/encrypted-store.test.ts +78 -0
- package/tests/blobstore.test.ts +91 -0
- package/tests/cache.test.ts +124 -0
- package/tests/crypto/encryption.test.ts +70 -0
- package/tests/crypto/x25519.test.ts +47 -0
- package/tests/dispute/client.test.ts +238 -0
- package/tests/events.test.ts +202 -0
- package/tests/evm/wallet.test.ts +101 -0
- package/tests/hybrid-store.test.ts +121 -0
- package/tests/identity.test.ts +161 -0
- package/tests/marketplace.test.ts +308 -0
- package/tests/metering/hash-chain.test.ts +32 -0
- package/tests/metering/meter.test.ts +23 -0
- package/tests/metering/streaming.test.ts +52 -0
- package/tests/metering/verification.test.ts +27 -0
- package/tests/payment/rail-selector.test.ts +95 -0
- package/tests/registry.test.ts +183 -0
- package/tests/relay-consumer-client.test.ts +119 -0
- package/tests/relay-registry/client.test.ts +261 -0
- package/tests/reputation/event-publisher.test.ts +70 -0
- package/tests/reputation/merkle.test.ts +44 -0
- package/tests/reputation/score-calculator.test.ts +104 -0
- package/tests/reputation/store.test.ts +94 -0
- package/tests/routing/circuit-breaker.test.ts +45 -0
- package/tests/routing/fan-out.test.ts +123 -0
- package/tests/routing/performance.test.ts +49 -0
- package/tests/routing/selector.test.ts +114 -0
- package/tests/spending.test.ts +133 -0
- package/tests/staking/client.test.ts +286 -0
- package/tests/sui-client.test.ts +85 -0
- package/tests/task.test.ts +249 -0
- package/tests/tx-helpers.test.ts +70 -0
- package/tests/walrus-spike.test.ts +100 -0
- package/tests/walrus-store.test.ts +196 -0
- package/tests/x402/client.test.ts +116 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { WalrusBlobStore } from '../src/index.js';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_WALRUS_AGGREGATOR_URL,
|
|
8
|
+
DEFAULT_WALRUS_PUBLISHER_URL,
|
|
9
|
+
WalrusNetworkError,
|
|
10
|
+
fetchBlobFromWalrus,
|
|
11
|
+
moveVectorToWalrusBlobId,
|
|
12
|
+
storeBlobOnWalrus,
|
|
13
|
+
walrusBlobIdToMoveVector,
|
|
14
|
+
} from '../src/blobstore/walrus-spike.js';
|
|
15
|
+
|
|
16
|
+
const runWalrusTestnet = process.env.RUN_WALRUS_TESTNET === '1';
|
|
17
|
+
const describeWalrus = runWalrusTestnet ? describe : describe.skip;
|
|
18
|
+
|
|
19
|
+
function sha256(data: Uint8Array): string {
|
|
20
|
+
return createHash('sha256').update(data).digest('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('Walrus spike helpers', () => {
|
|
24
|
+
it('round-trips Walrus blob IDs through Move-friendly bytes', () => {
|
|
25
|
+
const blobId = 'a7wmADgKiwLDFnpjyu6NBTkCS-1zLblj3TfmXZWxnew';
|
|
26
|
+
|
|
27
|
+
const moveBytes = walrusBlobIdToMoveVector(blobId);
|
|
28
|
+
|
|
29
|
+
expect(moveBytes).toHaveLength(32);
|
|
30
|
+
expect(moveVectorToWalrusBlobId(moveBytes)).toBe(blobId);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('wraps network errors', async () => {
|
|
34
|
+
const data = new TextEncoder().encode('offline');
|
|
35
|
+
|
|
36
|
+
await expect(
|
|
37
|
+
storeBlobOnWalrus(data, {
|
|
38
|
+
fetchImpl: async () => {
|
|
39
|
+
throw new TypeError('network offline');
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
).rejects.toBeInstanceOf(WalrusNetworkError);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('explains why delete is unsupported', async () => {
|
|
46
|
+
const store = new WalrusBlobStore({
|
|
47
|
+
publisherUrl: DEFAULT_WALRUS_PUBLISHER_URL,
|
|
48
|
+
aggregatorUrl: DEFAULT_WALRUS_AGGREGATOR_URL,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await expect(store.delete('ignored')).rejects.toThrow(/public HTTP API/i);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describeWalrus('Walrus testnet spike', () => {
|
|
56
|
+
const publisherUrl = process.env.WALRUS_PUBLISHER_URL ?? DEFAULT_WALRUS_PUBLISHER_URL;
|
|
57
|
+
const aggregatorUrl = process.env.WALRUS_AGGREGATOR_URL ?? DEFAULT_WALRUS_AGGREGATOR_URL;
|
|
58
|
+
const epochs = Number(process.env.WALRUS_EPOCHS ?? '1');
|
|
59
|
+
|
|
60
|
+
it('stores and fetches a small blob with matching SHA-256', async () => {
|
|
61
|
+
const data = new TextEncoder().encode(`Hello, Walrus! ${Date.now()}`);
|
|
62
|
+
const result = await storeBlobOnWalrus(data, {
|
|
63
|
+
publisherUrl,
|
|
64
|
+
aggregatorUrl,
|
|
65
|
+
epochs,
|
|
66
|
+
permanent: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const fetched = await fetchBlobFromWalrus(result.blobId, { aggregatorUrl });
|
|
70
|
+
|
|
71
|
+
expect(result.blobId).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
72
|
+
expect(result.storeMs).toBeGreaterThan(0);
|
|
73
|
+
expect(result.checksum).toBe(sha256(data));
|
|
74
|
+
expect(result.deletable).toBe(false);
|
|
75
|
+
expect(Buffer.from(fetched ?? [])).toEqual(Buffer.from(data));
|
|
76
|
+
expect(sha256(fetched ?? new Uint8Array())).toBe(result.checksum);
|
|
77
|
+
}, 120_000);
|
|
78
|
+
|
|
79
|
+
it('stores and fetches a 1 MiB blob through WalrusBlobStore', async () => {
|
|
80
|
+
const data = randomBytes(1024 * 1024);
|
|
81
|
+
const store = new WalrusBlobStore({ publisherUrl, aggregatorUrl, epochs });
|
|
82
|
+
|
|
83
|
+
const stored = await store.store(data);
|
|
84
|
+
const fetched = await store.fetch(stored.blobId);
|
|
85
|
+
|
|
86
|
+
expect(stored.blobId).toMatch(/^walrus:[A-Za-z0-9_-]{43}:[a-f0-9]{64}$/);
|
|
87
|
+
expect(stored.checksum).toBe(sha256(data));
|
|
88
|
+
expect(Buffer.from(fetched ?? [])).toEqual(Buffer.from(data));
|
|
89
|
+
expect(await store.exists(stored.blobId)).toBe(true);
|
|
90
|
+
}, 180_000);
|
|
91
|
+
|
|
92
|
+
it('returns null for a missing valid blob ID and errors for malformed IDs', async () => {
|
|
93
|
+
const store = new WalrusBlobStore({ publisherUrl, aggregatorUrl, epochs });
|
|
94
|
+
const missingBlobId = Buffer.alloc(32, 0x11).toString('base64url');
|
|
95
|
+
|
|
96
|
+
await expect(store.fetch(missingBlobId)).resolves.toBeNull();
|
|
97
|
+
await expect(store.exists(missingBlobId)).resolves.toBe(false);
|
|
98
|
+
await expect(store.fetch('not-a-real-id')).rejects.toThrow(/Invalid Walrus blob ID/);
|
|
99
|
+
}, 60_000);
|
|
100
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BlobIntegrityError,
|
|
7
|
+
WalrusBlobStore,
|
|
8
|
+
WalrusBlobTooLargeError,
|
|
9
|
+
WalrusHttpError,
|
|
10
|
+
WalrusResponseError,
|
|
11
|
+
WalrusTimeoutError,
|
|
12
|
+
createWalrusBlobReference,
|
|
13
|
+
} from '../src/index.js';
|
|
14
|
+
|
|
15
|
+
type FetchImpl = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
16
|
+
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const walrusStorageBlobId = Buffer.alloc(32, 0x11).toString('base64url');
|
|
19
|
+
|
|
20
|
+
function sha256(data: Uint8Array): string {
|
|
21
|
+
return createHash('sha256').update(data).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createStore(fetchImpl: typeof fetch): WalrusBlobStore {
|
|
25
|
+
return new WalrusBlobStore({
|
|
26
|
+
publisherUrl: 'https://publisher.example.com',
|
|
27
|
+
aggregatorUrl: 'https://aggregator.example.com',
|
|
28
|
+
epochs: 5,
|
|
29
|
+
retryAttempts: 3,
|
|
30
|
+
retryDelayMs: 1,
|
|
31
|
+
timeoutMs: 10,
|
|
32
|
+
fetchImpl,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('WalrusBlobStore', () => {
|
|
37
|
+
it('stores blobs with the Walrus publisher API', async () => {
|
|
38
|
+
const data = encoder.encode('store me');
|
|
39
|
+
const fetchImpl = vi.fn<FetchImpl>().mockResolvedValue(
|
|
40
|
+
new Response(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
newlyCreated: {
|
|
43
|
+
blobObject: {
|
|
44
|
+
id: '0xblob-object',
|
|
45
|
+
blobId: walrusStorageBlobId,
|
|
46
|
+
size: data.byteLength,
|
|
47
|
+
deletable: false,
|
|
48
|
+
storage: { endEpoch: 12 },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
const store = createStore(fetchImpl);
|
|
56
|
+
|
|
57
|
+
const result = await store.store(data);
|
|
58
|
+
const [url, init] = fetchImpl.mock.calls[0] ?? [];
|
|
59
|
+
const parsedUrl = new URL(String(url));
|
|
60
|
+
|
|
61
|
+
expect(parsedUrl.origin).toBe('https://publisher.example.com');
|
|
62
|
+
expect(parsedUrl.pathname).toBe('/v1/blobs');
|
|
63
|
+
expect(parsedUrl.searchParams.get('epochs')).toBe('5');
|
|
64
|
+
expect(parsedUrl.searchParams.get('permanent')).toBe('true');
|
|
65
|
+
expect(init?.method).toBe('PUT');
|
|
66
|
+
expect(init?.headers).toMatchObject({ 'content-type': 'application/octet-stream' });
|
|
67
|
+
expect(result).toMatchObject({
|
|
68
|
+
blobId: createWalrusBlobReference(walrusStorageBlobId, sha256(data)),
|
|
69
|
+
hash: sha256(data),
|
|
70
|
+
checksum: sha256(data),
|
|
71
|
+
contentHash: sha256(data),
|
|
72
|
+
size: data.byteLength,
|
|
73
|
+
storageBlobId: walrusStorageBlobId,
|
|
74
|
+
objectId: '0xblob-object',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('fetches blobs from the Walrus aggregator API', async () => {
|
|
79
|
+
const data = encoder.encode('fetch me');
|
|
80
|
+
const blobId = createWalrusBlobReference(walrusStorageBlobId, sha256(data));
|
|
81
|
+
const fetchImpl = vi.fn<FetchImpl>().mockResolvedValue(new Response(data, { status: 200 }));
|
|
82
|
+
const store = createStore(fetchImpl);
|
|
83
|
+
|
|
84
|
+
const fetched = await store.fetch(blobId);
|
|
85
|
+
const [url, init] = fetchImpl.mock.calls[0] ?? [];
|
|
86
|
+
|
|
87
|
+
expect(String(url)).toBe(`https://aggregator.example.com/v1/blobs/${walrusStorageBlobId}`);
|
|
88
|
+
expect(init?.method).toBe('GET');
|
|
89
|
+
expect(Buffer.from(fetched ?? [])).toEqual(Buffer.from(data));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('retries transient failures', async () => {
|
|
93
|
+
const data = encoder.encode('retry me');
|
|
94
|
+
const fetchImpl = vi
|
|
95
|
+
.fn<FetchImpl>()
|
|
96
|
+
.mockResolvedValueOnce(new Response('temporary outage', { status: 503 }))
|
|
97
|
+
.mockResolvedValueOnce(
|
|
98
|
+
new Response(
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
newlyCreated: {
|
|
101
|
+
blobObject: {
|
|
102
|
+
id: '0xblob-object',
|
|
103
|
+
blobId: walrusStorageBlobId,
|
|
104
|
+
size: data.byteLength,
|
|
105
|
+
deletable: false,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
const store = createStore(fetchImpl);
|
|
113
|
+
|
|
114
|
+
const result = await store.store(data);
|
|
115
|
+
|
|
116
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
117
|
+
expect(result.hash).toBe(sha256(data));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('fails fast on permanent errors', async () => {
|
|
121
|
+
const fetchImpl = vi.fn<FetchImpl>().mockResolvedValue(new Response('bad request', { status: 400 }));
|
|
122
|
+
const store = createStore(fetchImpl);
|
|
123
|
+
|
|
124
|
+
await expect(store.store(encoder.encode('boom'))).rejects.toBeInstanceOf(WalrusHttpError);
|
|
125
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('treats malformed publisher responses as permanent protocol errors', async () => {
|
|
129
|
+
const fetchImpl = vi.fn<FetchImpl>().mockResolvedValue(new Response('not-json', { status: 200 }));
|
|
130
|
+
const store = createStore(fetchImpl);
|
|
131
|
+
|
|
132
|
+
await expect(store.store(encoder.encode('boom'))).rejects.toBeInstanceOf(WalrusResponseError);
|
|
133
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('enforces the size limit before upload', async () => {
|
|
137
|
+
const fetchImpl = vi.fn<FetchImpl>();
|
|
138
|
+
const store = new WalrusBlobStore({
|
|
139
|
+
publisherUrl: 'https://publisher.example.com',
|
|
140
|
+
aggregatorUrl: 'https://aggregator.example.com',
|
|
141
|
+
maxBlobSize: 4,
|
|
142
|
+
fetchImpl,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await expect(store.store(encoder.encode('oversized'))).rejects.toBeInstanceOf(WalrusBlobTooLargeError);
|
|
146
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('times out stalled requests', async () => {
|
|
150
|
+
const fetchImpl = vi.fn<FetchImpl>().mockImplementation(async (_url, init) => {
|
|
151
|
+
await new Promise((_resolve, reject) => {
|
|
152
|
+
init?.signal?.addEventListener('abort', () => {
|
|
153
|
+
reject(new DOMException('The operation was aborted.', 'AbortError'));
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
return new Response(null, { status: 500 });
|
|
157
|
+
});
|
|
158
|
+
const store = new WalrusBlobStore({
|
|
159
|
+
publisherUrl: 'https://publisher.example.com',
|
|
160
|
+
aggregatorUrl: 'https://aggregator.example.com',
|
|
161
|
+
timeoutMs: 5,
|
|
162
|
+
retryAttempts: 1,
|
|
163
|
+
retryDelayMs: 1,
|
|
164
|
+
fetchImpl,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await expect(store.store(encoder.encode('slow'))).rejects.toBeInstanceOf(WalrusTimeoutError);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('verifies content integrity on fetch', async () => {
|
|
171
|
+
const data = encoder.encode('safe payload');
|
|
172
|
+
const corrupted = encoder.encode('corrupted payload');
|
|
173
|
+
const fetchImpl = vi
|
|
174
|
+
.fn<FetchImpl>()
|
|
175
|
+
.mockResolvedValueOnce(
|
|
176
|
+
new Response(
|
|
177
|
+
JSON.stringify({
|
|
178
|
+
newlyCreated: {
|
|
179
|
+
blobObject: {
|
|
180
|
+
id: '0xblob-object',
|
|
181
|
+
blobId: walrusStorageBlobId,
|
|
182
|
+
size: data.byteLength,
|
|
183
|
+
deletable: false,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
.mockResolvedValueOnce(new Response(corrupted, { status: 200 }));
|
|
191
|
+
const store = createStore(fetchImpl);
|
|
192
|
+
const stored = await store.store(data);
|
|
193
|
+
|
|
194
|
+
await expect(store.fetch(stored.blobId)).rejects.toBeInstanceOf(BlobIntegrityError);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { decodePaymentSignatureHeader, encodePaymentRequiredHeader } from '@x402/core/http';
|
|
3
|
+
import { hexToBytes } from 'viem';
|
|
4
|
+
|
|
5
|
+
import { EvmWallet, USDC_ADDRESS, X402Client, type X402PaymentRequest } from '../../src/index.js';
|
|
6
|
+
|
|
7
|
+
const PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
|
|
8
|
+
const PAYER_ADDRESS = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
|
|
9
|
+
const PAY_TO = '0x0000000000000000000000000000000000000abc';
|
|
10
|
+
|
|
11
|
+
function createClient() {
|
|
12
|
+
const wallet = new EvmWallet(hexToBytes(PRIVATE_KEY), { network: 'base-sepolia' });
|
|
13
|
+
return new X402Client(wallet);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createManualRequest(overrides: Partial<X402PaymentRequest> = {}): X402PaymentRequest {
|
|
17
|
+
return {
|
|
18
|
+
paymentAddress: PAY_TO,
|
|
19
|
+
amount: '1000000',
|
|
20
|
+
currency: 'USDC',
|
|
21
|
+
network: 'base-sepolia',
|
|
22
|
+
nonce: 'nonce-123',
|
|
23
|
+
expiresAt: Date.now() + 60_000,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('X402Client', () => {
|
|
29
|
+
it('parses PAYMENT-REQUIRED headers into a payment request', () => {
|
|
30
|
+
const client = createClient();
|
|
31
|
+
const header = encodePaymentRequiredHeader({
|
|
32
|
+
x402Version: 2,
|
|
33
|
+
resource: { url: 'https://relay.example.com/task' },
|
|
34
|
+
accepts: [
|
|
35
|
+
{
|
|
36
|
+
scheme: 'exact',
|
|
37
|
+
network: 'eip155:84532',
|
|
38
|
+
asset: USDC_ADDRESS['base-sepolia'],
|
|
39
|
+
amount: '1000000',
|
|
40
|
+
payTo: PAY_TO,
|
|
41
|
+
maxTimeoutSeconds: 300,
|
|
42
|
+
extra: {
|
|
43
|
+
assetTransferMethod: 'permit2',
|
|
44
|
+
currency: 'USDC',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const request = client.parse402Response({ 'PAYMENT-REQUIRED': header });
|
|
51
|
+
|
|
52
|
+
expect(request).toMatchObject({
|
|
53
|
+
paymentAddress: PAY_TO,
|
|
54
|
+
amount: '1000000',
|
|
55
|
+
currency: 'USDC',
|
|
56
|
+
network: 'base-sepolia',
|
|
57
|
+
});
|
|
58
|
+
expect(request.expiresAt).toBeGreaterThan(Date.now());
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('signs and verifies payment authorizations', async () => {
|
|
62
|
+
const client = createClient();
|
|
63
|
+
const request = createManualRequest();
|
|
64
|
+
|
|
65
|
+
const signature = await client.signPayment(request);
|
|
66
|
+
|
|
67
|
+
expect(signature.payerAddress).toBe(PAYER_ADDRESS);
|
|
68
|
+
await expect(client.verifyPayment(signature, request)).resolves.toBe(true);
|
|
69
|
+
await expect(client.verifyPayment(signature, { ...request, amount: '2000000' })).resolves.toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('creates a standard PAYMENT-SIGNATURE header for x402 challenges', async () => {
|
|
73
|
+
const client = createClient();
|
|
74
|
+
const request = client.parse402Response({
|
|
75
|
+
'payment-required': encodePaymentRequiredHeader({
|
|
76
|
+
x402Version: 2,
|
|
77
|
+
resource: { url: 'https://relay.example.com/task' },
|
|
78
|
+
accepts: [
|
|
79
|
+
{
|
|
80
|
+
scheme: 'exact',
|
|
81
|
+
network: 'eip155:84532',
|
|
82
|
+
asset: USDC_ADDRESS['base-sepolia'],
|
|
83
|
+
amount: '1000000',
|
|
84
|
+
payTo: PAY_TO,
|
|
85
|
+
maxTimeoutSeconds: 300,
|
|
86
|
+
extra: {
|
|
87
|
+
assetTransferMethod: 'permit2',
|
|
88
|
+
currency: 'USDC',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const header = await client.createPaymentHeader(request);
|
|
96
|
+
const decoded = decodePaymentSignatureHeader(header);
|
|
97
|
+
|
|
98
|
+
expect(decoded.x402Version).toBe(2);
|
|
99
|
+
expect(decoded.accepted.payTo).toBe(PAY_TO);
|
|
100
|
+
expect(decoded.accepted.amount).toBe('1000000');
|
|
101
|
+
expect(decoded.payload).toHaveProperty('signature');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('rejects invalid or expired payment authorizations', async () => {
|
|
105
|
+
const client = createClient();
|
|
106
|
+
const request = createManualRequest();
|
|
107
|
+
const signature = await client.signPayment(request);
|
|
108
|
+
|
|
109
|
+
await expect(
|
|
110
|
+
client.verifyPayment({ ...signature, signature: `0x${'0'.repeat(130)}` }, request),
|
|
111
|
+
).resolves.toBe(false);
|
|
112
|
+
await expect(
|
|
113
|
+
client.signPayment({ ...request, expiresAt: Date.now() - 1 }),
|
|
114
|
+
).rejects.toThrow('Payment challenge has expired.');
|
|
115
|
+
});
|
|
116
|
+
});
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED