@callcium/sdk 0.6.0 → 0.8.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.
@@ -0,0 +1,313 @@
1
+ import { bytesToHex, toAddress, writeBE16 } from "./bytes";
2
+ import { Op, Scope, ContextProperty, Limits } from "./constants";
3
+ import { CallciumError } from "./errors";
4
+
5
+ import type { Hex, Constraint } from "./types";
6
+
7
+ ///////////////////////////////////////////////////////////////////////////
8
+ // Value encoding helpers
9
+ ///////////////////////////////////////////////////////////////////////////
10
+
11
+ /** Accepted scalar value types for operator arguments. */
12
+ export type ScalarValue = bigint | number | boolean | string;
13
+
14
+ /** Strip an optional 0x prefix and validate a 40-hex-char (20-byte) address body. */
15
+ function addressBody(value: string): string {
16
+ return toAddress(value).slice(2);
17
+ }
18
+
19
+ /** Convert a scalar value to a 32-byte big-endian word. */
20
+ function encodeWord(value: ScalarValue): Uint8Array {
21
+ const word = new Uint8Array(32);
22
+
23
+ if (typeof value === "boolean") {
24
+ word[31] = value ? 1 : 0;
25
+ return word;
26
+ }
27
+
28
+ if (typeof value === "string") {
29
+ // Address encoding: validate 20-byte hex and right-align into 32 bytes.
30
+ const body = addressBody(value);
31
+ for (let i = 0; i < 20; i++) {
32
+ word[12 + i] = parseInt(body.substring(i * 2, i * 2 + 2), 16);
33
+ }
34
+ return word;
35
+ }
36
+
37
+ // bigint | number — unsigned 256-bit big-endian.
38
+ let bigValue = typeof value === "number" ? BigInt(value) : value;
39
+ for (let i = 31; i >= 0; i--) {
40
+ word[i] = Number(bigValue & 0xffn);
41
+ bigValue >>= 8n;
42
+ }
43
+ return word;
44
+ }
45
+
46
+ /** Pack a scalar value as a hex operator payload (opCode byte + 32-byte word). */
47
+ function singleOp(opCode: number, value: ScalarValue): Hex {
48
+ const buffer = new Uint8Array(33);
49
+ buffer[0] = opCode;
50
+ buffer.set(encodeWord(value), 1);
51
+ return bytesToHex(buffer);
52
+ }
53
+
54
+ /** Pack a range operator (opCode byte + min word + max word). */
55
+ function rangeOp(opCode: number, min: bigint, max: bigint): Hex {
56
+ if (min > max) {
57
+ throw new CallciumError("INVALID_RANGE", `Range min (${min}) must not exceed max (${max})`);
58
+ }
59
+ const buffer = new Uint8Array(65);
60
+ buffer[0] = opCode;
61
+ buffer.set(encodeWord(min), 1);
62
+ buffer.set(encodeWord(max), 33);
63
+ return bytesToHex(buffer);
64
+ }
65
+
66
+ /** Largest member count whose 32-byte words still fit a rule's data length field. */
67
+ export const MAX_SET_MEMBERS = Limits.MAX_SET_MEMBERS;
68
+
69
+ /** Convert values to bigint, sort ascending (unsigned), deduplicate, and pack as set payload. */
70
+ function setOp(opCode: number, values: readonly ScalarValue[]): Hex {
71
+ const bigs = values.map((v) => {
72
+ if (typeof v === "bigint") return v;
73
+ if (typeof v === "number") return BigInt(v);
74
+ if (typeof v === "boolean") return v ? 1n : 0n;
75
+ // String address.
76
+ return BigInt("0x" + addressBody(v));
77
+ });
78
+
79
+ bigs.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
80
+
81
+ const deduped: bigint[] = [];
82
+ for (const value of bigs) {
83
+ if (deduped.length === 0 || deduped[deduped.length - 1] !== value) {
84
+ deduped.push(value);
85
+ }
86
+ }
87
+
88
+ if (deduped.length === 0) {
89
+ throw new CallciumError("EMPTY_SET", "Set must contain at least one value");
90
+ }
91
+ if (deduped.length > MAX_SET_MEMBERS) {
92
+ throw new CallciumError("SET_TOO_LARGE", `Set must contain at most ${MAX_SET_MEMBERS} values`);
93
+ }
94
+
95
+ const buffer = new Uint8Array(1 + deduped.length * 32);
96
+ buffer[0] = opCode;
97
+ for (let i = 0; i < deduped.length; i++) {
98
+ buffer.set(encodeWord(deduped[i]!), 1 + i * 32);
99
+ }
100
+ return bytesToHex(buffer);
101
+ }
102
+
103
+ ///////////////////////////////////////////////////////////////////////////
104
+ // Path encoding
105
+ ///////////////////////////////////////////////////////////////////////////
106
+
107
+ /** Encode a sequence of uint16 path steps as a big-endian hex string. */
108
+ function encodePath(steps: readonly number[]): Hex {
109
+ const buffer = new Uint8Array(steps.length * 2);
110
+ for (let i = 0; i < steps.length; i++) {
111
+ writeBE16(buffer, i * 2, steps[i]!);
112
+ }
113
+ return bytesToHex(buffer);
114
+ }
115
+
116
+ ///////////////////////////////////////////////////////////////////////////
117
+ // ConstraintBuilder
118
+ ///////////////////////////////////////////////////////////////////////////
119
+
120
+ /**
121
+ * Mutable builder that accumulates operators targeting a single path.
122
+ * Implements the `Constraint` interface so it can be passed directly to policy builders.
123
+ */
124
+ export class ConstraintBuilder implements Constraint {
125
+ readonly scope: number;
126
+ readonly path: Hex;
127
+ readonly operators: Hex[];
128
+
129
+ /** @internal */
130
+ constructor(scope: number, path: Hex) {
131
+ this.scope = scope;
132
+ this.path = path;
133
+ this.operators = [];
134
+ }
135
+
136
+ /** Push a pre-encoded operator hex string and return this for chaining. */
137
+ private push(opHex: Hex): this {
138
+ this.operators.push(opHex);
139
+ return this;
140
+ }
141
+
142
+ ///////////////////////////////////////////////////////////////////////////
143
+ // Value operators
144
+ ///////////////////////////////////////////////////////////////////////////
145
+
146
+ /** Assert the value equals `value`. */
147
+ eq(value: ScalarValue): this {
148
+ return this.push(singleOp(Op.EQ, value));
149
+ }
150
+
151
+ /** Assert the value does not equal `value`. */
152
+ neq(value: ScalarValue): this {
153
+ return this.push(singleOp(Op.EQ | Op.NOT, value));
154
+ }
155
+
156
+ /** Assert the value is greater than `bound`. */
157
+ gt(bound: bigint | number): this {
158
+ return this.push(singleOp(Op.GT, bound));
159
+ }
160
+
161
+ /** Assert the value is less than `bound`. */
162
+ lt(bound: bigint | number): this {
163
+ return this.push(singleOp(Op.LT, bound));
164
+ }
165
+
166
+ /** Assert the value is greater than or equal to `bound`. */
167
+ gte(bound: bigint | number): this {
168
+ return this.push(singleOp(Op.GTE, bound));
169
+ }
170
+
171
+ /** Assert the value is less than or equal to `bound`. */
172
+ lte(bound: bigint | number): this {
173
+ return this.push(singleOp(Op.LTE, bound));
174
+ }
175
+
176
+ /**
177
+ * Assert the value is within [min, max] inclusive.
178
+ * @throws {CallciumError} If min > max.
179
+ */
180
+ between(min: bigint | number, max: bigint | number): this {
181
+ return this.push(rangeOp(Op.BETWEEN, BigInt(min), BigInt(max)));
182
+ }
183
+
184
+ ///////////////////////////////////////////////////////////////////////////
185
+ // Set membership
186
+ ///////////////////////////////////////////////////////////////////////////
187
+
188
+ /**
189
+ * Assert the value is a member of the set.
190
+ * Values are sorted and deduplicated before encoding.
191
+ * @throws {CallciumError} If the set is empty after deduplication.
192
+ */
193
+ isIn(values: readonly ScalarValue[]): this {
194
+ return this.push(setOp(Op.IN, values));
195
+ }
196
+
197
+ /**
198
+ * Assert the value is not a member of the set.
199
+ * @throws {CallciumError} If the set is empty after deduplication.
200
+ */
201
+ notIn(values: readonly ScalarValue[]): this {
202
+ return this.push(setOp(Op.IN | Op.NOT, values));
203
+ }
204
+
205
+ ///////////////////////////////////////////////////////////////////////////
206
+ // Bitmask operators
207
+ ///////////////////////////////////////////////////////////////////////////
208
+
209
+ /** Assert all bits in `mask` are set. */
210
+ bitmaskAll(mask: bigint): this {
211
+ return this.push(singleOp(Op.BITMASK_ALL, mask));
212
+ }
213
+
214
+ /** Assert at least one bit in `mask` is set. */
215
+ bitmaskAny(mask: bigint): this {
216
+ return this.push(singleOp(Op.BITMASK_ANY, mask));
217
+ }
218
+
219
+ /** Assert no bit in `mask` is set. */
220
+ bitmaskNone(mask: bigint): this {
221
+ return this.push(singleOp(Op.BITMASK_NONE, mask));
222
+ }
223
+
224
+ ///////////////////////////////////////////////////////////////////////////
225
+ // Length operators
226
+ ///////////////////////////////////////////////////////////////////////////
227
+
228
+ /** Assert the runtime length equals `n`. */
229
+ lengthEq(n: bigint | number): this {
230
+ return this.push(singleOp(Op.LENGTH_EQ, n));
231
+ }
232
+
233
+ /** Assert the runtime length is greater than `n`. */
234
+ lengthGt(n: bigint | number): this {
235
+ return this.push(singleOp(Op.LENGTH_GT, n));
236
+ }
237
+
238
+ /** Assert the runtime length is less than `n`. */
239
+ lengthLt(n: bigint | number): this {
240
+ return this.push(singleOp(Op.LENGTH_LT, n));
241
+ }
242
+
243
+ /** Assert the runtime length is greater than or equal to `n`. */
244
+ lengthGte(n: bigint | number): this {
245
+ return this.push(singleOp(Op.LENGTH_GTE, n));
246
+ }
247
+
248
+ /** Assert the runtime length is less than or equal to `n`. */
249
+ lengthLte(n: bigint | number): this {
250
+ return this.push(singleOp(Op.LENGTH_LTE, n));
251
+ }
252
+
253
+ /**
254
+ * Assert the runtime length is within [min, max] inclusive.
255
+ * @throws {CallciumError} If min > max.
256
+ */
257
+ lengthBetween(min: bigint | number, max: bigint | number): this {
258
+ return this.push(rangeOp(Op.LENGTH_BETWEEN, BigInt(min), BigInt(max)));
259
+ }
260
+ }
261
+
262
+ ///////////////////////////////////////////////////////////////////////////
263
+ // Target factories
264
+ ///////////////////////////////////////////////////////////////////////////
265
+
266
+ /**
267
+ * Target a calldata argument by path.
268
+ * Each argument is a big-endian uint16 step; multiple steps navigate into nested types.
269
+ */
270
+ export function arg(p0: number, ...rest: number[]): ConstraintBuilder;
271
+ export function arg(...steps: number[]): ConstraintBuilder {
272
+ return new ConstraintBuilder(Scope.CALLDATA, encodePath(steps));
273
+ }
274
+
275
+ /** Target the `msg.sender` context property. */
276
+ export function msgSender(): ConstraintBuilder {
277
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_SENDER]));
278
+ }
279
+
280
+ /** Target the `msg.value` context property. */
281
+ export function msgValue(): ConstraintBuilder {
282
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.MSG_VALUE]));
283
+ }
284
+
285
+ /** Target the `block.timestamp` context property. */
286
+ export function blockTimestamp(): ConstraintBuilder {
287
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_TIMESTAMP]));
288
+ }
289
+
290
+ /** Target the `block.number` context property. */
291
+ export function blockNumber(): ConstraintBuilder {
292
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BLOCK_NUMBER]));
293
+ }
294
+
295
+ /** Target the `block.chainid` context property. */
296
+ export function chainId(): ConstraintBuilder {
297
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.CHAIN_ID]));
298
+ }
299
+
300
+ /** Target the `tx.origin` context property. */
301
+ export function txOrigin(): ConstraintBuilder {
302
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.TX_ORIGIN]));
303
+ }
304
+
305
+ /** Target the `block.basefee` context property. */
306
+ export function baseFee(): ConstraintBuilder {
307
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.BASE_FEE]));
308
+ }
309
+
310
+ /** Target the `tx.gasprice` context property. */
311
+ export function gasPrice(): ConstraintBuilder {
312
+ return new ConstraintBuilder(Scope.CONTEXT, encodePath([ContextProperty.GAS_PRICE]));
313
+ }