Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ criterion/
*.tmp
*.log

# Node dependencies
node_modules/

# Environment files
.env
.env.local
Expand Down Expand Up @@ -57,4 +60,4 @@ flamegraph.svg
Thumbs.db

# Docker build artifacts
/dist/
/dist/
3 changes: 3 additions & 0 deletions clients/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
*.tgz
106 changes: 106 additions & 0 deletions clients/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# @evstack/evnode-viem

Viem client extension for EvNode transactions (type 0x76).

## Installation

```bash
npm install @evstack/evnode-viem viem
```

## Usage

### Basic Transaction

```typescript
import { createClient, http } from 'viem';
import { privateKeyToAccount, sign } from 'viem/accounts';
import { createEvnodeClient } from '@evstack/evnode-viem';

const client = createClient({
transport: http('http://localhost:8545'),
});

const account = privateKeyToAccount('0x...');

const evnode = createEvnodeClient({
client,
executor: {
address: account.address,
signHash: async (hash) => sign({ hash, privateKey: '0x...' }),
},
});

// Send a transaction
const txHash = await evnode.send({
calls: [
{ to: '0x...', value: 0n, data: '0x' },
],
});
```

### Batch Transactions

EvNode transactions support multiple calls in a single transaction:

```typescript
const txHash = await evnode.send({
calls: [
{ to: recipient1, value: 1000000000000000n, data: '0x' },
{ to: recipient2, value: 1000000000000000n, data: '0x' },
],
});
```

### Sponsored Transactions

A sponsor can pay gas fees on behalf of the executor:

```typescript
const evnode = createEvnodeClient({
client,
executor: { address: executorAddr, signHash: executorSignFn },
sponsor: { address: sponsorAddr, signHash: sponsorSignFn },
});

// Create intent (signed by executor)
const intent = await evnode.createIntent({
calls: [{ to: '0x...', value: 0n, data: '0x' }],
});

// Sponsor signs and sends
const txHash = await evnode.sponsorAndSend({ intent });
```

## API

### `createEvnodeClient(options)`

Creates a new EvNode client.

**Options:**
- `client` - Viem Client instance
- `executor` - (optional) Default executor signer
- `sponsor` - (optional) Default sponsor signer

### Client Methods

- `send(args)` - Sign and send an EvNode transaction
- `createIntent(args)` - Create a sponsorable intent
- `sponsorIntent(args)` - Add sponsor signature to an intent
- `sponsorAndSend(args)` - Sponsor and send in one call
- `serialize(signedTx)` - Serialize a signed transaction
- `deserialize(hex)` - Deserialize a signed transaction

### Utility Functions

- `computeExecutorSigningHash(tx)` - Get hash for executor to sign
- `computeSponsorSigningHash(tx, executorAddress)` - Get hash for sponsor to sign
- `computeTxHash(signedTx)` - Get transaction hash
- `recoverExecutor(signedTx)` - Recover executor address from signature
- `recoverSponsor(tx, executorAddress)` - Recover sponsor address from signature
- `estimateIntrinsicGas(calls)` - Estimate minimum gas for calls

## License

MIT
47 changes: 47 additions & 0 deletions clients/examples/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Basic example: Send a simple EvNode transaction
*
* Run with:
* PRIVATE_KEY=0x... npx tsx examples/basic.ts
*/
import { createClient, http } from 'viem';
import { privateKeyToAccount, sign } from 'viem/accounts';
import { createEvnodeClient } from '../src/index.ts';

const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8545';
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;

if (!PRIVATE_KEY) {
console.error('Usage: PRIVATE_KEY=0x... npx tsx examples/basic.ts');
process.exit(1);
}

async function main() {
const client = createClient({ transport: http(RPC_URL) });
const account = privateKeyToAccount(PRIVATE_KEY);

const evnode = createEvnodeClient({
client,
executor: {
address: account.address,
signHash: async (hash) => sign({ hash, privateKey: PRIVATE_KEY }),
},
});

console.log('Executor:', account.address);

// Send a simple transaction (self-transfer with no value)
const txHash = await evnode.send({
calls: [
{
to: account.address,
value: 0n,
data: '0x',
},
],
});

console.log('Transaction hash:', txHash);
}

main().catch(console.error);
52 changes: 52 additions & 0 deletions clients/examples/batch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Batch example: Send multiple calls in a single transaction
*
* Run with:
* PRIVATE_KEY=0x... npx tsx examples/batch.ts
*/
import { createClient, http, formatEther } from 'viem';
import { privateKeyToAccount, sign } from 'viem/accounts';
import { createEvnodeClient } from '../src/index.ts';

const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8545';
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;

if (!PRIVATE_KEY) {
console.error('Usage: PRIVATE_KEY=0x... npx tsx examples/batch.ts');
process.exit(1);
}

