580 lines
20 KiB
TypeScript
580 lines
20 KiB
TypeScript
|
import { decodeAddress, encodeAddress, isValidAddress } from 'algosdk'
|
||
|
import sha512 from "js-sha512"
|
||
|
import { Buffer } from 'buffer'
|
||
|
import { Address } from './AlgorandTypes'
|
||
|
import assert from 'assert'
|
||
|
import { ethers } from 'ethers'
|
||
|
|
||
|
export const TEAL_SIGNATURE_LENGTH = 64
|
||
|
export const SHA256_HASH_LENGTH = 32
|
||
|
|
||
|
export type AlgorandType = bigint | string | boolean | number | Uint8Array
|
||
|
|
||
|
export type IPackedInfoFixed = {
|
||
|
type: "uint" | "number" | "address" | "double" | "boolean" | "emptyString"
|
||
|
}
|
||
|
export type IPackedInfoVariable = {
|
||
|
type: "string" | "bytes" | "base64"
|
||
|
size: number
|
||
|
}
|
||
|
export type IPackedInfoObject = {
|
||
|
type: "object" | "hash"
|
||
|
info: IPackedInfo
|
||
|
}
|
||
|
|
||
|
export type IPackedInfoArray = {
|
||
|
type: "array"
|
||
|
info: IPackedInfoAny
|
||
|
}
|
||
|
|
||
|
export type IPackedInfoFixedBytes = {
|
||
|
type: "fixed"
|
||
|
valueHex: string
|
||
|
}
|
||
|
|
||
|
export type IPackedInfoAny = IPackedInfoFixed | IPackedInfoVariable | IPackedInfoObject | IPackedInfoArray | IPackedInfoFixedBytes
|
||
|
export type IPackedInfo = Record<string, IPackedInfoAny>
|
||
|
|
||
|
export type IStateType = 'uint' | 'bytes'
|
||
|
export type IStateMap = Record<string, IStateType>
|
||
|
|
||
|
export type IStateVar = Uint8Array | bigint
|
||
|
export type IState = Record<string, IStateVar>
|
||
|
|
||
|
// NOTE: !!!! ONLY MODIFY THIS BY APPENDING TO THE END. THE INDEXES EFFECT THE MERKLE LOG HASH VALUES !!!!
|
||
|
export const packedTypeMap = [
|
||
|
"uint",
|
||
|
"number",
|
||
|
"address",
|
||
|
"double",
|
||
|
"boolean",
|
||
|
"string",
|
||
|
"bytes",
|
||
|
"base64",
|
||
|
"object",
|
||
|
"hash",
|
||
|
"array",
|
||
|
"emptyString",
|
||
|
"fixed",
|
||
|
]
|
||
|
|
||
|
assert(packedTypeMap.length < 128, 'Too many types in packedTypeMap')
|
||
|
|
||
|
export function concatArrays(arrays: Uint8Array[]): Uint8Array {
|
||
|
const totalLength = arrays.reduce((accum, x) => accum + x.length, 0)
|
||
|
const result = new Uint8Array(totalLength)
|
||
|
|
||
|
for (let i = 0, offset = 0; i < arrays.length; i++) {
|
||
|
result.set(arrays[i], offset)
|
||
|
offset += arrays[i].length
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
// Encode the format itself as part of the data for forward compatibility
|
||
|
export function packFormat(format: IPackedInfo): Uint8Array {
|
||
|
const chunks: Uint8Array[] = []
|
||
|
|
||
|
// NOTE: Byte-size fields are capped at 128 to allow for future expansion with varints
|
||
|
// Encode number of fields
|
||
|
const fieldCount = Object.entries(format).length
|
||
|
assert(fieldCount < 128, `Too many fields in object: ${fieldCount}`)
|
||
|
chunks.push(new Uint8Array([fieldCount]))
|
||
|
|
||
|
for (const [name, type] of Object.entries(format)) {
|
||
|
// Encode name and type index
|
||
|
assert(name.length < 128, `Name of property ${name} too long`)
|
||
|
chunks.push(new Uint8Array([name.length]))
|
||
|
chunks.push(encodeString(name))
|
||
|
|
||
|
const typeIndex = packedTypeMap.indexOf(type.type)
|
||
|
assert(typeIndex >= 0, 'Type index not found in packedTypeMap')
|
||
|
|
||
|
chunks.push(new Uint8Array([typeIndex]))
|
||
|
|
||
|
// For complex types, encode additional data
|
||
|
switch (type.type) {
|
||
|
case "string":
|
||
|
case "bytes":
|
||
|
case "base64":
|
||
|
assert(type.size < 128, `Sized data was too large: ${type.size}`)
|
||
|
chunks.push(new Uint8Array([type.size]))
|
||
|
break
|
||
|
|
||
|
case "hash":
|
||
|
case "object":
|
||
|
case "array": {
|
||
|
const format = packFormat(type.type === 'array' ? { value: type.info } : type.info)
|
||
|
chunks.push(encodeUint64(format.length))
|
||
|
chunks.push(format)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return concatArrays(chunks)
|
||
|
}
|
||
|
|
||
|
export function unpackFormat(data: Uint8Array): IPackedInfo {
|
||
|
let index = 0
|
||
|
// Decode field count
|
||
|
const fieldCount = data[index]
|
||
|
index++
|
||
|
|
||
|
const format: IPackedInfo = {}
|
||
|
for (let i = 0; i < fieldCount; i++) {
|
||
|
// Decode name
|
||
|
const nameLen = data[index]
|
||
|
index++
|
||
|
|
||
|
const name = decodeString(data.slice(index, index + nameLen))
|
||
|
index += nameLen
|
||
|
|
||
|
// Decode type
|
||
|
const type = packedTypeMap[data[index]]
|
||
|
index++
|
||
|
|
||
|
switch (type) {
|
||
|
case "uint":
|
||
|
case "number":
|
||
|
case "address":
|
||
|
case "double":
|
||
|
case "boolean":
|
||
|
case "emptyString":
|
||
|
format[name] = { type }
|
||
|
break
|
||
|
|
||
|
case "string":
|
||
|
case "bytes":
|
||
|
case "base64": {
|
||
|
const size = data[index]
|
||
|
index++
|
||
|
|
||
|
format[name] = { type, size }
|
||
|
break
|
||
|
}
|
||
|
|
||
|
case "object":
|
||
|
case "hash":
|
||
|
case "array": {
|
||
|
const length = Number(decodeUint64(data.slice(index, index + 8)))
|
||
|
index += 8
|
||
|
|
||
|
const info = unpackFormat(data.slice(index, index + length))
|
||
|
index += length
|
||
|
|
||
|
if (type === "array") {
|
||
|
format[name] = { type, info: info.value }
|
||
|
} else {
|
||
|
format[name] = { type, info }
|
||
|
}
|
||
|
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return format
|
||
|
}
|
||
|
|
||
|
export function packData(value: Record<string, any>, format: IPackedInfo, includeType = false): Uint8Array {
|
||
|
const chunks: Uint8Array[] = []
|
||
|
|
||
|
if (includeType) {
|
||
|
const packedFormat = packFormat(format)
|
||
|
chunks.push(encodeUint64(packedFormat.length))
|
||
|
chunks.push(packedFormat)
|
||
|
}
|
||
|
|
||
|
// Encode the data fields
|
||
|
for (const [name, type] of Object.entries(format)) {
|
||
|
const v = value[name]
|
||
|
if (v === undefined && type.type !== 'fixed') {
|
||
|
throw new Error(`Key "${name}" missing from value:\n${value.keys}`)
|
||
|
}
|
||
|
|
||
|
switch (type.type) {
|
||
|
case 'object':
|
||
|
if (v instanceof Object) {
|
||
|
chunks.push(packData(v, type.info, false))
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected object, got ${v}`)
|
||
|
}
|
||
|
case 'hash':
|
||
|
if (v instanceof Object) {
|
||
|
// NOTE: Hashes always refer to the typed version of the data to enable forward compatibility
|
||
|
chunks.push(sha256Hash(packData(v, type.info, true)))
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected object for hashing, got ${v}`)
|
||
|
}
|
||
|
case 'array':
|
||
|
if (v instanceof Array) {
|
||
|
assert(v.length < 128, `Array too large to be encoded: ${v}`)
|
||
|
chunks.push(new Uint8Array([v.length]))
|
||
|
v.forEach((value) => {
|
||
|
chunks.push(packData({ value }, { value: type.info }, false))
|
||
|
})
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected array, got ${v}`)
|
||
|
}
|
||
|
|
||
|
case 'address':
|
||
|
if (v instanceof Uint8Array) {
|
||
|
if (v.length === 20) {
|
||
|
const newValue = new Uint8Array(32)
|
||
|
newValue.set(new TextEncoder().encode("EthereumAddr"))
|
||
|
newValue.set(v, 12)
|
||
|
chunks.push(newValue)
|
||
|
} else if (v.length === 32) {
|
||
|
chunks.push(v)
|
||
|
} else {
|
||
|
throw new Error(`Invalid address byte array length ${v.length}, expected 20 or 32`)
|
||
|
}
|
||
|
} else if (typeof v === 'string') {
|
||
|
if (ethers.utils.isAddress(v)) {
|
||
|
const newValue = new Uint8Array(32)
|
||
|
newValue.set(new TextEncoder().encode("EthereumAddr"))
|
||
|
newValue.set(Buffer.from(v.slice(2), 'hex'), 12)
|
||
|
chunks.push(newValue)
|
||
|
} else if (isValidAddress(v)) {
|
||
|
chunks.push(decodeAddress(v).publicKey)
|
||
|
} else {
|
||
|
throw new Error(`Invalid address string ${v}`)
|
||
|
}
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected address, got ${v}`)
|
||
|
}
|
||
|
|
||
|
break
|
||
|
|
||
|
case 'bytes':
|
||
|
if (v instanceof Uint8Array) {
|
||
|
if (v.length === type.size) {
|
||
|
chunks.push(v)
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Bytes length is wrong, expected ${type.size}, got ${v.length}`)
|
||
|
}
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected bytes[${type.size}], got ${v}`)
|
||
|
}
|
||
|
case 'base64':
|
||
|
if (typeof v === 'string') {
|
||
|
try {
|
||
|
const bytes = decodeBase64(v)
|
||
|
if (bytes.length === type.size) {
|
||
|
chunks.push(bytes)
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Base64 length is wrong, expected ${type.size}, got ${bytes.length}`)
|
||
|
}
|
||
|
} catch {
|
||
|
throw new Error(`${name}: Base64 encoding is wrong, got ${v}`)
|
||
|
}
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected Base64 string, got ${v}`)
|
||
|
}
|
||
|
case 'double':
|
||
|
if (typeof v === 'number') {
|
||
|
const bytes = new ArrayBuffer(8)
|
||
|
Buffer.from(bytes).writeDoubleLE(v, 0)
|
||
|
chunks.push(new Uint8Array(bytes))
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected double, got ${v}`)
|
||
|
}
|
||
|
case 'boolean':
|
||
|
if (typeof v === 'boolean') {
|
||
|
chunks.push(new Uint8Array([v ? 1 : 0]))
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected boolean, got ${v}`)
|
||
|
}
|
||
|
case 'number':
|
||
|
case 'uint':
|
||
|
if (typeof v === 'bigint' || typeof v === 'number') {
|
||
|
chunks.push(encodeUint64(v))
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected uint or number, got ${v}`)
|
||
|
}
|
||
|
case 'string':
|
||
|
if (typeof v === 'string') {
|
||
|
const str = encodeString(v)
|
||
|
if (str.length === type.size) {
|
||
|
chunks.push(str)
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected string length ${type.size}, got string length ${str.length}`)
|
||
|
}
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected string length ${type.size}, got ${v}`)
|
||
|
}
|
||
|
case 'emptyString':
|
||
|
if (typeof v === 'string') {
|
||
|
break
|
||
|
} else {
|
||
|
throw new Error(`${name}: Expected string, got ${v}`)
|
||
|
}
|
||
|
case 'fixed':
|
||
|
chunks.push(decodeBase16(type.valueHex))
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return concatArrays(chunks)
|
||
|
}
|
||
|
|
||
|
export function unpackData(data: Uint8Array, formatOpt?: IPackedInfo): Record<string, any> {
|
||
|
let format: IPackedInfo
|
||
|
let index = 0
|
||
|
|
||
|
// Decode format
|
||
|
if (formatOpt) {
|
||
|
format = formatOpt
|
||
|
} else {
|
||
|
const length = Number(decodeUint64(data.slice(index, index + 8)))
|
||
|
index += 8
|
||
|
|
||
|
format = unpackFormat(data.slice(index, index + length))
|
||
|
index += length
|
||
|
}
|
||
|
|
||
|
// Decode data
|
||
|
// NOTE: This needs to be an inner function to maintain the index across calls
|
||
|
const unpackInner = (data: Uint8Array, format: IPackedInfo) => {
|
||
|
const object: Record<string, any> = {}
|
||
|
for (const [name, type] of Object.entries(format)) {
|
||
|
if (index >= data.length) {
|
||
|
throw new Error(`Unpack data length was not enough for the format provided. Data: ${data}, format: ${JSON.stringify(format)}`)
|
||
|
}
|
||
|
|
||
|
let value: any
|
||
|
switch (type.type) {
|
||
|
case 'object':
|
||
|
value = unpackInner(data, type.info)
|
||
|
break
|
||
|
case 'hash':
|
||
|
value = new Uint8Array(data.slice(index, index + SHA256_HASH_LENGTH))
|
||
|
index += SHA256_HASH_LENGTH
|
||
|
break
|
||
|
case 'array': {
|
||
|
const count = data[index++]
|
||
|
value = []
|
||
|
for (let i = 0; i < count; i++) {
|
||
|
value.push(unpackInner(data, { value: type.info }).value)
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
case 'address':
|
||
|
value = encodeAddress(data.slice(index, index + 32))
|
||
|
index += 32
|
||
|
break
|
||
|
case 'bytes':
|
||
|
value = new Uint8Array(data.slice(index, index + type.size))
|
||
|
index += type.size
|
||
|
break
|
||
|
case 'base64':
|
||
|
value = encodeBase64(data.slice(index, index + type.size))
|
||
|
index += type.size
|
||
|
break
|
||
|
case 'double':
|
||
|
value = Buffer.from(data.slice(index, index + 8)).readDoubleLE(0)
|
||
|
index += 8
|
||
|
break
|
||
|
case 'boolean':
|
||
|
value = data.slice(index, index + 1)[0] === 1
|
||
|
index += 1
|
||
|
break
|
||
|
case 'number':
|
||
|
value = Number(decodeUint64(data.slice(index, index + 8)))
|
||
|
index += 8
|
||
|
break
|
||
|
case 'uint':
|
||
|
value = decodeUint64(data.slice(index, index + 8))
|
||
|
index += 8
|
||
|
break
|
||
|
case 'string':
|
||
|
value = decodeString(data.slice(index, index + type.size))
|
||
|
index += type.size
|
||
|
break
|
||
|
case 'emptyString':
|
||
|
value = ""
|
||
|
break
|
||
|
case 'fixed':
|
||
|
value = decodeBase16(type.valueHex)
|
||
|
break
|
||
|
default:
|
||
|
throw new Error(`Unknown decode type: ${type}`)
|
||
|
}
|
||
|
|
||
|
object[name] = value
|
||
|
}
|
||
|
|
||
|
return object
|
||
|
}
|
||
|
|
||
|
const result = unpackInner(data, format)
|
||
|
|
||
|
if (index !== data.length) {
|
||
|
throw new Error(`Data consumed(${index} bytes) did not match expected (${data.length} bytes) for format\nFormat: ${JSON.stringify(format)}\nValue: ${Buffer.from(data).toString('hex')}`)
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
export function encodeArgArray(params: AlgorandType[]): Uint8Array[] {
|
||
|
return params.map(param => {
|
||
|
if (param instanceof Uint8Array)
|
||
|
return new Uint8Array(param)
|
||
|
if (typeof param === "string")
|
||
|
return encodeString(param)
|
||
|
if (typeof param === "boolean")
|
||
|
param = BigInt(param ? 1 : 0)
|
||
|
if (typeof param === "number")
|
||
|
param = BigInt(param)
|
||
|
return encodeUint64(param)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
export function encodeString(value: string | Uint8Array): Uint8Array {
|
||
|
return new Uint8Array(Buffer.from(value))
|
||
|
}
|
||
|
|
||
|
export function decodeString(value: Uint8Array): string {
|
||
|
return Buffer.from(value).toString('utf-8')
|
||
|
}
|
||
|
|
||
|
export function decodeState(state: Record<string, Record<string, string>>[], stateMap: IStateMap, errorOnMissing = true): IState {
|
||
|
const result: IState = {}
|
||
|
for (const [name, type] of Object.entries(stateMap)) {
|
||
|
const stateName = encodeBase64(encodeString(name))
|
||
|
const key = state.find((v: any) => v['key'] === stateName)
|
||
|
if (errorOnMissing && key === undefined) {
|
||
|
throw new Error(`Expected key ${name} was not found in state`)
|
||
|
}
|
||
|
|
||
|
const value = key ? key['value'][type] : undefined
|
||
|
if (errorOnMissing && value === undefined) {
|
||
|
throw new Error(`Expected value for key ${name} was not found in state`)
|
||
|
}
|
||
|
|
||
|
const typedValue = type === 'bytes' ? decodeBase64(value ?? '') : BigInt(value ?? '')
|
||
|
result[name] = typedValue
|
||
|
}
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
export function encodeUint64(value: number | bigint): Uint8Array {
|
||
|
const bytes: Buffer = Buffer.alloc(8)
|
||
|
for (let index = 0; index < 8; index++)
|
||
|
bytes[7 - index] = Number((BigInt(value) >> BigInt(index * 8)) & BigInt(0xFF))
|
||
|
return new Uint8Array(bytes)
|
||
|
}
|
||
|
|
||
|
export function decodeUint64(value: Uint8Array): bigint {
|
||
|
assert(value.length >= 8, `Expected at least 8 bytes to decode a uint64, but got ${value.length} bytes\nValue: ${Buffer.from(value).toString('hex')}`)
|
||
|
|
||
|
let num = BigInt(0)
|
||
|
for (let index = 0; index < 8; index++) {
|
||
|
num = (num << BigInt(8)) | BigInt(value[index])
|
||
|
}
|
||
|
|
||
|
return num
|
||
|
}
|
||
|
|
||
|
export function encodeUint32(value: number): Uint8Array {
|
||
|
if (value >= 2 ** 32 || value < 0) {
|
||
|
throw new Error(`Out of bound value in Uint16: ${value}`)
|
||
|
}
|
||
|
|
||
|
const bytes: Buffer = Buffer.alloc(4)
|
||
|
for (let index = 0; index < 4; index++)
|
||
|
bytes[3 - index] = Number((BigInt(value) >> BigInt(index * 8)) & BigInt(0xFF))
|
||
|
return new Uint8Array(bytes)
|
||
|
}
|
||
|
|
||
|
export function decodeUint32(value: Uint8Array): number {
|
||
|
let num = BigInt(0)
|
||
|
for (let index = 0; index < 4; index++)
|
||
|
num = (num << BigInt(8)) | BigInt(value[index])
|
||
|
return Number(num)
|
||
|
}
|
||
|
|
||
|
export function encodeUint16(value: number): Uint8Array {
|
||
|
if (value >= 2 ** 16 || value < 0) {
|
||
|
throw new Error(`Out of bound value in Uint16: ${value}`)
|
||
|
}
|
||
|
|
||
|
return new Uint8Array([value >> 8, value & 0xFF])
|
||
|
}
|
||
|
|
||
|
export function decodeUint16(value: Uint8Array): number {
|
||
|
if (value.length !== 2) {
|
||
|
throw new Error(`Invalid value length, expected 2, got ${value.length}`)
|
||
|
}
|
||
|
|
||
|
return value[0] * 256 + value[1]
|
||
|
}
|
||
|
|
||
|
export function encodeUint8(value: number): Uint8Array {
|
||
|
if (value >= 2 ** 8 || value < 0) {
|
||
|
throw new Error(`Out of bound value in Uint8: ${value}`)
|
||
|
}
|
||
|
|
||
|
return new Uint8Array([value])
|
||
|
}
|
||
|
|
||
|
export function decodeBase16(value: string): Uint8Array {
|
||
|
return Buffer.from(value, 'hex')
|
||
|
}
|
||
|
|
||
|
export function encodeBase64(value: Uint8Array): string {
|
||
|
return Buffer.from(value).toString('base64')
|
||
|
}
|
||
|
|
||
|
export function decodeBase64(value: string): Uint8Array {
|
||
|
return Buffer.from(value, 'base64')
|
||
|
}
|
||
|
|
||
|
export function sha256Hash(arr: sha512.Message): Uint8Array {
|
||
|
return new Uint8Array(sha512.sha512_256.arrayBuffer(arr))
|
||
|
}
|
||
|
|
||
|
export function encodeApplicationAddress(id: number): Address {
|
||
|
const APP_ID_PREFIX = Buffer.from('appID');
|
||
|
const toBeSigned = concatArrays([APP_ID_PREFIX, encodeUint64(BigInt(id))]);
|
||
|
return encodeAddress(sha256Hash(toBeSigned));
|
||
|
}
|
||
|
|
||
|
export function compareArrays(a: Uint8Array[], b: Uint8Array[]) {
|
||
|
return (a===undefined || b===undefined) ? a===b : (a.length === b.length && a.reduce((equal, item, index) => equal && item===b[index], true))
|
||
|
}
|
||
|
|
||
|
function getDelta(response: any, key: string): any | undefined {
|
||
|
const delta = response['global-state-delta'].find((v: any) => v.key === key)
|
||
|
if (delta === undefined)
|
||
|
return undefined
|
||
|
return delta['value']
|
||
|
}
|
||
|
|
||
|
export function getDeltaUint(response: any, key: string): bigint | undefined {
|
||
|
const delta = getDelta(response, key)
|
||
|
if (delta === undefined)
|
||
|
return undefined
|
||
|
return BigInt(delta['uint'])
|
||
|
}
|
||
|
|
||
|
export function getDeltaBytes(response: any, key: string): Uint8Array | undefined {
|
||
|
const delta = getDelta(response, key)
|
||
|
if (delta === undefined)
|
||
|
return undefined
|
||
|
return decodeBase64(delta['bytes'])
|
||
|
}
|
||
|
|
||
|
export { encodeAddress } from 'algosdk'
|