Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
acd76ad
Add basic EmbeddedEIP191Signer
deuszx May 28, 2025
89496ed
Use IJsSigner interface exported from linera-protocol
deuszx May 29, 2025
1086fca
Work with string types in JS api
deuszx May 29, 2025
3d9516f
Move the sample Embedded signer to linera-protocol repo
deuszx May 30, 2025
a8bb8eb
Use updated APIs
deuszx Jun 2, 2025
01610ef
Integrate MetaMask example
deuszx Jun 3, 2025
f236412
Update hosted fungible demo to use embedded signer
deuszx Jun 3, 2025
052b2f3
Fix CI
deuszx Jun 3, 2025
369b78d
Adapt to work with latest version of linera-web
deuszx Jun 4, 2025
39c57b5
Update git submodule to point at main. Workspace deps
deuszx Jun 9, 2025
300de33
Update to work w/ latest main branch
deuszx Jun 11, 2025
23c9787
Pull in faucet url and app IDs via env vars
deuszx Jun 12, 2025
43f2b6f
Make base demos use the PrivateKey signers
deuszx Jun 12, 2025
6e71b49
Add separate counter with MetaMask integration
deuszx Jun 12, 2025
93f5409
Remove imported Buffer
deuszx Jun 12, 2025
4fad1ef
COmmit newlines at the end of the files
deuszx Jun 12, 2025
26be11f
Initialize a Signer in extension example
deuszx Jun 13, 2025
1ce1a4b
Add section to README about env vars
deuszx Jun 13, 2025
af5b5af
Replace magic-nix-cache with flakehub-cache
deuszx Jun 13, 2025
8575cf4
Remove extension from workspace
deuszx Jun 13, 2025
6730616
Move linera-protocol out of the workspace
deuszx Jun 13, 2025
01328d9
See if this helps
deuszx Jun 13, 2025
ad24510
Update submodule
deuszx Jun 14, 2025
67d2443
Move linera-protocol back to workspace member
deuszx Jun 14, 2025
a887355
Call install --frozen-lockfile in root package.json
deuszx Jun 14, 2025
ecb6e5f
Update to use main linera-protocol
deuszx Jun 14, 2025
c980910
Merge remote-tracking branch 'origin/main' into embedded-js-signer
deuszx Jun 23, 2025
b8867be
Do not install deps in pnpm build for hosted-counter eexample
deuszx Jun 23, 2025
1e31661
Generate new privkey for every app load
deuszx Jun 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_FAUCET_URL=https://faucet.testnet-babbage.linera.net
VITE_COUNTER_APP_ID=
VITE_FUNGIBLE_APP_ID=
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
with:
submodules: true
- uses: cachix/install-nix-action@v26
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: DeterminateSystems/flakehub-cache-action@main
- name: Run all CI tasks
run: |
nix develop '.?submodules=1' --override-input linera-protocol ./linera-protocol --command pnpm run ci
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,12 @@ For development, you can also use `pnpm build --watch` to
automatically rebuild the extension on change. Changes to the client
worker will not propagate to the extension, but once you run
`wasm-pack build` they will be picked up.


## Examples

In `/examples` directory there are a couple of examples that can be run
to test the Linera network. The faucet address and application addresses
are controlled via ENV variables. Those can be set in `.env` file in the root
of the project or you can overwrite them in `.env.production.local` -
a file ignored by git and so needs to be created locally.
112 changes: 112 additions & 0 deletions examples/hosted-counter-metamask/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linera | Counter</title>
<link href="/style.css" rel="stylesheet">
<link href="/icon.png" rel="icon">
<style type="text/css">
.ui {
display: flex;
flex-direction: column;
}

