Merge branch 'master' into test-login-user-resolver

This commit is contained in:
Ulf Gebhardt 2022-03-04 15:50:23 +01:00 committed by GitHub
commit 103f727798
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1278 additions and 740 deletions

View File

@ -36,6 +36,7 @@
"graphql": "^15.6.1",
"identity-obj-proxy": "^3.0.0",
"jest": "26.6.3",
"portal-vue": "^2.1.7",
"regenerator-runtime": "^0.13.9",
"stats-webpack-plugin": "^0.7.0",
"vue": "^2.6.11",
@ -43,7 +44,6 @@
"vue-i18n": "^8.26.5",
"vue-jest": "^3.0.7",
"vue-router": "^3.5.3",
"vue-toasted": "^1.1.28",
"vuex": "^3.6.2",
"vuex-persistedstate": "^4.1.0"
},

View File

@ -1,21 +1,17 @@
import { mount } from '@vue/test-utils'
import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue()
const toastSuccessMock = jest.fn()
const toastErrorMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$apollo: {
mutate: apolloMutateMock,
},
$toasted: {
success: toastSuccessMock,
error: toastErrorMock,
},
}
const propsData = {
@ -54,7 +50,7 @@ describe('ConfirmRegisterMailFormular', () => {
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith('unregister_mail.success')
expect(toastSuccessSpy).toBeCalledWith('unregister_mail.success')
})
})
@ -66,7 +62,7 @@ describe('ConfirmRegisterMailFormular', () => {
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('unregister_mail.error')
expect(toastErrorSpy).toBeCalledWith('unregister_mail.error')
})
})
})

View File

@ -48,10 +48,10 @@ export default {
},
})
.then(() => {
this.$toasted.success(this.$t('unregister_mail.success', { email: this.email }))
this.toastSuccess(this.$t('unregister_mail.success', { email: this.email }))
})
.catch((error) => {
this.$toasted.error(this.$t('unregister_mail.error', { message: error.message }))
this.toastError(this.$t('unregister_mail.error', { message: error.message }))
})
},
},

View File

