@agentlayer.tech/wallet 0.1.17 → 0.1.18

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.
@@ -440,6 +440,36 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
440
440
  self.address = address
441
441
  return address
442
442
 
443
+ def sign_x402_evm_exact_typed_data(
444
+ self,
445
+ *,
446
+ domain: dict[str, Any],
447
+ types: dict[str, Any],
448
+ primary_type: str,
449
+ message: dict[str, Any],
450
+ ) -> bytes:
451
+ data = self.client.post_sync(
452
+ "/v1/evm/x402/exact/sign",
453
+ {
454
+ "walletId": self.wallet_id,
455
+ "accountIndex": self.account_index,
456
+ "network": self.network,
457
+ "domain": domain,
458
+ "types": types,
459
+ "primaryType": primary_type,
460
+ "message": message,
461
+ },
462
+ )
463
+ signature = str(data.get("signature") or "").strip()
464
+ if not signature:
465
+ raise WalletBackendError("wdk-evm-wallet did not return an x402 EVM signature.")
466
+ if signature.startswith("0x"):
467
+ signature = signature[2:]
468
+ try:
469
+ return bytes.fromhex(signature)
470
+ except ValueError as exc:
471
+ raise WalletBackendError("wdk-evm-wallet returned an invalid x402 EVM signature.") from exc
472
+
443
473
  async def get_balance(self, address: str | None = None) -> dict[str, Any]:
444
474
  resolved_address = await self.get_address()
445
475
  if address is not None and address.strip() and address.strip() != resolved_address:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.17"
7
+ version = "0.1.18"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "python-dotenv>=1.0.0",
16
16
  "solana>=0.36.0",
17
17
  "solders>=0.27.0",
18
+ "x402[httpx,svm,evm]>=2.10.0",
18
19
  ]
19
20
 
20
21
  [project.optional-dependencies]
@@ -58,6 +58,13 @@ PAY_BRIDGE_TOOLS = [
58
58
  "pay_api_request",
59
59
  ]
60
60
 
61
+ X402_TOOLS = [
62
+ "x402_search_services",
63
+ "x402_get_service_details",
64
+ "x402_preview_request",
65
+ "x402_pay_request",
66
+ ]
67
+
61
68
 
62
69
  def _default_config_path() -> Path:
63
70
  return Path(os.path.expanduser("~/.openclaw/openclaw.json"))
@@ -329,7 +336,7 @@ def main() -> None:
329
336
 
330
337
  tools = data.setdefault("tools", {})
331
338
  also_allow = tools.setdefault("alsoAllow", [])
332
- for tool_name in OPTIONAL_TOOLS + PAY_BRIDGE_TOOLS:
339
+ for tool_name in OPTIONAL_TOOLS + PAY_BRIDGE_TOOLS + X402_TOOLS:
333
340
  if tool_name not in also_allow:
334
341
  also_allow.append(tool_name)
335
342
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -554,6 +554,12 @@ async function handleRequest(request, response) {
554
554
  return sendJson(response, 200, { ok: true, data });
555
555
  }
556
556
 
557
+ if (method === "POST" && url.pathname === "/v1/evm/x402/exact/sign") {
558
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
559
+ const data = await service.signX402ExactTypedData(body);
560
+ return sendJson(response, 200, { ok: true, data });
561
+ }
562
+
557
563
  return notFound(response);
