From ee49ff19c63066545ebb0d0dbbba07a37a423fcb Mon Sep 17 00:00:00 2001 From: lilbonekit Date: Wed, 13 May 2026 17:44:12 +0300 Subject: [PATCH 1/2] feat: integrate LWK network provider and context --- web-v2/docs/HOW_TO_BUILD_LWK_WASM.md | 28 ++++++ web-v2/package.json | 1 + web-v2/pnpm-lock.yaml | 37 ++++++++ web-v2/src/pages/Dashboard/index.tsx | 71 +++++++++++++- web-v2/src/providers/AppProviders.tsx | 8 +- .../src/providers/network/NetworkContext.ts | 9 ++ .../src/providers/network/NetworkProvider.tsx | 25 +++++ web-v2/src/providers/network/types.ts | 10 ++ web-v2/src/providers/network/useNetwork.ts | 12 +++ web-v2/src/simplicity/lwk.ts | 93 +++++++++++++++++++ web-v2/vite.config.ts | 19 ++++ 11 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 web-v2/docs/HOW_TO_BUILD_LWK_WASM.md create mode 100644 web-v2/src/providers/network/NetworkContext.ts create mode 100644 web-v2/src/providers/network/NetworkProvider.tsx create mode 100644 web-v2/src/providers/network/types.ts create mode 100644 web-v2/src/providers/network/useNetwork.ts create mode 100644 web-v2/src/simplicity/lwk.ts diff --git a/web-v2/docs/HOW_TO_BUILD_LWK_WASM.md b/web-v2/docs/HOW_TO_BUILD_LWK_WASM.md new file mode 100644 index 0000000..1f14ca2 --- /dev/null +++ b/web-v2/docs/HOW_TO_BUILD_LWK_WASM.md @@ -0,0 +1,28 @@ +# How to build lwk_wasm + +**Recommended — Docker (stable, no Rust on host)** + +## 1. **Build lwk-builder stage:** + +```bash +# run from repo root (contains web/ and web-v2/) +docker build -f web/Dockerfile --target lwk-builder -t lwk-builder . +``` + +## **2. Extract pkg_web to repo root pkg_web**: + +```bash +docker create --name tmp lwk-builder +docker cp tmp:/tmp/lwk/lwk_wasm/pkg_web ./pkg_web_from_docker +docker rm tmp +mkdir -p ./lwk_wasm +mv ./pkg_web_from_docker ./lwk_wasm/pkg_web +``` + +## **3. Install & run web-v2**: + +```bash +cd web-v2 +pnpm install +pnpm dev +``` diff --git a/web-v2/package.json b/web-v2/package.json index 9434161..881f8cb 100644 --- a/web-v2/package.json +++ b/web-v2/package.json @@ -16,6 +16,7 @@ "add-icon": "tsx ./scripts/add-icon.ts" }, "dependencies": { + "lwk_web": "file:../lwk_wasm/pkg_web", "@heroui/react": "^3.0.4", "@tanstack/react-query": "^5.100.10", "react": "^19.2.6", diff --git a/web-v2/pnpm-lock.yaml b/web-v2/pnpm-lock.yaml index 027a748..2bbfb1d 100644 --- a/web-v2/pnpm-lock.yaml +++ b/web-v2/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@tanstack/react-query': specifier: ^5.100.10 version: 5.100.10(react@19.2.6) + lwk_web: + specifier: file:../lwk_wasm/pkg_web + version: lwk_wasm@file:../lwk_wasm/pkg_web react: specifier: ^19.2.6 version: 19.2.6 @@ -689,66 +692,79 @@ packages: resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.3': resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.3': resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.3': resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.3': resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.3': resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.3': resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.3': resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.3': resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.3': resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.3': resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.3': resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.3': resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.3': resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} @@ -845,24 +861,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.0': resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.0': resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.0': resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.0': resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} @@ -1039,41 +1059,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1993,24 +2021,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -2042,6 +2074,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lwk_wasm@file:../lwk_wasm/pkg_web: + resolution: {directory: ../lwk_wasm/pkg_web, type: directory} + macos-version@6.0.0: resolution: {integrity: sha512-O2S8voA+pMfCHhBn/TIYDXzJ1qNHpPDU32oFxglKnVdJABiYYITt45oLkV9yhwA3E2FDwn3tQqUFrTsr1p3sBQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4713,6 +4748,8 @@ snapshots: dependencies: yallist: 3.1.1 + lwk_wasm@file:../lwk_wasm/pkg_web: {} + macos-version@6.0.0: dependencies: semver: 7.8.0 diff --git a/web-v2/src/pages/Dashboard/index.tsx b/web-v2/src/pages/Dashboard/index.tsx index 8103beb..a2ed8cc 100644 --- a/web-v2/src/pages/Dashboard/index.tsx +++ b/web-v2/src/pages/Dashboard/index.tsx @@ -1,3 +1,72 @@ +import { useEffect, useState } from 'react' + +import { useNetwork } from '@/providers/network/useNetwork' +import type { LwkNetwork } from '@/simplicity/lwk' + +interface NetworkInfo { + label: string + genesisBlockHash: string + defaultExplorerUrl: string + policyAsset: string +} + +// EXAMPLE OF LWK USAGE export default function DashboardPage() { - return

