Skip to content

Commit

Permalink
feat(merge): Add FT merge module
Browse files Browse the repository at this point in the history
  • Loading branch information
AricRedemption committed Apr 11, 2024
1 parent 8b102fd commit 5e5a588
Show file tree
Hide file tree
Showing 16 changed files with 403 additions and 47 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"decimal.js": "^10.4.3",
"ecpair": "^2.1.0",
"memfs": "^4.5.0",
"meta-contract": "^0.4.2",
"meta-contract": "^0.4.4",
"mvc-std-lib": "^1.1.4",
"object-hash": "^3.0.0",
"qrcode": "^1.5.3",
Expand Down
2 changes: 1 addition & 1 deletion src/assets/icons/loading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions src/components/Avatar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { computed } from 'vue'
const { id } = defineProps<{
id: string
}>()
const colors = [
'from-blue-100 to-blue-500',
'from-green-100 to-green-500',
'from-yellow-100 to-yellow-500',
'from-red-100 to-red-500',
'from-pink-100 to-pink-500',
'from-purple-100 to-purple-500',
'from-indigo-100 to-indigo-500',
]
const computedGradientColor = computed(() => {
const index = Math.abs(id.split('').reduce((prev, curr) => prev + curr.charCodeAt(0), 0)) % colors.length
return colors[index]
})
</script>