558
564
  } catch (error) {
559
565
  const shaped = toErrorResponse(error, new URL(request.url || "/", "http://localhost").pathname, 400);
@@ -149,6 +149,14 @@ function assertPositiveBigIntString(value, fieldName) {
149
149
  return parsed;
150
150
  }
151
151
 
152
+ function assertNonNegativeBigIntString(value, fieldName) {
153
+ const normalized = String(value ?? "").trim();
154
+ if (!/^[0-9]+$/.test(normalized)) {
155
+ throw new Error(`${fieldName} must be a non-negative base-10 integer string.`);
156
+ }
157
+ return normalized;
158
+ }
159
+
152
160
  function normalizeAddress(value, fieldName) {
153
161
  const address = assertNonEmptyString(value, fieldName);
154
162
  if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
@@ -305,6 +313,71 @@ function mergeBridgeLists(...values) {
305
313
  return items.length > 0 ? items.join(",") : null;
306
314
  }
307
315
 
316
+ function assertPlainObject(value, fieldName) {
317
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
318
+ throw new Error(`${fieldName} must be an object.`);
319
+ }
320
+ return value;
321
+ }
322
+
323
+ function normalizeX402ExactTypedData({ domain, types, primaryType, message }, runtimeConfig) {
324
+ const normalizedPrimaryType = assertNonEmptyString(primaryType, "primaryType");
325
+ if (normalizedPrimaryType !== "TransferWithAuthorization") {
326
+ throw new Error("primaryType must be TransferWithAuthorization for x402 exact EVM payments.");
327
+ }
328
+
329
+ const domainObject = assertPlainObject(domain, "domain");
330
+ const domainChainId = assertNonNegativeInteger(domainObject.chainId, "domain.chainId");
331
+ if (domainChainId !== runtimeConfig.chainId) {
332
+ throw new Error("domain.chainId must match the active network chain id.");
333
+ }
334
+ const normalizedDomain = {
335
+ name: assertNonEmptyString(domainObject.name, "domain.name"),
336
+ version: assertNonEmptyString(domainObject.version, "domain.version"),
337
+ chainId: domainChainId,
338
+ verifyingContract: normalizeAddress(domainObject.verifyingContract, "domain.verifyingContract"),
339
+ };
340
+
341
+ const typesObject = assertPlainObject(types, "types");
342
+ const primaryFields = typesObject[normalizedPrimaryType];
343
+ if (!Array.isArray(primaryFields) || primaryFields.length === 0) {
344
+ throw new Error(`types.${normalizedPrimaryType} must be a non-empty array.`);
345
+ }
346
+ const normalizedTypes = {};
347
+ for (const [typeName, fields] of Object.entries(typesObject)) {
348
+ if (!Array.isArray(fields) || fields.length === 0) {
349
+ throw new Error(`types.${typeName} must be a non-empty array.`);
350
+ }
351
+ normalizedTypes[typeName] = fields.map((field, index) => {
352
+ const normalizedField = assertPlainObject(field, `types.${typeName}[${index}]`);
353
+ return {
354
+ name: assertNonEmptyString(normalizedField.name, `types.${typeName}[${index}].name`),
355
+ type: assertNonEmptyString(normalizedField.type, `types.${typeName}[${index}].type`),
356
+ };
357
+ });
358
+ }
359
+
360
+ const messageObject = assertPlainObject(message, "message");
361
+ const normalizedMessage = {
362
+ from: normalizeAddress(messageObject.from, "message.from"),
363
+ to: normalizeAddress(messageObject.to, "message.to"),
364
+ value: assertPositiveBigIntString(messageObject.value, "message.value").toString(),
365
+ validAfter: assertNonNegativeBigIntString(messageObject.validAfter, "message.validAfter"),
366
+ validBefore: assertPositiveBigIntString(messageObject.validBefore, "message.validBefore").toString(),
367
+ nonce: assertNonEmptyString(messageObject.nonce, "message.nonce"),
368
+ };
369
+ if (!/^0x[a-fA-F0-9]{64}$/.test(normalizedMessage.nonce)) {
370
+ throw new Error("message.nonce must be a 32-byte hex string.");
371
+ }
372
+
373
+ return {
374
+ domain: normalizedDomain,
375
+ types: normalizedTypes,
376
+ primaryType: normalizedPrimaryType,
377
+ message: normalizedMessage,
378
+ };
379
+ }
380
+
308
381
  function buildSwapRequest({ tokenIn, tokenOut, tokenInAmount }) {
309
382
  const swapRequest = {
310
383
  tokenIn: normalizeAddress(tokenIn, "tokenIn"),
@@ -2335,6 +2408,41 @@ export class WdkEvmWalletService {
2335
2408
  });
2336
2409
  }
2337
2410
 
2411
+ async signX402ExactTypedData({
2412
+ seedPhrase,
2413
+ accountIndex = 0,
2414
+ network,
2415
+ domain,
2416
+ types,
2417
+ primaryType,
2418
+ message,
2419
+ }) {
2420
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
2421
+ const typedData = normalizeX402ExactTypedData(
2422
+ { domain, types, primaryType, message },
2423
+ runtimeConfig
2424
+ );
2425
+ const signerAddress = normalizeAddress(await account.getAddress(), "accountAddress");
2426
+ if (typedData.message.from.toLowerCase() !== signerAddress.toLowerCase()) {
2427
+ throw new Error("message.from must match the active wallet account address.");
2428
+ }
2429
+ const signature = await account.signTypedData({
2430
+ domain: typedData.domain,
2431
+ types: typedData.types,
2432
+ message: typedData.message,
2433
+ });
2434
+ return {
2435
+ network: runtimeConfig.network,
2436
+ chainId: runtimeConfig.chainId,
2437
+ accountIndex,
2438
+ address: signerAddress,
2439
+ primaryType: typedData.primaryType,
2440
+ signature,
2441
+ source: "wdk-wallet-evm",
2442
+ };
2443
+ });
2444
+ }
2445
+
2338
2446
  #resolveRuntimeConfig(networkOverride) {
2339
2447
  const network = assertValidNetwork(networkOverride) || this.config.network;
2340
2448
  const profile = this.config.networkProfiles?.[network];