@cofhe/sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/adapters/ethers5.test.ts +174 -0
- package/adapters/ethers5.ts +36 -0
- package/adapters/ethers6.test.ts +169 -0
- package/adapters/ethers6.ts +36 -0
- package/adapters/hardhat-node.ts +167 -0
- package/adapters/hardhat.hh2.test.ts +159 -0
- package/adapters/hardhat.ts +36 -0
- package/adapters/index.test.ts +20 -0
- package/adapters/index.ts +5 -0
- package/adapters/smartWallet.ts +99 -0
- package/adapters/test-utils.ts +53 -0
- package/adapters/types.ts +6 -0
- package/adapters/wagmi.test.ts +156 -0
- package/adapters/wagmi.ts +17 -0
- package/chains/chains/arbSepolia.ts +14 -0
- package/chains/chains/baseSepolia.ts +14 -0
- package/chains/chains/hardhat.ts +15 -0
- package/chains/chains/localcofhe.ts +14 -0
- package/chains/chains/sepolia.ts +14 -0
- package/chains/chains.test.ts +50 -0
- package/chains/defineChain.ts +18 -0
- package/chains/index.ts +35 -0
- package/chains/types.ts +32 -0
- package/core/baseBuilder.ts +119 -0
- package/core/client.test.ts +315 -0
- package/core/client.ts +292 -0
- package/core/clientTypes.ts +108 -0
- package/core/config.test.ts +235 -0
- package/core/config.ts +220 -0
- package/core/decrypt/MockQueryDecrypterAbi.ts +129 -0
- package/core/decrypt/cofheMocksSealOutput.ts +57 -0
- package/core/decrypt/decryptHandleBuilder.ts +287 -0
- package/core/decrypt/decryptUtils.ts +28 -0
- package/core/decrypt/tnSealOutputV1.ts +59 -0
- package/core/decrypt/tnSealOutputV2.ts +298 -0
- package/core/encrypt/MockZkVerifierAbi.ts +106 -0
- package/core/encrypt/cofheMocksZkVerifySign.ts +284 -0
- package/core/encrypt/encryptInputsBuilder.test.ts +751 -0
- package/core/encrypt/encryptInputsBuilder.ts +560 -0
- package/core/encrypt/encryptUtils.ts +67 -0
- package/core/encrypt/zkPackProveVerify.ts +335 -0
- package/core/error.ts +168 -0
- package/core/fetchKeys.test.ts +195 -0
- package/core/fetchKeys.ts +144 -0
- package/core/index.ts +89 -0
- package/core/keyStore.test.ts +226 -0
- package/core/keyStore.ts +154 -0
- package/core/permits.test.ts +494 -0
- package/core/permits.ts +200 -0
- package/core/types.ts +398 -0
- package/core/utils.ts +130 -0
- package/dist/adapters.cjs +88 -0
- package/dist/adapters.d.cts +14576 -0
- package/dist/adapters.d.ts +14576 -0
- package/dist/adapters.js +83 -0
- package/dist/chains.cjs +114 -0
- package/dist/chains.d.cts +121 -0
- package/dist/chains.d.ts +121 -0
- package/dist/chains.js +1 -0
- package/dist/chunk-UGBVZNRT.js +818 -0
- package/dist/chunk-WEAZ25JO.js +105 -0
- package/dist/chunk-WGCRJCBR.js +2523 -0
- package/dist/clientTypes-5_1nwtUe.d.cts +914 -0
- package/dist/clientTypes-Es7fyi65.d.ts +914 -0
- package/dist/core.cjs +3414 -0
- package/dist/core.d.cts +111 -0
- package/dist/core.d.ts +111 -0
- package/dist/core.js +3 -0
- package/dist/node.cjs +3286 -0
- package/dist/node.d.cts +22 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +91 -0
- package/dist/permit-fUSe6KKq.d.cts +349 -0
- package/dist/permit-fUSe6KKq.d.ts +349 -0
- package/dist/permits.cjs +871 -0
- package/dist/permits.d.cts +1045 -0
- package/dist/permits.d.ts +1045 -0
- package/dist/permits.js +1 -0
- package/dist/types-KImPrEIe.d.cts +48 -0
- package/dist/types-KImPrEIe.d.ts +48 -0
- package/dist/web.cjs +3478 -0
- package/dist/web.d.cts +38 -0
- package/dist/web.d.ts +38 -0
- package/dist/web.js +240 -0
- package/dist/zkProve.worker.cjs +93 -0
- package/dist/zkProve.worker.d.cts +2 -0
- package/dist/zkProve.worker.d.ts +2 -0
- package/dist/zkProve.worker.js +91 -0
- package/node/client.test.ts +147 -0
- package/node/config.test.ts +68 -0
- package/node/encryptInputs.test.ts +155 -0
- package/node/index.ts +97 -0
- package/node/storage.ts +51 -0
- package/package.json +27 -15
- package/permits/index.ts +68 -0
- package/permits/localstorage.test.ts +117 -0
- package/permits/permit.test.ts +477 -0
- package/permits/permit.ts +405 -0
- package/permits/sealing.test.ts +84 -0
- package/permits/sealing.ts +131 -0
- package/permits/signature.ts +79 -0
- package/permits/store.test.ts +128 -0
- package/permits/store.ts +166 -0
- package/permits/test-utils.ts +20 -0
- package/permits/types.ts +191 -0
- package/permits/utils.ts +62 -0
- package/permits/validation.test.ts +288 -0
- package/permits/validation.ts +369 -0
- package/web/client.web.test.ts +147 -0
- package/web/config.web.test.ts +69 -0
- package/web/encryptInputs.web.test.ts +172 -0
- package/web/index.ts +161 -0
- package/web/storage.ts +34 -0
- package/web/worker.builder.web.test.ts +148 -0
- package/web/worker.config.web.test.ts +329 -0
- package/web/worker.output.web.test.ts +84 -0
- package/web/workerManager.test.ts +80 -0
- package/web/workerManager.ts +214 -0
- package/web/workerManager.web.test.ts +114 -0
- package/web/zkProve.worker.ts +133 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @cofhe/sdk Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8fda09a: Removes `Promise<boolean>` return type from `client.connect(...)`, instead throws an error if the connection fails.
|
|
8
|
+
- e0caeca: Adds `environment: 'node' | 'web' | 'hardhat' | 'react'` option to config. Exposed via `client.config.enviroment`. Automatically populated appropriately within the various `createCofhesdkConfig` functions.
|
|
9
|
+
- 2a9d6c5: Updated to new CoFHE /sealoutputV2 endpoint - uses polling to fetch decryption results instead of long running open HTTP connections.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 4057a76: Add WebWorker for zkProve
|
|
14
|
+
- dba2759: Add getOrCreate permit functions
|
|
15
|
+
- 7c861af: Remove `initializationResults` from cofhesdk client.
|
|
16
|
+
|
|
17
|
+
## 0.1.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- a1d1323: Add repository info to package.json of public packages to fix npm publish provenance issue.
|
|
22
|
+
- d232d11: Ensure publish includes correct src and dist files
|
|
23
|
+
- b6521fb: Update publish workflow to create versioning PR upon merge with changeset.
|
|
24
|
+
|
|
25
|
+
## 0.1.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- 87fc8a0: Initial extraction from cofhejs. Split permit `create` into type specific creators: `createSelf`, `createShared`, and `importShared`
|
|
30
|
+
- 8d41cf2: Combine existing packages into more reasonable defaults. New package layout is @cofhe/sdk (includes all the core logic for configuring and creating a @cofhe/sdk client, encrypting values, and decrypting handles), mock-contracts, hardhat-plugin, and react.
|
|
31
|
+
- 738b9f5: Adding adapters for ethers5/6, Wagmi and HardHat
|
|
32
|
+
- a83facb: Prepare for initial release. Rename scope from `@cofhesdk` to `@cofhe` and rename `cofhesdk` package to `@cofhe/sdk`. Create `publish.yml` to publish `beta` packages on merged PR, and `latest` on changeset PR.
|
|
33
|
+
- 9a7c98e: Create core store, split initialize into `create` and `connect`, and port `encryptInputs` with improvements.
|
|
34
|
+
- 58e93a8: Migrate cofhe-mock-contracts and cofhe-hardhat-plugin into @cofhe/sdk.
|
|
35
|
+
- fdf26d4: Move storage handling to web/node targets; add `fheKeysStorage` config with environment-specific defaults.
|
|
36
|
+
- f5b8e25: Add @cofhe/sdk config type and parsing.
|
|
37
|
+
- 3b135a8: Create @cofhe/sdk/node and @cofhe/sdk/web
|
|
38
|
+
|
|
39
|
+
Additional changes:
|
|
40
|
+
|
|
41
|
+
- Fhe keys aren't fetched until `client.encryptInputs(...).encrypt()`, they aren't used anywhere else other than encrypting inputs, so their fetching is deferred until then.
|
|
42
|
+
- Initializing the tfhe wasm is also deferred until `client.encryptInputs(...).encrypt()` is called (allows for deferred async initialization)
|
|
43
|
+
|
|
44
|
+
- 5b7c43b: Create @cofhe/sdk client, hook in permits.
|
|
45
|
+
Create encryptInputs functionality. (+ mocks)
|
|
46
|
+
Create decryptHandles functionality. (+ mocks)
|
|
47
|
+
Improve @cofhe/sdk errors and error handling.
|
|
48
|
+
|
|
49
|
+
fix - seal_output include error_message in failing Error.
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- cba73fd: Add duration and context information to the step callback function for encryptInputs. Split fetch keys step into InitTfhe and FetchKeys.
|
|
54
|
+
InitTfhe context includes a `tfheInitializationExecuted` indicating if tfhe was initialized (true) or already initialized (false).
|
|
55
|
+
FetchKeys returns flags for whether the fhe and crs keys were fetch from remote (true) or from cache (false).
|
|
56
|
+
- 4bc8182: Add storage and key store
|
|
57
|
+
|
|
58
|
+
This changelog is maintained by Changesets and will be populated on each release.
|
|
59
|
+
|
|
60
|
+
- Do not edit this file by hand.
|
|
61
|
+
- Upcoming changes can be previewed with `pnpm changeset status --verbose`.
|
|
62
|
+
- Entries are generated when the Changesets "Version Packages" PR is created/merged.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { parseEther } from 'viem';
|
|
3
|
+
import { Ethers5Adapter } from './ethers5.js';
|
|
4
|
+
import { createMockEIP1193Provider } from './test-utils.js';
|
|
5
|
+
import * as ethers5 from 'ethers5';
|
|
6
|
+
|
|
7
|
+
describe('Ethers5Adapter', () => {
|
|
8
|
+
const testRpcUrl = 'https://ethereum-sepolia.rpc.subquery.network/public';
|
|
9
|
+
const SEPOLIA_CHAIN_ID = 11155111;
|
|
10
|
+
let provider: ethers5.providers.JsonRpcProvider;
|
|
11
|
+
let wallet: ethers5.Wallet;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Create common setup for all tests
|
|
15
|
+
provider = new ethers5.providers.JsonRpcProvider(testRpcUrl);
|
|
16
|
+
wallet = new ethers5.Wallet('0x' + '1'.repeat(64), provider);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should work with real JsonRpcProvider and Wallet', async () => {
|
|
20
|
+
const result = await Ethers5Adapter(provider, wallet);
|
|
21
|
+
|
|
22
|
+
expect(result).toHaveProperty('publicClient');
|
|
23
|
+
expect(result).toHaveProperty('walletClient');
|
|
24
|
+
expect(result.publicClient).toBeDefined();
|
|
25
|
+
expect(result.walletClient).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should work with Web3Provider signer', async () => {
|
|
29
|
+
const mockEthereum = createMockEIP1193Provider();
|
|
30
|
+
const web3Provider = new ethers5.providers.Web3Provider(mockEthereum as any);
|
|
31
|
+
const wallet = new ethers5.Wallet('0x' + '1'.repeat(64), web3Provider);
|
|
32
|
+
|
|
33
|
+
const result = await Ethers5Adapter(web3Provider, wallet);
|
|
34
|
+
|
|
35
|
+
expect(result).toHaveProperty('publicClient');
|
|
36
|
+
expect(result).toHaveProperty('walletClient');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should work without configuration', async () => {
|
|
40
|
+
const result = await Ethers5Adapter(provider, wallet);
|
|
41
|
+
|
|
42
|
+
expect(result).toHaveProperty('publicClient');
|
|
43
|
+
expect(result).toHaveProperty('walletClient');
|
|
44
|
+
expect(result.publicClient).toBeDefined();
|
|
45
|
+
expect(result.walletClient).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should throw error when signer has no provider', async () => {
|
|
49
|
+
const signerWithoutProvider = { provider: null };
|
|
50
|
+
|
|
51
|
+
await expect(async () => {
|
|
52
|
+
await Ethers5Adapter(signerWithoutProvider as any, wallet);
|
|
53
|
+
}).rejects.toThrow('Provider does not support EIP-1193 interface');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should work with real network call', async () => {
|
|
57
|
+
const result = await Ethers5Adapter(provider, wallet);
|
|
58
|
+
|
|
59
|
+
// Test real network call
|
|
60
|
+
const chainId = await result.publicClient.getChainId();
|
|
61
|
+
expect(typeof chainId).toBe('number');
|
|
62
|
+
expect(chainId).toBeGreaterThan(0);
|
|
63
|
+
}, 10000);
|
|
64
|
+
|
|
65
|
+
describe('Provider Functions', () => {
|
|
66
|
+
it('should support getChainId', async () => {
|
|
67
|
+
const { publicClient } = await Ethers5Adapter(provider, wallet);
|
|
68
|
+
|
|
69
|
+
const chainId = await publicClient.getChainId();
|
|
70
|
+
expect(typeof chainId).toBe('number');
|
|
71
|
+
expect(chainId).toBe(SEPOLIA_CHAIN_ID); // Sepolia
|
|
72
|
+
}, 10000);
|
|
73
|
+
|
|
74
|
+
it('should support call (contract read)', async () => {
|
|
75
|
+
const { publicClient } = await Ethers5Adapter(provider, wallet);
|
|
76
|
+
|
|
77
|
+
// Test eth_call - get ETH balance of zero address
|
|
78
|
+
const balance = await publicClient.getBalance({
|
|
79
|
+
address: '0x0000000000000000000000000000000000000000',
|
|
80
|
+
});
|
|
81
|
+
expect(typeof balance).toBe('bigint');
|
|
82
|
+
}, 10000);
|
|
83
|
+
|
|
84
|
+
it('should support request (raw RPC)', async () => {
|
|
85
|
+
const { publicClient } = await Ethers5Adapter(provider, wallet);
|
|
86
|
+
|
|
87
|
+
// Test raw RPC request - viem requires both method and params
|
|
88
|
+
const blockNumber = (await publicClient.request({
|
|
89
|
+
method: 'eth_blockNumber',
|
|
90
|
+
})) as string;
|
|
91
|
+
expect(typeof blockNumber).toBe('string');
|
|
92
|
+
expect(blockNumber.startsWith('0x')).toBe(true);
|
|
93
|
+
}, 10000);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Signer Functions', () => {
|
|
97
|
+
it('should support getAddress', async () => {
|
|
98
|
+
const { walletClient } = await Ethers5Adapter(provider, wallet);
|
|
99
|
+
|
|
100
|
+
const addresses = await walletClient.getAddresses();
|
|
101
|
+
expect(Array.isArray(addresses)).toBe(true);
|
|
102
|
+
// Note: For HTTP transport, this might return empty array
|
|
103
|
+
// For EIP-1193 providers, it would return actual addresses
|
|
104
|
+
}, 10000);
|
|
105
|
+
|
|
106
|
+
it('should support signTypedData', async () => {
|
|
107
|
+
const { walletClient } = await Ethers5Adapter(provider, wallet);
|
|
108
|
+
|
|
109
|
+
const domain = {
|
|
110
|
+
name: 'Test',
|
|
111
|
+
version: '1',
|
|
112
|
+
chainId: SEPOLIA_CHAIN_ID, // Sepolia
|
|
113
|
+
verifyingContract: '0x0000000000000000000000000000000000000000' as const,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const types = {
|
|
117
|
+
Message: [{ name: 'content', type: 'string' }],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const message = { content: 'Hello World' };
|
|
121
|
+
|
|
122
|
+
const signature = await walletClient.signTypedData({
|
|
123
|
+
domain,
|
|
124
|
+
types,
|
|
125
|
+
primaryType: 'Message',
|
|
126
|
+
message,
|
|
127
|
+
account: walletClient.account!,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(typeof signature).toBe('string');
|
|
131
|
+
expect(signature.startsWith('0x')).toBe(true);
|
|
132
|
+
}, 10000);
|
|
133
|
+
|
|
134
|
+
it('should support sendTransaction', async () => {
|
|
135
|
+
const { publicClient, walletClient } = await Ethers5Adapter(provider, wallet);
|
|
136
|
+
|
|
137
|
+
const account = await walletClient.account;
|
|
138
|
+
const chain = await publicClient.getChainId();
|
|
139
|
+
|
|
140
|
+
console.log('Wallet client ethers5', account, chain);
|
|
141
|
+
|
|
142
|
+
// Try to send a transaction - this will fail due to insufficient funds
|
|
143
|
+
try {
|
|
144
|
+
console.log('estimating gas');
|
|
145
|
+
const gas = await publicClient.estimateGas({
|
|
146
|
+
account: wallet.address as `0x${string}`,
|
|
147
|
+
to: '0x0000000000000000000000000000000000000000',
|
|
148
|
+
value: parseEther('0'),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
console.log('sending transaction', await wallet.getAddress());
|
|
152
|
+
const hash = await walletClient.sendTransaction({
|
|
153
|
+
to: '0x0000000000000000000000000000000000000000' as `0x${string}`,
|
|
154
|
+
value: parseEther('0'),
|
|
155
|
+
gas,
|
|
156
|
+
account: walletClient.account!,
|
|
157
|
+
chain: walletClient.chain,
|
|
158
|
+
});
|
|
159
|
+
console.log('transaction sent', hash);
|
|
160
|
+
|
|
161
|
+
// If it succeeds (shouldn't due to no funds), verify the format
|
|
162
|
+
expect(typeof hash).toBe('string');
|
|
163
|
+
expect(hash.startsWith('0x')).toBe(true);
|
|
164
|
+
expect(hash.length).toBe(66);
|
|
165
|
+
} catch (error: any) {
|
|
166
|
+
// Expected errors: either insufficient funds (good!) or method not supported
|
|
167
|
+
const isInsufficientFunds = error.message.includes('insufficient funds') || error.message.includes('balance');
|
|
168
|
+
|
|
169
|
+
expect(isInsufficientFunds).toBe(true);
|
|
170
|
+
console.log('Expected error (insufficient funds):', error.message);
|
|
171
|
+
}
|
|
172
|
+
}, 10000);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createPublicClient, createWalletClient, custom } from 'viem';
|
|
2
|
+
import { privateKeyToAccount, toAccount } from 'viem/accounts';
|
|
3
|
+
import type { Signer as AbstractSigner, Wallet, providers } from 'ethers5';
|
|
4
|
+
import { type AdapterResult } from './types.js';
|
|
5
|
+
|
|
6
|
+
type Ethers5Signer = AbstractSigner | Wallet;
|
|
7
|
+
|
|
8
|
+
export async function Ethers5Adapter(provider: providers.Provider, signer: Ethers5Signer): Promise<AdapterResult> {
|
|
9
|
+
// Create transport from provider
|
|
10
|
+
const transport =
|
|
11
|
+
provider && 'send' in provider && typeof provider.send === 'function'
|
|
12
|
+
? // @ts-ignore - ethers5 provider.send is not typed
|
|
13
|
+
custom({ request: ({ method, params }: any) => provider.send(method, params ?? []) })
|
|
14
|
+
: (() => {
|
|
15
|
+
throw new Error('Provider does not support EIP-1193 interface');
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
// build a viem Account
|
|
19
|
+
const address = (await signer.getAddress()) as `0x${string}`;
|
|
20
|
+
let account: ReturnType<typeof privateKeyToAccount> | ReturnType<typeof toAccount> | `0x${string}`;
|
|
21
|
+
|
|
22
|
+
if ('privateKey' in signer && typeof (signer as Wallet).privateKey === 'string') {
|
|
23
|
+
// Local (true offline) signing → works with Infura via sendRawTransaction
|
|
24
|
+
account = privateKeyToAccount((signer as Wallet).privateKey as `0x${string}`);
|
|
25
|
+
} else if (provider && typeof provider.send === 'function') {
|
|
26
|
+
// Injected wallet (MetaMask/Coinbase) → wallet signs via eth_sendTransaction
|
|
27
|
+
account = address; // JSON-RPC account (not local signing)
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error('Signer does not expose a private key and no injected wallet is available.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const publicClient = createPublicClient({ transport });
|
|
33
|
+
const walletClient = createWalletClient({ transport, account });
|
|
34
|
+
|
|
35
|
+
return { publicClient, walletClient };
|
|
36
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { parseEther } from 'viem';
|
|
3
|
+
import { Ethers6Adapter } from './ethers6.js';
|
|
4
|
+
import { createMockEIP1193Provider } from './test-utils.js';
|
|
5
|
+
import * as ethers6 from 'ethers6';
|
|
6
|
+
|
|
7
|
+
describe('Ethers6Adapter', () => {
|
|
8
|
+
const testRpcUrl = 'https://ethereum-sepolia.rpc.subquery.network/public';
|
|
9
|
+
const SEPOLIA_CHAIN_ID = 11155111;
|
|
10
|
+
let provider: ethers6.JsonRpcProvider;
|
|
11
|
+
let wallet: ethers6.Wallet;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Create common setup for all tests
|
|
15
|
+
provider = new ethers6.JsonRpcProvider(testRpcUrl);
|
|
16
|
+
wallet = new ethers6.Wallet('0x' + '1'.repeat(64), provider);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should work with real JsonRpcProvider and Wallet', async () => {
|
|
20
|
+
const result = await Ethers6Adapter(provider, wallet);
|
|
21
|
+
|
|
22
|
+
expect(result).toHaveProperty('publicClient');
|
|
23
|
+
expect(result).toHaveProperty('walletClient');
|
|
24
|
+
expect(result.publicClient).toBeDefined();
|
|
25
|
+
expect(result.walletClient).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should work with BrowserProvider signer', async () => {
|
|
29
|
+
const mockEthereum = createMockEIP1193Provider();
|
|
30
|
+
const browserProvider = new ethers6.BrowserProvider(mockEthereum as any);
|
|
31
|
+
const wallet = new ethers6.Wallet('0x' + '1'.repeat(64), browserProvider);
|
|
32
|
+
|
|
33
|
+
const result = await Ethers6Adapter(browserProvider, wallet);
|
|
34
|
+
|
|
35
|
+
expect(result).toHaveProperty('publicClient');
|
|
36
|
+
expect(result).toHaveProperty('walletClient');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should work without configuration', async () => {
|
|
40
|
+
const result = await Ethers6Adapter(provider, wallet);
|
|
41
|
+
|
|
42
|
+
expect(result).toHaveProperty('publicClient');
|
|
43
|
+
expect(result).toHaveProperty('walletClient');
|
|
44
|
+
expect(result.publicClient).toBeDefined();
|
|
45
|
+
expect(result.walletClient).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should throw error when signer has no provider', async () => {
|
|
49
|
+
const signerWithoutProvider = { provider: null };
|
|
50
|
+
|
|
51
|
+
await expect(async () => {
|
|
52
|
+
await Ethers6Adapter(signerWithoutProvider as any, wallet);
|
|
53
|
+
}).rejects.toThrow('Provider does not support EIP-1193 interface');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should work with real network call', async () => {
|
|
57
|
+
const result = await Ethers6Adapter(provider, wallet);
|
|
58
|
+
|
|
59
|
+
// Test real network call
|
|
60
|
+
const chainId = await result.publicClient.getChainId();
|
|
61
|
+
expect(typeof chainId).toBe('number');
|
|
62
|
+
expect(chainId).toBeGreaterThan(0);
|
|
63
|
+
}, 10000);
|
|
64
|
+
|
|
65
|
+
describe('Provider Functions', () => {
|
|
66
|
+
it('should support getChainId', async () => {
|
|
67
|
+
const { publicClient } = await Ethers6Adapter(provider, wallet);
|
|
68
|
+
|
|
69
|
+
const chainId = await publicClient.getChainId();
|
|
70
|
+
expect(typeof chainId).toBe('number');
|
|
71
|
+
expect(chainId).toBe(SEPOLIA_CHAIN_ID); // sepolia
|
|
72
|
+
}, 10000);
|
|
73
|
+
|
|
74
|
+
it('should support call (contract read)', async () => {
|
|
75
|
+
const { publicClient } = await Ethers6Adapter(provider, wallet);
|
|
76
|
+
|
|
77
|
+
// Test eth_call - get ETH balance of zero address
|
|
78
|
+
const balance = await publicClient.getBalance({
|
|
79
|
+
address: '0x0000000000000000000000000000000000000000',
|
|
80
|
+
});
|
|
81
|
+
expect(typeof balance).toBe('bigint');
|
|
82
|
+
}, 10000);
|
|
83
|
+
|
|
84
|
+
it('should support request (raw RPC)', async () => {
|
|
85
|
+
const { publicClient } = await Ethers6Adapter(provider, wallet);
|
|
86
|
+
|
|
87
|
+
// Test raw RPC request - viem requires both method and params
|
|
88
|
+
const blockNumber = (await publicClient.request({
|
|
89
|
+
method: 'eth_blockNumber',
|
|
90
|
+
})) as string;
|
|
91
|
+
expect(typeof blockNumber).toBe('string');
|
|
92
|
+
expect(blockNumber.startsWith('0x')).toBe(true);
|
|
93
|
+
}, 10000);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Signer Functions', () => {
|
|
97
|
+
it('should support getAddress', async () => {
|
|
98
|
+
const { walletClient } = await Ethers6Adapter(provider, wallet);
|
|
99
|
+
|
|
100
|
+
const addresses = await walletClient.getAddresses();
|
|
101
|
+
expect(Array.isArray(addresses)).toBe(true);
|
|
102
|
+
// Note: For HTTP transport, this might return empty array
|
|
103
|
+
// For EIP-1193 providers, it would return actual addresses
|
|
104
|
+
}, 10000);
|
|
105
|
+
|
|
106
|
+
it('should support signTypedData', async () => {
|
|
107
|
+
const { walletClient } = await Ethers6Adapter(provider, wallet);
|
|
108
|
+
|
|
109
|
+
const domain = {
|
|
110
|
+
name: 'Test',
|
|
111
|
+
version: '1',
|
|
112
|
+
chainId: SEPOLIA_CHAIN_ID, // Sepolia
|
|
113
|
+
verifyingContract: '0x0000000000000000000000000000000000000000' as const,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const types = {
|
|
117
|
+
Message: [{ name: 'content', type: 'string' }],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const message = { content: 'Hello World' };
|
|
121
|
+
|
|
122
|
+
const signature = await walletClient.signTypedData({
|
|
123
|
+
domain,
|
|
124
|
+
types,
|
|
125
|
+
primaryType: 'Message',
|
|
126
|
+
message,
|
|
127
|
+
account: walletClient.account!,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(typeof signature).toBe('string');
|
|
131
|
+
expect(signature.startsWith('0x')).toBe(true);
|
|
132
|
+
}, 10000);
|
|
133
|
+
|
|
134
|
+
it('should support sendTransaction', async () => {
|
|
135
|
+
const { publicClient, walletClient } = await Ethers6Adapter(provider, wallet);
|
|
136
|
+
|
|
137
|
+
// Try to send a transaction - this will fail with Infura but we'll catch the error
|
|
138
|
+
try {
|
|
139
|
+
console.log('estimating gas');
|
|
140
|
+
const gas = await publicClient.estimateGas({
|
|
141
|
+
account: wallet.address as `0x${string}`,
|
|
142
|
+
to: '0x0000000000000000000000000000000000000000',
|
|
143
|
+
value: parseEther('0'),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
console.log('sending transaction', await wallet.getAddress());
|
|
147
|
+
const hash = await walletClient.sendTransaction({
|
|
148
|
+
to: '0x0000000000000000000000000000000000000000',
|
|
149
|
+
value: parseEther('0'),
|
|
150
|
+
gas,
|
|
151
|
+
account: walletClient.account!,
|
|
152
|
+
chain: walletClient.chain,
|
|
153
|
+
});
|
|
154
|
+
console.log('transaction sent', hash);
|
|
155
|
+
|
|
156
|
+
// If it succeeds (shouldn't with Infura), verify the format
|
|
157
|
+
expect(typeof hash).toBe('string');
|
|
158
|
+
expect(hash.startsWith('0x')).toBe(true);
|
|
159
|
+
expect(hash.length).toBe(66);
|
|
160
|
+
} catch (error: any) {
|
|
161
|
+
// Expected errors: either insufficient funds (good!) or method not supported
|
|
162
|
+
const isInsufficientFunds = error.message.includes('insufficient funds') || error.message.includes('balance');
|
|
163
|
+
|
|
164
|
+
expect(isInsufficientFunds).toBe(true);
|
|
165
|
+
console.log('Expected error (insufficient funds or method not supported):', error.message);
|
|
166
|
+
}
|
|
167
|
+
}, 10000);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createPublicClient, createWalletClient, custom } from 'viem';
|
|
2
|
+
import { privateKeyToAccount, toAccount } from 'viem/accounts';
|
|
3
|
+
import type { Wallet, AbstractSigner, Provider } from 'ethers6';
|
|
4
|
+
import { type AdapterResult } from './types.js';
|
|
5
|
+
|
|
6
|
+
type Ethers6Signer = AbstractSigner | Wallet;
|
|
7
|
+
|
|
8
|
+
export async function Ethers6Adapter(provider: Provider, signer: Ethers6Signer): Promise<AdapterResult> {
|
|
9
|
+
// Create transport from provider
|
|
10
|
+
const transport =
|
|
11
|
+
provider && 'send' in provider && typeof provider.send === 'function'
|
|
12
|
+
? // @ts-ignore - ethers6 provider.send is not typed
|
|
13
|
+
custom({ request: ({ method, params }: any) => provider.send(method, params ?? []) })
|
|
14
|
+
: (() => {
|
|
15
|
+
throw new Error('Provider does not support EIP-1193 interface');
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
// build a viem Account
|
|
19
|
+
const address = (await signer.getAddress()) as `0x${string}`;
|
|
20
|
+
let account: ReturnType<typeof privateKeyToAccount> | ReturnType<typeof toAccount> | `0x${string}`;
|
|
21
|
+
|
|
22
|
+
if ('privateKey' in signer && typeof (signer as Wallet).privateKey === 'string') {
|
|
23
|
+
// Local (true offline) signing → works with Infura via sendRawTransaction
|
|
24
|
+
account = privateKeyToAccount((signer as Wallet).privateKey as `0x${string}`); // local account
|
|
25
|
+
} else if (provider && typeof provider.send === 'function') {
|
|
26
|
+
// Injected wallet (MetaMask/Coinbase) → wallet signs via eth_sendTransaction
|
|
27
|
+
account = address; // JSON-RPC account (not local signing)
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error('Signer does not expose a private key and no injected wallet is available.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const publicClient = createPublicClient({ transport });
|
|
33
|
+
const walletClient = createWalletClient({ transport, account });
|
|
34
|
+
|
|
35
|
+
return { publicClient, walletClient };
|
|
36
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { createConnection } from 'net';
|
|
4
|
+
|
|
5
|
+
const sleep = promisify(setTimeout);
|
|
6
|
+
|
|
7
|
+
async function isPortAvailable(port: number): Promise<boolean> {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const connection = createConnection({ port, host: '127.0.0.1' });
|
|
10
|
+
|
|
11
|
+
connection.on('connect', () => {
|
|
12
|
+
connection.destroy();
|
|
13
|
+
resolve(false); // Port is in use
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
connection.on('error', () => {
|
|
17
|
+
resolve(true); // Port is available
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function testHardhatNode(port: number): Promise<boolean> {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`http://127.0.0.1:${port}`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
jsonrpc: '2.0',
|
|
29
|
+
method: 'eth_chainId',
|
|
30
|
+
params: [],
|
|
31
|
+
id: 1,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (response.ok) {
|
|
36
|
+
const data = (await response.json()) as { result: string };
|
|
37
|
+
// Check if it returns Hardhat's chain ID (31337 = 0x7a69)
|
|
38
|
+
return data.result === '0x7a69';
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class HardhatNode {
|
|
47
|
+
private process: ChildProcess | null = null;
|
|
48
|
+
private isReady = false;
|
|
49
|
+
|
|
50
|
+
async start(): Promise<void> {
|
|
51
|
+
if (this.process) {
|
|
52
|
+
return; // Already started
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if port 8545 is already in use
|
|
56
|
+
const portAvailable = await isPortAvailable(8545);
|
|
57
|
+
if (!portAvailable) {
|
|
58
|
+
// Port is in use, check if it's a Hardhat node we can use
|
|
59
|
+
const isHardhatNode = await testHardhatNode(8545);
|
|
60
|
+
if (isHardhatNode) {
|
|
61
|
+
console.log('Found existing Hardhat node on port 8545, using it...');
|
|
62
|
+
this.isReady = true;
|
|
63
|
+
return; // Use the existing node
|
|
64
|
+
} else {
|
|
65
|
+
throw new Error('Port 8545 is in use by a non-Hardhat service. Please free the port.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log('Starting Hardhat node...');
|
|
70
|
+
|
|
71
|
+
this.process = spawn('npx', ['hardhat', 'node'], {
|
|
72
|
+
cwd: process.cwd(),
|
|
73
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Wait for the node to be ready
|
|
77
|
+
await new Promise<void>((resolve, reject) => {
|
|
78
|
+
const timeout = setTimeout(() => {
|
|
79
|
+
reject(new Error('Hardhat node failed to start within 30 seconds'));
|
|
80
|
+
}, 30000);
|
|
81
|
+
|
|
82
|
+
const onData = (data: Buffer) => {
|
|
83
|
+
const output = data.toString();
|
|
84
|
+
if (output.includes('Started HTTP and WebSocket JSON-RPC server at')) {
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
this.isReady = true;
|
|
87
|
+
console.log('Hardhat node is ready!');
|
|
88
|
+
resolve();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
this.process!.stdout?.on('data', onData);
|
|
93
|
+
this.process!.stderr?.on('data', onData);
|
|
94
|
+
|
|
95
|
+
this.process!.on('error', (error) => {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
reject(error);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.process!.on('exit', (code) => {
|
|
101
|
+
if (code !== 0 && !this.isReady) {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
reject(new Error(`Hardhat node exited with code ${code}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Give it a bit more time to be fully ready
|
|
109
|
+
await sleep(2000);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async stop(): Promise<void> {
|
|
113
|
+
if (this.process) {
|
|
114
|
+
console.log('Stopping Hardhat node...');
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Immediately force kill - no graceful shutdown to avoid hanging
|
|
118
|
+
this.process.kill('SIGKILL');
|
|
119
|
+
this.process = null;
|
|
120
|
+
this.isReady = false;
|
|
121
|
+
|
|
122
|
+
// Quick port cleanup
|
|
123
|
+
await this.killProcessOnPort(8545);
|
|
124
|
+
|
|
125
|
+
console.log('Hardhat node stopped');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log('Error stopping node:', error);
|
|
128
|
+
// Force cleanup regardless
|
|
129
|
+
this.process = null;
|
|
130
|
+
this.isReady = false;
|
|
131
|
+
await this.killProcessOnPort(8545);
|
|
132
|
+
}
|
|
133
|
+
} else if (this.isReady) {
|
|
134
|
+
// We're using an existing node, just mark as not ready
|
|
135
|
+
console.log('Using external Hardhat node, not stopping it...');
|
|
136
|
+
this.isReady = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isRunning(): boolean {
|
|
141
|
+
return this.process !== null && this.isReady;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async killProcessOnPort(port: number): Promise<void> {
|
|
145
|
+
try {
|
|
146
|
+
const { spawn } = await import('child_process');
|
|
147
|
+
|
|
148
|
+
// Kill any process using the port (Linux/macOS) - faster timeout
|
|
149
|
+
const killProcess = spawn('sh', ['-c', `lsof -ti :${port} | xargs -r kill -9`], {
|
|
150
|
+
stdio: 'ignore',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await new Promise<void>((resolve) => {
|
|
154
|
+
killProcess.on('exit', () => resolve());
|
|
155
|
+
killProcess.on('error', () => resolve()); // Ignore errors
|
|
156
|
+
setTimeout(resolve, 300); // Shorter timeout - 300ms
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore errors - this is a cleanup attempt
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Global instance
|
|
165
|
+
export const hardhatNode = new HardhatNode();
|
|
166
|
+
|
|
167
|
+
// Global instance - no process handlers to avoid interfering with test process
|