Skip to content

Commit 6a56166

Browse files
committed
fix: enforce fixed size for cairo bytes
1 parent 74b8d72 commit 6a56166

File tree

5 files changed

+114
-64
lines changed

5 files changed

+114
-64
lines changed

__tests__/utils/cairoDataTypes/CairoByteArray.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,8 @@ describe('CairoByteArray Unit Tests', () => {
707707
'0x' +
708708
'000000019900000000000002222222374206275726e206d65737aaaa000001' +
709709
'000000029900000000000002222222374206275726e206d65737aaaa000002' +
710-
'000000039900000000000002222222374206275726e206d65737aaaa000003';
710+
'000000039900000000000002222222374206275726e206d65737aaaa000003' +
711+
'00d0f0';
711712
const buffer = Buffer.from(content.slice(2), 'hex');
712713
const byteArray = new CairoByteArray(buffer);
713714
const apiRequest = byteArray.toApiRequest();
@@ -716,4 +717,33 @@ describe('CairoByteArray Unit Tests', () => {
716717
expect(reconstructedByteArray.toHexString()).toEqual(content);
717718
});
718719
});
720+
721+
describe('toElements method', () => {
722+
test('should convert empty string to empty array', () => {
723+
const byteArray = new CairoByteArray('');
724+
expect(byteArray.toElements()).toEqual([]);
725+
});
726+
727+
test('should convert short string into single element array corresponding to the pending word', () => {
728+
const byteArray = new CairoByteArray('Test');
729+
expect(byteArray.toElements()).toEqual([new Uint8Array([84, 101, 115, 116])]);
730+
});
731+
732+
test('should convert large string into full size elements', () => {
733+
const apiResponse = [
734+
'0x3',
735+
'0x19900000000000002222222374206275726e206d65737aaaa000001',
736+
'0x29900000000000002222222374206275726e206d65737aaaa000002',
737+
'0x39900000000000002222222374206275726e206d65737aaaa000003',
738+
'0xd0f0',
739+
'0xa',
740+
];
741+
const byteArray = CairoByteArray.factoryFromApiResponse(apiResponse.values());
742+
const elements = byteArray.toElements();
743+
744+
expect(elements).toHaveLength(Number(apiResponse[0]) + 1);
745+
elements.slice(0, -1).forEach((e) => expect(e).toHaveLength(31));
746+
expect(elements.at(-1)).toHaveLength(Number(apiResponse.at(-1)));
747+
});
748+
});
719749
});

__tests__/utils/cairoDataTypes/CairoBytes31.test.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,57 @@
1-
import { CairoBytes31, CairoFelt252 } from '../../../src';
1+
import { BigNumberish, CairoBytes31, CairoFelt252 } from '../../../src';
2+
import { addHexPrefix } from '../../../src/utils/encode';
3+
4+
function uint8ArrayToSize(input: Uint8Array | Array<number>, size: number = 31) {
5+
const output = new Uint8Array(size);
6+
output.set(input, size - input.length);
7+
return output;
8+
}
9+
10+
function toHex62(number: BigNumberish) {
11+
return addHexPrefix(BigInt(number).toString(16).padStart(62, '0'));
12+
}
213