.ui .counter {
font-size: 1.25rem;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="description">
<h1>Counter</h1>
<p>
This is a simple application tracking some on-chain state that remembers the value of an integer counter.
</p>
<p>
Click the button to submit a block that increments the counter, and watch your local node's state update in real-time.
</p>
</div>
<div class="ui">
<p class="counter">Clicks: <span id="count">0</span></p>
<button id="increment-btn">Click me!</button>
</div>
</div>

<div class="logs">
<h2>Connected as <code id="owner" class="hex">requesting owner…</code> </h2>
<h2>Chain history for <code id="chain-id" class="hex">requesting chain…</code></h2>
<ul id="logs">
<template>
<li>
<span class="height"></span>: <span class="code hash"></span>
</li>
</template>
</div>
</div>
</div>

<script type="importmap">
{
"imports": {
"@linera/client": "./js/@linera/client/linera_web.js",
"@linera/signer": "./js/@linera/signer/index.js"
}
}
</script>

<script type="module">
import * as linera from '@linera/client';
import * as linera_signer from '@linera/signer';

const COUNTER_APP_ID = import.meta.env.VITE_COUNTER_APP_ID;

async function run() {
await linera.default();
const faucet = await new linera.Faucet(import.meta.env.VITE_FAUCET_URL);
const signer = await new linera_signer.MetaMaskEIP191Signer();
const wallet = await faucet.createWallet();
const owner = await signer.address();
document.getElementById('owner').innerText = owner;
document.getElementById('chain-id').innerText = await faucet.claimChain(wallet, owner);
const client = await new linera.Client(wallet, signer);
const counter = await client.frontend().application(COUNTER_APP_ID);
const logs = document.getElementById('logs');
const incrementButton = document.getElementById('increment-btn');
const blockTemplate = document.getElementById('block-template');

function addLogEntry(block) {
const entry = logs.getElementsByTagName('template')[0].content.cloneNode(true);
entry.querySelector('.height').textContent = block.height;
entry.querySelector('.hash').textContent = block.hash;
logs.insertBefore(entry, logs.firstChild);
}

async function updateCount() {
const response = await counter.query('{ "query": "query { value }" }');
document.getElementById('count').innerText
= JSON.parse(response).data.value;
}

updateCount();
client.onNotification(notification => {
let newBlock = notification.reason.NewBlock;
if (!newBlock) return;
addLogEntry(newBlock);
updateCount();
});

incrementButton.addEventListener('click', () => {
counter.query('{ "query": "mutation { increment(value: 1) }" }');
});
}

if (document.readyState === 'loading')
document.addEventListener('DOMContentLoaded', run);
else
run();
</script>
</body>
</html>
21 changes: 21 additions & 0 deletions examples/hosted-counter-metamask/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@linera/examples/hosted-counter-metamask",
"private": true,
"author": "Linera <[email protected]>",
"license": "Apache-2.0",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"ci": "pnpm build"
},
"dependencies": {
"@linera/client": "workspace:*",
"@linera/signer": "workspace:*"
},
"devDependencies": {
"vite": "^5.4.11"
}
}
1 change: 1 addition & 0 deletions examples/hosted-counter-metamask/public/arrow.svg
1 change: 1 addition & 0 deletions examples/hosted-counter-metamask/public/icon.png
1 change: 1 addition & 0 deletions examples/hosted-counter-metamask/public/js/@linera/client
1 change: 1 addition & 0 deletions examples/hosted-counter-metamask/public/style.css
27 changes: 27 additions & 0 deletions examples/hosted-counter-metamask/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineConfig } from 'vite';

// https://vitejs.dev/config/
export default defineConfig({
base: '/hosted/counter/',
server: {
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
},
build: {
rollupOptions: {
external: ['@linera/client'],
},
},
esbuild: {
supported: {
'top-level-await': true,
},
},
optimizeDeps: {
exclude: [
'@linera/client',
],
},
})
17 changes: 11 additions & 6 deletions examples/hosted-counter/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ <h1>Counter</h1>
</div>

<div class="logs">
<h2>Connected as <code id="owner" class="hex">requesting owner…</code> </h2>
<h2>Chain history for <code id="chain-id" class="hex">requesting chain…</code></h2>
<ul id="logs">
<template>
Expand All @@ -50,22 +51,27 @@ <h2>Chain history for <code id="chain-id" class="hex">requesting chain…</code>
<script type="importmap">
{
"imports": {
"@linera/client": "./js/@linera/client/linera_web.js"
"@linera/client": "./js/@linera/client/linera_web.js",
"@linera/signer": "./js/@linera/signer/index.js"
}
}
</script>

<script type="module">
import * as linera from '@linera/client';
import { PrivateKey } from '@linera/signer';

const COUNTER_APP_ID = '2b1a0df8868206a4b7d6c2fdda911e4355d6c0115b896d4947ef8e535ee3c6b8';
const COUNTER_APP_ID = import.meta.env.VITE_COUNTER_APP_ID;