@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
import CreationFormular from './CreationFormular.vue'
import { createPendingCreation } from '../graphql/createPendingCreation'
import { createPendingCreations } from '../graphql/createPendingCreations'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -11,8 +12,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
},
})
const stateCommitMock = jest.fn()
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const mocks = {
$t: jest.fn((t, options) => (options ? [t, options] : t)),
@ -32,10 +31,6 @@ const mocks = {
},
},
},
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
}
const propsData = {
@ -140,7 +135,7 @@ describe('CreationFormular', () => {
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith([
expect(toastSuccessSpy).toBeCalledWith([
'creation_form.toasted',
{ email: 'benjamin@bluemchen.de', value: '90' },
])
@ -162,7 +157,7 @@ describe('CreationFormular', () => {
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
@ -292,7 +287,7 @@ describe('CreationFormular', () => {
})
it('toast success message', () => {
expect(toastedSuccessMock).toBeCalled()
expect(toastSuccessSpy).toBeCalled()
})
it('store commit openCreationPlus', () => {
@ -426,13 +421,14 @@ describe('CreationFormular', () => {
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 0)
})
it('toasts two errors', () => {
expect(toastedErrorMock).toBeCalledWith(
'Could not created PendingCreation for bob@baumeister.de',
)
expect(toastedErrorMock).toBeCalledWith(
'Could not created PendingCreation for bibi@bloxberg.de',
)
it('emits remove all bookmarks', () => {
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
})
it('emits toast failed creations with two emails', () => {
expect(wrapper.emitted('toast-failed-creations')).toEqual([
[['bob@baumeister.de', 'bibi@bloxberg.de']],
])
})
})
@ -454,7 +450,7 @@ describe('CreationFormular', () => {
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Oh no!')
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})

View File

@ -166,20 +166,21 @@ export default {
fetchPolicy: 'no-cache',
})
.then((result) => {
const failedCreations = []
this.$store.commit(
'openCreationsPlus',
result.data.createPendingCreations.successfulCreation.length,
)
if (result.data.createPendingCreations.failedCreation.length > 0) {
result.data.createPendingCreations.failedCreation.forEach((failed) => {
// TODO: Please localize this error message
this.$toasted.error('Could not created PendingCreation for ' + failed)
result.data.createPendingCreations.failedCreation.forEach((email) => {
failedCreations.push(email)
})
}
this.$emit('remove-all-bookmark')
this.$emit('toast-failed-creations', failedCreations)
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
} else if (this.type === 'singleCreation') {
submitObj = {
@ -196,19 +197,19 @@ export default {
})
.then((result) => {
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
this.$toasted.success(
this.$store.commit('openCreationsPlus', 1)
this.toastSuccess(
this.$t('creation_form.toasted', {
value: this.value,
email: this.item.email,
}),
)
this.$store.commit('openCreationsPlus', 1)
// what is this? Tests says that this.text is not reseted
this.$refs.creationForm.reset()
this.value = 0
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
this.$refs.creationForm.reset()
this.value = 0
})

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -50,17 +51,12 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
},
})
const toastedErrorMock = jest.fn()
const mocks = {
$d: jest.fn((t) => t),
$t: jest.fn((t) => t),
$apollo: {
query: apolloQueryMock,
},
$toasted: {
error: toastedErrorMock,
},
}
const propsData = {
@ -109,7 +105,7 @@ describe('CreationTransactionListFormular', () => {
})
it('toast error', () => {
expect(toastedErrorMock).toBeCalledWith('OUCH!')
expect(toastErrorSpy).toBeCalledWith('OUCH!')
})
})
})

View File

@ -62,7 +62,7 @@ export default {
this.items = result.data.transactionList.transactions.filter((t) => t.type === 'creation')
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
},

View File

@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
import DeletedUserFormular from './DeletedUserFormular.vue'
import { deleteUser } from '../graphql/deleteUser'
import { unDeleteUser } from '../graphql/unDeleteUser'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -13,9 +14,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
},
})
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$apollo: {
@ -29,10 +27,6 @@ const mocks = {
},
},
},
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
}
const propsData = {
@ -118,10 +112,6 @@ describe('DeletedUserFormular', () => {
)
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('user_deleted')
})
it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect.arrayContaining([
@ -147,7 +137,7 @@ describe('DeletedUserFormular', () => {
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Oh no!')
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
@ -215,10 +205,6 @@ describe('DeletedUserFormular', () => {
)
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('user_recovered')
})
it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect.arrayContaining([
@ -244,7 +230,7 @@ describe('DeletedUserFormular', () => {
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Oh no!')
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})

View File

@ -45,7 +45,6 @@ export default {
},
})
.then((result) => {
this.$toasted.success(this.$t('user_deleted'))
this.$emit('updateDeletedAt', {
userId: this.item.userId,
deletedAt: result.data.deleteUser,
@ -53,7 +52,7 @@ export default {
this.checked = false
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
unDeleteUser() {
@ -65,7 +64,7 @@ export default {
},
})
.then((result) => {
this.$toasted.success(this.$t('user_recovered'))
this.toastSuccess(this.$t('user_recovered'))
this.$emit('updateDeletedAt', {
userId: this.item.userId,
deletedAt: result.data.unDeleteUser,
@ -73,7 +72,7 @@ export default {
this.checked = false
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
},

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import EditCreationFormular from './EditCreationFormular.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -16,8 +17,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
})
const stateCommitMock = jest.fn()
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
@ -37,10 +36,6 @@ const mocks = {
},
commit: stateCommitMock,
},
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
}
const now = new Date(Date.now())
@ -142,7 +137,7 @@ describe('EditCreationFormular', () => {
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_update')
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_update')
})
})
@ -155,7 +150,7 @@ describe('EditCreationFormular', () => {
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Oh no!')
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})

View File

@ -132,7 +132,7 @@ export default {
moderator: Number(result.data.updatePendingCreation.moderator),
row: this.row,
})
this.$toasted.success(
this.toastSuccess(
this.$t('creation_form.toasted_update', {
value: this.value,
email: this.item.email,
@ -144,7 +144,7 @@ export default {
this.value = 0
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
// das creation Formular reseten
this.$refs.updateCreationForm.reset()
// Den geschöpften Wert auf o setzen

View File

@ -73,10 +73,6 @@ const mocks = {
},
},
},
$toasted: {
error: jest.fn(),
success: jest.fn(),
},
}
describe('SearchUserTable', () => {

View File

@ -5,6 +5,7 @@
"confirmed": "bestätigt",
"creation": "Schöpfung",
"creation_form": {
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
"creation_for": "Aktives Grundeinkommen für",
"enter_text": "Text eintragen",
"form": "Schöpfungsformular",
@ -28,6 +29,7 @@
"delete_user": "Nutzer löschen",
"details": "Details",
"edit": "Bearbeiten",
"error": "Fehler",
"e_mail": "E-Mail",
"firstname": "Vorname",
"gradido_admin_footer": "Gradido Akademie Adminkonsole",
@ -68,6 +70,7 @@
"remove_all": "alle Nutzer entfernen",
"save": "Speichern",
"status": "Status",
"success": "Erfolg",
"text": "Text",
"transaction": "Transaktion",
"transactionlist": {

View File

@ -5,6 +5,7 @@
"confirmed": "confirmed",
"creation": "Creation",
"creation_form": {
"creation_failed": "Could not create pending creation for {email}",
"creation_for": "Active Basic Income for",
"enter_text": "Enter text",
"form": "Creation form",
@ -28,6 +29,7 @@
"delete_user": "Delete user",
"details": "Details",
"edit": "Edit",
"error": "Error",
"e_mail": "E-mail",
"firstname": "Firstname",
"gradido_admin_footer": "Gradido Academy Admin Console",
@ -68,6 +70,7 @@
"remove_all": "Remove all users",
"save": "Speichern",
"status": "Status",
"success": "Success",
"text": "Text",
"transaction": "Transaction",
"transactionlist": {

View File

@ -13,31 +13,24 @@ import i18n from './i18n'
import VueApollo from 'vue-apollo'
import PortalVue from 'portal-vue'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import Toasted from 'vue-toasted'
import { toasters } from './mixins/toaster'
import { apolloProvider } from './plugins/apolloProvider'
Vue.use(PortalVue)
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)
Vue.use(VueApollo)
Vue.use(Toasted, {
position: 'top-center',
duration: 5000,
fullWidth: true,
action: {
text: 'x',
onClick: (e, toastObject) => {
toastObject.goAway(0)
},
},
})
Vue.mixin(toasters)
addNavigationGuards(router, store, apolloProvider.defaultClient, i18n)

View File

@ -0,0 +1,30 @@
export const toasters = {
methods: {
toastSuccess(message) {
this.toast(message, {
title: this.$t('success'),
variant: 'success',
})
},
toastError(message) {
this.toast(message, {
title: this.$t('error'),
variant: 'danger',
})
},
toast(message, options) {
// for unit tests, check that replace is present
if (message.replace) message = message.replace(/^GraphQL error: /, '')
this.$bvToast.toast(message, {
autoHideDelay: 5000,
appendToast: true,
solid: true,
toaster: 'b-toaster-top-right',
headerClass: 'gdd-toaster-title',
bodyClass: 'gdd-toaster-body',
toastClass: 'gdd-toaster',
...options,
})
},
},
}

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import Creation from './Creation.vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -29,18 +30,14 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
},
})
const toastErrorMock = jest.fn()
const storeCommitMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$t: jest.fn((t, options) => (options ? [t, options] : t)),
$d: jest.fn((d) => d),
$apollo: {
query: apolloQueryMock,
},
$toasted: {
error: toastErrorMock,
},
$store: {
commit: storeCommitMock,
state: {
@ -236,6 +233,25 @@ describe('Creation', () => {
})
})
describe('failed creations', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'CreationFormular' })
.vm.$emit('toast-failed-creations', ['bibi@bloxberg.de', 'benjamin@bluemchen.de'])
})
it('toasts two error messages', () => {
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'bibi@bloxberg.de' },
])
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'benjamin@bluemchen.de' },
])
})
})
describe('watchers', () => {
beforeEach(() => {
jest.clearAllMocks()
@ -298,7 +314,7 @@ describe('Creation', () => {
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Ouch')
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
})

View File

@ -56,6 +56,7 @@
:creation="creation"
:items="itemsMassCreation"
@remove-all-bookmark="removeAllBookmarks"
@toast-failed-creations="toastFailedCreations"
/>
</b-col>
</b-row>
@ -118,7 +119,7 @@ export default {
}
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
pushItem(selectedItem) {
@ -144,6 +145,11 @@ export default {
this.$store.commit('setUserSelectedInMassCreation', [])
this.getUsers()
},
toastFailedCreations(failedCreations) {
failedCreations.forEach((email) =>
this.toastError(this.$t('creation_form.creation_failed', { email })),
)
},
},
computed: {
Searchfields() {

View File

@ -2,12 +2,11 @@ import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue'
import { deletePendingCreation } from '../graphql/deletePendingCreation'
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
const storeCommitMock = jest.fn()
const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
getPendingCreations: [
@ -47,10 +46,6 @@ const mocks = {
query: apolloQueryMock,
mutate: apolloMutateMock,
},
$toasted: {
error: toastedErrorMock,
success: toastedSuccessMock,
},
}
describe('CreationConfirm', () => {
@ -101,7 +96,7 @@ describe('CreationConfirm', () => {
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_delete')
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
})
})
@ -112,7 +107,7 @@ describe('CreationConfirm', () => {
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouchhh!')
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
})
})
@ -158,7 +153,7 @@ describe('CreationConfirm', () => {
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_created')
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_created')
})
it('has 1 item left in the table', () => {
@ -173,7 +168,7 @@ describe('CreationConfirm', () => {
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouchhh!')
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
})
})
})
@ -189,7 +184,7 @@ describe('CreationConfirm', () => {
})
it('toast an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
})

View File

@ -43,10 +43,10 @@ export default {
})
.then((result) => {
this.updatePendingCreations(item.id)
this.$toasted.success(this.$t('creation_form.toasted_delete'))
this.toastSuccess(this.$t('creation_form.toasted_delete'))
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
confirmCreation() {
@ -60,11 +60,11 @@ export default {
.then((result) => {
this.overlay = false
this.updatePendingCreations(this.item.id)
this.$toasted.success(this.$t('creation_form.toasted_created'))
this.toastSuccess(this.$t('creation_form.toasted_created'))
})
.catch((error) => {
this.overlay = false
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
getPendingCreations() {
@ -79,7 +79,7 @@ export default {
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
updatePendingCreations(id) {

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import UserSearch from './UserSearch.vue'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -15,6 +16,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
deletedAt: null,
},
{
userId: 2,
@ -23,6 +25,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
emailChecked: true,
deletedAt: null,
},
{
userId: 3,
@ -31,6 +34,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'peter@lustig.de',
creation: [0, 0, 0],
emailChecked: true,
deletedAt: null,
},
{
userId: 4,
@ -39,23 +43,19 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'new@user.ch',
creation: [1000, 1000, 1000],
emailChecked: false,
deletedAt: null,
},
],
},
},
})
const toastErrorMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => String(d)),
$apollo: {
query: apolloQueryMock,
},
$toasted: {
error: toastErrorMock,
},
}
describe('UserSearch', () => {
@ -187,6 +187,21 @@ describe('UserSearch', () => {
})
})
describe('delete user', () => {
const now = new Date()
beforeEach(async () => {
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now)
})
it('marks the user as deleted', () => {
expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('user_deleted')
})
})
describe('apollo returns error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({
@ -196,7 +211,7 @@ describe('UserSearch', () => {
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('Ouch')
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
})

View File

@ -94,11 +94,12 @@ export default {
this.searchResult = result.data.searchUsers.userList
})
.catch((error) => {
this.$toasted.error(error.message)
this.toastError(error.message)
})
},
updateDeletedAt(userId, deletedAt) {
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
this.toastSuccess(this.$t('user_deleted'))
},
},
watch: {

View File

@ -5,11 +5,18 @@ import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
// without this async calls are not working
import 'regenerator-runtime'
import { toasters } from '../src/mixins/toaster'
export const toastErrorSpy = jest.spyOn(toasters.methods, 'toastError')
export const toastSuccessSpy = jest.spyOn(toasters.methods, 'toastSuccess')
global.localVue = createLocalVue()
global.localVue.use(BootstrapVue)
global.localVue.use(IconsPlugin)
global.localVue.mixin(toasters)
// throw errors for vue warnings to force the programmers to take care about warnings
Vue.config.warnHandler = (w) => {
throw new Error(w)

View File

@ -12512,11 +12512,6 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue-toasted@^1.1.28:
version "1.1.28"
resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.28.tgz#dbabb83acc89f7a9e8765815e491d79f0dc65c26"
integrity sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw==
vue@^2.6.11:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"

View File

@ -24,6 +24,7 @@
"axios": "^0.21.1",
"class-validator": "^0.13.1",
"cors": "^2.8.5",
"decimal.js-light": "^2.5.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"graphql": "^15.5.1",

View File

@ -1,10 +1,16 @@
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
import dotenv from 'dotenv'
import Decimal from 'decimal.js-light'
dotenv.config()
Decimal.set({
precision: 25,
rounding: Decimal.ROUND_HALF_UP,
})
const constants = {
DB_VERSION: '0027-clean_transaction_table',
DB_VERSION: '0029-clean_transaction_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
}

View File

@ -1,12 +1,13 @@
import { ArgsType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ArgsType()
export default class TransactionSendArgs {
@Field(() => String)
email: string
@Field(() => Number)
amount: number
@Field(() => Decimal)
amount: Decimal
@Field(() => String)
memo: string

View File

@ -4,6 +4,8 @@ export enum TransactionTypeId {
CREATION = 1,
SEND = 2,
RECEIVE = 3,
// This is a virtual property, never occurring on the database
DECAY = 4,
}
registerEnumType(TransactionTypeId, {

View File

@ -1,21 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ObjectType()
export class Balance {
constructor(json: any) {
this.balance = Number(json.balance)
this.decay = Number(json.decay)
this.balance = json.balance
this.decay = json.decay
this.decayDate = json.decay_date
}
@Field(() => Number)
balance: number
@Field(() => Decimal)
balance: Decimal
@Field(() => Number)
decay: number
@Field(() => Decimal)
decay: Decimal
@Field(() => String)
decayDate: string
@Field(() => Date)
decayDate: Date
}

View File

@ -1,33 +1,34 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field, Int } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ObjectType()
export class Decay {
constructor(json?: any) {
if (json) {
this.balance = Number(json.balance)
this.decayStart = json.decay_start
this.decayEnd = json.decay_end
this.decayDuration = json.decay_duration
this.decayStartBlock = json.decay_start_block
}
constructor(
balance: Decimal,
decay: Decimal,
start: Date | null,
end: Date | null,
duration: number | null,
) {
this.balance = balance
this.decay = decay
this.start = start
this.end = end
this.duration = duration
}
@Field(() => Number)
balance: number
@Field(() => Decimal)
balance: Decimal
// timestamp in seconds
@Field(() => Int, { nullable: true })
decayStart: string
@Field(() => Decimal)
decay: Decimal
// timestamp in seconds
@Field(() => Int, { nullable: true })
decayEnd: string
@Field(() => Date, { nullable: true })
start: Date | null
@Field(() => String, { nullable: true })
decayDuration?: number
@Field(() => Date, { nullable: true })
end: Date | null
@Field(() => Int, { nullable: true })
decayStartBlock?: string
duration: number | null
}

View File

@ -1,59 +1,70 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
import { Decay } from './Decay'
// we need a better solution for the decay block:
// the first transaction on the first page shows the decay since the last transaction
// the format is actually a Decay and not a Transaction.
// Therefore we have a lot of nullable fields, which should be always present
import { Transaction as dbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { User } from './User'
@ObjectType()
export class Transaction {
constructor() {
this.type = ''
this.balance = 0
this.totalBalance = 0
this.memo = ''
this.firstTransaction = false
constructor(transaction: dbTransaction, user: User, linkedUser: User | null = null) {
this.id = transaction.id
this.user = user
this.previous = transaction.previous
this.typeId = transaction.typeId
this.amount = transaction.amount
this.balance = transaction.balance
this.balanceDate = transaction.balanceDate
if (!transaction.decayStart) {
this.decay = new Decay(transaction.balance, new Decimal(0), null, null, null)
} else {
this.decay = new Decay(
transaction.balance,
transaction.decay,
transaction.decayStart,
transaction.balanceDate,
Math.round((transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000),
)
}
this.memo = transaction.memo
this.creationDate = transaction.creationDate
this.linkedUser = linkedUser
this.linkedTransactionId = transaction.linkedTransactionId
}
@Field(() => String)
type: string
@Field(() => Number)
balance: number
id: number
@Field(() => Number)
totalBalance: number
@Field(() => User)
user: User
@Field({ nullable: true })
decayStart?: string
@Field(() => Number, { nullable: true })
previous: number | null
@Field({ nullable: true })
decayEnd?: string
@Field(() => TransactionTypeId)
typeId: TransactionTypeId
@Field({ nullable: true })
decayDuration?: number
@Field(() => Decimal)
amount: Decimal
@Field(() => Decimal)
balance: Decimal
@Field(() => Date)
balanceDate: Date
@Field(() => Decay)
decay: Decay
@Field(() => String)
memo: string
@Field(() => Date, { nullable: true })
creationDate: Date | null
@Field(() => User, { nullable: true })
linkedUser: User | null
@Field(() => Number, { nullable: true })
transactionId?: number
@Field({ nullable: true })
name?: string
@Field({ nullable: true })
email?: string
@Field({ nullable: true })
date?: string
@Field({ nullable: true })
decay?: Decay
@Field(() => Boolean)
firstTransaction: boolean
linkedTransactionId?: number | null
}

View File

@ -1,32 +1,35 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
import CONFIG from '../../config'
import Decimal from 'decimal.js-light'
import { Transaction } from './Transaction'
@ObjectType()
export class TransactionList {
constructor() {
this.gdtSum = 0
this.count = 0
this.balance = 0
this.decay = 0
this.decayDate = ''
constructor(
balance: Decimal,
transactions: Transaction[],
count: number,
balanceGDT?: number | null,
decayStartBlock: Date = CONFIG.DECAY_START_TIME,
) {
this.balance = balance
this.transactions = transactions
this.count = count
this.balanceGDT = balanceGDT || null
this.decayStartBlock = decayStartBlock
}
@Field(() => Number, { nullable: true })
gdtSum: number | null
balanceGDT: number | null
@Field(() => Number)
count: number
@Field(() => Number)
balance: number
balance: Decimal
@Field(() => Number)
decay: number
@Field(() => String)
decayDate: string
@Field(() => Date)
decayStartBlock: Date
@Field(() => [Transaction])
transactions: Transaction[]

View File

@ -1,75 +1,74 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field, Int } from 'type-graphql'
import { ObjectType, Field } from 'type-graphql'
import { KlickTipp } from './KlickTipp'
import { User as dbUser } from '@entity/User'
@ObjectType()
export class User {
/*
@Field(() => ID)
@PrimaryGeneratedColumn()
id: number
*/
constructor(json?: any) {
if (json) {
this.id = json.id
this.email = json.email
this.firstName = json.first_name
this.lastName = json.last_name
this.pubkey = json.public_hex
this.language = json.language
this.publisherId = json.publisher_id
this.isAdmin = json.isAdmin
}
constructor(user: dbUser) {
this.id = user.id
this.email = user.email
this.firstName = user.firstName
this.lastName = user.lastName
this.deletedAt = user.deletedAt
this.createdAt = user.createdAt
this.emailChecked = user.emailChecked
this.language = user.language
this.publisherId = user.publisherId
// TODO
this.isAdmin = null
this.coinanimation = null
this.klickTipp = null
this.hasElopage = null
}
@Field(() => Number)
id: number
// `public_key` binary(32) DEFAULT NULL,
// `privkey` binary(80) DEFAULT NULL,
// TODO privacy issue here
@Field(() => String)
email: string
@Field(() => String)
firstName: string
@Field(() => String, { nullable: true })
firstName: string | null
@Field(() => String)
lastName: string
@Field(() => String, { nullable: true })
lastName: string | null
@Field(() => String)
pubkey: string
/*
@Field(() => String)
pubkey: string
@Field(() => Date, { nullable: true })
deletedAt: Date | null
// not sure about the type here. Maybe better to have a string
@Field(() => number)
created: number
// `password` bigint(20) unsigned DEFAULT 0,
// `email_hash` binary(32) DEFAULT NULL,
@Field(() =>>> Boolean)
@Field(() => Date)
createdAt: Date
@Field(() => Boolean)
emailChecked: boolean
*/
@Field(() => String)
language: string
/*
@Field(() => Boolean)
disabled: boolean
*/
// This is not the users publisherId, but the one of the users who recommend him
@Field(() => Number, { nullable: true })
publisherId: number | null
// what is publisherId?
@Field(() => Int, { nullable: true })
publisherId?: number
// `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
@Field(() => Boolean)
isAdmin: boolean
@Field(() => Boolean)
coinanimation: boolean
@Field(() => KlickTipp)
klickTipp: KlickTipp
// TODO this is a bit inconsistent with what we query from the database
// therefore all those fields are now nullable with default value null
@Field(() => Boolean, { nullable: true })
isAdmin: boolean | null
@Field(() => Boolean, { nullable: true })
hasElopage?: boolean
coinanimation: boolean | null
@Field(() => KlickTipp, { nullable: true })
klickTipp: KlickTipp | null
@Field(() => Boolean, { nullable: true })
hasElopage: boolean | null
}

View File

@ -27,7 +27,7 @@ import { hasElopageBuys } from '../../util/hasElopageBuys'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { User } from '@entity/User'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { Balance } from '@entity/Balance'
import Decimal from 'decimal.js-light'
// const EMAIL_OPT_IN_REGISTER = 1
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
@ -307,37 +307,30 @@ export class AdminResolver {
const receivedCallDate = new Date()
const transactionRepository = getCustomRepository(TransactionRepository)
const lastUserTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
const lastTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
let newBalance = 0
if (lastUserTransaction) {
let newBalance = new Decimal(0)
if (lastTransaction) {
newBalance = calculateDecay(
Number(lastUserTransaction.balance),
lastUserTransaction.balanceDate,
lastTransaction.balance,
lastTransaction.balanceDate,
receivedCallDate,
).balance
}
newBalance = Number(newBalance) + Number(parseInt(pendingCreation.amount.toString()))
// TODO pending creations decimal
newBalance = newBalance.add(new Decimal(Number(pendingCreation.amount)))
const transaction = new Transaction()
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = pendingCreation.memo
transaction.userId = pendingCreation.userId
transaction.amount = BigInt(parseInt(pendingCreation.amount.toString()))
// TODO pending creations decimal
transaction.amount = new Decimal(Number(pendingCreation.amount))
transaction.creationDate = pendingCreation.date
transaction.balance = BigInt(newBalance)
transaction.balance = newBalance
transaction.balanceDate = receivedCallDate
await transaction.save()
let userBalance = await Balance.findOne({ userId: pendingCreation.userId })
if (!userBalance) {
userBalance = new Balance()
userBalance.userId = pendingCreation.userId
}
userBalance.amount = Number(newBalance)
userBalance.modified = receivedCallDate
userBalance.recordDate = receivedCallDate
await userBalance.save()
await AdminPendingCreation.delete(pendingCreation)
return true

View File

@ -6,9 +6,9 @@ import { getCustomRepository } from '@dbTools/typeorm'
import { Balance } from '../model/Balance'
import { UserRepository } from '../../typeorm/repository/User'
import { calculateDecay } from '../../util/decay'
import { roundFloorFrom4 } from '../../util/round'
import { RIGHTS } from '../../auth/RIGHTS'
import { Balance as dbBalance } from '@entity/Balance'
import { Transaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
@Resolver()
export class BalanceResolver {
@ -18,24 +18,26 @@ export class BalanceResolver {
// load user and balance
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const balanceEntity = await dbBalance.findOne({ userId: userEntity.id })
const user = await userRepository.findByPubkeyHex(context.pubKey)
const now = new Date()
const lastTransaction = await Transaction.findOne(
{ userId: user.id },
{ order: { balanceDate: 'DESC' } },
)
// No balance found
if (!balanceEntity) {
if (!lastTransaction) {
return new Balance({
balance: 0,
decay: 0,
balance: new Decimal(0),
decay: new Decimal(0),
decay_date: now.toString(),
})
}
return new Balance({
balance: roundFloorFrom4(balanceEntity.amount),
decay: roundFloorFrom4(
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
),
balance: lastTransaction.balance,
decay: calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
decay_date: now.toString(),
})
}

View File

@ -4,7 +4,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
import { getCustomRepository, getConnection, QueryRunner } from '@dbTools/typeorm'
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
import CONFIG from '../../config'
import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail'
@ -22,66 +22,17 @@ import { TransactionRepository } from '../../typeorm/repository/Transaction'
import { User as dbUser } from '@entity/User'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { Balance as dbBalance } from '@entity/Balance'
import { apiPost } from '../../apis/HttpRequest'
import { roundFloorFrom4, roundCeilFrom4 } from '../../util/round'
import { calculateDecay } from '../../util/decay'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { TransactionType } from '../enum/TransactionType'
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
import { calculateBalance, isHexPublicKey } from '../../util/validate'
import { RIGHTS } from '../../auth/RIGHTS'
import { User } from '../model/User'
import { communityUser } from '../../util/communityUser'
import { virtualDecayTransaction } from '../../util/virtualDecayTransaction'
import Decimal from 'decimal.js-light'
import { calculateDecay } from '../../util/decay'
// helper helper function
async function updateStateBalance(
user: dbUser,
balance: number,
received: Date,
queryRunner: QueryRunner,
): Promise<dbBalance> {
let userBalance = await dbBalance.findOne({ userId: user.id })
if (!userBalance) {
userBalance = new dbBalance()
userBalance.userId = user.id
userBalance.amount = balance
userBalance.modified = received
} else {
userBalance.amount = balance
userBalance.modified = new Date()
}
if (userBalance.amount <= 0) {
throw new Error('error new balance <= 0')
}
userBalance.recordDate = received
return queryRunner.manager.save(userBalance).catch((error) => {
throw new Error('error saving balance:' + error)
})
}
async function calculateNewBalance(
userId: number,
transactionDate: Date,
centAmount: number,
): Promise<number> {
let newBalance = centAmount
const transactionRepository = getCustomRepository(TransactionRepository)
const lastUserTransaction = await transactionRepository.findLastForUser(userId)
if (lastUserTransaction) {
newBalance += Number(
calculateDecay(
Number(lastUserTransaction.balance),
lastUserTransaction.balanceDate,
transactionDate,
).balance,
)
}
if (newBalance <= 0) {
throw new Error('error new balance <= 0')
}
return newBalance
}
@Resolver()
export class TransactionResolver {
@Authorized([RIGHTS.TRANSACTION_LIST])
@ -97,165 +48,92 @@ export class TransactionResolver {
}: Paginated,
@Ctx() context: any,
): Promise<TransactionList> {
// load user
const now = new Date()
// find user
const userRepository = getCustomRepository(UserRepository)
// TODO: separate those usecases - this is a security issue
const user = userId
? await userRepository.findOneOrFail({ id: userId }, { withDeleted: true })
: await userRepository.findByPubkeyHex(context.pubKey)
let limit = pageSize
let offset = 0
let skipFirstTransaction = false
if (currentPage > 1) {
offset = (currentPage - 1) * pageSize - 1
limit++
}
if (offset && order === Order.ASC) {
offset--
}
const transactionRepository = getCustomRepository(TransactionRepository)
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
user.id,
limit,
offset,
order,
onlyCreations,
// find current balance
const lastTransaction = await dbTransaction.findOne(
{ userId: user.id },
{ order: { balanceDate: 'DESC' } },
)
skipFirstTransaction = userTransactionsCount > offset + limit
const decay = !(currentPage > 1)
const transactions: Transaction[] = []
if (userTransactions.length) {
if (order === Order.DESC) {
userTransactions.reverse()
}
const involvedUserIds: number[] = []
userTransactions.forEach((transaction: dbTransaction) => {
involvedUserIds.push(transaction.userId)
if (
transaction.typeId === TransactionTypeId.SEND ||
transaction.typeId === TransactionTypeId.RECEIVE
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
involvedUserIds.push(transaction.linkedUserId!) // TODO ensure not null properly
}
})
// remove duplicates
// https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates
const involvedUsersUnique = involvedUserIds.filter((v, i, a) => a.indexOf(v) === i)
const userRepository = getCustomRepository(UserRepository)
const userIndiced = await userRepository.getUsersIndiced(involvedUsersUnique)
for (let i = 0; i < userTransactions.length; i++) {
const userTransaction = userTransactions[i]
const finalTransaction = new Transaction()
finalTransaction.transactionId = userTransaction.id
finalTransaction.date = userTransaction.balanceDate.toISOString()
finalTransaction.memo = userTransaction.memo
finalTransaction.totalBalance = roundFloorFrom4(Number(userTransaction.balance))
const previousTransaction = i > 0 ? userTransactions[i - 1] : null
if (previousTransaction) {
const currentTransaction = userTransaction
const decay = calculateDecay(
Number(previousTransaction.balance),
previousTransaction.balanceDate,
currentTransaction.balanceDate,
)
const balance = Number(previousTransaction.balance) - decay.balance
if (CONFIG.DECAY_START_TIME < currentTransaction.balanceDate) {
finalTransaction.decay = decay
finalTransaction.decay.balance = roundFloorFrom4(balance)
if (
previousTransaction.balanceDate < CONFIG.DECAY_START_TIME &&
currentTransaction.balanceDate > CONFIG.DECAY_START_TIME
) {
finalTransaction.decay.decayStartBlock = (
CONFIG.DECAY_START_TIME.getTime() / 1000
).toString()
}
}
}
finalTransaction.balance = roundFloorFrom4(Number(userTransaction.amount)) // Todo unsafe conversion
const otherUser = userIndiced.find((u) => u.id === userTransaction.linkedUserId)
switch (userTransaction.typeId) {
case TransactionTypeId.CREATION:
finalTransaction.name = 'Gradido Akademie'
finalTransaction.type = TransactionType.CREATION
break
case TransactionTypeId.SEND:
finalTransaction.type = TransactionType.SEND
if (otherUser) {
finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName
finalTransaction.email = otherUser.email
}
break
case TransactionTypeId.RECEIVE:
finalTransaction.type = TransactionType.RECIEVE
if (otherUser) {
finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName
finalTransaction.email = otherUser.email
}
break
default:
throw new Error('invalid transaction')
}
if (i > 0 || !skipFirstTransaction) {
transactions.push(finalTransaction)
}
if (i === userTransactions.length - 1 && decay) {
const now = new Date()
const decay = calculateDecay(
Number(userTransaction.balance),
userTransaction.balanceDate,
now,
)
const balance = Number(userTransaction.balance) - decay.balance
const decayTransaction = new Transaction()
decayTransaction.type = 'decay'
decayTransaction.balance = roundCeilFrom4(balance)
decayTransaction.decayDuration = decay.decayDuration
decayTransaction.decayStart = decay.decayStart
decayTransaction.decayEnd = decay.decayEnd
transactions.push(decayTransaction)
}
}
if (order === Order.DESC) {
transactions.reverse()
}
}
const transactionList = new TransactionList()
transactionList.count = userTransactionsCount
transactionList.transactions = transactions
// get gdt sum
transactionList.gdtSum = null
// get GDT
let balanceGDT = null
try {
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
email: user.email,
})
if (resultGDTSum.success) transactionList.gdtSum = Number(resultGDTSum.data.sum) || 0
} catch (err: any) {}
// get balance
const balanceEntity = await dbBalance.findOne({ userId: user.id })
if (balanceEntity) {
const now = new Date()
transactionList.balance = roundFloorFrom4(balanceEntity.amount)
// TODO: Add a decay object here instead of static data representing the decay.
transactionList.decay = roundFloorFrom4(
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
)
transactionList.decayDate = now.toString()
if (!resultGDTSum.success) {
throw new Error('Call not successful')
}
balanceGDT = Number(resultGDTSum.data.sum) || 0
} catch (err: any) {
// eslint-disable-next-line no-console
console.log('Could not query GDT Server', err)
}
return transactionList
if (!lastTransaction) {
return new TransactionList(new Decimal(0), [], 0, balanceGDT)
}
// find transactions
// first page can contain 26 due to virtual decay transaction
const offset = (currentPage - 1) * pageSize
const transactionRepository = getCustomRepository(TransactionRepository)
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
user.id,
pageSize,
offset,
order,
onlyCreations,
)
// find involved users; I am involved
const involvedUserIds: number[] = [user.id]
userTransactions.forEach((transaction: dbTransaction) => {
if (transaction.linkedUserId && !involvedUserIds.includes(transaction.linkedUserId)) {
involvedUserIds.push(transaction.linkedUserId)
}
})
// We need to show the name for deleted users for old transactions
const involvedDbUsers = await dbUser
.createQueryBuilder()
.withDeleted()
.where('id IN (:...userIds)', { userIds: involvedUserIds })
.getMany()
const involvedUsers = involvedDbUsers.map((u) => new User(u))
const self = new User(user)
const transactions: Transaction[] = []
// decay transaction
if (currentPage === 1 && order === Order.DESC) {
transactions.push(
virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self),
)
}
// transactions
userTransactions.forEach((userTransaction) => {
const linkedUser =
userTransaction.typeId === TransactionTypeId.CREATION
? communityUser
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
transactions.push(new Transaction(userTransaction, self, linkedUser))
})
// Construct Result
return new TransactionList(
calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
transactions,
userTransactionsCount,
balanceGDT,
)
}
@Authorized([RIGHTS.SEND_COINS])
@ -271,7 +149,9 @@ export class TransactionResolver {
throw new Error('invalid sender public key')
}
// validate amount
if (!hasUserAmount(senderUser, amount)) {
const receivedCallDate = new Date()
const sendBalance = await calculateBalance(senderUser.id, amount.mul(-1), receivedCallDate)
if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0")
}
@ -287,24 +167,22 @@ export class TransactionResolver {
throw new Error('invalid recipient public key')
}
const centAmount = Math.round(amount * 10000)
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
const receivedCallDate = new Date()
// transaction
const transactionSend = new dbTransaction()
transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo
transactionSend.userId = senderUser.id
transactionSend.linkedUserId = recipientUser.id
transactionSend.amount = BigInt(centAmount)
const sendBalance = await calculateNewBalance(senderUser.id, receivedCallDate, -centAmount)
transactionSend.balance = BigInt(Math.trunc(sendBalance))
transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate
transactionSend.sendSenderFinalBalance = transactionSend.balance
transactionSend.decay = sendBalance.decay.decay
transactionSend.decayStart = sendBalance.decay.start
transactionSend.previous = sendBalance.lastTransactionId
await queryRunner.manager.insert(dbTransaction, transactionSend)
const transactionReceive = new dbTransaction()
@ -312,15 +190,16 @@ export class TransactionResolver {
transactionReceive.memo = memo
transactionReceive.userId = recipientUser.id
transactionReceive.linkedUserId = senderUser.id
transactionReceive.amount = BigInt(centAmount)
const receiveBalance = await calculateNewBalance(
recipientUser.id,
receivedCallDate,
centAmount,
)
transactionReceive.balance = BigInt(Math.trunc(receiveBalance))
transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipientUser.id, amount, receivedCallDate)
if (!receiveBalance) {
throw new Error('Sender user account corrupted')
}
transactionReceive.balance = receiveBalance.balance
transactionReceive.balanceDate = receivedCallDate
transactionReceive.sendSenderFinalBalance = transactionSend.balance
transactionReceive.decay = receiveBalance.decay.decay
transactionReceive.decayStart = receiveBalance.decay.start
transactionReceive.previous = receiveBalance.lastTransactionId
transactionReceive.linkedTransactionId = transactionSend.id
await queryRunner.manager.insert(dbTransaction, transactionReceive)
@ -328,17 +207,6 @@ export class TransactionResolver {
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
// Update Balance sender
await updateStateBalance(senderUser, Math.trunc(sendBalance), receivedCallDate, queryRunner)
// Update Balance recipient
await updateStateBalance(
recipientUser,
Math.trunc(receiveBalance),
receivedCallDate,
queryRunner,
)
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
@ -346,6 +214,7 @@ export class TransactionResolver {
} finally {
await queryRunner.release()
}
// send notification email
// TODO: translate
await sendTransactionReceivedEmail({

View File

@ -216,14 +216,8 @@ export class UserResolver {
// TODO refactor and do not have duplicate code with login(see below)
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const user = new User()
user.id = userEntity.id
user.email = userEntity.email
user.firstName = userEntity.firstName
user.lastName = userEntity.lastName
user.pubkey = userEntity.pubKey.toString('hex')
user.language = userEntity.language
const user = new User(userEntity)
// user.pubkey = userEntity.pubKey.toString('hex')
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context)
@ -271,12 +265,9 @@ export class UserResolver {
throw new Error('No user with this credentials')
}
const user = new User()
user.id = dbUser.id
user.email = email
user.firstName = dbUser.firstName
user.lastName = dbUser.lastName
user.pubkey = dbUser.pubKey.toString('hex')
const user = new User(dbUser)
// user.email = email
// user.pubkey = dbUser.pubKey.toString('hex')
user.language = dbUser.language
// Elopage Status & Stored PublisherId

View File

@ -0,0 +1,23 @@
import { GraphQLScalarType, Kind } from 'graphql'
import Decimal from 'decimal.js-light'
export default new GraphQLScalarType({
name: 'Decimal',
description: 'The `Decimal` scalar type to represent currency values',
serialize(value: Decimal) {
return value.toString()
},
parseValue(value) {
return new Decimal(value)
},
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new TypeError(`${String(ast)} is not a valid decimal value.`)
}
return new Decimal(ast.value)
},
})

View File

@ -3,11 +3,14 @@ import { buildSchema } from 'type-graphql'
import path from 'path'
import isAuthorized from './directive/isAuthorized'
import DecimalScalar from './scalar/Decimal'
import Decimal from 'decimal.js-light'
const schema = async (): Promise<GraphQLSchema> => {
return buildSchema({
resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)],
authChecker: isAuthorized,
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
})
}

View File

@ -1,5 +1,6 @@
import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail'
import { sendEMail } from './sendEMail'
import Decimal from 'decimal.js-light'
jest.mock('./sendEMail', () => {
return {
@ -16,7 +17,7 @@ describe('sendTransactionReceivedEmail', () => {
recipientFirstName: 'Peter',
recipientLastName: 'Lustig',
email: 'peter@lustig.de',
amount: 42.0,
amount: new Decimal(42.0),
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
})
})

View File

@ -1,3 +1,4 @@
import Decimal from 'decimal.js-light'
import { sendEMail } from './sendEMail'
import { transactionReceived } from './text/transactionReceived'
@ -7,7 +8,7 @@ export const sendTransactionReceivedEmail = (data: {
recipientFirstName: string
recipientLastName: string
email: string
amount: number
amount: Decimal
memo: string
}): Promise<boolean> => {
return sendEMail({

View File

@ -1,3 +1,5 @@
import Decimal from 'decimal.js-light'
export const transactionReceived = {
de: {
subject: 'Gradido Überweisung',
@ -7,7 +9,7 @@ export const transactionReceived = {
recipientFirstName: string
recipientLastName: string
email: string
amount: number
amount: Decimal
memo: string
}): string =>
`Hallo ${data.recipientFirstName} ${data.recipientLastName}

View File

@ -9,14 +9,6 @@ export class UserRepository extends Repository<User> {
.getOneOrFail()
}
async getUsersIndiced(userIds: number[]): Promise<User[]> {
return this.createQueryBuilder('user')
.withDeleted() // We need to show the name for deleted users for old transactions
.select(['user.id', 'user.firstName', 'user.lastName', 'user.email'])
.where('user.id IN (:...userIds)', { userIds })
.getMany()
}
async findBySearchCriteriaPagedFiltered(
select: string[],
searchCriteria: string,

View File

@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { User as dbUser } from '@entity/User'
import { User } from '../graphql/model/User'
const communityDbUser: dbUser = {
id: -1,
email: 'support@gradido.net',
firstName: 'Gradido',
lastName: 'Akademie',
pubKey: Buffer.from(''),
privKey: Buffer.from(''),
deletedAt: null,
password: BigInt(0),
emailHash: Buffer.from(''),
createdAt: new Date(),
emailChecked: false,
language: '',
publisherId: 0,
passphrase: '',
settings: [],
hasId: function (): boolean {
throw new Error('Function not implemented.')
},
save: function (options?: SaveOptions): Promise<dbUser> {
throw new Error('Function not implemented.')
},
remove: function (options?: RemoveOptions): Promise<dbUser> {
throw new Error('Function not implemented.')
},
softRemove: function (options?: SaveOptions): Promise<dbUser> {
throw new Error('Function not implemented.')
},
recover: function (options?: SaveOptions): Promise<dbUser> {
throw new Error('Function not implemented.')
},
reload: function (): Promise<void> {
throw new Error('Function not implemented.')
},
}
const communityUser = new User(communityDbUser)
export { communityDbUser, communityUser }

View File

@ -1,41 +1,41 @@
import Decimal from 'decimal.js-light'
import 'reflect-metadata' // This might be wise to load in a test setup file
import { decayFormula, calculateDecay } from './decay'
describe('utils/decay', () => {
describe('decayFormula', () => {
it('has base 0.99999997802044727', () => {
const amount = 1.0
const amount = new Decimal(1.0)
const seconds = 1
expect(decayFormula(amount, seconds)).toBe(0.99999997802044727)
})
// Not sure if the following skiped tests make sence!?
it('has negative decay?', async () => {
const amount = -1.0
const seconds = 1
expect(await decayFormula(amount, seconds)).toBe(-0.99999997802044727)
// TODO: toString() was required, we could not compare two decimals
expect(decayFormula(amount, seconds).toString()).toBe('0.999999978035040489732012')
})
it('has correct backward calculation', async () => {
const amount = 1.0
const amount = new Decimal(1.0)
const seconds = -1
expect(await decayFormula(amount, seconds)).toBe(1.0000000219795533)
expect(decayFormula(amount, seconds).toString()).toBe('1.000000021964959992727444')
})
// not possible, nodejs hasn't enough accuracy
it('has correct forward calculation', async () => {
const amount = 1.0 / 0.99999997802044727
// we get pretty close, but not exact here, skipping
it.skip('has correct forward calculation', async () => {
const amount = new Decimal(1.0).div(
new Decimal('0.99999997803504048973201202316767079413460520837376'),
)
const seconds = 1
expect(await decayFormula(amount, seconds)).toBe(1.0)
expect(decayFormula(amount, seconds).toString()).toBe('1.0')
})
})
it.skip('has base 0.99999997802044727', async () => {
it('has base 0.99999997802044727', async () => {
const now = new Date()
now.setSeconds(1)
const oneSecondAgo = new Date(now.getTime())
oneSecondAgo.setSeconds(0)
expect(await calculateDecay(1.0, oneSecondAgo, now)).toBe(0.99999997802044727)
expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now).balance.toString()).toBe(
'0.999999978035040489732012',
)
})
it('returns input amount when from and to is the same', async () => {
const now = new Date()
expect((await calculateDecay(100.0, now, now)).balance).toBe(100.0)
expect(calculateDecay(new Decimal(100.0), now, now).balance.toString()).toBe('100')
})
})

View File

@ -1,45 +1,57 @@
import Decimal from 'decimal.js-light'
import CONFIG from '../config'
import { Decay } from '../graphql/model/Decay'
function decayFormula(amount: number, seconds: number): number {
return amount * Math.pow(0.99999997802044727, seconds) // This number represents 50% decay a year
// TODO: externalize all those definitions and functions into an external decay library
function decayFormula(value: Decimal, seconds: number): Decimal {
// TODO why do we need to convert this here to a stting to work properly?
return value.mul(
new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds).toString(),
)
}
function calculateDecay(amount: number, from: Date, to: Date): Decay {
function calculateDecay(
amount: Decimal,
from: Date,
to: Date,
startBlock: Date = CONFIG.DECAY_START_TIME,
): Decay {
const fromMs = from.getTime()
const toMs = to.getTime()
const decayStartBlockMs = CONFIG.DECAY_START_TIME.getTime()
const startBlockMs = startBlock.getTime()
if (toMs < fromMs) {
throw new Error('to < from, reverse decay calculation is invalid')
}
// Initialize with no decay
const decay = new Decay({
const decay: Decay = {
balance: amount,
decayStart: null,
decayEnd: null,
decayDuration: 0,
decayStartBlock: (decayStartBlockMs / 1000).toString(),
})
decay: new Decimal(0),
start: null,
end: null,
duration: null,
}
// decay started after end date; no decay
if (decayStartBlockMs > toMs) {
if (startBlockMs > toMs) {
return decay
}
// decay started before start date; decay for full duration
else if (decayStartBlockMs < fromMs) {
decay.decayStart = (fromMs / 1000).toString()
decay.decayDuration = (toMs - fromMs) / 1000
if (startBlockMs < fromMs) {
decay.start = from
decay.duration = (toMs - fromMs) / 1000
}
// decay started between start and end date; decay from decay start till end date
else {
decay.decayStart = (decayStartBlockMs / 1000).toString()
decay.decayDuration = (toMs - decayStartBlockMs) / 1000
decay.start = startBlock
decay.duration = (toMs - startBlockMs) / 1000
}
decay.decayEnd = (toMs / 1000).toString()
decay.balance = decayFormula(amount, decay.decayDuration)
decay.end = to
decay.balance = decayFormula(amount, decay.duration)
decay.decay = decay.balance.minus(amount)
return decay
}

View File

@ -1,22 +0,0 @@
import { roundCeilFrom4, roundFloorFrom4, roundCeilFrom2, roundFloorFrom2 } from './round'
describe('utils/round', () => {
it('roundCeilFrom4', () => {
const amount = 11617
expect(roundCeilFrom4(amount)).toBe(1.17)
})
// Not sure if the following skiped tests make sence!?
it('roundFloorFrom4', () => {
const amount = 11617
expect(roundFloorFrom4(amount)).toBe(1.16)
})
it('roundCeilFrom2', () => {
const amount = 1216
expect(roundCeilFrom2(amount)).toBe(13)
})
// not possible, nodejs hasn't enough accuracy
it('roundFloorFrom2', () => {
const amount = 1216
expect(roundFloorFrom2(amount)).toBe(12)
})
})

View File

@ -1,17 +0,0 @@
function roundCeilFrom4(decimal: number): number {
return Math.ceil(decimal / 100) / 100
}
function roundFloorFrom4(decimal: number): number {
return Math.floor(decimal / 100) / 100
}
function roundCeilFrom2(decimal: number): number {
return Math.ceil(decimal / 100)
}
function roundFloorFrom2(decimal: number): number {
return Math.floor(decimal / 100)
}
export { roundCeilFrom4, roundFloorFrom4, roundCeilFrom2, roundFloorFrom2 }

View File

@ -1,7 +1,7 @@
import { User as dbUser } from '@entity/User'
import { Balance as dbBalance } from '@entity/Balance'
import { getRepository } from '@dbTools/typeorm'
import { calculateDecay } from './decay'
import Decimal from 'decimal.js-light'
import { Transaction } from '@entity/Transaction'
import { Decay } from '../graphql/model/Decay'
function isStringBoolean(value: string): boolean {
const lowerValue = value.toLowerCase()
@ -15,14 +15,21 @@ function isHexPublicKey(publicKey: string): boolean {
return /^[0-9A-Fa-f]{64}$/i.test(publicKey)
}
async function hasUserAmount(user: dbUser, amount: number): Promise<boolean> {
if (amount < 0) return false
const balanceRepository = getRepository(dbBalance)
const balance = await balanceRepository.findOne({ userId: user.id })
if (!balance) return false
async function calculateBalance(
userId: number,
amount: Decimal,
time: Date,
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
if (!lastTransaction) return null
const decay = calculateDecay(balance.amount, balance.recordDate, new Date()).balance
return decay > amount
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
// TODO why we have to use toString() here?
const balance = decay.balance.add(amount.toString())
if (balance.lessThan(0)) {
return null
}
return { balance, lastTransactionId: lastTransaction.id, decay }
}
export { isHexPublicKey, hasUserAmount, isStringBoolean }
export { isHexPublicKey, calculateBalance, isStringBoolean }

View File

@ -0,0 +1,52 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import Decimal from 'decimal.js-light'
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { Transaction as dbTransaction } from '@entity/Transaction'
import { calculateDecay } from './decay'
import { TransactionTypeId } from '../graphql/enum/TransactionTypeId'
import { Transaction } from '../graphql/model/Transaction'
import { User } from '../graphql/model/User'
const virtualDecayTransaction = (
balance: Decimal,
balanceDate: Date,
time: Date = new Date(),
user: User,
): Transaction => {
const decay = calculateDecay(balance, balanceDate, time)
// const balance = decay.balance.minus(lastTransaction.balance)
const decayDbTransaction: dbTransaction = {
id: -1,
userId: -1,
previous: -1,
typeId: TransactionTypeId.DECAY,
amount: decay.decay ? decay.decay : new Decimal(0), // new Decimal(0), // this kinda is wrong, but helps with the frontend query
balance: decay.balance,
balanceDate: time,
decay: decay.decay ? decay.decay : new Decimal(0),
decayStart: decay.start,
memo: '',
creationDate: null,
hasId: function (): boolean {
throw new Error('Function not implemented.')
},
save: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
remove: function (options?: RemoveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
softRemove: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
recover: function (options?: SaveOptions): Promise<dbTransaction> {
throw new Error('Function not implemented.')
},
reload: function (): Promise<void> {
throw new Error('Function not implemented.')
},
}
return new Transaction(decayDbTransaction, user)
}
export { virtualDecayTransaction }

View File

@ -1961,6 +1961,11 @@ debug@^3.2.6, debug@^3.2.7:
dependencies:
ms "^2.1.1"
decimal.js-light@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
decimal.js@^10.2.1:
version "10.3.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
import { Balance } from '../Balance'
import { Balance } from './Balance'
// Moriz: I do not like the idea of having two user tables
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })

View File

@ -0,0 +1,146 @@
import Decimal from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
@Entity('transactions')
export class Transaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({ unsigned: true, nullable: true, default: null })
previous: number
@Column({ name: 'type_id', unsigned: true, nullable: false })
typeId: number
@Column({
name: 'dec_amount',
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
decAmount: Decimal
@Column({
name: 'dec_balance',
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
decBalance: Decimal
@Column({
name: 'balance_date',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
balanceDate: Date
@Column({
name: 'dec_decay',
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
decDecay: Decimal
@Column({
name: 'decay_start',
type: 'datetime',
nullable: true,
default: null,
})
decayStart: Date | null
@Column({ type: 'bigint', nullable: false })
amount: BigInt
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({
name: 'send_sender_final_balance',
type: 'bigint',
nullable: true,
default: null,
})
sendSenderFinalBalance: BigInt | null
@Column({ name: 'balance', type: 'bigint', default: 0 })
balance: BigInt
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
creationDate: Date
@Column({
name: 'linked_user_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedUserId?: number | null
@Column({
name: 'linked_transaction_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedTransactionId?: number | null
@Column({
name: 'temp_dec_send_sender_final_balance',
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
default: null,
transformer: DecimalTransformer,
})
tempDecSendSenderFinalBalance: Decimal
@Column({
name: 'temp_dec_diff_send_sender_final_balance',
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
default: null,
transformer: DecimalTransformer,
})
tempDecDiffSendSenderFinalBalance: Decimal
@Column({
name: 'temp_dec_old_balance',
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
default: null,
transformer: DecimalTransformer,
})
tempDecOldBalance: Decimal
@Column({
name: 'temp_dec_diff_balance',
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
default: null,
transformer: DecimalTransformer,
})
tempDecDiffBalance: Decimal
}

View File

@ -0,0 +1,85 @@
import Decimal from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
@Entity('transactions')
export class Transaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({ unsigned: true, nullable: true, default: null })
previous: number
@Column({ name: 'type_id', unsigned: true, nullable: false })
typeId: number
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
balance: Decimal
@Column({
name: 'balance_date',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
balanceDate: Date
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
decay: Decimal
@Column({
name: 'decay_start',
type: 'datetime',
nullable: true,
default: null,
})
decayStart: Date | null
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
creationDate: Date | null
@Column({
name: 'linked_user_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedUserId?: number | null
@Column({
name: 'linked_transaction_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedTransactionId?: number | null
}

View File

@ -1 +0,0 @@
export { Balance } from './0001-init_db/Balance'

View File

@ -1 +1 @@
export { Transaction } from './0027-clean_transaction_table/Transaction'
export { Transaction } from './0029-clean_transaction_table/Transaction'

View File

@ -1,4 +1,3 @@
import { Balance } from './Balance'
import { LoginElopageBuys } from './LoginElopageBuys'
import { LoginEmailOptIn } from './LoginEmailOptIn'
import { Migration } from './Migration'
@ -10,7 +9,6 @@ import { AdminPendingCreation } from './AdminPendingCreation'
export const entities = [
AdminPendingCreation,
Balance,
LoginElopageBuys,
LoginEmailOptIn,
Migration,

View File

@ -0,0 +1,231 @@
/* MIGRATION TO INTRODUCE THE DECIMAL TYPE
*
* This migration adds fields of type DECIMAL
* and corrects the corresponding values of
* each by recalculating the history of all
* user transactions.
*
* Furthermore it increases precision of the
* stored values and stores additional data
* points to avoid repetitive calculations.
*
* It will also add a link to the last
* transaction, creating a linked list
*
* And it will convert all timestamps to
* datetime.
*
* WARNING: This Migration must be run in TZ=UTC
*/
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
import Decimal from 'decimal.js-light'
// Set precision value
Decimal.set({
precision: 25,
rounding: Decimal.ROUND_HALF_UP,
})
const DECAY_START_TIME = new Date('2021-05-13 17:46:31') // GMT+0
// TODO: externalize all those definitions and functions into an external decay library
interface Decay {
balance: Decimal
decay: Decimal | null
start: Date | null
end: Date | null
duration: number | null
}
export enum TransactionTypeId {
CREATION = 1,
SEND = 2,
RECEIVE = 3,
}
function decayFormula(value: Decimal, seconds: number): Decimal {
return value.mul(new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds))
}
function calculateDecay(
amount: Decimal,
from: Date,
to: Date,
startBlock: Date = DECAY_START_TIME,
): Decay {
const fromMs = from.getTime()
const toMs = to.getTime()
const startBlockMs = startBlock.getTime()
if (toMs < fromMs) {
throw new Error('to < from, reverse decay calculation is invalid')
}
// Initialize with no decay
const decay: Decay = {
balance: amount,
decay: null,
start: null,
end: null,
duration: null,
}
// decay started after end date; no decay
if (startBlockMs > toMs) {
return decay
}
// decay started before start date; decay for full duration
if (startBlockMs < fromMs) {
decay.start = from
decay.duration = (toMs - fromMs) / 1000
}
// decay started between start and end date; decay from decay start till end date
else {
decay.start = startBlock
decay.duration = (toMs - startBlockMs) / 1000
}
decay.end = to
decay.balance = decayFormula(amount, decay.duration)
decay.decay = decay.balance.minus(amount)
return decay
}
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Add Columns
// add column `previous` for a link to the previous transaction
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `previous` int(10) unsigned DEFAULT NULL AFTER `user_id`;',
)
// add column `dec_amount` with temporary NULL and DEFAULT NULL definition
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `dec_amount` DECIMAL(40,20) NULL DEFAULT NULL AFTER `type_id`;',
)
// add column `dec_balance` with temporary NULL and DEFAULT NULL definition
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `dec_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `dec_amount`;',
)
// add new column `dec_decay` with temporary NULL and DEFAULT NULL definition
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `dec_decay` DECIMAL(40,20) NULL DEFAULT NULL AFTER `dec_balance`;',
)
// add new column `decay_start`
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `decay_start` datetime DEFAULT NULL AFTER `dec_decay`;',
)
// Modify columns
// modify date type of `balance_date` to datetime
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `balance_date` datetime NOT NULL DEFAULT current_timestamp() AFTER `dec_balance`;',
)
// modify date type of `creation_date` to datetime
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `creation_date` datetime DEFAULT NULL AFTER `balance`;',
)
// Temporary columns
// temporary decimal column `temp_dec_send_sender_final_balance`
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_send_sender_final_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `linked_transaction_id`;',
)
// temporary decimal column `temp_dec_diff_send_sender_final_balance`
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_diff_send_sender_final_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `temp_dec_send_sender_final_balance`;',
)
// temporary decimal column `temp_dec_old_balance`
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_old_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `temp_dec_diff_send_sender_final_balance`;',
)
// temporary decimal column `temp_dec_diff_balance`
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_diff_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `temp_dec_old_balance`;',
)
// Find all users & loop over them
const users = await queryFn('SELECT user_id FROM transactions GROUP BY user_id')
for (let u = 0; u < users.length; u++) {
// find all transactions for a user
const transactions = await queryFn(
`SELECT * FROM transactions WHERE user_id = ${users[u].user_id} ORDER BY balance_date ASC;`,
)
let previous = null
let balance = new Decimal(0)
for (let t = 0; t < transactions.length; t++) {
const transaction = transactions[t]
// This should also fix the rounding error on amount
let decAmount = new Decimal(transaction.amount).dividedBy(10000).toDecimalPlaces(2)
if (transaction.type_id === TransactionTypeId.SEND) {
decAmount = decAmount.mul(-1)
}
const decayStartDate = previous ? previous.balance_date : transaction.balance_date
const decay = calculateDecay(balance, decayStartDate, transaction.balance_date)
// WARNING: `toISOString()` needs UTC Timezone to work properly!
const decayStart =
previous && decay.start
? '"' + decay.start.toISOString().slice(0, 19).replace('T', ' ') + '"'
: null
balance = decay.balance.add(decAmount)
const tempDecSendSenderFinalBalance = transaction.send_sender_final_balance
? new Decimal(transaction.send_sender_final_balance).dividedBy(10000)
: null
const tempDecDiffSendSenderFinalBalance = tempDecSendSenderFinalBalance
? balance.minus(tempDecSendSenderFinalBalance)
: null
const tempDecOldBalance = new Decimal(transaction.balance).dividedBy(10000)
const tempDecDiffBalance = balance.minus(tempDecOldBalance)
// Update
await queryFn(`
UPDATE transactions SET
previous = ${previous ? previous.id : null},
dec_amount = ${decAmount.toString()},
dec_balance = ${balance.toString()},
dec_decay = ${decay.decay ? decay.decay.toString() : '0'},
decay_start = ${decayStart},
temp_dec_send_sender_final_balance = ${
tempDecSendSenderFinalBalance ? tempDecSendSenderFinalBalance.toString() : null
},
temp_dec_diff_send_sender_final_balance = ${
tempDecDiffSendSenderFinalBalance ? tempDecDiffSendSenderFinalBalance.toString() : null
},
temp_dec_old_balance = ${tempDecOldBalance.toString()},
temp_dec_diff_balance = ${tempDecDiffBalance.toString()}
WHERE id = ${transaction.id};
`)
// previous
previous = transaction
}
}
// Remove null as value & default value from `dec_amount`, `dec_balance` and `dec_decay`
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `dec_amount` DECIMAL(40,20) NOT NULL;')
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `dec_balance` DECIMAL(40,20) NOT NULL;')
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `dec_decay` DECIMAL(40,20) NOT NULL;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_balance`')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_old_balance`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_send_sender_final_balance`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_send_sender_final_balance`;')
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `creation_date` timestamp NULL DEFAULT NULL AFTER `balance`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `balance_date` timestamp NOT NULL DEFAULT current_timestamp() AFTER `dec_balance`;',
)
await queryFn('ALTER TABLE `transactions` DROP COLUMN `decay_start`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `dec_decay`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `dec_balance`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `dec_amount`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `previous`;')
}

View File

@ -0,0 +1,105 @@
/* MIGRATION TO CLEAN THE TRANSACTION TABLE
*
* This migration deletes and renames several
* columns of the `transactions` table.
*
* This migration has data loss
*/
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Delete columns
// delete column `amount`
await queryFn('ALTER TABLE `transactions` DROP COLUMN `amount`;')
// delete column `send_sender_final_balance`
await queryFn('ALTER TABLE `transactions` DROP COLUMN `send_sender_final_balance`;')
// delete column `balance`
await queryFn('ALTER TABLE `transactions` DROP COLUMN `balance`;')
// delete column `temp_dec_send_sender_final_balance`
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_send_sender_final_balance`;')
// delete column `temp_dec_diff_send_sender_final_balance`
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_send_sender_final_balance`;')
// delete column `temp_dec_old_balance`
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_old_balance`;')
// delete column `temp_dec_diff_balance`
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_balance`;')
// Rename columns
// rename column `dec_amount` to `amount`
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `dec_amount` to `amount`;')
// rename column `dec_balance` to `balance`
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `dec_balance` to `balance`;')
// rename column `dec_decay` to `decay`
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `dec_decay` to `decay`;')
// Drop tables
// drop `state_balances`
await queryFn('DROP TABLE `state_balances`;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Not all data is recoverable here, this data is simulated,
// We lose all incorrect balances and wrongly rounded amounts.
await queryFn(`
CREATE TABLE \`state_balances\` (
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
\`state_user_id\` int(10) unsigned NOT NULL,
\`modified\` datetime NOT NULL,
\`record_date\` datetime DEFAULT NULL,
\`amount\` bigint(20) NOT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB AUTO_INCREMENT=568 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`)
await queryFn(`
INSERT INTO \`state_balances\`
(state_user_id, modified, record_date, amount)
SELECT user_id as state_user_id, balance_date as modified, balance_date as record_date, amount * 10000 as amount FROM
(SELECT user_id as uid, MAX(balance_date) AS date FROM transactions GROUP BY uid) AS t
LEFT JOIN transactions ON t.uid = transactions.user_id AND t.date = transactions.balance_date;
`)
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `decay` to `dec_decay`;')
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `balance` to `dec_balance`;')
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `amount` to `dec_amount`;')
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_diff_balance` decimal(40,20) DEFAULT NULL AFTER linked_transaction_id;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_old_balance` decimal(40,20) DEFAULT NULL AFTER linked_transaction_id;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_diff_send_sender_final_balance` decimal(40,20) DEFAULT NULL AFTER linked_transaction_id;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_send_sender_final_balance` decimal(40,20) DEFAULT NULL AFTER linked_transaction_id;',
)
await queryFn('ALTER TABLE `transactions` ADD COLUMN `balance` bigint(20) DEFAULT 0 AFTER memo;')
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `send_sender_final_balance` bigint(20) DEFAULT NULL AFTER memo;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `amount` bigint(20) DEFAULT NULL AFTER decay_start;',
)
await queryFn(`
UPDATE transactions SET
temp_dec_diff_balance = 0,
temp_dec_old_balance = dec_balance,
temp_dec_diff_send_sender_final_balance = 0,
temp_dec_send_sender_final_balance = dec_balance,
balance = dec_balance * 10000,
send_sender_final_balance = dec_balance * 10000,
amount = dec_amount * 10000;
`)
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `amount` bigint(20) NOT NULL;')
}

View File

@ -10,15 +10,15 @@
"scripts": {
"build": "mkdir -p build/src/config/ && cp src/config/*.txt build/src/config/ && tsc --build",
"clean": "tsc --build --clean",
"up": "node build/src/index.js up",
"down": "node build/src/index.js down",
"reset": "node build/src/index.js reset",
"dev_up": "ts-node src/index.ts up",
"dev_down": "ts-node src/index.ts down",
"dev_reset": "ts-node src/index.ts reset",
"up": "TZ=UTC node build/src/index.js up",
"down": "TZ=UTC node build/src/index.js down",
"reset": "TZ=UTC node build/src/index.js reset",
"dev_up": "TZ=UTC ts-node src/index.ts up",
"dev_down": "TZ=UTC ts-node src/index.ts down",
"dev_reset": "TZ=UTC ts-node src/index.ts reset",
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
"seed:config": "ts-node ./node_modules/typeorm-seeding/dist/cli.js config",
"seed": "ts-node src/index.ts seed"
"seed": "TZ=UTC ts-node src/index.ts seed"
},
"devDependencies": {
"@types/faker": "^5.5.9",
@ -38,6 +38,7 @@
},
"dependencies": {
"crypto": "^1.0.1",
"decimal.js-light": "^2.5.1",
"dotenv": "^10.0.0",
"faker": "^5.5.3",
"mysql2": "^2.3.0",

View File

@ -1,18 +0,0 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { Balance } from '../../entity/Balance'
import { BalanceContext } from '../interface/TransactionContext'
define(Balance, (faker: typeof Faker, context?: BalanceContext) => {
if (!context || !context.user) {
throw new Error('Balance: No user present!')
}
const balance = new Balance()
balance.modified = context.modified ? context.modified : faker.date.recent()
balance.recordDate = context.recordDate ? context.recordDate : faker.date.recent()
balance.amount = context.amount ? context.amount : 10000000
balance.user = context.user
return balance
})

View File

@ -2,6 +2,7 @@ import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { Transaction } from '../../entity/Transaction'
import { TransactionContext } from '../interface/TransactionContext'
import Decimal from 'decimal.js-light'
define(Transaction, (faker: typeof Faker, context?: TransactionContext) => {
if (!context) {
@ -12,11 +13,12 @@ define(Transaction, (faker: typeof Faker, context?: TransactionContext) => {
transaction.typeId = context.typeId // || 2
transaction.userId = context.userId
transaction.amount = context.amount
transaction.balance = context.balance
transaction.decay = new Decimal(0) // context.decay
transaction.memo = context.memo
transaction.creationDate = context.creationDate || new Date()
// transaction.sendReceiverPublicKey = context.sendReceiverPublicKey || null
transaction.linkedUserId = context.sendReceiverUserId || null
transaction.sendSenderFinalBalance = context.sendSenderFinalBalance || null
return transaction
})

View File

@ -1,20 +1,12 @@
import { User } from '../../entity/User'
import Decimal from 'decimal.js-light'
export interface TransactionContext {
typeId: number
userId: number
balance: BigInt
balance: Decimal
balanceDate: Date
amount: BigInt
amount: Decimal
memo: string
creationDate?: Date
sendReceiverUserId?: number
sendSenderFinalBalance?: BigInt
}
export interface BalanceContext {
modified?: Date
recordDate?: Date
amount?: number
user?: User
}

View File

@ -1,3 +1,5 @@
import Decimal from 'decimal.js-light'
export interface UserInterface {
// from user
email?: string
@ -24,10 +26,7 @@ export interface UserInterface {
// flag for balance (creation of 1000 GDD)
addBalance?: boolean
// balance
balanceModified?: Date
recordDate?: Date
creationDate?: Date
amount?: number
creationTxHash?: Buffer
signature?: Buffer
amount?: Decimal
}

View File

@ -1,11 +1,11 @@
import { UserContext, ServerUserContext } from '../../interface/UserContext'
import { BalanceContext, TransactionContext } from '../../interface/TransactionContext'
import { TransactionContext } from '../../interface/TransactionContext'
import { UserInterface } from '../../interface/UserInterface'
import { User } from '../../../entity/User'
import { ServerUser } from '../../../entity/ServerUser'
import { Balance } from '../../../entity/Balance'
import { Transaction } from '../../../entity/Transaction'
import { Factory } from 'typeorm-seeding'
import Decimal from 'decimal.js-light'
export const userSeeder = async (factory: Factory, userData: UserInterface): Promise<void> => {
const user = await factory(User)(createUserContext(userData)).create()
@ -16,7 +16,6 @@ export const userSeeder = async (factory: Factory, userData: UserInterface): Pro
if (userData.addBalance) {
// create some GDD for the user
await factory(Balance)(createBalanceContext(userData, user)).create()
await factory(Transaction)(
createTransactionContext(userData, user, 1, 'Herzlich Willkommen bei Gradido!'),
).create()
@ -52,15 +51,6 @@ const createServerUserContext = (context: UserInterface): ServerUserContext => {
}
}
const createBalanceContext = (context: UserInterface, user: User): BalanceContext => {
return {
modified: context.balanceModified,
recordDate: context.recordDate,
amount: context.amount,
user,
}
}
const createTransactionContext = (
context: UserInterface,
user: User,
@ -70,8 +60,8 @@ const createTransactionContext = (
return {
typeId: type,
userId: user.id,
amount: BigInt(context.amount || 100000),
balance: BigInt(context.amount || 100000),
amount: context.amount || new Decimal(1000),
balance: context.amount || new Decimal(1000),
balanceDate: new Date(context.recordDate || Date.now()),
memo,
creationDate: context.creationDate,

View File

@ -1,3 +1,4 @@
import Decimal from 'decimal.js-light'
import { UserInterface } from '../../interface/UserInterface'
export const bibiBloxberg: UserInterface = {
@ -19,16 +20,7 @@ export const bibiBloxberg: UserInterface = {
'knife normal level all hurdle crucial color avoid warrior stadium road bachelor affair topple hawk pottery right afford immune two ceiling budget glance hour ',
isAdmin: false,
addBalance: true,
balanceModified: new Date('2021-11-30T10:37:11'),
recordDate: new Date('2021-11-30T10:37:11'),
creationDate: new Date('2021-08-01 00:00:00'),
amount: 10000000,
creationTxHash: Buffer.from(
'51103dc0fc2ca5d5d75a9557a1e899304e5406cfdb1328d8df6414d527b0118100000000000000000000000000000000',
'hex',
),
signature: Buffer.from(
'2a2c71f3e41adc060bbc3086577e2d57d24eeeb0a7727339c3f85aad813808f601d7e1df56a26e0929d2e67fc054fca429ccfa283ed2782185c7f009fe008f0c',
'hex',
),
amount: new Decimal(1000),
}

View File

@ -1,3 +1,4 @@
import Decimal from 'decimal.js-light'
import { UserInterface } from '../../interface/UserInterface'
export const bobBaumeister: UserInterface = {
@ -19,16 +20,7 @@ export const bobBaumeister: UserInterface = {
'detail master source effort unable waste tilt flush domain orchard art truck hint barrel response gate impose peanut secret merry three uncle wink resource ',
isAdmin: false,
addBalance: true,
balanceModified: new Date('2021-11-30T10:37:14'),
recordDate: new Date('2021-11-30T10:37:14'),
creationDate: new Date('2021-08-01 00:00:00'),
amount: 10000000,
creationTxHash: Buffer.from(
'be095dc87acb94987e71168fee8ecbf50ecb43a180b1006e75d573b35725c69c00000000000000000000000000000000',
'hex',
),
signature: Buffer.from(
'1fbd6b9a3d359923b2501557f3bc79fa7e428127c8090fb16bc490b4d87870ab142b3817ddd902d22f0b26472a483233784a0e460c0622661752a13978903905',
'hex',
),
amount: new Decimal(1000),
}

View File

@ -1,3 +1,4 @@
import Decimal from 'decimal.js-light'
import { UserInterface } from '../../interface/UserInterface'
export const raeuberHotzenplotz: UserInterface = {
@ -19,16 +20,7 @@ export const raeuberHotzenplotz: UserInterface = {
'gospel trip tenant mouse spider skill auto curious man video chief response same little over expire drum display fancy clinic keen throw urge basket ',
isAdmin: false,
addBalance: true,
balanceModified: new Date('2021-11-30T10:37:13'),
recordDate: new Date('2021-11-30T10:37:13'),
creationDate: new Date('2021-08-01 00:00:00'),
amount: 10000000,
creationTxHash: Buffer.from(
'23ba44fd84deb59b9f32969ad0cb18bfa4588be1bdb99c396888506474c16c1900000000000000000000000000000000',
'hex',
),
signature: Buffer.from(
'756d3da061687c575d1dbc5073908f646aa5f498b0927b217c83b48af471450e571dfe8421fb8e1f1ebd1104526b7e7c6fa78684e2da59c8f7f5a8dc3d9e5b0b',
'hex',
),
amount: new Decimal(1000),
}

View File

@ -0,0 +1,19 @@
import Decimal from 'decimal.js-light'
import { ValueTransformer } from 'typeorm'
Decimal.set({
precision: 25,
rounding: Decimal.ROUND_HALF_UP,
})
export const DecimalTransformer: ValueTransformer = {
/**
* Used to marshal Decimal when writing to the database.
*/
to: (decimal: Decimal | null): string | null => (decimal ? decimal.toString() : null),
/**
* Used to unmarshal Decimal when reading from the database.
*/
from: (decimal: string | null): Decimal | null => (decimal ? new Decimal(decimal) : null),
}

View File

@ -560,6 +560,11 @@ decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
decimal.js-light@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
deep-is@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"

View File

@ -57,7 +57,7 @@ export const createUser = gql`
`
export const sendCoins = gql`
mutation($email: String!, $amount: Float!, $memo: String!) {
mutation($email: String!, $amount: Decimal!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo)
}
`

View File

@ -55,30 +55,26 @@ export const transactionsQuery = gql`
order: $order
onlyCreations: $onlyCreations
) {
gdtSum
balanceGDT
count
balance
decay
decayDate
decayStartBlock
transactions {
type
balance
decayStart
decayEnd
decayDuration
id
typeId
amount
balanceDate
memo
transactionId
name
email
date
decay {
balance
decayStart
decayEnd
decayDuration
decayStartBlock
linkedUser {
firstName
lastName
}
decay {
decay
start
end
duration
}
firstTransaction
}
}
}