hak-ledger-plugin 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jmgomezl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # hak-ledger-plugin
2
+
3
+ **The first Ledger hardware-wallet plugin for the [Hedera Agent Kit](https://github.com/hashgraph/hedera-agent-kit).**
4
+
5
+ Gives a Hedera agent a **human-in-the-loop signer**: a high-risk EVM transaction is **Clear-Signed on a physical Ledger** before it executes. The device is the final confirmation gate — nothing high-value leaves the agent without an explicit on-device approval.
6
+
7
+ Part of the [Aivy](https://aivylabs.xyz) ecosystem.
8
+
9
+ ## Why
10
+
11
+ Autonomous agents are great until they move real value. This plugin adds **device-backed security** to a HAK agent: keep small actions autonomous, and gate high-risk ones behind a hardware Clear-Sign. It pairs directly with [`hak-uniswap-plugin`](https://github.com/jmgomezl/hak-uniswap-plugin), which returns `{ status: 'requires_ledger_approval', unsignedTx }` above a threshold — hand that `unsignedTx` straight to `ledger_clear_sign`.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm i hak-ledger-plugin
17
+ # USB transport (Node) is an optional dep; install if you sign over USB:
18
+ npm i @ledgerhq/hw-transport-node-hid
19
+ ```
20
+
21
+ ## Use
22
+
23
+ ```js
24
+ import { HederaLangchainToolkit } from 'hedera-agent-kit';
25
+ import { ledgerPlugin } from 'hak-ledger-plugin';
26
+
27
+ const toolkit = new HederaLangchainToolkit({
28
+ client,
29
+ configuration: { plugins: [ledgerPlugin] },
30
+ });
31
+ ```
32
+
33
+ ## Tools
34
+
35
+ ### `ledger_clear_sign` — human-in-the-loop signer
36
+
37
+ Clear-Signs an unsigned EVM transaction on the device and (optionally) broadcasts it.
38
+
39
+ | param | type | notes |
40
+ |---|---|---|
41
+ | `unsignedTx` | string \| object | RLP-serialized unsigned tx (hex), or `{ to, value, data, … }` |
42
+ | `chainId` | number | e.g. `296` Hedera testnet, `11155111` Sepolia |
43
+ | `rpcUrl` | string? | RPC for nonce/gas fill + broadcast (defaults by `chainId`) |
44
+ | `derivationPath` | string | default `44'/60'/0'/0/0` |
45
+ | `broadcast` | boolean | default `true`; `false` returns the signed raw tx |
46
+
47
+ Returns `{ status: 'executed', txHash }`, or `{ status: 'signed', rawSigned }`, or `{ status: 'rejected' }` if the human declines on the device.
48
+
49
+ ### `ledger_get_address` — hardware-backed identity
50
+
51
+ | param | type | notes |
52
+ |---|---|---|
53
+ | `derivationPath` | string | default `44'/60'/0'/0/0` |
54
+ | `verify` | boolean | `true` shows the address on the device to confirm |
55
+
56
+ ## Pairing with `hak-uniswap-plugin` (HITL settlement)
57
+
58
+ ```js
59
+ const quote = await tools.uniswap_swap({ /* … large swap … */ });
60
+ if (quote.status === 'requires_ledger_approval') {
61
+ // the agent proposes; the human Clear-Signs on the Ledger; then it executes
62
+ const res = await tools.ledger_clear_sign({ unsignedTx: quote.unsignedTx, chainId: quote.chainId });
63
+ // res.status === 'executed' (or 'rejected' if the human declined)
64
+ }
65
+ ```
66
+
67
+ A runnable example is in [`examples/sample-agent.js`](examples/sample-agent.js).
68
+
69
+ ## Transport
70
+
71
+ USB over `@ledgerhq/hw-transport-node-hid` by default (Node + a plugged-in Ledger).
72
+ Browser **WebHID** or **Speculos** consumers can inject their own transport via
73
+ `context.ledgerTransport` — the plugin uses it instead of opening USB.
74
+
75
+ ## Security
76
+
77
+ The Ledger device is the **final confirmation gate**. Keep on-device confirmation
78
+ intact: do not auto-approve or bypass it. Clear Signing shows human-readable
79
+ details (recipient, amount, contract action) so the approver sees what they sign.
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,27 @@
1
+ // Minimal sample: a HAK-style agent that derives its Ledger-secured address and
2
+ // Clear-Signs a high-risk EVM transaction on the device before broadcasting.
3
+ //
4
+ // npm i hak-ledger-plugin @ledgerhq/hw-transport-node-hid
5
+ // node examples/sample-agent.js
6
+ //
7
+ // Plug in your Ledger, unlock it, and open the Ethereum app first.
8
+ import { ledgerPlugin } from 'hak-ledger-plugin';
9
+
10
+ const [getAddress, clearSign] = ledgerPlugin.tools(/* context */ {});
11
+
12
+ // 1. Hardware-backed identity — verify the address on the device screen.
13
+ const id = await getAddress.execute(null, {}, { verify: true });
14
+ console.log('Ledger-secured agent address:', id.address);
15
+
16
+ // 2. A high-risk action the agent wants to take (e.g. a settlement payout).
17
+ // In practice this `unsignedTx` is what hak-uniswap-plugin returns as
18
+ // `requires_ledger_approval`. Here we hand-build a small transfer.
19
+ const result = await clearSign.execute(null, {}, {
20
+ unsignedTx: { to: id.address, value: '100000000000000', data: '0x' }, // 0.0001 (self, demo)
21
+ chainId: 296, // Hedera EVM testnet
22
+ broadcast: true,
23
+ });
24
+
25
+ if (result.status === 'executed') console.log('Ledger-approved & broadcast:', result.txHash);
26
+ else if (result.status === 'rejected') console.log('Declined on device — nothing executed.');
27
+ else console.log(result);
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "hak-ledger-plugin",
3
+ "version": "0.1.0",
4
+ "description": "The first Ledger hardware-wallet plugin for the Hedera Agent Kit — a human-in-the-loop signer that Clear-Signs high-risk EVM transactions on a physical Ledger before a Hedera agent executes them. The device is the final gate.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "examples",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "keywords": [
17
+ "hedera",
18
+ "hedera-agent-kit",
19
+ "hak-plugin",
20
+ "ledger",
21
+ "hardware-wallet",
22
+ "clear-signing",
23
+ "human-in-the-loop",
24
+ "hitl",
25
+ "ai-agent",
26
+ "evm"
27
+ ],
28
+ "peerDependencies": {
29
+ "hedera-agent-kit": ">=3.0.0",
30
+ "zod": "^3.25.76"
31
+ },
32
+ "dependencies": {
33
+ "@ledgerhq/hw-app-eth": "^6.47.1",
34
+ "ethers": "^6.16.0"
35
+ },
36
+ "optionalDependencies": {
37
+ "@ledgerhq/hw-transport-node-hid": "^6.29.0"
38
+ },
39
+ "license": "MIT",
40
+ "author": "jmgomezl",
41
+ "repository": "https://github.com/jmgomezl/hak-ledger-plugin",
42
+ "homepage": "https://github.com/jmgomezl/hak-ledger-plugin#readme"
43
+ }
package/src/index.js ADDED
@@ -0,0 +1,166 @@
1
+ // hak-ledger-plugin — the first Ledger hardware-wallet plugin for the Hedera
2
+ // Agent Kit. Gives a HAK agent a human-in-the-loop signer: a high-risk EVM
3
+ // action is Clear-Signed on a physical Ledger before it executes, so the device
4
+ // is the final confirmation gate. Pairs with plugins (e.g. hak-uniswap-plugin)
5
+ // that return `{ status: 'requires_ledger_approval', unsignedTx }` above a
6
+ // threshold — feed that unsignedTx straight into `ledger_clear_sign`.
7
+ //
8
+ // Plugin shape matches hedera-agent-kit@3.x:
9
+ // Plugin { name, version, description, tools: (context) => Tool[] }
10
+ // Tool { method, name, description, parameters: ZodObject, execute(client, context, params) }
11
+ //
12
+ // Transport: USB via @ledgerhq/hw-transport-node-hid (Node). Browser/WebHID and
13
+ // Speculos consumers can inject their own transport via context.ledgerTransport.
14
+ import { z } from 'zod';
15
+ import { ethers } from 'ethers';
16
+
17
+ const DEFAULT_PATH = "44'/60'/0'/0/0";
18
+
19
+ // Minimal EVM chain registry for optional broadcast — extend as needed.
20
+ const CHAINS = {
21
+ 1: { name: 'ethereum', rpc: 'https://eth.llamarpc.com' },
22
+ 11155111: { name: 'ethereum-sepolia', rpc: 'https://ethereum-sepolia-rpc.publicnode.com' },
23
+ 296: { name: 'hedera-testnet', rpc: 'https://testnet.hashio.io/api' },
24
+ 295: { name: 'hedera-mainnet', rpc: 'https://mainnet.hashio.io/api' },
25
+ 130: { name: 'unichain', rpc: 'https://mainnet.unichain.org' },
26
+ 8453: { name: 'base', rpc: 'https://mainnet.base.org' },
27
+ };
28
+
29
+ // Open a Ledger Ethereum-app connection. Prefer an injected transport (WebHID,
30
+ // Speculos, BLE); otherwise fall back to a USB (node-hid) transport.
31
+ async function openEth(context) {
32
+ const { default: Eth, ledgerService } = await import('@ledgerhq/hw-app-eth');
33
+ let transport = context?.ledgerTransport ?? null;
34
+ if (!transport) {
35
+ try {
36
+ const { default: TransportNodeHid } = await import('@ledgerhq/hw-transport-node-hid');
37
+ transport = await TransportNodeHid.create();
38
+ } catch (err) {
39
+ throw new Error(
40
+ 'No Ledger transport. Plug in a Ledger and `npm i @ledgerhq/hw-transport-node-hid`, ' +
41
+ 'or pass context.ledgerTransport (WebHID / Speculos). Cause: ' + err.message
42
+ );
43
+ }
44
+ }
45
+ return { eth: new Eth(transport), ledgerService, transport };
46
+ }
47
+
48
+ // Normalize an unsigned tx (object or RLP hex) → an ethers Transaction we can
49
+ // serialize, plus the device-ready unsigned hex (no 0x prefix).
50
+ async function toUnsigned(unsignedTx, { chainId, rpcUrl }) {
51
+ let tx;
52
+ if (typeof unsignedTx === 'string') {
53
+ tx = ethers.Transaction.from(unsignedTx); // already-serialized unsigned tx
54
+ } else {
55
+ tx = ethers.Transaction.from({ type: 0, ...unsignedTx });
56
+ }
57
+ // Fill nonce / gas / chainId from chain if missing (only when an RPC is known).
58
+ const rpc = rpcUrl || CHAINS[chainId]?.rpc;
59
+ if (rpc && (tx.nonce == null || tx.gasLimit == null || tx.gasPrice == null || tx.chainId == null)) {
60
+ const provider = new ethers.JsonRpcProvider(rpc);
61
+ const from = tx.from;
62
+ if (tx.chainId == null) tx.chainId = BigInt(chainId ?? (await provider.getNetwork()).chainId);
63
+ if (tx.nonce == null && from) tx.nonce = await provider.getTransactionCount(from);
64
+ if (tx.gasPrice == null) {
65
+ const fee = await provider.getFeeData();
66
+ tx.gasPrice = fee.gasPrice ? (fee.gasPrice * 12n) / 10n : ethers.parseUnits('600', 'gwei');
67
+ }
68
+ if (tx.gasLimit == null) tx.gasLimit = 21000n;
69
+ }
70
+ const unsignedHex = tx.unsignedSerialized.slice(2); // hw-app-eth wants no 0x
71
+ return { tx, unsignedHex };
72
+ }
73
+
74
+ // ── Tool 1: derive + (optionally) verify the Ledger Ethereum address ──
75
+ const getAddressTool = (context) => ({
76
+ method: 'ledger_get_address',
77
+ name: 'Ledger Get Address',
78
+ description:
79
+ "Derive the agent's Ledger-secured Ethereum address. Set verify=true to display " +
80
+ 'it on the device for the human to confirm. Use to bind an agent to a hardware identity.',
81
+ parameters: z.object({
82
+ derivationPath: z.string().default(DEFAULT_PATH).describe("BIP-32 path, e.g. 44'/60'/0'/0/0"),
83
+ verify: z.boolean().default(false).describe('show the address on the device for confirmation'),
84
+ }),
85
+ async execute(_client, _ctx, params) {
86
+ const { eth } = await openEth(context);
87
+ const { address, publicKey } = await eth.getAddress(params.derivationPath, params.verify);
88
+ return { address: ethers.getAddress(address), publicKey };
89
+ },
90
+ });
91
+
92
+ // ── Tool 2: Clear-Sign a high-risk EVM transaction on the Ledger ──
93
+ const clearSignTool = (context) => ({
94
+ method: 'ledger_clear_sign',
95
+ name: 'Ledger Clear-Sign Transaction',
96
+ description:
97
+ 'Human-in-the-loop signer: Clear-Sign an unsigned EVM transaction on a physical Ledger and ' +
98
+ "(optionally) broadcast it. The device is the final gate — nothing executes without the human's " +
99
+ 'on-device approval. Pass the `unsignedTx` returned by a plugin that requires Ledger approval.',
100
+ parameters: z.object({
101
+ unsignedTx: z.union([z.string(), z.record(z.any())]).describe('RLP-serialized unsigned tx (hex) or a tx object {to,value,data,...}'),
102
+ chainId: z.number().describe('EVM chain id (e.g. 296 Hedera testnet, 11155111 Sepolia)'),
103
+ rpcUrl: z.string().optional().describe('RPC for nonce/gas fill + broadcast (defaults by chainId)'),
104
+ derivationPath: z.string().default(DEFAULT_PATH),
105
+ broadcast: z.boolean().default(true).describe('broadcast after signing; false returns the signed raw tx'),
106
+ }),
107
+ async execute(_client, _ctx, params) {
108
+ const { unsignedTx, chainId, rpcUrl, derivationPath, broadcast } = params;
109
+ const { eth, ledgerService } = await openEth(context);
110
+ const { tx, unsignedHex } = await toUnsigned(unsignedTx, { chainId, rpcUrl });
111
+
112
+ // Resolve Clear-Signing metadata (ERC-20 / known contracts / external plugins)
113
+ // so the device shows human-readable details, not raw hex.
114
+ let resolution = null;
115
+ try {
116
+ resolution = await ledgerService.resolveTransaction(unsignedHex, eth.loadConfig ?? {}, {
117
+ externalPlugins: true,
118
+ erc20: true,
119
+ });
120
+ } catch {
121
+ /* fall back to blind signing if CAL metadata is unavailable */
122
+ }
123
+
124
+ let sig;
125
+ try {
126
+ sig = await eth.signTransaction(derivationPath, unsignedHex, resolution);
127
+ } catch (err) {
128
+ // user declined on the device, or app/locked
129
+ return { status: 'rejected', reason: err.message };
130
+ }
131
+
132
+ tx.signature = ethers.Signature.from({
133
+ r: '0x' + sig.r,
134
+ s: '0x' + sig.s,
135
+ v: parseInt(sig.v, 16),
136
+ });
137
+ const rawSigned = tx.serialized;
138
+
139
+ if (!broadcast) {
140
+ return { status: 'signed', from: tx.from, hash: tx.hash, rawSigned, chainId };
141
+ }
142
+ const rpc = rpcUrl || CHAINS[chainId]?.rpc;
143
+ if (!rpc) return { status: 'signed', rawSigned, chainId, note: 'no rpc to broadcast — returned signed tx' };
144
+ const provider = new ethers.JsonRpcProvider(rpc);
145
+ const sent = await provider.broadcastTransaction(rawSigned);
146
+ return {
147
+ status: 'executed',
148
+ txHash: sent.hash,
149
+ from: tx.from,
150
+ to: tx.to,
151
+ chainId,
152
+ explorer: CHAINS[chainId]?.name ? `${CHAINS[chainId].name}:${sent.hash}` : sent.hash,
153
+ };
154
+ },
155
+ });
156
+
157
+ export const ledgerPlugin = {
158
+ name: 'hak-ledger-plugin',
159
+ version: '0.1.0',
160
+ description:
161
+ 'Human-in-the-loop Ledger signer for Hedera Agent Kit agents: Clear-Sign high-risk EVM ' +
162
+ 'transactions on a physical Ledger before they execute. The device is the final gate.',
163
+ tools: (context) => [getAddressTool(context), clearSignTool(context)],
164
+ };
165
+
166
+ export default ledgerPlugin;