Skip to content

Commit

Permalink
feat: web sdk rhp4
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Feb 5, 2024
1 parent bb65acb commit 8b9131e
Show file tree
Hide file tree
Showing 10 changed files with 875 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/stupid-teachers-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/sdk': patch
---

Initial web SDK with WebTransport RHP4 protocol support.
158 changes: 158 additions & 0 deletions libs/sdk/src/encoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
export type Encoder = {
dataView: DataView
offset: number
}

export function newEncoder(): Encoder {
const buffer = new ArrayBuffer((1 << 22) + 200) // TODO: set based on rpc
return {
dataView: new DataView(buffer),
offset: 0,
}
}

export type Decoder = {
dataView: DataView
offset: number
}

export function newDecoder(buf: Uint8Array): Decoder {
const dataView = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
return {
dataView,
offset: 0,
}
}

export function encodeBoolean(e: Encoder, b: boolean) {
e.dataView.setUint8(e.offset++, b ? 1 : 0)
}

export function decodeBoolean(d: Decoder): boolean {
return Boolean(d.dataView.getUint8(d.offset++) === 1)
}

export function encodeUint64(e: Encoder, n: number) {
e.dataView.setBigUint64(e.offset, BigInt(n), true)
e.offset += 8
}

export function decodeUint64(d: Decoder): number {
const n = Number(d.dataView.getBigUint64(d.offset, true))
d.offset += 8
return n
}

export function encodeBytes(e: Encoder, s: string) {
const encoder = new TextEncoder()
const arr = encoder.encode(s)

for (let i = 0; i < arr.length; i++) {
e.dataView.setUint8(e.offset++, arr[i])
}
}

export function decodeBytes(d: Decoder, length: number): string {
const arr = new Uint8Array(length)

for (let i = 0; i < length; i++) {
arr[i] = d.dataView.getUint8(d.offset++)
}

const decoder = new TextDecoder('utf-8')
return decoder.decode(arr)
}

export function encodeUint8Array(e: Encoder, a: Uint8Array) {
for (let i = 0; i < a.length; i++) {
e.dataView.setUint8(e.offset++, a[i])
}
}

export function decodeUint8Array(d: Decoder, length: number): Uint8Array {
const arr = new Uint8Array(length)

for (let i = 0; i < length; i++) {
arr[i] = d.dataView.getUint8(d.offset++)
}

return arr
}

export function encodeString(e: Encoder, s: string) {
encodeUint64(e, s.length)
encodeBytes(e, s)
}

export function decodeString(d: Decoder): string {
const length = decodeUint64(d)
const s = decodeBytes(d, length)
return s
}

export function encodeCurrency(e: Encoder, c: bigint) {
// currency is 128 bits, little endian
e.dataView.setBigUint64(e.offset, c & BigInt('0xFFFFFFFFFFFFFFFF'), true)
e.offset += 8
e.dataView.setBigUint64(e.offset, c >> BigInt(64), true)
e.offset += 8
}

export function decodeCurrency(d: Decoder): bigint {
const lo = d.dataView.getBigUint64(d.offset, true)
d.offset += 8
const hi = d.dataView.getBigUint64(d.offset, true)
d.offset += 8
return (hi << BigInt(64)) | lo
}

export function encodeAddress(e: Encoder, a: string) {
return encodeBytes(e, a)
}

export function decodeAddress(d: Decoder): string {
return decodeBytes(d, 32)
}

export function encodeSignature(e: Encoder, s: string) {
return encodeBytes(e, s)
}

export function decodeSignature(d: Decoder): string {
return decodeBytes(d, 64)
}

export function encodeTime(e: Encoder, t: string | Date) {
const time = new Date(t)
const seconds = BigInt(time.getTime()) / BigInt(1000)
e.dataView.setBigUint64(e.offset, seconds, true)
e.offset += 8
}

export function decodeTime(d: Decoder): string {
const seconds = d.dataView.getBigUint64(d.offset, true)
d.offset += 8
try {
const time = new Date(Number(seconds) * 1000)
return time.toISOString()
} catch (e) {
return new Date().toISOString()
}
}

export function encodeLengthPrefix(e: Encoder, length: number) {
encodeUint64(e, length)
}

export function decodeLengthPrefix(d: Decoder): number {
return decodeUint64(d)
}

export function encodeRpcId(e: Encoder, rpcName: string) {
const rpcId = rpcName.padEnd(16, '\0')
encodeUint8Array(e, new TextEncoder().encode(rpcId))
}

export function decodeRpcId(d: Decoder): string {
return decodeBytes(d, 16).replace(/\0/g, '')
}
71 changes: 71 additions & 0 deletions libs/sdk/src/encoding.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
encodeHostPrices,
decodeHostPrices,
encodeHostSettings,
decodeHostSettings,
} from './encoding'
import { newEncoder, newDecoder } from './encoder'
import { HostPrices, HostSettings } from './types'
import { TextDecoder, TextEncoder } from 'node:util'

Object.defineProperties(globalThis, {
TextDecoder: { value: TextDecoder },
TextEncoder: { value: TextEncoder },
})