<!-- Avatar.vue -->
<template>
<div :class="['h-9 w-9 shrink-0 rounded-full bg-gradient-to-br', computedGradientColor]"></div>
</template>
4 changes: 2 additions & 2 deletions src/components/Copy.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ const { text } = defineProps<{
@click="ClipboardStore.copy(text)"
:class="
twMerge(
'hover:text-blue-primary cursor-pointer',
'hover:text-primary-blue cursor-pointer',
$attrs.class as string,
ClipboardStore.copiedText === text ? 'text-blue-primary' : 'text-gray-primary'
ClipboardStore.copiedText === text ? 'text-primary-blue' : 'text-gray-primary'
)
"
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/LoadingIcon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import LoadingIcon from '@/assets/icons/loading.svg'
</script>

<template>
<LoadingIcon :class="twMerge('h-5 w-5 text-gray-primary', $attrs.class as string)" />
<LoadingIcon :class="twMerge('h-5 w-5 text-gray-primary animate-spin', $attrs.class as string)" />
</template>
8 changes: 2 additions & 6 deletions src/pages/nfts/MetaPinDetail.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { formatTimestamp } from '@/lib/formatters'
Expand All @@ -21,10 +20,7 @@ const toSendNFT = (id: string) => {
query: {
satoshis: metaPin.value?.outputValue,
content: metaPin.value?.contentSummary,
imgUrl:
metaPin.value?.contentType === 'image/jpeg'
? `https://man-test.metaid.io${metaPin.value?.contentSummary}`
: undefined,
imgUrl: metaPin.value?.contentType === 'image/jpeg' ? metaPin.value?.content : undefined,
},
})
}
Expand Down Expand Up @@ -108,7 +104,7 @@ const toSendNFT = (id: string) => {
</div>
</div>

<button @click="toSendNFT(metaPin.id)" class="main-btn-bg w-full rounded-lg py-3 text-sm text-sky-100">
<button @click="toSendNFT(metaPin!.id)" class="main-btn-bg w-full rounded-lg py-3 text-sm text-sky-100">
Transfers
</button>
</div>
Expand Down
8 changes: 4 additions & 4 deletions src/pages/settings/Toolkit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const toSpaceMerge = () => {
<div class="space-y-8 pt-4 text-sm">
<!-- security -->
<div class="space-y-2">
<div class="text-base ">BTC</div>
<div class="text-base">BTC</div>
<div class="divide-y divide-gray-100">
<div class="setting-item group cursor-pointer" @click="toBTCMerge">
<div class="text-gray-500 group-hover:underline">BTC Merge</div>
Expand All @@ -29,7 +29,7 @@ const toSpaceMerge = () => {
</div>

<div class="space-y-2">
<div class="text-base ">MVC</div>
<div class="text-base">MVC</div>
<div class="divide-y divide-gray-100">
<div class="setting-item group cursor-pointer" @click="toSpaceMerge">
<div class="text-gray-500 group-hover:underline">Space Merge</div>
Expand All @@ -38,8 +38,8 @@ const toSpaceMerge = () => {
</div>
</div>

<div class="setting-item group cursor-not-allowed" title="coming sonn">
<div class="text-gray-500 group-hover:underline">Ft Merge</div>
<div class="setting-item group cursor-pointer" @click="$router.push('/settings/toolkit/ft-merge')">
<div class="text-gray-500 group-hover:underline">FT Merge</div>
<div class="">
<ChevronRightIcon class="link-icon" />
</div>
Expand Down
280 changes: 280 additions & 0 deletions src/pages/settings/components/FTMerge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
<script setup lang="ts">
import { FEEB } from '@/data/config'
import { getApiHost } from '@/lib/host'
import { getNetwork } from '@/lib/network'
import type { Asset } from '@/data/assets'
import Avatar from '@/components/Avatar.vue'
import { UseImage } from '@vueuse/components'
import { useMVCAssetsQuery } from '@/queries/tokens'
import LoadingIcon from '@/components/LoadingIcon.vue'
import { computed, onMounted, ref, watch } from 'vue'
import { getPrivateKey, getAddress } from '@/lib/account'
import { API_NET, API_TARGET, FtManager } from 'meta-contract'
import TransactionResultModal, { type TransactionResult } from '@/pages/wallet/components/TransactionResultModal.vue'
const address = ref('')
const ftManager = ref()
const operation = ref('')
const loading = ref(false)
const isRefresh = ref(true)
const currentGenesis = ref('')
const currentCodehash = ref('')
const isOpenResultModal = ref(false)
const transactionResult = ref<TransactionResult>()
const ftAsssets = ref<(Asset & { utxoCount: number })[]>([])
const NeedToMergeCount = 3
const testSplit = false
type Receiver = {
address: string
amount: string
}
const split = async (genesis: string, codehash: string, symbol: string, decimal: number) => {
try {
currentGenesis.value = genesis
currentCodehash.value = codehash
operation.value = 'split'
const network: API_NET = (await getNetwork()) as API_NET
const purse = await getPrivateKey('mvc')
const apiHost = await getApiHost()
const ftManager = new FtManager({
network,
apiTarget: API_TARGET.MVC,
purse,
feeb: FEEB,
apiHost,
})
let receivers: Receiver[] = []
for (let i = 0; i < 99; i++) {
receivers.push({ address: address.value, amount: '1' })
}
loading.value = true
let { txid: splitTxId } = await ftManager
.transfer({
genesis,
codehash,
receivers,
senderWif: purse,
})
.catch((e) => {
isOpenResultModal.value = true
transactionResult.value = {
status: 'failed',
message: e,
}
throw e
})
isRefresh.value = true
isOpenResultModal.value = true
transactionResult.value = {
chain: 'mvc',
status: 'success',
txId: splitTxId,
fromAddress: address.value,
toAdddress: address.value,
amount: receivers.reduce((acc, cur) => acc + Number(cur.amount), 0),
token: {
symbol: symbol,
decimal,
},
}
} catch (error) {
transactionResult.value = {
status: 'failed',
message: error as string,
}
} finally {
loading.value = true
}
}
const merge = async (genesis: string, codehash: string) => {
try {
currentGenesis.value = genesis
currentCodehash.value = codehash
operation.value = 'merge'
const network: API_NET = (await getNetwork()) as API_NET
const purse = await getPrivateKey('mvc')
const apiHost = await getApiHost()
const ftManager = new FtManager({
network,
apiTarget: API_TARGET.MVC,
purse,
feeb: FEEB,
apiHost,
})
loading.value = true
const { txids } = await ftManager
.totalMerge({
genesis,
codehash,
ownerWif: purse,
})
.catch((e) => {
isOpenResultModal.value = true
transactionResult.value = {
status: 'failed',
message: e,
}
throw e
})
isRefresh.value = true
isOpenResultModal.value = true
transactionResult.value = {
chain: 'mvc',
status: 'successTxs',
txIds: txids,
}
} catch (error) {
transactionResult.value = {
status: 'failed',
message: error as string,
}
} finally {
loading.value = false
}
}
getAddress('mvc').then((_address) => {
address.value = _address
})
const { isLoading, data: mvcAssets } = useMVCAssetsQuery(address, {
enabled: computed(() => !!address.value),
autoRefresh: true,
})
// TODO: Change computed
watch(
[mvcAssets, ftManager, isRefresh],
([assets, manager, _isRefresh]) => {
if (manager && assets && _isRefresh) {
ftAsssets.value = []
for (let asset of assets || []) {
const { codeHash, genesis } = asset
manager.api.getFungibleTokenUnspents(codeHash, genesis, address.value).then((data: any) => {
ftAsssets.value.push({
...asset,
utxoCount: data.length,
})
isRefresh.value = false
})
}
}
},
{ immediate: true }
)
const hasMergeToken = computed(() => {
return ftAsssets.value.some((asset) => asset.utxoCount > NeedToMergeCount)
})
onMounted(async () => {
const network: API_NET = (await getNetwork()) as API_NET
const purse = await getPrivateKey('mvc')
const apiHost = await getApiHost()
ftManager.value = new FtManager({
network,
apiTarget: API_TARGET.MVC,
purse,
feeb: FEEB,
apiHost,
})
})
</script>

<template>
<div>
<div class="text-2xl font-medium">FT Merge</div>
<div class="mt-2 text-gray-primary text-xs">
Due to the technical characteristics of UTXO, when there are too many UTXOs of a certain token, problems such as
cycle failure will occur. The merge tool will automatically help you merge scattered UTXOs into one.
</div>
<div class="mt-4 py-3 flex gap-3 items-center">
<Avatar :id="address" />
<div class="flex flex-col gap-1">
<div class="text-sm font-medium">MVC Address</div>
<div class="text-gray-primary text-xs">{{ address }}</div>
</div>
</div>
<div class="-mx-5 px-5 bg-gray-light py-3">Token</div>
<div class="py-16 text-center" v-if="isLoading">Token List Loading...</div>
<div class="py-3" v-for="(asset, index) in ftAsssets" :key="index" v-else-if="hasMergeToken">
<div class="flex items-center justify-between" v-if="asset.utxoCount > NeedToMergeCount">
<div class="flex items-center gap-3">
<UseImage :src="asset.logo" v-if="asset.logo && asset.codeHash" class="h-10 w-10 rounded-md">
<template #error>
<div class="h-10 w-10 text-center leading-10 rounded-full text-white text-base bg-[#1E2BFF]">
{{ asset.symbol[0].toLocaleUpperCase() }}
</div>
</template>
</UseImage>
<div class="flex flex-col gap-1">
<span class="text-sm">{{ asset.tokenName }}</span>
<span class="text-gray-primary text-xs">UTXO Count:{{ asset.utxoCount }}</span>
</div>
</div>
<div class="flex items-center gap-3">
<button
v-if="testSplit"
:disabled="loading"
@click="split(asset.genesis!, asset.codeHash!, asset.symbol, asset.decimal)"
:class="[
{ 'cursor-not-allowed': loading },
'text-primary-blue py-2 px-4 rounded-3xl bg-blue-light text-xs',
]"
>
<div class="flex items-center gap-1 w-12 justify-center">
<LoadingIcon
class="w-4 text-primary-blue"
v-if="
loading &&
currentGenesis === asset.genesis &&
currentCodehash === asset.codeHash &&
operation === 'split'
"
/>
<span>Split</span>
</div>
</button>
<button
:disabled="loading"
@click="merge(asset.genesis!, asset.codeHash!)"
:class="[
{ 'cursor-not-allowed': loading },
'text-primary-blue py-2 px-4 rounded-3xl bg-blue-light text-xs',
]"
>
<div class="flex items-center gap-1 w-14 justify-center">
<LoadingIcon
class="w-4 text-primary-blue"
v-if="
loading &&
currentGenesis === asset.genesis &&
currentCodehash === asset.codeHash &&
operation === 'merge'
"
/>
<span>Merge</span>
</div>
</button>
</div>
</div>
</div>
<div class="py-16 text-center text-gray-primary" v-else>No Token need to merge.</div>

<TransactionResultModal v-model:is-open-result="isOpenResultModal" :result="transactionResult" />
</div>
</template>

<style lang="css" scoped>
.label {
@apply text-sm text-gray-500;
}
.value {
@apply text-sm text-gray-700;
}
</style>
Loading

0 comments on commit 5e5a588

Please sign in to comment.