@buildersgarden/siwa 0.0.2 → 0.0.4

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 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,52 @@ 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 };
325
+ }
326
+ /**
327
+ * Parse a numeric value from JSON (string/number) to bigint.
328
+ * Returns undefined for null, undefined, or zero values.
329
+ * Zero is returned as undefined so viem encodes it as empty (0x80 in RLP).
330
+ */
331
+ function parseBigIntFromJson(value) {
332
+ if (value === null || value === undefined)
333
+ return undefined;
334
+ let result;
335
+ if (typeof value === 'bigint') {
336
+ result = value;
337
+ }
338
+ else if (typeof value === 'number') {
339
+ result = BigInt(value);
340
+ }
341
+ else if (typeof value === 'string') {
342
+ // Handle hex strings (0x...) and decimal strings
343
+ result = BigInt(value);
344
+ }
345
+ else {
346
+ return undefined;
347
+ }
348
+ // Return undefined for zero so viem encodes it as empty (0x80)
349
+ // instead of 0x00 which is non-canonical RLP
350
+ return result === 0n ? undefined : result;
351
+ }
352
+ /**
353
+ * Parse a numeric value, keeping zero as 0n (for fields like nonce where 0 is valid).
354
+ */
355
+ function parseBigIntKeepZero(value) {
356
+ if (value === null || value === undefined)
357
+ return undefined;
358
+ if (typeof value === 'bigint')
359
+ return value;
360
+ if (typeof value === 'number')
361
+ return BigInt(value);
362
+ if (typeof value === 'string')
363
+ return BigInt(value);
364
+ return undefined;
246
365
  }
247
366
  /**
248
367
  * Sign a transaction.
@@ -255,16 +374,45 @@ export async function signTransaction(tx, config = {}) {
255
374
  const data = await proxyRequest(config, '/sign-transaction', { tx: tx });
256
375
  return { signedTx: data.signedTx, address: data.address };
257
376
  }
258
- const wallet = await _loadWalletInternal(config);
259
- if (!wallet)
377
+ const privateKey = await _loadPrivateKeyInternal(config);
378
+ if (!privateKey)
260
379
  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 };
380
+ const account = privateKeyToAccount(privateKey);
381
+ // Parse numeric fields from JSON representation (strings) to bigints.
382
+ // For 'value', zero is converted to undefined so viem encodes it as 0x80 (empty)
383
+ // instead of 0x00, which is non-canonical RLP and rejected by nodes.
384
+ const value = parseBigIntFromJson(tx.value);
385
+ const gas = parseBigIntKeepZero(tx.gasLimit ?? tx.gas);
386
+ const maxFeePerGas = parseBigIntKeepZero(tx.maxFeePerGas);
387
+ const maxPriorityFeePerGas = parseBigIntKeepZero(tx.maxPriorityFeePerGas);
388
+ const gasPrice = parseBigIntKeepZero(tx.gasPrice);
389
+ // Build transaction request for viem
390
+ const viemTx = {
391
+ to: tx.to,
392
+ data: tx.data,
393
+ value,
394
+ nonce: tx.nonce,
395
+ chainId: tx.chainId,
396
+ gas,
397
+ };
398
+ // Handle EIP-1559 vs legacy transactions
399
+ if (tx.type === 2 || tx.maxFeePerGas !== undefined) {
400
+ viemTx.type = 'eip1559';
401
+ viemTx.maxFeePerGas = maxFeePerGas;
402
+ viemTx.maxPriorityFeePerGas = maxPriorityFeePerGas;
403
+ }
404
+ else if (tx.gasPrice !== undefined) {
405
+ viemTx.type = 'legacy';
406
+ viemTx.gasPrice = gasPrice;
407
+ }
408
+ if (tx.accessList) {
409
+ viemTx.accessList = tx.accessList;
410
+ }
411
+ const signedTx = await account.signTransaction(viemTx);
412
+ return { signedTx, address: account.address };
264
413
  }
265
414
  /**
266
415
  * Sign an EIP-7702 authorization for delegating the EOA to a contract.
267
- * Requires ethers >= 6.14.3.
268
416
  *
269
417
  * This allows the agent's EOA to temporarily act as a smart contract
270
418
  * during a type 4 transaction. The private key is loaded, used, and
@@ -279,37 +427,54 @@ export async function signAuthorization(auth, config = {}) {
279
427
  const data = await proxyRequest(config, '/sign-authorization', { auth });
280
428
  return data;
281
429
  }
282
- const wallet = await _loadWalletInternal(config);
283
- if (!wallet)
430
+ const privateKey = await _loadPrivateKeyInternal(config);
431
+ if (!privateKey)
284
432
  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 }),
433
+ const account = privateKeyToAccount(privateKey);
434
+ // EIP-7702 authorization signing using viem experimental
435
+ const chainId = auth.chainId ?? 1;
436
+ const nonce = auth.nonce ?? 0;
437
+ // Hash the authorization struct according to EIP-7702
438
+ const authHash = hashAuthorization({
439
+ contractAddress: auth.address,
440
+ chainId,
441
+ nonce,
294
442
  });
295
- // wallet goes out of scope — private key discarded
296
- return authorization;
443
+ // Sign the authorization hash
444
+ const signature = await account.sign({ hash: authHash });
445
+ // Parse signature into r, s, yParity
446
+ const r = signature.slice(0, 66);
447
+ const s = `0x${signature.slice(66, 130)}`;
448
+ const v = parseInt(signature.slice(130, 132), 16);
449
+ const yParity = v - 27; // Convert v to yParity (0 or 1)
450
+ return {
451
+ address: auth.address,
452
+ nonce,
453
+ chainId,
454
+ yParity,
455
+ r,
456
+ s,
457
+ };
297
458
  }
298
459
  /**
299
- * Get a connected signer (for contract interactions).
300
- * NOTE: This returns a signer with the private key in memory.
460
+ * Get a wallet client for contract interactions.
461
+ * NOTE: This creates a client with the private key in memory.
301
462
  * Use only within a narrow scope and discard immediately.
302
463
  * Prefer signMessage() / signTransaction() when possible.
303
464
  */
