From 6c0ee4342cd8fa867716bd1b927aadbfa9a1579b Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Tue, 19 Mar 2024 16:46:02 -0400 Subject: [PATCH] refactor: sdk rhp --- .changeset/tiny-cougars-refuse.md | 5 + .env.example | 3 + .github/actions/setup/action.yml | 9 +- .github/workflows/pr.yml | 5 +- .github/workflows/release-main.yml | 3 + go.work | 4 +- go.work.sum | 6 + libs/sdk/.eslintrc.json | 2 +- libs/sdk/package.json | 1 - libs/sdk/project.json | 4 +- 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/rhp.spec.ts | 263 ++++++++ libs/sdk/src/sdk.ts | 11 + libs/sdk/src/transport.ts | 134 ++++ libs/sdk/src/types.ts | 82 ++- libs/sdk/src/{wasm => }/utils/wasm_exec.d.ts | 0 libs/sdk/src/utils/wasm_exec.js | 561 ++++++++++++++++ libs/sdk/src/wasm.ts | 12 + libs/sdk/src/wasm/index.ts | 24 - libs/sdk/src/wasm/types.ts | 68 -- libs/sdk/src/wasm/utils/wasm_exec.js | 666 ------------------- libs/sdk/src/wasm/utils/wasm_exec_tinygo.js | 666 ------------------- libs/sdk/src/wasmTest.ts | 14 + sdk/api.go | 23 + sdk/encode.go | 66 ++ sdk/encode/encode.go | 69 -- sdk/go.mod | 8 +- sdk/go.sum | 24 +- sdk/main.go | 47 +- sdk/marshal.go | 37 ++ sdk/other.go | 25 - sdk/rhp.go | 338 +++------- sdk/{utils => }/utils.go | 8 +- 45 files changed, 1507 insertions(+), 1846 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/rhp.spec.ts 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%) create mode 100644 libs/sdk/src/utils/wasm_exec.js 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 delete mode 100644 libs/sdk/src/wasm/utils/wasm_exec.js delete mode 100644 libs/sdk/src/wasm/utils/wasm_exec_tinygo.js create mode 100644 libs/sdk/src/wasmTest.ts create mode 100644 sdk/api.go create mode 100644 sdk/encode.go delete mode 100644 sdk/encode/encode.go create mode 100644 sdk/marshal.go delete mode 100644 sdk/other.go rename sdk/{utils => }/utils.go (80%) 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..daa956919 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -24,7 +24,7 @@ runs: registry-url: https://registry.npmjs.org - uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.21.7' - uses: acifani/setup-tinygo@v2 with: tinygo-version: '0.30.0' @@ -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/go.work b/go.work index 390c0833d..b2201135d 100644 --- a/go.work +++ b/go.work @@ -1,9 +1,9 @@ -go 1.20 +go 1.21.7 use ( ./ ./hostd ./renterd - ./walletd ./sdk + ./walletd ) diff --git a/go.work.sum b/go.work.sum index 87555cbed..0b7cdcd2e 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1 +1,7 @@ +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= go.sia.tech/mux v1.2.0/go.mod h1:Yyo6wZelOYTyvrHmJZ6aQfRoer3o4xyKQ4NmQLJrBSo= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/libs/sdk/.eslintrc.json b/libs/sdk/.eslintrc.json index 618f81e08..dcff14b50 100644 --- a/libs/sdk/.eslintrc.json +++ b/libs/sdk/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["plugin:@nx/react", "../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "src/utils/wasm_exec.js"], "rules": { "@nx/dependency-checks": [ "error", diff --git a/libs/sdk/package.json b/libs/sdk/package.json index aa514dc65..38b709698 100644 --- a/libs/sdk/package.json +++ b/libs/sdk/package.json @@ -3,7 +3,6 @@ "description": "SDK for interacting directly with the Sia network from browsers and web clients.", "version": "0.0.2", "license": "MIT", - "dependencies": {}, "devDependencies": { "undici": "5.28.3" }, diff --git a/libs/sdk/project.json b/libs/sdk/project.json index 0cd3464e0..838351a47 100644 --- a/libs/sdk/project.json +++ b/libs/sdk/project.json @@ -15,7 +15,8 @@ "cache": true, "options": { "commands": [ - "tinygo build -o libs/sdk/src/wasm/resources/sdk.wasm -target wasm ./sdk" + "cp $(go env GOROOT)/misc/wasm/wasm_exec.js libs/sdk/src/utils/wasm_exec.js", + "GOOS=js GOARCH=wasm go build -o libs/sdk/src/resources/sdk.wasm ./sdk" ] } }, @@ -50,6 +51,7 @@ "test": { "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/libs/sdk"], + "dependsOn": ["compile"], "options": { "jestConfig": "libs/sdk/jest.config.ts" } 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/rhp.spec.ts b/libs/sdk/src/rhp.spec.ts new file mode 100644 index 000000000..95fbc1101 --- /dev/null +++ b/libs/sdk/src/rhp.spec.ts @@ -0,0 +1,263 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { + HostPrices, + HostSettings, + RPCReadSectorRequest, + RPCReadSectorResponse, + RPCSettingsResponse, + RPCWriteSectorRequest, + RPCWriteSectorResponse, +} from './types' +import { initSDKTest } from './initTest' + +describe('rhp', () => { + describe('generateAccount', () => { + it('works', async () => { + const sdk = await initSDKTest() + const { privateKey, account, error } = sdk.rhp.generateAccount() + expect(error).toBeUndefined() + expect(privateKey).toBeDefined() + expect(privateKey?.length).toBeGreaterThan(40) + expect(account).toBeDefined() + expect(account?.length).toBeGreaterThan(40) + }) + }) + describe('settings', () => { + describe('request', () => { + it('valid', async () => { + const sdk = await initSDKTest() + const encode = sdk.rhp.encodeSettingsRequest() + expect(encode.rpc).toBeDefined() + expect(encode.error).not.toBeDefined() + const decode = sdk.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.rhp.encodeSettingsResponse(json) + expect(encode.rpc).toBeDefined() + expect(encode.rpc?.length).toEqual(323) + expect(encode.error).toBeUndefined() + const decode = sdk.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.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.rhp.encodeSettingsResponse(json) + // manipulate the valid rpc to make it invalid + encode.rpc!.set([1, 1], 30) + const decode = sdk.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.rhp.encodeReadSectorRequest(json) + expect(encode.rpc?.length).toEqual(312) + expect(encode.error).toBeUndefined() + const decode = sdk.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.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.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() + const decode = sdk.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.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.rhp.encodeWriteSectorRequest(json) + expect(encode.rpc?.length).toEqual(275) + expect(encode.error).toBeUndefined() + const decode = sdk.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.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.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() + const decode = sdk.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.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/sdk.ts b/libs/sdk/src/sdk.ts new file mode 100644 index 000000000..92c384e4c --- /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 { + rhp: wasm.rhp, + WebTransportClient, + } +} diff --git a/libs/sdk/src/transport.ts b/libs/sdk/src/transport.ts new file mode 100644 index 000000000..372c4e848 --- /dev/null +++ b/libs/sdk/src/transport.ts @@ -0,0 +1,134 @@ +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): ArrayBuffer { + const buffer = Buffer.from(base64, 'base64') + return buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ) +} diff --git a/libs/sdk/src/types.ts b/libs/sdk/src/types.ts index 3129f33ae..ca34508f1 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,64 @@ export type RPCWriteSector = { } export type RPC = RPCSettings | RPCReadSector | RPCWriteSector + +export type WASM = { + rhp: { + generateAccount: () => { + 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/utils/wasm_exec.js b/libs/sdk/src/utils/wasm_exec.js new file mode 100644 index 000000000..bc6f21024 --- /dev/null +++ b/libs/sdk/src/utils/wasm_exec.js @@ -0,0 +1,561 @@ +// 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. + +"use strict"; + +(() => { + 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.ts b/libs/sdk/src/wasm.ts new file mode 100644 index 000000000..d257acab8 --- /dev/null +++ b/libs/sdk/src/wasm.ts @@ -0,0 +1,12 @@ +import './utils/wasm_exec' +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/wasm/utils/wasm_exec.js b/libs/sdk/src/wasm/utils/wasm_exec.js deleted file mode 100644 index a1392f350..000000000 --- a/libs/sdk/src/wasm/utils/wasm_exec.js +++ /dev/null @@ -1,666 +0,0 @@ -/* 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 deleted file mode 100644 index 05bf5357f..000000000 --- a/libs/sdk/src/wasm/utils/wasm_exec_tinygo.js +++ /dev/null @@ -1,666 +0,0 @@ -/* 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/libs/sdk/src/wasmTest.ts b/libs/sdk/src/wasmTest.ts new file mode 100644 index 000000000..922ca81b6 --- /dev/null +++ b/libs/sdk/src/wasmTest.ts @@ -0,0 +1,14 @@ +import './utils/wasm_exec' +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/api.go b/sdk/api.go new file mode 100644 index 000000000..efb04fb95 --- /dev/null +++ b/sdk/api.go @@ -0,0 +1,23 @@ +package main + +import ( + "syscall/js" +) + +type result = map[string]any + +func resultErr(err error) result { + return map[string]any{"error": err.Error()} +} + +func resultErrStr(err string) result { + return map[string]any{"error": err} +} + +func resultRPC(rpc js.Value) result { + return map[string]any{"rpc": rpc} +} + +func resultData(data any) result { + return map[string]any{"data": data} +} diff --git a/sdk/encode.go b/sdk/encode.go new file mode 100644 index 000000000..48c107698 --- /dev/null +++ b/sdk/encode.go @@ -0,0 +1,66 @@ +package main + +import ( + "bytes" + "syscall/js" + + "go.sia.tech/core/rhp/v4" +) + +func encodeRPCRequest(data js.Value, req rhp.Request) result { + if data.Type() != js.TypeUndefined { + if err := unmarshalStruct(data, &req); err != nil { + return resultErr(err) + } + } + buf := bytes.NewBuffer(nil) + if err := rhp.WriteRequest(buf, req); err != nil { + return resultErr(err) + } + return resultRPC(marshalUint8Array(buf.Bytes())) +} + +func decodeRPCRequest(rpcJsData js.Value, res rhp.Request) result { + rpcData, err := unmarshalUint8Array(rpcJsData) + if err != nil { + return resultErr(err) + } + err = rhp.ReadRequest(bytes.NewReader(rpcData), res) + if err != nil { + return resultErr(err) + } + d, err := marshalStruct(res) + if err != nil { + return resultErr(err) + } + return resultData(d) +} + +func encodeRPCResponse(data js.Value, req rhp.Object) result { + if data.Type() != js.TypeUndefined { + if err := unmarshalStruct(data, &req); err != nil { + return resultErr(err) + } + } + buf := bytes.NewBuffer(nil) + if err := rhp.WriteResponse(buf, req); err != nil { + return resultErr(err) + } + return resultRPC(marshalUint8Array(buf.Bytes())) +} + +func decodeRPCResponse(rpcJsData js.Value, res rhp.Object) result { + rpcData, err := unmarshalUint8Array(rpcJsData) + if err != nil { + return resultErr(err) + } + err = rhp.ReadResponse(bytes.NewReader(rpcData), res) + if err != nil { + return resultErr(err) + } + d, err := marshalStruct(res) + if err != nil { + return resultErr(err) + } + return resultData(d) +} 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..075de5993 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -1,14 +1,16 @@ module go.sia.tech/web/sdk -go 1.20 +go 1.21.7 require ( - go.sia.tech/core v0.2.2-0.20240202170315-3e6d0eca7490 - go.sia.tech/web v0.0.0-20230628194305-c6e1696bad89 + go.sia.tech/core v0.2.2-0.20240229154321-d97c1d5b2172 + go.sia.tech/coreutils v0.0.3 ) require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // 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 index 3c736b17f..8feb98ce7 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -1,15 +1,27 @@ 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= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/coreutils v0.0.3 h1:ZxuzovRpQMvfy/pCOV4om1cPF6sE15GyJyK36kIrF1Y= +go.sia.tech/coreutils v0.0.3/go.mod h1:UBFc77wXiE//eyilO5HLOncIEj7F69j0Nv2OkFujtP0= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 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= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 index b00fb1b0e..f276e386e 100644 --- a/sdk/main.go +++ b/sdk/main.go @@ -1,35 +1,36 @@ 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), + js.Global().Set("sia", map[string]any{ + "rhp": map[string]any{ + "generateAccount": jsFunc(generateAccount), + // settings + "encodeSettingsRequest": jsFunc(encodeSettingsRequest), + "decodeSettingsRequest": jsFunc(decodeSettingsRequest), + "encodeSettingsResponse": jsFunc(encodeSettingsResponse), + "decodeSettingsResponse": jsFunc(decodeSettingsResponse), + // read sector + "encodeReadSectorRequest": jsFunc(encodeReadSectorRequest), + "decodeReadSectorRequest": jsFunc(decodeReadSectorRequest), + "encodeReadSectorResponse": jsFunc(encodeReadSectorResponse), + "decodeReadSectorResponse": jsFunc(decodeReadSectorResponse), + // write sector + "encodeWriteSectorRequest": jsFunc(encodeWriteSectorRequest), + "decodeWriteSectorRequest": jsFunc(decodeWriteSectorRequest), + "encodeWriteSectorResponse": jsFunc(encodeWriteSectorResponse), + "decodeWriteSectorResponse": jsFunc(decodeWriteSectorResponse), }, }) c := make(chan bool, 1) <-c } + +func jsFunc(method func(js.Value, []js.Value) map[string]any) js.Func { + return js.FuncOf(func(this js.Value, args []js.Value) any { + return method(this, args) + }) +} diff --git a/sdk/marshal.go b/sdk/marshal.go new file mode 100644 index 000000000..ee10e4b6a --- /dev/null +++ b/sdk/marshal.go @@ -0,0 +1,37 @@ +package main + +import ( + "encoding/json" + "fmt" + "syscall/js" +) + +func marshalStruct(obj any) (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 any) 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 index b71c0a778..5c892766b 100644 --- a/sdk/rhp.go +++ b/sdk/rhp.go @@ -1,276 +1,128 @@ package main import ( + "encoding/hex" "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()} +func generateAccount(this js.Value, args []js.Value) result { + if err := checkArgs(args); err != nil { + return resultErr(err) } - var r rhp.HostSettings - if err := encode.UnmarshalStruct(args[0], &r); err != nil { - return map[string]any{"error": err.Error()} - } + pk, a := rhp.GenerateAccount() - rpc, err := encode.EncodeRPC(&r) - if err != nil { - return map[string]any{"error": err.Error()} - } + privateKey := hex.EncodeToString(pk) + account := hex.EncodeToString(a[:]) - return map[string]any{"rpc": rpc} + return result(map[string]any{ + "privateKey": privateKey, + "account": account, + }) } -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()} - } +// settings - hsRpc, err := encode.UnmarshalUint8Array(args[0]) - if err != nil { - return map[string]any{"error": err.Error()} +func encodeSettingsRequest(this js.Value, args []js.Value) result { + if err := checkArgs(args); err != nil { + return resultErr(err) } + var r rhp.RPCSettingsRequest + return encodeRPCRequest(js.Undefined(), &r) +} - var r rhp.HostSettings - data, err := encode.DecodeRPC(hsRpc, &r) - if err != nil { - return map[string]any{"error": err.Error()} +func decodeSettingsRequest(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) } - - return map[string]any{"data": data} + var r rhp.RPCSettingsRequest + return decodeRPCRequest(args[0], &r) } -// 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()} -// } +func encodeSettingsResponse(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } + var r rhp.RPCSettingsResponse + return encodeRPCResponse(args[0], &r) +} -// return map[string]any{"rpc": rpc} -// } +func decodeSettingsResponse(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } + var r rhp.RPCSettingsResponse + return decodeRPCResponse(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()} -// } +// read sector -// hsRpc, err := encode.UnmarshalUint8Array(args[0]) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } +func encodeReadSectorRequest(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } -// var r rhp.RPCWriteSectorRequest -// data, err := encode.DecodeRPC(hsRpc, &r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } + var r rhp.RPCReadSectorRequest + return encodeRPCRequest(args[0], &r) +} -// return map[string]any{"data": data} -// } +func decodeReadSectorRequest(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } + var r rhp.RPCReadSectorRequest + 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()} -// } +func encodeReadSectorResponse(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } + var r rhp.RPCReadSectorResponse + return encodeRPCResponse(args[0], &r) +} -// var r rhp.RPCWriteSectorResponse -// if err := encode.UnmarshalStruct(args[0], &r); err != nil { -// return map[string]any{"error": err.Error()} -// } +func decodeReadSectorResponse(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } + var r rhp.RPCReadSectorResponse + return decodeRPCResponse(args[0], &r) +} -// rpc, err := encode.EncodeRPC(&r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } +// write sector -// return map[string]any{"rpc": rpc} -// } +func encodeWriteSectorRequest(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } + var r rhp.RPCWriteSectorRequest + return encodeRPCRequest(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()} -// } +func decodeWriteSectorRequest(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } + var r rhp.RPCWriteSectorRequest + return decodeRPCRequest(args[0], &r) +} -// hsRpc, err := encode.UnmarshalUint8Array(args[0]) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } +func encodeWriteSectorResponse(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } -// var r rhp.RPCWriteSectorResponse -// data, err := encode.DecodeRPC(hsRpc, &r) -// if err != nil { -// return map[string]any{"error": err.Error()} -// } + var r rhp.RPCWriteSectorResponse + return encodeRPCResponse(args[0], &r) +} -// return map[string]any{"data": data} -// } +func decodeWriteSectorResponse(this js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } + var r rhp.RPCWriteSectorResponse + return decodeRPCResponse(args[0], &r) +} diff --git a/sdk/utils/utils.go b/sdk/utils.go similarity index 80% rename from sdk/utils/utils.go rename to sdk/utils.go index d2de56d34..589976710 100644 --- a/sdk/utils/utils.go +++ b/sdk/utils.go @@ -1,4 +1,4 @@ -package utils +package main import ( "encoding/json" @@ -9,7 +9,7 @@ import ( "syscall/js" ) -func CheckArgs(args []js.Value, argTypes ...js.Type) error { +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)) } @@ -23,7 +23,7 @@ func CheckArgs(args []js.Value, argTypes ...js.Type) error { return nil } -func encodeJSON(w io.Writer, v interface{}) error { +func encodeJSON(w io.Writer, v any) 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")) @@ -34,6 +34,6 @@ func encodeJSON(w io.Writer, v interface{}) error { return enc.Encode(v) } -func PrintStruct(v interface{}) error { +func printStruct(v any) error { return encodeJSON(os.Stdout, v) }