-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Appv2 faucet #4189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Appv2 faucet #4189
Changes from all commits
59e9a40
7d79f93
8977b47
7d29526
dc2e8a2
e0b649b
421cb70
38c47fc
4de4f30
90ee13f
6feb433
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { graphql } from "gql.tada" | ||
import { createQueryGraphql } from "$lib/utils/queries" | ||
import { Schema } from "effect" | ||
import { URLS } from "$lib/constants" | ||
|
||
export const faucetUnoMutationDocument = graphql(` | ||
mutation UnoFaucetMutation( | ||
$chainId: String!, | ||
$denom: String!, | ||
$address: String!, | ||
$captchaToken: String! | ||
) { | ||
drip_drop { | ||
send( | ||
chainId: $chainId, | ||
denom: $denom, | ||
address: $address, | ||
captchaToken: $captchaToken | ||
) | ||
} | ||
} | ||
`) | ||
|
||
export const faucetUnoMutation = ({ | ||
chainId, | ||
denom, | ||
address, | ||
captchaToken | ||
}: { | ||
chainId: string | ||
denom: string | ||
address: string | ||
captchaToken: string | ||
}) => | ||
createQueryGraphql({ | ||
schema: Schema.Struct({ send: Schema.String }), | ||
document: faucetUnoMutationDocument, | ||
variables: { chainId, denom, address, captchaToken }, | ||
url: URLS().GRAPHQL | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,39 +3,225 @@ import Button from "$lib/components/ui/Button.svelte" | |
import Card from "$lib/components/ui/Card.svelte" | ||
import SharpWalletIcon from "$lib/components/icons/SharpWalletIcon.svelte" | ||
import AddressComponent from "$lib/components/model/AddressComponent.svelte" | ||
import { Option } from "effect" | ||
|
||
import { Option, Effect, Data, pipe } from "effect" | ||
import type { NoSuchElementException } from "effect/Cause" | ||
import { wallets } from "$lib/stores/wallets.svelte.ts" | ||
import { chains } from "$lib/stores/chains.svelte.ts" | ||
import AngleArrowIcon from "$lib/components/icons/AngleArrowIcon.svelte" | ||
import { Turnstile } from "svelte-turnstile" | ||
import request from "graphql-request" | ||
import { writable } from "svelte/store" | ||
import { faucetUnoMutationDocument } from "$lib/queries/faucet" | ||
import { URLS } from "$lib/constants" | ||
import { bech32, bytes } from "@scure/base" | ||
import { extractErrorDetails } from "@unionlabs/sdk/utils" | ||
|
||
// Define the faucet state type using Data.TaggedEnum. | ||
type FaucetProcessState = Data.TaggedEnum<{ | ||
Idle: {} | ||
Verifying: {} | ||
Verified: { token: string } | ||
Submitting: { token: string } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specify what the type of schema is of token by using one of the types from |
||
Success: { message: string } | ||
Failure: { error: string } | ||
}> | ||
|
||
// Create the tagged enum instance. | ||
const FaucetProcess = Data.taggedEnum<FaucetProcessState>() | ||
|
||
// Initialize the faucet process state to Idle. | ||
const faucetProcess = writable<FaucetProcessState>(FaucetProcess.Idle()) | ||
|
||
// Variables for managing the Turnstile component. | ||
let resetTurnstile: () => void | ||
let showTurnstile = false | ||
|
||
// When the user clicks "Claim", trigger verification. | ||
// For now, we bypass verification and use a dummy token. | ||
const startVerification = () => { | ||
faucetProcess.set(FaucetProcess.Verifying()) | ||
showTurnstile = true | ||
resetTurnstile?.() // resets/retriggers the Turnstile if available | ||
} | ||
|
||
// Callback for successful Turnstile captcha. | ||
const handleTurnstileCallback = ( | ||
e: CustomEvent<{ token: string; preClearanceObtained: boolean }> | ||
) => { | ||
const token = e.detail.token | ||
faucetProcess.set(FaucetProcess.Verified({ token })) | ||
// Immediately submit the faucet request. | ||
submitFaucetRequest(token) | ||
} | ||
|
||
// Callback for a Turnstile error. | ||
const handleTurnstileError = (e: CustomEvent<{ code: string }>) => { | ||
faucetProcess.set(FaucetProcess.Failure({ error: `Verification error: ${e.detail.code}` })) | ||
showTurnstile = false | ||
} | ||
Comment on lines
+58
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we only get a code and no other data? best to always propagate all error data all the way upwards, so that we get full context in the app if there is any error There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://github.com/ghostdevv/svelte-turnstile?tab=readme-ov-file#events according to this page we only get code on error scenerio |
||
|
||
class Bech32DecodeError extends Data.TaggedError("Bech32DecodeError")<{ | ||
message: string | ||
cause: unknown | ||
}> {} | ||
|
||
class FetchWalletError extends Data.TaggedError("FetchWalletError")<{ | ||
message: string | ||
cause: unknown | ||
}> {} | ||
|
||
const faucetEffect = ( | ||
token: string | ||
): Effect.Effect< | ||
void, | ||
Bech32DecodeError | Error | FetchWalletError | NoSuchElementException, | ||
never | ||
> => | ||
Effect.gen(function* () { | ||
const addr = yield* wallets.cosmosAddress | ||
|
||
const encodeAddr = Effect.try({ | ||
try: () => { | ||
const hex = addr.slice(2) | ||
const wordArray = bech32.toWords(bytes("hex", hex)) | ||
return bech32.encode("union", wordArray) | ||
}, | ||
catch: cause => | ||
new Bech32DecodeError({ | ||
message: `Failed to prepare wallet address: ${cause}`, | ||
cause: extractErrorDetails(cause as Error) | ||
}) | ||
}) | ||
|
||
const fetchWallet = (addr: string) => | ||
Effect.tryPromise({ | ||
try: () => | ||
request(URLS().GRAPHQL, faucetUnoMutationDocument, { | ||
chainId: "union-testnet-10", | ||
denom: "muno", | ||
address: addr, | ||
captchaToken: token | ||
}), | ||
catch: cause => | ||
new FetchWalletError({ | ||
message: `graphql request failed to fetch wallet with addr ${addr}`, | ||
cause: extractErrorDetails(cause) | ||
}) | ||
}) | ||
|
||
const result = yield* pipe(encodeAddr, Effect.flatMap(fetchWallet)) | ||
|
||
if (!result.drip_drop || !result.drip_drop.send) { | ||
return yield* Effect.fail(new Error("Empty faucet response")) | ||
} | ||
if (result.drip_drop.send.startsWith("ERROR")) { | ||
return yield* Effect.fail(new Error(`Error from faucet: ${result.drip_drop.send}`)) | ||
} | ||
|
||
yield* Effect.log("Faucet response:", result) | ||
|
||
// Wrap the store update and other side effects in Effect.sync | ||
return yield* Effect.sync(() => { | ||
faucetProcess.set(FaucetProcess.Success({ message: result.drip_drop.send })) | ||
showTurnstile = false | ||
}) | ||
}) | ||
|
||
const submitFaucetRequest = (token: string) => { | ||
// Update the state to "Submitting" before starting the effect | ||
faucetProcess.set(FaucetProcess.Submitting({ token })) | ||
|
||
Effect.runPromise(faucetEffect(token)).catch(error => { | ||
console.info("Faucet error:", error) | ||
faucetProcess.set(FaucetProcess.Failure({ error: `Faucet error: ${error}` })) | ||
showTurnstile = false | ||
}) | ||
} | ||
|
||
// Reset the faucet process to allow a new request. | ||
const resetProcess = () => { | ||
faucetProcess.set(FaucetProcess.Idle()) | ||
showTurnstile = false | ||
} | ||
</script> | ||
|
||
<Card divided class="self-center"> | ||
<div class="p-4 flex gap-1 "> | ||
<div class="p-4 flex gap-1"> | ||
<h2>UNO Faucet</h2> | ||
</div> | ||
{#if Option.isSome(chains.data)} | ||
{@const unionTestnet10 = Option.fromNullable(chains.data.value.find(c => c.universal_chain_id === "union.union-testnet-10"))} <div class="flex flex-col gap-4 p-4"> | ||
{@const unionTestnet10 = Option.fromNullable( | ||
chains.data.value.find( | ||
(c) => c.universal_chain_id === "union.union-testnet-10", | ||
), | ||
)} | ||
<div class="flex flex-col gap-4 p-4"> | ||
<div> | ||
<p>Official faucet for the UNO testnet token.</p> | ||
<p>This faucet is protected by CloudFlare Turnstile.</p> | ||
<p>You can use this faucet once a day.</p> | ||
</div> | ||
<div> | ||
<div class="flex items-center mr-5 text-zinc-400 justify-self-end"> | ||
{#if Option.isSome(wallets.cosmosAddress) && Option.isSome(unionTestnet10)} | ||
<p class="text-xs mb-2"> | ||
<AddressComponent truncate address={wallets.cosmosAddress.value} chain={unionTestnet10.value}/> | ||
</p> | ||
{:else} | ||
<p class="text-xs mb-2"> No receiver</p> | ||
<div class="flex items-center mr-5 text-zinc-400 justify-self-end"> | ||
{#if Option.isSome(wallets.cosmosAddress) && Option.isSome(unionTestnet10)} | ||
<p class="text-xs mb-2"> | ||
<AddressComponent | ||
truncate | ||
address={wallets.cosmosAddress.value} | ||
chain={unionTestnet10.value} | ||
/> | ||
</p> | ||
{:else} | ||
<p class="text-xs mb-2">No receiver</p> | ||
{/if} | ||
<AngleArrowIcon class="rotate-270" /> | ||
</div> | ||
{#if $faucetProcess._tag === "Idle"} | ||
<div class="flex gap-4"> | ||
<Button | ||
onclick={startVerification} | ||
class="flex-1" | ||
disabled={!Option.isSome(wallets.cosmosAddress)}>Claim</Button | ||
> | ||
<Button><SharpWalletIcon class="size-5" /></Button> | ||
</div> | ||
{:else if $faucetProcess._tag === "Verifying"} | ||
<div class="flex flex-col items-center"> | ||
<p class="text-xs">Verifying, please complete captcha...</p> | ||
{#if showTurnstile} | ||
<Turnstile | ||
siteKey="0x4AAAAAAA-eVs5k0b8Q1dl5" | ||
on:callback={handleTurnstileCallback} | ||
on:error={handleTurnstileError} | ||
theme="auto" | ||
size="normal" | ||
bind:reset={resetTurnstile} | ||
/> | ||
{/if} | ||
</div> | ||
{:else if $faucetProcess._tag === "Submitting"} | ||
<div class="flex flex-col items-center"> | ||
<p class="text-xs">Submitting faucet request...</p> | ||
</div> | ||
{:else if $faucetProcess._tag === "Success"} | ||
<div class="flex flex-col items-center"> | ||
<p class="text-xs">Tokens sent! Transaction hash:</p> | ||
<p class="text-xs break-all"> | ||
<a | ||
href={`https://explorer.testnet-10.union.build/union/tx/${$faucetProcess.message}`} | ||
target="_blank" | ||
> | ||
{$faucetProcess.message} | ||
</a> | ||
</p> | ||
<Button onclick={resetProcess} class="mt-2">New Request</Button> | ||
</div> | ||
{:else if $faucetProcess._tag === "Failure"} | ||
<div class="flex flex-col items-center"> | ||
<p class="text-xs text-red-500">Error: {$faucetProcess.error}</p> | ||
<Button onclick={resetProcess} class="mt-2">Retry</Button> | ||
</div> | ||
{/if} | ||
<AngleArrowIcon class="rotate-270"/> | ||
</div> | ||
<div class="flex gap-4"> | ||
<Button class="flex-1">Claim</Button> | ||
<Button><SharpWalletIcon class="size-5"/></Button> | ||
</div> | ||
</div> | ||
</div> | ||
{/if} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does this mean?