describe('encoding', () => {
it('encodeHostPrices', () => {
const hostPrices: HostPrices = {
contractPrice: BigInt(1000000000),
collateral: BigInt(2000000000),
storagePrice: BigInt(3000000000),
ingressPrice: BigInt(4000000000),
egressPrice: BigInt(5000000000),
tipHeight: 450_000,
validUntil: '2022-12-31T00:00:00.000Z',
signature:
'abcd567890123456789012345678901234567890123456789012345678901234',
}

const e = newEncoder()
encodeHostPrices(e, hostPrices)
const buf = new Uint8Array(e.dataView.buffer)
const decoded = decodeHostPrices(newDecoder(buf))
expect(decoded).toEqual(hostPrices)
})

it('encodeHostSettings', () => {
const prices: HostPrices = {
contractPrice: BigInt(1000000000),
collateral: BigInt(2000000000),
storagePrice: BigInt(3000000000),
ingressPrice: BigInt(4000000000),
egressPrice: BigInt(5000000000),
tipHeight: 450_000,
validUntil: '2022-12-31T00:00:00.000Z',
signature:
'abcd567890123456789012345678901234567890123456789012345678901234',
}
const hostSettings: HostSettings = {
version: '123',
netAddresses: [
{ protocol: 'protocol1', address: 'address1longer' },
{ protocol: 'protocol2longer', address: 'address2' },
],
// 32 bytes
walletAddress: '12345678901234567890123456789012',
acceptingContracts: true,
maxCollateral: BigInt(1000000000),
maxDuration: 100,
remainingStorage: 100,
totalStorage: 100,
prices,
}

const e = newEncoder()
encodeHostSettings(e, hostSettings)
const buf = new Uint8Array(e.dataView.buffer)
const decoded = decodeHostSettings(newDecoder(buf))
expect(decoded).toEqual(hostSettings)
})
})
111 changes: 111 additions & 0 deletions libs/sdk/src/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
encodeCurrency,
decodeCurrency,
encodeAddress,
encodeBoolean,
encodeBytes,
encodeString,
encodeUint64,
encodeLengthPrefix,
decodeAddress,
decodeBoolean,
decodeBytes,
decodeString,
decodeUint64,
decodeLengthPrefix,
encodeSignature,
decodeSignature,
encodeTime,
decodeTime,
Encoder,
Decoder,
} from './encoder'
import { HostPrices, HostSettings, NetAddress } from './types'

export function encodeHostPrices(e: Encoder, hostPrices: HostPrices) {
encodeCurrency(e, hostPrices.contractPrice)
encodeCurrency(e, hostPrices.collateral)
encodeCurrency(e, hostPrices.storagePrice)
encodeCurrency(e, hostPrices.ingressPrice)
encodeCurrency(e, hostPrices.egressPrice)
encodeUint64(e, hostPrices.tipHeight)
encodeTime(e, hostPrices.validUntil)
encodeSignature(e, hostPrices.signature)
}

export function decodeHostPrices(d: Decoder): HostPrices {
const contractPrice = decodeCurrency(d)
const collateral = decodeCurrency(d)
const storagePrice = decodeCurrency(d)
const ingressPrice = decodeCurrency(d)
const egressPrice = decodeCurrency(d)
const tipHeight = decodeUint64(d)
const validUntil = decodeTime(d)
const signature = decodeSignature(d)
return {
contractPrice,
collateral,
storagePrice,
ingressPrice,
egressPrice,
tipHeight,
validUntil,
signature,
}
}

export function encodeNetAddress(e: Encoder, n: NetAddress) {
encodeString(e, n.protocol)
encodeString(e, n.address)
}

export function decodeNetAddress(d: Decoder): NetAddress {
const protocol = decodeString(d)
const address = decodeString(d)
return {
protocol,
address,
}
}

export function encodeHostSettings(e: Encoder, hostSettings: HostSettings) {
encodeBytes(e, hostSettings.version)
encodeLengthPrefix(e, hostSettings.netAddresses.length)
for (let i = 0; i < hostSettings.netAddresses.length; i++) {
encodeNetAddress(e, hostSettings.netAddresses[i])
}
encodeAddress(e, hostSettings.walletAddress)
encodeBoolean(e, hostSettings.acceptingContracts)
encodeCurrency(e, hostSettings.maxCollateral)
encodeUint64(e, hostSettings.maxDuration)
encodeUint64(e, hostSettings.remainingStorage)
encodeUint64(e, hostSettings.totalStorage)
encodeHostPrices(e, hostSettings.prices)
}

export function decodeHostSettings(d: Decoder): HostSettings {
const version = decodeBytes(d, 3)
const netAddresses = []
const length = decodeLengthPrefix(d)
for (let i = 0; i < length; i++) {
netAddresses[i] = decodeNetAddress(d)
}
const walletAddress = decodeAddress(d)
const acceptingContracts = decodeBoolean(d)
const maxCollateral = decodeCurrency(d)
const maxDuration = decodeUint64(d)
const remainingStorage = decodeUint64(d)
const totalStorage = decodeUint64(d)
const prices = decodeHostPrices(d)
return {
version,
netAddresses,
walletAddress,
acceptingContracts,
maxCollateral,
maxDuration,
remainingStorage,
totalStorage,
prices,
}
}
11 changes: 11 additions & 0 deletions libs/sdk/src/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { WebTransportClient } from './transport'

export async function example() {
const client = new WebTransportClient(
'wss://your-webtransport-server.example',
'cert'
)
await client.connect()
const response = await client.sendRPCSettingsRequest()
console.log('Settings:', response)
}
2 changes: 2 additions & 0 deletions libs/sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './types'
export * from './transport'
Loading

0 comments on commit 8b9131e

Please sign in to comment.