Skip to content

Commit adb8163

Browse files
committed
refactor: allow fixed sized bytes to be created by Bytes factory methods by passing same options parameter
1 parent 1d574c1 commit adb8163

File tree

8 files changed

+215
-84
lines changed

8 files changed

+215
-84
lines changed

examples/voting/contract.algo.spec.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('VotingRoundApp', () => {
1616

1717
const createContract = () => {
1818
const contract = ctx.contract.create(VotingRoundApp)
19-
const snapshotPublicKey = Bytes(keyPair.publicKey).toFixed({ length: 32 })
19+
const snapshotPublicKey = Bytes(keyPair.publicKey, { length: 32 })
2020
const metadataIpfsCid = ctx.any.string(16)
2121
const startTime = ctx.any.uint64(Date.now() - 10_000, Date.now())
2222
const endTime = ctx.any.uint64(Date.now() + 10_000, Date.now() + 100_000)
@@ -52,7 +52,7 @@ describe('VotingRoundApp', () => {
5252
const account = ctx.any.account()
5353
const signature = nacl.sign.detached(toExternalValue(account.bytes), keyPair.secretKey)
5454
ctx.txn.createScope([ctx.any.txn.applicationCall({ sender: account })]).execute(() => {
55-
const preconditions = contract.getPreconditions(Bytes(signature).toFixed({ length: 64 }))
55+
const preconditions = contract.getPreconditions(Bytes(signature, { length: 64 }))
5656

5757
expect(preconditions.is_allowed_to_vote).toEqual(1)
5858
expect(preconditions.is_voting_open).toEqual(1)
@@ -75,11 +75,7 @@ describe('VotingRoundApp', () => {
7575
)
7676

7777
ctx.txn.createScope([ctx.any.txn.applicationCall({ appId: app, sender: account })]).execute(() => {
78-
contract.vote(
79-
ctx.any.txn.payment({ receiver: app.address, amount: voteMinBalanceReq }),
80-
Bytes(signature).toFixed({ length: 64 }),
81-
answerIds,
82-
)
78+
contract.vote(ctx.any.txn.payment({ receiver: app.address, amount: voteMinBalanceReq }), Bytes(signature, { length: 64 }), answerIds)
8379

8480
expect(contract.votesByAccount(account).value.bytes).toEqual(answerIds.bytes)
8581
expect(contract.voterCount.value).toEqual(13)

src/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ export const DEFAULT_GLOBAL_GENESIS_HASH = Bytes(
4545
133, 89, 181, 20, 120, 253, 137, 193, 118, 67, 208, 93, 21, 168, 174, 107, 16, 171, 71, 187, 109, 138, 49, 136, 17, 86, 230, 189, 59,
4646
174, 149, 209,
4747
]),
48-
).toFixed({ length: 32 })
48+
{ length: 32 },
49+
)
4950

5051
/** @internal
5152
* algorand encoded address of 32 zero bytes

src/impl/crypto.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,31 @@ import { Bytes, BytesCls, Uint64Cls } from './primitives'
1616
export const sha256 = (a: StubBytesCompat): bytes<32> => {
1717
const bytesA = BytesCls.fromCompat(a)
1818
const hashArray = js_sha256.sha256.create().update(bytesA.asUint8Array()).digest()
19-
const hashBytes = Bytes(new Uint8Array(hashArray)).toFixed({ length: 32 })
19+
const hashBytes = Bytes(new Uint8Array(hashArray), { length: 32 })
2020
return hashBytes
2121
}
2222

2323
/** @internal */
2424
export const sha3_256 = (a: StubBytesCompat): bytes<32> => {
2525
const bytesA = BytesCls.fromCompat(a)
2626
const hashArray = js_sha3.sha3_256.create().update(bytesA.asUint8Array()).digest()
27-
const hashBytes = Bytes(new Uint8Array(hashArray)).toFixed({ length: 32 })
27+
const hashBytes = Bytes(new Uint8Array(hashArray), { length: 32 })
2828
return hashBytes
2929
}
3030

3131
/** @internal */
3232
export const keccak256 = (a: StubBytesCompat): bytes<32> => {
3333
const bytesA = BytesCls.fromCompat(a)
3434
const hashArray = js_sha3.keccak256.create().update(bytesA.asUint8Array()).digest()
35-
const hashBytes = Bytes(new Uint8Array(hashArray)).toFixed({ length: 32 })
35+
const hashBytes = Bytes(new Uint8Array(hashArray), { length: 32 })
3636
return hashBytes
3737
}
3838

3939
/** @internal */
4040
export const sha512_256 = (a: StubBytesCompat): bytes<32> => {
4141
const bytesA = BytesCls.fromCompat(a)
4242
const hashArray = js_sha512.sha512_256.create().update(bytesA.asUint8Array()).digest()
43-
const hashBytes = Bytes(new Uint8Array(hashArray)).toFixed({ length: 32 })
43+
const hashBytes = Bytes(new Uint8Array(hashArray), { length: 32 })
4444
return hashBytes
4545
}
4646

@@ -114,7 +114,7 @@ export const ecdsaPkRecover = (
114114

115115
const x = pubKey.getX().toArray('be')
116116
const y = pubKey.getY().toArray('be')
117-
return [Bytes(x).toFixed({ length: 32 }), Bytes(y).toFixed({ length: 32 })]
117+
return [Bytes(x, { length: 32 }), Bytes(y, { length: 32 })]
118118
}
119119

120120
/** @internal */
@@ -127,7 +127,7 @@ export const ecdsaPkDecompress = (v: Ecdsa, a: StubBytesCompat): readonly [bytes
127127

128128
const x = pubKey.getX().toArray('be')
129129
const y = pubKey.getY().toArray('be')
130-
return [Bytes(x).toFixed({ length: 32 }), Bytes(y).toFixed({ length: 32 })]
130+
return [Bytes(x, { length: 32 }), Bytes(y, { length: 32 })]
131131
}
132132

133133
/** @internal */

src/impl/primitives.ts

Lines changed: 175 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -87,89 +87,223 @@ export function BigUint(v?: BigUintCompat | string): biguint {
8787
return BigUintCls.fromCompat(v).asAlgoTs()
8888
}
8989

90+
type ToFixedBytesOptions<TLength extends uint64 = uint64> = {
91+
/**
92+
* The length for the bounded type
93+
*/
94+
length: TLength
95+
/**
96+
* The strategy to use for converting to a fixed length bytes type (default: 'assert-length')
97+
*
98+
* - 'assert-length': Asserts that the byte sequence has the specified length and fails if it differs
99+
* - 'unsafe-cast': Reinterprets the byte sequence as a fixed length type without any checks. This will succeed even if the value
100+
* is not of the specified length but will result in undefined behaviour for any code that makes use of this value.
101+
*
102+
*/
103+
strategy?: 'assert-length' | 'unsafe-cast'
104+
}
105+
90106
/**
91-
* @internal
92107
* Create a byte array from a string interpolation template and compatible replacements
93108
* @param value
94109
* @param replacements
95110
*/
96-
export function Bytes(value: TemplateStringsArray, ...replacements: BytesCompat[]): bytes
111+
export function Bytes(value: TemplateStringsArray, ...replacements: BytesCompat[]): bytes<uint64>
97112
/**
98-
* @internal
99113
* Create a byte array from a utf8 string
100114
*/
101-
export function Bytes(value: string): bytes
115+
export function Bytes(value: string): bytes<uint64>
116+
/**
117+
* Create a byte array from a utf8 string
118+
*/
119+
export function Bytes<TLength extends uint64>(value: string, options: ToFixedBytesOptions<TLength>): bytes<TLength>
102120
/**
103-
* @internal
104121
* No op, returns the provided byte array.
105122
*/
106-
export function Bytes(value: bytes): bytes
123+
export function Bytes(value: bytes): bytes<uint64>
124+
/**
125+
* No op, returns the provided byte array.
126+
*/
127+
export function Bytes<TLength extends uint64>(value: bytes, options: ToFixedBytesOptions<TLength>): bytes<TLength>
107128
/**
108-
* @internal
109129
* Create a byte array from a biguint value encoded as a variable length big-endian number
110130
*/
111-
export function Bytes(value: biguint): bytes
131+
export function Bytes(value: biguint): bytes<uint64>
112132
/**
113-
* @internal
114-
* Create a byte array from a uint64 value encoded as a fixed length 64-bit number
133+
* Create a byte array from a biguint value encoded as a variable length big-endian number
115134
*/
116-
export function Bytes(value: uint64): bytes
135+
export function Bytes<TLength extends uint64>(value: biguint, options: ToFixedBytesOptions<TLength>): bytes<TLength>
136+
/**
137+
* Create a byte array from a uint64 value encoded as a a variable length 64-bit number
138+
*/
139+
export function Bytes(value: uint64): bytes<uint64>
140+
/**
141+
* Create a byte array from a uint64 value encoded as a a variable length 64-bit number
142+
*/
143+
export function Bytes<TLength extends uint64 = 8>(value: uint64, options: ToFixedBytesOptions<TLength>): bytes<TLength>
117144
/**
118-
* @internal
119145
* Create a byte array from an Iterable<uint64> where each item is interpreted as a single byte and must be between 0 and 255 inclusively
120146
*/
121-
export function Bytes(value: Iterable<uint64>): bytes
147+
export function Bytes(value: Iterable<uint64>): bytes<uint64>
148+
/**
149+
* Create a byte array from an Iterable<uint64> where each item is interpreted as a single byte and must be between 0 and 255 inclusively
150+
*/
151+
export function Bytes<TLength extends uint64>(value: Iterable<uint64>, options: ToFixedBytesOptions<TLength>): bytes<TLength>
122152
/**
123-
* @internal
124153
* Create an empty byte array
125154
*/
126-
export function Bytes(): bytes
127-
export function Bytes(
128-
value?: BytesCompat | TemplateStringsArray | biguint | uint64 | Iterable<number>,
129-
...replacements: BytesCompat[]
130-
): bytes {
131-
if (isTemplateStringsArray(value)) {
132-
return BytesCls.fromInterpolation(value, replacements).asAlgoTs()
133-
} else if (typeof value === 'bigint' || value instanceof BigUintCls) {
134-
return BigUintCls.fromCompat(value).toBytes().asAlgoTs()
135-
} else if (typeof value === 'number' || value instanceof Uint64Cls) {
136-
return Uint64Cls.fromCompat(value).toBytes().asAlgoTs()
137-
} else if (typeof value === 'object' && Symbol.iterator in value) {
138-
const valueItems = Array.from(value).map((v) => getNumber(v))
139-
const invalidValue = valueItems.find((v) => v < 0 && v > 255)
140-
if (invalidValue) {
141-
throw new CodeError(`Cannot convert ${invalidValue} to a byte`)
142-
}
143-
return new BytesCls(new Uint8Array(value)).asAlgoTs()
144-
} else {
145-
return BytesCls.fromCompat(value).asAlgoTs()
146-
}
155+
export function Bytes(): bytes<uint64>
156+
/**
157+
* Create an empty byte array
158+
*/
159+
export function Bytes<TLength extends uint64 = uint64>(options: ToFixedBytesOptions<TLength>): bytes<TLength>
160+
export function Bytes<TLength extends uint64 = uint64>(
161+
value?: BytesCompat | TemplateStringsArray | biguint | uint64 | Iterable<number> | ToFixedBytesOptions<TLength>,
162+
...replacements: [ToFixedBytesOptions<TLength>] | BytesCompat[] | undefined[]
163+
): bytes<TLength> {
164+
// Handle the case where only options are provided (empty bytes with fixed length)
165+
if (isOptionsOnly(value)) {
166+
const options = value as ToFixedBytesOptions<TLength>
167+
const emptyBytes = new BytesCls(new Uint8Array(options.length))
168+
return emptyBytes.toFixed(options)
169+
}
170+
171+
// Convert the input value to a BytesCls instance
172+
const result = convertValueToBytes(value, replacements)
173+
174+
// Extract options from replacements if provided
175+
const options = isTemplateStringsArray(value) ? undefined : extractOptionsFromReplacements(replacements)
176+
177+
// Return either fixed-length or variable-length bytes
178+
return options ? result.toFixed(options) : (result.asAlgoTs() as bytes<TLength>)
147179
}
148180

149181
/**
150182
* @internal
151183
* Create a new bytes value from a hexadecimal encoded string
152184
* @param hex
153185
*/
154-
Bytes.fromHex = (hex: string): bytes => {
155-
return BytesCls.fromHex(hex).asAlgoTs()
186+
Bytes.fromHex = <TLength extends uint64 = uint64>(hex: string, options?: ToFixedBytesOptions<TLength>): bytes<TLength> => {
187+
return options ? BytesCls.fromHex(hex).toFixed(options) : (BytesCls.fromHex(hex).asAlgoTs() as bytes<TLength>)
156188
}
157189
/**
158190
* @internal
159191
* Create a new bytes value from a base 64 encoded string
160192
* @param b64
161193
*/
162-
Bytes.fromBase64 = (b64: string): bytes => {
163-
return BytesCls.fromBase64(b64).asAlgoTs()
194+
Bytes.fromBase64 = <TLength extends uint64 = uint64>(b64: string, options?: ToFixedBytesOptions<TLength>): bytes<TLength> => {
195+
return options ? BytesCls.fromBase64(b64).toFixed(options) : (BytesCls.fromBase64(b64).asAlgoTs() as bytes<TLength>)
164196
}
165197

166198
/**
167199
* @internal
168200
* Create a new bytes value from a base 32 encoded string
169201
* @param b32
170202
*/
171-
Bytes.fromBase32 = (b32: string): bytes => {
172-
return BytesCls.fromBase32(b32).asAlgoTs()
203+
Bytes.fromBase32 = <TLength extends uint64 = uint64>(b32: string, options?: ToFixedBytesOptions<TLength>): bytes<TLength> => {
204+
return options ? BytesCls.fromBase32(b32).toFixed(options) : (BytesCls.fromBase32(b32).asAlgoTs() as bytes<TLength>)
205+
}
206+
207+
/**
208+
* Helper function to check if the value parameter is options-only (for empty bytes with fixed length)
209+
*/
210+
function isOptionsOnly<TLength extends uint64>(
211+
value?: BytesCompat | TemplateStringsArray | biguint | uint64 | Iterable<number> | ToFixedBytesOptions<TLength>,
212+
): value is ToFixedBytesOptions<TLength> {
213+
return (
214+
value !== null &&
215+
typeof value === 'object' &&
216+
!Array.isArray(value) &&
217+
!isTemplateStringsArray(value) &&
218+
!(Symbol.iterator in value) &&
219+
!(value instanceof BigUintCls) &&
220+
!(value instanceof Uint64Cls) &&
221+
!(value instanceof BytesCls) &&
222+
!(value instanceof Uint8Array) &&
223+
Object.keys(value).length <= 2 &&
224+
Object.keys(value).includes('length')
225+
)
226+
}
227+
228+
/**
229+
* Helper function to convert various input types to BytesCls
230+
*/
231+
function convertValueToBytes<TLength extends uint64>(
232+
value?: BytesCompat | TemplateStringsArray | biguint | uint64 | Iterable<number> | ToFixedBytesOptions<TLength>,
233+
replacements?: [ToFixedBytesOptions<TLength>] | BytesCompat[] | undefined[],
234+
): BytesCls {
235+
if (value === undefined) {
236+
return new BytesCls(new Uint8Array(0))
237+
}
238+
239+
if (isTemplateStringsArray(value)) {
240+
return BytesCls.fromInterpolation(value, replacements as BytesCompat[])
241+
}
242+
243+
if (typeof value === 'bigint' || value instanceof BigUintCls) {
244+
return BigUintCls.fromCompat(value).toBytes()
245+
}
246+
247+
if (typeof value === 'number' || value instanceof Uint64Cls) {
248+
return Uint64Cls.fromCompat(value).toBytes()
249+
}
250+
251+
if (isIterable(value)) {
252+
return convertIterableToBytes(value)
253+
}
254+
255+
// Default case: treat as BytesCompat
256+
return BytesCls.fromCompat(value as BytesCompat)
257+
}
258+
259+
/**
260+
* Helper function to check if a value is iterable (but not string or Uint8Array)
261+
*/
262+
function isIterable(value: unknown): value is Iterable<number> {
263+
return (
264+
value !== null &&
265+
typeof value === 'object' &&
266+
Symbol.iterator in value &&
267+
!isTemplateStringsArray(value) &&
268+
!(value instanceof Uint8Array) &&
269+
!(value instanceof BytesCls)
270+
)
271+
}
272+
273+
/**
274+
* Helper function to convert an iterable of numbers to BytesCls
275+
*/
276+
function convertIterableToBytes(value: Iterable<number>): BytesCls {
277+
const valueItems = Array.from(value).map((v) => getNumber(v))
278+
const invalidValue = valueItems.find((v) => v < 0 || v > 255)
279+
if (invalidValue !== undefined) {
280+
throw new CodeError(`Cannot convert ${invalidValue} to a byte`)
281+
}
282+
return new BytesCls(new Uint8Array(valueItems))
283+
}
284+
285+
/**
286+
* Helper function to extract options from the replacements parameter
287+
*/
288+
function extractOptionsFromReplacements<TLength extends uint64>(
289+
replacements: [ToFixedBytesOptions<TLength>] | BytesCompat[] | undefined[],
290+
): ToFixedBytesOptions<TLength> | undefined {
291+
if (!replacements || replacements.length !== 1) {
292+
return undefined
293+
}
294+
295+
const potentialOptions = replacements[0]
296+
// Check if the replacement looks like options
297+
if (
298+
typeof potentialOptions === 'object' &&
299+
potentialOptions !== null &&
300+
Object.keys(potentialOptions).length <= 2 &&
301+
Object.keys(potentialOptions).includes('length')
302+
) {
303+
return potentialOptions as ToFixedBytesOptions<TLength>
304+
}
305+
306+
return undefined
173307
}
174308

175309
/**

tests/artifacts/avm12/contract.algo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
@contract({ avmVersion: 12 })
1515
export class Avm12Contract extends Contract {
1616
testFalconVerify() {
17-
assert(!op.falconVerify(Bytes(), Bytes(), op.bzero(1793).toFixed({ length: 1793 })))
17+
assert(!op.falconVerify(Bytes(), Bytes(), op.bzero(1793)))
1818
}
1919

2020
testRejectVersion() {

0 commit comments

Comments
 (0)