@agentlayer.tech/wallet 0.1.16 → 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.
@@ -2600,10 +2600,6 @@ class SolanaWalletBackend(AgentWalletBackend):
2600
2600
  collateral_symbol,
2601
2601
  field_name="collateral_symbol",
2602
2602
  )
2603
- if normalized_collateral_symbol != normalized_market_symbol:
2604
- raise WalletBackendError(
2605
- "Phase 2 Flash preview currently supports only same-collateral opens where collateral_symbol matches market_symbol."
2606
- )
2607
2603
  normalized_collateral_amount_raw = _require_positive_integer_string(
2608
2604
  collateral_amount_raw,
2609
2605
  field_name="collateral_amount_raw",
@@ -2616,15 +2612,19 @@ class SolanaWalletBackend(AgentWalletBackend):
2616
2612
  item
2617
2613
  for item in market_snapshot["markets"]
2618
2614
  if isinstance(item, dict)
2619
- and str(item.get("symbol") or "").strip().upper() == normalized_market_symbol
2615
+ and str(item.get("market_symbol") or item.get("symbol") or "").strip().upper()
2616
+ == normalized_market_symbol
2617
+ and str(item.get("side") or "").strip().lower() == normalized_side
2618
+ and str(item.get("collateral_symbol") or "").strip().upper()
2619
+ == normalized_collateral_symbol
2620
2620
  ),
2621
2621
  None,
2622
2622
  )
2623
2623
  if matching_market is None:
2624
2624
  raise WalletBackendError(
2625
- "Requested Flash market is not available in the selected pool."
2625
+ "Requested Flash market is not available in the selected pool for the requested collateral and side."
2626
2626
  )