314
describe('CairoBytes31 class Unit Tests', () => {
415
describe('constructor with different input types', () => {
516
test('should handle string input', () => {
617
const bytes31 = new CairoBytes31('hello');
718
expect(bytes31.data).toBeInstanceOf(Uint8Array);
8-
expect(bytes31.data).toEqual(new Uint8Array([104, 101, 108, 108, 111]));
19+
expect(bytes31.data).toEqual(uint8ArrayToSize([104, 101, 108, 108, 111]));
920
});
1021

1122
test('should handle empty string', () => {
1223
const bytes31 = new CairoBytes31('');
13-
expect(bytes31.data).toEqual(new Uint8Array([]));
24+
expect(bytes31.data).toEqual(uint8ArrayToSize([]));
1425
});
1526

1627
test('should handle Unicode strings', () => {
1728
const bytes31 = new CairoBytes31('☥');
1829
// '☥' in UTF-8: [226, 152, 165]
19-
expect(bytes31.data).toEqual(new Uint8Array([226, 152, 165]));
30+
expect(bytes31.data).toEqual(uint8ArrayToSize([226, 152, 165]));
2031
});
2132

2233
test('should handle Buffer input', () => {
2334
const buffer = Buffer.from([72, 101, 108, 108, 111]); // "Hello"
2435
const bytes31 = new CairoBytes31(buffer);
25-
expect(bytes31.data).toEqual(new Uint8Array([72, 101, 108, 108, 111]));
36+
expect(bytes31.data).toEqual(uint8ArrayToSize([72, 101, 108, 108, 111]));
2637
});
2738

2839
test('should handle empty Buffer', () => {
2940
const buffer = Buffer.alloc(0);
3041
const bytes31 = new CairoBytes31(buffer);
31-
expect(bytes31.data).toEqual(new Uint8Array([]));
42+
expect(bytes31.data).toEqual(uint8ArrayToSize([]));
3243
});
3344

3445
test('should handle Uint8Array input', () => {
3546
const uint8Array = new Uint8Array([87, 111, 114, 108, 100]); // "World"
3647
const bytes31 = new CairoBytes31(uint8Array);
37-
expect(bytes31.data).toEqual(uint8Array);
48+
expect(bytes31.data).toEqual(uint8ArrayToSize(uint8Array));
3849
});
3950

4051
test('should handle empty Uint8Array', () => {
4152
const uint8Array = new Uint8Array([]);
4253
const bytes31 = new CairoBytes31(uint8Array);
43-
expect(bytes31.data).toEqual(new Uint8Array([]));
54+
expect(bytes31.data).toEqual(uint8ArrayToSize([]));
4455
});
4556

4657
test('should handle maximum length input (31 bytes)', () => {
@@ -174,46 +185,47 @@ describe('CairoBytes31 class Unit Tests', () => {
174185
test('should convert empty data to 0x0', () => {
175186
const bytes31 = new CairoBytes31('');
176187
expect(bytes31.toHexString()).toBe('0x0');
188+
expect(bytes31.toHexString('padded')).toBe(toHex62('0x0'));
177189
});
178190

179191
test('should convert single character to hex', () => {
180192
const bytes31 = new CairoBytes31('A'); // ASCII 65 = 0x41
181193
expect(bytes31.toHexString()).toBe('0x41');
194+
expect(bytes31.toHexString('padded')).toBe(toHex62('0x41'));
182195
});
183196

184197
test('should convert multi-character string to hex', () => {
185198
const bytes31 = new CairoBytes31('AB'); // [65, 66] = 0x4142
186199
expect(bytes31.toHexString()).toBe('0x4142');
200+
expect(bytes31.toHexString('padded')).toBe(toHex62('0x4142'));
187201
});
188202

189203
test('should convert Unicode to hex', () => {
190204
const bytes31 = new CairoBytes31('☥'); // [226, 152, 165] = 0xe298a5
191205
expect(bytes31.toHexString()).toBe('0xe298a5');
206+
expect(bytes31.toHexString('padded')).toBe(toHex62('0xe298a5'));
192207
});
193208

194209
test('should convert Buffer to hex', () => {
195210
const buffer = Buffer.from([255, 254]);
196211
const bytes31 = new CairoBytes31(buffer);
197212
expect(bytes31.toHexString()).toBe('0xfffe');
213+
expect(bytes31.toHexString('padded')).toBe(toHex62('0xfffe'));
198214
});
199215

200216
test('should convert Uint8Array to hex', () => {
201217
const array = new Uint8Array([1, 2, 3, 4]);
202218
const bytes31 = new CairoBytes31(array);
203-
expect(bytes31.toHexString()).toBe('0x01020304');
219+
expect(bytes31.toHexString()).toBe('0x1020304');
220+
expect(bytes31.toHexString('padded')).toBe(toHex62('0x1020304'));
204221
});
205222

206223
test('should handle maximum length data', () => {
207224
const maxArray = new Uint8Array(31).fill(255); // 31 bytes of 0xff
208225
const bytes31 = new CairoBytes31(maxArray);
209226
const expectedHex = `0x${'ff'.repeat(31)}`;
210227
expect(bytes31.toHexString()).toBe(expectedHex);
211-
});
212-
213-
test('should preserve leading zero values', () => {
214-
const buffer = Buffer.from([0, 0, 1]);
215-
const bytes31 = new CairoBytes31(buffer);
216-
expect(bytes31.toHexString()).toEqual(`0x${buffer.toString('hex')}`);
228+
expect(bytes31.toHexString('padded')).toBe(expectedHex);
217229
});
218230
});
219231

@@ -236,7 +248,7 @@ describe('CairoBytes31 class Unit Tests', () => {
236248
test('should return hex string array for Buffer input', () => {
237249
const buffer = Buffer.from([1, 0]); // 0x0100 = 256
238250
const bytes31 = new CairoBytes31(buffer);
239-
expect(bytes31.toApiRequest()).toEqual(['0x0100']);
251+
expect(bytes31.toApiRequest()).toEqual(['0x100']);
240252
});
241253

242254
test('should return hex string array for large values', () => {
@@ -342,7 +354,7 @@ describe('CairoBytes31 class Unit Tests', () => {
342354
test('should handle binary data correctly', () => {
343355
const binaryData = new Uint8Array([0, 1, 2, 254, 255]);
344356
const bytes31 = new CairoBytes31(binaryData);
345-
expect(bytes31.data).toEqual(binaryData);
357+
expect(bytes31.data).toEqual(uint8ArrayToSize(binaryData));
346358
expect(bytes31.toBigInt()).toBe(0x0102feffn);
347359
});
348360

@@ -381,7 +393,7 @@ describe('CairoBytes31 class Unit Tests', () => {
381393

382394
testCases.forEach((originalArray) => {
383395
const bytes31 = new CairoBytes31(originalArray);
384-
expect(bytes31.data).toEqual(originalArray);
396+
expect(bytes31.data).toEqual(uint8ArrayToSize(originalArray));
385397
});
386398
});
387399

src/utils/cairoDataTypes/byteArray.ts

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,13 @@ export class CairoByteArray {
138138
decodeUtf8() {
139139
// Convert all bytes to Uint8Array and decode as UTF-8 string
140140
// This ensures multi-byte UTF-8 characters are not split across chunk boundaries
141-
const allBytes = this.reconstructBytes();
141+
const allBytes = concatenateArrayBuffer(this.toElements());
142142
return new TextDecoder().decode(allBytes);
143143
}
144144

145145
toBigInt() {
146146
// Reconstruct the full byte sequence
147-
const allBytes = this.reconstructBytes();
147+
const allBytes = concatenateArrayBuffer(this.toElements());
148148

149149
// Convert bytes array to bigint
150150
if (allBytes.length === 0) {
@@ -160,17 +160,49 @@ export class CairoByteArray {
160160
}
161161

162162
toHexString() {
163-
const allBytes = this.reconstructBytes();
163+
// TODO: revisit empty data handling, how to differentiate empty and zero input
164+
const allBytes = concatenateArrayBuffer(this.toElements());
164165
const hexValue = allBytes.length === 0 ? '0' : buf2hex(allBytes);
165166
return addHexPrefix(hexValue);
166167
}
167168

168169
toBuffer() {
169-
this.assertInitialized();
170-
const allBytes = this.reconstructBytes();
170+
const allBytes = concatenateArrayBuffer(this.toElements());
171171
return Buffer.from(allBytes);
172172
}
173173

174+
/**
175+
* returns an array of all the data chunks and the pending word
176+
* when concatenated, represents the original bytes sequence
177+
*/
178+
toElements(): Uint8Array[] {
179+
this.assertInitialized();
180+
181+
// Add bytes from all complete chunks (each chunk contains exactly 31 bytes when full)
182+
const allChunks: Uint8Array[] = this.data.flatMap((chunk) => chunk.data);
183+
184+
// Add bytes from pending word
185+
const pendingLen = Number(this.pending_word_len.toBigInt());
186+
if (pendingLen) {
187+
const pending = new Uint8Array(pendingLen);
188+
const paddingDifference = pendingLen - this.pending_word.data.length;
189+
pending.set(this.pending_word.data, paddingDifference);
190+
allChunks.push(pending);
191+
}
192+
193+
return allChunks;
194+
}
195+
196+
/**
197+
* Private helper to check if the CairoByteArray is properly initialized
198+
*/
199+
private assertInitialized(): void {
200+
assert(
201+
this.data && this.pending_word !== undefined && this.pending_word_len !== undefined,
202+
'CairoByteArray is not properly initialized'
203+
);
204+
}
205+
174206
static validate(data: Uint8Array | Buffer | BigNumberish | unknown) {
175207
assert(data !== null && data !== undefined, 'Invalid input: null or undefined');
176208
assert(
@@ -227,37 +259,6 @@ export class CairoByteArray {
227259
return abiType === CairoByteArray.abiSelector;
228260
}
229261

230-
/**
231-
* Private helper to check if the CairoByteArray is properly initialized
232-
*/
233-
private assertInitialized(): void {
234-
assert(
235-
this.data && this.pending_word !== undefined && this.pending_word_len !== undefined,
236-
'CairoByteArray is not properly initialized'
237-
);
238-
}
239-
240-
/**
241-
* Private helper to reconstruct the full byte sequence from chunks and pending word
242-
*/
243-
private reconstructBytes(): Uint8Array {
244-
this.assertInitialized();
245-
246-
// Add bytes from all complete chunks (each chunk contains exactly 31 bytes when full)
247-
const allChunks: Uint8Array[] = this.data.flatMap((chunk) => chunk.data);
248-
249-
// // Add bytes from pending word
250-
const pendingLen = Number(this.pending_word_len.toBigInt());
251-
if (pendingLen) {
252-
const pending = new Uint8Array(pendingLen);
253-
const paddingDifference = pendingLen - this.pending_word.data.length;
254-
pending.set(this.pending_word.data, paddingDifference);
255-
allChunks.push(pending);
256-
}
257-
258-
return concatenateArrayBuffer(allChunks);
259-
}
260-
261262
static factoryFromApiResponse(responseIterator: Iterator<string>): CairoByteArray {
262263
const data = Array.from({ length: Number(getNext(responseIterator)) }, () =>
263264
CairoBytes31.factoryFromApiResponse(responseIterator)

src/utils/cairoDataTypes/bytes31.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export class CairoBytes31 {
1414

1515
constructor(data: string | Uint8Array | Buffer | unknown) {
1616
CairoBytes31.validate(data);
17-
this.data = CairoBytes31.__processData(data);
17+
const processedData = CairoBytes31.__processData(data);
18+
this.data = new Uint8Array(CairoBytes31.MAX_BYTE_SIZE); // ensure data has an exact size
19+
this.data.set(processedData, CairoBytes31.MAX_BYTE_SIZE - processedData.length);
1820
}
1921

2022
static __processData(data: Uint8Array | string | Buffer | unknown): Uint8Array {
@@ -39,15 +41,18 @@ export class CairoBytes31 {
3941
}
4042

4143
decodeUtf8() {
42-
return new TextDecoder().decode(this.data);
44+
// strip leading zeros for decode to avoid leading null characters
45+
const cutoff = this.data.findIndex((x) => x > 0);
46+
const pruned = this.data.subarray(cutoff >= 0 ? cutoff : Infinity);
47+
return new TextDecoder().decode(pruned);
4348
}
4449

45-
toHexString() {
46-
// TODO: revisit empty data handling for CairoBytes31 and CairoByteArray
47-
// how to differentiate empty and zero input
48-
const hexValue = this.data.length === 0 ? '0' : buf2hex(this.data);
49-
50-
return addHexPrefix(hexValue);
50+
/**
51+
* @param padded flag for including leading zeros
52+
*/
53+
toHexString(padded?: 'padded') {
54+
const hex = padded === 'padded' ? buf2hex(this.data) : this.toBigInt().toString(16);
55+
return addHexPrefix(hex);
5156
}
5257

5358
static validate(data: Uint8Array | string | Buffer | unknown): void {

src/utils/cairoDataTypes/felt.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ export class CairoFelt252 {
7171

7272
constructor(data: BigNumberish | boolean | unknown) {
7373
CairoFelt252.validate(data);
74-
this.data = CairoFelt252.__processData(data as BigNumberish | boolean);
74+
const processedData = CairoFelt252.__processData(data as BigNumberish | boolean);
75+
// remove leading zeros, ensure data is an exact value/number
76+
this.data = processedData.subarray(processedData.findIndex((x) => x > 0));
7577
}
7678

7779
static __processData(data: BigNumberish | boolean): Uint8Array {

0 commit comments

Comments
 (0)