ci: docker_tag=ext - Fix WASM compilation for web target
Some checks failed
Build and Push Docker image (ext) / docker (push) Failing after 3m44s

- Recompile sdk_client WASM with --target web for ES modules
- Update Dockerfile to build WASM in web target instead of bundler
- Modify index.html to initialize WASM before router
- Update service.ts and router.ts to use ES module imports
- Fix 'module is not defined' error in browser
This commit is contained in:
4NK CI Bot 2025-09-18 15:41:05 +00:00
parent 5da295be1a
commit 01b1b50d6b
4 changed files with 83 additions and 35 deletions

View File

@ -1,13 +1,46 @@
# syntax=docker/dockerfile:1.4 # syntax=docker/dockerfile:1.4
# Stage 1: Build WASM for web target
FROM rust:1.82-alpine AS wasm-builder
WORKDIR /build
# Install dependencies for WASM compilation
RUN apk update && apk add --no-cache git openssh-client curl nodejs npm build-base pkgconfig clang llvm musl-dev
# Install wasm-bindgen-cli
RUN cargo install wasm-bindgen-cli --version 0.2.103 --locked && rustup target add wasm32-unknown-unknown
# Setup SSH for git clone
RUN mkdir -p /root/.ssh && ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts
# Copy project files
COPY . ihm_client/
# Clone and build sdk_client for web target
RUN --mount=type=ssh git clone -b dev ssh://git@git.4nkweb.com/4nk/sdk_client.git
WORKDIR /build/sdk_client
# Build WASM for web target (ES modules)
RUN cargo build --target wasm32-unknown-unknown --profile dev && \
wasm-bindgen target/wasm32-unknown-unknown/debug/sdk_client.wasm \
--out-dir /build/ihm_client/pkg \
--typescript \
--target web \
--debug
# Stage 2: Final application
FROM node:20-alpine FROM node:20-alpine
WORKDIR /app WORKDIR /app
# Installation des dépendances nécessaires # Installation des dépendances nécessaires
RUN apk update && apk add --no-cache git nginx RUN apk update && apk add --no-cache git nginx
# Copie du projet (incluant pkg pré-compilé) # Copy project files
COPY . . COPY . .
# Copy the web-compiled WASM package
COPY --from=wasm-builder /build/ihm_client/pkg ./pkg
# Installation des dépendances Node.js # Installation des dépendances Node.js
RUN npm install RUN npm install

View File

@ -17,9 +17,23 @@
</div> </div>
<!-- <script type="module" src="/src/index.ts"></script> --> <!-- <script type="module" src="/src/index.ts"></script> -->
<script type="module"> <script type="module">
import { init } from '/src/router.ts'; // Initialize WASM first
import init, * as sdk from './pkg/sdk_client.js';
// Initialize the application
import { init as initRouter } from '/src/router.ts';
(async () => { (async () => {
await init(); try {
// Initialize WASM
await init();
console.log('WASM initialized successfully');
// Initialize the router
await initRouter();
} catch (error) {
console.error('Failed to initialize application:', error);
}
})(); })();
</script> </script>
</body> </body>

View File

