Skip to content

Commit 933d335

Browse files
randygrokjgimeno
andauthored
client impl part 1 (#112)
* client impl part 1 * add tests for client * fix: gas estimation for batch transactions and improve tests - Add 21000 gas per call in estimateIntrinsicGas for batch support - Unify env vars to use EXECUTOR_PRIVATE_KEY across all tests - Rewrite evnode-flows.ts with balance verification for all 4 flows * make package ready for npm * add examples for multiple types of tx * refactor: consolidate duplicate type definitions in client * refactor: extract BASE_TX_GAS constant for intrinsic gas estimation * refactor: replace ternary with if/else in calldata gas calculation * refactor: extract CREATE_GAS constant for contract deployment gas cost * refactor: add isCreateCall helper for contract creation checks * refactor: extract EVNODE_TX_FIELD_COUNT constant for RLP decode validation * refactor: split index.ts into types, encoding, signing, and client modules * test: add unit tests for encoding, signing, gas estimation, and validation * ci: add client unit tests job to unit workflow --------- Co-authored-by: Jonathan Gimeno <[email protected]>
1 parent 77c533c commit 933d335

25 files changed

+3067
-2
lines changed

.github/workflows/unit.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ jobs:
3838
-E "(kind(lib) | kind(bin) | kind(proc-macro))" \
3939
--no-tests=warn
4040
41+
client-test:
42+
name: client unit tests
43+
timeout-minutes: 10
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/checkout@v6
47+
- uses: actions/setup-node@v4
48+
with:
49+
node-version: '22'
50+
- name: Install dependencies
51+
working-directory: clients
52+
run: npm ci
53+
- name: Run unit tests
54+
working-directory: clients
55+
run: npm test
56+
4157
doc:
4258
name: doc tests
4359
env:
@@ -57,7 +73,7 @@ jobs:
5773
name: unit success
5874
runs-on: ubuntu-latest
5975
if: always()
60-
needs: [test]
76+
needs: [test, client-test]
6177
timeout-minutes: 30
6278
steps:
6379
- name: Decide whether the needed jobs succeeded or failed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ criterion/
2525
*.tmp
2626
*.log
2727

28+
# Node dependencies
29+
node_modules/
30+
2831
# Environment files
2932
.env
3033
.env.local
@@ -57,4 +60,4 @@ flamegraph.svg
5760
Thumbs.db
5861

5962
# Docker build artifacts
60-
/dist/
63+
/dist/

clients/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
dist/
3+
*.tgz

clients/README.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# @evstack/evnode-viem
2+
3+
Viem client extension for EvNode transactions (type 0x76).
4+
5+
## Installation
6+
7+
```bash
8+
npm install @evstack/evnode-viem viem
9+
```
10+
11+
## Usage
12+
13+
### Basic Transaction
14+
15+
```typescript
16+
import { createClient, http } from 'viem';
17+
import { privateKeyToAccount, sign } from 'viem/accounts';
18+
import { createEvnodeClient } from '@evstack/evnode-viem';
19+
20+
const client = createClient({
21+
transport: http('http://localhost:8545'),
22+
});
23+
24+
const account = privateKeyToAccount('0x...');
25+
26+
const evnode = createEvnodeClient({
27+
client,
28+
executor: {
29+
address: account.address,
30+
signHash: async (hash) => sign({ hash, privateKey: '0x...' }),
31+
},
32+
});
33+
34+
// Send a transaction
35+
const txHash = await evnode.send({
36+
calls: [
37+
{ to: '0x...', value: 0n, data: '0x' },
38+
],
39+
});
40+
```
41+
42+
### Batch Transactions
43+
44+
EvNode transactions support multiple calls in a single transaction:
45+
46+
```typescript
47+
const txHash = await evnode.send({
48+
calls: [
49+
{ to: recipient1, value: 1000000000000000n, data: '0x' },
50+
{ to: recipient2, value: 1000000000000000n, data: '0x' },
51+
],
52+
});
53+
```
54+
55+
### Sponsored Transactions
56+
57+
A sponsor can pay gas fees on behalf of the executor:
58+
59+
```typescript
60+
const evnode = createEvnodeClient({
61+
client,
62+
executor: { address: executorAddr, signHash: executorSignFn },
63+
sponsor: { address: sponsorAddr, signHash: sponsorSignFn },
64+
});
65+
66+
// Create intent (signed by executor)
67+
const intent = await evnode.createIntent({
68+
calls: [{ to: '0x...', value: 0n, data: '0x' }],
69+
});
70+
71+
// Sponsor signs and sends
72+
const txHash = await evnode.sponsorAndSend({ intent });
73+
```
74+
75+
## API
76+
77+
### `createEvnodeClient(options)`
78+
79+
Creates a new EvNode client.
80+
81+
**Options:**
82+
- `client` - Viem Client instance
83+
- `executor` - (optional) Default executor signer
84+
- `sponsor` - (optional) Default sponsor signer
85+
86+
### Client Methods
87+
88+
- `send(args)` - Sign and send an EvNode transaction
89+
- `createIntent(args)` - Create a sponsorable intent
90+
- `sponsorIntent(args)` - Add sponsor signature to an intent
91+
- `sponsorAndSend(args)` - Sponsor and send in one call
92+
- `serialize(signedTx)` - Serialize a signed transaction
93+
- `deserialize(hex)` - Deserialize a signed transaction
94+
95+
### Utility Functions
96+
97+
- `computeExecutorSigningHash(tx)` - Get hash for executor to sign
98+
- `computeSponsorSigningHash(tx, executorAddress)` - Get hash for sponsor to sign
99+
- `computeTxHash(signedTx)` - Get transaction hash
100+
- `recoverExecutor(signedTx)` - Recover executor address from signature
101+
- `recoverSponsor(tx, executorAddress)` - Recover sponsor address from signature
102+
- `estimateIntrinsicGas(calls)` - Estimate minimum gas for calls
103+
104+
## License
105+
106+
MIT

clients/examples/basic.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Basic example: Send a simple EvNode transaction
3+
*
4+
* Run with:
5+
* PRIVATE_KEY=0x... npx tsx examples/basic.ts
6+
*/
7+
import { createClient, http } from 'viem';
8+
import { privateKeyToAccount, sign } from 'viem/accounts';
9+
import { createEvnodeClient } from '../src/index.ts';
10+
11+
const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8545';
12+
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;
13+
14+
if (!PRIVATE_KEY) {
15+
console.error('Usage: PRIVATE_KEY=0x... npx tsx examples/basic.ts');
16+
process.exit(1);
17+
}
18+
19+
async function main() {
20+
const client = createClient({ transport: http(RPC_URL) });
21+
const account = privateKeyToAccount(PRIVATE_KEY);
22+
23+
const evnode = createEvnodeClient({
24+
client,
25+
executor: {
26+
address: account.address,
27+
signHash: async (hash) => sign({ hash, privateKey: PRIVATE_KEY }),
28+
},
29+
});
30+
31+
console.log('Executor:', account.address);
32+
33+
// Send a simple transaction (self-transfer with no value)
34+
const txHash = await evnode.send({
35+
calls: [
36+
{
37+
to: account.address,
38+
value: 0n,
39+
data: '0x',
40+
},
41+
],
42+
});
43+
44+
console.log('Transaction hash:', txHash);
45+
}
46+
47+
main().catch(console.error);

clients/examples/batch.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Batch example: Send multiple calls in a single transaction
3+
*
4+
* Run with:
5+
* PRIVATE_KEY=0x... npx tsx examples/batch.ts
6+
*/
7+
import { createClient, http, formatEther } from 'viem';
8+
import { privateKeyToAccount, sign } from 'viem/accounts';
9+
import { createEvnodeClient } from '../src/index.ts';
10+
11+
const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8545';
12+
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;
13+
14+
if (!PRIVATE_KEY) {
15+
console.error('Usage: PRIVATE_KEY=0x... npx tsx examples/batch.ts');
16+
process.exit(1);
17+
}
18+
19+
async function main() {
20+
const client = createClient({ transport: http(RPC_URL) });
21+
const account = privateKeyToAccount(PRIVATE_KEY);
22+
23+
const evnode = createEvnodeClient({
24+
client,
25+
executor: {
26+
address: account.address,
27+
signHash: async (hash) => sign({ hash, privateKey: PRIVATE_KEY }),
28+
},
29+
});
30+
31+
console.log('Executor:', account.address);
32+
33+
// Example recipients (in practice, use real addresses)
34+
const recipient1 = '0x1111111111111111111111111111111111111111' as const;
35+
const recipient2 = '0x2222222222222222222222222222222222222222' as const;
36+
const amount = 1000000000000000n; // 0.001 ETH
37+
38+
console.log(`\nSending ${formatEther(amount)} ETH to each recipient...`);
39+
40+
// Send batch transaction: multiple transfers in one tx
41+
const txHash = await evnode.send({
42+
calls: [
43+
{ to: recipient1, value: amount, data: '0x' },
44+
{ to: recipient2, value: amount, data: '0x' },
45+
],
46+
});
47+
48+
console.log('Transaction hash:', txHash);
49+
console.log('\nBoth transfers executed atomically in a single transaction.');
50+
}
51+
52+
main().catch(console.error);

clients/examples/contract-call.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Contract call example: Interact with a smart contract
3+
*
4+
* Run with:
5+
* PRIVATE_KEY=0x... CONTRACT=0x... npx tsx examples/contract-call.ts
6+
*/
7+
import { createClient, http, encodeFunctionData, parseAbi } from 'viem';
8+
import { privateKeyToAccount, sign } from 'viem/accounts';
9+
import { createEvnodeClient } from '../src/index.ts';
10+
11+
const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8545';
12+
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;
13+
const CONTRACT = process.env.CONTRACT as `0x${string}`;
14+
15+
if (!PRIVATE_KEY) {
16+
console.error('Usage: PRIVATE_KEY=0x... CONTRACT=0x... npx tsx examples/contract-call.ts');
17+
process.exit(1);
18+
}
19+
20+
async function main() {
21+
const client = createClient({ transport: http(RPC_URL) });
22+
const account = privateKeyToAccount(PRIVATE_KEY);
23+
24+
const evnode = createEvnodeClient({
25+
client,
26+
executor: {
27+
address: account.address,
28+
signHash: async (hash) => sign({ hash, privateKey: PRIVATE_KEY }),
29+
},
30+
});
31+
32+
console.log('Executor:', account.address);
33+
34+
// Example: ERC20 transfer
35+
// In practice, replace with your contract's ABI and function
36+
const abi = parseAbi([
37+
'function transfer(address to, uint256 amount) returns (bool)',
38+
]);
39+
40+
const data = encodeFunctionData({
41+
abi,
42+
functionName: 'transfer',
43+
args: ['0x1111111111111111111111111111111111111111', 1000000n],
44+
});
45+
46+
const contractAddress = CONTRACT ?? '0x0000000000000000000000000000000000000000';
47+
48+
console.log('\nCalling contract:', contractAddress);
49+
50+
const txHash = await evnode.send({
51+
calls: [
52+
{
53+
to: contractAddress,
54+
value: 0n,
55+
data,
56+
},
57+
],
58+
});
59+
60+
console.log('Transaction hash:', txHash);
61+
}
62+
63+
main().catch(console.error);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Contract deploy example: Deploy a smart contract
3+
*
4+
* Run with:
5+
* PRIVATE_KEY=0x... npx tsx examples/contract-deploy.ts
6+
*/
7+
import { createClient, http, type Hex } from 'viem';
8+
import { privateKeyToAccount, sign } from 'viem/accounts';
9+
import { createEvnodeClient } from '../src/index.ts';
10+
11+
const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8545';
12+
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;
13+
14+
if (!PRIVATE_KEY) {
15+
console.error('Usage: PRIVATE_KEY=0x... npx tsx examples/contract-deploy.ts');
16+
process.exit(1);
17+
}
18+
19+
async function main() {
20+
const client = createClient({ transport: http(RPC_URL) });
21+
const account = privateKeyToAccount(PRIVATE_KEY);
22+
23+
const evnode = createEvnodeClient({
24+
client,
25+
executor: {
26+
address: account.address,
27+
signHash: async (hash) => sign({ hash, privateKey: PRIVATE_KEY }),
28+
},
29+
});
30+
31+
console.log('Executor:', account.address);
32+
33+
// Simple storage contract bytecode
34+
// contract Storage { uint256 value; function set(uint256 v) { value = v; } function get() view returns (uint256) { return value; } }
35+
const bytecode: Hex = '0x608060405234801561001057600080fd5b5060df8061001f6000396000f3fe6080604052348015600f57600080fd5b5060043610603c5760003560e01c80636d4ce63c1460415780638a42ebe914605b575b600080fd5b60005460405190815260200160405180910390f35b606b6066366004606d565b600055565b005b600060208284031215607e57600080fd5b503591905056fea264697066735822122041c7f6d2d7b0d1c0d6c0d8e7f4c5b3a2918d7e6f5c4b3a291807d6e5f4c3b2a164736f6c63430008110033';
36+
37+
console.log('\nDeploying contract...');
38+
39+
// Deploy with to=null (CREATE)
40+
const txHash = await evnode.send({
41+
calls: [
42+
{
43+
to: null,
44+
value: 0n,
45+
data: bytecode,
46+
},
47+
],
48+
});
49+
50+
console.log('Transaction hash:', txHash);
51+
console.log('\nContract deployed. Check receipt for contract address.');
52+
}
53+
54+
main().catch(console.error);

0 commit comments

Comments
 (0)