Skip to content

Commit

Permalink
Update Identity and Address to use bigints rather than byte arrays (#119
Browse files Browse the repository at this point in the history
)

* Update Identity and Address to use bigints rather than byte arrays

* Add back accidentally-removed APIs

* Remove debugging log

* Add changeset

* Fix names

* Format

* Update creds

---------

Co-authored-by: Puru Vijay <[email protected]>
  • Loading branch information
kazimuth and PuruVJ authored Nov 4, 2024
1 parent 5d7304b commit 6547882
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 84 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-cats-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clockworklabs/spacetimedb-sdk': minor
---

Update Identity and Address to use bigints rather than byte arrays (see https://github.com/clockworklabs/SpacetimeDB/pull/1616)
8 changes: 8 additions & 0 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,11 @@ Given a reducer called `CreatePlayer` you can call it using a call method:
```ts
connection.reducers.createPlayer();
```

### Developer notes

To run the tests, do:

```sh
pnpm compile && pnpm test
```
47 changes: 19 additions & 28 deletions packages/sdk/src/address.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { hexStringToU128, u128ToHexString, u128ToUint8Array } from './utils';

/**
* A unique identifier for a client connected to a database.
*/
export class Address {
data: Uint8Array;
data: bigint;

get __address_bytes(): Uint8Array {
return this.toUint8Array();
get __address__(): bigint {
return this.data;
}

/**
* Creates a new `Address`.
*/
constructor(data: Uint8Array) {
constructor(data: bigint) {
this.data = data;
}

isZero(): boolean {
return this.data.every(b => b == 0);
return this.data === BigInt(0);
}

static nullIfZero(addr: Address): Address | null {
Expand All @@ -28,53 +30,42 @@ export class Address {
}

static random(): Address {
function randomByte(): number {
return Math.floor(Math.random() * 255);
function randomU8(): number {
return Math.floor(Math.random() * 0xff);
}
let data = new Uint8Array(16);
let result = BigInt(0);
for (let i = 0; i < 16; i++) {
data[i] = randomByte();
result = (result << BigInt(8)) | BigInt(randomU8());
}
return new Address(data);
return new Address(result);
}

/**
* Compare two addresses for equality.
*/
isEqual(other: Address): boolean {
if (this.data.length !== other.data.length) {
return false;
}
for (let i = 0; i < this.data.length; i++) {
if (this.data[i] !== other.data[i]) {
return false;
}
}
return true;
return this.data == other.data;
}

/**
* Print the address as a hexadecimal string.
*/
toHexString(): string {
return Array.prototype.map
.call(this.data, x => ('00' + x.toString(16)).slice(-2))
.join('');
return u128ToHexString(this.data);
}

/**
* Convert the address to a Uint8Array.
*/
toUint8Array(): Uint8Array {
return this.data;
return u128ToUint8Array(this.data);
}

/**
* Parse an Address from a hexadecimal string.
*/
static fromString(str: string): Address {
let matches = str.match(/.{1,2}/g) || [];
let data = Uint8Array.from(
matches.map((byte: string) => parseInt(byte, 16))
);
return new Address(data);
return new Address(hexStringToU128(str));
}

static fromStringOrNull(str: string): Address | null {
Expand Down
33 changes: 24 additions & 9 deletions packages/sdk/src/algebraic_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,12 @@ export class ProductType {
deserialize = (reader: BinaryReader): object => {
let result: { [key: string]: any } = {};
if (this.elements.length === 1) {
if (this.elements[0].name === '__identity_bytes') {
return new Identity(reader.readUInt8Array());
if (this.elements[0].name === '__identity__') {
return new Identity(reader.readU256());
}

if (this.elements[0].name === '__address_bytes') {
return new Address(reader.readUInt8Array());
if (this.elements[0].name === '__address__') {
return new Address(reader.readU128());
}
}

Expand Down Expand Up @@ -318,6 +318,12 @@ export class AlgebraicType {
static createU128Type(): AlgebraicType {
return this.#createType(Type.U128, null);
}
static createI256Type(): AlgebraicType {
return this.#createType(Type.I256, null);
}
static createU256Type(): AlgebraicType {
return this.#createType(Type.U256, null);
}
static createF32Type(): AlgebraicType {
return this.#createType(Type.F32, null);
}
Expand All @@ -338,12 +344,12 @@ export class AlgebraicType {
}
static createIdentityType(): AlgebraicType {
return this.createProductType([
new ProductTypeElement('__identity_bytes', this.createBytesType()),
new ProductTypeElement('__identity__', this.createU256Type()),
]);
}
static createAddressType(): AlgebraicType {
return this.createProductType([
new ProductTypeElement('__address_bytes', this.createBytesType()),
new ProductTypeElement('__address__', this.createU128Type()),
]);
}
static createScheduleAtType(): AlgebraicType {
Expand Down Expand Up @@ -377,17 +383,18 @@ export class AlgebraicType {
return (
this.isProductType() &&
this.product.elements.length === 1 &&
this.product.elements[0].algebraicType.#isBytes() &&
(this.product.elements[0].algebraicType.type == Type.U128 ||
this.product.elements[0].algebraicType.type == Type.U256) &&
this.product.elements[0].name === tag
);
}

isIdentity(): boolean {
return this.#isBytesNewtype('__identity_bytes');
return this.#isBytesNewtype('__identity__');
}

isAddress(): boolean {
return this.#isBytesNewtype('__address_bytes');
return this.#isBytesNewtype('__address__');
}

serialize(writer: BinaryWriter, value: any): void {
Expand Down Expand Up @@ -444,6 +451,12 @@ export class AlgebraicType {
case Type.U128:
writer.writeU128(value);
break;
case Type.I256:
writer.writeI256(value);
break;
case Type.U256:
writer.writeU256(value);
break;
case Type.F32:
writer.writeF32(value);
break;
Expand Down Expand Up @@ -530,6 +543,8 @@ export namespace AlgebraicType {
U64 = 'U64',
I128 = 'I128',
U128 = 'U128',
I256 = 'I256',
U256 = 'U256',
F32 = 'F32',
F64 = 'F64',
/** UTF-8 encoded */
Expand Down
7 changes: 5 additions & 2 deletions packages/sdk/src/algebraic_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ export class AlgebraicValue {
}
}

// TODO: all of the following methods should actually check the type of `self.value`
// and throw if it does not match.

asProductValue(): ProductValue {
return this.value as ProductValue;
}
Expand Down Expand Up @@ -305,11 +308,11 @@ export class AlgebraicValue {
}

asIdentity(): Identity {
return new Identity(this.asField(0).asBytes());
return new Identity(this.asField(0).asBigInt());
}

asAddress(): Address {
return new Address(this.asField(0).asBytes());
return new Address(this.asField(0).asBigInt());
}
}

Expand Down
32 changes: 31 additions & 1 deletion packages/sdk/src/binary_reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,43 @@ export default class BinaryReader {
}

readI128(): bigint {
const lowerPart = this.#buffer.getBigInt64(this.#offset, true);
const lowerPart = this.#buffer.getBigUint64(this.#offset, true);
const upperPart = this.#buffer.getBigInt64(this.#offset + 8, true);
this.#offset += 16;

return (upperPart << BigInt(64)) + lowerPart;
}

readU256(): bigint {
const p0 = this.#buffer.getBigUint64(this.#offset, true);
const p1 = this.#buffer.getBigUint64(this.#offset + 8, true);
const p2 = this.#buffer.getBigUint64(this.#offset + 16, true);
const p3 = this.#buffer.getBigUint64(this.#offset + 24, true);
this.#offset += 32;

return (
(p3 << BigInt(3 * 64)) +
(p2 << BigInt(2 * 64)) +
(p1 << BigInt(1 * 64)) +
p0
);
}

readI256(): bigint {
const p0 = this.#buffer.getBigUint64(this.#offset, true);
const p1 = this.#buffer.getBigUint64(this.#offset + 8, true);
const p2 = this.#buffer.getBigUint64(this.#offset + 16, true);
const p3 = this.#buffer.getBigInt64(this.#offset + 24, true);
this.#offset += 32;

return (
(p3 << BigInt(3 * 64)) +
(p2 << BigInt(2 * 64)) +
(p1 << BigInt(1 * 64)) +
p0
);
}

readF32(): number {
const value = this.#buffer.getFloat32(this.#offset, true);
this.#offset += 4;
Expand Down
28 changes: 28 additions & 0 deletions packages/sdk/src/binary_writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,34 @@ export default class BinaryWriter {
this.#offset += 16;
}

writeU256(value: bigint): void {
this.#expandBuffer(32);
const low_64_mask = BigInt('0xFFFFFFFFFFFFFFFF');
const p0 = value & low_64_mask;
const p1 = (value >> BigInt(64 * 1)) & low_64_mask;
const p2 = (value >> BigInt(64 * 2)) & low_64_mask;
const p3 = value >> BigInt(64 * 3);
this.#view.setBigUint64(this.#offset + 8 * 0, p0, true);
this.#view.setBigUint64(this.#offset + 8 * 1, p1, true);
this.#view.setBigUint64(this.#offset + 8 * 2, p2, true);
this.#view.setBigUint64(this.#offset + 8 * 3, p3, true);
this.#offset += 32;
}

writeI256(value: bigint): void {
this.#expandBuffer(32);
const low_64_mask = BigInt('0xFFFFFFFFFFFFFFFF');
const p0 = value & low_64_mask;
const p1 = (value >> BigInt(64 * 1)) & low_64_mask;
const p2 = (value >> BigInt(64 * 2)) & low_64_mask;
const p3 = value >> BigInt(64 * 3);
this.#view.setBigUint64(this.#offset + 8 * 0, p0, true);
this.#view.setBigUint64(this.#offset + 8 * 1, p1, true);
this.#view.setBigUint64(this.#offset + 8 * 2, p2, true);
this.#view.setBigInt64(this.#offset + 8 * 3, p3, true);
this.#offset += 32;
}

writeF32(value: number): void {
this.#expandBuffer(4);
this.#view.setFloat32(this.#offset, value, true);
Expand Down
42 changes: 17 additions & 25 deletions packages/sdk/src/identity.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,26 @@
// Helper function convert from string to Uint8Array
function hexStringToUint8Array(str: string): Uint8Array {
let matches = str.match(/.{1,2}/g) || [];
let data = Uint8Array.from(matches.map((byte: string) => parseInt(byte, 16)));
return data;
}

// Helper function for converting Uint8Array to hex string
function uint8ArrayToHexString(array: Uint8Array): string {
return Array.prototype.map
.call(array, x => ('00' + x.toString(16)).slice(-2))
.join('');
}
import BinaryReader from './binary_reader';
import BinaryWriter from './binary_writer';
import { hexStringToU256, u256ToHexString, u256ToUint8Array } from './utils';

/**
* A unique identifier for a user connected to a database.
*/
export class Identity {
data: Uint8Array;
data: bigint;

get __identity_bytes(): Uint8Array {
return this.toUint8Array();
get __identity__(): bigint {
return this.data;
}

/**
* Creates a new `Identity`.
*
* `data` can be a hexadecimal string or a `bigint`.
*/
constructor(data: string | Uint8Array) {
// we get a JSON with __identity_bytes when getting a token with a JSON API
// and an Uint8Array when using BSATN
this.data =
data.constructor === Uint8Array
? data
: hexStringToUint8Array(data as string);
constructor(data: string | bigint) {
// we get a JSON with __identity__ when getting a token with a JSON API
// and an bigint when using BSATN
this.data = typeof data === 'string' ? hexStringToU256(data) : data;
}

/**
Expand All @@ -45,11 +34,14 @@ export class Identity {
* Print the identity as a hexadecimal string.
*/
toHexString(): string {
return uint8ArrayToHexString(this.data);
return u256ToHexString(this.data);
}

/**
* Convert the address to a Uint8Array.
*/
toUint8Array(): Uint8Array {
return this.data;
return u256ToUint8Array(this.data);
}

/**
Expand Down
Loading

0 comments on commit 6547882

Please sign in to comment.