-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
87 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,16 +15,14 @@ Let's build a UI to send and redeem a gift card using smart contracts on Cardano | |
|
||
- [x] Writing `Aiken` inter-dependent `mint` & `spend` validators. | ||
- [x] Parameterizing validators. | ||
- [x] Using [Lucid](https://lucid.spacebudz.io/) with [Blockfrost](https://blockfrost.io)<sup>★</sup>. | ||
- [x] Using [Blaze](https://github.com/butaneprotocol/blaze-cardano/) with Blockfrost<sup>★</sup>. | ||
<sub> | ||
★ We'll once again be using the `Blockfrost` provider. So have your | ||
Blockfrost API key ready. | ||
</sub> | ||
- [x] Using [Deno fresh](https://fresh.deno.dev/)<sup>★</sup>. | ||
<sub> | ||
★ You can install deno using these | ||
[instructions](https://deno.land/[email protected]/getting_started/installation). | ||
★ You can easily get access by using [Demeter](https://demeter.run). Go | ||
there and grab your API key by creating a Blockfrost instance on their | ||
dashboard. | ||
</sub> | ||
- [x] Using [SvelteKit](https://kit.svelte.dev/)<sup>★</sup>. | ||
<sub>★ Make sure you have Node.js installed.</sub> | ||
|
||
<Callout type="info" emoji="📘"> | ||
When encountering an unfamiliar syntax or concept, do not hesitate to refer to | ||
|
@@ -379,152 +377,121 @@ aiken build | |
## Building a frontend | ||
|
||
With the easy part out of the way we can start building a frontend to interact with our | ||
smart contracts in the browser. Deno fresh is an interesting project for building | ||
web applications in Deno. | ||
smart contracts in the browser. SvelteKit is an interesting framework for building | ||
web applications. | ||
|
||
### Setting up | ||
|
||
Let's generate a Deno fresh project in the same directory as our Aiken project. | ||
Let's generate a SvelteKit project in the same directory as our Aiken project. | ||
|
||
```sh | ||
deno run -A -r https://fresh.deno.dev . | ||
npm create svelte@latest . | ||
``` | ||
|
||
<Callout type="warning">When prompted to enable Tailwind CSS say yes.</Callout> | ||
|
||
We need lucid and we should probably add an alias for better looking imports. | ||
Let's edit `import_map.json`. | ||
|
||
```json filename="import_map.json" {11-12} | ||
{ | ||
"imports": { | ||
"$fresh/": "...", | ||
"preact": "...", | ||
"preact/": "...", | ||
"preact-render-to-string": "...", | ||
"@preact/signals": "...", | ||
"@preact/signals-core": "...", | ||
"twind": "...", | ||
"twind/": "...", | ||
"lucid/": "https://deno.land/x/[email protected]/", | ||
"~/": "./" | ||
} | ||
} | ||
<Callout type="warning"> | ||
When prompted use the current directory, continue even though directory is not | ||
empty, choose a skeleton project, use Svelte 5, and enable typescript. | ||
</Callout> | ||
|
||
We need to add Blaze now. | ||
|
||
```sh | ||
npm add @blaze-cardano/sdk@latest @blaze-cardano/core@latest @blaze-cardano/query@latest @blaze-cardano/uplc@latest @blaze-cardano/wallet@latest @blaze-cardano/tx@latest | ||
``` | ||
|
||
We can delete a few things that come with the starter template that we don't need. | ||
Let's add tailwindcss to our project. | ||
|
||
```sh | ||
rm islands/Counter.tsx | ||
rm -rf routes/api | ||
rm routes/\[name\].tsx | ||
npx svelte-add@latest tailwindcss | ||
``` | ||
|
||
Let's also add some reusable components to our project. | ||
<Callout type="warning">When prompted just say yes to everything.</Callout> | ||
|
||
```tsx filename="components/Button.tsx" | ||
import { JSX } from "preact"; | ||
import { IS_BROWSER } from "$fresh/runtime.ts"; | ||
Let's also add some reusable components to our project. | ||
|
||
export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) { | ||
return ( | ||
<button | ||
{...props} | ||
disabled={!IS_BROWSER || props.disabled} | ||
class={`group inline-flex items-center justify-center rounded-full py-2 px-4 text-sm font-semibold focus:outline-none bg-blue-600 text-white hover:bg-blue-500 active:bg-blue-800 active:text-blue-100 ${props.class}`} | ||
/> | ||
); | ||
} | ||
``` | ||
```svelte filename="src/lib/components/Button.svelte" | ||
<script lang="ts"> | ||
import type { Snippet } from 'svelte'; | ||
<Callout type="info"> | ||
You can just replace the existing Button component with the above code | ||
</Callout> | ||
type Props = { | ||
disabled?: boolean; | ||
children: Snippet; | ||
}; | ||
```tsx filename="components/Input.tsx" | ||
import { ComponentChild, JSX } from "preact"; | ||
let { disabled = false, children }: Props = $props(); | ||
</script> | ||
export function Input({ | ||
children, | ||
id, | ||
...props | ||
}: JSX.HTMLAttributes<HTMLInputElement>) { | ||
return ( | ||
<div> | ||
<label for={id} class="block mb-3 text-sm font-medium text-gray-700"> | ||
{children} | ||
</label> | ||
<input | ||
{...props} | ||
id={id} | ||
class="block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm" | ||
/> | ||
</div> | ||
); | ||
} | ||
<button | ||
{disabled} | ||
class="group inline-flex items-center justify-center rounded-full bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 focus:outline-none active:bg-blue-800 active:text-blue-100" | ||
>{@render children()}</button | ||
> | ||
``` | ||
|
||
### Home page | ||
|
||
Everything we'll be doing with validators and transactions will happen fully client side. | ||
This means we can just have our route render a single `island` component and then | ||
we can write all of our code in this island for the most part. | ||
```svelte filename="src/lib/components/Input.svelte" | ||
<script lang="ts"> | ||
import type { Snippet } from 'svelte'; | ||
import type { HTMLInputAttributes } from 'svelte/elements'; | ||
Let's create a new file `islands/Oneshot.tsx` and add the following code. | ||
interface Props extends HTMLInputAttributes { | ||
children: Snippet<[]>; | ||
} | ||
```tsx filename="islands/Oneshot.tsx" | ||
export default function Oneshot() { | ||
return <div>Oneshot</div>; | ||
} | ||
let { id, children, ...props }: Props = $props(); | ||
</script> | ||
<div> | ||
<label for={id} class="mb-3 block text-sm font-medium text-gray-700"> | ||
{@render children()} | ||
</label> | ||
<input | ||
{...props} | ||
{id} | ||
class="block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm" | ||
/> | ||
</div> | ||
``` | ||
|
||
Now inside of `routes/index.tsx` we can import our new island and render it. | ||
### Home page | ||
|
||
```tsx filename="routes/index.tsx" {3,29} | ||
import { Head } from "$fresh/runtime.ts"; | ||
Everything we'll be doing with validators and transactions will happen fully client side. | ||
This means we can just have our app render a single `+page.svelte` component and then | ||
we can write all of our code in this page component for the most part. | ||
|
||
import Oneshot from "~/islands/Oneshot"; | ||
Let's edit `src/routes/+page.svelte` to contain the following code. | ||
|
||
export default function Home() { | ||
return ( | ||
<> | ||
<Head> | ||
<title>One Shot</title> | ||
</Head> | ||
```svelte filename="src/routes/+page.svelte" | ||
<svelte:head> | ||
<title>One Shot</title> | ||
</svelte:head> | ||
<div class="max-w-2xl mx-auto mt-20 mb-10"> | ||
<div class="mb-10"> | ||
<h2 class="text-lg font-semibold text-gray-900"> | ||
Make a one shot minting and lock contract | ||
</h2> | ||
<div class="mx-auto mb-10 mt-20 max-w-2xl"> | ||
<div class="mb-10"> | ||
<h2 class="text-lg font-semibold text-gray-900">Make a one shot minting and lock contract</h2> | ||
<h3 class="mt-4 mb-2">Redeem</h3> | ||
<pre class="bg-gray-200 p-2 rounded overflow-x-scroll"> | ||
TODO: Render non-parameterized redeem validator | ||
</pre> | ||
<h3 class="mb-2 mt-4">Redeem</h3> | ||
<pre class="overflow-x-scroll rounded bg-gray-200 p-2"> | ||
TODO: Render non-parameterized redeem validator | ||
</pre> | ||
<h3 class="mt-4 mb-2">Gift Card</h3> | ||
<pre class="bg-gray-200 p-2 rounded overflow-x-scroll"> | ||
TODO: Render non-parameterized gift_card validator | ||
</pre> | ||
</div> | ||
<h3 class="mb-2 mt-4">Gift Card</h3> | ||
<pre class="overflow-x-scroll rounded bg-gray-200 p-2"> | ||
TODO: Render non-parameterized gift_card validator | ||
</pre> | ||
</div> | ||
<Oneshot /> | ||
</div> | ||
</> | ||
); | ||
} | ||
<div>Oneshot</div> | ||
</div> | ||
``` | ||
|
||
You can replace everything that was in `routes/index.tsx` with the above code. | ||
We've left some `TODO`'s in the code to remind us to render the validators. We'll render | ||
We've left a `TODO` in the code to remind us to render the validator. We'll render | ||
the compiled aiken code as a hex encoded string. There not much of a reason to do this, it's just | ||
kinda cool to see. | ||
|
||
Next we should load the `plutus.json` file and get the compiled aiken code. Let's create | ||
a file called `utils.ts` and add the following code. | ||
a file called `lib/utils.ts` and add the following code. | ||
|
||
```ts filename="utils.ts" | ||
```ts filename="lib/utils.ts" | ||
import { MintingPolicy, SpendingValidator } from "lucid/mod.ts"; | ||
|
||
import blueprint from "~/plutus.json" assert { type: "json" }; | ||
|
@@ -780,7 +747,10 @@ export function applyParams( | |
|
||
return { | ||
redeem: { type: "PlutusV2", script: applyDoubleCborEncoding(redeem) }, | ||
giftCard: { type: "PlutusV2", script: applyDoubleCborEncoding(giftCard) }, | ||
giftCard: { | ||
type: "PlutusV2", | ||
script: applyDoubleCborEncoding(giftCard), | ||
}, | ||
policyId, | ||
lockAddress, | ||
}; | ||
|