-
-
Notifications
You must be signed in to change notification settings - Fork 306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add support for browser bundle for lightclient #6673
Changes from 19 commits
7ed55ae
c1e138c
9407a08
d06c336
6048592
e447b9e
21e9c50
60e36a4
d803c0e
cae32b2
b3831a7
38bcba0
202cc3c
2557e10
cfc03a7
fa9d5d5
3b6a171
ca35ddb
ba5886a
2451dc1
d800cb6
ebd79e3
0929b36
bf57160
0b74f5d
7e539ad
6d5cec7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,19 +48,22 @@ lodestar lightclient \ | |
|
||
## Light-Client Programmatic Example | ||
|
||
For this example we will assume there is a running beacon node at `https://beacon-node.your-domain.com` | ||
For this example we will assume there is a running beacon node at `https://lodestar-mainnet.chainsafe.io` | ||
|
||
```ts | ||
import {getClient} from "@lodestar/api"; | ||
import {createChainForkConfig} from "@lodestar/config"; | ||
import {networksChainConfig} from "@lodestar/config/networks"; | ||
import {Lightclient, LightclientEvent} from "@lodestar/light-client"; | ||
import {LightClientRestTransport} from "@lodestar/light-client/transport"; | ||
import {getFinalizedSyncCheckpoint, getGenesisData, getLcLoggerConsole} from "@lodestar/light-client/utils"; | ||
|
||
const config = createChainForkConfig(networksChainConfig.mainnet); | ||
const logger = getLcLoggerConsole({logDebug: Boolean(process.env.DEBUG)}); | ||
const api = getClient({urls: ["https://beacon-node.your-domain.com"]}, {config}); | ||
import { | ||
getFinalizedSyncCheckpoint, | ||
getGenesisData, | ||
getConsoleLogger, | ||
getApiFromUrl, | ||
getChainForkConfigFromNetwork, | ||
} from "@lodestar/light-client/utils"; | ||
|
||
const config = getChainForkConfigFromNetwork("mainnet"); | ||
const logger = getConsoleLogger({logDebug: Boolean(process.env.DEBUG)}); | ||
const api = getApiFromUrl({urls: ["https://lodestar-mainnet.chainsafe.io"]}, {config}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question about using our real URL There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to have an example which people can run by just copying the code. We have similar pattern in the prover already but pointing to sepolia url. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that is a fine idea.... just not our mainnet nodes |
||
|
||
const lightclient = await Lightclient.initializeFromCheckpointRoot({ | ||
config, | ||
|
@@ -88,6 +91,31 @@ lightclient.emitter.on(LightclientEvent.lightClientOptimisticHeader, async (opti | |
}); | ||
``` | ||
|
||
## Browser Integration | ||
|
||
If you want to use Lightclient in browser and facing some issues in building it with bundlers like webpack, vite. We suggest to use our distribution build. The support for single distribution build is started from `1.18.0` version. | ||
nazarhussain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Directly link the dist build with the `<script />` tag with tools like unpkg or other. e.g. | ||
|
||
```html | ||
<script src="https://www.unpkg.com/@lodestar/light-client@1.18.0/dist/lightclient.es.min.js" type="module"> | ||
``` | ||
|
||
Then the lightclient package will be exposed to `globalThis`, in case of browser environment that will be `window`. You can access the package as `window.lodestar.lightclient`. All named exports will also be available from this interface. e.g. `window.lodestar.lightclient.transport`. | ||
|
||
NOTE: Due to `top-level-await` used in one of dependent library, the package will not be available right after the load. You have to use a hack to clear up that await from the event loop. | ||
|
||
```html | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be nice to find a way to remove the need for this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it will not. there will always be a top level await in esm because the bindings path is programmatic so its not imported it looked up and then There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i suppose it would be possible in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's possible avoid top-level-import, but then would have to leave upto user of the library to init where appropriate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think there is a difference between using top-level await and a ESM module without it. Eg. Node 22 will ship a new feature to In any case, we might wanna avoid top-level await in packages that are used by others. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made the top-level await go away in |
||
<script> | ||
window.addEventListener("DOMContentLoaded", () => { | ||
setTimeout(function () { | ||
// here you can access the Lightclient | ||
// window.lodestar.lightclient | ||
}, 50); | ||
}); | ||
</script> | ||
``` | ||
|
||
## Contributors | ||
|
||
Read our [contribution documentation](https://chainsafe.github.io/lodestar/contribution/getting-started), [submit an issue](https://github.com/ChainSafe/lodestar/issues/new/choose) or talk to us on our [discord](https://discord.gg/yjyvFRP)! | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export * from "./index.js"; | ||
|
||
// To kep the consistent interface as npm package and browser | ||
// Use consistent names as named exports | ||
export * as utils from "./utils/index.js"; | ||
export * as validation from "./validation.js"; | ||
export * as transport from "./transport/index.js"; | ||
nazarhussain marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import {getClient, Api} from "@lodestar/api"; | ||
import {ChainForkConfig, createChainForkConfig} from "@lodestar/config"; | ||
import {NetworkName, networksChainConfig} from "@lodestar/config/networks"; | ||
|
||
export function getApiFromUrl(url: string, network: NetworkName): Api { | ||
if (!(network in networksChainConfig)) { | ||
throw Error(`Invalid network name "${network}". Valid options are: ${Object.keys(networksChainConfig).join()}`); | ||
} | ||
|
||
return getClient({urls: [url]}, {config: createChainForkConfig(networksChainConfig[network])}); | ||
} | ||
|
||
export function getChainForkConfigFromNetwork(network: NetworkName): ChainForkConfig { | ||
if (!(network in networksChainConfig)) { | ||
throw Error(`Invalid network name "${network}". Valid options are: ${Object.keys(networksChainConfig).join()}`); | ||
} | ||
|
||
return createChainForkConfig(networksChainConfig[network]); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call */ | ||
import {expect, describe, it, beforeEach, vi} from "vitest"; | ||
import "../../dist/lightclient.min.mjs"; | ||
|
||
describe("web bundle for lightclient", () => { | ||
vi.setConfig({testTimeout: 10_000}); | ||
|
||
let lightclient: any; | ||
|
||
beforeEach(() => { | ||
lightclient = (window as any)["lodestar"]["lightclient"]; | ||
}); | ||
|
||
it("should have a global interface", () => { | ||
expect(lightclient).toBeDefined(); | ||
}); | ||
|
||
it("should have all relevant exports", () => { | ||
expect(lightclient).toHaveProperty("Lightclient"); | ||
expect(lightclient).toHaveProperty("LightclientEvent"); | ||
expect(lightclient).toHaveProperty("RunStatusCode"); | ||
expect(lightclient).toHaveProperty("upgradeLightClientFinalityUpdate"); | ||
expect(lightclient).toHaveProperty("upgradeLightClientOptimisticUpdate"); | ||
expect(lightclient).toHaveProperty("utils"); | ||
expect(lightclient).toHaveProperty("transport"); | ||
expect(lightclient).toHaveProperty("validation"); | ||
|
||
expect(lightclient.Lightclient).toBeTypeOf("function"); | ||
}); | ||
|
||
it("should start the lightclient and sync", async () => { | ||
const {Lightclient, LightclientEvent, transport, utils} = lightclient; | ||
|
||
const logger = utils.getConsoleLogger({logDebug: true}); | ||
const config = utils.getChainForkConfigFromNetwork("mainnet"); | ||
|
||
// TODO: Decide to check which node to use in testing | ||
// We have one node in CI, but that only starts with e2e tests | ||
const api = utils.getApiFromUrl("https://lodestar-mainnet.chainsafe.io", "mainnet"); | ||
|
||
const lc = await Lightclient.initializeFromCheckpointRoot({ | ||
config, | ||
logger, | ||
transport: new transport.LightClientRestTransport(api), | ||
genesisData: await utils.getGenesisData(api), | ||
checkpointRoot: await utils.getFinalizedSyncCheckpoint(api), | ||
opts: { | ||
allowForcedUpdates: true, | ||
updateHeadersOnForcedUpdate: true, | ||
}, | ||
}); | ||
|
||
await expect(lc.start()).resolves.toBeUndefined(); | ||
|
||
await expect( | ||
new Promise((resolve) => { | ||
lc.emitter.on(LightclientEvent.lightClientOptimisticHeader, async (optimisticUpdate: unknown) => { | ||
resolve(optimisticUpdate); | ||
}); | ||
}) | ||
).resolves.toBeDefined(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"exclude": ["src/index.browser.ts"], | ||
"compilerOptions": {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import {defineConfig, mergeConfig} from "vite"; | ||
import {getBaseViteConfig} from "../../vite.base.config.js"; | ||
|
||
import pkgJSON from "./package.json"; | ||
|
||
export default mergeConfig( | ||
getBaseViteConfig(pkgJSON, {libName: "LightClient", entry: "src/index.browser.ts"}), | ||
defineConfig({ | ||
build: { | ||
rollupOptions: { | ||
output: { | ||
footer: ` | ||
globalThis.lodestar = globalThis.lodestar === undefined ? {} : globalThis.lodestar; | ||
globalThis.lodestar.lightclient = { | ||
Lightclient, | ||
LightclientEvent, | ||
RunStatusCode, | ||
upgradeLightClientFinalityUpdate, | ||
upgradeLightClientOptimisticUpdate, | ||
utils: index$1, | ||
transport: index, | ||
nazarhussain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
validation | ||
}; | ||
`, | ||
}, | ||
}, | ||
}, | ||
}) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure we want to broadcast this URL? @philknows @wemeetagain
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These URLs are already been shown and used in the light client demo, so if one want's to use they would have already gotten it from the demo page.
If anyone have different opinion we can revert it in other PR.