|
|
@@ -26,59 +26,43 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
<section> |
|
|
|
<section class="section is-small"> |
|
|
|
<div class="box"> |
|
|
|
<div class="title is-size-4 has-text-grey-dark has-text-centered"> |
|
|
|
Tracker |
|
|
|
</div> |
|
|
|
<div class="level" v-if="fund.fundInfo.bonuses.length > 0"> |
|
|
|
<div class="level-item has-text-centered" |
|
|
|
v-for="bonus in fund.fundInfo.bonuses.sort((v1, v2) => v1.goal - v2.goal)" |
|
|
|
v-bind:key="bonus.goal" |
|
|
|
> |
|
|
|
<div> |
|
|
|
<p |
|
|
|
class="heading" |
|
|
|
:class="amountHeld >= bonus.goal |
|
|
|
<div class="mb-4"> |
|
|
|
<div class="level" v-if="fund.fundInfo.bonuses.length > 0"> |
|
|
|
<div class="level-item has-text-centered" |
|
|
|
v-for="bonus in fund.fundInfo.bonuses.sort((v1, v2) => v1.goal - v2.goal)" |
|
|
|
v-bind:key="bonus.goal" |
|
|
|
> |
|
|
|
<div> |
|
|
|
<p |
|
|
|
class="heading" |
|
|
|
:class="amountHeld >= bonus.goal |
|
|
|
? 'has-text-success' : 'has-text-grey-dark'" |
|
|
|
> |
|
|
|
{{ bonus.goal.toLocaleString() }} XLM |
|
|
|
</p> |
|
|
|
<p |
|
|
|
class="title" |
|
|
|
:class="amountHeld >= bonus.goal |
|
|
|
> |
|
|
|
{{ bonus.goal.toLocaleString() }} XLM |
|
|
|
</p> |
|
|
|
<p |
|
|
|
class="title" |
|
|
|
:class="amountHeld >= bonus.goal |
|
|
|
? 'has-text-success' : 'has-text-grey-dark'" |
|
|
|
> |
|
|
|
{{ bonus.percent }}% |
|
|
|
</p> |
|
|
|
> |
|
|
|
{{ bonus.percent }}% |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div v-else> |
|
|
|
<div class="has-text-centered">This group fund has no rewards available</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
class="is-flex |
|
|
|
is-flex-direction-row |
|
|
|
is-justify-content-space-around |
|
|
|
is-size-4 |
|
|
|
has-text-white |
|
|
|
mb-3" |
|
|
|
> |
|
|
|
<div class="total"> |
|
|
|
<span class="total-label is-size-3 pr-2 has-text-weight-light"> |
|
|
|
Raised |
|
|
|
</span> |
|
|
|
<span class="pl-3 has-text-weight-bold"> |
|
|
|
{{ amountHeld.toLocaleString() }} XLM |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<div class="remaining"> |
|
|
|
<span class="remaining-label is-size-3 pr-2 has-text-weight-light"> |
|
|
|
Remaining |
|
|
|
</span> |
|
|
|
<span class="pl-3 has-text-weight-bold"> |
|
|
|
{{ amountAvailable.toLocaleString() }} XLM |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<progress class="progress is-large is-info" :value="calcPctComplete()" |
|
|
|
max="100"> |
|
|
|
{{ calcPctComplete() }}% |
|
|
|
</progress> |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
<section class="section is-small"> |
|
|
@@ -96,7 +80,7 @@ |
|
|
|
type="number" |
|
|
|
placeholder="Amount" |
|
|
|
aria-label="Amount" |
|
|
|
v-model="amt" |
|
|
|
v-model="amount" |
|
|
|
:max="acctBalance" |
|
|
|
:disabled="loading.balance" |
|
|
|
> |
|
|
@@ -145,26 +129,56 @@ |
|
|
|
<th>Wallet</th> |
|
|
|
<th>Amount</th> |
|
|
|
<th v-if="!enableConsolidation">Time</th> |
|
|
|
<th v-else>Tokens</th> |
|
|
|
<template v-else> |
|
|
|
<th>Bonus</th> |
|
|
|
<th v-if="store.getters.getToken && hasPermission(Privileges.Admin)">Post</th> |
|
|
|
</template> |
|
|
|
</tr> |
|
|
|
</thead> |
|
|
|
<tbody> |
|
|
|
<tr v-for="(contribution, i) in contributions" v-bind:key="i"> |
|
|
|
<td>{{ truncateWallet(contribution.wallet, 6, undefined) }}</td> |
|
|
|
<td>{{ truncateWallet(contribution.wallet, calculateWalletChars(), undefined) }}</td> |
|
|
|
<td>{{ contribution.amount }}</td> |
|
|
|
<td v-if="!enableConsolidation"> |
|
|
|
<span class="transaction-date" :title="formatDate(contribution.createdAt, true)"> |
|
|
|
{{ formatDate(contribution.createdAt, true) }} |
|
|
|
</span> |
|
|
|
</td> |
|
|
|
<td v-else> |
|
|
|
<span>{{ calculateReward(contribution.amount / fund.fundInfo.price) }}</span> |
|
|
|
</td> |
|
|
|
<template v-else> |
|
|
|
<td> |
|
|
|
<span>{{ calculateReward(contribution.amount.div(fund.fundInfo.price)) }}</span> |
|
|
|
</td> |
|
|
|
<td v-if="store.getters.getToken && hasPermission(Privileges.Admin)"> |
|
|
|
<button class="button is-small is-outlined">Send</button> |
|
|
|
</td> |
|
|
|
</template> |
|
|
|
</tr> |
|
|
|
</tbody> |
|
|
|
</table> |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
<section class="section is-small px-0" |
|
|
|
v-if="store.getters.getToken && hasPermission(Privileges.Admin)"> |
|
|
|
<div class="title is-size-4 has-text-white-ter has-text-centered"> |
|
|
|
Submit Group Fund |
|
|
|
</div> |
|
|
|
<div class="box is-flex is-flex-direction-row is-justify-content-space-between"> |
|
|
|
<div class="my-auto"> |
|
|
|
<label class="checkbox" for="submit-confirm"> |
|
|
|
<input type="checkbox" id="submit-confirm" v-model="allowSubmit"> Allow Submit |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
<div> |
|
|
|
<button |
|
|
|
class="button is-success" |
|
|
|
:disabled="!allowSubmit" |
|
|
|
@click="submitFund" |
|
|
|
> |
|
|
|
Submit |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</section> |
|
|
|
<section v-if="store.getters.getToken && hasPermission(Privileges.AdminPlus)"> |
|
|
|
<div class="title is-size-4 has-text-white-ter has-text-centered"> |
|
|
|
Close Group Fund |
|
|
@@ -172,7 +186,7 @@ |
|
|
|
<div class="box is-flex is-flex-direction-row is-justify-content-space-between"> |
|
|
|
<div class="my-auto"> |
|
|
|
<label class="checkbox" for="delete-confirm"> |
|
|
|
<input type="checkbox" id="delete-confirm" v-model="allowDelete"> Close |
|
|
|
<input type="checkbox" id="delete-confirm" v-model="allowDelete"> Allow Close |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
<div> |
|
|
@@ -207,7 +221,10 @@ import { |
|
|
|
ref, |
|
|
|
watch, |
|
|
|
} from 'vue'; |
|
|
|
import { useWebSocket } from '@vueuse/core'; |
|
|
|
import { |
|
|
|
useWebSocket, |
|
|
|
useWindowSize, |
|
|
|
} from '@vueuse/core'; |
|
|
|
import store from '@/store'; |
|
|
|
import { |
|
|
|
sanitize, |
|
|
@@ -222,7 +239,9 @@ import { |
|
|
|
getBalance, |
|
|
|
getContributions, |
|
|
|
getRewardFund, |
|
|
|
submitRewardFund, |
|
|
|
} from '@/api/composed'; |
|
|
|
import Decimal from 'decimal.js'; |
|
|
|
|
|
|
|
const route = useRoute(); |
|
|
|
const router = useRouter(); |
|
|
@@ -238,10 +257,23 @@ const formatDate = (time: string, includeTime = false) => { |
|
|
|
}; |
|
|
|
|
|
|
|
const pk = ref(''); |
|
|
|
const amt = ref(undefined as number | undefined); |
|
|
|
const amt = ref(undefined as Decimal | undefined); |
|
|
|
const selectableDates = ref([undefined] as (string | undefined)[]); |
|
|
|
const selectedDate = ref(undefined as string | undefined); |
|
|
|
|
|
|
|
const { width } = useWindowSize(); |
|
|
|
|
|
|
|
const amount = computed({ |
|
|
|
get: () => amt.value, |
|
|
|
set: (v) => { |
|
|
|
if (v) { |
|
|
|
amt.value = new Decimal(v); |
|
|
|
} else { |
|
|
|
amt.value = undefined; |
|
|
|
} |
|
|
|
}, |
|
|
|
}); |
|
|
|
|
|
|
|
const allowDelete = ref(false); |
|
|
|
const deleteFund = async () => { |
|
|
|
const deleted = await deleteRewardFund(identifier, allowDelete.value); |
|
|
@@ -249,6 +281,32 @@ const deleteFund = async () => { |
|
|
|
await router.push('/'); |
|
|
|
} |
|
|
|
}; |
|
|
|
const delTimeout = ref(undefined as number | undefined); |
|
|
|
|
|
|
|
watch(allowDelete, () => { |
|
|
|
if (delTimeout.value) window.clearTimeout(delTimeout.value); |
|
|
|
delTimeout.value = window.setTimeout(() => { |
|
|
|
allowDelete.value = false; |
|
|
|
delTimeout.value = undefined; |
|
|
|
}, 10000); |
|
|
|
}); |
|
|
|
|
|
|
|
const allowSubmit = ref(false); |
|
|
|
const submitFund = async () => { |
|
|
|
const submitted = await submitRewardFund(identifier, allowSubmit.value); |
|
|
|
if (submitted && submitted.success) { |
|
|
|
console.log('submitted'); // TODO: provide feedback for submission |
|
|
|
} |
|
|
|
}; |
|
|
|
const subTimeout = ref(undefined as number | undefined); |
|
|
|
|
|
|
|
watch(allowSubmit, () => { |
|
|
|
if (subTimeout.value) window.clearTimeout(subTimeout.value); |
|
|
|
subTimeout.value = window.setTimeout(() => { |
|
|
|
allowSubmit.value = false; |
|
|
|
subTimeout.value = undefined; |
|
|
|
}, 10000); |
|
|
|
}); |
|
|
|
|
|
|
|
const enableConsolidation = ref(false); |
|
|
|
const fund = ref( |
|
|
@@ -278,11 +336,11 @@ fundDetails.value = [ |
|
|
|
val: fund.value.fundInfo.asset, |
|
|
|
}, |
|
|
|
{ |
|
|
|
title: 'Min', |
|
|
|
title: 'Minimum', |
|
|
|
val: `${fund.value.fundInfo.minContribution.toLocaleString()}`, |
|
|
|
}, |
|
|
|
{ |
|
|
|
title: 'Goal', |
|
|
|
title: 'Remaining', |
|
|
|
val: `${fund.value.fundInfo.amountAvailable.toLocaleString()}`, |
|
|
|
}, |
|
|
|
{ |
|
|
@@ -296,29 +354,47 @@ if (fund.value.contributions.dates) { |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
const reward = ref(0); |
|
|
|
const processContributions = (contributions: Contribution[]) => contributions.map((c) => ({ |
|
|
|
...c, |
|
|
|
amount: new Decimal(c.amount), |
|
|
|
})); |
|
|
|
|
|
|
|
const reward = ref(new Decimal(0)); |
|
|
|
const maxBonus = ref(0); |
|
|
|
const bonus = ref(undefined as Bonus | undefined); |
|
|
|
const amountHeld = ref(fund.value.total.amountHeld); |
|
|
|
const amountAvailable = ref(fund.value.fundInfo.amountAvailable); |
|
|
|
const contributions: Ref<Contribution[]> = ref(fund.value.contributions.list ?? []); |
|
|
|
const amountHeld = ref(new Decimal(fund.value.total.amountHeld)); |
|
|
|
const amountAvailable = ref(new Decimal(fund.value.fundInfo.amountAvailable)); |
|
|
|
const contributions: Ref<Contribution[]> = ref(processContributions( |
|
|
|
fund.value.contributions.list ?? [], |
|
|
|
)); |
|
|
|
const offset = ref(contributions.value.length); |
|
|
|
const total = ref(fund.value.contributions.total); |
|
|
|
const contributionsLoading = ref(false); |
|
|
|
const acctBalance = ref(undefined as number | undefined); |
|
|
|
const acctBalance = ref(undefined as Decimal | undefined); |
|
|
|
const unknownAcct = ref(true); |
|
|
|
const loading = ref({ |
|
|
|
contribution: false, |
|
|
|
balance: false, |
|
|
|
}); |
|
|
|
|
|
|
|
const round = (num: number, figures = 1) => { |
|
|
|
const factor = 10 ** figures; |
|
|
|
return Math.round(num * factor) / factor; |
|
|
|
}; |
|
|
|
|
|
|
|
const calcPctComplete = () => round(amountHeld.value.div(amountAvailable.value) |
|
|
|
.mul(100) |
|
|
|
.toNumber()); |
|
|
|
|
|
|
|
const calculateWalletChars = () => round(width.value / 114, 0); |
|
|
|
|
|
|
|
const hasInvalidValues = () => { |
|
|
|
if (!fund.value) throw new Error('Fund was not loaded!'); |
|
|
|
return [pk, amt].every((v) => v.value !== undefined && v.value !== '') |
|
|
|
&& (amt.value === 0 |
|
|
|
|| amt.value! > amountAvailable.value |
|
|
|
|| amt.value! < fund.value.fundInfo.minContribution |
|
|
|
|| (acctBalance.value && amt.value! > acctBalance.value) |
|
|
|
return [pk, amount].every((v) => v.value !== undefined && v.value !== '') |
|
|
|
&& (amount.value!.isZero() |
|
|
|
|| amount.value! > amountAvailable.value |
|
|
|
|| amount.value!.lt(fund.value.fundInfo.minContribution) |
|
|
|
|| (acctBalance.value && amt.value!.gt(acctBalance.value)) |
|
|
|
|| unknownAcct.value); |
|
|
|
}; |
|
|
|
|
|
|
@@ -339,18 +415,17 @@ const getCurrentBonus = () => { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const calculateReward = (bought: number) => { |
|
|
|
const calculateReward = (bought: Decimal) => { |
|
|
|
if (bonus.value) { |
|
|
|
if (!bonus.value.percent) throw new Error('Bonus did not have percent for some reason'); |
|
|
|
reward.value = bought + bought * (bonus.value.percent / 100); |
|
|
|
reward.value = bought.add(bought) |
|
|
|
.mul(new Decimal(bonus.value.percent / 100)); |
|
|
|
} else { |
|
|
|
reward.value = bought; |
|
|
|
} |
|
|
|
return reward.value.toLocaleString(); |
|
|
|
}; |
|
|
|
|
|
|
|
const fixNewlines = (s: string) => s.replace('\n', '<br/>'); |
|
|
|
|
|
|
|
const errs: SignetError[] = [ |
|
|
|
{ |
|
|
|
text: 'Amount is required', |
|
|
@@ -358,11 +433,11 @@ const errs: SignetError[] = [ |
|
|
|
}, |
|
|
|
{ |
|
|
|
text: 'Amount must be greater than 0', |
|
|
|
condition: amt.value && amt.value === 0 && !fund.value.fundInfo.minContribution, |
|
|
|
condition: amt.value && amt.value.isZero() && !fund.value.fundInfo.minContribution, |
|
|
|
}, |
|
|
|
{ |
|
|
|
text: 'Amount is less than the minimum contribution', |
|
|
|
condition: amt.value && amt.value < fund.value.fundInfo.minContribution, |
|
|
|
condition: amt.value && amt.value.lt(fund.value.fundInfo.minContribution), |
|
|
|
}, |
|
|
|
{ |
|
|
|
text: `Not enough ${fund.value.fundInfo.asset} for sale in ICO`, |
|
|
@@ -370,7 +445,7 @@ const errs: SignetError[] = [ |
|
|
|
}, |
|
|
|
{ |
|
|
|
text: `Not enough XLM to send (${amt.value?.toLocaleString()})`, |
|
|
|
condition: amt.value && acctBalance.value && amt.value > acctBalance.value, |
|
|
|
condition: amt.value && acctBalance.value && amt.value.gt(acctBalance.value), |
|
|
|
}, |
|
|
|
{ |
|
|
|
text: 'Could not find Stellar wallet', |
|
|
@@ -385,7 +460,7 @@ watch(selectedDate, async (newVal) => { |
|
|
|
const conts = await getContributions(identifier, offset.value, newVal, enableConsolidation.value); |
|
|
|
if (!fund.value) throw new Error('Fund not found'); |
|
|
|
if (!conts) throw new Error('Contributions not found'); |
|
|
|
contributions.value = conts.list; |
|
|
|
contributions.value = processContributions(conts.list); |
|
|
|
offset.value = contributions.value.length; |
|
|
|
total.value = fund.value.contributions.total; |
|
|
|
}); |
|
|
@@ -407,7 +482,7 @@ const loadMoreIfNeeded = async (e: Event) => { |
|
|
|
if (!moreContribs) throw new Error('Contributions not found'); |
|
|
|
offset.value += moreContribs.list.length; |
|
|
|
total.value = moreContribs.total; |
|
|
|
contributions.value = contributions.value.concat(moreContribs.list); |
|
|
|
contributions.value = contributions.value.concat(processContributions(moreContribs.list)); |
|
|
|
contributionsLoading.value = false; |
|
|
|
} |
|
|
|
}; |
|
|
@@ -432,9 +507,9 @@ const queryAccount = async () => { |
|
|
|
acctBalance.value = undefined; |
|
|
|
} else { |
|
|
|
unknownAcct.value = false; |
|
|
|
if (resp?.balance) { |
|
|
|
acctBalance.value = resp?.balance; |
|
|
|
if (amt.value && amt.value > acctBalance.value) { |
|
|
|
if (resp && resp.balance) { |
|
|
|
acctBalance.value = new Decimal(resp.balance); |
|
|
|
if (amt.value && amt.value.gt(acctBalance.value)) { |
|
|
|
amt.value = acctBalance.value; |
|
|
|
} |
|
|
|
} |
|
|
@@ -445,12 +520,13 @@ const queryAccount = async () => { |
|
|
|
|
|
|
|
const makeContribution = async () => { |
|
|
|
if (!fund.value) throw new Error('Fund not found'); |
|
|
|
if (!amt.value) return; |
|
|
|
if (!/[^[0-9]+$/.test(amt.value.toString())) return; |
|
|
|
if (!amount.value) return; |
|
|
|
if (!/^[0-9]+$/.test(amount.value.toString())) return; |
|
|
|
if (unknownAcct.value) return; |
|
|
|
if (!loading.value.contribution && pk.value && amt.value && amt.value <= amountAvailable.value) { |
|
|
|
if (!loading.value.contribution && pk.value |
|
|
|
&& amount.value && amount.value <= amountAvailable.value) { |
|
|
|
loading.value.contribution = true; |
|
|
|
await contribute(sanitize(pk.value), amt.value, fund.value.fundInfo.id); |
|
|
|
await contribute(sanitize(pk.value), amount.value!.toNumber(), fund.value.fundInfo.id); |
|
|
|
loading.value.contribution = false; |
|
|
|
pk.value = ''; |
|
|
|
amt.value = undefined; |
|
|
@@ -467,7 +543,7 @@ watch(enableConsolidation, async () => { |
|
|
|
); |
|
|
|
if (!fund.value) throw new Error('Fund not found'); |
|
|
|
if (!conts) throw new Error('Contributions not found'); |
|
|
|
contributions.value = conts.list; |
|
|
|
contributions.value = processContributions(conts.list); |
|
|
|
offset.value = contributions.value.length; |
|
|
|
total.value = fund.value.contributions.total; |
|
|
|
}); |
|
|
@@ -488,13 +564,13 @@ watch(data, (newVal) => { |
|
|
|
.includes(v.wallet)) { |
|
|
|
const hasContribution = contributions.value.find((c: Contribution) => c.wallet === v.wallet); |
|
|
|
if (!hasContribution) throw new Error('Something went wrong'); |
|
|
|
hasContribution.amount += v.amount; |
|
|
|
hasContribution.amount = new Decimal(hasContribution.amount).add(v.amount); |
|
|
|
} else { |
|
|
|
contributions.value.splice(0, 0, v); |
|
|
|
offset.value += 1; |
|
|
|
} |
|
|
|
amountHeld.value += v.amount; |
|
|
|
amountAvailable.value -= v.amount; |
|
|
|
amountHeld.value = new Decimal(amountHeld.value).add(v.amount); |
|
|
|
amountAvailable.value = new Decimal(amountAvailable.value).sub(v.amount); |
|
|
|
} |
|
|
|
}); |
|
|
|
</script> |
|
|
|