@@ -12,10 +12,14 @@ | |||||
"@vueuse/core": "^9.5.0", | "@vueuse/core": "^9.5.0", | ||||
"bulma": "^0.9.4", | "bulma": "^0.9.4", | ||||
"core-js": "^3.8.3", | "core-js": "^3.8.3", | ||||
"decimal.js": "^10.4.3", | |||||
"jenesius-vue-modal": "^1.8.2", | "jenesius-vue-modal": "^1.8.2", | ||||
"jwt-decode": "^3.1.2", | |||||
"luxon": "^3.1.0", | "luxon": "^3.1.0", | ||||
"vue": "^3.2.13", | "vue": "^3.2.13", | ||||
"vue-draggable-next": "^2.1.1", | |||||
"vue-router": "^4.0.3", | "vue-router": "^4.0.3", | ||||
"vuedraggable": "^4.1.0", | |||||
"vuex": "^4.0.0" | "vuex": "^4.0.0" | ||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
@@ -5609,11 +5613,9 @@ | |||||
} | } | ||||
}, | }, | ||||
"node_modules/decimal.js": { | "node_modules/decimal.js": { | ||||
"version": "10.4.2", | |||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", | |||||
"integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==", | |||||
"dev": true, | |||||
"license": "MIT" | |||||
"version": "10.4.3", | |||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", | |||||
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" | |||||
}, | }, | ||||
"node_modules/decode-uri-component": { | "node_modules/decode-uri-component": { | ||||
"version": "0.2.0", | "version": "0.2.0", | ||||
@@ -8922,6 +8924,11 @@ | |||||
"graceful-fs": "^4.1.6" | "graceful-fs": "^4.1.6" | ||||
} | } | ||||
}, | }, | ||||
"node_modules/jwt-decode": { | |||||
"version": "3.1.2", | |||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", | |||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" | |||||
}, | |||||
"node_modules/kind-of": { | "node_modules/kind-of": { | ||||
"version": "6.0.3", | "version": "6.0.3", | ||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", | ||||
@@ -12671,6 +12678,12 @@ | |||||
"websocket-driver": "^0.7.4" | "websocket-driver": "^0.7.4" | ||||
} | } | ||||
}, | }, | ||||
"node_modules/sortablejs": { | |||||
"version": "1.15.0", | |||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz", | |||||
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==", | |||||
"peer": true | |||||
}, | |||||
"node_modules/source-map": { | "node_modules/source-map": { | ||||
"version": "0.6.1", | "version": "0.6.1", | ||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", | ||||
@@ -13847,6 +13860,15 @@ | |||||
} | } | ||||
} | } | ||||
}, | }, | ||||
"node_modules/vue-draggable-next": { | |||||
"version": "2.1.1", | |||||
"resolved": "https://registry.npmjs.org/vue-draggable-next/-/vue-draggable-next-2.1.1.tgz", | |||||
"integrity": "sha512-f5lmA7t6LMaL4viR7dU30zzvqJzaKQs0ymL0Jy9UDT9uiZ2tXF3MzPzEvpTH2UODXZJkT+SnjeV1fXHMsgXLYA==", | |||||
"peerDependencies": { | |||||
"sortablejs": "^1.14.0", | |||||
"vue": "^3.2.2" | |||||
} | |||||
}, | |||||
"node_modules/vue-eslint-parser": { | "node_modules/vue-eslint-parser": { | ||||
"version": "8.3.0", | "version": "8.3.0", | ||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", | "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", | ||||
@@ -14002,6 +14024,22 @@ | |||||
"dev": true, | "dev": true, | ||||
"license": "MIT" | "license": "MIT" | ||||
}, | }, | ||||
"node_modules/vuedraggable": { | |||||
"version": "4.1.0", | |||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", | |||||
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", | |||||
"dependencies": { | |||||
"sortablejs": "1.14.0" | |||||
}, | |||||
"peerDependencies": { | |||||
"vue": "^3.0.1" | |||||
} | |||||
}, | |||||
"node_modules/vuedraggable/node_modules/sortablejs": { | |||||
"version": "1.14.0", | |||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", | |||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" | |||||
}, | |||||
"node_modules/vuex": { | "node_modules/vuex": { | ||||
"version": "4.1.0", | "version": "4.1.0", | ||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", | "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", | ||||
@@ -18648,10 +18686,9 @@ | |||||
"dev": true | "dev": true | ||||
}, | }, | ||||
"decimal.js": { | "decimal.js": { | ||||
"version": "10.4.2", | |||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", | |||||
"integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==", | |||||
"dev": true | |||||
"version": "10.4.3", | |||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", | |||||
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" | |||||
}, | }, | ||||
"decode-uri-component": { | "decode-uri-component": { | ||||
"version": "0.2.0", | "version": "0.2.0", | ||||
@@ -20934,6 +20971,11 @@ | |||||
"universalify": "^2.0.0" | "universalify": "^2.0.0" | ||||
} | } | ||||
}, | }, | ||||
"jwt-decode": { | |||||
"version": "3.1.2", | |||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", | |||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" | |||||
}, | |||||
"kind-of": { | "kind-of": { | ||||
"version": "6.0.3", | "version": "6.0.3", | ||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", | ||||
@@ -23530,6 +23572,12 @@ | |||||
"websocket-driver": "^0.7.4" | "websocket-driver": "^0.7.4" | ||||
} | } | ||||
}, | }, | ||||
"sortablejs": { | |||||
"version": "1.15.0", | |||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz", | |||||
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==", | |||||
"peer": true | |||||
}, | |||||
"source-map": { | "source-map": { | ||||
"version": "0.6.1", | "version": "0.6.1", | ||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", | ||||
@@ -24339,6 +24387,12 @@ | |||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", | "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", | ||||
"requires": {} | "requires": {} | ||||
}, | }, | ||||
"vue-draggable-next": { | |||||
"version": "2.1.1", | |||||
"resolved": "https://registry.npmjs.org/vue-draggable-next/-/vue-draggable-next-2.1.1.tgz", | |||||
"integrity": "sha512-f5lmA7t6LMaL4viR7dU30zzvqJzaKQs0ymL0Jy9UDT9uiZ2tXF3MzPzEvpTH2UODXZJkT+SnjeV1fXHMsgXLYA==", | |||||
"requires": {} | |||||
}, | |||||
"vue-eslint-parser": { | "vue-eslint-parser": { | ||||
"version": "8.3.0", | "version": "8.3.0", | ||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", | "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", | ||||
@@ -24443,6 +24497,21 @@ | |||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", | "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", | ||||
"dev": true | "dev": true | ||||
}, | }, | ||||
"vuedraggable": { | |||||
"version": "4.1.0", | |||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", | |||||
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", | |||||
"requires": { | |||||
"sortablejs": "1.14.0" | |||||
}, | |||||
"dependencies": { | |||||
"sortablejs": { | |||||
"version": "1.14.0", | |||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", | |||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" | |||||
} | |||||
} | |||||
}, | |||||
"vuex": { | "vuex": { | ||||
"version": "4.1.0", | "version": "4.1.0", | ||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", | "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", | ||||
@@ -13,6 +13,7 @@ | |||||
"@vueuse/core": "^9.5.0", | "@vueuse/core": "^9.5.0", | ||||
"bulma": "^0.9.4", | "bulma": "^0.9.4", | ||||
"core-js": "^3.8.3", | "core-js": "^3.8.3", | ||||
"decimal.js": "^10.4.3", | |||||
"jenesius-vue-modal": "^1.8.2", | "jenesius-vue-modal": "^1.8.2", | ||||
"jwt-decode": "^3.1.2", | "jwt-decode": "^3.1.2", | ||||
"luxon": "^3.1.0", | "luxon": "^3.1.0", | ||||
@@ -23,6 +23,7 @@ import { | |||||
NearlyCompleteFundsRequest, | NearlyCompleteFundsRequest, | ||||
NearlyCompleteFundsResponse, | NearlyCompleteFundsResponse, | ||||
QueueMember, | QueueMember, | ||||
SubmitRewardFundRequest, | |||||
SuccessResponse, | SuccessResponse, | ||||
} from '@/api/types'; | } from '@/api/types'; | ||||
@@ -49,6 +50,7 @@ export const createQueue = (name: string) => controller.post<CreateQueueResponse | |||||
export const createRewardFund = ( | export const createRewardFund = ( | ||||
asset: string, | asset: string, | ||||
fundWallet: string, | fundWallet: string, | ||||
fundSecret: string, | |||||
sellingWallet: string, | sellingWallet: string, | ||||
issuerWallet: string, | issuerWallet: string, | ||||
memo: string, | memo: string, | ||||
@@ -59,6 +61,7 @@ export const createRewardFund = ( | |||||
) => controller.post<SuccessResponse, CreateRewardFundRequest>('CreateRewardFund', { | ) => controller.post<SuccessResponse, CreateRewardFundRequest>('CreateRewardFund', { | ||||
asset, | asset, | ||||
fundWallet, | fundWallet, | ||||
fundSecret, | |||||
sellingWallet, | sellingWallet, | ||||
issuerWallet, | issuerWallet, | ||||
memo, | memo, | ||||
@@ -112,3 +115,8 @@ export const reorderQueue = (queueID: number, fundOrders: QueueMember[]) => cont | |||||
queueID, | queueID, | ||||
fundOrders, | fundOrders, | ||||
}); | }); | ||||
export const submitRewardFund = (fundID: number, submit: boolean) => controller.post<SuccessResponse, SubmitRewardFundRequest>('SubmitRewardFund', { | |||||
fundID, | |||||
submit, | |||||
}); |
@@ -1,3 +1,6 @@ | |||||
// eslint-disable-next-line no-shadow | |||||
import Decimal from 'decimal.js'; | |||||
// eslint-disable-next-line no-shadow | // eslint-disable-next-line no-shadow | ||||
export enum Privileges { | export enum Privileges { | ||||
None = -1, | None = -1, | ||||
@@ -8,7 +11,7 @@ export enum Privileges { | |||||
export interface Contribution { | export interface Contribution { | ||||
createdAt: string; | createdAt: string; | ||||
amount: number; | |||||
amount: Decimal; | |||||
rewardFundID: number; | rewardFundID: number; | ||||
transactionID: string; | transactionID: string; | ||||
wallet: string; | wallet: string; | ||||
@@ -71,6 +74,7 @@ export interface Bonus { | |||||
export interface CreateRewardFundRequest { | export interface CreateRewardFundRequest { | ||||
asset: string; | asset: string; | ||||
fundWallet: string; | fundWallet: string; | ||||
fundSecret: string; | |||||
sellingWallet: string; | sellingWallet: string; | ||||
issuerWallet: string; | issuerWallet: string; | ||||
memo: string; | memo: string; | ||||
@@ -188,3 +192,8 @@ export interface EditQueueRequest { | |||||
queueID: number; | queueID: number; | ||||
fundOrders: QueueMember[]; | fundOrders: QueueMember[]; | ||||
} | } | ||||
export interface SubmitRewardFundRequest { | |||||
fundID: number; | |||||
submit: boolean; | |||||
} |
@@ -1,4 +1,11 @@ | |||||
export const truncateWallet: (wallet: string, preDigits: number, postDigits: number | undefined) => string = (wallet: string, preDigits: number, postDigits = preDigits) => `${wallet.slice(0, preDigits)}...${wallet.slice(-(postDigits + 1), -1)}`; | |||||
export const truncateWallet: ( | |||||
wallet: string, | |||||
preDigits: number, | |||||
postDigits: number | undefined, | |||||
) => string = (wallet: string, preDigits: number, postDigits = preDigits) => { | |||||
const [med, mtd] = [Math.max(preDigits, 3), Math.max(postDigits, 3)]; | |||||
return `${wallet.slice(0, med)}...${wallet.slice(-(mtd + 1), -1)}`; | |||||
}; | |||||
export const isNumber = (s: string) => /^[0-9]+$/.test(s); | export const isNumber = (s: string) => /^[0-9]+$/.test(s); | ||||
export const sanitize = (s: string) => { | export const sanitize = (s: string) => { | ||||
@@ -7,8 +14,8 @@ export const sanitize = (s: string) => { | |||||
'<': '<', | '<': '<', | ||||
'>': '>', | '>': '>', | ||||
'"': '"', | '"': '"', | ||||
"'": ''', | |||||
'\'': ''', | |||||
'/': '/', | '/': '/', | ||||
} as {[key: string]: string}; | |||||
} as { [key: string]: string }; | |||||
return s.replace(/[&<>"'/]/ig, (match) => chars[match]); | return s.replace(/[&<>"'/]/ig, (match) => chars[match]); | ||||
}; | }; |
@@ -6,12 +6,7 @@ | |||||
<div class="title is-5 has-text-white-ter">Post</div> | <div class="title is-5 has-text-white-ter">Post</div> | ||||
<div class="control my-2"> | <div class="control my-2"> | ||||
<input class="input is-normal has-background-white has-text-black" type="text" | <input class="input is-normal has-background-white has-text-black" type="text" | ||||
placeholder="Title" aria-label="Title" v-model="title"> | |||||
</div> | |||||
<div class="control my-2"> | |||||
<textarea class="textarea is-normal has-background-white has-text-black" | |||||
placeholder="Description" aria-label="Description" v-model="description"> | |||||
</textarea> | |||||
placeholder="Telegram Link" aria-label="Telegram Link" v-model="telegramLink"> | |||||
</div> | </div> | ||||
</section> | </section> | ||||
<section class="section px-0 py-4"> | <section class="section px-0 py-4"> | ||||
@@ -20,6 +15,10 @@ | |||||
<input class="input is-normal has-background-white has-text-black" type="text" | <input class="input is-normal has-background-white has-text-black" type="text" | ||||
placeholder="Fund Wallet" aria-label="Fund Wallet" v-model="fundWallet"> | placeholder="Fund Wallet" aria-label="Fund Wallet" v-model="fundWallet"> | ||||
</div> | </div> | ||||
<div class="control my-2"> | |||||
<input class="input is-normal has-background-white has-text-black" type="text" | |||||
placeholder="Fund Secret" aria-label="Fund Secret" v-model="fundSecret"> | |||||
</div> | |||||
<div class="control my-2"> | <div class="control my-2"> | ||||
<input class="input is-normal has-background-white has-text-black" type="text" | <input class="input is-normal has-background-white has-text-black" type="text" | ||||
placeholder="Selling Wallet" aria-label="Selling Wallet" v-model="sellWallet"> | placeholder="Selling Wallet" aria-label="Selling Wallet" v-model="sellWallet"> | ||||
@@ -68,7 +67,6 @@ | |||||
<script setup lang="ts"> | <script setup lang="ts"> | ||||
import SignetRequestController from '@/api/requests'; | |||||
import { | import { | ||||
Bonus, | Bonus, | ||||
CreateQueueResponse, | CreateQueueResponse, | ||||
@@ -87,13 +85,10 @@ const router = useRouter(); | |||||
document.title = 'Beignet - Add Fund'; | document.title = 'Beignet - Add Fund'; | ||||
const controller = new SignetRequestController(); | |||||
const title = ref(''); | |||||
const description = ref(''); | |||||
const telegramLink = ref(''); | |||||
const fundWallet = ref(''); | const fundWallet = ref(''); | ||||
const sellWallet = ref(''); | const sellWallet = ref(''); | ||||
// const fundSecret = ref(''); | |||||
const fundSecret = ref(''); | |||||
const issuerWallet = ref(''); | const issuerWallet = ref(''); | ||||
const asset = ref(''); | const asset = ref(''); | ||||
const memo = ref(''); | const memo = ref(''); | ||||
@@ -129,12 +124,12 @@ const doCreateQueue = async () => { | |||||
const constructFund = () => ({ | const constructFund = () => ({ | ||||
asset: asset.value, | asset: asset.value, | ||||
fundWallet: sanitize(fundWallet.value), | fundWallet: sanitize(fundWallet.value), | ||||
fundSecret: sanitize(fundSecret.value), | |||||
sellingWallet: sanitize(sellWallet.value), | sellingWallet: sanitize(sellWallet.value), | ||||
issuerWallet: sanitize(issuerWallet.value), | issuerWallet: sanitize(issuerWallet.value), | ||||
memo: sanitize(memo.value), | memo: sanitize(memo.value), | ||||
minContribution: minContribution.value, | minContribution: minContribution.value, | ||||
title: sanitize(title.value), | |||||
description: sanitize(description.value), | |||||
title: sanitize(telegramLink.value), | |||||
bonuses: bonuses.value, | bonuses: bonuses.value, | ||||
queueID: null, | queueID: null, | ||||
}); | }); | ||||
@@ -154,12 +149,12 @@ const submit = async () => { | |||||
const resp = await createRewardFund( | const resp = await createRewardFund( | ||||
asset.value, | asset.value, | ||||
sanitize(fundWallet.value), | sanitize(fundWallet.value), | ||||
sanitize(fundSecret.value), | |||||
sanitize(sellWallet.value), | sanitize(sellWallet.value), | ||||
sanitize(issuerWallet.value), | sanitize(issuerWallet.value), | ||||
sanitize(memo.value), | sanitize(memo.value), | ||||
minContribution.value, | minContribution.value, | ||||
sanitize(title.value), | |||||
sanitize(description.value), | |||||
sanitize(telegramLink.value), | |||||
bonuses.value, | bonuses.value, | ||||
forQueue.value, | forQueue.value, | ||||
); | ); | ||||
@@ -5,7 +5,7 @@ | |||||
<div class="title is-4 has-text-white-ter has-text-centered">Nearly Completed Funds</div> | <div class="title is-4 has-text-white-ter has-text-centered">Nearly Completed Funds</div> | ||||
<div v-for="(fund, ind) in nearlyCompletedFunds" :key="ind"> | <div v-for="(fund, ind) in nearlyCompletedFunds" :key="ind"> | ||||
<RouterLink :to="`/fund/${fund.id}`"> | <RouterLink :to="`/fund/${fund.id}`"> | ||||
<FundLink :fund="fund" :aside="`${round(fund.raised/fund.amountAvailable*100)}%`"/> | |||||
<FundLink :fund="fund" :aside="`${fund.amountAvailable} remaining`"/> | |||||
</RouterLink> | </RouterLink> | ||||
</div> | </div> | ||||
</template> | </template> | ||||
@@ -21,17 +21,12 @@ import { NearlyCompletedFund } from '@/api/types'; | |||||
const nearlyCompletedFunds = ref([] as NearlyCompletedFund[]); | const nearlyCompletedFunds = ref([] as NearlyCompletedFund[]); | ||||
const pctThreshold = 85; | |||||
const threshold = 5000; | |||||
const req = await getNearlyCompletedFunds(pctThreshold); | |||||
const req = await getNearlyCompletedFunds(threshold); | |||||
if (req?.funds) { | if (req?.funds) { | ||||
nearlyCompletedFunds.value = req?.funds; | nearlyCompletedFunds.value = req?.funds; | ||||
} | } | ||||
const round = (float: number, digits = 1) => { | |||||
const factor = 10 ** digits; | |||||
return Math.round(float * factor) / factor; | |||||
}; | |||||
</script> | </script> | ||||
<style scoped lang="stylus"> | <style scoped lang="stylus"> | ||||
@@ -26,59 +26,43 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</section> | </section> | ||||
<section> | |||||
<section class="section is-small"> | |||||
<div class="box"> | <div class="box"> | ||||
<div class="title is-size-4 has-text-grey-dark has-text-centered"> | <div class="title is-size-4 has-text-grey-dark has-text-centered"> | ||||
Tracker | Tracker | ||||
</div> | </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'" | ? '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'" | ? 'has-text-success' : 'has-text-grey-dark'" | ||||
> | |||||
{{ bonus.percent }}% | |||||
</p> | |||||
> | |||||
{{ bonus.percent }}% | |||||
</p> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div v-else> | |||||
<div class="has-text-centered">This group fund has no rewards available</div> | |||||
</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> | </div> | ||||
</section> | </section> | ||||
<section class="section is-small"> | <section class="section is-small"> | ||||
@@ -96,7 +80,7 @@ | |||||
type="number" | type="number" | ||||
placeholder="Amount" | placeholder="Amount" | ||||
aria-label="Amount" | aria-label="Amount" | ||||
v-model="amt" | |||||
v-model="amount" | |||||
:max="acctBalance" | :max="acctBalance" | ||||
:disabled="loading.balance" | :disabled="loading.balance" | ||||
> | > | ||||
@@ -145,26 +129,56 @@ | |||||
<th>Wallet</th> | <th>Wallet</th> | ||||
<th>Amount</th> | <th>Amount</th> | ||||
<th v-if="!enableConsolidation">Time</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> | </tr> | ||||
</thead> | </thead> | ||||
<tbody> | <tbody> | ||||
<tr v-for="(contribution, i) in contributions" v-bind:key="i"> | <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>{{ contribution.amount }}</td> | ||||
<td v-if="!enableConsolidation"> | <td v-if="!enableConsolidation"> | ||||
<span class="transaction-date" :title="formatDate(contribution.createdAt, true)"> | <span class="transaction-date" :title="formatDate(contribution.createdAt, true)"> | ||||
{{ formatDate(contribution.createdAt, true) }} | {{ formatDate(contribution.createdAt, true) }} | ||||
</span> | </span> | ||||
</td> | </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> | </tr> | ||||
</tbody> | </tbody> | ||||
</table> | </table> | ||||
</div> | </div> | ||||
</section> | </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)"> | <section v-if="store.getters.getToken && hasPermission(Privileges.AdminPlus)"> | ||||
<div class="title is-size-4 has-text-white-ter has-text-centered"> | <div class="title is-size-4 has-text-white-ter has-text-centered"> | ||||
Close Group Fund | Close Group Fund | ||||
@@ -172,7 +186,7 @@ | |||||
<div class="box is-flex is-flex-direction-row is-justify-content-space-between"> | <div class="box is-flex is-flex-direction-row is-justify-content-space-between"> | ||||
<div class="my-auto"> | <div class="my-auto"> | ||||
<label class="checkbox" for="delete-confirm"> | <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> | </label> | ||||
</div> | </div> | ||||
<div> | <div> | ||||
@@ -207,7 +221,10 @@ import { | |||||
ref, | ref, | ||||
watch, | watch, | ||||
} from 'vue'; | } from 'vue'; | ||||
import { useWebSocket } from '@vueuse/core'; | |||||
import { | |||||
useWebSocket, | |||||
useWindowSize, | |||||
} from '@vueuse/core'; | |||||
import store from '@/store'; | import store from '@/store'; | ||||
import { | import { | ||||
sanitize, | sanitize, | ||||
@@ -222,7 +239,9 @@ import { | |||||
getBalance, | getBalance, | ||||
getContributions, | getContributions, | ||||
getRewardFund, | getRewardFund, | ||||
submitRewardFund, | |||||
} from '@/api/composed'; | } from '@/api/composed'; | ||||
import Decimal from 'decimal.js'; | |||||
const route = useRoute(); | const route = useRoute(); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
@@ -238,10 +257,23 @@ const formatDate = (time: string, includeTime = false) => { | |||||
}; | }; | ||||
const pk = ref(''); | 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 selectableDates = ref([undefined] as (string | undefined)[]); | ||||
const selectedDate = 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 allowDelete = ref(false); | ||||
const deleteFund = async () => { | const deleteFund = async () => { | ||||
const deleted = await deleteRewardFund(identifier, allowDelete.value); | const deleted = await deleteRewardFund(identifier, allowDelete.value); | ||||
@@ -249,6 +281,32 @@ const deleteFund = async () => { | |||||
await router.push('/'); | 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 enableConsolidation = ref(false); | ||||
const fund = ref( | const fund = ref( | ||||
@@ -278,11 +336,11 @@ fundDetails.value = [ | |||||
val: fund.value.fundInfo.asset, | val: fund.value.fundInfo.asset, | ||||
}, | }, | ||||
{ | { | ||||
title: 'Min', | |||||
title: 'Minimum', | |||||
val: `${fund.value.fundInfo.minContribution.toLocaleString()}`, | val: `${fund.value.fundInfo.minContribution.toLocaleString()}`, | ||||
}, | }, | ||||
{ | { | ||||
title: 'Goal', | |||||
title: 'Remaining', | |||||
val: `${fund.value.fundInfo.amountAvailable.toLocaleString()}`, | 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 maxBonus = ref(0); | ||||
const bonus = ref(undefined as Bonus | undefined); | 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 offset = ref(contributions.value.length); | ||||
const total = ref(fund.value.contributions.total); | const total = ref(fund.value.contributions.total); | ||||
const contributionsLoading = ref(false); | const contributionsLoading = ref(false); | ||||
const acctBalance = ref(undefined as number | undefined); | |||||
const acctBalance = ref(undefined as Decimal | undefined); | |||||
const unknownAcct = ref(true); | const unknownAcct = ref(true); | ||||
const loading = ref({ | const loading = ref({ | ||||
contribution: false, | contribution: false, | ||||
balance: 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 = () => { | const hasInvalidValues = () => { | ||||
if (!fund.value) throw new Error('Fund was not loaded!'); | 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); | || unknownAcct.value); | ||||
}; | }; | ||||
@@ -339,18 +415,17 @@ const getCurrentBonus = () => { | |||||
} | } | ||||
}; | }; | ||||
const calculateReward = (bought: number) => { | |||||
const calculateReward = (bought: Decimal) => { | |||||
if (bonus.value) { | if (bonus.value) { | ||||
if (!bonus.value.percent) throw new Error('Bonus did not have percent for some reason'); | 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 { | } else { | ||||
reward.value = bought; | reward.value = bought; | ||||
} | } | ||||
return reward.value.toLocaleString(); | return reward.value.toLocaleString(); | ||||
}; | }; | ||||
const fixNewlines = (s: string) => s.replace('\n', '<br/>'); | |||||
const errs: SignetError[] = [ | const errs: SignetError[] = [ | ||||
{ | { | ||||
text: 'Amount is required', | text: 'Amount is required', | ||||
@@ -358,11 +433,11 @@ const errs: SignetError[] = [ | |||||
}, | }, | ||||
{ | { | ||||
text: 'Amount must be greater than 0', | 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', | 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`, | 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()})`, | 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', | text: 'Could not find Stellar wallet', | ||||
@@ -385,7 +460,7 @@ watch(selectedDate, async (newVal) => { | |||||
const conts = await getContributions(identifier, offset.value, newVal, enableConsolidation.value); | const conts = await getContributions(identifier, offset.value, newVal, enableConsolidation.value); | ||||
if (!fund.value) throw new Error('Fund not found'); | if (!fund.value) throw new Error('Fund not found'); | ||||
if (!conts) throw new Error('Contributions not found'); | if (!conts) throw new Error('Contributions not found'); | ||||
contributions.value = conts.list; | |||||
contributions.value = processContributions(conts.list); | |||||
offset.value = contributions.value.length; | offset.value = contributions.value.length; | ||||
total.value = fund.value.contributions.total; | total.value = fund.value.contributions.total; | ||||
}); | }); | ||||
@@ -407,7 +482,7 @@ const loadMoreIfNeeded = async (e: Event) => { | |||||
if (!moreContribs) throw new Error('Contributions not found'); | if (!moreContribs) throw new Error('Contributions not found'); | ||||
offset.value += moreContribs.list.length; | offset.value += moreContribs.list.length; | ||||
total.value = moreContribs.total; | total.value = moreContribs.total; | ||||
contributions.value = contributions.value.concat(moreContribs.list); | |||||
contributions.value = contributions.value.concat(processContributions(moreContribs.list)); | |||||
contributionsLoading.value = false; | contributionsLoading.value = false; | ||||
} | } | ||||
}; | }; | ||||
@@ -432,9 +507,9 @@ const queryAccount = async () => { | |||||
acctBalance.value = undefined; | acctBalance.value = undefined; | ||||
} else { | } else { | ||||
unknownAcct.value = false; | 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; | amt.value = acctBalance.value; | ||||
} | } | ||||
} | } | ||||
@@ -445,12 +520,13 @@ const queryAccount = async () => { | |||||
const makeContribution = async () => { | const makeContribution = async () => { | ||||
if (!fund.value) throw new Error('Fund not found'); | 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 (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; | 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; | loading.value.contribution = false; | ||||
pk.value = ''; | pk.value = ''; | ||||
amt.value = undefined; | amt.value = undefined; | ||||
@@ -467,7 +543,7 @@ watch(enableConsolidation, async () => { | |||||
); | ); | ||||
if (!fund.value) throw new Error('Fund not found'); | if (!fund.value) throw new Error('Fund not found'); | ||||
if (!conts) throw new Error('Contributions not found'); | if (!conts) throw new Error('Contributions not found'); | ||||
contributions.value = conts.list; | |||||
contributions.value = processContributions(conts.list); | |||||
offset.value = contributions.value.length; | offset.value = contributions.value.length; | ||||
total.value = fund.value.contributions.total; | total.value = fund.value.contributions.total; | ||||
}); | }); | ||||
@@ -488,13 +564,13 @@ watch(data, (newVal) => { | |||||
.includes(v.wallet)) { | .includes(v.wallet)) { | ||||
const hasContribution = contributions.value.find((c: Contribution) => c.wallet === v.wallet); | const hasContribution = contributions.value.find((c: Contribution) => c.wallet === v.wallet); | ||||
if (!hasContribution) throw new Error('Something went wrong'); | if (!hasContribution) throw new Error('Something went wrong'); | ||||
hasContribution.amount += v.amount; | |||||
hasContribution.amount = new Decimal(hasContribution.amount).add(v.amount); | |||||
} else { | } else { | ||||
contributions.value.splice(0, 0, v); | contributions.value.splice(0, 0, v); | ||||
offset.value += 1; | 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> | </script> | ||||