Dashboard

+ const { network, isTestnet, isMainnet, isRegtest, initLwkNetworkInstance } = useNetwork() + const [info, setInfo] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + let net: LwkNetwork | null = null + + initLwkNetworkInstance() + .then(instance => { + net = instance + setInfo({ + label: instance.toString(), + genesisBlockHash: instance.genesisBlockHash(), + defaultExplorerUrl: instance.defaultExplorerUrl(), + policyAsset: instance.policyAsset().toString(), + }) + }) + .catch(err => setError(String(err))) + + return () => { + net?.free() + } + }, [initLwkNetworkInstance, network]) + + return ( +
+

Dashboard

+

+ Network: {network} +

+

+ isTestnet: {isTestnet.toString()} / isMainnet: {isMainnet.toString()} / isRegtest:{' '} + {isRegtest.toString()} +

+ {error &&

{error}

} + {info && ( +
+
LWK label
+
+ {info.label} +
+
Genesis block hash
+
+ {info.genesisBlockHash} +
+
Default explorer
+
+ {info.defaultExplorerUrl} +
+
Policy asset
+
+ {info.policyAsset} +
+
+ )} +
+ ) } diff --git a/web-v2/src/providers/AppProviders.tsx b/web-v2/src/providers/AppProviders.tsx index 995506e..731c26a 100644 --- a/web-v2/src/providers/AppProviders.tsx +++ b/web-v2/src/providers/AppProviders.tsx @@ -1,8 +1,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import type { PropsWithChildren } from 'react' +import { NetworkProvider } from './network/NetworkProvider' + const queryClient = new QueryClient() export function AppProviders({ children }: PropsWithChildren) { - return {children} + return ( + + {children} + + ) } diff --git a/web-v2/src/providers/network/NetworkContext.ts b/web-v2/src/providers/network/NetworkContext.ts new file mode 100644 index 0000000..c824303 --- /dev/null +++ b/web-v2/src/providers/network/NetworkContext.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react' + +import type { NetworkContextValue } from './types' + +export const NETWORK_CONTEXT_UNINITIALIZED = Symbol('NETWORK_CONTEXT_UNINITIALIZED') + +export const NetworkContext = createContext< + NetworkContextValue | typeof NETWORK_CONTEXT_UNINITIALIZED +>(NETWORK_CONTEXT_UNINITIALIZED) diff --git a/web-v2/src/providers/network/NetworkProvider.tsx b/web-v2/src/providers/network/NetworkProvider.tsx new file mode 100644 index 0000000..0db95a5 --- /dev/null +++ b/web-v2/src/providers/network/NetworkProvider.tsx @@ -0,0 +1,25 @@ +import { useMemo, useState } from 'react' + +import { env } from '@/constants/env' +import { createLwkNetwork, getLwk, type NetworkName } from '@/simplicity/lwk' + +import { NetworkContext } from './NetworkContext' +import type { NetworkContextValue } from './types' + +export function NetworkProvider({ children }: { children: React.ReactNode }) { + const [network, setNetwork] = useState(env.VITE_NETWORK ?? 'liquidtestnet') + + const value = useMemo( + () => ({ + network, + isTestnet: network === 'liquidtestnet', + isMainnet: network === 'liquid', + isRegtest: network === 'regtest', + setNetwork: setNetwork, + initLwkNetworkInstance: async () => createLwkNetwork(network, await getLwk()), + }), + [network], + ) + + return {children} +} diff --git a/web-v2/src/providers/network/types.ts b/web-v2/src/providers/network/types.ts new file mode 100644 index 0000000..f46add1 --- /dev/null +++ b/web-v2/src/providers/network/types.ts @@ -0,0 +1,10 @@ +import type { LwkNetwork, NetworkName } from '@/simplicity/lwk' + +export interface NetworkContextValue { + network: NetworkName + isTestnet: boolean + isMainnet: boolean + isRegtest: boolean + setNetwork: (network: NetworkName) => void + initLwkNetworkInstance: () => Promise +} diff --git a/web-v2/src/providers/network/useNetwork.ts b/web-v2/src/providers/network/useNetwork.ts new file mode 100644 index 0000000..ce17a6d --- /dev/null +++ b/web-v2/src/providers/network/useNetwork.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react' + +import { NETWORK_CONTEXT_UNINITIALIZED, NetworkContext } from './NetworkContext' +import type { NetworkContextValue } from './types' + +export function useNetwork(): NetworkContextValue { + const ctx = useContext(NetworkContext) + if (ctx === NETWORK_CONTEXT_UNINITIALIZED) { + throw new Error('useNetwork() must be used within ') + } + return ctx +} diff --git a/web-v2/src/simplicity/lwk.ts b/web-v2/src/simplicity/lwk.ts new file mode 100644 index 0000000..84951a2 --- /dev/null +++ b/web-v2/src/simplicity/lwk.ts @@ -0,0 +1,93 @@ +/** + * LWK (Liquid Wallet Kit) integration for Simplicity programs. + * - Initializes wasm once, exposes program creation and P2TR address helpers. + */ + +let lwkInit: Promise | null = null + +export async function getLwk(): Promise { + if (!lwkInit) { + lwkInit = (async () => { + const lwk = await import('lwk_web') + if (typeof lwk.default === 'function') await lwk.default() + return lwk + })() + } + return lwkInit +} + +/** LWK module (return type of getLwk()). Use for typing lwk argument across the app. */ +export type Lwk = Awaited> + +/** Instance of LWK SimplicityArguments. */ +export type LwkSimplicityArguments = InstanceType + +/** Instance of LWK XOnlyPublicKey. */ +export type LwkXOnlyPublicKey = ReturnType + +/** Instance of LWK Script. */ +export type LwkScript = InstanceType + +/** Instance of LWK TxOut (use instead of InstanceType — TxOut has a private constructor). */ +export type LwkTxOut = ReturnType + +/** Array of LWK TxOut (e.g. for getSighashAll / finalizeTransaction prevouts). */ +export type LwkTxOutArray = LwkTxOut[] + +/** Instance of LWK SimplicityProgram. */ +export type LwkSimplicityProgram = ReturnType + +/** Instance of LWK SimplicityTypedValue. */ +export type LwkSimplicityTypedValue = ReturnType + +/** Instance of LWK SimplicityWitnessValues. */ +export type LwkSimplicityWitnessValues = InstanceType + +/** Instance of LWK SimplicityType (for parsing type strings). */ +export type LwkSimplicityType = ReturnType + +/** Instance of LWK Keypair. */ +export type LwkKeypair = ReturnType + +/** LWK Network (return type of Network.mainnet() / Network.testnet()). */ +export type LwkNetwork = ReturnType + +/** LWK transaction type (first argument of getSighashAll / return of finalizeTransaction). */ +export type LwkTransaction = ReturnType + +export type NetworkName = 'liquid' | 'liquidtestnet' | 'regtest' + +/** PSET that can yield the unsigned transaction for LWK signing (extractTx). */ +export interface PsetWithExtractTx { + extractTx(): LwkTransaction +} + +export interface CreateP2trAddressParams { + source: string + args: LwkSimplicityArguments + internalKey: LwkXOnlyPublicKey + network: NetworkName +} + +/** + * Compile a Simplicity program from source + arguments and create its P2TR address. + */ +export async function createP2trAddress(params: CreateP2trAddressParams): Promise { + const lwk = await getLwk() + const { SimplicityProgram } = lwk + const program = SimplicityProgram.load(params.source, params.args) + const net = createLwkNetwork(params.network, lwk) + const address = program.createP2trAddress(params.internalKey, net) + return address.toString() +} + +export function createLwkNetwork(network: NetworkName, lwk: Lwk): LwkNetwork { + switch (network) { + case 'liquid': + return lwk.Network.mainnet() + case 'liquidtestnet': + return lwk.Network.testnet() + case 'regtest': + return lwk.Network.regtestDefault() + } +} diff --git a/web-v2/vite.config.ts b/web-v2/vite.config.ts index 292303e..3c2c642 100644 --- a/web-v2/vite.config.ts +++ b/web-v2/vite.config.ts @@ -2,6 +2,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import react from '@vitejs/plugin-react' +import fs from 'fs' import { defineConfig } from 'vite' import { checker } from 'vite-plugin-checker' @@ -9,7 +10,25 @@ const root = path.dirname(fileURLToPath(import.meta.url)) // https://vite.dev/config/ export default defineConfig({ + server: { + fs: { + allow: [root, path.resolve(root, '..', 'lwk_wasm', 'pkg_web')], + }, + }, plugins: [ + { + name: 'lwk-wasm-dev', + configureServer(server) { + const wasmFile = path.resolve(root, '..', 'lwk_wasm', 'pkg_web', 'lwk_wasm_bg.wasm') + server.middlewares.use((req, res, next) => { + if (!req.url) return next() + if (!req.url.endsWith('lwk_wasm_bg.wasm')) return next() + if (!fs.existsSync(wasmFile)) return next() + res.setHeader('Content-Type', 'application/wasm') + fs.createReadStream(wasmFile).pipe(res) + }) + }, + }, react(), checker({ overlay: { From f3c6f04a8c9887e03bfaaa679b98f077e36570e2 Mon Sep 17 00:00:00 2001 From: lilbonekit Date: Thu, 14 May 2026 14:31:58 +0300 Subject: [PATCH 2/2] chore: added lwk provider and updated dockerfile --- web-v2/Dockerfile | 23 ++++- web-v2/README.md | 2 + web-v2/docs/HOW_TO_BUILD_LWK_WASM.md | 6 +- web-v2/src/constants/env.ts | 2 + web-v2/src/lwk/index.ts | 44 +++++++++ web-v2/src/pages/Dashboard/index.tsx | 44 ++------- web-v2/src/providers/AppProviders.tsx | 4 +- web-v2/src/providers/lwk/LwkContext.ts | 9 ++ web-v2/src/providers/lwk/LwkProvider.tsx | 52 +++++++++++ web-v2/src/providers/lwk/types.ts | 13 +++ web-v2/src/providers/lwk/useLwk.ts | 12 +++ .../src/providers/network/NetworkContext.ts | 9 -- .../src/providers/network/NetworkProvider.tsx | 25 ----- web-v2/src/providers/network/types.ts | 10 -- web-v2/src/providers/network/useNetwork.ts | 12 --- web-v2/src/simplicity/lwk.ts | 93 ------------------- 16 files changed, 170 insertions(+), 190 deletions(-) create mode 100644 web-v2/src/lwk/index.ts create mode 100644 web-v2/src/providers/lwk/LwkContext.ts create mode 100644 web-v2/src/providers/lwk/LwkProvider.tsx create mode 100644 web-v2/src/providers/lwk/types.ts create mode 100644 web-v2/src/providers/lwk/useLwk.ts delete mode 100644 web-v2/src/providers/network/NetworkContext.ts delete mode 100644 web-v2/src/providers/network/NetworkProvider.tsx delete mode 100644 web-v2/src/providers/network/types.ts delete mode 100644 web-v2/src/providers/network/useNetwork.ts delete mode 100644 web-v2/src/simplicity/lwk.ts diff --git a/web-v2/Dockerfile b/web-v2/Dockerfile index 5917d40..7849d14 100644 --- a/web-v2/Dockerfile +++ b/web-v2/Dockerfile @@ -1,9 +1,28 @@ -FROM node:20-alpine AS builder +FROM rust:1.90-bookworm AS lwk-builder + +ARG LWK_REF=wasm_0.16.0 +ARG LWK_COMMIT=4786e456db6efc05413721f6489899a6426eaa47 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git pkg-config libssl-dev ca-certificates clang \ + && rm -rf /var/lib/apt/lists/* +RUN rustup target add wasm32-unknown-unknown +RUN cargo install wasm-pack --locked +RUN git clone --depth 1 --branch "${LWK_REF}" https://github.com/Blockstream/lwk.git /tmp/lwk +RUN test "$(git -C /tmp/lwk rev-parse HEAD)" = "${LWK_COMMIT}" +WORKDIR /tmp/lwk/lwk_wasm +RUN RUSTFLAGS='--cfg web_sys_unstable_apis' wasm-pack build --target web --out-dir pkg_web --features simplicity,serial + +FROM node:22-bookworm AS builder WORKDIR /app -RUN corepack enable +RUN corepack enable && \ + corepack prepare pnpm@10.33.0 --activate COPY package.json pnpm-lock.yaml ./ + +COPY --from=lwk-builder /tmp/lwk/lwk_wasm/pkg_web /lwk_wasm/pkg_web + RUN pnpm install --frozen-lockfile COPY . . diff --git a/web-v2/README.md b/web-v2/README.md index b0bbb81..f6adc2a 100644 --- a/web-v2/README.md +++ b/web-v2/README.md @@ -27,6 +27,8 @@ Copy `.env.example` to `.env` and adjust as needed. ## Install +> To build the LWK WASM module required by this app, see [docs/HOW_TO_BUILD_LWK_WASM.md](docs/HOW_TO_BUILD_LWK_WASM.md). + ```bash pnpm install ``` diff --git a/web-v2/docs/HOW_TO_BUILD_LWK_WASM.md b/web-v2/docs/HOW_TO_BUILD_LWK_WASM.md index 1f14ca2..d2399f7 100644 --- a/web-v2/docs/HOW_TO_BUILD_LWK_WASM.md +++ b/web-v2/docs/HOW_TO_BUILD_LWK_WASM.md @@ -5,11 +5,11 @@ ## 1. **Build lwk-builder stage:** ```bash -# run from repo root (contains web/ and web-v2/) -docker build -f web/Dockerfile --target lwk-builder -t lwk-builder . +# run from repo root +docker build -f web-v2/Dockerfile --target lwk-builder -t lwk-builder . ``` -## **2. Extract pkg_web to repo root pkg_web**: +## **2. Extract pkg_web to repo root:** ```bash docker create --name tmp lwk-builder diff --git a/web-v2/src/constants/env.ts b/web-v2/src/constants/env.ts index 1d4379f..b60860f 100644 --- a/web-v2/src/constants/env.ts +++ b/web-v2/src/constants/env.ts @@ -13,3 +13,5 @@ export const env = envSchema.parse({ }) export type AppEnv = z.infer + +export type NetworkName = AppEnv['VITE_NETWORK'] diff --git a/web-v2/src/lwk/index.ts b/web-v2/src/lwk/index.ts new file mode 100644 index 0000000..7225c34 --- /dev/null +++ b/web-v2/src/lwk/index.ts @@ -0,0 +1,44 @@ +import type { Network, SimplicityArguments, Transaction, XOnlyPublicKey } from 'lwk_web' + +import type { NetworkName } from '@/constants/env' + +export type Lwk = typeof import('lwk_web') + +let lwk: Lwk | null = null + +export async function getLwk(): Promise { + if (!lwk) { + lwk = await import('lwk_web') + if (typeof lwk.default === 'function') await lwk.default() + } + return lwk +} + +export function createLwkNetwork(network: NetworkName, lwk: Lwk): Network { + switch (network) { + case 'liquid': + return lwk.Network.mainnet() + case 'liquidtestnet': + return lwk.Network.testnet() + case 'regtest': + return lwk.Network.regtestDefault() + } +} + +export interface PsetWithExtractTx { + extractTx(): Transaction +} + +export interface CreateP2trAddressParams { + source: string + args: SimplicityArguments + internalKey: XOnlyPublicKey + network: NetworkName +} + +export function createP2trAddress(lwk: Lwk, params: CreateP2trAddressParams): string { + const program = lwk.SimplicityProgram.load(params.source, params.args) + const net = createLwkNetwork(params.network, lwk) + const address = program.createP2trAddress(params.internalKey, net) + return address.toString() +} diff --git a/web-v2/src/pages/Dashboard/index.tsx b/web-v2/src/pages/Dashboard/index.tsx index a2ed8cc..fab02c8 100644 --- a/web-v2/src/pages/Dashboard/index.tsx +++ b/web-v2/src/pages/Dashboard/index.tsx @@ -1,40 +1,17 @@ -import { useEffect, useState } from 'react' - -import { useNetwork } from '@/providers/network/useNetwork' -import type { LwkNetwork } from '@/simplicity/lwk' - -interface NetworkInfo { - label: string - genesisBlockHash: string - defaultExplorerUrl: string - policyAsset: string -} +import { useLwk } from '@/providers/lwk/useLwk' // EXAMPLE OF LWK USAGE export default function DashboardPage() { - const { network, isTestnet, isMainnet, isRegtest, initLwkNetworkInstance } = useNetwork() - const [info, setInfo] = useState(null) - const [error, setError] = useState(null) - - useEffect(() => { - let net: LwkNetwork | null = null - - initLwkNetworkInstance() - .then(instance => { - net = instance - setInfo({ - label: instance.toString(), - genesisBlockHash: instance.genesisBlockHash(), - defaultExplorerUrl: instance.defaultExplorerUrl(), - policyAsset: instance.policyAsset().toString(), - }) - }) - .catch(err => setError(String(err))) + const { network, isTestnet, isMainnet, isRegtest, lwkNetwork } = useLwk() - return () => { - net?.free() - } - }, [initLwkNetworkInstance, network]) + const info = lwkNetwork + ? { + label: lwkNetwork.toString(), + genesisBlockHash: lwkNetwork.genesisBlockHash(), + defaultExplorerUrl: lwkNetwork.defaultExplorerUrl(), + policyAsset: lwkNetwork.policyAsset().toString(), + } + : null return (
@@ -46,7 +23,6 @@ export default function DashboardPage() { isTestnet: {isTestnet.toString()} / isMainnet: {isMainnet.toString()} / isRegtest:{' '} {isRegtest.toString()}

- {error &&

{error}

} {info && (
LWK label
diff --git a/web-v2/src/providers/AppProviders.tsx b/web-v2/src/providers/AppProviders.tsx index 731c26a..e15dbbf 100644 --- a/web-v2/src/providers/AppProviders.tsx +++ b/web-v2/src/providers/AppProviders.tsx @@ -1,14 +1,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import type { PropsWithChildren } from 'react' -import { NetworkProvider } from './network/NetworkProvider' +import { LwkProvider } from './lwk/LwkProvider' const queryClient = new QueryClient() export function AppProviders({ children }: PropsWithChildren) { return ( - {children} + {children} ) } diff --git a/web-v2/src/providers/lwk/LwkContext.ts b/web-v2/src/providers/lwk/LwkContext.ts new file mode 100644 index 0000000..63dfbfb --- /dev/null +++ b/web-v2/src/providers/lwk/LwkContext.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react' + +import type { LwkContextValue } from './types' + +export const LWK_CONTEXT_UNINITIALIZED = Symbol('LWK_CONTEXT_UNINITIALIZED') + +export const LwkContext = createContext( + LWK_CONTEXT_UNINITIALIZED, +) diff --git a/web-v2/src/providers/lwk/LwkProvider.tsx b/web-v2/src/providers/lwk/LwkProvider.tsx new file mode 100644 index 0000000..24aa382 --- /dev/null +++ b/web-v2/src/providers/lwk/LwkProvider.tsx @@ -0,0 +1,52 @@ +import { useEffect, useMemo, useState } from 'react' + +import { env } from '@/constants/env' +import { createLwkNetwork, getLwk, type Lwk } from '@/lwk' + +import { LwkContext } from './LwkContext' + +const network = env.VITE_NETWORK + +export function LwkProvider({ children }: { children: React.ReactNode }) { + const [lwk, setLwk] = useState(null) + + useEffect(() => { + let cancelled = false + + getLwk().then(instance => { + if (!cancelled) setLwk(instance) + }) + + return () => { + cancelled = true + } + }, []) + + const lwkNetwork = useMemo(() => (lwk ? createLwkNetwork(network, lwk) : null), [lwk]) + + useEffect(() => { + return () => { + lwkNetwork?.free() + } + }, [lwkNetwork]) + + if (!lwk || !lwkNetwork) { + // TODO: Replace with proper loader after UI framework setup + return
Loading...
+ } + + return ( + + {children} + + ) +} diff --git a/web-v2/src/providers/lwk/types.ts b/web-v2/src/providers/lwk/types.ts new file mode 100644 index 0000000..85e5e55 --- /dev/null +++ b/web-v2/src/providers/lwk/types.ts @@ -0,0 +1,13 @@ +import type { Network } from 'lwk_web' + +import type { NetworkName } from '@/constants/env' +import type { Lwk } from '@/lwk' + +export interface LwkContextValue { + lwk: Lwk + lwkNetwork: Network + network: NetworkName + isTestnet: boolean + isMainnet: boolean + isRegtest: boolean +} diff --git a/web-v2/src/providers/lwk/useLwk.ts b/web-v2/src/providers/lwk/useLwk.ts new file mode 100644 index 0000000..5b0a29c --- /dev/null +++ b/web-v2/src/providers/lwk/useLwk.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react' + +import { LWK_CONTEXT_UNINITIALIZED, LwkContext } from './LwkContext' +import type { LwkContextValue } from './types' + +export function useLwk(): LwkContextValue { + const ctx = useContext(LwkContext) + if (ctx === LWK_CONTEXT_UNINITIALIZED) { + throw new Error('useLwk() must be used within ') + } + return ctx +} diff --git a/web-v2/src/providers/network/NetworkContext.ts b/web-v2/src/providers/network/NetworkContext.ts deleted file mode 100644 index c824303..0000000 --- a/web-v2/src/providers/network/NetworkContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext } from 'react' - -import type { NetworkContextValue } from './types' - -export const NETWORK_CONTEXT_UNINITIALIZED = Symbol('NETWORK_CONTEXT_UNINITIALIZED') - -export const NetworkContext = createContext< - NetworkContextValue | typeof NETWORK_CONTEXT_UNINITIALIZED ->(NETWORK_CONTEXT_UNINITIALIZED) diff --git a/web-v2/src/providers/network/NetworkProvider.tsx b/web-v2/src/providers/network/NetworkProvider.tsx deleted file mode 100644 index 0db95a5..0000000 --- a/web-v2/src/providers/network/NetworkProvider.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useMemo, useState } from 'react' - -import { env } from '@/constants/env' -import { createLwkNetwork, getLwk, type NetworkName } from '@/simplicity/lwk' - -import { NetworkContext } from './NetworkContext' -import type { NetworkContextValue } from './types' - -export function NetworkProvider({ children }: { children: React.ReactNode }) { - const [network, setNetwork] = useState(env.VITE_NETWORK ?? 'liquidtestnet') - - const value = useMemo( - () => ({ - network, - isTestnet: network === 'liquidtestnet', - isMainnet: network === 'liquid', - isRegtest: network === 'regtest', - setNetwork: setNetwork, - initLwkNetworkInstance: async () => createLwkNetwork(network, await getLwk()), - }), - [network], - ) - - return {children} -} diff --git a/web-v2/src/providers/network/types.ts b/web-v2/src/providers/network/types.ts deleted file mode 100644 index f46add1..0000000 --- a/web-v2/src/providers/network/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { LwkNetwork, NetworkName } from '@/simplicity/lwk' - -export interface NetworkContextValue { - network: NetworkName - isTestnet: boolean - isMainnet: boolean - isRegtest: boolean - setNetwork: (network: NetworkName) => void - initLwkNetworkInstance: () => Promise -} diff --git a/web-v2/src/providers/network/useNetwork.ts b/web-v2/src/providers/network/useNetwork.ts deleted file mode 100644 index ce17a6d..0000000 --- a/web-v2/src/providers/network/useNetwork.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useContext } from 'react' - -import { NETWORK_CONTEXT_UNINITIALIZED, NetworkContext } from './NetworkContext' -import type { NetworkContextValue } from './types' - -export function useNetwork(): NetworkContextValue { - const ctx = useContext(NetworkContext) - if (ctx === NETWORK_CONTEXT_UNINITIALIZED) { - throw new Error('useNetwork() must be used within ') - } - return ctx -} diff --git a/web-v2/src/simplicity/lwk.ts b/web-v2/src/simplicity/lwk.ts deleted file mode 100644 index 84951a2..0000000 --- a/web-v2/src/simplicity/lwk.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * LWK (Liquid Wallet Kit) integration for Simplicity programs. - * - Initializes wasm once, exposes program creation and P2TR address helpers. - */ - -let lwkInit: Promise | null = null - -export async function getLwk(): Promise { - if (!lwkInit) { - lwkInit = (async () => { - const lwk = await import('lwk_web') - if (typeof lwk.default === 'function') await lwk.default() - return lwk - })() - } - return lwkInit -} - -/** LWK module (return type of getLwk()). Use for typing lwk argument across the app. */ -export type Lwk = Awaited> - -/** Instance of LWK SimplicityArguments. */ -export type LwkSimplicityArguments = InstanceType - -/** Instance of LWK XOnlyPublicKey. */ -export type LwkXOnlyPublicKey = ReturnType - -/** Instance of LWK Script. */ -export type LwkScript = InstanceType - -/** Instance of LWK TxOut (use instead of InstanceType — TxOut has a private constructor). */ -export type LwkTxOut = ReturnType - -/** Array of LWK TxOut (e.g. for getSighashAll / finalizeTransaction prevouts). */ -export type LwkTxOutArray = LwkTxOut[] - -/** Instance of LWK SimplicityProgram. */ -export type LwkSimplicityProgram = ReturnType - -/** Instance of LWK SimplicityTypedValue. */ -export type LwkSimplicityTypedValue = ReturnType - -/** Instance of LWK SimplicityWitnessValues. */ -export type LwkSimplicityWitnessValues = InstanceType - -/** Instance of LWK SimplicityType (for parsing type strings). */ -export type LwkSimplicityType = ReturnType - -/** Instance of LWK Keypair. */ -export type LwkKeypair = ReturnType - -/** LWK Network (return type of Network.mainnet() / Network.testnet()). */ -export type LwkNetwork = ReturnType - -/** LWK transaction type (first argument of getSighashAll / return of finalizeTransaction). */ -export type LwkTransaction = ReturnType - -export type NetworkName = 'liquid' | 'liquidtestnet' | 'regtest' - -/** PSET that can yield the unsigned transaction for LWK signing (extractTx). */ -export interface PsetWithExtractTx { - extractTx(): LwkTransaction -} - -export interface CreateP2trAddressParams { - source: string - args: LwkSimplicityArguments - internalKey: LwkXOnlyPublicKey - network: NetworkName -} - -/** - * Compile a Simplicity program from source + arguments and create its P2TR address. - */ -export async function createP2trAddress(params: CreateP2trAddressParams): Promise { - const lwk = await getLwk() - const { SimplicityProgram } = lwk - const program = SimplicityProgram.load(params.source, params.args) - const net = createLwkNetwork(params.network, lwk) - const address = program.createP2trAddress(params.internalKey, net) - return address.toString() -} - -export function createLwkNetwork(network: NetworkName, lwk: Lwk): LwkNetwork { - switch (network) { - case 'liquid': - return lwk.Network.mainnet() - case 'liquidtestnet': - return lwk.Network.testnet() - case 'regtest': - return lwk.Network.regtestDefault() - } -}