From 44a84f09ae1718615cc1911f6ff61b913a481434 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Wed, 7 Feb 2024 15:03:21 -0500 Subject: [PATCH] feat: web wasm sdk rhp4 --- .github/actions/setup/action.yml | 5 +- .vscode/settings.json | 6 + README.md | 40 +- apps/renterd/next.config.js | 12 +- go.work | 1 + libs/sdk/.eslintrc.json | 3 +- libs/sdk/.gitignore | 1 + libs/sdk/project.json | 18 +- libs/sdk/rollup.config.js | 9 +- libs/sdk/src/index.ts | 2 +- libs/sdk/src/{ => js}/encoder.ts | 0 libs/sdk/src/{ => js}/encoding.spec.ts | 2 +- libs/sdk/src/{ => js}/encoding.ts | 2 +- libs/sdk/src/{ => js}/example.ts | 0 libs/sdk/src/{ => js}/rpc.spec.ts | 2 +- libs/sdk/src/{ => js}/rpc.ts | 6 +- libs/sdk/src/{ => js}/transport.ts | 20 +- libs/sdk/src/wasm/global.d.ts | 10 + libs/sdk/src/wasm/index.ts | 24 + libs/sdk/src/wasm/resources/.gitkeep | 0 libs/sdk/src/wasm/types.ts | 68 ++ libs/sdk/src/wasm/utils/wasm_exec.d.ts | 10 + libs/sdk/src/wasm/utils/wasm_exec.js | 666 ++++++++++++++++++++ libs/sdk/src/wasm/utils/wasm_exec_tinygo.js | 666 ++++++++++++++++++++ package-lock.json | 77 +++ package.json | 3 +- sdk/encode/encode.go | 69 ++ sdk/go.mod | 15 + sdk/go.sum | 15 + sdk/main.go | 35 + sdk/other.go | 25 + sdk/rhp.go | 276 ++++++++ sdk/utils/utils.go | 39 ++ tsconfig.base.json | 2 +- 34 files changed, 2077 insertions(+), 52 deletions(-) create mode 100644 libs/sdk/.gitignore rename libs/sdk/src/{ => js}/encoder.ts (100%) rename libs/sdk/src/{ => js}/encoding.spec.ts (97%) rename libs/sdk/src/{ => js}/encoding.ts (97%) rename libs/sdk/src/{ => js}/example.ts (100%) rename libs/sdk/src/{ => js}/rpc.spec.ts (99%) rename libs/sdk/src/{ => js}/rpc.ts (92%) rename libs/sdk/src/{ => js}/transport.ts (99%) create mode 100644 libs/sdk/src/wasm/global.d.ts create mode 100644 libs/sdk/src/wasm/index.ts create mode 100644 libs/sdk/src/wasm/resources/.gitkeep create mode 100644 libs/sdk/src/wasm/types.ts create mode 100644 libs/sdk/src/wasm/utils/wasm_exec.d.ts create mode 100644 libs/sdk/src/wasm/utils/wasm_exec.js create mode 100644 libs/sdk/src/wasm/utils/wasm_exec_tinygo.js create mode 100644 sdk/encode/encode.go create mode 100644 sdk/go.mod create mode 100644 sdk/go.sum create mode 100644 sdk/main.go create mode 100644 sdk/other.go create mode 100644 sdk/rhp.go create mode 100644 sdk/utils/utils.go diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 1dc17a805..a2eaacd2e 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -24,7 +24,10 @@ runs: registry-url: https://registry.npmjs.org - uses: actions/setup-go@v4 with: - go-version: 'stable' + go-version: '1.20' + - uses: acifani/setup-tinygo@v2 + with: + tinygo-version: '0.30.0' - name: Set up node cache uses: actions/cache@v3 with: diff --git a/.vscode/settings.json b/.vscode/settings.json index ba187f846..37328ce58 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,11 @@ ], "[go]": { "editor.defaultFormatter": "golang.go" + }, + "gopls": { + "build.env": { + "GOOS": "js", + "GOARCH": "wasm" + } } } diff --git a/README.md b/README.md index b8afa38e5..fa80cc683 100644 --- a/README.md +++ b/README.md @@ -8,35 +8,16 @@ Web packages for the Sia project and ecosystem. User interfaces for the Sia software. The latest Sia software takes a modular approach, with separate daemons and user interfaces for renting, hosting, the explorer, and advanced wallet functionality. -### [renterd](apps/renterd) - -![stability-mature](https://img.shields.io/badge/stability-mature-008000.svg) - -The [`renterd`](https://github.com/siafoundation/renterd) user interface, focused on renting functionality. - -### [hostd](apps/hostd) - -![stability-mature](https://img.shields.io/badge/stability-mature-008000.svg) - -The [`hostd`](https://github.com/siafoundation/hostd) user interface, focused on hosting functionality. - -### [walletd](apps/walletd) - -![stability-alpha](https://img.shields.io/badge/stability-alpha-orange.svg) - -The [`walletd`](https://github.com/siafoundation/walletd) user interface, includes a wallet with support for hot, cold, and hardware wallets. - -### [explorer](apps/explorer) - -![stability-mature](https://img.shields.io/badge/stability-mature-008000.svg) - -The `explorer` user interface, a Sia blockchain explorer interface that powers [siascan.com](https://siascan.com) and [zen.siascan.com](https://zen.siascan.com). +- [renterd](apps/renterd) - The [`renterd`](https://github.com/siafoundation/renterd) user interface, focused on renting functionality. +- [hostd](apps/hostd) - The [`hostd`](https://github.com/siafoundation/hostd) user interface, focused on hosting functionality. +- [walletd](apps/walletd) - The [`walletd`](https://github.com/siafoundation/walletd) user interface, includes a wallet with support for hot, cold, and hardware wallets. +- [explorer](apps/explorer) - The `explorer` user interface, a Sia blockchain explorer interface that powers [siascan.com](https://siascan.com) and [zen.siascan.com](https://zen.siascan.com). ## Libraries -![stability-wip](https://img.shields.io/badge/stability-work_in_progress-orange.svg) +![stability-beta](https://img.shields.io/badge/stability-beta-yellow.svg) -> ⚠️ Please note that all libraries are currently unstable and subject to change or completely move. Do not depend on these libraries until version 1.0.0. +### TypeScript The Sia web libraries provide developers with convenient TypeScript SDKs for using Sia core types, blockchain utilities, data fetching, daemon-specific React hooks, and components for common functionality such as Siacoin/fiat input fields, transaction lists, files, and more. @@ -53,6 +34,15 @@ The Sia web libraries provide developers with convenient TypeScript SDKs for usi - [@siafoundation/units](libs/units) - Methods and types for converting and displaying units. - [@siafoundation/types](libs/types) - Core Sia types and library methods. +### Go + +- [go.sia.tech/web/ui](ui) - Library for embedding NextJS applications in Go. +- [go.sia.tech/web/sdk](sdk) - SDK for encoding RPCs, computing merkle proofs, and more. Compiled with WASM for use in the TypeScript SDK. +- [go.sia.tech/web/walletd/wasm](walletd/wasm) - Wallet library for signing transactions. Compiled with WASM for use in the `walletd` application. +- [go.sia.tech/web/walletd](walletd) - HTTP handler with embedded `walletd` application. +- [go.sia.tech/web/renterd](renterd) - HTTP handler with embedded `renterd` application. +- [go.sia.tech/web/hostd](hostd) - HTTP handler with embedded `hostd` application. + ## Internal - [website](apps/website) - The main [sia.tech](https://sia.tech) website with information on the Sia project and the Sia Foundation. diff --git a/apps/renterd/next.config.js b/apps/renterd/next.config.js index 0d5212fc4..7fb3f8e0e 100644 --- a/apps/renterd/next.config.js +++ b/apps/renterd/next.config.js @@ -1,4 +1,4 @@ -const { composePlugins, withNx } = require('@nx/next'); +const { composePlugins, withNx } = require('@nx/next') /** * @type {import('@nx/next/plugins/with-nx').WithNxOptions} @@ -32,10 +32,10 @@ const nextConfigExport = { output: 'export', } -const nextConfig = process.env.NEXT_OUTPUT_EXPORT ? nextConfigExport : nextConfigServe +const nextConfig = process.env.NEXT_OUTPUT_EXPORT + ? nextConfigExport + : nextConfigServe -const plugins = [ - withNx, -]; +const plugins = [withNx] -module.exports = composePlugins(...plugins)(nextConfig); +module.exports = composePlugins(...plugins)(nextConfig) diff --git a/go.work b/go.work index 80369f52d..390c0833d 100644 --- a/go.work +++ b/go.work @@ -5,4 +5,5 @@ use ( ./hostd ./renterd ./walletd + ./sdk ) diff --git a/libs/sdk/.eslintrc.json b/libs/sdk/.eslintrc.json index 99b3d19a0..fca2b038a 100644 --- a/libs/sdk/.eslintrc.json +++ b/libs/sdk/.eslintrc.json @@ -5,7 +5,8 @@ "@nx/dependency-checks": [ "error", { - "ignoredFiles": ["libs/sdk/rollup.config.js"] + "ignoredFiles": ["libs/sdk/rollup.config.js"], + "ignoredDependencies": ["util"] } ] }, diff --git a/libs/sdk/.gitignore b/libs/sdk/.gitignore new file mode 100644 index 000000000..19e1bced9 --- /dev/null +++ b/libs/sdk/.gitignore @@ -0,0 +1 @@ +*.wasm diff --git a/libs/sdk/project.json b/libs/sdk/project.json index 65cc40a4c..0cd3464e0 100644 --- a/libs/sdk/project.json +++ b/libs/sdk/project.json @@ -4,10 +4,26 @@ "sourceRoot": "libs/sdk/src", "projectType": "library", "tags": [], + "namedInputs": { + "go": ["{workspaceRoot}/sdk/**/*"], + "production": ["default", "go"] + }, "targets": { + "compile": { + "executor": "nx:run-commands", + "inputs": ["go"], + "cache": true, + "options": { + "commands": [ + "tinygo build -o libs/sdk/src/wasm/resources/sdk.wasm -target wasm ./sdk" + ] + } + }, "build": { - "executor": "@nrwl/rollup:rollup", + "executor": "@nx/rollup:rollup", "outputs": ["{options.outputPath}"], + "dependsOn": ["compile", "^build"], + "inputs": ["production", "^production"], "options": { "outputPath": "dist/libs/sdk", "tsConfig": "libs/sdk/tsconfig.lib.json", diff --git a/libs/sdk/rollup.config.js b/libs/sdk/rollup.config.js index 3f5e4e997..7f5b47ceb 100644 --- a/libs/sdk/rollup.config.js +++ b/libs/sdk/rollup.config.js @@ -1,5 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const preserveDirectives = require('rollup-plugin-preserve-directives') +const { wasm } = require('@rollup/plugin-wasm') // https://github.com/rollup/rollup/issues/4699#issuecomment-1465302665 function getRollupOptions(options) { @@ -11,7 +12,13 @@ function getRollupOptions(options) { format: 'esm', sourcemap: true, }, - plugins: options.plugins.concat(preserveDirectives.default()), + plugins: options.plugins + .concat( + wasm({ + targetEnv: 'auto-inline', + }) + ) + .concat(preserveDirectives.default()), } } diff --git a/libs/sdk/src/index.ts b/libs/sdk/src/index.ts index bacb29a20..97fc6c9a5 100644 --- a/libs/sdk/src/index.ts +++ b/libs/sdk/src/index.ts @@ -1,2 +1,2 @@ export * from './types' -export * from './transport' +export * from './wasm' diff --git a/libs/sdk/src/encoder.ts b/libs/sdk/src/js/encoder.ts similarity index 100% rename from libs/sdk/src/encoder.ts rename to libs/sdk/src/js/encoder.ts diff --git a/libs/sdk/src/encoding.spec.ts b/libs/sdk/src/js/encoding.spec.ts similarity index 97% rename from libs/sdk/src/encoding.spec.ts rename to libs/sdk/src/js/encoding.spec.ts index 656a72c45..078942e71 100644 --- a/libs/sdk/src/encoding.spec.ts +++ b/libs/sdk/src/js/encoding.spec.ts @@ -5,7 +5,7 @@ import { decodeHostSettings, } from './encoding' import { newEncoder, newDecoder } from './encoder' -import { HostPrices, HostSettings } from './types' +import { HostPrices, HostSettings } from '../types' import { TextDecoder, TextEncoder } from 'node:util' Object.defineProperties(globalThis, { diff --git a/libs/sdk/src/encoding.ts b/libs/sdk/src/js/encoding.ts similarity index 97% rename from libs/sdk/src/encoding.ts rename to libs/sdk/src/js/encoding.ts index e2445ecf4..12020cacb 100644 --- a/libs/sdk/src/encoding.ts +++ b/libs/sdk/src/js/encoding.ts @@ -20,7 +20,7 @@ import { Encoder, Decoder, } 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) diff --git a/libs/sdk/src/example.ts b/libs/sdk/src/js/example.ts similarity index 100% rename from libs/sdk/src/example.ts rename to libs/sdk/src/js/example.ts diff --git a/libs/sdk/src/rpc.spec.ts b/libs/sdk/src/js/rpc.spec.ts similarity index 99% rename from libs/sdk/src/rpc.spec.ts rename to libs/sdk/src/js/rpc.spec.ts index 10732c00a..e1a3ac36a 100644 --- a/libs/sdk/src/rpc.spec.ts +++ b/libs/sdk/src/js/rpc.spec.ts @@ -9,7 +9,7 @@ import { RPCSettingsResponse, RPCWriteSectorRequest, RPCWriteSectorResponse, -} from './types' +} from '../types' import { TextDecoder, TextEncoder } from 'node:util' import { decodeRpcRequestReadSector, diff --git a/libs/sdk/src/rpc.ts b/libs/sdk/src/js/rpc.ts similarity index 92% rename from libs/sdk/src/rpc.ts rename to libs/sdk/src/js/rpc.ts index 0659568ab..683070a9f 100644 --- a/libs/sdk/src/rpc.ts +++ b/libs/sdk/src/js/rpc.ts @@ -27,7 +27,11 @@ 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 +// RPC and encoding implementations which are used and exported from the SDK. // settings request diff --git a/libs/sdk/src/transport.ts b/libs/sdk/src/js/transport.ts similarity index 99% rename from libs/sdk/src/transport.ts rename to libs/sdk/src/js/transport.ts index 076cb4538..043887aa7 100644 --- a/libs/sdk/src/transport.ts +++ b/libs/sdk/src/js/transport.ts @@ -17,16 +17,7 @@ import { 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 -} +} from '../types' export class WebTransportClient { private transport!: WebTransport @@ -134,3 +125,12 @@ export class WebTransportClient { ) } } + +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/wasm/global.d.ts b/libs/sdk/src/wasm/global.d.ts new file mode 100644 index 000000000..44367b16f --- /dev/null +++ b/libs/sdk/src/wasm/global.d.ts @@ -0,0 +1,10 @@ +declare module '*.wasm' { + const func: ( + imports: WebAssembly.Imports + ) => Promise + export default func +} + +interface Window { + Go: typeof Go +} diff --git a/libs/sdk/src/wasm/index.ts b/libs/sdk/src/wasm/index.ts new file mode 100644 index 000000000..db4fb9e53 --- /dev/null +++ b/libs/sdk/src/wasm/index.ts @@ -0,0 +1,24 @@ +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/resources/.gitkeep b/libs/sdk/src/wasm/resources/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/libs/sdk/src/wasm/types.ts b/libs/sdk/src/wasm/types.ts new file mode 100644 index 000000000..ac7041376 --- /dev/null +++ b/libs/sdk/src/wasm/types.ts @@ -0,0 +1,68 @@ +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/wasm/utils/wasm_exec.d.ts b/libs/sdk/src/wasm/utils/wasm_exec.d.ts new file mode 100644 index 000000000..ca44e01a4 --- /dev/null +++ b/libs/sdk/src/wasm/utils/wasm_exec.d.ts @@ -0,0 +1,10 @@ +declare class Go { + constructor() + argv: string[] + env: { [envKey: string]: string } + exit: (code: number) => void + importObject: WebAssembly.Imports + exited: boolean + mem: DataView + run(instance: WebAssembly.Instance): Promise +} diff --git a/libs/sdk/src/wasm/utils/wasm_exec.js b/libs/sdk/src/wasm/utils/wasm_exec.js new file mode 100644 index 000000000..a1392f350 --- /dev/null +++ b/libs/sdk/src/wasm/utils/wasm_exec.js @@ -0,0 +1,666 @@ +/* eslint-disable */ +// ADAPTED FROM: https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js + +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +;(() => { + const enosys = () => { + const err = new Error('not implemented') + err.code = 'ENOSYS' + return err + } + + if (!globalThis.fs) { + let outputBuf = '' + globalThis.fs = { + constants: { + O_WRONLY: -1, + O_RDWR: -1, + O_CREAT: -1, + O_TRUNC: -1, + O_APPEND: -1, + O_EXCL: -1, + }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf) + const nl = outputBuf.lastIndexOf('\n') + if (nl != -1) { + console.log(outputBuf.substring(0, nl)) + outputBuf = outputBuf.substring(nl + 1) + } + return buf.length + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()) + return + } + const n = this.writeSync(fd, buf) + callback(null, n) + }, + chmod(path, mode, callback) { + callback(enosys()) + }, + chown(path, uid, gid, callback) { + callback(enosys()) + }, + close(fd, callback) { + callback(enosys()) + }, + fchmod(fd, mode, callback) { + callback(enosys()) + }, + fchown(fd, uid, gid, callback) { + callback(enosys()) + }, + fstat(fd, callback) { + callback(enosys()) + }, + fsync(fd, callback) { + callback(null) + }, + ftruncate(fd, length, callback) { + callback(enosys()) + }, + lchown(path, uid, gid, callback) { + callback(enosys()) + }, + link(path, link, callback) { + callback(enosys()) + }, + lstat(path, callback) { + callback(enosys()) + }, + mkdir(path, perm, callback) { + callback(enosys()) + }, + open(path, flags, mode, callback) { + callback(enosys()) + }, + read(fd, buffer, offset, length, position, callback) { + callback(enosys()) + }, + readdir(path, callback) { + callback(enosys()) + }, + readlink(path, callback) { + callback(enosys()) + }, + rename(from, to, callback) { + callback(enosys()) + }, + rmdir(path, callback) { + callback(enosys()) + }, + stat(path, callback) { + callback(enosys()) + }, + symlink(path, link, callback) { + callback(enosys()) + }, + truncate(path, length, callback) { + callback(enosys()) + }, + unlink(path, callback) { + callback(enosys()) + }, + utimes(path, atime, mtime, callback) { + callback(enosys()) + }, + } + } + + if (!globalThis.process) { + globalThis.process = { + getuid() { + return -1 + }, + getgid() { + return -1 + }, + geteuid() { + return -1 + }, + getegid() { + return -1 + }, + getgroups() { + throw enosys() + }, + pid: -1, + ppid: -1, + umask() { + throw enosys() + }, + cwd() { + throw enosys() + }, + chdir() { + throw enosys() + }, + } + } + + if (!globalThis.crypto) { + throw new Error( + 'globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)' + ) + } + + if (!globalThis.performance) { + throw new Error( + 'globalThis.performance is not available, polyfill required (performance.now only)' + ) + } + + if (!globalThis.TextEncoder) { + throw new Error( + 'globalThis.TextEncoder is not available, polyfill required' + ) + } + + if (!globalThis.TextDecoder) { + throw new Error( + 'globalThis.TextDecoder is not available, polyfill required' + ) + } + + const encoder = new TextEncoder('utf-8') + const decoder = new TextDecoder('utf-8') + + globalThis.Go = class { + constructor() { + this.argv = ['js'] + this.env = {} + this.exit = (code) => { + if (code !== 0) { + console.warn('exit code:', code) + } + } + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve + }) + this._pendingEvent = null + this._scheduledTimeouts = new Map() + this._nextCallbackTimeoutID = 1 + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true) + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true) + } + + const setInt32 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true) + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true) + const high = this.mem.getInt32(addr + 4, true) + return low + high * 4294967296 + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true) + if (f === 0) { + return undefined + } + if (!isNaN(f)) { + return f + } + + const id = this.mem.getUint32(addr, true) + return this._values[id] + } + + const storeValue = (addr, v) => { + const nanHead = 0x7ff80000 + + if (typeof v === 'number' && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true) + this.mem.setUint32(addr, 0, true) + return + } + this.mem.setFloat64(addr, v, true) + return + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true) + return + } + + let id = this._ids.get(v) + if (id === undefined) { + id = this._idPool.pop() + if (id === undefined) { + id = this._values.length + } + this._values[id] = v + this._goRefCounts[id] = 0 + this._ids.set(v, id) + } + this._goRefCounts[id]++ + let typeFlag = 0 + switch (typeof v) { + case 'object': + if (v !== null) { + typeFlag = 1 + } + break + case 'string': + typeFlag = 2 + break + case 'symbol': + typeFlag = 3 + break + case 'function': + typeFlag = 4 + break + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true) + this.mem.setUint32(addr, id, true) + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0) + const len = getInt64(addr + 8) + return new Uint8Array(this._inst.exports.mem.buffer, array, len) + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0) + const len = getInt64(addr + 8) + const a = new Array(len) + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8) + } + return a + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0) + const len = getInt64(addr + 8) + return decoder.decode( + new DataView(this._inst.exports.mem.buffer, saddr, len) + ) + } + + const timeOrigin = Date.now() - performance.now() + this.importObject = { + _gotest: { + add: (a, b) => a + b, + }, + gojs: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + 'runtime.wasmExit': (sp) => { + sp >>>= 0 + const code = this.mem.getInt32(sp + 8, true) + this.exited = true + delete this._inst + delete this._values + delete this._goRefCounts + delete this._ids + delete this._idPool + this.exit(code) + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + 'runtime.wasmWrite': (sp) => { + sp >>>= 0 + const fd = getInt64(sp + 8) + const p = getInt64(sp + 16) + const n = this.mem.getInt32(sp + 24, true) + fs.writeSync( + fd, + new Uint8Array(this._inst.exports.mem.buffer, p, n) + ) + }, + + // func resetMemoryDataView() + 'runtime.resetMemoryDataView': (sp) => { + sp >>>= 0 + this.mem = new DataView(this._inst.exports.mem.buffer) + }, + + // func nanotime1() int64 + 'runtime.nanotime1': (sp) => { + sp >>>= 0 + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000) + }, + + // func walltime() (sec int64, nsec int32) + 'runtime.walltime': (sp) => { + sp >>>= 0 + const msec = new Date().getTime() + setInt64(sp + 8, msec / 1000) + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true) + }, + + // func scheduleTimeoutEvent(delay int64) int32 + 'runtime.scheduleTimeoutEvent': (sp) => { + sp >>>= 0 + const id = this._nextCallbackTimeoutID + this._nextCallbackTimeoutID++ + this._scheduledTimeouts.set( + id, + setTimeout(() => { + this._resume() + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn('scheduleTimeoutEvent: missed timeout event') + this._resume() + } + }, getInt64(sp + 8)) + ) + this.mem.setInt32(sp + 16, id, true) + }, + + // func clearTimeoutEvent(id int32) + 'runtime.clearTimeoutEvent': (sp) => { + sp >>>= 0 + const id = this.mem.getInt32(sp + 8, true) + clearTimeout(this._scheduledTimeouts.get(id)) + this._scheduledTimeouts.delete(id) + }, + + // func getRandomData(r []byte) + 'runtime.getRandomData': (sp) => { + sp >>>= 0 + crypto.getRandomValues(loadSlice(sp + 8)) + }, + + // func finalizeRef(v ref) + 'syscall/js.finalizeRef': (sp) => { + sp >>>= 0 + const id = this.mem.getUint32(sp + 8, true) + this._goRefCounts[id]-- + if (this._goRefCounts[id] === 0) { + const v = this._values[id] + this._values[id] = null + this._ids.delete(v) + this._idPool.push(id) + } + }, + + // func stringVal(value string) ref + 'syscall/js.stringVal': (sp) => { + sp >>>= 0 + storeValue(sp + 24, loadString(sp + 8)) + }, + + // func valueGet(v ref, p string) ref + 'syscall/js.valueGet': (sp) => { + sp >>>= 0 + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)) + sp = this._inst.exports.getsp() >>> 0 // see comment above + storeValue(sp + 32, result) + }, + + // func valueSet(v ref, p string, x ref) + 'syscall/js.valueSet': (sp) => { + sp >>>= 0 + Reflect.set( + loadValue(sp + 8), + loadString(sp + 16), + loadValue(sp + 32) + ) + }, + + // func valueDelete(v ref, p string) + 'syscall/js.valueDelete': (sp) => { + sp >>>= 0 + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)) + }, + + // func valueIndex(v ref, i int) ref + 'syscall/js.valueIndex': (sp) => { + sp >>>= 0 + storeValue( + sp + 24, + Reflect.get(loadValue(sp + 8), getInt64(sp + 16)) + ) + }, + + // valueSetIndex(v ref, i int, x ref) + 'syscall/js.valueSetIndex': (sp) => { + sp >>>= 0 + Reflect.set( + loadValue(sp + 8), + getInt64(sp + 16), + loadValue(sp + 24) + ) + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + 'syscall/js.valueCall': (sp) => { + sp >>>= 0 + try { + const v = loadValue(sp + 8) + const m = Reflect.get(v, loadString(sp + 16)) + const args = loadSliceOfValues(sp + 32) + const result = Reflect.apply(m, v, args) + sp = this._inst.exports.getsp() >>> 0 // see comment above + storeValue(sp + 56, result) + this.mem.setUint8(sp + 64, 1) + } catch (err) { + sp = this._inst.exports.getsp() >>> 0 // see comment above + storeValue(sp + 56, err) + this.mem.setUint8(sp + 64, 0) + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + 'syscall/js.valueInvoke': (sp) => { + sp >>>= 0 + try { + const v = loadValue(sp + 8) + const args = loadSliceOfValues(sp + 16) + const result = Reflect.apply(v, undefined, args) + sp = this._inst.exports.getsp() >>> 0 // see comment above + storeValue(sp + 40, result) + this.mem.setUint8(sp + 48, 1) + } catch (err) { + sp = this._inst.exports.getsp() >>> 0 // see comment above + storeValue(sp + 40, err) + this.mem.setUint8(sp + 48, 0) + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + 'syscall/js.valueNew': (sp) => { + sp >>>= 0 + try { + const v = loadValue(sp + 8) + const args = loadSliceOfValues(sp + 16) + const result = Reflect.construct(v, args) + sp = this._inst.exports.getsp() >>> 0 // see comment above + storeValue(sp + 40, result) + this.mem.setUint8(sp + 48, 1) + } catch (err) { + sp = this._inst.exports.getsp() >>> 0 // see comment above + storeValue(sp + 40, err) + this.mem.setUint8(sp + 48, 0) + } + }, + + // func valueLength(v ref) int + 'syscall/js.valueLength': (sp) => { + sp >>>= 0 + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)) + }, + + // valuePrepareString(v ref) (ref, int) + 'syscall/js.valuePrepareString': (sp) => { + sp >>>= 0 + const str = encoder.encode(String(loadValue(sp + 8))) + storeValue(sp + 16, str) + setInt64(sp + 24, str.length) + }, + + // valueLoadString(v ref, b []byte) + 'syscall/js.valueLoadString': (sp) => { + sp >>>= 0 + const str = loadValue(sp + 8) + loadSlice(sp + 16).set(str) + }, + + // func valueInstanceOf(v ref, t ref) bool + 'syscall/js.valueInstanceOf': (sp) => { + sp >>>= 0 + this.mem.setUint8( + sp + 24, + loadValue(sp + 8) instanceof loadValue(sp + 16) ? 1 : 0 + ) + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + 'syscall/js.copyBytesToGo': (sp) => { + sp >>>= 0 + const dst = loadSlice(sp + 8) + const src = loadValue(sp + 32) + if ( + !(src instanceof Uint8Array || src instanceof Uint8ClampedArray) + ) { + this.mem.setUint8(sp + 48, 0) + return + } + const toCopy = src.subarray(0, dst.length) + dst.set(toCopy) + setInt64(sp + 40, toCopy.length) + this.mem.setUint8(sp + 48, 1) + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + 'syscall/js.copyBytesToJS': (sp) => { + sp >>>= 0 + const dst = loadValue(sp + 8) + const src = loadSlice(sp + 16) + if ( + !(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray) + ) { + this.mem.setUint8(sp + 48, 0) + return + } + const toCopy = src.subarray(0, dst.length) + dst.set(toCopy) + setInt64(sp + 40, toCopy.length) + this.mem.setUint8(sp + 48, 1) + }, + + debug: (value) => { + console.log(value) + }, + }, + } + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error('Go.run: WebAssembly.Instance expected') + } + this._inst = instance + this.mem = new DataView(this._inst.exports.mem.buffer) + this._values = [ + // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + globalThis, + this, + ] + this._goRefCounts = new Array(this._values.length).fill(Infinity) // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ + // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]) + this._idPool = [] // unused ids that have been garbage collected + this.exited = false // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096 + + const strPtr = (str) => { + const ptr = offset + const bytes = encoder.encode(str + '\0') + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes) + offset += bytes.length + if (offset % 8 !== 0) { + offset += 8 - (offset % 8) + } + return ptr + } + + const argc = this.argv.length + + const argvPtrs = [] + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)) + }) + argvPtrs.push(0) + + const keys = Object.keys(this.env).sort() + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)) + }) + argvPtrs.push(0) + + const argv = offset + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true) + this.mem.setUint32(offset + 4, 0, true) + offset += 8 + }) + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192 + if (offset >= wasmMinDataAddr) { + throw new Error( + 'total length of command line and environment variables exceeds limit' + ) + } + + this._inst.exports.run(argc, argv) + if (this.exited) { + this._resolveExitPromise() + } + await this._exitPromise + } + + _resume() { + if (this.exited) { + throw new Error('Go program has already exited') + } + this._inst.exports.resume() + if (this.exited) { + this._resolveExitPromise() + } + } + + _makeFuncWrapper(id) { + const go = this + return function () { + const event = { id: id, this: this, args: arguments } + go._pendingEvent = event + go._resume() + return event.result + } + } + } +})() diff --git a/libs/sdk/src/wasm/utils/wasm_exec_tinygo.js b/libs/sdk/src/wasm/utils/wasm_exec_tinygo.js new file mode 100644 index 000000000..05bf5357f --- /dev/null +++ b/libs/sdk/src/wasm/utils/wasm_exec_tinygo.js @@ -0,0 +1,666 @@ +/* eslint-disable */ + +// ADAPTED FROM: https://github.com/tinygo-org/tinygo/blob/release/targets/wasm_exec.js +// MODIFICATION: Changed to use globalThis instead of global. +// Additionally see below MODIFICATION notes. + +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// This file has been modified for use by the TinyGo compiler. + +;(() => { + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + + // MODIFICATION: commented out and replaced with globalThis. + // if (typeof global !== 'undefined') { + // // global already exists + // } else if (typeof window !== 'undefined') { + // window.global = window + // } else if (typeof self !== 'undefined') { + // self.global = self + // } else { + // throw new Error( + // 'cannot export Go (neither global, window nor self is defined)' + // ) + // } + + // if (!global.require && typeof require !== 'undefined') { + // global.require = require + // } + + // if (!global.fs && global.require) { + // global.fs = require('fs') + // } + const global = globalThis + + const enosys = () => { + const err = new Error('not implemented') + err.code = 'ENOSYS' + return err + } + + if (!global.fs) { + let outputBuf = '' + global.fs = { + constants: { + O_WRONLY: -1, + O_RDWR: -1, + O_CREAT: -1, + O_TRUNC: -1, + O_APPEND: -1, + O_EXCL: -1, + }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf) + const nl = outputBuf.lastIndexOf('\n') + if (nl != -1) { + console.log(outputBuf.substr(0, nl)) + outputBuf = outputBuf.substr(nl + 1) + } + return buf.length + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()) + return + } + const n = this.writeSync(fd, buf) + callback(null, n) + }, + chmod(path, mode, callback) { + callback(enosys()) + }, + chown(path, uid, gid, callback) { + callback(enosys()) + }, + close(fd, callback) { + callback(enosys()) + }, + fchmod(fd, mode, callback) { + callback(enosys()) + }, + fchown(fd, uid, gid, callback) { + callback(enosys()) + }, + fstat(fd, callback) { + callback(enosys()) + }, + fsync(fd, callback) { + callback(null) + }, + ftruncate(fd, length, callback) { + callback(enosys()) + }, + lchown(path, uid, gid, callback) { + callback(enosys()) + }, + link(path, link, callback) { + callback(enosys()) + }, + lstat(path, callback) { + callback(enosys()) + }, + mkdir(path, perm, callback) { + callback(enosys()) + }, + open(path, flags, mode, callback) { + callback(enosys()) + }, + read(fd, buffer, offset, length, position, callback) { + callback(enosys()) + }, + readdir(path, callback) { + callback(enosys()) + }, + readlink(path, callback) { + callback(enosys()) + }, + rename(from, to, callback) { + callback(enosys()) + }, + rmdir(path, callback) { + callback(enosys()) + }, + stat(path, callback) { + callback(enosys()) + }, + symlink(path, link, callback) { + callback(enosys()) + }, + truncate(path, length, callback) { + callback(enosys()) + }, + unlink(path, callback) { + callback(enosys()) + }, + utimes(path, atime, mtime, callback) { + callback(enosys()) + }, + } + } + + if (!global.process) { + global.process = { + getuid() { + return -1 + }, + getgid() { + return -1 + }, + geteuid() { + return -1 + }, + getegid() { + return -1 + }, + getgroups() { + throw enosys() + }, + pid: -1, + ppid: -1, + umask() { + throw enosys() + }, + cwd() { + throw enosys() + }, + chdir() { + throw enosys() + }, + } + } + + if (!global.crypto) { + const nodeCrypto = require('crypto') + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b) + }, + } + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime() + return sec * 1000 + nsec / 1000000 + }, + } + } + + if (!global.TextEncoder) { + global.TextEncoder = require('util').TextEncoder + } + + if (!global.TextDecoder) { + global.TextDecoder = require('util').TextDecoder + } + + // End of polyfills for common API. + + const encoder = new TextEncoder('utf-8') + const decoder = new TextDecoder('utf-8') + let reinterpretBuf = new DataView(new ArrayBuffer(8)) + var logLine = [] + + global.Go = class { + constructor() { + this._callbackTimeouts = new Map() + this._nextCallbackTimeoutID = 1 + + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.memory.buffer) + } + + const unboxValue = (v_ref) => { + reinterpretBuf.setBigInt64(0, v_ref, true) + const f = reinterpretBuf.getFloat64(0, true) + if (f === 0) { + return undefined + } + if (!isNaN(f)) { + return f + } + + const id = v_ref & 0xffffffffn + return this._values[id] + } + + const loadValue = (addr) => { + let v_ref = mem().getBigUint64(addr, true) + return unboxValue(v_ref) + } + + const boxValue = (v) => { + const nanHead = 0x7ff80000n + + if (typeof v === 'number') { + if (isNaN(v)) { + return nanHead << 32n + } + if (v === 0) { + return (nanHead << 32n) | 1n + } + reinterpretBuf.setFloat64(0, v, true) + return reinterpretBuf.getBigInt64(0, true) + } + + switch (v) { + case undefined: + return 0n + case null: + return (nanHead << 32n) | 2n + case true: + return (nanHead << 32n) | 3n + case false: + return (nanHead << 32n) | 4n + } + + let id = this._ids.get(v) + if (id === undefined) { + id = this._idPool.pop() + if (id === undefined) { + id = BigInt(this._values.length) + } + this._values[id] = v + this._goRefCounts[id] = 0 + this._ids.set(v, id) + } + this._goRefCounts[id]++ + let typeFlag = 1n + switch (typeof v) { + case 'string': + typeFlag = 2n + break + case 'symbol': + typeFlag = 3n + break + case 'function': + typeFlag = 4n + break + } + return id | ((nanHead | typeFlag) << 32n) + } + + const storeValue = (addr, v) => { + let v_ref = boxValue(v) + mem().setBigUint64(addr, v_ref, true) + } + + const loadSlice = (array, len, cap) => { + return new Uint8Array(this._inst.exports.memory.buffer, array, len) + } + + const loadSliceOfValues = (array, len, cap) => { + const a = new Array(len) + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8) + } + return a + } + + const loadString = (ptr, len) => { + return decoder.decode( + new DataView(this._inst.exports.memory.buffer, ptr, len) + ) + } + + const timeOrigin = Date.now() - performance.now() + this.importObject = { + wasi_snapshot_preview1: { + // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write + fd_write: function (fd, iovs_ptr, iovs_len, nwritten_ptr) { + let nwritten = 0 + if (fd == 1) { + for (let iovs_i = 0; iovs_i < iovs_len; iovs_i++) { + let iov_ptr = iovs_ptr + iovs_i * 8 // assuming wasm32 + let ptr = mem().getUint32(iov_ptr + 0, true) + let len = mem().getUint32(iov_ptr + 4, true) + nwritten += len + for (let i = 0; i < len; i++) { + let c = mem().getUint8(ptr + i) + if (c == 13) { + // CR + // ignore + } else if (c == 10) { + // LF + // write line + let line = decoder.decode(new Uint8Array(logLine)) + logLine = [] + console.log(line) + } else { + logLine.push(c) + } + } + } + } else { + console.error('invalid file descriptor:', fd) + } + mem().setUint32(nwritten_ptr, nwritten, true) + return 0 + }, + fd_close: () => 0, // dummy + fd_fdstat_get: () => 0, // dummy + fd_seek: () => 0, // dummy + proc_exit: (code) => { + if (global.process) { + // Node.js + process.exit(code) + } else { + // Can't exit in a browser. + throw 'trying to exit with code ' + code + } + }, + random_get: (bufPtr, bufLen) => { + crypto.getRandomValues(loadSlice(bufPtr, bufLen)) + return 0 + }, + }, + gojs: { + // func ticks() float64 + 'runtime.ticks': () => { + return timeOrigin + performance.now() + }, + + // func sleepTicks(timeout float64) + 'runtime.sleepTicks': (timeout) => { + // Do not sleep, only reactivate scheduler after the given timeout. + setTimeout(this._inst.exports.go_scheduler, timeout) + }, + + // // func finalizeRef(v ref) + // 'syscall/js.finalizeRef': (v_ref) => { + // // Note: TinyGo does not support finalizers so this should never be + // // called. + // console.error('syscall/js.finalizeRef not implemented') + // }, + // MODIFICATION: https://github.com/tinygo-org/tinygo/issues/1140 + 'syscall/js.finalizeRef': (v_ref) => { + const id = mem().getUint32(unboxValue(v_ref), true) + this._goRefCounts[id]-- + if (this._goRefCounts[id] === 0) { + const v = this._values[id] + this._values[id] = null + this._ids.delete(v) + this._idPool.push(id) + } + }, + + // func stringVal(value string) ref + 'syscall/js.stringVal': (value_ptr, value_len) => { + const s = loadString(value_ptr, value_len) + return boxValue(s) + }, + + // func valueGet(v ref, p string) ref + 'syscall/js.valueGet': (v_ref, p_ptr, p_len) => { + let prop = loadString(p_ptr, p_len) + let v = unboxValue(v_ref) + let result = Reflect.get(v, prop) + return boxValue(result) + }, + + // func valueSet(v ref, p string, x ref) + 'syscall/js.valueSet': (v_ref, p_ptr, p_len, x_ref) => { + const v = unboxValue(v_ref) + const p = loadString(p_ptr, p_len) + const x = unboxValue(x_ref) + Reflect.set(v, p, x) + }, + + // func valueDelete(v ref, p string) + 'syscall/js.valueDelete': (v_ref, p_ptr, p_len) => { + const v = unboxValue(v_ref) + const p = loadString(p_ptr, p_len) + Reflect.deleteProperty(v, p) + }, + + // func valueIndex(v ref, i int) ref + 'syscall/js.valueIndex': (v_ref, i) => { + return boxValue(Reflect.get(unboxValue(v_ref), i)) + }, + + // valueSetIndex(v ref, i int, x ref) + 'syscall/js.valueSetIndex': (v_ref, i, x_ref) => { + Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)) + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + 'syscall/js.valueCall': ( + ret_addr, + v_ref, + m_ptr, + m_len, + args_ptr, + args_len, + args_cap + ) => { + const v = unboxValue(v_ref) + const name = loadString(m_ptr, m_len) + const args = loadSliceOfValues(args_ptr, args_len, args_cap) + try { + const m = Reflect.get(v, name) + storeValue(ret_addr, Reflect.apply(m, v, args)) + mem().setUint8(ret_addr + 8, 1) + } catch (err) { + storeValue(ret_addr, err) + mem().setUint8(ret_addr + 8, 0) + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + 'syscall/js.valueInvoke': ( + ret_addr, + v_ref, + args_ptr, + args_len, + args_cap + ) => { + try { + const v = unboxValue(v_ref) + const args = loadSliceOfValues(args_ptr, args_len, args_cap) + storeValue(ret_addr, Reflect.apply(v, undefined, args)) + mem().setUint8(ret_addr + 8, 1) + } catch (err) { + storeValue(ret_addr, err) + mem().setUint8(ret_addr + 8, 0) + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + 'syscall/js.valueNew': ( + ret_addr, + v_ref, + args_ptr, + args_len, + args_cap + ) => { + const v = unboxValue(v_ref) + const args = loadSliceOfValues(args_ptr, args_len, args_cap) + try { + storeValue(ret_addr, Reflect.construct(v, args)) + mem().setUint8(ret_addr + 8, 1) + } catch (err) { + storeValue(ret_addr, err) + mem().setUint8(ret_addr + 8, 0) + } + }, + + // func valueLength(v ref) int + 'syscall/js.valueLength': (v_ref) => { + return unboxValue(v_ref).length + }, + + // valuePrepareString(v ref) (ref, int) + 'syscall/js.valuePrepareString': (ret_addr, v_ref) => { + const s = String(unboxValue(v_ref)) + const str = encoder.encode(s) + storeValue(ret_addr, str) + mem().setInt32(ret_addr + 8, str.length, true) + }, + + // valueLoadString(v ref, b []byte) + 'syscall/js.valueLoadString': ( + v_ref, + slice_ptr, + slice_len, + slice_cap + ) => { + const str = unboxValue(v_ref) + loadSlice(slice_ptr, slice_len, slice_cap).set(str) + }, + + // func valueInstanceOf(v ref, t ref) bool + 'syscall/js.valueInstanceOf': (v_ref, t_ref) => { + return unboxValue(v_ref) instanceof unboxValue(t_ref) + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + 'syscall/js.copyBytesToGo': ( + ret_addr, + dest_addr, + dest_len, + dest_cap, + src_ref + ) => { + let num_bytes_copied_addr = ret_addr + let returned_status_addr = ret_addr + 4 // Address of returned boolean status variable + + const dst = loadSlice(dest_addr, dest_len) + const src = unboxValue(src_ref) + if ( + !(src instanceof Uint8Array || src instanceof Uint8ClampedArray) + ) { + mem().setUint8(returned_status_addr, 0) // Return "not ok" status + return + } + const toCopy = src.subarray(0, dst.length) + dst.set(toCopy) + mem().setUint32(num_bytes_copied_addr, toCopy.length, true) + mem().setUint8(returned_status_addr, 1) // Return "ok" status + }, + + // copyBytesToJS(dst ref, src []byte) (int, bool) + // Originally copied from upstream Go project, then modified: + // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 + 'syscall/js.copyBytesToJS': ( + ret_addr, + dst_ref, + src_addr, + src_len, + src_cap + ) => { + let num_bytes_copied_addr = ret_addr + let returned_status_addr = ret_addr + 4 // Address of returned boolean status variable + + const dst = unboxValue(dst_ref) + const src = loadSlice(src_addr, src_len) + if ( + !(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray) + ) { + mem().setUint8(returned_status_addr, 0) // Return "not ok" status + return + } + const toCopy = src.subarray(0, dst.length) + dst.set(toCopy) + mem().setUint32(num_bytes_copied_addr, toCopy.length, true) + mem().setUint8(returned_status_addr, 1) // Return "ok" status + }, + }, + } + + // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. + // For compatibility, we use both as long as Go 1.20 is supported. + this.importObject.env = this.importObject.gojs + } + + async run(instance) { + this._inst = instance + this._values = [ + // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + global, + this, + ] + this._goRefCounts = [] // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map() // mapping from JS values to reference ids + this._idPool = [] // unused ids that have been garbage collected + this.exited = false // whether the Go program has exited + + const mem = new DataView(this._inst.exports.memory.buffer) + + while (true) { + const callbackPromise = new Promise((resolve) => { + this._resolveCallbackPromise = () => { + if (this.exited) { + throw new Error('bad callback: Go program has already exited') + } + setTimeout(resolve, 0) // make sure it is asynchronous + } + }) + this._inst.exports._start() + if (this.exited) { + break + } + await callbackPromise + } + } + + _resume() { + if (this.exited) { + throw new Error('Go program has already exited') + } + this._inst.exports.resume() + if (this.exited) { + this._resolveExitPromise() + } + } + + _makeFuncWrapper(id) { + const go = this + return function () { + const event = { id: id, this: this, args: arguments } + go._pendingEvent = event + go._resume() + return event.result + } + } + } + + if ( + global.require && + global.require.main === module && + global.process && + global.process.versions && + !global.process.versions.electron + ) { + if (process.argv.length != 3) { + console.error('usage: go_js_wasm_exec [wasm binary] [arguments]') + process.exit(1) + } + + const go = new Go() + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject) + .then((result) => { + return go.run(result.instance) + }) + .catch((err) => { + console.error(err) + process.exit(1) + }) + } +})() diff --git a/package-lock.json b/package-lock.json index 02f4687e4..9c120a1e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,6 +126,7 @@ "@nx/webpack": "17.2.8", "@nx/workspace": "17.2.8", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", + "@rollup/plugin-wasm": "^6.2.2", "@svgr/webpack": "8.1.0", "@swc/core": "1.3.102", "@swc/jest": "0.2.20", @@ -429,6 +430,11 @@ "swr": "^2.1.1" } }, + "libs/sdk": { + "name": "@siafoundation/sdk", + "version": "0.0.1", + "license": "MIT" + }, "libs/shoebar": { "version": "0.0.1", "extraneous": true @@ -7515,6 +7521,48 @@ "rollup": "^2.42.0" } }, + "node_modules/@rollup/plugin-wasm": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-wasm/-/plugin-wasm-6.2.2.tgz", + "integrity": "sha512-gpC4R1G9Ni92ZIRTexqbhX7U+9estZrbhP+9SRb0DW9xpB9g7j34r+J2hqrcW/lRI7dJaU84MxZM0Rt82tqYPQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-wasm/node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", @@ -7625,6 +7673,10 @@ "resolved": "libs/react-walletd", "link": true }, + "node_modules/@siafoundation/sdk": { + "resolved": "libs/sdk", + "link": true + }, "node_modules/@siafoundation/sia-central": { "resolved": "libs/sia-central", "link": true @@ -33765,6 +33817,28 @@ "resolve": "^1.19.0" } }, + "@rollup/plugin-wasm": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-wasm/-/plugin-wasm-6.2.2.tgz", + "integrity": "sha512-gpC4R1G9Ni92ZIRTexqbhX7U+9estZrbhP+9SRb0DW9xpB9g7j34r+J2hqrcW/lRI7dJaU84MxZM0Rt82tqYPQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.2" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + } + } + }, "@rollup/pluginutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", @@ -33922,6 +33996,9 @@ "version": "file:libs/react-walletd", "requires": {} }, + "@siafoundation/sdk": { + "version": "file:libs/sdk" + }, "@siafoundation/sia-central": { "version": "file:libs/sia-central", "requires": { diff --git a/package.json b/package.json index 17131b664..1dadd1ffe 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test": "nx test", "release": "changeset publish", "version": "changeset version && npm install", - "build:wasm": "GOOS=js GOARCH=wasm go build -o apps/walletd/public/walletd.wasm ./walletd/wasm" + "build:wasm": "GOOS=js GOARCH=wasm go build -o apps/walletd/public/walletd.wasm ./walletd/wasm" }, "private": true, "workspaces": { @@ -138,6 +138,7 @@ "@nx/webpack": "17.2.8", "@nx/workspace": "17.2.8", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", + "@rollup/plugin-wasm": "^6.2.2", "@svgr/webpack": "8.1.0", "@swc/core": "1.3.102", "@swc/jest": "0.2.20", diff --git a/sdk/encode/encode.go b/sdk/encode/encode.go new file mode 100644 index 000000000..88427850b --- /dev/null +++ b/sdk/encode/encode.go @@ -0,0 +1,69 @@ +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 new file mode 100644 index 000000000..10a19f65b --- /dev/null +++ b/sdk/go.mod @@ -0,0 +1,15 @@ +module go.sia.tech/web/sdk + +go 1.20 + +require ( + go.sia.tech/core v0.2.2-0.20240202170315-3e6d0eca7490 + go.sia.tech/web v0.0.0-20230628194305-c6e1696bad89 +) + +require ( + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 // indirect + golang.org/x/sys v0.5.0 // indirect + lukechampine.com/frand v1.4.2 // indirect +) diff --git a/sdk/go.sum b/sdk/go.sum new file mode 100644 index 000000000..3c736b17f --- /dev/null +++ b/sdk/go.sum @@ -0,0 +1,15 @@ +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +go.sia.tech/core v0.1.12-0.20230807160906-ad76cac3058f h1:ZPWqj1RphySPSdVvhW09VYI/nKc+TiqJlUnx9FcI0lY= +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/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= +golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= +lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= diff --git a/sdk/main.go b/sdk/main.go new file mode 100644 index 000000000..b00fb1b0e --- /dev/null +++ b/sdk/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "syscall/js" +) + +func main() { + fmt.Println("WASM SDK: init") + js.Global().Set("sdk", map[string]interface{}{ + "generateAccountID": js.FuncOf(generateAccountID), + "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), + }, + }) + c := make(chan bool, 1) + <-c +} diff --git a/sdk/other.go b/sdk/other.go new file mode 100644 index 000000000..b03a36964 --- /dev/null +++ b/sdk/other.go @@ -0,0 +1,25 @@ +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 new file mode 100644 index 000000000..b71c0a778 --- /dev/null +++ b/sdk/rhp.go @@ -0,0 +1,276 @@ +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/utils/utils.go b/sdk/utils/utils.go new file mode 100644 index 000000000..d2de56d34 --- /dev/null +++ b/sdk/utils/utils.go @@ -0,0 +1,39 @@ +package utils + +import ( + "encoding/json" + "fmt" + "io" + "os" + "reflect" + "syscall/js" +) + +func CheckArgs(args []js.Value, argTypes ...js.Type) error { + if len(args) != len(argTypes) { + return fmt.Errorf("incorrect number of arguments - expected: %d, got: %d", len(argTypes), len(args)) + } + + for i, arg := range args { + if arg.Type() != argTypes[i] { + return fmt.Errorf("incorrect argument %d - expected: %s, got: %s", i, argTypes[i], arg.Type()) + } + } + + return nil +} + +func encodeJSON(w io.Writer, v interface{}) error { + // encode nil slices as [] instead of null + if val := reflect.ValueOf(v); val.Kind() == reflect.Slice && val.Len() == 0 { + _, err := w.Write([]byte("[]\n")) + return err + } + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(v) +} + +func PrintStruct(v interface{}) error { + return encodeJSON(os.Stdout, v) +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 97d2a9905..7e51fad04 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -30,7 +30,7 @@ "@siafoundation/sia-central": ["libs/sia-central/src/index.ts"], "@siafoundation/types": ["libs/types/src/index.ts"], "@siafoundation/units": ["libs/units/src/index.ts"], - "@siafoundation/sdk": ["libs/sdk/src/index.ts"] + "@siafoundation/sdk": ["dist/libs/sdk"] } }, "exclude": ["node_modules", "tmp"]