Skip to content

Commit 9b299a9

Browse files
feat: util helpers (#52)
* feat: add util helpers * feat: add utils fns for types * fix: imports
1 parent fbf8b86 commit 9b299a9

File tree

17 files changed

+367
-46
lines changed

17 files changed

+367
-46
lines changed

src/adapters/__tests__/adapter-harness.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
L2_NATIVE_TOKEN_VAULT_ADDRESS,
2929
L2_BASE_TOKEN_ADDRESS,
3030
} from '../../core/constants';
31+
import { isBigint } from '../../core/utils';
3132

3233
const IBridgehub = new Interface(IBridgehubABI as any);
3334
const IL1AssetRouter = new Interface(IL1AssetRouterABI as any);
@@ -63,9 +64,7 @@ class CallRegistry {
6364
}
6465

6566
private argsKey(args: unknown[]) {
66-
return JSON.stringify(args, (_, value) =>
67-
typeof value === 'bigint' ? value.toString() : value,
68-
);
67+
return JSON.stringify(args, (_, value) => (isBigint(value) ? value.toString() : value));
6968
}
7069
}
7170

@@ -153,7 +152,7 @@ function makeEthersL1(state: EthersL1State) {
153152
state.estimateGasSpy?.(tx);
154153
const { estimateGasValue } = state;
155154
if (estimateGasValue instanceof Error) throw estimateGasValue;
156-
if (typeof estimateGasValue === 'bigint') return estimateGasValue;
155+
if (isBigint(estimateGasValue)) return estimateGasValue;
157156
return 100_000n;
158157
},
159158
async getFeeData() {
@@ -189,7 +188,7 @@ function makeEthersL2(state: EthersL2State) {
189188
state.estimateGasSpy?.(tx);
190189
const { estimateGasValue } = state;
191190
if (estimateGasValue instanceof Error) throw estimateGasValue;
192-
if (typeof estimateGasValue === 'bigint') return estimateGasValue;
191+
if (isBigint(estimateGasValue)) return estimateGasValue;
193192
return 100_000n;
194193
},
195194
async getNetwork() {
@@ -311,7 +310,7 @@ function makeViemClient(state: ViemClientState): PublicClient {
311310
state.lastArgs = args;
312311
const val = state.estimateGasValue;
313312
if (val instanceof Error) throw val;
314-
if (typeof val === 'bigint') return val;
313+
if (isBigint(val)) return val;
315314
return 100_000n;
316315
},
317316
async estimateFeesPerGas() {

src/adapters/ethers/resources/deposits/services/verification.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Interface, type Log, type Provider, type TransactionReceipt } from 'ethers';
22
import type { Hex } from '../../../../../core/types/primitives';
3-
import { isHash66 } from '../../../../../core/utils/addr';
3+
import { isHash66 } from '../../../../../core/utils/hash';
44
import { TOPIC_CANONICAL_ASSIGNED, TOPIC_CANONICAL_SUCCESS } from '../../../../../core/constants';
55

66
import { createError } from '../../../../../core/errors/factory.ts';

src/adapters/viem/resources/deposits/services/verification.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { PublicClient, TransactionReceipt, Log, AbiEvent } from 'viem';
44
import type { Hex } from '../../../../../core/types/primitives';
55
import { decodeEventLog } from 'viem';
6-
import { isHash66 } from '../../../../../core/utils/addr';
6+
import { isHash66 } from '../../../../../core/utils/hash';
77
import { TOPIC_CANONICAL_ASSIGNED, TOPIC_CANONICAL_SUCCESS } from '../../../../../core/constants';
88
import { createError } from '../../../../../core/errors/factory';
99

src/core/errors/formatter.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
/* -------------------- Formatting helpers -------------------- */
44
import type { ErrorEnvelope } from '../types';
5+
import { isBigint, isNumber } from '../utils';
56

67
function elideMiddle(s: string, max = 96): string {
78
if (s.length <= max) return s;
@@ -12,7 +13,7 @@ function elideMiddle(s: string, max = 96): string {
1213
function shortJSON(v: unknown, max = 240): string {
1314
try {
1415
const s = JSON.stringify(v, (_k: string, val: unknown): unknown =>
15-
typeof val === 'bigint' ? `${val.toString()}n` : val,
16+
isBigint(val) ? `${val.toString()}n` : val,
1617
);
1718
return s.length > max ? elideMiddle(s, max) : s;
1819
} catch {
@@ -36,7 +37,7 @@ function formatContextLine(ctx?: Record<string, unknown>): string | undefined {
3637
parts.push(`txHash=${typeof txHash === 'string' ? txHash : shortJSON(txHash, 96)}`);
3738
if (nonce !== undefined) {
3839
const nonceStr =
39-
typeof nonce === 'string' || typeof nonce === 'number' || typeof nonce === 'bigint'
40+
typeof nonce === 'string' || isNumber(nonce) || isBigint(nonce)
4041
? String(nonce)
4142
: shortJSON(nonce, 48);
4243
parts.push(`nonce=${nonceStr}`);
@@ -75,8 +76,8 @@ function formatCause(c?: unknown): string[] {
7576
const nameVal = obj.name;
7677
const nameStr =
7778
typeof nameVal === 'string' ||
78-
typeof nameVal === 'number' ||
79-
typeof nameVal === 'bigint' ||
79+
isNumber(nameVal) ||
80+
isBigint(nameVal) ||
8081
typeof nameVal === 'boolean'
8182
? String(nameVal)
8283
: shortJSON(nameVal, 120);
@@ -86,8 +87,8 @@ function formatCause(c?: unknown): string[] {
8687
const codeVal = obj.code;
8788
const codeStr =
8889
typeof codeVal === 'string' ||
89-
typeof codeVal === 'number' ||
90-
typeof codeVal === 'bigint' ||
90+
isNumber(codeVal) ||
91+
isBigint(codeVal) ||
9192
typeof codeVal === 'boolean'
9293
? String(codeVal)
9394
: shortJSON(codeVal, 120);
@@ -98,8 +99,8 @@ function formatCause(c?: unknown): string[] {
9899
if (obj.message) {
99100
const messageStr =
100101
typeof obj.message === 'string' ||
101-
typeof obj.message === 'number' ||
102-
typeof obj.message === 'bigint' ||
102+
isNumber(obj.message) ||
103+
isBigint(obj.message) ||
103104
typeof obj.message === 'boolean'
104105
? String(obj.message)
105106
: shortJSON(obj.message, 600);

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type { ZksRpc } from './rpc/zks';
1919
export { makeTransportFromEthers, makeTransportFromViem } from './rpc/transport';
2020

2121
export * from './utils/addr';
22+
export * from './utils/hash';
2223

2324
// Core resources (routes, events, logs)
2425
export * from './resources/deposits/route';

src/core/resources/interop/attributes/__tests__/call.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { describe, it, expect } from 'bun:test';
33
import { createCallAttributes } from '../call';
44
import type { AttributesCodec } from '../types';
55
import type { Hex } from '../../../../types/primitives';
6+
import { isBigint } from '../../../../utils';
67

78
describe('interop/attributes/call', () => {
89
describe('createCallAttributes', () => {
910
const mockCodec: AttributesCodec = {
1011
encode: (fn: string, args: readonly unknown[]): Hex => {
1112
// Convert BigInt to string for serialization
12-
const serializable = args.map((a) => (typeof a === 'bigint' ? a.toString() : a));
13+
const serializable = args.map((a) => (isBigint(a) ? a.toString() : a));
1314
return `0x${fn}:${JSON.stringify(serializable)}` as Hex;
1415
},
1516
decode: () => ({ selector: '0x00000000', name: 'mock', args: [] }),

src/core/rpc/zks.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { Hex, Address } from '../types/primitives';
1313
import { createError, shapeCause } from '../errors/factory';
1414
import { withRpcOp } from '../errors/rpc';
1515
import { isZKsyncError, type Resource } from '../types/errors';
16+
import { isBigint, isNumber } from '../utils';
1617

1718
/** ZKsync-specific RPC methods. */
1819
export interface ZksRpc {
@@ -68,9 +69,9 @@ export function normalizeProof(p: unknown): ProofNormalized {
6869
}
6970

7071
const toBig = (x: unknown) =>
71-
typeof x === 'bigint'
72+
isBigint(x)
7273
? x
73-
: typeof x === 'number'
74+
: isNumber(x)
7475
? BigInt(x)
7576
: typeof x === 'string'
7677
? BigInt(x)
@@ -121,8 +122,8 @@ function ensureNumber(
121122
const operation = opts?.operation ?? 'zksrpc.normalizeGenesis';
122123
const messagePrefix = opts?.messagePrefix ?? 'Malformed genesis response';
123124

124-
if (typeof value === 'number' && Number.isFinite(value)) return value;
125-
if (typeof value === 'bigint') return Number(value);
125+
if (isNumber(value)) return value;
126+
if (isBigint(value)) return Number(value);
126127
if (typeof value === 'string' && value.trim() !== '') {
127128
const parsed = Number(value);
128129
if (Number.isFinite(parsed)) return parsed;
@@ -145,8 +146,8 @@ function ensureBigInt(
145146
const operation = opts?.operation ?? 'zksrpc.normalizeBlockMetadata';
146147
const messagePrefix = opts?.messagePrefix ?? 'Malformed block metadata response';
147148

148-
if (typeof value === 'bigint') return value;
149-
if (typeof value === 'number' && Number.isFinite(value)) {
149+
if (isBigint(value)) return value;
150+
if (isNumber(value)) {
150151
if (!Number.isInteger(value)) {
151152
throw createError('RPC', {
152153
resource: 'zksrpc' as Resource,
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { describe, it, expect } from 'bun:test';
2+
import type {
3+
InteropExpectedRoot,
4+
InteropFinalizationInfo,
5+
InteropMessageProof,
6+
} from '../flows/interop';
7+
import {
8+
isInteropExpectedRoot,
9+
isInteropFinalizationInfo,
10+
isInteropMessageProof,
11+
} from '../flows/interop';
12+
import type { Hex } from '../primitives';
13+
14+
const hash66a = ('0x' + 'a'.repeat(64)) as Hex;
15+
const hash66b = ('0x' + 'b'.repeat(64)) as Hex;
16+
const address = '0x1111111111111111111111111111111111111111' as const;
17+
18+
describe('types/flows/interop.isInteropExpectedRoot', () => {
19+
it('returns true for valid expected root shape', () => {
20+
const value: InteropExpectedRoot = {
21+
rootChainId: 1n,
22+
batchNumber: 2n,
23+
expectedRoot: hash66a,
24+
};
25+
26+
expect(isInteropExpectedRoot(value)).toBe(true);
27+
});
28+
29+
it('returns false for invalid or missing fields', () => {
30+
expect(isInteropExpectedRoot(null)).toBe(false);
31+
expect(
32+
isInteropExpectedRoot({
33+
rootChainId: 1,
34+
batchNumber: 2n,
35+
expectedRoot: hash66a,
36+
}),
37+
).toBe(false);
38+
expect(
39+
isInteropExpectedRoot({
40+
rootChainId: 1n,
41+
batchNumber: 2n,
42+
expectedRoot: 'not-hex',
43+
}),
44+
).toBe(false);
45+
});
46+
});
47+
48+
describe('types/flows/interop.isInteropMessageProof', () => {
49+
it('returns true for valid proof shape', () => {
50+
const value: InteropMessageProof = {
51+
chainId: 324n,
52+
l1BatchNumber: 100n,
53+
l2MessageIndex: 0n,
54+
message: {
55+
txNumberInBatch: 2,
56+
sender: address,
57+
data: '0x1234',
58+
},
59+
proof: [hash66a, hash66b],
60+
};
61+
62+
expect(isInteropMessageProof(value)).toBe(true);
63+
});
64+
65+
it('returns false for invalid nested fields', () => {
66+
expect(
67+
isInteropMessageProof({
68+
chainId: 324n,
69+
l1BatchNumber: 100n,
70+
l2MessageIndex: 0n,
71+
message: {
72+
txNumberInBatch: 2,
73+
sender: '0x1234',
74+
data: '0x1234',
75+
},
76+
proof: [hash66a],
77+
}),
78+
).toBe(false);
79+
80+
expect(
81+
isInteropMessageProof({
82+
chainId: 324n,
83+
l1BatchNumber: 100n,
84+
l2MessageIndex: 0n,
85+
message: {
86+
txNumberInBatch: 2,
87+
sender: address,
88+
data: '0x1234',
89+
},
90+
proof: ['0x1234'],
91+
}),
92+
).toBe(false);
93+
});
94+
});
95+
96+
describe('types/flows/interop.isInteropFinalizationInfo', () => {
97+
it('returns true for a complete valid payload', () => {
98+
const value: InteropFinalizationInfo = {
99+
l2SrcTxHash: hash66a,
100+
bundleHash: hash66b,
101+
dstChainId: 324n,
102+
expectedRoot: {
103+
rootChainId: 1n,
104+
batchNumber: 7n,
105+
expectedRoot: hash66a,
106+
},
107+
proof: {
108+
chainId: 324n,
109+
l1BatchNumber: 100n,
110+
l2MessageIndex: 0n,
111+
message: {
112+
txNumberInBatch: 2,
113+
sender: address,
114+
data: '0x1234',
115+
},
116+
proof: [hash66a],
117+
},
118+
encodedData: '0xdeadbeef',
119+
};
120+
121+
expect(isInteropFinalizationInfo(value)).toBe(true);
122+
});
123+
124+
it('returns false for invalid top-level or nested values', () => {
125+
expect(
126+
isInteropFinalizationInfo({
127+
l2SrcTxHash: '0x1234',
128+
bundleHash: hash66b,
129+
dstChainId: 324n,
130+
expectedRoot: {
131+
rootChainId: 1n,
132+
batchNumber: 7n,
133+
expectedRoot: hash66a,
134+
},
135+
proof: {
136+
chainId: 324n,
137+
l1BatchNumber: 100n,
138+
l2MessageIndex: 0n,
139+
message: {
140+
txNumberInBatch: 2,
141+
sender: address,
142+
data: '0x1234',
143+
},
144+
proof: [hash66a],
145+
},
146+
encodedData: '0xdeadbeef',
147+
}),
148+
).toBe(false);
149+
150+
expect(
151+
isInteropFinalizationInfo({
152+
l2SrcTxHash: hash66a,
153+
bundleHash: hash66b,
154+
dstChainId: 324n,
155+
expectedRoot: {
156+
rootChainId: 1n,
157+
batchNumber: 7n,
158+
expectedRoot: hash66a,
159+
},
160+
proof: {
161+
chainId: 324n,
162+
l1BatchNumber: 100n,
163+
l2MessageIndex: 0n,
164+
message: {
165+
txNumberInBatch: 2,
166+
sender: address,
167+
data: '0x1234',
168+
},
169+
proof: ['0x1234'],
170+
},
171+
encodedData: '0xdeadbeef',
172+
}),
173+
).toBe(false);
174+
});
175+
});

0 commit comments

Comments
 (0)