async function run() {
await linera.default();
const faucet = await new linera.Faucet('https://faucet.testnet-babbage.linera.net');
const faucet = await new linera.Faucet(import.meta.env.VITE_FAUCET_URL);
const signer = new PrivateKey("f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66");
const wallet = await faucet.createWallet();
const client = await new linera.Client(wallet);
document.getElementById('chain-id').innerText = await faucet.claimChain(client);
const owner = await signer.address();
document.getElementById('owner').innerText = owner;
document.getElementById('chain-id').innerText = await faucet.claimChain(wallet, owner);
const client = await new linera.Client(wallet, signer);
const counter = await client.frontend().application(COUNTER_APP_ID);
const logs = document.getElementById('logs');
const incrementButton = document.getElementById('increment-btn');
Expand All @@ -78,7 +84,6 @@ <h2>Chain history for <code id="chain-id" class="hex">requesting chain…</code>
logs.insertBefore(entry, logs.firstChild);
}


async function updateCount() {
const response = await counter.query('{ "query": "query { value }" }');
document.getElementById('count').innerText
Expand Down
5 changes: 3 additions & 2 deletions examples/hosted-counter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
"ci": "pnpm build"
},
"dependencies": {
"@linera/client": "workspace:^"
"@linera/client": "workspace:*",
"@linera/signer": "workspace:*"
},
"devDependencies": {
"vite": "^5.4.10"
"vite": "^5.4.11"
}
}
14 changes: 10 additions & 4 deletions examples/hosted-fungible/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ <h4>Transfer</h4>
</div>

<div class="logs">
<h2>Connected as <code id="owner" class="hex">requesting owner…</code> </h2>
<h2>Chain history for <code id="chain-id" class="hex">requesting a new microchain…</code></h2>
<ul id="logs">
<template>
Expand All @@ -140,8 +141,10 @@ <h2>Chain history for <code id="chain-id" class="hex">requesting a new microchai

<script type="module">
import * as linera from '@linera/client';
import { PrivateKey } from '@linera/signer';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think it would be better to use this qualified as signer.PrivateKey. Otherwise it's not necessarily clear to the reader that this is a private-key signer and not just a private key.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I guess it was imported unqualified because the module name used to be suffixed to the class name as well, but that's no longer the case)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought signer.PrivateKey is unnecessary and files are small enough to easily see where the import comes from.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I guess it was imported unqualified because the module name used to be suffixed to the class name as well, but that's no longer the case)

Exactly :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of agree, which is why it's a nit, but for people encountering this API for the first time it might be best to be super explicit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why I wanted to call this PrivateKeySigner - to avoid confusion like this :)

Copy link
Collaborator

@Twey Twey Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've written this before but — the reason modules are nice to have as a language construct is that the information that is obvious about a name changes depending on the context. Back in the good old days of C we would always have to write struct linera_signer_private_key everywhere because there weren't any kind of modules to put things in. That's usually sufficiently informative, but in a lot of cases you want to do something a bit different:

  • inside the Linera codebase (or a Linera-specific module in an external codebase) it's pretty obvious that you're talking about Linera things, so calling it a ‘Linera private key signer’ (LineraPrivateKeySigner) isn't necessary, and it's enough to refer to it as a ‘private key signer’ (signer.PrivateKey);
  • inside a ‘signer’ module (e.g. abstraction layer) of some external project it might not be obvious that this thing is specific to Linera but it is obvious that it's a signer, so you want to refer to it as a ‘Linera private key’ (linera.PrivateKey);
  • inside a ‘Linera signer’ module there's no need for any of this information (everything here is a Linera signer, or helpers for one) and the only thing you care about is that it is a private key signer (PrivateKey);
  • bit of a contrived example for this one in particular (because ‘private key’ is jargon usually specific to crypto), but inside a very high-level context about generic authentication mechanisms you might have a mechanism that's about actual physical signatures made with a pen on paper! So you might want to disambiguate that by referring to this as the Linera crypto signer (signer.crypto.linera.PrivateKey).

