Skip to content

Enforce byte length from concatenated string #514

@smonn

Description

@smonn

Reporting in case this is a bug, but guessing it's just not possible or could be done in a different way:

import { Contract } from '@algorandfoundation/tealscript';

export class Simple extends Contract {
  private int_to_ascii(value: uint64): string {
    return extract3('0123456789', value, 1);
  }

  private itoa(value: uint256): string {
    if (value === 0) return '0';
    return concat(value / 10 > <uint256>0 ? this.itoa(value / 10) : '', this.int_to_ascii(<uint64>(value % 10)));
  }

  test(): bytes<256> {
    return ('hello : ' + this.itoa(1337 as uint256)) as bytes<256>;
  }
}

Getting this when I build it:

Error: Bad Request: 139:8: extract s beyond 255: 256

The goal is to have test()bytes<256> return a "clamped" string at 256 bytes.

It's a bit of a convoluted example. For context, I'm messing around with the ARC-72 example and trying to get the URI to include the index/token ID as a suffix. So yes, I'm aware the above example works if bytes<256> is replaced with string. Due to the ARC-72 spec, even if I compute this in arc72_tokenURI I'd still run into this error as it also must return bytes<256>.

As a side-note, would be great if that itoa utility was included out-of-the-box, I believe there's a PyTeal utillity equivalent?

Let me know if I can provide anything else. Thanks!


Update:

This sort of works:

import { Contract } from '@algorandfoundation/tealscript';

type Bytes256 = bytes<256>;

export class Simple extends Contract {
  /**
   * Map single-digit integer to its string representation.
   */
  private digitToASCII(value: uint64): string {
    return extract3('0123456789', value, 1);
  }

  /**
   * Convert a number to its human-readable string representation.
   */
  private numberToString(value: uint256): string {
    if (value === 0) return '0';

    const prefix = value / 10 > <uint256>0 ? this.numberToString(value / 10) : '';
    const suffix = this.digitToASCII(<uint64>(value % 10));

    return concat(prefix, suffix);
  }

  /**
   * Concat string with zero bytes to achieve 256 byte length
   */
  private stringToBytes256(value: string): Bytes256 {
    return castBytes<Bytes256>(value + bzero(256 - len(value)));
  }

  test(): bytes<256> {
    return this.stringToBytes256(this.numberToString(<uint256>1337));
  }
}

The downside with this approach is that the output, once decoded, is a string with null-terminators. This is probably fine for most scenarios though, just need to be mindful in JS due to how it handles '\x00' when in a string. Most systems will not render that part.

/** Utility to decode a zero-byte padded bytes[256] to a string */
function decodeBytes256ToString(bytes: Uint8Array): string {
  return new TextDecoder().decode(new Uint8Array(bytes.filter((n) => n !== 0)));
}

From what I can tell, the core issue is that casting to bytes<256> on a plain string works, e.g.:

'some string' as bytes<256>

But casting a string value, like after doing some concatenation, does not work, e.g.:

('hello' + ' ' + 'world') as bytes<256> // errors with "extract l beyond 255: 256"
output

With concatenation:

192: test2:
193: 	proto 0 1
194:
195: 	// contracts/Simple.algo.ts:38
196: 	// return ('hello' + ' ' + 'world') as bytes<256>;
197: 	byte 0x68656c6c6f20776f726c6400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
198: 	byte 0x
199: 	dup
200: 	b==
201: 	assert
202: 	extract 0 256
203: 	retsub

Without:

test2:
	proto 0 1

	// contracts/Simple.algo.ts:37
	// return 'hello world' as bytes<256>;
	byte 0x68656c6c6f20776f726c640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 // "hello world"
	retsub

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions