@teneo-protocol/sdk 3.1.4 → 3.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/README.md +1 -1
- package/dist/core/websocket-client.d.ts +5 -0
- package/dist/core/websocket-client.d.ts.map +1 -1
- package/dist/core/websocket-client.js +51 -37
- package/dist/core/websocket-client.js.map +1 -1
- package/dist/handlers/webhook-handler.d.ts.map +1 -1
- package/dist/handlers/webhook-handler.js +4 -2
- package/dist/handlers/webhook-handler.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/managers/message-router.d.ts +2 -0
- package/dist/managers/message-router.d.ts.map +1 -1
- package/dist/managers/message-router.js +28 -4
- package/dist/managers/message-router.js.map +1 -1
- package/dist/teneo-sdk.d.ts +7 -3
- package/dist/teneo-sdk.d.ts.map +1 -1
- package/dist/teneo-sdk.js +7 -2
- package/dist/teneo-sdk.js.map +1 -1
- package/dist/types/config.d.ts +39 -12
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +22 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -2
- package/dist/types/index.js.map +1 -1
- package/dist/types/messages.d.ts +65 -6
- package/dist/types/messages.d.ts.map +1 -1
- package/dist/types/messages.js +29 -8
- package/dist/types/messages.js.map +1 -1
- package/dist/utils/erc20.d.ts +65 -0
- package/dist/utils/erc20.d.ts.map +1 -0
- package/dist/utils/erc20.js +116 -0
- package/dist/utils/erc20.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +9 -1
- package/dist/utils/index.js.map +1 -1
- package/examples/api-key-payment-flow.ts +103 -0
- package/package.json +1 -1
- package/src/core/websocket-client.ts +60 -39
- package/src/handlers/webhook-handler.ts +4 -2
- package/src/index.ts +8 -1
- package/src/managers/message-router.ts +40 -4
- package/src/teneo-sdk.ts +8 -3
- package/src/types/config.ts +23 -0
- package/src/types/index.ts +1 -0
- package/src/types/messages.ts +38 -8
- package/src/utils/erc20.test.ts +161 -0
- package/src/utils/erc20.ts +125 -0
- package/src/utils/index.ts +10 -0
- package/tsconfig.json +2 -1
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
BaseMessageSchema,
|
|
20
20
|
createRequestChallenge,
|
|
21
21
|
createCheckCachedAuth,
|
|
22
|
+
createApiKeyAuth,
|
|
22
23
|
createPing,
|
|
23
24
|
safeParseMessage,
|
|
24
25
|
Logger
|
|
@@ -606,7 +607,7 @@ export class WebSocketClient extends EventEmitter<SDKEvents> {
|
|
|
606
607
|
* Authenticate with the server
|
|
607
608
|
*/
|
|
608
609
|
private async authenticate(): Promise<void> {
|
|
609
|
-
if (!this.account && !this.config.walletAddress) {
|
|
610
|
+
if (!this.account && !this.config.walletAddress && !this.config.apiKey) {
|
|
610
611
|
this.logger.info("No authentication configured, continuing without auth");
|
|
611
612
|
this.updateAuthState({ authenticated: false });
|
|
612
613
|
this.emit("ready");
|
|
@@ -614,6 +615,22 @@ export class WebSocketClient extends EventEmitter<SDKEvents> {
|
|
|
614
615
|
}
|
|
615
616
|
|
|
616
617
|
try {
|
|
618
|
+
// API-key auth: single message, no challenge-response needed
|
|
619
|
+
if (this.config.apiKey) {
|
|
620
|
+
const address = this.account?.address || this.config.walletAddress;
|
|
621
|
+
if (!address) {
|
|
622
|
+
throw new AuthenticationError("walletAddress is required for API-key authentication");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
this.logger.debug("Authenticating with API key");
|
|
626
|
+
await this.sendMessage(
|
|
627
|
+
createApiKeyAuth(address, this.config.apiKey, this.config.clientType || "user")
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
await this.waitForAuthCompletion();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
617
634
|
// Check for cached authentication first
|
|
618
635
|
if (this.config.walletAddress) {
|
|
619
636
|
const sessionToken = this.authState.sessionToken;
|
|
@@ -631,7 +648,7 @@ export class WebSocketClient extends EventEmitter<SDKEvents> {
|
|
|
631
648
|
}
|
|
632
649
|
}
|
|
633
650
|
|
|
634
|
-
//
|
|
651
|
+
// Challenge-response auth (requires privateKey)
|
|
635
652
|
this.logger.debug("Requesting authentication challenge");
|
|
636
653
|
await this.sendMessage(
|
|
637
654
|
createRequestChallenge(
|
|
@@ -641,49 +658,53 @@ export class WebSocketClient extends EventEmitter<SDKEvents> {
|
|
|
641
658
|
);
|
|
642
659
|
|
|
643
660
|
// Wait for authentication to complete
|
|
644
|
-
await
|
|
645
|
-
let timeout: NodeJS.Timeout | undefined;
|
|
646
|
-
const pollTimeouts: NodeJS.Timeout[] = [];
|
|
647
|
-
|
|
648
|
-
// Centralized cleanup function - guarantees cleanup in all scenarios
|
|
649
|
-
const cleanup = () => {
|
|
650
|
-
if (timeout) {
|
|
651
|
-
clearTimeout(timeout);
|
|
652
|
-
timeout = undefined;
|
|
653
|
-
}
|
|
654
|
-
// Clear all polling timeouts
|
|
655
|
-
pollTimeouts.forEach((t) => clearTimeout(t));
|
|
656
|
-
pollTimeouts.length = 0;
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
// Set main authentication timeout
|
|
660
|
-
timeout = setTimeout(() => {
|
|
661
|
-
cleanup();
|
|
662
|
-
reject(new AuthenticationError("Authentication timeout"));
|
|
663
|
-
}, TIMEOUTS.AUTH_TIMEOUT);
|
|
664
|
-
|
|
665
|
-
const checkAuth = () => {
|
|
666
|
-
if (this.authState.authenticated) {
|
|
667
|
-
cleanup();
|
|
668
|
-
resolve();
|
|
669
|
-
} else if (this.connectionState.lastError) {
|
|
670
|
-
cleanup();
|
|
671
|
-
reject(this.connectionState.lastError);
|
|
672
|
-
} else {
|
|
673
|
-
// Store polling timeout for cleanup
|
|
674
|
-
const pollTimeout = setTimeout(checkAuth, TIMEOUTS.AUTH_POLL_INTERVAL);
|
|
675
|
-
pollTimeouts.push(pollTimeout);
|
|
676
|
-
}
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
checkAuth();
|
|
680
|
-
});
|
|
661
|
+
await this.waitForAuthCompletion();
|
|
681
662
|
} catch (error) {
|
|
682
663
|
this.logger.error("Authentication failed", error);
|
|
683
664
|
throw new AuthenticationError("Failed to authenticate", error);
|
|
684
665
|
}
|
|
685
666
|
}
|
|
686
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Waits for authentication state to become authenticated.
|
|
670
|
+
* Used by both API-key and challenge-response auth flows.
|
|
671
|
+
*/
|
|
672
|
+
private async waitForAuthCompletion(): Promise<void> {
|
|
673
|
+
await new Promise<void>((resolve, reject) => {
|
|
674
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
675
|
+
const pollTimeouts: NodeJS.Timeout[] = [];
|
|
676
|
+
|
|
677
|
+
const cleanup = () => {
|
|
678
|
+
if (timeout) {
|
|
679
|
+
clearTimeout(timeout);
|
|
680
|
+
timeout = undefined;
|
|
681
|
+
}
|
|
682
|
+
pollTimeouts.forEach((t) => clearTimeout(t));
|
|
683
|
+
pollTimeouts.length = 0;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
timeout = setTimeout(() => {
|
|
687
|
+
cleanup();
|
|
688
|
+
reject(new AuthenticationError("Authentication timeout"));
|
|
689
|
+
}, TIMEOUTS.AUTH_TIMEOUT);
|
|
690
|
+
|
|
691
|
+
const checkAuth = () => {
|
|
692
|
+
if (this.authState.authenticated) {
|
|
693
|
+
cleanup();
|
|
694
|
+
resolve();
|
|
695
|
+
} else if (this.connectionState.lastError) {
|
|
696
|
+
cleanup();
|
|
697
|
+
reject(this.connectionState.lastError);
|
|
698
|
+
} else {
|
|
699
|
+
const pollTimeout = setTimeout(checkAuth, TIMEOUTS.AUTH_POLL_INTERVAL);
|
|
700
|
+
pollTimeouts.push(pollTimeout);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
checkAuth();
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
687
708
|
/**
|
|
688
709
|
* Create handler context with all dependencies
|
|
689
710
|
*/
|
|
@@ -280,8 +280,10 @@ export class WebhookHandler extends EventEmitter<SDKEvents> {
|
|
|
280
280
|
await this.sendWebhook(eventType, validatedMessage, metadata);
|
|
281
281
|
} catch (error) {
|
|
282
282
|
if (error instanceof z.ZodError) {
|
|
283
|
-
this.logger.
|
|
284
|
-
|
|
283
|
+
this.logger.debug("Skipping webhook for unrecognized message type", {
|
|
284
|
+
type: message.type
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
285
287
|
}
|
|
286
288
|
throw error;
|
|
287
289
|
}
|
package/src/index.ts
CHANGED
|
@@ -151,6 +151,7 @@ export {
|
|
|
151
151
|
createRequestChallenge,
|
|
152
152
|
createCheckCachedAuth,
|
|
153
153
|
createAuth,
|
|
154
|
+
createApiKeyAuth,
|
|
154
155
|
createUserMessage,
|
|
155
156
|
createPing,
|
|
156
157
|
createSubscribe,
|
|
@@ -233,6 +234,12 @@ export {
|
|
|
233
234
|
*/
|
|
234
235
|
export { SecurePrivateKey } from "./utils/secure-private-key";
|
|
235
236
|
|
|
237
|
+
/**
|
|
238
|
+
* ERC20 utilities for wallet transaction flows
|
|
239
|
+
* Check allowances to skip unnecessary approval transactions
|
|
240
|
+
*/
|
|
241
|
+
export { checkERC20Allowance, parseApproveCalldata, ERC20_APPROVE_SELECTOR } from "./utils/erc20";
|
|
242
|
+
|
|
236
243
|
/**
|
|
237
244
|
* Payment utilities for x402 payment signing
|
|
238
245
|
* Enables SDK users to make payments directly using private keys
|
|
@@ -264,7 +271,7 @@ export {
|
|
|
264
271
|
/**
|
|
265
272
|
* SDK version string
|
|
266
273
|
*/
|
|
267
|
-
export const VERSION = "3.0
|
|
274
|
+
export const VERSION = "3.2.0";
|
|
268
275
|
|
|
269
276
|
/**
|
|
270
277
|
* Convenience type re-exports for message operations
|
|
@@ -86,6 +86,7 @@ export interface MessageRouterConfig {
|
|
|
86
86
|
maxPricePerRequest?: number;
|
|
87
87
|
quoteTimeout?: number;
|
|
88
88
|
wsUrl?: string;
|
|
89
|
+
apiKey?: string; // API key for session-key payment flow
|
|
89
90
|
paymentNetwork?: string; // CAIP-2 format (e.g., "eip155:3338")
|
|
90
91
|
paymentAsset?: string;
|
|
91
92
|
network?: string; // Network name (e.g., "peaq", "base", "avalanche")
|
|
@@ -107,6 +108,7 @@ export class MessageRouter extends EventEmitter<SDKEvents> {
|
|
|
107
108
|
private readonly maxPricePerRequest?: number;
|
|
108
109
|
private readonly quoteTimeout: number;
|
|
109
110
|
private readonly wsUrl: string;
|
|
111
|
+
private readonly apiKey?: string; // API key for session-key payment
|
|
110
112
|
private readonly paymentNetwork: string; // CAIP-2 format if set
|
|
111
113
|
private readonly paymentAsset: string;
|
|
112
114
|
private readonly networkName: string; // Network name (peaq, base, avalanche)
|
|
@@ -135,6 +137,7 @@ export class MessageRouter extends EventEmitter<SDKEvents> {
|
|
|
135
137
|
this.maxPricePerRequest = config.maxPricePerRequest;
|
|
136
138
|
this.quoteTimeout = config.quoteTimeout ?? 30000;
|
|
137
139
|
this.wsUrl = config.wsUrl ?? "";
|
|
140
|
+
this.apiKey = config.apiKey;
|
|
138
141
|
|
|
139
142
|
// Store config values - dynamic network resolution happens lazily in getPaymentNetwork/Asset()
|
|
140
143
|
// because networks are only initialized after connect() is called
|
|
@@ -627,7 +630,7 @@ export class MessageRouter extends EventEmitter<SDKEvents> {
|
|
|
627
630
|
let paymentHeader: string | undefined;
|
|
628
631
|
|
|
629
632
|
// Create payment header if payment client is configured and price > 0
|
|
630
|
-
if (this.paymentClient && quote.pricing.pricePerUnit > 0) {
|
|
633
|
+
if (this.paymentClient && quote.pricing.pricePerUnit > 0 && !this.apiKey) {
|
|
631
634
|
try {
|
|
632
635
|
// Pass backend-provided settlement data to payment header creation
|
|
633
636
|
paymentHeader = await this.paymentClient.createPaymentHeader(
|
|
@@ -650,7 +653,12 @@ export class MessageRouter extends EventEmitter<SDKEvents> {
|
|
|
650
653
|
}
|
|
651
654
|
}
|
|
652
655
|
|
|
653
|
-
|
|
656
|
+
// Build confirm message — x402 payment or API-key, whichever is available
|
|
657
|
+
const confirmMessage = createConfirmTask(taskId, {
|
|
658
|
+
x402Payment: paymentHeader,
|
|
659
|
+
apiKey: this.apiKey,
|
|
660
|
+
network: this.apiKey ? this.networkName || undefined : undefined
|
|
661
|
+
});
|
|
654
662
|
|
|
655
663
|
this.logger.debug("MessageRouter: Confirming quote", { taskId });
|
|
656
664
|
|
|
@@ -661,12 +669,40 @@ export class MessageRouter extends EventEmitter<SDKEvents> {
|
|
|
661
669
|
|
|
662
670
|
if (options?.waitForResponse) {
|
|
663
671
|
const timeout = options.timeout ?? this.messageTimeout;
|
|
664
|
-
|
|
672
|
+
|
|
673
|
+
// Race agent:response against error events so backend rejections
|
|
674
|
+
// (e.g. 402 "Payment verification failed") surface immediately
|
|
675
|
+
// instead of silently waiting until timeout.
|
|
676
|
+
const responsePromise = waitForEvent<AgentResponse>(this.wsClient, "agent:response", {
|
|
665
677
|
timeout,
|
|
666
678
|
filter: (r) => r.taskId === taskId,
|
|
667
679
|
timeoutMessage: `Task response timed out after ${timeout}ms (taskId: ${taskId})`
|
|
668
680
|
});
|
|
669
|
-
|
|
681
|
+
|
|
682
|
+
const errorPromise = waitForEvent<MessageError>(this.wsClient, "error", {
|
|
683
|
+
timeout: timeout + 1000
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const agentErrorPromise = waitForEvent<{ agentName?: string; content?: string; taskId?: string; room?: string }>(
|
|
687
|
+
this.wsClient, "agent:error", {
|
|
688
|
+
timeout: timeout + 1000,
|
|
689
|
+
filter: (e) => e.taskId === taskId
|
|
690
|
+
}
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
const result = await Promise.race([
|
|
694
|
+
responsePromise,
|
|
695
|
+
errorPromise.then((err) => { throw err; }),
|
|
696
|
+
agentErrorPromise.then((e) => {
|
|
697
|
+
throw new SDKError(
|
|
698
|
+
e.content || "Agent error during task execution",
|
|
699
|
+
ErrorCode.MESSAGE_ERROR,
|
|
700
|
+
{ taskId, agentName: e.agentName }
|
|
701
|
+
);
|
|
702
|
+
})
|
|
703
|
+
]);
|
|
704
|
+
|
|
705
|
+
return result as FormattedResponse;
|
|
670
706
|
}
|
|
671
707
|
}
|
|
672
708
|
|
package/src/teneo-sdk.ts
CHANGED
|
@@ -211,6 +211,7 @@ export class TeneoSDK extends EventEmitter<SDKEvents> {
|
|
|
211
211
|
maxPricePerRequest: this.config.maxPricePerRequest,
|
|
212
212
|
quoteTimeout: this.config.quoteTimeout,
|
|
213
213
|
wsUrl: this.config.wsUrl,
|
|
214
|
+
apiKey: this.config.apiKey,
|
|
214
215
|
paymentNetwork: this.config.paymentNetwork,
|
|
215
216
|
paymentAsset: this.config.paymentAsset,
|
|
216
217
|
network: this.config.network, // Network name from withNetwork()
|
|
@@ -1344,8 +1345,8 @@ export class TeneoSDK extends EventEmitter<SDKEvents> {
|
|
|
1344
1345
|
* when a chainId is provided, matching the format the UI uses.
|
|
1345
1346
|
*
|
|
1346
1347
|
* @param taskId - The task ID from the wallet:tx_requested event
|
|
1347
|
-
* @param status - Transaction result: "confirmed", "rejected", or "failed"
|
|
1348
|
-
* @param txHash - The on-chain transaction hash (required for "confirmed" status)
|
|
1348
|
+
* @param status - Transaction result: "broadcasted" (tx sent, hash available), "confirmed" (on-chain receipt), "rejected" (user declined), or "failed" (error)
|
|
1349
|
+
* @param txHash - The on-chain transaction hash (required for "broadcasted" and "confirmed" status)
|
|
1349
1350
|
* @param error - Error message (optional, for "failed" status)
|
|
1350
1351
|
* @param room - The room ID from the wallet:tx_requested event (required for routing)
|
|
1351
1352
|
* @param chainId - The chain ID from data.tx.chainId (used to format txHash with network name)
|
|
@@ -1356,6 +1357,10 @@ export class TeneoSDK extends EventEmitter<SDKEvents> {
|
|
|
1356
1357
|
* sdk.on("wallet:tx_requested", async (data) => {
|
|
1357
1358
|
* try {
|
|
1358
1359
|
* const txHash = await wallet.sendTransaction(data.tx);
|
|
1360
|
+
* // Notify agent that tx was broadcast (hash available, not yet confirmed)
|
|
1361
|
+
* await sdk.sendTxResult(data.taskId, "broadcasted", txHash, undefined, data.room, data.tx.chainId);
|
|
1362
|
+
* // Wait for on-chain confirmation
|
|
1363
|
+
* await waitForReceipt(txHash);
|
|
1359
1364
|
* await sdk.sendTxResult(data.taskId, "confirmed", txHash, undefined, data.room, data.tx.chainId);
|
|
1360
1365
|
* } catch (err) {
|
|
1361
1366
|
* await sdk.sendTxResult(data.taskId, "failed", undefined, err.message, data.room, data.tx.chainId);
|
|
@@ -1365,7 +1370,7 @@ export class TeneoSDK extends EventEmitter<SDKEvents> {
|
|
|
1365
1370
|
*/
|
|
1366
1371
|
public async sendTxResult(
|
|
1367
1372
|
taskId: string,
|
|
1368
|
-
status: "confirmed" | "rejected" | "failed",
|
|
1373
|
+
status: "broadcasted" | "confirmed" | "rejected" | "failed",
|
|
1369
1374
|
txHash?: string,
|
|
1370
1375
|
error?: string,
|
|
1371
1376
|
room?: string,
|
package/src/types/config.ts
CHANGED
|
@@ -86,6 +86,7 @@ const SDKConfigBaseSchema = z.object({
|
|
|
86
86
|
// Authentication
|
|
87
87
|
privateKey: PrivateKeySchema.optional(),
|
|
88
88
|
walletAddress: z.string().optional(),
|
|
89
|
+
apiKey: z.string().optional(), // API key for session-key auth & payment (no privateKey needed)
|
|
89
90
|
|
|
90
91
|
// Client identification
|
|
91
92
|
clientType: ClientTypeSchema.optional(),
|
|
@@ -432,6 +433,28 @@ export class SDKConfigBuilder {
|
|
|
432
433
|
return this;
|
|
433
434
|
}
|
|
434
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Sets the API key for session-key-based authentication and payment.
|
|
438
|
+
* When set, the SDK authenticates with the API key directly (no challenge-response)
|
|
439
|
+
* and uses it for task confirmations instead of x402 payment signing.
|
|
440
|
+
*
|
|
441
|
+
* @param apiKey - API key string (e.g., 'sk_live_...')
|
|
442
|
+
* @param walletAddress - Wallet address associated with the API key
|
|
443
|
+
* @returns this builder for method chaining
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```typescript
|
|
447
|
+
* builder.withApiKey('sk_live_kQp3x...', '0xYourWalletAddress')
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
withApiKey(apiKey: string, walletAddress?: string): this {
|
|
451
|
+
this.config.apiKey = z.string().min(1).parse(apiKey);
|
|
452
|
+
if (walletAddress) {
|
|
453
|
+
this.config.walletAddress = z.string().parse(walletAddress);
|
|
454
|
+
}
|
|
455
|
+
return this;
|
|
456
|
+
}
|
|
457
|
+
|
|
435
458
|
/**
|
|
436
459
|
* Configures webhook URL and optional HTTP headers for receiving real-time event notifications.
|
|
437
460
|
* Webhook URL must use HTTPS for non-localhost endpoints (security requirement).
|
package/src/types/index.ts
CHANGED
package/src/types/messages.ts
CHANGED
|
@@ -31,7 +31,7 @@ const stringToBoolean = z
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Accept falsy values
|
|
34
|
-
if (normalized === "false" || normalized === "0" || normalized === "no") {
|
|
34
|
+
if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "skipped") {
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -135,7 +135,10 @@ export const MessageTypeSchema = z.enum([
|
|
|
135
135
|
|
|
136
136
|
// Wallet Transaction Flow
|
|
137
137
|
"trigger_wallet_tx",
|
|
138
|
-
"tx_result"
|
|
138
|
+
"tx_result",
|
|
139
|
+
|
|
140
|
+
// Agent Owner Wallets
|
|
141
|
+
"agent_owner_wallets"
|
|
139
142
|
]);
|
|
140
143
|
|
|
141
144
|
export const ContentTypeSchema = z.enum([
|
|
@@ -327,7 +330,10 @@ export const AuthMessageSchema = BaseMessageSchema.extend({
|
|
|
327
330
|
jwt_token: z.string().optional(), // JWT token for KeyVault API authentication
|
|
328
331
|
session_token: z.string().optional(), // 64-char hex token for fast re-auth (24h validity)
|
|
329
332
|
whitelist_verified: z.union([z.boolean(), z.string()]).optional(), // Whitelist verification status
|
|
330
|
-
user_count: z.number().optional() // Total user count (admin only)
|
|
333
|
+
user_count: z.number().optional(), // Total user count (admin only)
|
|
334
|
+
// API-key auth fields
|
|
335
|
+
api_key: z.string().optional(), // API key for session-key auth (no challenge-response needed)
|
|
336
|
+
platform: z.string().optional() // Platform identifier (e.g., "community")
|
|
331
337
|
})
|
|
332
338
|
.optional()
|
|
333
339
|
});
|
|
@@ -504,7 +510,9 @@ export const TaskQuoteMessageSchema = BaseMessageSchema.extend({
|
|
|
504
510
|
export const ConfirmTaskMessageSchema = BaseMessageSchema.extend({
|
|
505
511
|
type: z.literal("confirm_task"),
|
|
506
512
|
data: z.object({
|
|
507
|
-
task_id: z.string()
|
|
513
|
+
task_id: z.string(),
|
|
514
|
+
api_key: z.string().optional(), // API key for session-key payment flow
|
|
515
|
+
network: z.string().optional() // Payment network: "peaq", "base", "avalanche", "x-layer"
|
|
508
516
|
}),
|
|
509
517
|
payment: z.string().optional() // x402 payment at top level (backend checks msg.Payment)
|
|
510
518
|
});
|
|
@@ -1043,7 +1051,7 @@ export const AgentErrorMessageSchema = BaseMessageSchema.extend({
|
|
|
1043
1051
|
export type AgentErrorMessage = z.infer<typeof AgentErrorMessageSchema>;
|
|
1044
1052
|
|
|
1045
1053
|
// Wallet Transaction schemas
|
|
1046
|
-
export const TxResultStatusSchema = z.enum(["confirmed", "rejected", "failed"]);
|
|
1054
|
+
export const TxResultStatusSchema = z.enum(["broadcasted", "confirmed", "rejected", "failed"]);
|
|
1047
1055
|
export type TxResultStatus = z.infer<typeof TxResultStatusSchema>;
|
|
1048
1056
|
|
|
1049
1057
|
export const TriggerWalletTxMessageSchema = BaseMessageSchema.extend({
|
|
@@ -1283,6 +1291,23 @@ export function createAuth(
|
|
|
1283
1291
|
});
|
|
1284
1292
|
}
|
|
1285
1293
|
|
|
1294
|
+
export function createApiKeyAuth(
|
|
1295
|
+
address: string,
|
|
1296
|
+
apiKey: string,
|
|
1297
|
+
userType: ClientType = "user",
|
|
1298
|
+
platform: string = "community"
|
|
1299
|
+
): AuthMessage {
|
|
1300
|
+
return AuthMessageSchema.parse({
|
|
1301
|
+
type: "auth",
|
|
1302
|
+
data: {
|
|
1303
|
+
address,
|
|
1304
|
+
userType,
|
|
1305
|
+
api_key: apiKey,
|
|
1306
|
+
platform
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1286
1311
|
export function createUserMessage(content: string, room: string, from?: string): UserMessage {
|
|
1287
1312
|
return UserMessageSchema.parse({
|
|
1288
1313
|
type: "message",
|
|
@@ -1332,13 +1357,18 @@ export function createRequestTask(
|
|
|
1332
1357
|
});
|
|
1333
1358
|
}
|
|
1334
1359
|
|
|
1335
|
-
export function createConfirmTask(
|
|
1360
|
+
export function createConfirmTask(
|
|
1361
|
+
taskId: string,
|
|
1362
|
+
options?: { x402Payment?: string; apiKey?: string; network?: string }
|
|
1363
|
+
): ConfirmTaskMessage {
|
|
1336
1364
|
return ConfirmTaskMessageSchema.parse({
|
|
1337
1365
|
type: "confirm_task",
|
|
1338
1366
|
data: {
|
|
1339
|
-
task_id: taskId
|
|
1367
|
+
task_id: taskId,
|
|
1368
|
+
...(options?.apiKey && { api_key: options.apiKey }),
|
|
1369
|
+
...(options?.network && { network: options.network })
|
|
1340
1370
|
},
|
|
1341
|
-
...(x402Payment && { payment: x402Payment })
|
|
1371
|
+
...(options?.x402Payment && { payment: options.x402Payment })
|
|
1342
1372
|
});
|
|
1343
1373
|
}
|
|
1344
1374
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { parseApproveCalldata, checkERC20Allowance, ERC20_APPROVE_SELECTOR } from "./erc20";
|
|
3
|
+
|
|
4
|
+
describe("parseApproveCalldata", () => {
|
|
5
|
+
// Valid approve calldata: approve(0x1234...5678, max_uint256)
|
|
6
|
+
const spender = "1234567890abcdef1234567890abcdef12345678";
|
|
7
|
+
const maxUint256Hex = "f".repeat(64);
|
|
8
|
+
const validCalldata = ERC20_APPROVE_SELECTOR +
|
|
9
|
+
spender.padStart(64, "0") +
|
|
10
|
+
maxUint256Hex;
|
|
11
|
+
|
|
12
|
+
it("should parse valid approve calldata", () => {
|
|
13
|
+
const result = parseApproveCalldata(validCalldata);
|
|
14
|
+
expect(result).not.toBeNull();
|
|
15
|
+
expect(result!.spender).toBe(`0x${spender}`);
|
|
16
|
+
expect(result!.amount).toBe(BigInt("0x" + maxUint256Hex));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should return null for undefined data", () => {
|
|
20
|
+
expect(parseApproveCalldata(undefined)).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return null for empty string", () => {
|
|
24
|
+
expect(parseApproveCalldata("")).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should return null for short data", () => {
|
|
28
|
+
expect(parseApproveCalldata("0x095ea7b3")).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should return null for wrong selector", () => {
|
|
32
|
+
const wrongSelector = "0xa9059cbb" + "0".repeat(128); // transfer() selector
|
|
33
|
+
expect(parseApproveCalldata(wrongSelector)).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should return null for data shorter than 138 chars", () => {
|
|
37
|
+
const shortData = ERC20_APPROVE_SELECTOR + "0".repeat(60); // only 68 + 10 = 78 chars
|
|
38
|
+
expect(parseApproveCalldata(shortData)).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should handle uppercase hex", () => {
|
|
42
|
+
const upper = validCalldata.toUpperCase();
|
|
43
|
+
// selector check lowercases, so "0X" prefix won't match "0x"
|
|
44
|
+
const withLowerPrefix = "0x" + upper.slice(2);
|
|
45
|
+
const result = parseApproveCalldata(withLowerPrefix);
|
|
46
|
+
expect(result).not.toBeNull();
|
|
47
|
+
expect(result!.spender).toBe(`0x${spender.toUpperCase()}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should parse a specific amount correctly", () => {
|
|
51
|
+
const amount = "0000000000000000000000000000000000000000000000000de0b6b3a7640000"; // 1e18
|
|
52
|
+
const calldata = ERC20_APPROVE_SELECTOR + "0".repeat(24) + spender + amount;
|
|
53
|
+
const result = parseApproveCalldata(calldata);
|
|
54
|
+
expect(result).not.toBeNull();
|
|
55
|
+
expect(result!.amount).toBe(BigInt("0xde0b6b3a7640000")); // 1e18 in hex
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should parse zero amount", () => {
|
|
59
|
+
const calldata = ERC20_APPROVE_SELECTOR + "0".repeat(24) + spender + "0".repeat(64);
|
|
60
|
+
const result = parseApproveCalldata(calldata);
|
|
61
|
+
expect(result).not.toBeNull();
|
|
62
|
+
expect(result!.amount).toBe(0n);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("checkERC20Allowance", () => {
|
|
67
|
+
const mockFetch = vi.fn();
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
vi.restoreAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const rpcUrl = "https://rpc.example.com";
|
|
78
|
+
const tokenAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
|
79
|
+
const owner = "0x1111111111111111111111111111111111111111";
|
|
80
|
+
const spender = "0x2222222222222222222222222222222222222222";
|
|
81
|
+
|
|
82
|
+
it("should return allowance as bigint", async () => {
|
|
83
|
+
const allowanceHex = "0x00000000000000000000000000000000000000000000000000000000000f4240"; // 1000000
|
|
84
|
+
mockFetch.mockResolvedValueOnce({
|
|
85
|
+
ok: true,
|
|
86
|
+
json: () => Promise.resolve({ result: allowanceHex }),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await checkERC20Allowance(rpcUrl, tokenAddress, owner, spender);
|
|
90
|
+
expect(result).toBe(1000000n);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should return 0n for zero allowance", async () => {
|
|
94
|
+
mockFetch.mockResolvedValueOnce({
|
|
95
|
+
ok: true,
|
|
96
|
+
json: () => Promise.resolve({ result: "0x" }),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = await checkERC20Allowance(rpcUrl, tokenAddress, owner, spender);
|
|
100
|
+
expect(result).toBe(0n);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should throw on RPC error response", async () => {
|
|
104
|
+
mockFetch.mockResolvedValueOnce({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: () => Promise.resolve({ error: { message: "execution reverted" } }),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await expect(checkERC20Allowance(rpcUrl, tokenAddress, owner, spender))
|
|
110
|
+
.rejects.toThrow("RPC error: execution reverted");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should throw on HTTP error", async () => {
|
|
114
|
+
mockFetch.mockResolvedValueOnce({
|
|
115
|
+
ok: false,
|
|
116
|
+
status: 503,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await expect(checkERC20Allowance(rpcUrl, tokenAddress, owner, spender))
|
|
120
|
+
.rejects.toThrow("RPC request failed with status 503");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should throw on network failure", async () => {
|
|
124
|
+
mockFetch.mockRejectedValueOnce(new Error("fetch failed"));
|
|
125
|
+
|
|
126
|
+
await expect(checkERC20Allowance(rpcUrl, tokenAddress, owner, spender))
|
|
127
|
+
.rejects.toThrow("fetch failed");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should send correct eth_call params", async () => {
|
|
131
|
+
mockFetch.mockResolvedValueOnce({
|
|
132
|
+
ok: true,
|
|
133
|
+
json: () => Promise.resolve({ result: "0x0" }),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await checkERC20Allowance(rpcUrl, tokenAddress, owner, spender);
|
|
137
|
+
|
|
138
|
+
expect(mockFetch).toHaveBeenCalledWith(rpcUrl, expect.objectContaining({
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: { "Content-Type": "application/json" },
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
144
|
+
expect(body.method).toBe("eth_call");
|
|
145
|
+
expect(body.params[0].to).toBe(tokenAddress);
|
|
146
|
+
// Should contain allowance selector 0xdd62ed3e
|
|
147
|
+
expect(body.params[0].data.startsWith("0xdd62ed3e")).toBe(true);
|
|
148
|
+
expect(body.params[1]).toBe("latest");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should handle max uint256 allowance", async () => {
|
|
152
|
+
const maxAllowance = "0x" + "f".repeat(64);
|
|
153
|
+
mockFetch.mockResolvedValueOnce({
|
|
154
|
+
ok: true,
|
|
155
|
+
json: () => Promise.resolve({ result: maxAllowance }),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = await checkERC20Allowance(rpcUrl, tokenAddress, owner, spender);
|
|
159
|
+
expect(result).toBe(BigInt(maxAllowance));
|
|
160
|
+
});
|
|
161
|
+
});
|