When you try to include all possible context into the base name, you're cursing your users to always include all of that context, even when it's not relevant, and to not be able to decide (or at least be strongly discouraged from deciding) what information is relevant to them. Conversely, you're taking on the burden of trying to guess all the possible contexts that the name could appear in and creating a name that's suitably unambiguous in every single one. Using the language's module facilities, on the other hand, means consumers (including you, the producer!) can adapt the name in different contexts to omit the information that's obvious and keep or add information that's pertinent.

The fact that the word ‘signer’ is pertinent in this context doesn't mean it's pertinent in all contexts such that it should be made part of the core name. In fact, a great example of a context in which it's not pertinent at all (because it's super obvious) is the module where it's defined, linera_base::crypto::signer.


const FUNGIBLE_APP_ID = '465e465b050db5034fd8a51df46b9a7cf02a8a6414cc2b94b102e912208d4a4b';
// This needs to point at actual deployed fungible application ID.
const FUNGIBLE_APP_ID = import.meta.env.VITE_FUNGIBLE_APP_ID;

const gql = (query, variables = {}) => JSON.stringify({ query, variables });

Expand Down Expand Up @@ -216,11 +219,14 @@ <h2>Chain history for <code id="chain-id" class="hex">requesting a new microchai
});

await linera.default();
const faucet = await new linera.Faucet('https://faucet.testnet-babbage.linera.net');
const faucet = await new linera.Faucet(import.meta.env.VITE_FAUCET_URL);
const signer = new PrivateKey("f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66");
const wallet = await faucet.createWallet();
const client = await new linera.Client(wallet);
const chainId = await faucet.claimChain(client);
const owner = signer.address();
const chainId = await faucet.claimChain(wallet, owner);
const client = await new linera.Client(wallet, signer);
document.querySelector('#chain-id').innerText = chainId;
document.querySelector('#owner').innerText = owner;

const application = await client.frontend().application(FUNGIBLE_APP_ID);

Expand Down
3 changes: 2 additions & 1 deletion examples/hosted-fungible/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"ci": "pnpm build"
},
"dependencies": {
"@linera/client": "workspace:^"
"@linera/client": "workspace:*",
"@linera/signer": "workspace:*"
},
"devDependencies": {
"vite": "^5.4.11"
Expand Down
4 changes: 4 additions & 0 deletions examples/hosted-fungible/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,9 @@ export default defineConfig({
exclude: [
'@linera/client',
],
include: ['@adraffy/ens-normalize'],
},
ssr: {
noExternal: ['@adraffy/ens-normalize'],
}
})
10 changes: 2 additions & 8 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@
"lit": "^3.2.0",
"randomstring": "^1.3.0",
"webext-bridge": "^6.0.1",
"@linera/client": "workspace:^"
},
"devDependencies": {
"@types/chrome": "^0.0.267",
"@types/randomstring": "^1.3.0",
"ts-auto-guard": "^5.0.1",
"typescript": "^5.6.2",
"vite": "^5.4.5"
"@linera/client": "workspace:*",
"@linera/signer": "workspace:*"
}
}
3 changes: 2 additions & 1 deletion extension/src/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as linera from '@linera/client';
import type { Client } from '@linera/client';
import { PrivateKey } from '@linera/signer';

import * as guard from './message.guard';

Expand All @@ -11,7 +12,7 @@ export class Server {
async setWallet(wallet: string) {
this.wallet = wallet;
await linera;
this.client = await new linera.Client({} as linera.Wallet); // Replace with actual wallet initialization
this.client = await new linera.Client({} as linera.Wallet, new PrivateKey("f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66") ); // Replace with actual wallet initialization
this.client.onNotification((notification: any) => {
console.debug('got notification for', this.subscribers.size, 'subscribers:', notification);
for (const subscriber of this.subscribers.values()) {
Expand Down
16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,23 @@
"keywords": [],
"author": "Linera <[email protected]>",
"license": "Apache-2.0",
"dependencies": {
"@linera/client": "workspace:*",
"@linera/signer": "workspace:*"
},
"devDependencies": {
"@types/chrome": "^0.0.267",
"@types/jest": "^29.5.14",
"@types/node": "^24.0.0",
"@types/randomstring": "^1.3.0",
"ts-auto-guard": "^5.0.1",
"typescript": "^5.8.3",
"vite": "^5.4.5"
},
"pnpm": {
"onlyBuiltDependencies": [
"@linera/client"
"@linera/client",
"@linera/signer"
]
}
}
Loading