@ -10,7 +10,7 @@ import { prepareAndSendPairingTx } from './utils/sp-address.utils';
import ModalService from './services/modal.service'; import ModalService from './services/modal.service';
import { MessageType } from './models/process.model'; import { MessageType } from './models/process.model';
import { splitPrivateData, isValid32ByteHex } from './utils/service.utils'; import { splitPrivateData, isValid32ByteHex } from './utils/service.utils';
import { MerkleProofResult } from 'pkg/sdk_client'; import { MerkleProofResult } from 'pkg/sdk_client.js';
const routes: { [key: string]: string } = { const routes: { [key: string]: string } = {
home: '/src/pages/home/home.html', home: '/src/pages/home/home.html',

View File

@ -1,7 +1,7 @@
import { INotification } from '~/models/notification.model'; import { INotification } from '~/models/notification.model';
import { IProcess } from '~/models/process.model'; import { IProcess } from '~/models/process.model';
import { initWebsocket, sendMessage } from '../websockets'; import { initWebsocket, sendMessage } from '../websockets';
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client'; import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client.js';
import ModalService from './modal.service'; import ModalService from './modal.service';
import Database from './database.service'; import Database from './database.service';
import { navigate } from '../router'; import { navigate } from '../router';
@ -58,7 +58,8 @@ export default class Services {
public async init(): Promise<void> { public async init(): Promise<void> {
this.notifications = this.getNotifications(); this.notifications = this.getNotifications();
this.sdkClient = await import('../../pkg/sdk_client'); // SDK is now imported as ES module at the top of the file
this.sdkClient = await import('../../pkg/sdk_client.js');
this.sdkClient.setup(); this.sdkClient.setup();
for (const wsurl of Object.values(BOOTSTRAPURL)) { for (const wsurl of Object.values(BOOTSTRAPURL)) {
this.updateRelay(wsurl, ''); this.updateRelay(wsurl, '');
@ -87,7 +88,7 @@ export default class Services {
*/ */
public async connectAllRelays(): Promise<void> { public async connectAllRelays(): Promise<void> {
const connectedUrls: string[] = []; const connectedUrls: string[] = [];
// Connect to all relays // Connect to all relays
for (const wsurl of Object.keys(this.relayAddresses)) { for (const wsurl of Object.keys(this.relayAddresses)) {
try { try {
@ -99,7 +100,7 @@ export default class Services {
console.error(`Failed to connect to ${wsurl}:`, error); console.error(`Failed to connect to ${wsurl}:`, error);
} }
} }
// Wait for at least one handshake message if we have connections // Wait for at least one handshake message if we have connections
if (connectedUrls.length > 0) { if (connectedUrls.length > 0) {
await this.waitForHandshakeMessage(); await this.waitForHandshakeMessage();
@ -381,18 +382,18 @@ export default class Services {
roles: Record<string, RoleDefinition>, roles: Record<string, RoleDefinition>,
): Promise<ApiReturn> { ): Promise<ApiReturn> {
let relayAddress = this.getAllRelays()[0]?.spAddress; let relayAddress = this.getAllRelays()[0]?.spAddress;
if (!relayAddress || relayAddress === '') { if (!relayAddress || relayAddress === '') {
console.log('No relay address found, connecting to relays...'); console.log('No relay address found, connecting to relays...');
await this.connectAllRelays(); await this.connectAllRelays();
// After connectAllRelays completes, relay addresses should be updated // After connectAllRelays completes, relay addresses should be updated
relayAddress = this.getAllRelays()[0]?.spAddress; relayAddress = this.getAllRelays()[0]?.spAddress;
if (!relayAddress || relayAddress === '') { if (!relayAddress || relayAddress === '') {
throw new Error('No relay address available after connecting to relays'); throw new Error('No relay address available after connecting to relays');
} }
} }
const feeRate = 1; const feeRate = 1;
// We can't encode files as the rest because Uint8Array is not valid json // We can't encode files as the rest because Uint8Array is not valid json
@ -400,12 +401,12 @@ export default class Services {
// TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking // TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking
const privateSplitData = this.splitData(privateData); const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData); const publicSplitData = this.splitData(publicData);
const encodedPrivateData = { const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(privateSplitData.binaryData) ...this.sdkClient.encode_binary(privateSplitData.binaryData)
}; };
const encodedPublicData = { const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(publicSplitData.binaryData) ...this.sdkClient.encode_binary(publicSplitData.binaryData)
}; };
@ -413,10 +414,10 @@ export default class Services {
console.log('encoded data:', encodedPublicData); console.log('encoded data:', encodedPublicData);
const result = this.sdkClient.create_new_process ( const result = this.sdkClient.create_new_process (
encodedPrivateData, encodedPrivateData,
roles, roles,
encodedPublicData, encodedPublicData,
relayAddress, relayAddress,
feeRate, feeRate,
this.getAllMembers() this.getAllMembers()
); );
@ -440,12 +441,12 @@ export default class Services {
} }
const privateSplitData = this.splitData(privateData); const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData); const publicSplitData = this.splitData(publicData);
const encodedPrivateData = { const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(privateSplitData.binaryData) ...this.sdkClient.encode_binary(privateSplitData.binaryData)
}; };
const encodedPublicData = { const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(publicSplitData.binaryData) ...this.sdkClient.encode_binary(publicSplitData.binaryData)
}; };
try { try {
@ -569,7 +570,7 @@ export default class Services {
} }
async parseNewTx(newTxMsg: string) { async parseNewTx(newTxMsg: string) {
const parsedMsg: NewTxMessage = JSON.parse(newTxMsg); const parsedMsg: NewTxMessage = JSON.parse(newTxMsg);
if (parsedMsg.error !== null) { if (parsedMsg.error !== null) {
console.error('Received error in new tx message:', parsedMsg.error); console.error('Received error in new tx message:', parsedMsg.error);
return; return;
@ -864,7 +865,7 @@ export default class Services {
try { try {
const device = await this.getDeviceFromDatabase(); const device = await this.getDeviceFromDatabase();
if (device) { if (device) {
const pairedMember = device['paired_member']; const pairedMember = device['paired_member'];
return pairedMember.sp_addresses; return pairedMember.sp_addresses;
} else { } else {
return null; return null;
@ -1051,7 +1052,7 @@ export default class Services {
} catch (e) { } catch (e) {
console.error(`Failed to save data to db: ${e}`); console.error(`Failed to save data to db: ${e}`);
} }
} }
public async getBlobFromDb(hash: string): Promise<Blob | null> { public async getBlobFromDb(hash: string): Promise<Blob | null> {
const db = await Database.getInstance(); const db = await Database.getInstance();
@ -1178,7 +1179,7 @@ export default class Services {
}); });
} }
// Now we can transfer them to memory // Now we can transfer them to memory
await this.restoreSecretsFromDB(); await this.restoreSecretsFromDB();
} }
@ -1219,7 +1220,7 @@ export default class Services {
if (!key) { if (!key) {
const roles = state.roles; const roles = state.roles;
let hasAccess = false; let hasAccess = false;
// If we're not supposed to have access to this attribute, ignore // If we're not supposed to have access to this attribute, ignore
for (const role of Object.values(roles)) { for (const role of Object.values(roles)) {
for (const rule of Object.values(role.validation_rules)) { for (const rule of Object.values(role.validation_rules)) {
if (rule.fields.includes(attribute)) { if (rule.fields.includes(attribute)) {
@ -1241,7 +1242,7 @@ export default class Services {
const maxRetries = 5; const maxRetries = 5;
const retryDelay = 500; // delay in milliseconds const retryDelay = 500; // delay in milliseconds
let retries = 0; let retries = 0;
while ((!hash || !key) && retries < maxRetries) { while ((!hash || !key) && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay)); await new Promise(resolve => setTimeout(resolve, retryDelay));
// Re-read hash and key after waiting // Re-read hash and key after waiting
@ -1274,7 +1275,7 @@ export default class Services {
} }
} }
} }
return null; return null;
} }
@ -1316,7 +1317,7 @@ export default class Services {
await this.resetDevice(); await this.resetDevice();
await this.saveDeviceInDatabase(device); await this.saveDeviceInDatabase(device);
this.restoreDevice(device); this.restoreDevice(device);
// TODO restore secrets and processes from file // TODO restore secrets and processes from file
@ -1495,7 +1496,7 @@ export default class Services {
private async waitForHandshakeMessage(timeoutMs: number = 10000): Promise<void> { private async waitForHandshakeMessage(timeoutMs: number = 10000): Promise<void> {
const startTime = Date.now(); const startTime = Date.now();
const pollInterval = 100; // Check every 100ms const pollInterval = 100; // Check every 100ms
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const checkForHandshake = () => { const checkForHandshake = () => {
// Check if we have any members or any relays (indicating handshake was received) // Check if we have any members or any relays (indicating handshake was received)
@ -1504,17 +1505,17 @@ export default class Services {
resolve(); resolve();
return; return;
} }
// Check timeout // Check timeout
if (Date.now() - startTime >= timeoutMs) { if (Date.now() - startTime >= timeoutMs) {
reject(new Error(`No handshake message received after ${timeoutMs}ms timeout`)); reject(new Error(`No handshake message received after ${timeoutMs}ms timeout`));
return; return;
} }
// Continue polling // Continue polling
setTimeout(checkForHandshake, pollInterval); setTimeout(checkForHandshake, pollInterval);
}; };
checkForHandshake(); checkForHandshake();
}); });
} }
@ -1565,7 +1566,7 @@ export default class Services {
this.sendCommitMessage(JSON.stringify(content)); this.sendCommitMessage(JSON.stringify(content));
}, 1000) }, 1000)
} }
public getRoles(process: Process): Record<string, RoleDefinition> | null { public getRoles(process: Process): Record<string, RoleDefinition> | null {
const lastCommitedState = this.getLastCommitedState(process); const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) { if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) {
@ -1745,7 +1746,7 @@ export default class Services {
return process.states[index + 1]; return process.states[index + 1];
} }
return null; return null;
} }
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean { public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {