@buildersgarden/siwa 0.0.2 → 0.0.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Builders Garden SRL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -64,7 +64,7 @@ See [`references/security-model.md`](references/security-model.md) for the full
64
64
  ## Tech Stack
65
65
 
66
66
  - **TypeScript** (ES modules, strict mode)
67
- - **ethers.js** v6 — wallet management and contract interaction
67
+ - **viem** — wallet management and contract interaction
68
68
  - **pnpm** — package manager
69
69
 
70
70
  ## References
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Three backends, in order of preference:
7
7
  * 0. Keyring Proxy (via HMAC-authenticated HTTP) — key never enters agent process
8
- * 1. Ethereum V3 Encrypted JSON Keystore (via ethers.js) — password-encrypted file on disk
8
+ * 1. Ethereum V3 Encrypted JSON Keystore (via @noble/ciphers) — password-encrypted file on disk
9
9
  * 2. Environment variable fallback (AGENT_PRIVATE_KEY) — least secure, for CI/testing only
10
10
  *
11
11
  * The private key NEVER leaves this module as a return value.
@@ -18,15 +18,15 @@
18
18
  * - getAddress() → returns the public address
19
19
  * - hasWallet() → returns boolean
20
20
  *
21
- * EIP-7702 Support (requires ethers >= 6.14.3):
22
- * Wallets are standard EOAs created via ethers.Wallet.createRandom().
21
+ * EIP-7702 Support:
22
+ * Wallets are standard EOAs created via viem's generatePrivateKey().
23
23
  * EIP-7702 allows these EOAs to temporarily delegate to smart contract
24
24
  * implementations via authorization lists in type 4 transactions.
25
25
  * Use signAuthorization() to sign delegation authorizations without
26
26
  * exposing the private key.
27
27
  *
28
28
  * Dependencies:
29
- * npm install ethers
29
+ * npm install viem
30
30
  *
31
31
  * Configuration (via env vars or passed options):
32
32
  * KEYSTORE_BACKEND — "encrypted-file" | "env" | "proxy" (auto-detected if omitted)
@@ -34,7 +34,7 @@
34
34
  * KEYSTORE_PATH — Path to encrypted keystore file (default: ./agent-keystore.json)
35
35
  * AGENT_PRIVATE_KEY — Fallback for env backend only
36
36
  */
37
- import { ethers } from 'ethers';
37
+ import { type WalletClient, type Account, type Chain, type Transport } from 'viem';
38
38
  export type KeystoreBackend = 'encrypted-file' | 'env' | 'proxy';
