This commit is contained in:
waalge 2025-02-16 11:30:41 +00:00
parent 712c7cac44
commit 194492234e
31 changed files with 2977 additions and 550 deletions

View File

@ -1,16 +1,6 @@
## Commands
# Kompact.io site
recompile css
```sh
tailwindcss -i ./content/css/main.css -o ./content/css/mini.css --minify
```
## Commands
build, serve and watch
```sh
cabal run site -- watch
```
deploy
```sh
rsync -r --delete ./_site/* genesis:/var/www/kompactio-landing/
```
Enter devshell, and run `menu`
See flake for details.

View File

@ -5,7 +5,8 @@
@font-face {
/* Set in tailwindconfig */
font-family: "jetbrains-mono";
src: local("jetbrains-mono"),
src:
local("jetbrains-mono"),
url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
}
@ -13,59 +14,61 @@ article {
margin-bottom: 2rem;
}
article>section> :is(pre, p, h1, h2, h3, h4, h5, h6) {
article > section > :is(pre, p, h1, h2, h3, h4, h5, h6) {
margin-top: 2rem;
}
article>section {
font-family: "Lucida" Grande, sans-serif;
article > section {
font-family:
"Lucida" Grande,
sans-serif;
}
article>section> :is(h1, h2, h3, h4, h5, h6, code) {
article > section > :is(h1, h2, h3, h4, h5, h6, code) {
font-family: "jetbrains-mono";
}
article>section>blockquote {
article > section > blockquote {
padding: 1rem;
border-left-width: 4px;
border-color: rgb(239 68 68);
font-style: italic;
}
article>section>h1 {
article > section > h1 {
margin-top: 2rem;
font-size: 3rem;
}
article>section>h1::before {
article > section > h1::before {
content: "# ";
}
article>section>h2 {
article > section > h2 {
font-size: 2rem;
}
article>section>h2::before {
article > section > h2::before {
content: "## ";
}
article>section>h3 {
article > section > h3 {
font-size: 1.5rem;
}
article>section>h3::before {
article > section > h3::before {
content: "### ";
}
article>section>h4 {
article > section > h4 {
font-size: 1.3rem;
}
article>section>h4::before {
article > section > h4::before {
content: "#### ";
}
article>section {
article > section {
margin-top: 4rem;
}
@ -90,4 +93,16 @@ article ul {
article ol {
margin-left: 1rem;
list-style: decimal inside;
}
}
#footnotes {
padding-top: 1rem;
}
#footnotes > ol > li {
margin-top: 1rem;
}
#footnotes > ol > li > p {
display: inline;
}

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,118 @@
/* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+haskell+json+nix+racket+rust+scheme */
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
code[class*="language-"],
pre[class*="language-"] {
color: #000;
background: 0 0;
text-shadow: 0 1px #fff;
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
pre[class*="language-"]::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
code[class*="language-"] ::selection,
code[class*="language-"]::selection,
pre[class*="language-"] ::selection,
pre[class*="language-"]::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog {
color: #708090;
}
.token.punctuation {
color: #999;
}
.token.namespace {
opacity: 0.7;
}
.token.boolean,
.token.constant,
.token.deleted,
.token.number,
.token.property,
.token.symbol,
.token.tag {
color: #905;
}
.token.attr-name,
.token.builtin,
.token.char,
.token.inserted,
.token.selector,
.token.string {
color: #690;
}
.language-css .token.string,
.style .token.string,
.token.entity,
.token.operator,
.token.url {
color: #9a6e3a;
background: hsla(0, 0%, 100%, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.class-name,
.token.function {
color: #dd4a68;
}
.token.important,
.token.regex,
.token.variable {
color: #e90;
}
.token.bold,
.token.important {
font-weight: 700;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -0,0 +1,59 @@
---
title: why is building txs hard?
---
## What is a dapp?
A typical dapp has a number of components:
- Validators: also called the _on-chain_ part.
The decentralized network of nodes that maintain the chain run this code as part of the process of deciding
whether a tx is to be added to the chain or rejected.
- Chain indexing: watches the chain and records the data relevant to the dapp.
- Pretty front-end: typically how a user interacts with a dapp.
- Tx-building code: A component of the frontend.
It takes data from the user, the user's wallet, the chain-indexer and possibly elsewhere
to construct txs to be submitted to the chain that the validators will deem acceptable.
Here we have really described a browser-based plutus dapp.
Considering the term _dapp_ more generally, the Daedalus wallet is a dapp which is neither browser based nor involves any Plutus.
Cli-based dapps also exist, such as multi-sig using native scripts.
## What is a tx?
At its core, the chain is a list of transaction outputs.
The chain is changed by submitting a tx which "spends" existing unspent transaction outputs (utxos) and appending new ones.
(There's other possible modifications too, like minting native assets and staking _etc_, but the key part is spending.)
The on-chain part is where the dapp has its integrity, but users can only interact with the on-chain part of the dapp by submitting txs.
The on-chain part is relatively simple.
It inspects each tx that it is concerned with, and if it does not like what it sees, it fails.
Cardano is, in this sense, a lean chain.
The off-chain part is relatively complex.
There may be no, one, or many potential valid txs that would satisfy the user's intent.
At its core, a transaction is a list of inputs and outputs.
The inputs are spent, and the outputs created.
It must also contain the necessary signatures.
## Some history
When Plutus was first dreamed up, it wasn't just a language.
It was whole environment in which dapps would be engaged with.
The on-chain and off-chain code were coupled into a single framework with seamless extensive testing.
Dapps ran in the _PAB_, Plutus Application Backend.
Everything was great.
Except that it didn't work.
The chain-indexer would periodically fall-over,
it required users had to maintain a full node,
and the api never matured in to something stable, complete, and bug-free.
In addition, the validators it produced were bloated and un-optimized and would quickly hit the constraints of the cardano blockchain.
As a result teams turned to coming up with alternatives.
One of the first was MLabs and co in creating pluto and plutarch.
Later Aiken appeared to meet similar needs.
These began resolving issues with the on-chain part.
This left the off-chain part wanting.

View File

@ -1,3 +1,5 @@
---
title: Kompact.io
---
---
## Hero

View File

@ -3,74 +3,73 @@ title: Are we zk-Cardano yet?
date: 2023-08-07
---
Not so long ago Emurgo announced they were doing a Cardano centered hackathon.
Not so long ago Emurgo announced they were doing a Cardano centered hackathon.
It was a welcome prospect - very few similar such events seem to exist in the space.
Things went monotonically south ever since the announcement, but that's a different story.
Things went monotonically south ever since the announcement, but that's a different story.
One particularly interesting quirk was that of the three "tracks" of the hackathon,
one was _Zero Knowledge_ (aka zk).
One particularly interesting quirk was that of the three "tracks" of the hackathon,
one was _Zero Knowledge_ (aka zk).
Why particularly interesting quirk? In some sense it is not surprising:
zk has been very trendy these last few years around blockchains.
However, building on Cardano is notoriously challenging.
Building with zk on a zk-native blockchain is itself a very steep learning curve.
So combining the two, zk on Cardano seemed... a bit mad.
So combining the two, zk on Cardano seemed... a bit mad.
This post is borne out of a best effort of how far "zk on Cardano" can be pushed.
This post is borne out of a best effort of how far "zk on Cardano" can be pushed.
## What is zk?
There is no shortage of explanations describing what zk is
( _eg_ [by Vitalik](https://vitalik.ca/general/2021/01/26/snarks.html){target="_blank"} or
[a full mooc](https://zk-learning.org/){target="_blank"} ).
There is no shortage of explanations describing what zk is
( _eg_ [by Vitalik](https://vitalik.ca/general/2021/01/26/snarks.html){target="\_blank"} or
[a full mooc](https://zk-learning.org/){target="\_blank"} ).
There is also a reasonable breath to the field of zk that includes things like distributed compute.
Zk involves some really neat maths that lets you do some seemingly magical feats
and pairs well with blockchain in extending what is functionally possible.
Let's stick to a simple and prototypical example.
Let's stick to a simple and prototypical example.
Suppose Alice and Bob are playing battleships.
The game begins with Alice and Bob placing their ships within their own coordinate grid.
They then take turns picking coordinates to "strike".
If they hit nothing then their turn ends, but if they hit a ship then they strike again.
The winner is the first to strike all coordinates containing their opponent's ships.
They then take turns picking coordinates to "strike".
If they hit nothing then their turn ends, but if they hit a ship then they strike again.
The winner is the first to strike all coordinates containing their opponent's ships.
Alice knows Bob as being a notorious liar; how can she enjoy the game?
Each guess she makes, Bob gleefully shouts "Miss!".
She can't ask Bob to show he's not lying by revealing the actual locations of the ships.
She could ask Charlie to independently verify Bob's not lying,
but then what if Charlie is actually on team Bob and also lies.
She could ask Charlie to independently verify Bob's not lying,
but then what if Charlie is actually on team Bob and also lies.
Or Bob might suspect Charlie is actually on team Alice, slyly brought in to give Alice some hints.
Is there a way that Bob can prove to Alice that each guess is a miss,
Is there a way that Bob can prove to Alice that each guess is a miss,
but without revealing the locations of the ships either to Alice or anyone else?
The answer is yes.
Using zk Bob can produce a proof each time Alice's guess misses if and only if it honestly does.
The answer is yes.
Using zk Bob can produce a proof each time Alice's guess misses if and only if it honestly does.
Alice can inspect each proof and verify Bob's response.
Alice can interrogate the proof as much as she wants, but she won't learn anything more than
her guess was a miss.
her guess was a miss.
There are a multitude of different ways to do this,
but essentially it involves modeling the problem as a bunch of algebra
over finite fields - like a lot of cryptography.
over finite fields - like a lot of cryptography.
What's the _snark_ of zk-snark?
Snark stands for _Succinct Non-Interactive Argument of Knowledge_.
And without saying anything more, it means that Alice has to do way less algebra than Bob.
Snark stands for _Succinct Non-Interactive Argument of Knowledge_.
And without saying anything more, it means that Alice has to do way less algebra than Bob.
In applications this is important because Bob might not be able to lie anymore but he could still waste Alice's time.
## Sudoku snark
Sudoku snark was the entrant to Emurgo's hackathon.
The summary-pitch-story deck is [here](https://pub.kompact.io/sudoku-snark){target="_blank"}.
Links to the associated repos: [plutus-zk](https://github.com/waalge/plutus-zk){target="_blank"}
and [sudoku-snark](https://github.com/waalge/sudoku-snark){target="_blank"}.
Sudoku snark was the entrant to Emurgo's hackathon.
The summary-pitch-story deck is [here](https://pub.kompact.io/sudoku-snark){target="\_blank"}.
Links to the associated repos: [plutus-zk](https://github.com/waalge/plutus-zk){target="\_blank"}
and [sudoku-snark](https://github.com/waalge/sudoku-snark){target="\_blank"}.
Just after the hackathon got underway there was a
[large PR merged](https://github.com/input-output-hk/plutus/pull/5231){target="_blank"}
Just after the hackathon got underway there was a
[large PR merged](https://github.com/input-output-hk/plutus/pull/5231){target="\_blank"}
into the main branch of plutus.
It's a mammoth culmination of many many months of work.
In it were some fundamental primitives needed for running zk algorithms.
It's a mammoth culmination of many many months of work.
In it were some fundamental primitives needed for running zk algorithms.
The idea of the project was as follows:
@ -79,35 +78,35 @@ The idea of the project was as follows:
- try to get a version of hydra running this newest version of plutus
- wrap up in a gui
Unsurprisingly to anyone who's hung around the Cardano ecosystem long enough,
this third part is where things got stuck.
Unsurprisingly to anyone who's hung around the Cardano ecosystem long enough,
this third part is where things got stuck.
We did get as far as running a cluster of nodes in the Conway era with the latest version of plutus
but unrelated changes seemed to thwart any chance of building transactions here.
but unrelated changes seemed to thwart any chance of building transactions here.
A quick shout-out to the [modulo-p.io](https://modulo-p.io/){target="_blank"} team.
They had a different approach and managed to implement a zk algorithm with the existing plutus primitives.
A quick shout-out to the [modulo-p.io](https://modulo-p.io/){target="\_blank"} team.
They had a different approach and managed to implement a zk algorithm with the existing plutus primitives.
This spared the need to play the foolhardy dependency bumping game with the Cardano node.
However, because zk is so arithmetically intense,
However, because zk is so arithmetically intense,
the app wont run outside a hydra head and with very generous max unit budgets (afaics).
This approach won't be necessary when we have the new version of plutus available.
Nonetheless, it's very neat to see it done and they packaged it very nicely.
The validator in Sudoku snark uses [groth16](https://eprint.iacr.org/2016/260.pdf).
In part because this was already mostly available from the plutus repo itself.
In part because this was already mostly available from the plutus repo itself.
It is also the most obvious candidate to begin with.
It's relatively mature, relatively simple, can be implemented from the new primitives,
and importantly in Cardano land has small proof size.
It's relatively mature, relatively simple, can be implemented from the new primitives,
and importantly in Cardano land has small proof size.
(As far as I know, the smallest of comparable algorithms.)
The program to generate the setup and proofs uses the Arkworks framework.
Again this choice was initially inspired by a script from the IOG team,
but again it seems like a smart choice.
Arkworks is a well conceived, highly modular framework for zk,
which makes it easy to pull in the bits we need to perform our off-chain logic.
but again it seems like a smart choice.
Arkworks is a well conceived, highly modular framework for zk,
which makes it easy to pull in the bits we need to perform our off-chain logic.
The choice of game, sudoku, was in turn inspired by an arkworks example.
It's not the most compelling of choices, but it's simple and it did for now.
Battleships would have been more compelling or mastermind as the modulo-p team used.
Battleships would have been more compelling or mastermind as the modulo-p team used.
The intended game play involved locking Ada at a utxo correspondinig to a sudoku puzzle,
and spendable only if a player could provide proof they knew the solution.
@ -118,10 +117,10 @@ Other details were TBC: is it first and second prizes? are players whitelisted?
We're close.
There is potentially still quite a while before these new primitives in plutus reach mainnet.
The word on the street is that it might happen before the end of 2023.
There is potentially still quite a while before these new primitives in plutus reach mainnet.
The word on the street is that it might happen before the end of 2023.
Even sooner, there will be versions of the Cardano node available with the new primitives,
Even sooner, there will be versions of the Cardano node available with the new primitives,
and so possibly plumb-able into hydra without causing oneself an aneurysm.
In development time that's not so long: we can start thinking about what to build with zk on Cardano.

View File

@ -1,104 +1,104 @@
---
title: "Hydra is neat: You don't need Hydra"
title: "Hydra is cool: You don't need Hydra"
date: 2023-09-20
---
---
## Hydra is neat
## Hydra is cool
Hydra[^1] is a very cool project. It is a layer 2 for Cardano that is _isomorphic_ to the L1.
Hydra[^1] is a very cool project. It is a layer 2 for Cardano that is _isomorphic_ to the L1.
Here isomorphic means that Plutus runs in Hydra just like it does on the L1.
That dapp you've just toiled over for months to run on the L1 can be put in Hydra and 'just work'.
[^1]:
This post does not distinguish between Hydra and Hydra Head referring to both as Hydra.
If you want to know more about Hydra, then check out their
[explainers](https://hydra.family/head-protocol/core-concepts).
## Hydra's compromise
Hydra boasts it can achieve higher throughput and lower transaction fees compared to the Cardano L1
as well as near instant settling and no roll-backs.
You may be asking _If my dapp just works on Hydra and it's better in all key respects,
as well as near instant settling and no roll-backs.
You may be asking _If my dapp just works on Hydra and it's better in all key respects,
then why don't we all just use Hydra?_.
The answer is because these improvements come at a cost.
Consensus in Hydra differs from that on the L1.
Hydra doesn't use ouroboros; all participating hydra nodes
Hydra doesn't use ouroboros. Instead all participating hydra nodes
must sign-off on all updates to the chain state.
Practically speaking, far fewer nodes can participate in Hydra
and one quiet node stops the whole Hydra chain updating.
Not great for an L1.
Practically speaking, far fewer nodes can participate in Hydra
and one quiet node stops the whole Hydra chain updating.
Not great for an L1.
## You don't need Hydra
Hydra is an example of a way to do state channels.
A state channel relies on the integrity of the L1, while accumulating state separately from it (L2).
At some point the the layers are brought into sync.
Hydra is an example of a way to do state channels.
A state channel relies on the integrity of the L1, while accumulating state separately from it (L2).
At some point the layers are brought into sync.
This is when funds on the L1 can be unlocked, and/or the state of the L2 updated.
Hydra could be thought to be providing some future-proofing.
Hydra could be thought to be providing some future-proofing.
It is possible for a Hydra instance to run indefinitely
and scripts yet written will be executable in some already running instance.
However, because Hydra's consensus is so brittle the longevity of an instance is not something to depend on.
and Plutus scripts not yet written will be executable in some already running instance.
However, because Hydra's consensus is so brittle the longevity of an instance is not something to depend on.
Each and any transaction may be its last.
A key question is when considering Hydra is _Do I need isomorphic-ness?_.
A key question when considering Hydra is _Do I need isomorphic-ness?_.
If you know all your business logic before instantiation
then the answer is **no, you don't care for isomorphic-ness**.
Instead, you can roll-your-own L2. It depends on your use case as to how much work that ends up being.
then the answer is **no, you don't care for isomorphic-ness**.
Instead, you can roll-your-own L2.
It depends on your use case as to how much work that ends up being.
It can be very simple.
## You don't want Hydra
In Hydra, the latest agreed state in the L2 is the one that the L1 will accept as the most legitimate.
This is a sensible default.
In Hydra, the latest agreed state in the L2 is the one that the L1 will accept as the most legitimate.
This is a sensible default.
Suppose however you have a game of poker where one player learns that they've lost and rage quits.
From the game's perspective, that final transaction should be forced through - the player's loss is inevitable.
From the game's perspective, that final transaction should be forced through - the player's loss is inevitable.
At present this isn't possible with Hydra.
If a party doesn't sign then a state isn't valid.
If a party doesn't sign, then a state isn't valid.
In another use case, suppose there is some particularly intense on-chain verification
that would be prohibitive on the L1 but that you'd like the results of which to
persist onto the L1 and/or be recovered in future L2 instances.
This could be done with validity tokens but anything minted in the L2 won't persist onto the L1.
that would be prohibitive on the L1 but that you'd like the results of which to
persist onto the L1 and/or be recovered in future L2 instances.
This could be done with validity tokens but anything minted in the L2 won't persist onto the L1.
Another key question then is _What is the right way to sync the L1 and L2 states?_.
Hydra has a way of it doing it which might or might not be appropriate for your use case.
Rolling your own L2 means that the sync logic can fit your business needs.
Another key question then is _What is the right way to sync the L1 and L2 states?_.
Hydra has a way of it doing it which might or might not be appropriate for your use case.
Rolling your own L2 means that the sync logic can fit your business needs.
Both the cases above are resolvable with custom sync logic.
## An Example: Subbit.xyz
Probably the simplest, non-trivial example using state channels is [Subbit.xyz][https://subbit.xyz].
Subbit.xyz is premised on the observation that subscription is very common use case:
Probably the simplest, non-trivial example using state channels is [Subbit.xyz](https://subbit.xyz).
Subbit.xyz is premised on the observation that subscription is a very common use case:
there are two parties where one pays the other incrementally.
It sacrifices generality to gain absolutely minimal overhead for both parties.
It sacrifices generality to gain absolutely minimal overhead for both parties.
In Subbit.xyz, Alice, a consumer, subscribes to some service of Bob, a provider.
Alice instantiates the channel by locking funds, similar to Hydra.
There are only two mechanisms for unlocking - one for Alice and the other for Bob.
All logic is known at instantiation.
All logic is known at instantiation.
A consumer needs only to keep track of their account balance,
ascertain the cost of each outgoing request,
and produce valid signatures for a few dozen bytes of data at a time.
They don't need to watch the L1 and its a non-chatty protocol.
The low resource needs opens it up to applications
on intermittently connected user devices such as laptops and mobile,
A consumer needs only to keep track of their account balance,
ascertain the cost of each outgoing request,
and produce valid signatures for a few dozen bytes of data at a time.
They don't need to watch the L1 and it's a non-chatty protocol.
The low resource needs opens it up to applications
on intermittently connected user devices such as laptops and mobile,
and even micro-controllers.
High throughput remains achievable.
A provider must track each subscriber's account, and periodically check the state of the L1.
This could conceivably be as little as once a week or once a month.
A provider must track each subscriber's account, and periodically check the state of the L1.
This could conceivably be as little as once a week or once a month.
The low resource needs for a provider means they have the ability to serve more with less.
## Hydra for QoL
When Hydra reaches a point of maturity that its plug and play,
When Hydra reaches a point of maturity that it's plug and play,
it's potentially far easier to deploy with Hydra then roll-your-own L2.
Isomorphic-ness gives Hydra incredible flexibility and generality.
You don't need isomorphic-ness but because of it, Hydra could be an easy and convenient solution.
As for custom sync logic, it is surely the case that there is a tranche on interesting applications where
its far easier and more effective to reuse Hydra infra and modify it than creating your own L2 from scratch.
[^1]: This post does not distinguish between Hydra and Hydra Head referring to both as Hydra.
If you want to know more about Hydra, then check out their
[explainers](https://hydra.family/head-protocol/core-concepts).
You don't need isomorphic-ness but because of it, Hydra could be an easy and convenient solution.
As for custom sync logic, it is surely the case that there is a tranche of interesting applications where
it's far easier and more effective to reuse Hydra infra and modify it than creating your own L2 from scratch.

View File

@ -3,160 +3,160 @@ title: Tracing Aiken Build
date: 2023-09-02
---
Aims:
Aims:
> Describe the pipeline and components getting from Aiken to Uplc.
> Describe the pipeline and components getting from Aiken to Uplc.
## The Preface
### Motivations
The motivation for writing this came from a desire to add additional features to Aiken not yet available.
One such feature would evaluate an arbitrary function in Aiken callable from JavaScript.
This would help a lot with testing and when trying to align on and off-chain code.
One such feature would evaluate an arbitrary function in Aiken callable from JavaScript.
This would help a lot with testing and when trying to align on and off-chain code.
Another more pipe dreamy, ad-hoc function extraction - from a span of code, generate a function.
A digression to answer _why would this be at all helpful?!_
Validator logic often needs a broad context throughout.
How then to best factor code?
Possible solutions:
Possible solutions:
1. Introduce types / structs
1. Introduce types / structs
2. Have functions with lots of arguments
3. Don't
The problems are:
1. Requires relentless constructing and deconstructing across the function call.
This adds costs.
2. Becomes tedious aligning the definition and function call.
3. Ends up with very long validators which are hard to unit test.
This adds costs.
2. Becomes tedious aligning the definition and function call.
3. Ends up with very long validators which are hard to unit test.
My current preferred way is to accept that validator functions are long.
Ad-hoc function extraction would allow for sections of code to be tested without needing to be factored out.
To do either of these, we need to get to grips with the Aiken compilation pipeline.
### This won't age well
### This won't age well
Aiken is undergoing active development.
This post started life with Aiken ~v1.14.
Aiken v1.15 introduced reasonably significant changes to the compilation pipeline.
The word is that there aren't any more big changes in the near future,
but this article will undoubtedly begin to diverge from the current code-base even before publishing.
Aiken is undergoing active development.
This post started life with Aiken ~v1.14.
Aiken v1.15 introduced reasonably significant changes to the compilation pipeline.
The word is that there aren't any more big changes in the near future,
but this article will undoubtedly begin to diverge from the current code-base even before publishing.
### Limitations of narrating code
Narrating code becomes a compromise between being honest and accurate, and being readable and digestible.
Narrating code becomes a compromise between being honest and accurate, and being readable and digestible.
The command `aiken build` covers well in excess of 10,000 LoC.
The writing of this post ground to a halt as it reached deeper into the code-base.
To redeem it, some (possibly large) sections remain black boxes.
## Aiken build
Tracing `aiken build`, the pipeline is roughly:
Tracing `aiken build`, the pipeline is roughly:
```sample
. -> Project::read_source_files ->
. -> Project::read_source_files ->
Vec<Source> -> Project::parse_sources ->
ParsedModules -> Project::type_check ->
CheckedModules -> CodeGenerator::build ->
AirTree -> AirTree::to_vec ->
Vec<Air> -> CodeGenerator::uplc_code_gen ->
Program / Term<Name> -> serialize ->
CheckedModules -> CodeGenerator::build ->
AirTree -> AirTree::to_vec ->
Vec<Air> -> CodeGenerator::uplc_code_gen ->
Program / Term<Name> -> serialize ->
.
```
We'll pick our way through these steps
At a high level we are trying to do something straightforward: reformulate Aiken code as Uplc.
Some Aiken expressions are relatively easy to handle for example an Aiken `Int` goes to an `Int` in Uplc.
Some Aiken expressions require more involved handling, for example an Aiken `If... If Else... Else `
Some Aiken expressions are relatively easy to handle for example an Aiken `Int` goes to an `Int` in Uplc.
Some Aiken expressions require more involved handling, for example an Aiken `If... If Else... Else `
must have the branches "nested" in Uplc.
Aiken has lots of nice-to-haves like pattern matching, modules, and generics;
Uplc has none of these.
### The Preamble
### The Preamble
#### Cli handling
The cli enters at `aiken/src/cmd/mod.rs` which parses the command.
The cli enters at `aiken/src/cmd/mod.rs` which parses the command.
With some establishing of context, the program enters `Project::build` (`crates/aiken-project/src/lib.rs`),
which in turn calls `Project::compile`.
which in turn calls `Project::compile`.
#### File crawl
The program looks for Aiken files in both `./lib` and `./validator` sub-directories.
For each it walks over all contents (recursively) looking for `.ak` extensions.
It treats these two sets of files a little differently.
The program looks for Aiken files in both `./lib` and `./validator` sub-directories.
For each it walks over all contents (recursively) looking for `.ak` extensions.
It treats these two sets of files a little differently.
For example, only validator files can contain the special validator functions.
#### Parse and Type check
`Project::parse_sources` parses the module source code.
The heavy lifting is done by `aiken_lang::parser::module`, which is evaluated on each file.
The heavy lifting is done by `aiken_lang::parser::module`, which is evaluated on each file.
It produces a `Module` containing a list of parsed definitions of the file: functions, types _etc_,
together with metadata like docstrings and the file path.
`Project::type_check` inspects the parsed modules and, as the name implies, checks the types.
together with metadata like docstrings and the file path.
`Project::type_check` inspects the parsed modules and, as the name implies, checks the types.
It flags type level warnings and errors and constructs a hash map of `CheckedModule`s.
#### Code generator
The code generator `CodeGenerator` (`aiken-lang/src/gen_uplc.rs`) is given
the definitions found from the previous step,
together with the plutus builtins.
It has additional fields for things like debugging.
The code generator `CodeGenerator` (`aiken-lang/src/gen_uplc.rs`) is given
the definitions found from the previous step,
together with the plutus builtins.
It has additional fields for things like debugging.
This is handed over to a `Blueprint` (`aiken-project/src/blueprint/mod.rs`).
The blueprint does little more than find the validators on which to run the code gen.
The blueprint does little more than find the validators on which to run the code gen.
The heavy lifting is done by `CodeGenerator::generate`.
We are now ready to take the source code and create plutus.
We are now ready to take the source code and create plutus.
### In the air
Things become a bit intimidating at this point in terms of sheer lines of code:
`gen_uplc.rs` and three modules in `gen_uplc/` totals > 8500 LoC.
`gen_uplc.rs` and three modules in `gen_uplc/` totals > 8500 LoC.
Aiken has its own _intermediate representation_ called `air` (as in Aiken Intermediate Representation).
Aiken has its own _intermediate representation_ called `air` (as in Aiken Intermediate Representation).
Intermediate representations are common in compiled languages.
`Air` is defined in `aiken-lang/src/gen_uplc/air.rs`.
Unsurprisingly, it looks a little bit like a language between Aiken and plutus.
`Air` is defined in `aiken-lang/src/gen_uplc/air.rs`.
Unsurprisingly, it looks a little bit like a language between Aiken and plutus.
In fact, Aiken has another intermediate representation: `AirTree`.
This is constructed between the `TypedExpr` and `Vec<Air>` ie between parsed Aiken and air.
In fact, Aiken has another intermediate representation: `AirTree`.
This is constructed between the `TypedExpr` and `Vec<Air>` ie between parsed Aiken and air.
#### Climbing the AirTree
#### Climbing the AirTree
Within `CodeGenerator::generate`, `CodeGenerator::build` is called on the function body.
Within `CodeGenerator::generate`, `CodeGenerator::build` is called on the function body.
This takes a `TypedExpr` and constructs and returns an `AirTree`.
The construction is recursive as it traverses the recursive `TypedExpr` data structure.
More on what an airtree is and its construction below.
At the same time `self` is treated as `mut`, so we need to keep an eye on this too.
The method which is called and uses this mutability of self is `self.assignment`.
The method which is called and uses this mutability of self is `self.assignment`.
It does so by
```sample
- self.assignment
└ self.expect_type_assign
```sample
- self.assignment
└ self.expect_type_assign
└ self.code_gen_functions.insert
```
and thus is creating a hashmap of all the functions that appear in the definition.
From the call to return of `assign` covers > 600 LoC so we'll leave this as a black box.
(`self.handle_each_clause` is also called with `mut` which in turn calls `self.build` for which `mut` it is needed.)
(`self.handle_each_clause` is also called with `mut` which in turn calls `self.build` for which `mut` it is needed.)
Validators in Aiken are boolean functions while in Uplc they are unit-valued (aka void-valued) functions.
Thus the air tree is wrapped such that `false` results in an error (`wrap_validator_condition`).
I don't know why there is a prevailing thought that boolean functions are preferable to functions
Thus the air tree is wrapped such that `false` results in an error (`wrap_validator_condition`).
I don't know why there is a prevailing thought that boolean functions are preferable to functions
that error if anything is wrong - which is what validators are.
`check_validator_args` again extends the airtree from the previous step,
`check_validator_args` again extends the airtree from the previous step,
and again calls `self.assignment` mutating self.
Something interesting is happening here.
Something interesting is happening here.
Script context is the final argument of a validator - for any script purpose.
`check_validator_args` treats the script context like it is an unused argument.
`check_validator_args` treats the script context like it is an unused argument.
The importance of this is not immediate, and I've still yet to appreciate why this happens.
Let's take a look at what AirTree actually is
@ -172,8 +172,8 @@ pub enum AirTree {
}
```
Note that `AirStatement` and `AirExpression` are mutually recursive definitions with `AirTree`.
Otherwise, it would be unclear from first inspection how tree-like this really is.
Note that `AirStatement` and `AirExpression` are mutually recursive definitions with `AirTree`.
Otherwise, it would be unclear from first inspection how tree-like this really is.
`AirExpression` has multiple constructors. These include (non-exhaustive)
@ -183,55 +183,55 @@ Otherwise, it would be unclear from first inspection how tree-like this really i
- handling when and if
- handling error and tracing
`AirStatement` also has multiple constructors. These include
`AirStatement` also has multiple constructors. These include
- let assignments and named function definitions
- handling expect assignments
- pattern matching
- handling expect assignments
- pattern matching
- unwrapping data structures
Note that `AirTree` has many methods that are partial functions,
as in there are possible states that are not considered legitimate
Note that `AirTree` has many methods that are partial functions,
as in there are possible states that are not considered legitimate
at different points of its construction and use.
For example `hoist_over` will throw an error if called on an `Expression`.
As `AirTree` is for internal use only, the scope for potential problems is reasonably contained.
It seems likely this is to avoid similar-yet-different IRs between steps.
However, the trade off is that it partially obfuscates what is a valid state where.
However, the trade off is that it partially obfuscates what is a valid state where.
What is hoisting? Hoisting gives the airtree depth.
What is hoisting? Hoisting gives the airtree depth.
The motivation is that by the time we hit Uplc it is "generally better"
that
that
- function definitions appear once rather than being inlined multiple times
- the definition appears as close to use as possible
- the definition appears as close to use as possible
Hoisting creates tree paths.
Hoisting creates tree paths.
The final airtree to airtree step, `self.hoist_functions_to_validator`, traverses these paths.
There is a lot of mutating of self, making it quite hard to keep a handle on things.
There is a lot of mutating of self, making it quite hard to keep a handle on things.
In all this (several thousand?) LoC, it is essentially ascertaining in which node of the tree
to insert each function definition.
to insert each function definition.
In a resource constrained environment like plutus, this effort is warranted.
At the same time this function deals with
At the same time this function deals with
- monomophisation - no more generics
- erasing opaque types
Neither of which exist at the Uplc level.
Neither of which exist at the Uplc level.
#### Into Air
The `to_vec : AirTree -> Vec<Air>` is much easier to digest.
The `to_vec : AirTree -> Vec<Air>` is much easier to digest.
For one, it is not evaluated in the context of the code generator,
and two, there is no mutation of the airtree.
and two, there is no mutation of the airtree.
The function recursively takes nodes of the tree and maps them to entries in a mutable vector.
It flattens the tree to a vec.
### Down to Uplc
### Down to Uplc
Next we go from `Vec<Air> -> Term<Name>`.
This step is a little more involved than the previous.
For one, this is executed in the context of the code generator.
This step is a little more involved than the previous.
For one, this is executed in the context of the code generator.
Moreover, the code generator is treated as mutable - ouch.
On further inspection we see that the only mutation is setting `self.needs_field_access = true`.
@ -242,45 +242,45 @@ As noted above, some of the mappings from air to terms are immediate like `Air::
Others are less so.
Some examples:
- `Air::Var` require 100 LoC to do case handling on different constructors.
- `Air::Var` require 100 LoC to do case handling on different constructors.
- Lists in air have no immediate analogue in uplc
- builtins, as in built-in functions (standard shorthand), have to be mediated
with some combination of `force` and `delay` in order to behave as they should.
- user functions must be "uncurried", ie treated as a sequence of single argument functions,
and recursion must be handled
- builtins, as in built-in functions (standard shorthand), have to be mediated
with some combination of `force` and `delay` in order to behave as they should.
- user functions must be "uncurried", ie treated as a sequence of single argument functions,
and recursion must be handled
- Do some magic in order to efficiently allow "record updates".
#### Cranking the Optimizer
There is a sequence of operations performed on the Uplc, mapping `Term<Name> -> Term<Name>`.
This removes inconsequential parts of the logic which have been generated, including:
This removes inconsequential parts of the logic which have been generated, including:
- removing application of the identity function
- directly substituting where apply lambda is applied to a constant or builtin
- inline or simplify where apply lambda is applied to a parameter that appears once or not at all
Each of these optimizing methods has a its own relatively narrow focus,
Each of these optimizing methods has a its own relatively narrow focus,
and so although there is a fair number of LoC, it's reasonably straightforward to follow.
Some are applied multiple times.
Some are applied multiple times.
### The End
### The End
The generated program can now be serialized and included in the blueprint.
### Plutus Core Signposting
All this fuss is to get us to a point where we can write Uplc - and good Uplc at that.
All this fuss is to get us to a point where we can write Uplc - and good Uplc at that.
Note that there are many ways to generate code and most of them are bad.
The various design decisions and compilation steps make more sense
when we have a better understanding of the target language.
The various design decisions and compilation steps make more sense
when we have a better understanding of the target language.
Uplc is a lambda calculus.
For a comprehensive definition on Uplc checkout the specification found
[here](https://github.com/input-output-hk/plutus/#specifications-and-design) from the plutus GitHub repo.
Uplc is a lambda calculus.
For a comprehensive definition on Uplc checkout the specification found
[here](https://github.com/input-output-hk/plutus/#specifications-and-design) from the plutus GitHub repo.
(I imagine this link will be maintained longer than the current actual link.)
If you're not at all familiar with lambda calculus I recommend
If you're not at all familiar with lambda calculus I recommend
[an unpacking](https://crypto.stanford.edu/~blynn/lambda/) by Ben Lynn.
### What next?
I think it would be helpful to have some examples... Watch this space.
I think it would be helpful to have some examples... Watch this space.

View File

@ -1,15 +1,18 @@
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener("DOMContentLoaded", function () {
// Get all "navbar-burger" elements
var $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
var $navbarBurgers = Array.prototype.slice.call(
document.querySelectorAll(".navbar-burger"),
0,
);
// Check if there are any navbar burgers
if ($navbarBurgers.length > 0) {
// Add a click event on each of them
$navbarBurgers.forEach(function ($el) {
$el.addEventListener('click', function () {
$el.addEventListener("click", function () {
// Get the "main-nav" element
var $target = document.getElementById('main-nav');
var $target = document.getElementById("main-nav");
// Toggle the class on "main-nav"
$target.classList.toggle('hidden');
$target.classList.toggle("hidden");
});
});
}

File diff suppressed because one or more lines are too long

View File

@ -2,15 +2,14 @@
"nodes": {
"devshell": {
"inputs": {
"nixpkgs": "nixpkgs",
"systems": "systems"
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1688380630,
"narHash": "sha256-8ilApWVb1mAi4439zS3iFeIT0ODlbrifm/fegWwgHjA=",
"lastModified": 1735644329,
"narHash": "sha256-tO3HrHriyLvipc4xr+Ewtdlo7wM1OjXNjlWRgmM7peY=",
"owner": "numtide",
"repo": "devshell",
"rev": "f9238ec3d75cefbb2b42a44948c4e8fb1ae9a205",
"rev": "f7795ede5b02664b57035b3b757876703e2c3eac",
"type": "github"
},
"original": {
@ -24,11 +23,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1683560683,
"narHash": "sha256-XAygPMN5Xnk/W2c1aW0jyEa6lfMDZWlQgiNtmHXytPc=",
"lastModified": 1738453229,
"narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "006c75898cf814ef9497252b022e91c946ba8e17",
"rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd",
"type": "github"
},
"original": {
@ -38,11 +37,11 @@
},
"flake-root": {
"locked": {
"lastModified": 1680964220,
"narHash": "sha256-dIdTYcf+KW9a4pKHsEbddvLVSfR1yiAJynzg2x0nfWg=",
"lastModified": 1723604017,
"narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=",
"owner": "srid",
"repo": "flake-root",
"rev": "f1c0b93d05bdbea6c011136ba1a135c80c5b326c",
"rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e",
"type": "github"
},
"original": {
@ -53,11 +52,11 @@
},
"haskell-flake": {
"locked": {
"lastModified": 1684180957,
"narHash": "sha256-qtEZf4gcmQU5ePbFtltqpAS0PajWLURVC7nuoS46dSk=",
"lastModified": 1739669127,
"narHash": "sha256-2s3wYTqKq7aBa41VHWg/G2XAOii8MW+WAMtLdgy1cek=",
"owner": "srid",
"repo": "haskell-flake",
"rev": "4e1c76de8795608bb47295c018b37a563c492fd2",
"rev": "eabf8cf32e5f6a267ea637e1b3eabc9b7ddf29e1",
"type": "github"
},
"original": {
@ -68,11 +67,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1677383253,
"narHash": "sha256-UfpzWfSxkfXHnb4boXZNaKsAcUrZT9Hw+tao1oZxd08=",
"lastModified": 1722073938,
"narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9952d6bc395f5841262b006fbace8dd7e143b634",
"rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae",
"type": "github"
},
"original": {
@ -84,29 +83,23 @@
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1682879489,
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
"type": "github"
"lastModified": 1738452942,
"narHash": "sha256-vJzFZGaCpnmo7I6i416HaBLpC+hvcURh/BQwROcGIp8=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1684385584,
"narHash": "sha256-O7y0gK8OLIDqz+LaHJJyeu09IGiXlZIS3+JgEzGmmJA=",
"lastModified": 1739446958,
"narHash": "sha256-+/bYK3DbPxMIvSL4zArkMX0LQvS7rzBKXnDXLfKyRVc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "48a0fb7aab511df92a17cf239c37f2bd2ec9ae3a",
"rev": "2ff53fe64443980e139eaa286017f53f88336dd0",
"type": "github"
},
"original": {
@ -118,16 +111,16 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1680945546,
"narHash": "sha256-8FuaH5t/aVi/pR1XxnF0qi4WwMYC+YxlfdsA0V+TEuQ=",
"lastModified": 1735554305,
"narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d9f759f2ea8d265d974a6e1259bd510ac5844c5d",
"rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
@ -142,31 +135,16 @@
"treefmt-nix": "treefmt-nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1684416994,
"narHash": "sha256-KkZ9diPRl3Y05TngWYs/QhZKnI/3tA3s+2Hhmei8FnE=",
"lastModified": 1738953846,
"narHash": "sha256-yrK3Hjcr8F7qS/j2F+r7C7o010eVWWlm4T1PrbKBOxQ=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "42045102f90cfd23ca44ae4ef8362180fefcd7fd",
"rev": "4f09b473c936d41582dd744e19f34ec27592c5fd",
"type": "github"
},
"original": {

View File

@ -28,10 +28,11 @@
# system.
haskellProjects.default = {
# packages.haskell-template.root = ./.; # Auto-discovered by haskell-flake
overrides = self: super: { };
devShell = {
tools = hp: {
fourmolu = hp.fourmolu;
hoogle = hp.hoogle;
haskell-language-server = hp.haskell-language-server;
treefmt = config.treefmt.build.wrapper;
} // config.treefmt.build.programs;
hlsCheck.enable = false;
@ -59,27 +60,53 @@
"-XImportQualifiedPost"
];
};
programs.prettier.enable = true;
};
# Equivalent to inputs'.nixpkgs.legacyPackages.hello;
devShells.default = pkgs.mkShell {
inputsFrom = [
config.haskellProjects.default.outputs.devShell
config.flake-root.devShell
];
packages = with pkgs; [
caddy
nil
nodePackages_latest.vscode-langservers-extracted
nodePackages_latest.tailwindcss
nodePackages_latest.typescript-language-server
haskellPackages.hakyll
zlib
];
};
devShells.default =
let
menu = pkgs.writeShellScriptBin "menu"
''
echo -e "\nCommands available: \n${
builtins.foldl' (x: y: x + " -> " + (pkgs.lib.getName y) + "\n") "" my-packages
}"
'';
my-packages = [
menu
build
watch
deploy
];
build = pkgs.writeShellScriptBin "build" ''
tailwindcss -i ./content/css/main.css -o ./content/css/mini.css --minify
cabal run site -- build
'';
watch = pkgs.writeShellScriptBin "watch" ''
tailwindcss -i ./content/css/main.css -o ./content/css/mini.css --minify
cabal run site -- watch
'';
deploy = pkgs.writeShellScriptBin "deploy" ''
rsync -r --delete ./docs/* genesis:/var/www/kompactio-landing/
'';
in
pkgs.mkShell {
inputsFrom = [
config.haskellProjects.default.outputs.devShell
config.flake-root.devShell
];
packages = with pkgs; [
caddy
nil
nodePackages_latest.vscode-langservers-extracted
nodePackages_latest.tailwindcss
nodePackages_latest.typescript-language-server
haskellPackages.hakyll
zlib
] ++ my-packages;
};
};
flake = {
# The usual flake attributes can be defined here, including system-

View File

@ -1,13 +0,0 @@
name: example
version: 0.1.0.0
build-type: Simple
cabal-version: >= 1.10
executable site
main-is: site.hs
build-depends: base == 4.*
, hakyll == 4.15.*
, hip == 1.5.*
, filepath
ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010

15
site.cabal Normal file
View File

@ -0,0 +1,15 @@
name: site
version: 0.1.0.0
build-type: Simple
cabal-version: >=1.10
executable site
main-is: site.hs
build-depends:
base >=4 && <5
, filepath
, hakyll >=4.16 && <4.17
, hip >=1.5 && <1.6
ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010

38
site.hs
View File

@ -1,9 +1,9 @@
--------------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}
import Data.Monoid (mappend)
import Hakyll
import System.FilePath (splitExtension, joinPath, splitDirectories, replaceExtension)
import Data.Monoid (mappend)
import Hakyll
import System.FilePath (joinPath, replaceExtension, splitDirectories, splitExtension)
--------------------------------------------------------------------------------
main :: IO ()
@ -17,38 +17,43 @@ main = hakyll $ do
compile copyFileCompiler
match "content/scripts/*" $ do
route rmPrefix
route rmPrefix
compile copyFileCompiler
match "content/css/*" $ do
route rmPrefix
route rmPrefix
compile compressCssCompiler
match "content/fonts/*" $ do
route rmPrefix
route rmPrefix
compile copyFileCompiler
match "content/posts/*.md" $ do
route rmPrefixMd
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
compile $
pandocCompiler
>>= loadAndApplyTemplate "templates/post.html" postCtx
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
create ["blog.html"] $ do
route idRoute
compile $ do
posts <- recentFirst =<< loadAll "content/posts/*.md"
let archiveCtx =
listField "posts" postCtx (return posts) `mappend`
constField "title" "Blog" `mappend`
defaultContext
listField "posts" postCtx (return posts)
`mappend` constField "title" "Blog"
`mappend` defaultContext
makeItem ""
>>= loadAndApplyTemplate "templates/blog.html" archiveCtx
>>= loadAndApplyTemplate "templates/default.html" archiveCtx
>>= relativizeUrls
-- match "content/index/*" $ do
-- compile $
-- pandocCompilerWith x
match "content/index.md" $ do
route rmPrefixMd
@ -63,18 +68,17 @@ main = hakyll $ do
match "templates/*" $ compile templateBodyCompiler
--------------------------------------------------------------------------------
postCtx :: Context String
postCtx =
dateField "date" "%Y-%m-%d" `mappend`
defaultContext
dateField "date" "%Y-%m-%d"
`mappend` defaultContext
setExtensionInner :: String -> FilePath -> FilePath
setExtensionInner = flip replaceExtension
rmPrefixInner :: FilePath -> FilePath
rmPrefixInner = joinPath . tail . splitDirectories
rmPrefixInner = joinPath . tail . splitDirectories
rmPrefix :: Routes
rmPrefix = customRoute $ rmPrefixInner . toFilePath

View File

@ -1,19 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./content/**/*.{html,js}",
"./templates/**/*.{html,js}",
],
content: ["./content/**/*.{html,js}", "./templates/**/*.{html,js}"],
theme: {
extend: {},
fontFamily: {
'sans' : ['jetbrains-mono',],
sans: ["jetbrains-mono"],
},
typography: (theme) => ({}),
},
darkMode: 'class',
darkMode: "class",
variants: {},
// plugins: [require('@tailwindcss/typography')],
plugins: [],
}
};

View File

@ -1,27 +1,15 @@
<section id="about" class="py-12 px-2 flex flex-col gap-12">
<header class="text-3xl">
# about
</header>
<header class="text-3xl"># about</header>
<div>
Kompact.io is dapp dev house.
Our focus:
Kompact.io is dapp dev house. Our focus:
<ul class="list-decoration">
<li>
safety-first
</li>
<li>
fast turn around
</li>
<li>
integration support
</li>
<li>safety-first</li>
<li>fast turn around</li>
<li>integration support</li>
</ul>
<div>
Our typical process:
<div>
Idea -> Spec -> Impl -> Test -> Handover
</div>
<div>Idea -> Spec -> Impl -> Test -> Handover</div>
</div>
</div>
</section>
</section>

View File

@ -1,7 +1,5 @@
<section id="services" class="py-6 px-2 flex flex-col gap-12">
<header class="text-3xl">
# blog
</header>
<header class="text-3xl"># blog</header>
<div class="text-gray-800 dark:text-gray-200 mt-4">
A nascent initiative sharing some of the things happening at Kompact.io.
</div>
@ -9,4 +7,4 @@
<section class="py-6 px-2 flex flex-col gap-12">
$partial("templates/post-list.html")$
</section>
</section>

View File

@ -1,11 +1,10 @@
<section id="contact" class="py-12 px-2 flex flex-col gap-12">
<header class="text-3xl">
# contact
</header>
<header class="text-3xl"># contact</header>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Questions? We'll be happy to help answer any of your questions. Send us an email and we'll get back to you shortly.
Questions? We'll be happy to help answer any of your questions. Send us an
email and we'll get back to you shortly.
</div>
<div>
Reach us on : <a href="mailto:kompactio@proton.me">kompactio@proton.me</a>
</div>
</section>
</section>

View File

@ -1,42 +1,41 @@
<!doctype html>
<html class="">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="/favicon.png">
<link href="/css/mini.css" rel="stylesheet">
<link href="/css/prism.css" rel="stylesheet" />
<title>$title$</title>
</head>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
function updateTheme() {
if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/favicon.png" />
<link href="/css/mini.css" rel="stylesheet" />
<link href="/css/prism.css" rel="stylesheet" />
<title>$title$</title>
</head>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
function updateTheme() {
if (
localStorage.theme === "dark" ||
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
updateTheme();
</script>
}
updateTheme()
</script>
<body
class="bg-white text-gray-900 min-h-screen dark:bg-gradient-to-br dark:from-slate-950 dark:to-black dark:text-white"
>
<div class="container mx-auto">
<hr />
$partial("templates/nav.html")$
<hr />
$body$
<hr />
$partial("templates/footer.html")$
</div>
</body>
<body class="bg-white text-gray-900 min-h-screen
dark:bg-gradient-to-br dark:from-slate-950 dark:to-black dark:text-white">
<div class="container mx-auto ">
<hr />
$partial("templates/nav.html")$
<hr />
$body$
<hr />
$partial("templates/footer.html")$
</div>
</body>
<script src="/scripts/prism.js"></script>
</html>
<script src="/scripts/prism.js"></script>
</html>

View File

@ -1,22 +1,34 @@
<section id="footer" class="py-12 px-2 flex flex-row gap-12 mx-2 sm:mx-4 items-start justify-between
text-gray-800 dark:text-gray-200 dark:fill-white">
<div class="text-sm">
&reg; 2023 kompact.io &trade; All Rights Reserved.
</div>
<section
id="footer"
class="py-12 px-2 flex flex-row gap-12 mx-2 sm:mx-4 items-start justify-between text-gray-800 dark:text-gray-200 dark:fill-white"
>
<div class="text-sm">&reg; 2023 kompact.io &trade; All Rights Reserved.</div>
<div class="flex flex-row gap-4">
<a href="https://www.linkedin.com/in/dominic-algernon-wallis-123b42187/">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" height="20" preserveAspectRatio="xMidYMid meet">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
height="20"
preserveAspectRatio="xMidYMid meet"
>
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z" />
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"
/>
</svg>
</a>
<a href="https://twitter.com/waalge">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="20" preserveAspectRatio="xMidYMid meet">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
height="20"
preserveAspectRatio="xMidYMid meet"
>
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" />
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
/>
</svg>
</a>
</div>
</section>
</section>

View File

@ -1,12 +1,13 @@
<section id="hero" class="py-8 px-2 h-96 min-h-[50vh] m-auto">
<div class="h-full flex justify-around align-center items-center">
<div class="text-6xl">
⟨K⟩
</div>
<div class="text-6xl">⟨K⟩</div>
<div class="flex flex-col gap-2 truncate">
<div>withKompact &#36; <span class="text-red-500 dark:text-yellow-400">do</span> </div>
<div><span class="text-gray-400">· ·</span> dapp <- lean dev </div>
<div><span class="text-gray-400">· ·</span> run dapp </div>
<div>
withKompact &#36;
<span class="text-red-500 dark:text-yellow-400">do</span>
</div>
<div><span class="text-gray-400">· ·</span> dapp <- lean dev</div>
<div><span class="text-gray-400">· ·</span> run dapp</div>
</div>
</section>
</div>
</section>

View File

@ -4,4 +4,4 @@ $partial("templates/services.html")$
<hr />
$partial("templates/pricing.html")$
<hr />
$partial("templates/contact.html")$
$partial("templates/contact.html")$

View File

@ -9,7 +9,8 @@
<div>
<ul class="flex flex-row gap-4 md:gap-8">
<li>
<button onClick="
<button
onClick="
(() => {
if (!('theme' in localStorage)) {
localStorage.theme = 'light'
@ -22,19 +23,18 @@
}
updateTheme()
})()
">◧</button>
"
>
</button>
</li>
<li>
<a href="/index.html#contact">
contact
</a>
<a href="/index.html#contact"> contact </a>
</li>
<li>
<a href="/blog.html">
blog
</a>
<a href="/blog.html"> blog </a>
</li>
</ul>
</div>
</div>
</nav>
</nav>

View File

@ -2,11 +2,9 @@
$for(posts)$
<li class="mt-4">
<a href="$url$">
<span class="text-gray-800 dark:text-gray-200">
$date$ ::
</span>
<span class="text-gray-800 dark:text-gray-200"> $date$ :: </span>
$title$
</a>
</li>
$endfor$
</ul>
</ul>

View File

@ -1,20 +1,11 @@
<article class="mx-auto px-4 max-w-prose">
<section class="header">
<h1>
$title$
</h1>
<h1>$title$</h1>
$if(date)$
<p>
Posted on $date$
</p>
$endif$
$if(author)$
<p>
by $author$
</p>
<p>Posted on $date$</p>
$endif$ $if(author)$
<p>by $author$</p>
$endif$
</section>
<section>
$body$
</section>
</article>
<section>$body$</section>
</article>

View File

@ -1,46 +1,39 @@
<section id="pricing" class="py-12 px-2 flex flex-col gap-12">
<header class="text-3xl">
# pricing
</header>
<header class="text-3xl"># pricing</header>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Plutus development has traditionally meant long development schedules, and expensive ( &#36; 25k+/mo FTE) engineers.
We can work with you at competitive rates in either deliverable or retainer based engagements.
Plutus development has traditionally meant long development schedules, and
expensive ( &#36; 25k+/mo FTE) engineers. We can work with you at
competitive rates in either deliverable or retainer based engagements.
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-8 md:mx-24">
<div class="max-w-48">
<div class="text-1xl font-bold">
## retainer
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Time-based
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:gap-8">
<div class="flex-1">
<div class="text-1xl font-bold">## retainer</div>
<p class="text-gray-800 dark:text-gray-200 mt-4">Time-based</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
Still figuring out your project scope?
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
Need an extra pair of hands on an existing project?
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
Then a retainer based engagement is for you.
</div>
</p>
</div>
<div class="max-w-48">
<div class="text-1xl font-bold">
## deliverable
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Output-based
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
<div class="flex-1">
<div class="text-1xl font-bold">## deliverable</div>
<p class="text-gray-800 dark:text-gray-200 mt-4">Output-based</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
You know what you want and need help implementing it?
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
We'll first produce a spec on how the dapp will operate technically.
This involves discussing different options and trade-offs on things from UX to validator complexity.
</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Once settled we'll begin the implementation phase and finally integration phase.
</div>
This involves discussing different options and trade-offs on things from
UX to validator complexity.
</p>
<p class="text-gray-800 dark:text-gray-200 mt-4">
Once settled we'll begin the implementation phase and finally
integration phase.
</p>
</div>
</div>
</section>
</section>

View File

@ -1,35 +1,30 @@
<section id="services" class="py-12 px-2 flex flex-col gap-12">
<header class="text-3xl">
# services
</header>
<header class="text-3xl"># services</header>
<div class="text-gray-800 dark:text-gray-200 mt-4">
We are cardano native dapp dev outfit focused on helping you going from 0 to launch ASAP.
We are cardano native dapp dev outfit focused on helping you going from 0 to
launch ASAP.
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3 sm:gap-8">
<div class="max-w-48">
<div class="text-1xl font-bold">
## strategy
</div>
<div class="text-1xl font-bold">## strategy</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
We'll work with you to validate your concept, and translate it into an implementable Proof of Concept
We'll work with you to validate your concept, and translate it into an
implementable Proof of Concept
</div>
</div>
<div class="max-w-48">
<div class="text-1xl font-bold">
## implementation
</div>
<div class="text-1xl font-bold">## implementation</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
Cook up appropriate Plutus validators to meet your needs
</div>
</div>
<div class="max-w-48">
<div class="text-1xl font-bold">
## deployment
</div>
<div class="text-1xl font-bold">## deployment</div>
<div class="text-gray-800 dark:text-gray-200 mt-4">
We facilitate integrating the on-chain aspects with the rest of your stack
We facilitate integrating the on-chain aspects with the rest of your
stack
</div>
</div>
</div>
</section>
</section>

View File

@ -3,55 +3,135 @@
<div class="relative flex h-16 items-center justify-between">
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
<!-- Mobile menu button-->
<button type="button" class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
<button
type="button"
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<!--
Icon when menu is closed.
Menu open: "hidden", Menu closed: "block"
-->
<svg class="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
<svg
class="block h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
<!--
Icon when menu is open.
Menu open: "block", Menu closed: "hidden"
-->
<svg class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
<svg
class="hidden h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div
class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start"
>
<div class="flex flex-shrink-0 items-center">
<img class="block h-8 w-auto lg:hidden" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company">
<img class="hidden h-8 w-auto lg:block" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company">
<img
class="block h-8 w-auto lg:hidden"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500"
alt="Your Company"
/>
<img
class="hidden h-8 w-auto lg:block"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=500"
alt="Your Company"
/>
</div>
<div class="hidden sm:ml-6 sm:block">
<div class="flex space-x-4">
<!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
<a href="#" class="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium" aria-current="page">Dashboard</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Team</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Projects</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Calendar</a>
<a
href="#"
class="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium"
aria-current="page"
>Dashboard</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium"
>Team</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium"
>Projects</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium"
>Calendar</a
>
</div>
</div>
</div>
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
<button type="button" class="rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
<div
class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0"
>
<button
type="button"
class="rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
>
<span class="sr-only">View notifications</span>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
/>
</svg>
</button>
<!-- Profile dropdown -->
<div class="relative ml-3">
<div>
<button type="button" class="flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<button
type="button"
class="flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
id="user-menu-button"
aria-expanded="false"
aria-haspopup="true"
>
<span class="sr-only">Open user menu</span>
<img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
<img
class="h-8 w-8 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</button>
</div>
@ -65,11 +145,38 @@
From: "transform opacity-100 scale-100"
To: "transform opacity-0 scale-95"
-->
<div class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
<div
class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu-button"
tabindex="-1"
>
<!-- Active: "bg-gray-100", Not Active: "" -->
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-0">Your Profile</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-1">Settings</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</a>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"
id="user-menu-item-0"
>Your Profile</a
>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"
id="user-menu-item-1"
>Settings</a
>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"
id="user-menu-item-2"
>Sign out</a
>
</div>
</div>
</div>
@ -80,10 +187,27 @@
<div class="sm:hidden" id="mobile-menu">
<div class="space-y-1 px-2 pb-3 pt-2">
<!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
<a href="#" class="bg-gray-900 text-white block rounded-md px-3 py-2 text-base font-medium" aria-current="page">Dashboard</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Team</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Projects</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Calendar</a>
<a
href="#"
class="bg-gray-900 text-white block rounded-md px-3 py-2 text-base font-medium"
aria-current="page"
>Dashboard</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium"
>Team</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium"
>Projects</a
>
<a
href="#"
class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium"
>Calendar</a
>
</div>
</div>
</nav>
</nav>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
@ -12,13 +12,13 @@
/>
<meta name="theme-color" content="#ffffff" />
<title>$title$</title>
<meta
name="description"
content="Lean dapp development"
<meta name="description" content="Lean dapp development" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/normalize.css" />
<link rel="stylesheet" href="/css/terminal.css" />
<link rel="stylesheet" href="/css/custom.css" />