diff --git a/.changeset/stupid-teachers-tie.md b/.changeset/stupid-teachers-tie.md new file mode 100644 index 000000000..fa52ac343 --- /dev/null +++ b/.changeset/stupid-teachers-tie.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/sdk': patch +--- + +Initial web SDK with WebTransport RHP4 protocol support. diff --git a/libs/sdk/src/encoder.ts b/libs/sdk/src/encoder.ts new file mode 100644 index 000000000..5dec78a86 --- /dev/null +++ b/libs/sdk/src/encoder.ts @@ -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, '') +} diff --git a/libs/sdk/src/encoding.spec.ts b/libs/sdk/src/encoding.spec.ts new file mode 100644 index 000000000..656a72c45 --- /dev/null +++ b/libs/sdk/src/encoding.spec.ts @@ -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) + }) +}) diff --git a/libs/sdk/src/encoding.ts b/libs/sdk/src/encoding.ts new file mode 100644 index 000000000..e2445ecf4 --- /dev/null +++ b/libs/sdk/src/encoding.ts @@ -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, + } +} diff --git a/libs/sdk/src/example.ts b/libs/sdk/src/example.ts new file mode 100644 index 000000000..04876e15b --- /dev/null +++ b/libs/sdk/src/example.ts @@ -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) +} diff --git a/libs/sdk/src/index.ts b/libs/sdk/src/index.ts new file mode 100644 index 000000000..bacb29a20 --- /dev/null +++ b/libs/sdk/src/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './transport' diff --git a/libs/sdk/src/rpc.spec.ts b/libs/sdk/src/rpc.spec.ts new file mode 100644 index 000000000..10732c00a --- /dev/null +++ b/libs/sdk/src/rpc.spec.ts @@ -0,0 +1,150 @@ +import crypto from 'crypto' +import { newEncoder, newDecoder } from './encoder' +import { + HostPrices, + HostSettings, + RPCReadSectorRequest, + RPCReadSectorResponse, + RPCSettingsRequest, + RPCSettingsResponse, + RPCWriteSectorRequest, + RPCWriteSectorResponse, +} from './types' +import { TextDecoder, TextEncoder } from 'node:util' +import { + decodeRpcRequestReadSector, + decodeRpcRequestSettings, + decodeRpcRequestWriteSector, + decodeRpcResponseReadSector, + decodeRpcResponseSettings, + decodeRpcResponseWriteSector, + encodeRpcRequestReadSector, + encodeRpcRequestSettings, + encodeRpcRequestWriteSector, + encodeRpcResponseReadSector, + encodeRpcResponseSettings, + encodeRpcResponseWriteSector, +} from './rpc' + +Object.defineProperties(globalThis, { + TextDecoder: { value: TextDecoder }, + TextEncoder: { value: TextEncoder }, +}) + +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, +} + +describe('rpc', () => { + describe('settings', () => { + it('request', () => { + const e = newEncoder() + const rpc: RPCSettingsRequest = undefined + encodeRpcRequestSettings(e, rpc) + const buf = new Uint8Array(e.dataView.buffer) + const decoded = decodeRpcRequestSettings(newDecoder(buf)) + expect(decoded).toEqual(rpc) + }) + + it('response', () => { + const e = newEncoder() + const rpc: RPCSettingsResponse = { + settings: hostSettings, + } + encodeRpcResponseSettings(e, rpc) + const buf = new Uint8Array(e.dataView.buffer) + const decoded = decodeRpcResponseSettings(newDecoder(buf)) + expect(decoded).toEqual(rpc) + }) + }) + describe('read sector', () => { + it('request', () => { + const e = newEncoder() + const rpc: RPCReadSectorRequest = { + prices, + accountId: 'accountId', + root: '1'.repeat(32), + offset: 44, + length: 32, + } + encodeRpcRequestReadSector(e, rpc) + const buf = new Uint8Array(e.dataView.buffer) + const decoded = decodeRpcRequestReadSector(newDecoder(buf)) + expect(decoded).toEqual(rpc) + }) + + it('response', () => { + const e = newEncoder() + const rpc: RPCReadSectorResponse = { + proof: ['1'.repeat(32), '2'.repeat(32)], + sector: new Uint8Array(1 << 22), // 4MiB + } + encodeRpcResponseReadSector(e, rpc) + const buf = new Uint8Array(e.dataView.buffer) + const decoded = decodeRpcResponseReadSector(newDecoder(buf)) + expect(decoded.proof).toEqual(rpc.proof) + // Compare binary parts by hashes + const originalHash = hashBuffer(rpc.sector) + const decodedHash = hashBuffer(decoded.sector) + expect(decodedHash).toEqual(originalHash) + }) + }) + + describe('write sector', () => { + it('request', () => { + const e = newEncoder() + const rpc: RPCWriteSectorRequest = { + prices, + accountId: 'accountId', + sector: new Uint8Array(1 << 22), // 4MiB + } + encodeRpcRequestWriteSector(e, rpc) + const buf = new Uint8Array(e.dataView.buffer) + const decoded = decodeRpcRequestWriteSector(newDecoder(buf)) + expect(decoded.accountId).toEqual(rpc.accountId) + expect(decoded.prices).toEqual(rpc.prices) + // Compare binary parts by hashes + const originalHash = hashBuffer(rpc.sector) + const decodedHash = hashBuffer(decoded.sector) + expect(decodedHash).toEqual(originalHash) + }) + + it('response', () => { + const e = newEncoder() + const rpc: RPCWriteSectorResponse = { + root: '1'.repeat(32), + } + encodeRpcResponseWriteSector(e, rpc) + const buf = new Uint8Array(e.dataView.buffer) + const decoded = decodeRpcResponseWriteSector(newDecoder(buf)) + expect(decoded).toEqual(rpc) + }) + }) +}) + +function hashBuffer(buffer: Uint8Array) { + return crypto.createHash('sha256').update(buffer).digest('hex') +} diff --git a/libs/sdk/src/rpc.ts b/libs/sdk/src/rpc.ts new file mode 100644 index 000000000..0659568ab --- /dev/null +++ b/libs/sdk/src/rpc.ts @@ -0,0 +1,152 @@ +import { + encodeBytes, + encodeString, + encodeUint64, + encodeLengthPrefix, + decodeBytes, + decodeString, + decodeUint64, + decodeLengthPrefix, + Encoder, + Decoder, + encodeUint8Array, + decodeUint8Array, + encodeRpcId, + decodeRpcId, +} from './encoder' +import { + decodeHostPrices, + decodeHostSettings, + encodeHostPrices, + encodeHostSettings, +} from './encoding' +import { + RPCReadSectorRequest, + RPCReadSectorResponse, + RPCSettingsRequest, + RPCSettingsResponse, + RPCWriteSectorRequest, + RPCWriteSectorResponse, +} from './types' + +// settings request + +export function encodeRpcRequestSettings( + e: Encoder, + _rpcSettings: RPCSettingsRequest +) { + encodeRpcId(e, 'Settings') +} + +export function decodeRpcRequestSettings(d: Decoder): RPCSettingsRequest { + decodeRpcId(d) +} + +// settings response + +export function encodeRpcResponseSettings(e: Encoder, r: RPCSettingsResponse) { + encodeHostSettings(e, r.settings) +} + +export function decodeRpcResponseSettings(d: Decoder): RPCSettingsResponse { + const settings = decodeHostSettings(d) + return { settings } +} + +// read sector request + +export function encodeRpcRequestReadSector( + e: Encoder, + readSector: RPCReadSectorRequest +) { + encodeRpcId(e, 'ReadSector') + encodeHostPrices(e, readSector.prices) + encodeString(e, readSector.accountId) + encodeBytes(e, readSector.root) + encodeUint64(e, readSector.offset) + encodeUint64(e, readSector.length) +} + +export function decodeRpcRequestReadSector(d: Decoder): RPCReadSectorRequest { + decodeRpcId(d) + const prices = decodeHostPrices(d) + const accountId = decodeString(d) + const root = decodeBytes(d, 32) + const offset = decodeUint64(d) + const length = decodeUint64(d) + return { + prices, + accountId, + root, + offset, + length, + } +} + +// read sector response + +export function encodeRpcResponseReadSector( + e: Encoder, + rsr: RPCReadSectorResponse +) { + encodeLengthPrefix(e, rsr.proof.length) + for (let i = 0; i < rsr.proof.length; i++) { + encodeBytes(e, rsr.proof[i]) + } + encodeUint8Array(e, rsr.sector) +} + +export function decodeRpcResponseReadSector(d: Decoder): RPCReadSectorResponse { + const proofCount = decodeLengthPrefix(d) + const proof = [] + for (let i = 0; i < proofCount; i++) { + proof[i] = decodeBytes(d, 32) + } + const sector = decodeUint8Array(d, 1 << 22) // 4MiB + return { + proof, + sector, + } +} + +// write sector request + +export function encodeRpcRequestWriteSector( + e: Encoder, + writeSector: RPCWriteSectorRequest +) { + encodeRpcId(e, 'WriteSector') + encodeHostPrices(e, writeSector.prices) + encodeString(e, writeSector.accountId) + encodeUint8Array(e, writeSector.sector) +} + +export function decodeRpcRequestWriteSector(d: Decoder): RPCWriteSectorRequest { + decodeRpcId(d) + const prices = decodeHostPrices(d) + const accountId = decodeString(d) + const sector = decodeUint8Array(d, 1 << 22) // 4MiB + return { + prices, + accountId, + sector, + } +} + +// write sector response + +export function encodeRpcResponseWriteSector( + e: Encoder, + wsr: RPCWriteSectorResponse +) { + encodeBytes(e, wsr.root) +} + +export function decodeRpcResponseWriteSector( + d: Decoder +): RPCWriteSectorResponse { + const root = decodeBytes(d, 32) + return { + root, + } +} diff --git a/libs/sdk/src/transport.ts b/libs/sdk/src/transport.ts new file mode 100644 index 000000000..076cb4538 --- /dev/null +++ b/libs/sdk/src/transport.ts @@ -0,0 +1,136 @@ +import { Decoder, Encoder, newDecoder, newEncoder } from './encoder' +import { + decodeRpcResponseSettings, + decodeRpcResponseReadSector, + decodeRpcResponseWriteSector, + encodeRpcRequestSettings, + encodeRpcRequestReadSector, + encodeRpcRequestWriteSector, +} from './rpc' +import { + RPCReadSectorResponse, + RPCSettingsResponse, + RPCWriteSectorResponse, + RPCReadSectorRequest, + RPCWriteSectorRequest, + RPC, + RPCReadSector, + RPCWriteSector, + RPCSettings, +} from './types' + +function base64ToArrayBuffer(base64: string) { + const binaryString = window.atob(base64) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + return bytes.buffer +} + +export class WebTransportClient { + private transport!: WebTransport + private url: string + private cert: string + + constructor(url: string, cert: string) { + this.url = url + this.cert = cert + } + + async connect() { + if (!('WebTransport' in window)) { + throw new Error('WebTransport is not supported in your browser.') + } + + try { + this.transport = new WebTransport(this.url, { + serverCertificateHashes: this.cert + ? [ + { + algorithm: 'sha-256', + value: base64ToArrayBuffer(this.cert), + }, + ] + : undefined, + }) + await this.transport.ready + } catch (e) { + console.error('connect', e) + throw e + } + } + + private async sendRequest( + rpcRequest: T['request'], + encodeFn: (e: Encoder, data: T['request']) => void, + decodeFn: (d: Decoder) => T['response'] + ): Promise { + let stream: WebTransportBidirectionalStream | undefined + try { + stream = await this.transport.createBidirectionalStream() + if (!stream) { + throw new Error('Bidirectional stream not opened') + } + + const writer = stream.writable.getWriter() + const e = newEncoder() + encodeFn(e, rpcRequest) + + const buf = new Uint8Array(e.dataView.buffer, 0, e.offset) + await writer.write(buf) + await writer.close() + + return this.handleIncomingData(stream, decodeFn) + } catch (e) { + console.error('sendRequest', e) + throw e + } + } + + private async handleIncomingData( + stream: WebTransportBidirectionalStream, + decodeFn: (d: Decoder) => T + ): Promise { + try { + const reader = stream.readable.getReader() + const { value, done } = await reader.read() + if (done) { + throw new Error('Stream closed by the server.') + } + await reader.cancel() + return decodeFn(newDecoder(value)) + } catch (e) { + console.error('handleIncomingData', e) + throw e + } + } + + async sendReadSectorRequest( + readSector: RPCReadSectorRequest + ): Promise { + return this.sendRequest( + readSector, + encodeRpcRequestReadSector, + decodeRpcResponseReadSector + ) + } + + async sendWriteSectorRequest( + writeSector: RPCWriteSectorRequest + ): Promise { + return this.sendRequest( + writeSector, + encodeRpcRequestWriteSector, + decodeRpcResponseWriteSector + ) + } + + async sendRPCSettingsRequest(): Promise { + return this.sendRequest( + undefined, + encodeRpcRequestSettings, + decodeRpcResponseSettings + ) + } +} diff --git a/libs/sdk/src/types.ts b/libs/sdk/src/types.ts new file mode 100644 index 000000000..3129f33ae --- /dev/null +++ b/libs/sdk/src/types.ts @@ -0,0 +1,79 @@ +type Currency = bigint +type Signature = string +type Address = string +type Hash256 = string // 32 bytes +type AccountID = string // 16 bytes + +export type HostPrices = { + contractPrice: Currency + collateral: Currency + storagePrice: Currency + ingressPrice: Currency + egressPrice: Currency + tipHeight: number + validUntil: string + signature: Signature +} + +export type NetAddress = { + protocol: string + address: string +} + +export type HostSettings = { + version: string // 3 bytes + netAddresses: NetAddress[] + walletAddress: Address // 32 bytes + acceptingContracts: boolean + maxCollateral: Currency + maxDuration: number + remainingStorage: number + totalStorage: number + prices: HostPrices +} + +export type RPCSettingsRequest = void + +export type RPCSettingsResponse = { + settings: HostSettings +} + +export type RPCSettings = { + request: RPCSettingsRequest + response: RPCSettingsResponse +} + +export type RPCReadSectorRequest = { + prices: HostPrices + accountId: AccountID // 16 bytes + root: Hash256 // 32 bytes - types.Hash256 + offset: number // uint64 + length: number // uint64 +} + +export type RPCReadSectorResponse = { + proof: Hash256[] // 32 bytes each - types.Hash256 + sector: Uint8Array // []byte +} + +export type RPCReadSector = { + request: RPCReadSectorRequest + response: RPCReadSectorResponse +} + +export type RPCWriteSectorRequest = { + prices: HostPrices + accountId: AccountID // 16 bytes + sector: Uint8Array // []byte - extended to SectorSize by host +} + +export type RPCWriteSectorResponse = { + root: Hash256 // 32 bytes - types.Hash256 +} + +export type RPCWriteSector = { + request: RPCWriteSectorRequest + response: RPCWriteSectorResponse +} + +export type RPC = RPCSettings | RPCReadSector | RPCWriteSector