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

TypedEncoding serialization introduced for the recipients' args #152

Merged
merged 11 commits into from
Oct 18, 2023
14 changes: 6 additions & 8 deletions src/transaction_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import {
toByteArray,
uint8ArrayToHex
} from "./utils.js";
import TE from "./typed_encoding.js"
import { deriveAddress, deriveKeyPair, sign } from "./crypto.js";

const VERSION = 2
const VERSION = 3

function getTransactionTypeId(type: UserTypeTransaction): number {
switch (type) {
Expand Down Expand Up @@ -359,10 +360,7 @@ export default class TransactionBuilder {
// address
address)
} else {
// we need to order object keys ASC because that's what elixir does
const orderedArgs = args.map((arg) => sortObjectKeysASC(arg))
const jsonArgs = JSON.stringify(orderedArgs)
const bufJsonLength = toByteArray(jsonArgs.length)
const serializedArgs = args.map((arg) => TE.serialize(arg))

return concatUint8Arrays(
// 1 = named action
Expand All @@ -372,10 +370,10 @@ export default class TransactionBuilder {
// action
Uint8Array.from([action.length]),
new TextEncoder().encode(action),
// args count
Uint8Array.from([serializedArgs.length]),
// args
Uint8Array.from([bufJsonLength.length]),
bufJsonLength,
new TextEncoder().encode(jsonArgs),
...serializedArgs
)

}
Expand Down
166 changes: 166 additions & 0 deletions src/typed_encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import {
concatUint8Arrays,
toBigInt,
fromBigInt,
sortObjectKeysASC,
deserializeString,
serializeString,
nextUint8
} from "./utils.js"

import VarInt from "./varint.js"

export default {
serialize,
deserialize
}

/**
* Serialize any data
* @param data
* @returns the data encoded
*/
function serialize(data: any, version: number = 1): Uint8Array {
// we need to order object keys ASC because that's what elixir does
data = sortObjectKeysASC(data)

switch (version) {
default:
return do_serialize_v1(data)
}
}
/**
* Deserialize an encoded data
* @param encoded_data
* @returns the data decoded
*/
function deserialize(encoded_data: Uint8Array, version: number = 1): any {
const iter = encoded_data.entries()

switch (version) {
default:
return do_deserialize_v1(iter)
}
}


const TYPE_INT = 0
const TYPE_FLOAT = 1
const TYPE_STR = 2
const TYPE_LIST = 3
const TYPE_MAP = 4
const TYPE_BOOL = 5
const TYPE_NIL = 6


function do_serialize_v1(data: any): Uint8Array {
if (data === null) {
return Uint8Array.from([TYPE_NIL])
} else if (data === true) {
return Uint8Array.from([TYPE_BOOL, 1])
} else if (data === false) {
return Uint8Array.from([TYPE_BOOL, 0])
} else if (Number(data) === data) {
const sign = data >= 0

if (Number.isInteger(data)) {
return concatUint8Arrays(
Uint8Array.from([TYPE_INT]),
Uint8Array.from([sign ? 1 : 0]),
VarInt.serialize(Math.abs(data))
)
} else {
return concatUint8Arrays(
Uint8Array.from([TYPE_FLOAT]),
Uint8Array.from([sign ? 1 : 0]),
VarInt.serialize(toBigInt(Math.abs(data)))
)
}
} else if (typeof data === 'string') {
return concatUint8Arrays(
Uint8Array.from([TYPE_STR]),
VarInt.serialize(byte_size(data)),
serializeString(data)
)
} else if (Array.isArray(data)) {
const serializedItems = data.map((item) => do_serialize_v1(item))
return concatUint8Arrays(
Uint8Array.from([TYPE_LIST]),
VarInt.serialize(data.length),
...serializedItems
)
} else if (typeof data == "object") {
const serializedKeyValues =
Object.keys(data)
.reduce(function (acc: Uint8Array[], key: any) {
acc.push(do_serialize_v1(key))
acc.push(do_serialize_v1(data[key]))
return acc
}, []);

return concatUint8Arrays(
Uint8Array.from([TYPE_MAP]),
VarInt.serialize(Object.keys(data).length),
...serializedKeyValues
)
} else {
throw new Error("Unhandled data type")
}
}

function do_deserialize_v1(iter: IterableIterator<[number, number]>): any {
switch (nextUint8(iter)) {
case TYPE_NIL:
return null

case TYPE_BOOL:
return nextUint8(iter) == 1

case TYPE_INT:
return nextUint8(iter) == 1
? VarInt.deserialize(iter)
: VarInt.deserialize(iter) * -1

case TYPE_FLOAT:
return nextUint8(iter) == 1
? fromBigInt(VarInt.deserialize(iter))
: fromBigInt(VarInt.deserialize(iter) * -1)


case TYPE_STR:
const strLen = VarInt.deserialize(iter)

let bytes = []
for (let i = 0; i < strLen; i++) {
bytes.push(nextUint8(iter))
}

return deserializeString(Uint8Array.from(bytes))

case TYPE_LIST:
const listLen = VarInt.deserialize(iter)

let list = []
for (let i = 0; i < listLen; i++) {
list.push(do_deserialize_v1(iter))
}

return list

case TYPE_MAP:
const keysLen = VarInt.deserialize(iter)

// we use a map here because keys can be of any type
let map = new Map()
for (let i = 0; i < keysLen; i++) {
map.set(do_deserialize_v1(iter), do_deserialize_v1(iter))
}

return Object.fromEntries(map.entries())

}
}

function byte_size(str: string) {
return (new TextEncoder().encode(str)).length
}
59 changes: 53 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export function sortObjectKeysASC(term: any): any {
if (Array.isArray(term))
return term.map((item: any) => sortObjectKeysASC(item))

if (term instanceof Map)
// we can't sort keys of a map
// because the keys aren't strings
// FIXME: this might cause an issue because elixir order & javascript order may differ
return term

// object: sort and map over elements
if (isObject(term))
return Object.keys(term).sort().reduce((newObj: any, key: string) => {
Expand Down Expand Up @@ -206,12 +212,53 @@ export function base64url(arraybuffer: ArrayBuffer): string {
* Convert any number into a byte array
*/
export function toByteArray(number: number): Uint8Array {
if (!number) return Uint8Array.from([0]);
const a = [];
a.unshift(number & 255);
if (number === 0) return Uint8Array.from([0]);

const arr = [];
while (number >= 256) {
number = number >>> 8;
a.unshift(number & 255);
arr.push(number % 256);
number = Math.floor(number / 256);
}
return Uint8Array.from(a);

arr.push(number % 256)

return Uint8Array.from(arr.reverse());
}

/**
* Alias of uint8ArrayToInt
*
* @param bytes
* @returns the number
*/
export function fromByteArray(bytes: Uint8Array): number {
return uint8ArrayToInt(bytes)
}

/**
* Return the next Uint8 from an iterator of Uint8Array
* There is an assumption on success
* @param iter
* @returns
*/
export function nextUint8(iter: IterableIterator<[number, number]>): number {
return iter.next().value[1]
}

/**
* String to Uint8Array
* @param str
* @returns
*/
export function serializeString(str: string): Uint8Array {
return new TextEncoder().encode(str)
}

/**
* Uint8Array to String
* @param str
* @returns
*/
export function deserializeString(encoded_str: Uint8Array): string {
return new TextDecoder().decode(encoded_str)
}
26 changes: 26 additions & 0 deletions src/varint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { toByteArray, concatUint8Arrays, nextUint8, fromByteArray } from "./utils.js"

export default {
serialize,
deserialize
}

function serialize(int: number): Uint8Array {
const buff = toByteArray(int)

return concatUint8Arrays(
Uint8Array.from([buff.length]),
buff
)
}

function deserialize(iter: IterableIterator<[number, number]>): number {
const length = nextUint8(iter)

let bytes = []
for (let i = 0; i < length; i++) {
bytes.push(nextUint8(iter))
}

return fromByteArray(Uint8Array.from(bytes))
}
27 changes: 12 additions & 15 deletions tests/transaction_builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
uint8ArrayToHex,
} from "../src/utils";
import { Curve } from "../src/types";
import TE from "../src/typed_encoding";

const VERSION = 3

// all assert should be transformed to jest expect
describe("Transaction builder", () => {
Expand Down Expand Up @@ -207,7 +210,7 @@ describe("Transaction builder", () => {

const expected_binary = concatUint8Arrays(
//Version
intToUint8Array(2),
intToUint8Array(VERSION),
tx.address,
Uint8Array.from([253]),
//Code size
Expand Down Expand Up @@ -327,7 +330,7 @@ describe("Transaction builder", () => {

const expected_binary = concatUint8Arrays(
//Version
intToUint8Array(2),
intToUint8Array(VERSION),
tx.address,
Uint8Array.from([253]),
//Code size
Expand Down Expand Up @@ -395,13 +398,10 @@ describe("Transaction builder", () => {
Uint8Array.from([14]),
// action value
new TextEncoder().encode("vote_for_mayor"),
// args
// args size bytes
Uint8Array.from([1]),
// args size
Uint8Array.from([13]),
Uint8Array.from([1]),
// args value
new TextEncoder().encode("[\"Ms. Smith\"]"),
TE.serialize("Ms. Smith")
);
expect(payload).toEqual(expected_binary);

Expand All @@ -424,7 +424,7 @@ describe("Transaction builder", () => {

const expected_binary = concatUint8Arrays(
//Version
intToUint8Array(2),
intToUint8Array(VERSION),
tx.address,
Uint8Array.from([253]),
//Code size
Expand Down Expand Up @@ -457,13 +457,10 @@ describe("Transaction builder", () => {
Uint8Array.from([10]),
// action value
new TextEncoder().encode("set_geopos"),
// args
// args size bytes
Uint8Array.from([1]),
// args size
Uint8Array.from([19]),
Uint8Array.from([1]),
// args value
new TextEncoder().encode(`[{"lat":1,"lng":2}]`),
TE.serialize({ "lng": 2, "lat": 1 })
);
expect(payload).toEqual(expected_binary);

Expand Down Expand Up @@ -585,7 +582,7 @@ describe("Transaction builder", () => {
const payload = tx.originSignaturePayload();
const expected_binary = concatUint8Arrays(
//Version
intToUint8Array(2),
intToUint8Array(VERSION),
tx.address,
Uint8Array.from([253]),
//Code size
Expand Down Expand Up @@ -750,7 +747,7 @@ describe("Transaction builder", () => {
const txRPC = tx.toRPC();

// @ts-ignore
expect(txRPC.version).toStrictEqual(2);
expect(txRPC.version).toStrictEqual(VERSION);
// @ts-ignore
expect(txRPC.data.ledger.uco.transfers[0]).toStrictEqual(
{
Expand Down
Loading
Loading