mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
refactor with gradido-blockchain-js
This commit is contained in:
parent
64008b1564
commit
0bb04a845a
@ -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()
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import { BackendTransaction } from '@entity/BackendTransaction'
|
||||
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
export const BackendTransactionRepository = getDataSource()
|
||||
.getRepository(BackendTransaction)
|
||||
.extend({})
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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({}))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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',
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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.')
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
}
|
||||
@ -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[]
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export const PROTO_TRANSACTION_BODY_VERSION_NUMBER = '3.3'
|
||||
@ -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
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
export abstract class AbstractTransaction {
|
||||
public abstract fillTransactionRecipe(recipe: Transaction): void
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
@ -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, {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@ -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', {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) },
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) + '..',
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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[]
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export { BackendTransaction } from './0003-refactor_transaction_recipe/BackendTransaction'
|
||||
@ -1 +1 @@
|
||||
export { Community } from './0003-refactor_transaction_recipe/Community'
|
||||
export { Community } from './0005-refactor_with_gradido_blockchain_lib/Community'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { Transaction } from './0004-fix_spelling/Transaction'
|
||||
export { Transaction } from './0005-refactor_with_gradido_blockchain_lib/Transaction'
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user