diff --git a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue index 9a1d92c7e4ca026786e19ec61185476dc5766be0..3aa61432eff2d79c4d87075edf81be4f5d3bc257 100644 --- a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue +++ b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue @@ -1,6 +1,8 @@ <script setup lang="ts"> import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' import { ref, onMounted } from 'vue'; import step1 from "@/assets/icons/wallets/metamask/step1.webp"; import step2 from "@/assets/icons/wallets/metamask/step2.webp"; @@ -11,11 +13,25 @@ import { Combobox, ComboboxAnchor, ComboboxTrigger, ComboboxEmpty, ComboboxGroup import { Check, Search } from 'lucide-vue-next'; import { Separator } from '@/components/ui/separator'; import PublicKey from '@/components/hive/PublicKey.vue'; +import { Checkbox } from '@/components/ui/checkbox' import { getWax } from '@/stores/wax.store'; +import { AccountAuthorityUpdateOperation, type TRole } from "@hiveio/wax/vite"; const emit = defineEmits(["setaccount", "close"]); +const showUpdateAccountModal = ref(false); +const showCreateAccountModal = ref(false); + +const updateAuthType: Record<TRole, boolean> = { + owner: true, + active: true, + posting: true, + memo: true +}; + const close = () => { + showUpdateAccountModal.value = false; + showCreateAccountModal.value = false; emit("close"); }; @@ -24,6 +40,8 @@ const metamaskStore = useMetamaskStore(); const isLoading = ref(false); const errorMsg = ref<string | null>(null); const accountName = ref<string | null>(null); +const createAccountNameOperation = ref<string | null>(null); +const updateAccountNameOperation = ref<string | null>(null); const accountsMatchingKeys = ref<string[] | null>(null); const metamaskPublicKeys = ref<Array<{ role: string; publicKey: string }> | null>(null); const isMetamaskConnected = ref<boolean>(false); @@ -110,8 +128,39 @@ onMounted(() => { void connect(false); }); -const gotoSignTx = () => { - window.open(`${window.location.protocol}//${window.location.host}/sign/?tx=`, '_blank')!.focus(); +const copyContent = (content: string) => { + navigator.clipboard.writeText(String(content)); +}; +const generateAccountUpdateTransaction = async(): Promise<string> => { + const wax = await getWax(); + const tx = await wax.createTransaction(); + const accountName = updateAccountNameOperation.value!.startsWith('@') ? updateAccountNameOperation.value!.slice(1) : updateAccountNameOperation.value!; + const op = await AccountAuthorityUpdateOperation.createFor(wax, accountName); + for(const key in updateAuthType) { + if (updateAuthType[key as TRole]) + if (key === "memo") + op.role("memo").set(metamaskPublicKeys.value!.find(node => node.role === key)!.publicKey); + else + op.role(key as Exclude<TRole, "memo">).add(metamaskPublicKeys.value!.find(node => node.role === key)!.publicKey); + } + tx.pushOperation(op); + return tx.toApi(); +}; +const getAccountCreateSigningLink = async () => { + const accountName = createAccountNameOperation.value!.startsWith('@') ? createAccountNameOperation.value!.slice(1) : createAccountNameOperation.value!; + return `${window.location.protocol}//${window.location.host}/account/create?acc=${accountName}&posting=${ + metamaskPublicKeys.value!.find(node => node.role === "posting")!.publicKey + }&active=${ + metamaskPublicKeys.value!.find(node => node.role === "active")!.publicKey + }&owner=${ + metamaskPublicKeys.value!.find(node => node.role === "owner")!.publicKey + }&memo=${ + metamaskPublicKeys.value!.find(node => node.role === "memo")!.publicKey + }`; +}; +const getSigningLink = async () => { + const tx = await generateAccountUpdateTransaction(); + return `${window.location.protocol}//${window.location.host}/sign/transaction?data=${btoa(tx)}`; }; const updateAccountName = (value: string | any) => { @@ -165,7 +214,54 @@ const updateAccountName = (value: string | any) => { </div> <div v-if="isMetamaskSnapInstalled"> <div v-if="accountsMatchingKeys"> - <div v-if="accountsMatchingKeys.length === 0"> + <div v-if="showUpdateAccountModal"> + <p class="mb-4">Step 6: Fill in this form in order to create account update operation, replacing memo public key and adding posting, active and owner keys to your account:</p> + <div class="grid mb-2 w-full max-w-sm items-center gap-1.5"> + <Label for="metamask_updateAuth_account">Account name</Label> + <Input v-model="updateAccountNameOperation as string" id="metamask_updateAuth_account" /> + </div> + <div v-for="key in metamaskPublicKeys" :key="key.publicKey"> + <div class="flex items-center p-1"> + <Checkbox :id="`metamask_updateAuth_key-${key.role}`" :defaultValue="true" @update:modelValue="value => { updateAuthType[key.role as TRole] = value as boolean }" /> + <label :for="`metamask_updateAuth_key-${key.role}`" class="pl-2 w-full flex items-center"> + <span class="font-bold">{{ key.role[0].toUpperCase() }}{{ key.role.slice(1) }}</span> + <div class="mx-2 border flex-grow border-[hsl(var(--foreground))] opacity-[0.1]" /> + <PublicKey :value="key.publicKey"/> + </label> + </div> + </div> + <div class="flex items-center flex-col"> + <Button :disabled="isLoading" @click="getSigningLink().then(copyContent)" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> + <span class="text-md font-bold">Copy signing link</span> + </Button> + <Separator label="Or" class="mt-8" /> + <div class="flex justify-center mt-4"> + <Button :disabled="isLoading" @click="generateAccountUpdateTransaction().then(copyContent)" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> + <span class="text-md font-bold">Copy entire transaction</span> + </Button> + </div> + </div> + </div> + <div v-else-if="showCreateAccountModal"> + <p class="mb-4">Step 6: Fill in this form in order to create account create operation with requested metadata. Copy the signing link and send it to someone who already has an account:</p> + <div class="grid mb-2 w-full max-w-sm items-center gap-1.5"> + <Label for="metamask_createAuth_account">New account name</Label> + <Input v-model="createAccountNameOperation as string" id="metamask_createAuth_account" /> + </div> + <div v-for="key in metamaskPublicKeys" :key="key.publicKey"> + <div class="flex items-center p-1"> + <span class="font-bold">{{ key.role[0].toUpperCase() }}{{ key.role.slice(1) }}</span> + <div class="mx-2 border flex-grow border-[hsl(var(--foreground))] opacity-[0.1]" /> + <PublicKey :value="key.publicKey"/> + </div> + </div> + <div class="flex items-center flex-col"> + <Button :disabled="isLoading" @click="getAccountCreateSigningLink().then(copyContent)" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> + <span class="text-md font-bold">Copy signing link</span> + </Button> + </div> + </div> + <div v-else-if="accountsMatchingKeys.length === 0"> <p class="mb-2">Step 5: Import <b>at least one</b> Metamask derived key into your Hive account and re-check for Hive Accounts matching those keys:</p> <div v-for="key in metamaskPublicKeys" :key="key.publicKey"> <div class="flex items-center p-1"> @@ -181,12 +277,12 @@ const updateAccountName = (value: string | any) => { </div> <Separator label="Or" class="mt-8" /> <div class="flex justify-center mt-4"> - <Button :disabled="isLoading" @click="gotoSignTx" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> + <Button :disabled="isLoading" @click="showUpdateAccountModal = true" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Update account authority</span> </Button> </div> <div class="flex justify-center mt-4"> - <Button :disabled="isLoading" @click="gotoSignTx" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> + <Button :disabled="isLoading" @click="showCreateAccountModal = true" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Request account creation</span> </Button> </div> diff --git a/src/components/ui/checkbox/Checkbox.vue b/src/components/ui/checkbox/Checkbox.vue new file mode 100644 index 0000000000000000000000000000000000000000..8eb8a185eff9d217d1f0e2b9d14a1cab8c4e6838 --- /dev/null +++ b/src/components/ui/checkbox/Checkbox.vue @@ -0,0 +1,33 @@ +<script setup lang="ts"> +import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui' +import { cn } from '@/lib/utils' +import { Check } from 'lucide-vue-next' +import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui' +import { computed, type HTMLAttributes } from 'vue' + +const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>() +const emits = defineEmits<CheckboxRootEmits>() + +const delegatedProps = computed(() => { + const { class: _, ...delegated } = props + + return delegated +}) + +const forwarded = useForwardPropsEmits(delegatedProps, emits) +</script> + +<template> + <CheckboxRoot + v-bind="forwarded" + :class=" + cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', + props.class)" + > + <CheckboxIndicator class="flex h-full w-full items-center justify-center text-current"> + <slot> + <Check class="h-4 w-4" /> + </slot> + </CheckboxIndicator> + </CheckboxRoot> +</template> diff --git a/src/components/ui/checkbox/index.ts b/src/components/ui/checkbox/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c28c2864b73b98063c383028c9a57e483afc719 --- /dev/null +++ b/src/components/ui/checkbox/index.ts @@ -0,0 +1 @@ +export { default as Checkbox } from './Checkbox.vue' diff --git a/src/components/ui/copybutton/Button.vue b/src/components/ui/copybutton/Button.vue index dd7cbb4bcedd3a52a168da1cbb066b71e5a48706..6694833d4dfc5257aae85a0608e77e29f9710e74 100644 --- a/src/components/ui/copybutton/Button.vue +++ b/src/components/ui/copybutton/Button.vue @@ -32,10 +32,12 @@ const copyBtn = (event: MouseEvent) => { <Primitive :as="as" :as-child="asChild" - :class="cn(buttonVariants(), props.class, 'px-2')" + :class="cn(buttonVariants(), 'px-2', props.class)" @click="copyBtn" :data-copy="props.value" > - <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill: hsl(var(--foreground))" :d="mdiContentCopy"/></svg> + <slot> + <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path style="fill: hsl(var(--foreground))" :d="mdiContentCopy"/></svg> + </slot> </Primitive> </template> diff --git a/src/components/utilcards/ConfirmCreateAccountCard.vue b/src/components/utilcards/ConfirmCreateAccountCard.vue index 6a4cf41e03b38f243b933ebe155ee4c86539bf1b..a245c2f7715e85192dc5c1b2028e06a4dc964dcd 100644 --- a/src/components/utilcards/ConfirmCreateAccountCard.vue +++ b/src/components/utilcards/ConfirmCreateAccountCard.vue @@ -3,7 +3,69 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { mdiAccountPlusOutline } from '@mdi/js'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; -import { Input } from '@/components/ui/input'; +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { useSettingsStore } from '@/stores/settings.store'; +import { onMounted, ref } from 'vue'; +import { useRouter } from 'vue-router'; +import { getWax } from '@/stores/wax.store'; +import { useWalletStore } from '@/stores/wallet.store'; + +const settings = useSettingsStore(); + +const creator = ref<string>(''); +const accountName = ref<string>(''); +const memoKey = ref<string>(''); +const postingKey = ref<string>(''); +const activeKey = ref<string>(''); +const ownerKey = ref<string>(''); +const postingMetadata = ref<string>('{}'); + +const router = useRouter(); +const wallet = useWalletStore(); + +onMounted(() => { + creator.value = settings.account ? `@${settings.account}` : ''; + + accountName.value = router.currentRoute.value.query.acc as string ?? ''; + memoKey.value = router.currentRoute.value.query.memo as string ?? ''; + postingKey.value = router.currentRoute.value.query.posting as string ?? ''; + activeKey.value = router.currentRoute.value.query.active as string ?? ''; + ownerKey.value = router.currentRoute.value.query.owner as string ?? ''; +}); + +const createAccount = async() => { + const wax = await getWax(); + const tx = await wax.createTransaction(); + const { median_props: { account_creation_fee } } = await wax.api.database_api.get_witness_schedule({}); + tx.pushOperation({ + account_create: { + creator: settings.account!, + new_account_name: accountName.value, + memo_key: memoKey.value, + owner: { + weight_threshold: 1, + key_auths: {[ownerKey.value]: 1}, + account_auths: {} + }, + active: { + weight_threshold: 1, + key_auths: {[activeKey.value]: 1}, + account_auths: {} + }, + posting: { + weight_threshold: 1, + key_auths: {[postingKey.value]: 1}, + account_auths: {} + }, + json_metadata: postingMetadata.value, + fee: account_creation_fee + } + }); + const signature = await wallet.wallet!.signTransaction(tx, "active"); + tx.sign(signature); + await wax.broadcast(tx); +} </script> <template> @@ -16,14 +78,36 @@ import { Input } from '@/components/ui/input'; <CardDescription class="mr-8">Use this module to process account creation request sent by other users</CardDescription> </CardHeader> <CardContent> - <div class="my-4"> - <Input placeholder="Account username" class="my-2" disabled/> - <Input placeholder="Memo key" class="my-2" disabled/> - <Input placeholder="Posting key" class="my-2" disabled/> - <Input placeholder="Active key" class="my-2" disabled/> - <Input placeholder="Owner key" class="my-2" disabled/> - <Textarea placeholder="Posting metadata" class="my-2" disabled/> - <Button class="my-2">Create account</Button> + <div class="my-4 space-y-2"> + <div class="grid w-full max-w-sm items-center"> + <Label for="createAccount_creator">Account name</Label> + <Input id="createAccount_creator" v-model="creator" class="my-2" disabled /> + </div> + <div class="grid w-full max-w-sm items-center"> + <Label for="createAccount_accountName">Account Name</Label> + <Input id="createAccount_accountName" v-model="accountName" class="my-2" /> + </div> + <div class="grid w-full max-w-sm items-center"> + <Label for="createAccount_memoKey">Memo Key</Label> + <Input id="createAccount_memoKey" v-model="memoKey" class="my-2" /> + </div> + <div class="grid w-full max-w-sm items-center"> + <Label for="createAccount_postingKey">Posting Key</Label> + <Input id="createAccount_postingKey" v-model="postingKey" class="my-2" /> + </div> + <div class="grid w-full max-w-sm items-center"> + <Label for="createAccount_activeKey">Active Key</Label> + <Input id="createAccount_activeKey" v-model="activeKey" class="my-2" /> + </div> + <div class="grid w-full max-w-sm items-center"> + <Label for="createAccount_ownerKey">Owner Key</Label> + <Input id="createAccount_ownerKey" v-model="ownerKey" class="my-2" /> + </div> + <div class="grid w-full max-w-sm items-center"> + <Label for="createAccount_postingMetadata">Posting Metadata</Label> + <Textarea id="createAccount_postingMetadata" v-model="postingMetadata" class="my-2" /> + </div> + <Button class="my-2" @click="createAccount">Create account</Button> </div> </CardContent> </Card> diff --git a/src/stores/wax.store.ts b/src/stores/wax.store.ts index 39366133affe27391e65d2b946bf16468138d441..0ebd22bffe087242ad9ccd68326a5b66c1bf606f 100644 --- a/src/stores/wax.store.ts +++ b/src/stores/wax.store.ts @@ -1,10 +1,23 @@ -import { createHiveChain, type IHiveChainInterface } from "@hiveio/wax/vite"; +import { createHiveChain, type TWaxExtended, type asset } from "@hiveio/wax/vite"; -let chain: IHiveChainInterface; +export interface WaxApi { + database_api: { + get_witness_schedule: { + params: {}; + result: { + median_props: { + account_creation_fee: asset; + }; + }; + }; + }; +}; + +let chain: TWaxExtended<WaxApi>; -export const getWax = async(): Promise<IHiveChainInterface> => { +export const getWax = async() => { if (!chain) - chain = await createHiveChain(); + chain = (await createHiveChain()).extend<WaxApi>(); return chain; };