Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Identity and Address to use bigints rather than byte arrays #119

Merged
merged 7 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()),
]);
gefjon marked this conversation as resolved.
Show resolved Hide resolved
}
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