feat: convert gift card tutorial to lucid-evolution and weld
This commit is contained in:
36
examples/gift_card/src/app.d.ts
vendored
36
examples/gift_card/src/app.d.ts
vendored
@@ -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 {};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
43
examples/gift_card/src/lib/wallet.svelte.ts
Normal file
43
examples/gift_card/src/lib/wallet.svelte.ts
Normal 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);
|
||||
}
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user