async function main() {
const client = createClient({ transport: http(RPC_URL) });
const account = privateKeyToAccount(PRIVATE_KEY);

const evnode = createEvnodeClient({
client,
executor: {
address: account.address,
signHash: async (hash) => sign({ hash, privateKey: PRIVATE_KEY }),
},
});

console.log('Executor:', account.address);

// Example recipients (in practice, use real addresses)
const recipient1 = '0x1111111111111111111111111111111111111111' as const;
const recipient2 = '0x2222222222222222222222222222222222222222' as const;
const amount = 1000000000000000n; // 0.001 ETH

console.log(`\nSending ${formatEther(amount)} ETH to each recipient...`);

// Send batch transaction: multiple transfers in one tx
const txHash = await evnode.send({
calls: [
{ to: recipient1, value: amount, data: '0x' },
{ to: recipient2, value: amount, data: '0x' },
],
});

console.log('Transaction hash:', txHash);
console.log('\nBoth transfers executed atomically in a single transaction.');
}

main().catch(console.error);
63 changes: 63 additions & 0 deletions clients/examples/contract-call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Contract call example: Interact with a smart contract
*
* Run with:
* PRIVATE_KEY=0x... CONTRACT=0x... npx tsx examples/contract-call.ts
*/
import { createClient, http, encodeFunctionData, parseAbi } from 'viem';
import { privateKeyToAccount, sign } from 'viem/accounts';
import { createEvnodeClient } from '../src/index.ts';

const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8545';
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;
const CONTRACT = process.env.CONTRACT as `0x${string}`;

if (!PRIVATE_KEY) {
console.error('Usage: PRIVATE_KEY=0x... CONTRACT=0x... npx tsx examples/contract-call.ts');
process.exit(1);
}

async function main() {
const client = createClient({ transport: http(RPC_URL) });
const account = privateKeyToAccount(PRIVATE_KEY);

const evnode = createEvnodeClient({
client,
executor: {
address: account.address,
signHash: async (hash) => sign({ hash, privateKey: PRIVATE_KEY }),
},
});

console.log('Executor:', account.address);

// Example: ERC20 transfer
// In practice, replace with your contract's ABI and function
const abi = parseAbi([
'function transfer(address to, uint256 amount) returns (bool)',
]);

const data = encodeFunctionData({
abi,
functionName: 'transfer',
args: ['0x1111111111111111111111111111111111111111', 1000000n],
});

const contractAddress = CONTRACT ?? '0x0000000000000000000000000000000000000000';

console.log('\nCalling contract:', contractAddress);

const txHash = await evnode.send({
calls: [
{
to: contractAddress,
value: 0n,
data,
},
],
});

console.log('Transaction hash:', txHash);
}

main().catch(console.error);
54 changes: 54 additions & 0 deletions clients/examples/contract-deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Contract deploy example: Deploy a smart contract
*
* Run with:
* PRIVATE_KEY=0x... npx tsx examples/contract-deploy.ts
*/
import { createClient, http, type Hex } from 'viem';
import { privateKeyToAccount, sign } from 'viem/accounts';
import { createEvnodeClient } from '../src/index.ts';

const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8545';
const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;

if (!PRIVATE_KEY) {
console.error('Usage: PRIVATE_KEY=0x... npx tsx examples/contract-deploy.ts');
process.exit(1);
}

async function main() {
const client = createClient({ transport: http(RPC_URL) });
const account = privateKeyToAccount(PRIVATE_KEY);

const evnode = createEvnodeClient({
client,
executor: {
address: account.address,
signHash: async (hash) => sign({ hash, privateKey: PRIVATE_KEY }),
},
});

console.log('Executor:', account.address);

// Simple storage contract bytecode
// contract Storage { uint256 value; function set(uint256 v) { value = v; } function get() view returns (uint256) { return value; } }
const bytecode: Hex = '0x608060405234801561001057600080fd5b5060df8061001f6000396000f3fe6080604052348015600f57600080fd5b5060043610603c5760003560e01c80636d4ce63c1460415780638a42ebe914605b575b600080fd5b60005460405190815260200160405180910390f35b606b6066366004606d565b600055565b005b600060208284031215607e57600080fd5b503591905056fea264697066735822122041c7f6d2d7b0d1c0d6c0d8e7f4c5b3a2918d7e6f5c4b3a291807d6e5f4c3b2a164736f6c63430008110033';

console.log('\nDeploying contract...');

// Deploy with to=null (CREATE)
const txHash = await evnode.send({
calls: [
{
to: null,
value: 0n,
data: bytecode,
},
],
});

console.log('Transaction hash:', txHash);
console.log('\nContract deployed. Check receipt for contract address.');
}

main().catch(console.error);
Loading