39
39
  export interface KeystoreConfig {
40
40
  backend?: KeystoreBackend;
@@ -65,6 +65,20 @@ export interface SignedAuthorization {
65
65
  r: string;
66
66
  s: string;
67
67
  }
68
+ export interface TransactionLike {
69
+ to?: string;
70
+ data?: string;
71
+ value?: bigint;
72
+ nonce?: number;
73
+ chainId?: number;
74
+ type?: number;
75
+ maxFeePerGas?: bigint | null;
76
+ maxPriorityFeePerGas?: bigint | null;
77
+ gasLimit?: bigint;
78
+ gas?: bigint;
79
+ gasPrice?: bigint | null;
80
+ accessList?: any[];
81
+ }
68
82
  export declare function detectBackend(): Promise<KeystoreBackend>;
69
83
  /**
70
84
  * Create a new random wallet and store it securely.
@@ -96,13 +110,12 @@ export declare function signMessage(message: string, config?: KeystoreConfig): P
96
110
  * The private key is loaded, used, and immediately discarded.
97
111
  * Only the signed transaction is returned.
98
112
  */
99
- export declare function signTransaction(tx: ethers.TransactionRequest, config?: KeystoreConfig): Promise<{
113
+ export declare function signTransaction(tx: TransactionLike, config?: KeystoreConfig): Promise<{
100
114
  signedTx: string;
101
115
  address: string;
102
116
  }>;
103
117
  /**
104
118
  * Sign an EIP-7702 authorization for delegating the EOA to a contract.
105
- * Requires ethers >= 6.14.3.
106
119
  *
107
120
  * This allows the agent's EOA to temporarily act as a smart contract
108
121
  * during a type 4 transaction. The private key is loaded, used, and
@@ -113,12 +126,12 @@ export declare function signTransaction(tx: ethers.TransactionRequest, config?:
113
126
  */
114
127
  export declare function signAuthorization(auth: AuthorizationRequest, config?: KeystoreConfig): Promise<SignedAuthorization>;
115
128
  /**
116
- * Get a connected signer (for contract interactions).
117
- * NOTE: This returns a signer with the private key in memory.
129
+ * Get a wallet client for contract interactions.
130
+ * NOTE: This creates a client with the private key in memory.
118
131
  * Use only within a narrow scope and discard immediately.
119
132
  * Prefer signMessage() / signTransaction() when possible.
120
133
  */
121
- export declare function getSigner(provider: ethers.Provider, config?: KeystoreConfig): Promise<ethers.Wallet>;
134
+ export declare function getWalletClient(rpcUrl: string, config?: KeystoreConfig): Promise<WalletClient<Transport, Chain | undefined, Account>>;
122
135
  /**
123
136
  * Delete the stored wallet from the active backend.
124
137
  * DESTRUCTIVE — the identity is lost if no backup exists.
package/dist/keystore.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Three backends, in order of preference:
7
7
  * 0. Keyring Proxy (via HMAC-authenticated HTTP) — key never enters agent process
8
- * 1. Ethereum V3 Encrypted JSON Keystore (via ethers.js) — password-encrypted file on disk
8
+ * 1. Ethereum V3 Encrypted JSON Keystore (via @noble/ciphers) — password-encrypted file on disk
9
9
  * 2. Environment variable fallback (AGENT_PRIVATE_KEY) — least secure, for CI/testing only
10
10
  *
11
11
  * The private key NEVER leaves this module as a return value.
@@ -18,15 +18,15 @@
18
18
  * - getAddress() → returns the public address
19
19
  * - hasWallet() → returns boolean
20
20
  *
21
- * EIP-7702 Support (requires ethers >= 6.14.3):
22
- * Wallets are standard EOAs created via ethers.Wallet.createRandom().
21
+ * EIP-7702 Support:
22
+ * Wallets are standard EOAs created via viem's generatePrivateKey().
23
23
  * EIP-7702 allows these EOAs to temporarily delegate to smart contract
24
24
  * implementations via authorization lists in type 4 transactions.
25
25
  * Use signAuthorization() to sign delegation authorizations without
26
26
  * exposing the private key.
27
27
  *
28
28
  * Dependencies:
29
- * npm install ethers
29
+ * npm install viem
30
30
  *
31
31
  * Configuration (via env vars or passed options):
32
32
  * KEYSTORE_BACKEND — "encrypted-file" | "env" | "proxy" (auto-detected if omitted)
@@ -34,7 +34,12 @@
34
34
  * KEYSTORE_PATH — Path to encrypted keystore file (default: ./agent-keystore.json)
35
35
  * AGENT_PRIVATE_KEY — Fallback for env backend only
36
36
  */
37
- import { ethers } from 'ethers';
37
+ import { createWalletClient, http, keccak256, toBytes, toHex, concat, } from 'viem';
38
+ import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
39
+ import { hashAuthorization } from 'viem/experimental';
40
+ import { scrypt } from '@noble/hashes/scrypt';
41
+ import { randomBytes } from '@noble/hashes/utils';
42
+ import { ctr } from '@noble/ciphers/aes';
38
43
  import * as fs from 'fs';
39
44
  import * as crypto from 'crypto';
40
45
  import * as os from 'os';
@@ -43,6 +48,84 @@ import { computeHmac } from './proxy-auth.js';
43
48
  // Constants
44
49
  // ---------------------------------------------------------------------------
45
50
  const DEFAULT_KEYSTORE_PATH = './agent-keystore.json';
51
+ function generateUUID() {
52
+ const bytes = randomBytes(16);
53
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
54
+ bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant
55
+ const hex = toHex(bytes).slice(2);
56
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
57
+ }
58
+ async function encryptKeystore(privateKey, password) {
59
+ const privateKeyBytes = toBytes(privateKey);
60
+ const salt = randomBytes(32);
61
+ const iv = randomBytes(16);
62
+ // Scrypt parameters (standard for V3 keystores)
63
+ const n = 262144; // 2^18
64
+ const r = 8;
65
+ const p = 1;
66
+ const dklen = 32;
67
+ // Derive key using scrypt
68
+ const derivedKey = scrypt(new TextEncoder().encode(password), salt, { N: n, r, p, dkLen: dklen });
69
+ // Encrypt private key with AES-128-CTR
70
+ const encryptionKey = derivedKey.slice(0, 16);
71
+ const cipher = ctr(encryptionKey, iv);
72
+ const ciphertext = cipher.encrypt(privateKeyBytes);
73
+ // Calculate MAC: keccak256(derivedKey[16:32] + ciphertext)
74
+ const macData = concat([toHex(derivedKey.slice(16, 32)), toHex(ciphertext)]);
75
+ const mac = keccak256(macData);
76
+ // Get address from private key
77
+ const account = privateKeyToAccount(privateKey);
78
+ const keystore = {
79
+ version: 3,
80
+ id: generateUUID(),
81
+ address: account.address.toLowerCase().slice(2),
82
+ crypto: {
83
+ ciphertext: toHex(ciphertext).slice(2),
84
+ cipherparams: { iv: toHex(iv).slice(2) },
85
+ cipher: 'aes-128-ctr',
86
+ kdf: 'scrypt',
87
+ kdfparams: {
88
+ dklen,
89
+ salt: toHex(salt).slice(2),
90
+ n,
91
+ r,
92
+ p,
93
+ },
94
+ mac: mac.slice(2),
95
+ },
96
+ };
97
+ return JSON.stringify(keystore);
98
+ }
99
+ async function decryptKeystore(json, password) {
100
+ const keystore = JSON.parse(json);
101
+ if (keystore.version !== 3) {
102
+ throw new Error(`Unsupported keystore version: ${keystore.version}`);
103
+ }
104
+ const { crypto: cryptoData } = keystore;
105
+ if (cryptoData.kdf !== 'scrypt') {
106
+ throw new Error(`Unsupported KDF: ${cryptoData.kdf}`);
107
+ }
108
+ if (cryptoData.cipher !== 'aes-128-ctr') {
109
+ throw new Error(`Unsupported cipher: ${cryptoData.cipher}`);
110
+ }
111
+ const { kdfparams } = cryptoData;
112
+ const salt = toBytes(`0x${kdfparams.salt}`);
113
+ const iv = toBytes(`0x${cryptoData.cipherparams.iv}`);
114
+ const ciphertext = toBytes(`0x${cryptoData.ciphertext}`);
115
+ // Derive key using scrypt
116
+ const derivedKey = scrypt(new TextEncoder().encode(password), salt, { N: kdfparams.n, r: kdfparams.r, p: kdfparams.p, dkLen: kdfparams.dklen });
117
+ // Verify MAC
118
+ const macData = concat([toHex(derivedKey.slice(16, 32)), toHex(ciphertext)]);
119
+ const calculatedMac = keccak256(macData).slice(2);
120
+ if (calculatedMac.toLowerCase() !== cryptoData.mac.toLowerCase()) {
121
+ throw new Error('Invalid password or corrupted keystore');
122
+ }
123
+ // Decrypt private key with AES-128-CTR
124
+ const encryptionKey = derivedKey.slice(0, 16);
125
+ const cipher = ctr(encryptionKey, iv);
126
+ const privateKeyBytes = cipher.decrypt(ciphertext);
127
+ return toHex(privateKeyBytes);
128
+ }
46
129
  // ---------------------------------------------------------------------------
47
130
  // Proxy backend — HMAC-authenticated HTTP to a keyring proxy server
48
131
  // ---------------------------------------------------------------------------
@@ -87,20 +170,17 @@ export async function detectBackend() {
87
170
  return 'encrypted-file';
88
171
  }
89
172
  // ---------------------------------------------------------------------------
90
- // Encrypted V3 JSON Keystore backend (ethers.js built-in)
173
+ // Encrypted V3 JSON Keystore backend
91
174
  // ---------------------------------------------------------------------------
92
- async function encryptedFileStore(privateKey, address, password, filePath) {
93
- const account = { address, privateKey };
94
- // ethers v6: encryptKeystoreJsonSync or encryptKeystoreJson
95
- const json = await ethers.encryptKeystoreJson(account, password);
175
+ async function encryptedFileStore(privateKey, password, filePath) {
176
+ const json = await encryptKeystore(privateKey, password);
96
177
  fs.writeFileSync(filePath, json, { mode: 0o600 }); // Owner-only read/write
97
178
  }
98
179
  async function encryptedFileLoad(password, filePath) {
99
180
  if (!fs.existsSync(filePath))
100
181
  return null;
101
182
  const json = fs.readFileSync(filePath, 'utf-8');
102
- const wallet = await ethers.Wallet.fromEncryptedJson(json, password);
103
- return wallet.privateKey;
183
+ return decryptKeystore(json, password);
104
184
  }
105
185
  function encryptedFileExists(filePath) {
106
186
  return fs.existsSync(filePath);
@@ -139,13 +219,13 @@ export async function createWallet(config = {}) {
139
219
  const data = await proxyRequest(config, '/create-wallet');
140
220
  return { address: data.address, backend, keystorePath: undefined };
141
221
  }
142
- const wallet = ethers.Wallet.createRandom();
143
- const privateKey = wallet.privateKey;
144
- const address = wallet.address;
222
+ const privateKey = generatePrivateKey();
223
+ const account = privateKeyToAccount(privateKey);
224
+ const address = account.address;
145
225
  switch (backend) {
146
226
  case 'encrypted-file': {
147
227
  const password = config.password || process.env.KEYSTORE_PASSWORD || deriveMachinePassword();
148
- await encryptedFileStore(privateKey, address, password, keystorePath);
228
+ await encryptedFileStore(privateKey, password, keystorePath);
149
229
  break;
150
230
  }
151
231
  case 'env':
@@ -174,12 +254,13 @@ export async function importWallet(privateKey, config = {}) {
174
254
  throw new Error('importWallet() is not supported via proxy. Import the wallet on the proxy server directly.');
175
255
  }
176
256
  const keystorePath = config.keystorePath || process.env.KEYSTORE_PATH || DEFAULT_KEYSTORE_PATH;
177
- const wallet = new ethers.Wallet(privateKey);
178
- const address = wallet.address;
257
+ const hexKey = (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`);
258
+ const account = privateKeyToAccount(hexKey);
259
+ const address = account.address;
179
260
  switch (backend) {
180
261
  case 'encrypted-file': {
181
262
  const password = config.password || process.env.KEYSTORE_PASSWORD || deriveMachinePassword();
182
- await encryptedFileStore(privateKey, address, password, keystorePath);
263
+ await encryptedFileStore(hexKey, password, keystorePath);
183
264
  break;
184
265
  }
185
266
  case 'env':
@@ -218,12 +299,11 @@ export async function getAddress(config = {}) {
218
299
  const data = await proxyRequest(config, '/get-address');
219
300
  return data.address;
220
301
  }
221
- const wallet = await _loadWalletInternal(config);
222
- if (!wallet)
302
+ const privateKey = await _loadPrivateKeyInternal(config);
303
+ if (!privateKey)
223
304
  return null;
224
- const address = wallet.address;
225
- // wallet goes out of scope and is GC'd — private key not returned
226
- return address;
305
+ const account = privateKeyToAccount(privateKey);
306
+ return account.address;
227
307
  }
228
308
  /**
229
309
  * Sign a message (EIP-191 personal_sign).
@@ -236,13 +316,12 @@ export async function signMessage(message, config = {}) {
236
316
  const data = await proxyRequest(config, '/sign-message', { message });
237
317
  return { signature: data.signature, address: data.address };
238
318
  }
239
- const wallet = await _loadWalletInternal(config);
240
- if (!wallet)
319
+ const privateKey = await _loadPrivateKeyInternal(config);
320
+ if (!privateKey)
241
321
  throw new Error('No wallet found. Run createWallet() first.');
242
- const signature = await wallet.signMessage(message);
243
- const address = wallet.address;
244
- // wallet goes out of scope — private key discarded
245
- return { signature, address };
322
+ const account = privateKeyToAccount(privateKey);
323
+ const signature = await account.signMessage({ message });
324
+ return { signature, address: account.address };
246
325
  }
247
326
  /**
248
327
  * Sign a transaction.
@@ -255,16 +334,37 @@ export async function signTransaction(tx, config = {}) {
255
334
  const data = await proxyRequest(config, '/sign-transaction', { tx: tx });
256
335
  return { signedTx: data.signedTx, address: data.address };
257
336
  }
258
- const wallet = await _loadWalletInternal(config);
259
- if (!wallet)
337
+ const privateKey = await _loadPrivateKeyInternal(config);
338
+ if (!privateKey)
260
339
  throw new Error('No wallet found. Run createWallet() first.');
261
- const signedTx = await wallet.signTransaction(tx);
262
- const address = wallet.address;
263
- return { signedTx, address };
340
+ const account = privateKeyToAccount(privateKey);
341
+ // Build transaction request for viem
342
+ const viemTx = {
343
+ to: tx.to,
344
+ data: tx.data,
345
+ value: tx.value,
346
+ nonce: tx.nonce,
347
+ chainId: tx.chainId,
348
+ gas: tx.gasLimit ?? tx.gas,
349
+ };
350
+ // Handle EIP-1559 vs legacy transactions
351
+ if (tx.type === 2 || tx.maxFeePerGas !== undefined) {
352
+ viemTx.type = 'eip1559';
353
+ viemTx.maxFeePerGas = tx.maxFeePerGas;
354
+ viemTx.maxPriorityFeePerGas = tx.maxPriorityFeePerGas;
355
+ }
356
+ else if (tx.gasPrice !== undefined) {
357
+ viemTx.type = 'legacy';
358
+ viemTx.gasPrice = tx.gasPrice;
359
+ }
360
+ if (tx.accessList) {
361
+ viemTx.accessList = tx.accessList;
362
+ }
363
+ const signedTx = await account.signTransaction(viemTx);
364
+ return { signedTx, address: account.address };
264
365
  }
265
366
  /**
266
367
  * Sign an EIP-7702 authorization for delegating the EOA to a contract.
267
- * Requires ethers >= 6.14.3.
268
368
  *
269
369
  * This allows the agent's EOA to temporarily act as a smart contract
270
370
  * during a type 4 transaction. The private key is loaded, used, and
@@ -279,37 +379,54 @@ export async function signAuthorization(auth, config = {}) {
279
379
  const data = await proxyRequest(config, '/sign-authorization', { auth });
280
380
  return data;
281
381
  }
282
- const wallet = await _loadWalletInternal(config);
283
- if (!wallet)
382
+ const privateKey = await _loadPrivateKeyInternal(config);
383
+ if (!privateKey)
284
384
  throw new Error('No wallet found. Run createWallet() first.');
285
- // ethers v6.14.3+ exposes wallet.authorize()
286
- if (typeof wallet.authorize !== 'function') {
287
- throw new Error('wallet.authorize() not available. EIP-7702 requires ethers >= 6.14.3. ' +
288
- 'Run: pnpm add ethers@latest');
289
- }
290
- const authorization = await wallet.authorize({
291
- address: auth.address,
292
- ...(auth.chainId !== undefined && { chainId: auth.chainId }),
293
- ...(auth.nonce !== undefined && { nonce: auth.nonce }),
385
+ const account = privateKeyToAccount(privateKey);
386
+ // EIP-7702 authorization signing using viem experimental
387
+ const chainId = auth.chainId ?? 1;
388
+ const nonce = auth.nonce ?? 0;
389
+ // Hash the authorization struct according to EIP-7702
390
+ const authHash = hashAuthorization({
391
+ contractAddress: auth.address,
392
+ chainId,
393
+ nonce,
294
394
  });
295
- // wallet goes out of scope — private key discarded
296
- return authorization;
395
+ // Sign the authorization hash
396
+ const signature = await account.sign({ hash: authHash });
397
+ // Parse signature into r, s, yParity
398
+ const r = signature.slice(0, 66);
399
+ const s = `0x${signature.slice(66, 130)}`;
400
+ const v = parseInt(signature.slice(130, 132), 16);
401
+ const yParity = v - 27; // Convert v to yParity (0 or 1)
402
+ return {
403
+ address: auth.address,
404
+ nonce,
405
+ chainId,
406
+ yParity,
407
+ r,
408
+ s,
409
+ };
297
410
  }
298
411
  /**
299
- * Get a connected signer (for contract interactions).
300
- * NOTE: This returns a signer with the private key in memory.
412
+ * Get a wallet client for contract interactions.
413
+ * NOTE: This creates a client with the private key in memory.
301
414
  * Use only within a narrow scope and discard immediately.
302
415
  * Prefer signMessage() / signTransaction() when possible.
303
416
  */
304
- export async function getSigner(provider, config = {}) {
417
+ export async function getWalletClient(rpcUrl, config = {}) {
305
418
  const backend = config.backend || await detectBackend();
306
419
  if (backend === 'proxy') {
307
- throw new Error('getSigner() is not supported via proxy. The private key cannot be serialized over HTTP. Use signMessage() or signTransaction() instead.');
420
+ throw new Error('getWalletClient() is not supported via proxy. The private key cannot be serialized over HTTP. Use signMessage() or signTransaction() instead.');
308
421
  }
309
- const wallet = await _loadWalletInternal(config);
310
- if (!wallet)
422
+ const privateKey = await _loadPrivateKeyInternal(config);
423
+ if (!privateKey)
311
424
  throw new Error('No wallet found. Run createWallet() first.');
312
- return wallet.connect(provider);
425
+ const account = privateKeyToAccount(privateKey);
426
+ return createWalletClient({
427
+ account,
428
+ transport: http(rpcUrl),
429
+ });
313
430
  }
314
431
  /**
315
432
  * Delete the stored wallet from the active backend.
@@ -334,9 +451,9 @@ export async function deleteWallet(config = {}) {
334
451
  }
335
452
  }
336
453
  // ---------------------------------------------------------------------------
337
- // Internal — loads the wallet. NEVER exposed publicly.
454
+ // Internal — loads the private key. NEVER exposed publicly.
338
455
  // ---------------------------------------------------------------------------
339
- async function _loadWalletInternal(config = {}) {
456
+ async function _loadPrivateKeyInternal(config = {}) {
340
457
  const backend = config.backend || await detectBackend();
341
458
  const keystorePath = config.keystorePath || process.env.KEYSTORE_PATH || DEFAULT_KEYSTORE_PATH;
342
459
  let privateKey = null;
@@ -346,11 +463,13 @@ async function _loadWalletInternal(config = {}) {
346
463
  privateKey = await encryptedFileLoad(password, keystorePath);
347
464
  break;
348
465
  }
349
- case 'env':
350
- privateKey = process.env.AGENT_PRIVATE_KEY || null;
466
+ case 'env': {
467
+ const envKey = process.env.AGENT_PRIVATE_KEY || null;
468
+ if (envKey) {
469
+ privateKey = (envKey.startsWith('0x') ? envKey : `0x${envKey}`);
470
+ }
351
471
  break;
472
+ }
352
473
  }
353
- if (!privateKey)
354
- return null;
355
- return new ethers.Wallet(privateKey);
474
+ return privateKey;
356
475
  }
@@ -46,8 +46,8 @@ export function verifyHmac(secret, method, path, body, timestamp, signature) {
46
46
  .update(payload)
47
47
  .digest('hex');
48
48
  // Constant-time comparison
49
- const sigBuf = Buffer.from(signature, 'utf-8');
50
- const expBuf = Buffer.from(expected, 'utf-8');
49
+ const sigBuf = new Uint8Array(Buffer.from(signature, 'utf-8'));
50
+ const expBuf = new Uint8Array(Buffer.from(expected, 'utf-8'));
51
51
  if (sigBuf.length !== expBuf.length) {
52
52
  return { valid: false, error: 'Signature mismatch' };
53
53
  }
@@ -5,9 +5,9 @@
5
5
  * Provides functions to read agent profiles and reputation from on-chain registries.
6
6
  *
7
7
  * Dependencies:
8
- * npm install ethers
8
+ * npm install viem
9
9
  */
10
- import { ethers } from 'ethers';
10
+ import { type PublicClient } from 'viem';
11
11
  /** Service endpoint types defined in ERC-8004 */
12
12
  export type ServiceType = 'web' | 'A2A' | 'MCP' | 'OASF' | 'ENS' | 'DID' | 'email';
13
13
  /** Trust models defined in ERC-8004 */
@@ -42,7 +42,7 @@ export interface AgentProfile {
42
42
  }
43
43
  export interface GetAgentOptions {
44
44
  registryAddress: string;
45
- provider: ethers.Provider;
45
+ client: PublicClient;
46
46
  fetchMetadata?: boolean;
47
47
  }
48
48
  export interface ReputationSummary {
@@ -53,7 +53,7 @@ export interface ReputationSummary {
53
53
  }
54
54
  export interface GetReputationOptions {
55
55
  reputationRegistryAddress: string;
56
- provider: ethers.Provider;
56
+ client: PublicClient;
57
57
  clients?: string[];
58
58
  tag1?: ReputationTag | (string & {});
59
59
  tag2?: string;
@@ -62,13 +62,13 @@ export interface GetReputationOptions {
62
62
  * Read an agent from the Identity Registry and parse its profile.
63
63
  *
64
64
  * @param agentId The on-chain agent token ID
65
- * @param options Registry address, provider, and optional fetchMetadata flag
65
+ * @param options Registry address, client, and optional fetchMetadata flag
66
66
  */
67
67
  export declare function getAgent(agentId: number, options: GetAgentOptions): Promise<AgentProfile>;
68
68
  /**
69
69
  * Read an agent's reputation summary from the Reputation Registry.
70
70
  *
71
71
  * @param agentId The on-chain agent token ID
72
- * @param options Reputation registry address, provider, and optional filters
72
+ * @param options Reputation registry address, client, and optional filters
73
73
  */
74
74
  export declare function getReputation(agentId: number, options: GetReputationOptions): Promise<ReputationSummary>;
package/dist/registry.js CHANGED
@@ -5,17 +5,50 @@
5
5
  * Provides functions to read agent profiles and reputation from on-chain registries.
6
6
  *
7
7
  * Dependencies:
8
- * npm install ethers
8
+ * npm install viem
9
9
  */
10
- import { ethers } from 'ethers';
10
+ import { zeroAddress, } from 'viem';
11
11
  // ─── ABI Fragments ──────────────────────────────────────────────────
12
12
  const IDENTITY_REGISTRY_ABI = [
13
- 'function ownerOf(uint256 tokenId) view returns (address)',
14
- 'function tokenURI(uint256 tokenId) view returns (string)',
15
- 'function getAgentWallet(uint256 agentId) view returns (address)',
13
+ {
14
+ name: 'ownerOf',
15
+ type: 'function',
16
+ stateMutability: 'view',
17
+ inputs: [{ name: 'tokenId', type: 'uint256' }],
18
+ outputs: [{ name: '', type: 'address' }],
19
+ },
20
+ {
21
+ name: 'tokenURI',
22
+ type: 'function',
23
+ stateMutability: 'view',
24
+ inputs: [{ name: 'tokenId', type: 'uint256' }],
25
+ outputs: [{ name: '', type: 'string' }],
26
+ },
27
+ {
28
+ name: 'getAgentWallet',
29
+ type: 'function',
30
+ stateMutability: 'view',
31
+ inputs: [{ name: 'agentId', type: 'uint256' }],
32
+ outputs: [{ name: '', type: 'address' }],
33
+ },
16
34
  ];
17
35
  const REPUTATION_REGISTRY_ABI = [
18
- 'function getSummary(uint256 agentId, address[] clients, string tag1, string tag2) view returns (uint64 count, int128 summaryValue, uint8 valueDecimals)',
36
+ {
37
+ name: 'getSummary',
38
+ type: 'function',
39
+ stateMutability: 'view',
40
+ inputs: [
41
+ { name: 'agentId', type: 'uint256' },
42
+ { name: 'clients', type: 'address[]' },
43
+ { name: 'tag1', type: 'string' },
44
+ { name: 'tag2', type: 'string' },
45
+ ],
46
+ outputs: [
47
+ { name: 'count', type: 'uint64' },
48
+ { name: 'summaryValue', type: 'int128' },
49
+ { name: 'valueDecimals', type: 'uint8' },
50
+ ],
51
+ },
19
52
  ];
20
53
  // ─── Internal Helpers ───────────────────────────────────────────────
21
54
  const IPFS_GATEWAY = 'https://gateway.pinata.cloud/ipfs/';
@@ -49,17 +82,31 @@ async function resolveURI(uri) {
49
82
  * Read an agent from the Identity Registry and parse its profile.
50
83
  *
51
84
  * @param agentId The on-chain agent token ID
52
- * @param options Registry address, provider, and optional fetchMetadata flag
85
+ * @param options Registry address, client, and optional fetchMetadata flag
53
86
  */
54
87
  export async function getAgent(agentId, options) {
55
- const { registryAddress, provider, fetchMetadata = true } = options;
56
- const registry = new ethers.Contract(registryAddress, IDENTITY_REGISTRY_ABI, provider);
88
+ const { registryAddress, client, fetchMetadata = true } = options;
57
89
  const [owner, uri, walletAddr] = await Promise.all([
58
- registry.ownerOf(agentId),
59
- registry.tokenURI(agentId),
60
- registry.getAgentWallet(agentId),
90
+ client.readContract({
91
+ address: registryAddress,
92
+ abi: IDENTITY_REGISTRY_ABI,
93
+ functionName: 'ownerOf',
94
+ args: [BigInt(agentId)],
95
+ }),
96
+ client.readContract({
97
+ address: registryAddress,
98
+ abi: IDENTITY_REGISTRY_ABI,
99
+ functionName: 'tokenURI',
100
+ args: [BigInt(agentId)],
101
+ }),
102
+ client.readContract({
103
+ address: registryAddress,
104
+ abi: IDENTITY_REGISTRY_ABI,
105
+ functionName: 'getAgentWallet',
106
+ args: [BigInt(agentId)],
107
+ }),
61
108
  ]);
62
- const agentWallet = walletAddr === ethers.ZeroAddress ? null : walletAddr;
109
+ const agentWallet = walletAddr === zeroAddress ? null : walletAddr;
63
110
  let metadata = null;
64
111
  if (fetchMetadata) {
65
112
  try {
@@ -76,12 +123,17 @@ export async function getAgent(agentId, options) {
76
123
  * Read an agent's reputation summary from the Reputation Registry.
77
124
  *
78
125
  * @param agentId The on-chain agent token ID
79
- * @param options Reputation registry address, provider, and optional filters
126
+ * @param options Reputation registry address, client, and optional filters
80
127
  */
81
128
  export async function getReputation(agentId, options) {
82
- const { reputationRegistryAddress, provider, clients = [], tag1 = '', tag2 = '', } = options;
83
- const reputation = new ethers.Contract(reputationRegistryAddress, REPUTATION_REGISTRY_ABI, provider);
84
- const [count, summaryValue, valueDecimals] = await reputation.getSummary(agentId, clients, tag1, tag2);
129
+ const { reputationRegistryAddress, client, clients = [], tag1 = '', tag2 = '', } = options;
130
+ const result = await client.readContract({
131
+ address: reputationRegistryAddress,
132
+ abi: REPUTATION_REGISTRY_ABI,
133
+ functionName: 'getSummary',
134
+ args: [BigInt(agentId), clients, tag1, tag2],
135
+ });
136
+ const [count, summaryValue, valueDecimals] = result;
85
137
  const decimals = Number(valueDecimals);
86
138
  const rawValue = BigInt(summaryValue);
87
139
  const score = Number(rawValue) / 10 ** decimals;
package/dist/siwa.d.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  * Provides message building, signing (agent-side), and verification (server-side).
6
6
  *
7
7
  * Dependencies:
8
- * npm install ethers
8
+ * npm install viem
9
9
  */
10
- import { ethers } from 'ethers';
10
+ import { type PublicClient } from 'viem';
11
11
  import { AgentProfile, ServiceType, TrustModel } from './registry.js';
12
12
  export interface SIWAMessageFields {
13
13
  domain: string;
@@ -93,7 +93,7 @@ export declare function signSIWAMessageUnsafe(privateKey: string, fields: SIWAMe
93
93
  * @param signature EIP-191 signature hex string
94
94
  * @param expectedDomain The server's domain (for domain binding)
95
95
  * @param nonceValid Callback that returns true if the nonce is valid and unconsumed
96
- * @param provider ethers Provider for onchain verification
96
+ * @param client viem PublicClient for onchain verification
97
97
  * @param criteria Optional criteria to validate agent profile/reputation after ownership check
98
98
  */
99
- export declare function verifySIWA(message: string, signature: string, expectedDomain: string, nonceValid: (nonce: string) => boolean | Promise<boolean>, provider: ethers.Provider, criteria?: SIWAVerifyCriteria): Promise<SIWAVerificationResult>;
99
+ export declare function verifySIWA(message: string, signature: string, expectedDomain: string, nonceValid: (nonce: string) => boolean | Promise<boolean>, client: PublicClient, criteria?: SIWAVerifyCriteria): Promise<SIWAVerificationResult>;
package/dist/siwa.js CHANGED
@@ -5,9 +5,10 @@
5
5
  * Provides message building, signing (agent-side), and verification (server-side).
6
6
  *
7
7
  * Dependencies:
8
- * npm install ethers
8
+ * npm install viem
9
9
  */
10
- import { ethers } from 'ethers';
10
+ import { verifyMessage, hashMessage, } from 'viem';
11
+ import { privateKeyToAccount } from 'viem/accounts';
11
12
  import * as crypto from 'crypto';
12
13
  import { getAgent, getReputation } from './registry.js';
13
14
  // ─── Message Construction ────────────────────────────────────────────
@@ -136,12 +137,13 @@ export async function signSIWAMessage(fields, keystoreConfig) {
136
137
  * Kept only for server-side testing or environments without keystore.
137
138
  */
138
139
  export async function signSIWAMessageUnsafe(privateKey, fields) {
139
- const wallet = new ethers.Wallet(privateKey);
140
- if (wallet.address.toLowerCase() !== fields.address.toLowerCase()) {
141
- throw new Error(`Address mismatch: wallet is ${wallet.address}, message says ${fields.address}`);
140
+ const hexKey = (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`);
141
+ const account = privateKeyToAccount(hexKey);
142
+ if (account.address.toLowerCase() !== fields.address.toLowerCase()) {
143
+ throw new Error(`Address mismatch: wallet is ${account.address}, message says ${fields.address}`);
142
144
  }
143
145
  const message = buildSIWAMessage(fields);
144
- const signature = await wallet.signMessage(message);
146
+ const signature = await account.signMessage({ message });
145
147
  return { message, signature };
146
148
  }
147
149
  // ─── Server-Side Verification ────────────────────────────────────────
@@ -161,19 +163,24 @@ export async function signSIWAMessageUnsafe(privateKey, fields) {
161
163
  * @param signature EIP-191 signature hex string
162
164
  * @param expectedDomain The server's domain (for domain binding)
163
165
  * @param nonceValid Callback that returns true if the nonce is valid and unconsumed
164
- * @param provider ethers Provider for onchain verification
166
+ * @param client viem PublicClient for onchain verification
165
167
  * @param criteria Optional criteria to validate agent profile/reputation after ownership check
166
168
  */
167
- export async function verifySIWA(message, signature, expectedDomain, nonceValid, provider, criteria) {
169
+ export async function verifySIWA(message, signature, expectedDomain, nonceValid, client, criteria) {
168
170
  try {
169
171
  // 1. Parse
170
172
  const fields = parseSIWAMessage(message);
171
173
  // 2. Recover signer
172
- const recovered = ethers.verifyMessage(message, signature);
173
- // 3. Address match
174
- if (recovered.toLowerCase() !== fields.address.toLowerCase()) {
175
- return { valid: false, address: recovered, agentId: fields.agentId, agentRegistry: fields.agentRegistry, chainId: fields.chainId, error: 'Recovered address does not match message address' };
174
+ const isValid = await verifyMessage({
175
+ address: fields.address,
176
+ message,
177
+ signature: signature,
178
+ });
179
+ if (!isValid) {
180
+ return { valid: false, address: fields.address, agentId: fields.agentId, agentRegistry: fields.agentRegistry, chainId: fields.chainId, error: 'Invalid signature' };
176
181
  }
182
+ const recovered = fields.address;
183
+ // 3. Address match is implicit in verifyMessage (it checks against the address)
177
184
  // 4. Domain binding
178
185
  if (fields.domain !== expectedDomain) {
179
186
  return { valid: false, address: recovered, agentId: fields.agentId, agentRegistry: fields.agentRegistry, chainId: fields.chainId, error: `Domain mismatch: expected ${expectedDomain}, got ${fields.domain}` };
@@ -197,16 +204,24 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
197
204
  return { valid: false, address: recovered, agentId: fields.agentId, agentRegistry: fields.agentRegistry, chainId: fields.chainId, error: 'Invalid agentRegistry format' };
198
205
  }
199
206
  const registryAddress = registryParts[2];
200
- const registry = new ethers.Contract(registryAddress, ['function ownerOf(uint256) view returns (address)'], provider);
201
- const owner = await registry.ownerOf(fields.agentId);
207
+ const owner = await client.readContract({
208
+ address: registryAddress,
209
+ abi: [{ name: 'ownerOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'tokenId', type: 'uint256' }], outputs: [{ name: '', type: 'address' }] }],
210
+ functionName: 'ownerOf',
211
+ args: [BigInt(fields.agentId)],
212
+ });
202
213
  if (owner.toLowerCase() !== recovered.toLowerCase()) {
203
214
  // 7b. ERC-1271 fallback for smart contract wallets / EIP-7702 delegated accounts.
204
215
  // If ecrecover doesn't match the NFT owner, the owner may be a contract
205
216
  // that validates signatures via isValidSignature (ERC-1271).
206
- const messageHash = ethers.hashMessage(message);
217
+ const messageHash = hashMessage(message);
207
218
  try {
208
- const ownerContract = new ethers.Contract(owner, ['function isValidSignature(bytes32, bytes) view returns (bytes4)'], provider);
209
- const magicValue = await ownerContract.isValidSignature(messageHash, signature);
219
+ const magicValue = await client.readContract({
220
+ address: owner,
221
+ abi: [{ name: 'isValidSignature', type: 'function', stateMutability: 'view', inputs: [{ name: 'hash', type: 'bytes32' }, { name: 'signature', type: 'bytes' }], outputs: [{ name: '', type: 'bytes4' }] }],
222
+ functionName: 'isValidSignature',
223
+ args: [messageHash, signature],
224
+ });
210
225
  // ERC-1271 magic value: 0x1626ba7e
211
226
  if (magicValue !== '0x1626ba7e') {
212
227
  return { valid: false, address: recovered, agentId: fields.agentId, agentRegistry: fields.agentRegistry, chainId: fields.chainId, error: 'Signer is not the owner of this agent NFT (ERC-1271 check also failed)' };
@@ -230,7 +245,7 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
230
245
  return baseResult;
231
246
  const agent = await getAgent(fields.agentId, {
232
247
  registryAddress: registryAddress,
233
- provider,
248
+ client,
234
249
  fetchMetadata: true,
235
250
  });
236
251
  baseResult.agent = agent;
@@ -261,7 +276,7 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
261
276
  }
262
277
  const rep = await getReputation(fields.agentId, {
263
278
  reputationRegistryAddress: criteria.reputationRegistryAddress,
264
- provider,
279
+ client,
265
280
  });
266
281
  if (criteria.minFeedbackCount !== undefined && rep.count < criteria.minFeedbackCount) {
267
282
  return { ...baseResult, valid: false, error: `Agent feedback count ${rep.count} below minimum ${criteria.minFeedbackCount}` };
package/package.json CHANGED
@@ -1,28 +1,56 @@
1
1
  {
2
2
  "name": "@buildersgarden/siwa",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
7
- "./keystore": { "types": "./dist/keystore.d.ts", "default": "./dist/keystore.js" },
8
- "./siwa": { "types": "./dist/siwa.d.ts", "default": "./dist/siwa.js" },
9
- "./memory": { "types": "./dist/memory.d.ts", "default": "./dist/memory.js" },
10
- "./proxy-auth": { "types": "./dist/proxy-auth.d.ts", "default": "./dist/proxy-auth.js" },
11
- "./registry": { "types": "./dist/registry.d.ts", "default": "./dist/registry.js" },
12
- "./addresses": { "types": "./dist/addresses.d.ts", "default": "./dist/addresses.js" }
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ },
10
+ "./keystore": {
11
+ "types": "./dist/keystore.d.ts",
12
+ "default": "./dist/keystore.js"
13
+ },
14
+ "./siwa": {
15
+ "types": "./dist/siwa.d.ts",
16
+ "default": "./dist/siwa.js"
17
+ },
18
+ "./memory": {
19
+ "types": "./dist/memory.d.ts",
20
+ "default": "./dist/memory.js"
21
+ },
22
+ "./proxy-auth": {
23
+ "types": "./dist/proxy-auth.d.ts",
24
+ "default": "./dist/proxy-auth.js"
25
+ },
26
+ "./registry": {
27
+ "types": "./dist/registry.d.ts",
28
+ "default": "./dist/registry.js"
29
+ },
30
+ "./addresses": {
31
+ "types": "./dist/addresses.d.ts",
32
+ "default": "./dist/addresses.js"
33
+ }
13
34
  },
14
35
  "main": "./dist/index.js",
15
36
  "types": "./dist/index.d.ts",
16
- "files": ["dist"],
17
- "publishConfig": { "access": "public" },
18
- "scripts": {
19
- "build": "tsc",
20
- "clean": "rm -rf dist"
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
21
42
  },
22
43
  "dependencies": {
23
- "ethers": "^6.14.3"
44
+ "@noble/ciphers": "^0.5.0",
45
+ "@noble/hashes": "^1.4.0",
46
+ "viem": "^2.21.0"
24
47
  },
25
48
  "devDependencies": {
49
+ "@types/node": "^25.2.1",
26
50
  "typescript": "^5.5.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc",
54
+ "clean": "rm -rf dist"
27
55
  }
28
- }
56
+ }