refactor with gradido-blockchain-js

This commit is contained in:
einhornimmond 2024-09-20 17:22:41 +02:00
parent 64008b1564
commit 0bb04a845a
89 changed files with 3340 additions and 4502 deletions

View File

@ -104,6 +104,10 @@ export class DltConnectorClient {
* and update dltTransactionId of transaction in db with iota message id
*/
public async transmitTransaction(transaction: DbTransaction): Promise<boolean> {
// we don't need the receive transactions, there contain basically the same data as the send transactions
if ((transaction.typeId as TransactionTypeId) === TransactionTypeId.RECEIVE) {
return true
}
const typeString = getTransactionTypeString(transaction.typeId)
// no negative values in dlt connector, gradido concept don't use negative values so the code don't use it too
const amountString = transaction.amount.abs().toString()

View File

@ -21,6 +21,11 @@ TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
# DLT-Connector
DLT_CONNECTOR_PORT=6010
# Gradido Blockchain
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET=21ffbbc616fe
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD=YourPassword
# Route to Backend
BACKEND_SERVER_URL=http://localhost:4000
JWT_SECRET=secret123

View File

@ -19,5 +19,10 @@ TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
# DLT-Connector
DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT
# Gradido Blockchain
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET=$GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY=$GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY
GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD=$GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD
# Route to Backend
BACKEND_SERVER_URL=http://localhost:4000

View File

@ -19,17 +19,15 @@
"@apollo/server": "^4.7.5",
"@apollo/utils.fetcher": "^3.0.0",
"@iota/client": "^2.2.4",
"bip32-ed25519": "^0.0.4",
"bip39": "^3.1.0",
"body-parser": "^1.20.2",
"class-validator": "^0.14.0",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"decimal.js-light": "^2.5.1",
"dlt-database": "file:../dlt-database",
"dotenv": "10.0.0",
"express": "4.17.1",
"express-slow-down": "^2.0.1",
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#master",
"graphql": "^16.7.1",
"graphql-request": "^6.1.0",
"graphql-scalars": "^1.22.2",
@ -37,9 +35,7 @@
"jose": "^5.2.2",
"log4js": "^6.7.1",
"nodemon": "^2.0.20",
"protobufjs": "^7.2.5",
"reflect-metadata": "^0.1.13",
"sodium-native": "^4.0.4",
"tsconfig-paths": "^4.1.2",
"type-graphql": "^2.0.0-beta.2",
"uuid": "^9.0.1"

View File

@ -4,12 +4,12 @@ dotenv.config()
const constants = {
LOG4JS_CONFIG: 'log4js-config.json',
DB_VERSION: '0004-fix_spelling',
DB_VERSION: '0005-refactor_with_gradido_blockchain_lib',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v6.2024-02-20',
EXPECTED: 'v7.2024-09-24',
CURRENT: '',
},
}
@ -39,6 +39,15 @@ const dltConnector = {
DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010,
}
const gradidoBlockchain = {
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET:
process.env.GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET ?? 'invalid',
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY:
process.env.GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY ?? 'invalid',
GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD:
process.env.GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD,
}
const backendServer = {
BACKEND_SERVER_URL: process.env.BACKEND_SERVER_URL ?? 'http://backend:4000',
}
@ -61,5 +70,6 @@ export const CONFIG = {
...database,
...iota,
...dltConnector,
...gradidoBlockchain,
...backendServer,
}

View File

@ -1,8 +1,13 @@
/* eslint-disable camelcase */
import { Account } from '@entity/Account'
import Decimal from 'decimal.js-light'
import {
AddressType,
AddressType_COMMUNITY_AUF,
AddressType_COMMUNITY_GMW,
} from 'gradido-blockchain-js'
import { KeyPair } from '@/data/KeyPair'
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { hardenDerivationIndex } from '@/utils/derivationHelper'
import { accountTypeToAddressType } from '@/utils/typeConverter'
@ -44,7 +49,7 @@ export class AccountFactory {
return AccountFactory.createAccount(
createdAt,
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
AddressType.COMMUNITY_GMW,
AddressType_COMMUNITY_GMW,
keyPair,
)
}
@ -53,7 +58,7 @@ export class AccountFactory {
return AccountFactory.createAccount(
createdAt,
hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX),
AddressType.COMMUNITY_AUF,
AddressType_COMMUNITY_AUF,
keyPair,
)
}

View File

@ -1,8 +1,15 @@
/* eslint-disable camelcase */
import 'reflect-metadata'
import { Decimal } from 'decimal.js-light'
import { TestDB } from '@test/TestDB'
import {
AddressType_COMMUNITY_AUF,
AddressType_COMMUNITY_GMW,
AddressType_COMMUNITY_HUMAN,
} from 'gradido-blockchain-js'
import { AccountType } from '@/graphql/enum/AccountType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
@ -11,7 +18,6 @@ import { AccountFactory } from './Account.factory'
import { AccountRepository } from './Account.repository'
import { KeyPair } from './KeyPair'
import { Mnemonic } from './Mnemonic'
import { AddressType } from './proto/3_3/enum/AddressType'
import { UserFactory } from './User.factory'
import { UserLogic } from './User.logic'
@ -37,14 +43,14 @@ describe('data/Account test factory and repository', () => {
})
it('test createAccount', () => {
const account = AccountFactory.createAccount(now, 1, AddressType.COMMUNITY_HUMAN, keyPair1)
const account = AccountFactory.createAccount(now, 1, AddressType_COMMUNITY_HUMAN, keyPair1)
expect(account).toMatchObject({
derivationIndex: 1,
derive2Pubkey: Buffer.from(
'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994',
'hex',
),
type: AddressType.COMMUNITY_HUMAN,
type: AddressType_COMMUNITY_HUMAN,
createdAt: now,
balanceCreatedAt: now,
balanceOnConfirmation: new Decimal(0),
@ -65,7 +71,7 @@ describe('data/Account test factory and repository', () => {
'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994',
'hex',
),
type: AddressType.COMMUNITY_HUMAN,
type: AddressType_COMMUNITY_HUMAN,
createdAt: now,
balanceCreatedAt: now,
balanceOnConfirmation: new Decimal(0),
@ -81,7 +87,7 @@ describe('data/Account test factory and repository', () => {
'05f0060357bb73bd290283870fc47a10b3764f02ca26938479ed853f46145366',
'hex',
),
type: AddressType.COMMUNITY_GMW,
type: AddressType_COMMUNITY_GMW,
createdAt: now,
balanceCreatedAt: now,
balanceOnConfirmation: new Decimal(0),
@ -97,7 +103,7 @@ describe('data/Account test factory and repository', () => {
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
'hex',
),
type: AddressType.COMMUNITY_AUF,
type: AddressType_COMMUNITY_AUF,
createdAt: now,
balanceCreatedAt: now,
balanceOnConfirmation: new Decimal(0),
@ -151,7 +157,7 @@ describe('data/Account test factory and repository', () => {
'0fa996b73b624592fe326b8500cb1e3f10026112b374d84c87d097f4d489c019',
'hex',
),
type: AddressType.COMMUNITY_GMW,
type: AddressType_COMMUNITY_GMW,
}),
expect.objectContaining({
derivationIndex: 2147483650,
@ -159,7 +165,7 @@ describe('data/Account test factory and repository', () => {
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
'hex',
),
type: AddressType.COMMUNITY_AUF,
type: AddressType_COMMUNITY_AUF,
}),
]),
)
@ -176,7 +182,7 @@ describe('data/Account test factory and repository', () => {
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
'hex',
),
type: AddressType.COMMUNITY_AUF,
type: AddressType_COMMUNITY_AUF,
})
})
@ -190,7 +196,7 @@ describe('data/Account test factory and repository', () => {
'2099c004a26e5387c9fbbc9bb0f552a9642d3fd7c710ae5802b775d24ff36f93',
'hex',
),
type: AddressType.COMMUNITY_HUMAN,
type: AddressType_COMMUNITY_HUMAN,
})
})
})

View File

@ -1,13 +0,0 @@
import { BackendTransaction } from '@entity/BackendTransaction'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
export class BackendTransactionFactory {
public static createFromTransactionDraft(transactionDraft: TransactionDraft): BackendTransaction {
const backendTransaction = BackendTransaction.create()
backendTransaction.backendTransactionId = transactionDraft.backendTransactionId
backendTransaction.typeId = transactionDraft.type
backendTransaction.createdAt = new Date(transactionDraft.createdAt)
return backendTransaction
}
}

View File

@ -1,7 +0,0 @@
import { BackendTransaction } from '@entity/BackendTransaction'
import { getDataSource } from '@/typeorm/DataSource'
export const BackendTransactionRepository = getDataSource()
.getRepository(BackendTransaction)
.extend({})

View File

