From 84f1dfdfe932ab9eb758e97073ae30331e2babba Mon Sep 17 00:00:00 2001
From: mtyszczak <mateusz.tyszczak@gmail.com>
Date: Wed, 12 Mar 2025 14:13:33 +0100
Subject: [PATCH] Allow account creation

---
 .../wallets/metamask/MetamaskConnect.vue      | 106 +++++++++++++++++-
 src/components/ui/checkbox/Checkbox.vue       |  33 ++++++
 src/components/ui/checkbox/index.ts           |   1 +
 src/components/ui/copybutton/Button.vue       |   6 +-
 .../utilcards/ConfirmCreateAccountCard.vue    | 102 +++++++++++++++--
 src/stores/wax.store.ts                       |  21 +++-
 6 files changed, 249 insertions(+), 20 deletions(-)
 create mode 100644 src/components/ui/checkbox/Checkbox.vue
 create mode 100644 src/components/ui/checkbox/index.ts

diff --git a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue
index 9a1d92c..3aa6143 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 0000000..8eb8a18
--- /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 0000000..8c28c28
--- /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 dd7cbb4..6694833 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 6a4cf41..a245c2f 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 3936613..0ebd22b 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;
 };
-- 
GitLab