@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,215 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
|
|
3
|
+
import type { NetworkConfig, Task } from '@hivemind-os/collective-types';
|
|
4
|
+
import type { Signer } from '@mysten/sui/cryptography';
|
|
5
|
+
|
|
6
|
+
import { parseTaskFields } from '../internal/parsing.js';
|
|
7
|
+
import { MeshSuiClient } from '../sui/client.js';
|
|
8
|
+
import {
|
|
9
|
+
buildAcceptTaskTx,
|
|
10
|
+
buildCancelTaskTx,
|
|
11
|
+
buildClaimPaymentTx,
|
|
12
|
+
buildCompleteMeteredTaskTx,
|
|
13
|
+
buildCompleteTaskTx,
|
|
14
|
+
buildPostMeteredTaskTx,
|
|
15
|
+
buildPostTaskTx,
|
|
16
|
+
buildRefundExpiredTaskTx,
|
|
17
|
+
buildReleaseMeteredPaymentTx,
|
|
18
|
+
buildReleasePaymentTx,
|
|
19
|
+
} from '../sui/tx-helpers.js';
|
|
20
|
+
|
|
21
|
+
const logger = pino({ name: '@hivemind-os/collective-core:task' });
|
|
22
|
+
|
|
23
|
+
export class TaskClient {
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly suiClient: MeshSuiClient,
|
|
26
|
+
private readonly config: NetworkConfig,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async postTask(params: {
|
|
30
|
+
capability: string;
|
|
31
|
+
category: string;
|
|
32
|
+
inputBlobId: string;
|
|
33
|
+
agreementHash?: string;
|
|
34
|
+
priceMist: bigint;
|
|
35
|
+
disputeWindowMs: number;
|
|
36
|
+
expiryHours: number;
|
|
37
|
+
keypair: Signer;
|
|
38
|
+
}): Promise<{ txDigest: string; taskId: string }> {
|
|
39
|
+
const tx = buildPostTaskTx({
|
|
40
|
+
packageId: this.config.packageId,
|
|
41
|
+
capability: params.capability,
|
|
42
|
+
category: params.category,
|
|
43
|
+
inputBlobId: params.inputBlobId,
|
|
44
|
+
agreementHash: params.agreementHash,
|
|
45
|
+
priceMist: params.priceMist,
|
|
46
|
+
disputeWindowMs: params.disputeWindowMs,
|
|
47
|
+
expiryHours: params.expiryHours,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return await this.submitTaskCreation(tx, params.keypair);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async postMeteredTask(params: {
|
|
54
|
+
capability: string;
|
|
55
|
+
category: string;
|
|
56
|
+
inputBlobId: string;
|
|
57
|
+
agreementHash?: string;
|
|
58
|
+
maxPriceMist: bigint;
|
|
59
|
+
unitPriceMist: bigint;
|
|
60
|
+
disputeWindowMs: number;
|
|
61
|
+
expiryHours: number;
|
|
62
|
+
keypair: Signer;
|
|
63
|
+
}): Promise<{ txDigest: string; taskId: string }> {
|
|
64
|
+
const tx = buildPostMeteredTaskTx({
|
|
65
|
+
packageId: this.config.packageId,
|
|
66
|
+
capability: params.capability,
|
|
67
|
+
category: params.category,
|
|
68
|
+
inputBlobId: params.inputBlobId,
|
|
69
|
+
agreementHash: params.agreementHash,
|
|
70
|
+
maxPriceMist: params.maxPriceMist,
|
|
71
|
+
unitPriceMist: params.unitPriceMist,
|
|
72
|
+
disputeWindowMs: params.disputeWindowMs,
|
|
73
|
+
expiryHours: params.expiryHours,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return await this.submitTaskCreation(tx, params.keypair);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async acceptTask(params: {
|
|
80
|
+
taskId: string;
|
|
81
|
+
keypair: Signer;
|
|
82
|
+
}): Promise<{ txDigest: string }> {
|
|
83
|
+
const tx = buildAcceptTaskTx({ packageId: this.config.packageId, taskId: params.taskId });
|
|
84
|
+
const response = await this.suiClient.executeTransaction(tx, params.keypair);
|
|
85
|
+
return { txDigest: response.digest };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async completeTask(params: {
|
|
89
|
+
taskId: string;
|
|
90
|
+
resultBlobId: string;
|
|
91
|
+
keypair: Signer;
|
|
92
|
+
providerCardId?: string;
|
|
93
|
+
}): Promise<{ txDigest: string }> {
|
|
94
|
+
const tx = buildCompleteTaskTx({
|
|
95
|
+
packageId: this.config.packageId,
|
|
96
|
+
taskId: params.taskId,
|
|
97
|
+
resultBlobId: params.resultBlobId,
|
|
98
|
+
providerCardId: params.providerCardId,
|
|
99
|
+
});
|
|
100
|
+
const response = await this.suiClient.executeTransaction(tx, params.keypair);
|
|
101
|
+
return { txDigest: response.digest };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async completeMeteredTask(params: {
|
|
105
|
+
taskId: string;
|
|
106
|
+
resultBlobId: string;
|
|
107
|
+
meteredUnits: number;
|
|
108
|
+
verificationHash: string;
|
|
109
|
+
keypair: Signer;
|
|
110
|
+
providerCardId?: string;
|
|
111
|
+
}): Promise<{ txDigest: string }> {
|
|
112
|
+
const tx = buildCompleteMeteredTaskTx({
|
|
113
|
+
packageId: this.config.packageId,
|
|
114
|
+
taskId: params.taskId,
|
|
115
|
+
resultBlobId: params.resultBlobId,
|
|
116
|
+
meteredUnits: params.meteredUnits,
|
|
117
|
+
verificationHash: params.verificationHash,
|
|
118
|
+
providerCardId: params.providerCardId,
|
|
119
|
+
});
|
|
120
|
+
const response = await this.suiClient.executeTransaction(tx, params.keypair);
|
|
121
|
+
return { txDigest: response.digest };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async releasePayment(params: {
|
|
125
|
+
taskId: string;
|
|
126
|
+
keypair: Signer;
|
|
127
|
+
}): Promise<{ txDigest: string }> {
|
|
128
|
+
const tx = buildReleasePaymentTx({ packageId: this.config.packageId, taskId: params.taskId });
|
|
129
|
+
const response = await this.suiClient.executeTransaction(tx, params.keypair);
|
|
130
|
+
return { txDigest: response.digest };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async releaseMeteredPayment(params: {
|
|
134
|
+
taskId: string;
|
|
135
|
+
keypair: Signer;
|
|
136
|
+
}): Promise<{ txDigest: string }> {
|
|
137
|
+
const tx = buildReleaseMeteredPaymentTx({ packageId: this.config.packageId, taskId: params.taskId });
|
|
138
|
+
const response = await this.suiClient.executeTransaction(tx, params.keypair);
|
|
139
|
+
return { txDigest: response.digest };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async claimPayment(params: {
|
|
143
|
+
taskId: string;
|
|
144
|
+
keypair: Signer;
|
|
145
|
+
providerCardId?: string;
|
|
146
|
+
}): Promise<{ txDigest: string }> {
|
|
147
|
+
const tx = buildClaimPaymentTx({
|
|
148
|
+
packageId: this.config.packageId,
|
|
149
|
+
taskId: params.taskId,
|
|
150
|
+
providerCardId: params.providerCardId,
|
|
151
|
+
});
|
|
152
|
+
const response = await this.suiClient.executeTransaction(tx, params.keypair);
|
|
153
|
+
return { txDigest: response.digest };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async cancelTask(params: {
|
|
157
|
+
taskId: string;
|
|
158
|
+
keypair: Signer;
|
|
159
|
+
}): Promise<{ txDigest: string }> {
|
|
160
|
+
const tx = buildCancelTaskTx({ packageId: this.config.packageId, taskId: params.taskId });
|
|
161
|
+
const response = await this.suiClient.executeTransaction(tx, params.keypair);
|
|
162
|
+
return { txDigest: response.digest };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async refundExpiredTask(params: {
|
|
166
|
+
taskId: string;
|
|
167
|
+
keypair: Signer;
|
|
168
|
+
}): Promise<{ txDigest: string }> {
|
|
169
|
+
const tx = buildRefundExpiredTaskTx({ packageId: this.config.packageId, taskId: params.taskId });
|
|
170
|
+
const response = await this.suiClient.executeTransaction(tx, params.keypair);
|
|
171
|
+
return { txDigest: response.digest };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getTask(taskId: string): Promise<Task | null> {
|
|
175
|
+
try {
|
|
176
|
+
const object = await this.suiClient.getObject<Record<string, unknown>>(taskId);
|
|
177
|
+
return parseTaskFields(object, taskId);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (isObjectMissingError(error)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async submitTaskCreation(tx: ReturnType<typeof buildPostTaskTx>, keypair: Signer): Promise<{ txDigest: string; taskId: string }> {
|
|
188
|
+
const response = await this.suiClient.executeTransaction(tx, keypair);
|
|
189
|
+
const taskId = extractObjectId(response.objectChanges, /::task::Task$/);
|
|
190
|
+
|
|
191
|
+
if (!taskId) {
|
|
192
|
+
logger.warn({ response }, 'Task posting succeeded without a Task object change.');
|
|
193
|
+
throw new Error('Unable to determine task id from transaction response.');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { txDigest: response.digest, taskId };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function extractObjectId(
|
|
201
|
+
objectChanges: Array<Record<string, unknown>> | null | undefined,
|
|
202
|
+
objectTypePattern: RegExp,
|
|
203
|
+
): string | undefined {
|
|
204
|
+
return objectChanges?.find(
|
|
205
|
+
(change) =>
|
|
206
|
+
(change.type === 'created' || change.type === 'mutated') &&
|
|
207
|
+
typeof change.objectType === 'string' &&
|
|
208
|
+
objectTypePattern.test(change.objectType) &&
|
|
209
|
+
typeof change.objectId === 'string',
|
|
210
|
+
)?.objectId as string | undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isObjectMissingError(error: unknown): boolean {
|
|
214
|
+
return error instanceof Error && /not found|does not contain move object data/i.test(error.message);
|
|
215
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './client.js';
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { encodePaymentSignatureHeader, decodePaymentRequiredHeader } from '@x402/core/http';
|
|
2
|
+
import type { PaymentRequirements } from '@x402/core/types';
|
|
3
|
+
import { ExactEvmScheme } from '@x402/evm/exact/client';
|
|
4
|
+
import { getAddress, verifyTypedData } from 'viem';
|
|
5
|
+
|
|
6
|
+
import { PERMIT2_ADDRESS, USDC_ADDRESS } from '../evm/constants.js';
|
|
7
|
+
import type { EvmWallet } from '../evm/wallet.js';
|
|
8
|
+
|
|
9
|
+
const X402_TYPED_DATA_TYPES = {
|
|
10
|
+
PaymentAuthorization: [
|
|
11
|
+
{ name: 'payerAddress', type: 'address' },
|
|
12
|
+
{ name: 'paymentAddress', type: 'address' },
|
|
13
|
+
{ name: 'amount', type: 'uint256' },
|
|
14
|
+
{ name: 'currency', type: 'string' },
|
|
15
|
+
{ name: 'nonce', type: 'string' },
|
|
16
|
+
{ name: 'expiresAt', type: 'uint256' },
|
|
17
|
+
],
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
type InternalX402PaymentRequest = X402PaymentRequest & {
|
|
21
|
+
__x402?: {
|
|
22
|
+
x402Version: number;
|
|
23
|
+
requirements: PaymentRequirements;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface X402PaymentRequest {
|
|
28
|
+
paymentAddress: string;
|
|
29
|
+
amount: string;
|
|
30
|
+
currency: string;
|
|
31
|
+
network: string;
|
|
32
|
+
nonce: string;
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface X402PaymentSignature {
|
|
37
|
+
signature: string;
|
|
38
|
+
payerAddress: string;
|
|
39
|
+
network: string;
|
|
40
|
+
nonce: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class X402Client {
|
|
44
|
+
constructor(private readonly wallet: EvmWallet) {}
|
|
45
|
+
|
|
46
|
+
parse402Response(headers: Record<string, string>, body?: unknown): X402PaymentRequest {
|
|
47
|
+
const normalizedHeaders = normalizeHeaders(headers);
|
|
48
|
+
const paymentRequiredHeader = normalizedHeaders['payment-required'];
|
|
49
|
+
|
|
50
|
+
if (paymentRequiredHeader) {
|
|
51
|
+
const paymentRequired = decodePaymentRequiredHeader(paymentRequiredHeader);
|
|
52
|
+
const requirements = selectRequirement(paymentRequired.accepts, this.wallet.chain.id);
|
|
53
|
+
const request: InternalX402PaymentRequest = {
|
|
54
|
+
paymentAddress: requirements.payTo,
|
|
55
|
+
amount: requirements.amount,
|
|
56
|
+
currency: inferCurrency(requirements),
|
|
57
|
+
network: normalizeNetwork(requirements.network),
|
|
58
|
+
nonce: readString(requirements.extra?.nonce) ?? '',
|
|
59
|
+
expiresAt: readPositiveNumber(requirements.extra?.expiresAt) ?? Date.now() + requirements.maxTimeoutSeconds * 1_000,
|
|
60
|
+
__x402: {
|
|
61
|
+
x402Version: paymentRequired.x402Version,
|
|
62
|
+
requirements,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return request;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const payload = extractBodyPayload(body);
|
|
70
|
+
const paymentAddress = readString(payload.paymentAddress) ?? normalizedHeaders['payment-address'];
|
|
71
|
+
const amount = readString(payload.amount) ?? normalizedHeaders['payment-amount'];
|
|
72
|
+
const currency = readString(payload.currency) ?? normalizedHeaders['payment-currency'];
|
|
73
|
+
const network = readString(payload.network) ?? normalizedHeaders['payment-network'];
|
|
74
|
+
const nonce = readString(payload.nonce) ?? normalizedHeaders['payment-nonce'];
|
|
75
|
+
const expiresAt = readPositiveNumber(payload.expiresAt ?? normalizedHeaders['payment-expires-at']);
|
|
76
|
+
|
|
77
|
+
if (!paymentAddress || !amount || !currency || !network || !nonce || expiresAt === undefined) {
|
|
78
|
+
throw new Error('402 response did not contain a supported payment challenge.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
paymentAddress,
|
|
83
|
+
amount,
|
|
84
|
+
currency,
|
|
85
|
+
network: normalizeNetwork(network),
|
|
86
|
+
nonce,
|
|
87
|
+
expiresAt,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async signPayment(request: X402PaymentRequest): Promise<X402PaymentSignature> {
|
|
92
|
+
assertNotExpired(request);
|
|
93
|
+
assertWalletMatchesNetwork(this.wallet.chain.id, request.network);
|
|
94
|
+
|
|
95
|
+
const signature = await this.wallet.signTypedData(buildTypedData(request, this.wallet.address));
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
signature,
|
|
99
|
+
payerAddress: this.wallet.address,
|
|
100
|
+
network: normalizeNetwork(request.network),
|
|
101
|
+
nonce: request.nonce,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async createPaymentHeader(request: X402PaymentRequest): Promise<string> {
|
|
106
|
+
const internalRequest = request as InternalX402PaymentRequest;
|
|
107
|
+
if (internalRequest.__x402) {
|
|
108
|
+
const scheme = new ExactEvmScheme({
|
|
109
|
+
address: this.wallet.address as `0x${string}`,
|
|
110
|
+
signTypedData: (message) => this.wallet.signTypedData(message) as Promise<`0x${string}`>,
|
|
111
|
+
readContract: (args) => this.wallet.getPublicClient().readContract(args),
|
|
112
|
+
});
|
|
113
|
+
const { x402Version, requirements } = internalRequest.__x402;
|
|
114
|
+
const result = await scheme.createPaymentPayload(x402Version, requirements);
|
|
115
|
+
|
|
116
|
+
return encodePaymentSignatureHeader({
|
|
117
|
+
x402Version: result.x402Version,
|
|
118
|
+
accepted: requirements,
|
|
119
|
+
payload: result.payload,
|
|
120
|
+
extensions: result.extensions,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const signature = await this.signPayment(request);
|
|
125
|
+
return Buffer.from(JSON.stringify(signature)).toString('base64');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async verifyPayment(signature: X402PaymentSignature, request: X402PaymentRequest): Promise<boolean> {
|
|
129
|
+
if (Date.now() > request.expiresAt) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (normalizeNetwork(signature.network) !== normalizeNetwork(request.network) || signature.nonce !== request.nonce) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
return await verifyTypedData({
|
|
139
|
+
address: normalizeAddress(signature.payerAddress),
|
|
140
|
+
...buildTypedData(request, signature.payerAddress),
|
|
141
|
+
signature: signature.signature as `0x${string}`,
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildTypedData(request: X402PaymentRequest, payerAddress: string) {
|
|
150
|
+
return {
|
|
151
|
+
domain: {
|
|
152
|
+
name: 'AgenticMeshX402',
|
|
153
|
+
version: '1',
|
|
154
|
+
chainId: getChainId(request.network),
|
|
155
|
+
verifyingContract: normalizeAddress(request.paymentAddress),
|
|
156
|
+
},
|
|
157
|
+
types: X402_TYPED_DATA_TYPES,
|
|
158
|
+
primaryType: 'PaymentAuthorization' as const,
|
|
159
|
+
message: {
|
|
160
|
+
payerAddress: normalizeAddress(payerAddress),
|
|
161
|
+
paymentAddress: normalizeAddress(request.paymentAddress),
|
|
162
|
+
amount: BigInt(request.amount),
|
|
163
|
+
currency: request.currency,
|
|
164
|
+
nonce: request.nonce,
|
|
165
|
+
expiresAt: BigInt(request.expiresAt),
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeHeaders(headers: Record<string, string>): Record<string, string> {
|
|
171
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function extractBodyPayload(body: unknown): Record<string, unknown> {
|
|
175
|
+
if (!body || typeof body !== 'object') {
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const record = body as Record<string, unknown>;
|
|
180
|
+
if (record.payment && typeof record.payment === 'object') {
|
|
181
|
+
return record.payment as Record<string, unknown>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (record.paymentRequest && typeof record.paymentRequest === 'object') {
|
|
185
|
+
return record.paymentRequest as Record<string, unknown>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return record;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function selectRequirement(
|
|
192
|
+
accepts: PaymentRequirements | PaymentRequirements[],
|
|
193
|
+
chainId: number,
|
|
194
|
+
): PaymentRequirements {
|
|
195
|
+
const requirements = Array.isArray(accepts) ? accepts : [accepts];
|
|
196
|
+
const preferredNetwork = chainIdToNetwork(chainId);
|
|
197
|
+
return requirements.find((entry) => normalizeNetwork(entry.network) === preferredNetwork) ?? requirements[0]!;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function inferCurrency(requirements: PaymentRequirements): string {
|
|
201
|
+
const explicit = readString(requirements.extra?.currency) ?? readString(requirements.extra?.symbol);
|
|
202
|
+
if (explicit) {
|
|
203
|
+
return explicit.toUpperCase();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const network = normalizeNetwork(requirements.network);
|
|
207
|
+
const usdcAddress = network in USDC_ADDRESS ? USDC_ADDRESS[network as keyof typeof USDC_ADDRESS] : undefined;
|
|
208
|
+
if (usdcAddress && normalizeAddress(requirements.asset) === normalizeAddress(usdcAddress)) {
|
|
209
|
+
return 'USDC';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (normalizeAddress(requirements.asset) === normalizeAddress(PERMIT2_ADDRESS)) {
|
|
213
|
+
return 'ETH';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return requirements.asset;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeNetwork(network: string): string {
|
|
220
|
+
const normalized = network.trim().toLowerCase();
|
|
221
|
+
switch (normalized) {
|
|
222
|
+
case 'base':
|
|
223
|
+
case 'eip155:8453':
|
|
224
|
+
return 'base';
|
|
225
|
+
case 'base-sepolia':
|
|
226
|
+
case 'eip155:84532':
|
|
227
|
+
return 'base-sepolia';
|
|
228
|
+
case 'localhost':
|
|
229
|
+
case 'anvil':
|
|
230
|
+
case 'eip155:31337':
|
|
231
|
+
return 'localhost';
|
|
232
|
+
default:
|
|
233
|
+
return normalized;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function chainIdToNetwork(chainId: number): string {
|
|
238
|
+
switch (chainId) {
|
|
239
|
+
case 8453:
|
|
240
|
+
return 'base';
|
|
241
|
+
case 84_532:
|
|
242
|
+
return 'base-sepolia';
|
|
243
|
+
case 31_337:
|
|
244
|
+
return 'localhost';
|
|
245
|
+
default:
|
|
246
|
+
return `eip155:${chainId}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getChainId(network: string): number {
|
|
251
|
+
switch (normalizeNetwork(network)) {
|
|
252
|
+
case 'base':
|
|
253
|
+
return 8453;
|
|
254
|
+
case 'base-sepolia':
|
|
255
|
+
return 84_532;
|
|
256
|
+
case 'localhost':
|
|
257
|
+
return 31_337;
|
|
258
|
+
default:
|
|
259
|
+
throw new Error(`Unsupported x402 network: ${network}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function assertNotExpired(request: X402PaymentRequest): void {
|
|
264
|
+
if (Date.now() > request.expiresAt) {
|
|
265
|
+
throw new Error('Payment challenge has expired.');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function assertWalletMatchesNetwork(chainId: number, network: string): void {
|
|
270
|
+
const walletNetwork = chainIdToNetwork(chainId);
|
|
271
|
+
const requestNetwork = normalizeNetwork(network);
|
|
272
|
+
if (walletNetwork !== requestNetwork) {
|
|
273
|
+
throw new Error(`Wallet network ${walletNetwork} does not match payment network ${requestNetwork}.`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeAddress(address: string): `0x${string}` {
|
|
278
|
+
return getAddress(address);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function readString(value: unknown): string | undefined {
|
|
282
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function readPositiveNumber(value: unknown): number | undefined {
|
|
286
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim())) {
|
|
291
|
+
return Number(value.trim());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './client.js';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { pollDeviceFlow, startDeviceFlow } from '../../src/auth/device-flow.js';
|
|
4
|
+
import type { OAuthConfig } from '../../src/auth/types.js';
|
|
5
|
+
|
|
6
|
+
const config: OAuthConfig = {
|
|
7
|
+
provider: 'google',
|
|
8
|
+
clientId: 'test-client',
|
|
9
|
+
redirectUri: 'http://127.0.0.1/callback',
|
|
10
|
+
deviceCodeEndpoint: 'https://oidc.example/device',
|
|
11
|
+
tokenEndpoint: 'https://oidc.example/token',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.unstubAllGlobals();
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('device flow', () => {
|
|
20
|
+
it('starts the device authorization flow', async () => {
|
|
21
|
+
const fetchMock = vi.fn().mockResolvedValue(
|
|
22
|
+
new Response(
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
user_code: 'ABCD-EFGH',
|
|
25
|
+
verification_uri: 'https://oidc.example/verify',
|
|
26
|
+
device_code: 'device-code',
|
|
27
|
+
interval: 3,
|
|
28
|
+
expires_in: 900,
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
33
|
+
|
|
34
|
+
const result = await startDeviceFlow(config);
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual({
|
|
37
|
+
userCode: 'ABCD-EFGH',
|
|
38
|
+
verificationUri: 'https://oidc.example/verify',
|
|
39
|
+
deviceCode: 'device-code',
|
|
40
|
+
pollInterval: 3,
|
|
41
|
+
expiresIn: 900,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns null while authorization is pending and then returns tokens', async () => {
|
|
46
|
+
const fetchMock = vi
|
|
47
|
+
.fn()
|
|
48
|
+
.mockResolvedValueOnce(
|
|
49
|
+
new Response(JSON.stringify({ error: 'authorization_pending' }), { status: 400 }),
|
|
50
|
+
)
|
|
51
|
+
.mockResolvedValueOnce(
|
|
52
|
+
new Response(JSON.stringify({ id_token: 'jwt-token', refresh_token: 'refresh-token' })),
|
|
53
|
+
);
|
|
54
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
55
|
+
|
|
56
|
+
expect(await pollDeviceFlow('device-code', config)).toBeNull();
|
|
57
|
+
await expect(pollDeviceFlow('device-code', config)).resolves.toEqual({
|
|
58
|
+
jwt: 'jwt-token',
|
|
59
|
+
refreshToken: 'refresh-token',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Ed25519AuthProvider } from '../../src/auth/ed25519-provider.js';
|
|
4
|
+
import { createDID } from '../../src/identity/did.js';
|
|
5
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
6
|
+
|
|
7
|
+
describe('Ed25519AuthProvider', () => {
|
|
8
|
+
it('wraps an Ed25519 keypair as an auth provider', async () => {
|
|
9
|
+
const keypair = Ed25519Keypair.generate();
|
|
10
|
+
const provider = new Ed25519AuthProvider(keypair);
|
|
11
|
+
|
|
12
|
+
expect(await provider.getAddress()).toBe(keypair.toSuiAddress());
|
|
13
|
+
expect(provider.getDID()).toBe(createDID(keypair.getPublicKey().toRawBytes()));
|
|
14
|
+
expect(provider.isAuthenticated()).toBe(true);
|
|
15
|
+
expect(provider.getPublicKey()).toEqual(keypair.getPublicKey().toRawBytes());
|
|
16
|
+
|
|
17
|
+
const signedTransaction = await provider.signTransaction(new Uint8Array([1, 2, 3]));
|
|
18
|
+
const signedMessage = await provider.signPersonalMessage(new Uint8Array([4, 5, 6]));
|
|
19
|
+
|
|
20
|
+
expect(signedTransaction.length).toBeGreaterThan(0);
|
|
21
|
+
expect(signedMessage.signature.length).toBeGreaterThan(0);
|
|
22
|
+
expect(provider.toSuiSigner().toSuiAddress()).toBe(await provider.getAddress());
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { deriveEvmKey } from '../../src/auth/evm-key.js';
|
|
4
|
+
|
|
5
|
+
describe('deriveEvmKey', () => {
|
|
6
|
+
it('derives the same key for the same inputs', () => {
|
|
7
|
+
const first = deriveEvmKey(new Uint8Array([1, 2, 3, 4]), 'salt-1', 'user-1');
|
|
8
|
+
const second = deriveEvmKey(new Uint8Array([1, 2, 3, 4]), 'salt-1', 'user-1');
|
|
9
|
+
|
|
10
|
+
expect(Buffer.from(first).toString('hex')).toBe(Buffer.from(second).toString('hex'));
|
|
11
|
+
expect(first).toHaveLength(32);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('changes when any input changes, even for concatenation-collision cases', () => {
|
|
15
|
+
const base = Buffer.from(deriveEvmKey(new Uint8Array([1, 2, 3, 4]), 'salt-1', 'user-1')).toString('hex');
|
|
16
|
+
const differentSalt = Buffer.from(deriveEvmKey(new Uint8Array([1, 2, 3, 4]), 'salt-2', 'user-1')).toString('hex');
|
|
17
|
+
const differentSub = Buffer.from(deriveEvmKey(new Uint8Array([1, 2, 3, 4]), 'salt-1', 'user-2')).toString('hex');
|
|
18
|
+
const collisionA = Buffer.from(deriveEvmKey(new Uint8Array([1, 2, 3, 4]), 'ab', 'c')).toString('hex');
|
|
19
|
+
const collisionB = Buffer.from(deriveEvmKey(new Uint8Array([1, 2, 3, 4]), 'a', 'bc')).toString('hex');
|
|
20
|
+
|
|
21
|
+
expect(differentSalt).not.toBe(base);
|
|
22
|
+
expect(differentSub).not.toBe(base);
|
|
23
|
+
expect(collisionA).not.toBe(collisionB);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('rejects empty inputs', () => {
|
|
27
|
+
expect(() => deriveEvmKey(new Uint8Array(), 'salt-1', 'user-1')).toThrow('identityPrivateKey must not be empty.');
|
|
28
|
+
expect(() => deriveEvmKey(new Uint8Array([1]), ' ', 'user-1')).toThrow('userSalt must not be empty.');
|
|
29
|
+
expect(() => deriveEvmKey(new Uint8Array([1]), 'salt-1', ' ')).toThrow('oauthSub must not be empty.');
|
|
30
|
+
});
|
|
31
|
+
});
|