@ -66,9 +66,9 @@ export const CommunityRepository = getDataSource()
async loadHomeCommunityKeyPair(): Promise<KeyPair> {
const community = await this.findOneOrFail({
where: { foreign: false },
select: { rootChaincode: true, rootPubkey: true, rootPrivkey: true },
select: { rootChaincode: true, rootPubkey: true, rootEncryptedPrivkey: true },
})
if (!community.rootChaincode || !community.rootPrivkey) {
if (!community.rootChaincode || !community.rootEncryptedPrivkey) {
throw new LogError('Missing chaincode or private key for home community')
}
return new KeyPair(community)

View File

@ -1,40 +1,52 @@
import { Community } from '@entity/Community'
// https://www.npmjs.com/package/bip32-ed25519
import {
KeyPairEd25519,
MemoryBlock,
Passphrase,
SecretKeyCryptography,
SignaturePair,
} from 'gradido-blockchain-js'
import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519'
import { Mnemonic } from './Mnemonic'
import { SignaturePair } from './proto/3_3/SignaturePair'
/**
* Class Managing Key Pair and also generate, sign and verify signature with it
*/
export class KeyPair {
private _publicKey: Buffer
private _chainCode: Buffer
private _privateKey: Buffer
private _ed25519KeyPair: KeyPairEd25519
/**
* @param input: Mnemonic = Mnemonic or Passphrase which work as seed for generating algorithms
* @param input: Buffer = extended private key, returned from bip32-ed25519 generateFromSeed or from derivePrivate
* @param input: KeyPairEd25519 = already loaded KeyPairEd25519
* @param input: Passphrase = Passphrase which work as seed for generating algorithms
* @param input: MemoryBlock = a seed at least 32 byte
* @param input: Community = community entity with keys loaded from db
*
*/
public constructor(input: Mnemonic | Buffer | Community) {
if (input instanceof Mnemonic) {
this.loadFromExtendedPrivateKey(generateFromSeed(input.seed))
} else if (input instanceof Buffer) {
this.loadFromExtendedPrivateKey(input)
public constructor(input: KeyPairEd25519 | Passphrase | MemoryBlock | Community) {
let keyPair: KeyPairEd25519 | null = null
if (input instanceof KeyPairEd25519) {
keyPair = input
} else if (input instanceof Passphrase) {
keyPair = KeyPairEd25519.create(input)
} else if (input instanceof MemoryBlock) {
keyPair = KeyPairEd25519.create(input)
} else if (input instanceof Community) {
if (!input.rootPrivkey || !input.rootChaincode || !input.rootPubkey) {
throw new LogError('missing private key or chaincode or public key in commmunity entity')
if (!input.rootEncryptedPrivkey || !input.rootChaincode || !input.rootPubkey) {
throw new LogError(
'missing encrypted private key or chaincode or public key in commmunity entity',
)
}
this._privateKey = input.rootPrivkey
this._publicKey = input.rootPubkey
this._chainCode = input.rootChaincode
const secretBox = this.createSecretBox(input.iotaTopic)
keyPair = new KeyPairEd25519(
new MemoryBlock(input.rootPubkey),
secretBox.decrypt(new MemoryBlock(input.rootEncryptedPrivkey)),
new MemoryBlock(input.rootChaincode),
)
}
if (!keyPair) {
throw new LogError("couldn't create KeyPairEd25519 from input")
}
this._ed25519KeyPair = keyPair
}
/**
@ -42,47 +54,54 @@ export class KeyPair {
* @param community
*/
public fillInCommunityKeys(community: Community) {
community.rootPubkey = this._publicKey
community.rootPrivkey = this._privateKey
community.rootChaincode = this._chainCode
}
private loadFromExtendedPrivateKey(extendedPrivateKey: Buffer) {
if (extendedPrivateKey.length !== 96) {
throw new LogError('invalid extended private key')
}
this._privateKey = extendedPrivateKey.subarray(0, 64)
this._chainCode = extendedPrivateKey.subarray(64, 96)
this._publicKey = toPublic(extendedPrivateKey).subarray(0, 32)
}
public getExtendPrivateKey(): Buffer {
return Buffer.concat([this._privateKey, this._chainCode])
}
public getExtendPublicKey(): Buffer {
return Buffer.concat([this._publicKey, this._chainCode])
const secretBox = this.createSecretBox(community.iotaTopic)
community.rootPubkey = this._ed25519KeyPair.getPublicKey()?.data()
community.rootEncryptedPrivkey = this._ed25519KeyPair.getCryptedPrivKey(secretBox).data()
community.rootChaincode = this._ed25519KeyPair.getChainCode()?.data()
}
public get publicKey(): Buffer {
return this._publicKey
const publicKey = this._ed25519KeyPair.getPublicKey()
if (!publicKey) {
throw new LogError('invalid key pair, get empty public key')
}
return publicKey.data()
}
public get keyPair(): KeyPairEd25519 {
return this._ed25519KeyPair
}
public derive(path: number[]): KeyPair {
const extendedPrivateKey = this.getExtendPrivateKey()
return new KeyPair(
path.reduce(
(extendPrivateKey: Buffer, node: number) => derivePrivate(extendPrivateKey, node),
extendedPrivateKey,
(keyPair: KeyPairEd25519, node: number) => keyPair.deriveChild(node),
this._ed25519KeyPair,
),
)
}
public sign(message: Buffer): Buffer {
return sign(message, this.getExtendPrivateKey())
return this._ed25519KeyPair.sign(new MemoryBlock(message)).data()
}
public static verify(message: Buffer, { signature, pubKey }: SignaturePair): boolean {
return verify(message, signature, pubKey)
public static verify(message: Buffer, signaturePair: SignaturePair): boolean {
const publicKeyPair = new KeyPairEd25519(signaturePair.getPubkey())
const signature = signaturePair.getSignature()
if (!signature) {
throw new LogError('missing signature')
}
return publicKeyPair.verify(new MemoryBlock(message), signature)
}
private createSecretBox(salt: string): SecretKeyCryptography {
if (!CONFIG.GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD) {
throw new LogError(
'missing GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD in env or config',
)
}
const secretBox = new SecretKeyCryptography()
secretBox.createKey(salt, CONFIG.GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD)
return secretBox
}
}

View File

@ -1,48 +0,0 @@
// https://www.npmjs.com/package/bip39
import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39'
// eslint-disable-next-line camelcase
import { randombytes_buf } from 'sodium-native'
import { LogError } from '@/server/LogError'
export class Mnemonic {
private _passphrase = ''
public constructor(seed?: Buffer | string) {
if (seed) {
Mnemonic.validateSeed(seed)
this._passphrase = entropyToMnemonic(seed)
return
}
const entropy = Buffer.alloc(256)
randombytes_buf(entropy)
this._passphrase = entropyToMnemonic(entropy)
}
public get passphrase(): string {
return this._passphrase
}
public get seed(): Buffer {
return mnemonicToSeedSync(this._passphrase)
}
public static validateSeed(seed: Buffer | string): void {
let seedBuffer: Buffer
if (!Buffer.isBuffer(seed)) {
seedBuffer = Buffer.from(seed, 'hex')
} else {
seedBuffer = seed
}
if (seedBuffer.length < 16 || seedBuffer.length > 32 || seedBuffer.length % 4 !== 0) {
throw new LogError(
'invalid seed, must be in binary between 16 and 32 Bytes, Power of 4, for more infos: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic',
{
seedBufferHex: seedBuffer.toString('hex'),
toShort: seedBuffer.length < 16,
toLong: seedBuffer.length > 32,
powerOf4: seedBuffer.length % 4,
},
)
}
}
}

View File

@ -1,18 +1,17 @@
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
import {
GradidoTransaction,
InteractionSerialize,
InteractionToJson,
TransactionBody,
} from 'gradido-blockchain-js'
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { LogError } from '@/server/LogError'
import { bodyBytesToTransactionBody, transactionBodyToBodyBytes } from '@/utils/typeConverter'
import { AccountRepository } from './Account.repository'
import { BackendTransactionFactory } from './BackendTransaction.factory'
import { CommunityRepository } from './Community.repository'
import { TransactionBodyBuilder } from './proto/TransactionBody.builder'
export class TransactionBuilder {
private transaction: Transaction
@ -97,16 +96,6 @@ export class TransactionBuilder {
return this
}
public addBackendTransaction(transactionDraft: TransactionDraft): TransactionBuilder {
if (!this.transaction.backendTransactions) {
this.transaction.backendTransactions = []
}
this.transaction.backendTransactions.push(
BackendTransactionFactory.createFromTransactionDraft(transactionDraft),
)
return this
}
public async setCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
// get sender community
const community = await CommunityRepository.getCommunityForUserIdentifier(user)
@ -122,58 +111,43 @@ export class TransactionBuilder {
return this.setOtherCommunity(otherCommunity)
}
public async fromGradidoTransactionSearchForAccounts(
gradidoTransaction: GradidoTransaction,
): Promise<TransactionBuilder> {
this.transaction.bodyBytes = Buffer.from(gradidoTransaction.bodyBytes)
const transactionBody = bodyBytesToTransactionBody(this.transaction.bodyBytes)
this.fromTransactionBody(transactionBody)
const firstSigPair = gradidoTransaction.getFirstSignature()
// TODO: adapt if transactions with more than one signatures where added
// get recipient and signer accounts if not already set
this.transaction.signingAccount ??= await AccountRepository.findAccountByPublicKey(
firstSigPair.pubKey,
)
this.transaction.recipientAccount ??= await AccountRepository.findAccountByPublicKey(
transactionBody.getRecipientPublicKey(),
)
this.transaction.signature = Buffer.from(firstSigPair.signature)
return this
public fromGradidoTransaction(transaction: GradidoTransaction): TransactionBuilder {
const body = transaction.getTransactionBody()
if (!body) {
throw new LogError('missing transaction body on Gradido Transaction')
}
// set first signature
const firstSignature = transaction.getSignatureMap().getSignaturePairs().get(0).getSignature()
if (!firstSignature) {
throw new LogError('error missing first signature')
}
this.transaction.signature = firstSignature.data()
return this.fromTransactionBody(body, transaction.getBodyBytes()?.data())
}
public fromGradidoTransaction(gradidoTransaction: GradidoTransaction): TransactionBuilder {
this.transaction.bodyBytes = Buffer.from(gradidoTransaction.bodyBytes)
const transactionBody = bodyBytesToTransactionBody(this.transaction.bodyBytes)
this.fromTransactionBody(transactionBody)
const firstSigPair = gradidoTransaction.getFirstSignature()
// TODO: adapt if transactions with more than one signatures where added
this.transaction.signature = Buffer.from(firstSigPair.signature)
return this
}
public fromTransactionBody(transactionBody: TransactionBody): TransactionBuilder {
transactionBody.fillTransactionRecipe(this.transaction)
this.transaction.bodyBytes ??= transactionBodyToBodyBytes(transactionBody)
return this
}
public fromTransactionBodyBuilder(
transactionBodyBuilder: TransactionBodyBuilder,
public fromTransactionBody(
transactionBody: TransactionBody,
bodyBytes: Buffer | null | undefined,
): TransactionBuilder {
const signingAccount = transactionBodyBuilder.getSigningAccount()
if (signingAccount) {
this.setSigningAccount(signingAccount)
if (!bodyBytes) {
bodyBytes = new InteractionSerialize(transactionBody).run()?.data()
}
const recipientAccount = transactionBodyBuilder.getRecipientAccount()
if (recipientAccount) {
this.setRecipientAccount(recipientAccount)
if (!bodyBytes) {
throw new LogError(
'cannot serialize TransactionBody',
JSON.parse(new InteractionToJson(transactionBody).run()),
)
}
this.fromTransactionBody(transactionBodyBuilder.getTransactionBody())
this.transaction.type = transactionBody.getTransactionType()
this.transaction.createdAt = new Date(transactionBody.getCreatedAt().getDate())
this.transaction.protocolVersion = transactionBody.getVersionNumber()
const transferAmount = transactionBody.getTransferAmount()
this.transaction.amount = transferAmount
? transferAmount.getAmount().getGradidoCent()
: undefined
this.transaction.bodyBytes ??= bodyBytes
return this
}
}

View File

@ -1,323 +0,0 @@
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
import { Decimal } from 'decimal.js-light'
import { logger } from '@/logging/logger'
import { CommunityRoot } from './proto/3_3/CommunityRoot'
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
import { GradidoCreation } from './proto/3_3/GradidoCreation'
import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer'
import { GradidoTransfer } from './proto/3_3/GradidoTransfer'
import { RegisterAddress } from './proto/3_3/RegisterAddress'
import { TransactionBody } from './proto/3_3/TransactionBody'
import { TransactionLogic } from './Transaction.logic'
let a: Transaction
let b: Transaction
describe('data/transaction.logic', () => {
describe('isBelongTogether', () => {
beforeEach(() => {
const now = new Date()
let body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.transfer = new GradidoTransfer()
body.otherGroup = 'recipient group'
a = new Transaction()
a.community = new Community()
a.communityId = 1
a.otherCommunityId = 2
a.id = 1
a.signingAccountId = 1
a.recipientAccountId = 2
a.createdAt = now
a.amount = new Decimal('100')
a.signature = Buffer.alloc(64)
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = new TransactionBody()
body.type = CrossGroupType.INBOUND
body.transfer = new GradidoTransfer()
body.otherGroup = 'sending group'
b = new Transaction()
b.community = new Community()
b.communityId = 2
b.otherCommunityId = 1
b.id = 2
b.signingAccountId = 1
b.recipientAccountId = 2
b.createdAt = now
b.amount = new Decimal('100')
b.signature = Buffer.alloc(64)
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
})
const spy = jest.spyOn(logger, 'info')
it('true', () => {
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(true)
})
it('false because of same id', () => {
b.id = 1
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('id is the same, it is the same transaction!')
})
it('false because of different signing accounts', () => {
b.signingAccountId = 17
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
it('false because of different recipient accounts', () => {
b.recipientAccountId = 21
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
it('false because of different community ids', () => {
b.communityId = 6
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
it('false because of different other community ids', () => {
b.otherCommunityId = 3
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
it('false because of different createdAt', () => {
b.createdAt = new Date('2021-01-01T17:12')
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
describe('false because of mismatching cross group type', () => {
const body = new TransactionBody()
it('a is LOCAL', () => {
body.type = CrossGroupType.LOCAL
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenNthCalledWith(7, 'no one can be LOCAL')
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('b is LOCAL', () => {
body.type = CrossGroupType.LOCAL
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenNthCalledWith(9, 'no one can be LOCAL')
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('both are INBOUND', () => {
body.type = CrossGroupType.INBOUND
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('both are OUTBOUND', () => {
body.type = CrossGroupType.OUTBOUND
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('a is CROSS', () => {
body.type = CrossGroupType.CROSS
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('b is CROSS', () => {
body.type = CrossGroupType.CROSS
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('true with a as INBOUND and b as OUTBOUND', () => {
let body = TransactionBody.fromBodyBytes(a.bodyBytes)
body.type = CrossGroupType.INBOUND
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = TransactionBody.fromBodyBytes(b.bodyBytes)
body.type = CrossGroupType.OUTBOUND
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(true)
})
})
describe('false because of transaction type not suitable for cross group transactions', () => {
const body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
it('without transaction type (broken TransactionBody)', () => {
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(() => logic.isBelongTogether(b)).toThrowError("couldn't determine transaction type")
})
it('not the same transaction types', () => {
const body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.registerAddress = new RegisterAddress()
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"transaction types don't match",
expect.objectContaining({}),
)
})
it('community root cannot be a cross group transaction', () => {
let body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.communityRoot = new CommunityRoot()
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = new TransactionBody()
body.type = CrossGroupType.INBOUND
body.communityRoot = new CommunityRoot()
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"TransactionType COMMUNITY_ROOT couldn't be a CrossGroup Transaction",
)
})
it('Gradido Creation cannot be a cross group transaction', () => {
let body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.creation = new GradidoCreation()
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = new TransactionBody()
body.type = CrossGroupType.INBOUND
body.creation = new GradidoCreation()
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"TransactionType GRADIDO_CREATION couldn't be a CrossGroup Transaction",
)
})
it('Deferred Transfer cannot be a cross group transaction', () => {
let body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.deferredTransfer = new GradidoDeferredTransfer()
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = new TransactionBody()
body.type = CrossGroupType.INBOUND
body.deferredTransfer = new GradidoDeferredTransfer()
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"TransactionType GRADIDO_DEFERRED_TRANSFER couldn't be a CrossGroup Transaction",
)
})
})
describe('false because of wrong amount', () => {
it('amount missing on a', () => {
a.amount = undefined
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('missing amount')
})
it('amount missing on b', () => {
b.amount = undefined
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('missing amount')
})
it('amount not the same', () => {
a.amount = new Decimal('101')
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('amounts mismatch', expect.objectContaining({}))
})
})
it('false because otherGroup are the same', () => {
const body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.transfer = new GradidoTransfer()
body.otherGroup = 'sending group'
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('otherGroups are the same', expect.objectContaining({}))
})
it('false because of different memos', () => {
const body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.transfer = new GradidoTransfer()
body.otherGroup = 'recipient group'
body.memo = 'changed memo'
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('memo differ', expect.objectContaining({}))
})
})
})

View File

@ -1,200 +0,0 @@
import { Transaction } from '@entity/Transaction'
import { Not } from 'typeorm'
import { logger } from '@/logging/logger'
import { TransactionBodyLoggingView } from '@/logging/TransactionBodyLogging.view'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { LogError } from '@/server/LogError'
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
import { TransactionType } from './proto/3_3/enum/TransactionType'
import { TransactionBody } from './proto/3_3/TransactionBody'
export class TransactionLogic {
protected transactionBody: TransactionBody | undefined
// eslint-disable-next-line no-useless-constructor
public constructor(private self: Transaction) {}
/**
* search for transaction pair for Cross Group Transaction
* @returns
*/
public async findPairTransaction(): Promise<Transaction> {
const type = this.getBody().type
if (type === CrossGroupType.LOCAL) {
throw new LogError("local transaction don't has a pairing transaction")
}
// check if already was loaded from db
if (this.self.pairingTransaction) {
return this.self.pairingTransaction
}
if (this.self.pairingTransaction) {
const pairingTransaction = await Transaction.findOneBy({ id: this.self.pairingTransaction })
if (pairingTransaction) {
return pairingTransaction
}
}
// check if we find some in db
const sameCreationDateTransactions = await Transaction.findBy({
createdAt: this.self.createdAt,
id: Not(this.self.id),
})
if (
sameCreationDateTransactions.length === 1 &&
this.isBelongTogether(sameCreationDateTransactions[0])
) {
return sameCreationDateTransactions[0]
}
// this approach only work if all entities get ids really incremented by one
if (type === CrossGroupType.OUTBOUND) {
const prevTransaction = await Transaction.findOneBy({ id: this.self.id - 1 })
if (prevTransaction && this.isBelongTogether(prevTransaction)) {
return prevTransaction
}
} else if (type === CrossGroupType.INBOUND) {
const nextTransaction = await Transaction.findOneBy({ id: this.self.id + 1 })
if (nextTransaction && this.isBelongTogether(nextTransaction)) {
return nextTransaction
}
}
throw new LogError("couldn't find valid pairing transaction", {
id: this.self.id,
type: CrossGroupType[type],
transactionCountWithSameCreatedAt: sameCreationDateTransactions.length,
})
}
/**
* check if two transactions belong together
* are they pairs for a cross group transaction
* @param otherTransaction
*/
public isBelongTogether(otherTransaction: Transaction): boolean {
if (this.self.id === otherTransaction.id) {
logger.info('id is the same, it is the same transaction!')
return false
}
if (
this.self.signingAccountId !== otherTransaction.signingAccountId ||
this.self.recipientAccountId !== otherTransaction.recipientAccountId ||
this.self.communityId !== otherTransaction.otherCommunityId ||
this.self.otherCommunityId !== otherTransaction.communityId ||
this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime()
) {
logger.info('transaction a and b are not pairs', {
a: new TransactionLoggingView(this.self).toJSON(),
b: new TransactionLoggingView(otherTransaction).toJSON(),
})
return false
}
const body = this.getBody()
const otherBody = TransactionBody.fromBodyBytes(otherTransaction.bodyBytes)
/**
* both must be Cross or
* one can be OUTBOUND and one can be INBOUND
* no one can be LOCAL
*/
if (!this.validCrossGroupTypes(body.type, otherBody.type)) {
logger.info("cross group types don't match", {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
return false
}
const type = body.getTransactionType()
const otherType = otherBody.getTransactionType()
if (!type || !otherType) {
throw new LogError("couldn't determine transaction type", {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
}
if (type !== otherType) {
logger.info("transaction types don't match", {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
return false
}
if (
[
TransactionType.COMMUNITY_ROOT,
TransactionType.GRADIDO_CREATION,
TransactionType.GRADIDO_DEFERRED_TRANSFER,
].includes(type)
) {
logger.info(`TransactionType ${TransactionType[type]} couldn't be a CrossGroup Transaction`)
return false
}
if (
[
TransactionType.GRADIDO_CREATION,
TransactionType.GRADIDO_TRANSFER,
TransactionType.GRADIDO_DEFERRED_TRANSFER,
].includes(type)
) {
if (!this.self.amount || !otherTransaction.amount) {
logger.info('missing amount')
return false
}
if (this.self.amount.cmp(otherTransaction.amount.toString())) {
logger.info('amounts mismatch', {
a: this.self.amount.toString(),
b: otherTransaction.amount.toString(),
})
return false
}
}
if (body.otherGroup === otherBody.otherGroup) {
logger.info('otherGroups are the same', {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
return false
}
if (body.memo !== otherBody.memo) {
logger.info('memo differ', {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
return false
}
return true
}
/**
* both must be CROSS or
* one can be OUTBOUND and one can be INBOUND
* no one can be LOCAL
* @return true if crossGroupTypes are valid
*/
protected validCrossGroupTypes(a: CrossGroupType, b: CrossGroupType): boolean {
logger.debug('compare ', {
a: CrossGroupType[a],
b: CrossGroupType[b],
})
if (a === CrossGroupType.LOCAL || b === CrossGroupType.LOCAL) {
logger.info('no one can be LOCAL')
return false
}
if (
(a === CrossGroupType.INBOUND && b === CrossGroupType.OUTBOUND) ||
(a === CrossGroupType.OUTBOUND && b === CrossGroupType.INBOUND)
) {
return true // One can be INBOUND and one can be OUTBOUND
}
return a === CrossGroupType.CROSS && b === CrossGroupType.CROSS
}
public getBody(): TransactionBody {
if (!this.transactionBody) {
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
}
return this.transactionBody
}
}

View File

@ -1,35 +0,0 @@
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
import { Field, Message } from 'protobufjs'
import { AbstractTransaction } from '../AbstractTransaction'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class CommunityRoot extends Message<CommunityRoot> implements AbstractTransaction {
public constructor(community?: Community) {
if (community) {
super({
rootPubkey: community.rootPubkey,
gmwPubkey: community.gmwAccount?.derive2Pubkey,
aufPubkey: community.aufAccount?.derive2Pubkey,
})
} else {
super()
}
}
@Field.d(1, 'bytes')
public rootPubkey: Buffer
// community public budget account
@Field.d(2, 'bytes')
public gmwPubkey: Buffer
// community compensation and environment founds account
@Field.d(3, 'bytes')
public aufPubkey: Buffer
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
public fillTransactionRecipe(recipe: Transaction): void {}
}

View File

@ -1,41 +0,0 @@
import { Field, Message } from 'protobufjs'
import { base64ToBuffer } from '@/utils/typeConverter'
import { GradidoTransaction } from './GradidoTransaction'
import { TimestampSeconds } from './TimestampSeconds'
/*
id will be set by Node server
running_hash will be also set by Node server,
calculated from previous transaction running_hash and this id, transaction and received
*/
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class ConfirmedTransaction extends Message<ConfirmedTransaction> {
static fromBase64(base64: string): ConfirmedTransaction {
return ConfirmedTransaction.decode(new Uint8Array(base64ToBuffer(base64)))
}
@Field.d(1, 'uint64')
id: Long
@Field.d(2, 'GradidoTransaction')
transaction: GradidoTransaction
@Field.d(3, 'TimestampSeconds')
confirmedAt: TimestampSeconds
@Field.d(4, 'string')
versionNumber: string
@Field.d(5, 'bytes')
runningHash: Buffer
@Field.d(6, 'bytes')
messageId: Buffer
@Field.d(7, 'string')
accountBalance: string
}

View File

@ -1,22 +0,0 @@
import 'reflect-metadata'
import { TransactionErrorType } from '@enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { GradidoCreation } from './GradidoCreation'
describe('proto/3.3/GradidoCreation', () => {
it('test with missing targetDate', () => {
const transactionDraft = new TransactionDraft()
expect(() => {
// eslint-disable-next-line no-new
new GradidoCreation(transactionDraft)
}).toThrowError(
new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing targetDate for contribution',
),
)
})
})

View File

@ -1,53 +0,0 @@
import { Account } from '@entity/Account'
import { Transaction } from '@entity/Transaction'
import { Decimal } from 'decimal.js-light'
import { Field, Message } from 'protobufjs'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { AbstractTransaction } from '../AbstractTransaction'
import { TimestampSeconds } from './TimestampSeconds'
import { TransferAmount } from './TransferAmount'
// need signature from group admin or
// percent of group users another than the receiver
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoCreation extends Message<GradidoCreation> implements AbstractTransaction {
constructor(transaction?: TransactionDraft, recipientAccount?: Account) {
if (transaction) {
if (!transaction.targetDate) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing targetDate for contribution',
)
}
super({
recipient: new TransferAmount({
amount: transaction.amount.toString(),
pubkey: recipientAccount?.derive2Pubkey,
}),
targetDate: new TimestampSeconds(new Date(transaction.targetDate)),
})
} else {
super()
}
}
// recipient: TransferAmount contain
// - recipient public key
// - amount
// - communityId // only set if not the same as recipient community
@Field.d(1, TransferAmount)
public recipient: TransferAmount
@Field.d(3, 'TimestampSeconds')
public targetDate: TimestampSeconds
public fillTransactionRecipe(recipe: Transaction): void {
recipe.amount = new Decimal(this.recipient.amount ?? 0)
}
}

View File

@ -1,42 +0,0 @@
import { Transaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { Field, Message } from 'protobufjs'
import { AbstractTransaction } from '../AbstractTransaction'
import { GradidoTransfer } from './GradidoTransfer'
import { TimestampSeconds } from './TimestampSeconds'
// transaction type for chargeable transactions
// for transaction for people which haven't a account already
// consider using a seed number for key pair generation for recipient
// using seed as redeem key for claiming transaction, technically make a default Transfer transaction from recipient address
// seed must be long enough to prevent brute force, maybe base64 encoded
// to own account
// https://www.npmjs.com/package/@apollo/protobufjs
export class GradidoDeferredTransfer
// eslint-disable-next-line no-use-before-define
extends Message<GradidoDeferredTransfer>
implements AbstractTransaction
{
// amount is amount with decay for time span between transaction was received and timeout
// useable amount can be calculated
// recipient address don't need to be registered in blockchain with register address
@Field.d(1, GradidoTransfer)
public transfer: GradidoTransfer
// if timeout timestamp is reached if it wasn't used, it will be booked back minus decay
// technically on blockchain no additional transaction will be created because how should sign it?
// the decay for amount and the seconds until timeout is lost no matter what happened
// consider is as fee for this service
// rest decay could be transferred back as separate transaction
@Field.d(2, 'TimestampSeconds')
public timeout: TimestampSeconds
// split for n recipient
// max gradido per recipient? or per transaction with cool down?
public fillTransactionRecipe(recipe: Transaction): void {
recipe.amount = new Decimal(this.transfer.sender.amount ?? 0)
}
}

View File

@ -1,48 +0,0 @@
import { Field, Message } from 'protobufjs'
import { LogError } from '@/server/LogError'
import { SignatureMap } from './SignatureMap'
import { SignaturePair } from './SignaturePair'
import { TransactionBody } from './TransactionBody'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoTransaction extends Message<GradidoTransaction> {
constructor(body?: TransactionBody) {
if (body) {
super({
sigMap: new SignatureMap(),
bodyBytes: Buffer.from(TransactionBody.encode(body).finish()),
})
} else {
super()
}
}
@Field.d(1, SignatureMap)
public sigMap: SignatureMap
// inspired by Hedera
// bodyBytes are the payload for signature
// bodyBytes are serialized TransactionBody
@Field.d(2, 'bytes')
public bodyBytes: Buffer
// if it is a cross group transaction the parent message
// id from outbound transaction or other by cross
@Field.d(3, 'bytes')
public parentMessageId?: Buffer
getFirstSignature(): SignaturePair {
const sigPair = this.sigMap.sigPair
if (sigPair.length !== 1) {
throw new LogError("signature count don't like expected")
}
return sigPair[0]
}
getTransactionBody(): TransactionBody {
return TransactionBody.fromBodyBytes(this.bodyBytes)
}
}

View File

@ -1,49 +0,0 @@
import { Account } from '@entity/Account'
import { Transaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { Field, Message } from 'protobufjs'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { AbstractTransaction } from '../AbstractTransaction'
import { TransferAmount } from './TransferAmount'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GradidoTransfer extends Message<GradidoTransfer> implements AbstractTransaction {
constructor(
transaction?: TransactionDraft,
signingAccount?: Account,
recipientAccount?: Account,
coinOrigin?: string,
) {
if (transaction) {
super({
sender: new TransferAmount({
amount: transaction.amount.toString(),
pubkey: signingAccount?.derive2Pubkey,
communityId: coinOrigin,
}),
recipient: recipientAccount?.derive2Pubkey,
})
} else {
super()
}
}
// sender: TransferAmount contain
// - sender public key
// - amount
// - communityId // only set if not the same as sender and recipient community
@Field.d(1, TransferAmount)
public sender: TransferAmount
// the recipient public key
@Field.d(2, 'bytes')
public recipient: Buffer
public fillTransactionRecipe(recipe: Transaction): void {
recipe.amount = new Decimal(this.sender?.amount ?? 0)
}
}

View File

@ -1,23 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Transaction } from '@entity/Transaction'
import { Field, Message } from 'protobufjs'
import { AbstractTransaction } from '../AbstractTransaction'
// connect group together
// only CrossGroupType CROSS (in TransactionBody)
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class GroupFriendsUpdate extends Message<GroupFriendsUpdate> implements AbstractTransaction {
// if set to true, colors of this both groups are trait as the same
// on creation user get coins still in there color
// on transfer into another group which a connection exist,
// coins will be automatic swapped into user group color coin
// (if fusion between src coin and dst coin is enabled)
@Field.d(1, 'bool')
public colorFusion: boolean
public fillTransactionRecipe(recipe: Transaction): void {
throw new Error('Method not implemented.')
}
}

View File

@ -1,49 +0,0 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Account } from '@entity/Account'
import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User'
import { Field, Message } from 'protobufjs'
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
import { AccountType } from '@/graphql/enum/AccountType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { accountTypeToAddressType } from '@/utils/typeConverter'
import { AbstractTransaction } from '../AbstractTransaction'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class RegisterAddress extends Message<RegisterAddress> implements AbstractTransaction {
constructor(transaction?: UserAccountDraft, account?: Account) {
if (transaction) {
super({ addressType: accountTypeToAddressType(transaction.accountType) })
if (account) {
this.derivationIndex = account.derivationIndex
this.accountPubkey = account.derive2Pubkey
if (account.user) {
this.userPubkey = account.user.derive1Pubkey
}
}
} else {
super()
}
}
@Field.d(1, 'bytes')
public userPubkey: Buffer
@Field.d(2, AddressType)
public addressType: AddressType
@Field.d(3, 'bytes')
public nameHash: Buffer
@Field.d(4, 'bytes')
public accountPubkey: Buffer
@Field.d(5, 'uint32')
public derivationIndex?: number
public fillTransactionRecipe(_recipe: Transaction): void {}
}

View File

@ -1,14 +0,0 @@
import { Field, Message } from 'protobufjs'
import { SignaturePair } from './SignaturePair'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class SignatureMap extends Message<SignatureMap> {
constructor() {
super({ sigPair: [] })
}
@Field.d(1, SignaturePair, 'repeated')
public sigPair: SignaturePair[]
}

View File

@ -1,15 +0,0 @@
import { Field, Message } from 'protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class SignaturePair extends Message<SignaturePair> {
@Field.d(1, 'bytes')
public pubKey: Buffer
@Field.d(2, 'bytes')
public signature: Buffer
public validate(): boolean {
return this.pubKey.length === 32 && this.signature.length === 64
}
}

View File

@ -1,16 +0,0 @@
import { Timestamp } from './Timestamp'
describe('test timestamp constructor', () => {
it('with date input object', () => {
const now = new Date('2011-04-17T12:01:10.109')
const timestamp = new Timestamp(now)
expect(timestamp.seconds).toEqual(1303041670)
expect(timestamp.nanoSeconds).toEqual(109000000)
})
it('with milliseconds number input', () => {
const timestamp = new Timestamp(1303041670109)
expect(timestamp.seconds).toEqual(1303041670)
expect(timestamp.nanoSeconds).toEqual(109000000)
})
})

View File

@ -1,27 +0,0 @@
import { Field, Message } from 'protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class Timestamp extends Message<Timestamp> {
public constructor(input?: Date | number) {
let seconds = 0
let nanoSeconds = 0
if (input instanceof Date) {
seconds = Math.floor(input.getTime() / 1000)
nanoSeconds = (input.getTime() % 1000) * 1000000 // Convert milliseconds to nanoseconds
} else if (typeof input === 'number') {
// Calculate seconds and nanoseconds from milliseconds
seconds = Math.floor(input / 1000)
nanoSeconds = (input % 1000) * 1000000
}
super({ seconds, nanoSeconds })
}
// Number of complete seconds since the start of the epoch
@Field.d(1, 'int64')
public seconds: number
// Number of nanoseconds since the start of the last second
@Field.d(2, 'int32')
public nanoSeconds: number
}

View File

@ -1,14 +0,0 @@
import { TimestampSeconds } from './TimestampSeconds'
describe('test TimestampSeconds constructor', () => {
it('with date input object', () => {
const now = new Date('2011-04-17T12:01:10.109')
const timestamp = new TimestampSeconds(now)
expect(timestamp.seconds).toEqual(1303041670)
})
it('with milliseconds number input', () => {
const timestamp = new TimestampSeconds(1303041670109)
expect(timestamp.seconds).toEqual(1303041670)
})
})

View File

@ -1,20 +0,0 @@
import { Field, Message } from 'protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class TimestampSeconds extends Message<TimestampSeconds> {
public constructor(input?: Date | number) {
let seconds = 0
// Calculate seconds from milliseconds
if (input instanceof Date) {
seconds = Math.floor(input.getTime() / 1000)
} else if (typeof input === 'number') {
seconds = Math.floor(input / 1000)
}
super({ seconds })
}
// Number of complete seconds since the start of the epoch
@Field.d(1, 'int64')
public seconds: number
}

View File

@ -1,157 +0,0 @@
import { Transaction } from '@entity/Transaction'
import { Field, Message, OneOf } from 'protobufjs'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { LogError } from '@/server/LogError'
import { timestampToDate } from '@/utils/typeConverter'
import { AbstractTransaction } from '../AbstractTransaction'
import { determineCrossGroupType, determineOtherGroup } from '../transactionBody.logic'
import { CommunityRoot } from './CommunityRoot'
import { PROTO_TRANSACTION_BODY_VERSION_NUMBER } from './const'
import { CrossGroupType } from './enum/CrossGroupType'
import { TransactionType } from './enum/TransactionType'
import { GradidoCreation } from './GradidoCreation'
import { GradidoDeferredTransfer } from './GradidoDeferredTransfer'
import { GradidoTransfer } from './GradidoTransfer'
import { GroupFriendsUpdate } from './GroupFriendsUpdate'
import { RegisterAddress } from './RegisterAddress'
import { Timestamp } from './Timestamp'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class TransactionBody extends Message<TransactionBody> {
public constructor(transaction?: TransactionDraft | CommunityDraft | UserAccountDraft) {
if (transaction) {
let type = CrossGroupType.LOCAL
let otherGroup = ''
if (transaction instanceof TransactionDraft) {
type = determineCrossGroupType(transaction)
otherGroup = determineOtherGroup(type, transaction)
}
super({
memo: 'Not implemented yet',
createdAt: new Timestamp(new Date(transaction.createdAt)),
versionNumber: PROTO_TRANSACTION_BODY_VERSION_NUMBER,
type,
otherGroup,
})
} else {
super()
}
}
public static fromBodyBytes(bodyBytes: Buffer) {
try {
return TransactionBody.decode(new Uint8Array(bodyBytes))
} catch (error) {
logger.error('error decoding body from gradido transaction: %s', error)
throw new TransactionError(
TransactionErrorType.PROTO_DECODE_ERROR,
'cannot decode body from gradido transaction',
)
}
}
@Field.d(1, 'string')
public memo: string
@Field.d(2, Timestamp)
public createdAt: Timestamp
@Field.d(3, 'string')
public versionNumber: string
@Field.d(4, CrossGroupType)
public type: CrossGroupType
@Field.d(5, 'string')
public otherGroup: string
@OneOf.d(
'gradidoTransfer',
'gradidoCreation',
'groupFriendsUpdate',
'registerAddress',
'gradidoDeferredTransfer',
'communityRoot',
)
public data: string
@Field.d(6, 'GradidoTransfer')
transfer?: GradidoTransfer
@Field.d(7, 'GradidoCreation')
creation?: GradidoCreation
@Field.d(8, 'GroupFriendsUpdate')
groupFriendsUpdate?: GroupFriendsUpdate
@Field.d(9, 'RegisterAddress')
registerAddress?: RegisterAddress
@Field.d(10, 'GradidoDeferredTransfer')
deferredTransfer?: GradidoDeferredTransfer
@Field.d(11, 'CommunityRoot')
communityRoot?: CommunityRoot
public getTransactionType(): TransactionType | undefined {
if (this.transfer) return TransactionType.GRADIDO_TRANSFER
else if (this.creation) return TransactionType.GRADIDO_CREATION
else if (this.groupFriendsUpdate) return TransactionType.GROUP_FRIENDS_UPDATE
else if (this.registerAddress) return TransactionType.REGISTER_ADDRESS
else if (this.deferredTransfer) return TransactionType.GRADIDO_DEFERRED_TRANSFER
else if (this.communityRoot) return TransactionType.COMMUNITY_ROOT
}
// The `TransactionBody` class utilizes Protobuf's `OneOf` field structure which, according to Protobuf documentation
// (https://protobuf.dev/programming-guides/proto3/#oneof), allows only one field within the group to be set at a time.
// Therefore, accessing the `getTransactionDetails()` method returns the first initialized value among the defined fields,
// each of which should be of type AbstractTransaction. It's important to note that due to the nature of Protobuf's `OneOf`,
// only one type from the defined options can be set within the object obtained from Protobuf.
//
// If multiple fields are set in a single object, the method `getTransactionDetails()` will return the first defined value
// based on the order of checks. Developers should handle this behavior according to the expected Protobuf structure.
public getTransactionDetails(): AbstractTransaction | undefined {
if (this.transfer) return this.transfer
if (this.creation) return this.creation
if (this.groupFriendsUpdate) return this.groupFriendsUpdate
if (this.registerAddress) return this.registerAddress
if (this.deferredTransfer) return this.deferredTransfer
if (this.communityRoot) return this.communityRoot
}
public fillTransactionRecipe(recipe: Transaction): void {
recipe.createdAt = timestampToDate(this.createdAt)
recipe.protocolVersion = this.versionNumber
const transactionType = this.getTransactionType()
if (!transactionType) {
throw new LogError("invalid TransactionBody couldn't determine transaction type")
}
recipe.type = transactionType.valueOf()
this.getTransactionDetails()?.fillTransactionRecipe(recipe)
}
public getRecipientPublicKey(): Buffer | undefined {
if (this.transfer) {
// this.transfer.recipient contains the publicKey of the recipient
return this.transfer.recipient
}
if (this.creation) {
return this.creation.recipient.pubkey
}
if (this.deferredTransfer) {
// this.deferredTransfer.transfer.recipient contains the publicKey of the recipient
return this.deferredTransfer.transfer.recipient
}
return undefined
}
}

View File

@ -1,16 +0,0 @@
import { Field, Message } from 'protobufjs'
// https://www.npmjs.com/package/@apollo/protobufjs
// eslint-disable-next-line no-use-before-define
export class TransferAmount extends Message<TransferAmount> {
@Field.d(1, 'bytes')
public pubkey: Buffer
@Field.d(2, 'string')
public amount: string
// community which created this coin
// used for colored coins
@Field.d(3, 'string')
public communityId: string
}

View File

@ -1 +0,0 @@
export const PROTO_TRANSACTION_BODY_VERSION_NUMBER = '3.3'

View File

@ -1,14 +0,0 @@
/**
* Enum for protobuf
* used from RegisterAddress to determine account type
* master implementation: https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/register_address.proto
*/
export enum AddressType {
NONE = 0, // if no address was found
COMMUNITY_HUMAN = 1, // creation account for human
COMMUNITY_GMW = 2, // community public budget account
COMMUNITY_AUF = 3, // community compensation and environment founds account
COMMUNITY_PROJECT = 4, // no creations allowed
SUBACCOUNT = 5, // no creations allowed
CRYPTO_ACCOUNT = 6, // user control his keys, no creations
}

View File

@ -1,22 +0,0 @@
/**
* Enum for protobuf
* Determine Cross Group type of Transactions
* LOCAL: no cross group transactions, sender and recipient community are the same, only one transaction
* INBOUND: cross group transaction, Inbound part. On recipient community chain. Recipient side by Transfer Transactions
* OUTBOUND: cross group transaction, Outbound part. On sender community chain. Sender side by Transfer Transactions
* CROSS: for cross group transaction which haven't a direction like group friend update
* master implementation: https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/transaction_body.proto
*
* Transaction Handling differ from database focused backend
* In Backend for each transfer transaction there are always two entries in db,
* on for sender user and one for recipient user despite storing basically the same data two times
* In Blockchain Implementation there only two transactions on cross group transactions, one for
* the sender community chain, one for the recipient community chain
* if the transaction stay in the community there is only one transaction
*/
export enum CrossGroupType {
LOCAL = 0,
INBOUND = 1,
OUTBOUND = 2,
CROSS = 3,
}

View File

@ -4,8 +4,8 @@
* for storing type in db as number
*/
export enum TransactionType {
GRADIDO_TRANSFER = 1,
GRADIDO_CREATION = 2,
GRADIDO_CREATION = 1,
GRADIDO_TRANSFER = 2,
GROUP_FRIENDS_UPDATE = 3,
REGISTER_ADDRESS = 4,
GRADIDO_DEFERRED_TRANSFER = 5,

View File

@ -1,5 +0,0 @@
import { Transaction } from '@entity/Transaction'
export abstract class AbstractTransaction {
public abstract fillTransactionRecipe(recipe: Transaction): void
}

View File

@ -1,147 +0,0 @@
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { LogError } from '@/server/LogError'
import { CommunityRoot } from './3_3/CommunityRoot'
import { CrossGroupType } from './3_3/enum/CrossGroupType'
import { GradidoCreation } from './3_3/GradidoCreation'
import { GradidoTransfer } from './3_3/GradidoTransfer'
import { RegisterAddress } from './3_3/RegisterAddress'
import { TransactionBody } from './3_3/TransactionBody'
export class TransactionBodyBuilder {
private signingAccount?: Account
private recipientAccount?: Account
private body: TransactionBody | undefined
// https://refactoring.guru/design-patterns/builder/typescript/example
/**
* A fresh builder instance should contain a blank product object, which is
* used in further assembly.
*/
constructor() {
this.reset()
}
public reset(): void {
this.body = undefined
this.signingAccount = undefined
this.recipientAccount = undefined
}
/**
* Concrete Builders are supposed to provide their own methods for
* retrieving results. That's because various types of builders may create
* entirely different products that don't follow the same interface.
* Therefore, such methods cannot be declared in the base Builder interface
* (at least in a statically typed programming language).
*
* Usually, after returning the end result to the client, a builder instance
* is expected to be ready to start producing another product. That's why
* it's a usual practice to call the reset method at the end of the
* `getProduct` method body. However, this behavior is not mandatory, and
* you can make your builders wait for an explicit reset call from the
* client code before disposing of the previous result.
*/
public build(): TransactionBody {
const result = this.getTransactionBody()
this.reset()
return result
}
public getTransactionBody(): TransactionBody {
if (!this.body) {
throw new LogError(
'cannot build Transaction Body, missing information, please call at least fromTransactionDraft or fromCommunityDraft',
)
}
return this.body
}
public getSigningAccount(): Account | undefined {
return this.signingAccount
}
public getRecipientAccount(): Account | undefined {
return this.recipientAccount
}
public setSigningAccount(signingAccount: Account): TransactionBodyBuilder {
this.signingAccount = signingAccount
return this
}
public setRecipientAccount(recipientAccount: Account): TransactionBodyBuilder {
this.recipientAccount = recipientAccount
return this
}
public setCrossGroupType(type: CrossGroupType): this {
if (!this.body) {
throw new LogError(
'body is undefined, please call fromTransactionDraft or fromCommunityDraft before',
)
}
this.body.type = type
return this
}
public setOtherGroup(otherGroup: string): this {
if (!this.body) {
throw new LogError(
'body is undefined, please call fromTransactionDraft or fromCommunityDraft before',
)
}
this.body.otherGroup = otherGroup
return this
}
public fromUserAccountDraft(userAccountDraft: UserAccountDraft, account: Account): this {
this.body = new TransactionBody(userAccountDraft)
this.body.registerAddress = new RegisterAddress(userAccountDraft, account)
this.body.data = 'registerAddress'
return this
}
public fromTransactionDraft(transactionDraft: TransactionDraft): TransactionBodyBuilder {
this.body = new TransactionBody(transactionDraft)
// TODO: load public keys for sender and recipient user from db
switch (transactionDraft.type) {
case InputTransactionType.CREATION:
if (!this.recipientAccount) {
throw new LogError('missing recipient account for creation transaction!')
}
this.body.creation = new GradidoCreation(transactionDraft, this.recipientAccount)
this.body.data = 'gradidoCreation'
break
case InputTransactionType.SEND:
case InputTransactionType.RECEIVE:
if (!this.recipientAccount || !this.signingAccount) {
throw new LogError('missing signing and/or recipient account for transfer transaction!')
}
this.body.transfer = new GradidoTransfer(
transactionDraft,
this.signingAccount,
this.recipientAccount,
)
this.body.data = 'gradidoTransfer'
break
}
return this
}
public fromCommunityDraft(
communityDraft: CommunityDraft,
community: Community,
): TransactionBodyBuilder {
this.body = new TransactionBody(communityDraft)
this.body.communityRoot = new CommunityRoot(community)
this.body.data = 'communityRoot'
return this
}
}

View File

@ -1,59 +0,0 @@
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { CrossGroupType } from './3_3/enum/CrossGroupType'
export const determineCrossGroupType = ({
user,
linkedUser,
type,
}: TransactionDraft): CrossGroupType => {
if (
!linkedUser.communityUuid ||
!user.communityUuid ||
linkedUser.communityUuid === '' ||
user.communityUuid === '' ||
user.communityUuid === linkedUser.communityUuid ||
type === InputTransactionType.CREATION
) {
return CrossGroupType.LOCAL
} else if (type === InputTransactionType.SEND) {
return CrossGroupType.INBOUND
} else if (type === InputTransactionType.RECEIVE) {
return CrossGroupType.OUTBOUND
}
throw new TransactionError(
TransactionErrorType.NOT_IMPLEMENTED_YET,
'cannot determine CrossGroupType',
)
}
export const determineOtherGroup = (
type: CrossGroupType,
{ user, linkedUser }: TransactionDraft,
): string => {
switch (type) {
case CrossGroupType.LOCAL:
return ''
case CrossGroupType.INBOUND:
if (!linkedUser.communityUuid || linkedUser.communityUuid === '') {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing linkedUser community id for cross group transaction',
)
}
return linkedUser.communityUuid
case CrossGroupType.OUTBOUND:
if (!user.communityUuid || user.communityUuid === '') {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing user community id for cross group transaction',
)
}
return user.communityUuid
case CrossGroupType.CROSS:
throw new TransactionError(TransactionErrorType.NOT_IMPLEMENTED_YET, 'not implemented yet')
}
}

View File

@ -12,6 +12,7 @@ export enum TransactionErrorType {
INVALID_SIGNATURE = 'Invalid Signature',
LOGIC_ERROR = 'Logic Error',
NOT_FOUND = 'Not found',
VALIDATION_ERROR = 'Validation Error',
}
registerEnumType(TransactionErrorType, {

View File

@ -7,7 +7,7 @@ import { getEnumValue } from '@/utils/typeConverter'
@ObjectType()
export class TransactionRecipe {
public constructor({ id, createdAt, type, community }: Transaction) {
public constructor({ id, createdAt, type, community, signature }: Transaction) {
const transactionType = getEnumValue(TransactionType, type)
if (!transactionType) {
throw new LogError('invalid transaction, type is missing')
@ -16,6 +16,7 @@ export class TransactionRecipe {
this.createdAt = createdAt.toString()
this.type = transactionType.toString()
this.topic = community.iotaTopic
this.signatureHex = signature.toString('hex')
}
@Field(() => Int)
@ -29,4 +30,7 @@ export class TransactionRecipe {
@Field(() => String)
topic: string
@Field(() => String)
signatureHex: string
}

View File

@ -1,11 +1,10 @@
import { Resolver, Query, Arg, Mutation, Args } from 'type-graphql'
import { CommunityArg } from '@arg/CommunityArg'
import { TransactionErrorType } from '@enum/TransactionErrorType'
import { CommunityDraft } from '@input/CommunityDraft'
import { Community } from '@model/Community'
import { TransactionError } from '@model/TransactionError'
import { TransactionResult } from '@model/TransactionResult'
import { Resolver, Query, Arg, Mutation, Args } from 'type-graphql'
import { CommunityRepository } from '@/data/Community.repository'
import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context'

View File

@ -5,12 +5,11 @@ import { TransactionDraft } from '@input/TransactionDraft'
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
import { TransactionRepository } from '@/data/Transaction.repository'
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransactionRecipe.context'
import { BackendTransactionLoggingView } from '@/logging/BackendTransactionLogging.view'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
import { LogError } from '@/server/LogError'
import { TransactionErrorType } from '../enum/TransactionErrorType'
import { TransactionError } from '../model/TransactionError'
import { TransactionRecipe } from '../model/TransactionRecipe'
import { TransactionResult } from '../model/TransactionResult'
@ -24,30 +23,30 @@ export class TransactionResolver {
): Promise<TransactionResult> {
const createTransactionRecipeContext = new CreateTransactionRecipeContext(transactionDraft)
try {
await createTransactionRecipeContext.run()
const result = await createTransactionRecipeContext.run()
if (!result) {
return new TransactionResult(
new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'cannot work with this parameters',
),
)
}
const transactionRecipe = createTransactionRecipeContext.getTransactionRecipe()
// check if a transaction with this signature already exist
const existingRecipe = await TransactionRepository.findBySignature(
transactionRecipe.signature,
)
if (existingRecipe) {
// transaction recipe with this signature already exist, we need only to store the backendTransaction
if (transactionRecipe.backendTransactions.length !== 1) {
throw new LogError('unexpected backend transaction count', {
count: transactionRecipe.backendTransactions.length,
transactionId: transactionRecipe.id,
})
}
const backendTransaction = transactionRecipe.backendTransactions[0]
backendTransaction.transactionId = transactionRecipe.id
logger.debug(
'store backendTransaction',
new BackendTransactionLoggingView(backendTransaction),
return new TransactionResult(
new TransactionError(
TransactionErrorType.ALREADY_EXIST,
'Transaction with same signature already exist',
),
)
await backendTransaction.save()
} else {
logger.debug('store transaction recipe', new TransactionLoggingView(transactionRecipe))
// we can store the transaction and with that automatic the backend transaction
// we store the transaction
await transactionRecipe.save()
}
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)

View File

@ -1,11 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import 'reflect-metadata'
import { loadCryptoKeys, MemoryBlock } from 'gradido-blockchain-js'
import { CONFIG } from '@/config'
import { BackendClient } from './client/BackendClient'
import { CommunityRepository } from './data/Community.repository'
import { Mnemonic } from './data/Mnemonic'
import { CommunityDraft } from './graphql/input/CommunityDraft'
import { AddCommunityContext } from './interactions/backendToDb/community/AddCommunity.context'
import { logger } from './logging/logger'
@ -39,8 +40,22 @@ async function waitForServer(
async function main() {
if (CONFIG.IOTA_HOME_COMMUNITY_SEED) {
Mnemonic.validateSeed(CONFIG.IOTA_HOME_COMMUNITY_SEED)
try {
const seed = MemoryBlock.fromHex(CONFIG.IOTA_HOME_COMMUNITY_SEED)
if (seed.size() < 32) {
throw new Error('seed need to be greater than 32 Bytes')
}
} catch (_) {
throw new LogError(
'IOTA_HOME_COMMUNITY_SEED must be a valid hex string, at least 64 characters long',
)
}
}
// load crypto keys for gradido blockchain lib
loadCryptoKeys(
MemoryBlock.fromHex(CONFIG.GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET),
MemoryBlock.fromHex(CONFIG.GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY),
)
// eslint-disable-next-line no-console
console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`)
const { app } = await createServer()

View File

@ -18,6 +18,7 @@ import { getDataSource } from '@/typeorm/DataSource'
import { CreateTransactionRecipeContext } from '../transaction/CreateTransactionRecipe.context'
import { CommunityRole } from './Community.role'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
export class HomeCommunityRole extends CommunityRole {
private transactionRecipe: Transaction

View File

@ -1,9 +1,12 @@
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
/* eslint-disable camelcase */
import { Account } from '@entity/Account'
import { GradidoTransactionBuilder } from 'gradido-blockchain-js'
import { UserRepository } from '@/data/User.repository'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { TransactionError } from '@/graphql/model/TransactionError'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
export abstract class AbstractTransactionRole {
// eslint-disable-next-line no-useless-constructor
@ -11,7 +14,7 @@ export abstract class AbstractTransactionRole {
abstract getSigningUser(): UserIdentifier
abstract getRecipientUser(): UserIdentifier
abstract getCrossGroupType(): CrossGroupType
abstract getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder>
public isCrossGroupTransaction(): boolean {
return (
@ -20,44 +23,14 @@ export abstract class AbstractTransactionRole {
)
}
/**
* otherGroup is the group/community on which this part of the transaction isn't stored
* Alice from 'gdd1' Send 10 GDD to Bob in 'gdd2'
* OUTBOUND came from sender, stored on sender community blockchain
* OUTBOUND: stored on 'gdd1', otherGroup: 'gdd2'
* INBOUND: goes to receiver, stored on receiver community blockchain
* INBOUND: stored on 'gdd2', otherGroup: 'gdd1'
* @returns iota topic
*/
public getOtherGroup(): string {
let user: UserIdentifier
const type = this.getCrossGroupType()
switch (type) {
case CrossGroupType.LOCAL:
return ''
case CrossGroupType.INBOUND:
user = this.getSigningUser()
if (!user.communityUuid) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing sender/signing user community id for cross group transaction',
)
}
return iotaTopicFromCommunityUUID(user.communityUuid)
case CrossGroupType.OUTBOUND:
user = this.getRecipientUser()
if (!user.communityUuid) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing recipient user community id for cross group transaction',
)
}
return iotaTopicFromCommunityUUID(user.communityUuid)
default:
throw new TransactionError(
TransactionErrorType.NOT_IMPLEMENTED_YET,
`type not implemented yet ${type}`,
)
public async loadUser(user: UserIdentifier): Promise<Account> {
const account = await UserRepository.findAccountByUserIdentifier(user)
if (!account) {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"couldn't found user account in db",
)
}
return account
}
}

View File

@ -1,13 +1,7 @@
import { Community } from '@entity/Community'
/* eslint-disable camelcase */
import { AccountLogic } from '@/data/Account.logic'
import { KeyPair } from '@/data/KeyPair'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { UserRepository } from '@/data/User.repository'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { AbstractTransactionRole } from './AbstractTransaction.role'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
@ -17,62 +11,30 @@ export class BalanceChangingTransactionRecipeRole extends AbstractTransactionRec
transactionDraft: TransactionDraft,
transactionTypeRole: AbstractTransactionRole,
): Promise<BalanceChangingTransactionRecipeRole> {
const signingUser = transactionTypeRole.getSigningUser()
const recipientUser = transactionTypeRole.getRecipientUser()
// loading signing and recipient account
// TODO: look for ways to use only one db call for both
const signingAccount = await UserRepository.findAccountByUserIdentifier(signingUser)
if (!signingAccount) {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"couldn't found sender user account in db",
)
}
const recipientAccount = await UserRepository.findAccountByUserIdentifier(recipientUser)
if (!recipientAccount) {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"couldn't found recipient user account in db",
)
}
// create proto transaction body
const transactionBodyBuilder = new TransactionBodyBuilder()
.setSigningAccount(signingAccount)
.setRecipientAccount(recipientAccount)
.fromTransactionDraft(transactionDraft)
.setCrossGroupType(transactionTypeRole.getCrossGroupType())
.setOtherGroup(transactionTypeRole.getOtherGroup())
const signingAccount = await transactionTypeRole.loadUser(transactionTypeRole.getSigningUser())
const recipientAccount = await transactionTypeRole.loadUser(
transactionTypeRole.getRecipientUser(),
)
const accountLogic = new AccountLogic(signingAccount)
await this.transactionBuilder.setCommunityFromUser(transactionDraft.user)
const communityKeyPair = new KeyPair(this.transactionBuilder.getCommunity())
const gradidoTransactionBuilder = await transactionTypeRole.getGradidoTransactionBuilder()
const transaction = gradidoTransactionBuilder
.setCreatedAt(new Date(transactionDraft.createdAt))
.sign(accountLogic.calculateKeyPair(communityKeyPair).keyPair)
.build()
// build transaction entity
this.transactionBuilder
.fromTransactionBodyBuilder(transactionBodyBuilder)
.addBackendTransaction(transactionDraft)
.fromGradidoTransaction(transaction)
.setRecipientAccount(recipientAccount)
.setSigningAccount(signingAccount)
await this.transactionBuilder.setCommunityFromUser(transactionDraft.user)
if (recipientUser.communityUuid !== signingUser.communityUuid) {
if (transactionTypeRole.isCrossGroupTransaction()) {
await this.transactionBuilder.setOtherCommunityFromUser(transactionDraft.linkedUser)
}
const transaction = this.transactionBuilder.getTransaction()
const communityKeyPair = new KeyPair(
this.getSigningCommunity(transactionTypeRole.getCrossGroupType()),
)
const accountLogic = new AccountLogic(signingAccount)
// sign
this.transactionBuilder.setSignature(
accountLogic.calculateKeyPair(communityKeyPair).sign(transaction.bodyBytes),
)
return this
}
public getSigningCommunity(crossGroupType: CrossGroupType): Community {
if (crossGroupType === CrossGroupType.INBOUND) {
const otherCommunity = this.transactionBuilder.getOtherCommunity()
if (!otherCommunity) {
throw new TransactionError(TransactionErrorType.NOT_FOUND, 'missing other community')
}
return otherCommunity
}
return this.transactionBuilder.getCommunity()
}
}

View File

@ -1,7 +1,9 @@
import { Community } from '@entity/Community'
// eslint-disable-next-line camelcase
import { MemoryBlock, GradidoTransactionBuilder } from 'gradido-blockchain-js'
import { KeyPair } from '@/data/KeyPair'
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
// import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
@ -11,15 +13,26 @@ export class CommunityRootTransactionRole extends AbstractTransactionRecipeRole
communityDraft: CommunityDraft,
community: Community,
): AbstractTransactionRecipeRole {
if (
!community.rootPubkey ||
!community.gmwAccount?.derive2Pubkey ||
!community.aufAccount?.derive2Pubkey
) {
throw new Error('missing one of the public keys for community')
}
// create proto transaction body
const transactionBody = new TransactionBodyBuilder()
.fromCommunityDraft(communityDraft, community)
const transaction = new GradidoTransactionBuilder()
.setCommunityRoot(
new MemoryBlock(community.rootPubkey),
new MemoryBlock(community.gmwAccount?.derive2Pubkey),
new MemoryBlock(community.aufAccount?.derive2Pubkey),
)
.setCreatedAt(new Date(communityDraft.createdAt))
.sign(new KeyPair(community).keyPair)
.build()
// build transaction entity
this.transactionBuilder.fromTransactionBody(transactionBody).setCommunity(community)
const transaction = this.transactionBuilder.getTransaction()
// sign
this.transactionBuilder.setSignature(new KeyPair(community).sign(transaction.bodyBytes))
this.transactionBuilder.fromGradidoTransaction(transaction).setCommunity(community)
return this
}
}

View File

@ -1,18 +1,23 @@
/* eslint-disable camelcase */
import 'reflect-metadata'
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
import { Decimal } from 'decimal.js-light'
import {
AddressType_COMMUNITY_HUMAN,
CrossGroupType_INBOUND,
CrossGroupType_LOCAL,
CrossGroupType_OUTBOUND,
InteractionDeserialize,
MemoryBlock,
TransactionType_CREATION,
} from 'gradido-blockchain-js'
import { v4 } from 'uuid'
import { TestDB } from '@test/TestDB'
import { CONFIG } from '@/config'
import { KeyPair } from '@/data/KeyPair'
import { Mnemonic } from '@/data/Mnemonic'
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionType } from '@/data/proto/3_3/enum/TransactionType'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { AccountType } from '@/graphql/enum/AccountType'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
@ -34,9 +39,9 @@ CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa
const homeCommunityUuid = v4()
const foreignCommunityUuid = v4()
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED))
const keyPair = new KeyPair(MemoryBlock.fromHex(CONFIG.IOTA_HOME_COMMUNITY_SEED))
const foreignKeyPair = new KeyPair(
new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'),
MemoryBlock.fromHex('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'),
)
let moderator: UserSet
@ -94,16 +99,17 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context
derive2Pubkey: firstUser.account.derive2Pubkey,
},
})
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
expect(body.registerAddress).toBeDefined()
if (!body.registerAddress) throw new Error()
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
expect(body?.isRegisterAddress()).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType.LOCAL,
type: CrossGroupType_LOCAL,
registerAddress: {
derivationIndex: 1,
addressType: AddressType.COMMUNITY_HUMAN,
addressType: AddressType_COMMUNITY_HUMAN,
},
})
})
@ -121,9 +127,8 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context
await context.run()
const transaction = context.getTransactionRecipe()
// console.log(new TransactionLoggingView(transaction))
expect(transaction).toMatchObject({
type: TransactionType.GRADIDO_CREATION,
type: TransactionType_CREATION,
protocolVersion: '3.3',
community: {
rootPubkey: keyPair.publicKey,
@ -144,15 +149,23 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context
],
})
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
// console.log(new TransactionBodyLoggingView(body))
expect(body.creation).toBeDefined()
if (!body.creation) throw new Error()
const bodyReceiverPubkey = Buffer.from(body.creation.recipient.pubkey)
expect(bodyReceiverPubkey.compare(firstUser.account.derive2Pubkey)).toBe(0)
expect(body?.isCreation()).toBeTruthy()
expect(
body
?.getCreation()
?.getRecipient()
.getPubkey()
?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType.LOCAL,
type: CrossGroupType_LOCAL,
creation: {
recipient: {
amount: '2000',
@ -196,16 +209,23 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context
],
})
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
// console.log(new TransactionBodyLoggingView(body))
expect(body.transfer).toBeDefined()
if (!body.transfer) throw new Error()
expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0)
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
0,
)
expect(body?.isTransfer()).toBeTruthy()
const transfer = body?.getTransfer()
expect(transfer).not.toBeNull()
expect(
transfer?.getRecipient()?.equal(new MemoryBlock(secondUser.account.derive2Pubkey)),
).toBeTruthy()
expect(
transfer?.getSender().getPubkey()?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType.LOCAL,
type: CrossGroupType_LOCAL,
transfer: {
sender: {
amount: '100',
@ -248,16 +268,22 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context
],
})
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
// console.log(new TransactionBodyLoggingView(body))
expect(body.transfer).toBeDefined()
if (!body.transfer) throw new Error()
expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0)
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
0,
)
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
expect(body?.isTransfer()).toBeTruthy()
const transfer = body?.getTransfer()
expect(transfer).not.toBeNull()
expect(
transfer?.getRecipient()?.equal(new MemoryBlock(secondUser.account.derive2Pubkey)),
).toBeTruthy()
expect(
transfer?.getSender().getPubkey()?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType.LOCAL,
type: CrossGroupType_LOCAL,
transfer: {
sender: {
amount: '100',
@ -304,16 +330,22 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context
},
],
})
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
// console.log(new TransactionBodyLoggingView(body))
expect(body.transfer).toBeDefined()
if (!body.transfer) throw new Error()
expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0)
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
0,
)
expect(body?.isTransfer()).toBeTruthy()
const transfer = body?.getTransfer()
expect(transfer).not.toBeNull()
expect(
transfer?.getRecipient()?.equal(new MemoryBlock(foreignUser.account.derive2Pubkey)),
).toBeTruthy()
expect(
transfer?.getSender().getPubkey()?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType.OUTBOUND,
type: CrossGroupType_OUTBOUND,
otherGroup: foreignTopic,
transfer: {
sender: {
@ -361,16 +393,22 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context
},
],
})
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
// console.log(new TransactionBodyLoggingView(body))
expect(body.transfer).toBeDefined()
if (!body.transfer) throw new Error()
expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0)
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
0,
)
expect(body?.isTransfer()).toBeTruthy()
const transfer = body?.getTransfer()
expect(transfer).not.toBeNull()
expect(
transfer?.getRecipient()?.equal(new MemoryBlock(foreignUser.account.derive2Pubkey)),
).toBeTruthy()
expect(
transfer?.getSender().getPubkey()?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType.INBOUND,
type: CrossGroupType_INBOUND,
otherGroup: topic,
transfer: {
sender: {

View File

@ -14,7 +14,6 @@ import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
import { BalanceChangingTransactionRecipeRole } from './BalanceChangingTransactionRecipeRole'
import { CommunityRootTransactionRole } from './CommunityRootTransaction.role'
import { CreationTransactionRole } from './CreationTransaction.role'
import { ReceiveTransactionRole } from './ReceiveTransaction.role'
import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role'
import { SendTransactionRole } from './SendTransaction.role'
@ -55,8 +54,7 @@ export class CreateTransactionRecipeContext {
transactionTypeRole = new SendTransactionRole(this.draft)
break
case InputTransactionType.RECEIVE:
transactionTypeRole = new ReceiveTransactionRole(this.draft)
break
return false
}
this.transactionRecipe = await new BalanceChangingTransactionRecipeRole().create(
this.draft,

View File

@ -1,7 +1,8 @@
/* eslint-disable camelcase */
import { Community } from '@entity/Community'
import { MemoryBlock, GradidoTransactionBuilder, TransferAmount } from 'gradido-blockchain-js'
import { CommunityRepository } from '@/data/Community.repository'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { TransactionError } from '@/graphql/model/TransactionError'
@ -19,15 +20,31 @@ export class CreationTransactionRole extends AbstractTransactionRole {
return this.self.user
}
public getCrossGroupType(): CrossGroupType {
return CrossGroupType.LOCAL
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const recipientUser = await this.loadUser(this.self.user)
if (!this.self.targetDate) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing targetDate for contribution',
)
}
return builder
.setTransactionCreation(
new TransferAmount(
new MemoryBlock(recipientUser.derive2Pubkey),
this.self.amount.toString(),
),
new Date(this.self.targetDate),
)
.setMemo('dummy memo for creation')
}
public async getCommunity(): Promise<Community> {
if (this.self.user.communityUuid !== this.self.linkedUser.communityUuid) {
throw new TransactionError(
TransactionErrorType.LOGIC_ERROR,
'mismatch community uuids on creation transaction',
'mismatch community uuids on contribution',
)
}
const community = await CommunityRepository.getCommunityForUserIdentifier(this.self.user)

View File

@ -1,21 +0,0 @@
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { AbstractTransactionRole } from './AbstractTransaction.role'
export class ReceiveTransactionRole extends AbstractTransactionRole {
public getSigningUser(): UserIdentifier {
return this.self.linkedUser
}
public getRecipientUser(): UserIdentifier {
return this.self.user
}
public getCrossGroupType(): CrossGroupType {
if (this.isCrossGroupTransaction()) {
return CrossGroupType.INBOUND
}
return CrossGroupType.LOCAL
}
}

View File

@ -1,9 +1,14 @@
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
import {
// eslint-disable-next-line camelcase
AddressType_COMMUNITY_HUMAN,
MemoryBlock,
GradidoTransactionBuilder,
} from 'gradido-blockchain-js'
import { AccountLogic } from '@/data/Account.logic'
import { CommunityRepository } from '@/data/Community.repository'
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
@ -15,17 +20,32 @@ export class RegisterAddressTransactionRole extends AbstractTransactionRecipeRol
userAccountDraft: UserAccountDraft,
account: Account,
community: Community,
): Promise<AbstractTransactionRecipeRole> {
const bodyBuilder = new TransactionBodyBuilder()
): Promise<RegisterAddressTransactionRole> {
const user = account.user
if (!user) {
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'missing user for account')
}
const gradidoTransactionBuilder = new GradidoTransactionBuilder()
const communityKeyPair = await CommunityRepository.loadHomeCommunityKeyPair()
const signingKeyPair = new AccountLogic(account).calculateKeyPair(communityKeyPair)
if (!signingKeyPair) {
throw new TransactionError(TransactionErrorType.NOT_FOUND, "couldn't found signing key pair")
}
const transaction = gradidoTransactionBuilder
.setRegisterAddress(
new MemoryBlock(user.derive1Pubkey),
AddressType_COMMUNITY_HUMAN,
null,
new MemoryBlock(account.derive2Pubkey),
)
.setCreatedAt(new Date(userAccountDraft.createdAt))
.sign(signingKeyPair.keyPair)
.sign(communityKeyPair.keyPair)
.build()
this.transactionBuilder
.fromTransactionBodyBuilder(bodyBuilder.fromUserAccountDraft(userAccountDraft, account))
.fromGradidoTransaction(transaction)
.setCommunity(community)
.setSignature(signingKeyPair.sign(this.transactionBuilder.getTransaction().bodyBytes))
.setSigningAccount(account)
return this
}

View File

@ -1,4 +1,13 @@
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
/* eslint-disable camelcase */
import {
CrossGroupType,
CrossGroupType_LOCAL,
CrossGroupType_OUTBOUND,
MemoryBlock,
GradidoTransactionBuilder,
TransferAmount,
} from 'gradido-blockchain-js'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { AbstractTransactionRole } from './AbstractTransaction.role'
@ -12,10 +21,15 @@ export class SendTransactionRole extends AbstractTransactionRole {
return this.self.linkedUser
}
public getCrossGroupType(): CrossGroupType {
if (this.isCrossGroupTransaction()) {
return CrossGroupType.OUTBOUND
}
return CrossGroupType.LOCAL
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const signingUser = await this.loadUser(this.self.user)
const recipientUser = await this.loadUser(this.self.linkedUser)
return builder
.setTransactionTransfer(
new TransferAmount(new MemoryBlock(signingUser.derive2Pubkey), this.self.amount.toString()),
new MemoryBlock(recipientUser.derive2Pubkey),
)
.setMemo('dummy memo for transfer')
}
}

View File

@ -1,68 +1,78 @@
/* eslint-disable camelcase */
import { Transaction } from '@entity/Transaction'
import {
GradidoTransaction,
GradidoTransactionBuilder,
InteractionSerialize,
InteractionValidate,
MemoryBlock,
TransactionType_COMMUNITY_ROOT,
ValidateType_SINGLE,
} from 'gradido-blockchain-js'
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
import { KeyPair } from '@/data/KeyPair'
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
import { SignaturePair } from '@/data/proto/3_3/SignaturePair'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionError } from '@/graphql/model/TransactionError'
import { GradidoTransactionLoggingView } from '@/logging/GradidoTransactionLogging.view'
import { logger } from '@/logging/logger'
export abstract class AbstractTransactionRecipeRole {
protected transactionBody: TransactionBody
public constructor(protected self: Transaction) {
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
}
// eslint-disable-next-line no-useless-constructor
public constructor(protected self: Transaction) {}
public abstract transmitToIota(): Promise<Transaction>
public abstract getCrossGroupTypeName(): string
protected getGradidoTransaction(): GradidoTransaction {
const transaction = new GradidoTransaction(this.transactionBody)
public validate(transactionBuilder: GradidoTransactionBuilder): GradidoTransaction {
const transaction = transactionBuilder.build()
try {
// throw an exception when something is wrong
const validator = new InteractionValidate(transaction)
validator.run(ValidateType_SINGLE)
} catch (e) {
if (e instanceof Error) {
throw new TransactionError(TransactionErrorType.VALIDATION_ERROR, e.message)
} else if (typeof e === 'string') {
throw new TransactionError(TransactionErrorType.VALIDATION_ERROR, e)
} else {
throw e
}
}
return transaction
}
protected getGradidoTransactionBuilder(): GradidoTransactionBuilder {
if (!this.self.signature) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing signature in transaction recipe',
)
}
const signaturePair = new SignaturePair()
if (this.self.signature.length !== 64) {
throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, "signature isn't 64 bytes")
}
signaturePair.signature = this.self.signature
if (this.transactionBody.communityRoot) {
const publicKey = this.self.community.rootPubkey
let publicKey: Buffer | undefined
if (this.self.type === TransactionType_COMMUNITY_ROOT) {
publicKey = this.self.community.rootPubkey
if (!publicKey) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing community public key for community root transaction',
)
}
signaturePair.pubKey = publicKey
} else if (this.self.signingAccount) {
const publicKey = this.self.signingAccount.derive2Pubkey
publicKey = this.self.signingAccount.derive2Pubkey
if (!publicKey) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing signing account public key for transaction',
)
}
signaturePair.pubKey = publicKey
} else {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"signingAccount not exist and it isn't a community root transaction",
)
}
if (signaturePair.validate()) {
transaction.sigMap.sigPair.push(signaturePair)
}
if (!KeyPair.verify(transaction.bodyBytes, signaturePair)) {
logger.debug('invalid signature', new GradidoTransactionLoggingView(transaction))
throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, 'signature is invalid')
}
return transaction
return new GradidoTransactionBuilder()
.setTransactionBody(new MemoryBlock(this.self.bodyBytes))
.addSignaturePair(new MemoryBlock(publicKey), new MemoryBlock(this.self.signature))
}
/**
@ -76,9 +86,15 @@ export abstract class AbstractTransactionRecipeRole {
topic: string,
): Promise<Buffer> {
// protobuf serializing function
const messageBuffer = GradidoTransaction.encode(gradidoTransaction).finish()
const serialized = new InteractionSerialize(gradidoTransaction).run()
if (!serialized) {
throw new TransactionError(
TransactionErrorType.PROTO_ENCODE_ERROR,
'cannot serialize transaction',
)
}
const resultMessage = await iotaSendMessage(
messageBuffer,
Uint8Array.from(serialized.data()),
Uint8Array.from(Buffer.from(topic, 'hex')),
)
logger.info('transmitted Gradido Transaction to Iota', {

View File

@ -1,6 +1,6 @@
import { Transaction } from '@entity/Transaction'
import { MemoryBlock } from 'gradido-blockchain-js'
import { TransactionLogic } from '@/data/Transaction.logic'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { LogError } from '@/server/LogError'
@ -12,9 +12,13 @@ import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
* need to set gradido id from OUTBOUND transaction!
*/
export class InboundTransactionRecipeRole extends AbstractTransactionRecipeRole {
public getCrossGroupTypeName(): string {
return 'INBOUND'
}
public async transmitToIota(): Promise<Transaction> {
logger.debug('transmit INBOUND transaction to iota', new TransactionLoggingView(this.self))
const gradidoTransaction = this.getGradidoTransaction()
const builder = this.getGradidoTransactionBuilder()
const pairingTransaction = await new TransactionLogic(this.self).findPairTransaction()
if (!pairingTransaction.iotaMessageId || pairingTransaction.iotaMessageId.length !== 32) {
throw new LogError(
@ -22,7 +26,7 @@ export class InboundTransactionRecipeRole extends AbstractTransactionRecipeRole
new TransactionLoggingView(pairingTransaction),
)
}
gradidoTransaction.parentMessageId = pairingTransaction.iotaMessageId
builder.setParentMessageId(new MemoryBlock(pairingTransaction.iotaMessageId))
this.self.pairingTransactionId = pairingTransaction.id
this.self.pairingTransaction = pairingTransaction
pairingTransaction.pairingTransactionId = this.self.id
@ -32,7 +36,7 @@ export class InboundTransactionRecipeRole extends AbstractTransactionRecipeRole
}
this.self.iotaMessageId = await this.sendViaIota(
gradidoTransaction,
this.validate(builder),
this.self.otherCommunity.iotaTopic,
)
return this.self

View File

@ -1,23 +1,22 @@
import { Transaction } from '@entity/Transaction'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
export class LocalTransactionRecipeRole extends AbstractTransactionRecipeRole {
public getCrossGroupTypeName(): string {
return 'LOCAL'
}
public async transmitToIota(): Promise<Transaction> {
let transactionCrossGroupTypeName = 'LOCAL'
if (this.transactionBody) {
transactionCrossGroupTypeName = CrossGroupType[this.transactionBody.type]
}
logger.debug(
`transmit ${transactionCrossGroupTypeName} transaction to iota`,
`transmit ${this.getCrossGroupTypeName()} transaction to iota`,
new TransactionLoggingView(this.self),
)
this.self.iotaMessageId = await this.sendViaIota(
this.getGradidoTransaction(),
this.validate(this.getGradidoTransactionBuilder()),
this.self.community.iotaTopic,
)
return this.self

View File

@ -3,4 +3,8 @@ import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role'
/**
* Outbound Transaction on sender community, mark the gradidos as sended out of community
*/
export class OutboundTransactionRecipeRole extends LocalTransactionRecipeRole {}
export class OutboundTransactionRecipeRole extends LocalTransactionRecipeRole {
public getCrossGroupTypeName(): string {
return 'OUTBOUND'
}
}

View File

@ -1,6 +1,7 @@
import 'reflect-metadata'
import { Account } from '@entity/Account'
import { Decimal } from 'decimal.js-light'
import { CrossGroupType_INBOUND, CrossGroupType_OUTBOUND, InteractionDeserialize, InteractionToJson, InteractionValidate, MemoryBlock } from 'gradido-blockchain-js'
import { v4 } from 'uuid'
import { TestDB } from '@test/TestDB'
@ -8,8 +9,6 @@ import { TestDB } from '@test/TestDB'
import { CONFIG } from '@/config'
import { KeyPair } from '@/data/KeyPair'
import { Mnemonic } from '@/data/Mnemonic'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { logger } from '@/logging/logger'
@ -76,7 +75,7 @@ describe('interactions/transmitToIota/TransmitToIotaContext', () => {
it('LOCAL transaction', async () => {
const creationTransactionDraft = new TransactionDraft()
creationTransactionDraft.amount = new Decimal('2000')
creationTransactionDraft.amount = new Decimal('1000')
creationTransactionDraft.backendTransactionId = 1
creationTransactionDraft.createdAt = new Date().toISOString()
creationTransactionDraft.linkedUser = moderator.identifier
@ -116,8 +115,11 @@ describe('interactions/transmitToIota/TransmitToIotaContext', () => {
await transactionRecipeContext.run()
const transaction = transactionRecipeContext.getTransactionRecipe()
await transaction.save()
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
expect(body.type).toBe(CrossGroupType.OUTBOUND)
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
expect(body?.getType()).toEqual(CrossGroupType_OUTBOUND)
const context = new TransmitToIotaContext(transaction)
const debugSpy = jest.spyOn(logger, 'debug')
await context.run()
@ -148,8 +150,10 @@ describe('interactions/transmitToIota/TransmitToIotaContext', () => {
const transaction = transactionRecipeContext.getTransactionRecipe()
await transaction.save()
// console.log(new TransactionLoggingView(transaction))
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
expect(body.type).toBe(CrossGroupType.INBOUND)
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body?.getType()).toEqual(CrossGroupType_INBOUND)
const context = new TransmitToIotaContext(transaction)
const debugSpy = jest.spyOn(logger, 'debug')

View File

@ -1,7 +1,15 @@
/* eslint-disable camelcase */
import { Transaction } from '@entity/Transaction'
import {
CrossGroupType_INBOUND,
CrossGroupType_LOCAL,
CrossGroupType_OUTBOUND,
InteractionDeserialize,
MemoryBlock,
} from 'gradido-blockchain-js'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { LogError } from '@/server/LogError'
@ -21,19 +29,27 @@ export class TransmitToIotaContext {
private transactionRecipeRole: AbstractTransactionRecipeRole
public constructor(transaction: Transaction) {
const transactionBody = TransactionBody.fromBodyBytes(transaction.bodyBytes)
switch (transactionBody.type) {
case CrossGroupType.LOCAL:
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const transactionBody = deserializer.getTransactionBody()
if (!transactionBody) {
throw new TransactionError(
TransactionErrorType.PROTO_DECODE_ERROR,
'error decoding body bytes',
)
}
switch (transactionBody.getType()) {
case CrossGroupType_LOCAL:
this.transactionRecipeRole = new LocalTransactionRecipeRole(transaction)
break
case CrossGroupType.INBOUND:
case CrossGroupType_INBOUND:
this.transactionRecipeRole = new InboundTransactionRecipeRole(transaction)
break
case CrossGroupType.OUTBOUND:
case CrossGroupType_OUTBOUND:
this.transactionRecipeRole = new OutboundTransactionRecipeRole(transaction)
break
default:
throw new LogError('unknown cross group type', transactionBody.type)
throw new LogError('unknown cross group type', transactionBody.getType())
}
}

View File

@ -1,10 +1,7 @@
import util from 'util'
import { Decimal } from 'decimal.js-light'
import { Timestamp } from '@/data/proto/3_3/Timestamp'
import { TimestampSeconds } from '@/data/proto/3_3/TimestampSeconds'
import { timestampSecondsToDate, timestampToDate } from '@/utils/typeConverter'
import { Timestamp, TimestampSeconds } from 'gradido-blockchain-js'
export abstract class AbstractLoggingView {
protected bufferStringFormat: BufferEncoding = 'hex'
@ -36,14 +33,14 @@ export abstract class AbstractLoggingView {
}
protected timestampSecondsToDateString(timestamp: TimestampSeconds): string | undefined {
if (timestamp && timestamp.seconds) {
return timestampSecondsToDate(timestamp).toISOString()
if (timestamp && timestamp.getSeconds()) {
return timestamp.getDate().toISOString()
}
}
protected timestampToDateString(timestamp: Timestamp): string | undefined {
if (timestamp && (timestamp.seconds || timestamp.nanoSeconds)) {
return timestampToDate(timestamp).toISOString()
if (timestamp && (timestamp.getSeconds() || timestamp.getNanos())) {
return timestamp.getDate().toISOString()
}
}
}

View File

@ -1,7 +1,8 @@
import { Account } from '@entity/Account'
import { addressTypeToString } from 'gradido-blockchain-js'
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
import { getEnumValue } from '@/utils/typeConverter'
import { AccountType } from '@/graphql/enum/AccountType'
import { accountTypeToAddressType } from '@/utils/typeConverter'
import { AbstractLoggingView } from './AbstractLogging.view'
import { UserLoggingView } from './UserLogging.view'
@ -17,7 +18,9 @@ export class AccountLoggingView extends AbstractLoggingView {
user: this.account.user ? new UserLoggingView(this.account.user).toJSON() : null,
derivationIndex: this.account.derivationIndex,
derive2Pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat),
type: getEnumValue(AddressType, this.account.type),
type: addressTypeToString(
accountTypeToAddressType(this.account.type as unknown as AccountType),
),
createdAt: this.dateToString(this.account.createdAt),
confirmedAt: this.dateToString(this.account.confirmedAt),
balanceOnConfirmation: this.decimalToString(this.account.balanceOnConfirmation),

View File

@ -1,18 +0,0 @@
import { CommunityRoot } from '@/data/proto/3_3/CommunityRoot'
import { AbstractLoggingView } from './AbstractLogging.view'
export class CommunityRootLoggingView extends AbstractLoggingView {
public constructor(private self: CommunityRoot) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
rootPubkey: Buffer.from(this.self.rootPubkey).toString(this.bufferStringFormat),
gmwPubkey: Buffer.from(this.self.gmwPubkey).toString(this.bufferStringFormat),
aufPubkey: Buffer.from(this.self.aufPubkey).toString(this.bufferStringFormat),
}
}
}

View File

@ -1,24 +0,0 @@
import { ConfirmedTransaction } from '@/data/proto/3_3/ConfirmedTransaction'
import { timestampSecondsToDate } from '@/utils/typeConverter'
import { AbstractLoggingView } from './AbstractLogging.view'
import { GradidoTransactionLoggingView } from './GradidoTransactionLogging.view'
export class ConfirmedTransactionLoggingView extends AbstractLoggingView {
public constructor(private self: ConfirmedTransaction) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
id: this.self.id.toString(),
transaction: new GradidoTransactionLoggingView(this.self.transaction).toJSON(),
confirmedAt: this.dateToString(timestampSecondsToDate(this.self.confirmedAt)),
versionNumber: this.self.versionNumber,
runningHash: Buffer.from(this.self.runningHash).toString(this.bufferStringFormat),
messageId: Buffer.from(this.self.messageId).toString(this.bufferStringFormat),
accountBalance: this.self.accountBalance,
}
}
}

View File

@ -1,18 +0,0 @@
import { GradidoCreation } from '@/data/proto/3_3/GradidoCreation'
import { AbstractLoggingView } from './AbstractLogging.view'
import { TransferAmountLoggingView } from './TransferAmountLogging.view'
export class GradidoCreationLoggingView extends AbstractLoggingView {
public constructor(private self: GradidoCreation) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
recipient: new TransferAmountLoggingView(this.self.recipient).toJSON(),
targetDate: this.timestampSecondsToDateString(this.self.targetDate),
}
}
}

View File

@ -1,18 +0,0 @@
import { GradidoDeferredTransfer } from '@/data/proto/3_3/GradidoDeferredTransfer'
import { AbstractLoggingView } from './AbstractLogging.view'
import { GradidoTransferLoggingView } from './GradidoTransferLogging.view'
export class GradidoDeferredTransferLoggingView extends AbstractLoggingView {
public constructor(private self: GradidoDeferredTransfer) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
...new GradidoTransferLoggingView(this.self.transfer).toJSON(),
...{ timeout: this.timestampSecondsToDateString(this.self.timeout) },
}
}
}

View File

@ -1,29 +0,0 @@
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { AbstractLoggingView } from './AbstractLogging.view'
import { SignatureMapLoggingView } from './SignatureMapLogging.view'
import { TransactionBodyLoggingView } from './TransactionBodyLogging.view'
export class GradidoTransactionLoggingView extends AbstractLoggingView {
public constructor(private self: GradidoTransaction) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
let transactionBody: TransactionBody | null | unknown = null
try {
transactionBody = new TransactionBodyLoggingView(this.self.getTransactionBody())
} catch (e) {
transactionBody = e
}
return {
sigMap: new SignatureMapLoggingView(this.self.sigMap).toJSON(),
bodyBytes: transactionBody,
parentMessageId: this.self.parentMessageId
? Buffer.from(this.self.parentMessageId).toString(this.bufferStringFormat)
: undefined,
}
}
}

View File

@ -1,18 +0,0 @@
import { GradidoTransfer } from '@/data/proto/3_3/GradidoTransfer'
import { AbstractLoggingView } from './AbstractLogging.view'
import { TransferAmountLoggingView } from './TransferAmountLogging.view'
export class GradidoTransferLoggingView extends AbstractLoggingView {
public constructor(private self: GradidoTransfer) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
sender: new TransferAmountLoggingView(this.self.sender),
recipient: Buffer.from(this.self.recipient).toString(this.bufferStringFormat),
}
}
}

View File

@ -1,16 +0,0 @@
import { GroupFriendsUpdate } from '@/data/proto/3_3/GroupFriendsUpdate'
import { AbstractLoggingView } from './AbstractLogging.view'
export class GroupFriendsUpdateLoggingView extends AbstractLoggingView {
public constructor(private self: GroupFriendsUpdate) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
colorFusion: this.self.colorFusion,
}
}
}

View File

@ -1,22 +0,0 @@
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
import { RegisterAddress } from '@/data/proto/3_3/RegisterAddress'
import { getEnumValue } from '@/utils/typeConverter'
import { AbstractLoggingView } from './AbstractLogging.view'
export class RegisterAddressLoggingView extends AbstractLoggingView {
public constructor(private self: RegisterAddress) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
userPublicKey: Buffer.from(this.self.userPubkey).toString(this.bufferStringFormat),
addressType: getEnumValue(AddressType, this.self.addressType),
nameHash: Buffer.from(this.self.nameHash).toString(this.bufferStringFormat),
accountPublicKey: Buffer.from(this.self.accountPubkey).toString(this.bufferStringFormat),
derivationIndex: this.self.derivationIndex,
}
}
}

View File

@ -1,17 +0,0 @@
import { SignatureMap } from '@/data/proto/3_3/SignatureMap'
import { AbstractLoggingView } from './AbstractLogging.view'
import { SignaturePairLoggingView } from './SignaturePairLogging.view'
export class SignatureMapLoggingView extends AbstractLoggingView {
public constructor(private self: SignatureMap) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
sigPair: this.self.sigPair.map((value) => new SignaturePairLoggingView(value).toJSON()),
}
}
}

View File

@ -1,18 +0,0 @@
import { SignaturePair } from '@/data/proto/3_3/SignaturePair'
import { AbstractLoggingView } from './AbstractLogging.view'
export class SignaturePairLoggingView extends AbstractLoggingView {
public constructor(private self: SignaturePair) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
pubkey: Buffer.from(this.self.pubKey).toString(this.bufferStringFormat),
signature:
Buffer.from(this.self.signature).subarray(0, 31).toString(this.bufferStringFormat) + '..',
}
}
}

View File

@ -1,46 +0,0 @@
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { getEnumValue } from '@/utils/typeConverter'
import { AbstractLoggingView } from './AbstractLogging.view'
import { CommunityRootLoggingView } from './CommunityRootLogging.view'
import { GradidoCreationLoggingView } from './GradidoCreationLogging.view'
import { GradidoDeferredTransferLoggingView } from './GradidoDeferredTransferLogging.view'
import { GradidoTransferLoggingView } from './GradidoTransferLogging.view'
import { GroupFriendsUpdateLoggingView } from './GroupFriendsUpdateLogging.view'
import { RegisterAddressLoggingView } from './RegisterAddressLogging.view'
export class TransactionBodyLoggingView extends AbstractLoggingView {
public constructor(private self: TransactionBody) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
memo: this.self.memo,
createdAt: this.timestampToDateString(this.self.createdAt),
versionNumber: this.self.versionNumber,
type: getEnumValue(CrossGroupType, this.self.type),
otherGroup: this.self.otherGroup,
transfer: this.self.transfer
? new GradidoTransferLoggingView(this.self.transfer).toJSON()
: undefined,
creation: this.self.creation
? new GradidoCreationLoggingView(this.self.creation).toJSON()
: undefined,
groupFriendsUpdate: this.self.groupFriendsUpdate
? new GroupFriendsUpdateLoggingView(this.self.groupFriendsUpdate).toJSON()
: undefined,
registerAddress: this.self.registerAddress
? new RegisterAddressLoggingView(this.self.registerAddress).toJSON()
: undefined,
deferredTransfer: this.self.deferredTransfer
? new GradidoDeferredTransferLoggingView(this.self.deferredTransfer).toJSON()
: undefined,
communityRoot: this.self.communityRoot
? new CommunityRootLoggingView(this.self.communityRoot).toJSON()
: undefined,
}
}
}

View File

@ -1,18 +0,0 @@
import { TransferAmount } from '@/data/proto/3_3/TransferAmount'
import { AbstractLoggingView } from './AbstractLogging.view'
export class TransferAmountLoggingView extends AbstractLoggingView {
public constructor(private self: TransferAmount) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
pubkey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat),
amount: this.self.amount,
communityId: this.self.communityId,
}
}
}

View File

@ -1,16 +1,10 @@
import 'reflect-metadata'
import { Timestamp } from '../data/proto/3_3/Timestamp'
import { hardenDerivationIndex, HARDENED_KEY_BITMASK } from './derivationHelper'
import { timestampToDate } from './typeConverter'
describe('utils', () => {
it('test bitmask for hardened keys', () => {
const derivationIndex = hardenDerivationIndex(1)
expect(derivationIndex).toBeGreaterThan(HARDENED_KEY_BITMASK)
})
it('test TimestampToDate', () => {
const date = new Date('2011-04-17T12:01:10.109')
expect(timestampToDate(new Timestamp(date))).toEqual(date)
})
})

View File

@ -1,14 +1,6 @@
import 'reflect-metadata'
import { Timestamp } from '@/data/proto/3_3/Timestamp'
import {
base64ToBuffer,
iotaTopicFromCommunityUUID,
timestampSecondsToDate,
timestampToDate,
uuid4ToBuffer,
} from './typeConverter'
import { base64ToBuffer, iotaTopicFromCommunityUUID, uuid4ToBuffer } from './typeConverter'
describe('utils/typeConverter', () => {
it('uuid4ToBuffer', () => {
@ -23,20 +15,6 @@ describe('utils/typeConverter', () => {
)
})
it('timestampToDate', () => {
const now = new Date('Thu, 05 Oct 2023 11:55:18.102 +0000')
const timestamp = new Timestamp(now)
expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000))
expect(timestampToDate(timestamp)).toEqual(now)
})
it('timestampSecondsToDate', () => {
const now = new Date('Thu, 05 Oct 2023 11:55:18.102 +0000')
const timestamp = new Timestamp(now)
expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000))
expect(timestampSecondsToDate(timestamp)).toEqual(new Date('Thu, 05 Oct 2023 11:55:18 +0000'))
})
it('base64ToBuffer', () => {
expect(base64ToBuffer('MTizWQMR/fCoI+FzyqlIe30nXCP6sHEGtLE2TLA4r/0=')).toStrictEqual(
Buffer.from('3138b3590311fdf0a823e173caa9487b7d275c23fab07106b4b1364cb038affd', 'hex'),

View File

@ -1,14 +1,17 @@
/* eslint-disable camelcase */
import {
AddressType,
AddressType_COMMUNITY_AUF,
AddressType_COMMUNITY_GMW,
AddressType_COMMUNITY_HUMAN,
AddressType_COMMUNITY_PROJECT,
AddressType_CRYPTO_ACCOUNT,
AddressType_NONE,
AddressType_SUBACCOUNT,
} from 'gradido-blockchain-js'
import { crypto_generichash as cryptoHash } from 'sodium-native'
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
import { Timestamp } from '@/data/proto/3_3/Timestamp'
import { TimestampSeconds } from '@/data/proto/3_3/TimestampSeconds'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { AccountType } from '@/graphql/enum/AccountType'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { LogError } from '@/server/LogError'
export const uuid4ToBuffer = (uuid: string): Buffer => {
// Remove dashes from the UUIDv4 string
@ -26,44 +29,10 @@ export const iotaTopicFromCommunityUUID = (communityUUID: string): string => {
return hash.toString('hex')
}
export const timestampToDate = (timestamp: Timestamp): Date => {
let milliseconds = timestamp.nanoSeconds / 1000000
milliseconds += timestamp.seconds * 1000
return new Date(milliseconds)
}
export const timestampSecondsToDate = (timestamp: TimestampSeconds): Date => {
return new Date(timestamp.seconds * 1000)
}
export const base64ToBuffer = (base64: string): Buffer => {
return Buffer.from(base64, 'base64')
}
export const bodyBytesToTransactionBody = (bodyBytes: Buffer): TransactionBody => {
try {
return TransactionBody.decode(new Uint8Array(bodyBytes))
} catch (error) {
logger.error('error decoding body from gradido transaction: %s', error)
throw new TransactionError(
TransactionErrorType.PROTO_DECODE_ERROR,
'cannot decode body from gradido transaction',
)
}
}
export const transactionBodyToBodyBytes = (transactionBody: TransactionBody): Buffer => {
try {
return Buffer.from(TransactionBody.encode(transactionBody).finish())
} catch (error) {
logger.error('error encoding transaction body to body bytes', error)
throw new TransactionError(
TransactionErrorType.PROTO_ENCODE_ERROR,
'cannot encode transaction body',
)
}
}
export function getEnumValue<T extends Record<string, unknown>>(
enumType: T,
value: number | string,
@ -81,27 +50,39 @@ export function getEnumValue<T extends Record<string, unknown>>(
}
export const accountTypeToAddressType = (type: AccountType): AddressType => {
const typeString: string = AccountType[type]
const addressType: AddressType = AddressType[typeString as keyof typeof AddressType]
if (!addressType) {
throw new LogError("couldn't find corresponding AddressType for AccountType", {
accountType: type,
addressTypes: Object.keys(AddressType),
})
switch (type) {
case AccountType.COMMUNITY_AUF:
return AddressType_COMMUNITY_AUF
case AccountType.COMMUNITY_GMW:
return AddressType_COMMUNITY_GMW
case AccountType.COMMUNITY_HUMAN:
return AddressType_COMMUNITY_HUMAN
case AccountType.COMMUNITY_PROJECT:
return AddressType_COMMUNITY_PROJECT
case AccountType.CRYPTO_ACCOUNT:
return AddressType_CRYPTO_ACCOUNT
case AccountType.SUBACCOUNT:
return AddressType_SUBACCOUNT
default:
return AddressType_NONE
}
return addressType
}
export const addressTypeToAccountType = (type: AddressType): AccountType => {
const typeString: string = AddressType[type]
const accountType: AccountType = AccountType[typeString as keyof typeof AccountType]
if (!accountType) {
throw new LogError("couldn't find corresponding AccountType for AddressType", {
addressTypes: type,
accountType: Object.keys(AccountType),
})
switch (type) {
case AddressType_COMMUNITY_AUF:
return AccountType.COMMUNITY_AUF
case AddressType_COMMUNITY_GMW:
return AccountType.COMMUNITY_GMW
case AddressType_COMMUNITY_HUMAN:
return AccountType.COMMUNITY_HUMAN
case AddressType_COMMUNITY_PROJECT:
return AccountType.COMMUNITY_PROJECT
case AddressType_CRYPTO_ACCOUNT:
return AccountType.CRYPTO_ACCOUNT
case AddressType_SUBACCOUNT:
return AccountType.SUBACCOUNT
default:
return AccountType.NONE
}
return accountType
}

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,8 @@ import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, JoinColu
import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Transaction } from '../Transaction'
// BackendTransaction was removed in newer migrations, so only the version from this folder can be linked
import { Transaction } from './Transaction'
@Entity('backend_transactions')
export class BackendTransaction extends BaseEntity {

View File

@ -13,7 +13,8 @@ import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Account } from '../Account'
import { Community } from '../Community'
import { BackendTransaction } from '../BackendTransaction'
// BackendTransaction was removed in newer migrations, so only the version from this folder can be linked
import { BackendTransaction } from './BackendTransaction'
@Entity('transactions')
export class Transaction extends BaseEntity {

View File

@ -13,7 +13,8 @@ import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Account } from '../Account'
import { Community } from '../Community'
import { BackendTransaction } from '../BackendTransaction'
// BackendTransaction was removed in newer migrations, so only the version from this folder can be linked
import { BackendTransaction } from '../0003-refactor_transaction_recipe/BackendTransaction'
@Entity('transactions')
export class Transaction extends BaseEntity {

View File

@ -0,0 +1,64 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
JoinColumn,
OneToOne,
OneToMany,
BaseEntity,
} from 'typeorm'
import { Account } from '../Account'
import { Transaction } from '../Transaction'
import { AccountCommunity } from '../AccountCommunity'
@Entity('communities')
export class Community extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'iota_topic', collation: 'utf8mb4_unicode_ci', unique: true })
iotaTopic: string
@Column({ name: 'root_pubkey', type: 'binary', length: 32, unique: true, nullable: true })
rootPubkey?: Buffer
@Column({ name: 'root_privkey', type: 'binary', length: 80, nullable: true })
rootEncryptedPrivkey?: Buffer
@Column({ name: 'root_chaincode', type: 'binary', length: 32, nullable: true })
rootChaincode?: Buffer
@Column({ type: 'tinyint', default: true })
foreign: boolean
@Column({ name: 'gmw_account_id', type: 'int', unsigned: true, nullable: true })
gmwAccountId?: number
@OneToOne(() => Account, { cascade: true })
@JoinColumn({ name: 'gmw_account_id' })
gmwAccount?: Account
@Column({ name: 'auf_account_id', type: 'int', unsigned: true, nullable: true })
aufAccountId?: number
@OneToOne(() => Account, { cascade: true })
@JoinColumn({ name: 'auf_account_id' })
aufAccount?: Account
@Column({ name: 'created_at', type: 'datetime', precision: 3 })
createdAt: Date
// use timestamp from iota milestone which is only in seconds precision, so no need to use 3 Bytes extra here
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
confirmedAt?: Date
@OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.community)
@JoinColumn({ name: 'community_id' })
accountCommunities: AccountCommunity[]
@OneToMany(() => Transaction, (transaction) => transaction.community)
transactions?: Transaction[]
@OneToMany(() => Transaction, (transaction) => transaction.otherCommunity)
friendCommunitiesTransactions?: Transaction[]
}

View File

@ -0,0 +1,109 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
OneToOne,
JoinColumn,
BaseEntity,
} from 'typeorm'
import { Account } from '../Account'
import { Community } from '../Community'
@Entity('transactions')
export class Transaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
id: number
@Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true })
iotaMessageId?: Buffer
@OneToOne(() => Transaction, { cascade: ['update'] })
// eslint-disable-next-line no-use-before-define
pairingTransaction?: Transaction
@Column({ name: 'pairing_transaction_id', type: 'bigint', unsigned: true, nullable: true })
pairingTransactionId?: number
// if transaction has a sender than it is also the sender account
@ManyToOne(() => Account, (account) => account.transactionSigning)
@JoinColumn({ name: 'signing_account_id' })
signingAccount?: Account
@Column({ name: 'signing_account_id', type: 'int', unsigned: true, nullable: true })
signingAccountId?: number
@ManyToOne(() => Account, (account) => account.transactionRecipient)
@JoinColumn({ name: 'recipient_account_id' })
recipientAccount?: Account
@Column({ name: 'recipient_account_id', type: 'int', unsigned: true, nullable: true })
recipientAccountId?: number
@ManyToOne(() => Community, (community) => community.transactions, {
eager: true,
})
@JoinColumn({ name: 'community_id' })
community: Community
@Column({ name: 'community_id', type: 'int', unsigned: true })
communityId: number
@ManyToOne(() => Community, (community) => community.friendCommunitiesTransactions)
@JoinColumn({ name: 'other_community_id' })
otherCommunity?: Community
@Column({ name: 'other_community_id', type: 'int', unsigned: true, nullable: true })
otherCommunityId?: number
@Column({
type: 'bigint',
nullable: true,
})
amount?: number
// account balance for sender based on creation date
@Column({
name: 'account_balance_on_creation',
type: 'bigint',
nullable: true,
})
accountBalanceOnCreation?: number
@Column({ type: 'tinyint' })
type: number
@Column({ name: 'created_at', type: 'datetime', precision: 3 })
createdAt: Date
@Column({ name: 'body_bytes', type: 'blob' })
bodyBytes: Buffer
@Column({ type: 'binary', length: 64, unique: true })
signature: Buffer
@Column({ name: 'protocol_version', type: 'varchar', length: 255, default: '1' })
protocolVersion: string
@Column({ type: 'bigint', nullable: true })
nr?: number
@Column({ name: 'running_hash', type: 'binary', length: 48, nullable: true })
runningHash?: Buffer
// account balance for sender based on confirmation date (iota milestone)
@Column({
name: 'account_balance_on_confirmation',
type: 'bigint',
nullable: true,
})
accountBalanceOnConfirmation?: number
@Column({ name: 'iota_milestone', type: 'bigint', nullable: true })
iotaMilestone?: number
// use timestamp from iota milestone which is only in seconds precision, so no need to use 3 Bytes extra here
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
confirmedAt?: Date
}

View File

@ -1 +0,0 @@
export { BackendTransaction } from './0003-refactor_transaction_recipe/BackendTransaction'

View File

@ -1 +1 @@
export { Community } from './0003-refactor_transaction_recipe/Community'
export { Community } from './0005-refactor_with_gradido_blockchain_lib/Community'

View File

@ -1 +1 @@
export { Transaction } from './0004-fix_spelling/Transaction'
export { Transaction } from './0005-refactor_with_gradido_blockchain_lib/Transaction'

View File

@ -1,6 +1,5 @@
import { Account } from './Account'
import { AccountCommunity } from './AccountCommunity'
import { BackendTransaction } from './BackendTransaction'
import { Community } from './Community'
import { InvalidTransaction } from './InvalidTransaction'
import { Migration } from './Migration'
@ -10,7 +9,6 @@ import { User } from './User'
export const entities = [
AccountCommunity,
Account,
BackendTransaction,
Community,
InvalidTransaction,
Migration,

View File

@ -0,0 +1,28 @@
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
`ALTER TABLE \`communities\` CHANGE COLUMN \`root_privkey\` \`root_encrypted_privkey\` binary(80) NULL DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`transactions\` MODIFY COLUMN \`account_balance_on_confirmation\` int NULL DEFAULT 0;`,
)
await queryFn(
`ALTER TABLE \`transactions\` MODIFY COLUMN \`account_balance_on_creation\` int NULL DEFAULT 0;`,
)
await queryFn(`ALTER TABLE \`transactions\` MODIFY COLUMN \`amount\` int NULL DEFAULT 0;`)
await queryFn(`DROP TABLE \`backend_transactions\`;`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
`ALTER TABLE \`communities\` CHANGE COLUMN \`root_encrypted_privkey\` \`root_privkey\` binary(64) NULL DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`transactions\` MODIFY COLUMN \`account_balance_on_confirmation\` decimal(40, 20) NULL DEFAULT 0.00000000000000000000;`,
)
await queryFn(
`ALTER TABLE \`transactions\` MODIFY COLUMN \`account_balance_on_creation\` decimal(40, 20) NULL DEFAULT 0.00000000000000000000;`,
)
await queryFn(
`ALTER TABLE \`transactions\` MODIFY COLUMN \`amount\` decimal(40, 20) NULL DEFAULT NULL;`,
)
}

File diff suppressed because it is too large Load Diff