From ea3724178b3ffe0ddf6c3956a4ac54ce3dfb6ce5 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Wed, 28 Feb 2024 14:59:47 -0500 Subject: [PATCH] refactor: sdk rhp --- .changeset/tiny-cougars-refuse.md | 5 + .env.example | 3 + .github/actions/setup/action.yml | 7 +- .github/workflows/pr.yml | 5 +- .github/workflows/release-main.yml | 3 + libs/sdk/project.json | 2 +- libs/sdk/src/{wasm => }/global.d.ts | 0 libs/sdk/src/index.ts | 2 +- libs/sdk/src/init.ts | 7 + libs/sdk/src/initTest.ts | 7 + libs/sdk/src/{js => legacy}/encoder.ts | 14 +- libs/sdk/src/{js => legacy}/encoding.spec.ts | 26 +- libs/sdk/src/{js => legacy}/encoding.ts | 10 +- libs/sdk/src/{js => legacy}/example.ts | 0 libs/sdk/src/{js => legacy}/rpc.spec.ts | 16 +- libs/sdk/src/{js => legacy}/rpc.ts | 2 +- libs/sdk/src/{js => legacy}/transport.ts | 2 +- libs/sdk/src/legacy/types.ts | 79 +++++ libs/sdk/src/{wasm => }/resources/.gitkeep | 0 libs/sdk/src/sdk.ts | 11 + libs/sdk/src/transport.ts | 135 ++++++++ libs/sdk/src/types.ts | 84 ++++- libs/sdk/src/{wasm => }/utils/wasm_exec.d.ts | 0 libs/sdk/src/{wasm => }/utils/wasm_exec.js | 0 .../src/{wasm => }/utils/wasm_exec_tinygo.js | 0 libs/sdk/src/wasm.spec.ts | 290 ++++++++++++++++++ libs/sdk/src/wasm.ts | 12 + libs/sdk/src/wasm/index.ts | 24 -- libs/sdk/src/wasm/types.ts | 68 ---- libs/sdk/src/wasmTest.ts | 14 + sdk/encode/encode.go | 69 ----- sdk/go.mod | 2 +- sdk/go.sum | 6 + sdk/main.go | 50 +-- sdk/marshal/marshal.go | 37 +++ sdk/other.go | 25 -- sdk/rhp.go | 276 ----------------- sdk/rhp/encode.go | 67 ++++ sdk/rhp/rhp.go | 131 ++++++++ 39 files changed, 962 insertions(+), 529 deletions(-) create mode 100644 .changeset/tiny-cougars-refuse.md rename libs/sdk/src/{wasm => }/global.d.ts (100%) create mode 100644 libs/sdk/src/init.ts create mode 100644 libs/sdk/src/initTest.ts rename libs/sdk/src/{js => legacy}/encoder.ts (91%) rename libs/sdk/src/{js => legacy}/encoding.spec.ts (74%) rename libs/sdk/src/{js => legacy}/encoding.ts (93%) rename libs/sdk/src/{js => legacy}/example.ts (100%) rename libs/sdk/src/{js => legacy}/rpc.spec.ts (94%) rename libs/sdk/src/{js => legacy}/rpc.ts (99%) rename libs/sdk/src/{js => legacy}/transport.ts (99%) create mode 100644 libs/sdk/src/legacy/types.ts rename libs/sdk/src/{wasm => }/resources/.gitkeep (100%) create mode 100644 libs/sdk/src/sdk.ts create mode 100644 libs/sdk/src/transport.ts rename libs/sdk/src/{wasm => }/utils/wasm_exec.d.ts (100%) rename libs/sdk/src/{wasm => }/utils/wasm_exec.js (100%) rename libs/sdk/src/{wasm => }/utils/wasm_exec_tinygo.js (100%) create mode 100644 libs/sdk/src/wasm.spec.ts create mode 100644 libs/sdk/src/wasm.ts delete mode 100644 libs/sdk/src/wasm/index.ts delete mode 100644 libs/sdk/src/wasm/types.ts create mode 100644 libs/sdk/src/wasmTest.ts delete mode 100644 sdk/encode/encode.go create mode 100644 sdk/marshal/marshal.go delete mode 100644 sdk/other.go delete mode 100644 sdk/rhp.go create mode 100644 sdk/rhp/encode.go create mode 100644 sdk/rhp/rhp.go diff --git a/.changeset/tiny-cougars-refuse.md b/.changeset/tiny-cougars-refuse.md new file mode 100644 index 000000000..4fc124f2a --- /dev/null +++ b/.changeset/tiny-cougars-refuse.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/sdk': minor +--- + +Updated SDK to latest core changes, updated structure. diff --git a/.env.example b/.env.example index d472cf8e3..57f9624cf 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ GITHUB_TOKEN=secret_token NOTION_TOKEN=secret_token ASSETS=/User/bob/web/assets + +# Make Go use UTC for time formatting +TZ=UTC diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index a2eaacd2e..ab978b107 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -41,8 +41,13 @@ runs: # If source files changed but packages didn't, rebuild from a prior cache. restore-keys: | ${{ runner.os }}-${{ inputs.node_version }}-${{ hashFiles('**/package-lock.json') }}- - - name: Install + - name: Install JavaScript dependencies # could do this since its a ci, but it force rebuilds node_modules # run: npm ci run: npm install shell: bash + - name: Install Go dependencies + run: | + go mod tidy + go mod download + shell: bash diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 513638e2c..cf2aa1bb6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -25,13 +25,16 @@ jobs: - name: Commit lint shell: bash run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} - - name: Lint + - name: Lint TypeScript shell: bash run: npx nx affected --target=lint --parallel=5 - name: Lint Go uses: golangci/golangci-lint-action@v3 with: skip-cache: true + - name: Compile + shell: bash + run: npx nx affected --target=compile --parallel=5 - name: Test shell: bash run: npx nx affected --target=test --parallel=5 diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index a693341e9..087cd0c26 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -32,6 +32,9 @@ jobs: - name: Lint shell: bash run: npx nx run-many --target=lint --all --parallel=5 + - name: Compile + shell: bash + run: npx nx run-many --target=compile --all --parallel=5 - name: Test shell: bash run: npx nx run-many --target=test --all --parallel=5 diff --git a/libs/sdk/project.json b/libs/sdk/project.json index 0cd3464e0..857bc9882 100644 --- a/libs/sdk/project.json +++ b/libs/sdk/project.json @@ -15,7 +15,7 @@ "cache": true, "options": { "commands": [ - "tinygo build -o libs/sdk/src/wasm/resources/sdk.wasm -target wasm ./sdk" + "tinygo build -o libs/sdk/src/resources/sdk.wasm -target wasm ./sdk" ] } }, diff --git a/libs/sdk/src/wasm/global.d.ts b/libs/sdk/src/global.d.ts similarity index 100% rename from libs/sdk/src/wasm/global.d.ts rename to libs/sdk/src/global.d.ts diff --git a/libs/sdk/src/index.ts b/libs/sdk/src/index.ts index 97fc6c9a5..e59a87889 100644 --- a/libs/sdk/src/index.ts +++ b/libs/sdk/src/index.ts @@ -1,2 +1,2 @@ +export * from './init' export * from './types' -export * from './wasm' diff --git a/libs/sdk/src/init.ts b/libs/sdk/src/init.ts new file mode 100644 index 000000000..e30aaacd3 --- /dev/null +++ b/libs/sdk/src/init.ts @@ -0,0 +1,7 @@ +import { getSDK } from './sdk' +import { initWASM } from './wasm' + +export async function initSDK() { + await initWASM() + return getSDK() +} diff --git a/libs/sdk/src/initTest.ts b/libs/sdk/src/initTest.ts new file mode 100644 index 000000000..aee0c1ad5 --- /dev/null +++ b/libs/sdk/src/initTest.ts @@ -0,0 +1,7 @@ +import { getSDK } from './sdk' +import { initWASMTest } from './wasmTest' + +export async function initSDKTest() { + await initWASMTest() + return getSDK() +} diff --git a/libs/sdk/src/js/encoder.ts b/libs/sdk/src/legacy/encoder.ts similarity index 91% rename from libs/sdk/src/js/encoder.ts rename to libs/sdk/src/legacy/encoder.ts index 5dec78a86..30e303f98 100644 --- a/libs/sdk/src/js/encoder.ts +++ b/libs/sdk/src/legacy/encoder.ts @@ -90,20 +90,24 @@ export function decodeString(d: Decoder): string { return s } -export function encodeCurrency(e: Encoder, c: bigint) { +export function encodeCurrency(e: Encoder, c: string) { // currency is 128 bits, little endian - e.dataView.setBigUint64(e.offset, c & BigInt('0xFFFFFFFFFFFFFFFF'), true) + e.dataView.setBigUint64( + e.offset, + BigInt(c) & BigInt('0xFFFFFFFFFFFFFFFF'), + true + ) e.offset += 8 - e.dataView.setBigUint64(e.offset, c >> BigInt(64), true) + e.dataView.setBigUint64(e.offset, BigInt(c) >> BigInt(64), true) e.offset += 8 } -export function decodeCurrency(d: Decoder): bigint { +export function decodeCurrency(d: Decoder): string { 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 + return String((hi << BigInt(64)) | lo) } export function encodeAddress(e: Encoder, a: string) { diff --git a/libs/sdk/src/js/encoding.spec.ts b/libs/sdk/src/legacy/encoding.spec.ts similarity index 74% rename from libs/sdk/src/js/encoding.spec.ts rename to libs/sdk/src/legacy/encoding.spec.ts index bc8220cff..4f9012b8e 100644 --- a/libs/sdk/src/js/encoding.spec.ts +++ b/libs/sdk/src/legacy/encoding.spec.ts @@ -5,16 +5,16 @@ import { decodeHostSettings, } from './encoding' import { newEncoder, newDecoder } from './encoder' -import { HostPrices, HostSettings } from '../types' +import { HostPrices, HostSettings } from './types' describe('encoding', () => { it('encodeHostPrices', () => { const hostPrices: HostPrices = { - contractPrice: BigInt(1000000000), - collateral: BigInt(2000000000), - storagePrice: BigInt(3000000000), - ingressPrice: BigInt(4000000000), - egressPrice: BigInt(5000000000), + contractPrice: '1000000000', + collateral: '2000000000', + storagePrice: '3000000000', + ingressPrice: '4000000000', + egressPrice: '5000000000', tipHeight: 450_000, validUntil: '2022-12-31T00:00:00.000Z', signature: @@ -30,18 +30,18 @@ describe('encoding', () => { it('encodeHostSettings', () => { const prices: HostPrices = { - contractPrice: BigInt(1000000000), - collateral: BigInt(2000000000), - storagePrice: BigInt(3000000000), - ingressPrice: BigInt(4000000000), - egressPrice: BigInt(5000000000), + contractPrice: '1000000000', + collateral: '2000000000', + storagePrice: '3000000000', + ingressPrice: '4000000000', + egressPrice: '5000000000', tipHeight: 450_000, validUntil: '2022-12-31T00:00:00.000Z', signature: 'abcd567890123456789012345678901234567890123456789012345678901234', } const hostSettings: HostSettings = { - version: '123', + version: new Uint8Array([1, 2, 3]), netAddresses: [ { protocol: 'protocol1', address: 'address1longer' }, { protocol: 'protocol2longer', address: 'address2' }, @@ -49,7 +49,7 @@ describe('encoding', () => { // 32 bytes walletAddress: '12345678901234567890123456789012', acceptingContracts: true, - maxCollateral: BigInt(1000000000), + maxCollateral: '1000000000', maxDuration: 100, remainingStorage: 100, totalStorage: 100, diff --git a/libs/sdk/src/js/encoding.ts b/libs/sdk/src/legacy/encoding.ts similarity index 93% rename from libs/sdk/src/js/encoding.ts rename to libs/sdk/src/legacy/encoding.ts index 12020cacb..029f14fc3 100644 --- a/libs/sdk/src/js/encoding.ts +++ b/libs/sdk/src/legacy/encoding.ts @@ -3,13 +3,11 @@ import { decodeCurrency, encodeAddress, encodeBoolean, - encodeBytes, encodeString, encodeUint64, encodeLengthPrefix, decodeAddress, decodeBoolean, - decodeBytes, decodeString, decodeUint64, decodeLengthPrefix, @@ -19,8 +17,10 @@ import { decodeTime, Encoder, Decoder, + decodeUint8Array, + encodeUint8Array, } from './encoder' -import { HostPrices, HostSettings, NetAddress } from '../types' +import { HostPrices, HostSettings, NetAddress } from './types' export function encodeHostPrices(e: Encoder, hostPrices: HostPrices) { encodeCurrency(e, hostPrices.contractPrice) @@ -69,7 +69,7 @@ export function decodeNetAddress(d: Decoder): NetAddress { } export function encodeHostSettings(e: Encoder, hostSettings: HostSettings) { - encodeBytes(e, hostSettings.version) + encodeUint8Array(e, hostSettings.version) encodeLengthPrefix(e, hostSettings.netAddresses.length) for (let i = 0; i < hostSettings.netAddresses.length; i++) { encodeNetAddress(e, hostSettings.netAddresses[i]) @@ -84,7 +84,7 @@ export function encodeHostSettings(e: Encoder, hostSettings: HostSettings) { } export function decodeHostSettings(d: Decoder): HostSettings { - const version = decodeBytes(d, 3) + const version = decodeUint8Array(d, 3) const netAddresses = [] const length = decodeLengthPrefix(d) for (let i = 0; i < length; i++) { diff --git a/libs/sdk/src/js/example.ts b/libs/sdk/src/legacy/example.ts similarity index 100% rename from libs/sdk/src/js/example.ts rename to libs/sdk/src/legacy/example.ts diff --git a/libs/sdk/src/js/rpc.spec.ts b/libs/sdk/src/legacy/rpc.spec.ts similarity index 94% rename from libs/sdk/src/js/rpc.spec.ts rename to libs/sdk/src/legacy/rpc.spec.ts index d6149c5d1..dc359648c 100644 --- a/libs/sdk/src/js/rpc.spec.ts +++ b/libs/sdk/src/legacy/rpc.spec.ts @@ -9,7 +9,7 @@ import { RPCSettingsResponse, RPCWriteSectorRequest, RPCWriteSectorResponse, -} from '../types' +} from './types' import { decodeRpcRequestReadSector, decodeRpcRequestSettings, @@ -26,18 +26,18 @@ import { } from './rpc' const prices: HostPrices = { - contractPrice: BigInt(1000000000), - collateral: BigInt(2000000000), - storagePrice: BigInt(3000000000), - ingressPrice: BigInt(4000000000), - egressPrice: BigInt(5000000000), + contractPrice: '1000000000', + collateral: '2000000000', + storagePrice: '3000000000', + ingressPrice: '4000000000', + egressPrice: '5000000000', tipHeight: 450_000, validUntil: '2022-12-31T00:00:00.000Z', signature: 'abcd567890123456789012345678901234567890123456789012345678901234', } const hostSettings: HostSettings = { - version: '123', + version: new Uint8Array([1, 2, 3]), netAddresses: [ { protocol: 'protocol1', address: 'address1longer' }, { protocol: 'protocol2longer', address: 'address2' }, @@ -45,7 +45,7 @@ const hostSettings: HostSettings = { // 32 bytes walletAddress: '12345678901234567890123456789012', acceptingContracts: true, - maxCollateral: BigInt(1000000000), + maxCollateral: '1000000000', maxDuration: 100, remainingStorage: 100, totalStorage: 100, diff --git a/libs/sdk/src/js/rpc.ts b/libs/sdk/src/legacy/rpc.ts similarity index 99% rename from libs/sdk/src/js/rpc.ts rename to libs/sdk/src/legacy/rpc.ts index 683070a9f..8fc9919dc 100644 --- a/libs/sdk/src/js/rpc.ts +++ b/libs/sdk/src/legacy/rpc.ts @@ -27,7 +27,7 @@ import { RPCSettingsResponse, RPCWriteSectorRequest, RPCWriteSectorResponse, -} from '../types' +} from './types' // NOTE: This JavaScript RPC and encoding implementations is not currently used // and may be incomplete or incorrect. It was written as a comparison to the WASM diff --git a/libs/sdk/src/js/transport.ts b/libs/sdk/src/legacy/transport.ts similarity index 99% rename from libs/sdk/src/js/transport.ts rename to libs/sdk/src/legacy/transport.ts index 043887aa7..cfd959907 100644 --- a/libs/sdk/src/js/transport.ts +++ b/libs/sdk/src/legacy/transport.ts @@ -17,7 +17,7 @@ import { RPCReadSector, RPCWriteSector, RPCSettings, -} from '../types' +} from './types' export class WebTransportClient { private transport!: WebTransport diff --git a/libs/sdk/src/legacy/types.ts b/libs/sdk/src/legacy/types.ts new file mode 100644 index 000000000..617730374 --- /dev/null +++ b/libs/sdk/src/legacy/types.ts @@ -0,0 +1,79 @@ +type Currency = string +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: Uint8Array // 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 diff --git a/libs/sdk/src/wasm/resources/.gitkeep b/libs/sdk/src/resources/.gitkeep similarity index 100% rename from libs/sdk/src/wasm/resources/.gitkeep rename to libs/sdk/src/resources/.gitkeep diff --git a/libs/sdk/src/sdk.ts b/libs/sdk/src/sdk.ts new file mode 100644 index 000000000..0cd01daac --- /dev/null +++ b/libs/sdk/src/sdk.ts @@ -0,0 +1,11 @@ +import { WebTransportClient } from './transport' +import { WASM } from './types' + +export function getSDK() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const wasm = (global as any).sia as WASM + return { + wasm, + WebTransportClient, + } +} diff --git a/libs/sdk/src/transport.ts b/libs/sdk/src/transport.ts new file mode 100644 index 000000000..eba730027 --- /dev/null +++ b/libs/sdk/src/transport.ts @@ -0,0 +1,135 @@ +import { + RPCReadSectorResponse, + RPCSettingsResponse, + RPCWriteSectorResponse, + RPCReadSectorRequest, + RPCWriteSectorRequest, + RPC, + RPCReadSector, + RPCWriteSector, + RPCSettings, +} from './types' +import { WASM } from './types' + +export class WebTransportClient { + #url: string + #cert: string + #wasm: WASM + + #transport!: WebTransport + + constructor(url: string, cert: string, wasm: WASM) { + this.#url = url + this.#cert = cert + this.#wasm = wasm + } + + 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: (data: T['request']) => { rpc?: Uint8Array; error?: string }, + decodeFn: (rpc: Uint8Array) => { data?: T['response']; error?: string } + ): 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 { rpc, error } = encodeFn(rpcRequest) + if (!rpc || error) { + throw new Error(error) + } + await writer.write(rpc) + await writer.close() + + return this.handleIncomingData(stream, decodeFn) + } catch (e) { + console.error('sendRequest', e) + throw e + } + } + + private async handleIncomingData( + stream: WebTransportBidirectionalStream, + decodeFn: (rpc: Uint8Array) => { data?: T['response']; error?: string } + ): 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() + const { data, error } = decodeFn(value) + if (!data || error) { + throw new Error(error) + } + return data + } catch (e) { + console.error('handleIncomingData', e) + throw e + } + } + + async sendReadSectorRequest( + readSector: RPCReadSectorRequest + ): Promise { + return this.sendRequest( + readSector, + this.#wasm.rhp.encodeReadSectorRequest, + this.#wasm.rhp.decodeReadSectorResponse + ) + } + + async sendWriteSectorRequest( + writeSector: RPCWriteSectorRequest + ): Promise { + return this.sendRequest( + writeSector, + this.#wasm.rhp.encodeWriteSectorRequest, + this.#wasm.rhp.decodeWriteSectorResponse + ) + } + + async sendRPCSettingsRequest(): Promise { + return this.sendRequest( + undefined, + this.#wasm.rhp.encodeSettingsRequest, + this.#wasm.rhp.decodeSettingsResponse + ) + } +} + +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 +} diff --git a/libs/sdk/src/types.ts b/libs/sdk/src/types.ts index 3129f33ae..87e64ef04 100644 --- a/libs/sdk/src/types.ts +++ b/libs/sdk/src/types.ts @@ -1,8 +1,15 @@ -type Currency = bigint +type Currency = string type Signature = string type Address = string type Hash256 = string // 32 bytes -type AccountID = string // 16 bytes +type PrivateKey = string +type PublicKey = string // 32 bytes + +type AccountToken = { + account: PublicKey + validUntil: string + signature: Signature +} export type HostPrices = { contractPrice: Currency @@ -21,7 +28,7 @@ export type NetAddress = { } export type HostSettings = { - version: string // 3 bytes + version: [number, number, number] // 3 bytes netAddresses: NetAddress[] walletAddress: Address // 32 bytes acceptingContracts: boolean @@ -45,7 +52,7 @@ export type RPCSettings = { export type RPCReadSectorRequest = { prices: HostPrices - accountId: AccountID // 16 bytes + token: AccountToken root: Hash256 // 32 bytes - types.Hash256 offset: number // uint64 length: number // uint64 @@ -53,7 +60,7 @@ export type RPCReadSectorRequest = { export type RPCReadSectorResponse = { proof: Hash256[] // 32 bytes each - types.Hash256 - sector: Uint8Array // []byte + sector: string // 4MiB sector, Go marshaling expects a base64-encoded representation of a byte array } export type RPCReadSector = { @@ -63,8 +70,8 @@ export type RPCReadSector = { export type RPCWriteSectorRequest = { prices: HostPrices - accountId: AccountID // 16 bytes - sector: Uint8Array // []byte - extended to SectorSize by host + token: AccountToken + sector: string // 4MiB sector, Go marshaling expects a base64-encoded representation of a byte array } export type RPCWriteSectorResponse = { @@ -77,3 +84,66 @@ export type RPCWriteSector = { } export type RPC = RPCSettings | RPCReadSector | RPCWriteSector + +export type WASM = { + rhp: { + generateAccount: () => { + data?: { + privateKey: PrivateKey + account?: PublicKey + } + error?: string + } + // settings + encodeSettingsRequest: (data: RPCSettingsRequest) => { + rpc?: Uint8Array + error?: string + } + decodeSettingsRequest: (rpc: Uint8Array) => { + data?: Record + error?: string + } + encodeSettingsResponse: (data: RPCSettingsResponse) => { + rpc?: Uint8Array + error?: string + } + decodeSettingsResponse: (rpc: Uint8Array) => { + data?: RPCSettingsResponse + error?: string + } + // read sector + encodeReadSectorRequest: (data: RPCReadSectorRequest) => { + rpc?: Uint8Array + error?: string + } + decodeReadSectorRequest: (rpc: Uint8Array) => { + data?: RPCReadSectorRequest + error?: string + } + encodeReadSectorResponse: (data: RPCReadSectorResponse) => { + rpc?: Uint8Array + error?: string + } + decodeReadSectorResponse: (rpc: Uint8Array) => { + data?: RPCReadSectorResponse + error?: string + } + // read sector + encodeWriteSectorRequest: (data: RPCWriteSectorRequest) => { + rpc?: Uint8Array + error?: string + } + decodeWriteSectorRequest: (rpc: Uint8Array) => { + data?: RPCWriteSectorRequest + error?: string + } + encodeWriteSectorResponse: (data: RPCWriteSectorResponse) => { + rpc?: Uint8Array + error?: string + } + decodeWriteSectorResponse: (rpc: Uint8Array) => { + data?: RPCWriteSectorResponse + error?: string + } + } +} diff --git a/libs/sdk/src/wasm/utils/wasm_exec.d.ts b/libs/sdk/src/utils/wasm_exec.d.ts similarity index 100% rename from libs/sdk/src/wasm/utils/wasm_exec.d.ts rename to libs/sdk/src/utils/wasm_exec.d.ts diff --git a/libs/sdk/src/wasm/utils/wasm_exec.js b/libs/sdk/src/utils/wasm_exec.js similarity index 100% rename from libs/sdk/src/wasm/utils/wasm_exec.js rename to libs/sdk/src/utils/wasm_exec.js diff --git a/libs/sdk/src/wasm/utils/wasm_exec_tinygo.js b/libs/sdk/src/utils/wasm_exec_tinygo.js similarity index 100% rename from libs/sdk/src/wasm/utils/wasm_exec_tinygo.js rename to libs/sdk/src/utils/wasm_exec_tinygo.js diff --git a/libs/sdk/src/wasm.spec.ts b/libs/sdk/src/wasm.spec.ts new file mode 100644 index 000000000..7c99ac155 --- /dev/null +++ b/libs/sdk/src/wasm.spec.ts @@ -0,0 +1,290 @@ +import { + HostPrices, + HostSettings, + RPCReadSectorRequest, + RPCReadSectorResponse, + RPCSettingsResponse, + RPCWriteSectorRequest, + RPCWriteSectorResponse, +} from './types' +import { initSDKTest } from './initTest' + +describe('wasm', () => { + describe('rhp', () => { + describe('generateAccout', () => { + it('works', async () => { + const sdk = await initSDKTest() + const { data, error } = sdk.wasm.rhp.generateAccount() + expect(error).toBeUndefined() + expect(data?.privateKey).toBeDefined() + expect(data?.privateKey?.length).toBeGreaterThan(40) + expect(data?.account).toBeDefined() + expect(data?.account?.length).toBeGreaterThan(40) + }) + }) + describe('settings', () => { + describe('request', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const encode = sdk.wasm.rhp.encodeSettingsRequest() + expect(encode.rpc).toBeDefined() + expect(encode.error).not.toBeDefined() + if (!encode.rpc) { + throw new Error('rpc is undefined') + } + const decode = sdk.wasm.rhp.decodeSettingsRequest(encode.rpc) + expect(decode.data).toEqual({}) + expect(decode.error).toBeUndefined() + }) + }) + describe('response', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const json = getSampleRPCSettingsResponse() + const encode = sdk.wasm.rhp.encodeSettingsResponse(json) + expect(encode.rpc).toBeDefined() + expect(encode.rpc?.length).toEqual(323) + expect(encode.error).toBeUndefined() + if (!encode.rpc) { + throw new Error('rpc is undefined') + } + const decode = sdk.wasm.rhp.decodeSettingsResponse(encode.rpc) + expect(decode.data).toEqual(json) + expect(decode.error).toBeUndefined() + }) + it('encode error', async () => { + const sdk = await initSDKTest() + const json = { + settings: { + walletAddress: 'invalid', + }, + } as RPCSettingsResponse + const encode = sdk.wasm.rhp.encodeSettingsResponse(json) + expect(encode.rpc).toBeUndefined() + expect(encode.error).toEqual( + "decoding addr: failed: encoding/hex: invalid byte: U+0069 'i'" + ) + }) + it('decode error', async () => { + const sdk = await initSDKTest() + const json = getSampleRPCSettingsResponse() + const encode = sdk.wasm.rhp.encodeSettingsResponse(json) + if (!encode.rpc) { + throw new Error('rpc is undefined') + } + // manipulate the valid rpc to make it invalid + encode.rpc.set([1, 1], 30) + const decode = sdk.wasm.rhp.decodeSettingsResponse(encode.rpc) + expect(decode.data).not.toEqual(json) + expect(decode.error).toEqual( + 'encoded object contains invalid length prefix (65806 elems > 11227 bytes left in stream)' + ) + }) + }) + }) + describe('read', () => { + describe('request', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const json: RPCReadSectorRequest = { + token: { + account: + 'acct:1b6793e900df020dc9a43c6df5f5d10dc5793956d44831ca5bbfec659021b75e', + validUntil: '2022-12-31T00:00:00Z', + signature: + 'sig:457256d6a1603bef7fa957a70b5ba96a9def2fea8b4c1483060d7ba5cf8a072cfddf242a1ef033dd7d669c711e846c59cb916f804a03d72d279ffef7e6583404', + }, + root: 'h:457256d6a1603bef7fa957a70b5ba96a9def2fea8b4c1483060d7ba5cf8a072c', + prices: getSampleHostPrices(), + offset: 0, + length: 4, + } + const encode = sdk.wasm.rhp.encodeReadSectorRequest(json) + expect(encode.rpc?.length).toEqual(312) + expect(encode.error).toBeUndefined() + if (!encode.rpc) { + throw new Error('rpc is undefined') + } + const decode = sdk.wasm.rhp.decodeReadSectorRequest(encode.rpc) + expect(decode.data).toEqual(json) + expect(decode.error).toBeUndefined() + }) + it('encode error', async () => { + const sdk = await initSDKTest() + const json: RPCReadSectorRequest = { + token: { + account: 'invalid', + validUntil: '2022-12-31T00:00:00Z', + signature: + 'sig:457256d6a1603bef7fa957a70b5ba96a9def2fea8b4c1483060d7ba5cf8a072cfddf242a1ef033dd7d669c711e846c59cb916f804a03d72d279ffef7e6583404', + }, + root: 'h:457256d6a1603bef7fa957a70b5ba96a9def2fea8b4c1483060d7ba5cf8a072c', + prices: getSampleHostPrices(), + offset: 0, + length: 4, + } + const encode = sdk.wasm.rhp.encodeReadSectorRequest(json) + expect(encode.rpc).toBeUndefined() + expect(encode.error).toEqual( + "decoding acct: failed: encoding/hex: invalid byte: U+0069 'i'" + ) + }) + }) + describe('response', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const json: RPCReadSectorResponse = { + proof: [ + 'h:457256d6a1603bef7fa957a70b5ba96a9def2fea8b4c1483060d7ba5cf8a072c', + ], + sector: 'AQID', + } + const encode = sdk.wasm.rhp.encodeReadSectorResponse(json) + expect(encode.rpc?.toString()).toEqual( + [ + 0, 1, 0, 0, 0, 0, 0, 0, 0, 69, 114, 86, 214, 161, 96, 59, 239, + 127, 169, 87, 167, 11, 91, 169, 106, 157, 239, 47, 234, 139, 76, + 20, 131, 6, 13, 123, 165, 207, 138, 7, 44, 3, 0, 0, 0, 0, 0, 0, 0, + 1, 2, 3, + ].toString() + ) + expect(encode.error).toBeUndefined() + if (!encode.rpc) { + throw new Error('rpc is undefined') + } + const decode = sdk.wasm.rhp.decodeReadSectorResponse(encode.rpc) + expect(decode.data).toEqual(json) + expect(decode.error).toBeUndefined() + }) + it('encode error', async () => { + const sdk = await initSDKTest() + const json: RPCReadSectorResponse = { + proof: ['invalid'], + sector: 'AQID', + } + const encode = sdk.wasm.rhp.encodeReadSectorResponse(json) + expect(encode.rpc).toBeUndefined() + expect(encode.error).toEqual( + 'decoding h: failed: unexpected EOF' + ) + }) + }) + }) + describe('write', () => { + describe('request', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const json: RPCWriteSectorRequest = { + token: { + account: + 'acct:1b6793e900df020dc9a43c6df5f5d10dc5793956d44831ca5bbfec659021b75e', + validUntil: '2022-12-31T00:00:00Z', + signature: + 'sig:457256d6a1603bef7fa957a70b5ba96a9def2fea8b4c1483060d7ba5cf8a072cfddf242a1ef033dd7d669c711e846c59cb916f804a03d72d279ffef7e6583404', + }, + sector: 'AQID', + prices: getSampleHostPrices(), + } + const encode = sdk.wasm.rhp.encodeWriteSectorRequest(json) + expect(encode.rpc?.length).toEqual(275) + expect(encode.error).toBeUndefined() + if (!encode.rpc) { + throw new Error('rpc is undefined') + } + const decode = sdk.wasm.rhp.decodeWriteSectorRequest(encode.rpc) + expect(decode.data).toEqual(json) + expect(decode.error).toBeUndefined() + }) + it('encode error', async () => { + const sdk = await initSDKTest() + const json = { + token: { + account: 'invalid', + validUntil: '2022-12-31T00:00:00Z', + signature: + 'sig:457256d6a1603bef7fa957a70b5ba96a9def2fea8b4c1483060d7ba5cf8a072cfddf242a1ef033dd7d669c711e846c59cb916f804a03d72d279ffef7e6583404', + }, + sector: 'AQID', + prices: getSampleHostPrices(), + } as RPCWriteSectorRequest + const encode = sdk.wasm.rhp.encodeWriteSectorRequest(json) + expect(encode.rpc).toBeUndefined() + expect(encode.error).toEqual( + "decoding acct: failed: encoding/hex: invalid byte: U+0069 'i'" + ) + }) + }) + describe('response', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const json: RPCWriteSectorResponse = { + root: 'h:457256d6a1603bef7fa957a70b5ba96a9def2fea8b4c1483060d7ba5cf8a072c', + } + const encode = sdk.wasm.rhp.encodeWriteSectorResponse(json) + expect(encode.rpc?.toString()).toEqual( + [ + 0, 69, 114, 86, 214, 161, 96, 59, 239, 127, 169, 87, 167, 11, 91, + 169, 106, 157, 239, 47, 234, 139, 76, 20, 131, 6, 13, 123, 165, + 207, 138, 7, 44, + ].toString() + ) + expect(encode.error).toBeUndefined() + if (!encode.rpc) { + throw new Error('rpc is undefined') + } + const decode = sdk.wasm.rhp.decodeWriteSectorResponse(encode.rpc) + expect(decode.data).toEqual(json) + expect(decode.error).toBeUndefined() + }) + it('encode error', async () => { + const sdk = await initSDKTest() + const json = { + root: 'invalid', + } as RPCWriteSectorResponse + const encode = sdk.wasm.rhp.encodeWriteSectorResponse(json) + expect(encode.rpc).toBeUndefined() + expect(encode.error).toEqual( + 'decoding h: failed: unexpected EOF' + ) + }) + }) + }) + }) +}) + +function getSampleHostPrices(): HostPrices { + return { + contractPrice: '1000000000', + collateral: '2000000000', + storagePrice: '3000000000', + ingressPrice: '4000000000', + egressPrice: '5000000000', + tipHeight: 450_000, + validUntil: '2022-12-31T00:00:00Z', + signature: + 'sig:457256d6a1603bef7fa957a70b5ba96a9def2fea8b4c1483060d7ba5cf8a072cfddf242a1ef033dd7d669c711e846c59cb916f804a03d72d279ffef7e6583404', + } +} + +function getSampleRPCSettingsResponse(): RPCSettingsResponse { + const prices = getSampleHostPrices() + const settings: HostSettings = { + version: [1, 2, 3], + netAddresses: [ + { protocol: 'protocol1', address: 'address1longer' }, + { protocol: 'protocol2longer', address: 'address2' }, + ], + // 32 bytes + walletAddress: + 'addr:eec8160897cf7058332040675d120c008dc32d96925e9b32a812b646e31676d7d52c118cad2c', + acceptingContracts: true, + maxCollateral: '1000000000', + maxDuration: 100, + remainingStorage: 100, + totalStorage: 100, + prices, + } + return { + settings, + } +} diff --git a/libs/sdk/src/wasm.ts b/libs/sdk/src/wasm.ts new file mode 100644 index 000000000..2e38dcce8 --- /dev/null +++ b/libs/sdk/src/wasm.ts @@ -0,0 +1,12 @@ +import './utils/wasm_exec_tinygo' +import wasm from './resources/sdk.wasm' + +export async function initWASM(): Promise { + try { + const go = new window.Go() + const source = await wasm(go.importObject) + await go.run(source.instance) + } catch (e) { + throw new Error(`failed to initialize WASM: ${(e as Error).message}`) + } +} diff --git a/libs/sdk/src/wasm/index.ts b/libs/sdk/src/wasm/index.ts deleted file mode 100644 index db4fb9e53..000000000 --- a/libs/sdk/src/wasm/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import './utils/wasm_exec_tinygo' -import wasm from './resources/sdk.wasm' -import { SDK } from './types' - -export function getSDK() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (global as any).sdk as SDK -} - -export async function initSDK(): Promise<{ sdk?: SDK; error?: string }> { - try { - const go = new window.Go() - const source = await wasm(go.importObject) - await go.run(source.instance) - return { - sdk: getSDK(), - } - } catch (e) { - console.log(e) - return { - error: (e as Error).message, - } - } -} diff --git a/libs/sdk/src/wasm/types.ts b/libs/sdk/src/wasm/types.ts deleted file mode 100644 index ac7041376..000000000 --- a/libs/sdk/src/wasm/types.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - RPCReadSectorRequest, - RPCReadSectorResponse, - RPCSettingsRequest, - RPCSettingsResponse, - RPCWriteSectorRequest, - RPCWriteSectorResponse, -} from '../types' - -export type SDK = { - rhp: { - // settings - encodeSettingsRequest: () => { - rpc?: Uint8Array - error?: string - } - decodeSettingsRequest: () => { - data?: RPCSettingsRequest - error?: string - } - encodeSettingsResponse: () => { - rpc?: Uint8Array - error?: string - } - decodeSettingsResponse: () => { - data?: RPCSettingsResponse - error?: string - } - // read sector - encodeReadSectorRequest: () => { - rpc?: Uint8Array - error?: string - } - decodeReadSectorRequest: () => { - data?: RPCReadSectorRequest - error?: string - } - encodeReadSectorResponse: () => { - rpc?: Uint8Array - error?: string - } - decodeReadSectorResponse: () => { - data?: RPCReadSectorResponse - error?: string - } - // read sector - encodeWriteSectorRequest: () => { - rpc?: Uint8Array - error?: string - } - decodeWriteSectorRequest: () => { - data?: RPCWriteSectorRequest - error?: string - } - encodeWriteSectorResponse: () => { - rpc?: Uint8Array - error?: string - } - decodeWriteSectorResponse: () => { - data?: RPCWriteSectorResponse - error?: string - } - } - generateAccountID: () => { - accountID?: string - error?: string - } -} diff --git a/libs/sdk/src/wasmTest.ts b/libs/sdk/src/wasmTest.ts new file mode 100644 index 000000000..b7f53c3c6 --- /dev/null +++ b/libs/sdk/src/wasmTest.ts @@ -0,0 +1,14 @@ +import './utils/wasm_exec_tinygo' +import fs from 'fs' +import { join } from 'path' + +export async function initWASMTest(): Promise { + try { + const wasm = fs.readFileSync(join(__dirname, 'resources/sdk.wasm')) + const go = new window.Go() + const source = await WebAssembly.instantiate(wasm, go.importObject) + go.run(source.instance) + } catch (e) { + throw new Error(`failed to initialize WASM: ${(e as Error).message}`) + } +} diff --git a/sdk/encode/encode.go b/sdk/encode/encode.go deleted file mode 100644 index 88427850b..000000000 --- a/sdk/encode/encode.go +++ /dev/null @@ -1,69 +0,0 @@ -package encode - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "syscall/js" - - "go.sia.tech/core/types" -) - -func MarshalStruct(obj interface{}) (js.Value, error) { - jsonData, err := json.Marshal(obj) - if err != nil { - return js.Null(), err - } - jsObject := js.Global().Get("JSON").Call("parse", string(jsonData)) - return jsObject, nil -} - -func UnmarshalStruct(jsValue js.Value, target interface{}) error { - jsonSettings := js.Global().Get("JSON").Call("stringify", jsValue).String() - return json.Unmarshal([]byte(jsonSettings), target) -} - -func UnmarshalUint8Array(jsArray js.Value) ([]uint8, error) { - if jsArray.Type() != js.TypeObject || jsArray.Get("constructor").Get("name").String() != "Uint8Array" { - return nil, fmt.Errorf("expected Uint8Array") - } - length := jsArray.Length() - goBytes := make([]byte, length) - js.CopyBytesToGo(goBytes, jsArray) - return goBytes, nil -} - -func MarshalUint8Array(bytes []byte) js.Value { - jsArray := js.Global().Get("Uint8Array").New(len(bytes)) - js.CopyBytesToJS(jsArray, bytes) - return jsArray -} - -type Encodable interface { - EncodeTo(encoder *types.Encoder) -} - -func EncodeRPC(encodable Encodable) (js.Value, error) { - var buffer bytes.Buffer - encoder := types.NewEncoder(&buffer) - encodable.EncodeTo(encoder) - if err := encoder.Flush(); err != nil { - return js.Null(), err - } - encoded := buffer.Bytes() - return MarshalUint8Array(encoded), nil -} - -type Decodable interface { - DecodeFrom(decoder *types.Decoder) -} - -func DecodeRPC(encodedData []byte, decodable Decodable) (js.Value, error) { - buffer := bytes.NewBuffer(encodedData) - lr := io.LimitedReader{R: buffer, N: int64(len(encodedData))} - decoder := types.NewDecoder(lr) - // TODO: add error handling - decodable.DecodeFrom(decoder) - return MarshalStruct(decodable) -} diff --git a/sdk/go.mod b/sdk/go.mod index 10a19f65b..dee208d71 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -3,7 +3,7 @@ module go.sia.tech/web/sdk go 1.20 require ( - go.sia.tech/core v0.2.2-0.20240202170315-3e6d0eca7490 + go.sia.tech/core v0.2.2-0.20240229154321-d97c1d5b2172 go.sia.tech/web v0.0.0-20230628194305-c6e1696bad89 ) diff --git a/sdk/go.sum b/sdk/go.sum index 3c736b17f..7470405a9 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -4,6 +4,12 @@ go.sia.tech/core v0.1.12-0.20230807160906-ad76cac3058f h1:ZPWqj1RphySPSdVvhW09VY go.sia.tech/core v0.1.12-0.20230807160906-ad76cac3058f/go.mod h1:D17UWSn99SEfQnEaR9G9n6Kz9+BwqMoUgZ6Cl424LsQ= go.sia.tech/core v0.2.2-0.20240202170315-3e6d0eca7490 h1:pfmR0dva8GQ1Oxb5VpF7JWfDuWgmfbgZKK8oAKWT24g= go.sia.tech/core v0.2.2-0.20240202170315-3e6d0eca7490/go.mod h1:3EoY+rR78w1/uGoXXVqcYdwSjSJKuEMI5bL7WROA27Q= +go.sia.tech/core v0.2.2-0.20240209000234-ef2053eea93e h1:Bps0MIQGHkWpyG4aPnG7G2v2bvUajI5Ly3zXPkQ1RFo= +go.sia.tech/core v0.2.2-0.20240209000234-ef2053eea93e/go.mod h1:3EoY+rR78w1/uGoXXVqcYdwSjSJKuEMI5bL7WROA27Q= +go.sia.tech/core v0.2.2-0.20240229150213-044552edecf7 h1:YDurKmoU+oVzlZgAcaDSTrdr8mpebMW9oJieXrhFh6A= +go.sia.tech/core v0.2.2-0.20240229150213-044552edecf7/go.mod h1:3EoY+rR78w1/uGoXXVqcYdwSjSJKuEMI5bL7WROA27Q= +go.sia.tech/core v0.2.2-0.20240229154321-d97c1d5b2172 h1:uET7VyK5mz02bsicyNeoEut/RarvmQXAvXgUqZ1YJoE= +go.sia.tech/core v0.2.2-0.20240229154321-d97c1d5b2172/go.mod h1:3EoY+rR78w1/uGoXXVqcYdwSjSJKuEMI5bL7WROA27Q= go.sia.tech/web v0.0.0-20230628194305-c6e1696bad89 h1:wB/JRFeTEs6gviB6k7QARY7Goh54ufkADsdBdn0ZhRo= go.sia.tech/web v0.0.0-20230628194305-c6e1696bad89/go.mod h1:RKODSdOmR3VtObPAcGwQqm4qnqntDVFylbvOBbWYYBU= golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= diff --git a/sdk/main.go b/sdk/main.go index b00fb1b0e..60c4c6b6e 100644 --- a/sdk/main.go +++ b/sdk/main.go @@ -1,34 +1,40 @@ package main import ( - "fmt" "syscall/js" + + "go.sia.tech/web/sdk/rhp" ) func main() { - fmt.Println("WASM SDK: init") - js.Global().Set("sdk", map[string]interface{}{ - "generateAccountID": js.FuncOf(generateAccountID), + js.Global().Set("sia", map[string]interface{}{ "rhp": map[string]interface{}{ - // test - "encodeSettings": js.FuncOf(encodeSettings), - "decodeSettings": js.FuncOf(decodeSettings), - // // settings - // "encodeSettingsRequest": js.FuncOf(encodeSettingsRequest), - // "decodeSettingsRequest": js.FuncOf(decodeSettingsRequest), - // "encodeSettingsResponse": js.FuncOf(encodeSettingsResponse), - // "decodeSettingsResponse": js.FuncOf(decodeSettingsResponse), - // // read sector - // "encodeReadSectorRequest": js.FuncOf(encodeReadSectorRequest), - // "decodeReadSectorRequest": js.FuncOf(decodeReadSectorRequest), - // "encodeReadSectorResponse": js.FuncOf(encodeReadSectorResponse), - // "decodeReadSectorResponse": js.FuncOf(decodeReadSectorResponse), - // // write sector - // "encodeWriteSectorRequest": js.FuncOf(encodeWriteSectorRequest), - // "decodeWriteSectorRequest": js.FuncOf(decodeWriteSectorRequest), - // "encodeWriteSectorResponse": js.FuncOf(encodeWriteSectorResponse), - // "decodeWriteSectorResponse": js.FuncOf(decodeWriteSectorResponse), + "generateAccount": js.FuncOf(rhp.GenerateAccount), + // settings + "encodeSettingsRequest": js.FuncOf(rhp.EncodeSettingsRequest), + "decodeSettingsRequest": js.FuncOf(rhp.DecodeSettingsRequest), + "encodeSettingsResponse": js.FuncOf(rhp.EncodeSettingsResponse), + "decodeSettingsResponse": js.FuncOf(rhp.DecodeSettingsResponse), + // read sector + "encodeReadSectorRequest": js.FuncOf(rhp.EncodeReadSectorRequest), + "decodeReadSectorRequest": js.FuncOf(rhp.DecodeReadSectorRequest), + "encodeReadSectorResponse": js.FuncOf(rhp.EncodeReadSectorResponse), + "decodeReadSectorResponse": js.FuncOf(rhp.DecodeReadSectorResponse), + // write sector + "encodeWriteSectorRequest": js.FuncOf(rhp.EncodeWriteSectorRequest), + "decodeWriteSectorRequest": js.FuncOf(rhp.DecodeWriteSectorRequest), + "encodeWriteSectorResponse": js.FuncOf(rhp.EncodeWriteSectorResponse), + "decodeWriteSectorResponse": js.FuncOf(rhp.DecodeWriteSectorResponse), }, + // "wallet": map[string]interface{}{ + // "newSeedPhrase": js.FuncOf(newSeedPhrase), + // "seedFromPhrase": js.FuncOf(seedFromPhrase), + // "privateKeyFromSeed": js.FuncOf(privateKeyFromSeed), + // "unlockConditionsFromSeed": js.FuncOf(unlockConditionsFromSeed), + // "publicKeyAndAddressFromSeed": js.FuncOf(publicKeyAndAddressFromSeed), + // "encodeTransaction": js.FuncOf(encodeTransaction), + // "signTransaction": js.FuncOf(signTransaction), + // }, }) c := make(chan bool, 1) <-c diff --git a/sdk/marshal/marshal.go b/sdk/marshal/marshal.go new file mode 100644 index 000000000..4e57071d7 --- /dev/null +++ b/sdk/marshal/marshal.go @@ -0,0 +1,37 @@ +package marshal + +import ( + "encoding/json" + "fmt" + "syscall/js" +) + +func MarshalStruct(obj interface{}) (js.Value, error) { + jsonData, err := json.Marshal(obj) + if err != nil { + return js.Null(), err + } + jsObject := js.Global().Get("JSON").Call("parse", string(jsonData)) + return jsObject, nil +} + +func UnmarshalStruct(jsValue js.Value, target interface{}) error { + jsonData := js.Global().Get("JSON").Call("stringify", jsValue).String() + return json.Unmarshal([]byte(jsonData), target) +} + +func UnmarshalUint8Array(jsArray js.Value) ([]uint8, error) { + if jsArray.Type() != js.TypeObject || jsArray.Get("constructor").Get("name").String() != "Uint8Array" { + return nil, fmt.Errorf("expected Uint8Array") + } + length := jsArray.Length() + goBytes := make([]byte, length) + js.CopyBytesToGo(goBytes, jsArray) + return goBytes, nil +} + +func MarshalUint8Array(bytes []byte) js.Value { + jsArray := js.Global().Get("Uint8Array").New(len(bytes)) + js.CopyBytesToJS(jsArray, bytes) + return jsArray +} diff --git a/sdk/other.go b/sdk/other.go deleted file mode 100644 index b03a36964..000000000 --- a/sdk/other.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "encoding/hex" - "syscall/js" - - "go.sia.tech/core/rhp/v4" - "go.sia.tech/web/sdk/utils" -) - -func generateAccountID(this js.Value, args []js.Value) interface{} { - if err := utils.CheckArgs(args); err != nil { - return map[string]any{ - "error": err.Error(), - } - } - - id := rhp.GenerateAccountID() - data := hex.EncodeToString(id[:]) - - return map[string]any{ - "accountID": data, - "error": nil, - } -} diff --git a/sdk/rhp.go b/sdk/rhp.go deleted file mode 100644 index b71c0a778..000000000 --- a/sdk/rhp.go +++ /dev/null @@ -1,276 +0,0 @@ -package main - -import ( - "syscall/js" - - "go.sia.tech/core/rhp/v4" - "go.sia.tech/web/sdk/encode" - "go.sia.tech/web/sdk/utils" -) - -// settings test - -func encodeSettings(this js.Value, args []js.Value) interface{} { - if err := utils.CheckArgs(args, js.TypeObject); err != nil { - return map[string]any{"error": err.Error()} - } - - var r rhp.HostSettings - if err := encode.UnmarshalStruct(args[0], &r); err != nil { - return map[string]any{"error": err.Error()} - } - - rpc, err := encode.EncodeRPC(&r) - if err != nil { - return map[string]any{"error": err.Error()} - } - - return map[string]any{"rpc": rpc} -} - -func decodeSettings(this js.Value, args []js.Value) interface{} { - if err := utils.CheckArgs(args, js.TypeObject); err != nil { - return map[string]any{"error": err.Error()} - } - - hsRpc, err := encode.UnmarshalUint8Array(args[0]) - if err != nil { - return map[string]any{"error": err.Error()} - } - - var r rhp.HostSettings - data, err := encode.DecodeRPC(hsRpc, &r) - if err != nil { - return map[string]any{"error": err.Error()} - } - - return map[string]any{"data": data} -} - -// settings - -// func encodeSettingsRequest(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCSettingsRequest -// if err := encode.UnmarshalStruct(args[0], &r); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// rpc, err := encode.EncodeRPC(&r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"rpc": rpc} -// } - -// func decodeSettingsRequest(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// hsRpc, err := encode.UnmarshalUint8Array(args[0]) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCSettingsRequest -// data, err := encode.DecodeRPC(hsRpc, &r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"data": data} -// } - -// func encodeSettingsResponse(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCSettingsResponse -// if err := encode.UnmarshalStruct(args[0], &r); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// rpc, err := encode.EncodeRPC(&r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"rpc": rpc} -// } - -// func decodeSettingsResponse(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// hsRpc, err := encode.UnmarshalUint8Array(args[0]) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCSettingsResponse -// data, err := encode.DecodeRPC(hsRpc, &r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"data": data} -// } - -// // read sector - -// func encodeReadSectorRequest(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCReadSectorRequest -// if err := encode.UnmarshalStruct(args[0], &r); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// rpc, err := encode.EncodeRPC(&r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"rpc": rpc} -// } - -// func decodeReadSectorRequest(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// hsRpc, err := encode.UnmarshalUint8Array(args[0]) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCReadSectorRequest -// data, err := encode.DecodeRPC(hsRpc, &r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"data": data} -// } - -// func encodeReadSectorResponse(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCReadSectorResponse -// if err := encode.UnmarshalStruct(args[0], &r); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// rpc, err := encode.EncodeRPC(&r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"rpc": rpc} -// } - -// func decodeReadSectorResponse(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// hsRpc, err := encode.UnmarshalUint8Array(args[0]) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCReadSectorResponse -// data, err := encode.DecodeRPC(hsRpc, &r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"data": data} -// } - -// // write sector - -// func encodeWriteSectorRequest(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCWriteSectorRequest -// if err := encode.UnmarshalStruct(args[0], &r); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// rpc, err := encode.EncodeRPC(&r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"rpc": rpc} -// } - -// func decodeWriteSectorRequest(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// hsRpc, err := encode.UnmarshalUint8Array(args[0]) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCWriteSectorRequest -// data, err := encode.DecodeRPC(hsRpc, &r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"data": data} -// } - -// func encodeWriteSectorResponse(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCWriteSectorResponse -// if err := encode.UnmarshalStruct(args[0], &r); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// rpc, err := encode.EncodeRPC(&r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"rpc": rpc} -// } - -// func decodeWriteSectorResponse(this js.Value, args []js.Value) interface{} { -// if err := utils.CheckArgs(args, js.TypeObject); err != nil { -// return map[string]any{"error": err.Error()} -// } - -// hsRpc, err := encode.UnmarshalUint8Array(args[0]) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// var r rhp.RPCWriteSectorResponse -// data, err := encode.DecodeRPC(hsRpc, &r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } - -// return map[string]any{"data": data} -// } diff --git a/sdk/rhp/encode.go b/sdk/rhp/encode.go new file mode 100644 index 000000000..8d89b737e --- /dev/null +++ b/sdk/rhp/encode.go @@ -0,0 +1,67 @@ +package rhp + +import ( + "bytes" + "syscall/js" + + "go.sia.tech/core/rhp/v4" + "go.sia.tech/web/sdk/marshal" +) + +func encodeRPCRequest(data js.Value, req rhp.Request) interface{} { + if data.Type() != js.TypeUndefined { + if err := marshal.UnmarshalStruct(data, &req); err != nil { + return map[string]any{"error": err.Error()} + } + } + buf := bytes.NewBuffer(nil) + if err := rhp.WriteRequest(buf, req); err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"rpc": marshal.MarshalUint8Array(buf.Bytes())} +} + +func decodeRPCRequest(rpcJsData js.Value, res rhp.Request) interface{} { + rpcData, err := marshal.UnmarshalUint8Array(rpcJsData) + if err != nil { + return map[string]any{"error": err.Error()} + } + err = rhp.ReadRequest(bytes.NewReader(rpcData), res) + if err != nil { + return map[string]any{"error": err.Error()} + } + d, err := marshal.MarshalStruct(res) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"data": d} +} + +func encodeRPCResponse(data js.Value, req rhp.Object) interface{} { + if data.Type() != js.TypeUndefined { + if err := marshal.UnmarshalStruct(data, &req); err != nil { + return map[string]any{"error": err.Error()} + } + } + buf := bytes.NewBuffer(nil) + if err := rhp.WriteResponse(buf, req); err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"rpc": marshal.MarshalUint8Array(buf.Bytes())} +} + +func decodeRPCResponse(rpcJsData js.Value, res rhp.Object) interface{} { + rpcData, err := marshal.UnmarshalUint8Array(rpcJsData) + if err != nil { + return map[string]any{"error": err.Error()} + } + err = rhp.ReadResponse(bytes.NewReader(rpcData), res) + if err != nil { + return map[string]any{"error": err.Error()} + } + d, err := marshal.MarshalStruct(res) + if err != nil { + return map[string]any{"error": err.Error()} + } + return map[string]any{"data": d} +} diff --git a/sdk/rhp/rhp.go b/sdk/rhp/rhp.go new file mode 100644 index 000000000..aa2bf19b6 --- /dev/null +++ b/sdk/rhp/rhp.go @@ -0,0 +1,131 @@ +package rhp + +import ( + "encoding/hex" + "syscall/js" + + "go.sia.tech/core/rhp/v4" + "go.sia.tech/web/sdk/utils" +) + +func GenerateAccount(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args); err != nil { + return map[string]any{"error": err.Error()} + } + + pk, a := rhp.GenerateAccount() + + privateKey := hex.EncodeToString(pk) + account := hex.EncodeToString(a[:]) + + return map[string]any{ + "data": map[string]any{ + "privateKey": privateKey, + "account": account, + }, + } +} + +// settings + +func EncodeSettingsRequest(this js.Value, args []js.Value) any { + if err := utils.CheckArgs(args); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCSettingsRequest + return encodeRPCRequest(js.Undefined(), &r) +} + +func DecodeSettingsRequest(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCSettingsRequest + return decodeRPCRequest(args[0], &r) +} + +func EncodeSettingsResponse(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCSettingsResponse + return encodeRPCResponse(args[0], &r) +} + +func DecodeSettingsResponse(this js.Value, args []js.Value) any { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCSettingsResponse + return decodeRPCResponse(args[0], &r) +} + +// read sector + +func EncodeReadSectorRequest(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + + var r rhp.RPCReadSectorRequest + return encodeRPCRequest(args[0], &r) +} + +func DecodeReadSectorRequest(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCReadSectorRequest + return decodeRPCRequest(args[0], &r) +} + +func EncodeReadSectorResponse(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCReadSectorResponse + return encodeRPCResponse(args[0], &r) +} + +func DecodeReadSectorResponse(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCReadSectorResponse + return decodeRPCResponse(args[0], &r) +} + +// write sector + +func EncodeWriteSectorRequest(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCWriteSectorRequest + return encodeRPCRequest(args[0], &r) +} + +func DecodeWriteSectorRequest(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCWriteSectorRequest + return decodeRPCRequest(args[0], &r) +} + +func EncodeWriteSectorResponse(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + + var r rhp.RPCWriteSectorResponse + return encodeRPCResponse(args[0], &r) +} + +func DecodeWriteSectorResponse(this js.Value, args []js.Value) interface{} { + if err := utils.CheckArgs(args, js.TypeObject); err != nil { + return map[string]any{"error": err.Error()} + } + var r rhp.RPCWriteSectorResponse + return decodeRPCResponse(args[0], &r) +}