feat: convert gift card tutorial to lucid-evolution and weld

This commit is contained in:
rvcas
2024-11-25 16:09:10 -05:00
committed by Lucas
parent 7c1cd81554
commit 9d59333757
23 changed files with 8566 additions and 9995 deletions

View File

@@ -1,33 +1,13 @@
import type { CIP30Interface } from '@blaze-cardano/wallet';
// See https://kit.svelte.dev/docs/types#app
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
type WalletOption = {
name: string;
icon: string;
apiVersion: string;
enable(): Promise<CIP30Interface>;
isEnabled(): Promise<boolean>;
};
type Cardano = {
[key: string]: WalletOption;
};
declare global {
interface Window {
cardano?: Cardano;
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -1,12 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -6,7 +6,7 @@
children: Snippet<[]>;
}
let { id, children, ...props }: Props = $props();
let { id, children, value = $bindable(), ...props }: Props = $props();
</script>
<div>
@@ -14,6 +14,7 @@
{@render children()}
</label>
<input
bind:value
{...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"

View File

@@ -1,11 +1,38 @@
import {
applyDoubleCborEncoding,
applyParamsToScript,
Constr,
fromText,
validatorToAddress,
validatorToScriptHash,
type MintingPolicy,
type OutRef,
type SpendingValidator
} from '@lucid-evolution/lucid';
import blueprint from '../../plutus.json' assert { type: 'json' };
export type Validators = {
giftCard: string;
};
export type LocalCache = {
tokenName: string;
giftADA: string;
lockTxHash: string;
parameterizedValidators: AppliedValidators;
};
export type AppliedValidators = {
redeem: SpendingValidator;
giftCard: MintingPolicy;
policyId: string;
lockAddress: string;
};
export function readValidators(): Validators {
const giftCard = blueprint.validators.find((v) => v.title === 'oneshot.gift_card.spend');
const giftCard = blueprint.validators.find(
(v) => v.title === 'oneshot.gift_card.spend'
);
if (!giftCard) {
throw new Error('Gift Card validator not found');
@@ -15,3 +42,36 @@ export function readValidators(): Validators {
giftCard: giftCard.compiledCode
};
}
export function applyParams(
tokenName: string,
outputReference: OutRef,
validator: string
): AppliedValidators {
const outRef = new Constr(0, [
new Constr(0, [outputReference.txHash]),
BigInt(outputReference.outputIndex)
]);
const giftCard = applyParamsToScript(validator, [
fromText(tokenName),
outRef
]);
const policyId = validatorToScriptHash({
type: 'PlutusV2',
script: giftCard
});
const lockAddress = validatorToAddress('Preprod', {
type: 'PlutusV2',
script: giftCard
});
return {
redeem: { type: 'PlutusV2', script: applyDoubleCborEncoding(giftCard) },
giftCard: { type: 'PlutusV2', script: applyDoubleCborEncoding(giftCard) },
policyId,
lockAddress
};
}

View File

@@ -0,0 +1,43 @@
import { createWeldInstance, type WeldConfig } from '@ada-anvil/weld';
import { getContext, setContext } from 'svelte';
export class Weld {
weld = createWeldInstance();
// Use the $state rune to create a reactive object for each Weld store
config = $state(this.weld.config.getState());
wallet = $state(this.weld.wallet.getState());
extensions = $state(this.weld.extensions.getState());
constructor(persist?: Partial<WeldConfig>) {
this.weld.config.update({ updateInterval: 2000 });
if (persist) this.weld.persist(persist);
$effect(() => {
this.weld.init();
// Subscribe to Weld stores and update reactive objects when changse occur
// Note: No need to use subscribeWithSelector as $state objects are deeply reactive
this.weld.config.subscribe((s) => (this.config = s));
this.weld.wallet.subscribe((s) => (this.wallet = s));
this.weld.extensions.subscribe((s) => (this.extensions = s));
return () => this.weld.cleanup();
});
}
}
// Use the context API to scope weld stores and prevent unwanted sharing
// of data between clients when rendering on the server
const weldKey = Symbol('weld');
export function setWeldContext(persist?: Partial<WeldConfig>) {
const value = new Weld(persist);
setContext(weldKey, value);
return value;
}
export function getWeldContext() {
return getContext<ReturnType<typeof setWeldContext>>(weldKey);
}

View File

@@ -1,5 +1,11 @@
<script>
<script lang="ts">
import '../app.css';
import { setWeldContext } from '$lib/wallet.svelte';
let { children } = $props();
setWeldContext({ enablePersistence: true });
</script>
<slot></slot>
{@render children()}

View File

@@ -1,10 +1,26 @@
<script lang="ts">
import { Blaze, Blockfrost, WebWallet } from '@blaze-cardano/sdk';
import { onMount } from 'svelte';
import { getWeldContext } from '$lib/wallet.svelte';
import {
Lucid,
Blockfrost,
type LucidEvolution,
Data,
Constr,
fromText
} from '@lucid-evolution/lucid';
// Components
import Input from '$lib/components/Input.svelte';
import Button from '$lib/components/Button.svelte';
// Utils
import {
applyParams,
type AppliedValidators,
type LocalCache
} from '$lib/utils';
// Local Types
import type { PageData } from './$types';
@@ -15,30 +31,167 @@
let { data }: Props = $props();
// Page Local State
let blaze: Blaze<Blockfrost, WebWallet> | undefined = $state();
const weld = getWeldContext();
const displayedBalance = $derived(weld.wallet.balanceAda?.toFixed(2) ?? '-');
let blockfrostAPIKey = $state('');
let tokenName = $state('');
let giftADA: string | undefined = $state();
let lockTxHash: string | undefined = $state();
let unlockTxHash: string | undefined = $state();
let parameterizedContracts: AppliedValidators | undefined = $state();
async function setupBlaze(e: Event) {
let waitingLockTx = $state(false);
let waitingUnlockTx = $state(false);
let lucid: LucidEvolution | undefined = $state();
onMount(() => {
weld.wallet.connect('eternl');
});
async function setupBlockfrost(e: Event) {
e.preventDefault();
const walletApi = await window.cardano?.eternl.enable();
lucid = await Lucid(
new Blockfrost(
'https://cardano-preprod.blockfrost.io/api/v0',
blockfrostAPIKey
),
'Preprod'
);
if (walletApi) {
blaze = await Blaze.from(
new Blockfrost({ network: 'cardano-preview', projectId: blockfrostAPIKey }),
new WebWallet(walletApi)
);
const cache = localStorage.getItem('cache');
if (cache) {
const localCache: LocalCache = JSON.parse(cache);
tokenName = localCache.tokenName;
giftADA = localCache.giftADA;
parameterizedContracts = localCache.parameterizedValidators;
lockTxHash = localCache.lockTxHash;
}
// @ts-expect-error this is normal
lucid.selectWallet.fromAPI(weld.wallet.handler!.enabledApi);
}
async function submitTokenName(e: Event) {
e.preventDefault();
const utxos = await lucid!.wallet().getUtxos()!;
const utxo = utxos[0];
const outputReference = {
txHash: utxo.txHash,
outputIndex: utxo.outputIndex
};
const contracts = applyParams(tokenName, outputReference, data.validator);
parameterizedContracts = contracts;
}
async function createGiftCard(e: Event) {
e.preventDefault();
waitingLockTx = true;
try {
const lovelace = Number(giftADA) * 1000000;
const assetName = `${parameterizedContracts!.policyId}${fromText(
tokenName
)}`;
// Action::Mint
const mintRedeemer = Data.to(new Constr(0, []));
const utxos = await lucid!.wallet().getUtxos()!;
const utxo = utxos[0];
const tx = await lucid!
.newTx()
.collectFrom([utxo])
.attach.MintingPolicy(parameterizedContracts!.giftCard)
.mintAssets({ [assetName]: BigInt(1) }, mintRedeemer)
.pay.ToContract(
parameterizedContracts!.lockAddress,
{ kind: 'inline', value: Data.void() },
{ lovelace: BigInt(lovelace) }
)
.complete();
const txSigned = await tx.sign.withWallet().complete();
const txHash = await txSigned.submit();
const success = await lucid!.awaitTx(txHash);
// Wait a little bit longer so ExhaustedUTxOError doesn't happen
// in the next Tx
setTimeout(() => {
waitingLockTx = false;
if (success) {
localStorage.setItem(
'cache',
JSON.stringify({
tokenName,
giftADA,
parameterizedValidators: parameterizedContracts,
lockTxHash: txHash
})
);
lockTxHash = txHash;
}
}, 3000);
} catch {
waitingLockTx = false;
}
}
function submitTokenName(e: Event) {
async function redeemGiftCard(e: Event) {
e.preventDefault();
console.log('TODO: apply params to raw validators');
waitingUnlockTx = true;
try {
const utxos = await lucid!.utxosAt(parameterizedContracts!.lockAddress);
const assetName = `${parameterizedContracts!.policyId}${fromText(
tokenName
)}`;
// Action::Burn
const burnRedeemer = Data.to(new Constr(1, []));
const tx = await lucid!
.newTx()
.collectFrom(utxos, Data.void())
.attach.MintingPolicy(parameterizedContracts!.giftCard)
.attach.SpendingValidator(parameterizedContracts!.redeem)
.mintAssets({ [assetName]: BigInt(-1) }, burnRedeemer)
.complete();
const txSigned = await tx.sign.withWallet().complete();
const txHash = await txSigned.submit();
const success = await lucid!.awaitTx(txHash);
waitingUnlockTx = false;
if (success) {
localStorage.removeItem('cache');
unlockTxHash = txHash;
}
} catch {
waitingUnlockTx = false;
}
}
</script>
@@ -48,29 +201,100 @@
<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>
<h2 class="text-lg font-semibold text-gray-900">
Make a one shot minting and lock contract
</h2>
<h3 class="mb-2 mt-4">Gift Card</h3>
<pre class="overflow-x-scroll rounded bg-gray-200 p-2">{data.validator}</pre>
balance: {displayedBalance}
<h3 class="mb-2 mt-4">Gift Card Template</h3>
<pre
class="overflow-x-scroll rounded bg-gray-200 p-2">{data.validator}</pre>
</div>
<div>
{#if blaze}
<form class="mt-10 grid grid-cols-1 gap-y-8" onsubmit={setupBlaze}>
<Input type="password" id="blockfrostAPIKey" bind:value={blockfrostAPIKey}>
{#if !lucid}
<form class="mt-10 grid grid-cols-1 gap-y-8" onsubmit={setupBlockfrost}>
<Input
type="password"
id="blockfrostAPIKey"
bind:value={blockfrostAPIKey}
>
Blockfrost API Key
</Input>
<Button type="submit">Setup Blaze</Button>
<Button type="submit">Setup Wallet</Button>
</form>
{:else}
<form class="mt-10 grid grid-cols-1 gap-y-8" onsubmit={submitTokenName}>
<Input type="text" name="tokenName" id="tokenName" bind:value={tokenName}>Token Name</Input>
<Input
type="text"
name="tokenName"
id="tokenName"
bind:value={tokenName}>Token Name</Input
>
{#if tokenName.length > 0}
<Button type="submit">Make Contracts</Button>
{/if}
</form>
{/if}
{#if lucid && parameterizedContracts}
<h3 class="mb-2 mt-4">New Gift Card</h3>
<pre
class="overflow-x-scroll rounded bg-gray-200 p-2">{parameterizedContracts
.redeem.script}</pre>
<div class="mt-10 grid grid-cols-1 gap-y-8">
<form onsubmit={createGiftCard}>
<Input type="text" name="giftADA" id="giftADA" bind:value={giftADA}>
ADA Amount
</Input>
<Button type="submit" disabled={waitingLockTx || !!lockTxHash}>
{#if waitingLockTx}
Waiting for Tx...
{:else}
Create Gift Card (Locks ADA)
{/if}
</Button>
</form>
{#if lockTxHash}
<h3 class="mb-2 mt-4">ADA Locked</h3>
<a
class="mb-2"
target="_blank"
href={`https://preprod.cardanoscan.io/transaction/${lockTxHash}`}
>
{lockTxHash}
</a>
<form onsubmit={redeemGiftCard}>
<Button type="submit" disabled={waitingLockTx || !!unlockTxHash}>
{#if waitingUnlockTx}
Waiting for Tx...
{:else}
Redeem Gift Card (Unlocks ADA)
{/if}
</Button>
</form>
{/if}
{#if unlockTxHash}
<h3 class="mb-2 mt-4">ADA Unlocked</h3>
<a
class="mb-2"
target="_blank"
href={`https://preprod.cardanoscan.io/transaction/${unlockTxHash}`}
>
{unlockTxHash}
</a>
{/if}
</div>
{/if}
</div>
</div>