@hardkas/l2 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 +21 -0
- package/dist/index.d.ts +177 -0
- package/dist/index.js +470 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javier Rodriguez
|
|
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/dist/index.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { IgraTxPlanArtifact } from '@hardkas/artifacts';
|
|
2
|
+
|
|
3
|
+
type L2NetworkType = "evm-based-rollup";
|
|
4
|
+
type L2BridgePhase = "pre-zk" | "mpc" | "zk" | "unknown";
|
|
5
|
+
type L2RiskProfile = "high" | "medium" | "low" | "unknown";
|
|
6
|
+
interface L2SecurityAssumptions {
|
|
7
|
+
readonly bridgePhase: L2BridgePhase;
|
|
8
|
+
readonly trustlessExit: boolean;
|
|
9
|
+
readonly custodyModel: string;
|
|
10
|
+
readonly riskProfile: L2RiskProfile;
|
|
11
|
+
readonly notes: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
interface L2NetworkProfile {
|
|
14
|
+
readonly schema: "hardkas.l2Profile.v1";
|
|
15
|
+
readonly hardkasVersion: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly displayName: string;
|
|
18
|
+
readonly type: L2NetworkType;
|
|
19
|
+
readonly settlementLayer: "kaspa";
|
|
20
|
+
readonly executionLayer: "evm";
|
|
21
|
+
readonly gasToken: string;
|
|
22
|
+
readonly nativeTokenDecimals?: number;
|
|
23
|
+
readonly chainId?: number;
|
|
24
|
+
readonly rpcUrl?: string;
|
|
25
|
+
readonly explorerUrl?: string;
|
|
26
|
+
readonly security: L2SecurityAssumptions;
|
|
27
|
+
}
|
|
28
|
+
declare const BUILTIN_L2_PROFILES: readonly L2NetworkProfile[];
|
|
29
|
+
|
|
30
|
+
declare function getBuiltInL2Profiles(): readonly L2NetworkProfile[];
|
|
31
|
+
declare function listL2Profiles(): readonly L2NetworkProfile[];
|
|
32
|
+
declare function getL2Profile(name: string): L2NetworkProfile | null;
|
|
33
|
+
declare function validateL2Profile(profile: any): {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
errors: string[];
|
|
36
|
+
};
|
|
37
|
+
declare function assertValidL2Profile(profile: any): L2NetworkProfile;
|
|
38
|
+
|
|
39
|
+
interface EvmJsonRpcClientOptions {
|
|
40
|
+
readonly url: string;
|
|
41
|
+
readonly timeoutMs?: number;
|
|
42
|
+
readonly fetcher?: typeof fetch;
|
|
43
|
+
}
|
|
44
|
+
interface EvmCallRequest {
|
|
45
|
+
readonly from?: string;
|
|
46
|
+
readonly to?: string;
|
|
47
|
+
readonly gas?: string;
|
|
48
|
+
readonly gasPrice?: string;
|
|
49
|
+
readonly value?: string;
|
|
50
|
+
readonly data?: string;
|
|
51
|
+
}
|
|
52
|
+
declare class EvmJsonRpcClient {
|
|
53
|
+
private readonly url;
|
|
54
|
+
private readonly timeoutMs;
|
|
55
|
+
private readonly fetcher;
|
|
56
|
+
constructor(options: EvmJsonRpcClientOptions);
|
|
57
|
+
callRpc<T>(method: string, params?: unknown[]): Promise<T>;
|
|
58
|
+
getChainId(): Promise<number>;
|
|
59
|
+
getBlockNumber(): Promise<bigint>;
|
|
60
|
+
getGasPriceWei(): Promise<bigint>;
|
|
61
|
+
getBalanceWei(address: string, blockTag?: "latest" | "pending"): Promise<bigint>;
|
|
62
|
+
getTransactionCount(address: string, blockTag?: "latest" | "pending"): Promise<bigint>;
|
|
63
|
+
call(request: EvmCallRequest, blockTag?: "latest" | "pending"): Promise<string>;
|
|
64
|
+
estimateGas(request: EvmCallRequest, blockTag?: "latest" | "pending"): Promise<bigint>;
|
|
65
|
+
sendRawTransaction(rawTransaction: string): Promise<string>;
|
|
66
|
+
getTransactionReceipt(txHash: string): Promise<any | null>;
|
|
67
|
+
private validateTxHash;
|
|
68
|
+
private validateCallRequest;
|
|
69
|
+
private validateAddress;
|
|
70
|
+
private validateHexData;
|
|
71
|
+
private validateHexQuantity;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface EvmRpcHealthResult {
|
|
75
|
+
readonly url: string;
|
|
76
|
+
readonly ready: boolean;
|
|
77
|
+
readonly checkedAt: string;
|
|
78
|
+
readonly latencyMs?: number;
|
|
79
|
+
readonly chainId?: number;
|
|
80
|
+
readonly blockNumber?: bigint;
|
|
81
|
+
readonly gasPriceWei?: bigint;
|
|
82
|
+
readonly error?: string;
|
|
83
|
+
}
|
|
84
|
+
declare function checkEvmRpcHealth(options: EvmJsonRpcClientOptions): Promise<EvmRpcHealthResult>;
|
|
85
|
+
declare function waitForEvmRpcReady(options: EvmJsonRpcClientOptions & {
|
|
86
|
+
readonly intervalMs?: number;
|
|
87
|
+
readonly maxWaitMs?: number;
|
|
88
|
+
}): Promise<EvmRpcHealthResult>;
|
|
89
|
+
|
|
90
|
+
declare function formatWeiAsEtherLike(valueWei: bigint, symbol: string, decimals?: number): string;
|
|
91
|
+
declare function toHexQuantity(value: bigint | string | number): string;
|
|
92
|
+
|
|
93
|
+
interface IgraTxSigningInput {
|
|
94
|
+
readonly plan: IgraTxPlanArtifact;
|
|
95
|
+
readonly account?: {
|
|
96
|
+
readonly name?: string;
|
|
97
|
+
readonly address: string;
|
|
98
|
+
readonly privateKey?: string;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
interface IgraTxSigningResult {
|
|
102
|
+
readonly rawTransaction: string;
|
|
103
|
+
readonly txHash?: string;
|
|
104
|
+
}
|
|
105
|
+
interface IgraTxSigner {
|
|
106
|
+
sign(input: IgraTxSigningInput): Promise<IgraTxSigningResult>;
|
|
107
|
+
}
|
|
108
|
+
declare class UnsupportedIgraTxSigner implements IgraTxSigner {
|
|
109
|
+
sign(): Promise<IgraTxSigningResult>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface ViemIgraTxSignerOptions {
|
|
113
|
+
/**
|
|
114
|
+
* Optional custom loader for viem main package.
|
|
115
|
+
* Useful for testing without real viem dependency.
|
|
116
|
+
*/
|
|
117
|
+
readonly viemLoader?: () => Promise<any>;
|
|
118
|
+
/**
|
|
119
|
+
* Optional custom loader for viem/accounts package.
|
|
120
|
+
*/
|
|
121
|
+
readonly accountsLoader?: () => Promise<any>;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Real EVM-compatible signer adapter for Igra L2 transactions using viem.
|
|
125
|
+
*/
|
|
126
|
+
declare class ViemIgraTxSigner implements IgraTxSigner {
|
|
127
|
+
private readonly viemLoader;
|
|
128
|
+
private readonly accountsLoader;
|
|
129
|
+
constructor(options?: ViemIgraTxSignerOptions);
|
|
130
|
+
getAddress(): Promise<string>;
|
|
131
|
+
sign(input: IgraTxSigningInput): Promise<IgraTxSigningResult>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface EvmTransactionReceiptSummary {
|
|
135
|
+
txHash: string;
|
|
136
|
+
blockHash?: string;
|
|
137
|
+
blockNumber?: bigint | undefined;
|
|
138
|
+
from?: string;
|
|
139
|
+
to?: string | null | undefined;
|
|
140
|
+
gasUsed?: bigint | undefined;
|
|
141
|
+
effectiveGasPrice?: bigint | undefined;
|
|
142
|
+
status?: "success" | "reverted" | "unknown";
|
|
143
|
+
raw: any;
|
|
144
|
+
}
|
|
145
|
+
declare function normalizeEvmTransactionReceipt(raw: any): EvmTransactionReceiptSummary | null;
|
|
146
|
+
|
|
147
|
+
interface L2BridgeAssumptions {
|
|
148
|
+
readonly schema: "hardkas.l2BridgeAssumptions.v1";
|
|
149
|
+
readonly hardkasVersion: string;
|
|
150
|
+
readonly l2Network: string;
|
|
151
|
+
readonly bridgePhase: L2BridgePhase;
|
|
152
|
+
readonly trustlessExit: boolean;
|
|
153
|
+
readonly custodyModel: string;
|
|
154
|
+
readonly validatorModel?: string;
|
|
155
|
+
readonly exitModel: string;
|
|
156
|
+
readonly riskProfile: L2RiskProfile;
|
|
157
|
+
readonly notes: readonly string[];
|
|
158
|
+
readonly updatedAt: string;
|
|
159
|
+
}
|
|
160
|
+
declare function getL2BridgeAssumptions(network: string): L2BridgeAssumptions | null;
|
|
161
|
+
declare function listL2BridgeAssumptions(): readonly L2BridgeAssumptions[];
|
|
162
|
+
declare function validateL2BridgeAssumptions(input: unknown): {
|
|
163
|
+
ok: boolean;
|
|
164
|
+
errors: string[];
|
|
165
|
+
};
|
|
166
|
+
declare function assertValidL2BridgeAssumptions(input: unknown): L2BridgeAssumptions;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Encodes deployment bytecode and constructor arguments using viem.
|
|
170
|
+
*
|
|
171
|
+
* @param bytecode 0x-prefixed hex string
|
|
172
|
+
* @param constructorSignature e.g. "constructor(address,uint256)"
|
|
173
|
+
* @param args Array of argument strings from CLI
|
|
174
|
+
*/
|
|
175
|
+
declare function encodeConstructorArgs(bytecode: string, constructorSignature: string, args: string[]): string;
|
|
176
|
+
|
|
177
|
+
export { BUILTIN_L2_PROFILES, type EvmCallRequest, EvmJsonRpcClient, type EvmJsonRpcClientOptions, type EvmRpcHealthResult, type EvmTransactionReceiptSummary, type IgraTxSigner, type IgraTxSigningInput, type IgraTxSigningResult, type L2BridgeAssumptions, type L2BridgePhase, type L2NetworkProfile, type L2NetworkType, type L2RiskProfile, type L2SecurityAssumptions, UnsupportedIgraTxSigner, ViemIgraTxSigner, type ViemIgraTxSignerOptions, assertValidL2BridgeAssumptions, assertValidL2Profile, checkEvmRpcHealth, encodeConstructorArgs, formatWeiAsEtherLike, getBuiltInL2Profiles, getL2BridgeAssumptions, getL2Profile, listL2BridgeAssumptions, listL2Profiles, normalizeEvmTransactionReceipt, toHexQuantity, validateL2BridgeAssumptions, validateL2Profile, waitForEvmRpcReady };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
// src/profiles.ts
|
|
2
|
+
import { HARDKAS_VERSION } from "@hardkas/artifacts";
|
|
3
|
+
var BUILTIN_L2_PROFILES = [
|
|
4
|
+
{
|
|
5
|
+
schema: "hardkas.l2Profile.v1",
|
|
6
|
+
hardkasVersion: HARDKAS_VERSION,
|
|
7
|
+
name: "igra",
|
|
8
|
+
displayName: "Igra",
|
|
9
|
+
type: "evm-based-rollup",
|
|
10
|
+
settlementLayer: "kaspa",
|
|
11
|
+
executionLayer: "evm",
|
|
12
|
+
gasToken: "iKAS",
|
|
13
|
+
nativeTokenDecimals: 18,
|
|
14
|
+
security: {
|
|
15
|
+
bridgePhase: "pre-zk",
|
|
16
|
+
trustlessExit: false,
|
|
17
|
+
custodyModel: "Phase-dependent bridge custody; verify live Igra bridge phase before use.",
|
|
18
|
+
riskProfile: "high",
|
|
19
|
+
notes: [
|
|
20
|
+
"Kaspa L1 does not execute EVM.",
|
|
21
|
+
"Igra execution occurs on L2.",
|
|
22
|
+
"Kaspa L1 provides sequencing, data availability, state commitment anchoring and finality.",
|
|
23
|
+
"Bridge security is phase-dependent: pre-ZK -> MPC -> ZK.",
|
|
24
|
+
"Trustless exit exists only in the ZK phase."
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// src/registry.ts
|
|
31
|
+
function getBuiltInL2Profiles() {
|
|
32
|
+
return BUILTIN_L2_PROFILES;
|
|
33
|
+
}
|
|
34
|
+
function listL2Profiles() {
|
|
35
|
+
return BUILTIN_L2_PROFILES;
|
|
36
|
+
}
|
|
37
|
+
function getL2Profile(name) {
|
|
38
|
+
return listL2Profiles().find((p) => p.name === name) || null;
|
|
39
|
+
}
|
|
40
|
+
function validateL2Profile(profile) {
|
|
41
|
+
const errors = [];
|
|
42
|
+
if (!profile || typeof profile !== "object") {
|
|
43
|
+
return { ok: false, errors: ["Profile must be an object"] };
|
|
44
|
+
}
|
|
45
|
+
if (profile.schema !== "hardkas.l2Profile.v1") {
|
|
46
|
+
errors.push(`Invalid schema: expected 'hardkas.l2Profile.v1', got '${profile.schema}'`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof profile.hardkasVersion !== "string") {
|
|
49
|
+
errors.push("Missing or invalid hardkasVersion");
|
|
50
|
+
}
|
|
51
|
+
if (!profile.name || typeof profile.name !== "string") {
|
|
52
|
+
errors.push("Missing or invalid name");
|
|
53
|
+
}
|
|
54
|
+
if (profile.type !== "evm-based-rollup") {
|
|
55
|
+
errors.push(`Invalid type: expected 'evm-based-rollup', got '${profile.type}'`);
|
|
56
|
+
}
|
|
57
|
+
if (profile.settlementLayer !== "kaspa") {
|
|
58
|
+
errors.push(`Invalid settlementLayer: expected 'kaspa', got '${profile.settlementLayer}'`);
|
|
59
|
+
}
|
|
60
|
+
if (profile.executionLayer !== "evm") {
|
|
61
|
+
errors.push(`Invalid executionLayer: expected 'evm', got '${profile.executionLayer}'`);
|
|
62
|
+
}
|
|
63
|
+
if (!profile.gasToken || typeof profile.gasToken !== "string") {
|
|
64
|
+
errors.push("Missing or invalid gasToken");
|
|
65
|
+
}
|
|
66
|
+
if (profile.security) {
|
|
67
|
+
if (profile.security.bridgePhase === "zk") {
|
|
68
|
+
} else {
|
|
69
|
+
if (profile.security.trustlessExit === true) {
|
|
70
|
+
errors.push("Security invariant violation: trustlessExit must be false when bridgePhase is not 'zk'");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(profile.security.notes) || profile.security.notes.length === 0) {
|
|
74
|
+
errors.push("Security notes must not be empty");
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
errors.push("Missing security assumptions");
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
ok: errors.length === 0,
|
|
81
|
+
errors
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function assertValidL2Profile(profile) {
|
|
85
|
+
const { ok, errors } = validateL2Profile(profile);
|
|
86
|
+
if (!ok) {
|
|
87
|
+
throw new Error(`Invalid L2 profile:
|
|
88
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
89
|
+
}
|
|
90
|
+
return profile;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/evm-rpc-client.ts
|
|
94
|
+
var EvmJsonRpcClient = class {
|
|
95
|
+
url;
|
|
96
|
+
timeoutMs;
|
|
97
|
+
fetcher;
|
|
98
|
+
constructor(options) {
|
|
99
|
+
this.url = options.url;
|
|
100
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
101
|
+
this.fetcher = options.fetcher ?? globalThis.fetch;
|
|
102
|
+
if (!this.fetcher) {
|
|
103
|
+
throw new Error("No fetch implementation found. Ensure global fetch is available or provide a fetcher.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async callRpc(method, params = []) {
|
|
107
|
+
const id = Math.floor(Math.random() * 1e6);
|
|
108
|
+
const body = JSON.stringify({
|
|
109
|
+
jsonrpc: "2.0",
|
|
110
|
+
id,
|
|
111
|
+
method,
|
|
112
|
+
params
|
|
113
|
+
});
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
116
|
+
try {
|
|
117
|
+
const response = await this.fetcher(this.url, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "Content-Type": "application/json" },
|
|
120
|
+
body,
|
|
121
|
+
signal: controller.signal
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
|
125
|
+
}
|
|
126
|
+
const json = await response.json();
|
|
127
|
+
if (json.error) {
|
|
128
|
+
throw new Error(`JSON-RPC error: ${json.error.message} (code: ${json.error.code})`);
|
|
129
|
+
}
|
|
130
|
+
return json.result;
|
|
131
|
+
} finally {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async getChainId() {
|
|
136
|
+
const hex = await this.callRpc("eth_chainId");
|
|
137
|
+
return parseInt(hex, 16);
|
|
138
|
+
}
|
|
139
|
+
async getBlockNumber() {
|
|
140
|
+
const hex = await this.callRpc("eth_blockNumber");
|
|
141
|
+
return BigInt(hex);
|
|
142
|
+
}
|
|
143
|
+
async getGasPriceWei() {
|
|
144
|
+
const hex = await this.callRpc("eth_gasPrice");
|
|
145
|
+
return BigInt(hex);
|
|
146
|
+
}
|
|
147
|
+
async getBalanceWei(address, blockTag = "latest") {
|
|
148
|
+
this.validateAddress(address);
|
|
149
|
+
const hex = await this.callRpc("eth_getBalance", [address, blockTag]);
|
|
150
|
+
return BigInt(hex);
|
|
151
|
+
}
|
|
152
|
+
async getTransactionCount(address, blockTag = "latest") {
|
|
153
|
+
this.validateAddress(address);
|
|
154
|
+
const hex = await this.callRpc("eth_getTransactionCount", [address, blockTag]);
|
|
155
|
+
return BigInt(hex);
|
|
156
|
+
}
|
|
157
|
+
async call(request, blockTag = "latest") {
|
|
158
|
+
this.validateCallRequest(request);
|
|
159
|
+
return await this.callRpc("eth_call", [request, blockTag]);
|
|
160
|
+
}
|
|
161
|
+
async estimateGas(request, blockTag = "latest") {
|
|
162
|
+
this.validateCallRequest(request);
|
|
163
|
+
const hex = await this.callRpc("eth_estimateGas", [request, blockTag]);
|
|
164
|
+
return BigInt(hex);
|
|
165
|
+
}
|
|
166
|
+
async sendRawTransaction(rawTransaction) {
|
|
167
|
+
this.validateHexData(rawTransaction, "rawTransaction");
|
|
168
|
+
return await this.callRpc("eth_sendRawTransaction", [rawTransaction]);
|
|
169
|
+
}
|
|
170
|
+
async getTransactionReceipt(txHash) {
|
|
171
|
+
this.validateTxHash(txHash, "txHash");
|
|
172
|
+
return await this.callRpc("eth_getTransactionReceipt", [txHash]);
|
|
173
|
+
}
|
|
174
|
+
validateTxHash(txHash, fieldName) {
|
|
175
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(txHash)) {
|
|
176
|
+
throw new Error(`Invalid EVM ${fieldName}: must be a 0x-prefixed 64-character hex string.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
validateCallRequest(request) {
|
|
180
|
+
if (request.to) this.validateAddress(request.to, "to");
|
|
181
|
+
if (request.from) this.validateAddress(request.from, "from");
|
|
182
|
+
if (request.data) this.validateHexData(request.data, "data");
|
|
183
|
+
if (request.value) this.validateHexQuantity(request.value, "value");
|
|
184
|
+
if (request.gas) this.validateHexQuantity(request.gas, "gas");
|
|
185
|
+
if (request.gasPrice) this.validateHexQuantity(request.gasPrice, "gasPrice");
|
|
186
|
+
}
|
|
187
|
+
validateAddress(address, fieldName = "address") {
|
|
188
|
+
if (!address || typeof address !== "string") {
|
|
189
|
+
throw new Error(`Invalid ${fieldName}: must be a non-empty string.`);
|
|
190
|
+
}
|
|
191
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
|
|
192
|
+
throw new Error(`Invalid EVM ${fieldName}: must be a 0x-prefixed 40-character hex string.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
validateHexData(data, fieldName) {
|
|
196
|
+
if (!/^0x([a-fA-F0-9]{2})*$/.test(data)) {
|
|
197
|
+
throw new Error(`Invalid hex ${fieldName}: must be a 0x-prefixed even-length hex string.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
validateHexQuantity(qty, fieldName) {
|
|
201
|
+
if (!/^0x[0-9a-fA-F]+$/.test(qty)) {
|
|
202
|
+
throw new Error(`Invalid hex ${fieldName}: must be a 0x-prefixed hex quantity.`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/evm-rpc-health.ts
|
|
208
|
+
async function checkEvmRpcHealth(options) {
|
|
209
|
+
const start = Date.now();
|
|
210
|
+
const client = new EvmJsonRpcClient(options);
|
|
211
|
+
try {
|
|
212
|
+
const [chainId, blockNumber, gasPriceWei] = await Promise.all([
|
|
213
|
+
client.getChainId(),
|
|
214
|
+
client.getBlockNumber(),
|
|
215
|
+
client.getGasPriceWei()
|
|
216
|
+
]);
|
|
217
|
+
const latencyMs = Date.now() - start;
|
|
218
|
+
return {
|
|
219
|
+
url: options.url,
|
|
220
|
+
ready: true,
|
|
221
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
222
|
+
latencyMs,
|
|
223
|
+
chainId,
|
|
224
|
+
blockNumber,
|
|
225
|
+
gasPriceWei
|
|
226
|
+
};
|
|
227
|
+
} catch (e) {
|
|
228
|
+
return {
|
|
229
|
+
url: options.url,
|
|
230
|
+
ready: false,
|
|
231
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
232
|
+
error: e instanceof Error ? e.message : String(e)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function waitForEvmRpcReady(options) {
|
|
237
|
+
const interval = options.intervalMs ?? 1e3;
|
|
238
|
+
const maxWait = options.maxWaitMs ?? 6e4;
|
|
239
|
+
const start = Date.now();
|
|
240
|
+
while (true) {
|
|
241
|
+
const health = await checkEvmRpcHealth(options);
|
|
242
|
+
if (health.ready) return health;
|
|
243
|
+
if (Date.now() - start > maxWait) {
|
|
244
|
+
return health;
|
|
245
|
+
}
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/formatters.ts
|
|
251
|
+
function formatWeiAsEtherLike(valueWei, symbol, decimals = 18) {
|
|
252
|
+
const s = valueWei.toString().padStart(decimals + 1, "0");
|
|
253
|
+
const pos = s.length - decimals;
|
|
254
|
+
const intPart = s.substring(0, pos);
|
|
255
|
+
const fracPart = s.substring(pos);
|
|
256
|
+
return `${intPart}.${fracPart} ${symbol}`;
|
|
257
|
+
}
|
|
258
|
+
function toHexQuantity(value) {
|
|
259
|
+
const big = BigInt(value);
|
|
260
|
+
return "0x" + big.toString(16);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/igra-signer.ts
|
|
264
|
+
var UnsupportedIgraTxSigner = class {
|
|
265
|
+
async sign() {
|
|
266
|
+
throw new Error(
|
|
267
|
+
"Igra L2 transaction signing is not configured yet. Configure an EVM-compatible signer adapter in a future phase."
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// src/viem-igra-signer.ts
|
|
273
|
+
var ViemIgraTxSigner = class {
|
|
274
|
+
viemLoader;
|
|
275
|
+
accountsLoader;
|
|
276
|
+
constructor(options) {
|
|
277
|
+
this.viemLoader = options?.viemLoader || (() => import("viem"));
|
|
278
|
+
this.accountsLoader = options?.accountsLoader || (() => import("viem/accounts"));
|
|
279
|
+
}
|
|
280
|
+
async getAddress() {
|
|
281
|
+
throw new Error("ViemIgraTxSigner requires explicit account input for signing.");
|
|
282
|
+
}
|
|
283
|
+
async sign(input) {
|
|
284
|
+
const { plan, account } = input;
|
|
285
|
+
if (plan.schema !== "hardkas.igraTxPlan.v1") {
|
|
286
|
+
throw new Error(`Invalid plan schema: ${plan.schema}. Expected hardkas.igraTxPlan.v1`);
|
|
287
|
+
}
|
|
288
|
+
if (plan.status !== "built") {
|
|
289
|
+
throw new Error(`Plan must be in 'built' status to be signed. Current: ${plan.status}`);
|
|
290
|
+
}
|
|
291
|
+
if (!account) {
|
|
292
|
+
throw new Error("Signer requires an account input.");
|
|
293
|
+
}
|
|
294
|
+
if (!account.privateKey) {
|
|
295
|
+
throw new Error(`Account '${account.name}' has no private key available for signing.`);
|
|
296
|
+
}
|
|
297
|
+
if (!account.address || !account.address.startsWith("0x")) {
|
|
298
|
+
throw new Error("Igra L2 signing requires an EVM 0x account address.");
|
|
299
|
+
}
|
|
300
|
+
if (plan.request.from && plan.request.from.toLowerCase() !== account.address.toLowerCase()) {
|
|
301
|
+
throw new Error(`Account address '${account.address}' does not match plan 'from' address '${plan.request.from}'.`);
|
|
302
|
+
}
|
|
303
|
+
if (plan.request.gasLimit === void 0 || plan.request.gasLimit === null) {
|
|
304
|
+
throw new Error("Igra transaction plan is incomplete. Rebuild the plan with gas limit.");
|
|
305
|
+
}
|
|
306
|
+
if (plan.request.gasPriceWei === void 0 || plan.request.gasPriceWei === null) {
|
|
307
|
+
throw new Error("Igra transaction plan is incomplete. Rebuild the plan with gas price.");
|
|
308
|
+
}
|
|
309
|
+
if (plan.request.nonce === void 0 || plan.request.nonce === null) {
|
|
310
|
+
throw new Error("Igra transaction plan is incomplete. Rebuild the plan with nonce.");
|
|
311
|
+
}
|
|
312
|
+
let normalizedPk = account.privateKey;
|
|
313
|
+
if (!normalizedPk.startsWith("0x")) {
|
|
314
|
+
normalizedPk = `0x${normalizedPk}`;
|
|
315
|
+
}
|
|
316
|
+
const pkRegex = /^0x[a-fA-F0-9]{64}$/;
|
|
317
|
+
if (!pkRegex.test(normalizedPk)) {
|
|
318
|
+
throw new Error(`Invalid EVM private key format for account '${account.name}'.`);
|
|
319
|
+
}
|
|
320
|
+
let viem;
|
|
321
|
+
let accounts;
|
|
322
|
+
try {
|
|
323
|
+
viem = await this.viemLoader();
|
|
324
|
+
accounts = await this.accountsLoader();
|
|
325
|
+
} catch (e) {
|
|
326
|
+
throw new Error("EVM signing dependency (viem) is not installed. Install viem to enable Igra L2 signing.");
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const viemAccount = accounts.privateKeyToAccount(normalizedPk);
|
|
330
|
+
const signed = await viemAccount.signTransaction({
|
|
331
|
+
chainId: plan.chainId,
|
|
332
|
+
to: plan.request.to,
|
|
333
|
+
data: plan.request.data,
|
|
334
|
+
value: BigInt(plan.request.valueWei),
|
|
335
|
+
gas: BigInt(plan.request.gasLimit),
|
|
336
|
+
gasPrice: BigInt(plan.request.gasPriceWei),
|
|
337
|
+
nonce: Number(plan.request.nonce)
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
rawTransaction: signed,
|
|
341
|
+
// Viem's signTransaction usually returns the raw serialized tx.
|
|
342
|
+
// We can use keccak256 to get the hash if viem provides it.
|
|
343
|
+
txHash: viem.keccak256 ? viem.keccak256(signed) : void 0
|
|
344
|
+
};
|
|
345
|
+
} catch (e) {
|
|
346
|
+
throw new Error(`Igra signing failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// src/evm-receipts.ts
|
|
352
|
+
function normalizeEvmTransactionReceipt(raw) {
|
|
353
|
+
if (!raw || typeof raw !== "object") return null;
|
|
354
|
+
const txHash = raw.transactionHash;
|
|
355
|
+
if (!txHash || typeof txHash !== "string") return null;
|
|
356
|
+
let status = "unknown";
|
|
357
|
+
if (raw.status === "0x1") status = "success";
|
|
358
|
+
else if (raw.status === "0x0") status = "reverted";
|
|
359
|
+
return {
|
|
360
|
+
txHash,
|
|
361
|
+
blockHash: typeof raw.blockHash === "string" ? raw.blockHash : void 0,
|
|
362
|
+
blockNumber: raw.blockNumber ? BigInt(raw.blockNumber) : void 0,
|
|
363
|
+
from: typeof raw.from === "string" ? raw.from : void 0,
|
|
364
|
+
to: typeof raw.to === "string" || raw.to === null ? raw.to : void 0,
|
|
365
|
+
gasUsed: raw.gasUsed ? BigInt(raw.gasUsed) : void 0,
|
|
366
|
+
effectiveGasPrice: raw.effectiveGasPrice ? BigInt(raw.effectiveGasPrice) : void 0,
|
|
367
|
+
status,
|
|
368
|
+
raw
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/bridge.ts
|
|
373
|
+
import { HARDKAS_VERSION as HARDKAS_VERSION2 } from "@hardkas/artifacts";
|
|
374
|
+
var IGRA_BRIDGE_ASSUMPTIONS = {
|
|
375
|
+
schema: "hardkas.l2BridgeAssumptions.v1",
|
|
376
|
+
hardkasVersion: HARDKAS_VERSION2,
|
|
377
|
+
l2Network: "igra",
|
|
378
|
+
bridgePhase: "pre-zk",
|
|
379
|
+
trustlessExit: false,
|
|
380
|
+
custodyModel: "Phase-dependent bridge custody; verify current bridge implementation before use.",
|
|
381
|
+
validatorModel: "Phase-dependent",
|
|
382
|
+
exitModel: "Trustless exit is available only in the ZK phase.",
|
|
383
|
+
riskProfile: "high",
|
|
384
|
+
notes: [
|
|
385
|
+
"Bridge security is phase-dependent: pre-ZK -> MPC -> ZK.",
|
|
386
|
+
"pre-ZK implies stronger trust assumptions.",
|
|
387
|
+
"MPC implies threshold committee trust assumptions.",
|
|
388
|
+
"ZK phase enables validity-proof based trustless exit.",
|
|
389
|
+
"HardKAS does not perform bridge operations in v0.2-alpha."
|
|
390
|
+
],
|
|
391
|
+
updatedAt: "2026-05-07T00:00:00Z"
|
|
392
|
+
// Reference date for Phase 33
|
|
393
|
+
};
|
|
394
|
+
var BRIDGE_ASSUMPTIONS_REGISTRY = [
|
|
395
|
+
IGRA_BRIDGE_ASSUMPTIONS
|
|
396
|
+
];
|
|
397
|
+
function getL2BridgeAssumptions(network) {
|
|
398
|
+
return BRIDGE_ASSUMPTIONS_REGISTRY.find((a) => a.l2Network === network) || null;
|
|
399
|
+
}
|
|
400
|
+
function listL2BridgeAssumptions() {
|
|
401
|
+
return BRIDGE_ASSUMPTIONS_REGISTRY;
|
|
402
|
+
}
|
|
403
|
+
function validateL2BridgeAssumptions(input) {
|
|
404
|
+
const errors = [];
|
|
405
|
+
if (!input || typeof input !== "object") return { ok: false, errors: ["Input must be an object"] };
|
|
406
|
+
const v = input;
|
|
407
|
+
if (v.schema !== "hardkas.l2BridgeAssumptions.v1") errors.push("Invalid schema: expected 'hardkas.l2BridgeAssumptions.v1'");
|
|
408
|
+
if (typeof v.hardkasVersion !== "string" || !v.hardkasVersion) errors.push("Missing hardkasVersion");
|
|
409
|
+
if (typeof v.l2Network !== "string" || !v.l2Network) errors.push("Missing l2Network");
|
|
410
|
+
const validPhases = ["pre-zk", "mpc", "zk", "unknown"];
|
|
411
|
+
if (!validPhases.includes(v.bridgePhase)) errors.push(`Invalid bridgePhase: ${v.bridgePhase}`);
|
|
412
|
+
if (v.bridgePhase !== "zk" && v.trustlessExit === true) {
|
|
413
|
+
errors.push("trustlessExit must be false if bridgePhase is not 'zk'");
|
|
414
|
+
}
|
|
415
|
+
if (!Array.isArray(v.notes) || v.notes.length === 0) errors.push("Notes must be a non-empty array");
|
|
416
|
+
if (!v.updatedAt) errors.push("Missing updatedAt");
|
|
417
|
+
return { ok: errors.length === 0, errors };
|
|
418
|
+
}
|
|
419
|
+
function assertValidL2BridgeAssumptions(input) {
|
|
420
|
+
const { ok, errors } = validateL2BridgeAssumptions(input);
|
|
421
|
+
if (!ok) {
|
|
422
|
+
throw new Error(`Invalid L2 bridge assumptions:
|
|
423
|
+
${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
424
|
+
}
|
|
425
|
+
return input;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/abi.ts
|
|
429
|
+
import { encodeDeployData, parseAbiItem } from "viem";
|
|
430
|
+
function encodeConstructorArgs(bytecode, constructorSignature, args) {
|
|
431
|
+
const formattedBytecode = bytecode.startsWith("0x") ? bytecode : `0x${bytecode}`;
|
|
432
|
+
const abiItem = parseAbiItem(constructorSignature);
|
|
433
|
+
if (abiItem.type !== "constructor") {
|
|
434
|
+
throw new Error("Invalid constructor signature. Must start with 'constructor'.");
|
|
435
|
+
}
|
|
436
|
+
const parsedArgs = args.map((arg) => {
|
|
437
|
+
const trimmed = arg.trim();
|
|
438
|
+
if (trimmed.startsWith("0x")) return trimmed;
|
|
439
|
+
if (/^\d+$/.test(trimmed)) return BigInt(trimmed);
|
|
440
|
+
if (trimmed.toLowerCase() === "true") return true;
|
|
441
|
+
if (trimmed.toLowerCase() === "false") return false;
|
|
442
|
+
return trimmed;
|
|
443
|
+
});
|
|
444
|
+
return encodeDeployData({
|
|
445
|
+
abi: [abiItem],
|
|
446
|
+
bytecode: formattedBytecode,
|
|
447
|
+
args: parsedArgs
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
export {
|
|
451
|
+
BUILTIN_L2_PROFILES,
|
|
452
|
+
EvmJsonRpcClient,
|
|
453
|
+
UnsupportedIgraTxSigner,
|
|
454
|
+
ViemIgraTxSigner,
|
|
455
|
+
assertValidL2BridgeAssumptions,
|
|
456
|
+
assertValidL2Profile,
|
|
457
|
+
checkEvmRpcHealth,
|
|
458
|
+
encodeConstructorArgs,
|
|
459
|
+
formatWeiAsEtherLike,
|
|
460
|
+
getBuiltInL2Profiles,
|
|
461
|
+
getL2BridgeAssumptions,
|
|
462
|
+
getL2Profile,
|
|
463
|
+
listL2BridgeAssumptions,
|
|
464
|
+
listL2Profiles,
|
|
465
|
+
normalizeEvmTransactionReceipt,
|
|
466
|
+
toHexQuantity,
|
|
467
|
+
validateL2BridgeAssumptions,
|
|
468
|
+
validateL2Profile,
|
|
469
|
+
waitForEvmRpcReady
|
|
470
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hardkas/l2",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"viem": "^2.48.8",
|
|
11
|
+
"@hardkas/artifacts": "0.1.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"tsup": "^8.3.5",
|
|
15
|
+
"typescript": "^5.6.3",
|
|
16
|
+
"vitest": "^2.1.4"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Javier Rodriguez",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/jrodrg92/Hardkas.git",
|
|
23
|
+
"directory": "packages/l2"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/jrodrg92/Hardkas/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/jrodrg92/Hardkas/tree/main/packages/l2#readme",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
36
|
+
"dev": "tsup src/index.ts --format esm --watch --dts",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"lint": "tsc --noEmit"
|
|
39
|
+
}
|
|
40
|
+
}
|