2627
- bridge_preview = await flash_sdk_bridge.preview_open_position_same_collateral(
2627
+ bridge_preview = await flash_sdk_bridge.preview_open_position(
2628
2628
  owner=owner,
2629
2629
  pool_name=normalized_pool_name,
2630
2630
  market_symbol=normalized_market_symbol,
@@ -2927,7 +2927,7 @@ class SolanaWalletBackend(AgentWalletBackend):
2927
2927
  leverage=leverage,
2928
2928
  side=side,
2929
2929
  )
2930
- bridge_prepared = await flash_sdk_bridge.prepare_open_position_same_collateral(
2930
+ bridge_prepared = await flash_sdk_bridge.prepare_open_position(
2931
2931
  owner=str(preview["owner"]),
2932
2932
  pool_name=str(preview["pool_name"]),
2933
2933
  market_symbol=str(preview["market_symbol"]),
@@ -2948,7 +2948,7 @@ class SolanaWalletBackend(AgentWalletBackend):
2948
2948
  self,
2949
2949
  preview: dict[str, Any],
2950
2950
  ) -> dict[str, Any]:
2951
- bridge_prepared = await flash_sdk_bridge.prepare_open_position_same_collateral(
2951
+ bridge_prepared = await flash_sdk_bridge.prepare_open_position(
2952
2952
  owner=str(preview["owner"]),
2953
2953
  pool_name=str(preview["pool_name"]),
2954
2954
  market_symbol=str(preview["market_symbol"]),
@@ -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.16"
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]
@@ -13,7 +13,7 @@ Current goals:
13
13
  - `FLASH_SDK_BRIDGE_MODE=mock`
14
14
  Returns deterministic payloads for local smoke checks without installing SDK dependencies.
15
15
  - `FLASH_SDK_BRIDGE_MODE=real`
16
- Loads `flash-sdk` and validates runtime config. This mode now supports market discovery, user-position discovery, open/close previews, and unsigned transaction preparation for the current same-collateral Flash perps MVP.
16
+ Loads `flash-sdk` and validates runtime config. This mode now supports market discovery, user-position discovery, open/close previews, and unsigned transaction preparation for Flash perps using the collateral supported by the selected Flash market.
17
17
 
18
18
  ## Command
19
19
 
@@ -28,6 +28,6 @@ For local smoke:
28
28
  ```bash
29
29
  FLASH_SDK_BRIDGE_MODE=mock \
30
30
  node agent-wallet/scripts/flash-sdk-bridge/bridge.mjs <<'EOF'
31
- {"action":"preview_open_position_same_collateral","owner":"Fake11111111111111111111111111111111111111111","pool_name":"Crypto.1","market_symbol":"SOL","collateral_symbol":"SOL","collateral_amount_raw":"100000000","leverage":"5","side":"long","network":"mainnet"}
31
+ {"action":"preview_open_position","owner":"Fake11111111111111111111111111111111111111111","pool_name":"Crypto.1","market_symbol":"SOL","collateral_symbol":"USDC","collateral_amount_raw":"5000000","leverage":"2","side":"short","network":"mainnet"}
32
32
  EOF
33
33
  ```
@@ -110,7 +110,7 @@ function mockResponse(normalized) {
110
110
  pool_name: normalized.poolName ?? "Crypto.1",
111
111
  symbol: "SOL",
112
112
  market_symbol: "SOL",
113
- collateral_symbol: "SOL",
113
+ collateral_symbol: "USDC",
114
114
  side: "short",
115
115
  market_address: "MockFlashMarketShort1111111111111111111111111",
116
116
  },
@@ -145,7 +145,10 @@ function mockResponse(normalized) {
145
145
  };
146
146
  }
147
147
 
148
- if (normalized.action === "preview_open_position_same_collateral") {
148
+ if (
149
+ normalized.action === "preview_open_position" ||
150
+ normalized.action === "preview_open_position_same_collateral"
151
+ ) {
149
152
  return {
150
153
  ok: true,
151
154
  preview: {
@@ -177,7 +180,10 @@ function mockResponse(normalized) {
177
180
  };
178
181
  }
179
182
 
180
- if (normalized.action === "prepare_open_position_same_collateral") {
183
+ if (
184
+ normalized.action === "prepare_open_position" ||
185
+ normalized.action === "prepare_open_position_same_collateral"
186
+ ) {
181
187
  return {
182
188
  ok: true,
183
189
  prepared: {
@@ -608,12 +614,6 @@ async function getOpenPositionPreview(runtime, normalized) {
608
614
  "collateral_symbol, collateral_amount_raw, and leverage are required for open preview",
609
615
  );
610
616
  }
611
- if (normalized.collateralSymbol !== normalized.marketSymbol) {
612
- throw new Error(
613
- "Current bridge MVP supports only same-collateral opens where collateral_symbol matches market_symbol",
614
- );
615
- }
616
-
617
617
  const { BN } = runtime;
618
618
  const privilege = runtime.flashSdk.Privilege.None;
619
619
  const ownerPublicKey = runtime.provider.wallet.publicKey;
@@ -810,12 +810,6 @@ async function prepareOpenPosition(runtime, normalized) {
810
810
  "collateral_symbol, collateral_amount_raw, and leverage are required for open prepare",
811
811
  );
812
812
  }
813
- if (normalized.collateralSymbol !== normalized.marketSymbol) {
814
- throw new Error(
815
- "Current bridge MVP supports only same-collateral opens where collateral_symbol matches market_symbol",
816
- );
817
- }
818
-
819
813
  const { BN } = runtime;
820
814
  const privilege = runtime.flashSdk.Privilege.None;
821
815
  const ownerPublicKey = runtime.provider.wallet.publicKey;
@@ -1113,13 +1107,19 @@ async function realResponse(normalized) {
1113
1107
  return getPositionsReal(normalized);
1114
1108
  }
1115
1109
  const runtime = await buildRuntimeContext(normalized);
1116
- if (normalized.action === "preview_open_position_same_collateral") {
1110
+ if (
1111
+ normalized.action === "preview_open_position" ||
1112
+ normalized.action === "preview_open_position_same_collateral"
1113
+ ) {
1117
1114
  return getOpenPositionPreview(runtime, normalized);
1118
1115
  }
1119
1116
  if (normalized.action === "preview_close_position_same_collateral") {
1120
1117
  return getClosePositionPreview(runtime, normalized);
1121
1118
  }
1122
- if (normalized.action === "prepare_open_position_same_collateral") {
1119
+ if (
1120
+ normalized.action === "prepare_open_position" ||
1121
+ normalized.action === "prepare_open_position_same_collateral"
1122
+ ) {
1123
1123
  return prepareOpenPosition(runtime, normalized);
1124
1124
  }
1125
1125
  if (normalized.action === "prepare_close_position_same_collateral") {
@@ -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.16",
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];