304
- export async function getSigner(provider, config = {}) {
465
+ export async function getWalletClient(rpcUrl, config = {}) {
305
466
  const backend = config.backend || await detectBackend();
306
467
  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.');
468
+ throw new Error('getWalletClient() is not supported via proxy. The private key cannot be serialized over HTTP. Use signMessage() or signTransaction() instead.');
308
469
  }
309
- const wallet = await _loadWalletInternal(config);
310
- if (!wallet)
470
+ const privateKey = await _loadPrivateKeyInternal(config);
471
+ if (!privateKey)
311
472
  throw new Error('No wallet found. Run createWallet() first.');
312
- return wallet.connect(provider);
473
+ const account = privateKeyToAccount(privateKey);
474
+ return createWalletClient({
475
+ account,
476
+ transport: http(rpcUrl),
477
+ });
313
478
  }
314
479
  /**
315
480
  * Delete the stored wallet from the active backend.
@@ -334,9 +499,9 @@ export async function deleteWallet(config = {}) {
334
499
  }
335
500
  }
336
501
  // ---------------------------------------------------------------------------
337
- // Internal — loads the wallet. NEVER exposed publicly.
502
+ // Internal — loads the private key. NEVER exposed publicly.
338
503
  // ---------------------------------------------------------------------------
339
- async function _loadWalletInternal(config = {}) {
504
+ async function _loadPrivateKeyInternal(config = {}) {
340
505
  const backend = config.backend || await detectBackend();
341
506
  const keystorePath = config.keystorePath || process.env.KEYSTORE_PATH || DEFAULT_KEYSTORE_PATH;
342
507
  let privateKey = null;
@@ -346,11 +511,13 @@ async function _loadWalletInternal(config = {}) {
346
511
  privateKey = await encryptedFileLoad(password, keystorePath);
347
512
  break;
348
513
  }
349
- case 'env':
350
- privateKey = process.env.AGENT_PRIVATE_KEY || null;
514
+ case 'env': {
515
+ const envKey = process.env.AGENT_PRIVATE_KEY || null;
516
+ if (envKey) {
517
+ privateKey = (envKey.startsWith('0x') ? envKey : `0x${envKey}`);
518
+ }
351
519
  break;
520
+ }
352
521
  }
353
- if (!privateKey)
354
- return null;
355
- return new ethers.Wallet(privateKey);
522
+ return privateKey;
356
523
  }
@@ -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,61 @@
1
1
  {
2
2
  "name": "@buildersgarden/siwa",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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" },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/builders-garden/siwa",
43
+ "directory": "packages/siwa"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
18
48
  "scripts": {
19
49
  "build": "tsc",
20
50
  "clean": "rm -rf dist"
21
51
  },
22
52
  "dependencies": {
23
- "ethers": "^6.14.3"
53
+ "@noble/ciphers": "^0.5.0",
54
+ "@noble/hashes": "^1.4.0",
55
+ "viem": "^2.21.0"
24
56
  },
25
57
  "devDependencies": {
58
+ "@types/node": "^25.2.1",
26
59
  "typescript": "^5.5.0"
27
60
  }
28
61
  }